@agent-native/core 0.80.9 → 0.80.11

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