@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
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useActionMutation,
|
|
3
|
+
useActionQuery,
|
|
4
|
+
useT,
|
|
5
|
+
} from "@agent-native/core/client";
|
|
2
6
|
import type { TweakDefinition } from "@shared/api";
|
|
3
7
|
import {
|
|
4
8
|
alphaToOpacity,
|
|
@@ -14,13 +18,15 @@ import {
|
|
|
14
18
|
IconAlignRight,
|
|
15
19
|
IconArrowAutofitHeight,
|
|
16
20
|
IconArrowAutofitWidth,
|
|
21
|
+
IconArrowRight,
|
|
17
22
|
IconBackground,
|
|
18
23
|
IconBlur,
|
|
19
24
|
IconBorderStyle,
|
|
20
25
|
IconBrush,
|
|
21
|
-
IconChevronDown,
|
|
22
26
|
IconCode,
|
|
23
27
|
IconComponents,
|
|
28
|
+
IconDeviceDesktop,
|
|
29
|
+
IconExternalLink,
|
|
24
30
|
IconDroplet,
|
|
25
31
|
IconEye,
|
|
26
32
|
IconEyeOff,
|
|
@@ -29,6 +35,8 @@ import {
|
|
|
29
35
|
IconFrame,
|
|
30
36
|
IconLayoutDistributeHorizontal,
|
|
31
37
|
IconLayoutGrid,
|
|
38
|
+
IconLoader2,
|
|
39
|
+
IconShieldCheck,
|
|
32
40
|
IconSlice,
|
|
33
41
|
IconLayoutAlignBottom,
|
|
34
42
|
IconLayoutAlignCenter,
|
|
@@ -52,6 +60,7 @@ import {
|
|
|
52
60
|
IconUnlink,
|
|
53
61
|
IconVector,
|
|
54
62
|
} from "@tabler/icons-react";
|
|
63
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
55
64
|
import {
|
|
56
65
|
useCallback,
|
|
57
66
|
useEffect,
|
|
@@ -59,8 +68,17 @@ import {
|
|
|
59
68
|
useState,
|
|
60
69
|
type ReactNode,
|
|
61
70
|
} from "react";
|
|
71
|
+
import { useParams } from "react-router";
|
|
62
72
|
|
|
63
73
|
import { Button } from "@/components/ui/button";
|
|
74
|
+
import {
|
|
75
|
+
Dialog,
|
|
76
|
+
DialogContent,
|
|
77
|
+
DialogDescription,
|
|
78
|
+
DialogFooter,
|
|
79
|
+
DialogHeader,
|
|
80
|
+
DialogTitle,
|
|
81
|
+
} from "@/components/ui/dialog";
|
|
64
82
|
import {
|
|
65
83
|
DropdownMenu,
|
|
66
84
|
DropdownMenuContent,
|
|
@@ -82,6 +100,7 @@ import {
|
|
|
82
100
|
SelectValue,
|
|
83
101
|
} from "@/components/ui/select";
|
|
84
102
|
import { Slider } from "@/components/ui/slider";
|
|
103
|
+
import { Switch } from "@/components/ui/switch";
|
|
85
104
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
86
105
|
import {
|
|
87
106
|
Tooltip,
|
|
@@ -117,11 +136,24 @@ import {
|
|
|
117
136
|
} from "./inspector";
|
|
118
137
|
import { IconLayoutSettings } from "./inspector/design-icons";
|
|
119
138
|
import type { DesignPaintType } from "./inspector/DesignColorPicker";
|
|
139
|
+
import { InspectorAiActions } from "./inspector/InspectorAiActions";
|
|
140
|
+
import { ReviewPanel } from "./ReviewPanel";
|
|
141
|
+
import type { ReviewPanelProps } from "./ReviewPanel";
|
|
142
|
+
import { StatesPanel } from "./StatesPanel";
|
|
143
|
+
import type { StatesPanelProps } from "./StatesPanel";
|
|
144
|
+
import { TokensPanel } from "./TokensPanel";
|
|
120
145
|
import { TweaksPanelContent } from "./TweaksPanel";
|
|
121
146
|
import type { ElementInfo } from "./types";
|
|
122
147
|
|
|
123
148
|
export type InspectorTab = "design" | "tweaks" | "extensions";
|
|
124
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Sub-tab within the Tweaks area: "tokens" = TokensPanel face (CSS-var
|
|
152
|
+
* editing through the Tweaks loop); "tweaks" = raw TweakDefinition controls.
|
|
153
|
+
* Defaults to "tokens" when a designId is provided, otherwise "tweaks".
|
|
154
|
+
*/
|
|
155
|
+
type TweaksSubTab = "tokens" | "tweaks";
|
|
156
|
+
|
|
125
157
|
interface EditPanelProps {
|
|
126
158
|
selectedElement: ElementInfo | null;
|
|
127
159
|
pageStyles?: Record<string, string>;
|
|
@@ -137,9 +169,84 @@ interface EditPanelProps {
|
|
|
137
169
|
onRequestTweaks?: (anchor: HTMLElement) => void;
|
|
138
170
|
onStyleChange: (property: string, value: string) => void;
|
|
139
171
|
onStylesChange?: (styles: Record<string, string>) => void;
|
|
172
|
+
onAutoLayoutConvert?: (
|
|
173
|
+
targetNodeId: string,
|
|
174
|
+
opts?: { direction?: "row" | "column"; gap?: string },
|
|
175
|
+
) => void;
|
|
140
176
|
onExport?: (settings: ExportSettingsValue[]) => void;
|
|
141
177
|
exporting?: boolean;
|
|
142
178
|
readOnly?: boolean;
|
|
179
|
+
/** Active file id — forwarded to InspectorAiActions for agent context. */
|
|
180
|
+
fileId?: string;
|
|
181
|
+
/** Active file name (e.g. "index.html") — forwarded to InspectorAiActions. */
|
|
182
|
+
filename?: string;
|
|
183
|
+
// -------------------------------------------------------------------------
|
|
184
|
+
// Design Studio panels (§6.2, §6.4, §6.5)
|
|
185
|
+
// Pass `designId` to unlock Tokens, States, and Review sections.
|
|
186
|
+
// -------------------------------------------------------------------------
|
|
187
|
+
/** The active design's id — required to mount Tokens / States / Review. */
|
|
188
|
+
designId?: string;
|
|
189
|
+
/**
|
|
190
|
+
* Called after a token edit is applied so the parent can push the resolved
|
|
191
|
+
* CSS-var map into the iframe via the tweak-values postMessage.
|
|
192
|
+
*/
|
|
193
|
+
onTokensApplied?: (resolvedCssVars: Record<string, string>) => void;
|
|
194
|
+
/** Props forwarded to the StatesPanel (§6.4). Requires `designId`. */
|
|
195
|
+
statesPanelProps?: Omit<StatesPanelProps, "designId">;
|
|
196
|
+
/** Props forwarded to the ReviewPanel (§6.5). */
|
|
197
|
+
reviewPanelProps?: Omit<ReviewPanelProps, "className">;
|
|
198
|
+
// -------------------------------------------------------------------------
|
|
199
|
+
// Component section (§6.1)
|
|
200
|
+
// When a component instance is selected, pass its node id here to unlock
|
|
201
|
+
// the contextual Component section at the top of the Design tab.
|
|
202
|
+
// -------------------------------------------------------------------------
|
|
203
|
+
/**
|
|
204
|
+
* The `data-agent-native-node-id` of the currently-selected component root
|
|
205
|
+
* element. When provided (along with `designId`), a Component section is
|
|
206
|
+
* shown at the top of the Design tab with name, source path, prop controls,
|
|
207
|
+
* and an Edit component action.
|
|
208
|
+
*/
|
|
209
|
+
componentNodeId?: string;
|
|
210
|
+
/**
|
|
211
|
+
* Source capabilities for the current design. Used to gate the Edit
|
|
212
|
+
* component / jump-to-source affordances. When absent all writes default
|
|
213
|
+
* to disabled (inline / Alpine tier behaviour).
|
|
214
|
+
*/
|
|
215
|
+
sourceCapabilities?: string[];
|
|
216
|
+
// -------------------------------------------------------------------------
|
|
217
|
+
// Selection header quick actions ("Create component" + "Inspect code")
|
|
218
|
+
// -------------------------------------------------------------------------
|
|
219
|
+
/**
|
|
220
|
+
* Promote the current selection into a reusable component. Receives the
|
|
221
|
+
* (already-normalized-by-the-action) component name the user typed. When
|
|
222
|
+
* omitted the "Create component" button is disabled.
|
|
223
|
+
*/
|
|
224
|
+
onCreateComponent?: (name: string) => void;
|
|
225
|
+
/** Suggested default name for the create-component dialog. */
|
|
226
|
+
defaultComponentName?: string;
|
|
227
|
+
/** Code-inspection data for the "Inspect code" popover. */
|
|
228
|
+
inspectCode?: InspectCodeData;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Data backing the "Inspect code" popover. The parent resolves the selected
|
|
233
|
+
* node's HTML and (for real-app sources) its source file location.
|
|
234
|
+
*/
|
|
235
|
+
export interface InspectCodeData {
|
|
236
|
+
/** Outer HTML of the selected element (inline / Alpine source). */
|
|
237
|
+
html?: string | null;
|
|
238
|
+
/**
|
|
239
|
+
* Resolved source file for real-app sources (localhost / fusion), when the
|
|
240
|
+
* resolveNodeToFile capability is available.
|
|
241
|
+
*/
|
|
242
|
+
sourceLocation?: {
|
|
243
|
+
/** Absolute path on disk — used to build the vscode:// deep link. */
|
|
244
|
+
absolutePath: string;
|
|
245
|
+
line?: number;
|
|
246
|
+
column?: number;
|
|
247
|
+
/** Optional snippet to show above the Open-in-VS-Code button. */
|
|
248
|
+
snippet?: string;
|
|
249
|
+
} | null;
|
|
143
250
|
}
|
|
144
251
|
|
|
145
252
|
/**
|
|
@@ -574,7 +681,7 @@ function ColorInput({
|
|
|
574
681
|
onImageFillChange={onImageFillChange}
|
|
575
682
|
blendMode={blendMode}
|
|
576
683
|
onBlendModeChange={onBlendModeChange}
|
|
577
|
-
showBlendMode={
|
|
684
|
+
showBlendMode={Boolean(onBlendModeChange)}
|
|
578
685
|
fillRows={fillRows}
|
|
579
686
|
selectedFillId={selectedFillId}
|
|
580
687
|
onFillSelect={supportsLayeredFills ? setSelectedFillId : undefined}
|
|
@@ -1851,6 +1958,325 @@ function commitElementSizing(
|
|
|
1851
1958
|
commitStylePatch(patch, onStyleChange, onStylesChange);
|
|
1852
1959
|
}
|
|
1853
1960
|
|
|
1961
|
+
/**
|
|
1962
|
+
* Small modal that prompts for a component name, then promotes the current
|
|
1963
|
+
* selection into a reusable component via `onSubmit`.
|
|
1964
|
+
*/
|
|
1965
|
+
function CreateComponentDialog({
|
|
1966
|
+
open,
|
|
1967
|
+
onOpenChange,
|
|
1968
|
+
defaultName,
|
|
1969
|
+
onSubmit,
|
|
1970
|
+
}: {
|
|
1971
|
+
open: boolean;
|
|
1972
|
+
onOpenChange: (open: boolean) => void;
|
|
1973
|
+
defaultName: string;
|
|
1974
|
+
onSubmit: (name: string) => void;
|
|
1975
|
+
}) {
|
|
1976
|
+
const [name, setName] = useState(defaultName);
|
|
1977
|
+
|
|
1978
|
+
// Reset the field to the freshest default each time the dialog opens.
|
|
1979
|
+
useEffect(() => {
|
|
1980
|
+
if (open) setName(defaultName);
|
|
1981
|
+
}, [open, defaultName]);
|
|
1982
|
+
|
|
1983
|
+
const commit = () => {
|
|
1984
|
+
const trimmed = name.trim();
|
|
1985
|
+
if (!trimmed) return;
|
|
1986
|
+
onSubmit(trimmed);
|
|
1987
|
+
onOpenChange(false);
|
|
1988
|
+
};
|
|
1989
|
+
|
|
1990
|
+
return (
|
|
1991
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
1992
|
+
<DialogContent className="sm:max-w-sm">
|
|
1993
|
+
<DialogHeader>
|
|
1994
|
+
<DialogTitle>
|
|
1995
|
+
{"Create component" /* i18n-ignore design inspector action */}
|
|
1996
|
+
</DialogTitle>
|
|
1997
|
+
<DialogDescription>
|
|
1998
|
+
{
|
|
1999
|
+
"Name this element so it becomes a reusable component. The agent can then extract props and replace repeated instances." /* i18n-ignore design inspector copy */
|
|
2000
|
+
}
|
|
2001
|
+
</DialogDescription>
|
|
2002
|
+
</DialogHeader>
|
|
2003
|
+
<div className="space-y-1.5">
|
|
2004
|
+
<Label
|
|
2005
|
+
htmlFor="create-component-name"
|
|
2006
|
+
className="text-[11px] font-medium text-muted-foreground"
|
|
2007
|
+
>
|
|
2008
|
+
{"Component name" /* i18n-ignore design inspector label */}
|
|
2009
|
+
</Label>
|
|
2010
|
+
<Input
|
|
2011
|
+
id="create-component-name"
|
|
2012
|
+
autoFocus
|
|
2013
|
+
value={name}
|
|
2014
|
+
onChange={(e) => setName(e.target.value)}
|
|
2015
|
+
onKeyDown={(e) => {
|
|
2016
|
+
if (e.key === "Enter") {
|
|
2017
|
+
e.preventDefault();
|
|
2018
|
+
commit();
|
|
2019
|
+
}
|
|
2020
|
+
}}
|
|
2021
|
+
placeholder={
|
|
2022
|
+
"PrimaryButton" /* i18n-ignore design inspector placeholder */
|
|
2023
|
+
}
|
|
2024
|
+
/>
|
|
2025
|
+
</div>
|
|
2026
|
+
<DialogFooter>
|
|
2027
|
+
<Button
|
|
2028
|
+
type="button"
|
|
2029
|
+
variant="ghost"
|
|
2030
|
+
onClick={() => onOpenChange(false)}
|
|
2031
|
+
>
|
|
2032
|
+
{"Cancel" /* i18n-ignore design inspector action */}
|
|
2033
|
+
</Button>
|
|
2034
|
+
<Button type="button" onClick={commit} disabled={!name.trim()}>
|
|
2035
|
+
{"Create" /* i18n-ignore design inspector action */}
|
|
2036
|
+
</Button>
|
|
2037
|
+
</DialogFooter>
|
|
2038
|
+
</DialogContent>
|
|
2039
|
+
</Dialog>
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/** Build a `vscode://file/...` deep link for an absolute path + position. */
|
|
2044
|
+
function vscodeDeepLink(
|
|
2045
|
+
absolutePath: string,
|
|
2046
|
+
line?: number,
|
|
2047
|
+
column?: number,
|
|
2048
|
+
): string {
|
|
2049
|
+
const base = `vscode://file/${absolutePath}`;
|
|
2050
|
+
if (line == null) return base;
|
|
2051
|
+
return column == null ? `${base}:${line}` : `${base}:${line}:${column}`;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* Extract the *opening tag* from an element's outer HTML for an at-a-glance
|
|
2056
|
+
* summary (e.g. `<main class="hero" data-x="y">`). Self-closing tags keep
|
|
2057
|
+
* their `/>`. Returns `null` when no tag can be parsed.
|
|
2058
|
+
*
|
|
2059
|
+
* Pure — exported for tests.
|
|
2060
|
+
*/
|
|
2061
|
+
export function openingTagOf(html: string | null | undefined): string | null {
|
|
2062
|
+
if (!html) return null;
|
|
2063
|
+
const trimmed = html.trimStart();
|
|
2064
|
+
// Match the first `<tag ...>` (greedy up to the first unquoted `>`), allowing
|
|
2065
|
+
// quoted attribute values to contain `>`.
|
|
2066
|
+
const match = /^<([a-zA-Z][\w-]*)((?:"[^"]*"|'[^']*'|[^>])*?)\/?>/.exec(
|
|
2067
|
+
trimmed,
|
|
2068
|
+
);
|
|
2069
|
+
if (!match) return null;
|
|
2070
|
+
return match[0];
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/**
|
|
2074
|
+
* Collapse long attribute values in an opening tag so the at-a-glance summary
|
|
2075
|
+
* stays readable. Values longer than `max` chars are truncated with an
|
|
2076
|
+
* ellipsis (the surrounding quotes are preserved).
|
|
2077
|
+
*
|
|
2078
|
+
* Pure — exported for tests.
|
|
2079
|
+
*/
|
|
2080
|
+
export function truncateOpeningTag(openTag: string, max = 32): string {
|
|
2081
|
+
return openTag.replace(
|
|
2082
|
+
/("|')((?:\\.|(?!\1)[^\\])*)\1/g,
|
|
2083
|
+
(full, quote, value) => {
|
|
2084
|
+
if (typeof value !== "string" || value.length <= max) return full;
|
|
2085
|
+
return `${quote}${value.slice(0, max - 1)}…${quote}`;
|
|
2086
|
+
},
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
/**
|
|
2091
|
+
* Parse the top-level `key: value` pairs from an Alpine `x-data` object literal
|
|
2092
|
+
* (e.g. `{ variant: 'outline', size: 'lg', disabled: false }`).
|
|
2093
|
+
*
|
|
2094
|
+
* Best-effort: only handles a flat object of simple string / boolean / number
|
|
2095
|
+
* literals — exactly the shape used for component variant + state props. Nested
|
|
2096
|
+
* objects, methods, and computed expressions are ignored. Returns `null` when
|
|
2097
|
+
* the value is not a recognizable flat object literal.
|
|
2098
|
+
*
|
|
2099
|
+
* Pure — exported for tests.
|
|
2100
|
+
*/
|
|
2101
|
+
export function parseAlpineDataObject(
|
|
2102
|
+
xData: string | null | undefined,
|
|
2103
|
+
): Record<string, string> | null {
|
|
2104
|
+
if (!xData) return null;
|
|
2105
|
+
const trimmed = xData.trim();
|
|
2106
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
|
2107
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
2108
|
+
if (!inner) return {};
|
|
2109
|
+
|
|
2110
|
+
const out: Record<string, string> = {};
|
|
2111
|
+
// Split on top-level commas only (no nesting / quotes inside values here).
|
|
2112
|
+
const pairRe =
|
|
2113
|
+
/(?:^|,)\s*(?:'([^']+)'|"([^"]+)"|([A-Za-z_$][\w$]*))\s*:\s*('[^']*'|"[^"]*"|true|false|-?\d+(?:\.\d+)?)/g;
|
|
2114
|
+
let m: RegExpExecArray | null;
|
|
2115
|
+
let matched = false;
|
|
2116
|
+
while ((m = pairRe.exec(inner)) !== null) {
|
|
2117
|
+
matched = true;
|
|
2118
|
+
const key = m[1] ?? m[2] ?? m[3];
|
|
2119
|
+
let raw = m[4]!;
|
|
2120
|
+
// Unwrap quotes for string literals; keep booleans / numbers verbatim.
|
|
2121
|
+
if (
|
|
2122
|
+
(raw.startsWith("'") && raw.endsWith("'")) ||
|
|
2123
|
+
(raw.startsWith('"') && raw.endsWith('"'))
|
|
2124
|
+
) {
|
|
2125
|
+
raw = raw.slice(1, -1);
|
|
2126
|
+
}
|
|
2127
|
+
if (key) out[key] = raw;
|
|
2128
|
+
}
|
|
2129
|
+
// If there was content but nothing parsed, the shape is too complex to edit
|
|
2130
|
+
// safely — bail so the caller falls back to attribute-based prop edits.
|
|
2131
|
+
if (!matched) return null;
|
|
2132
|
+
return out;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
/**
|
|
2136
|
+
* Re-serialize a flat Alpine data object back into an `x-data` literal,
|
|
2137
|
+
* preserving boolean / number literals unquoted and single-quoting strings.
|
|
2138
|
+
*
|
|
2139
|
+
* Pure — exported for tests.
|
|
2140
|
+
*/
|
|
2141
|
+
export function serializeAlpineDataObject(obj: Record<string, string>): string {
|
|
2142
|
+
const parts = Object.entries(obj).map(([key, value]) => {
|
|
2143
|
+
const isBoolean = value === "true" || value === "false";
|
|
2144
|
+
const isNumber = /^-?\d+(\.\d+)?$/.test(value);
|
|
2145
|
+
const literal =
|
|
2146
|
+
isBoolean || isNumber ? value : `'${value.replace(/'/g, "\\'")}'`;
|
|
2147
|
+
return `${key}: ${literal}`;
|
|
2148
|
+
});
|
|
2149
|
+
return parts.length ? `{ ${parts.join(", ")} }` : "{}";
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
/** A boolean-ish prop value (`"true"` / `"false"`), case-insensitive. */
|
|
2153
|
+
export function isBooleanPropValue(value: string): boolean {
|
|
2154
|
+
const v = value.trim().toLowerCase();
|
|
2155
|
+
return v === "true" || v === "false";
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
/**
|
|
2159
|
+
* Popover anchored to the "Inspect code" button showing the selected node's
|
|
2160
|
+
* code. Inline/Alpine sources show the element's outer HTML with a Copy
|
|
2161
|
+
* button; real-app sources additionally render an "Open in VS Code" button.
|
|
2162
|
+
*
|
|
2163
|
+
* TODO: replace the read-only <pre> with an inline Monaco editor for richer
|
|
2164
|
+
* (editable) code inspection once the editor bundle is wired into the inspector.
|
|
2165
|
+
*/
|
|
2166
|
+
function InspectCodePopover({
|
|
2167
|
+
trigger,
|
|
2168
|
+
data,
|
|
2169
|
+
}: {
|
|
2170
|
+
trigger: ReactNode;
|
|
2171
|
+
data: InspectCodeData;
|
|
2172
|
+
}) {
|
|
2173
|
+
const [copied, setCopied] = useState(false);
|
|
2174
|
+
const html = data.html ?? "";
|
|
2175
|
+
const source = data.sourceLocation ?? null;
|
|
2176
|
+
const snippet = source?.snippet || html;
|
|
2177
|
+
// At-a-glance opening tag (tag name + attributes), truncated for readability.
|
|
2178
|
+
const openingTag = (() => {
|
|
2179
|
+
const raw = openingTagOf(html);
|
|
2180
|
+
return raw ? truncateOpeningTag(raw) : null;
|
|
2181
|
+
})();
|
|
2182
|
+
|
|
2183
|
+
const handleCopy = () => {
|
|
2184
|
+
if (!snippet) return;
|
|
2185
|
+
void navigator.clipboard
|
|
2186
|
+
?.writeText(snippet)
|
|
2187
|
+
.then(() => {
|
|
2188
|
+
setCopied(true);
|
|
2189
|
+
window.setTimeout(() => setCopied(false), 1200);
|
|
2190
|
+
})
|
|
2191
|
+
.catch(() => {
|
|
2192
|
+
/* clipboard may be unavailable; ignore */
|
|
2193
|
+
});
|
|
2194
|
+
};
|
|
2195
|
+
|
|
2196
|
+
return (
|
|
2197
|
+
<Popover>
|
|
2198
|
+
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
2199
|
+
<PopoverContent align="end" className="w-80 space-y-2 p-2 text-[11px]">
|
|
2200
|
+
<div className="flex items-center justify-between gap-2">
|
|
2201
|
+
<span className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">
|
|
2202
|
+
{"Inspect code" /* i18n-ignore design inspector label */}
|
|
2203
|
+
</span>
|
|
2204
|
+
<Button
|
|
2205
|
+
type="button"
|
|
2206
|
+
variant="ghost"
|
|
2207
|
+
size="sm"
|
|
2208
|
+
className="h-6 px-2 text-[10px]"
|
|
2209
|
+
onClick={handleCopy}
|
|
2210
|
+
disabled={!snippet}
|
|
2211
|
+
>
|
|
2212
|
+
{
|
|
2213
|
+
copied
|
|
2214
|
+
? "Copied" /* i18n-ignore design inspector action */
|
|
2215
|
+
: "Copy" /* i18n-ignore design inspector action */
|
|
2216
|
+
}
|
|
2217
|
+
</Button>
|
|
2218
|
+
</div>
|
|
2219
|
+
|
|
2220
|
+
{/* At-a-glance opening tag (tag name + attributes) above the full HTML. */}
|
|
2221
|
+
{openingTag && (
|
|
2222
|
+
<code
|
|
2223
|
+
className="block truncate rounded bg-[var(--design-editor-control-bg)] px-2 py-1 font-mono text-[10px] text-foreground"
|
|
2224
|
+
title={openingTag}
|
|
2225
|
+
>
|
|
2226
|
+
{openingTag}
|
|
2227
|
+
</code>
|
|
2228
|
+
)}
|
|
2229
|
+
|
|
2230
|
+
{source && (
|
|
2231
|
+
<div
|
|
2232
|
+
className="flex items-center gap-1 rounded bg-[var(--design-editor-control-bg)] px-2 py-1"
|
|
2233
|
+
title={source.absolutePath}
|
|
2234
|
+
>
|
|
2235
|
+
<IconCode className="size-3 shrink-0 text-muted-foreground/60" />
|
|
2236
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground">
|
|
2237
|
+
{source.absolutePath}
|
|
2238
|
+
{source.line != null ? `:${source.line}` : ""}
|
|
2239
|
+
</span>
|
|
2240
|
+
</div>
|
|
2241
|
+
)}
|
|
2242
|
+
|
|
2243
|
+
{snippet ? (
|
|
2244
|
+
<pre className="max-h-64 overflow-auto rounded bg-[var(--design-editor-control-bg)] p-2 font-mono text-[10px] leading-relaxed text-foreground">
|
|
2245
|
+
<code>{snippet}</code>
|
|
2246
|
+
</pre>
|
|
2247
|
+
) : (
|
|
2248
|
+
<p className="px-1 py-2 text-muted-foreground">
|
|
2249
|
+
{
|
|
2250
|
+
"No source available for this element." /* i18n-ignore design inspector empty */
|
|
2251
|
+
}
|
|
2252
|
+
</p>
|
|
2253
|
+
)}
|
|
2254
|
+
|
|
2255
|
+
{source && (
|
|
2256
|
+
<a
|
|
2257
|
+
href={vscodeDeepLink(
|
|
2258
|
+
source.absolutePath,
|
|
2259
|
+
source.line,
|
|
2260
|
+
source.column,
|
|
2261
|
+
)}
|
|
2262
|
+
className="block"
|
|
2263
|
+
>
|
|
2264
|
+
<Button
|
|
2265
|
+
type="button"
|
|
2266
|
+
variant="outline"
|
|
2267
|
+
size="sm"
|
|
2268
|
+
className="h-7 w-full gap-1.5 text-[11px]"
|
|
2269
|
+
>
|
|
2270
|
+
<IconExternalLink className="size-3.5" />
|
|
2271
|
+
{"Open in VS Code" /* i18n-ignore design inspector action */}
|
|
2272
|
+
</Button>
|
|
2273
|
+
</a>
|
|
2274
|
+
)}
|
|
2275
|
+
</PopoverContent>
|
|
2276
|
+
</Popover>
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
|
|
1854
2280
|
function elementTypeIcon(element: ElementInfo) {
|
|
1855
2281
|
const tag = element.tagName || "element";
|
|
1856
2282
|
if (TEXT_TAGS.has(tag)) return IconTypography;
|
|
@@ -1860,35 +2286,55 @@ function elementTypeIcon(element: ElementInfo) {
|
|
|
1860
2286
|
return IconFrame;
|
|
1861
2287
|
}
|
|
1862
2288
|
|
|
1863
|
-
function SelectionHeader({
|
|
2289
|
+
function SelectionHeader({
|
|
2290
|
+
element,
|
|
2291
|
+
onCreateComponent,
|
|
2292
|
+
inspectCodePopover,
|
|
2293
|
+
}: {
|
|
2294
|
+
element: ElementInfo | null;
|
|
2295
|
+
/** Promote the current selection into a reusable component. Omit/undefined to disable. */
|
|
2296
|
+
onCreateComponent?: () => void;
|
|
2297
|
+
/**
|
|
2298
|
+
* Render prop for the "Inspect code" affordance. Receives the trigger button
|
|
2299
|
+
* so the parent can anchor a Popover to it; returns the full popover element.
|
|
2300
|
+
* When omitted the button renders disabled.
|
|
2301
|
+
*/
|
|
2302
|
+
inspectCodePopover?: (trigger: ReactNode) => ReactNode;
|
|
2303
|
+
}) {
|
|
1864
2304
|
if (!element) return null;
|
|
1865
2305
|
|
|
1866
2306
|
const title = inspectorObjectTitle(element);
|
|
1867
2307
|
const TypeIcon = elementTypeIcon(element);
|
|
1868
2308
|
|
|
2309
|
+
const inspectTrigger = (
|
|
2310
|
+
<SectionIconButton
|
|
2311
|
+
label={"Inspect code" /* i18n-ignore design inspector action */}
|
|
2312
|
+
disabled={!inspectCodePopover}
|
|
2313
|
+
>
|
|
2314
|
+
<IconCode className="size-3.5" />
|
|
2315
|
+
</SectionIconButton>
|
|
2316
|
+
);
|
|
2317
|
+
|
|
1869
2318
|
return (
|
|
1870
2319
|
<div className="flex min-h-8 shrink-0 items-center justify-between gap-2 border-b border-border/90 px-3">
|
|
1871
|
-
{/*
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
className="flex min-w-0 items-center gap-1.5 bg-transparent text-left text-[13px] font-semibold text-foreground"
|
|
1875
|
-
>
|
|
2320
|
+
{/* Node-type label. Rename lives in the layers panel and device sizing
|
|
2321
|
+
lives elsewhere, so this is a plain non-interactive label. */}
|
|
2322
|
+
<div className="flex min-w-0 items-center gap-1.5 text-left text-[13px] font-semibold text-foreground">
|
|
1876
2323
|
<TypeIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
|
1877
2324
|
<span className="truncate">{title}</span>
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
{/* M3 · Right-aligned quick actions: create-component + dev inspect (</>) */}
|
|
2325
|
+
</div>
|
|
2326
|
+
{/* Right-aligned quick actions: create-component + dev inspect (</>) */}
|
|
1881
2327
|
<div className="flex shrink-0 items-center gap-0.5">
|
|
1882
2328
|
<SectionIconButton
|
|
1883
2329
|
label={"Create component" /* i18n-ignore design inspector action */}
|
|
2330
|
+
disabled={!onCreateComponent}
|
|
2331
|
+
onClick={onCreateComponent}
|
|
1884
2332
|
>
|
|
1885
2333
|
<IconComponents className="size-3.5" />
|
|
1886
2334
|
</SectionIconButton>
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
<IconCode className="size-3.5" />
|
|
1891
|
-
</SectionIconButton>
|
|
2335
|
+
{inspectCodePopover
|
|
2336
|
+
? inspectCodePopover(inspectTrigger)
|
|
2337
|
+
: inspectTrigger}
|
|
1892
2338
|
</div>
|
|
1893
2339
|
</div>
|
|
1894
2340
|
);
|
|
@@ -2959,10 +3405,15 @@ function FlexContainerControls({
|
|
|
2959
3405
|
element,
|
|
2960
3406
|
onStyleChange,
|
|
2961
3407
|
onStylesChange,
|
|
3408
|
+
onAutoLayoutConvert,
|
|
2962
3409
|
}: {
|
|
2963
3410
|
element: ElementInfo;
|
|
2964
3411
|
onStyleChange: (property: string, value: string) => void;
|
|
2965
3412
|
onStylesChange?: (styles: Record<string, string>) => void;
|
|
3413
|
+
onAutoLayoutConvert?: (
|
|
3414
|
+
targetNodeId: string,
|
|
3415
|
+
opts?: { direction?: "row" | "column"; gap?: string },
|
|
3416
|
+
) => void;
|
|
2966
3417
|
}) {
|
|
2967
3418
|
const t = useT();
|
|
2968
3419
|
const styles = element.computedStyles;
|
|
@@ -2975,12 +3426,55 @@ function FlexContainerControls({
|
|
|
2975
3426
|
const isFlex = display.includes("flex");
|
|
2976
3427
|
const flexDirection: AutoLayoutMatrixValue["direction"] =
|
|
2977
3428
|
styles.flexDirection?.includes("column") ? "vertical" : "horizontal";
|
|
3429
|
+
const mainGapAxis =
|
|
3430
|
+
flexDirection === "horizontal" ? "horizontal" : "vertical";
|
|
2978
3431
|
// When the element is in normal flow (not flex yet), picking any flow option
|
|
2979
3432
|
// must first turn it into a flex container; otherwise setting flex-direction
|
|
2980
3433
|
// alone is a no-op against a block element.
|
|
2981
3434
|
const ensureFlex = () => {
|
|
2982
3435
|
if (!isFlex) onStyleChange("display", "flex");
|
|
2983
3436
|
};
|
|
3437
|
+
|
|
3438
|
+
/**
|
|
3439
|
+
* Handle the Flow control switching between flex and normal-flow (block).
|
|
3440
|
+
*
|
|
3441
|
+
* For 'flex': ensures display:flex is set (ensureFlex path).
|
|
3442
|
+
* For 'block': sets display:block and leaves children unchanged — mirrors
|
|
3443
|
+
* the { kind:"autoLayout", enabled:false } substrate intent exactly.
|
|
3444
|
+
*/
|
|
3445
|
+
const handleDisplayChange = (nextDisplay: "flex" | "block") => {
|
|
3446
|
+
if (nextDisplay === "flex") {
|
|
3447
|
+
ensureFlex();
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
// Turn auto-layout off: set display:block, leaving children unchanged.
|
|
3451
|
+
// This is the direct equivalent of the autoLayout substrate with enabled:false.
|
|
3452
|
+
onStyleChange("display", "block");
|
|
3453
|
+
};
|
|
3454
|
+
|
|
3455
|
+
/**
|
|
3456
|
+
* "Convert to auto layout" — enables flex on a non-flex container and strips
|
|
3457
|
+
* position:absolute from direct children (full reflow via the autoLayout
|
|
3458
|
+
* substrate intent). Falls back to a style-only patch when no substrate
|
|
3459
|
+
* handler is wired (e.g. read-only or non-editable contexts).
|
|
3460
|
+
*/
|
|
3461
|
+
const handleConvertToAutoLayout = () => {
|
|
3462
|
+
if (onAutoLayoutConvert && element.sourceId) {
|
|
3463
|
+
onAutoLayoutConvert(element.sourceId, { direction: "row", gap: "8px" });
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3466
|
+
// Fallback: set display:flex on the parent only (no child strip).
|
|
3467
|
+
commitStylePatch(
|
|
3468
|
+
{
|
|
3469
|
+
display: "flex",
|
|
3470
|
+
flexDirection: "row",
|
|
3471
|
+
gap: "8px",
|
|
3472
|
+
alignItems: "flex-start",
|
|
3473
|
+
},
|
|
3474
|
+
onStyleChange,
|
|
3475
|
+
onStylesChange,
|
|
3476
|
+
);
|
|
3477
|
+
};
|
|
2984
3478
|
const padding = {
|
|
2985
3479
|
top: parseNumericValue(styles.paddingTop || "0"),
|
|
2986
3480
|
right: parseNumericValue(styles.paddingRight || "0"),
|
|
@@ -3022,12 +3516,27 @@ function FlexContainerControls({
|
|
|
3022
3516
|
// optional contract field consumed by AutoLayoutMatrix; harmless when the
|
|
3023
3517
|
// matrix ignores it.
|
|
3024
3518
|
...({ display } as Partial<AutoLayoutMatrixValue>),
|
|
3519
|
+
spaceBetween: styles.justifyContent === "space-between",
|
|
3025
3520
|
};
|
|
3026
3521
|
|
|
3027
3522
|
return (
|
|
3028
3523
|
<div className="space-y-2">
|
|
3524
|
+
{/* Convert to auto layout — shown when the container is not yet flex */}
|
|
3525
|
+
{!isFlex ? (
|
|
3526
|
+
<Button
|
|
3527
|
+
type="button"
|
|
3528
|
+
variant="outline"
|
|
3529
|
+
size="sm"
|
|
3530
|
+
className="h-7 w-full gap-1.5 text-[11px]"
|
|
3531
|
+
onClick={handleConvertToAutoLayout}
|
|
3532
|
+
>
|
|
3533
|
+
<IconLayoutSettings className="size-3.5 shrink-0" />
|
|
3534
|
+
{"Convert to auto layout" /* i18n-ignore design inspector action */}
|
|
3535
|
+
</Button>
|
|
3536
|
+
) : null}
|
|
3029
3537
|
<AutoLayoutMatrix
|
|
3030
3538
|
value={autoLayoutValue}
|
|
3539
|
+
onDisplayChange={handleDisplayChange}
|
|
3031
3540
|
onDirectionChange={(direction) => {
|
|
3032
3541
|
ensureFlex();
|
|
3033
3542
|
onStyleChange(
|
|
@@ -3079,16 +3588,20 @@ function FlexContainerControls({
|
|
|
3079
3588
|
onStyleChange("overflow", clipContent ? "hidden" : "visible")
|
|
3080
3589
|
}
|
|
3081
3590
|
onDistribute={(axis) => {
|
|
3082
|
-
|
|
3083
|
-
autoLayoutValue.direction === "horizontal"
|
|
3084
|
-
? "horizontal"
|
|
3085
|
-
: "vertical";
|
|
3086
|
-
if (axis === mainAxis) {
|
|
3591
|
+
if (axis === mainGapAxis) {
|
|
3087
3592
|
onStyleChange("justifyContent", "space-between");
|
|
3088
3593
|
} else if (autoLayoutValue.wrap === "wrap") {
|
|
3089
3594
|
onStyleChange("alignContent", "space-between");
|
|
3090
3595
|
}
|
|
3091
3596
|
}}
|
|
3597
|
+
onGapModeChange={(gapMode, axis) => {
|
|
3598
|
+
if (axis !== mainGapAxis) return;
|
|
3599
|
+
ensureFlex();
|
|
3600
|
+
onStyleChange(
|
|
3601
|
+
"justifyContent",
|
|
3602
|
+
gapMode === "auto" ? "space-between" : "flex-start",
|
|
3603
|
+
);
|
|
3604
|
+
}}
|
|
3092
3605
|
availableChildSizing={availableSizingForElement(element)}
|
|
3093
3606
|
onChildSizingChange={(axis, sizing) => {
|
|
3094
3607
|
commitElementSizing(
|
|
@@ -3207,10 +3720,15 @@ function LayoutContextProperties({
|
|
|
3207
3720
|
element,
|
|
3208
3721
|
onStyleChange,
|
|
3209
3722
|
onStylesChange,
|
|
3723
|
+
onAutoLayoutConvert,
|
|
3210
3724
|
}: {
|
|
3211
3725
|
element: ElementInfo;
|
|
3212
3726
|
onStyleChange: (property: string, value: string) => void;
|
|
3213
3727
|
onStylesChange?: (styles: Record<string, string>) => void;
|
|
3728
|
+
onAutoLayoutConvert?: (
|
|
3729
|
+
targetNodeId: string,
|
|
3730
|
+
opts?: { direction?: "row" | "column"; gap?: string },
|
|
3731
|
+
) => void;
|
|
3214
3732
|
}) {
|
|
3215
3733
|
const t = useT();
|
|
3216
3734
|
const flexChild = isParentFlex(element);
|
|
@@ -3303,6 +3821,7 @@ function LayoutContextProperties({
|
|
|
3303
3821
|
element={element}
|
|
3304
3822
|
onStyleChange={onStyleChange}
|
|
3305
3823
|
onStylesChange={onStylesChange}
|
|
3824
|
+
onAutoLayoutConvert={onAutoLayoutConvert}
|
|
3306
3825
|
/>
|
|
3307
3826
|
{childControls}
|
|
3308
3827
|
</PanelSection>
|
|
@@ -4650,6 +5169,580 @@ const TEXT_TAGS = new Set([
|
|
|
4650
5169
|
"li",
|
|
4651
5170
|
]);
|
|
4652
5171
|
|
|
5172
|
+
// ─── Make it real — inline upgrade card (§3, §6.6) ──────────────────────────
|
|
5173
|
+
|
|
5174
|
+
/**
|
|
5175
|
+
* Payload shape returned by `connect-builder-app`. Only the fields used by
|
|
5176
|
+
* the card UI are typed here; the action may return additional fields.
|
|
5177
|
+
*/
|
|
5178
|
+
interface ConnectBuilderAppResult {
|
|
5179
|
+
connected: boolean;
|
|
5180
|
+
builderEnabled: boolean;
|
|
5181
|
+
connectUrl: string;
|
|
5182
|
+
appHost: string;
|
|
5183
|
+
branchProjectId?: string;
|
|
5184
|
+
cta: {
|
|
5185
|
+
kind: "connect-builder" | "configure-project";
|
|
5186
|
+
label: string;
|
|
5187
|
+
description: string;
|
|
5188
|
+
primaryAction: string;
|
|
5189
|
+
connectUrl: string;
|
|
5190
|
+
} | null;
|
|
5191
|
+
message: string;
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
/**
|
|
5195
|
+
* Inline "Make it real" upgrade card.
|
|
5196
|
+
*
|
|
5197
|
+
* Rendered wherever a real-app-only control is reached on an inline design
|
|
5198
|
+
* (Component source jump, token write-back, live captures, etc.). Queries
|
|
5199
|
+
* `connect-builder-app` to determine the current connection state, then
|
|
5200
|
+
* offers the appropriate CTA:
|
|
5201
|
+
*
|
|
5202
|
+
* - Not connected → "Connect Builder.io" button (opens connectUrl)
|
|
5203
|
+
* - Connected, no project → "Open Builder settings" (configure project ID)
|
|
5204
|
+
* - Fully enabled → "Make it real" button (calls migrate-inline-design-to-app)
|
|
5205
|
+
*
|
|
5206
|
+
* The card is progressively disclosed: it only mounts when a gated control is
|
|
5207
|
+
* actually reached, so it never appears for users who are already on a real-app
|
|
5208
|
+
* source (`localhost` / `fusion`) or whose `sourceCapabilities` already include
|
|
5209
|
+
* the needed capability.
|
|
5210
|
+
*
|
|
5211
|
+
* Matches the design-editor panel chrome: dashed-border, accent tint, small
|
|
5212
|
+
* text at 10px — same idiom as the existing `ctaRequired` block in
|
|
5213
|
+
* ComponentSection.
|
|
5214
|
+
*/
|
|
5215
|
+
function MakeItRealCard({
|
|
5216
|
+
designId,
|
|
5217
|
+
featureLabel,
|
|
5218
|
+
}: {
|
|
5219
|
+
/** The active design id — required to call connect-builder-app. */
|
|
5220
|
+
designId: string;
|
|
5221
|
+
/**
|
|
5222
|
+
* Short human-readable label for the gated feature (e.g. "token write-back",
|
|
5223
|
+
* "component source jump", "live captures"). Shown in the card body so the
|
|
5224
|
+
* user understands exactly what they're unlocking.
|
|
5225
|
+
*/
|
|
5226
|
+
featureLabel: string;
|
|
5227
|
+
}) {
|
|
5228
|
+
const { data, isLoading } = useActionQuery<ConnectBuilderAppResult>(
|
|
5229
|
+
"connect-builder-app",
|
|
5230
|
+
{ designId },
|
|
5231
|
+
);
|
|
5232
|
+
|
|
5233
|
+
const migrateMutation = useActionMutation("migrate-inline-design-to-app");
|
|
5234
|
+
|
|
5235
|
+
// While fetching status, show a muted placeholder that matches the card
|
|
5236
|
+
// height so the inspector doesn't jump when the data arrives.
|
|
5237
|
+
if (isLoading || !data) {
|
|
5238
|
+
return (
|
|
5239
|
+
<div className="rounded-md border border-dashed border-[var(--design-editor-accent-color)]/20 bg-[var(--design-editor-accent-color)]/3 px-2.5 py-2">
|
|
5240
|
+
<div className="h-3 w-32 animate-pulse rounded bg-muted/40" />
|
|
5241
|
+
</div>
|
|
5242
|
+
);
|
|
5243
|
+
}
|
|
5244
|
+
|
|
5245
|
+
// Determine which CTA to show.
|
|
5246
|
+
const cta = data.cta;
|
|
5247
|
+
|
|
5248
|
+
// Already fully enabled — no CTA needed (caller should already have gated
|
|
5249
|
+
// this component away, but guard here for safety).
|
|
5250
|
+
if (!cta) return null;
|
|
5251
|
+
|
|
5252
|
+
const isPending = migrateMutation.isPending;
|
|
5253
|
+
const migrateError = migrateMutation.error;
|
|
5254
|
+
|
|
5255
|
+
// "Make it real" primary action: open the connect URL or migrate.
|
|
5256
|
+
const handlePrimary = () => {
|
|
5257
|
+
if (cta.kind === "connect-builder") {
|
|
5258
|
+
// Open the Builder OAuth connect flow in a new tab. The user completes
|
|
5259
|
+
// it there and comes back; the card will re-query on next render.
|
|
5260
|
+
window.open(cta.connectUrl, "_blank", "noopener,noreferrer");
|
|
5261
|
+
return;
|
|
5262
|
+
}
|
|
5263
|
+
if (cta.kind === "configure-project") {
|
|
5264
|
+
window.open(cta.connectUrl, "_blank", "noopener,noreferrer");
|
|
5265
|
+
return;
|
|
5266
|
+
}
|
|
5267
|
+
};
|
|
5268
|
+
|
|
5269
|
+
const handleMigrate = () => {
|
|
5270
|
+
migrateMutation.mutate({ designId });
|
|
5271
|
+
};
|
|
5272
|
+
|
|
5273
|
+
// Migration result — show branch link.
|
|
5274
|
+
const migrateResult = migrateMutation.data as
|
|
5275
|
+
| {
|
|
5276
|
+
status: "processing";
|
|
5277
|
+
branchName?: string;
|
|
5278
|
+
url?: string;
|
|
5279
|
+
message?: string;
|
|
5280
|
+
}
|
|
5281
|
+
| undefined;
|
|
5282
|
+
|
|
5283
|
+
if (migrateResult?.status === "processing" && migrateResult.url) {
|
|
5284
|
+
return (
|
|
5285
|
+
<div className="rounded-md border border-[var(--design-editor-accent-color)]/30 bg-[var(--design-editor-accent-color)]/8 px-2.5 py-2 space-y-1.5">
|
|
5286
|
+
<p className="text-[10px] font-semibold text-[var(--design-editor-accent-color)]">
|
|
5287
|
+
{"Builder is working on it" /* i18n-ignore make-it-real card */}
|
|
5288
|
+
</p>
|
|
5289
|
+
<p className="text-[10px] leading-snug text-muted-foreground">
|
|
5290
|
+
{migrateResult.message ??
|
|
5291
|
+
`Branch "${migrateResult.branchName}" is being generated.`}
|
|
5292
|
+
</p>
|
|
5293
|
+
<a
|
|
5294
|
+
href={migrateResult.url}
|
|
5295
|
+
target="_blank"
|
|
5296
|
+
rel="noopener noreferrer"
|
|
5297
|
+
className="inline-flex items-center gap-1 text-[10px] font-medium text-[var(--design-editor-accent-color)] hover:underline"
|
|
5298
|
+
>
|
|
5299
|
+
{"Open in Builder" /* i18n-ignore make-it-real card */}
|
|
5300
|
+
<IconExternalLink className="size-2.5" />
|
|
5301
|
+
</a>
|
|
5302
|
+
</div>
|
|
5303
|
+
);
|
|
5304
|
+
}
|
|
5305
|
+
|
|
5306
|
+
return (
|
|
5307
|
+
<div className="rounded-md border border-dashed border-[var(--design-editor-accent-color)]/30 bg-[var(--design-editor-accent-color)]/5 px-2.5 py-2 space-y-1.5">
|
|
5308
|
+
{/* Header */}
|
|
5309
|
+
<p className="text-[10px] font-semibold text-foreground">
|
|
5310
|
+
{"Make this a real app" /* i18n-ignore make-it-real card */}
|
|
5311
|
+
</p>
|
|
5312
|
+
|
|
5313
|
+
{/* Body */}
|
|
5314
|
+
<p className="text-[10px] leading-snug text-muted-foreground">
|
|
5315
|
+
{cta.description}
|
|
5316
|
+
{featureLabel ? (
|
|
5317
|
+
<>
|
|
5318
|
+
{" "}
|
|
5319
|
+
<span className="italic">{featureLabel}</span>
|
|
5320
|
+
{" requires a real-app source."}
|
|
5321
|
+
</>
|
|
5322
|
+
) : null}
|
|
5323
|
+
</p>
|
|
5324
|
+
|
|
5325
|
+
{/* Error */}
|
|
5326
|
+
{migrateError ? (
|
|
5327
|
+
<p className="text-[10px] text-destructive">
|
|
5328
|
+
{migrateError instanceof Error
|
|
5329
|
+
? migrateError.message
|
|
5330
|
+
: "Migration failed. Please try again."}
|
|
5331
|
+
</p>
|
|
5332
|
+
) : null}
|
|
5333
|
+
|
|
5334
|
+
{/* Actions */}
|
|
5335
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
5336
|
+
<button
|
|
5337
|
+
type="button"
|
|
5338
|
+
onClick={handlePrimary}
|
|
5339
|
+
className="inline-flex items-center gap-1 rounded px-2 py-1 text-[10px] font-medium bg-[var(--design-editor-accent-color)] text-white hover:opacity-90 transition-opacity"
|
|
5340
|
+
>
|
|
5341
|
+
{cta.primaryAction}
|
|
5342
|
+
<IconArrowRight className="size-2.5" />
|
|
5343
|
+
</button>
|
|
5344
|
+
|
|
5345
|
+
{/* When Builder is fully connected, also offer direct migration */}
|
|
5346
|
+
{data.connected && data.builderEnabled && (
|
|
5347
|
+
<button
|
|
5348
|
+
type="button"
|
|
5349
|
+
onClick={handleMigrate}
|
|
5350
|
+
disabled={isPending}
|
|
5351
|
+
className="inline-flex items-center gap-1 rounded border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 py-1 text-[10px] font-medium text-muted-foreground hover:text-foreground transition-colors disabled:cursor-wait disabled:opacity-60"
|
|
5352
|
+
>
|
|
5353
|
+
{isPending ? (
|
|
5354
|
+
<>
|
|
5355
|
+
<IconLoader2 className="size-2.5 animate-spin" />
|
|
5356
|
+
{"Migrating…" /* i18n-ignore make-it-real card */}
|
|
5357
|
+
</>
|
|
5358
|
+
) : (
|
|
5359
|
+
<>{"Generate React app" /* i18n-ignore make-it-real card */}</>
|
|
5360
|
+
)}
|
|
5361
|
+
</button>
|
|
5362
|
+
)}
|
|
5363
|
+
</div>
|
|
5364
|
+
</div>
|
|
5365
|
+
);
|
|
5366
|
+
}
|
|
5367
|
+
|
|
5368
|
+
// ─── Component section (§6.1) ─────────────────────────────────────────────────
|
|
5369
|
+
|
|
5370
|
+
/**
|
|
5371
|
+
* Shape returned by `get-component-details`. Only the fields the UI needs are
|
|
5372
|
+
* typed here; the action may return additional fields.
|
|
5373
|
+
*/
|
|
5374
|
+
interface ComponentDetailsResult {
|
|
5375
|
+
name: string;
|
|
5376
|
+
sourceType: string;
|
|
5377
|
+
observedProps: Array<{ name: string; value: string }>;
|
|
5378
|
+
persistedVariants: Record<string, string[]>;
|
|
5379
|
+
sourceLocation?: { filePath: string; exportName?: string } | null;
|
|
5380
|
+
/** Component instance shape, including the Alpine `x-data` expression. */
|
|
5381
|
+
instance?: {
|
|
5382
|
+
alpineData?: string | null;
|
|
5383
|
+
nodeId?: string;
|
|
5384
|
+
selector?: string;
|
|
5385
|
+
} | null;
|
|
5386
|
+
capabilities: {
|
|
5387
|
+
canResolveToFile: boolean;
|
|
5388
|
+
hasFullIndex: boolean;
|
|
5389
|
+
canEditProps: boolean;
|
|
5390
|
+
ctaRequired: boolean;
|
|
5391
|
+
ctaMessage?: string;
|
|
5392
|
+
};
|
|
5393
|
+
}
|
|
5394
|
+
|
|
5395
|
+
/**
|
|
5396
|
+
* Contextual COMPONENT section rendered inside the Design tab when the
|
|
5397
|
+
* selected element is a component instance (carries
|
|
5398
|
+
* `data-agent-native-component`).
|
|
5399
|
+
*
|
|
5400
|
+
* Shows: component name, source path (when capability available), observed
|
|
5401
|
+
* prop values, variant/size/state controls from `get-component-details`, and
|
|
5402
|
+
* an "Edit component source" action. Real-app features are gated by the
|
|
5403
|
+
* capabilities returned by the action; Alpine gets a lightweight read-only
|
|
5404
|
+
* view plus a Connect-Builder CTA.
|
|
5405
|
+
*
|
|
5406
|
+
* Matches the workbench artboard spec in DESIGN-STUDIO-PLAN.md §6.1.
|
|
5407
|
+
*/
|
|
5408
|
+
function ComponentSection({
|
|
5409
|
+
designId,
|
|
5410
|
+
nodeId,
|
|
5411
|
+
sourceCapabilities = [],
|
|
5412
|
+
}: {
|
|
5413
|
+
designId: string;
|
|
5414
|
+
nodeId: string;
|
|
5415
|
+
/** Capability names advertised by the current source. */
|
|
5416
|
+
sourceCapabilities?: string[];
|
|
5417
|
+
}) {
|
|
5418
|
+
const queryClient = useQueryClient();
|
|
5419
|
+
const detailsKey = ["action", "get-component-details", { designId, nodeId }];
|
|
5420
|
+
|
|
5421
|
+
const { data, isLoading, error } = useActionQuery<ComponentDetailsResult>(
|
|
5422
|
+
"get-component-details",
|
|
5423
|
+
{ designId, nodeId },
|
|
5424
|
+
);
|
|
5425
|
+
|
|
5426
|
+
const openSourceMutation = useActionMutation("open-component-source");
|
|
5427
|
+
const applyPropMutation = useActionMutation("apply-component-prop-edit");
|
|
5428
|
+
|
|
5429
|
+
// Persist a single prop change through apply-component-prop-edit. The canvas
|
|
5430
|
+
// re-renders from the freshly written design content (get-design refetch),
|
|
5431
|
+
// so no postMessage into the iframe is needed. The get-component-details
|
|
5432
|
+
// cache is optimistically patched so the control reflects the new value
|
|
5433
|
+
// immediately, then reconciled with the server on settle.
|
|
5434
|
+
const persistPropEdit = (
|
|
5435
|
+
edit:
|
|
5436
|
+
| { kind: "alpineData"; value: string }
|
|
5437
|
+
| { kind: "attribute"; attribute: string; value: string },
|
|
5438
|
+
optimistic: (prev: ComponentDetailsResult) => ComponentDetailsResult,
|
|
5439
|
+
) => {
|
|
5440
|
+
queryClient.setQueryData<ComponentDetailsResult>(detailsKey, (prev) =>
|
|
5441
|
+
prev ? optimistic(prev) : prev,
|
|
5442
|
+
);
|
|
5443
|
+
applyPropMutation.mutate(
|
|
5444
|
+
{ designId, nodeId, edit },
|
|
5445
|
+
{
|
|
5446
|
+
onSettled: () => {
|
|
5447
|
+
// Re-render the canvas from the new content and re-read the props.
|
|
5448
|
+
void queryClient.invalidateQueries({
|
|
5449
|
+
queryKey: ["action", "get-design"],
|
|
5450
|
+
});
|
|
5451
|
+
void queryClient.invalidateQueries({ queryKey: detailsKey });
|
|
5452
|
+
},
|
|
5453
|
+
},
|
|
5454
|
+
);
|
|
5455
|
+
};
|
|
5456
|
+
|
|
5457
|
+
// While loading, show a compact skeleton that matches the section width.
|
|
5458
|
+
if (isLoading) {
|
|
5459
|
+
return (
|
|
5460
|
+
<section className="shrink-0 border-t border-[var(--design-editor-control-border)] first:border-t-0">
|
|
5461
|
+
<div className="flex min-h-9 items-center gap-2 px-3">
|
|
5462
|
+
<div className="h-3 w-24 animate-pulse rounded bg-muted/50" />
|
|
5463
|
+
</div>
|
|
5464
|
+
<div className="space-y-1.5 px-3 pb-3 pt-0.5">
|
|
5465
|
+
<div className="h-5 w-full animate-pulse rounded bg-muted/40" />
|
|
5466
|
+
<div className="h-5 w-3/4 animate-pulse rounded bg-muted/40" />
|
|
5467
|
+
</div>
|
|
5468
|
+
</section>
|
|
5469
|
+
);
|
|
5470
|
+
}
|
|
5471
|
+
|
|
5472
|
+
// Hard error (node not found, no access, etc.) — collapse silently so
|
|
5473
|
+
// the rest of the inspector is not disrupted.
|
|
5474
|
+
if (error || !data) return null;
|
|
5475
|
+
|
|
5476
|
+
const {
|
|
5477
|
+
name,
|
|
5478
|
+
sourceType,
|
|
5479
|
+
sourceLocation,
|
|
5480
|
+
observedProps,
|
|
5481
|
+
persistedVariants,
|
|
5482
|
+
instance,
|
|
5483
|
+
capabilities,
|
|
5484
|
+
} = data;
|
|
5485
|
+
|
|
5486
|
+
// ── Editable prop model ───────────────────────────────────────────────────
|
|
5487
|
+
// Inline/Alpine designs persist through apply-component-prop-edit. Two write
|
|
5488
|
+
// surfaces:
|
|
5489
|
+
// • x-data keys → kind "alpineData" (rewrites the whole object)
|
|
5490
|
+
// • data-prop-* attrs → kind "attribute" (data-agent-native-prop-<kebab>)
|
|
5491
|
+
// Real-app sources keep the deeper source-prop controls gated as-is, so for
|
|
5492
|
+
// non-inline sources the controls are read-only here.
|
|
5493
|
+
const isInline = sourceType === "inline";
|
|
5494
|
+
const editingEnabled = isInline; // gated; real-app stays read-only for now
|
|
5495
|
+
const alpineData = parseAlpineDataObject(instance?.alpineData);
|
|
5496
|
+
|
|
5497
|
+
// Each editable row: name + current value + how it persists + its options.
|
|
5498
|
+
type PropRow = {
|
|
5499
|
+
name: string;
|
|
5500
|
+
value: string;
|
|
5501
|
+
/** Variant/enum options when the prop is a known group. */
|
|
5502
|
+
options?: string[];
|
|
5503
|
+
/** Persist surface for this prop. */
|
|
5504
|
+
surface: "alpineData" | "attribute";
|
|
5505
|
+
};
|
|
5506
|
+
|
|
5507
|
+
const rows: PropRow[] = [];
|
|
5508
|
+
const seen = new Set<string>();
|
|
5509
|
+
|
|
5510
|
+
// 1) Alpine x-data keys come first — they drive the live variant/state.
|
|
5511
|
+
if (alpineData) {
|
|
5512
|
+
for (const [key, value] of Object.entries(alpineData)) {
|
|
5513
|
+
rows.push({
|
|
5514
|
+
name: key,
|
|
5515
|
+
value,
|
|
5516
|
+
options: persistedVariants[key],
|
|
5517
|
+
surface: "alpineData",
|
|
5518
|
+
});
|
|
5519
|
+
seen.add(key);
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5523
|
+
// 2) data-agent-native-prop-* attributes not already covered by x-data.
|
|
5524
|
+
for (const prop of observedProps) {
|
|
5525
|
+
if (seen.has(prop.name)) continue;
|
|
5526
|
+
rows.push({
|
|
5527
|
+
name: prop.name,
|
|
5528
|
+
value: prop.value,
|
|
5529
|
+
options: persistedVariants[prop.name],
|
|
5530
|
+
surface: "attribute",
|
|
5531
|
+
});
|
|
5532
|
+
seen.add(prop.name);
|
|
5533
|
+
}
|
|
5534
|
+
|
|
5535
|
+
// 3) persistedVariant groups with no observed value yet (default to first).
|
|
5536
|
+
for (const [group, options] of Object.entries(persistedVariants)) {
|
|
5537
|
+
if (seen.has(group)) continue;
|
|
5538
|
+
rows.push({
|
|
5539
|
+
name: group,
|
|
5540
|
+
value: options[0] ?? "",
|
|
5541
|
+
options,
|
|
5542
|
+
surface: alpineData ? "alpineData" : "attribute",
|
|
5543
|
+
});
|
|
5544
|
+
seen.add(group);
|
|
5545
|
+
}
|
|
5546
|
+
|
|
5547
|
+
const hasRows = rows.length > 0;
|
|
5548
|
+
|
|
5549
|
+
// Build the apply-component-prop-edit payload + optimistic cache patch for a
|
|
5550
|
+
// single prop change.
|
|
5551
|
+
const commitProp = (row: PropRow, nextValue: string) => {
|
|
5552
|
+
if (!editingEnabled || nextValue === row.value) return;
|
|
5553
|
+
|
|
5554
|
+
if (row.surface === "alpineData") {
|
|
5555
|
+
const nextData = { ...(alpineData ?? {}), [row.name]: nextValue };
|
|
5556
|
+
const serialized = serializeAlpineDataObject(nextData);
|
|
5557
|
+
persistPropEdit({ kind: "alpineData", value: serialized }, (prev) => ({
|
|
5558
|
+
...prev,
|
|
5559
|
+
instance: { ...(prev.instance ?? {}), alpineData: serialized },
|
|
5560
|
+
observedProps: prev.observedProps.map((p) =>
|
|
5561
|
+
p.name === row.name ? { ...p, value: nextValue } : p,
|
|
5562
|
+
),
|
|
5563
|
+
}));
|
|
5564
|
+
} else {
|
|
5565
|
+
// camelCase prop name → data-agent-native-prop-<kebab>
|
|
5566
|
+
const kebab = row.name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
5567
|
+
persistPropEdit(
|
|
5568
|
+
{
|
|
5569
|
+
kind: "attribute",
|
|
5570
|
+
attribute: `data-agent-native-prop-${kebab}`,
|
|
5571
|
+
value: nextValue,
|
|
5572
|
+
},
|
|
5573
|
+
(prev) => {
|
|
5574
|
+
const exists = prev.observedProps.some((p) => p.name === row.name);
|
|
5575
|
+
return {
|
|
5576
|
+
...prev,
|
|
5577
|
+
observedProps: exists
|
|
5578
|
+
? prev.observedProps.map((p) =>
|
|
5579
|
+
p.name === row.name ? { ...p, value: nextValue } : p,
|
|
5580
|
+
)
|
|
5581
|
+
: [...prev.observedProps, { name: row.name, value: nextValue }],
|
|
5582
|
+
};
|
|
5583
|
+
},
|
|
5584
|
+
);
|
|
5585
|
+
}
|
|
5586
|
+
};
|
|
5587
|
+
|
|
5588
|
+
// ── Capability gates ──
|
|
5589
|
+
const canJumpToSource =
|
|
5590
|
+
capabilities.canResolveToFile &&
|
|
5591
|
+
Boolean(sourceLocation?.filePath) &&
|
|
5592
|
+
sourceCapabilities.includes("resolveNodeToFile");
|
|
5593
|
+
|
|
5594
|
+
// ── Source chip text ──
|
|
5595
|
+
const sourceChip = sourceLocation?.exportName
|
|
5596
|
+
? `${sourceLocation.exportName} — ${sourceLocation.filePath}`
|
|
5597
|
+
: (sourceLocation?.filePath ?? null);
|
|
5598
|
+
|
|
5599
|
+
return (
|
|
5600
|
+
<section
|
|
5601
|
+
className="shrink-0 border-t border-[var(--design-editor-control-border)] first:border-t-0"
|
|
5602
|
+
data-testid="component-section"
|
|
5603
|
+
>
|
|
5604
|
+
{/* ── Section header ── */}
|
|
5605
|
+
<div className="flex min-h-9 items-center gap-2 px-3">
|
|
5606
|
+
{/* Accent diamond matching the workbench artboard component rows */}
|
|
5607
|
+
<span
|
|
5608
|
+
className="size-2 shrink-0 rotate-45 rounded-[2px] bg-[var(--design-editor-accent-color)]"
|
|
5609
|
+
aria-hidden="true"
|
|
5610
|
+
/>
|
|
5611
|
+
<h3 className="min-w-0 flex-1 truncate text-[11px] font-semibold text-foreground">
|
|
5612
|
+
{name}
|
|
5613
|
+
</h3>
|
|
5614
|
+
{/* Jump-to-source action */}
|
|
5615
|
+
<Tooltip>
|
|
5616
|
+
<TooltipTrigger asChild>
|
|
5617
|
+
<Button
|
|
5618
|
+
type="button"
|
|
5619
|
+
variant="ghost"
|
|
5620
|
+
size="icon"
|
|
5621
|
+
className="size-6 rounded-md text-muted-foreground hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
|
|
5622
|
+
disabled={!canJumpToSource}
|
|
5623
|
+
aria-label={
|
|
5624
|
+
"Edit component source" /* i18n-ignore design inspector action */
|
|
5625
|
+
}
|
|
5626
|
+
onClick={() => {
|
|
5627
|
+
openSourceMutation.mutate({ designId, nodeId });
|
|
5628
|
+
}}
|
|
5629
|
+
>
|
|
5630
|
+
<IconExternalLink className="size-3.5" />
|
|
5631
|
+
</Button>
|
|
5632
|
+
</TooltipTrigger>
|
|
5633
|
+
<TooltipContent>
|
|
5634
|
+
{
|
|
5635
|
+
canJumpToSource
|
|
5636
|
+
? "Edit component source" /* i18n-ignore design inspector action */
|
|
5637
|
+
: (capabilities.ctaMessage ??
|
|
5638
|
+
"Connect Builder to jump to component source") /* i18n-ignore design inspector tooltip */
|
|
5639
|
+
}
|
|
5640
|
+
</TooltipContent>
|
|
5641
|
+
</Tooltip>
|
|
5642
|
+
</div>
|
|
5643
|
+
|
|
5644
|
+
{/* ── Body ── */}
|
|
5645
|
+
<div className="space-y-1.5 px-3 pb-3 pt-0.5 text-[11px]">
|
|
5646
|
+
{/* Source path chip */}
|
|
5647
|
+
{sourceChip && (
|
|
5648
|
+
<div
|
|
5649
|
+
className="flex items-center gap-1 rounded bg-[var(--design-editor-control-bg)] px-2 py-1"
|
|
5650
|
+
title={sourceChip}
|
|
5651
|
+
>
|
|
5652
|
+
<IconCode className="size-3 shrink-0 text-muted-foreground/60" />
|
|
5653
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground">
|
|
5654
|
+
{sourceChip}
|
|
5655
|
+
</span>
|
|
5656
|
+
</div>
|
|
5657
|
+
)}
|
|
5658
|
+
|
|
5659
|
+
{/* Typed prop controls. Inline/Alpine designs are editable and persist
|
|
5660
|
+
through apply-component-prop-edit; real-app sources are read-only
|
|
5661
|
+
until the deeper source-prop controls land. */}
|
|
5662
|
+
{hasRows && (
|
|
5663
|
+
<div className="space-y-1">
|
|
5664
|
+
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">
|
|
5665
|
+
{"Props" /* i18n-ignore design inspector label */}
|
|
5666
|
+
</p>
|
|
5667
|
+
{rows.map((row) => {
|
|
5668
|
+
const hasOptions = (row.options?.length ?? 0) > 0;
|
|
5669
|
+
const isBoolean = !hasOptions && isBooleanPropValue(row.value);
|
|
5670
|
+
return (
|
|
5671
|
+
<div key={row.name} className="flex items-center gap-1.5">
|
|
5672
|
+
<Label className="w-[64px] shrink-0 truncate text-[11px] font-medium capitalize text-muted-foreground">
|
|
5673
|
+
{row.name}
|
|
5674
|
+
</Label>
|
|
5675
|
+
{hasOptions ? (
|
|
5676
|
+
// Dropdown for variant / enum groups.
|
|
5677
|
+
<Select
|
|
5678
|
+
value={row.value || row.options![0] || ""}
|
|
5679
|
+
onValueChange={(v) => commitProp(row, v)}
|
|
5680
|
+
disabled={!editingEnabled}
|
|
5681
|
+
>
|
|
5682
|
+
<SelectTrigger className="h-6 min-w-0 flex-1 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-[11px] shadow-none focus:ring-1 focus:ring-[var(--design-editor-accent-color)]">
|
|
5683
|
+
<SelectValue />
|
|
5684
|
+
</SelectTrigger>
|
|
5685
|
+
<SelectContent>
|
|
5686
|
+
{row.options!.map((opt) => (
|
|
5687
|
+
<SelectItem
|
|
5688
|
+
key={opt}
|
|
5689
|
+
value={opt}
|
|
5690
|
+
className="text-[11px]"
|
|
5691
|
+
>
|
|
5692
|
+
{opt}
|
|
5693
|
+
</SelectItem>
|
|
5694
|
+
))}
|
|
5695
|
+
</SelectContent>
|
|
5696
|
+
</Select>
|
|
5697
|
+
) : isBoolean ? (
|
|
5698
|
+
// Toggle for boolean props.
|
|
5699
|
+
<div className="flex min-w-0 flex-1 items-center">
|
|
5700
|
+
<Switch
|
|
5701
|
+
checked={row.value.trim().toLowerCase() === "true"}
|
|
5702
|
+
onCheckedChange={(checked) =>
|
|
5703
|
+
commitProp(row, checked ? "true" : "false")
|
|
5704
|
+
}
|
|
5705
|
+
disabled={!editingEnabled}
|
|
5706
|
+
className="h-4 w-7 [&>span]:size-3 [&>span]:data-[state=checked]:translate-x-3"
|
|
5707
|
+
aria-label={
|
|
5708
|
+
row.name /* i18n-ignore dynamic prop name */
|
|
5709
|
+
}
|
|
5710
|
+
/>
|
|
5711
|
+
</div>
|
|
5712
|
+
) : (
|
|
5713
|
+
// Text input for string props (e.g. a label).
|
|
5714
|
+
<Input
|
|
5715
|
+
defaultValue={row.value}
|
|
5716
|
+
key={`${row.name}:${row.value}`}
|
|
5717
|
+
disabled={!editingEnabled}
|
|
5718
|
+
onBlur={(e) => commitProp(row, e.target.value)}
|
|
5719
|
+
onKeyDown={(e) => {
|
|
5720
|
+
if (e.key === "Enter") {
|
|
5721
|
+
e.preventDefault();
|
|
5722
|
+
e.currentTarget.blur();
|
|
5723
|
+
}
|
|
5724
|
+
}}
|
|
5725
|
+
className="h-6 min-w-0 flex-1 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-[11px] shadow-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
|
|
5726
|
+
/>
|
|
5727
|
+
)}
|
|
5728
|
+
</div>
|
|
5729
|
+
);
|
|
5730
|
+
})}
|
|
5731
|
+
</div>
|
|
5732
|
+
)}
|
|
5733
|
+
|
|
5734
|
+
{/* Connect-Builder CTA (inline, only when real-app features are gated) */}
|
|
5735
|
+
{capabilities.ctaRequired && (
|
|
5736
|
+
<MakeItRealCard
|
|
5737
|
+
designId={designId}
|
|
5738
|
+
featureLabel="component source jump and full prop controls"
|
|
5739
|
+
/>
|
|
5740
|
+
)}
|
|
5741
|
+
</div>
|
|
5742
|
+
</section>
|
|
5743
|
+
);
|
|
5744
|
+
}
|
|
5745
|
+
|
|
4653
5746
|
export function EditPanel({
|
|
4654
5747
|
selectedElement,
|
|
4655
5748
|
pageStyles = {},
|
|
@@ -4664,15 +5757,35 @@ export function EditPanel({
|
|
|
4664
5757
|
onRequestTweaks,
|
|
4665
5758
|
onStyleChange,
|
|
4666
5759
|
onStylesChange,
|
|
5760
|
+
onAutoLayoutConvert,
|
|
4667
5761
|
onExport,
|
|
4668
5762
|
exporting = false,
|
|
4669
5763
|
readOnly = false,
|
|
5764
|
+
fileId,
|
|
5765
|
+
filename,
|
|
5766
|
+
designId,
|
|
5767
|
+
onTokensApplied,
|
|
5768
|
+
statesPanelProps,
|
|
5769
|
+
reviewPanelProps,
|
|
5770
|
+
componentNodeId,
|
|
5771
|
+
sourceCapabilities = [],
|
|
5772
|
+
onCreateComponent,
|
|
5773
|
+
defaultComponentName = "Component",
|
|
5774
|
+
inspectCode,
|
|
4670
5775
|
}: EditPanelProps) {
|
|
4671
5776
|
const t = useT();
|
|
5777
|
+
const [createComponentOpen, setCreateComponentOpen] = useState(false);
|
|
4672
5778
|
const [exportSettings, setExportSettings] = useState<ExportSettingsValue>(
|
|
4673
5779
|
DEFAULT_EXPORT_SETTINGS,
|
|
4674
5780
|
);
|
|
4675
5781
|
const [showExportPreview, setShowExportPreview] = useState(false);
|
|
5782
|
+
// Sub-tab within the Tweaks area: "tokens" (TokensPanel face) or "tweaks"
|
|
5783
|
+
// (raw TweakDefinition controls). Always default to the existing "tweaks"
|
|
5784
|
+
// controls so opening Tweaks lands on the user's TweakDefinition controls
|
|
5785
|
+
// (and the + affordance) rather than auto-firing token indexing. "Tokens"
|
|
5786
|
+
// remains available as a sub-tab the user can click when designId is set.
|
|
5787
|
+
const [tweaksSubTab, setTweaksSubTab] = useState<TweaksSubTab>("tweaks");
|
|
5788
|
+
|
|
4676
5789
|
const selectedElementKey = selectedElement
|
|
4677
5790
|
? elementIdentityKey(selectedElement)
|
|
4678
5791
|
: "none";
|
|
@@ -4689,6 +5802,16 @@ export function EditPanel({
|
|
|
4689
5802
|
},
|
|
4690
5803
|
[onTweakChange],
|
|
4691
5804
|
);
|
|
5805
|
+
// The "Add Tweaks" (+) flow lives only on the "tweaks" sub-tab. Switch to it
|
|
5806
|
+
// before opening the prompt so the + button and the newly added control are
|
|
5807
|
+
// visible (avoids leaving the user on the Tokens sub-tab where + isn't shown).
|
|
5808
|
+
const handleRequestTweaks = useCallback(
|
|
5809
|
+
(anchor: HTMLElement) => {
|
|
5810
|
+
setTweaksSubTab("tweaks");
|
|
5811
|
+
onRequestTweaks?.(anchor);
|
|
5812
|
+
},
|
|
5813
|
+
[onRequestTweaks],
|
|
5814
|
+
);
|
|
4692
5815
|
|
|
4693
5816
|
useEffect(() => {
|
|
4694
5817
|
setExportSettings(DEFAULT_EXPORT_SETTINGS);
|
|
@@ -4755,7 +5878,29 @@ export function EditPanel({
|
|
|
4755
5878
|
|
|
4756
5879
|
{activeTab === "design" ? (
|
|
4757
5880
|
<>
|
|
4758
|
-
<SelectionHeader
|
|
5881
|
+
<SelectionHeader
|
|
5882
|
+
element={selectedElement}
|
|
5883
|
+
onCreateComponent={
|
|
5884
|
+
onCreateComponent && selectedElement
|
|
5885
|
+
? () => setCreateComponentOpen(true)
|
|
5886
|
+
: undefined
|
|
5887
|
+
}
|
|
5888
|
+
inspectCodePopover={
|
|
5889
|
+
inspectCode && selectedElement
|
|
5890
|
+
? (trigger) => (
|
|
5891
|
+
<InspectCodePopover trigger={trigger} data={inspectCode} />
|
|
5892
|
+
)
|
|
5893
|
+
: undefined
|
|
5894
|
+
}
|
|
5895
|
+
/>
|
|
5896
|
+
{onCreateComponent && (
|
|
5897
|
+
<CreateComponentDialog
|
|
5898
|
+
open={createComponentOpen}
|
|
5899
|
+
onOpenChange={setCreateComponentOpen}
|
|
5900
|
+
defaultName={defaultComponentName}
|
|
5901
|
+
onSubmit={onCreateComponent}
|
|
5902
|
+
/>
|
|
5903
|
+
)}
|
|
4759
5904
|
|
|
4760
5905
|
<div
|
|
4761
5906
|
className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain"
|
|
@@ -4821,6 +5966,16 @@ export function EditPanel({
|
|
|
4821
5966
|
next?.focus();
|
|
4822
5967
|
}}
|
|
4823
5968
|
>
|
|
5969
|
+
{/* §6.1 Component section — shown at the top when a component
|
|
5970
|
+
instance is selected. Requires designId + componentNodeId. */}
|
|
5971
|
+
{designId && componentNodeId && (
|
|
5972
|
+
<ComponentSection
|
|
5973
|
+
designId={designId}
|
|
5974
|
+
nodeId={componentNodeId}
|
|
5975
|
+
sourceCapabilities={sourceCapabilities}
|
|
5976
|
+
/>
|
|
5977
|
+
)}
|
|
5978
|
+
|
|
4824
5979
|
{!selectedElement && (
|
|
4825
5980
|
<PageProperties
|
|
4826
5981
|
styles={pageStyles}
|
|
@@ -4839,6 +5994,7 @@ export function EditPanel({
|
|
|
4839
5994
|
element={selectedElement}
|
|
4840
5995
|
onStyleChange={onStyleChange}
|
|
4841
5996
|
onStylesChange={onStylesChange}
|
|
5997
|
+
onAutoLayoutConvert={onAutoLayoutConvert}
|
|
4842
5998
|
/>
|
|
4843
5999
|
<AppearanceProperties
|
|
4844
6000
|
element={selectedElement}
|
|
@@ -4875,6 +6031,17 @@ export function EditPanel({
|
|
|
4875
6031
|
onStyleChange={onStyleChange}
|
|
4876
6032
|
/>
|
|
4877
6033
|
) : null}
|
|
6034
|
+
{/* AI edit request block — collapsible, collapsed by default */}
|
|
6035
|
+
<section className="shrink-0 border-t border-[var(--design-editor-control-border)]">
|
|
6036
|
+
<InspectorAiActions
|
|
6037
|
+
selector={selectedElement.selector}
|
|
6038
|
+
sourceId={selectedElement.sourceId}
|
|
6039
|
+
designId={designId}
|
|
6040
|
+
fileId={fileId}
|
|
6041
|
+
filename={filename}
|
|
6042
|
+
canEdit={!readOnly}
|
|
6043
|
+
/>
|
|
6044
|
+
</section>
|
|
4878
6045
|
</>
|
|
4879
6046
|
)}
|
|
4880
6047
|
{onExport ? (
|
|
@@ -4909,40 +6076,212 @@ export function EditPanel({
|
|
|
4909
6076
|
) : null}
|
|
4910
6077
|
</PanelSection>
|
|
4911
6078
|
) : null}
|
|
6079
|
+
|
|
6080
|
+
{/* §6.4 States & Responsive — contextual section in Design tab.
|
|
6081
|
+
Collapsed by default so existing Design content is not buried.
|
|
6082
|
+
Only mounts when designId is provided so there is something to
|
|
6083
|
+
load; without it the panel would fire an action with no id. */}
|
|
6084
|
+
{designId && statesPanelProps ? (
|
|
6085
|
+
<PanelSection
|
|
6086
|
+
title={"States" /* i18n-ignore design inspector section */}
|
|
6087
|
+
defaultCollapsed
|
|
6088
|
+
actions={
|
|
6089
|
+
<Tooltip>
|
|
6090
|
+
<TooltipTrigger asChild>
|
|
6091
|
+
<Button
|
|
6092
|
+
type="button"
|
|
6093
|
+
variant="ghost"
|
|
6094
|
+
size="icon"
|
|
6095
|
+
className="size-6 rounded-md text-muted-foreground hover:text-foreground"
|
|
6096
|
+
aria-label={
|
|
6097
|
+
"Breakpoints & states" /* i18n-ignore design inspector action */
|
|
6098
|
+
}
|
|
6099
|
+
>
|
|
6100
|
+
<IconDeviceDesktop className="size-3.5" />
|
|
6101
|
+
</Button>
|
|
6102
|
+
</TooltipTrigger>
|
|
6103
|
+
<TooltipContent>
|
|
6104
|
+
{
|
|
6105
|
+
"Breakpoints & states" /* i18n-ignore design inspector action */
|
|
6106
|
+
}
|
|
6107
|
+
</TooltipContent>
|
|
6108
|
+
</Tooltip>
|
|
6109
|
+
}
|
|
6110
|
+
>
|
|
6111
|
+
<StatesPanel designId={designId} {...statesPanelProps} />
|
|
6112
|
+
{/* §6.4 / §6.6 — live captures (real running-app data, route
|
|
6113
|
+
props, API responses) are a real-app capability. When
|
|
6114
|
+
canCapture is false (inline source) surface a compact
|
|
6115
|
+
"Make it real" CTA so the user knows it's one step away. */}
|
|
6116
|
+
{!statesPanelProps.canCapture && (
|
|
6117
|
+
<MakeItRealCard
|
|
6118
|
+
designId={designId}
|
|
6119
|
+
featureLabel="live data captures and real-app states"
|
|
6120
|
+
/>
|
|
6121
|
+
)}
|
|
6122
|
+
</PanelSection>
|
|
6123
|
+
) : null}
|
|
6124
|
+
|
|
6125
|
+
{/* §6.5 Review — contextual section in Design tab.
|
|
6126
|
+
Collapsed by default. Renders when reviewPanelProps is provided,
|
|
6127
|
+
no designId check needed since ReviewPanel is statically fed. */}
|
|
6128
|
+
{reviewPanelProps ? (
|
|
6129
|
+
<PanelSection
|
|
6130
|
+
title={"Review" /* i18n-ignore design inspector section */}
|
|
6131
|
+
defaultCollapsed
|
|
6132
|
+
actions={
|
|
6133
|
+
<Tooltip>
|
|
6134
|
+
<TooltipTrigger asChild>
|
|
6135
|
+
<Button
|
|
6136
|
+
type="button"
|
|
6137
|
+
variant="ghost"
|
|
6138
|
+
size="icon"
|
|
6139
|
+
className="size-6 rounded-md text-muted-foreground hover:text-foreground"
|
|
6140
|
+
aria-label={
|
|
6141
|
+
"Accessibility & visual diff" /* i18n-ignore design inspector action */
|
|
6142
|
+
}
|
|
6143
|
+
>
|
|
6144
|
+
<IconShieldCheck className="size-3.5" />
|
|
6145
|
+
</Button>
|
|
6146
|
+
</TooltipTrigger>
|
|
6147
|
+
<TooltipContent>
|
|
6148
|
+
{
|
|
6149
|
+
"Accessibility & visual diff" /* i18n-ignore design inspector action */
|
|
6150
|
+
}
|
|
6151
|
+
</TooltipContent>
|
|
6152
|
+
</Tooltip>
|
|
6153
|
+
}
|
|
6154
|
+
>
|
|
6155
|
+
{/* ReviewPanel manages its own scroll; no extra wrapper needed. */}
|
|
6156
|
+
<ReviewPanel {...reviewPanelProps} />
|
|
6157
|
+
</PanelSection>
|
|
6158
|
+
) : null}
|
|
4912
6159
|
</div>
|
|
4913
6160
|
</>
|
|
4914
6161
|
) : activeTab === "tweaks" ? (
|
|
4915
6162
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
</
|
|
4936
|
-
|
|
4937
|
-
|
|
6163
|
+
{/* Tweaks tab header: when designId is present show a Tokens | Tweaks
|
|
6164
|
+
sub-tab switcher so the TokensPanel is the first-class face of
|
|
6165
|
+
this area per §6.2. When there is no designId only the raw tweaks
|
|
6166
|
+
title is shown (original behaviour). */}
|
|
6167
|
+
{designId ? (
|
|
6168
|
+
<div className="flex h-9 shrink-0 items-center gap-0.5 border-b border-border/90 px-2">
|
|
6169
|
+
<Button
|
|
6170
|
+
type="button"
|
|
6171
|
+
variant="ghost"
|
|
6172
|
+
size="sm"
|
|
6173
|
+
className={cn(
|
|
6174
|
+
"h-6 rounded-md px-2 text-[11px] font-semibold text-muted-foreground transition-colors hover:text-foreground",
|
|
6175
|
+
tweaksSubTab === "tokens" &&
|
|
6176
|
+
"bg-[var(--design-editor-panel-raised-bg)] text-foreground",
|
|
6177
|
+
)}
|
|
6178
|
+
onClick={() => setTweaksSubTab("tokens")}
|
|
6179
|
+
>
|
|
6180
|
+
<IconPalette className="mr-1 size-3" />
|
|
6181
|
+
{"Tokens" /* i18n-ignore design inspector sub-tab */}
|
|
6182
|
+
</Button>
|
|
6183
|
+
<Button
|
|
6184
|
+
type="button"
|
|
6185
|
+
variant="ghost"
|
|
6186
|
+
size="sm"
|
|
6187
|
+
className={cn(
|
|
6188
|
+
"h-6 rounded-md px-2 text-[11px] font-semibold text-muted-foreground transition-colors hover:text-foreground",
|
|
6189
|
+
tweaksSubTab === "tweaks" &&
|
|
6190
|
+
"bg-[var(--design-editor-panel-raised-bg)] text-foreground",
|
|
6191
|
+
)}
|
|
6192
|
+
onClick={() => setTweaksSubTab("tweaks")}
|
|
6193
|
+
>
|
|
6194
|
+
{t("designEditor.tweaks")}
|
|
6195
|
+
</Button>
|
|
6196
|
+
</div>
|
|
6197
|
+
) : (
|
|
6198
|
+
<div className="flex h-10 shrink-0 items-center justify-between gap-2 border-b border-border/90 px-3">
|
|
6199
|
+
<h3 className="min-w-0 flex-1 truncate text-[13px] font-semibold text-foreground">
|
|
6200
|
+
{t("designEditor.tweaks")}
|
|
6201
|
+
</h3>
|
|
6202
|
+
{onRequestTweaks ? (
|
|
6203
|
+
<Tooltip>
|
|
6204
|
+
<TooltipTrigger asChild>
|
|
6205
|
+
<Button
|
|
6206
|
+
type="button"
|
|
6207
|
+
variant="ghost"
|
|
6208
|
+
size="icon"
|
|
6209
|
+
className="size-7 rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
6210
|
+
aria-label={t("designEditor.addTweaks")}
|
|
6211
|
+
onClick={(event) =>
|
|
6212
|
+
handleRequestTweaks(event.currentTarget)
|
|
6213
|
+
}
|
|
6214
|
+
>
|
|
6215
|
+
<IconPlus className="size-3.5" />
|
|
6216
|
+
</Button>
|
|
6217
|
+
</TooltipTrigger>
|
|
6218
|
+
<TooltipContent>{t("designEditor.addTweaks")}</TooltipContent>
|
|
6219
|
+
</Tooltip>
|
|
6220
|
+
) : null}
|
|
6221
|
+
</div>
|
|
6222
|
+
)}
|
|
6223
|
+
|
|
4938
6224
|
<div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
6225
|
+
{designId && tweaksSubTab === "tokens" ? (
|
|
6226
|
+
/* §6.2 Tokens face — first-class view of the Tweaks area.
|
|
6227
|
+
TokensPanel calls apply-design-token-edit which routes through
|
|
6228
|
+
the Tweaks loop (designs.data.tweakSelections persist). */
|
|
6229
|
+
<>
|
|
6230
|
+
<TokensPanel
|
|
6231
|
+
designId={designId}
|
|
6232
|
+
onTokensApplied={onTokensApplied}
|
|
6233
|
+
/>
|
|
6234
|
+
{/* §5 / §6.6 — token write-back to source is a real-app
|
|
6235
|
+
capability. On inline sources (sourceCapabilities lacks
|
|
6236
|
+
"writeFile") show a compact "Make it real" CTA after the
|
|
6237
|
+
token list so the user knows write-back is one step away. */}
|
|
6238
|
+
{!sourceCapabilities.includes("writeFile") && (
|
|
6239
|
+
<div className="px-3 pb-3 pt-1">
|
|
6240
|
+
<MakeItRealCard
|
|
6241
|
+
designId={designId}
|
|
6242
|
+
featureLabel="token write-back to source files"
|
|
6243
|
+
/>
|
|
6244
|
+
</div>
|
|
6245
|
+
)}
|
|
6246
|
+
</>
|
|
6247
|
+
) : (
|
|
6248
|
+
/* Raw TweakDefinition controls — shown when no designId, or when
|
|
6249
|
+
the user switches to the Tweaks sub-tab. */
|
|
6250
|
+
<>
|
|
6251
|
+
{designId && onRequestTweaks ? (
|
|
6252
|
+
/* When in sub-tab mode, show the add-tweaks action in the
|
|
6253
|
+
scrollable content area above the raw controls. */
|
|
6254
|
+
<div className="flex items-center justify-end px-3 pt-2">
|
|
6255
|
+
<Tooltip>
|
|
6256
|
+
<TooltipTrigger asChild>
|
|
6257
|
+
<Button
|
|
6258
|
+
type="button"
|
|
6259
|
+
variant="ghost"
|
|
6260
|
+
size="icon"
|
|
6261
|
+
className="size-7 rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
6262
|
+
aria-label={t("designEditor.addTweaks")}
|
|
6263
|
+
onClick={(event) =>
|
|
6264
|
+
handleRequestTweaks(event.currentTarget)
|
|
6265
|
+
}
|
|
6266
|
+
>
|
|
6267
|
+
<IconPlus className="size-3.5" />
|
|
6268
|
+
</Button>
|
|
6269
|
+
</TooltipTrigger>
|
|
6270
|
+
<TooltipContent>
|
|
6271
|
+
{t("designEditor.addTweaks")}
|
|
6272
|
+
</TooltipContent>
|
|
6273
|
+
</Tooltip>
|
|
6274
|
+
</div>
|
|
6275
|
+
) : null}
|
|
6276
|
+
<TweaksPanelContent
|
|
6277
|
+
tweaks={tweaks}
|
|
6278
|
+
values={tweakValues}
|
|
6279
|
+
onChange={handleTweakChange}
|
|
6280
|
+
onRequestTweaks={!designId ? handleRequestTweaks : undefined}
|
|
6281
|
+
className="px-3 py-3"
|
|
6282
|
+
/>
|
|
6283
|
+
</>
|
|
6284
|
+
)}
|
|
4946
6285
|
</div>
|
|
4947
6286
|
</div>
|
|
4948
6287
|
) : extensionContext ? (
|