@agent-native/core 0.80.9 → 0.80.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +13 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +108 -15
- package/corpus/core/src/cli/design-connect.ts +28 -2
- package/corpus/core/src/cli/skills.ts +45 -24
- package/corpus/core/src/client/AssistantChat.tsx +123 -26
- package/corpus/core/src/client/sse-event-processor.ts +9 -0
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
- package/corpus/templates/assets/AGENTS.md +4 -0
- package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
- package/corpus/templates/assets/actions/generate-image.ts +118 -67
- package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
- package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
- package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
- package/corpus/templates/assets/app/i18n-data.ts +61 -0
- package/corpus/templates/assets/app/routes/_index.tsx +4 -0
- package/corpus/templates/assets/app/routes/library.tsx +145 -38
- package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
- package/corpus/templates/assets/server/lib/generation.ts +135 -11
- package/corpus/templates/assets/shared/api.ts +4 -0
- package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
- 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 +441 -0
- package/corpus/templates/design/actions/apply-design-state.ts +213 -0
- package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
- package/corpus/templates/design/actions/apply-motion-edit.ts +413 -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/delete-design.ts +20 -0
- package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
- package/corpus/templates/design/actions/get-component-details.ts +251 -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 +277 -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 +90 -0
- package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
- package/corpus/templates/design/actions/open-component-source.ts +242 -0
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +260 -0
- package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
- package/corpus/templates/design/actions/preview-shader-fill.ts +187 -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 +436 -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 +1550 -82
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +649 -43
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1788 -52
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +114 -33
- package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +1071 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1053 -99
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +766 -0
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +581 -0
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +578 -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 +32 -0
- package/corpus/templates/design/app/i18n-data.ts +280 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +3087 -490
- 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-design-token-edits-now-stay-visible-in-previews-and-token-li.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/global-setup.ts +94 -2
- 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 +274 -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 +349 -0
- package/corpus/templates/design/shared/motion-timeline.ts +193 -0
- package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
- 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/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +94 -10
- package/dist/agent/production-agent.js.map +1 -1
- 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/client/AssistantChat.d.ts +14 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +115 -22
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/sse-event-processor.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 +8 -8
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,21 +1,32 @@
|
|
|
1
|
+
// i18n-raw-literal-disable-file — new Design Studio panel; UI strings are localized when this feature is finalized in the follow-up PR.
|
|
1
2
|
import {
|
|
2
3
|
PromptComposer,
|
|
3
4
|
agentNativePath,
|
|
4
5
|
sendToAgentChat,
|
|
6
|
+
useActionMutation,
|
|
5
7
|
useChangeVersions,
|
|
6
8
|
useT,
|
|
7
9
|
type PromptComposerProps,
|
|
8
10
|
type PromptComposerSubmitOptions,
|
|
9
11
|
} from "@agent-native/core/client";
|
|
10
12
|
import { EmbeddedExtension } from "@agent-native/core/client/extensions";
|
|
13
|
+
import {
|
|
14
|
+
EmbeddedApp,
|
|
15
|
+
type EmbeddedAppRef,
|
|
16
|
+
} from "@agent-native/core/embedding/react";
|
|
17
|
+
import type { ShaderDescriptor } from "@shared/shader-presets";
|
|
11
18
|
import {
|
|
12
19
|
IconExternalLink,
|
|
20
|
+
IconLock,
|
|
21
|
+
IconPalette,
|
|
22
|
+
IconPhoto,
|
|
23
|
+
IconPlayerPlay,
|
|
13
24
|
IconPlus,
|
|
14
25
|
IconPuzzle,
|
|
15
26
|
IconSparkles,
|
|
16
27
|
} from "@tabler/icons-react";
|
|
17
28
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
18
|
-
import { useMemo, useState } from "react";
|
|
29
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
19
30
|
import { toast } from "sonner";
|
|
20
31
|
|
|
21
32
|
import { Button } from "@/components/ui/button";
|
|
@@ -32,10 +43,13 @@ import {
|
|
|
32
43
|
} from "@/components/ui/tooltip";
|
|
33
44
|
import { cn } from "@/lib/utils";
|
|
34
45
|
|
|
46
|
+
import { ShaderFillsPanel } from "./inspector/ShaderFillsPanel";
|
|
35
47
|
import type { ElementInfo } from "./types";
|
|
36
48
|
|
|
37
49
|
export const DESIGN_EDITOR_EXTENSION_SLOT_ID = "design.editor.inspector";
|
|
38
50
|
|
|
51
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
39
53
|
interface SlotInstall {
|
|
40
54
|
installId: string;
|
|
41
55
|
extensionId: string;
|
|
@@ -72,6 +86,8 @@ export interface DesignExtensionSlotContext extends Record<string, unknown> {
|
|
|
72
86
|
mode: string;
|
|
73
87
|
activeTool: string;
|
|
74
88
|
tweakValues: Record<string, string | number | boolean>;
|
|
89
|
+
onShaderFillPreview?: (descriptor: ShaderDescriptor, css: string) => void;
|
|
90
|
+
onShaderFillPreviewClear?: () => void;
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
interface DesignExtensionsPanelProps {
|
|
@@ -81,6 +97,55 @@ interface DesignExtensionsPanelProps {
|
|
|
81
97
|
|
|
82
98
|
type PromptComposerSubmitHandler = PromptComposerProps["onSubmit"];
|
|
83
99
|
|
|
100
|
+
// ─── First-party extension ids ───────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
type FirstPartyExtId =
|
|
103
|
+
| "design.asset-library"
|
|
104
|
+
| "design.shader-fills"
|
|
105
|
+
| "design.token-auditor"
|
|
106
|
+
| "design.motion-presets";
|
|
107
|
+
|
|
108
|
+
// ─── Assets picker types (mirrors PromptDialog) ───────────────────────────
|
|
109
|
+
|
|
110
|
+
const DEFAULT_ASSETS_PICKER_URL = "https://assets.agent-native.com/picker";
|
|
111
|
+
|
|
112
|
+
interface PickedAssetPayload {
|
|
113
|
+
url?: unknown;
|
|
114
|
+
previewUrl?: unknown;
|
|
115
|
+
downloadUrl?: unknown;
|
|
116
|
+
embedUrl?: unknown;
|
|
117
|
+
altText?: unknown;
|
|
118
|
+
title?: unknown;
|
|
119
|
+
assetId?: unknown;
|
|
120
|
+
mimeType?: unknown;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function assetsPickerUrl(): string {
|
|
124
|
+
return (
|
|
125
|
+
(typeof import.meta !== "undefined" &&
|
|
126
|
+
(import.meta as { env?: Record<string, string> }).env
|
|
127
|
+
?.VITE_AGENT_NATIVE_ASSETS_PICKER_URL) ||
|
|
128
|
+
DEFAULT_ASSETS_PICKER_URL
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function pickedAssetString(value: unknown): string | null {
|
|
133
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function pickedAssetImageSource(payload: unknown): string | null {
|
|
137
|
+
if (!payload || typeof payload !== "object") return null;
|
|
138
|
+
const p = payload as PickedAssetPayload;
|
|
139
|
+
return (
|
|
140
|
+
pickedAssetString(p.url) ??
|
|
141
|
+
pickedAssetString(p.previewUrl) ??
|
|
142
|
+
pickedAssetString(p.downloadUrl) ??
|
|
143
|
+
pickedAssetString(p.embedUrl)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Build extension create context ──────────────────────────────────────────
|
|
148
|
+
|
|
84
149
|
function buildExtensionCreateContext(
|
|
85
150
|
prompt: string,
|
|
86
151
|
context: DesignExtensionSlotContext,
|
|
@@ -131,6 +196,8 @@ function buildExtensionCreateContext(
|
|
|
131
196
|
].join("\n");
|
|
132
197
|
}
|
|
133
198
|
|
|
199
|
+
// ─── Hooks ───────────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
134
201
|
function useSlotInstalls(slotId: string) {
|
|
135
202
|
const versions = useChangeVersions(["action"]);
|
|
136
203
|
return useQuery<SlotInstall[]>({
|
|
@@ -165,6 +232,479 @@ function useAvailableExtensions(slotId: string) {
|
|
|
165
232
|
});
|
|
166
233
|
}
|
|
167
234
|
|
|
235
|
+
// ─── First-party extension rows ───────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
interface FirstPartyRowProps {
|
|
238
|
+
id: FirstPartyExtId;
|
|
239
|
+
label: string;
|
|
240
|
+
description: string;
|
|
241
|
+
icon: React.ReactNode;
|
|
242
|
+
badge?: React.ReactNode;
|
|
243
|
+
isOpen: boolean;
|
|
244
|
+
onToggle: () => void;
|
|
245
|
+
children: React.ReactNode;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function FirstPartyExtRow({
|
|
249
|
+
label,
|
|
250
|
+
description,
|
|
251
|
+
icon,
|
|
252
|
+
badge,
|
|
253
|
+
isOpen,
|
|
254
|
+
onToggle,
|
|
255
|
+
children,
|
|
256
|
+
}: FirstPartyRowProps) {
|
|
257
|
+
return (
|
|
258
|
+
<div className="overflow-hidden rounded border border-border bg-background">
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
onClick={onToggle}
|
|
262
|
+
className="flex w-full cursor-pointer items-center gap-2 px-2.5 py-2 text-left transition-colors hover:bg-accent/50 active:bg-accent"
|
|
263
|
+
>
|
|
264
|
+
<span className="flex size-6 shrink-0 items-center justify-center rounded bg-muted text-muted-foreground">
|
|
265
|
+
{icon}
|
|
266
|
+
</span>
|
|
267
|
+
<span className="min-w-0 flex-1">
|
|
268
|
+
<span className="block truncate text-[11px] font-medium text-foreground">
|
|
269
|
+
{label}
|
|
270
|
+
</span>
|
|
271
|
+
<span className="block truncate text-[10px] leading-tight text-muted-foreground">
|
|
272
|
+
{description}
|
|
273
|
+
</span>
|
|
274
|
+
</span>
|
|
275
|
+
{badge}
|
|
276
|
+
</button>
|
|
277
|
+
{isOpen && <div className="border-t border-border/60">{children}</div>}
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ─── Asset Library panel ──────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
interface AssetLibraryPanelProps {
|
|
285
|
+
context: DesignExtensionSlotContext;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
|
|
289
|
+
const t = useT();
|
|
290
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
291
|
+
const [pickerReady, setPickerReady] = useState(false);
|
|
292
|
+
const insertAsset = useActionMutation("insert-asset");
|
|
293
|
+
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (pickerOpen) setPickerReady(false);
|
|
296
|
+
}, [pickerOpen]);
|
|
297
|
+
|
|
298
|
+
const handleReady = useCallback(
|
|
299
|
+
(_payload: unknown, _event: MessageEvent, ref: EmbeddedAppRef) => {
|
|
300
|
+
setPickerReady(true);
|
|
301
|
+
ref.postMessage("configure", {});
|
|
302
|
+
},
|
|
303
|
+
[],
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const handleMessage = useCallback(
|
|
307
|
+
(name: string, payload: unknown) => {
|
|
308
|
+
if (name === "close") {
|
|
309
|
+
setPickerOpen(false);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (name !== "chooseImage" && name !== "chooseAsset") return;
|
|
313
|
+
|
|
314
|
+
const url = pickedAssetImageSource(payload);
|
|
315
|
+
if (!url) {
|
|
316
|
+
toast.error("No image URL returned from Assets picker.");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const p = payload as PickedAssetPayload;
|
|
321
|
+
const title = pickedAssetString(p.title) ?? undefined;
|
|
322
|
+
const altText = pickedAssetString(p.altText) ?? undefined;
|
|
323
|
+
const assetId = pickedAssetString(p.assetId) ?? undefined;
|
|
324
|
+
const mimeType = pickedAssetString(p.mimeType) ?? "";
|
|
325
|
+
const mediaType = mimeType.startsWith("video/") ? "video" : "image";
|
|
326
|
+
|
|
327
|
+
insertAsset.mutate(
|
|
328
|
+
{
|
|
329
|
+
assetUrl: url,
|
|
330
|
+
assetId,
|
|
331
|
+
title,
|
|
332
|
+
altText,
|
|
333
|
+
mediaType: mediaType as "image" | "video",
|
|
334
|
+
designId: context.designId || undefined,
|
|
335
|
+
fileId: context.activeFileId || undefined,
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
onSuccess: () => {
|
|
339
|
+
toast.success("Asset inserted into design.");
|
|
340
|
+
},
|
|
341
|
+
onError: () => {
|
|
342
|
+
toast.error("Failed to insert asset.");
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
setPickerOpen(false);
|
|
348
|
+
},
|
|
349
|
+
[context.designId, context.activeFileId, insertAsset],
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const handleAskAgent = () => {
|
|
353
|
+
sendToAgentChat({
|
|
354
|
+
message:
|
|
355
|
+
"Open the Assets picker so I can choose an image to insert into the design.",
|
|
356
|
+
context: [
|
|
357
|
+
`Design id: ${context.designId}`,
|
|
358
|
+
context.activeFileId ? `Active file id: ${context.activeFileId}` : null,
|
|
359
|
+
context.selectedElement?.selector
|
|
360
|
+
? `Selected element selector: ${context.selectedElement.selector}`
|
|
361
|
+
: null,
|
|
362
|
+
"Call insert-asset with the chosen URL after the user picks from the Assets picker.",
|
|
363
|
+
]
|
|
364
|
+
.filter(Boolean)
|
|
365
|
+
.join("\n"),
|
|
366
|
+
submit: true,
|
|
367
|
+
openSidebar: true,
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
<div className="p-2.5">
|
|
373
|
+
<p className="mb-2 text-[10px] leading-snug text-muted-foreground">
|
|
374
|
+
Browse and insert generated or uploaded images into the active design
|
|
375
|
+
screen. Inserts near the selected element when one is active.
|
|
376
|
+
</p>
|
|
377
|
+
<div className="flex flex-wrap gap-1.5">
|
|
378
|
+
<Button
|
|
379
|
+
type="button"
|
|
380
|
+
size="sm"
|
|
381
|
+
className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
|
|
382
|
+
disabled={insertAsset.isPending || !context.activeFileId}
|
|
383
|
+
onClick={() => setPickerOpen(true)}
|
|
384
|
+
>
|
|
385
|
+
<IconPhoto className="size-3" />
|
|
386
|
+
{insertAsset.isPending ? "Inserting…" : "Browse Assets"}
|
|
387
|
+
</Button>
|
|
388
|
+
<Button
|
|
389
|
+
type="button"
|
|
390
|
+
variant="outline"
|
|
391
|
+
size="sm"
|
|
392
|
+
className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
|
|
393
|
+
onClick={handleAskAgent}
|
|
394
|
+
>
|
|
395
|
+
<IconSparkles className="size-3" />
|
|
396
|
+
{t("designEditor.askAgent") || "Ask agent"}
|
|
397
|
+
</Button>
|
|
398
|
+
</div>
|
|
399
|
+
{!context.activeFileId && (
|
|
400
|
+
<p className="mt-1.5 text-[10px] text-amber-600 dark:text-amber-400">
|
|
401
|
+
Open a design screen first to insert assets.
|
|
402
|
+
</p>
|
|
403
|
+
)}
|
|
404
|
+
|
|
405
|
+
{/* Assets picker overlay */}
|
|
406
|
+
{pickerOpen && (
|
|
407
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
408
|
+
<div
|
|
409
|
+
data-assets-picker-overlay
|
|
410
|
+
className="relative flex h-[min(86vh,760px)] w-[min(96vw,1040px)] flex-col overflow-hidden rounded-xl border border-border bg-popover shadow-2xl"
|
|
411
|
+
>
|
|
412
|
+
<div className="flex h-10 shrink-0 items-center border-b border-border px-4">
|
|
413
|
+
<span className="flex-1 text-sm font-medium">Assets Library</span>
|
|
414
|
+
<button
|
|
415
|
+
type="button"
|
|
416
|
+
aria-label="Close picker"
|
|
417
|
+
onClick={() => setPickerOpen(false)}
|
|
418
|
+
className="flex size-7 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
419
|
+
>
|
|
420
|
+
×
|
|
421
|
+
</button>
|
|
422
|
+
</div>
|
|
423
|
+
<div className="relative min-h-0 flex-1">
|
|
424
|
+
{!pickerReady && (
|
|
425
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
426
|
+
<Skeleton className="h-full w-full rounded-none" />
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
429
|
+
<EmbeddedApp
|
|
430
|
+
url={assetsPickerUrl()}
|
|
431
|
+
title="Assets picker"
|
|
432
|
+
onReady={handleReady}
|
|
433
|
+
onMessage={handleMessage}
|
|
434
|
+
className="h-full w-full"
|
|
435
|
+
/>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
)}
|
|
440
|
+
</div>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ─── Shader Fills panel ───────────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
interface ShaderFillsExtPanelProps {
|
|
447
|
+
context: DesignExtensionSlotContext;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function ShaderFillsExtPanel({ context }: ShaderFillsExtPanelProps) {
|
|
451
|
+
const [showShaders, setShowShaders] = useState(false);
|
|
452
|
+
const clearPreviewRef = useRef(context.onShaderFillPreviewClear);
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
clearPreviewRef.current = context.onShaderFillPreviewClear;
|
|
455
|
+
}, [context.onShaderFillPreviewClear]);
|
|
456
|
+
useEffect(
|
|
457
|
+
() => () => {
|
|
458
|
+
clearPreviewRef.current?.();
|
|
459
|
+
},
|
|
460
|
+
[],
|
|
461
|
+
);
|
|
462
|
+
const closeShaders = () => {
|
|
463
|
+
context.onShaderFillPreviewClear?.();
|
|
464
|
+
setShowShaders(false);
|
|
465
|
+
};
|
|
466
|
+
// The most recently previewed descriptor — Apply persists exactly this one,
|
|
467
|
+
// so the write is intentional (one atomic call) rather than firing on every
|
|
468
|
+
// slider tweak.
|
|
469
|
+
const [previewed, setPreviewed] = useState<ShaderDescriptor | null>(null);
|
|
470
|
+
const applyShaderFill = useActionMutation("apply-shader-fill");
|
|
471
|
+
|
|
472
|
+
// The persisting apply path needs an HTML file plus a target element. Without
|
|
473
|
+
// a selected element we can still preview, but we cannot write a fill.
|
|
474
|
+
const targetNodeId = context.selectedElement?.sourceId ?? undefined;
|
|
475
|
+
const targetSelector = context.selectedElement?.selector ?? undefined;
|
|
476
|
+
const canPersist = Boolean(
|
|
477
|
+
context.activeFileId && (targetNodeId || targetSelector),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const persistFill = (descriptor: ShaderDescriptor) => {
|
|
481
|
+
if (!canPersist) return;
|
|
482
|
+
applyShaderFill.mutate(
|
|
483
|
+
{
|
|
484
|
+
descriptor: {
|
|
485
|
+
preset: descriptor.preset,
|
|
486
|
+
params: descriptor.params,
|
|
487
|
+
colors: descriptor.colors,
|
|
488
|
+
speed: descriptor.speed,
|
|
489
|
+
frame: descriptor.frame,
|
|
490
|
+
fit: descriptor.fit,
|
|
491
|
+
scale: descriptor.scale,
|
|
492
|
+
rotation: descriptor.rotation,
|
|
493
|
+
offsetX: descriptor.offsetX,
|
|
494
|
+
offsetY: descriptor.offsetY,
|
|
495
|
+
},
|
|
496
|
+
target: { nodeId: targetNodeId, selector: targetSelector },
|
|
497
|
+
source: {
|
|
498
|
+
kind: "design-file" as const,
|
|
499
|
+
designId: context.designId || undefined,
|
|
500
|
+
fileId: context.activeFileId || undefined,
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
onSuccess: (res) => {
|
|
505
|
+
const r = res as { persisted?: boolean } | undefined;
|
|
506
|
+
if (r?.persisted) {
|
|
507
|
+
toast.success("Shader fill applied to the selected element.");
|
|
508
|
+
} else {
|
|
509
|
+
toast.message("Shader fill previewed — nothing was written.");
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
onError: () => {
|
|
513
|
+
toast.error("Failed to apply shader fill.");
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
if (!showShaders) {
|
|
520
|
+
return (
|
|
521
|
+
<div className="p-2.5">
|
|
522
|
+
<p className="mb-2 text-[10px] leading-snug text-muted-foreground">
|
|
523
|
+
GPU shader fill presets — MeshGradient, GrainGradient, Voronoi,
|
|
524
|
+
Metaballs, Warp, GodRays, Dithering, PaperTexture. Preview as a CSS
|
|
525
|
+
gradient, then apply it to the selected element as a CSS background.
|
|
526
|
+
</p>
|
|
527
|
+
{!canPersist && (
|
|
528
|
+
<div className="mb-2 flex items-start gap-1.5 rounded-md border border-amber-500/30 bg-amber-500/5 px-2 py-1.5">
|
|
529
|
+
<IconLock className="mt-0.5 size-3 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
530
|
+
<p className="text-[10px] leading-snug text-amber-700 dark:text-amber-300">
|
|
531
|
+
Select an element on the canvas to apply a fill. Without a
|
|
532
|
+
selection you can still browse and preview presets.
|
|
533
|
+
</p>
|
|
534
|
+
</div>
|
|
535
|
+
)}
|
|
536
|
+
<div className="flex flex-wrap gap-1.5">
|
|
537
|
+
<Button
|
|
538
|
+
type="button"
|
|
539
|
+
size="sm"
|
|
540
|
+
className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
|
|
541
|
+
onClick={() => setShowShaders(true)}
|
|
542
|
+
>
|
|
543
|
+
<IconSparkles className="size-3" />
|
|
544
|
+
Browse Shaders
|
|
545
|
+
</Button>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return (
|
|
552
|
+
<div className="flex flex-col">
|
|
553
|
+
<ShaderFillsPanel
|
|
554
|
+
onApply={(descriptor, css) => {
|
|
555
|
+
// Preview only: ShaderFillsPanel fires apply-shader (planning/codegen)
|
|
556
|
+
// for agent context on every tune and the iframe shows the gradient.
|
|
557
|
+
// We just record the latest descriptor here; the explicit Apply
|
|
558
|
+
// button below performs the single intentional persist write.
|
|
559
|
+
setPreviewed(descriptor);
|
|
560
|
+
context.onShaderFillPreview?.(descriptor, css);
|
|
561
|
+
}}
|
|
562
|
+
onBack={closeShaders}
|
|
563
|
+
applyContext={{
|
|
564
|
+
designId: context.designId || undefined,
|
|
565
|
+
fileId: context.activeFileId || undefined,
|
|
566
|
+
nodeId: targetNodeId,
|
|
567
|
+
selector: targetSelector,
|
|
568
|
+
}}
|
|
569
|
+
/>
|
|
570
|
+
|
|
571
|
+
{/* ── Apply bar: persists the previewed fill onto the selected element ── */}
|
|
572
|
+
<div className="flex items-center gap-2 border-t border-border/60 px-3 py-2">
|
|
573
|
+
{canPersist ? (
|
|
574
|
+
<Button
|
|
575
|
+
type="button"
|
|
576
|
+
size="sm"
|
|
577
|
+
className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
|
|
578
|
+
disabled={!previewed || applyShaderFill.isPending}
|
|
579
|
+
onClick={() => {
|
|
580
|
+
if (previewed) persistFill(previewed);
|
|
581
|
+
}}
|
|
582
|
+
>
|
|
583
|
+
<IconSparkles className="size-3" />
|
|
584
|
+
{applyShaderFill.isPending
|
|
585
|
+
? "Applying…"
|
|
586
|
+
: previewed
|
|
587
|
+
? "Apply fill"
|
|
588
|
+
: "Pick a preset"}
|
|
589
|
+
</Button>
|
|
590
|
+
) : (
|
|
591
|
+
<p className="flex items-start gap-1.5 text-[10px] leading-snug text-amber-700 dark:text-amber-300">
|
|
592
|
+
<IconLock className="mt-0.5 size-3 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
593
|
+
Select an element on the canvas to apply this fill.
|
|
594
|
+
</p>
|
|
595
|
+
)}
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ─── Token Auditor panel ─────────────────────────────────────────────────────
|
|
602
|
+
|
|
603
|
+
interface TokenAuditorPanelProps {
|
|
604
|
+
context: DesignExtensionSlotContext;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function TokenAuditorPanel({ context }: TokenAuditorPanelProps) {
|
|
608
|
+
const t = useT();
|
|
609
|
+
|
|
610
|
+
const handleAskAgent = () => {
|
|
611
|
+
sendToAgentChat({
|
|
612
|
+
message:
|
|
613
|
+
"Run a token audit on the active design: index CSS custom properties, surface any hard-coded colours that should be tokens, flag clashes, and suggest fixes.",
|
|
614
|
+
context: [
|
|
615
|
+
`Design id: ${context.designId}`,
|
|
616
|
+
context.activeFileId ? `Active file id: ${context.activeFileId}` : null,
|
|
617
|
+
"Call index-design-tokens to parse CSS vars, then preview-design-token-edit for suggested changes.",
|
|
618
|
+
]
|
|
619
|
+
.filter(Boolean)
|
|
620
|
+
.join("\n"),
|
|
621
|
+
submit: true,
|
|
622
|
+
openSidebar: true,
|
|
623
|
+
});
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
return (
|
|
627
|
+
<div className="p-2.5">
|
|
628
|
+
<p className="mb-2 text-[10px] leading-snug text-muted-foreground">
|
|
629
|
+
Index CSS custom property usage across the active design, surface
|
|
630
|
+
hard-coded colours that should be tokens, and flag clashes.
|
|
631
|
+
</p>
|
|
632
|
+
<div className="mb-1.5 text-[10px] text-muted-foreground">
|
|
633
|
+
Token reads and tweaks are available for all source types. Source
|
|
634
|
+
write-back (globals.css / tailwind.config) is gated on bridge hardening.
|
|
635
|
+
</div>
|
|
636
|
+
<div className="flex flex-wrap gap-1.5">
|
|
637
|
+
<Button
|
|
638
|
+
type="button"
|
|
639
|
+
size="sm"
|
|
640
|
+
className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
|
|
641
|
+
onClick={handleAskAgent}
|
|
642
|
+
>
|
|
643
|
+
<IconSparkles className="size-3" />
|
|
644
|
+
{t("designEditor.askAgent") || "Ask agent"}
|
|
645
|
+
</Button>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ─── Motion Presets panel ─────────────────────────────────────────────────────
|
|
652
|
+
|
|
653
|
+
interface MotionPresetsPanelProps {
|
|
654
|
+
context: DesignExtensionSlotContext;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function MotionPresetsPanel({ context }: MotionPresetsPanelProps) {
|
|
658
|
+
const t = useT();
|
|
659
|
+
|
|
660
|
+
const handleAskAgent = () => {
|
|
661
|
+
sendToAgentChat({
|
|
662
|
+
message:
|
|
663
|
+
"Apply a motion preset to the selected element. Suggest fade-in, slide-up, pulse, bounce, or spin, then call apply-motion-edit to write the keyframes atomically.",
|
|
664
|
+
context: [
|
|
665
|
+
`Design id: ${context.designId}`,
|
|
666
|
+
context.activeFileId ? `Active file id: ${context.activeFileId}` : null,
|
|
667
|
+
context.selectedElement
|
|
668
|
+
? `Selected element: ${JSON.stringify({ selector: context.selectedElement.selector, sourceId: context.selectedElement.sourceId }, null, 2)}`
|
|
669
|
+
: "No element selected — ask the user to click an element first.",
|
|
670
|
+
"Call get-motion-timeline to check for an existing timeline, then preview-motion-frame first, then apply-motion-edit for the atomic write.",
|
|
671
|
+
]
|
|
672
|
+
.filter(Boolean)
|
|
673
|
+
.join("\n"),
|
|
674
|
+
submit: true,
|
|
675
|
+
openSidebar: true,
|
|
676
|
+
});
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
return (
|
|
680
|
+
<div className="p-2.5">
|
|
681
|
+
<p className="mb-2 text-[10px] leading-snug text-muted-foreground">
|
|
682
|
+
One-click animation presets — fade-in, slide-up, pulse, bounce, spin —
|
|
683
|
+
applied to the selected element via the motion timeline.
|
|
684
|
+
</p>
|
|
685
|
+
{!context.selectedElement && (
|
|
686
|
+
<p className="mb-1.5 text-[10px] text-muted-foreground">
|
|
687
|
+
Select an element on the canvas first, then ask the agent to apply a
|
|
688
|
+
motion preset.
|
|
689
|
+
</p>
|
|
690
|
+
)}
|
|
691
|
+
<div className="flex flex-wrap gap-1.5">
|
|
692
|
+
<Button
|
|
693
|
+
type="button"
|
|
694
|
+
size="sm"
|
|
695
|
+
className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
|
|
696
|
+
onClick={handleAskAgent}
|
|
697
|
+
>
|
|
698
|
+
<IconSparkles className="size-3" />
|
|
699
|
+
{t("designEditor.askAgent") || "Ask agent"}
|
|
700
|
+
</Button>
|
|
701
|
+
</div>
|
|
702
|
+
</div>
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ─── Main panel ──────────────────────────────────────────────────────────────
|
|
707
|
+
|
|
168
708
|
export function DesignExtensionsPanel({
|
|
169
709
|
context,
|
|
170
710
|
className,
|
|
@@ -173,6 +713,9 @@ export function DesignExtensionsPanel({
|
|
|
173
713
|
const queryClient = useQueryClient();
|
|
174
714
|
const [createOpen, setCreateOpen] = useState(false);
|
|
175
715
|
const [installingId, setInstallingId] = useState<string | null>(null);
|
|
716
|
+
const [openFirstParty, setOpenFirstParty] = useState<FirstPartyExtId | null>(
|
|
717
|
+
null,
|
|
718
|
+
);
|
|
176
719
|
const slotId = DESIGN_EDITOR_EXTENSION_SLOT_ID;
|
|
177
720
|
const { data: installs = [], isLoading } = useSlotInstalls(slotId);
|
|
178
721
|
const { data: available = [] } = useAvailableExtensions(slotId);
|
|
@@ -184,6 +727,10 @@ export function DesignExtensionsPanel({
|
|
|
184
727
|
(extension) => !installedIds.has(extension.extensionId),
|
|
185
728
|
);
|
|
186
729
|
|
|
730
|
+
const toggleFirstParty = (id: FirstPartyExtId) => {
|
|
731
|
+
setOpenFirstParty((prev) => (prev === id ? null : id));
|
|
732
|
+
};
|
|
733
|
+
|
|
187
734
|
const submitCreatePrompt: PromptComposerSubmitHandler = (
|
|
188
735
|
text: string,
|
|
189
736
|
_files,
|
|
@@ -232,6 +779,45 @@ export function DesignExtensionsPanel({
|
|
|
232
779
|
}
|
|
233
780
|
};
|
|
234
781
|
|
|
782
|
+
// First-party extension row config
|
|
783
|
+
const firstPartyRows: Array<{
|
|
784
|
+
id: FirstPartyExtId;
|
|
785
|
+
label: string;
|
|
786
|
+
description: string;
|
|
787
|
+
icon: React.ReactNode;
|
|
788
|
+
badge?: React.ReactNode;
|
|
789
|
+
panel: React.ReactNode;
|
|
790
|
+
}> = [
|
|
791
|
+
{
|
|
792
|
+
id: "design.asset-library",
|
|
793
|
+
label: "Asset Library",
|
|
794
|
+
description: "Browse & insert assets into the active screen",
|
|
795
|
+
icon: <IconPhoto className="size-3.5" />,
|
|
796
|
+
panel: <AssetLibraryPanel context={context} />,
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
id: "design.shader-fills",
|
|
800
|
+
label: "Shader Fills",
|
|
801
|
+
description: "GPU shader fill presets — preview & apply",
|
|
802
|
+
icon: <IconSparkles className="size-3.5" />,
|
|
803
|
+
panel: <ShaderFillsExtPanel context={context} />,
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
id: "design.token-auditor",
|
|
807
|
+
label: "Token Auditor",
|
|
808
|
+
description: "Audit CSS token usage, flag clashes",
|
|
809
|
+
icon: <IconPalette className="size-3.5" />,
|
|
810
|
+
panel: <TokenAuditorPanel context={context} />,
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
id: "design.motion-presets",
|
|
814
|
+
label: "Motion Presets",
|
|
815
|
+
description: "One-click animation presets for elements",
|
|
816
|
+
icon: <IconPlayerPlay className="size-3.5" />,
|
|
817
|
+
panel: <MotionPresetsPanel context={context} />,
|
|
818
|
+
},
|
|
819
|
+
];
|
|
820
|
+
|
|
235
821
|
return (
|
|
236
822
|
<div className={cn("flex min-h-0 flex-1 flex-col", className)}>
|
|
237
823
|
{/* Section header — 32 px tall, matches PanelSection / design inspector headers */}
|
|
@@ -261,54 +847,47 @@ export function DesignExtensionsPanel({
|
|
|
261
847
|
</div>
|
|
262
848
|
|
|
263
849
|
<div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 pb-2 pt-1.5">
|
|
850
|
+
{/* ── First-party built-in extensions ──────────────────────────── */}
|
|
851
|
+
<div className="mb-3 space-y-1">
|
|
852
|
+
<p className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
853
|
+
Built-in
|
|
854
|
+
</p>
|
|
855
|
+
{firstPartyRows.map((row) => (
|
|
856
|
+
<FirstPartyExtRow
|
|
857
|
+
key={row.id}
|
|
858
|
+
id={row.id}
|
|
859
|
+
label={row.label}
|
|
860
|
+
description={row.description}
|
|
861
|
+
icon={row.icon}
|
|
862
|
+
badge={row.badge}
|
|
863
|
+
isOpen={openFirstParty === row.id}
|
|
864
|
+
onToggle={() => toggleFirstParty(row.id)}
|
|
865
|
+
>
|
|
866
|
+
{row.panel}
|
|
867
|
+
</FirstPartyExtRow>
|
|
868
|
+
))}
|
|
869
|
+
</div>
|
|
870
|
+
|
|
871
|
+
{/* ── Agent help text ───────────────────────────────────────────── */}
|
|
872
|
+
<div className="mb-3 flex items-start gap-1.5 rounded-md bg-muted/40 px-2 py-1.5">
|
|
873
|
+
<IconSparkles className="mt-0.5 size-3 shrink-0 text-muted-foreground/70" />
|
|
874
|
+
<p className="text-[10px] leading-snug text-muted-foreground">
|
|
875
|
+
The agent can insert assets, apply shader fills, audit tokens, and
|
|
876
|
+
apply motion presets. Ask in the chat sidebar for help.
|
|
877
|
+
</p>
|
|
878
|
+
</div>
|
|
879
|
+
|
|
880
|
+
{/* ── User-installed extensions ──────────────────────────────────── */}
|
|
264
881
|
{isLoading ? (
|
|
265
882
|
<div className="space-y-1.5">
|
|
266
883
|
<Skeleton className="h-24 rounded" />
|
|
267
884
|
<Skeleton className="h-32 rounded" />
|
|
268
885
|
</div>
|
|
269
|
-
) : installs.length
|
|
270
|
-
/* Empty state — compact, matches TweaksPanel empty style */
|
|
271
|
-
<div className="flex flex-col items-center gap-2 py-6 text-center">
|
|
272
|
-
<div className="flex size-8 items-center justify-center rounded-lg bg-muted/60">
|
|
273
|
-
<IconPuzzle className="size-4 text-muted-foreground/70" />
|
|
274
|
-
</div>
|
|
275
|
-
<div>
|
|
276
|
-
<p className="text-[11px] font-medium text-foreground">
|
|
277
|
-
{t("designEditor.extensionsEmptyTitle")}
|
|
278
|
-
</p>
|
|
279
|
-
<p className="mt-0.5 text-[11px] leading-snug text-muted-foreground">
|
|
280
|
-
{t("designEditor.extensionsEmptyDescription")}
|
|
281
|
-
</p>
|
|
282
|
-
</div>
|
|
283
|
-
<div className="flex flex-wrap items-center justify-center gap-1.5">
|
|
284
|
-
<Button
|
|
285
|
-
type="button"
|
|
286
|
-
size="sm"
|
|
287
|
-
className="h-6 cursor-pointer px-2.5 text-[11px]"
|
|
288
|
-
onClick={() => setCreateOpen(true)}
|
|
289
|
-
>
|
|
290
|
-
<IconPlus className="size-3" />
|
|
291
|
-
{t("designEditor.addExtension")}
|
|
292
|
-
</Button>
|
|
293
|
-
<Button
|
|
294
|
-
asChild
|
|
295
|
-
type="button"
|
|
296
|
-
variant="outline"
|
|
297
|
-
size="sm"
|
|
298
|
-
className="h-6 cursor-pointer px-2.5 text-[11px]"
|
|
299
|
-
>
|
|
300
|
-
<a
|
|
301
|
-
href="https://www.agent-native.com/docs/extensions"
|
|
302
|
-
target="_blank"
|
|
303
|
-
rel="noreferrer"
|
|
304
|
-
>
|
|
305
|
-
{t("designEditor.extensionsDocs")}
|
|
306
|
-
</a>
|
|
307
|
-
</Button>
|
|
308
|
-
</div>
|
|
309
|
-
</div>
|
|
310
|
-
) : (
|
|
886
|
+
) : installs.length > 0 ? (
|
|
311
887
|
<div className="space-y-1.5">
|
|
888
|
+
<p className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
889
|
+
Installed
|
|
890
|
+
</p>
|
|
312
891
|
{installs.map((install) => (
|
|
313
892
|
<EmbeddedExtension
|
|
314
893
|
key={install.installId}
|
|
@@ -320,8 +899,33 @@ export function DesignExtensionsPanel({
|
|
|
320
899
|
/>
|
|
321
900
|
))}
|
|
322
901
|
</div>
|
|
902
|
+
) : (
|
|
903
|
+
/* Empty state for user extensions — compact */
|
|
904
|
+
<div className="flex flex-col items-center gap-2 py-4 text-center">
|
|
905
|
+
<div className="flex size-7 items-center justify-center rounded-lg bg-muted/60">
|
|
906
|
+
<IconPuzzle className="size-3.5 text-muted-foreground/70" />
|
|
907
|
+
</div>
|
|
908
|
+
<div>
|
|
909
|
+
<p className="text-[11px] font-medium text-foreground">
|
|
910
|
+
{t("designEditor.extensionsEmptyTitle")}
|
|
911
|
+
</p>
|
|
912
|
+
<p className="mt-0.5 text-[10px] leading-snug text-muted-foreground">
|
|
913
|
+
{t("designEditor.extensionsEmptyDescription")}
|
|
914
|
+
</p>
|
|
915
|
+
</div>
|
|
916
|
+
<Button
|
|
917
|
+
type="button"
|
|
918
|
+
size="sm"
|
|
919
|
+
className="h-6 cursor-pointer px-2.5 text-[11px]"
|
|
920
|
+
onClick={() => setCreateOpen(true)}
|
|
921
|
+
>
|
|
922
|
+
<IconPlus className="size-3" />
|
|
923
|
+
{t("designEditor.addExtension")}
|
|
924
|
+
</Button>
|
|
925
|
+
</div>
|
|
323
926
|
)}
|
|
324
927
|
|
|
928
|
+
{/* ── Available (installable) extensions ────────────────────────── */}
|
|
325
929
|
{installable.length > 0 ? (
|
|
326
930
|
<div className="mt-3 border-t border-border/60 pt-1.5">
|
|
327
931
|
<p className="mb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
@@ -358,6 +962,8 @@ export function DesignExtensionsPanel({
|
|
|
358
962
|
);
|
|
359
963
|
}
|
|
360
964
|
|
|
965
|
+
// ─── Create extension popover ─────────────────────────────────────────────────
|
|
966
|
+
|
|
361
967
|
function CreateExtensionPopover({
|
|
362
968
|
open,
|
|
363
969
|
onOpenChange,
|