@agent-native/core 0.139.0 → 0.140.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 (46) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +44 -1
  3. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +265 -1
  4. package/corpus/templates/design/actions/create-design-system.ts +9 -10
  5. package/corpus/templates/design/actions/get-design-system.ts +53 -0
  6. package/corpus/templates/design/actions/import-design-tokens.ts +5 -71
  7. package/corpus/templates/design/actions/index-design-tokens.ts +61 -80
  8. package/corpus/templates/design/actions/update-design-system.ts +7 -10
  9. package/corpus/templates/design/app/components/design/EditPanel.tsx +45 -2
  10. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +12 -0
  11. package/corpus/templates/design/app/components/design/edit-panel/design-system-swatches.ts +215 -0
  12. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +10 -6
  13. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +19 -3
  14. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +36 -10
  15. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +15 -0
  16. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +19 -1
  17. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +6 -0
  18. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +53 -10
  19. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +142 -4
  20. package/corpus/templates/design/app/components/design/types.ts +6 -0
  21. package/corpus/templates/design/app/i18n-data.ts +20 -0
  22. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +1 -1
  23. package/corpus/templates/design/app/pages/DesignSystems.tsx +78 -1
  24. package/corpus/templates/design/app/pages/design-editor/html-layer-positioning.ts +6 -1
  25. package/corpus/templates/design/changelog/2026-08-04-design-system-tokens-show-by-name-in-the-editor.md +6 -0
  26. package/corpus/templates/design/e2e/helpers.ts +17 -0
  27. package/corpus/templates/design/server/lib/design-system-tokens.ts +35 -0
  28. package/corpus/templates/design/shared/authored-color-styles.ts +401 -0
  29. package/corpus/templates/design/shared/resolve-tweaks.ts +10 -18
  30. package/dist/brand-kit/index.d.ts +1 -0
  31. package/dist/brand-kit/index.js +1 -0
  32. package/dist/brand-kit/tokens.d.ts +92 -0
  33. package/dist/brand-kit/tokens.js +280 -0
  34. package/dist/brand-kit/types.d.ts +25 -0
  35. package/dist/collab/awareness.d.ts +2 -2
  36. package/dist/collab/routes.d.ts +1 -1
  37. package/dist/collab/struct-routes.d.ts +1 -1
  38. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  39. package/dist/notifications/routes.d.ts +3 -3
  40. package/dist/progress/routes.d.ts +1 -1
  41. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  42. package/dist/provider-api/actions/provider-api.d.ts +14 -14
  43. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  44. package/dist/resources/handlers.d.ts +1 -1
  45. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  46. package/package.json +2 -1
@@ -1,4 +1,10 @@
1
1
  import { defineAction } from "@agent-native/core";
2
+ import {
3
+ brandKitRoleTokens,
4
+ classifyBrandKitToken,
5
+ friendlyTokenName,
6
+ resolveBrandKitTokens,
7
+ } from "@agent-native/core/brand-kit/tokens";
2
8
  import { extractCssVars } from "@agent-native/core/server/design-token-utils";
3
9
  import { resolveAccess } from "@agent-native/core/sharing";
4
10
  import { eq } from "drizzle-orm";
@@ -12,52 +18,12 @@ import {
12
18
  resolveTweaksToCssVars,
13
19
  } from "../shared/resolve-tweaks.js";
14
20
 
15
- // ---------------------------------------------------------------------------
16
- // Token type classification
17
- // ---------------------------------------------------------------------------
18
-
19
- function classifyVar(
20
- name: string,
21
- value: string,
22
- ): "color" | "typography" | "spacing" | "radius" | "shadow" | "other" {
23
- const n = name.toLowerCase();
24
- if (
25
- /color|bg|background|text|border|accent|primary|secondary|surface|muted|foreground|fill|stroke/i.test(
26
- n,
27
- ) ||
28
- /^(#[0-9a-fA-F]{3,8}|rgba?\(|hsla?\(|oklch\(|color\()/.test(value.trim())
29
- ) {
30
- return "color";
31
- }
32
- if (/font|size|leading|tracking|weight|heading|body|type/i.test(n)) {
33
- return "typography";
34
- }
35
- if (/radius|rounded/i.test(n)) {
36
- return "radius";
37
- }
38
- if (/spacing|gap|padding|margin|space/i.test(n)) {
39
- return "spacing";
40
- }
41
- if (/shadow|blur|drop/i.test(n)) {
42
- return "shadow";
43
- }
44
- return "other";
45
- }
46
-
47
- /** Derive a friendly display name from a CSS var name like `--primary-color`. */
48
- function friendlyName(cssVar: string): string {
49
- return cssVar
50
- .replace(/^--/, "")
51
- .replace(/[-_]/g, " ")
52
- .replace(/\b\w/g, (c) => c.toUpperCase());
53
- }
54
-
55
21
  // ---------------------------------------------------------------------------
56
22
  // Token entry shape
57
23
  // ---------------------------------------------------------------------------
58
24
 
59
25
  export interface DesignToken {
60
- /** Human-readable display name, e.g. "Primary Color". */
26
+ /** The linked Brand Kit's own token name when it has one, else derived. */
61
27
  name: string;
62
28
  /** CSS custom property, e.g. "--primary-color". */
63
29
  cssVar: string;
@@ -67,6 +33,13 @@ export interface DesignToken {
67
33
  type: "color" | "typography" | "spacing" | "radius" | "shadow" | "other";
68
34
  /** Opaque source chip label, e.g. "globals.css" or "Brand Kit". */
69
35
  source: string;
36
+ /** Collection path from the Brand Kit, e.g. "Colors/Interactive". */
37
+ group?: string;
38
+ /**
39
+ * Which layer supplied the token. Filter on this, not on `source` — that is a
40
+ * free-text chip label (a filename, "Brand Kit", "Tweaks", …).
41
+ */
42
+ origin: "design" | "brand-kit";
70
43
  /**
71
44
  * True when the value comes from the design's own tweak selections (i.e.
72
45
  * the user has already customised this token in the editor).
@@ -74,6 +47,16 @@ export interface DesignToken {
74
47
  isTweakOverride?: boolean;
75
48
  }
76
49
 
50
+ /** Layer of a token's provenance, before it is flattened into a DesignToken. */
51
+ interface RawToken {
52
+ value: string;
53
+ source: string;
54
+ origin: DesignToken["origin"];
55
+ /** Present only when a Brand Kit supplied the token's own name. */
56
+ name?: string;
57
+ group?: string;
58
+ }
59
+
77
60
  // ---------------------------------------------------------------------------
78
61
  // Action
79
62
  // ---------------------------------------------------------------------------
@@ -81,9 +64,11 @@ export interface DesignToken {
81
64
  export default defineAction({
82
65
  description:
83
66
  "Index the design tokens for a design as a friendly { name, cssVar, " +
84
- "value, type, source } list. Parses CSS custom properties from the " +
67
+ "value, type, source, group } list. Parses CSS custom properties from the " +
85
68
  "design's HTML :root block, the linked Brand Kit / design system, and " +
86
- "the user's applied tweak selections. Returns tokens grouped by type " +
69
+ "the user's applied tweak selections. Tokens the linked Brand Kit stores " +
70
+ "by name keep that name and collection group; the rest get a name derived " +
71
+ "from their CSS variable. Returns tokens grouped by type " +
87
72
  "(color, typography, spacing, radius, shadow, other).",
88
73
  schema: z.object({
89
74
  designId: z.string().describe("Design project ID"),
@@ -109,19 +94,21 @@ export default defineAction({
109
94
  .from(schema.designFiles)
110
95
  .where(eq(schema.designFiles.designId, designId));
111
96
 
112
- /** cssVar -> { value, source } */
113
- const rawTokens: Map<string, { value: string; source: string }> = new Map();
97
+ /** cssVar -> latest layer that set it */
98
+ const rawTokens: Map<string, RawToken> = new Map();
114
99
  // Persisted Brand Kit JSON can outlive its schema. Keep one malformed token
115
100
  // from taking down the whole read action while preserving valid siblings.
116
101
  const setRawToken = (
117
102
  cssVar: string,
118
103
  value: unknown,
119
104
  source: unknown,
105
+ provenance: Omit<RawToken, "value" | "source">,
120
106
  ): void => {
121
107
  if (typeof value !== "string") return;
122
108
  rawTokens.set(cssVar, {
123
109
  value,
124
110
  source: typeof source === "string" && source ? source : "Unknown",
111
+ ...provenance,
125
112
  });
126
113
  };
127
114
 
@@ -138,7 +125,7 @@ export default defineAction({
138
125
  };
139
126
  extractCssVars(state, file.content);
140
127
  for (const [k, v] of Object.entries(state.cssCustomProperties)) {
141
- setRawToken(k, v, file.filename);
128
+ setRawToken(k, v, file.filename, { origin: "design" });
142
129
  }
143
130
  }
144
131
 
@@ -165,38 +152,24 @@ export default defineAction({
165
152
  if (dsRow?.data) {
166
153
  try {
167
154
  const dsData = JSON.parse(dsRow.data) as Partial<DesignSystemData>;
168
- // Flatten the Brand Kit's known token fields into CSS vars
169
- const brandColors = dsData.colors ?? {};
170
- const colorRoleMap: Record<string, string> = {
171
- primary: "--color-primary",
172
- secondary: "--color-secondary",
173
- accent: "--color-accent",
174
- background: "--color-background",
175
- surface: "--color-surface",
176
- text: "--color-text",
177
- textMuted: "--color-text-muted",
178
- };
179
- for (const [role, cssVar] of Object.entries(colorRoleMap)) {
180
- const v = (brandColors as Record<string, string>)[role];
181
- if (v) setRawToken(cssVar, v, "Brand Kit");
182
- }
183
- // Border radius
184
- if (dsData.borders?.radius) {
185
- setRawToken("--radius", dsData.borders.radius, "Brand Kit");
155
+ // The role -> var mapping lives in core so a rewriter cannot drift.
156
+ for (const token of brandKitRoleTokens(dsData)) {
157
+ setRawToken(token.cssVar, token.value, "Brand Kit", {
158
+ origin: "brand-kit",
159
+ });
186
160
  }
187
- // Spacing
188
- if (dsData.spacing?.elementGap) {
161
+ // Layered after the roles: the kit's own names win over the
162
+ // seven-slot summary.
163
+ for (const token of resolveBrandKitTokens(dsData, "Brand Kit")) {
189
164
  setRawToken(
190
- "--spacing-element-gap",
191
- dsData.spacing.elementGap,
192
- "Brand Kit",
193
- );
194
- }
195
- if (dsData.spacing?.pagePadding) {
196
- setRawToken(
197
- "--spacing-page-padding",
198
- dsData.spacing.pagePadding,
199
- "Brand Kit",
165
+ token.cssVar,
166
+ token.value,
167
+ token.source ?? "Brand Kit",
168
+ {
169
+ origin: "brand-kit",
170
+ name: token.name,
171
+ ...(token.group ? { group: token.group } : {}),
172
+ },
200
173
  );
201
174
  }
202
175
  } catch {
@@ -250,20 +223,28 @@ export default defineAction({
250
223
  ...Object.keys(tweakSelections).filter(isDirectCssVarSelectionKey),
251
224
  ]);
252
225
  for (const [cssVar, value] of Object.entries(resolvedOverrides)) {
253
- setRawToken(cssVar, value, tokenImportSources[cssVar] ?? "Tweaks");
226
+ // Retuning a value must not rename the token it came from.
227
+ const inherited = rawTokens.get(cssVar);
228
+ setRawToken(cssVar, value, tokenImportSources[cssVar] ?? "Tweaks", {
229
+ origin: inherited?.origin ?? "design",
230
+ ...(inherited?.name ? { name: inherited.name } : {}),
231
+ ...(inherited?.group ? { group: inherited.group } : {}),
232
+ });
254
233
  }
255
234
 
256
235
  // ------------------------------------------------------------------
257
236
  // 4. Build friendly token list
258
237
  // ------------------------------------------------------------------
259
238
  const tokens: DesignToken[] = [];
260
- for (const [cssVar, { value, source }] of rawTokens) {
239
+ for (const [cssVar, { value, source, name, group, origin }] of rawTokens) {
261
240
  tokens.push({
262
- name: friendlyName(cssVar),
241
+ name: name ?? friendlyTokenName(cssVar),
263
242
  cssVar,
264
243
  value,
265
- type: classifyVar(cssVar, value),
244
+ type: classifyBrandKitToken(cssVar, value),
266
245
  source,
246
+ origin,
247
+ ...(group ? { group } : {}),
267
248
  isTweakOverride: tweakCssVars.has(cssVar),
268
249
  });
269
250
  }
@@ -4,6 +4,7 @@ import { eq } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
+ import { assertStorableDesignSystemTokens } from "../server/lib/design-system-tokens.js";
7
8
 
8
9
  export default defineAction({
9
10
  description:
@@ -23,7 +24,11 @@ export default defineAction({
23
24
  .trim()
24
25
  .min(1, "data cannot be empty")
25
26
  .optional()
26
- .describe("Updated JSON string of DesignSystemData"),
27
+ .describe(
28
+ "Updated JSON string of DesignSystemData. Replaces the whole object, so " +
29
+ "include the existing `tokens` array (the source system's named tokens) " +
30
+ "alongside any edit, or those names are dropped.",
31
+ ),
27
32
  assets: z
28
33
  .string()
29
34
  .optional()
@@ -36,16 +41,8 @@ export default defineAction({
36
41
  ),
37
42
  }),
38
43
  run: async ({ id, title, description, data, assets, customInstructions }) => {
39
- // Validate that data/assets are valid JSON when provided
40
44
  if (data !== undefined) {
41
- try {
42
- const parsed = JSON.parse(data);
43
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
44
- throw new Error();
45
- }
46
- } catch {
47
- throw new Error("data must be a valid JSON object string");
48
- }
45
+ assertStorableDesignSystemTokens(data);
49
46
  }
50
47
  if (assets !== undefined) {
51
48
  try {
@@ -155,6 +155,12 @@ import {
155
155
  vscodeDeepLink,
156
156
  } from "./edit-panel/code-inspect-helpers";
157
157
  import { ComponentSection } from "./edit-panel/component-section";
158
+ import {
159
+ parseTokenReference,
160
+ resolveTokenNameForColor,
161
+ toDesignSystemColorSwatches,
162
+ type DesignSystemColorSwatch,
163
+ } from "./edit-panel/design-system-swatches";
158
164
  import {
159
165
  type DocumentColorSourceFile,
160
166
  extractDocumentColorPalette,
@@ -383,6 +389,9 @@ export { deriveLockedAspectSize };
383
389
  export { mergeRotationValue, normalizeRotationDegrees };
384
390
  export { mixedElementFromSelection };
385
391
  export { authoredStyleValue, resolveInteractionStateValue };
392
+
393
+ /** A `var()` reference names itself, so no kit lookup is needed to label it. */
394
+ const EMPTY_SWATCHES: DesignSystemColorSwatch[] = [];
386
395
  export { isTextElement };
387
396
  export { ComponentSection };
388
397
  export { extractDocumentColorPalette, type DocumentColorSourceFile };
@@ -1467,12 +1476,15 @@ function PageProperties({
1467
1476
  styles,
1468
1477
  onStyleChange,
1469
1478
  onStylesChange,
1479
+ designSystemColors,
1470
1480
  canvasBackground,
1471
1481
  onCanvasBackgroundChange,
1472
1482
  }: {
1473
1483
  styles: Record<string, string>;
1474
1484
  onStyleChange: StyleChangeHandler;
1475
1485
  onStylesChange?: StylesChangeHandler;
1486
+ /** Kit tokens offered by name in the canvas/page background pickers. */
1487
+ designSystemColors?: DesignSystemColorSwatch[];
1476
1488
  canvasBackground?: string | null;
1477
1489
  onCanvasBackgroundChange?: (value: string, meta?: StyleChangeMeta) => void;
1478
1490
  }) {
@@ -1503,6 +1515,7 @@ function PageProperties({
1503
1515
  <PanelSection title={t("editPanel.sections.canvas")}>
1504
1516
  <ColorInput
1505
1517
  label={t("editPanel.labels.background")}
1518
+ designSystemColors={designSystemColors}
1506
1519
  value={canvasBackground ?? ""}
1507
1520
  // meta carries phase: "preview" while dragging vs "commit" on
1508
1521
  // release. Dropping it persists every tick and the picker jumps.
@@ -1513,6 +1526,7 @@ function PageProperties({
1513
1526
  <PanelSection title={t("editPanel.sections.page")}>
1514
1527
  <ColorInput
1515
1528
  label={t("editPanel.labels.background")}
1529
+ designSystemColors={designSystemColors}
1516
1530
  value={styles.backgroundColor || ""}
1517
1531
  onChange={(v, meta) => onStyleChange("backgroundColor", v, meta)}
1518
1532
  backgroundImage={styles.backgroundImage}
@@ -1724,8 +1738,15 @@ function SelectionColorsProperties({
1724
1738
  className="size-4 shrink-0 rounded-[3px] border border-border/60"
1725
1739
  style={swatchStyle(color.value)}
1726
1740
  />
1727
- <span className="min-w-0 flex-1 truncate text-left uppercase tabular-nums">
1728
- {selectionDisplayHex(color.value)}
1741
+ <span
1742
+ className={cn(
1743
+ "min-w-0 flex-1 truncate text-left tabular-nums",
1744
+ // Hex is conventionally upper-case; a token name is not.
1745
+ !parseTokenReference(color.value) && "uppercase",
1746
+ )}
1747
+ >
1748
+ {resolveTokenNameForColor(color.value, EMPTY_SWATCHES) ??
1749
+ selectionDisplayHex(color.value)}
1729
1750
  </span>
1730
1751
  <span className="shrink-0 tabular-nums text-muted-foreground">
1731
1752
  {opacity}%
@@ -1920,6 +1941,24 @@ export const EditPanel = memo(function EditPanel({
1920
1941
  // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on filesContentKey (cheap length+id fingerprint) instead of `files` itself so an unstable-but-equal array identity from the parent doesn't force a full re-scan every render.
1921
1942
  [filesContentKey],
1922
1943
  );
1944
+ // The linked Brand Kit's named colour tokens, for the fill picker. Shares
1945
+ // react-query's cache with the Tokens panel, so opening both costs one fetch.
1946
+ const { data: indexedTokens } = useActionQuery<{
1947
+ tokens?: {
1948
+ name: string;
1949
+ cssVar: string;
1950
+ value: string;
1951
+ type: string;
1952
+ group?: string;
1953
+ source?: string;
1954
+ }[];
1955
+ }>("index-design-tokens", designId ? { designId } : undefined, {
1956
+ enabled: Boolean(designId),
1957
+ });
1958
+ const designSystemColors = useMemo(
1959
+ () => toDesignSystemColorSwatches(indexedTokens?.tokens),
1960
+ [indexedTokens],
1961
+ );
1923
1962
  const selectionAlreadyComponent =
1924
1963
  selectedCount === 1 &&
1925
1964
  (selectedElementAlreadyComponent ||
@@ -2326,6 +2365,7 @@ export const EditPanel = memo(function EditPanel({
2326
2365
  {!inspectorElement && !selectedScreenGeometry && (
2327
2366
  <PageProperties
2328
2367
  styles={pageStyles}
2368
+ designSystemColors={designSystemColors}
2329
2369
  onStyleChange={onStyleChange}
2330
2370
  onStylesChange={onStylesChange}
2331
2371
  canvasBackground={canvasBackground}
@@ -2382,11 +2422,13 @@ export const EditPanel = memo(function EditPanel({
2382
2422
  onStyleChange={onStyleChange}
2383
2423
  onStylesChange={onStylesChange}
2384
2424
  documentColorPalette={documentColorPalette}
2425
+ designSystemColors={designSystemColors}
2385
2426
  glslShaderContext={glslShaderContext}
2386
2427
  motionKeyframeContext={motionKeyframeFieldContext}
2387
2428
  breakpointOverrideContext={breakpointOverrideFieldContext}
2388
2429
  />
2389
2430
  <StrokeProperties
2431
+ designSystemColors={designSystemColors}
2390
2432
  element={stateResolvedInspectorElement ?? inspectorElement}
2391
2433
  onStyleChange={onStyleChange}
2392
2434
  onStylesChange={onStylesChange}
@@ -2397,6 +2439,7 @@ export const EditPanel = memo(function EditPanel({
2397
2439
  element={stateResolvedInspectorElement ?? inspectorElement}
2398
2440
  onStyleChange={onStyleChange}
2399
2441
  onStylesChange={onStylesChange}
2442
+ designSystemColors={designSystemColors}
2400
2443
  glslShaderContext={glslShaderContext}
2401
2444
  motionKeyframeContext={motionKeyframeFieldContext}
2402
2445
  />
@@ -29,6 +29,8 @@
29
29
  */
30
30
  import { createCanvasGestureController } from "@agent-native/toolkit/canvas-interactions";
31
31
 
32
+ import { collectAuthoredColorStyles } from "../../../../shared/authored-color-styles";
33
+
32
34
  declare var __READ_ONLY__: boolean;
33
35
  declare var __TEXT_EDITING_ENABLED__: boolean;
34
36
  declare var __EDITOR_CHROME_SCALE_X__: string;
@@ -1852,6 +1854,9 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
1852
1854
  // and `right` — only the inline style tells you which side was actually
1853
1855
  // set). Empty-string values are omitted so callers can treat key-absence as
1854
1856
  // "not authored".
1857
+ // color/backgroundColor/borderColor/outlineColor are reported because
1858
+ // getComputedStyle resolves `var()`: without the authored declaration the
1859
+ // inspector cannot tell a design system token from the colour it resolves to.
1855
1860
  var INLINE_STYLE_PROPERTIES = [
1856
1861
  "position",
1857
1862
  "left",
@@ -1862,6 +1867,12 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
1862
1867
  "height",
1863
1868
  "transform",
1864
1869
  "whiteSpace",
1870
+ "color",
1871
+ "backgroundColor",
1872
+ "borderColor",
1873
+ "outlineColor",
1874
+ "boxShadow",
1875
+ "textShadow",
1865
1876
  ];
1866
1877
 
1867
1878
  function collectInlineStyles(el: Element): Record<string, string> {
@@ -2121,6 +2132,7 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
2121
2132
  whiteSpace: cs.whiteSpace,
2122
2133
  },
2123
2134
  inlineStyles: collectInlineStyles(el),
2135
+ authoredColorStyles: collectAuthoredColorStyles(el),
2124
2136
  primitiveKind: el.getAttribute("data-an-primitive") || undefined,
2125
2137
  portableStyleSnapshot: collectPortableStyleSnapshot(el),
2126
2138
  boundingRect: {
@@ -0,0 +1,215 @@
1
+ import { parseCssColorExtended, rgbaToHex } from "@shared/color-utils";
2
+
3
+ /** One colour token from the design's linked Brand Kit, as the kit names it. */
4
+ export interface DesignSystemColorSwatch {
5
+ name: string;
6
+ cssVar: string;
7
+ value: string;
8
+ group?: string;
9
+ source?: string;
10
+ }
11
+
12
+ interface IndexedToken {
13
+ name: string;
14
+ cssVar: string;
15
+ value: string;
16
+ type: string;
17
+ group?: string;
18
+ source?: string;
19
+ origin?: string;
20
+ }
21
+
22
+ /** Extended: the classifier stores `oklch()`/`color()` tokens as colours. */
23
+ function normalizeHex(value: string): string | null {
24
+ const parsed = parseCssColorExtended(value.trim());
25
+ return parsed ? rgbaToHex(parsed).toUpperCase() : null;
26
+ }
27
+
28
+ /** Guards a cyclic alias chain (`--a: var(--b); --b: var(--a)`). */
29
+ const MAX_ALIAS_DEPTH = 8;
30
+
31
+ /**
32
+ * A token's value as a paintable literal, following aliases through the kit's
33
+ * own vocabulary. A kit legitimately defines one token as another, and dropping
34
+ * those loses a selectable token; painting them raw gives an empty swatch.
35
+ */
36
+ function paintableTokenValue(
37
+ value: string,
38
+ byCssVar: ReadonlyMap<string, string>,
39
+ depth = 0,
40
+ ): string | null {
41
+ if (normalizeHex(value)) return value;
42
+ if (depth >= MAX_ALIAS_DEPTH) return null;
43
+ const reference = parseTokenReference(value);
44
+ if (!reference) return null;
45
+
46
+ const aliased = byCssVar.get(reference.cssVar);
47
+ if (aliased !== undefined) {
48
+ const resolved = paintableTokenValue(aliased, byCssVar, depth + 1);
49
+ if (resolved) return resolved;
50
+ }
51
+ return reference.fallback
52
+ ? paintableTokenValue(reference.fallback, byCssVar, depth + 1)
53
+ : null;
54
+ }
55
+
56
+ /**
57
+ * The linked kit's swatchable colour tokens from an `index-design-tokens`
58
+ * result, which mixes the design's own `:root` colours into the same list.
59
+ * Values that resolve to no colour at all are dropped, since they would paint
60
+ * an empty swatch.
61
+ */
62
+ export function toDesignSystemColorSwatches(
63
+ tokens: readonly IndexedToken[] | undefined,
64
+ limit = 48,
65
+ ): DesignSystemColorSwatch[] {
66
+ if (!tokens) return [];
67
+ // Every token, not just kit ones: a kit alias may point at a design `:root`
68
+ // variable, and that still resolves to a real colour.
69
+ const byCssVar = new Map(tokens.map((token) => [token.cssVar, token.value]));
70
+ const seen = new Set<string>();
71
+ const out: DesignSystemColorSwatch[] = [];
72
+
73
+ for (const token of tokens) {
74
+ if (token.type !== "color") continue;
75
+ if (token.origin !== "brand-kit") continue;
76
+ const value = paintableTokenValue(token.value, byCssVar);
77
+ if (!value) continue;
78
+ if (seen.has(token.cssVar)) continue;
79
+ seen.add(token.cssVar);
80
+ out.push({
81
+ name: token.name,
82
+ cssVar: token.cssVar,
83
+ value,
84
+ ...(token.group ? { group: token.group } : {}),
85
+ ...(token.source ? { source: token.source } : {}),
86
+ });
87
+ if (out.length >= limit) break;
88
+ }
89
+
90
+ return out;
91
+ }
92
+
93
+ /**
94
+ * What to call a token in the picker: its CSS variable, minus the leading `--`.
95
+ *
96
+ * The indexed `name` is title-cased for tokens the kit never named ("Color
97
+ * Primary"), which reads as a different vocabulary than the kit's own
98
+ * `cds-background-brand` sitting next to it. The variable is what the design
99
+ * references, so it is the name worth showing.
100
+ */
101
+ export function swatchLabel(swatch: DesignSystemColorSwatch): string {
102
+ return swatch.cssVar.replace(/^--/, "");
103
+ }
104
+
105
+ const TOKEN_REFERENCE_HEAD = /^var\(\s*(--[-_a-zA-Z0-9]+)\s*(,?)/;
106
+
107
+ export interface TokenReference {
108
+ cssVar: string;
109
+ /** Literal after the comma. Absent for a bare `var(--x)`. */
110
+ fallback: string | null;
111
+ }
112
+
113
+ /**
114
+ * A fallback is itself a CSS value, so it can nest parentheses. Scanning to the
115
+ * balanced close rather than matching `[^()]+` is what keeps a function-valued
116
+ * token from reading as "not a token" and being dropped by the commit guard.
117
+ */
118
+ export function parseTokenReference(value: string): TokenReference | null {
119
+ const trimmed = value.trim();
120
+ const head = TOKEN_REFERENCE_HEAD.exec(trimmed);
121
+ if (!head) return null;
122
+
123
+ let depth = 1;
124
+ let index = head[0].length;
125
+ for (; index < trimmed.length && depth > 0; index += 1) {
126
+ const char = trimmed[index];
127
+ if (char === "(") depth += 1;
128
+ else if (char === ")") depth -= 1;
129
+ }
130
+ // Anything after the matching `)` means this is not a lone reference.
131
+ if (depth !== 0 || index !== trimmed.length) return null;
132
+
133
+ const fallback = trimmed.slice(head[0].length, index - 1).trim();
134
+ if (fallback && !head[2]) return null;
135
+ return { cssVar: head[1], fallback: fallback || null };
136
+ }
137
+
138
+ /**
139
+ * The declaration to persist for a picked token.
140
+ *
141
+ * The fallback is not optional padding: outside the editor — an export, a
142
+ * published page, a design whose kit link was removed — nothing defines the
143
+ * custom property, and a bare `var(--x)` renders transparent.
144
+ */
145
+ export function tokenReferenceValue(swatch: DesignSystemColorSwatch): string {
146
+ return `var(${swatch.cssVar}, ${swatch.value})`;
147
+ }
148
+
149
+ const HIDDEN_TOKEN_MIX =
150
+ /^color-mix\(\s*in\s+srgb\s*,\s*(var\(.+\))\s+0%\s*,\s*transparent\s*\)$/i;
151
+
152
+ /**
153
+ * What to write when hiding a colour. Zeroing alpha needs literal channels,
154
+ * which a `var()` reference has none of — so a token is hidden as a 0% mix that
155
+ * keeps the reference inside the persisted CSS. React state would not survive
156
+ * the deselect/reselect that unmounts the inspector.
157
+ */
158
+ export function hiddenColorWrite(
159
+ color: string,
160
+ zeroAlpha: (literal: string) => string | null,
161
+ ): string {
162
+ const reference = parseTokenReference(color);
163
+ if (reference) return `color-mix(in srgb, ${color} 0%, transparent)`;
164
+ return zeroAlpha(color) ?? "transparent";
165
+ }
166
+
167
+ /** The reference a hidden token colour carries, or null if it is not one. */
168
+ export function hiddenTokenReference(value: string): string | null {
169
+ return HIDDEN_TOKEN_MIX.exec(value.trim())?.[1] ?? null;
170
+ }
171
+
172
+ /**
173
+ * Name the token a value came from, or null when that cannot be known. A
174
+ * `var()` reference names itself; a bare colour is matched by value, and a tie
175
+ * returns null — Carbon points four tokens at one blue.
176
+ */
177
+ export function resolveTokenNameForColor(
178
+ value: string,
179
+ swatches: readonly DesignSystemColorSwatch[],
180
+ ): string | null {
181
+ const reference = parseTokenReference(value);
182
+ if (reference) return reference.cssVar.replace(/^--/, "");
183
+
184
+ const target = normalizeHex(value);
185
+ if (!target) return null;
186
+
187
+ let match: string | null = null;
188
+ for (const swatch of swatches) {
189
+ if (normalizeHex(swatch.value) !== target) continue;
190
+ if (match) return null;
191
+ match = swatchLabel(swatch);
192
+ }
193
+ return match;
194
+ }
195
+
196
+ /** Group swatches under their kit collection path, preserving token order. */
197
+ export function groupColorSwatches(
198
+ swatches: readonly DesignSystemColorSwatch[],
199
+ ): { label: string | null; swatches: DesignSystemColorSwatch[] }[] {
200
+ const groups = new Map<
201
+ string,
202
+ { label: string | null; swatches: DesignSystemColorSwatch[] }
203
+ >();
204
+ for (const swatch of swatches) {
205
+ const label = swatch.group?.trim() || null;
206
+ const key = label ?? "";
207
+ const existing = groups.get(key);
208
+ if (existing) {
209
+ existing.swatches.push(swatch);
210
+ continue;
211
+ }
212
+ groups.set(key, { label, swatches: [swatch] });
213
+ }
214
+ return [...groups.values()];
215
+ }
@@ -1,6 +1,7 @@
1
1
  import { parseCssColor, rgbaToHex } from "@shared/color-utils";
2
2
 
3
3
  import type { ElementInfo } from "../types";
4
+ import { authoredColorValue } from "./interaction-state-helpers";
4
5
 
5
6
  export interface DocumentColorSourceFile {
6
7
  id: string;
@@ -63,13 +64,16 @@ export interface SelectionColorValue {
63
64
  export function selectionColorValues(
64
65
  element: ElementInfo,
65
66
  ): SelectionColorValue[] {
66
- const styles = element.computedStyles;
67
+ // Authored, not computed: getComputedStyle has already flattened `var()`.
67
68
  const rawValues: SelectionColorValue[] = [
68
- { property: "color", value: styles.color },
69
- { property: "backgroundColor", value: styles.backgroundColor },
70
- { property: "borderColor", value: styles.borderColor },
71
- { property: "outlineColor", value: styles.outlineColor },
72
- ];
69
+ "color",
70
+ "backgroundColor",
71
+ "borderColor",
72
+ "outlineColor",
73
+ ].map((property) => ({
74
+ property,
75
+ value: authoredColorValue(element, property) as string,
76
+ }));
73
77
  const seen = new Set<string>();
74
78
  return rawValues
75
79
  .map((color) => ({ ...color, value: color.value?.trim() }))