@agent-native/core 0.80.9 → 0.80.10

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 (111) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/design-connect.ts +28 -2
  5. package/corpus/core/src/cli/skills.ts +45 -24
  6. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
  7. package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
  8. package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
  9. package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
  10. package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
  11. package/corpus/templates/design/actions/apply-design-state.ts +200 -0
  12. package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
  13. package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
  14. package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
  15. package/corpus/templates/design/actions/apply-visual-edit.ts +191 -5
  16. package/corpus/templates/design/actions/capture-design-state.ts +224 -0
  17. package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
  18. package/corpus/templates/design/actions/create-component.ts +447 -0
  19. package/corpus/templates/design/actions/create-design-branch.ts +263 -0
  20. package/corpus/templates/design/actions/create-design-state.ts +162 -0
  21. package/corpus/templates/design/actions/delete-design-state.ts +55 -0
  22. package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
  23. package/corpus/templates/design/actions/get-component-details.ts +242 -0
  24. package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
  25. package/corpus/templates/design/actions/get-design-review.ts +314 -0
  26. package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
  27. package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
  28. package/corpus/templates/design/actions/index-components.ts +258 -0
  29. package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
  30. package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
  31. package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
  32. package/corpus/templates/design/actions/list-design-states.ts +72 -0
  33. package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
  34. package/corpus/templates/design/actions/open-component-source.ts +237 -0
  35. package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
  36. package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
  37. package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
  38. package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
  39. package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
  40. package/corpus/templates/design/actions/run-design-audit.ts +421 -0
  41. package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
  42. package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
  43. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1414 -71
  44. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
  45. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
  46. package/corpus/templates/design/app/components/design/EditPanel.tsx +1391 -52
  47. package/corpus/templates/design/app/components/design/LayersPanel.tsx +4 -3
  48. package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
  49. package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
  50. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1052 -98
  51. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
  52. package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
  53. package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
  54. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
  55. package/corpus/templates/design/app/components/design/index.ts +16 -0
  56. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
  57. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +66 -3
  58. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
  59. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
  60. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
  61. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
  62. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +3 -1
  63. package/corpus/templates/design/app/i18n/zh-TW.ts +31 -0
  64. package/corpus/templates/design/app/i18n-data.ts +269 -0
  65. package/corpus/templates/design/app/pages/DesignEditor.tsx +2478 -386
  66. package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
  67. package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
  68. package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
  69. package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
  70. package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
  71. package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
  72. package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
  73. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
  74. package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
  75. package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
  76. package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
  77. package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
  78. package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
  79. package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
  80. package/corpus/templates/design/e2e/helpers.ts +10 -4
  81. package/corpus/templates/design/server/db/schema.ts +123 -0
  82. package/corpus/templates/design/server/plugins/db.ts +90 -0
  83. package/corpus/templates/design/shared/builder-app.ts +297 -0
  84. package/corpus/templates/design/shared/capability-resolver.ts +123 -0
  85. package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
  86. package/corpus/templates/design/shared/code-layer.ts +1016 -71
  87. package/corpus/templates/design/shared/component-model.ts +239 -0
  88. package/corpus/templates/design/shared/design-review.ts +275 -0
  89. package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
  90. package/corpus/templates/design/shared/design-state.ts +112 -0
  91. package/corpus/templates/design/shared/design-surface-index.ts +258 -0
  92. package/corpus/templates/design/shared/motion-compiler.ts +278 -0
  93. package/corpus/templates/design/shared/motion-timeline.ts +193 -0
  94. package/corpus/templates/design/shared/responsive-classes.ts +452 -0
  95. package/corpus/templates/design/shared/shader-fill.ts +323 -0
  96. package/corpus/templates/design/shared/source-mode.ts +245 -0
  97. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
  98. package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
  99. package/dist/cli/design-connect.d.ts.map +1 -1
  100. package/dist/cli/design-connect.js +28 -2
  101. package/dist/cli/design-connect.js.map +1 -1
  102. package/dist/cli/skills.d.ts.map +1 -1
  103. package/dist/cli/skills.js +37 -21
  104. package/dist/cli/skills.js.map +1 -1
  105. package/dist/collab/routes.d.ts +1 -1
  106. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  107. package/dist/notifications/routes.d.ts +2 -2
  108. package/dist/observability/routes.d.ts +7 -7
  109. package/dist/resources/handlers.d.ts +2 -2
  110. package/dist/server/transcribe-voice.d.ts +1 -1
  111. package/package.json +1 -1
@@ -0,0 +1,573 @@
1
+ // i18n-raw-literal-disable-file — new Design Studio panel; UI strings are localized when this feature is finalized in the follow-up PR.
2
+ /**
3
+ * Tokens inspector panel — §6.2 of DESIGN-STUDIO-PLAN.md.
4
+ *
5
+ * Friendly token names + colour swatches, CSS-var name on the right, type-scale
6
+ * section, radius input, a source chip, and a New token action. Grouped by
7
+ * type: color → typography → spacing → radius → shadow → other.
8
+ *
9
+ * Alpine (Tier-A): edits go through `apply-design-token-edit` which routes
10
+ * through the Tweaks loop (live tweak-values preview + persist in
11
+ * designs.data.tweakSelections). No source file write-back yet.
12
+ *
13
+ * Real app (Tier-B): the write-back advisory from the action surfaces inline
14
+ * as a migration CTA; no additional UI logic is needed here.
15
+ */
16
+
17
+ import {
18
+ useActionMutation,
19
+ useActionQuery,
20
+ useT,
21
+ } from "@agent-native/core/client";
22
+ import {
23
+ IconBrush,
24
+ IconChevronDown,
25
+ IconChevronRight,
26
+ IconCircle,
27
+ IconCode,
28
+ IconLetterCase,
29
+ IconPalette,
30
+ IconPlus,
31
+ IconRefresh,
32
+ IconRuler,
33
+ IconSpacingVertical,
34
+ IconShadow,
35
+ } from "@tabler/icons-react";
36
+ import { useEffect, useRef, useState } from "react";
37
+
38
+ import { Badge } from "@/components/ui/badge";
39
+ import { Button } from "@/components/ui/button";
40
+ import { Input } from "@/components/ui/input";
41
+ import {
42
+ Popover,
43
+ PopoverContent,
44
+ PopoverTrigger,
45
+ } from "@/components/ui/popover";
46
+ import {
47
+ Tooltip,
48
+ TooltipContent,
49
+ TooltipTrigger,
50
+ } from "@/components/ui/tooltip";
51
+ import { cn } from "@/lib/utils";
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Types (mirroring the action shape)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ interface DesignToken {
58
+ name: string;
59
+ cssVar: string;
60
+ value: string;
61
+ type: "color" | "typography" | "spacing" | "radius" | "shadow" | "other";
62
+ source: string;
63
+ isTweakOverride?: boolean;
64
+ }
65
+
66
+ interface TokenGroup {
67
+ type: DesignToken["type"];
68
+ tokens: DesignToken[];
69
+ }
70
+
71
+ interface IndexDesignTokensResult {
72
+ designId: string;
73
+ tokenCount: number;
74
+ groups: TokenGroup[];
75
+ tokens: DesignToken[];
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Props
80
+ // ---------------------------------------------------------------------------
81
+
82
+ export interface TokensPanelProps {
83
+ designId: string;
84
+ /**
85
+ * Called after a token edit is persisted so the parent can push the
86
+ * resolved CSS var map into the iframe via the tweak-values postMessage.
87
+ */
88
+ onTokensApplied?: (resolvedCssVars: Record<string, string>) => void;
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Helpers
93
+ // ---------------------------------------------------------------------------
94
+
95
+ /** True when the value looks like an opaque colour we can render as a swatch. */
96
+ function isColorValue(value: string): boolean {
97
+ const v = value.trim();
98
+ return (
99
+ /^#[0-9a-fA-F]{3,8}$/.test(v) ||
100
+ /^rgba?\(/.test(v) ||
101
+ /^hsla?\(/.test(v) ||
102
+ /^oklch\(/.test(v) ||
103
+ /^color\(/.test(v)
104
+ );
105
+ }
106
+
107
+ /** Type label + icon for a section header. */
108
+ function typeLabel(type: DesignToken["type"]): {
109
+ label: string;
110
+ Icon: React.ComponentType<{ className?: string }>;
111
+ } {
112
+ switch (type) {
113
+ case "color":
114
+ return { label: "Colors", Icon: IconPalette };
115
+ case "typography":
116
+ return { label: "Typography", Icon: IconLetterCase };
117
+ case "spacing":
118
+ return { label: "Spacing", Icon: IconSpacingVertical };
119
+ case "radius":
120
+ return { label: "Radius", Icon: IconRuler };
121
+ case "shadow":
122
+ return { label: "Shadows & Effects", Icon: IconShadow };
123
+ default:
124
+ return { label: "Other", Icon: IconBrush };
125
+ }
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Individual token row
130
+ // ---------------------------------------------------------------------------
131
+
132
+ interface TokenRowProps {
133
+ token: DesignToken;
134
+ onEdit: (cssVar: string, value: string) => void;
135
+ editing: boolean;
136
+ editDraft: string;
137
+ onDraftChange: (v: string) => void;
138
+ onCommit: () => void;
139
+ onStartEdit: () => void;
140
+ onCancelEdit: () => void;
141
+ }
142
+
143
+ function TokenRow({
144
+ token,
145
+ editing,
146
+ editDraft,
147
+ onDraftChange,
148
+ onCommit,
149
+ onStartEdit,
150
+ onCancelEdit,
151
+ }: TokenRowProps) {
152
+ const inputRef = useRef<HTMLInputElement>(null);
153
+ const showSwatch = token.type === "color" && isColorValue(token.value);
154
+
155
+ useEffect(() => {
156
+ if (editing) {
157
+ inputRef.current?.focus();
158
+ inputRef.current?.select();
159
+ }
160
+ }, [editing]);
161
+
162
+ return (
163
+ <div
164
+ className={cn(
165
+ "group flex min-h-[28px] items-center gap-2 rounded px-2 py-0.5",
166
+ "hover:bg-accent/40 transition-colors",
167
+ editing && "bg-accent/60",
168
+ )}
169
+ >
170
+ {/* Swatch or type icon */}
171
+ {showSwatch ? (
172
+ <span
173
+ className="size-3.5 flex-none rounded-sm ring-1 ring-border/50"
174
+ style={{ backgroundColor: token.value }}
175
+ aria-hidden
176
+ />
177
+ ) : (
178
+ <IconCircle
179
+ className="size-3.5 flex-none text-muted-foreground/30"
180
+ aria-hidden
181
+ />
182
+ )}
183
+
184
+ {/* Friendly name */}
185
+ <span
186
+ className="flex-1 cursor-pointer truncate text-[11px] text-foreground"
187
+ onClick={onStartEdit}
188
+ title={token.name}
189
+ >
190
+ {token.name}
191
+ </span>
192
+
193
+ {/* Value / edit input */}
194
+ {editing ? (
195
+ <Input
196
+ ref={inputRef}
197
+ value={editDraft}
198
+ onChange={(e) => onDraftChange(e.target.value)}
199
+ onKeyDown={(e) => {
200
+ if (e.key === "Enter") onCommit();
201
+ if (e.key === "Escape") onCancelEdit();
202
+ }}
203
+ onBlur={onCommit}
204
+ className="h-5 w-24 px-1 py-0 text-[11px] font-mono"
205
+ />
206
+ ) : (
207
+ <span
208
+ className="max-w-[6rem] cursor-pointer truncate text-right font-mono text-[10px] text-muted-foreground hover:text-foreground"
209
+ title={token.value}
210
+ onClick={onStartEdit}
211
+ >
212
+ {token.value}
213
+ </span>
214
+ )}
215
+
216
+ {/* CSS var chip (hidden when editing, visible on hover) */}
217
+ {!editing && (
218
+ <Tooltip>
219
+ <TooltipTrigger asChild>
220
+ <span className="hidden cursor-default select-all rounded bg-muted px-1 py-0 font-mono text-[9px] text-muted-foreground/70 group-hover:inline">
221
+ {token.cssVar}
222
+ </span>
223
+ </TooltipTrigger>
224
+ <TooltipContent className="font-mono text-xs">
225
+ {token.cssVar}
226
+ </TooltipContent>
227
+ </Tooltip>
228
+ )}
229
+
230
+ {/* Source chip */}
231
+ {!editing && (
232
+ <Badge
233
+ variant="outline"
234
+ className="hidden h-4 cursor-default px-1 py-0 text-[9px] text-muted-foreground/60 group-hover:flex"
235
+ >
236
+ {token.source}
237
+ </Badge>
238
+ )}
239
+ </div>
240
+ );
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Token group section
245
+ // ---------------------------------------------------------------------------
246
+
247
+ interface TokenGroupSectionProps {
248
+ group: TokenGroup;
249
+ editingKey: string | null;
250
+ editDraft: string;
251
+ onStartEdit: (cssVar: string, currentValue: string) => void;
252
+ onDraftChange: (v: string) => void;
253
+ onCommit: () => void;
254
+ onCancelEdit: () => void;
255
+ onEdit: (cssVar: string, value: string) => void;
256
+ }
257
+
258
+ function TokenGroupSection({
259
+ group,
260
+ editingKey,
261
+ editDraft,
262
+ onStartEdit,
263
+ onDraftChange,
264
+ onCommit,
265
+ onCancelEdit,
266
+ onEdit,
267
+ }: TokenGroupSectionProps) {
268
+ const [collapsed, setCollapsed] = useState(false);
269
+ const { label, Icon } = typeLabel(group.type);
270
+
271
+ return (
272
+ <div>
273
+ <button
274
+ type="button"
275
+ onClick={() => setCollapsed((c) => !c)}
276
+ className="flex w-full items-center gap-1.5 px-2 py-1.5 text-left hover:bg-accent/30"
277
+ >
278
+ {collapsed ? (
279
+ <IconChevronRight className="size-3 text-muted-foreground/50" />
280
+ ) : (
281
+ <IconChevronDown className="size-3 text-muted-foreground/50" />
282
+ )}
283
+ <Icon className="size-3 text-muted-foreground/60" />
284
+ <span className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
285
+ {label}
286
+ </span>
287
+ <span className="ml-auto text-[10px] tabular-nums text-muted-foreground/40">
288
+ {group.tokens.length}
289
+ </span>
290
+ </button>
291
+
292
+ {!collapsed && (
293
+ <div className="pb-1">
294
+ {group.tokens.map((token) => (
295
+ <TokenRow
296
+ key={token.cssVar}
297
+ token={token}
298
+ editing={editingKey === token.cssVar}
299
+ editDraft={editDraft}
300
+ onDraftChange={onDraftChange}
301
+ onCommit={onCommit}
302
+ onStartEdit={() => onStartEdit(token.cssVar, token.value)}
303
+ onCancelEdit={onCancelEdit}
304
+ onEdit={onEdit}
305
+ />
306
+ ))}
307
+ </div>
308
+ )}
309
+ </div>
310
+ );
311
+ }
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // New Token popover
315
+ // ---------------------------------------------------------------------------
316
+
317
+ interface NewTokenPopoverProps {
318
+ onAdd: (cssVar: string, value: string) => void;
319
+ }
320
+
321
+ function NewTokenPopover({ onAdd }: NewTokenPopoverProps) {
322
+ const [open, setOpen] = useState(false);
323
+ const [cssVar, setCssVar] = useState("--my-token");
324
+ const [value, setValue] = useState("#000000");
325
+ const t = useT();
326
+
327
+ const handleAdd = () => {
328
+ const cleanVar = cssVar.startsWith("--") ? cssVar : `--${cssVar}`;
329
+ onAdd(cleanVar, value);
330
+ setOpen(false);
331
+ setCssVar("--my-token");
332
+ setValue("#000000");
333
+ };
334
+
335
+ return (
336
+ <Popover open={open} onOpenChange={setOpen}>
337
+ <PopoverTrigger asChild>
338
+ <Button
339
+ type="button"
340
+ variant="ghost"
341
+ size="sm"
342
+ className="h-6 cursor-pointer gap-1 px-2 text-[11px] text-muted-foreground hover:text-foreground"
343
+ >
344
+ <IconPlus className="size-3" />
345
+ {t("designEditor.tokens.newToken")}
346
+ </Button>
347
+ </PopoverTrigger>
348
+ <PopoverContent align="end" className="w-56 p-3 text-[12px]">
349
+ <div className="space-y-2">
350
+ <div className="space-y-1">
351
+ <label className="text-[10px] font-medium text-muted-foreground">
352
+ {t("designEditor.tokens.cssVar")}
353
+ </label>
354
+ <Input
355
+ value={cssVar}
356
+ onChange={(e) => setCssVar(e.target.value)}
357
+ className="h-6 font-mono text-[11px]"
358
+ placeholder="--my-token"
359
+ />
360
+ </div>
361
+ <div className="space-y-1">
362
+ <label className="text-[10px] font-medium text-muted-foreground">
363
+ {t("designEditor.tokens.value")}
364
+ </label>
365
+ <Input
366
+ value={value}
367
+ onChange={(e) => setValue(e.target.value)}
368
+ className="h-6 font-mono text-[11px]"
369
+ placeholder="#3B82F6"
370
+ />
371
+ </div>
372
+ <Button
373
+ type="button"
374
+ className="h-7 w-full cursor-pointer text-[11px]"
375
+ onClick={handleAdd}
376
+ disabled={!cssVar || !value}
377
+ >
378
+ {t("designEditor.tokens.add")}
379
+ </Button>
380
+ </div>
381
+ </PopoverContent>
382
+ </Popover>
383
+ );
384
+ }
385
+
386
+ // ---------------------------------------------------------------------------
387
+ // Main panel
388
+ // ---------------------------------------------------------------------------
389
+
390
+ /**
391
+ * Tokens inspector panel — displays design tokens grouped by type, supports
392
+ * inline editing that persists through the Tweaks loop, and provides a "New
393
+ * token" action. Matches the tokens artboard in §6.2 of DESIGN-STUDIO-PLAN.md.
394
+ */
395
+ export function TokensPanel({ designId, onTokensApplied }: TokensPanelProps) {
396
+ const t = useT();
397
+
398
+ // ------------------------------------------------------------------
399
+ // Data
400
+ // ------------------------------------------------------------------
401
+ const { data, isLoading, refetch } = useActionQuery<IndexDesignTokensResult>(
402
+ "index-design-tokens",
403
+ { designId },
404
+ );
405
+
406
+ const applyMutation = useActionMutation("apply-design-token-edit");
407
+
408
+ // ------------------------------------------------------------------
409
+ // Local edit state
410
+ // ------------------------------------------------------------------
411
+ const [editingKey, setEditingKey] = useState<string | null>(null);
412
+ const [editDraft, setEditDraft] = useState("");
413
+
414
+ const startEdit = (cssVar: string, currentValue: string) => {
415
+ setEditingKey(cssVar);
416
+ setEditDraft(currentValue);
417
+ };
418
+
419
+ const cancelEdit = () => {
420
+ setEditingKey(null);
421
+ setEditDraft("");
422
+ };
423
+
424
+ const commitEdit = () => {
425
+ if (!editingKey || !editDraft.trim()) {
426
+ cancelEdit();
427
+ return;
428
+ }
429
+ const cssVar = editingKey;
430
+ const value = editDraft.trim();
431
+ cancelEdit();
432
+ applyMutation.mutate(
433
+ { designId, edits: [{ cssVar, value }] },
434
+ {
435
+ onSuccess: (result) => {
436
+ void refetch();
437
+ const r = result as { resolvedCssVars?: Record<string, string> };
438
+ if (r?.resolvedCssVars && onTokensApplied) {
439
+ onTokensApplied(r.resolvedCssVars);
440
+ }
441
+ },
442
+ },
443
+ );
444
+ };
445
+
446
+ const handleNewToken = (cssVar: string, value: string) => {
447
+ applyMutation.mutate(
448
+ { designId, edits: [{ cssVar, value }] },
449
+ {
450
+ onSuccess: (result) => {
451
+ void refetch();
452
+ const r = result as { resolvedCssVars?: Record<string, string> };
453
+ if (r?.resolvedCssVars && onTokensApplied) {
454
+ onTokensApplied(r.resolvedCssVars);
455
+ }
456
+ },
457
+ },
458
+ );
459
+ };
460
+
461
+ // ------------------------------------------------------------------
462
+ // Render
463
+ // ------------------------------------------------------------------
464
+ const groups = data?.groups ?? [];
465
+ const tokenCount = data?.tokenCount ?? 0;
466
+
467
+ return (
468
+ <div className="flex h-full flex-col overflow-hidden">
469
+ {/* Header */}
470
+ <div className="flex items-center justify-between border-b border-border/60 px-3 py-2">
471
+ <div className="flex items-center gap-1.5">
472
+ <IconCode className="size-3.5 text-muted-foreground/60" />
473
+ <span className="text-[11px] font-semibold text-foreground">
474
+ {t("designEditor.tokens.title")}
475
+ </span>
476
+ {tokenCount > 0 && (
477
+ <span className="tabular-nums text-[10px] text-muted-foreground/50">
478
+ ({tokenCount})
479
+ </span>
480
+ )}
481
+ </div>
482
+ <div className="flex items-center gap-1">
483
+ <Tooltip>
484
+ <TooltipTrigger asChild>
485
+ <Button
486
+ type="button"
487
+ variant="ghost"
488
+ size="icon"
489
+ className="size-6 cursor-pointer text-muted-foreground/60 hover:text-foreground"
490
+ onClick={() => void refetch()}
491
+ aria-label={t("designEditor.tokens.refresh")}
492
+ >
493
+ <IconRefresh className="size-3" />
494
+ </Button>
495
+ </TooltipTrigger>
496
+ <TooltipContent>{t("designEditor.tokens.refresh")}</TooltipContent>
497
+ </Tooltip>
498
+ <NewTokenPopover onAdd={handleNewToken} />
499
+ </div>
500
+ </div>
501
+
502
+ {/* Body */}
503
+ <div className="min-h-0 flex-1 overflow-y-auto">
504
+ {isLoading && (
505
+ <div className="flex flex-col gap-1.5 px-3 py-4">
506
+ {[1, 2, 3, 4, 5].map((i) => (
507
+ <div
508
+ key={i}
509
+ className="h-6 animate-pulse rounded bg-muted/40"
510
+ style={{ width: `${60 + (i % 3) * 15}%` }}
511
+ />
512
+ ))}
513
+ </div>
514
+ )}
515
+
516
+ {!isLoading && groups.length === 0 && (
517
+ <div className="flex flex-col items-center gap-2 px-4 py-8 text-center">
518
+ <IconPalette className="size-6 text-muted-foreground/30" />
519
+ <p className="text-[11px] leading-snug text-muted-foreground/60">
520
+ {t("designEditor.tokens.empty")}
521
+ </p>
522
+ <p className="text-[10px] text-muted-foreground/40">
523
+ {t("designEditor.tokens.emptyHint")}
524
+ </p>
525
+ </div>
526
+ )}
527
+
528
+ {!isLoading && groups.length > 0 && (
529
+ <div className="pb-2">
530
+ {groups.map((group) => (
531
+ <TokenGroupSection
532
+ key={group.type}
533
+ group={group}
534
+ editingKey={editingKey}
535
+ editDraft={editDraft}
536
+ onStartEdit={startEdit}
537
+ onDraftChange={setEditDraft}
538
+ onCommit={commitEdit}
539
+ onCancelEdit={cancelEdit}
540
+ onEdit={(cssVar, value) =>
541
+ applyMutation.mutate(
542
+ { designId, edits: [{ cssVar, value }] },
543
+ {
544
+ onSuccess: (result) => {
545
+ void refetch();
546
+ const r = result as {
547
+ resolvedCssVars?: Record<string, string>;
548
+ };
549
+ if (r?.resolvedCssVars && onTokensApplied) {
550
+ onTokensApplied(r.resolvedCssVars);
551
+ }
552
+ },
553
+ },
554
+ )
555
+ }
556
+ />
557
+ ))}
558
+ </div>
559
+ )}
560
+ </div>
561
+
562
+ {/* Pending indicator */}
563
+ {applyMutation.isPending && (
564
+ <div className="flex items-center gap-1.5 border-t border-border/60 px-3 py-1.5">
565
+ <span className="size-1.5 animate-pulse rounded-full bg-primary" />
566
+ <span className="text-[10px] text-muted-foreground">
567
+ {t("designEditor.tokens.applying")}
568
+ </span>
569
+ </div>
570
+ )}
571
+ </div>
572
+ );
573
+ }