@1agh/maude 0.18.2 → 0.19.1

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 (31) hide show
  1. package/cli/bin/maude.mjs +16 -0
  2. package/cli/lib/update-check.mjs +145 -0
  3. package/cli/lib/update-check.test.mjs +32 -0
  4. package/package.json +8 -8
  5. package/plugins/design/dev-server/annotations-context-toolbar.tsx +14 -6
  6. package/plugins/design/dev-server/annotations-layer.tsx +144 -22
  7. package/plugins/design/dev-server/api.ts +72 -19
  8. package/plugins/design/dev-server/artboard-marquee.tsx +170 -0
  9. package/plugins/design/dev-server/canvas-lib-resolver.ts +10 -1
  10. package/plugins/design/dev-server/canvas-lib.tsx +190 -94
  11. package/plugins/design/dev-server/canvas-shell.tsx +478 -34
  12. package/plugins/design/dev-server/client/app.jsx +177 -56
  13. package/plugins/design/dev-server/client/styles/1-tokens.css +15 -8
  14. package/plugins/design/dev-server/client/styles/4-components.css +32 -2
  15. package/plugins/design/dev-server/config.schema.json +31 -5
  16. package/plugins/design/dev-server/context-menu.tsx +3 -3
  17. package/plugins/design/dev-server/context.ts +37 -3
  18. package/plugins/design/dev-server/dist/client.bundle.js +219 -107
  19. package/plugins/design/dev-server/dist/styles.css +38 -2
  20. package/plugins/design/dev-server/export-dialog.tsx +3 -3
  21. package/plugins/design/dev-server/http.ts +14 -2
  22. package/plugins/design/dev-server/input-router.tsx +22 -8
  23. package/plugins/design/dev-server/server.mjs +86 -20
  24. package/plugins/design/dev-server/server.ts +76 -30
  25. package/plugins/design/dev-server/test/context-resolve-tokens.test.ts +97 -0
  26. package/plugins/design/dev-server/test/snap-distance-pill.test.ts +68 -0
  27. package/plugins/design/dev-server/test/system-endpoint.test.ts +144 -0
  28. package/plugins/design/dev-server/tool-palette.tsx +71 -15
  29. package/plugins/design/dev-server/use-annotation-resize.tsx +295 -0
  30. package/plugins/design/dev-server/use-snap-guides.tsx +25 -1
  31. package/plugins/design/dev-server/use-tool-mode.tsx +31 -2
@@ -1049,7 +1049,7 @@ function Wordmark({ project, port, version }) {
1049
1049
  );
1050
1050
  }
1051
1051
 
1052
- function Viewport({ tabs, activePath, registerIframe, systemData, onOpenFromSystem, project, cfg }) {
1052
+ function Viewport({ tabs, activePath, registerIframe, systemData, onOpenFromSystem, onSelectDs, project, cfg }) {
1053
1053
  return (
1054
1054
  <div className="viewport">
1055
1055
  {tabs.length === 0 && (
@@ -1073,7 +1073,7 @@ function Viewport({ tabs, activePath, registerIframe, systemData, onOpenFromSyst
1073
1073
  if (t.path === SYSTEM_TAB) {
1074
1074
  return (
1075
1075
  <div key={t.path} className={'system-view' + (t.path === activePath ? ' active' : '')}>
1076
- <SystemView data={systemData} onOpen={onOpenFromSystem} />
1076
+ <SystemView data={systemData} onOpen={onOpenFromSystem} cfg={cfg} onSelectDs={onSelectDs} />
1077
1077
  </div>
1078
1078
  );
1079
1079
  }
@@ -1092,83 +1092,184 @@ function Viewport({ tabs, activePath, registerIframe, systemData, onOpenFromSyst
1092
1092
  }
1093
1093
 
1094
1094
  // ---------- SystemView ----------
1095
-
1096
- const TOKEN_NAMES = [
1097
- '--bg-0', '--bg-1', '--bg-2', '--bg-3', '--bg-4',
1098
- '--fg-0', '--fg-1', '--fg-2', '--fg-3',
1099
- '--accent', '--accent-hover', '--accent-active', '--accent-fg', '--accent-tint',
1100
- '--status-success', '--status-warn', '--status-error', '--status-info',
1101
- '--border-subtle', '--border-default', '--border-strong',
1095
+ //
1096
+ // DDR-048 the System view renders the USER's design-system tokens. It does
1097
+ // NOT read from `document.documentElement` (that would surface the dev-server
1098
+ // shell's amber-rust chrome theme from styles/1-tokens.css, which is NOT a
1099
+ // user template) and it does NOT assume any canonical token-name contract.
1100
+ // Whatever the user's `colors_and_type.css` declared — names, values, theme
1101
+ // blocks — is what shows up here.
1102
+
1103
+ // Order kinds match the typical reading flow of a tokens file. Unknown kinds
1104
+ // fall through to `other` so a custom token group still renders, just last.
1105
+ const TOKEN_GROUP_ORDER = [
1106
+ 'color', 'space', 'radius', 'shadow', 'leading', 'weight', 'motion', 'font', 'other',
1102
1107
  ];
1103
- const TYPE_STEPS = ['xs', 'sm', 'base', 'md', 'lg', 'xl', '2xl', '3xl'];
1108
+ const TOKEN_GROUP_LABELS = {
1109
+ color: 'colors',
1110
+ space: 'spacing',
1111
+ radius: 'radii',
1112
+ shadow: 'shadows',
1113
+ leading: 'leading',
1114
+ weight: 'weights',
1115
+ motion: 'motion',
1116
+ font: 'font stacks',
1117
+ other: 'other',
1118
+ };
1104
1119
 
1105
- function readTokens(names) {
1106
- if (typeof window === 'undefined') return names.map(name => ({ name, value: '' }));
1107
- const cs = getComputedStyle(document.documentElement);
1108
- return names.map(name => ({ name, value: cs.getPropertyValue(name).trim() }));
1109
- }
1120
+ function isSwatchKind(kind) { return kind === 'color'; }
1121
+
1122
+ function TokenLadder({ tokens, tokenGroups, tokensPath }) {
1123
+ if (!tokens || tokens.length === 0) {
1124
+ return (
1125
+ <section className="sv-section sv-section-tokens">
1126
+ <h2>tokens<span className="sv-h-num">0</span></h2>
1127
+ <div className="sv-empty">
1128
+ <p>
1129
+ No tokens parsed from {tokensPath ? <code>{tokensPath}</code> : 'the configured tokens file'}.
1130
+ Does the file exist and contain CSS custom properties (<code>--name: value;</code>)?
1131
+ </p>
1132
+ </div>
1133
+ </section>
1134
+ );
1135
+ }
1136
+
1137
+ const groups = tokenGroups || {};
1138
+ const kinds = Array.from(
1139
+ new Set([...TOKEN_GROUP_ORDER, ...Object.keys(groups)])
1140
+ ).filter((k) => groups[k]?.length);
1110
1141
 
1111
- function TokenLadder() {
1112
- const [tokens, setTokens] = useState(() => readTokens(TOKEN_NAMES));
1113
- useEffect(() => {
1114
- setTokens(readTokens(TOKEN_NAMES));
1115
- const obs = new MutationObserver(() => setTokens(readTokens(TOKEN_NAMES)));
1116
- obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
1117
- return () => obs.disconnect();
1118
- }, []);
1119
1142
  return (
1120
- <section className="sv-section sv-section-tokens">
1121
- <h2>tokens · surfaces & ink<span className="sv-h-num">{tokens.length}</span></h2>
1122
- <div className="sv-tokens-ladder">
1123
- {tokens.map(t => (
1124
- <div className="sv-tok-cell" key={t.name}>
1125
- <div className="sv-tok-swatch" style={{ background: `var(${t.name})` }} />
1126
- <div className="sv-tok-meta">
1127
- <code className="sv-tok-name">{t.name}</code>
1128
- <span className="sv-tok-value">{t.value || '—'}</span>
1143
+ <>
1144
+ {kinds.map((kind) => {
1145
+ const list = groups[kind];
1146
+ const swatch = isSwatchKind(kind);
1147
+ return (
1148
+ <section className={'sv-section sv-section-tokens sv-section-' + kind} key={kind}>
1149
+ <h2>
1150
+ tokens · {TOKEN_GROUP_LABELS[kind] || kind}
1151
+ <span className="sv-h-num">{list.length}</span>
1152
+ </h2>
1153
+ <div className="sv-tokens-ladder">
1154
+ {list.map((t) => (
1155
+ <div className="sv-tok-cell" key={t.name + '|' + t.value}>
1156
+ {swatch ? (
1157
+ <div className="sv-tok-swatch" style={{ background: t.value }} />
1158
+ ) : null}
1159
+ <div className="sv-tok-meta">
1160
+ <code className="sv-tok-name">{t.name}</code>
1161
+ <span className="sv-tok-value">{t.value || '—'}</span>
1162
+ </div>
1163
+ </div>
1164
+ ))}
1129
1165
  </div>
1130
- </div>
1131
- ))}
1132
- </div>
1133
- </section>
1166
+ </section>
1167
+ );
1168
+ })}
1169
+ </>
1134
1170
  );
1135
1171
  }
1136
1172
 
1137
- function TypeLadder() {
1173
+ // Find a leading partner by name suffix: --fs-base → --lh-base, --type-xl → --lh-xl.
1174
+ // Returns null when no convention match exists; caller omits the lineHeight style.
1175
+ function findLeadingFor(typeToken, leadingTokens) {
1176
+ const m = typeToken.name.match(/^--(?:type|fs|text)-(.+)$/);
1177
+ if (!m) return null;
1178
+ const suffix = m[1];
1179
+ return (leadingTokens || []).find((t) =>
1180
+ /^--(?:lh|leading|line-height)-/.test(t.name) && t.name.endsWith('-' + suffix)
1181
+ )?.value ?? null;
1182
+ }
1183
+
1184
+ // Best-effort sample font: prefer body / sans / display tokens, fall back to
1185
+ // the first font-kind token, fall back to system-ui. Avoids the shell's
1186
+ // Berkeley Mono leaking into user-facing previews.
1187
+ function sampleFontFamily(fontTokens) {
1188
+ if (!fontTokens?.length) return undefined;
1189
+ const prefer = ['body', 'sans', 'display', 'text', 'family'];
1190
+ for (const tag of prefer) {
1191
+ const hit = fontTokens.find((t) => t.name.includes(tag));
1192
+ if (hit) return hit.value;
1193
+ }
1194
+ return fontTokens[0].value;
1195
+ }
1196
+
1197
+ function TypeLadder({ tokenGroups }) {
1198
+ const typeTokens = tokenGroups?.fontsize || [];
1199
+ if (typeTokens.length === 0) return null;
1200
+ const leadingTokens = tokenGroups?.leading || [];
1201
+ const sampleFont = sampleFontFamily(tokenGroups?.font);
1202
+
1138
1203
  return (
1139
1204
  <section className="sv-section sv-section-type">
1140
- <h2>type · 8-step ladder<span className="sv-h-num">{TYPE_STEPS.length}</span></h2>
1205
+ <h2>
1206
+ type · ladder<span className="sv-h-num">{typeTokens.length}</span>
1207
+ </h2>
1141
1208
  <div className="sv-type-list">
1142
- {TYPE_STEPS.map(s => (
1143
- <div className="sv-type-row" key={s}>
1144
- <code className="sv-type-tok">--type-{s}</code>
1145
- <span className="sv-type-sample" style={{ fontSize: `var(--type-${s})`, lineHeight: `var(--lh-${s})` }}>
1146
- The catalog is the system.
1147
- </span>
1148
- </div>
1149
- ))}
1209
+ {typeTokens.map((t) => {
1210
+ const lh = findLeadingFor(t, leadingTokens);
1211
+ const style = { fontSize: t.value };
1212
+ if (lh) style.lineHeight = lh;
1213
+ if (sampleFont) style.fontFamily = sampleFont;
1214
+ return (
1215
+ <div className="sv-type-row" key={t.name}>
1216
+ <code className="sv-type-tok">{t.name}</code>
1217
+ <span className="sv-type-sample" style={style}>
1218
+ The catalog is the system.
1219
+ </span>
1220
+ </div>
1221
+ );
1222
+ })}
1150
1223
  </div>
1151
1224
  </section>
1152
1225
  );
1153
1226
  }
1154
1227
 
1155
- function SystemView({ data, onOpen }) {
1228
+ function SystemView({ data, onOpen, cfg, onSelectDs }) {
1156
1229
  if (!data) {
1157
1230
  return <div className="sv-empty"><p>Loading design system…</p></div>;
1158
1231
  }
1159
- const { previewGallery, uiKitsGallery, systemDir } = data;
1232
+ const {
1233
+ previewGallery,
1234
+ uiKitsGallery,
1235
+ systemDir,
1236
+ tokens,
1237
+ tokenGroups,
1238
+ tokensPath,
1239
+ ds,
1240
+ availableDesignSystems,
1241
+ } = data;
1160
1242
  const empty = (!previewGallery || !previewGallery.length) && (!uiKitsGallery || !uiKitsGallery.length);
1243
+ const hasPicker = Array.isArray(availableDesignSystems) && availableDesignSystems.length > 1;
1244
+ const selectedName = ds?.name ?? availableDesignSystems?.[0]?.name ?? '';
1161
1245
 
1162
1246
  return (
1163
1247
  <div className="sv">
1164
1248
  <header className="sv-header">
1165
1249
  <span className="sv-sku">MDCC-DSN/01</span>
1166
1250
  <span className="sv-title">design system view</span>
1251
+ {hasPicker ? (
1252
+ <label className="sv-ds-picker">
1253
+ <span className="sv-ds-picker-label">DS</span>
1254
+ <select
1255
+ value={selectedName}
1256
+ onChange={(e) => onSelectDs && onSelectDs(e.target.value)}
1257
+ >
1258
+ {availableDesignSystems.map((d) => (
1259
+ <option key={d.name} value={d.name}>{d.name}</option>
1260
+ ))}
1261
+ </select>
1262
+ </label>
1263
+ ) : null}
1167
1264
  <span className="sv-loc"><code>{systemDir}</code></span>
1168
1265
  </header>
1169
1266
 
1170
- <TokenLadder />
1171
- <TypeLadder />
1267
+ {ds?.description ? (
1268
+ <p className="sv-ds-description">{ds.description}</p>
1269
+ ) : null}
1270
+
1271
+ <TokenLadder tokens={tokens} tokenGroups={tokenGroups} tokensPath={tokensPath} />
1272
+ <TypeLadder tokenGroups={tokenGroups} />
1172
1273
 
1173
1274
  {empty ? (
1174
1275
  <div className="sv-empty">
@@ -1176,15 +1277,15 @@ function SystemView({ data, onOpen }) {
1176
1277
  </div>
1177
1278
  ) : (
1178
1279
  <>
1179
- <Gallery title="preview" items={previewGallery} onOpen={onOpen} kind="preview" />
1180
- <Gallery title="ui kits" items={uiKitsGallery} onOpen={onOpen} kind="ui_kits" />
1280
+ <Gallery title="preview" items={previewGallery} onOpen={onOpen} kind="preview" cfg={cfg} />
1281
+ <Gallery title="ui kits" items={uiKitsGallery} onOpen={onOpen} kind="ui_kits" cfg={cfg} />
1181
1282
  </>
1182
1283
  )}
1183
1284
  </div>
1184
1285
  );
1185
1286
  }
1186
1287
 
1187
- function Gallery({ title, items, onOpen, kind }) {
1288
+ function Gallery({ title, items, onOpen, kind, cfg }) {
1188
1289
  if (!items || items.length === 0) return null;
1189
1290
  return (
1190
1291
  <section className="sv-section">
@@ -1193,7 +1294,7 @@ function Gallery({ title, items, onOpen, kind }) {
1193
1294
  {items.map(p => (
1194
1295
  <article key={p.path} className="sv-preview-card" onClick={() => onOpen(p.path)}>
1195
1296
  <div className="sv-preview-frame">
1196
- <iframe src={urlOf(p.path)} title={p.label} scrolling="no" />
1297
+ <iframe src={canvasUrl(p.path, cfg)} title={p.label} scrolling="no" />
1197
1298
  </div>
1198
1299
  <div className="sv-preview-foot">
1199
1300
  <strong>{p.label}</strong>
@@ -1494,10 +1595,29 @@ function App() {
1494
1595
  useEffect(() => { loadTree(); }, [loadTree]);
1495
1596
 
1496
1597
  // ----- System data (lazy) -----
1497
- const loadSystemData = useCallback(async () => {
1598
+ // `dsName` scopes to a single design-system entry (DDR-048). The initial
1599
+ // call is unscoped — server returns `availableDesignSystems[]` + a default
1600
+ // — so the picker can render without a probe round-trip. Subsequent calls
1601
+ // (e.g. picker change) pass the chosen DS name and we replace systemData
1602
+ // wholesale (tokens + previews + ds metadata all shift together).
1603
+ const loadSystemData = useCallback(async (dsName) => {
1498
1604
  try {
1499
- const r = await fetch('/_system-data');
1605
+ const url = dsName ? `/_system-data?ds=${encodeURIComponent(dsName)}` : '/_system-data';
1606
+ const r = await fetch(url);
1607
+ if (!r.ok) {
1608
+ console.error('failed to load system-data', r.status);
1609
+ return;
1610
+ }
1500
1611
  const data = await r.json();
1612
+ // If the initial unscoped fetch has a defaultDesignSystem but no `ds`
1613
+ // attached (multi-DS project), kick off a scoped fetch so the visible
1614
+ // tokens + previews match the default DS, not the union root scan.
1615
+ if (!dsName && data?.defaultDesignSystem && !data.ds) {
1616
+ setSystemData(data);
1617
+ const r2 = await fetch(`/_system-data?ds=${encodeURIComponent(data.defaultDesignSystem)}`);
1618
+ if (r2.ok) setSystemData(await r2.json());
1619
+ return;
1620
+ }
1501
1621
  setSystemData(data);
1502
1622
  } catch (e) {
1503
1623
  console.error('failed to load system-data', e);
@@ -1918,6 +2038,7 @@ function App() {
1918
2038
  registerIframe={registerIframe}
1919
2039
  systemData={systemData}
1920
2040
  onOpenFromSystem={openTab}
2041
+ onSelectDs={loadSystemData}
1921
2042
  project={project}
1922
2043
  cfg={cfg}
1923
2044
  />
@@ -1,13 +1,20 @@
1
- /* @layer tokens — project DS bridge for the dev-server shell.
1
+ /* @layer tokens — SHELL theme for the dev-server UI itself.
2
2
  *
3
- * Source of truth: .design/system/project/colors_and_type.css (MDCC-DSN/01).
4
- * The OKLCH ladders below are pasted in so the dev-server bundle stays
5
- * self-contained (a relative `@import` would have to cross from
6
- * plugins/design/dev-server/ into the repo's .design/ folder fragile
7
- * once installed centrally). If the DS evolves, re-sync this file
8
- * manually and keep the values verbatim.
3
+ * This is NOT a template for user design systems and NOT a fallback for
4
+ * the System view's token ladder. It themes the dev-server chrome — sidebar,
5
+ * status bar, header, menubar, comments panel, System tab outer shell. The
6
+ * amber-rust "catalog stamp" aesthetic below belongs to Maude as a tool, not
7
+ * to any project that runs against it.
9
8
  *
10
- * Two themes both first-class, both equal:
9
+ * Per DDR-048, the System view (MDCC-DSN/01) reads user tokens from the
10
+ * project's own `colors_and_type.css` (parsed by the server, returned via
11
+ * /_system-data) and renders them by raw value — never by `var(--name)`
12
+ * resolving against this document. That isolation is load-bearing: prior
13
+ * to DDR-048 the System view leaked these shell tokens into the user's
14
+ * "design system overview" because both happened to share the canonical
15
+ * `--bg-0..4 / --fg-0..3 / --accent*` naming.
16
+ *
17
+ * Two shell themes — both first-class, both equal:
11
18
  * :root + [data-theme="light"] — paper-light (cream + ink)
12
19
  * [data-theme="dark"] — phosphor-dark (default for dev-server)
13
20
  *
@@ -36,14 +36,44 @@
36
36
 
37
37
  /* CV-09 header — SKU framing, hairline divider underneath. */
38
38
  .sv-header {
39
- display: grid;
40
- grid-template-columns: auto 1fr auto;
39
+ display: flex;
40
+ flex-wrap: wrap;
41
41
  align-items: baseline;
42
42
  gap: var(--space-5);
43
43
  border-bottom: 2px solid var(--u-fg-0);
44
44
  padding-bottom: var(--u-s-3);
45
45
  margin-bottom: var(--u-s-5);
46
46
  }
47
+ .sv-header .sv-title { flex: 1 1 auto; }
48
+ .sv-ds-picker {
49
+ display: inline-flex;
50
+ align-items: baseline;
51
+ gap: var(--u-s-2);
52
+ font-family: var(--u-font-mono);
53
+ font-size: 10px;
54
+ letter-spacing: var(--tracking-sku);
55
+ text-transform: uppercase;
56
+ color: var(--u-fg-2);
57
+ }
58
+ .sv-ds-picker select {
59
+ font-family: var(--u-font-mono);
60
+ font-size: 11px;
61
+ color: var(--u-fg-0);
62
+ background: var(--u-bg-1);
63
+ border: 1px solid var(--u-border);
64
+ border-radius: var(--u-r-sm);
65
+ padding: 2px 6px;
66
+ text-transform: none;
67
+ letter-spacing: 0;
68
+ }
69
+ .sv-ds-description {
70
+ font-family: var(--u-font-body);
71
+ font-size: var(--type-sm);
72
+ line-height: 1.5;
73
+ color: var(--u-fg-1);
74
+ margin: 0 0 var(--u-s-5);
75
+ max-width: 72ch;
76
+ }
47
77
  .sv-sku {
48
78
  font-family: var(--u-font-mono);
49
79
  font-size: var(--type-lg);
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "tokensCssRel": {
54
54
  "type": "string",
55
- "description": "Path to the design system CSS, relative to designRoot. New canvases <link> to this file.",
55
+ "description": "Project-wide tokens CSS path, relative to designRoot. Used as the fallback when 'designSystems' is unset OR when an entry omits its own 'tokensCssRel'. New canvases <link> to this file by default. Per DDR-048, per-DS entries auto-resolve to '<designSystems[i].path>/colors_and_type.css' when their own field is missing.",
56
56
  "default": "system/colors_and_type.css"
57
57
  },
58
58
  "teamAccentDefault": {
@@ -130,7 +130,7 @@
130
130
  },
131
131
  "designSystems": {
132
132
  "type": "array",
133
- "description": "Design systems available in this project. Single-DS projects have one entry; multi-DS projects (marketing vs. admin vs. mobile) list each here. Each canvas's .meta.json declares which DS it uses via the 'designSystem' field.",
133
+ "description": "Design systems available in this project. Single-DS projects have one entry; multi-DS projects (marketing vs. admin vs. mobile) list each here. Each canvas's .meta.json declares which DS it uses via the 'designSystem' field. The System view's DS picker iterates this array.",
134
134
  "items": {
135
135
  "type": "object",
136
136
  "required": ["name", "path"],
@@ -138,13 +138,39 @@
138
138
  "name": {
139
139
  "type": "string",
140
140
  "pattern": "^[a-z][a-z0-9-]*$",
141
- "description": "Kebab-case slug. Used to match canvas .meta.json.designSystem references."
141
+ "description": "Kebab-case slug. Used to match canvas .meta.json.designSystem references and as the ?ds=<name> query param on /_system-data."
142
142
  },
143
143
  "path": {
144
144
  "type": "string",
145
- "description": "Folder under designRoot (e.g. 'system/marketing')."
145
+ "description": "Folder under designRoot (e.g. 'system/marketing'). The DS-scoped System view reads tokens, previews, and ui_kits from inside this folder."
146
146
  },
147
- "description": { "type": "string" }
147
+ "description": { "type": "string" },
148
+ "tokensCssRel": {
149
+ "type": "string",
150
+ "description": "Per-DS tokens CSS path, relative to designRoot. When omitted, auto-resolves to '<path>/colors_and_type.css' (DDR-048). Set explicitly when the DS's tokens live under a non-standard filename."
151
+ },
152
+ "rootClass": {
153
+ "type": "string",
154
+ "description": "Optional root class applied to canvases in this DS (overrides the project-wide rootClass for this DS's previews + new canvases)."
155
+ },
156
+ "themeDefault": {
157
+ "type": "string",
158
+ "enum": ["dark", "light"],
159
+ "description": "Per-DS theme default. Overrides the project-wide themeDefault when rendering this DS's specimens."
160
+ },
161
+ "themes": {
162
+ "type": "array",
163
+ "items": { "type": "string" },
164
+ "description": "Optional list of theme names this DS supports (used by the System view's theme toggle when more than one theme is declared)."
165
+ },
166
+ "newCanvasDir": {
167
+ "type": "string",
168
+ "description": "Override of the project-wide newCanvasDir for canvases scaffolded inside this DS."
169
+ },
170
+ "newComponentDir": {
171
+ "type": "string",
172
+ "description": "Override of the project-wide newComponentDir for components scaffolded inside this DS."
173
+ }
148
174
  },
149
175
  "additionalProperties": false
150
176
  },
@@ -158,10 +158,10 @@ const MENU_CSS = `
158
158
  .dc-context-menu {
159
159
  position: fixed;
160
160
  z-index: 7;
161
- background: var(--u-bg-2, var(--bg-1, #fff));
161
+ background: var(--u-bg-0, var(--bg-0, #fff));
162
162
  border: 1px solid var(--u-fg-0, #1c1917);
163
- border-radius: 0;
164
- box-shadow: 4px 4px 0 var(--u-fg-0, #1c1917);
163
+ border-radius: 8px;
164
+ box-shadow: 0 6px 24px color-mix(in oklab, var(--u-fg-0, #1c1917) 10%, transparent);
165
165
  padding: 4px;
166
166
  min-width: 220px;
167
167
  font-family: var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
@@ -31,6 +31,7 @@ export interface DevServerConfig {
31
31
  designRoot: string;
32
32
  canvasGroups: CanvasGroup[];
33
33
  designSystems?: DesignSystemEntry[];
34
+ defaultDesignSystem?: string;
34
35
  rootClass: string;
35
36
  themeDefault: 'dark' | 'light';
36
37
  tokensCssRel: string;
@@ -124,18 +125,51 @@ function loadConfig(repoRoot: string): DevServerConfig {
124
125
  try {
125
126
  raw = readFileSync(configPath, 'utf8');
126
127
  } catch {
127
- return { ...DEFAULT_CONFIG, _source: 'defaults' };
128
+ return normalizeConfig({ ...DEFAULT_CONFIG, _source: 'defaults' });
128
129
  }
129
130
  try {
130
131
  const parsed = JSON.parse(raw);
131
- return { ...DEFAULT_CONFIG, ...parsed, _source: '.design/config.json' };
132
+ return normalizeConfig({ ...DEFAULT_CONFIG, ...parsed, _source: '.design/config.json' });
132
133
  } catch (e) {
133
134
  const msg = e instanceof Error ? e.message : String(e);
134
135
  console.error(` warn: ${configPath} is not valid JSON: ${msg}. Using defaults.`);
135
- return { ...DEFAULT_CONFIG, _source: 'defaults (config invalid)' };
136
+ return normalizeConfig({ ...DEFAULT_CONFIG, _source: 'defaults (config invalid)' });
136
137
  }
137
138
  }
138
139
 
140
+ /**
141
+ * Fill in per-DS `tokensCssRel` defaults so the system view can read each DS's
142
+ * tokens without forcing every config author to spell out the path. When an
143
+ * entry omits `tokensCssRel`, derive it from `<entry.path>/colors_and_type.css`
144
+ * — the scaffold layout `/design:setup-ds` produces. Also strips leading /
145
+ * trailing slashes from `entry.path` so downstream `path.posix.join` calls
146
+ * don't produce double-slash artifacts.
147
+ *
148
+ * The top-level `cfg.tokensCssRel` is preserved untouched as the
149
+ * project-wide fallback for legacy single-DS configs that don't declare
150
+ * `designSystems[]` at all.
151
+ *
152
+ * DDR-048: the system view renders user tokens only; this normalization is the
153
+ * load-bearing step that makes per-DS rendering possible.
154
+ */
155
+ export function normalizeDesignSystems<T extends DevServerConfig>(cfg: T): T {
156
+ if (!cfg.designSystems?.length) return cfg;
157
+ const designSystems = cfg.designSystems.map((entry) => {
158
+ const p = entry.path.replace(/^\/+|\/+$/g, '');
159
+ return {
160
+ ...entry,
161
+ path: p,
162
+ tokensCssRel:
163
+ entry.tokensCssRel?.replace(/^\/+/, '') ?? path.posix.join(p, 'colors_and_type.css'),
164
+ };
165
+ });
166
+ return { ...cfg, designSystems };
167
+ }
168
+
169
+ function normalizeConfig(cfg: DevServerConfig): DevServerConfig {
170
+ return normalizeDesignSystems(cfg);
171
+ }
172
+
139
173
  export function createContext(): Context {
140
174
  const repoRoot = resolveRepoRoot();
141
175