@agent-native/core 0.84.9 → 0.84.12
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 +43 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/run-store.ts +29 -6
- package/corpus/core/src/cli/skills.ts +6 -3
- package/corpus/core/src/client/agent-chat-adapter.ts +31 -11
- package/corpus/core/src/client/require-session.tsx +20 -1
- package/corpus/core/src/client/sharing/ShareButton.tsx +9 -3
- package/corpus/core/src/collab/storage.ts +40 -5
- package/corpus/core/src/server/auth.ts +6 -0
- package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +7 -6
- package/corpus/templates/assets/AGENTS.md +15 -0
- package/corpus/templates/assets/actions/_helpers.ts +3 -1
- package/corpus/templates/assets/actions/create-generation-preset.ts +12 -1
- package/corpus/templates/assets/actions/generate-asset.ts +5 -1
- package/corpus/templates/assets/actions/generate-image-batch.ts +6 -1
- package/corpus/templates/assets/actions/generate-image.ts +18 -11
- package/corpus/templates/assets/actions/open-asset-picker.ts +4 -2
- package/corpus/templates/assets/actions/update-generation-preset.ts +16 -3
- package/corpus/templates/assets/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/assets/app/i18n-data.ts +30 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +42 -7
- package/corpus/templates/assets/app/routes/library.tsx +4 -2
- package/corpus/templates/assets/changelog/2026-07-01-generation-presets-can-now-composite-your-brand-s-canonical-.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-01-tagging-a-preset-now-briefs-the-agent-on-that-preset-s-aesth.md +6 -0
- package/corpus/templates/assets/server/lib/preset-chat-context.ts +118 -0
- package/corpus/templates/assets/server/plugins/agent-chat.ts +5 -0
- package/corpus/templates/assets/shared/api.ts +1 -0
- package/corpus/templates/design/actions/export-html.ts +4 -2
- package/corpus/templates/design/actions/export-pdf.ts +3 -1
- package/corpus/templates/design/actions/export-svg.ts +4 -2
- package/corpus/templates/design/actions/export-zip.ts +10 -3
- package/corpus/templates/design/actions/get-design-snapshot.ts +41 -4
- package/corpus/templates/design/actions/present-design-variants.ts +21 -6
- package/corpus/templates/design/actions/update-file.ts +68 -17
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +3 -2
- package/corpus/templates/design/app/components/design/EditPanel.tsx +35 -3
- package/corpus/templates/design/app/components/design/MotionDock.tsx +46 -11
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +80 -30
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +60 -10
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +2 -0
- package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +4 -0
- package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
- package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +2 -2
- package/corpus/templates/design/app/i18n-data.ts +1 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +493 -109
- package/corpus/templates/design/app/pages/VisualEdit.tsx +4 -16
- package/corpus/templates/design/changelog/2026-07-01-improved-variant-pick-follow-ups-so-selected-directions-cont.md +6 -0
- package/corpus/templates/design/e2e/helpers.ts +2 -2
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +28 -6
- package/dist/agent/run-store.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +6 -3
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +26 -11
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/require-session.d.ts +14 -0
- package/dist/client/require-session.d.ts.map +1 -1
- package/dist/client/require-session.js +19 -1
- package/dist/client/require-session.js.map +1 -1
- package/dist/client/sharing/ShareButton.d.ts +4 -2
- package/dist/client/sharing/ShareButton.d.ts.map +1 -1
- package/dist/client/sharing/ShareButton.js +3 -1
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/storage.d.ts.map +1 -1
- package/dist/collab/storage.js +31 -5
- package/dist/collab/storage.js.map +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 +6 -6
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +7 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -40,7 +40,17 @@ const FALLBACK_INSTRUCTIONS =
|
|
|
40
40
|
"The generated directions have been saved as normal screens on the Design " +
|
|
41
41
|
"overview board. The chat shows one button per screen. Ask the user to pick " +
|
|
42
42
|
"a screen by name if the inline buttons are not available; after they pick, " +
|
|
43
|
-
"delete the other variant screens
|
|
43
|
+
"delete the other variant screens, call get-design-snapshot with fileId for " +
|
|
44
|
+
"the kept screen, and continue from that one file in a bounded pass.";
|
|
45
|
+
|
|
46
|
+
const VARIANT_PICK_SUBMIT_MESSAGE =
|
|
47
|
+
"Use this design direction. Delete the other variant screens, call " +
|
|
48
|
+
"get-design-snapshot with the selected option's fileId (or filename if " +
|
|
49
|
+
"needed) for the kept file, then continue from that current HTML in a " +
|
|
50
|
+
"bounded single-file pass. Prefer edit-design for refinements; if " +
|
|
51
|
+
"generate-design is needed, update only the kept file compactly. Do not " +
|
|
52
|
+
"generate a full multi-screen rewrite or resend a huge payload. Stop after " +
|
|
53
|
+
"the first successful save.";
|
|
44
54
|
|
|
45
55
|
const variantSchema = z.object({
|
|
46
56
|
id: z.string().min(1).describe("Stable variant id, e.g. 'minimal-focus'"),
|
|
@@ -410,7 +420,11 @@ export default defineAction({
|
|
|
410
420
|
"Provide 2-5 variants (3 is the sweet spot). Use this for design " +
|
|
411
421
|
"exploration before follow-up refinement. After the user's choice, keep " +
|
|
412
422
|
"the chosen screen, delete the other generated variant screens, and " +
|
|
413
|
-
"
|
|
423
|
+
"call get-design-snapshot with fileId for the kept screen before " +
|
|
424
|
+
"continuing from that single file in a bounded pass. Prefer edit-design " +
|
|
425
|
+
"for refinements; if generate-design is needed, update only the kept file " +
|
|
426
|
+
"compactly and stop after the first successful save. For complex apps, " +
|
|
427
|
+
"make each variant a " +
|
|
414
428
|
"compact representative screen; pass concise labels/descriptions/features " +
|
|
415
429
|
"and omit content when full HTML would be too large. Design will render " +
|
|
416
430
|
"compact screens from the direction data. Expand the chosen direction " +
|
|
@@ -563,9 +577,9 @@ export default defineAction({
|
|
|
563
577
|
await writeAppStateForCurrentTab("guided-questions", {
|
|
564
578
|
title: prompt ?? "Pick a direction",
|
|
565
579
|
description:
|
|
566
|
-
"All options are on the board. Choose
|
|
580
|
+
"All options are on the board. Choose one to keep; I will delete the others, read only the kept screen, and continue from that single file in a bounded pass.",
|
|
567
581
|
submitLabel: "Use selected direction",
|
|
568
|
-
submitMessage:
|
|
582
|
+
submitMessage: VARIANT_PICK_SUBMIT_MESSAGE,
|
|
569
583
|
skipLabel: "Show another set",
|
|
570
584
|
skipMessage: "None of these directions are right.",
|
|
571
585
|
questions: [
|
|
@@ -590,7 +604,8 @@ export default defineAction({
|
|
|
590
604
|
label: screen.label || optionName(index),
|
|
591
605
|
value:
|
|
592
606
|
`Keep "${screen.label}" (${screen.filename}, file id ${screen.id}) ` +
|
|
593
|
-
`from variant set ${variantSetId}. Delete the other variant screens: ${otherScreens}
|
|
607
|
+
`from variant set ${variantSetId}. Delete the other variant screens: ${otherScreens}. ` +
|
|
608
|
+
`Then call get-design-snapshot with designId ${designId} and fileId ${screen.id} (filename ${screen.filename}) and continue from that current HTML in a bounded single-file pass. Prefer edit-design for refinements; if generate-design is needed, update only this kept file compactly. Do not generate a full multi-screen rewrite or resend a huge payload. Stop after the first successful save.`,
|
|
594
609
|
};
|
|
595
610
|
}),
|
|
596
611
|
},
|
|
@@ -608,7 +623,7 @@ export default defineAction({
|
|
|
608
623
|
embed: true,
|
|
609
624
|
fallbackInstructions: FALLBACK_INSTRUCTIONS,
|
|
610
625
|
nextRequiredAction:
|
|
611
|
-
"Wait for the user to pick a screen in chat. Then delete the unchosen variant screens with delete-file and continue from the
|
|
626
|
+
"Wait for the user to pick a screen in chat. Then delete the unchosen variant screens with delete-file, call get-design-snapshot with fileId for the chosen screen, and continue from that single file in a bounded pass. Prefer edit-design; if generate-design is needed, update only the kept file compactly and stop after the first successful save.",
|
|
612
627
|
};
|
|
613
628
|
},
|
|
614
629
|
link: ({ result }) => {
|
|
@@ -4,12 +4,24 @@ import {
|
|
|
4
4
|
applyText,
|
|
5
5
|
seedFromText,
|
|
6
6
|
} from "@agent-native/core/collab";
|
|
7
|
+
import { isPostgres } from "@agent-native/core/db";
|
|
7
8
|
import { accessFilter, assertAccess } from "@agent-native/core/sharing";
|
|
8
|
-
import { and, eq } from "drizzle-orm";
|
|
9
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
9
10
|
import { z } from "zod";
|
|
10
11
|
|
|
11
12
|
import { getDb, schema } from "../server/db/index.js";
|
|
12
13
|
|
|
14
|
+
function rowsAffected(result: unknown): number | undefined {
|
|
15
|
+
const candidate = result as {
|
|
16
|
+
rowsAffected?: unknown;
|
|
17
|
+
rowCount?: unknown;
|
|
18
|
+
changes?: unknown;
|
|
19
|
+
} | null;
|
|
20
|
+
const value =
|
|
21
|
+
candidate?.rowsAffected ?? candidate?.rowCount ?? candidate?.changes;
|
|
22
|
+
return typeof value === "number" ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
export default defineAction({
|
|
14
26
|
description:
|
|
15
27
|
"Update an existing file in a design project. " +
|
|
@@ -70,13 +82,19 @@ export default defineAction({
|
|
|
70
82
|
|
|
71
83
|
await assertAccess("design", file.designId, "editor");
|
|
72
84
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
const updates: Record<string, unknown> = { updatedAt: now };
|
|
86
|
+
if (content !== undefined) updates.content = content;
|
|
87
|
+
if (filename !== undefined) updates.filename = filename;
|
|
88
|
+
if (fileType !== undefined) updates.fileType = fileType;
|
|
89
|
+
|
|
90
|
+
if (filename !== undefined && isPostgres()) {
|
|
91
|
+
await db.transaction(async (tx) => {
|
|
92
|
+
// Postgres evaluates concurrent NOT EXISTS updates under MVCC, so a
|
|
93
|
+
// guarded UPDATE alone can still race. Serialize design-file renames in
|
|
94
|
+
// this rare path without using SQLite's fragile async savepoint wrapper.
|
|
95
|
+
await (
|
|
96
|
+
tx as unknown as { execute: (query: unknown) => Promise<unknown> }
|
|
97
|
+
).execute(sql`LOCK TABLE design_files IN SHARE ROW EXCLUSIVE MODE`);
|
|
80
98
|
const [collision] = await tx
|
|
81
99
|
.select({ id: schema.designFiles.id })
|
|
82
100
|
.from(schema.designFiles)
|
|
@@ -92,18 +110,51 @@ export default defineAction({
|
|
|
92
110
|
`File "${filename}" already exists in design ${file.designId}`,
|
|
93
111
|
);
|
|
94
112
|
}
|
|
95
|
-
|
|
113
|
+
await tx
|
|
114
|
+
.update(schema.designFiles)
|
|
115
|
+
.set(updates)
|
|
116
|
+
.where(eq(schema.designFiles.id, id));
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
// Reject colliding SQLite renames as part of the write. SQLite's local
|
|
120
|
+
// async transaction wrapper can fail under concurrent editor/collab writes,
|
|
121
|
+
// so keep this to one guarded UPDATE instead of a SELECT-then-UPDATE window.
|
|
122
|
+
const updateWhere =
|
|
123
|
+
filename === undefined
|
|
124
|
+
? eq(schema.designFiles.id, id)
|
|
125
|
+
: and(
|
|
126
|
+
eq(schema.designFiles.id, id),
|
|
127
|
+
sql`NOT EXISTS (
|
|
128
|
+
SELECT 1 FROM design_files AS sibling
|
|
129
|
+
WHERE sibling.design_id = ${file.designId}
|
|
130
|
+
AND sibling.filename = ${filename}
|
|
131
|
+
AND sibling.id <> ${id}
|
|
132
|
+
)`,
|
|
133
|
+
);
|
|
96
134
|
|
|
97
|
-
const
|
|
98
|
-
if (content !== undefined) updates.content = content;
|
|
99
|
-
if (filename !== undefined) updates.filename = filename;
|
|
100
|
-
if (fileType !== undefined) updates.fileType = fileType;
|
|
101
|
-
|
|
102
|
-
await tx
|
|
135
|
+
const updateResult = await db
|
|
103
136
|
.update(schema.designFiles)
|
|
104
137
|
.set(updates)
|
|
105
|
-
.where(
|
|
106
|
-
|
|
138
|
+
.where(updateWhere);
|
|
139
|
+
|
|
140
|
+
if (filename !== undefined && rowsAffected(updateResult) === 0) {
|
|
141
|
+
const [collision] = await db
|
|
142
|
+
.select({ id: schema.designFiles.id })
|
|
143
|
+
.from(schema.designFiles)
|
|
144
|
+
.where(
|
|
145
|
+
and(
|
|
146
|
+
eq(schema.designFiles.designId, file.designId),
|
|
147
|
+
eq(schema.designFiles.filename, filename),
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
.limit(1);
|
|
151
|
+
if (collision && collision.id !== id) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`File "${filename}" already exists in design ${file.designId}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
107
158
|
|
|
108
159
|
// Push content through the collab layer so live editors see the change
|
|
109
160
|
if (content !== undefined && syncCollab) {
|
|
@@ -1811,14 +1811,15 @@ export function DesignCanvas({
|
|
|
1811
1811
|
: "allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin"
|
|
1812
1812
|
}
|
|
1813
1813
|
data-design-preview-iframe
|
|
1814
|
-
data-screen-iframe-id={
|
|
1814
|
+
data-screen-iframe-id={
|
|
1815
|
+
boardSurface ? undefined : (screenId ?? undefined)
|
|
1816
|
+
}
|
|
1815
1817
|
data-design-source-type={
|
|
1816
1818
|
sourceType ??
|
|
1817
1819
|
(externalPreviewUrl
|
|
1818
1820
|
? "localhost" // inferred — content is a URL
|
|
1819
1821
|
: "inline")
|
|
1820
1822
|
}
|
|
1821
|
-
allowTransparency={transparentBackground || undefined}
|
|
1822
1823
|
className="block h-full w-full border-0 bg-transparent"
|
|
1823
1824
|
style={{
|
|
1824
1825
|
background: iframeBackgroundColor,
|
|
@@ -138,7 +138,7 @@ import type { StatesPanelProps } from "./StatesPanel";
|
|
|
138
138
|
import { TweaksPanelContent } from "./TweaksPanel";
|
|
139
139
|
import type { ElementInfo } from "./types";
|
|
140
140
|
|
|
141
|
-
export type InspectorTab = "design" | "tweaks";
|
|
141
|
+
export type InspectorTab = "design" | "tweaks" | "extensions";
|
|
142
142
|
|
|
143
143
|
const MIXED_VALUE = "Mixed";
|
|
144
144
|
|
|
@@ -216,6 +216,7 @@ interface EditPanelProps {
|
|
|
216
216
|
tweakValues?: Record<string, string | number | boolean>;
|
|
217
217
|
onTweakChange?: (id: string, value: string | number | boolean) => void;
|
|
218
218
|
onRequestTweaks?: (anchor: HTMLElement) => void;
|
|
219
|
+
extensionsPanel?: ReactNode;
|
|
219
220
|
onStyleChange: (property: string, value: string) => void;
|
|
220
221
|
onStylesChange?: (styles: Record<string, string>) => void;
|
|
221
222
|
onExport?: (settings: ExportSettingsValue[]) => void;
|
|
@@ -283,6 +284,8 @@ interface EditPanelProps {
|
|
|
283
284
|
defaultComponentName?: string;
|
|
284
285
|
/** Code-inspection data for the "Inspect code" popover. */
|
|
285
286
|
inspectCode?: InspectCodeData;
|
|
287
|
+
/** Optional compact AI edit controls for selected/local source elements. */
|
|
288
|
+
aiActions?: ReactNode;
|
|
286
289
|
}
|
|
287
290
|
|
|
288
291
|
/**
|
|
@@ -2960,10 +2963,12 @@ function InspectorTabsHeader({
|
|
|
2960
2963
|
activeTab,
|
|
2961
2964
|
onActiveTabChange,
|
|
2962
2965
|
trailing,
|
|
2966
|
+
showExtensions,
|
|
2963
2967
|
}: {
|
|
2964
2968
|
activeTab: InspectorTab;
|
|
2965
2969
|
onActiveTabChange: (tab: InspectorTab) => void;
|
|
2966
2970
|
trailing?: ReactNode;
|
|
2971
|
+
showExtensions?: boolean;
|
|
2967
2972
|
}) {
|
|
2968
2973
|
const t = useT();
|
|
2969
2974
|
|
|
@@ -2986,6 +2991,14 @@ function InspectorTabsHeader({
|
|
|
2986
2991
|
>
|
|
2987
2992
|
{t("designEditor.tweaks")}
|
|
2988
2993
|
</TabsTrigger>
|
|
2994
|
+
{showExtensions ? (
|
|
2995
|
+
<TabsTrigger
|
|
2996
|
+
value="extensions"
|
|
2997
|
+
className="h-6 rounded-md px-1.5 !text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
|
2998
|
+
>
|
|
2999
|
+
{"Extensions" /* i18n-ignore design inspector tab */}
|
|
3000
|
+
</TabsTrigger>
|
|
3001
|
+
) : null}
|
|
2989
3002
|
</TabsList>
|
|
2990
3003
|
</Tabs>
|
|
2991
3004
|
{trailing ? <div className="shrink-0">{trailing}</div> : null}
|
|
@@ -3282,7 +3295,7 @@ function CornerRadiusControl({
|
|
|
3282
3295
|
value={radius}
|
|
3283
3296
|
onChange={commitRadius}
|
|
3284
3297
|
min={0}
|
|
3285
|
-
precision={
|
|
3298
|
+
precision={0}
|
|
3286
3299
|
/>
|
|
3287
3300
|
<Tooltip>
|
|
3288
3301
|
<TooltipTrigger asChild>
|
|
@@ -3308,6 +3321,7 @@ function CornerRadiusControl({
|
|
|
3308
3321
|
<>
|
|
3309
3322
|
<AppearanceScrubField
|
|
3310
3323
|
label={t("editPanel.labels.topLeft")}
|
|
3324
|
+
ariaLabel="Top left"
|
|
3311
3325
|
icon={IconRadiusTopLeft}
|
|
3312
3326
|
value={corners.topLeft}
|
|
3313
3327
|
onChange={(value) =>
|
|
@@ -3321,6 +3335,7 @@ function CornerRadiusControl({
|
|
|
3321
3335
|
/>
|
|
3322
3336
|
<AppearanceScrubField
|
|
3323
3337
|
label={t("editPanel.labels.topRight")}
|
|
3338
|
+
ariaLabel="Top right"
|
|
3324
3339
|
icon={IconRadiusTopRight}
|
|
3325
3340
|
value={corners.topRight}
|
|
3326
3341
|
onChange={(value) =>
|
|
@@ -3335,6 +3350,7 @@ function CornerRadiusControl({
|
|
|
3335
3350
|
<span aria-hidden="true" />
|
|
3336
3351
|
<AppearanceScrubField
|
|
3337
3352
|
label={t("editPanel.labels.bottomLeft")}
|
|
3353
|
+
ariaLabel="Bottom left"
|
|
3338
3354
|
icon={IconRadiusBottomLeft}
|
|
3339
3355
|
value={corners.bottomLeft}
|
|
3340
3356
|
onChange={(value) =>
|
|
@@ -3348,6 +3364,7 @@ function CornerRadiusControl({
|
|
|
3348
3364
|
/>
|
|
3349
3365
|
<AppearanceScrubField
|
|
3350
3366
|
label={t("editPanel.labels.bottomRight")}
|
|
3367
|
+
ariaLabel="Bottom right"
|
|
3351
3368
|
icon={IconRadiusBottomRight}
|
|
3352
3369
|
value={corners.bottomRight}
|
|
3353
3370
|
onChange={(value) =>
|
|
@@ -3368,6 +3385,7 @@ function CornerRadiusControl({
|
|
|
3368
3385
|
|
|
3369
3386
|
function AppearanceScrubField({
|
|
3370
3387
|
label,
|
|
3388
|
+
ariaLabel,
|
|
3371
3389
|
icon,
|
|
3372
3390
|
value,
|
|
3373
3391
|
onChange,
|
|
@@ -3379,6 +3397,7 @@ function AppearanceScrubField({
|
|
|
3379
3397
|
precision,
|
|
3380
3398
|
}: {
|
|
3381
3399
|
label: string;
|
|
3400
|
+
ariaLabel?: string;
|
|
3382
3401
|
icon: (props: { className?: string }) => ReactNode;
|
|
3383
3402
|
value: number;
|
|
3384
3403
|
onChange: (value: number) => void;
|
|
@@ -3392,7 +3411,7 @@ function AppearanceScrubField({
|
|
|
3392
3411
|
return (
|
|
3393
3412
|
<ScrubInput
|
|
3394
3413
|
label={label}
|
|
3395
|
-
ariaLabel={label}
|
|
3414
|
+
ariaLabel={ariaLabel ?? label}
|
|
3396
3415
|
icon={icon}
|
|
3397
3416
|
value={value}
|
|
3398
3417
|
onChange={onChange}
|
|
@@ -6842,6 +6861,7 @@ export function EditPanel({
|
|
|
6842
6861
|
tweakValues = {},
|
|
6843
6862
|
onTweakChange,
|
|
6844
6863
|
onRequestTweaks,
|
|
6864
|
+
extensionsPanel,
|
|
6845
6865
|
onStyleChange,
|
|
6846
6866
|
onStylesChange,
|
|
6847
6867
|
onExport,
|
|
@@ -6858,6 +6878,7 @@ export function EditPanel({
|
|
|
6858
6878
|
selectedElementAlreadyComponent = false,
|
|
6859
6879
|
defaultComponentName = "Component",
|
|
6860
6880
|
inspectCode,
|
|
6881
|
+
aiActions,
|
|
6861
6882
|
}: EditPanelProps) {
|
|
6862
6883
|
const t = useT();
|
|
6863
6884
|
const [createComponentOpen, setCreateComponentOpen] = useState(false);
|
|
@@ -6950,6 +6971,7 @@ export function EditPanel({
|
|
|
6950
6971
|
activeTab={activeTab}
|
|
6951
6972
|
onActiveTabChange={handleActiveTabChange}
|
|
6952
6973
|
trailing={headerTrailing}
|
|
6974
|
+
showExtensions={!!extensionsPanel}
|
|
6953
6975
|
/>
|
|
6954
6976
|
|
|
6955
6977
|
{activeTab === "design" ? (
|
|
@@ -7058,6 +7080,12 @@ export function EditPanel({
|
|
|
7058
7080
|
/>
|
|
7059
7081
|
)}
|
|
7060
7082
|
|
|
7083
|
+
{aiActions ? (
|
|
7084
|
+
<div className="border-b border-[var(--design-editor-control-border)] px-2 py-1.5">
|
|
7085
|
+
{aiActions}
|
|
7086
|
+
</div>
|
|
7087
|
+
) : null}
|
|
7088
|
+
|
|
7061
7089
|
{!inspectorElement && (
|
|
7062
7090
|
<PageProperties
|
|
7063
7091
|
styles={pageStyles}
|
|
@@ -7228,6 +7256,10 @@ export function EditPanel({
|
|
|
7228
7256
|
/>
|
|
7229
7257
|
</div>
|
|
7230
7258
|
</div>
|
|
7259
|
+
) : activeTab === "extensions" && extensionsPanel ? (
|
|
7260
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
7261
|
+
{extensionsPanel}
|
|
7262
|
+
</div>
|
|
7231
7263
|
) : null}
|
|
7232
7264
|
</div>
|
|
7233
7265
|
);
|
|
@@ -117,6 +117,14 @@ export interface MotionDockProps {
|
|
|
117
117
|
canvasIframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
|
118
118
|
/** Whether the parent autosave mutation is in flight. */
|
|
119
119
|
applying?: boolean;
|
|
120
|
+
/** Controlled auto-keyframe state. */
|
|
121
|
+
autoKeyframe?: boolean;
|
|
122
|
+
/** Called when the auto-keyframe toggle changes. */
|
|
123
|
+
onAutoKeyframeChange?: (enabled: boolean) => void;
|
|
124
|
+
/** Controlled playhead position, normalized to [0, 1]. */
|
|
125
|
+
playhead?: number;
|
|
126
|
+
/** Called whenever the playhead moves. */
|
|
127
|
+
onPlayheadChange?: (t: number) => void;
|
|
120
128
|
/**
|
|
121
129
|
* The currently-selected canvas element, if any. Required to create the FIRST
|
|
122
130
|
* track for a layer: the picker animates this node's
|
|
@@ -139,6 +147,10 @@ export function MotionDock({
|
|
|
139
147
|
onDurationChange,
|
|
140
148
|
canvasIframeRef,
|
|
141
149
|
applying = false,
|
|
150
|
+
autoKeyframe: autoKeyframeProp,
|
|
151
|
+
onAutoKeyframeChange,
|
|
152
|
+
playhead: playheadProp,
|
|
153
|
+
onPlayheadChange,
|
|
142
154
|
selectedTarget = null,
|
|
143
155
|
}: MotionDockProps) {
|
|
144
156
|
// Controlled / uncontrolled open state.
|
|
@@ -153,13 +165,36 @@ export function MotionDock({
|
|
|
153
165
|
);
|
|
154
166
|
|
|
155
167
|
// Playhead position: normalised [0, 1].
|
|
156
|
-
const [playhead, setPlayhead] = useState(0);
|
|
168
|
+
const [playhead, setPlayhead] = useState(playheadProp ?? 0);
|
|
157
169
|
const [playing, setPlaying] = useState(false);
|
|
158
170
|
const playRafRef = useRef<number | null>(null);
|
|
159
171
|
const playStartRef = useRef<{ wallMs: number; startT: number } | null>(null);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (playheadProp === undefined) return;
|
|
174
|
+
setPlayhead(Math.max(0, Math.min(1, playheadProp)));
|
|
175
|
+
}, [playheadProp]);
|
|
176
|
+
|
|
177
|
+
// Auto-keyframe mode: inspector/style edits create keyframes at the playhead.
|
|
178
|
+
const [autoKeyframeInternal, setAutoKeyframeInternal] = useState(false);
|
|
179
|
+
const autoKeyframe = autoKeyframeProp ?? autoKeyframeInternal;
|
|
180
|
+
const setAutoKeyframe = useCallback(
|
|
181
|
+
(next: boolean | ((current: boolean) => boolean)) => {
|
|
182
|
+
const resolved =
|
|
183
|
+
typeof next === "function"
|
|
184
|
+
? (next as (current: boolean) => boolean)(autoKeyframe)
|
|
185
|
+
: next;
|
|
186
|
+
setAutoKeyframeInternal(resolved);
|
|
187
|
+
onAutoKeyframeChange?.(resolved);
|
|
188
|
+
},
|
|
189
|
+
[autoKeyframe, onAutoKeyframeChange],
|
|
190
|
+
);
|
|
191
|
+
const setPlayheadValue = useCallback(
|
|
192
|
+
(next: number) => {
|
|
193
|
+
setPlayhead(next);
|
|
194
|
+
onPlayheadChange?.(next);
|
|
195
|
+
},
|
|
196
|
+
[onPlayheadChange],
|
|
197
|
+
);
|
|
163
198
|
|
|
164
199
|
// Dock height (resizable via the top drag handle).
|
|
165
200
|
const [dockHeight, setDockHeight] = useState(DEFAULT_DOCK_HEIGHT);
|
|
@@ -235,7 +270,7 @@ export function MotionDock({
|
|
|
235
270
|
if (!playStartRef.current) return;
|
|
236
271
|
const elapsed = now - playStartRef.current.wallMs;
|
|
237
272
|
const t = Math.min(1, playStartRef.current.startT + elapsed / durationMs);
|
|
238
|
-
|
|
273
|
+
setPlayheadValue(t);
|
|
239
274
|
sendPreview(t);
|
|
240
275
|
if (t < 1) {
|
|
241
276
|
playRafRef.current = requestAnimationFrame(tick);
|
|
@@ -244,7 +279,7 @@ export function MotionDock({
|
|
|
244
279
|
}
|
|
245
280
|
};
|
|
246
281
|
playRafRef.current = requestAnimationFrame(tick);
|
|
247
|
-
}, [durationMs, playhead, sendPreview, stopPlayback]);
|
|
282
|
+
}, [durationMs, playhead, sendPreview, setPlayheadValue, stopPlayback]);
|
|
248
283
|
|
|
249
284
|
useEffect(() => {
|
|
250
285
|
return () => {
|
|
@@ -263,10 +298,10 @@ export function MotionDock({
|
|
|
263
298
|
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
264
299
|
const rect = trackAreaRef.current.getBoundingClientRect();
|
|
265
300
|
const t = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
266
|
-
|
|
301
|
+
setPlayheadValue(t);
|
|
267
302
|
sendPreview(t);
|
|
268
303
|
},
|
|
269
|
-
[sendPreview, stopPlayback],
|
|
304
|
+
[sendPreview, setPlayheadValue, stopPlayback],
|
|
270
305
|
);
|
|
271
306
|
|
|
272
307
|
const handleRulerPointerMove = useCallback(
|
|
@@ -274,10 +309,10 @@ export function MotionDock({
|
|
|
274
309
|
if (!isDraggingPlayhead.current || !trackAreaRef.current) return;
|
|
275
310
|
const rect = trackAreaRef.current.getBoundingClientRect();
|
|
276
311
|
const t = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
277
|
-
|
|
312
|
+
setPlayheadValue(t);
|
|
278
313
|
sendPreview(t);
|
|
279
314
|
},
|
|
280
|
-
[sendPreview],
|
|
315
|
+
[sendPreview, setPlayheadValue],
|
|
281
316
|
);
|
|
282
317
|
|
|
283
318
|
const handleRulerPointerUp = useCallback(() => {
|
|
@@ -493,7 +528,7 @@ export function MotionDock({
|
|
|
493
528
|
className="size-6 shrink-0"
|
|
494
529
|
onClick={() => {
|
|
495
530
|
stopPlayback();
|
|
496
|
-
|
|
531
|
+
setPlayheadValue(0);
|
|
497
532
|
sendPreview(0);
|
|
498
533
|
}}
|
|
499
534
|
aria-label="Reset playhead"
|