feat(ui): scaffold the codegraph ui viewer as a Svelte 5 + Vite workspace (CG-40)

Adds `ui/` as an npm workspace (Svelte 5.56 + Vite 7, devDependencies only —
the engine's runtime dependencies are untouched) and chains its build into
`npm run build`, so the browser viewer ships inside `dist/` with everything
else: `build-bundle.sh` already copies `dist` wholesale and `pack-npm.sh`
packs that bundle.

Output is `dist/viewer/`, NOT `dist/ui/`: `src/ui/` is the engine's terminal
ui (shimmer progress + its worker) and tsc compiles it to `dist/ui/`, so
emitting there both deletes those modules — the CLI then dies at startup with
`Cannot find module '../ui/shimmer-progress'` — and would leave the static
server handing out compiled engine internals. The design spec is corrected to
match.

`scripts/check-ui-build.mjs` is the release guard: index.html must exist, be
non-trivial, and every local asset it references must be on disk, and the
compiled engine next door must still be intact. It runs after every UI build,
again in `build-bundle.sh` once the bundle stage has copied `dist`, and again
in `pack-npm.sh` once each archive is unpacked — so a broken viewer fails the
release instead of shipping a CLI that serves a 404.

`vite build` does not override an ambient NODE_ENV, so a shell or runner with
NODE_ENV=development silently shipped dev-mode Svelte (~13 kB of dev-only
runtime checks, warning in the user's console). The config now pins production
for `command === 'build'`; macOS and Windows ARM64 then emit byte-identical
bundle hashes.

The shell itself follows docs/design/codegraph-ui-design-spec.md §2–§3.1:
design tokens as CSS custom properties (light on bare `:root`, dark under both
`prefers-color-scheme` and `[data-theme="dark"]`), square corners, hairline
rules, one oxblood accent; top bar 48px / trail bar 34px / main; a hash router
over `#/s/<id>`, `#/file/<path>`, with `#/map` and `#/flow` reserved for phase
2. Fonts are vendored through @fontsource rather than fetched, so a local
reader works offline and never announces the project to a CDN.

Verified: clean `npm run build` from an empty dist on macOS and on the Windows
ARM64 VM (forward-slash asset URLs, CLI still starts, both assertion failure
modes exit 1); `dist/viewer` present in a real darwin-arm64 bundle and in the
packed npm platform package; shell geometry, tokens, all seven routes, both
themes and font loading checked in headless Chromium with no console errors;
`npm test` unaffected.
This commit is contained in:
Colby McHenry
2026-08-26 15:55:10 -05:00
parent 6a056ec5db
commit a72f22a6d3
27 changed files with 2970 additions and 4 deletions
+202
View File
@@ -0,0 +1,202 @@
/* =====================================================================
codegraph ui — design tokens + global primitives
The engine's paper/ink editorial system (site/src/styles/theme.css),
as specified in docs/design/codegraph-ui-design-spec.md §2: flat,
hairline rules, square corners everywhere, no shadows, no gradients,
sentence case, one oxblood accent, one amber (the "untested" badge).
Component-specific rules live in each .svelte file's scoped <style>.
Only tokens, resets and cross-view primitives belong here.
===================================================================== */
/* ---------- tokens: light / paper (the bare :root set) ---------- */
:root {
--paper: #f7f6f2;
--paper-2: #f1efe8;
--press: #e8e6dd;
--press-2: #dedbd0;
--ink: #16150f;
--ink-2: #56544a;
--ink-3: #87847a;
--ink-4: #b4b1a5;
--rule: #16150f;
--rule-soft: #d6d3c8;
--rule-faint: #e6e3d9;
--accent: #7a2230;
--accent-ink: #5e1a25;
--accent-soft: #f0e3e5;
--accent-line: #d9b3b9;
--amber: #8a5a0b;
--amber-soft: #f3e9d2;
--sans: 'Archivo Variable', 'Archivo', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif;
--mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
--code-size: 12.5px;
--code-lh: 20px;
/* App-shell geometry, shared by the grid and by anything that has to
offset itself under the bars (sticky rail headers, SVG overlays). */
--topbar-h: 48px;
--trailbar-h: 34px;
color-scheme: light dark;
}
/* ---------- tokens: dark / ink ----------
Every colour is defined on the bare :root above; these blocks only
redefine. `:not([data-theme="light"])` lets an explicit light choice
win over the OS preference. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
--paper: #16150f;
--paper-2: #1c1a14;
--press: #23211a;
--press-2: #2c2a22;
--ink: #f3f1ea;
--ink-2: #b8b5a8;
--ink-3: #87847a;
--ink-4: #5d5b52;
--rule: #f3f1ea;
--rule-soft: #34322a;
--rule-faint: #26241d;
--accent: #d48b96;
--accent-ink: #e5a5ae;
--accent-soft: #33201f;
--accent-line: #6b3a42;
--amber: #d9a94a;
--amber-soft: #2e2716;
}
}
:root[data-theme='dark'] {
--paper: #16150f;
--paper-2: #1c1a14;
--press: #23211a;
--press-2: #2c2a22;
--ink: #f3f1ea;
--ink-2: #b8b5a8;
--ink-3: #87847a;
--ink-4: #5d5b52;
--rule: #f3f1ea;
--rule-soft: #34322a;
--rule-faint: #26241d;
--accent: #d48b96;
--accent-ink: #e5a5ae;
--accent-soft: #33201f;
--accent-line: #6b3a42;
--amber: #d9a94a;
--amber-soft: #2e2716;
}
/* ---------- reset ---------- */
html,
body {
height: 100%;
}
body {
margin: 0;
/* body always paints --paper: the bars are transparent over it and a
short view must not reveal the browser's own canvas colour. */
background: var(--paper);
color: var(--ink);
font-family: var(--sans);
font-size: 13px;
line-height: 1.45;
-webkit-font-smoothing: antialiased;
}
*,
*::before,
*::after {
box-sizing: border-box;
/* Square corners are non-negotiable in this system, including on the
UA-styled controls (input, select, button) we do not restyle. */
border-radius: 0 !important;
}
a {
color: inherit;
text-decoration: none;
}
button {
font: inherit;
color: inherit;
background: none;
border: 0;
padding: 0;
cursor: pointer;
}
h1,
h2,
h3 {
font-weight: 600;
text-wrap: balance;
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition: none !important;
animation: none !important;
scroll-behavior: auto !important;
}
}
/* ---------- app shell ----------
Design spec §3.1: top bar 48px / trail bar 34px / main. The grid is on
index.html's mount host, which App.svelte fills directly (no wrapper — a
second #app would duplicate the id). */
#app {
height: 100vh;
display: grid;
grid-template-rows: var(--topbar-h) var(--trailbar-h) 1fr;
}
/* ---------- cross-view primitives ---------- */
.mono {
font-family: var(--mono);
}
.dim {
color: var(--ink-3);
}
.tnum {
font-variant-numeric: tabular-nums;
}
/* Empty / not-yet-loaded states. Sentence case, no exclamation marks —
say what is missing and what to do about it. */
.emptystate {
padding: 40px;
max-width: 60ch;
color: var(--ink-2);
line-height: 1.5;
}
.emptystate h2 {
margin: 0 0 8px;
font-size: 16px;
color: var(--ink);
}
.emptystate p {
margin: 0 0 10px;
}
.emptystate code {
font-family: var(--mono);
font-size: 12px;
background: var(--press);
padding: 1px 4px;
}