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
+29
View File
@@ -0,0 +1,29 @@
import type { Plugin } from "vite";
// The design lab (`src/design-lab/`) is a dev-only page: main.tsx reaches it behind
// `import.meta.env.DEV`, which Vite folds to `false` in a production build, so the dynamic import
// is dead code and the chunk is never emitted. That rule used to be a sentence; this makes it a
// build failure. If a future edit imports the lab unguarded (a static import, a shared helper, a
// re-export), the production build refuses instead of shipping the lab to every visitor.
const DEV_ONLY_DIR = "/src/design-lab/";
export function devOnlyGuard(): Plugin {
return {
name: "siftlode-dev-only-guard",
apply: "build",
generateBundle(_options, bundle) {
for (const [fileName, output] of Object.entries(bundle)) {
if (output.type !== "chunk") continue;
const leaked = Object.keys(output.modules).find((id) =>
id.replaceAll("\\", "/").includes(DEV_ONLY_DIR),
);
if (leaked) {
this.error(
`dev-only module ${leaked} reached the production bundle (${fileName}). ` +
"Import src/design-lab only behind `import.meta.env.DEV`.",
);
}
}
},
};
}