@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
@@ -42,6 +42,7 @@ import {
42
42
  type GlslShaderPanelContext,
43
43
  } from "../inspector/GlslShaderPanel";
44
44
  import type { ElementInfo } from "../types";
45
+ import type { DesignSystemColorSwatch } from "./design-system-swatches";
45
46
  import { isTextElement } from "./element-classification";
46
47
  import { elementStableKey } from "./element-identity";
47
48
  import { FieldTrailer } from "./field-primitives";
@@ -51,6 +52,7 @@ import {
51
52
  SectionIconButton,
52
53
  useRowDragReorder,
53
54
  } from "./inspector-controls";
55
+ import { authoredColorValue } from "./interaction-state-helpers";
54
56
  import { ColorInput, PanelSection } from "./panel-primitives";
55
57
  import {
56
58
  colorHasVisibleAlpha,
@@ -295,10 +297,15 @@ function ShadowEffectRow({
295
297
  rowProps,
296
298
  handleProps,
297
299
  element,
300
+ designSystemColors,
301
+ resolvedColor,
298
302
  motionKeyframeContext,
299
303
  }: {
300
304
  layer: ShadowLayer;
301
305
  index: number;
306
+ designSystemColors?: DesignSystemColorSwatch[];
307
+ /** The layer's painted colour, for the swatch when `layer.color` is a token. */
308
+ resolvedColor?: string;
302
309
  onChange: (patch: Partial<ShadowLayer>, meta?: StyleChangeMeta) => void;
303
310
  onRemove: () => void;
304
311
  onToggleVisibility: () => void;
@@ -452,6 +459,8 @@ function ShadowEffectRow({
452
459
  <ColorInput
453
460
  label={t("editPanel.labels.color")}
454
461
  value={cssColorOrFallback(layer.color, "rgba(0, 0, 0, 0.25)")}
462
+ designSystemColors={designSystemColors}
463
+ resolvedColor={resolvedColor}
455
464
  onChange={(value, meta) => onChange({ color: value }, meta)}
456
465
  />
457
466
  </div>
@@ -464,12 +473,15 @@ export function EffectsProperties({
464
473
  element,
465
474
  onStyleChange,
466
475
  onStylesChange,
476
+ designSystemColors,
467
477
  glslShaderContext,
468
478
  motionKeyframeContext,
469
479
  }: {
470
480
  element: ElementInfo;
471
481
  onStyleChange: StyleChangeHandler;
472
482
  onStylesChange?: StylesChangeHandler;
483
+ /** Kit tokens offered by name in the drop-shadow colour picker. */
484
+ designSystemColors?: DesignSystemColorSwatch[];
473
485
  /**
474
486
  * Persistence context for the code-backed Shader effect type (GLSL
475
487
  * overlay rendered above the element's content, saved into the screen
@@ -508,11 +520,13 @@ export function EffectsProperties({
508
520
  const shadowTargetsText = isTextElement(element);
509
521
  // Read from the same property the writer targets, or the rows would show a
510
522
  // box-shadow that is no longer what this element uses.
523
+ const shadowProperty = shadowTargetsText ? "textShadow" : "boxShadow";
524
+ const paintedShadowLayers = effectsAreMixed
525
+ ? []
526
+ : parseShadowLayers(styles[shadowProperty]);
511
527
  const shadowLayers = effectsAreMixed
512
528
  ? []
513
- : parseShadowLayers(
514
- shadowTargetsText ? styles.textShadow : styles.boxShadow,
515
- );
529
+ : parseShadowLayers(authoredColorValue(element, shadowProperty));
516
530
  const setShadowLayers = (layers: ShadowLayer[], meta?: StyleChangeMeta) => {
517
531
  setHiddenEffectStash((stash) =>
518
532
  remapIndexedShadowStash(stash, effectStashKey, layers),
@@ -676,6 +690,8 @@ export function EffectsProperties({
676
690
  }
677
691
  rowProps={shadowDrag.getRowProps(index)}
678
692
  handleProps={shadowDrag.getHandleProps(index)}
693
+ designSystemColors={designSystemColors}
694
+ resolvedColor={paintedShadowLayers[index]?.color}
679
695
  onChange={(patch, meta) => {
680
696
  const next = shadowLayers.map((candidate) =>
681
697
  candidate.id === layer.id
@@ -23,6 +23,12 @@ import {
23
23
  import { DesignColorPicker, imageFillToBackgroundStyles } from "../inspector";
24
24
  import type { GlslShaderPanelContext } from "../inspector/GlslShaderPanel";
25
25
  import type { ElementInfo } from "../types";
26
+ import {
27
+ hiddenColorWrite,
28
+ hiddenTokenReference,
29
+ parseTokenReference,
30
+ type DesignSystemColorSwatch,
31
+ } from "./design-system-swatches";
26
32
  import { selectionColorValues } from "./document-colors";
27
33
  import { isTextElement } from "./element-classification";
28
34
  import { elementStableKey } from "./element-identity";
@@ -46,11 +52,13 @@ import {
46
52
  SectionIconButton,
47
53
  useRowDragReorder,
48
54
  } from "./inspector-controls";
55
+ import { authoredColorValue } from "./interaction-state-helpers";
49
56
  import { ColorInput, PanelSection } from "./panel-primitives";
50
57
  import {
51
58
  colorHasVisibleAlpha,
52
59
  cssColorOrFallback,
53
60
  swatchStyle,
61
+ zeroAlphaLiteral,
54
62
  } from "./position-helpers";
55
63
  import { isMixedValue } from "./selection-helpers";
56
64
  import type {
@@ -109,6 +117,7 @@ export function FillProperties({
109
117
  onStyleChange,
110
118
  onStylesChange,
111
119
  documentColorPalette = [],
120
+ designSystemColors,
112
121
  glslShaderContext,
113
122
  motionKeyframeContext,
114
123
  breakpointOverrideContext,
@@ -121,6 +130,9 @@ export function FillProperties({
121
130
  * colors below so a real, always-populated "Document colors" row is
122
131
  * available even before any file content has been scanned. */
123
132
  documentColorPalette?: string[];
133
+ /** Colour tokens from the design's linked Brand Kit, shown by name in the
134
+ * fill picker above the document palette. */
135
+ designSystemColors?: DesignSystemColorSwatch[];
124
136
  /**
125
137
  * Persistence context for the code-backed Shader paint type (GLSL source
126
138
  * saved into the screen HTML). Threaded into the fill picker so its
@@ -144,9 +156,20 @@ export function FillProperties({
144
156
  Record<string, string>
145
157
  >({});
146
158
  const fillStashKey = elementStableKey(element);
147
- const fillValue = isTextFillElement
159
+ // getComputedStyle resolves `var()` before we can read it, so a token-backed
160
+ // fill would come back as a bare colour and lose the name it was set from.
161
+ // The authored inline value still holds the reference.
162
+ const authoredFill = authoredColorValue(element, fillProperty);
163
+ const computedFill = isTextFillElement
148
164
  ? styles.color || ""
149
165
  : styles.backgroundColor || "";
166
+ // A hidden token's `color-mix` also has to survive here, or the reference it
167
+ // carries is replaced by the transparent colour it computes to.
168
+ const fillValue =
169
+ authoredFill &&
170
+ (parseTokenReference(authoredFill) || hiddenTokenReference(authoredFill))
171
+ ? authoredFill
172
+ : computedFill;
150
173
  const backgroundLayers = isTextFillElement
151
174
  ? []
152
175
  : splitCssLayers(styles.backgroundImage || "");
@@ -183,24 +206,25 @@ export function FillProperties({
183
206
  // separately: computed style strips comments but keeps rgba() channels).
184
207
  // Showing again just restores alpha to 1 using those same channels, so no
185
208
  // stash is required and the hide survives reselect/reload.
186
- const isHidden = !colorHasVisibleAlpha(fillValue);
209
+ const hiddenFillToken = hiddenTokenReference(fillValue);
210
+ const isHidden = Boolean(hiddenFillToken) || !colorHasVisibleAlpha(fillValue);
211
+
187
212
  const handleFillVisibilityToggle = () => {
188
- const parsed = parseCssColor(fillValue);
189
213
  if (isHidden) {
214
+ if (hiddenFillToken) {
215
+ onStyleChange(fillProperty, hiddenFillToken);
216
+ return;
217
+ }
218
+ const parsed = parseCssColor(fillValue);
190
219
  const restored = parsed
191
220
  ? rgbaToCss(withColorOpacity(parsed, 100))
192
221
  : isTextFillElement
193
222
  ? "#000000"
194
223
  : "#ffffff";
195
224
  onStyleChange(fillProperty, restored);
196
- } else if (parsed) {
197
- onStyleChange(fillProperty, rgbaToCss(withColorOpacity(parsed, 0)));
198
- } else {
199
- // Value wasn't a parseable color (e.g. already the literal
200
- // "transparent") — nothing to preserve, transparent is the best we
201
- // can do.
202
- onStyleChange(fillProperty, "transparent");
225
+ return;
203
226
  }
227
+ onStyleChange(fillProperty, hiddenColorWrite(fillValue, zeroAlphaLiteral));
204
228
  };
205
229
 
206
230
  // Reorder fill layers by dragging: permute all four index-aligned parallel
@@ -385,6 +409,8 @@ export function FillProperties({
385
409
  commitStylePatch(patch, onStyleChange, onStylesChange)
386
410
  }
387
411
  documentColors={documentColors}
412
+ designSystemColors={designSystemColors}
413
+ resolvedColor={computedFill}
388
414
  pickerKey={[
389
415
  element.sourceId ??
390
416
  element.id ??
@@ -1,5 +1,20 @@
1
1
  import type { ElementInfo } from "../types";
2
2
 
3
+ /**
4
+ * The colour a property was authored with, else the computed value. A generated
5
+ * design styles through classes, so `el.style` alone is not enough.
6
+ */
7
+ export function authoredColorValue(
8
+ element: ElementInfo,
9
+ property: string,
10
+ ): string | undefined {
11
+ const inline = element.inlineStyles?.[property];
12
+ if (inline) return inline;
13
+ const authored = element.authoredColorStyles?.[property];
14
+ if (authored) return authored;
15
+ return element.computedStyles[property];
16
+ }
17
+
3
18
  export function authoredStyleValue(
4
19
  element: ElementInfo,
5
20
  property: string,
@@ -26,6 +26,10 @@ import {
26
26
  } from "../inspector";
27
27
  import type { DesignPaintType } from "../inspector/DesignColorPicker";
28
28
  import type { GlslShaderPanelContext } from "../inspector/GlslShaderPanel";
29
+ import {
30
+ parseTokenReference,
31
+ type DesignSystemColorSwatch,
32
+ } from "./design-system-swatches";
29
33
  import {
30
34
  buildGradientLayer,
31
35
  defaultGradientLayer,
@@ -225,6 +229,8 @@ export function ColorInput({
225
229
  onBlendModeChange,
226
230
  supportsLayeredFills = false,
227
231
  documentColors,
232
+ designSystemColors,
233
+ resolvedColor,
228
234
  supportedPaintTypes,
229
235
  pickerKey,
230
236
  glslShaderContext,
@@ -270,6 +276,8 @@ export function ColorInput({
270
276
  supportsLayeredFills?: boolean;
271
277
  /** Hex strings already in use on the page — forwarded to the color picker swatch grid. */
272
278
  documentColors?: string[];
279
+ designSystemColors?: DesignSystemColorSwatch[];
280
+ resolvedColor?: string;
273
281
  /**
274
282
  * Restricts which paint-type tabs the popover renders. Omit for the full
275
283
  * set (solid + gradients + image + …). Pass `["solid"]` for properties
@@ -369,7 +377,15 @@ export function ColorInput({
369
377
  // before clobbering the last-known-good value in this component's own
370
378
  // state. Reject anything that doesn't parse as a plain solid color in
371
379
  // that case instead of forwarding it.
372
- if (!supportsLayeredFills && !parseCssColor(next)) return;
380
+ // A design system token reference is valid on a plain color property even
381
+ // though it is not itself a color, so it must survive this guard.
382
+ if (
383
+ !supportsLayeredFills &&
384
+ !parseCssColor(next) &&
385
+ !parseTokenReference(next)
386
+ ) {
387
+ return;
388
+ }
373
389
  pendingGestureRef.current = phase === "preview";
374
390
  setDraft(next);
375
391
  onChange(next, { phase });
@@ -574,6 +590,8 @@ export function ColorInput({
574
590
  gradientType={activeGradient?.type}
575
591
  onGradientTypeChange={handleGradientTypeChange}
576
592
  documentColors={documentColors}
593
+ designSystemColors={designSystemColors}
594
+ resolvedColor={resolvedColor}
577
595
  supportedPaintTypes={supportedPaintTypes}
578
596
  glslShaderContext={glslShaderContext}
579
597
  />
@@ -213,6 +213,12 @@ export function resolveRestoredStrokeStyle(
213
213
  * (a single undo/history step, matching every other multi-property commit
214
214
  * in this file, e.g. TextStrokeProperties' equivalent show handler).
215
215
  */
216
+ /** A literal colour at zero alpha, or null when it does not parse as one. */
217
+ export function zeroAlphaLiteral(literal: string): string | null {
218
+ const parsed = parseCssColor(literal);
219
+ return parsed ? rgbaToCss(withColorOpacity(parsed, 0)) : null;
220
+ }
221
+
216
222
  export function strokeShowPatch(
217
223
  prefix: "border" | "outline",
218
224
  color: string,
@@ -23,6 +23,12 @@ import {
23
23
  import { ScrubInput } from "../inspector";
24
24
  import type { DesignPaintType } from "../inspector/DesignColorPicker";
25
25
  import type { ElementInfo } from "../types";
26
+ import {
27
+ hiddenColorWrite,
28
+ hiddenTokenReference,
29
+ parseTokenReference,
30
+ type DesignSystemColorSwatch,
31
+ } from "./design-system-swatches";
26
32
  import { isTextElement } from "./element-classification";
27
33
  import { commitStylePatch, FieldTrailer } from "./field-primitives";
28
34
  import { SectionIconButton } from "./inspector-controls";
@@ -41,6 +47,7 @@ import {
41
47
  strokeShowPatch,
42
48
  textStrokeAddPatch,
43
49
  textStrokeIsVisible,
50
+ zeroAlphaLiteral,
44
51
  } from "./position-helpers";
45
52
  import { isMixedValue } from "./selection-helpers";
46
53
  import type {
@@ -60,6 +67,26 @@ import { STROKE_POSITION_OPTIONS } from "./style-options";
60
67
  */
61
68
  const SOLID_ONLY_PAINT_TYPES: DesignPaintType[] = ["solid"];
62
69
 
70
+ /**
71
+ * The stroke colour as authored: its token name when it has one, else the
72
+ * resolved colour. Empty when no rule set it at all — the border then paints
73
+ * CSS's `currentColor` default, and echoing the inherited text colour here
74
+ * would claim a stroke colour the design never chose.
75
+ */
76
+ export function authoredStrokeColor(
77
+ element: ElementInfo,
78
+ property: string,
79
+ computed: string | undefined,
80
+ ): string {
81
+ const authored =
82
+ element.inlineStyles?.[property] ?? element.authoredColorStyles?.[property];
83
+ if (!authored) return "";
84
+ if (parseTokenReference(authored) || hiddenTokenReference(authored)) {
85
+ return authored;
86
+ }
87
+ return computed || authored;
88
+ }
89
+
63
90
  type StrokeLayerKind = "border" | "outline";
64
91
  type StrokePosition = "inside" | "outside" | "center";
65
92
 
@@ -67,6 +94,7 @@ function StrokeLayerControl({
67
94
  kind,
68
95
  visible,
69
96
  color,
97
+ designSystemColors,
70
98
  width,
71
99
  styleValue,
72
100
  outlineOffset,
@@ -80,6 +108,7 @@ function StrokeLayerControl({
80
108
  kind: StrokeLayerKind;
81
109
  visible: boolean;
82
110
  color: string;
111
+ designSystemColors?: DesignSystemColorSwatch[];
83
112
  width: string;
84
113
  styleValue: string;
85
114
  /** Only meaningful when `kind === "outline"` — distinguishes outside vs
@@ -104,6 +133,7 @@ function StrokeLayerControl({
104
133
  label: t(`editPanel.labels.${option.key}`),
105
134
  }));
106
135
  const prefix = kind === "border" ? "border" : "outline";
136
+ const hiddenToken = hiddenTokenReference(color);
107
137
  const position: StrokePosition =
108
138
  kind === "border"
109
139
  ? "inside"
@@ -162,7 +192,10 @@ function StrokeLayerControl({
162
192
  <div className="min-w-0 flex-1">
163
193
  <ColorInput
164
194
  label=""
165
- value={cssColorOrFallback(color, "#000000")}
195
+ // Not cssColorOrFallback: an unset stroke must stay unset here
196
+ // rather than render as a black the design never chose.
197
+ value={hiddenToken ?? color}
198
+ designSystemColors={designSystemColors}
166
199
  onChange={(value, meta) =>
167
200
  onStyleChange(`${prefix}Color`, value, meta)
168
201
  }
@@ -183,21 +216,18 @@ function StrokeLayerControl({
183
216
  // style permanently, since there is no round-trippable "unset"
184
217
  // for that keyword once it's overwritten.
185
218
  if (visible) {
186
- const parsed = parseCssColor(color);
187
219
  onStyleChange(
188
220
  `${prefix}Color`,
189
- parsed ? rgbaToCss(withColorOpacity(parsed, 0)) : "transparent",
221
+ hiddenColorWrite(color, zeroAlphaLiteral),
190
222
  );
191
223
  return;
192
224
  }
193
225
  // Restore color/width/style as ONE commit (single undo step)
194
226
  // rather than three sequential onStyleChange calls — see
195
227
  // strokeShowPatch's doc comment.
196
- commitStylePatch(
197
- strokeShowPatch(prefix, color, width, styleValue),
198
- onStyleChange,
199
- onStylesChange,
200
- );
228
+ const showPatch = strokeShowPatch(prefix, color, width, styleValue);
229
+ if (hiddenToken) showPatch[`${prefix}Color`] = hiddenToken;
230
+ commitStylePatch(showPatch, onStyleChange, onStylesChange);
201
231
  }}
202
232
  >
203
233
  {visible ? (
@@ -293,12 +323,15 @@ export function StrokeProperties({
293
323
  element,
294
324
  onStyleChange,
295
325
  onStylesChange,
326
+ designSystemColors,
296
327
  motionKeyframeContext,
297
328
  breakpointOverrideContext,
298
329
  }: {
299
330
  element: ElementInfo;
300
331
  onStyleChange: StyleChangeHandler;
301
332
  onStylesChange?: StylesChangeHandler;
333
+ /** Linked Brand Kit colour tokens, offered by name in the stroke picker. */
334
+ designSystemColors?: DesignSystemColorSwatch[];
302
335
  motionKeyframeContext?: MotionKeyframeFieldContext;
303
336
  breakpointOverrideContext?: BreakpointOverrideFieldContext;
304
337
  }) {
@@ -449,7 +482,12 @@ export function StrokeProperties({
449
482
  <StrokeLayerControl
450
483
  kind="border"
451
484
  visible={borderVisible}
452
- color={styles.borderColor || "#000000"}
485
+ color={authoredStrokeColor(
486
+ element,
487
+ "borderColor",
488
+ styles.borderColor,
489
+ )}
490
+ designSystemColors={designSystemColors}
453
491
  width={styles.borderWidth || "0px"}
454
492
  styleValue={styles.borderStyle || "none"}
455
493
  onStyleChange={onStyleChange}
@@ -470,7 +508,12 @@ export function StrokeProperties({
470
508
  <StrokeLayerControl
471
509
  kind="outline"
472
510
  visible={outlineVisible}
473
- color={styles.outlineColor || styles.borderColor || "#000000"}
511
+ color={authoredStrokeColor(
512
+ element,
513
+ "outlineColor",
514
+ styles.outlineColor || styles.borderColor,
515
+ )}
516
+ designSystemColors={designSystemColors}
474
517
  width={styles.outlineWidth || "0px"}
475
518
  styleValue={styles.outlineStyle || "solid"}
476
519
  outlineOffset={styles.outlineOffset || "0px"}
@@ -43,6 +43,14 @@ import {
43
43
  } from "@/components/ui/tooltip";
44
44
  import { cn } from "@/lib/utils";
45
45
 
46
+ import {
47
+ groupColorSwatches,
48
+ parseTokenReference,
49
+ resolveTokenNameForColor,
50
+ swatchLabel,
51
+ tokenReferenceValue,
52
+ type DesignSystemColorSwatch,
53
+ } from "../edit-panel/design-system-swatches";
46
54
  import {
47
55
  GlslShaderPanel,
48
56
  type GlslShaderPanelContext,
@@ -190,6 +198,16 @@ export interface DesignColorPickerProps {
190
198
  * limited in the caller; the component renders whatever is passed.
191
199
  */
192
200
  documentColors?: string[];
201
+ /**
202
+ * Colour tokens from the design's linked Brand Kit, listed by name above
203
+ * "Document colors". Picking one writes `var(--token, #hex)`.
204
+ */
205
+ designSystemColors?: DesignSystemColorSwatch[];
206
+ /**
207
+ * The colour the canvas resolved `value` to. Only consulted when `value` is a
208
+ * token reference this document cannot resolve on its own.
209
+ */
210
+ resolvedColor?: string;
193
211
  /**
194
212
  * Restricts which paint-type tabs are rendered (Solid, Linear, Radial, …).
195
213
  * Omit to show every type (default, backward compatible). Callers that
@@ -703,6 +721,8 @@ export function DesignColorPicker({
703
721
  gradientType,
704
722
  onGradientTypeChange,
705
723
  documentColors,
724
+ designSystemColors,
725
+ resolvedColor,
706
726
  supportedPaintTypes,
707
727
  shaderContext,
708
728
  glslShaderContext,
@@ -712,7 +732,15 @@ export function DesignColorPicker({
712
732
  className,
713
733
  }: DesignColorPickerProps) {
714
734
  const copy = { ...DEFAULT_LABELS, ...labels };
715
- const color = parseCssColorExtended(value) ?? FALLBACK_COLOR;
735
+ // A `var(--token, #hex)` fill has no colour of its own outside the preview
736
+ // document; read the literal so the SV field and chip are not blank.
737
+ const tokenReference = parseTokenReference(value);
738
+ // Generated CSS writes bare `var(--token)`, unresolvable here, so the colour
739
+ // has to come from what the canvas painted.
740
+ const color =
741
+ parseCssColorExtended(tokenReference?.fallback ?? value) ??
742
+ (tokenReference ? parseCssColorExtended(resolvedColor ?? "") : null) ??
743
+ FALLBACK_COLOR;
716
744
  const hsv = rgbaToHsv(color);
717
745
  const hsl = rgbaToHsl(color);
718
746
  const effectiveOpacity = opacity ?? alphaToOpacity(color.a);
@@ -814,6 +842,16 @@ export function DesignColorPicker({
814
842
  ? rawEffectivePaintType
815
843
  : "solid";
816
844
 
845
+ const tokenName = useMemo(() => {
846
+ if (effectivePaintType !== "solid" || !designSystemColors?.length) {
847
+ return null;
848
+ }
849
+ return resolveTokenNameForColor(
850
+ tokenReference ? value : rgbaToCss(color),
851
+ designSystemColors,
852
+ );
853
+ }, [color, designSystemColors, effectivePaintType, tokenReference, value]);
854
+
817
855
  // Resolve the active gradient: prefer EditPanel-driven props; otherwise parse
818
856
  // the live CSS value, falling back to local edit state.
819
857
  const parsedGradient = useMemo(
@@ -896,6 +934,15 @@ export function DesignColorPicker({
896
934
  onChange(next);
897
935
  };
898
936
 
937
+ /**
938
+ * Same route as `emitColor`: a solid fill must not detour through the
939
+ * layered-paint merge just because its value is an expression.
940
+ */
941
+ const emitTokenReference = (nextValue: string) => {
942
+ lastEmittedValueRef.current = nextValue;
943
+ onChange(nextValue);
944
+ };
945
+
899
946
  const emitPaintValue = (nextValue: string) => {
900
947
  lastEmittedValueRef.current = nextValue;
901
948
  if (onPaintValueChange) onPaintValueChange(nextValue);
@@ -1336,8 +1383,14 @@ export function DesignColorPicker({
1336
1383
  className="size-4 shrink-0 rounded-[3px] border border-border/60"
1337
1384
  style={triggerSwatchStyle(value, color)}
1338
1385
  />
1339
- <span className="min-w-0 flex-1 truncate text-left tabular-nums uppercase !text-[11px]">
1340
- {triggerLabel(effectivePaintType, color)}
1386
+ <span
1387
+ className={cn(
1388
+ "min-w-0 flex-1 truncate text-left tabular-nums !text-[11px]",
1389
+ // Hex is conventionally upper-case; a token name is not.
1390
+ !tokenName && "uppercase",
1391
+ )}
1392
+ >
1393
+ {tokenName ?? triggerLabel(effectivePaintType, color)}
1341
1394
  </span>
1342
1395
  <span className="tabular-nums text-muted-foreground !text-[11px]">
1343
1396
  {effectiveOpacity}%
@@ -1350,7 +1403,7 @@ export function DesignColorPicker({
1350
1403
  side="left"
1351
1404
  align="start"
1352
1405
  sideOffset={8}
1353
- className="z-[10000] w-[252px] p-0 shadow-xl"
1406
+ className="z-[10000] max-h-[calc(100vh-5rem)] w-[252px] overflow-y-auto p-0 shadow-xl"
1354
1407
  // Keep the picker open when the style change triggered by a paint-type
1355
1408
  // switch causes the canvas to re-project the element. Without this,
1356
1409
  // Radix treats the resulting focus shift as an "interact outside" event
@@ -1779,6 +1832,91 @@ export function DesignColorPicker({
1779
1832
  </div>
1780
1833
  )}
1781
1834
 
1835
+ {/* ── Design system ────────────────────────────────────────── */}
1836
+ {/* The linked Brand Kit's colour tokens, under the names the
1837
+ source system uses. Clicking one applies its value; it does
1838
+ not bind to the CSS variable, so the fill will not follow a
1839
+ later kit edit. */}
1840
+ {designSystemColors && designSystemColors.length > 0 && (
1841
+ <div className="border-t border-border/70 px-3 py-2.5">
1842
+ <div className="mb-2 flex h-6 w-full items-center justify-between gap-2 px-0.5 !text-[11px] text-muted-foreground">
1843
+ <span className="truncate">
1844
+ {"Design system" /* i18n-ignore design picker source */}
1845
+ </span>
1846
+ {designSystemColors[0]?.source ? (
1847
+ <span className="shrink-0 truncate opacity-80">
1848
+ {designSystemColors[0].source}
1849
+ </span>
1850
+ ) : null}
1851
+ </div>
1852
+
1853
+ {/* A list, not a swatch grid: the token's name is the whole
1854
+ point of this section, and a grid hides it behind hover. */}
1855
+ <div className="max-h-40 space-y-2 overflow-y-auto pe-0.5">
1856
+ {groupColorSwatches(designSystemColors).map((group) => (
1857
+ <div key={group.label ?? "ungrouped"}>
1858
+ {group.label ? (
1859
+ <div className="mb-1 truncate px-0.5 !text-[10px] text-muted-foreground/80">
1860
+ {group.label}
1861
+ </div>
1862
+ ) : null}
1863
+ <div className="space-y-0.5">
1864
+ {group.swatches.map((swatch) => {
1865
+ const isActive =
1866
+ !activeGradient && tokenName === swatch.name;
1867
+ return (
1868
+ <button
1869
+ key={swatch.cssVar}
1870
+ type="button"
1871
+ disabled={disabled}
1872
+ aria-label={swatchLabel(swatch)}
1873
+ aria-pressed={isActive}
1874
+ title={`${swatch.name} · ${swatch.cssVar}`}
1875
+ className={cn(
1876
+ "flex w-full items-center gap-2 rounded-sm border px-1 py-0.5 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1877
+ isActive
1878
+ ? "border-primary bg-accent/40"
1879
+ : "border-transparent hover:bg-accent/40",
1880
+ )}
1881
+ onClick={() => {
1882
+ if (activeGradient) {
1883
+ // The stop parser round-trips colours,
1884
+ // not var() expressions.
1885
+ emitStopColor(
1886
+ parseCssColorExtended(swatch.value) ??
1887
+ color,
1888
+ );
1889
+ } else {
1890
+ emitTokenReference(
1891
+ tokenReferenceValue(swatch),
1892
+ );
1893
+ }
1894
+ notifyChangeComplete();
1895
+ }}
1896
+ >
1897
+ <span
1898
+ className="size-4 shrink-0 rounded-[3px] border border-border/60"
1899
+ style={swatchStyle(swatch.value)}
1900
+ />
1901
+ <span className="min-w-0 flex-1 truncate !text-[11px] text-foreground">
1902
+ {swatchLabel(swatch)}
1903
+ </span>
1904
+ <span className="shrink-0 font-mono uppercase !text-[10px] text-muted-foreground">
1905
+ {rgbaToHex(
1906
+ parseCssColorExtended(swatch.value) ??
1907
+ color,
1908
+ )}
1909
+ </span>
1910
+ </button>
1911
+ );
1912
+ })}
1913
+ </div>
1914
+ </div>
1915
+ ))}
1916
+ </div>
1917
+ </div>
1918
+ )}
1919
+
1782
1920
  {/* ── Document colors ──────────────────────────────────────── */}
1783
1921
  {/* Renders the palette of colors already used in the design.
1784
1922
  When `documentColors` is provided, those swatches are shown;
@@ -95,6 +95,12 @@ export interface ElementInfo {
95
95
  * must fall back to computedStyles-based inference when absent.
96
96
  */
97
97
  inlineStyles?: Record<string, string>;
98
+ /**
99
+ * Colour declarations resolved through the cascade (inline, else the last
100
+ * matching rule). Generated designs style via classes, so `inlineStyles` is
101
+ * empty and only this preserves a `var(--token, …)` reference.
102
+ */
103
+ authoredColorStyles?: Record<string, string>;
98
104
  /**
99
105
  * Value of the element's `data-an-primitive` attribute (e.g. "text",
100
106
  * "rectangle", "frame", "ellipse") when present. Canvas-drawn primitives —