feat(design-systems): the master sheet — purpose tokens, not per-element values
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 42s
CI & Build / Build & push image (push) Successful in 34s

Operator's new requirement (#2299, architecture in #2296): a design system does
not just hold tokens, it generates and manages a master CSS sheet. That settles
the milestone's open "authority mechanism" question — the record is
authoritative because the stylesheet comes out of it.

**The sheet is shaped by purpose and styles no elements.** It declares custom
properties, grouped by what they mean, and contains no `.btn-primary`, no
`table`, no `input`. That is the design, not a shortcut: a sheet that styled
elements would restate the same handful of values once per element and grow with
the UI, where purpose-named values are stated once and reused. Components live
as SNIPPETS that reference these names — a surface that already exists and
already carries prose, locations, drift checks, merge and write-path recall.

A token named after an element (`--fs-button-bg`) is the smell that the two have
been mixed; a purpose name (`--fs-action-primary`) is reused across all of them.

Alongside the CSS the endpoint returns what the text cannot say for itself:
which tokens are still valueless, and which VALUES are declared under more than
one name. The second is the operator's "reuse consistent values" constraint made
checkable — and it reports rather than refuses, because a design system
legitimately aligns colours on purpose ("Success = Moss, by design") and only a
human knows which case it is.

Mode maps to selector the way the codebase already does it: base on the root
selector, every other mode layered on `[data-theme="…"]`. The root selector is a
PARAMETER — #251 recorded that a container-scoped preview cannot use `:root`, so
hardcoding it would have made the generator useless to the preview surface.

A token the rulebook names but states no value for is emitted as a commented-out
declaration IN ITS GROUP rather than dropped. Its absence is the finding, and a
comment puts that finding where the reader already is.

Values are validated, not escaped, and this is a real boundary rather than
tidiness: design systems are shareable records (rule #47), so `red; } body {
display: none` in a system shared with you would otherwise inject CSS into your
page. A value containing `{ } ; @ < >`, a comment delimiter or a newline is
REFUSED and rendered as a comment saying so — rejecting beats stripping, since a
partially-sanitised value is one the operator never wrote and the sheet's whole
claim is that it is the record.

Not in scope, and deliberately: serving this as the app's actual stylesheet.
Generating and exposing a sheet is reversible; swapping theme.css for a
generated one is not, and it should be an explicit call rather than a side
effect.
This commit is contained in:
2026-07-30 21:42:08 -04:00
parent 4dc57f8ab2
commit b0a7d9e89b
9 changed files with 737 additions and 1 deletions
+19
View File
@@ -159,3 +159,22 @@ export const importFromRulebook = (
rulebook_id: rulebookId,
apply,
});
export interface StylesheetResult {
design_system_id: number;
/** The master sheet: purpose tokens only, no element or class rules. */
css: string;
token_count: number;
/** Tokens the system names but has no value for yet. */
valueless: string[];
/** Values declared under more than one name — alias, or one idea twice. */
duplicates: Record<string, string[]>;
}
/** The master CSS sheet a design system generates.
*
* Purpose tokens only. Components (buttons, tables, input schemes) are
* snippets that reference these names, so a value is stated once and reused
* rather than restated per element. */
export const fetchStylesheet = (id: number) =>
apiGet<StylesheetResult>(`/api/design-systems/${id}/stylesheet`);
+139
View File
@@ -29,6 +29,7 @@ import {
fetchDesignSystems,
fetchDesignTokens,
fetchResolvedTokens,
fetchStylesheet,
importFromRulebook,
updateDesignSystem,
updateDesignToken,
@@ -36,6 +37,7 @@ import {
type DesignToken,
type ImportReport,
type ResolvedToken,
type StylesheetResult,
} from "@/api/designSystems";
import { listRulebooks, type Rulebook } from "@/api/rulebooks";
import { ApiError } from "@/api/client";
@@ -385,6 +387,52 @@ const overrideCount = computed(
() => resolved.value.filter((t) => Object.values(t.origin_by_mode).some((id) => id === selectedId.value)).length,
);
// --- the master sheet -------------------------------------------------------
const sheet = ref<StylesheetResult | null>(null);
const sheetLoading = ref(false);
const showSheet = ref(false);
async function loadSheet() {
if (selectedId.value === null) return;
sheetLoading.value = true;
try {
sheet.value = await fetchStylesheet(selectedId.value);
} catch {
sheet.value = null;
toast.show("Failed to render the stylesheet", "error");
} finally {
sheetLoading.value = false;
}
}
/** Toggle, and render on first open rather than on mount — the sheet is a
* derived view most visits don't need, and rendering it unasked would cost a
* round trip per system selection. */
function toggleSheet() {
showSheet.value = !showSheet.value;
if (showSheet.value && !sheet.value) loadSheet();
}
async function copySheet() {
if (!sheet.value) return;
try {
await navigator.clipboard.writeText(sheet.value.css);
toast.show("Stylesheet copied");
} catch {
toast.show("Couldn't copy — select the text instead", "error");
}
}
/** Re-render whenever the tokens behind it change, so the sheet on screen is
* never a stale answer to a question the operator has already moved past. */
watch([selectedId, ownTokens], () => {
sheet.value = null;
if (showSheet.value) loadSheet();
});
const duplicateEntries = computed(() => Object.entries(sheet.value?.duplicates ?? {}));
// --- import from a rulebook -------------------------------------------------
const rulebooks = ref<Rulebook[]>([]);
@@ -582,6 +630,69 @@ function isColourish(value: string): boolean {
</div>
</section>
<!-- The master sheet -->
<section class="ds-section">
<div class="section-head">
<h2>Master stylesheet</h2>
<button
class="btn-ghost btn-small"
@click="toggleSheet"
>
{{ showSheet ? "Hide" : "Show" }}
</button>
</div>
<template v-if="showSheet">
<p class="section-note">
What this system generates. <strong>Purpose tokens only</strong> —
it declares what values mean and styles no elements. Buttons,
tables and input schemes live as snippets that reference these
names, so each value is stated once and reused rather than
restated per element.
</p>
<p v-if="sheetLoading" class="muted">Rendering…</p>
<template v-else-if="sheet">
<p class="section-note">
<strong>{{ sheet.token_count }}</strong> tokens ·
<strong>{{ sheet.valueless.length }}</strong> still without a value ·
<strong>{{ duplicateEntries.length }}</strong> values declared
under more than one name
</p>
<div v-if="duplicateEntries.length" class="notice notice-warn">
<strong>Some values are declared twice.</strong>
<p>
Either a deliberate alias or one idea recorded under two
names — only you can tell which, so nothing is changed here.
</p>
<ul class="dupe-list">
<li v-for="[value, names] in duplicateEntries" :key="value">
<span
v-if="isColourish(value)" class="swatch"
:style="{ background: value }" aria-hidden="true"
/>
<code>{{ value }}</code> — {{ names.join(", ") }}
</li>
</ul>
</div>
<div class="row-actions">
<button class="btn-ghost btn-small" @click="copySheet">Copy</button>
<a
class="btn-ghost btn-small"
:href="`/api/design-systems/${selectedId}/stylesheet?format=css`"
download
>Download</a>
<button class="btn-ghost btn-small" @click="loadSheet">Re-render</button>
</div>
<pre class="sheet"><code>{{ sheet.css }}</code></pre>
</template>
</template>
</section>
<!-- Import from a rulebook -->
<section class="ds-section">
<div class="section-head">
@@ -1219,6 +1330,34 @@ function isColourish(value: string): boolean {
gap: 0.35rem;
}
.sheet {
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
padding: 0.75rem 1rem;
margin-top: 0.75rem;
overflow-x: auto;
font-family: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;
font-size: 0.8rem;
line-height: 1.6;
max-height: 30rem;
overflow-y: auto;
}
.dupe-list {
list-style: none;
margin: 0.5rem 0 0;
padding: 0;
font-size: 0.85rem;
}
.dupe-list li {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.15rem 0;
}
.import-summary {
margin-top: 0.75rem;
}