@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,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
|
+
}
|