Skip to content

Commit bba0666

Browse files
authored
Design audit 1/4: Foundations (#124)
Design audit 1/4: foundations (tokens, motion system, touch, reduced motion)
2 parents bb1b7f8 + 80f9490 commit bba0666

31 files changed

Lines changed: 556 additions & 445 deletions

‎.claude/skills/verify-debatelab/SKILL.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ It never touches `.verify/artifacts/` or `.verify/logs/`, and it is safe to run
172172
## Gotchas
173173

174174
- Live runs are slow: sparring and crossfire take several minutes each because the model must reason. Use `-g` to run one test while iterating.
175+
- Vite does not reload `tailwind.config.ts` on a running stack: new theme keys, variants and the `future` flags produce no CSS until `$H down && $H up`. Edits to `src/` and `src/index.css` apply on reload as usual.
175176
- The fake provider rejects prompts it doesn't recognize. If a prompt change makes a feature fail with a provider error, update `e2e/full-stack/fake-openrouter.mjs`. Don't mock around it.
176177
- Once, the fake-provider container was SIGKILLed about 20 seconds after `up`, and the cause was never found. Doctor catches this, and `drive` refuses to run until you `down && up`.
177178
- Demo-code users are Pro with a daily limit of 999. To test quota or tier paths, change `user_settings` through `admin` inside the recipe, as `e2e/full-stack/case-generation.spec.ts` does.

‎e2e/animation-polish.spec.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ test('removes non-essential route and state movement for reduced motion', async
7070
const options = await openGenerateOptions(page);
7171

7272
await expect(page.locator('[data-route-transition-ms]')).toHaveCount(0);
73-
await expect.poll(() => options.evaluate((element) => getComputedStyle(element).transitionDuration)).toBe('0s');
73+
// Reduced motion keeps gentle fades but nothing that moves: no transform, height or grid-row transitions.
74+
await expect.poll(() => options.evaluate((element) => getComputedStyle(element).transitionProperty)).not.toMatch(/transform|grid-template-rows|height|all/);
7475
await expect.poll(() => page.evaluate(() => document.getAnimations().filter((animation) => animation.playState === 'running').length)).toBe(0);
7576
await page.screenshot({ path: testInfo.outputPath('reduced-empty-options.png'), fullPage: true });
7677

‎index.html‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
<meta name="author" content="DebateLab" />
1414
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
1515
<meta name="language" content="English" />
16-
<meta name="theme-color" content="#0f172a" />
16+
<meta name="theme-color" content="#f1f5f9" media="(prefers-color-scheme: light)" />
17+
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)" />
1718
<link rel="icon" type="image/png" href="/favicon.png" />
1819
<link rel="canonical" href="https://debate.pi-stack.org/" />
1920
<link rel="manifest" href="/manifest.webmanifest" />

‎plans/2026-10-09-design-audit.md‎

Lines changed: 223 additions & 0 deletions
Large diffs are not rendered by default.

‎plans/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Every plan lives in this folder and opens with a `**Status:**` line; a plan list
66

77
## In progress
88

9+
- **Design audit.** UI audit against Emil Kowalski's Apple-design and motion skills: broken toast stacking, collapsed text-colour tokens, a global CSS layer that overrides the primitives, touch and reduced-motion gaps, wayfinding and undo. Shipping as a four-PR stack: Foundations, Shell, AI tools, Landing. Plan: [2026-10-09 design audit](2026-10-09-design-audit.md).
910
- **Staging that tracks main.** Audit of staging, the VPS sync timer, the seed, and what to delete. Plan: [2026-10-03 staging loop](2026-10-03-staging-loop.md). Previews come from #95 and the staging spec fix from #100.
1011

1112
## Backlog

‎src/components/OfflineIndicator.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function OfflineIndicator() {
99
if (!offline) return null;
1010

1111
return (
12-
<div className="fixed bottom-4 left-1/2 z-[60] flex -translate-x-1/2 items-center gap-2 rounded-full border bg-background/95 px-4 py-2 text-sm font-medium text-foreground shadow-lg backdrop-blur" role="status" aria-live="polite">
12+
<div className="native-safe-fixed-bottom fixed left-1/2 z-[60] flex -translate-x-1/2 items-center gap-2 rounded-full border bg-background/95 px-4 py-2 text-sm font-medium text-foreground shadow-lg backdrop-blur" role="status" aria-live="polite">
1313
<WifiOff className="h-4 w-4 text-amber-500" />
1414
{t('offlineIndicator.label', 'Offline mode')}
1515
</div>

‎src/components/ThemeProvider.tsx‎

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,61 @@
11
// src/components/ThemeProvider.tsx
2-
import { useEffect } from 'react';
2+
import { useEffect, useRef } from 'react';
33
import { useSettingsStore } from '@/lib/stores/settings-store';
44

5-
export function ThemeProvider({ children }: { children: React.ReactNode }) {
6-
const theme = useSettingsStore((state) => state.theme);
5+
type ResolvedTheme = 'light' | 'dark';
76

8-
useEffect(() => {
9-
const root = window.document.documentElement;
7+
// Browser chrome colours, matching --background in each theme.
8+
const THEME_COLORS: Record<ResolvedTheme, string> = { light: '#f1f5f9', dark: '#0f172a' };
9+
10+
function syncThemeColor(theme: 'light' | 'dark' | 'system', resolved: ResolvedTheme) {
11+
document.querySelectorAll<HTMLMetaElement>('meta[name="theme-color"]').forEach((meta) => {
12+
const media = meta.getAttribute('media') ?? '';
13+
const own: ResolvedTheme = media.includes('dark') ? 'dark' : 'light';
14+
// In system mode each meta keeps its own media colour; an explicit choice wins everywhere.
15+
meta.content = THEME_COLORS[theme === 'system' ? own : resolved];
16+
});
17+
}
1018

11-
// Remove existing theme classes
19+
function applyTheme(resolved: ResolvedTheme, animate: boolean) {
20+
const root = window.document.documentElement;
21+
const swap = () => {
1222
root.classList.remove('light', 'dark');
23+
root.classList.add(resolved);
24+
};
25+
26+
if (!animate || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
27+
swap();
28+
return;
29+
}
30+
31+
// One 200ms crossfade of the whole page instead of every surface easing at its own speed.
32+
if (typeof document.startViewTransition === 'function') {
33+
const transition = document.startViewTransition(swap);
34+
// A newer switch skips this one's animation; the class swap still runs, so that's fine.
35+
transition.ready.catch(() => {});
36+
transition.finished.catch(() => {});
37+
return;
38+
}
39+
40+
// No View Transitions: switch in a single frame so nothing ripples.
41+
root.classList.add('theme-switching');
42+
swap();
43+
requestAnimationFrame(() => requestAnimationFrame(() => root.classList.remove('theme-switching')));
44+
}
45+
46+
const systemTheme = (): ResolvedTheme =>
47+
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
48+
49+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
50+
const theme = useSettingsStore((state) => state.theme);
51+
const mounted = useRef(false);
1352

14-
if (theme === 'system') {
15-
// Check system preference
16-
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
17-
? 'dark'
18-
: 'light';
19-
root.classList.add(systemTheme);
20-
} else {
21-
root.classList.add(theme);
22-
}
53+
useEffect(() => {
54+
const resolved = theme === 'system' ? systemTheme() : theme;
55+
// theme-init.js already applied the stored theme before first paint.
56+
applyTheme(resolved, mounted.current);
57+
syncThemeColor(theme, resolved);
58+
mounted.current = true;
2359
}, [theme]);
2460

2561
// Listen for system theme changes when in 'system' mode
@@ -29,9 +65,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
2965
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
3066

3167
const handleChange = (e: MediaQueryListEvent) => {
32-
const root = window.document.documentElement;
33-
root.classList.remove('light', 'dark');
34-
root.classList.add(e.matches ? 'dark' : 'light');
68+
applyTheme(e.matches ? 'dark' : 'light', true);
3569
};
3670

3771
mediaQuery.addEventListener('change', handleChange);
@@ -40,5 +74,3 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
4074

4175
return <>{children}</>;
4276
}
43-
44-

‎src/components/features/ask/AskChat.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ const MessageBubble = React.memo(function MessageBubble({ message, onSave }: { m
9999
<Button
100100
variant="ghost"
101101
size="icon"
102-
className="absolute -right-10 top-0 opacity-0 group-hover:opacity-100 transition-opacity h-8 w-8"
102+
className="absolute -right-10 top-0 h-8 w-8 opacity-60 transition-opacity hover:opacity-100 focus-visible:opacity-100 group-hover:opacity-100"
103103
onClick={() => {
104104
const title = message.content.length > 30
105105
? `${message.content.slice(0, 30)}...`

‎src/components/features/attack/AttackForm.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export function AttackForm({ onSubmit, isLoading, importedCase, debateContext =
118118
<SlidersHorizontal className="h-4 w-4" />
119119
{t('attackForm.moreOptions')}
120120
</span>
121-
<ChevronDown className={`h-4 w-4 transition-transform duration-300 ${optionsOpen ? 'rotate-180' : ''}`} />
121+
<ChevronDown className={`h-4 w-4 transition-transform duration-150 ease-out-strong ${optionsOpen ? 'rotate-180' : ''}`} />
122122
</Button>
123123
</div>
124124

‎src/components/features/attack/AttackResults.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function WeaknessCard({ weakness }: { weakness: Weakness }) {
3939
const Icon = config.icon;
4040

4141
return (
42-
<div className={cn('rounded-lg border p-4 space-y-3 motion-list-item card-hover-glow', config.border, config.bg)}>
42+
<div className={cn('rounded-lg border p-4 space-y-3 motion-list-item', config.border, config.bg)}>
4343
<div className="flex items-start justify-between gap-2">
4444
<div className="flex items-start gap-2">
4545
<Icon className={cn('h-5 w-5 mt-0.5', config.color)} />

0 commit comments

Comments
 (0)