@1agh/maude 0.17.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/cli/commands/design.mjs +92 -29
  2. package/package.json +8 -8
  3. package/plugins/design/dev-server/bin/screenshot.sh +12 -0
  4. package/plugins/design/dev-server/boot-self-heal.ts +90 -0
  5. package/plugins/design/dev-server/build.ts +18 -2
  6. package/plugins/design/dev-server/config.schema.json +12 -0
  7. package/plugins/design/dev-server/dist/client.bundle.js +3 -3
  8. package/plugins/design/dev-server/runtime-bundle.ts +29 -1
  9. package/plugins/design/dev-server/server.ts +6 -0
  10. package/plugins/design/dev-server/test/boot-self-heal.test.ts +112 -0
  11. package/plugins/design/dev-server/test/runtime-bundle-error-mapping.test.ts +43 -0
  12. package/plugins/design/templates/canvas.tsx.template +7 -7
  13. package/plugins/design/templates/design-system-inspiration/audience-pro/colors-presence.html +1 -1
  14. package/plugins/design/templates/design-system-inspiration/core/README.philosophy.md.tpl +11 -7
  15. package/plugins/design/templates/design-system-inspiration/core/SKILL.md.tpl +4 -3
  16. package/plugins/design/templates/design-system-inspiration/core/colors_and_type.css.tpl +61 -57
  17. package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +2 -0
  18. package/plugins/design/templates/design-system-inspiration/core/preview/colors-accent.html +4 -4
  19. package/plugins/design/templates/design-system-inspiration/meta/presence-multiplayer.html +1 -1
  20. package/plugins/design/templates/design-system-inspiration/platform-desktop/ui_kits-desktop-showcase.html +3 -0
  21. package/plugins/design/templates/design-system-inspiration/platform-mobile/ui_kits-mobile-showcase.html +1 -1
  22. package/plugins/design/templates/design-system-inspiration/theme-both/colors-themes-side-by-side.html +6 -6
  23. package/plugins/design/templates/design-system-inspiration/universal/components-dialogs.html +3 -2
  24. package/plugins/design/templates/design-system-inspiration/universal/logo.html +2 -1
@@ -194,7 +194,12 @@ export async function getRuntimeBundle(pkg: RuntimePackage): Promise<BundleCache
194
194
  return `[${lvl}] ${l.message}`;
195
195
  })
196
196
  .join('\n');
197
- throw new Error(`Failed to build runtime bundle for "${pkg}":\n${msg || '(no log messages)'}`);
197
+ const remediation = bunCacheRemediation(pkg, msg);
198
+ throw new Error(
199
+ `Failed to build runtime bundle for "${pkg}":\n${msg || '(no log messages)'}${
200
+ remediation ? `\n\n${remediation}` : ''
201
+ }`
202
+ );
198
203
  }
199
204
 
200
205
  const out = built.outputs[0];
@@ -206,6 +211,29 @@ export async function getRuntimeBundle(pkg: RuntimePackage): Promise<BundleCache
206
211
  return entry;
207
212
  }
208
213
 
214
+ /**
215
+ * Detect the "Bun's global install cache is in a bad state" failure mode and
216
+ * return a one-paragraph remediation message. Returns null when the build
217
+ * failure has a different shape (real syntax error, missing package, etc.) —
218
+ * the original log is enough then.
219
+ *
220
+ * Symptoms: log messages like `EISDIR reading '/Users/foo/.bun/install/cache/
221
+ * react@19.2.6@@@1 @@1/index.js'` or `ENOENT … .bun/install/cache/<pkg>@…`.
222
+ * Surfacing the cache path + the exact `bun pm cache rm <pkg>` command saves
223
+ * the user from grepping the error to figure out what to do. Phase 19 / DDR-044.
224
+ */
225
+ export function bunCacheRemediation(pkg: string, log: string): string | null {
226
+ const cacheHit = /(EISDIR|ENOENT).*\.bun\/install\/cache\/([\w@/.-]+?)(?:@@@|\/)/i.test(log);
227
+ if (!cacheHit) return null;
228
+ const basePkg = pkg.split('/')[0] ?? pkg;
229
+ return [
230
+ ` ⚠ Bun's global package cache for "${basePkg}" appears to be in a bad state`,
231
+ ` (truncated install, EISDIR/ENOENT on an index file).`,
232
+ ``,
233
+ ` Fix: run \`bun pm cache rm ${basePkg}\` then reload the page.`,
234
+ ].join('\n');
235
+ }
236
+
209
237
  function escapeRegex(s: string): string {
210
238
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
211
239
  }
@@ -23,6 +23,12 @@ import { createHttp } from './http.ts';
23
23
  import { createInspect } from './inspect.ts';
24
24
  import { startHeapWatch } from './mem.ts';
25
25
  import { createWs } from './ws.ts';
26
+ import { bootSelfHeal } from './boot-self-heal.ts';
27
+
28
+ // Phase 19 / DDR-044 — covers the marketplace-cache-install gap where
29
+ // node_modules/ ships empty (git clone honors .gitignore). Auto-installs +
30
+ // builds on first boot; opt out with MAUDE_NO_AUTOBUILD=1.
31
+ await bootSelfHeal();
26
32
 
27
33
  const ctx = createContext();
28
34
 
@@ -0,0 +1,112 @@
1
+ // Boot self-heal logic — Phase 19 / DDR-044.
2
+ // Covers the marketplace-cache-install gap: if dist/ or node_modules/ is
3
+ // missing on boot, self-heal runs bun install + build. Opt out with
4
+ // MAUDE_NO_AUTOBUILD=1.
5
+
6
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
7
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+
11
+ import { bootSelfHeal, type SelfHealOptions } from '../boot-self-heal.ts';
12
+
13
+ let TMP: string;
14
+
15
+ beforeEach(() => {
16
+ TMP = mkdtempSync(join(tmpdir(), 'maude-self-heal-'));
17
+ });
18
+
19
+ afterEach(() => {
20
+ rmSync(TMP, { recursive: true, force: true });
21
+ });
22
+
23
+ function harness(extra: Partial<SelfHealOptions> = {}) {
24
+ const calls: { cmd: readonly string[]; cwd: string }[] = [];
25
+ const logs: string[] = [];
26
+ let exited: number | null = null;
27
+ const opts: SelfHealOptions = {
28
+ here: TMP,
29
+ optOut: false,
30
+ spawn: async (cmd, cwd) => {
31
+ calls.push({ cmd, cwd });
32
+ return { code: 0 };
33
+ },
34
+ log: (m) => logs.push(m),
35
+ exit: ((code: number) => {
36
+ exited = code;
37
+ throw new Error(`__exit:${code}`);
38
+ }) as never,
39
+ ...extra,
40
+ };
41
+ return { opts, calls, logs, getExited: () => exited };
42
+ }
43
+
44
+ function seedDist() {
45
+ mkdirSync(join(TMP, 'dist'), { recursive: true });
46
+ writeFileSync(join(TMP, 'dist', 'client.bundle.js'), '/* stub */');
47
+ }
48
+
49
+ function seedDeps() {
50
+ mkdirSync(join(TMP, 'node_modules', 'react'), { recursive: true });
51
+ writeFileSync(join(TMP, 'node_modules', 'react', 'package.json'), '{}');
52
+ }
53
+
54
+ describe('bootSelfHeal', () => {
55
+ test('skips when dist + node_modules both present', async () => {
56
+ seedDist();
57
+ seedDeps();
58
+ const { opts, calls } = harness();
59
+ const result = await bootSelfHeal(opts);
60
+ expect(result.skipped).toBe('all-present');
61
+ expect(result.ran).toEqual([]);
62
+ expect(calls).toEqual([]);
63
+ });
64
+
65
+ test('runs `bun install --production` when node_modules/react is missing', async () => {
66
+ seedDist();
67
+ const { opts, calls } = harness();
68
+ const result = await bootSelfHeal(opts);
69
+ expect(result.ran).toEqual(['install']);
70
+ expect(calls).toHaveLength(1);
71
+ expect(calls[0]?.cmd).toEqual(['bun', 'install', '--production']);
72
+ expect(calls[0]?.cwd).toBe(TMP);
73
+ });
74
+
75
+ test('runs `bun run build.ts` when dist/client.bundle.js is missing', async () => {
76
+ seedDeps();
77
+ const { opts, calls } = harness();
78
+ const result = await bootSelfHeal(opts);
79
+ expect(result.ran).toEqual(['build']);
80
+ expect(calls).toHaveLength(1);
81
+ expect(calls[0]?.cmd).toEqual(['bun', 'run', 'build.ts']);
82
+ });
83
+
84
+ test('runs install BEFORE build when both missing (build needs deps)', async () => {
85
+ const { opts, calls } = harness();
86
+ const result = await bootSelfHeal(opts);
87
+ expect(result.ran).toEqual(['install', 'build']);
88
+ expect(calls[0]?.cmd).toEqual(['bun', 'install', '--production']);
89
+ expect(calls[1]?.cmd).toEqual(['bun', 'run', 'build.ts']);
90
+ });
91
+
92
+ test('MAUDE_NO_AUTOBUILD=1: exits 1 with remediation message; no spawn', async () => {
93
+ const { opts, calls, logs, getExited } = harness({ optOut: true });
94
+ await expect(bootSelfHeal(opts)).rejects.toThrow('__exit:1');
95
+ expect(getExited()).toBe(1);
96
+ expect(calls).toEqual([]);
97
+ expect(logs.join('\n')).toMatch(/MAUDE_NO_AUTOBUILD=1/);
98
+ expect(logs.join('\n')).toMatch(/dist\/client\.bundle\.js/);
99
+ expect(logs.join('\n')).toMatch(/node_modules\/react/);
100
+ });
101
+
102
+ test('spawn failure aborts with exit 1 + remediation hint', async () => {
103
+ seedDist(); // only deps missing
104
+ const { opts, logs, getExited } = harness({
105
+ spawn: async () => ({ code: 42 }),
106
+ });
107
+ await expect(bootSelfHeal(opts)).rejects.toThrow('__exit:1');
108
+ expect(getExited()).toBe(1);
109
+ expect(logs.join('\n')).toMatch(/exited 42/);
110
+ expect(logs.join('\n')).toMatch(/MAUDE_NO_AUTOBUILD=1/);
111
+ });
112
+ });
@@ -0,0 +1,43 @@
1
+ // Phase 19 / DDR-044 — runtime-bundle error-message remediation.
2
+ // When Bun.build fails because the global install cache is corrupted
3
+ // (EISDIR/ENOENT on a cached package's entry file), surface an actionable
4
+ // `bun pm cache rm <pkg>` hint instead of just relaying the raw log.
5
+
6
+ import { describe, expect, test } from 'bun:test';
7
+
8
+ import { bunCacheRemediation } from '../runtime-bundle.ts';
9
+
10
+ describe('bunCacheRemediation', () => {
11
+ test('returns null on unrelated build failures (real syntax errors etc.)', () => {
12
+ expect(bunCacheRemediation('react', '[error] Unexpected token <')).toBeNull();
13
+ expect(bunCacheRemediation('react-dom', '[error] Could not resolve "missing-pkg"')).toBeNull();
14
+ });
15
+
16
+ test('matches EISDIR on .bun/install/cache + names the base package', () => {
17
+ const log =
18
+ "[error] EISDIR reading '/Users/iagh/.bun/install/cache/react@19.2.6@@@1 @@1/index.js'";
19
+ const out = bunCacheRemediation('react', log);
20
+ expect(out).not.toBeNull();
21
+ expect(out).toMatch(/bun pm cache rm react/);
22
+ expect(out).toMatch(/bad state/);
23
+ });
24
+
25
+ test('matches ENOENT on .bun/install/cache too', () => {
26
+ const log = "[error] ENOENT: '/Users/x/.bun/install/cache/react-dom@19.0.0/index.js'";
27
+ const out = bunCacheRemediation('react-dom', log);
28
+ expect(out).toMatch(/bun pm cache rm react-dom/);
29
+ });
30
+
31
+ test('subpath specifiers strip to the base package for the cache rm command', () => {
32
+ // bun pm cache works on base package names — "react/jsx-runtime" → "react".
33
+ const log = "[error] EISDIR '/Users/x/.bun/install/cache/react@19.2.6/jsx-runtime.js'";
34
+ const out = bunCacheRemediation('react/jsx-runtime', log);
35
+ expect(out).toMatch(/bun pm cache rm react\b/);
36
+ expect(out).not.toMatch(/bun pm cache rm react\//);
37
+ });
38
+
39
+ test('case-insensitive match (Bun has bounced casing across versions)', () => {
40
+ const log = '[error] eisdir reading /Users/x/.bun/install/cache/react@19/index.js';
41
+ expect(bunCacheRemediation('react', log)).not.toBeNull();
42
+ });
43
+ });
@@ -11,11 +11,11 @@
11
11
  *
12
12
  * Authored under {{DS_NAME}}. Tokens + shared component classes load via the
13
13
  * dev-server's _shell.html harness — link tags arrive automatically from the
14
- * iframe's ?tokens= / ?components= query. Class names match the DS's
15
- * `_components.css` (`.btn`, `.tile`, `.sku`, `.seg`, ...). Bespoke classes go
16
- * into a sibling .module.css iff `css_mode === "modules"`; for `inline` (the
17
- * MDCC-DSN/01 default) prefer existing classes + `style={{}}` for arbitrary
18
- * one-off values.
14
+ * iframe's ?tokens= / ?components= query. Class names come from the DS's
15
+ * own `_components.css` see `system/{{DS_NAME}}/preview/` for the available
16
+ * class shapes. Bespoke classes go into a sibling .module.css iff
17
+ * `css_mode === "modules"`; for `inline` prefer existing classes + `style={{}}`
18
+ * for arbitrary one-off values.
19
19
  *
20
20
  * The envelope primitives (`DesignCanvas`, `DCSection`, `DCArtboard`) and any
21
21
  * specimen helpers come from the dev-server-bundled canvas library via the
@@ -40,11 +40,11 @@ export default function {{COMPONENT_NAME}}() {
40
40
  <DesignCanvas>
41
41
  <DCSection id="overview" title="{{SUBTITLE}}">
42
42
  <DCArtboard id="primary" label="A · primary" width={1280} height={800}>
43
- <div className="mdcc" data-theme="{{THEME_DEFAULT}}">
43
+ <div className="{{ROOT_CLASS}}" data-theme="{{THEME_DEFAULT}}">
44
44
  {/* Replace this scaffold with the actual canvas content. The
45
45
  envelope this template was rendered against is in
46
46
  {{HISTORY_DIR}}/000-envelope.md. */}
47
- <h1 className="sku">{{NAME}}</h1>
47
+ <h1>{{NAME}}</h1>
48
48
  <p>{{BRIEF}}</p>
49
49
  </div>
50
50
  </DCArtboard>
@@ -4,7 +4,7 @@ DEMONSTRATES: --presence-online, --presence-away, --presence-offline; presence d
4
4
  COMPOSITION: 4 avatars with presence dots (online / away / offline / idle) + a "live cursors" mockup with 3 simultaneous users
5
5
  COPY VOICE: real names, real timestamps
6
6
  WHEN SCAFFOLDED: presence family (IF "presence" ∈ activeFamilies — typically pro tools with multiplayer)
7
- NOTES: Presence dot is 8px circle bottom-right of 32-40px avatars (anchor on the visual bottom-right, not on a math grid). Cursor labels are subtle (pill at 11px, low opacity bg). Don't blink presence — too noisy in a busy view.
7
+ NOTES: Presence dot is 8px circle bottom-right of 32-40px avatars (anchor on the visual bottom-right, not on a math grid). Per-user cursor colors below are illustrative only — the inline `oklch(...)` values are NOT part of the project's design system. A real implementation should source these from a `--presence-user-*` family or derive from `--accent` deterministically. Cursor labels are subtle (pill at 11px, low opacity bg). Don't blink presence — too noisy in a busy view.
8
8
  -->
9
9
  <!doctype html>
10
10
  <html lang="en">
@@ -19,13 +19,15 @@
19
19
 
20
20
  ## Foundations
21
21
 
22
- ### One-accent rule
22
+ ### Token contract
23
23
 
24
- Exactly one accent family lives in `colors_and_type.css`: `--accent`, `--accent-hover`, `--accent-active`, `--accent-fg`. **No `--accent2`.** Calls-to-attention compete with each other; one accent forces hierarchy decisions at design time, not runtime.
24
+ All visuals reference `var(--*)` tokens declared in `colors_and_type.css`. Adding a new visual concept means **extending the tokens CSS first**, never inventing values inline in a canvas.
25
25
 
26
- ### Token contract
26
+ {{color_space_block}}
27
+
28
+ ### Accent strategy
27
29
 
28
- All visuals reference `var(--*)` tokens declared in `colors_and_type.css`. No hardcoded hex / px / rem in canvases or production code. Adding a new visual concept means **extending the tokens CSS first**, never inventing values inline.
30
+ {{accent_rules_block}}
29
31
 
30
32
  ### Active token families
31
33
 
@@ -33,8 +35,9 @@ All visuals reference `var(--*)` tokens declared in `colors_and_type.css`. No ha
33
35
 
34
36
  ## Hard rules (non-negotiable)
35
37
 
36
- - **Accessibility:** WCAG 2.1 AA contrast at every theme. Focus-visible always rendered. Touch targets ≥ 44×44. `prefers-reduced-motion: reduce` respected.
37
- - **No off-token colors / radii / spacings** in canvases. Extend tokens; don't inline.
38
+ - **Accessibility:** WCAG 2.1 AA contrast at every theme. Focus-visible always rendered. `prefers-reduced-motion: reduce` respected.
39
+ - **Touch targets:** {{touch_target_rule}}
40
+ - **No off-token values** in canvases. Extend tokens; don't inline.
38
41
  - **No placeholder copy.** Real product strings only — no "Lorem Solutions Inc.", no "Click here".
39
42
  - **Type ladder:** {{type_scale_summary}}
40
43
  - **Motion:** every animation uses a `var(--dur-*)` and `var(--ease-*)` token. No magic numbers.
@@ -52,7 +55,8 @@ All visuals reference `var(--*)` tokens declared in `colors_and_type.css`. No ha
52
55
  ## Hard-stops the completeness-critic enforces
53
56
 
54
57
  - Core tokens present (`--accent`, `--bg-0..4`, `--fg-0..3`, `--dur-flip`)
55
- - Exactly one accent family
58
+ - Accent family count matches the declared strategy ({{accent_strategy_summary}})
59
+ - Color space matches the declared choice ({{color_space_summary}})
56
60
  - `system/{{ds_dirname}}/preview/` populated with at least 8 specimens
57
61
  - `colors_and_type.css` linked from every specimen
58
62
  - `prefers-reduced-motion: reduce` guard present in tokens CSS
@@ -24,8 +24,9 @@ It is **not** auto-invoked by `/design:edit` or `/design:new` on a project with
24
24
 
25
25
  ## What the agent should remember
26
26
 
27
- - **One accent.** No `--accent2`. Calls-to-attention compete; one accent forces hierarchy decisions.
28
- - **All visuals reference `var(--*)` tokens.** No hardcoded hex / px / rem in canvases.
27
+ - **Accent strategy:** {{accent_strategy_summary}}. {{accent_rules_summary}}
28
+ - **All visuals reference `var(--*)` tokens.** No off-token values in canvases.
29
+ - **Color space:** {{color_space_summary}}.
29
30
  - **Voice:** {{voice_tone_summary}}
30
31
  - **Iconography:** {{iconography_summary}}
31
32
  - **Theme default:** `{{theme_default}}`. {{theme_extra}}
@@ -47,4 +48,4 @@ It is **not** auto-invoked by `/design:edit` or `/design:new` on a project with
47
48
 
48
49
  ## How to extend
49
50
 
50
- If a canvas iteration needs a value not currently in the system, **extend `colors_and_type.css` first**. Adding a new variant of an existing token (e.g. `--accent-tertiary`) is fine; adding a parallel accent family (e.g. `--accent2`) violates the one-accent rule and the completeness-critic will block.
51
+ If a canvas iteration needs a value not currently in the system, **extend `colors_and_type.css` first**. Adding a new variant of an existing token (e.g. `--accent-tertiary`) is fine. Adding a parallel accent family (e.g. `--accent2`) is only allowed if the project's declared `accentStrategy` in `config.json` permits it (e.g. `paired` or `chromatic-N`) the completeness-critic enforces the declared strategy.
@@ -4,55 +4,59 @@
4
4
  * Authoritative token file. Every canvas in <designRoot>/ui/ links to this.
5
5
  * Production code should consume the same values (compiled to TS/JS or kept as CSS vars).
6
6
  *
7
- * Contract:
8
- * - OKLCH for accent + status colors (better gamut control than HSL/hex)
9
- * - One accent family only (no --accent2)
10
- * - Theme: {{theme_default}} (single-block) duplicate for both-equal projects
11
- * - Motion durations + easings + reduced-motion guard
12
- * - 8-step type ladder, 4px-base spacing scale
7
+ * This file is the single source of truth for the project's visual language.
8
+ * Every concrete value below is supplied by the discovery payload there are
9
+ * NO universal defaults baked into this template. Spacing, type, easing,
10
+ * shadows, max-width, accent strategy, and color space are all project-flavored.
11
+ *
12
+ * Two invariants this template does enforce:
13
+ * 1. `prefers-reduced-motion: reduce` collapses every duration to 1ms (a11y).
14
+ * 2. Tokens used by canvases live under one of the documented family prefixes
15
+ * (--bg-*, --fg-*, --accent*, --border-*, --status-*, --space-*, --type-*,
16
+ * --lh-*, --radius-*, --shadow-*, --dur-*, --ease-*, --layout-*, --font-*).
17
+ *
18
+ * Everything else — palette structure, type ladder, motion personality — is
19
+ * a project decision recorded during /design:setup-ds.
13
20
  */
14
21
 
15
22
  :root,
16
23
  .{{root_class}}[data-theme="{{theme_default}}"] {
17
24
  /* ─── Surfaces (deepest → highest) ─────────────────────────────────── */
18
- --bg-0: {{bg_0_oklch}}; /* page bg */
19
- --bg-1: {{bg_1_oklch}}; /* card / panel bg */
20
- --bg-2: {{bg_2_oklch}}; /* nested panel / popover */
21
- --bg-3: {{bg_3_oklch}}; /* input bg / subtle row hover */
22
- --bg-4: {{bg_4_oklch}}; /* hover / pressed state */
25
+ --bg-0: {{bg_0}}; /* page bg */
26
+ --bg-1: {{bg_1}}; /* card / panel bg */
27
+ --bg-2: {{bg_2}}; /* nested panel / popover */
28
+ --bg-3: {{bg_3}}; /* input bg / subtle row hover */
29
+ --bg-4: {{bg_4}}; /* hover / pressed state */
23
30
 
24
31
  /* ─── Borders ──────────────────────────────────────────────────────── */
25
- --border-subtle: oklch(from var(--bg-1) calc(l + 0.04) c h);
26
- --border-default: oklch(from var(--bg-1) calc(l + 0.08) c h);
27
- --border-strong: oklch(from var(--bg-1) calc(l + 0.14) c h);
32
+ --border-subtle: {{border_subtle}};
33
+ --border-default: {{border_default}};
34
+ --border-strong: {{border_strong}};
28
35
 
29
36
  /* ─── Text ─────────────────────────────────────────────────────────── */
30
- --fg-0: {{fg_0_oklch}}; /* primary text */
31
- --fg-1: {{fg_1_oklch}}; /* secondary text */
32
- --fg-2: {{fg_2_oklch}}; /* tertiary / muted */
33
- --fg-3: {{fg_3_oklch}}; /* disabled */
37
+ --fg-0: {{fg_0}}; /* primary text */
38
+ --fg-1: {{fg_1}}; /* secondary text */
39
+ --fg-2: {{fg_2}}; /* tertiary / muted */
40
+ --fg-3: {{fg_3}}; /* disabled */
34
41
 
35
- /* ─── Accent (ONE family only) ─────────────────────────────────────── */
36
- --accent: {{accent_oklch}};
37
- --accent-hover: oklch(from var(--accent) calc(l - 0.04) c h);
38
- --accent-active: oklch(from var(--accent) calc(l - 0.08) c h);
39
- --accent-fg: {{accent_fg_oklch}};
42
+ /* ─── Accent ({{accent_strategy_summary}}) ─────────────────────────── */
43
+ {{accent_block}}
40
44
 
41
45
  /* ─── Status (only if "status" ∈ activeFamilies) ───────────────────── */
42
- --status-success: oklch(70% 0.18 145);
43
- --status-warn: oklch(78% 0.16 85);
44
- --status-error: oklch(64% 0.21 25);
45
- --status-info: oklch(70% 0.13 230);
46
+ --status-success: {{status_success}};
47
+ --status-warn: {{status_warn}};
48
+ --status-error: {{status_error}};
49
+ --status-info: {{status_info}};
46
50
 
47
51
  /* ─── Presence (only if "presence" ∈ activeFamilies) ───────────────── */
48
- --presence-online: var(--status-success);
49
- --presence-away: var(--status-warn);
50
- --presence-offline: var(--fg-3);
52
+ --presence-online: {{presence_online}};
53
+ --presence-away: {{presence_away}};
54
+ --presence-offline: {{presence_offline}};
51
55
 
52
56
  /* ─── Shadows / elevation ──────────────────────────────────────────── */
53
- --shadow-sm: 0 1px 2px oklch(0 0 0 / 0.20);
54
- --shadow-md: 0 4px 12px oklch(0 0 0 / 0.25);
55
- --shadow-lg: 0 12px 32px oklch(0 0 0 / 0.30);
57
+ --shadow-sm: {{shadow_sm}};
58
+ --shadow-md: {{shadow_md}};
59
+ --shadow-lg: {{shadow_lg}};
56
60
 
57
61
  /* ─── Radii ────────────────────────────────────────────────────────── */
58
62
  --radius-xs: {{radius_xs}};
@@ -60,45 +64,45 @@
60
64
  --radius-md: {{radius_md}};
61
65
  --radius-lg: {{radius_lg}};
62
66
  --radius-xl: {{radius_xl}};
63
- --radius-pill: 999px;
67
+ --radius-pill: {{radius_pill}};
64
68
 
65
- /* ─── Spacing (4px base) ───────────────────────────────────────────── */
66
- --space-0: 0;
67
- --space-1: 4px;
68
- --space-2: 8px;
69
- --space-3: 12px;
70
- --space-4: 16px;
71
- --space-5: 24px;
72
- --space-6: 32px;
73
- --space-7: 48px;
74
- --space-8: 64px;
69
+ /* ─── Spacing ──────────────────────────────────────────────────────── */
70
+ --space-0: {{space_0}};
71
+ --space-1: {{space_1}};
72
+ --space-2: {{space_2}};
73
+ --space-3: {{space_3}};
74
+ --space-4: {{space_4}};
75
+ --space-5: {{space_5}};
76
+ --space-6: {{space_6}};
77
+ --space-7: {{space_7}};
78
+ --space-8: {{space_8}};
75
79
 
76
80
  /* ─── Typography ───────────────────────────────────────────────────── */
77
81
  --font-display: {{font_display}};
78
82
  --font-body: {{font_body}};
79
83
  --font-mono: {{font_mono}};
80
84
 
81
- /* 8-step type scale */
82
- --type-xs: 12px; --lh-xs: 16px;
83
- --type-sm: 13px; --lh-sm: 18px;
84
- --type-base: 14px; --lh-base: 20px;
85
- --type-md: 16px; --lh-md: 22px;
86
- --type-lg: 18px; --lh-lg: 26px;
87
- --type-xl: 22px; --lh-xl: 30px;
88
- --type-2xl: 28px; --lh-2xl: 36px;
89
- --type-3xl: 36px; --lh-3xl: 44px;
85
+ /* Type scale */
86
+ --type-xs: {{type_xs}}; --lh-xs: {{lh_xs}};
87
+ --type-sm: {{type_sm}}; --lh-sm: {{lh_sm}};
88
+ --type-base: {{type_base}}; --lh-base: {{lh_base}};
89
+ --type-md: {{type_md}}; --lh-md: {{lh_md}};
90
+ --type-lg: {{type_lg}}; --lh-lg: {{lh_lg}};
91
+ --type-xl: {{type_xl}}; --lh-xl: {{lh_xl}};
92
+ --type-2xl: {{type_2xl}}; --lh-2xl: {{lh_2xl}};
93
+ --type-3xl: {{type_3xl}}; --lh-3xl: {{lh_3xl}};
90
94
 
91
95
  /* ─── Motion ───────────────────────────────────────────────────────── */
92
96
  --dur-flip: {{dur_flip}};
93
97
  --dur-panel: {{dur_panel}};
94
98
  --dur-route: {{dur_route}};
95
99
  --dur-soft: {{dur_soft}};
96
- --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
97
- --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
100
+ --ease-out: {{ease_out_curve}};
101
+ --ease-in-out: {{ease_in_out_curve}};
98
102
 
99
103
  /* ─── Layout ───────────────────────────────────────────────────────── */
100
- --layout-max-w: 1200px;
101
- --layout-gutter: var(--space-4);
104
+ --layout-max-w: {{layout_max_w}};
105
+ --layout-gutter: {{layout_gutter}};
102
106
  }
103
107
 
104
108
  @media (prefers-reduced-motion: reduce) {
@@ -17,6 +17,8 @@
17
17
  "extensions": [],
18
18
  "completenessProfile": "standard",
19
19
  "activeFamilies": {{active_families}},
20
+ "accentStrategy": "{{accent_strategy}}",
21
+ "colorSpace": "{{color_space}}",
20
22
  "designSystems": [
21
23
  {
22
24
  "name": "{{ds_dirname}}",
@@ -1,10 +1,10 @@
1
1
  <!--
2
2
  SPECIMEN: colors-accent
3
3
  DEMONSTRATES: --accent, --accent-hover, --accent-active, --accent-fg
4
- COMPOSITION: 4-state swatch grid + contextual button + link sample, demonstrating one-accent rule
4
+ COMPOSITION: 4-state swatch grid + contextual button + link sample, demonstrating the project's accent strategy
5
5
  COPY VOICE: action verbs ("Save", "Confirm", "Continue") — never "Click here" / "Get Started"
6
6
  WHEN SCAFFOLDED: always (Core)
7
- NOTES: This is THE place to demonstrate one-accent rule. The card should NOT show any --accent2 variant. accent-fg must clear WCAG AA against the accent background.
7
+ NOTES: Demonstrates the project's declared `accentStrategy` from config.json. For `single` (default) the card shows one accent family; for `paired` or `chromatic-N` the card should be extended to show every family. accent-fg must clear WCAG AA against the accent background.
8
8
  -->
9
9
  <!doctype html>
10
10
  <html lang="en">
@@ -17,7 +17,7 @@ NOTES: This is THE place to demonstrate one-accent rule. The card should NOT sho
17
17
  <body class="app" data-theme="dark">
18
18
  <main class="specimen">
19
19
  <h1>Accent</h1>
20
- <p class="lede">One accent family — four states. The one-accent rule means: when something needs attention, the answer is always this color, never a competing second hue. Sub-states cover hover, active, and the readable foreground that sits on top of it.</p>
20
+ <p class="lede">Accent family — four states (idle / hover / active / foreground). The project's `accentStrategy` declaration in <code>config.json</code> controls how many families exist; this specimen renders one. Extend it horizontally for `paired` or `chromatic-N` projects so every declared family appears side-by-side.</p>
21
21
 
22
22
  <div class="grid">
23
23
  <div class="swatch"><div class="chip" style="background: var(--accent);"></div><div class="meta"><strong>--accent</strong>Idle</div></div>
@@ -41,7 +41,7 @@ NOTES: This is THE place to demonstrate one-accent rule. The card should NOT sho
41
41
  </div>
42
42
 
43
43
  <footer class="legend">
44
- <p>The one-accent rule: no <code>--accent2</code>. Calls-to-attention should compete with each other through hierarchy and size, not through parallel hues.</p>
44
+ <p>Accent count follows <code>config.accentStrategy</code>. For `single` projects, calls-to-attention compete through hierarchy and size rather than parallel hues. For `paired` / `chromatic-N` projects, each declared family takes its own semantic role (e.g. <code>--accent</code> = primary action, <code>--accent-2</code> = exploratory). The completeness-critic gates the family count against the declared strategy.</p>
45
45
  </footer>
46
46
  </main>
47
47
  </body>
@@ -4,7 +4,7 @@ DEMONSTRATES: forward-pointer to v1.x multiplayer features — live cursors with
4
4
  COMPOSITION: full mockup of a shared canvas with 3 cursors + awareness rail + typing indicator
5
5
  COPY VOICE: real names, real "X is editing" strings
6
6
  WHEN SCAFFOLDED: meta + presence family (typically v1.1+ projects with Yjs)
7
- NOTES: This is a forward-pointer specimen showing what the multiplayer UX looks like once Phase 8 (Yjs LAN) lands. The cursor color is per-user (hashed from user ID into an OKLCH band). Don't blink cursors — too noisy.
7
+ NOTES: This is a forward-pointer specimen showing what the multiplayer UX looks like once Phase 8 (Yjs LAN) lands. Per-user cursor colors are illustrative only — the inline `oklch(...)` values below are NOT part of the project's design system. A real implementation should source per-user colors from a `--presence-user-*` family (token-driven) or derive from `--accent` via a deterministic hash on user ID. Don't blink cursors — too noisy.
8
8
  -->
9
9
  <!doctype html>
10
10
  <html lang="en">
@@ -132,6 +132,9 @@ NOTES:
132
132
  </div>
133
133
  <div class="spacer"></div>
134
134
  <input class="search" type="search" placeholder="search… (⌘ K)" />
135
+ <!-- Per-team accent picker. The inline `oklch(...)` values below are illustrative only — a real per-tenant
136
+ implementation should source team accents from a `--accent-team-*` family or compute analogous hues
137
+ from `--accent`. Don't hardcode tenant brand colors in the design system. -->
135
138
  <div class="accent-picker" role="group" aria-label="Accent">
136
139
  <button class="accent-swatch is-on" data-team="default" style="background: var(--accent);" aria-label="default"></button>
137
140
  <button class="accent-swatch" data-team="cyan" style="background: oklch(72% 0.16 220);" aria-label="cyan"></button>
@@ -22,7 +22,7 @@ NOTES:
22
22
  <link rel="stylesheet" href="../core/colors_and_type.css" />
23
23
  <link rel="stylesheet" href="../core/preview/_layout.css" />
24
24
  <style>
25
- body { padding: 0; margin: 0; background: oklch(8% 0.005 60); display: grid; place-items: start center; min-height: 100vh; }
25
+ body { padding: 0; margin: 0; background: var(--bg-0); display: grid; place-items: start center; min-height: 100vh; }
26
26
  .specimen { max-width: none; margin: 0; padding: var(--space-5) 0; }
27
27
 
28
28
  .artboard {
@@ -28,7 +28,7 @@ NOTES: Both themes use the SAME tokens (--bg-0..4 etc.); only their OKLCH values
28
28
  <p class="lede">The exact same UI rendered in both themes. Tokens carry the difference; the HTML structure is identical. If a component renders differently across themes (beyond color), that's a token-resolution bug, not a per-theme design.</p>
29
29
 
30
30
  <div class="grid" style="grid-template-columns: 1fr 1fr; gap: 16px;">
31
- <div class="pane app" data-theme="dark" style="background: oklch(16% 0.012 245); color: oklch(96% 0.008 245);">
31
+ <div class="pane app" data-theme="dark" style="background: var(--bg-0); color: var(--fg-0);">
32
32
  <h2 style="margin: 0 0 16px;">Dark</h2>
33
33
  <div class="card">
34
34
  <h3>Match recap</h3>
@@ -40,20 +40,20 @@ NOTES: Both themes use the SAME tokens (--bg-0..4 etc.); only their OKLCH values
40
40
  </div>
41
41
  </div>
42
42
 
43
- <div class="pane app" data-theme="light" style="background: oklch(98% 0.005 245); color: oklch(20% 0.012 245);">
43
+ <div class="pane app" data-theme="light" style="background: var(--bg-0); color: var(--fg-0);">
44
44
  <h2 style="margin: 0 0 16px;">Light</h2>
45
- <div class="card" style="background: oklch(95% 0.005 245); border-color: oklch(82% 0.010 245);">
45
+ <div class="card">
46
46
  <h3>Match recap</h3>
47
- <p class="meta" style="color: oklch(48% 0.012 245);">vs. Sparta U17 · 3-1</p>
47
+ <p class="meta">vs. Sparta U17 · 3-1</p>
48
48
  <div class="row" style="gap: 8px;">
49
49
  <button class="btn-primary">View</button>
50
- <button class="btn-secondary" style="background: oklch(91% 0.005 245); color: oklch(20% 0.012 245); border-color: oklch(78% 0.010 245);">Share</button>
50
+ <button class="btn-secondary">Share</button>
51
51
  </div>
52
52
  </div>
53
53
  </div>
54
54
  </div>
55
55
 
56
- <footer class="legend"><p>Inline OKLCH values here are just for the specimen; real projects swap them via <code>[data-theme="light"]</code> selectors in <code>colors_and_type.css</code>. The cascade carries every component without per-component theme overrides.</p></footer>
56
+ <footer class="legend"><p>Both panes use the same <code>var(--*)</code> tokens; only the <code>data-theme</code> attribute changes. Real projects define both themes inside <code>colors_and_type.css</code> via <code>[data-theme="dark"]</code> and <code>[data-theme="light"]</code> selectors. The cascade carries every component without per-component theme overrides.</p></footer>
57
57
  </main>
58
58
  </body>
59
59
  </html>
@@ -15,7 +15,8 @@ NOTES: Modal blocks the page; sheet slides from edge; alert is for irreversible
15
15
  <link rel="stylesheet" href="../core/preview/_layout.css" />
16
16
  <style>
17
17
  .dialog-frame { background: var(--bg-0); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); padding: var(--space-4); margin-bottom: 24px; position: relative; overflow: hidden; min-height: 220px; }
18
- .dialog-frame .veil { position: absolute; inset: 0; background: rgba(0,0,0,0.4); }
18
+ /* Veil opacity is a base value, not a token (per foundations/opacity.html). Color-space-agnostic. */
19
+ .dialog-frame .veil { position: absolute; inset: 0; background: color-mix(in srgb, black 40%, transparent); }
19
20
  .dialog { background: var(--bg-1); border: 1px solid var(--border-default); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); padding: var(--space-5); width: 360px; position: relative; }
20
21
  .dialog .title { font-family: var(--font-display); font-size: var(--type-lg); font-weight: 600; line-height: var(--lh-lg); margin: 0 0 8px; }
21
22
  .dialog .body { color: var(--fg-1); margin: 0 0 var(--space-4); }
@@ -48,7 +49,7 @@ NOTES: Modal blocks the page; sheet slides from edge; alert is for irreversible
48
49
 
49
50
  <h2>Sheet</h2>
50
51
  <div class="dialog-frame">
51
- <div class="veil" style="background: rgba(0,0,0,0.25);"></div>
52
+ <div class="veil" style="background: color-mix(in srgb, black 25%, transparent);"></div>
52
53
  <aside class="dialog dialog-sheet" role="dialog" aria-labelledby="sheet-t">
53
54
  <p class="title" id="sheet-t">Player details</p>
54
55
  <p class="body">#9 · Forward · Captain</p>
@@ -19,7 +19,8 @@ NOTES:
19
19
  <style>
20
20
  body { padding: 24px; display: flex; gap: 20px; align-items: center; flex-wrap: wrap; }
21
21
  .frame { padding: 18px 22px; border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); background: var(--bg-1); }
22
- .light { background: #ffffff; }
22
+ /* `.light` frame shows the mark on the opposite-theme surface — sourced from the project's tokens, not hardcoded white. */
23
+ .light { background: var(--bg-0-light, var(--bg-0)); }
23
24
  .label { font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--fg-2); text-transform: uppercase; letter-spacing: 0.04em; margin-top: 10px; display: block; }
24
25
  </style>
25
26
  </head>