@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,579 @@
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
+ * States & Responsive panel (§6.4).
4
+ *
5
+ * Two-part panel rendered in the right inspector:
6
+ * 1. Breakpoint segmented control — Auto / Mobile / Tablet / Desktop
7
+ * Sets the active canvas frame and the responsive-class edit scope.
8
+ * 2. State rows — Default, plus any named states/fixtures/captures.
9
+ * Selecting a row applies the state to the canvas preview.
10
+ *
11
+ * Data is loaded via useActionQuery / useActionMutation so the agent and UI
12
+ * share the same surface (§architecture contract).
13
+ */
14
+
15
+ import { useActionMutation, useActionQuery } from "@agent-native/core/client";
16
+ import {
17
+ IconCamera,
18
+ IconChevronRight,
19
+ IconDeviceDesktop,
20
+ IconDeviceMobile,
21
+ IconDeviceTablet,
22
+ IconDots,
23
+ IconPlus,
24
+ IconRefresh,
25
+ IconTrash,
26
+ } from "@tabler/icons-react";
27
+ import { useState } from "react";
28
+
29
+ import { Button } from "@/components/ui/button";
30
+ import {
31
+ DropdownMenu,
32
+ DropdownMenuContent,
33
+ DropdownMenuItem,
34
+ DropdownMenuSeparator,
35
+ DropdownMenuTrigger,
36
+ } from "@/components/ui/dropdown-menu";
37
+ import {
38
+ Tooltip,
39
+ TooltipContent,
40
+ TooltipTrigger,
41
+ } from "@/components/ui/tooltip";
42
+ import { cn } from "@/lib/utils";
43
+
44
+ import type {
45
+ DesignStateBreakpoint,
46
+ DesignStateKind,
47
+ } from "../../../shared/design-state.js";
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Types
51
+ // ---------------------------------------------------------------------------
52
+
53
+ interface DesignStateRow {
54
+ id: string;
55
+ designId: string;
56
+ name: string;
57
+ kind: DesignStateKind;
58
+ breakpoint: DesignStateBreakpoint;
59
+ sourceRef?: string | null;
60
+ route?: string | null;
61
+ previewRef?: string | null;
62
+ createdAt?: string | null;
63
+ updatedAt?: string | null;
64
+ }
65
+
66
+ export interface StatesPanelProps {
67
+ designId: string;
68
+ /** Currently active state id. `null` means the Default (live) state. */
69
+ activeStateId: string | null;
70
+ /** Currently active breakpoint id, or `"auto"` for single-frame view. */
71
+ activeBreakpointId: string;
72
+ /**
73
+ * Ordered list of breakpoint frames the canvas is currently showing.
74
+ * Each entry must have at least an id, label, and widthPx.
75
+ */
76
+ breakpoints: Array<{ id: string; label: string; widthPx: number }>;
77
+ /** Whether the design's source supports live captures. */
78
+ canCapture?: boolean;
79
+ onStateSelect: (stateId: string | null) => void;
80
+ onBreakpointSelect: (breakpointId: string) => void;
81
+ onAddBreakpoint?: () => void;
82
+ onCapture?: () => void;
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Breakpoint icon map
87
+ // ---------------------------------------------------------------------------
88
+
89
+ function BreakpointIcon({
90
+ widthPx,
91
+ className,
92
+ }: {
93
+ widthPx: number;
94
+ className?: string;
95
+ }) {
96
+ if (widthPx >= 1024) {
97
+ return <IconDeviceDesktop className={cn("size-3.5", className)} />;
98
+ }
99
+ if (widthPx >= 600) {
100
+ return <IconDeviceTablet className={cn("size-3.5", className)} />;
101
+ }
102
+ return <IconDeviceMobile className={cn("size-3.5", className)} />;
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // State kind badge
107
+ // ---------------------------------------------------------------------------
108
+
109
+ const KIND_LABELS: Record<DesignStateKind, string> = {
110
+ state: "State",
111
+ fixture: "Fixture",
112
+ capture: "Capture",
113
+ };
114
+
115
+ const KIND_COLORS: Record<DesignStateKind, string> = {
116
+ state: "bg-primary/10 text-primary",
117
+ fixture: "bg-amber-500/10 text-amber-600 dark:text-amber-400",
118
+ capture: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
119
+ };
120
+
121
+ function KindBadge({ kind }: { kind: DesignStateKind }) {
122
+ return (
123
+ <span
124
+ className={cn(
125
+ "rounded px-1 py-0.5 text-[9px] font-semibold uppercase leading-none tracking-wide",
126
+ KIND_COLORS[kind],
127
+ )}
128
+ >
129
+ {KIND_LABELS[kind]}
130
+ </span>
131
+ );
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Single state row
136
+ // ---------------------------------------------------------------------------
137
+
138
+ function StateRow({
139
+ row,
140
+ isActive,
141
+ onSelect,
142
+ onDelete,
143
+ }: {
144
+ row: DesignStateRow;
145
+ isActive: boolean;
146
+ onSelect: () => void;
147
+ onDelete: () => void;
148
+ }) {
149
+ return (
150
+ <div
151
+ role="button"
152
+ tabIndex={0}
153
+ onClick={onSelect}
154
+ onKeyDown={(e) => {
155
+ if (e.key === "Enter" || e.key === " ") onSelect();
156
+ }}
157
+ className={cn(
158
+ "group flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 transition-colors",
159
+ isActive
160
+ ? "bg-primary/10 text-foreground"
161
+ : "text-muted-foreground hover:bg-accent hover:text-foreground",
162
+ )}
163
+ >
164
+ {/* Active indicator */}
165
+ <div
166
+ className={cn(
167
+ "size-1.5 shrink-0 rounded-full",
168
+ isActive ? "bg-primary" : "bg-muted-foreground/30",
169
+ )}
170
+ />
171
+
172
+ {/* Name */}
173
+ <span className="min-w-0 flex-1 truncate text-[12px] font-medium">
174
+ {row.name}
175
+ </span>
176
+
177
+ {/* Kind badge */}
178
+ <KindBadge kind={row.kind} />
179
+
180
+ {/* Overflow menu */}
181
+ <DropdownMenu>
182
+ <DropdownMenuTrigger asChild>
183
+ <Button
184
+ type="button"
185
+ variant="ghost"
186
+ size="icon"
187
+ className="size-5 shrink-0 cursor-pointer text-muted-foreground/50 opacity-0 hover:text-foreground group-hover:opacity-100"
188
+ onClick={(e) => e.stopPropagation()}
189
+ aria-label={`Options for ${row.name}`}
190
+ >
191
+ <IconDots className="size-3" />
192
+ </Button>
193
+ </DropdownMenuTrigger>
194
+ <DropdownMenuContent align="end" className="w-40">
195
+ <DropdownMenuItem onClick={onSelect}>
196
+ <IconChevronRight className="mr-2 size-3.5" />
197
+ Apply state
198
+ </DropdownMenuItem>
199
+ <DropdownMenuSeparator />
200
+ <DropdownMenuItem
201
+ onClick={(e) => {
202
+ e.stopPropagation();
203
+ onDelete();
204
+ }}
205
+ className="text-destructive focus:text-destructive"
206
+ >
207
+ <IconTrash className="mr-2 size-3.5" />
208
+ Delete
209
+ </DropdownMenuItem>
210
+ </DropdownMenuContent>
211
+ </DropdownMenu>
212
+ </div>
213
+ );
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Main panel
218
+ // ---------------------------------------------------------------------------
219
+
220
+ export function StatesPanel({
221
+ designId,
222
+ activeStateId,
223
+ activeBreakpointId,
224
+ breakpoints,
225
+ canCapture = false,
226
+ onStateSelect,
227
+ onBreakpointSelect,
228
+ onAddBreakpoint,
229
+ onCapture,
230
+ }: StatesPanelProps) {
231
+ const [isAdding, setIsAdding] = useState(false);
232
+ const [newStateName, setNewStateName] = useState("");
233
+
234
+ // --- Data ---
235
+ const { data, isLoading, refetch } = useActionQuery<{
236
+ count: number;
237
+ states: DesignStateRow[];
238
+ }>("list-design-states", { designId });
239
+
240
+ const createState = useActionMutation("create-design-state");
241
+ const deleteState = useActionMutation("delete-design-state");
242
+ const setActiveBreakpoint = useActionMutation("set-active-breakpoint");
243
+
244
+ const states = data?.states ?? [];
245
+
246
+ // --- Breakpoint control ---
247
+ const handleBreakpointClick = (id: string) => {
248
+ onBreakpointSelect(id);
249
+ setActiveBreakpoint.mutate({ designId, breakpointId: id });
250
+ };
251
+
252
+ // --- Create state ---
253
+ const handleCreateState = async () => {
254
+ const name = newStateName.trim();
255
+ if (!name) return;
256
+ setIsAdding(false);
257
+ setNewStateName("");
258
+ await createState.mutateAsync({ designId, name, kind: "state" });
259
+ await refetch();
260
+ };
261
+
262
+ // --- Delete state ---
263
+ const handleDeleteState = async (id: string) => {
264
+ await deleteState.mutateAsync({ id, designId });
265
+ if (activeStateId === id) onStateSelect(null);
266
+ await refetch();
267
+ };
268
+
269
+ return (
270
+ <div className="flex flex-col gap-3 px-3 py-3">
271
+ {/* ── Responsive breakpoints ── */}
272
+ <section aria-label="Breakpoints">
273
+ <div className="mb-1.5 flex items-center justify-between">
274
+ <span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
275
+ Responsive
276
+ </span>
277
+ {onAddBreakpoint && (
278
+ <Tooltip>
279
+ <TooltipTrigger asChild>
280
+ <Button
281
+ type="button"
282
+ variant="ghost"
283
+ size="icon"
284
+ className="size-5 cursor-pointer text-muted-foreground/50 hover:text-foreground"
285
+ onClick={onAddBreakpoint}
286
+ aria-label="Add breakpoint"
287
+ >
288
+ <IconPlus className="size-3" />
289
+ </Button>
290
+ </TooltipTrigger>
291
+ <TooltipContent>Add breakpoint</TooltipContent>
292
+ </Tooltip>
293
+ )}
294
+ </div>
295
+
296
+ {/* Segmented control */}
297
+ <div className="flex h-7 overflow-hidden rounded-md border border-border">
298
+ {/* Auto = single-frame, all breakpoints */}
299
+ <BreakpointButton
300
+ id="auto"
301
+ label="Auto"
302
+ isActive={activeBreakpointId === "auto"}
303
+ onClick={() => handleBreakpointClick("auto")}
304
+ />
305
+
306
+ {breakpoints.map((bp) => (
307
+ <BreakpointButton
308
+ key={bp.id}
309
+ id={bp.id}
310
+ label={bp.label}
311
+ widthPx={bp.widthPx}
312
+ isActive={activeBreakpointId === bp.id}
313
+ onClick={() => handleBreakpointClick(bp.id)}
314
+ />
315
+ ))}
316
+
317
+ {/* Default device shortcuts when no custom breakpoints are configured */}
318
+ {breakpoints.length === 0 && (
319
+ <>
320
+ <BreakpointButton
321
+ id="bp-mobile"
322
+ label="Mobile"
323
+ widthPx={390}
324
+ isActive={activeBreakpointId === "bp-mobile"}
325
+ onClick={() => handleBreakpointClick("bp-mobile")}
326
+ />
327
+ <BreakpointButton
328
+ id="bp-tablet"
329
+ label="Tablet"
330
+ widthPx={768}
331
+ isActive={activeBreakpointId === "bp-tablet"}
332
+ onClick={() => handleBreakpointClick("bp-tablet")}
333
+ />
334
+ <BreakpointButton
335
+ id="bp-desktop"
336
+ label="Desktop"
337
+ widthPx={1280}
338
+ isActive={activeBreakpointId === "bp-desktop"}
339
+ onClick={() => handleBreakpointClick("bp-desktop")}
340
+ />
341
+ </>
342
+ )}
343
+ </div>
344
+
345
+ {/* Active-frame hint */}
346
+ {activeBreakpointId !== "auto" && (
347
+ <p className="mt-1 text-[10px] text-muted-foreground/60">
348
+ Edits in this frame write{" "}
349
+ <span className="font-mono font-semibold">
350
+ {activeBreakpointId === "bp-mobile"
351
+ ? "base:"
352
+ : activeBreakpointId === "bp-tablet"
353
+ ? "md:"
354
+ : activeBreakpointId === "bp-desktop"
355
+ ? "xl:"
356
+ : "breakpoint:"}
357
+ </span>{" "}
358
+ classes
359
+ </p>
360
+ )}
361
+ </section>
362
+
363
+ {/* ── Design states ── */}
364
+ <section aria-label="Design states">
365
+ <div className="mb-1.5 flex items-center justify-between">
366
+ <span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
367
+ States
368
+ </span>
369
+ <div className="flex items-center gap-0.5">
370
+ {canCapture && onCapture && (
371
+ <Tooltip>
372
+ <TooltipTrigger asChild>
373
+ <Button
374
+ type="button"
375
+ variant="ghost"
376
+ size="icon"
377
+ className="size-5 cursor-pointer text-muted-foreground/50 hover:text-foreground"
378
+ onClick={onCapture}
379
+ aria-label="Capture from running app"
380
+ >
381
+ <IconCamera className="size-3" />
382
+ </Button>
383
+ </TooltipTrigger>
384
+ <TooltipContent>Capture from running app</TooltipContent>
385
+ </Tooltip>
386
+ )}
387
+ <Tooltip>
388
+ <TooltipTrigger asChild>
389
+ <Button
390
+ type="button"
391
+ variant="ghost"
392
+ size="icon"
393
+ className="size-5 cursor-pointer text-muted-foreground/50 hover:text-foreground"
394
+ onClick={() => refetch()}
395
+ aria-label="Refresh states"
396
+ disabled={isLoading}
397
+ >
398
+ <IconRefresh
399
+ className={cn("size-3", isLoading && "animate-spin")}
400
+ />
401
+ </Button>
402
+ </TooltipTrigger>
403
+ <TooltipContent>Refresh</TooltipContent>
404
+ </Tooltip>
405
+ <Tooltip>
406
+ <TooltipTrigger asChild>
407
+ <Button
408
+ type="button"
409
+ variant="ghost"
410
+ size="icon"
411
+ className="size-5 cursor-pointer text-muted-foreground/50 hover:text-foreground"
412
+ onClick={() => setIsAdding(true)}
413
+ aria-label="Add state"
414
+ >
415
+ <IconPlus className="size-3" />
416
+ </Button>
417
+ </TooltipTrigger>
418
+ <TooltipContent>Add state</TooltipContent>
419
+ </Tooltip>
420
+ </div>
421
+ </div>
422
+
423
+ {/* Default row (always present) */}
424
+ <div
425
+ role="button"
426
+ tabIndex={0}
427
+ onClick={() => onStateSelect(null)}
428
+ onKeyDown={(e) => {
429
+ if (e.key === "Enter" || e.key === " ") onStateSelect(null);
430
+ }}
431
+ className={cn(
432
+ "flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 transition-colors",
433
+ activeStateId === null
434
+ ? "bg-primary/10 text-foreground"
435
+ : "text-muted-foreground hover:bg-accent hover:text-foreground",
436
+ )}
437
+ >
438
+ <div
439
+ className={cn(
440
+ "size-1.5 shrink-0 rounded-full",
441
+ activeStateId === null ? "bg-primary" : "bg-muted-foreground/30",
442
+ )}
443
+ />
444
+ <span className="text-[12px] font-medium">Default</span>
445
+ <span className="ml-auto text-[10px] text-muted-foreground/40">
446
+ Live
447
+ </span>
448
+ </div>
449
+
450
+ {/* Named states */}
451
+ {states.length > 0 && (
452
+ <div className="mt-0.5 flex flex-col gap-0.5">
453
+ {states.map((row) => (
454
+ <StateRow
455
+ key={row.id}
456
+ row={row}
457
+ isActive={activeStateId === row.id}
458
+ onSelect={() => onStateSelect(row.id)}
459
+ onDelete={() => handleDeleteState(row.id)}
460
+ />
461
+ ))}
462
+ </div>
463
+ )}
464
+
465
+ {/* Loading placeholder */}
466
+ {isLoading && states.length === 0 && (
467
+ <div className="mt-2 space-y-1">
468
+ {[1, 2].map((i) => (
469
+ <div
470
+ key={i}
471
+ className="h-7 animate-pulse rounded-md bg-accent/60"
472
+ />
473
+ ))}
474
+ </div>
475
+ )}
476
+
477
+ {/* Empty state (no user-created states yet) */}
478
+ {!isLoading && states.length === 0 && !isAdding && (
479
+ <p className="mt-2 text-center text-[11px] leading-snug text-muted-foreground/50">
480
+ Add states like Loading, Empty, or Error to preview different UI
481
+ variants.
482
+ </p>
483
+ )}
484
+
485
+ {/* Inline add-state form */}
486
+ {isAdding && (
487
+ <form
488
+ className="mt-1 flex items-center gap-1"
489
+ onSubmit={(e) => {
490
+ e.preventDefault();
491
+ void handleCreateState();
492
+ }}
493
+ >
494
+ <input
495
+ autoFocus
496
+ value={newStateName}
497
+ onChange={(e) => setNewStateName(e.target.value)}
498
+ placeholder="State name…"
499
+ className="h-7 flex-1 rounded-md border border-border bg-background px-2 text-[12px] text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-ring"
500
+ onKeyDown={(e) => {
501
+ if (e.key === "Escape") {
502
+ setIsAdding(false);
503
+ setNewStateName("");
504
+ }
505
+ }}
506
+ />
507
+ <Button
508
+ type="submit"
509
+ size="sm"
510
+ className="h-7 cursor-pointer px-2.5 text-[11px]"
511
+ disabled={!newStateName.trim() || createState.isPending}
512
+ >
513
+ Add
514
+ </Button>
515
+ <Button
516
+ type="button"
517
+ variant="ghost"
518
+ size="sm"
519
+ className="h-7 cursor-pointer px-2 text-[11px]"
520
+ onClick={() => {
521
+ setIsAdding(false);
522
+ setNewStateName("");
523
+ }}
524
+ >
525
+ Cancel
526
+ </Button>
527
+ </form>
528
+ )}
529
+
530
+ {/* Real-app capture CTA (inline, only when not already canCapture) */}
531
+ {!canCapture && (
532
+ <div className="mt-3 rounded-md border border-dashed border-border/60 bg-muted/30 px-3 py-2.5 text-center">
533
+ <p className="text-[11px] leading-snug text-muted-foreground/70">
534
+ Connect Builder to capture live app data and route states.
535
+ </p>
536
+ </div>
537
+ )}
538
+ </section>
539
+ </div>
540
+ );
541
+ }
542
+
543
+ // ---------------------------------------------------------------------------
544
+ // Breakpoint button helper
545
+ // ---------------------------------------------------------------------------
546
+
547
+ function BreakpointButton({
548
+ id,
549
+ label,
550
+ widthPx,
551
+ isActive,
552
+ onClick,
553
+ }: {
554
+ id: string;
555
+ label: string;
556
+ widthPx?: number;
557
+ isActive: boolean;
558
+ onClick: () => void;
559
+ }) {
560
+ return (
561
+ <button
562
+ type="button"
563
+ onClick={onClick}
564
+ aria-pressed={isActive}
565
+ className={cn(
566
+ "flex flex-1 cursor-pointer items-center justify-center gap-1 px-1 text-[11px] font-medium transition-colors",
567
+ isActive
568
+ ? "bg-accent text-foreground"
569
+ : "text-muted-foreground/70 hover:bg-accent/50 hover:text-foreground",
570
+ )}
571
+ title={widthPx ? `${label} (${widthPx}px)` : label}
572
+ >
573
+ {widthPx != null && (
574
+ <BreakpointIcon widthPx={widthPx} className="shrink-0" />
575
+ )}
576
+ <span className="hidden sm:inline">{label}</span>
577
+ </button>
578
+ );
579
+ }