@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +7 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/design-connect.ts +28 -2
- package/corpus/core/src/cli/skills.ts +45 -24
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
- package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
- package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
- package/corpus/templates/design/actions/apply-design-state.ts +200 -0
- package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
- package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
- package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +191 -5
- package/corpus/templates/design/actions/capture-design-state.ts +224 -0
- package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
- package/corpus/templates/design/actions/create-component.ts +447 -0
- package/corpus/templates/design/actions/create-design-branch.ts +263 -0
- package/corpus/templates/design/actions/create-design-state.ts +162 -0
- package/corpus/templates/design/actions/delete-design-state.ts +55 -0
- package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
- package/corpus/templates/design/actions/get-component-details.ts +242 -0
- package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
- package/corpus/templates/design/actions/get-design-review.ts +314 -0
- package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
- package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
- package/corpus/templates/design/actions/index-components.ts +258 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
- package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
- package/corpus/templates/design/actions/list-design-states.ts +72 -0
- package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
- package/corpus/templates/design/actions/open-component-source.ts +237 -0
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
- package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
- package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
- package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
- package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
- package/corpus/templates/design/actions/run-design-audit.ts +421 -0
- package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
- package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1414 -71
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1391 -52
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +4 -3
- package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1052 -98
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
- package/corpus/templates/design/app/components/design/index.ts +16 -0
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +66 -3
- package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +3 -1
- package/corpus/templates/design/app/i18n/zh-TW.ts +31 -0
- package/corpus/templates/design/app/i18n-data.ts +269 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2478 -386
- package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
- package/corpus/templates/design/e2e/helpers.ts +10 -4
- package/corpus/templates/design/server/db/schema.ts +123 -0
- package/corpus/templates/design/server/plugins/db.ts +90 -0
- package/corpus/templates/design/shared/builder-app.ts +297 -0
- package/corpus/templates/design/shared/capability-resolver.ts +123 -0
- package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
- package/corpus/templates/design/shared/code-layer.ts +1016 -71
- package/corpus/templates/design/shared/component-model.ts +239 -0
- package/corpus/templates/design/shared/design-review.ts +275 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
- package/corpus/templates/design/shared/design-state.ts +112 -0
- package/corpus/templates/design/shared/design-surface-index.ts +258 -0
- package/corpus/templates/design/shared/motion-compiler.ts +278 -0
- package/corpus/templates/design/shared/motion-timeline.ts +193 -0
- package/corpus/templates/design/shared/responsive-classes.ts +452 -0
- package/corpus/templates/design/shared/shader-fill.ts +323 -0
- package/corpus/templates/design/shared/source-mode.ts +245 -0
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
- package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +28 -2
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +37 -21
- package/dist/cli/skills.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +7 -7
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/transcribe-voice.d.ts +1 -1
- 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
|
+
}
|