diff --git a/.claude/skills/verify-debatelab/SKILL.md b/.claude/skills/verify-debatelab/SKILL.md index 59197bbef..d1408950e 100644 --- a/.claude/skills/verify-debatelab/SKILL.md +++ b/.claude/skills/verify-debatelab/SKILL.md @@ -172,6 +172,7 @@ It never touches `.verify/artifacts/` or `.verify/logs/`, and it is safe to run ## Gotchas - Live runs are slow: sparring and crossfire take several minutes each because the model must reason. Use `-g` to run one test while iterating. +- 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. - 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. - 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`. - 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. diff --git a/e2e/animation-polish.spec.ts b/e2e/animation-polish.spec.ts index 63abc5f7d..750ff2cdf 100644 --- a/e2e/animation-polish.spec.ts +++ b/e2e/animation-polish.spec.ts @@ -70,7 +70,8 @@ test('removes non-essential route and state movement for reduced motion', async const options = await openGenerateOptions(page); await expect(page.locator('[data-route-transition-ms]')).toHaveCount(0); - await expect.poll(() => options.evaluate((element) => getComputedStyle(element).transitionDuration)).toBe('0s'); + // Reduced motion keeps gentle fades but nothing that moves: no transform, height or grid-row transitions. + await expect.poll(() => options.evaluate((element) => getComputedStyle(element).transitionProperty)).not.toMatch(/transform|grid-template-rows|height|all/); await expect.poll(() => page.evaluate(() => document.getAnimations().filter((animation) => animation.playState === 'running').length)).toBe(0); await page.screenshot({ path: testInfo.outputPath('reduced-empty-options.png'), fullPage: true }); diff --git a/index.html b/index.html index 9d35cd96c..2b8b3d796 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,8 @@ - + + diff --git a/plans/2026-10-09-design-audit.md b/plans/2026-10-09-design-audit.md new file mode 100644 index 000000000..921914a9d --- /dev/null +++ b/plans/2026-10-09-design-audit.md @@ -0,0 +1,223 @@ +# Design audit: Apple design and Emil Kowalski standards + +**Status:** In progress. Audit of `main` at `56a94d7` on 2026-10-09, shipping as the four-PR stack in [Suggested order of work](#suggested-order-of-work). Rows marked ✅ are done, with the PR that did them; rows marked ➖ were changed or skipped, with the reason in the row. + +## What this is + +A full UI audit of DebateLab against Emil Kowalski's design-engineering skills (`apple-design`, `emil-design-eng` and `review-animations` from [emilkowalski/skills](https://github.com/emilkowalski/skills) at `e8a175d`). `apple-design` is Apple's WWDC guidance on fluid interfaces, materials, typography and the eight design principles, translated for the web. The other two supply the motion rules and values: easing curves, duration budgets, press feedback, transform origins and interruptibility. + +Four Opus reviewers read every page, every component in `components/ui/` and elsewhere, and the whole of `src/index.css`. Then the real app ran locally (`verify-debatelab`, fake provider). A Playwright sweep covered 20 routes on desktop (1440×900) and iPhone 13, in light and dark. A probe opened every overlay type and read real durations, easings and transform origins from `document.getAnimations()`. Further checks covered reduced motion, `prefers-contrast: more`, and Hungarian at 375px. Evidence is in `.verify/artifacts/audit-20261009T052123-design/` (gitignored): 118 screenshots, `metrics.json`, `probe.json`, and five screen recordings. + +Verdict under `review-animations`: **Block.** Two of the feel-breaking issues are system-wide bugs, not taste: toasts can't stack or leave, and secondary text has no colour hierarchy. Both are cheap to fix. + +## The short version + +The product structure is sound. The motion tokens are good (`--motion-ease-fluid` is a strong ease-out), `MotionConfig reducedMotion="user"` wraps the app, routes prefetch on intent, and the voice, Sparring and Crossfire work is careful. The weak spots are five systemic problems: + +1. **A global CSS layer overrides the shared UI components.** About 600 unlayered lines in `src/index.css` (roughly 491–847) target ARIA roles, Radix `data-state` attributes and Lucide icon class names. They override Sonner, Select, Progress, Tabs, Switch, focus rings and Tailwind's own `.tabular-nums`. Most of the motion bugs below come from this layer. Deleting most of it and moving what's worth keeping into each component's `cva` classes fixes the largest share of findings for the least work. +2. **The colour tokens are miswired.** `--muted-foreground` equals `--foreground` in both themes, so 379 uses of `text-muted-foreground` render at full body contrast. Light `--muted` is a mid grey, `--accent` (the ghost-button hover) is 1.05:1 against the page, the light popover is darker than the page, dark shadows are invisible, and the light primary button label is 3.85:1. +3. **Nothing is built for touch.** Hover motion isn't gated, there's no tap-highlight reset, nothing reads the safe-area insets despite `viewport-fit=cover`, and most controls on every mobile page are under 44px. +4. **Wayfinding and agency.** On all eight tool routes, nothing in the header shows as active. There are five different delete patterns and no undo anywhere, even though the toast adapter supports an action button. Streaming tools can't be stopped. +5. **Too much decoration.** The landing page loops an aurora, orbs, a drifting grid and CTA shimmer forever across five sections. The app replays staggers on tab switches, animates the nav underline from `scale(0)` on every navigation, and stacks five looping indicators on the AI loading panel. + +## Confirmed in the browser + +These rows were observed in the running app, not only inferred from code. + +| Finding | What the browser showed | +| --- | --- | +| Toasts can't stack, expand or exit | With two toasts open, both sat at the same position (top 823 and 825px) with an identity transform. Hovering didn't expand them, and only the newest was visible. 150ms after `toast.dismiss()` both were marked removed but still fully opaque and unmoved. Cause: `[data-sonner-toast]{animation: toast-slide-in .3s … both !important}` at `src/index.css:622-624` holds the transform Sonner uses for stacking, swipe and exit. Screenshots: `probe-toast-stacked.png`, `probe-toast-expanded.png`, `probe-toast-exit-150ms.png`. | +| Landing card hovers never render | `.feature-card`, `.tilt-card` and every `hover:-translate-y-1` card keep an inline `transform: none` from framer's `whileInView` after the reveal, so hovering changes nothing. | +| Select closes with `ease-in` | Measured: the Select exit is 150ms `ease-in`, leaked from `src/index.css:709-711`. | +| Popovers scale from their centre | Measured: Select content origin is `94.5px 37px` and the debate menu popover is `176px 151px`, the centres of their boxes, not the trigger. | +| `duration-200` on Dialog is dead | Measured: the dialog enters in 150ms and exits in 150ms with `ease`, so enter and exit are symmetric. | +| Press feedback is too faint | Measured: hover lifts 1px, and `:active` scales to 0.985 over 160ms in both directions. | +| Timer ring eases every second | Measured: `clip-path 1000ms cubic-bezier(0.4,0,0.2,1)` per tick, so the ring surges and stops each second. Digits are Space Mono at 72px with −3.6px tracking. | +| Mobile menu is slow and has no scrim | Measured: a 420ms `motion-reveal` on a plain `