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