feat(design): design lab + DESIGN.md skeleton (E28 S2a)

- docs/DESIGN.md: the written source of truth for the UI. Decided chapters
  (principles, device adaptation, action placement, text stays selectable,
  interaction rules, copy, anti-patterns, decision log); controls and
  surfaces are marked Pending until the owner picks from the lab.
- /design-lab (dev only): renders each open decision's variants with the
  app's real CSS, themes and font scale. Batch A = six control decisions.
- devOnlyGuard: a production build fails if anything under src/design-lab
  reaches the bundle (seen red with an unguarded import).
- Size contract: tablet portrait + landscape and up, replacing the
  1024x768 floor with its never-built "too small" message.
This commit is contained in:
2026-09-24 01:34:12 +02:00
parent 58878958d8
commit d2e40dcffb
14 changed files with 1087 additions and 10 deletions
+12 -2
View File
@@ -38,6 +38,10 @@ const App = lazyWithPreload(() => import("./App"));
const PrivacyPolicy = lazy(() => import("./components/legal/PrivacyPolicy"));
const Terms = lazy(() => import("./components/legal/Terms"));
const WatchPage = lazy(() => import("./components/WatchPage"));
// The design lab (E28): the living style guide, rendered with the app's real CSS so a design decision
// is taken on what will ship. DEV only — Vite folds the condition to `false` in a production build,
// the import becomes dead code and the chunk is never emitted (devOnlyGuard fails the build if it is).
const DesignLab = import.meta.env.DEV ? lazy(() => import("./design-lab/DesignLab")) : null;
const queryClient = createQueryClient();
@@ -49,13 +53,19 @@ de.theme ||= "dark";
de.scheme ||= "midnight";
const path = window.location.pathname;
const isPublicPage = path === "/privacy" || path === "/terms" || path.startsWith("/watch/");
const isLab = DesignLab !== null && path === "/design-lab";
const isPublicPage =
path === "/privacy" || path === "/terms" || path.startsWith("/watch/") || isLab;
// Fetch the app chunk NOW, alongside BootGate's identity probe, instead of only once the gate opens:
// the two waits then overlap rather than queue, and the gate usually opens straight onto the app. A
// failed fetch is left to the render's own load, which retries it and reports to the ErrorBoundary.
if (!isPublicPage) App.preload().catch(() => {});
const root =
path === "/privacy" ? (
isLab && DesignLab ? (
<FloatingTree>
<DesignLab />
</FloatingTree>
) : path === "/privacy" ? (
<PrivacyPolicy />
) : path === "/terms" ? (
<Terms />