Add dark mode with toggle for prototyping tool
- Add ThemeProvider context with system/light/dark modes - Add ThemeToggle button to all pages (Gallery, DemoMode, UserStoriesPage, SpecsPage) - Add --tool-* CSS variables for outer app theming - Keep inner Festipod mockup screens always in light mode - Add subtle glow around phone frame in dark mode for visibility Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+6
-3
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { RouterProvider, useRouter } from './router';
|
||||
import { ThemeProvider } from './context/ThemeContext';
|
||||
import { Gallery } from './components/Gallery';
|
||||
import { DemoMode } from './components/DemoMode';
|
||||
import { UserStoriesPage } from './components/UserStoriesPage';
|
||||
@@ -50,9 +51,11 @@ function AppContent() {
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<RouterProvider>
|
||||
<AppContent />
|
||||
</RouterProvider>
|
||||
<ThemeProvider>
|
||||
<RouterProvider>
|
||||
<AppContent />
|
||||
</RouterProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+53
-21
@@ -3,6 +3,7 @@ import { PhoneFrame } from './sketchy';
|
||||
import { screens, getScreen } from '../screens';
|
||||
import { getStoriesForScreen, categoryLabels, categoryColors, priorityColors } from '../data';
|
||||
import { getStoryUrl } from '../router';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
|
||||
interface DemoModeProps {
|
||||
initialScreenId: string;
|
||||
@@ -52,8 +53,9 @@ export function DemoMode({ initialScreenId, onBack, onNavigateToStory }: DemoMod
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
height: '100vh',
|
||||
background: 'var(--sketch-bg)',
|
||||
background: 'var(--tool-bg)',
|
||||
overflow: 'hidden',
|
||||
transition: 'background-color 0.2s ease',
|
||||
}}>
|
||||
{/* Left Sidebar */}
|
||||
<div style={{
|
||||
@@ -61,26 +63,36 @@ export function DemoMode({ initialScreenId, onBack, onNavigateToStory }: DemoMod
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRight: '2px solid var(--sketch-black)',
|
||||
background: 'var(--sketch-white)',
|
||||
borderRight: '2px solid var(--tool-border)',
|
||||
background: 'var(--tool-surface)',
|
||||
transition: 'background-color 0.2s ease, border-color 0.2s ease',
|
||||
}}>
|
||||
{/* Back button */}
|
||||
<div style={{ padding: 16, borderBottom: '1px solid var(--sketch-light-gray)' }}>
|
||||
{/* Back button and theme toggle */}
|
||||
<div style={{ padding: 16, borderBottom: '1px solid var(--tool-border-light)', display: 'flex', gap: 8 }}>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="sketchy-btn"
|
||||
style={{ padding: '8px 16px', width: '100%' }}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 16px',
|
||||
background: 'none',
|
||||
border: '2px solid var(--tool-border)',
|
||||
borderRadius: '255px 15px 225px 15px/15px 225px 15px 255px',
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--tool-text)',
|
||||
}}
|
||||
>
|
||||
← Galerie
|
||||
</button>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
{/* Current screen & navigation */}
|
||||
<div style={{ padding: 16, borderBottom: '1px solid var(--sketch-light-gray)' }}>
|
||||
<div style={{ padding: 16, borderBottom: '1px solid var(--tool-border-light)' }}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 12,
|
||||
color: 'var(--sketch-gray)',
|
||||
color: 'var(--tool-text-muted)',
|
||||
marginBottom: 8,
|
||||
}}>
|
||||
Écran actuel
|
||||
@@ -90,22 +102,41 @@ export function DemoMode({ initialScreenId, onBack, onNavigateToStory }: DemoMod
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 12,
|
||||
color: 'var(--tool-text)',
|
||||
}}>
|
||||
{currentScreen?.name}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button
|
||||
onClick={goBack}
|
||||
className="sketchy-btn"
|
||||
style={{ padding: '6px 12px', opacity: canGoBack ? 1 : 0.4, flex: 1 }}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
opacity: canGoBack ? 1 : 0.4,
|
||||
flex: 1,
|
||||
background: 'none',
|
||||
border: '2px solid var(--tool-border)',
|
||||
borderRadius: '255px 15px 225px 15px/15px 225px 15px 255px',
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
cursor: canGoBack ? 'pointer' : 'default',
|
||||
color: 'var(--tool-text)',
|
||||
}}
|
||||
disabled={!canGoBack}
|
||||
>
|
||||
‹ Retour
|
||||
</button>
|
||||
<button
|
||||
onClick={goForward}
|
||||
className="sketchy-btn"
|
||||
style={{ padding: '6px 12px', opacity: canGoForward ? 1 : 0.4, flex: 1 }}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
opacity: canGoForward ? 1 : 0.4,
|
||||
flex: 1,
|
||||
background: 'none',
|
||||
border: '2px solid var(--tool-border)',
|
||||
borderRadius: '255px 15px 225px 15px/15px 225px 15px 255px',
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
cursor: canGoForward ? 'pointer' : 'default',
|
||||
color: 'var(--tool-text)',
|
||||
}}
|
||||
disabled={!canGoForward}
|
||||
>
|
||||
Suivant ›
|
||||
@@ -116,18 +147,18 @@ export function DemoMode({ initialScreenId, onBack, onNavigateToStory }: DemoMod
|
||||
{/* User Stories for this screen */}
|
||||
{linkedStories.length > 0 && (
|
||||
<div style={{
|
||||
borderBottom: '1px solid var(--sketch-light-gray)',
|
||||
borderBottom: '1px solid var(--tool-border-light)',
|
||||
maxHeight: '40%',
|
||||
overflow: 'auto',
|
||||
}}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 12,
|
||||
color: 'var(--sketch-gray)',
|
||||
color: 'var(--tool-text-muted)',
|
||||
padding: '12px 16px 8px',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
background: 'var(--sketch-white)',
|
||||
background: 'var(--tool-surface)',
|
||||
}}>
|
||||
User Stories ({linkedStories.length})
|
||||
</div>
|
||||
@@ -142,9 +173,9 @@ export function DemoMode({ initialScreenId, onBack, onNavigateToStory }: DemoMod
|
||||
style={{
|
||||
display: 'block',
|
||||
padding: '8px 16px',
|
||||
borderBottom: '1px solid var(--sketch-light-gray)',
|
||||
borderBottom: '1px solid var(--tool-border-light)',
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
color: 'var(--tool-text)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
@@ -193,7 +224,7 @@ export function DemoMode({ initialScreenId, onBack, onNavigateToStory }: DemoMod
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 12,
|
||||
color: 'var(--sketch-gray)',
|
||||
color: 'var(--tool-text-muted)',
|
||||
padding: '8px 16px',
|
||||
}}>
|
||||
Tous les écrans
|
||||
@@ -207,8 +238,9 @@ export function DemoMode({ initialScreenId, onBack, onNavigateToStory }: DemoMod
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 14,
|
||||
cursor: 'pointer',
|
||||
background: s.id === currentScreenId ? 'var(--sketch-light-gray)' : 'transparent',
|
||||
borderLeft: s.id === currentScreenId ? '3px solid var(--sketch-black)' : '3px solid transparent',
|
||||
background: s.id === currentScreenId ? 'var(--tool-border-light)' : 'transparent',
|
||||
borderLeft: s.id === currentScreenId ? '3px solid var(--tool-text)' : '3px solid transparent',
|
||||
color: 'var(--tool-text)',
|
||||
}}
|
||||
>
|
||||
{s.name}
|
||||
|
||||
+18
-11
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { PhoneFrame } from './sketchy';
|
||||
import { screenGroups, type Screen } from '../screens';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
|
||||
interface GalleryProps {
|
||||
onSelectScreen: (screenId: string) => void;
|
||||
@@ -19,8 +20,9 @@ export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryP
|
||||
<div>
|
||||
<div style={{
|
||||
padding: '24px 32px',
|
||||
borderBottom: '2px solid var(--sketch-black)',
|
||||
background: 'var(--sketch-white)',
|
||||
borderBottom: '2px solid var(--tool-border)',
|
||||
background: 'var(--tool-surface)',
|
||||
transition: 'background-color 0.2s ease, border-color 0.2s ease',
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
@@ -28,13 +30,14 @@ export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryP
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 28,
|
||||
margin: 0,
|
||||
color: 'var(--tool-text)',
|
||||
}}>
|
||||
Festipod
|
||||
</h1>
|
||||
<p style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 16,
|
||||
color: 'var(--sketch-gray)',
|
||||
color: 'var(--tool-text-muted)',
|
||||
margin: '8px 0 0 0',
|
||||
}}>
|
||||
Cliquez sur un écran pour le prévisualiser
|
||||
@@ -51,12 +54,13 @@ export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryP
|
||||
onClick={onShowStories}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: '2px solid var(--sketch-black)',
|
||||
border: '2px solid var(--tool-border)',
|
||||
borderRadius: '255px 15px 225px 15px/15px 225px 15px 255px',
|
||||
padding: '8px 16px',
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 14,
|
||||
cursor: 'pointer',
|
||||
color: 'var(--tool-text)',
|
||||
}}
|
||||
>
|
||||
User Stories
|
||||
@@ -67,9 +71,9 @@ export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryP
|
||||
<button
|
||||
onClick={onShowSpecs}
|
||||
style={{
|
||||
background: 'var(--sketch-black)',
|
||||
color: 'var(--sketch-white)',
|
||||
border: '2px solid var(--sketch-black)',
|
||||
background: 'var(--tool-text)',
|
||||
color: 'var(--tool-bg)',
|
||||
border: '2px solid var(--tool-border)',
|
||||
borderRadius: '255px 15px 225px 15px/15px 225px 15px 255px',
|
||||
padding: '8px 16px',
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
@@ -88,7 +92,7 @@ export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryP
|
||||
gap: 12,
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
}}>
|
||||
<span style={{ fontSize: 14, color: 'var(--sketch-gray)' }}>Zoom</span>
|
||||
<span style={{ fontSize: 14, color: 'var(--tool-text-muted)' }}>Zoom</span>
|
||||
<input
|
||||
type="range"
|
||||
min={MIN_SCALE * 100}
|
||||
@@ -97,11 +101,14 @@ export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryP
|
||||
onChange={(e) => setScale(Number(e.target.value) / 100)}
|
||||
style={{
|
||||
width: 100,
|
||||
accentColor: 'var(--sketch-black)',
|
||||
accentColor: 'var(--tool-text)',
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: 14, width: 40 }}>{Math.round(scale * 100)}%</span>
|
||||
</div>
|
||||
|
||||
{/* Theme toggle */}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,7 +121,7 @@ export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryP
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 18,
|
||||
margin: '0 0 16px 32px',
|
||||
color: 'var(--sketch-black)',
|
||||
color: 'var(--tool-text)',
|
||||
}}>
|
||||
{group.name}
|
||||
</h2>
|
||||
@@ -179,7 +186,7 @@ function GalleryItem({ screen, scale, onClick }: GalleryItemProps) {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
marginTop: 8,
|
||||
color: 'var(--sketch-black)',
|
||||
color: 'var(--tool-text)',
|
||||
}}>
|
||||
{screen.name}
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
import { Sun, Moon, Monitor } from 'lucide-react';
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const cycleTheme = () => {
|
||||
if (theme === 'system') setTheme('light');
|
||||
else if (theme === 'light') setTheme('dark');
|
||||
else setTheme('system');
|
||||
};
|
||||
|
||||
const getIcon = () => {
|
||||
switch (theme) {
|
||||
case 'light':
|
||||
return <Sun size={18} />;
|
||||
case 'dark':
|
||||
return <Moon size={18} />;
|
||||
case 'system':
|
||||
return <Monitor size={18} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getLabel = () => {
|
||||
switch (theme) {
|
||||
case 'light':
|
||||
return 'Clair';
|
||||
case 'dark':
|
||||
return 'Sombre';
|
||||
case 'system':
|
||||
return 'Auto';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={cycleTheme}
|
||||
title={`Mode: ${getLabel()} (cliquez pour changer)`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
background: 'none',
|
||||
border: '2px solid var(--tool-border)',
|
||||
borderRadius: '255px 15px 225px 15px/15px 225px 15px 255px',
|
||||
padding: '6px 12px',
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 14,
|
||||
cursor: 'pointer',
|
||||
color: 'var(--tool-text)',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
{getIcon()}
|
||||
<span>{getLabel()}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type StoryCategory,
|
||||
} from '../data';
|
||||
import { getScreen, screens } from '../screens';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
|
||||
interface UserStoriesPageProps {
|
||||
selectedStoryId?: string;
|
||||
@@ -101,64 +102,71 @@ export function UserStoriesPage({ selectedStoryId, onBack, onSelectScreen }: Use
|
||||
const hasFilters = selectedCategories.size > 0 || selectedPriorities.size > 0 || selectedScreens.size > 0;
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: 'var(--sketch-white)' }}>
|
||||
<div style={{ minHeight: '100vh', background: 'var(--tool-bg)', transition: 'background-color 0.2s ease' }}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
padding: '24px 32px',
|
||||
borderBottom: '2px solid var(--sketch-black)',
|
||||
background: 'var(--sketch-white)',
|
||||
borderBottom: '2px solid var(--tool-border)',
|
||||
background: 'var(--tool-surface)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
justifyContent: 'space-between',
|
||||
transition: 'background-color 0.2s ease, border-color 0.2s ease',
|
||||
}}>
|
||||
<button
|
||||
onClick={onBack}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: '2px solid var(--sketch-black)',
|
||||
borderRadius: '255px 15px 225px 15px/15px 225px 15px 255px',
|
||||
padding: '8px 16px',
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 14,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
← Retour
|
||||
</button>
|
||||
<div>
|
||||
<h1 style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 28,
|
||||
margin: 0,
|
||||
}}>
|
||||
User Stories
|
||||
</h1>
|
||||
<p style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 16,
|
||||
color: 'var(--sketch-gray)',
|
||||
margin: '8px 0 0 0',
|
||||
}}>
|
||||
{filteredStories.length} / {userStories.length} stories · Cliquez sur un écran pour voir le mockup
|
||||
</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<button
|
||||
onClick={onBack}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: '2px solid var(--tool-border)',
|
||||
borderRadius: '255px 15px 225px 15px/15px 225px 15px 255px',
|
||||
padding: '8px 16px',
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 14,
|
||||
cursor: 'pointer',
|
||||
color: 'var(--tool-text)',
|
||||
}}
|
||||
>
|
||||
← Retour
|
||||
</button>
|
||||
<div>
|
||||
<h1 style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 28,
|
||||
margin: 0,
|
||||
color: 'var(--tool-text)',
|
||||
}}>
|
||||
User Stories
|
||||
</h1>
|
||||
<p style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 16,
|
||||
color: 'var(--tool-text-muted)',
|
||||
margin: '8px 0 0 0',
|
||||
}}>
|
||||
{filteredStories.length} / {userStories.length} stories · Cliquez sur un écran pour voir le mockup
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
{/* Filter bar */}
|
||||
<div style={{
|
||||
padding: '16px 32px',
|
||||
borderBottom: '1px solid var(--sketch-light-gray)',
|
||||
background: 'var(--sketch-white)',
|
||||
borderBottom: '1px solid var(--tool-border-light)',
|
||||
background: 'var(--tool-surface)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
transition: 'background-color 0.2s ease, border-color 0.2s ease',
|
||||
}}>
|
||||
{/* Category filters */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<span style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 13,
|
||||
color: 'var(--sketch-gray)',
|
||||
color: 'var(--tool-text-muted)',
|
||||
minWidth: 70,
|
||||
}}>
|
||||
Catégorie
|
||||
@@ -179,7 +187,7 @@ export function UserStoriesPage({ selectedStoryId, onBack, onSelectScreen }: Use
|
||||
<span style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 13,
|
||||
color: 'var(--sketch-gray)',
|
||||
color: 'var(--tool-text-muted)',
|
||||
minWidth: 70,
|
||||
}}>
|
||||
Priorité
|
||||
@@ -200,7 +208,7 @@ export function UserStoriesPage({ selectedStoryId, onBack, onSelectScreen }: Use
|
||||
<span style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 13,
|
||||
color: 'var(--sketch-gray)',
|
||||
color: 'var(--tool-text-muted)',
|
||||
minWidth: 70,
|
||||
}}>
|
||||
Écran
|
||||
@@ -209,7 +217,7 @@ export function UserStoriesPage({ selectedStoryId, onBack, onSelectScreen }: Use
|
||||
<FilterChip
|
||||
key={screen.id}
|
||||
label={screen.name}
|
||||
color="var(--sketch-black)"
|
||||
color="var(--tool-text)"
|
||||
selected={selectedScreens.has(screen.id)}
|
||||
onClick={() => toggleScreen(screen.id)}
|
||||
/>
|
||||
@@ -243,7 +251,7 @@ export function UserStoriesPage({ selectedStoryId, onBack, onSelectScreen }: Use
|
||||
<p style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 16,
|
||||
color: 'var(--sketch-gray)',
|
||||
color: 'var(--tool-text-muted)',
|
||||
textAlign: 'center',
|
||||
padding: 40,
|
||||
}}>
|
||||
@@ -259,6 +267,7 @@ export function UserStoriesPage({ selectedStoryId, onBack, onSelectScreen }: Use
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
color: 'var(--tool-text)',
|
||||
}}>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
@@ -273,7 +282,7 @@ export function UserStoriesPage({ selectedStoryId, onBack, onSelectScreen }: Use
|
||||
Priorité {priorityLabels[priority]}
|
||||
<span style={{
|
||||
fontSize: 14,
|
||||
color: 'var(--sketch-gray)',
|
||||
color: 'var(--tool-text-muted)',
|
||||
fontWeight: 'normal',
|
||||
}}>
|
||||
({stories.length} stories)
|
||||
@@ -345,10 +354,10 @@ const StoryCard = React.forwardRef<HTMLDivElement, StoryCardProps>(
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
border: isSelected ? '3px solid #2563eb' : '2px solid var(--sketch-black)',
|
||||
border: isSelected ? '3px solid #2563eb' : '2px solid var(--tool-border)',
|
||||
borderRadius: '255px 15px 225px 15px/15px 225px 15px 255px',
|
||||
padding: 16,
|
||||
background: isSelected ? '#eff6ff' : 'var(--sketch-white)',
|
||||
background: isSelected ? '#eff6ff' : 'var(--tool-surface)',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
@@ -370,6 +379,7 @@ const StoryCard = React.forwardRef<HTMLDivElement, StoryCardProps>(
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 16,
|
||||
margin: 0,
|
||||
color: 'var(--tool-text)',
|
||||
}}>
|
||||
{story.title}
|
||||
</h3>
|
||||
@@ -378,7 +388,7 @@ const StoryCard = React.forwardRef<HTMLDivElement, StoryCardProps>(
|
||||
<p style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 13,
|
||||
color: 'var(--sketch-gray)',
|
||||
color: 'var(--tool-text-muted)',
|
||||
margin: '0 0 12px 0',
|
||||
lineHeight: 1.5,
|
||||
}}>
|
||||
@@ -392,13 +402,14 @@ const StoryCard = React.forwardRef<HTMLDivElement, StoryCardProps>(
|
||||
key={id}
|
||||
onClick={() => onSelectScreen(id)}
|
||||
style={{
|
||||
background: 'var(--sketch-light-gray)',
|
||||
border: '1px solid var(--sketch-black)',
|
||||
background: 'var(--tool-border-light)',
|
||||
border: '1px solid var(--tool-border)',
|
||||
borderRadius: '255px 15px 225px 15px/15px 225px 15px 255px',
|
||||
padding: '6px 12px',
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 13,
|
||||
cursor: 'pointer',
|
||||
color: 'var(--tool-text)',
|
||||
}}
|
||||
>
|
||||
→ {screen!.name}
|
||||
@@ -409,7 +420,7 @@ const StoryCard = React.forwardRef<HTMLDivElement, StoryCardProps>(
|
||||
<p style={{
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
fontSize: 13,
|
||||
color: 'var(--sketch-gray)',
|
||||
color: 'var(--tool-text-muted)',
|
||||
fontStyle: 'italic',
|
||||
margin: 0,
|
||||
}}>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function PhoneFrame({ children, scale = 1, className = '' }: PhoneFramePr
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
className={`phone-frame-wrapper ${className}`}
|
||||
style={{
|
||||
width: width * scale,
|
||||
height: height * scale,
|
||||
@@ -67,6 +67,7 @@ export function PhoneFrame({ children, scale = 1, className = '' }: PhoneFramePr
|
||||
fontSize: 12 * scale,
|
||||
fontFamily: 'var(--font-sketch)',
|
||||
flexShrink: 0,
|
||||
color: 'var(--sketch-black)',
|
||||
}}
|
||||
>
|
||||
<span>9:41</span>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '../ui
|
||||
import { Button } from '../ui/button';
|
||||
import { ArrowLeft, FileText, Monitor, CheckCircle2, XCircle, AlertCircle, ExternalLink } from 'lucide-react';
|
||||
import type { ParsedFeature } from '../../types/gherkin';
|
||||
import { ThemeToggle } from '../ThemeToggle';
|
||||
|
||||
interface SpecsPageProps {
|
||||
selectedFeatureId?: string;
|
||||
@@ -109,8 +110,10 @@ export function SpecsPage({ selectedFeatureId, onBack, onSelectScreen, onSelectS
|
||||
Rapport HTML
|
||||
</Button>
|
||||
</a>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
)}
|
||||
{testSummary.totalScenarios === 0 && <ThemeToggle />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system';
|
||||
|
||||
interface ThemeContextValue {
|
||||
theme: Theme;
|
||||
resolvedTheme: 'light' | 'dark';
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
|
||||
const STORAGE_KEY = 'festipod-theme';
|
||||
|
||||
function getSystemTheme(): 'light' | 'dark' {
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function getStoredTheme(): Theme {
|
||||
if (typeof window === 'undefined') return 'system';
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === 'light' || stored === 'dark' || stored === 'system') {
|
||||
return stored;
|
||||
}
|
||||
return 'system';
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setThemeState] = useState<Theme>(getStoredTheme);
|
||||
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(
|
||||
theme === 'system' ? getSystemTheme() : theme
|
||||
);
|
||||
|
||||
const setTheme = (newTheme: Theme) => {
|
||||
setThemeState(newTheme);
|
||||
localStorage.setItem(STORAGE_KEY, newTheme);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const resolved = theme === 'system' ? getSystemTheme() : theme;
|
||||
setResolvedTheme(resolved);
|
||||
|
||||
const root = document.documentElement;
|
||||
if (resolved === 'dark') {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme !== 'system') return;
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handler = (e: MediaQueryListEvent) => {
|
||||
setResolvedTheme(e.matches ? 'dark' : 'light');
|
||||
const root = document.documentElement;
|
||||
if (e.matches) {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
return () => mediaQuery.removeEventListener('change', handler);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
+33
-2
@@ -10,6 +10,24 @@
|
||||
--sketch-accent: #4a90d9;
|
||||
--sketch-line-width: 2px;
|
||||
--font-sketch: 'Architects Daughter', cursive;
|
||||
|
||||
/* Prototyping tool theme (outer app) */
|
||||
--tool-bg: #fafafa;
|
||||
--tool-surface: #ffffff;
|
||||
--tool-text: #2d2d2d;
|
||||
--tool-text-muted: #666;
|
||||
--tool-border: #2d2d2d;
|
||||
--tool-border-light: #e5e5e5;
|
||||
}
|
||||
|
||||
/* Dark mode for prototyping tool only */
|
||||
.dark {
|
||||
--tool-bg: #1a1a1a;
|
||||
--tool-surface: #2d2d2d;
|
||||
--tool-text: #f5f5f5;
|
||||
--tool-text-muted: #a0a0a0;
|
||||
--tool-border: #4a4a4a;
|
||||
--tool-border-light: #3a3a3a;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -18,10 +36,11 @@
|
||||
|
||||
body {
|
||||
font-family: var(--font-sketch);
|
||||
background-color: var(--sketch-bg);
|
||||
color: var(--sketch-black);
|
||||
background-color: var(--tool-bg);
|
||||
color: var(--tool-text);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Sketchy border effect using border-radius variations */
|
||||
@@ -312,6 +331,18 @@ body {
|
||||
- Blue = user-provided content only (names, event titles, descriptions, usernames)
|
||||
=========================================== */
|
||||
|
||||
/* Force phone screen to always use light mode colors */
|
||||
.phone-screen {
|
||||
color: var(--sketch-black);
|
||||
background: var(--sketch-white);
|
||||
}
|
||||
|
||||
/* Dark mode: add outer glow to phone frame for visibility */
|
||||
.dark .phone-frame-wrapper {
|
||||
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.15);
|
||||
border-radius: 44px;
|
||||
}
|
||||
|
||||
/* User content - displayed in blue */
|
||||
.phone-screen .user-content {
|
||||
color: var(--sketch-accent);
|
||||
|
||||
Reference in New Issue
Block a user