Skip to content

block 5: UI polish (error boundary, state trio, a11y, reduced-motion, theme-flash, mobile viewport)#71

Merged
aksOps merged 10 commits intomainfrom
feat/block5-ui-polish
Apr 24, 2026
Merged

block 5: UI polish (error boundary, state trio, a11y, reduced-motion, theme-flash, mobile viewport)#71
aksOps merged 10 commits intomainfrom
feat/block5-ui-polish

Conversation

@aksOps
Copy link
Copy Markdown
Contributor

@aksOps aksOps commented Apr 24, 2026

Summary

Block 5 of the docsiq production-polish roadmap — ten items making the React 19 + TypeScript + Vite SPA production-grade on desktop and iOS Safari.

All changes live in ui/. No Go code, no CI workflow touched.

Tasks landed

  1. 5.2 — Reusable state trio (EmptyState, LoadingSkeleton, ErrorState) under ui/src/components/empty/, threaded into every fetching route (Home, Notes*, Documents*, Graph, MCPConsole).
  2. 5.1 — RouteBoundary wrapping <Suspense> in App.tsx. Catches render errors, shows sanitized card with Reload/Report (mailto with truncated stack).
  3. 5.3 — Dynamic document.title — extended useDocumentTitle with optional parts threading, wired through NoteView + DocumentView.
  4. 5.4 — iOS safe-area insets on header/sidebar/main (env(safe-area-inset-*)); Playwright iPhone-14 viewport test.
  5. 5.5 — "Maximum update depth" fix — root cause: useLastVisit returned a fresh touch each render. Home's cleanup useEffect depended on touch, causing infinite re-renders. Fixed with useCallback. Playwright console-capture regression across all 5 routes.
  6. 5.6 — Axe a11y audit@axe-core/playwright@^4.11. Violations fixed: SelectTrigger aria-label in app-sidebar; primary green darkened from oklch(0.68...) to oklch(0.52...) for 5.1:1 contrast (was 2.7:1). 0 wcag2a/wcag2aa violations across all 5 routes.
  7. 5.7 — Reduced-motion gatingfadeTransition/slideTransition/popTransition factories + CSS prefers-reduced-motion global gate. No motion.* callsites exist in src/ today, so factories ship ready-for-use. Legacy exports preserved.
  8. 5.8 — Focus management — Shell captures invoker on palette open, restores on close via rAF. Playwright covers skip-link → main#main, palette refocus, and Radix dialog focus-trap.
  9. 5.9 — Theme-flash inline script<head> script reads Zustand-persisted theme at key docsiq-ui and applies .dark + data-theme before React hydrates. Playwright covers dark/light/system.
  10. 5.10 — Mobile viewport pass — 44×44 header tap targets, full-viewport command palette under 480px, .table-scroll wrapper on DocumentsList table. 4 Playwright assertions at 375×812.

Verification

  • npm run typecheck — clean
  • npm test -- --run — 25 files, 81 tests passing (up from 18/54 baseline)
  • npm run build — succeeds
  • Playwright full suite: 26/26 passing (smoke, safe-area, no-console-errors, a11y, focus, mobile, theme-flash)

Notable deviations

  • /mcp route: The Vite dev-server proxy rule for /mcp intercepts document requests, so the axe + no-console-errors specs navigate to /mcp via SPA history.pushState instead of page.goto("/mcp"). Noted inline.
  • Safe-area Playwright test: uses a manual 390×844 viewport on the chromium project instead of devices["iPhone 14"] (which pins webkit). The CSS is engine-agnostic so the test value is preserved.
  • Bundle size: JS+CSS totals 860 KB at tip vs. 852 KB at base (Block 5 adds ~8 KB). The plan's 640 KB (655360 bytes) gate was already exceeded at baseline — flagged as pre-existing, not introduced by this block.
  • eslint baseline: npm run lint is pre-broken on main (missing eslint.config.js after ESLint v9 migration). Not in Block 5 scope.
  • Task 7 Step 6: No <motion.*> callsites exist in src/, so the motion-preset wiring is prep-only. Factories + CSS gate both ship.
  • CSP for theme-flash: the inline script will need a SHA-256 allowlist entry once Block 2's CSP lands. Flagged in commit message as a Block-2 follow-up.

Follow-ups

  • Bundle budget: the existing 655 KB CI gate is exceeded at main baseline (852 KB). Block 5 does not move the needle meaningfully (+8 KB), but a dedicated bundle-size reduction pass is owed (candidates: lazy markdown-it/shiki, tree-shake framer-motion).
  • CSP hash for the theme-flash script once Block 2 lands.
  • eslint.config.js migration (ESLint v9) — pre-broken at main.

🤖 Generated with Claude Code

aksOps and others added 10 commits April 24, 2026 02:05
Adds consistent loading/empty/error primitives under ui/src/components/empty
and applies them to every fetching route (Home, Notes*, Documents*, Graph,
MCPConsole). Addresses Block 5.2 of the production-polish roadmap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Catches render errors at the Suspense fallback level and shows a sanitized
state card with "Reload this view" (resets the boundary) and "Report"
(opens a mailto with a truncated stack). Vitest-covered via a child that
throws on render. Addresses Block 5.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends useDocumentTitle to accept optional `parts` for dynamic segments
(document/note titles) and threads them through DocumentView + NoteView.
All titles suffixed " — docsiq". Addresses Block 5.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Header padding-top uses max(var(--header-pad), env(safe-area-inset-top))
and sidebar/main respect left/right/bottom insets. Covered by a Playwright
iPhone-14-viewport test (Chromium project, logical 390x844 viewport) asserting
paddingTop >= 16px. Addresses Block 5.4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause: useLastVisit returned a fresh `touch` function on every render.
Home.tsx then used that `touch` in a useEffect cleanup with `touch` as the
sole dependency. React re-subscribed the effect every render, running the
prior cleanup (which calls `touch` → setLast → re-render) in a loop.

Fix: wrap `touch` in useCallback with a stable dep list. `touch` is now
reference-stable, so the Home unmount-cleanup useEffect only runs once.

Regression coverage:
- ui/e2e/no-console-errors.spec.ts — Playwright console capture across
  all five primary routes, asserts no "Maximum update depth" warnings.
- ui/src/hooks/__tests__/useLastVisit.test.ts — asserts `touch` identity
  is stable across re-renders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds @axe-core/playwright audit asserting 0 wcag2a/wcag2aa violations on
/, /notes, /docs, /graph, and /mcp.

Violations fixed:
- button-name: project SelectTrigger in app-sidebar.tsx had no accessible
  name. Added aria-label="Select project".
- color-contrast: primary green (oklch(0.68 0.16 152) / ~#32b364) against
  white --primary-foreground only reached 2.7:1. Darkened to
  oklch(0.52 0.16 152) → ~5.1:1, passing WCAG AA. --ring kept at original
  brightness since it's a focus outline, not text.

Covered across all primary routes via e2e/a11y.spec.ts (Chromium project,
/mcp reached via SPA history to avoid the Vite dev-server proxy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e (5.7)

Adds fade/slide/pop transition factories that collapse duration to 0 when
useReducedMotion() returns true, plus fadeInVariants/slideUpVariants for
common mount animations. Preserves existing enterTransition/exitTransition
exports for backward compat. No motion.* callsites exist in src/ today,
so Step 6 wiring is vacuous — the factories are ready for future callsites.

Also adds a prefers-reduced-motion CSS media query so non-Framer animations
(CSS keyframes, transitions) also respect user preference.

Covered by Vitest (ui/src/lib/__tests__/motion.test.ts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shell now captures document.activeElement when the palette opens and
restores focus on close via requestAnimationFrame. Also exposes an
invoker-aware openPalette callback that both the header button and
the Ctrl/Cmd+K hotkey call through.

Playwright coverage (ui/e2e/focus.spec.ts):
- skip-link is the first Tab target and Enter moves focus to main#main
- palette returns focus to the invoking search button on close
- Radix dialog traps focus (20 consecutive Tabs stay inside the dialog)

Addresses Block 5.8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inline <head> script reads the Zustand-persisted theme at key `docsiq-ui`
and applies `document.documentElement.classList` and `data-theme` before
React hydrates, eliminating FOUC on dark-theme first paint.

The script is defensive (try/catch silently falls through when localStorage
is unavailable, e.g. privacy mode) and kept small (~40 lines). Note: when
Block 2's CSP rolls out with `script-src` restrictions, the SHA-256 hash of
this inline script must be added to the allowlist (computable via
`node -e "const crypto=require('crypto');..."`). Left as a Block-2 follow-up.

Playwright coverage (ui/e2e/theme-flash.spec.ts):
- dark: .dark class applied + data-theme="dark"
- light: no .dark class
- system: resolves via prefers-color-scheme
Smoke suite also passes unchanged.

Addresses Block 5.9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l-screen palette (5.10)

Enforces 44x44 CSS-pixel minimum on header buttons under 480px, makes
the command palette fill the viewport on small screens, and wraps the
DocumentsList table in .table-scroll so horizontal overflow stays
contained (not pushed to body).

Playwright coverage at 375x812 (ui/e2e/mobile.spec.ts):
- sidebar collapses/hides
- all header buttons >= 44x44
- command palette >= 320px wide (fills viewport via CSS)
- /docs body does not horizontally overflow

Addresses Block 5.10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aksOps aksOps enabled auto-merge (squash) April 24, 2026 02:24
@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​axe-core/​playwright@​4.11.21001001009970

View full report

@aksOps aksOps merged commit d5aa179 into main Apr 24, 2026
11 of 12 checks passed
@aksOps aksOps deleted the feat/block5-ui-polish branch April 24, 2026 02:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant