/** * Design systems — the stylesheet held as records (milestone #254). * * A design system is a named set of tokens with an optional parent. A system * with no parent is a "family"; one with a parent holds ONLY what it changes, * so "what does this app alter?" is a plain list rather than a diff. */ import { apiDelete, apiGet, apiPatch, apiPost, apiPut } from "@/api/client"; export interface DesignSystem { id: number; owner_user_id: number; title: string; description: string; /** Narrative a token table cannot hold: aesthetic, voice, what's out of scope. */ guidance: string; parent_id: number | null; created_at: string | null; updated_at: string | null; } /** A token as STORED — one system's own row for it. */ export interface DesignToken { id: number; design_system_id: number; name: string; /** Values keyed by mode. `base` applies when no mode is more specific. */ value_by_mode: Record; group_name: string | null; purpose: string | null; /** WHY it is this value — distinct from `purpose`, which is what it is FOR. */ rationale: string | null; /** Literal values this token should be used INSTEAD OF, e.g. ["#fff"]. * * How a design system records what a prohibition was trying to say: not * "white is banned" but "write this token instead". Declared rather than * inferred, because a superseded literal and the token's own value are * usually different values and nothing could connect them by matching. */ supersedes: string[]; order_index: number; } export interface Contribution { system_id: number; value: string; } /** * A token after the cascade. * * `contributions` is every system that offered a value, per mode, DEEPEST * FIRST — entry 0 won and the rest were shadowed. `value_by_mode` and * `origin_by_mode` are the winners, provided so the client never has to derive * them (and so it cannot derive them differently). * * Provenance is per MODE because overriding is: a system can own `base` and * inherit `dark` at the same time. */ export interface ResolvedToken { name: string; group_name: string | null; purpose: string | null; rationale: string | null; supersedes: string[]; order_index: number; value_by_mode: Record; origin_by_mode: Record; contributions: Record; } export const fetchDesignSystems = () => apiGet<{ design_systems: DesignSystem[] }>("/api/design-systems"); export const fetchDesignSystem = (id: number) => apiGet(`/api/design-systems/${id}`); export const createDesignSystem = (body: { title: string; description?: string; guidance?: string; parent_id?: number | null; }) => apiPost("/api/design-systems", body); /** Omit `parent_id` to leave it alone; send `null` to make the system a family. */ export const updateDesignSystem = ( id: number, body: { title?: string; description?: string; guidance?: string; parent_id?: number | null; }, ) => apiPatch(`/api/design-systems/${id}`, body); export const deleteDesignSystem = (id: number) => apiDelete(`/api/design-systems/${id}`); /** The EFFECTIVE set: everything inherited, with this system's on top. */ export const fetchResolvedTokens = (id: number) => apiGet<{ design_system_id: number; tokens: ResolvedToken[] }>( `/api/design-systems/${id}/resolved`, ); /** This system's OWN tokens — its override set. */ export const fetchDesignTokens = (id: number) => apiGet<{ tokens: DesignToken[] }>(`/api/design-systems/${id}/tokens`); export const createDesignToken = ( designSystemId: number, body: { name: string; value_by_mode?: Record; group_name?: string | null; purpose?: string | null; rationale?: string | null; supersedes?: string[]; order_index?: number; }, ) => apiPost(`/api/design-systems/${designSystemId}/tokens`, body); export const updateDesignToken = ( tokenId: number, body: Partial>, ) => apiPatch(`/api/design-tokens/${tokenId}`, body); export const deleteDesignToken = (tokenId: number) => apiDelete(`/api/design-tokens/${tokenId}`); /** Point a project at a design system. `null` clears it. */ export const setProjectDesignSystem = ( projectId: number, designSystemId: number | null, ) => apiPut<{ project_id: number; design_system_id: number | null }>( `/api/projects/${projectId}/design-system`, { design_system_id: designSystemId }, ); 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; derivation: { /** Tokens computed from others, mapped to what they're computed from. */ derived: Record; /** Formulas pointing at tokens that don't exist — the browser drops these. */ unknown_refs: Record; /** Derivation loops, which resolve to nothing for the same reason. */ cycles: 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(`/api/design-systems/${id}/stylesheet`); export interface SnippetFinding { snippet_id: number; title: string; /** References that resolve against the sheet. */ used: string[]; /** `var(--x)` where the system has no `--x` — renders as nothing at all. */ unknown: string[]; /** Literals the sheet says to stop writing, paired with what to write. */ superseded_literals: { literal: string; use_instead: string }[]; /** Custom properties the snippet mints for itself instead of reusing. */ local_definitions: string[]; } export interface SnippetCheck { design_system_id: number; checked: number; /** Only snippets with something to act on; clean ones are omitted. */ findings: SnippetFinding[]; } /** Which recorded snippets disagree with this design system's sheet. */ export const checkSnippets = (id: number) => apiGet(`/api/design-systems/${id}/snippet-check`);