@agent-native/core 0.80.11 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +47 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/design-connect.ts +419 -19
  5. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  6. package/corpus/core/src/client/chat/index.ts +1 -0
  7. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  8. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  9. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  10. package/corpus/core/src/client/index.ts +1 -0
  11. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  12. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  13. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  14. package/corpus/core/src/db/client.ts +29 -2
  15. package/corpus/core/src/provider-api/index.ts +29 -0
  16. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  17. package/corpus/core/src/styles/agent-native.css +54 -0
  18. package/corpus/core/src/usage/store.ts +2 -2
  19. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  20. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  21. package/corpus/templates/analytics/app/global.css +8 -0
  22. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  23. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  24. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  25. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  26. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  27. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  28. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  29. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  33. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  34. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  35. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  36. package/corpus/templates/assets/app/global.css +15 -0
  37. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  38. package/corpus/templates/assets/app/routes/library.tsx +90 -16
  39. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  40. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  41. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  42. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  43. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  45. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  46. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  47. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  48. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  49. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  50. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  51. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  52. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  53. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  54. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  55. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  56. package/corpus/templates/design/AGENTS.md +27 -0
  57. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  58. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  59. package/corpus/templates/design/actions/apply-component-prop-edit.ts +63 -20
  60. package/corpus/templates/design/actions/apply-motion-edit.ts +67 -69
  61. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  62. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  63. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  64. package/corpus/templates/design/actions/export-zip.ts +23 -8
  65. package/corpus/templates/design/actions/generate-design.ts +53 -1
  66. package/corpus/templates/design/actions/get-component-details.ts +5 -19
  67. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  68. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  69. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  70. package/corpus/templates/design/actions/index-design-tokens.ts +10 -1
  71. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  72. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  73. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  74. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  75. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  76. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  77. package/corpus/templates/design/actions/navigate.ts +18 -3
  78. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  79. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  80. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  81. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  82. package/corpus/templates/design/actions/run-design-audit.ts +53 -4
  83. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  84. package/corpus/templates/design/actions/view-screen.ts +1 -1
  85. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  86. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +230 -3308
  87. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  88. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +776 -287
  89. package/corpus/templates/design/app/components/design/EditPanel.tsx +212 -208
  90. package/corpus/templates/design/app/components/design/LayersPanel.tsx +14 -2
  91. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  92. package/corpus/templates/design/app/components/design/MotionDock.tsx +235 -266
  93. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +490 -55
  94. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +43 -62
  95. package/corpus/templates/design/app/components/design/StatesPanel.tsx +34 -61
  96. package/corpus/templates/design/app/components/design/TokensPanel.tsx +251 -0
  97. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  98. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  99. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  100. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  101. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  102. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  103. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  104. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  105. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  106. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  107. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  108. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  109. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  110. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  111. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  112. package/corpus/templates/design/app/global.css +7 -0
  113. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  114. package/corpus/templates/design/app/i18n/zh-TW.ts +21 -0
  115. package/corpus/templates/design/app/i18n-data.ts +358 -0
  116. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  117. package/corpus/templates/design/app/pages/DesignEditor.tsx +2402 -499
  118. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  119. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  120. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  121. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  122. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  123. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  124. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  125. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  126. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  127. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  128. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  129. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  130. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  131. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  132. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  133. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  134. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  135. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  136. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  137. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  138. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  139. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  140. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  141. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  142. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  143. package/corpus/templates/design/e2e/helpers.ts +168 -65
  144. package/corpus/templates/design/package.json +3 -1
  145. package/corpus/templates/design/server/db/schema.ts +23 -0
  146. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  147. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  148. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  149. package/corpus/templates/design/server/plugins/db.ts +39 -0
  150. package/corpus/templates/design/server/register-secrets.ts +38 -0
  151. package/corpus/templates/design/shared/board-file.ts +228 -0
  152. package/corpus/templates/design/shared/board-objects.ts +288 -0
  153. package/corpus/templates/design/shared/code-layer.ts +52 -0
  154. package/corpus/templates/design/shared/motion-compiler.ts +58 -7
  155. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  156. package/corpus/templates/design/tsconfig.json +2 -1
  157. package/corpus/templates/slides/AGENTS.md +9 -0
  158. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  159. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  160. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  161. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  162. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  163. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  164. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  165. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  166. package/corpus/templates/slides/package.json +1 -0
  167. package/corpus/templates/slides/vite.config.ts +1 -0
  168. package/dist/cli/design-connect.d.ts +25 -0
  169. package/dist/cli/design-connect.d.ts.map +1 -1
  170. package/dist/cli/design-connect.js +357 -18
  171. package/dist/cli/design-connect.js.map +1 -1
  172. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  173. package/dist/client/ConnectBuilderCard.js +1 -0
  174. package/dist/client/ConnectBuilderCard.js.map +1 -1
  175. package/dist/client/chat/index.d.ts +1 -1
  176. package/dist/client/chat/index.d.ts.map +1 -1
  177. package/dist/client/chat/index.js +1 -1
  178. package/dist/client/chat/index.js.map +1 -1
  179. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  180. package/dist/client/chat/run-recovery.js +5 -5
  181. package/dist/client/chat/run-recovery.js.map +1 -1
  182. package/dist/client/chat-view-transition.d.ts +6 -0
  183. package/dist/client/chat-view-transition.d.ts.map +1 -1
  184. package/dist/client/chat-view-transition.js +23 -2
  185. package/dist/client/chat-view-transition.js.map +1 -1
  186. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  187. package/dist/client/composer/PromptComposer.js +3 -2
  188. package/dist/client/composer/PromptComposer.js.map +1 -1
  189. package/dist/client/index.d.ts +1 -1
  190. package/dist/client/index.d.ts.map +1 -1
  191. package/dist/client/index.js +1 -1
  192. package/dist/client/index.js.map +1 -1
  193. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  194. package/dist/client/org/OrgSwitcher.js +11 -4
  195. package/dist/client/org/OrgSwitcher.js.map +1 -1
  196. package/dist/client/sharing/ShareButton.js +3 -3
  197. package/dist/client/sharing/ShareButton.js.map +1 -1
  198. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  199. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  200. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  201. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  202. package/dist/collab/awareness.d.ts +2 -2
  203. package/dist/collab/awareness.d.ts.map +1 -1
  204. package/dist/collab/routes.d.ts +2 -2
  205. package/dist/db/client.d.ts +1 -0
  206. package/dist/db/client.d.ts.map +1 -1
  207. package/dist/db/client.js +28 -2
  208. package/dist/db/client.js.map +1 -1
  209. package/dist/notifications/routes.d.ts +3 -3
  210. package/dist/observability/routes.d.ts +2 -2
  211. package/dist/progress/routes.d.ts +1 -1
  212. package/dist/provider-api/index.d.ts +5 -4
  213. package/dist/provider-api/index.d.ts.map +1 -1
  214. package/dist/provider-api/index.js +28 -0
  215. package/dist/provider-api/index.js.map +1 -1
  216. package/dist/resources/handlers.d.ts +3 -3
  217. package/dist/server/core-routes-plugin.d.ts +25 -0
  218. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  219. package/dist/server/core-routes-plugin.js +43 -18
  220. package/dist/server/core-routes-plugin.js.map +1 -1
  221. package/dist/server/transcribe-voice.d.ts +1 -1
  222. package/dist/styles/agent-native.css +54 -0
  223. package/dist/usage/store.js +2 -2
  224. package/dist/usage/store.js.map +1 -1
  225. package/package.json +1 -1
@@ -1,13 +1,11 @@
1
1
  // i18n-raw-literal-disable-file — new Design Studio panel; UI strings are localized when this feature is finalized in the follow-up PR.
2
2
  import {
3
- PromptComposer,
4
3
  agentNativePath,
5
4
  sendToAgentChat,
5
+ useActionQuery,
6
6
  useActionMutation,
7
7
  useChangeVersions,
8
8
  useT,
9
- type PromptComposerProps,
10
- type PromptComposerSubmitOptions,
11
9
  } from "@agent-native/core/client";
12
10
  import { EmbeddedExtension } from "@agent-native/core/client/extensions";
13
11
  import {
@@ -16,31 +14,45 @@ import {
16
14
  } from "@agent-native/core/embedding/react";
17
15
  import type { ShaderDescriptor } from "@shared/shader-presets";
18
16
  import {
17
+ IconAdjustmentsHorizontal,
18
+ IconAssembly,
19
+ IconBrandFigma,
20
+ IconChevronDown,
19
21
  IconExternalLink,
20
22
  IconLock,
21
- IconPalette,
22
23
  IconPhoto,
23
24
  IconPlayerPlay,
24
- IconPlus,
25
25
  IconPuzzle,
26
+ IconSearch,
26
27
  IconSparkles,
27
28
  } from "@tabler/icons-react";
28
29
  import { useQuery, useQueryClient } from "@tanstack/react-query";
29
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
30
+ import {
31
+ useCallback,
32
+ useEffect,
33
+ useMemo,
34
+ useRef,
35
+ useState,
36
+ type FormEvent,
37
+ } from "react";
30
38
  import { toast } from "sonner";
31
39
 
32
40
  import { Button } from "@/components/ui/button";
41
+ import {
42
+ DropdownMenu,
43
+ DropdownMenuContent,
44
+ DropdownMenuItem,
45
+ DropdownMenuTrigger,
46
+ } from "@/components/ui/dropdown-menu";
47
+ import { Input } from "@/components/ui/input";
33
48
  import {
34
49
  Popover,
35
50
  PopoverContent,
36
51
  PopoverTrigger,
37
52
  } from "@/components/ui/popover";
38
53
  import { Skeleton } from "@/components/ui/skeleton";
39
- import {
40
- Tooltip,
41
- TooltipContent,
42
- TooltipTrigger,
43
- } from "@/components/ui/tooltip";
54
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
55
+ import { Textarea } from "@/components/ui/textarea";
44
56
  import { cn } from "@/lib/utils";
45
57
 
46
58
  import { ShaderFillsPanel } from "./inspector/ShaderFillsPanel";
@@ -74,6 +86,8 @@ export interface DesignExtensionSlotContext extends Record<string, unknown> {
74
86
  designTitle: string | null;
75
87
  activeFileId: string | null;
76
88
  activeFilename: string | null;
89
+ activeFileUpdatedAt: string | null;
90
+ activeContent: string;
77
91
  viewMode: "single" | "overview";
78
92
  zoom: number;
79
93
  screens: Array<{
@@ -88,14 +102,21 @@ export interface DesignExtensionSlotContext extends Record<string, unknown> {
88
102
  tweakValues: Record<string, string | number | boolean>;
89
103
  onShaderFillPreview?: (descriptor: ShaderDescriptor, css: string) => void;
90
104
  onShaderFillPreviewClear?: () => void;
105
+ onShaderFillApplied?: (
106
+ fileId: string,
107
+ content: string,
108
+ updatedAt?: string,
109
+ ) => void;
91
110
  }
92
111
 
93
112
  interface DesignExtensionsPanelProps {
94
113
  context: DesignExtensionSlotContext;
95
114
  className?: string;
115
+ hideAssetLibrary?: boolean;
116
+ title?: string;
96
117
  }
97
118
 
98
- type PromptComposerSubmitHandler = PromptComposerProps["onSubmit"];
119
+ type CreateExtensionSubmitHandler = (text: string) => void;
99
120
 
100
121
  // ─── First-party extension ids ───────────────────────────────────────────────
101
122
 
@@ -105,9 +126,23 @@ type FirstPartyExtId =
105
126
  | "design.token-auditor"
106
127
  | "design.motion-presets";
107
128
 
129
+ type ToolSourceFilter = "all" | "built-in" | "extensions";
130
+ type ToolCategoryFilter = "all" | "shader" | "tokens" | "motion" | "plugins";
131
+
132
+ type FirstPartyRow = {
133
+ id: FirstPartyExtId;
134
+ label: string;
135
+ description: string;
136
+ category: ToolCategoryFilter;
137
+ icon: React.ReactNode;
138
+ badge?: React.ReactNode;
139
+ panel: React.ReactNode;
140
+ };
141
+
108
142
  // ─── Assets picker types (mirrors PromptDialog) ───────────────────────────
109
143
 
110
- const DEFAULT_ASSETS_PICKER_URL = "https://assets.agent-native.com/picker";
144
+ const DEFAULT_ASSETS_PICKER_URL =
145
+ "https://assets.agent-native.com/library?__an_picker=1&mediaType=image&layout=vertical";
111
146
 
112
147
  interface PickedAssetPayload {
113
148
  url?: unknown;
@@ -121,12 +156,30 @@ interface PickedAssetPayload {
121
156
  }
122
157
 
123
158
  function assetsPickerUrl(): string {
124
- return (
159
+ const configured =
125
160
  (typeof import.meta !== "undefined" &&
126
161
  (import.meta as { env?: Record<string, string> }).env
127
162
  ?.VITE_AGENT_NATIVE_ASSETS_PICKER_URL) ||
128
- DEFAULT_ASSETS_PICKER_URL
129
- );
163
+ DEFAULT_ASSETS_PICKER_URL;
164
+ try {
165
+ const base =
166
+ typeof window !== "undefined"
167
+ ? window.location.origin
168
+ : "https://assets.agent-native.com";
169
+ const url = new URL(configured, base);
170
+ if (url.pathname === "/picker") url.pathname = "/library";
171
+ url.searchParams.set("__an_picker", "1");
172
+ url.searchParams.set(
173
+ "mediaType",
174
+ url.searchParams.get("mediaType") || "image",
175
+ );
176
+ url.searchParams.set("layout", "vertical");
177
+ url.searchParams.set("embedded", "1");
178
+ url.searchParams.set("callerAppId", "design");
179
+ return url.toString();
180
+ } catch {
181
+ return configured;
182
+ }
130
183
  }
131
184
 
132
185
  function pickedAssetString(value: unknown): string | null {
@@ -154,7 +207,7 @@ function buildExtensionCreateContext(
154
207
  ? JSON.stringify(context.selectedElement, null, 2)
155
208
  : "No element is currently selected.";
156
209
  return [
157
- `The user is in the Design editor Extensions inspector for design id "${context.designId}"${context.designTitle ? ` (title: "${context.designTitle}")` : ""}.`,
210
+ `The user is in the Design editor Tools panel for design id "${context.designId}"${context.designTitle ? ` (title: "${context.designTitle}")` : ""}.`,
158
211
  context.activeFileId
159
212
  ? `Active screen: "${context.activeFilename ?? context.activeFileId}" (file id: "${context.activeFileId}").`
160
213
  : "There is no active screen yet.",
@@ -162,7 +215,7 @@ function buildExtensionCreateContext(
162
215
  `User request: "${prompt}"`,
163
216
  "",
164
217
  "After create-extension succeeds, call add-extension-slot-target with this slot id, then install-extension with this slot id so the extension appears in the editor panel immediately.",
165
- 'If create-extension opens the standalone extension editor, call navigate with view "editor", the current design id, and inspectorTab "extensions" after install so the user returns to this inline panel.',
218
+ 'If create-extension opens the standalone extension editor, call navigate with view "editor", the current design id, and leftPanel "tools" after install so the user returns to this inline panel.',
166
219
  "",
167
220
  "The extension will receive window.slotContext and onSlotContext updates with the current design selection:",
168
221
  JSON.stringify(
@@ -247,7 +300,6 @@ interface FirstPartyRowProps {
247
300
 
248
301
  function FirstPartyExtRow({
249
302
  label,
250
- description,
251
303
  icon,
252
304
  badge,
253
305
  isOpen,
@@ -255,50 +307,171 @@ function FirstPartyExtRow({
255
307
  children,
256
308
  }: FirstPartyRowProps) {
257
309
  return (
258
- <div className="overflow-hidden rounded border border-border bg-background">
310
+ <div className="overflow-hidden rounded-md">
259
311
  <button
260
312
  type="button"
261
313
  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"
314
+ className="group flex w-full cursor-pointer items-center gap-3 rounded-md px-2 py-2.5 text-left transition-colors hover:bg-accent/60 active:bg-accent"
263
315
  >
264
- <span className="flex size-6 shrink-0 items-center justify-center rounded bg-muted text-muted-foreground">
316
+ <span className="flex size-10 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/70 text-muted-foreground transition-colors group-hover:border-border group-hover:bg-muted">
265
317
  {icon}
266
318
  </span>
267
319
  <span className="min-w-0 flex-1">
268
- <span className="block truncate text-[11px] font-medium text-foreground">
320
+ <span className="block truncate text-sm font-medium leading-tight text-foreground">
269
321
  {label}
270
322
  </span>
271
- <span className="block truncate text-[10px] leading-tight text-muted-foreground">
272
- {description}
323
+ <span className="mt-0.5 flex items-center gap-1.5 truncate text-xs leading-none text-muted-foreground">
324
+ <IconPuzzle className="size-3 shrink-0" />
325
+ <span className="truncate">Built-in</span>
273
326
  </span>
274
327
  </span>
275
328
  {badge}
276
329
  </button>
277
- {isOpen && <div className="border-t border-border/60">{children}</div>}
330
+ {isOpen && (
331
+ <div className="mb-2 ml-[3.75rem] overflow-hidden rounded-md border border-border/70 bg-background/70">
332
+ {children}
333
+ </div>
334
+ )}
278
335
  </div>
279
336
  );
280
337
  }
281
338
 
339
+ function ToolFilterMenu<T extends string>({
340
+ label,
341
+ value,
342
+ options,
343
+ onChange,
344
+ }: {
345
+ label: string;
346
+ value: T;
347
+ options: Array<{ value: T; label: string }>;
348
+ onChange: (value: T) => void;
349
+ }) {
350
+ const selected = options.find((option) => option.value === value);
351
+ return (
352
+ <DropdownMenu>
353
+ <DropdownMenuTrigger asChild>
354
+ <Button
355
+ type="button"
356
+ variant="outline"
357
+ size="sm"
358
+ className="h-8 cursor-pointer gap-1.5 rounded-md bg-transparent px-2.5 text-sm font-medium"
359
+ >
360
+ <IconAdjustmentsHorizontal className="size-4 text-muted-foreground" />
361
+ <span>{selected?.label ?? label}</span>
362
+ <IconChevronDown className="size-3.5 text-muted-foreground" />
363
+ </Button>
364
+ </DropdownMenuTrigger>
365
+ <DropdownMenuContent align="start" className="min-w-36">
366
+ {options.map((option) => (
367
+ <DropdownMenuItem
368
+ key={option.value}
369
+ onClick={() => onChange(option.value)}
370
+ >
371
+ {option.label}
372
+ </DropdownMenuItem>
373
+ ))}
374
+ </DropdownMenuContent>
375
+ </DropdownMenu>
376
+ );
377
+ }
378
+
282
379
  // ─── Asset Library panel ──────────────────────────────────────────────────────
283
380
 
284
381
  interface AssetLibraryPanelProps {
285
382
  context: DesignExtensionSlotContext;
286
383
  }
287
384
 
288
- function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
289
- const t = useT();
290
- const [pickerOpen, setPickerOpen] = useState(false);
291
- const [pickerReady, setPickerReady] = useState(false);
385
+ type FigmaLibraryAsset = {
386
+ id: string;
387
+ kind: "component" | "component_set";
388
+ fileKey: string;
389
+ nodeId: string | null;
390
+ componentKey: string | null;
391
+ name: string;
392
+ description: string | null;
393
+ thumbnailUrl: string | null;
394
+ renderUrl: string | null;
395
+ insertUrl: string | null;
396
+ sourceUrl: string | null;
397
+ containingFrame: { name: string | null; nodeId: string | null } | null;
398
+ updatedAt: string | null;
399
+ };
400
+
401
+ type FigmaLibraryResponse = {
402
+ fileKey: string;
403
+ total: number;
404
+ returned: number;
405
+ assets: FigmaLibraryAsset[];
406
+ };
407
+
408
+ type DesignNativeAssetKind =
409
+ | "section-frame"
410
+ | "text-block"
411
+ | "button"
412
+ | "card"
413
+ | "input"
414
+ | "nav-bar"
415
+ | "hero"
416
+ | "feature-grid";
417
+
418
+ type DesignNativeAsset = {
419
+ kind: DesignNativeAssetKind;
420
+ title: string;
421
+ description: string;
422
+ category: "primitive" | "component" | "layout";
423
+ componentName: string;
424
+ };
425
+
426
+ type DesignNativeAssetsResponse = {
427
+ source: "design-native";
428
+ assets: DesignNativeAsset[];
429
+ };
430
+
431
+ const NATIVE_ASSET_CATEGORY_LABELS: Record<
432
+ DesignNativeAsset["category"],
433
+ string
434
+ > = {
435
+ primitive: "Primitive",
436
+ component: "Component",
437
+ layout: "Layout",
438
+ };
439
+
440
+ export function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
441
+ const [assetPickerReady, setAssetPickerReady] = useState(false);
442
+ const [figmaFileInput, setFigmaFileInput] = useState("");
443
+ const [figmaQueryInput, setFigmaQueryInput] = useState("");
444
+ const [figmaRequest, setFigmaRequest] = useState<{
445
+ fileUrl: string;
446
+ query?: string;
447
+ } | null>(null);
448
+ const nativeAssets = useActionQuery<DesignNativeAssetsResponse>(
449
+ "list-design-native-assets",
450
+ );
451
+ const insertNativeAsset = useActionMutation("insert-design-native-asset");
292
452
  const insertAsset = useActionMutation("insert-asset");
293
-
294
- useEffect(() => {
295
- if (pickerOpen) setPickerReady(false);
296
- }, [pickerOpen]);
453
+ const insertFigmaAsset = useActionMutation("insert-figma-library-asset");
454
+ const figmaAssets = useActionQuery<FigmaLibraryResponse>(
455
+ "list-figma-library-assets",
456
+ figmaRequest
457
+ ? {
458
+ fileUrl: figmaRequest.fileUrl,
459
+ query: figmaRequest.query,
460
+ limit: 48,
461
+ renderFormat: "svg",
462
+ }
463
+ : undefined,
464
+ { enabled: Boolean(figmaRequest) },
465
+ );
297
466
 
298
467
  const handleReady = useCallback(
299
468
  (_payload: unknown, _event: MessageEvent, ref: EmbeddedAppRef) => {
300
- setPickerReady(true);
301
- ref.postMessage("configure", {});
469
+ setAssetPickerReady(true);
470
+ ref.postMessage("configure", {
471
+ mediaType: "image",
472
+ layout: "vertical",
473
+ callerAppId: "design",
474
+ });
302
475
  },
303
476
  [],
304
477
  );
@@ -306,7 +479,7 @@ function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
306
479
  const handleMessage = useCallback(
307
480
  (name: string, payload: unknown) => {
308
481
  if (name === "close") {
309
- setPickerOpen(false);
482
+ setAssetPickerReady(false);
310
483
  return;
311
484
  }
312
485
  if (name !== "chooseImage" && name !== "chooseAsset") return;
@@ -343,99 +516,293 @@ function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
343
516
  },
344
517
  },
345
518
  );
346
-
347
- setPickerOpen(false);
348
519
  },
349
520
  [context.designId, context.activeFileId, insertAsset],
350
521
  );
351
522
 
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
- });
523
+ const handleInsertNativeAsset = (asset: DesignNativeAsset) => {
524
+ insertNativeAsset.mutate(
525
+ {
526
+ kind: asset.kind,
527
+ designId: context.designId || undefined,
528
+ fileId: context.activeFileId || undefined,
529
+ },
530
+ {
531
+ onSuccess: () => {
532
+ toast.success(`${asset.title} inserted into design.`);
533
+ },
534
+ onError: (error) => {
535
+ toast.error(error.message || "Failed to insert native asset.");
536
+ },
537
+ },
538
+ );
539
+ };
540
+
541
+ const handleBrowseFigma = (event: FormEvent<HTMLFormElement>) => {
542
+ event.preventDefault();
543
+ const fileUrl = figmaFileInput.trim();
544
+ if (!fileUrl) return;
545
+ const query = figmaQueryInput.trim();
546
+ setFigmaRequest({ fileUrl, query: query || undefined });
547
+ };
548
+
549
+ const handleInsertFigmaAsset = (asset: FigmaLibraryAsset) => {
550
+ const renderUrl = asset.insertUrl ?? asset.renderUrl ?? asset.thumbnailUrl;
551
+ if (!renderUrl) {
552
+ toast.error("Figma did not return an insertable render URL.");
553
+ return;
554
+ }
555
+ insertFigmaAsset.mutate(
556
+ {
557
+ renderUrl,
558
+ fileKey: asset.fileKey,
559
+ nodeId: asset.nodeId ?? undefined,
560
+ componentKey: asset.componentKey ?? undefined,
561
+ kind: asset.kind,
562
+ name: asset.name,
563
+ description: asset.description ?? undefined,
564
+ sourceUrl: asset.sourceUrl ?? undefined,
565
+ designId: context.designId || undefined,
566
+ fileId: context.activeFileId || undefined,
567
+ },
568
+ {
569
+ onSuccess: () => {
570
+ toast.success("Figma asset inserted into design.");
571
+ },
572
+ onError: (error) => {
573
+ toast.error(error.message || "Failed to insert Figma asset.");
574
+ },
575
+ },
576
+ );
369
577
  };
370
578
 
371
579
  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)}
580
+ <div className="flex min-h-0 flex-1 flex-col">
581
+ <Tabs defaultValue="native" className="flex min-h-0 flex-1 flex-col">
582
+ <div className="border-b border-border/60 px-2 py-1.5">
583
+ <TabsList className="grid h-7 w-full grid-cols-3 rounded-md p-0.5">
584
+ <TabsTrigger value="native" className="h-6 gap-1 px-2 text-[11px]">
585
+ <IconAssembly className="size-3" />
586
+ Native
587
+ </TabsTrigger>
588
+ <TabsTrigger value="media" className="h-6 gap-1 px-2 text-[11px]">
589
+ <IconPhoto className="size-3" />
590
+ Media
591
+ </TabsTrigger>
592
+ <TabsTrigger value="figma" className="h-6 gap-1 px-2 text-[11px]">
593
+ <IconBrandFigma className="size-3" />
594
+ Figma
595
+ </TabsTrigger>
596
+ </TabsList>
597
+ </div>
598
+
599
+ <TabsContent
600
+ value="native"
601
+ className="design-inspector-scroll m-0 min-h-0 flex-1 overflow-y-auto p-2.5"
384
602
  >
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}
603
+ {nativeAssets.isLoading && (
604
+ <div className="space-y-1.5">
605
+ {Array.from({ length: 5 }).map((_, index) => (
606
+ <Skeleton key={index} className="h-16 rounded-md" />
607
+ ))}
608
+ </div>
609
+ )}
610
+
611
+ {nativeAssets.isError && (
612
+ <p className="rounded border border-destructive/30 bg-destructive/5 px-2 py-1.5 text-[10px] leading-snug text-destructive">
613
+ {nativeAssets.error.message}
614
+ </p>
615
+ )}
616
+
617
+ {!nativeAssets.isLoading && nativeAssets.data && (
618
+ <div className="space-y-1.5">
619
+ {nativeAssets.data.assets.map((asset) => (
620
+ <button
621
+ key={asset.kind}
622
+ type="button"
623
+ disabled={
624
+ !context.activeFileId || insertNativeAsset.isPending
625
+ }
626
+ onClick={() => handleInsertNativeAsset(asset)}
627
+ className="group flex w-full cursor-pointer items-start gap-2 rounded-md border border-border bg-background px-2 py-2 text-left transition hover:border-primary/50 hover:bg-accent/30 disabled:cursor-not-allowed disabled:opacity-55"
628
+ >
629
+ <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/50 text-muted-foreground group-hover:text-foreground">
630
+ <IconAssembly className="size-4" />
631
+ </span>
632
+ <span className="min-w-0 flex-1">
633
+ <span className="flex items-center gap-1.5">
634
+ <span className="truncate text-[11px] font-medium leading-tight text-foreground">
635
+ {asset.title}
636
+ </span>
637
+ <span className="shrink-0 rounded border border-border/70 px-1 py-0.5 text-[9px] leading-none text-muted-foreground">
638
+ {NATIVE_ASSET_CATEGORY_LABELS[asset.category]}
639
+ </span>
640
+ </span>
641
+ <span className="mt-1 line-clamp-2 text-[10px] leading-snug text-muted-foreground">
642
+ {asset.description}
643
+ </span>
644
+ </span>
645
+ </button>
646
+ ))}
647
+ </div>
648
+ )}
649
+ </TabsContent>
650
+
651
+ <TabsContent
652
+ value="media"
653
+ className="m-0 flex min-h-0 flex-1 flex-col overflow-hidden"
394
654
  >
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
- )}
655
+ <div className="relative min-h-[540px] flex-1 overflow-hidden bg-background">
656
+ {!assetPickerReady && (
657
+ <div className="absolute inset-0 z-10 p-2.5">
658
+ <Skeleton className="h-full w-full rounded-none" />
659
+ </div>
660
+ )}
661
+ <EmbeddedApp
662
+ url={assetsPickerUrl()}
663
+ title="Assets picker"
664
+ onLoad={() => setAssetPickerReady(true)}
665
+ onReady={handleReady}
666
+ onMessage={handleMessage}
667
+ className="h-full w-full"
668
+ />
669
+ </div>
670
+ </TabsContent>
404
671
 
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"
672
+ <TabsContent
673
+ value="figma"
674
+ className="design-inspector-scroll m-0 min-h-0 flex-1 overflow-y-auto p-2.5"
675
+ >
676
+ <form className="space-y-1.5" onSubmit={handleBrowseFigma}>
677
+ <Input
678
+ value={figmaFileInput}
679
+ onChange={(event) => setFigmaFileInput(event.target.value)}
680
+ placeholder="Figma file URL or key"
681
+ className="h-7 text-[11px]"
682
+ />
683
+ <div className="flex gap-1.5">
684
+ <Input
685
+ value={figmaQueryInput}
686
+ onChange={(event) => setFigmaQueryInput(event.target.value)}
687
+ placeholder="Search components"
688
+ className="h-7 min-w-0 flex-1 text-[11px]"
689
+ />
690
+ <Button
691
+ type="submit"
692
+ size="sm"
693
+ className="h-7 cursor-pointer gap-1 px-2 text-[11px]"
694
+ disabled={!figmaFileInput.trim() || figmaAssets.isFetching}
419
695
  >
420
- ×
421
- </button>
696
+ <IconSearch className="size-3" />
697
+ {figmaAssets.isFetching ? "Loading" : "Browse"}
698
+ </Button>
422
699
  </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
- />
700
+ </form>
701
+
702
+ {figmaAssets.isError && (
703
+ <p className="mt-2 rounded border border-destructive/30 bg-destructive/5 px-2 py-1.5 text-[10px] leading-snug text-destructive">
704
+ {figmaAssets.error.message}
705
+ </p>
706
+ )}
707
+
708
+ {figmaAssets.isFetching && (
709
+ <div className="mt-2 grid grid-cols-2 gap-1.5">
710
+ {Array.from({ length: 4 }).map((_, index) => (
711
+ <Skeleton key={index} className="aspect-square rounded-md" />
712
+ ))}
436
713
  </div>
437
- </div>
438
- </div>
714
+ )}
715
+
716
+ {!figmaAssets.isFetching &&
717
+ figmaAssets.data &&
718
+ figmaAssets.data.assets.length === 0 && (
719
+ <p className="mt-2 rounded border border-border bg-muted/30 px-2 py-1.5 text-[10px] leading-snug text-muted-foreground">
720
+ No components matched this Figma file and search.
721
+ </p>
722
+ )}
723
+
724
+ {!figmaAssets.isFetching &&
725
+ figmaAssets.data &&
726
+ figmaAssets.data.assets.length > 0 && (
727
+ <div className="mt-2 space-y-1.5">
728
+ <div className="flex items-center justify-between gap-2 text-[10px] text-muted-foreground">
729
+ <span>
730
+ {figmaAssets.data.returned} of {figmaAssets.data.total}
731
+ </span>
732
+ <span className="truncate">{figmaAssets.data.fileKey}</span>
733
+ </div>
734
+ <div className="grid grid-cols-2 gap-1.5">
735
+ {figmaAssets.data.assets.map((asset) => {
736
+ const preview = asset.thumbnailUrl ?? asset.renderUrl;
737
+ const canInsert = Boolean(
738
+ context.activeFileId &&
739
+ (asset.insertUrl ??
740
+ asset.renderUrl ??
741
+ asset.thumbnailUrl),
742
+ );
743
+ return (
744
+ <div
745
+ key={asset.id}
746
+ className="overflow-hidden rounded-md border border-border bg-background"
747
+ >
748
+ <button
749
+ type="button"
750
+ className="block aspect-square w-full cursor-pointer bg-muted/30"
751
+ disabled={!canInsert || insertFigmaAsset.isPending}
752
+ onClick={() => handleInsertFigmaAsset(asset)}
753
+ >
754
+ {preview ? (
755
+ <img
756
+ src={preview}
757
+ alt={asset.name}
758
+ className="size-full object-contain p-2"
759
+ loading="lazy"
760
+ />
761
+ ) : (
762
+ <span className="flex size-full items-center justify-center text-muted-foreground">
763
+ <IconAssembly className="size-5" />
764
+ </span>
765
+ )}
766
+ </button>
767
+ <div className="border-t border-border/70 px-1.5 py-1">
768
+ <div className="flex items-center gap-1">
769
+ <span className="min-w-0 flex-1 truncate text-[10px] font-medium leading-tight">
770
+ {asset.name}
771
+ </span>
772
+ {asset.sourceUrl && (
773
+ <a
774
+ href={asset.sourceUrl}
775
+ target="_blank"
776
+ rel="noreferrer"
777
+ className="shrink-0 text-muted-foreground hover:text-foreground"
778
+ aria-label="Open in Figma"
779
+ >
780
+ <IconExternalLink className="size-3" />
781
+ </a>
782
+ )}
783
+ </div>
784
+ <p className="mt-0.5 truncate text-[9px] text-muted-foreground">
785
+ {asset.kind === "component_set"
786
+ ? "Component set"
787
+ : "Component"}
788
+ {asset.containingFrame?.name
789
+ ? ` · ${asset.containingFrame.name}`
790
+ : ""}
791
+ </p>
792
+ </div>
793
+ </div>
794
+ );
795
+ })}
796
+ </div>
797
+ </div>
798
+ )}
799
+ </TabsContent>
800
+ </Tabs>
801
+
802
+ {!context.activeFileId && (
803
+ <p className="border-t border-border/60 px-2.5 py-1.5 text-[10px] text-amber-600 dark:text-amber-400">
804
+ Open a design screen first to insert assets.
805
+ </p>
439
806
  )}
440
807
  </div>
441
808
  );
@@ -447,6 +814,13 @@ interface ShaderFillsExtPanelProps {
447
814
  context: DesignExtensionSlotContext;
448
815
  }
449
816
 
817
+ interface PreviewedShaderFill {
818
+ descriptor: ShaderDescriptor;
819
+ fileId?: string;
820
+ nodeId?: string;
821
+ selector?: string;
822
+ }
823
+
450
824
  function ShaderFillsExtPanel({ context }: ShaderFillsExtPanelProps) {
451
825
  const [showShaders, setShowShaders] = useState(false);
452
826
  const clearPreviewRef = useRef(context.onShaderFillPreviewClear);
@@ -466,7 +840,7 @@ function ShaderFillsExtPanel({ context }: ShaderFillsExtPanelProps) {
466
840
  // The most recently previewed descriptor — Apply persists exactly this one,
467
841
  // so the write is intentional (one atomic call) rather than firing on every
468
842
  // slider tweak.
469
- const [previewed, setPreviewed] = useState<ShaderDescriptor | null>(null);
843
+ const [previewed, setPreviewed] = useState<PreviewedShaderFill | null>(null);
470
844
  const applyShaderFill = useActionMutation("apply-shader-fill");
471
845
 
472
846
  // The persisting apply path needs an HTML file plus a target element. Without
@@ -476,6 +850,16 @@ function ShaderFillsExtPanel({ context }: ShaderFillsExtPanelProps) {
476
850
  const canPersist = Boolean(
477
851
  context.activeFileId && (targetNodeId || targetSelector),
478
852
  );
853
+ const previewMatchesTarget = Boolean(
854
+ previewed &&
855
+ previewed.fileId === context.activeFileId &&
856
+ previewed.nodeId === targetNodeId &&
857
+ previewed.selector === targetSelector,
858
+ );
859
+
860
+ useEffect(() => {
861
+ setPreviewed(null);
862
+ }, [context.activeFileId, targetNodeId, targetSelector]);
479
863
 
480
864
  const persistFill = (descriptor: ShaderDescriptor) => {
481
865
  if (!canPersist) return;
@@ -498,13 +882,41 @@ function ShaderFillsExtPanel({ context }: ShaderFillsExtPanelProps) {
498
882
  kind: "design-file" as const,
499
883
  designId: context.designId || undefined,
500
884
  fileId: context.activeFileId || undefined,
885
+ revision: context.activeFileUpdatedAt || undefined,
886
+ currentContent: context.activeContent,
501
887
  },
502
888
  },
503
889
  {
504
890
  onSuccess: (res) => {
505
- const r = res as { persisted?: boolean } | undefined;
891
+ const r = res as
892
+ | {
893
+ fileId?: unknown;
894
+ patchedContent?: unknown;
895
+ persisted?: boolean;
896
+ conflict?: boolean;
897
+ error?: unknown;
898
+ note?: unknown;
899
+ updatedAt?: unknown;
900
+ }
901
+ | undefined;
506
902
  if (r?.persisted) {
903
+ if (
904
+ typeof r.fileId === "string" &&
905
+ typeof r.patchedContent === "string"
906
+ ) {
907
+ context.onShaderFillApplied?.(
908
+ r.fileId,
909
+ r.patchedContent,
910
+ typeof r.updatedAt === "string" ? r.updatedAt : undefined,
911
+ );
912
+ }
507
913
  toast.success("Shader fill applied to the selected element.");
914
+ } else if (r?.conflict) {
915
+ toast.error(
916
+ typeof r.error === "string" || typeof r.note === "string"
917
+ ? String(r.error ?? r.note)
918
+ : "This file changed since the shader fill was previewed. Refresh and try again.",
919
+ );
508
920
  } else {
509
921
  toast.message("Shader fill previewed — nothing was written.");
510
922
  }
@@ -556,7 +968,12 @@ function ShaderFillsExtPanel({ context }: ShaderFillsExtPanelProps) {
556
968
  // for agent context on every tune and the iframe shows the gradient.
557
969
  // We just record the latest descriptor here; the explicit Apply
558
970
  // button below performs the single intentional persist write.
559
- setPreviewed(descriptor);
971
+ setPreviewed({
972
+ descriptor,
973
+ fileId: context.activeFileId || undefined,
974
+ nodeId: targetNodeId,
975
+ selector: targetSelector,
976
+ });
560
977
  context.onShaderFillPreview?.(descriptor, css);
561
978
  }}
562
979
  onBack={closeShaders}
@@ -575,15 +992,17 @@ function ShaderFillsExtPanel({ context }: ShaderFillsExtPanelProps) {
575
992
  type="button"
576
993
  size="sm"
577
994
  className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
578
- disabled={!previewed || applyShaderFill.isPending}
995
+ disabled={!previewMatchesTarget || applyShaderFill.isPending}
579
996
  onClick={() => {
580
- if (previewed) persistFill(previewed);
997
+ if (previewMatchesTarget && previewed) {
998
+ persistFill(previewed.descriptor);
999
+ }
581
1000
  }}
582
1001
  >
583
1002
  <IconSparkles className="size-3" />
584
1003
  {applyShaderFill.isPending
585
1004
  ? "Applying…"
586
- : previewed
1005
+ : previewMatchesTarget
587
1006
  ? "Apply fill"
588
1007
  : "Pick a preset"}
589
1008
  </Button>
@@ -708,11 +1127,17 @@ function MotionPresetsPanel({ context }: MotionPresetsPanelProps) {
708
1127
  export function DesignExtensionsPanel({
709
1128
  context,
710
1129
  className,
1130
+ hideAssetLibrary = false,
1131
+ title,
711
1132
  }: DesignExtensionsPanelProps) {
712
1133
  const t = useT();
713
1134
  const queryClient = useQueryClient();
714
1135
  const [createOpen, setCreateOpen] = useState(false);
715
1136
  const [installingId, setInstallingId] = useState<string | null>(null);
1137
+ const [searchQuery, setSearchQuery] = useState("");
1138
+ const [sourceFilter, setSourceFilter] = useState<ToolSourceFilter>("all");
1139
+ const [categoryFilter, setCategoryFilter] =
1140
+ useState<ToolCategoryFilter>("all");
716
1141
  const [openFirstParty, setOpenFirstParty] = useState<FirstPartyExtId | null>(
717
1142
  null,
718
1143
  );
@@ -731,12 +1156,7 @@ export function DesignExtensionsPanel({
731
1156
  setOpenFirstParty((prev) => (prev === id ? null : id));
732
1157
  };
733
1158
 
734
- const submitCreatePrompt: PromptComposerSubmitHandler = (
735
- text: string,
736
- _files,
737
- _references,
738
- options: PromptComposerSubmitOptions,
739
- ) => {
1159
+ const submitCreatePrompt: CreateExtensionSubmitHandler = (text: string) => {
740
1160
  const trimmed = text.trim();
741
1161
  if (!trimmed) return;
742
1162
  sendToAgentChat({
@@ -745,9 +1165,6 @@ export function DesignExtensionsPanel({
745
1165
  submit: true,
746
1166
  openSidebar: true,
747
1167
  newTab: true,
748
- model: options.model,
749
- engine: options.engine,
750
- effort: options.effort,
751
1168
  });
752
1169
  setCreateOpen(false);
753
1170
  };
@@ -780,18 +1197,12 @@ export function DesignExtensionsPanel({
780
1197
  };
781
1198
 
782
1199
  // 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
- }> = [
1200
+ const allFirstPartyRows: FirstPartyRow[] = [
791
1201
  {
792
1202
  id: "design.asset-library",
793
1203
  label: "Asset Library",
794
1204
  description: "Browse & insert assets into the active screen",
1205
+ category: "plugins",
795
1206
  icon: <IconPhoto className="size-3.5" />,
796
1207
  panel: <AssetLibraryPanel context={context} />,
797
1208
  },
@@ -799,6 +1210,7 @@ export function DesignExtensionsPanel({
799
1210
  id: "design.shader-fills",
800
1211
  label: "Shader Fills",
801
1212
  description: "GPU shader fill presets — preview & apply",
1213
+ category: "shader",
802
1214
  icon: <IconSparkles className="size-3.5" />,
803
1215
  panel: <ShaderFillsExtPanel context={context} />,
804
1216
  },
@@ -806,39 +1218,62 @@ export function DesignExtensionsPanel({
806
1218
  id: "design.token-auditor",
807
1219
  label: "Token Auditor",
808
1220
  description: "Audit CSS token usage, flag clashes",
809
- icon: <IconPalette className="size-3.5" />,
1221
+ category: "tokens",
1222
+ icon: <IconAssembly className="size-3.5" />,
810
1223
  panel: <TokenAuditorPanel context={context} />,
811
1224
  },
812
1225
  {
813
1226
  id: "design.motion-presets",
814
1227
  label: "Motion Presets",
815
1228
  description: "One-click animation presets for elements",
1229
+ category: "motion",
816
1230
  icon: <IconPlayerPlay className="size-3.5" />,
817
1231
  panel: <MotionPresetsPanel context={context} />,
818
1232
  },
819
1233
  ];
1234
+ const firstPartyRows = allFirstPartyRows.filter(
1235
+ (row) => !hideAssetLibrary || row.id !== "design.asset-library",
1236
+ );
1237
+ const normalizedSearch = searchQuery.trim().toLowerCase();
1238
+ const sourceMatches = (source: ToolSourceFilter) =>
1239
+ sourceFilter === "all" || sourceFilter === source;
1240
+ const categoryMatches = (category: ToolCategoryFilter) =>
1241
+ categoryFilter === "all" || categoryFilter === category;
1242
+ const textMatches = (name: string, description?: string | null) => {
1243
+ if (!normalizedSearch) return true;
1244
+ return `${name} ${description ?? ""}`
1245
+ .toLowerCase()
1246
+ .includes(normalizedSearch);
1247
+ };
1248
+ const visibleFirstPartyRows = firstPartyRows.filter(
1249
+ (row) =>
1250
+ sourceMatches("built-in") &&
1251
+ categoryMatches(row.category) &&
1252
+ textMatches(row.label, row.description),
1253
+ );
1254
+ const visibleInstalls = installs.filter(
1255
+ (install) =>
1256
+ sourceMatches("extensions") &&
1257
+ categoryMatches("plugins") &&
1258
+ textMatches(install.name, install.description),
1259
+ );
1260
+ const visibleInstallable = installable.filter(
1261
+ (extension) =>
1262
+ sourceMatches("extensions") &&
1263
+ categoryMatches("plugins") &&
1264
+ textMatches(extension.name, extension.description),
1265
+ );
1266
+ const hasAnyVisibleTool =
1267
+ visibleFirstPartyRows.length > 0 ||
1268
+ visibleInstalls.length > 0 ||
1269
+ visibleInstallable.length > 0;
820
1270
 
821
1271
  return (
822
1272
  <div className={cn("flex min-h-0 flex-1 flex-col", className)}>
823
- {/* Section header 32 px tall, matches PanelSection / design inspector headers */}
824
- <div className="flex min-h-8 shrink-0 items-center gap-1.5 border-b border-border/60 px-3">
825
- <h3 className="min-w-0 flex-1 truncate text-xs font-semibold text-foreground">
826
- {t("designEditor.extensions")}
1273
+ <div className="flex h-16 shrink-0 items-center gap-3 border-b border-border/60 px-4">
1274
+ <h3 className="min-w-0 flex-1 truncate text-xl font-semibold tracking-tight text-foreground">
1275
+ {title ?? t("designEditor.extensions")}
827
1276
  </h3>
828
- <Tooltip>
829
- <TooltipTrigger asChild>
830
- <a
831
- href="https://www.agent-native.com/docs/extensions"
832
- target="_blank"
833
- rel="noreferrer"
834
- className="inline-flex size-6 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
835
- aria-label={t("designEditor.extensionsDocs")}
836
- >
837
- <IconExternalLink className="size-3.5" />
838
- </a>
839
- </TooltipTrigger>
840
- <TooltipContent>{t("designEditor.extensionsDocs")}</TooltipContent>
841
- </Tooltip>
842
1277
  <CreateExtensionPopover
843
1278
  open={createOpen}
844
1279
  onOpenChange={setCreateOpen}
@@ -846,117 +1281,146 @@ export function DesignExtensionsPanel({
846
1281
  />
847
1282
  </div>
848
1283
 
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
- ))}
1284
+ <div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-4 pt-4">
1285
+ <div className="relative mb-4">
1286
+ <IconSearch className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
1287
+ <Input
1288
+ value={searchQuery}
1289
+ onChange={(event) => setSearchQuery(event.target.value)}
1290
+ placeholder="Search all tools"
1291
+ className="h-9 border-0 bg-muted/70 pl-8 text-sm shadow-none focus-visible:ring-1"
1292
+ />
869
1293
  </div>
870
1294
 
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>
1295
+ <div className="mb-5 flex flex-wrap gap-2">
1296
+ <ToolFilterMenu
1297
+ label="Source"
1298
+ value={sourceFilter}
1299
+ onChange={setSourceFilter}
1300
+ options={[
1301
+ { value: "all", label: "Source" },
1302
+ { value: "built-in", label: "Built-in" },
1303
+ { value: "extensions", label: "Extensions" },
1304
+ ]}
1305
+ />
1306
+ <ToolFilterMenu
1307
+ label="Category"
1308
+ value={categoryFilter}
1309
+ onChange={setCategoryFilter}
1310
+ options={[
1311
+ { value: "all", label: "Category" },
1312
+ { value: "shader", label: "Shaders" },
1313
+ { value: "tokens", label: "Tokens" },
1314
+ { value: "motion", label: "Motion" },
1315
+ { value: "plugins", label: "Plugins" },
1316
+ ]}
1317
+ />
878
1318
  </div>
879
1319
 
880
- {/* ── User-installed extensions ──────────────────────────────────── */}
881
1320
  {isLoading ? (
882
- <div className="space-y-1.5">
883
- <Skeleton className="h-24 rounded" />
884
- <Skeleton className="h-32 rounded" />
885
- </div>
886
- ) : installs.length > 0 ? (
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>
891
- {installs.map((install) => (
892
- <EmbeddedExtension
893
- key={install.installId}
894
- extensionId={install.extensionId}
895
- slotId={slotId}
896
- context={context}
897
- initialHeight={180}
898
- className="overflow-hidden rounded border border-border bg-background"
899
- />
900
- ))}
1321
+ <div className="space-y-3">
1322
+ <Skeleton className="h-12 rounded-md" />
1323
+ <Skeleton className="h-12 rounded-md" />
1324
+ <Skeleton className="h-12 rounded-md" />
901
1325
  </div>
902
1326
  ) : (
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>
926
- )}
1327
+ <>
1328
+ {visibleInstalls.length > 0 ? (
1329
+ <div className="mb-6">
1330
+ <p className="mb-3 text-sm font-medium text-muted-foreground">
1331
+ Recents
1332
+ </p>
1333
+ <div className="space-y-2">
1334
+ {visibleInstalls.map((install) => (
1335
+ <EmbeddedExtension
1336
+ key={install.installId}
1337
+ extensionId={install.extensionId}
1338
+ slotId={slotId}
1339
+ context={context}
1340
+ initialHeight={180}
1341
+ className="overflow-hidden rounded-md border border-border bg-background"
1342
+ />
1343
+ ))}
1344
+ </div>
1345
+ </div>
1346
+ ) : null}
1347
+
1348
+ {visibleFirstPartyRows.length > 0 ? (
1349
+ <div className="mb-6">
1350
+ <p className="mb-3 text-sm font-medium text-muted-foreground">
1351
+ Suggested
1352
+ </p>
1353
+ <div className="space-y-1">
1354
+ {visibleFirstPartyRows.map((row) => (
1355
+ <FirstPartyExtRow
1356
+ key={row.id}
1357
+ id={row.id}
1358
+ label={row.label}
1359
+ description={row.description}
1360
+ icon={row.icon}
1361
+ badge={row.badge}
1362
+ isOpen={openFirstParty === row.id}
1363
+ onToggle={() => toggleFirstParty(row.id)}
1364
+ >
1365
+ {row.panel}
1366
+ </FirstPartyExtRow>
1367
+ ))}
1368
+ </div>
1369
+ </div>
1370
+ ) : null}
1371
+
1372
+ {visibleInstallable.length > 0 ? (
1373
+ <div className="border-t border-border/60 pt-4">
1374
+ <p className="mb-3 text-sm font-medium text-muted-foreground">
1375
+ Available
1376
+ </p>
1377
+ <div className="space-y-1">
1378
+ {visibleInstallable.map((extension) => (
1379
+ <button
1380
+ key={extension.extensionId}
1381
+ type="button"
1382
+ disabled={installingId === extension.extensionId}
1383
+ onClick={() => installExtension(extension.extensionId)}
1384
+ className="group flex w-full cursor-pointer items-center gap-3 rounded-md px-2 py-2.5 text-left transition-colors hover:bg-accent/60 active:bg-accent disabled:cursor-default disabled:opacity-50"
1385
+ >
1386
+ <span className="flex size-10 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/70 text-muted-foreground transition-colors group-hover:border-border group-hover:bg-muted">
1387
+ <IconPuzzle className="size-4" />
1388
+ </span>
1389
+ <span className="min-w-0 flex-1">
1390
+ <span className="block truncate text-sm font-medium leading-tight text-foreground">
1391
+ {extension.name}
1392
+ </span>
1393
+ <span className="mt-0.5 flex items-center gap-1.5 truncate text-xs leading-none text-muted-foreground">
1394
+ <IconPuzzle className="size-3 shrink-0" />
1395
+ <span className="truncate">
1396
+ {extension.description || "Extension"}
1397
+ </span>
1398
+ </span>
1399
+ </span>
1400
+ <span className="shrink-0 text-xs font-medium text-muted-foreground">
1401
+ {t("designEditor.extensionsInstall")}
1402
+ </span>
1403
+ </button>
1404
+ ))}
1405
+ </div>
1406
+ </div>
1407
+ ) : null}
927
1408
 
928
- {/* ── Available (installable) extensions ────────────────────────── */}
929
- {installable.length > 0 ? (
930
- <div className="mt-3 border-t border-border/60 pt-1.5">
931
- <p className="mb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
932
- {t("designEditor.extensionsAvailable")}
933
- </p>
934
- <div className="space-y-px">
935
- {installable.map((extension) => (
936
- <button
937
- key={extension.extensionId}
938
- type="button"
939
- disabled={installingId === extension.extensionId}
940
- onClick={() => installExtension(extension.extensionId)}
941
- className="flex h-6 w-full cursor-pointer items-center gap-1.5 rounded px-2 text-left transition-colors hover:bg-accent active:bg-accent/80 disabled:cursor-default disabled:opacity-50"
942
- >
943
- <IconSparkles className="size-3 shrink-0 text-muted-foreground" />
944
- <span className="min-w-0 flex-1 truncate text-[11px] font-medium text-foreground">
945
- {extension.name}
946
- </span>
947
- {extension.description ? (
948
- <span className="hidden truncate text-[11px] text-muted-foreground sm:block">
949
- {extension.description}
950
- </span>
951
- ) : null}
952
- <span className="shrink-0 text-[11px] text-muted-foreground">
953
- {t("designEditor.extensionsInstall")}
954
- </span>
955
- </button>
956
- ))}
957
- </div>
958
- </div>
959
- ) : null}
1409
+ {!hasAnyVisibleTool ? (
1410
+ <div className="py-12 text-center">
1411
+ <div className="mx-auto mb-3 flex size-9 items-center justify-center rounded-md bg-muted/70">
1412
+ <IconSearch className="size-4 text-muted-foreground" />
1413
+ </div>
1414
+ <p className="text-sm font-medium text-foreground">
1415
+ No tools found
1416
+ </p>
1417
+ <p className="mx-auto mt-1 max-w-52 text-xs leading-5 text-muted-foreground">
1418
+ Try another search or clear the filters.
1419
+ </p>
1420
+ </div>
1421
+ ) : null}
1422
+ </>
1423
+ )}
960
1424
  </div>
961
1425
  </div>
962
1426
  );
@@ -971,40 +1435,65 @@ function CreateExtensionPopover({
971
1435
  }: {
972
1436
  open: boolean;
973
1437
  onOpenChange: (open: boolean) => void;
974
- onSubmit: PromptComposerSubmitHandler;
1438
+ onSubmit: CreateExtensionSubmitHandler;
975
1439
  }) {
976
1440
  const t = useT();
1441
+ const [draft, setDraft] = useState("");
1442
+ const canSubmit = draft.trim().length > 0;
1443
+ const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
1444
+ event.preventDefault();
1445
+ if (!canSubmit) return;
1446
+ onSubmit(draft);
1447
+ setDraft("");
1448
+ };
1449
+
977
1450
  return (
978
1451
  <Popover open={open} onOpenChange={onOpenChange}>
979
- <Tooltip>
980
- <TooltipTrigger asChild>
981
- <PopoverTrigger asChild>
1452
+ <PopoverTrigger asChild>
1453
+ <Button
1454
+ type="button"
1455
+ variant="outline"
1456
+ size="sm"
1457
+ className="h-8 cursor-pointer gap-1.5 rounded-md bg-transparent px-3 text-sm font-medium"
1458
+ aria-label={t("designEditor.addExtension")}
1459
+ >
1460
+ Create
1461
+ <IconChevronDown className="size-3.5 text-muted-foreground" />
1462
+ </Button>
1463
+ </PopoverTrigger>
1464
+ <PopoverContent align="end" sideOffset={8} className="w-80 p-3">
1465
+ <form onSubmit={handleSubmit} className="space-y-3">
1466
+ <p className="px-0.5 text-sm font-semibold text-foreground">
1467
+ {t("designEditor.extensionsPromptTitle")}
1468
+ </p>
1469
+ <Textarea
1470
+ autoFocus
1471
+ value={draft}
1472
+ onChange={(event) => setDraft(event.target.value)}
1473
+ placeholder={t("designEditor.extensionsPlaceholder")}
1474
+ className="min-h-24 resize-none border-border/80 bg-background/80 text-sm shadow-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0"
1475
+ />
1476
+ <div className="flex justify-end gap-2">
982
1477
  <Button
983
1478
  type="button"
984
1479
  variant="ghost"
985
- size="icon"
986
- className="size-6 rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
987
- aria-label={t("designEditor.addExtension")}
1480
+ size="sm"
1481
+ className="h-8 px-3"
1482
+ onClick={() => onOpenChange(false)}
988
1483
  >
989
- <IconPlus className="size-3.5" />
1484
+ Cancel
990
1485
  </Button>
991
- </PopoverTrigger>
992
- </TooltipTrigger>
993
- <TooltipContent>{t("designEditor.addExtension")}</TooltipContent>
994
- </Tooltip>
995
- <PopoverContent align="end" sideOffset={6} className="w-80 p-3">
996
- <p className="px-1 pb-2 text-sm font-semibold text-foreground">
997
- {t("designEditor.extensionsPromptTitle")}
998
- </p>
999
- <PromptComposer
1000
- autoFocus
1001
- attachmentsEnabled={false}
1002
- plusMenuMode="hidden"
1003
- layoutVariant="compact"
1004
- draftScope="design:editor-extension-create"
1005
- placeholder={t("designEditor.extensionsPlaceholder")}
1006
- onSubmit={onSubmit}
1007
- />
1486
+ <Button
1487
+ type="submit"
1488
+ size="sm"
1489
+ className="h-8 gap-1.5 px-3"
1490
+ disabled={!canSubmit}
1491
+ >
1492
+ <IconSparkles className="size-3.5" />
1493
+ Create
1494
+ </Button>
1495
+ </div>
1496
+ </form>
1008
1497
  </PopoverContent>
1009
1498
  </Popover>
1010
1499
  );