@agent-native/core 0.79.27 → 0.80.1

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 (497) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +34 -0
  3. package/corpus/core/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  4. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +55 -1
  5. package/corpus/core/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  6. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +66 -1
  7. package/corpus/core/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  8. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +65 -1
  9. package/corpus/core/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  10. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +64 -1
  11. package/corpus/core/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  12. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +59 -1
  13. package/corpus/core/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  14. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +62 -1
  15. package/corpus/core/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  16. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +58 -1
  17. package/corpus/core/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  18. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +62 -1
  19. package/corpus/core/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  20. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +50 -1
  21. package/corpus/core/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  22. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +52 -1
  23. package/corpus/core/docs/content/skills-guide.mdx +5 -0
  24. package/corpus/core/docs/content/template-design.mdx +59 -1
  25. package/corpus/core/package.json +2 -1
  26. package/corpus/core/src/agent/engine/index.ts +1 -0
  27. package/corpus/core/src/agent/engine/registry.ts +40 -0
  28. package/corpus/core/src/agent/production-agent.ts +6 -2
  29. package/corpus/core/src/application-state/handlers.ts +1 -1
  30. package/corpus/core/src/brand-kit/index.ts +1 -1
  31. package/corpus/core/src/cli/design-connect.ts +490 -0
  32. package/corpus/core/src/cli/index.ts +16 -0
  33. package/corpus/core/src/cli/skills.ts +144 -3
  34. package/corpus/core/src/client/AgentPanel.tsx +1 -1
  35. package/corpus/core/src/client/AssistantChat.tsx +43 -7
  36. package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
  37. package/corpus/core/src/client/agent-chat.ts +27 -3
  38. package/corpus/core/src/client/blocks/library/MermaidBlock.tsx +1 -1
  39. package/corpus/core/src/client/blocks/library/diagram.tsx +2 -2
  40. package/corpus/core/src/client/blocks/library/wireframe.tsx +1 -1
  41. package/corpus/core/src/client/chat/message-components.tsx +1 -1
  42. package/corpus/core/src/client/chat-model-groups.ts +1 -2
  43. package/corpus/core/src/client/components/LiveCursorOverlay.tsx +33 -66
  44. package/corpus/core/src/client/composer/TiptapComposer.tsx +99 -24
  45. package/corpus/core/src/client/composer/useVoiceDictation.ts +429 -260
  46. package/corpus/core/src/client/extensions/portable-extension.ts +1 -1
  47. package/corpus/core/src/client/frame-protocol.ts +17 -2
  48. package/corpus/core/src/client/rich-markdown-editor/RegistryBlockNode.tsx +2 -2
  49. package/corpus/core/src/client/sharing/ShareButton.tsx +20 -15
  50. package/corpus/core/src/client/use-agent-engine-configured.ts +80 -28
  51. package/corpus/core/src/code-agents/transcript-normalizer.ts +1 -1
  52. package/corpus/core/src/extensions/path.ts +1 -4
  53. package/corpus/core/src/integrations/a2a-continuation-processor.ts +1 -1
  54. package/corpus/core/src/mcp/build-server.ts +1 -1
  55. package/corpus/core/src/org/handlers.ts +5 -5
  56. package/corpus/core/src/org/plugin.ts +2 -2
  57. package/corpus/core/src/scripts/call-agent.ts +1 -1
  58. package/corpus/core/src/scripts/db/safety.ts +1 -1
  59. package/corpus/core/src/server/auth.ts +3 -0
  60. package/corpus/core/src/server/builder-design-systems.ts +308 -0
  61. package/corpus/core/src/server/core-routes-plugin.ts +29 -3
  62. package/corpus/core/src/server/index.ts +8 -0
  63. package/corpus/core/src/server/transcribe-voice.ts +57 -22
  64. package/corpus/core/src/shared/mcp-embed-headers.ts +23 -1
  65. package/corpus/core/src/styles/agent-native.css +60 -4
  66. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  67. package/corpus/core/src/usage/store.ts +2 -2
  68. package/corpus/core/src/vite/client.ts +2 -0
  69. package/corpus/core/src/voice/index.ts +18 -0
  70. package/corpus/core/src/voice/voice-cleanup-prompt.ts +38 -0
  71. package/corpus/core/src/voice/voice-context.ts +217 -0
  72. package/corpus/core/src/voice/voice-replacements.ts +75 -0
  73. package/corpus/templates/analytics/AGENTS.md +4 -3
  74. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +298 -15
  75. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +9 -4
  76. package/corpus/templates/analytics/actions/list-session-recordings.ts +6 -1
  77. package/corpus/templates/analytics/actions/mutate-dashboard.ts +18 -2
  78. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +12 -2
  79. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +557 -400
  80. package/corpus/templates/analytics/app/global.css +0 -4
  81. package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +66 -0
  82. package/corpus/templates/analytics/app/i18n/zh-TW.ts +39 -0
  83. package/corpus/templates/analytics/app/i18n-data.ts +424 -0
  84. package/corpus/templates/analytics/app/pages/Settings.tsx +333 -0
  85. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -1
  86. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +121 -3
  87. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +82 -34
  88. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +7 -0
  89. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +144 -36
  90. package/corpus/templates/analytics/changelog/2026-06-28-ask-tab-now-matches-the-darker-app-background.md +6 -0
  91. package/corpus/templates/analytics/changelog/2026-06-28-dashboard-email-reports-now-use-cleaner-light-mode-snapshots.md +6 -0
  92. package/corpus/templates/analytics/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  93. package/corpus/templates/analytics/changelog/2026-06-28-sessions-now-show-only-signed-in-recordings-with-playable-re.md +6 -0
  94. package/corpus/templates/analytics/changelog/2026-06-29-analytics-agents-can-now-place-new-charts-into-requested-das.md +6 -0
  95. package/corpus/templates/analytics/changelog/2026-06-29-analytics-queries-now-report-failed-source-query-errors-inst.md +6 -0
  96. package/corpus/templates/analytics/changelog/2026-06-29-charts-can-now-be-dropped-between-full-dashboard-rows-to-cre.md +6 -0
  97. package/corpus/templates/analytics/changelog/2026-06-29-dashboard-email-reports-include-the-full-dashboard-image-wit.md +6 -0
  98. package/corpus/templates/analytics/changelog/2026-06-29-main-surface-border-reaches-the-top.md +6 -0
  99. package/corpus/templates/analytics/changelog/2026-06-29-session-replay-now-records-playable-sessions-in-production-a.md +6 -0
  100. package/corpus/templates/analytics/changelog/2026-06-29-set-up-session-replay-storage-from-settings-connect-builder-.md +6 -0
  101. package/corpus/templates/analytics/scripts/cleanup-empty-session-recordings.ts +102 -0
  102. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  103. package/corpus/templates/analytics/server/lib/dashboard-report.ts +75 -30
  104. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +5 -2
  105. package/corpus/templates/analytics/server/lib/real-data-actions.ts +81 -20
  106. package/corpus/templates/analytics/server/lib/replay-storage.ts +79 -0
  107. package/corpus/templates/analytics/server/lib/s3-upload-provider.ts +255 -0
  108. package/corpus/templates/analytics/server/lib/session-replay.ts +53 -12
  109. package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -2
  110. package/corpus/templates/analytics/server/plugins/onboarding.ts +97 -0
  111. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -4
  112. package/corpus/templates/assets/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  113. package/corpus/templates/brain/app/components/layout/Layout.tsx +29 -2
  114. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +131 -42
  115. package/corpus/templates/brain/app/i18n/zh-TW.ts +2 -0
  116. package/corpus/templates/brain/app/i18n-data.ts +20 -0
  117. package/corpus/templates/brain/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  118. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +29 -1
  119. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +417 -299
  120. package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
  121. package/corpus/templates/calendar/app/i18n-data.ts +20 -0
  122. package/corpus/templates/calendar/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  123. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +6 -19
  124. package/corpus/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  125. package/corpus/templates/clips/AGENTS.md +4 -0
  126. package/corpus/templates/clips/actions/cleanup-transcript.ts +39 -9
  127. package/corpus/templates/clips/actions/finalize-meeting.ts +81 -12
  128. package/corpus/templates/clips/actions/lib/agents-md-context.ts +4 -1
  129. package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -3
  130. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +1 -1
  131. package/corpus/templates/clips/app/components/player/share-dialog.tsx +2 -2
  132. package/corpus/templates/clips/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  133. package/corpus/templates/clips/desktop/src/lib/personal-vocabulary.ts +13 -8
  134. package/corpus/templates/clips/desktop/src/lib/recorder.ts +24 -0
  135. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +105 -5
  136. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +60 -1
  137. package/corpus/templates/content/actions/set-image-alt-text.ts +1 -1
  138. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1 -1
  139. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -1
  140. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -5
  141. package/corpus/templates/content/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  142. package/corpus/templates/content/shared/document-export.ts +1 -1
  143. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -18
  144. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +154 -0
  145. package/corpus/templates/design/AGENTS.md +95 -5
  146. package/corpus/templates/design/DESIGN.md +82 -0
  147. package/corpus/templates/design/DEVELOPING.md +43 -0
  148. package/corpus/templates/design/actions/add-localhost-screens.ts +453 -0
  149. package/corpus/templates/design/actions/apply-shader.ts +277 -0
  150. package/corpus/templates/design/actions/apply-visual-edit.ts +303 -0
  151. package/corpus/templates/design/actions/connect-localhost.ts +183 -0
  152. package/corpus/templates/design/actions/edit-design.ts +195 -42
  153. package/corpus/templates/design/actions/generate-design.ts +71 -1
  154. package/corpus/templates/design/actions/generate-screens.ts +193 -0
  155. package/corpus/templates/design/actions/get-code-layer-projection.ts +191 -0
  156. package/corpus/templates/design/actions/get-shader.ts +119 -0
  157. package/corpus/templates/design/actions/index-design-system-with-builder.ts +259 -0
  158. package/corpus/templates/design/actions/list-localhost-connections.ts +95 -0
  159. package/corpus/templates/design/actions/navigate.ts +55 -2
  160. package/corpus/templates/design/actions/update-file.ts +9 -2
  161. package/corpus/templates/design/actions/view-screen.ts +68 -1
  162. package/corpus/templates/design/agent-native.app-skill.json +10 -0
  163. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +719 -0
  164. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +2120 -103
  165. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +74 -38
  166. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +401 -0
  167. package/corpus/templates/design/app/components/design/EditPanel.tsx +3966 -440
  168. package/corpus/templates/design/app/components/design/LayersPanel.tsx +1473 -0
  169. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +3940 -147
  170. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +700 -13
  171. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +105 -63
  172. package/corpus/templates/design/app/components/design/index.ts +0 -5
  173. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +353 -0
  174. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +1589 -0
  175. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +511 -0
  176. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +395 -0
  177. package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +2010 -0
  178. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +601 -0
  179. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +242 -0
  180. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +420 -0
  181. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +238 -0
  182. package/corpus/templates/design/app/components/design/inspector/ShaderControls.stories.tsx +71 -0
  183. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +575 -0
  184. package/corpus/templates/design/app/components/design/inspector/ShaderEffectRow.tsx +106 -0
  185. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +423 -0
  186. package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +741 -0
  187. package/corpus/templates/design/app/components/design/inspector/index.ts +87 -0
  188. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +201 -0
  189. package/corpus/templates/design/app/components/design/types.ts +23 -0
  190. package/corpus/templates/design/app/components/layout/Layout.tsx +24 -13
  191. package/corpus/templates/design/app/components/layout/Sidebar.tsx +4 -4
  192. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +474 -36
  193. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +22 -5
  194. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +24 -11
  195. package/corpus/templates/design/app/global.css +149 -5
  196. package/corpus/templates/design/app/hooks/use-agent-generating.ts +4 -0
  197. package/corpus/templates/design/app/hooks/use-navigation-state.ts +130 -8
  198. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +329 -0
  199. package/corpus/templates/design/app/i18n/zh-TW.ts +277 -0
  200. package/corpus/templates/design/app/i18n-data.ts +3028 -1
  201. package/corpus/templates/design/app/pages/DesignEditor.tsx +5831 -904
  202. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +57 -221
  203. package/corpus/templates/design/changelog/2026-06-28-annotate-mode-now-combines-drawing-and-comment-pins-in-one-c.md +6 -0
  204. package/corpus/templates/design/changelog/2026-06-28-design-editor-panel-borders-are-cleaner-so-the-canvas-edge-s.md +6 -0
  205. package/corpus/templates/design/changelog/2026-06-28-drawing-prompts-now-reliably-open-and-submit-to-the-design-a.md +6 -0
  206. package/corpus/templates/design/changelog/2026-06-28-editor-app-menus-now-use-an-outline-agent-native-mark-with-b.md +6 -0
  207. package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +6 -0
  208. package/corpus/templates/design/changelog/2026-06-28-layer-selection-now-updates-chat-context-without-opening-the.md +6 -0
  209. package/corpus/templates/design/changelog/2026-06-28-layer-selections-now-preserve-hidden-and-locked-canvas-state.md +6 -0
  210. package/corpus/templates/design/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  211. package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +6 -0
  212. package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +6 -0
  213. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-handles-zooming-drawing-opening-and-dele.md +6 -0
  214. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-selects-screens-on-click-and-opens-them-.md +6 -0
  215. package/corpus/templates/design/changelog/2026-06-28-screen-overview-zooms-into-the-active-screen-with-smoother-e.md +6 -0
  216. package/corpus/templates/design/changelog/2026-06-28-selected-element-chips-in-chat-are-shorter-and-easier-to-sca.md +6 -0
  217. package/corpus/templates/design/changelog/2026-06-28-the-design-editor-toolbar-is-more-compact-and-the-app-menu-m.md +6 -0
  218. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-at-the-top-of-the-design-sidebar-instead-of-.md +6 -0
  219. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-in-the-right-inspector-beside-the-design-tab.md +6 -0
  220. package/corpus/templates/design/changelog/2026-06-29-active-sidebar-rows-use-soft-highlights.md +6 -0
  221. package/corpus/templates/design/changelog/2026-06-29-agents-can-now-open-screen-overview-focus-screens-and-add.md +6 -0
  222. package/corpus/templates/design/changelog/2026-06-29-canvas-selection-no-longer-flashes-broken-prototype-html-whe.md +6 -0
  223. package/corpus/templates/design/changelog/2026-06-29-command-menu-selections-are-clearer-in-dark-mode.md +6 -0
  224. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-edit-broad-copy-changes-more-reliably.md +6 -0
  225. package/corpus/templates/design/changelog/2026-06-29-design-editor-extensions-now-run-inline-beside-the-artboard.md +6 -0
  226. package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +6 -0
  227. package/corpus/templates/design/changelog/2026-06-29-element-selection-now-stays-on-the-intended-layer-after-sele.md +6 -0
  228. package/corpus/templates/design/changelog/2026-06-29-screen-overview-full-view-controls-now-sit-above-frames-inst.md +6 -0
  229. package/corpus/templates/design/changelog/2026-06-29-screen-overview-tools-can-draw-shapes-create-screens-and-kee.md +6 -0
  230. package/corpus/templates/design/changelog/2026-06-29-sidebars-use-darker-chrome-and-chat-warns-immediately.md +6 -0
  231. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-now-creates-anchor-based-bezier-vectors-with-ha.md +6 -0
  232. package/corpus/templates/design/changelog/2026-06-29-visual-edit-can-open-localhost-routes-as-url-backed-screens.md +6 -0
  233. package/corpus/templates/design/package.json +1 -0
  234. package/corpus/templates/design/server/db/schema.ts +26 -0
  235. package/corpus/templates/design/server/handlers/import-figma-system.ts +38 -85
  236. package/corpus/templates/design/server/lib/design-snapshot.ts +9 -1
  237. package/corpus/templates/design/server/plugins/agent-chat.ts +3 -0
  238. package/corpus/templates/design/shared/api.ts +27 -0
  239. package/corpus/templates/design/shared/canvas-frames.ts +103 -0
  240. package/corpus/templates/design/shared/canvas-math.ts +1058 -0
  241. package/corpus/templates/design/shared/code-layer.ts +2412 -0
  242. package/corpus/templates/design/shared/color-utils.ts +180 -0
  243. package/corpus/templates/design/shared/generation-session.ts +61 -0
  244. package/corpus/templates/design/shared/html-content.ts +21 -0
  245. package/corpus/templates/design/shared/pen-path.ts +207 -0
  246. package/corpus/templates/design/shared/shader-presets.ts +782 -0
  247. package/corpus/templates/design/shared/shader-safety.ts +137 -0
  248. package/corpus/templates/design/shared/source-mode.ts +237 -0
  249. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +149 -6
  250. package/corpus/templates/forms/app/i18n/ar-SA.ts +2 -0
  251. package/corpus/templates/forms/app/i18n/de-DE.ts +2 -0
  252. package/corpus/templates/forms/app/i18n/en-US.ts +2 -0
  253. package/corpus/templates/forms/app/i18n/es-ES.ts +2 -0
  254. package/corpus/templates/forms/app/i18n/fr-FR.ts +2 -0
  255. package/corpus/templates/forms/app/i18n/hi-IN.ts +2 -0
  256. package/corpus/templates/forms/app/i18n/ja-JP.ts +2 -0
  257. package/corpus/templates/forms/app/i18n/ko-KR.ts +2 -0
  258. package/corpus/templates/forms/app/i18n/pt-BR.ts +2 -0
  259. package/corpus/templates/forms/app/i18n/zh-CN.ts +2 -0
  260. package/corpus/templates/forms/app/i18n/zh-TW.ts +2 -0
  261. package/corpus/templates/forms/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  262. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +2 -2
  263. package/corpus/templates/macros/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  264. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +408 -247
  265. package/corpus/templates/mail/app/i18n/ar-SA.ts +4 -0
  266. package/corpus/templates/mail/app/i18n/de-DE.ts +4 -0
  267. package/corpus/templates/mail/app/i18n/en-US.ts +4 -0
  268. package/corpus/templates/mail/app/i18n/es-ES.ts +4 -0
  269. package/corpus/templates/mail/app/i18n/fr-FR.ts +4 -0
  270. package/corpus/templates/mail/app/i18n/hi-IN.ts +4 -0
  271. package/corpus/templates/mail/app/i18n/ja-JP.ts +4 -0
  272. package/corpus/templates/mail/app/i18n/ko-KR.ts +4 -0
  273. package/corpus/templates/mail/app/i18n/pt-BR.ts +4 -0
  274. package/corpus/templates/mail/app/i18n/zh-CN.ts +4 -0
  275. package/corpus/templates/mail/app/i18n/zh-TW.ts +4 -0
  276. package/corpus/templates/mail/app/lib/utils.ts +2 -2
  277. package/corpus/templates/mail/changelog/2026-06-28-pinned-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  278. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +6 -8
  279. package/corpus/templates/plan/app/components/plan/wireframe/Wireframe.tsx +1 -1
  280. package/corpus/templates/plan/app/global.css +61 -78
  281. package/corpus/templates/plan/app/pages/PlansPage.tsx +8 -0
  282. package/corpus/templates/plan/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  283. package/corpus/templates/plan/changelog/2026-06-28-plans-use-the-shared-neutral-light-and-dark-theme.md +6 -0
  284. package/corpus/templates/plan/changelog/2026-06-28-sending-open-feedback-from-a-plan-now-opens-the-inline-plan-.md +6 -0
  285. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +9 -10
  286. package/corpus/templates/slides/AGENTS.md +3 -2
  287. package/corpus/templates/slides/actions/import-file.ts +26 -17
  288. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +259 -0
  289. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +35 -167
  290. package/corpus/templates/slides/app/components/design-system/fig-import-response.ts +9 -13
  291. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +5 -5
  292. package/corpus/templates/slides/app/context/DeckContext.tsx +1 -1
  293. package/corpus/templates/slides/app/i18n/ar-SA.ts +9 -37
  294. package/corpus/templates/slides/app/i18n/de-DE.ts +10 -25
  295. package/corpus/templates/slides/app/i18n/en-US.ts +9 -26
  296. package/corpus/templates/slides/app/i18n/es-ES.ts +10 -28
  297. package/corpus/templates/slides/app/i18n/fr-FR.ts +10 -28
  298. package/corpus/templates/slides/app/i18n/hi-IN.ts +9 -26
  299. package/corpus/templates/slides/app/i18n/ja-JP.ts +10 -22
  300. package/corpus/templates/slides/app/i18n/ko-KR.ts +9 -23
  301. package/corpus/templates/slides/app/i18n/pt-BR.ts +10 -28
  302. package/corpus/templates/slides/app/i18n/zh-CN.ts +9 -22
  303. package/corpus/templates/slides/app/i18n/zh-TW.ts +9 -22
  304. package/corpus/templates/slides/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  305. package/corpus/templates/slides/server/handlers/import-figma-system.ts +44 -12
  306. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  307. package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +1 -1
  308. package/corpus/templates/videos/app/components/Sidebar.tsx +2 -2
  309. package/corpus/templates/videos/app/components/layout/Layout.tsx +29 -2
  310. package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +113 -30
  311. package/corpus/templates/videos/app/i18n/ar-SA.ts +2 -0
  312. package/corpus/templates/videos/app/i18n/de-DE.ts +2 -0
  313. package/corpus/templates/videos/app/i18n/en-US.ts +2 -0
  314. package/corpus/templates/videos/app/i18n/es-ES.ts +2 -0
  315. package/corpus/templates/videos/app/i18n/fr-FR.ts +2 -0
  316. package/corpus/templates/videos/app/i18n/hi-IN.ts +2 -0
  317. package/corpus/templates/videos/app/i18n/ja-JP.ts +2 -0
  318. package/corpus/templates/videos/app/i18n/ko-KR.ts +2 -0
  319. package/corpus/templates/videos/app/i18n/pt-BR.ts +2 -0
  320. package/corpus/templates/videos/app/i18n/zh-CN.ts +2 -0
  321. package/corpus/templates/videos/app/i18n/zh-TW.ts +2 -0
  322. package/corpus/templates/videos/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  323. package/dist/agent/engine/index.d.ts +1 -1
  324. package/dist/agent/engine/index.d.ts.map +1 -1
  325. package/dist/agent/engine/index.js +1 -1
  326. package/dist/agent/engine/index.js.map +1 -1
  327. package/dist/agent/engine/registry.d.ts +9 -0
  328. package/dist/agent/engine/registry.d.ts.map +1 -1
  329. package/dist/agent/engine/registry.js +37 -0
  330. package/dist/agent/engine/registry.js.map +1 -1
  331. package/dist/agent/production-agent.d.ts.map +1 -1
  332. package/dist/agent/production-agent.js +4 -3
  333. package/dist/agent/production-agent.js.map +1 -1
  334. package/dist/application-state/handlers.js +1 -1
  335. package/dist/application-state/handlers.js.map +1 -1
  336. package/dist/brand-kit/index.d.ts +1 -1
  337. package/dist/brand-kit/index.js +1 -1
  338. package/dist/brand-kit/index.js.map +1 -1
  339. package/dist/cli/design-connect.d.ts +52 -0
  340. package/dist/cli/design-connect.d.ts.map +1 -0
  341. package/dist/cli/design-connect.js +409 -0
  342. package/dist/cli/design-connect.js.map +1 -0
  343. package/dist/cli/index.js +15 -0
  344. package/dist/cli/index.js.map +1 -1
  345. package/dist/cli/skills.d.ts +3 -0
  346. package/dist/cli/skills.d.ts.map +1 -1
  347. package/dist/cli/skills.js +139 -3
  348. package/dist/cli/skills.js.map +1 -1
  349. package/dist/client/AgentPanel.js +1 -1
  350. package/dist/client/AgentPanel.js.map +1 -1
  351. package/dist/client/AssistantChat.d.ts.map +1 -1
  352. package/dist/client/AssistantChat.js +38 -8
  353. package/dist/client/AssistantChat.js.map +1 -1
  354. package/dist/client/MultiTabAssistantChat.js +1 -1
  355. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  356. package/dist/client/agent-chat.d.ts +6 -0
  357. package/dist/client/agent-chat.d.ts.map +1 -1
  358. package/dist/client/agent-chat.js +17 -3
  359. package/dist/client/agent-chat.js.map +1 -1
  360. package/dist/client/blocks/library/MermaidBlock.js +1 -1
  361. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  362. package/dist/client/blocks/library/diagram.js +1 -1
  363. package/dist/client/blocks/library/diagram.js.map +1 -1
  364. package/dist/client/blocks/library/wireframe.js +1 -1
  365. package/dist/client/blocks/library/wireframe.js.map +1 -1
  366. package/dist/client/chat/message-components.js +1 -1
  367. package/dist/client/chat/message-components.js.map +1 -1
  368. package/dist/client/chat-model-groups.d.ts.map +1 -1
  369. package/dist/client/chat-model-groups.js +1 -2
  370. package/dist/client/chat-model-groups.js.map +1 -1
  371. package/dist/client/components/LiveCursorOverlay.d.ts +1 -2
  372. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -1
  373. package/dist/client/components/LiveCursorOverlay.js +19 -40
  374. package/dist/client/components/LiveCursorOverlay.js.map +1 -1
  375. package/dist/client/composer/TiptapComposer.d.ts +3 -1
  376. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  377. package/dist/client/composer/TiptapComposer.js +86 -24
  378. package/dist/client/composer/TiptapComposer.js.map +1 -1
  379. package/dist/client/composer/useVoiceDictation.d.ts +9 -0
  380. package/dist/client/composer/useVoiceDictation.d.ts.map +1 -1
  381. package/dist/client/composer/useVoiceDictation.js +154 -32
  382. package/dist/client/composer/useVoiceDictation.js.map +1 -1
  383. package/dist/client/extensions/portable-extension.d.ts +1 -1
  384. package/dist/client/extensions/portable-extension.d.ts.map +1 -1
  385. package/dist/client/extensions/portable-extension.js.map +1 -1
  386. package/dist/client/frame-protocol.d.ts +17 -2
  387. package/dist/client/frame-protocol.d.ts.map +1 -1
  388. package/dist/client/frame-protocol.js.map +1 -1
  389. package/dist/client/rich-markdown-editor/RegistryBlockNode.js +2 -2
  390. package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
  391. package/dist/client/sharing/ShareButton.d.ts +2 -0
  392. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  393. package/dist/client/sharing/ShareButton.js +7 -6
  394. package/dist/client/sharing/ShareButton.js.map +1 -1
  395. package/dist/client/use-agent-engine-configured.d.ts +5 -0
  396. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  397. package/dist/client/use-agent-engine-configured.js +51 -25
  398. package/dist/client/use-agent-engine-configured.js.map +1 -1
  399. package/dist/code-agents/transcript-normalizer.js +1 -1
  400. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  401. package/dist/collab/routes.d.ts +1 -1
  402. package/dist/extensions/path.d.ts +1 -1
  403. package/dist/extensions/path.d.ts.map +1 -1
  404. package/dist/extensions/path.js.map +1 -1
  405. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  406. package/dist/integrations/a2a-continuation-processor.js +1 -1
  407. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  408. package/dist/mcp/build-server.d.ts +1 -1
  409. package/dist/mcp/build-server.d.ts.map +1 -1
  410. package/dist/mcp/build-server.js.map +1 -1
  411. package/dist/notifications/routes.d.ts +3 -3
  412. package/dist/observability/routes.d.ts +5 -5
  413. package/dist/org/handlers.js +5 -5
  414. package/dist/org/handlers.js.map +1 -1
  415. package/dist/org/plugin.js +2 -2
  416. package/dist/org/plugin.js.map +1 -1
  417. package/dist/scripts/call-agent.js +1 -1
  418. package/dist/scripts/call-agent.js.map +1 -1
  419. package/dist/scripts/db/safety.js +1 -1
  420. package/dist/scripts/db/safety.js.map +1 -1
  421. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  422. package/dist/server/auth.d.ts.map +1 -1
  423. package/dist/server/auth.js +3 -0
  424. package/dist/server/auth.js.map +1 -1
  425. package/dist/server/builder-design-systems.d.ts +28 -0
  426. package/dist/server/builder-design-systems.d.ts.map +1 -0
  427. package/dist/server/builder-design-systems.js +217 -0
  428. package/dist/server/builder-design-systems.js.map +1 -0
  429. package/dist/server/core-routes-plugin.d.ts +1 -0
  430. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  431. package/dist/server/core-routes-plugin.js +20 -3
  432. package/dist/server/core-routes-plugin.js.map +1 -1
  433. package/dist/server/index.d.ts +1 -0
  434. package/dist/server/index.d.ts.map +1 -1
  435. package/dist/server/index.js +1 -0
  436. package/dist/server/index.js.map +1 -1
  437. package/dist/server/transcribe-voice.d.ts +1 -1
  438. package/dist/server/transcribe-voice.d.ts.map +1 -1
  439. package/dist/server/transcribe-voice.js +40 -24
  440. package/dist/server/transcribe-voice.js.map +1 -1
  441. package/dist/shared/mcp-embed-headers.d.ts +1 -0
  442. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  443. package/dist/shared/mcp-embed-headers.js +20 -1
  444. package/dist/shared/mcp-embed-headers.js.map +1 -1
  445. package/dist/styles/agent-native.css +60 -4
  446. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  447. package/dist/usage/store.js +2 -2
  448. package/dist/usage/store.js.map +1 -1
  449. package/dist/vite/client.d.ts.map +1 -1
  450. package/dist/vite/client.js +2 -0
  451. package/dist/vite/client.js.map +1 -1
  452. package/dist/voice/index.d.ts +5 -0
  453. package/dist/voice/index.d.ts.map +1 -0
  454. package/dist/voice/index.js +4 -0
  455. package/dist/voice/index.js.map +1 -0
  456. package/dist/voice/voice-cleanup-prompt.d.ts +6 -0
  457. package/dist/voice/voice-cleanup-prompt.d.ts.map +1 -0
  458. package/dist/voice/voice-cleanup-prompt.js +26 -0
  459. package/dist/voice/voice-cleanup-prompt.js.map +1 -0
  460. package/dist/voice/voice-context.d.ts +25 -0
  461. package/dist/voice/voice-context.d.ts.map +1 -0
  462. package/dist/voice/voice-context.js +156 -0
  463. package/dist/voice/voice-context.js.map +1 -0
  464. package/dist/voice/voice-replacements.d.ts +4 -0
  465. package/dist/voice/voice-replacements.d.ts.map +1 -0
  466. package/dist/voice/voice-replacements.js +52 -0
  467. package/dist/voice/voice-replacements.js.map +1 -0
  468. package/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  469. package/docs/content/locales/ar-SA/template-design.mdx +55 -1
  470. package/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  471. package/docs/content/locales/de-DE/template-design.mdx +66 -1
  472. package/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  473. package/docs/content/locales/es-ES/template-design.mdx +65 -1
  474. package/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  475. package/docs/content/locales/fr-FR/template-design.mdx +64 -1
  476. package/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  477. package/docs/content/locales/hi-IN/template-design.mdx +59 -1
  478. package/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  479. package/docs/content/locales/ja-JP/template-design.mdx +62 -1
  480. package/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  481. package/docs/content/locales/ko-KR/template-design.mdx +58 -1
  482. package/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  483. package/docs/content/locales/pt-BR/template-design.mdx +62 -1
  484. package/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  485. package/docs/content/locales/zh-CN/template-design.mdx +50 -1
  486. package/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  487. package/docs/content/locales/zh-TW/template-design.mdx +52 -1
  488. package/docs/content/skills-guide.mdx +5 -0
  489. package/docs/content/template-design.mdx +59 -1
  490. package/package.json +2 -1
  491. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  492. package/corpus/templates/design/actions/import-figma-file.ts +0 -166
  493. package/corpus/templates/design/app/components/design/DesignToolbar.tsx +0 -339
  494. package/corpus/templates/design/app/components/design/PresentMode.tsx +0 -68
  495. package/corpus/templates/design/app/components/design/ViewportTabs.tsx +0 -149
  496. package/corpus/templates/design/app/components/design/ZoomControls.tsx +0 -157
  497. package/corpus/templates/slides/server/lib/fig-design-system.ts +0 -110
@@ -0,0 +1,1473 @@
1
+ import { useT } from "@agent-native/core/client";
2
+ import {
3
+ IconChevronDown,
4
+ IconChevronRight,
5
+ IconEye,
6
+ IconEyeOff,
7
+ IconLock,
8
+ IconLockOpen,
9
+ IconLayoutGrid,
10
+ IconPlus,
11
+ IconSearch,
12
+ } from "@tabler/icons-react";
13
+ import {
14
+ useCallback,
15
+ useEffect,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ type KeyboardEvent,
20
+ type DragEvent,
21
+ type MouseEvent,
22
+ type ReactNode,
23
+ } from "react";
24
+
25
+ import { Button } from "@/components/ui/button";
26
+ import { Input } from "@/components/ui/input";
27
+ import {
28
+ Tooltip,
29
+ TooltipContent,
30
+ TooltipTrigger,
31
+ } from "@/components/ui/tooltip";
32
+ import { cn } from "@/lib/utils";
33
+
34
+ export type LayersPanelNodeType =
35
+ | "file"
36
+ | "screen"
37
+ | "frame"
38
+ | "group"
39
+ | "component"
40
+ | "instance"
41
+ | "section"
42
+ | "shape"
43
+ | "rectangle"
44
+ | "text"
45
+ | "image"
46
+ | "code"
47
+ | "element"
48
+ | "unknown";
49
+
50
+ export interface LayersPanelNode {
51
+ id: string;
52
+ name: string;
53
+ type?: LayersPanelNodeType;
54
+ layout?: {
55
+ display?: string;
56
+ flexDirection?: string;
57
+ alignItems?: string;
58
+ justifyContent?: string;
59
+ isFlexContainer?: boolean;
60
+ isGridContainer?: boolean;
61
+ };
62
+ children?: LayersPanelNode[];
63
+ detail?: string;
64
+ badge?: string | number;
65
+ hidden?: boolean;
66
+ locked?: boolean;
67
+ selectable?: boolean;
68
+ renamable?: boolean;
69
+ lockable?: boolean;
70
+ hideable?: boolean;
71
+ icon?: ReactNode;
72
+ }
73
+
74
+ export interface LayersPanelScreen extends Omit<
75
+ LayersPanelNode,
76
+ "children" | "type"
77
+ > {
78
+ type?: "screen" | "frame";
79
+ layers?: LayersPanelNode[];
80
+ }
81
+
82
+ export interface LayersPanelFile extends Omit<
83
+ LayersPanelNode,
84
+ "children" | "type"
85
+ > {
86
+ type?: "file";
87
+ filename?: string;
88
+ fileType?: string;
89
+ screens?: LayersPanelScreen[];
90
+ layers?: LayersPanelNode[];
91
+ }
92
+
93
+ export interface LayersPanelSelectionIntent {
94
+ id: string;
95
+ selectedIds: string[];
96
+ additive: boolean;
97
+ range: boolean;
98
+ source: "keyboard" | "pointer";
99
+ }
100
+
101
+ export interface LayersPanelMoveIntent {
102
+ draggedIds: string[];
103
+ targetId: string;
104
+ placement: "before" | "after" | "inside";
105
+ }
106
+
107
+ export interface LayersPanelLabels {
108
+ title: string;
109
+ screens: string;
110
+ screenOverview: string;
111
+ addScreen: string;
112
+ searchPlaceholder: string;
113
+ empty: string;
114
+ noMatches: string;
115
+ designLayers: string;
116
+ codeLayers: string;
117
+ elementLayers: string;
118
+ collapse: string;
119
+ expand: string;
120
+ lock: string;
121
+ unlock: string;
122
+ hide: string;
123
+ show: string;
124
+ rename: string;
125
+ selected: (count: number) => string;
126
+ }
127
+
128
+ export interface LayersPanelProps {
129
+ screens?: LayersPanelFile[];
130
+ activeScreenId?: string;
131
+ files?: LayersPanelFile[];
132
+ layers?: LayersPanelNode[];
133
+ codeLayers?: LayersPanelNode[];
134
+ elementLayers?: LayersPanelNode[];
135
+ selectedIds: readonly string[];
136
+ expandedIds: readonly string[];
137
+ searchQuery: string;
138
+ className?: string;
139
+ labels?: Partial<LayersPanelLabels>;
140
+ onSearchQueryChange: (query: string) => void;
141
+ onScreenSelect?: (id: string) => void;
142
+ onScreenOverview?: () => void;
143
+ onAddScreen?: () => void;
144
+ onExpandedIdsChange: (ids: string[]) => void;
145
+ onSelectionChange: (
146
+ ids: string[],
147
+ intent: LayersPanelSelectionIntent,
148
+ ) => void;
149
+ onRename?: (id: string, name: string) => void;
150
+ onToggleLocked?: (id: string, locked: boolean) => void;
151
+ onToggleHidden?: (id: string, hidden: boolean) => void;
152
+ onHoverLayer?: (id: string) => void;
153
+ onLeaveLayer?: (id: string) => void;
154
+ onMoveLayer?: (intent: LayersPanelMoveIntent) => void;
155
+ }
156
+
157
+ interface FlatLayerRow {
158
+ node: LayersPanelNode;
159
+ rowKey: string;
160
+ depth: number;
161
+ ancestorIds: string[];
162
+ hasChildren: boolean;
163
+ }
164
+
165
+ const SECTION_CODE_ID = "__design_layers_code__";
166
+ const SECTION_ELEMENT_ID = "__design_layers_elements__";
167
+
168
+ function defaultLabels(t: ReturnType<typeof useT>): LayersPanelLabels {
169
+ return {
170
+ title: t("layersPanel.title"),
171
+ screens: t("layersPanel.screens"),
172
+ screenOverview: t("designEditor.screenOverview"),
173
+ addScreen: t("layersPanel.addScreen"),
174
+ searchPlaceholder: t("layersPanel.searchPlaceholder"),
175
+ empty: t("layersPanel.empty"),
176
+ noMatches: t("layersPanel.noMatches"),
177
+ designLayers: t("layersPanel.designLayers"),
178
+ codeLayers: t("layersPanel.codeLayers"),
179
+ elementLayers: t("layersPanel.elementLayers"),
180
+ collapse: t("layersPanel.collapse"),
181
+ expand: t("layersPanel.expand"),
182
+ lock: t("layersPanel.lock"),
183
+ unlock: t("layersPanel.unlock"),
184
+ hide: t("layersPanel.hide"),
185
+ show: t("layersPanel.show"),
186
+ rename: t("layersPanel.rename"),
187
+ selected: (count) => t("layersPanel.selected", { count }),
188
+ };
189
+ }
190
+
191
+ function mergeLabels(
192
+ labels: LayersPanelProps["labels"],
193
+ t: ReturnType<typeof useT>,
194
+ ): LayersPanelLabels {
195
+ return { ...defaultLabels(t), ...labels };
196
+ }
197
+
198
+ function asFileNode(file: LayersPanelFile): LayersPanelNode {
199
+ const screens = file.screens?.map(asScreenNode) ?? [];
200
+ return {
201
+ ...file,
202
+ type: "file",
203
+ name: file.name || file.filename || "Untitled file",
204
+ detail: file.detail ?? file.fileType,
205
+ children: [...screens, ...(file.layers ?? [])],
206
+ };
207
+ }
208
+
209
+ function asScreenNode(screen: LayersPanelScreen): LayersPanelNode {
210
+ return {
211
+ ...screen,
212
+ type: screen.type ?? "screen",
213
+ children: screen.layers ?? [],
214
+ };
215
+ }
216
+
217
+ function sectionNode(
218
+ id: string,
219
+ name: string,
220
+ children: LayersPanelNode[] | undefined,
221
+ ): LayersPanelNode | null {
222
+ if (!children?.length) return null;
223
+ return {
224
+ id,
225
+ name,
226
+ type: "section",
227
+ selectable: false,
228
+ renamable: false,
229
+ lockable: false,
230
+ hideable: false,
231
+ children,
232
+ };
233
+ }
234
+
235
+ function buildRootNodes({
236
+ files,
237
+ layers,
238
+ codeLayers,
239
+ elementLayers,
240
+ labels,
241
+ }: Pick<
242
+ LayersPanelProps,
243
+ "files" | "layers" | "codeLayers" | "elementLayers"
244
+ > & {
245
+ labels: LayersPanelLabels;
246
+ }) {
247
+ const roots: LayersPanelNode[] = [
248
+ ...(files?.map(asFileNode) ?? []),
249
+ ...(layers ?? []),
250
+ ];
251
+ const codeSection = sectionNode(
252
+ SECTION_CODE_ID,
253
+ labels.codeLayers,
254
+ codeLayers,
255
+ );
256
+ const elementSection = sectionNode(
257
+ SECTION_ELEMENT_ID,
258
+ labels.elementLayers,
259
+ elementLayers,
260
+ );
261
+
262
+ if (codeSection) roots.push(codeSection);
263
+ if (elementSection) roots.push(elementSection);
264
+ return roots;
265
+ }
266
+
267
+ function nodeMatches(node: LayersPanelNode, query: string) {
268
+ if (!query) return true;
269
+ const haystack = [node.name, node.detail, node.type, node.badge]
270
+ .filter((value) => value !== null && value !== undefined)
271
+ .join(" ")
272
+ .toLowerCase();
273
+ return haystack.includes(query);
274
+ }
275
+
276
+ function filterNode(
277
+ node: LayersPanelNode,
278
+ query: string,
279
+ ): LayersPanelNode | null {
280
+ const normalized = query.trim().toLowerCase();
281
+ if (!normalized) return node;
282
+
283
+ const children = node.children
284
+ ?.map((child) => filterNode(child, normalized))
285
+ .filter((child): child is LayersPanelNode => Boolean(child));
286
+
287
+ if (nodeMatches(node, normalized) || children?.length) {
288
+ return { ...node, children };
289
+ }
290
+ return null;
291
+ }
292
+
293
+ function flattenRows(
294
+ nodes: LayersPanelNode[],
295
+ expandedIds: ReadonlySet<string>,
296
+ forceExpanded: boolean,
297
+ depth = 0,
298
+ parentKey = "root",
299
+ ancestorIds: string[] = [],
300
+ rows: FlatLayerRow[] = [],
301
+ ) {
302
+ nodes.forEach((node, index) => {
303
+ const children = node.children ?? [];
304
+ const hasChildren = children.length > 0;
305
+ const rowKey = `${parentKey}/${node.id}:${index}`;
306
+ rows.push({ node, rowKey, depth, ancestorIds, hasChildren });
307
+ if (hasChildren && (forceExpanded || expandedIds.has(node.id))) {
308
+ flattenRows(
309
+ children,
310
+ expandedIds,
311
+ forceExpanded,
312
+ depth + 1,
313
+ rowKey,
314
+ [...ancestorIds, node.id],
315
+ rows,
316
+ );
317
+ }
318
+ });
319
+ return rows;
320
+ }
321
+
322
+ function nextExpandedIds(
323
+ ids: readonly string[],
324
+ nodeId: string,
325
+ expanded: boolean,
326
+ ) {
327
+ const next = new Set(ids);
328
+ if (expanded) {
329
+ next.add(nodeId);
330
+ } else {
331
+ next.delete(nodeId);
332
+ }
333
+ return Array.from(next);
334
+ }
335
+
336
+ function collectAncestorIds(
337
+ nodes: LayersPanelNode[],
338
+ targetIds: ReadonlySet<string>,
339
+ ): string[] {
340
+ const ancestors = new Set<string>();
341
+
342
+ function visit(node: LayersPanelNode, path: string[]): boolean {
343
+ const children = node.children ?? [];
344
+ let containsSelectedChild = false;
345
+ children.forEach((child) => {
346
+ if (visit(child, [...path, node.id])) {
347
+ containsSelectedChild = true;
348
+ }
349
+ });
350
+ const containsSelected = targetIds.has(node.id) || containsSelectedChild;
351
+ if (containsSelected) {
352
+ path.forEach((id) => ancestors.add(id));
353
+ }
354
+ return containsSelected;
355
+ }
356
+
357
+ nodes.forEach((node) => visit(node, []));
358
+ return Array.from(ancestors);
359
+ }
360
+
361
+ function layerCanShowBadge(node: LayersPanelNode) {
362
+ return (
363
+ node.type === "file" ||
364
+ node.type === "screen" ||
365
+ (node.type === "frame" && node.id.startsWith("__"))
366
+ );
367
+ }
368
+
369
+ export function LayersPanel({
370
+ screens,
371
+ activeScreenId,
372
+ files,
373
+ layers,
374
+ codeLayers,
375
+ elementLayers,
376
+ selectedIds,
377
+ expandedIds,
378
+ searchQuery,
379
+ className,
380
+ labels: labelsProp,
381
+ onSearchQueryChange,
382
+ onScreenSelect,
383
+ onScreenOverview,
384
+ onAddScreen,
385
+ onExpandedIdsChange,
386
+ onSelectionChange,
387
+ onRename,
388
+ onToggleLocked,
389
+ onToggleHidden,
390
+ onHoverLayer,
391
+ onLeaveLayer,
392
+ onMoveLayer,
393
+ }: LayersPanelProps) {
394
+ const t = useT();
395
+ const labels = useMemo(() => mergeLabels(labelsProp, t), [labelsProp, t]);
396
+ const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]);
397
+ const expandedIdSet = useMemo(() => new Set(expandedIds), [expandedIds]);
398
+ const lastSelectionAnchorRef = useRef<string | null>(selectedIds[0] ?? null);
399
+ const searchInputRef = useRef<HTMLInputElement>(null);
400
+ const rowElementRefs = useRef(new Map<string, HTMLDivElement>());
401
+ const [renamingId, setRenamingId] = useState<string | null>(null);
402
+ const [renameDraft, setRenameDraft] = useState("");
403
+ const [dropIndicator, setDropIndicator] =
404
+ useState<LayersPanelMoveIntent | null>(null);
405
+ const [searchOpen, setSearchOpen] = useState(Boolean(searchQuery));
406
+
407
+ const roots = useMemo(
408
+ () =>
409
+ buildRootNodes({
410
+ files,
411
+ layers,
412
+ codeLayers,
413
+ elementLayers,
414
+ labels,
415
+ }),
416
+ [codeLayers, elementLayers, files, labels, layers],
417
+ );
418
+
419
+ const visibleRows = useMemo(() => {
420
+ const filtered = roots
421
+ .map((node) => filterNode(node, searchQuery))
422
+ .filter((node): node is LayersPanelNode => Boolean(node));
423
+ return flattenRows(filtered, expandedIdSet, Boolean(searchQuery.trim()));
424
+ }, [expandedIdSet, roots, searchQuery]);
425
+
426
+ const selectedAncestorIds = useMemo(
427
+ () => collectAncestorIds(roots, selectedIdSet),
428
+ [roots, selectedIdSet],
429
+ );
430
+
431
+ useEffect(() => {
432
+ if (selectedAncestorIds.length === 0) return;
433
+ const next = new Set(expandedIds);
434
+ let changed = false;
435
+ selectedAncestorIds.forEach((id) => {
436
+ if (!next.has(id)) {
437
+ next.add(id);
438
+ changed = true;
439
+ }
440
+ });
441
+ if (changed) onExpandedIdsChange(Array.from(next));
442
+ }, [expandedIds, onExpandedIdsChange, selectedAncestorIds]);
443
+
444
+ const selectedScrollId = selectedIds[selectedIds.length - 1] ?? null;
445
+ const selectedScrollRowKey = useMemo(() => {
446
+ if (!selectedScrollId) return null;
447
+ return (
448
+ visibleRows.find((row) => row.node.id === selectedScrollId)?.rowKey ??
449
+ null
450
+ );
451
+ }, [selectedScrollId, visibleRows]);
452
+
453
+ useEffect(() => {
454
+ if (!selectedScrollRowKey) return;
455
+ const frame = window.requestAnimationFrame(() => {
456
+ rowElementRefs.current.get(selectedScrollRowKey)?.scrollIntoView({
457
+ block: "nearest",
458
+ });
459
+ });
460
+ return () => window.cancelAnimationFrame(frame);
461
+ }, [selectedScrollRowKey]);
462
+
463
+ const selectableVisibleIds = useMemo(
464
+ () =>
465
+ visibleRows
466
+ .map(({ node }) => node)
467
+ .filter((node) => node.selectable !== false)
468
+ .map((node) => node.id),
469
+ [visibleRows],
470
+ );
471
+
472
+ const selectNode = useCallback(
473
+ (
474
+ id: string,
475
+ options: {
476
+ additive: boolean;
477
+ range: boolean;
478
+ source: "keyboard" | "pointer";
479
+ },
480
+ ) => {
481
+ let nextIds: string[];
482
+ if (options.range && lastSelectionAnchorRef.current) {
483
+ const from = selectableVisibleIds.indexOf(
484
+ lastSelectionAnchorRef.current,
485
+ );
486
+ const to = selectableVisibleIds.indexOf(id);
487
+ if (from >= 0 && to >= 0) {
488
+ const [start, end] = from < to ? [from, to] : [to, from];
489
+ const rangeIds = selectableVisibleIds.slice(start, end + 1);
490
+ nextIds = options.additive
491
+ ? Array.from(new Set([...selectedIds, ...rangeIds]))
492
+ : rangeIds;
493
+ } else {
494
+ nextIds = [id];
495
+ }
496
+ } else if (options.additive) {
497
+ nextIds = selectedIdSet.has(id)
498
+ ? selectedIds.filter((selectedId) => selectedId !== id)
499
+ : [...selectedIds, id];
500
+ } else {
501
+ nextIds = [id];
502
+ }
503
+
504
+ lastSelectionAnchorRef.current = id;
505
+ onSelectionChange(nextIds, { id, selectedIds: nextIds, ...options });
506
+ },
507
+ [onSelectionChange, selectableVisibleIds, selectedIdSet, selectedIds],
508
+ );
509
+
510
+ const commitRename = useCallback(
511
+ (id: string) => {
512
+ const nextName = renameDraft.trim();
513
+ // The panel only emits rename intent. Code-backed DOM layer renames must
514
+ // persist through a safe source edit that updates data-agent-native-layer-name.
515
+ if (nextName) onRename?.(id, nextName);
516
+ setRenamingId(null);
517
+ setRenameDraft("");
518
+ },
519
+ [onRename, renameDraft],
520
+ );
521
+
522
+ const startRename = useCallback(
523
+ (node: LayersPanelNode) => {
524
+ if (!onRename || node.renamable === false) return;
525
+ setRenamingId(node.id);
526
+ setRenameDraft(node.name);
527
+ },
528
+ [onRename],
529
+ );
530
+
531
+ const registerRowElement = useCallback(
532
+ (rowKey: string, element: HTMLDivElement | null) => {
533
+ if (element) {
534
+ rowElementRefs.current.set(rowKey, element);
535
+ } else {
536
+ rowElementRefs.current.delete(rowKey);
537
+ }
538
+ },
539
+ [],
540
+ );
541
+
542
+ const hasAnyRows = roots.length > 0;
543
+ const selectedCount = selectedIds.length;
544
+ const screenRows = screens ?? files ?? [];
545
+ const openSearch = useCallback(() => {
546
+ setSearchOpen(true);
547
+ window.requestAnimationFrame(() => searchInputRef.current?.focus());
548
+ }, []);
549
+ const shouldShowSearch = searchOpen || Boolean(searchQuery.trim());
550
+ const collapseTargetId = useMemo(() => {
551
+ for (let index = selectedIds.length - 1; index >= 0; index -= 1) {
552
+ const selectedRow = visibleRows.find(
553
+ (row) => row.node.id === selectedIds[index],
554
+ );
555
+ if (!selectedRow) continue;
556
+ if (selectedRow.hasChildren && expandedIdSet.has(selectedRow.node.id)) {
557
+ return selectedRow.node.id;
558
+ }
559
+ }
560
+ return null;
561
+ }, [expandedIdSet, selectedIds, visibleRows]);
562
+
563
+ const collapseSelectedLayer = useCallback(() => {
564
+ if (!collapseTargetId) return;
565
+ onExpandedIdsChange(
566
+ expandedIds.filter((expandedId) => expandedId !== collapseTargetId),
567
+ );
568
+ }, [collapseTargetId, expandedIds, onExpandedIdsChange]);
569
+
570
+ return (
571
+ <aside
572
+ className={cn(
573
+ "flex h-full min-h-0 w-full flex-col overflow-hidden bg-[var(--design-editor-panel-bg)] text-[12px] text-foreground",
574
+ className,
575
+ )}
576
+ aria-label={labels.title}
577
+ >
578
+ {screenRows.length > 0 ? (
579
+ <div className="shrink-0 border-b border-[var(--design-editor-panel-divider-color)] pb-2">
580
+ <div className="flex h-10 items-center justify-between px-3">
581
+ <h2 className="truncate text-[12px] font-semibold text-foreground">
582
+ {labels.screens}
583
+ </h2>
584
+ <div className="flex items-center gap-0.5 text-muted-foreground">
585
+ <IconTooltipButton
586
+ label={labels.screenOverview}
587
+ onClick={onScreenOverview}
588
+ >
589
+ <IconLayoutGrid className="size-3.5" />
590
+ </IconTooltipButton>
591
+ <IconTooltipButton
592
+ label={labels.searchPlaceholder}
593
+ onClick={openSearch}
594
+ >
595
+ <IconSearch className="size-4" />
596
+ </IconTooltipButton>
597
+ <IconTooltipButton
598
+ label={labels.addScreen}
599
+ disabled={!onAddScreen}
600
+ onClick={onAddScreen}
601
+ >
602
+ <IconPlus className="size-4" />
603
+ </IconTooltipButton>
604
+ </div>
605
+ </div>
606
+ <div className="space-y-0.5 px-2">
607
+ {screenRows.map((screen) => {
608
+ const isActive = screen.id === activeScreenId;
609
+ return (
610
+ <button
611
+ key={screen.id}
612
+ type="button"
613
+ className={cn(
614
+ "flex h-8 w-full cursor-default items-center gap-2 rounded-[5px] px-2 text-left text-[12px] font-semibold outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]",
615
+ isActive
616
+ ? "bg-[var(--design-editor-active-row-color)] text-foreground"
617
+ : "text-foreground/85 hover:bg-[var(--design-editor-active-row-color)] hover:text-foreground",
618
+ )}
619
+ onClick={() => onScreenSelect?.(screen.id)}
620
+ title={screen.filename ?? screen.name}
621
+ >
622
+ <LayerGlyph
623
+ node={{ ...screen, type: "file" }}
624
+ selected={false}
625
+ />
626
+ <span className="min-w-0 flex-1 truncate">{screen.name}</span>
627
+ {screen.badge ? (
628
+ <span className="rounded-sm bg-muted px-1 text-[10px] font-normal text-muted-foreground">
629
+ {screen.badge}
630
+ </span>
631
+ ) : null}
632
+ </button>
633
+ );
634
+ })}
635
+ </div>
636
+ </div>
637
+ ) : null}
638
+
639
+ <div className="flex h-10 shrink-0 items-center justify-between px-3">
640
+ <div className="min-w-0">
641
+ <h2 className="truncate text-[12px] font-semibold text-foreground">
642
+ {labels.title}
643
+ </h2>
644
+ {selectedCount > 1 ? (
645
+ <p className="truncate text-[10px] text-muted-foreground">
646
+ {labels.selected(selectedCount)}
647
+ </p>
648
+ ) : null}
649
+ </div>
650
+ <div className="flex items-center gap-0.5 text-muted-foreground">
651
+ <button
652
+ type="button"
653
+ className="flex size-6 items-center justify-center rounded-sm text-muted-foreground hover:bg-[var(--design-editor-layer-hover-color)] hover:text-foreground disabled:pointer-events-none disabled:opacity-35"
654
+ aria-label={labels.collapse}
655
+ disabled={!collapseTargetId}
656
+ onClick={collapseSelectedLayer}
657
+ >
658
+ <LayerOptionsGlyph className="size-4" />
659
+ </button>
660
+ </div>
661
+ </div>
662
+
663
+ {shouldShowSearch ? (
664
+ <div className="shrink-0 p-2">
665
+ <div className="relative">
666
+ <IconSearch className="pointer-events-none absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
667
+ <Input
668
+ ref={searchInputRef}
669
+ value={searchQuery}
670
+ onChange={(event) => onSearchQueryChange(event.target.value)}
671
+ onKeyDown={(event) => {
672
+ if (event.key === "Escape" && !searchQuery.trim()) {
673
+ setSearchOpen(false);
674
+ }
675
+ }}
676
+ placeholder={labels.searchPlaceholder}
677
+ className="h-7 rounded-[4px] border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] pl-7 text-[12px] shadow-none placeholder:text-muted-foreground/70 focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
678
+ />
679
+ </div>
680
+ </div>
681
+ ) : null}
682
+
683
+ <div className="min-h-0 flex-1 overflow-y-auto py-2">
684
+ {visibleRows.length ? (
685
+ <div className="px-2" role="tree" aria-label={labels.title}>
686
+ {visibleRows.map((row) => (
687
+ <LayerRow
688
+ key={row.rowKey}
689
+ row={row}
690
+ labels={labels}
691
+ isExpanded={expandedIdSet.has(row.node.id)}
692
+ isSelected={selectedIdSet.has(row.node.id)}
693
+ isInSelectedSubtree={row.ancestorIds.some((id) =>
694
+ selectedIdSet.has(id),
695
+ )}
696
+ isActiveScreen={
697
+ row.node.id === activeScreenId &&
698
+ (row.node.type === "file" ||
699
+ row.node.type === "screen" ||
700
+ row.node.type === "frame")
701
+ }
702
+ isRenaming={renamingId === row.node.id}
703
+ rowRef={(element) => registerRowElement(row.rowKey, element)}
704
+ renameDraft={renameDraft}
705
+ onRenameDraftChange={setRenameDraft}
706
+ onCommitRename={commitRename}
707
+ onCancelRename={() => setRenamingId(null)}
708
+ onStartRename={startRename}
709
+ onSelect={selectNode}
710
+ onToggleExpanded={(expanded) =>
711
+ onExpandedIdsChange(
712
+ nextExpandedIds(expandedIds, row.node.id, expanded),
713
+ )
714
+ }
715
+ onToggleLocked={onToggleLocked}
716
+ onToggleHidden={onToggleHidden}
717
+ onHoverLayer={onHoverLayer}
718
+ onLeaveLayer={onLeaveLayer}
719
+ onMoveLayer={onMoveLayer}
720
+ dropIndicator={dropIndicator}
721
+ onDropIndicatorChange={setDropIndicator}
722
+ selectedIds={selectedIds}
723
+ />
724
+ ))}
725
+ </div>
726
+ ) : (
727
+ <div className="px-3 py-8 text-center text-[11px] text-muted-foreground">
728
+ {hasAnyRows ? labels.noMatches : labels.empty}
729
+ </div>
730
+ )}
731
+ </div>
732
+ </aside>
733
+ );
734
+ }
735
+
736
+ function LayerRow({
737
+ row,
738
+ labels,
739
+ isExpanded,
740
+ isSelected,
741
+ isInSelectedSubtree,
742
+ isActiveScreen,
743
+ isRenaming,
744
+ rowRef,
745
+ renameDraft,
746
+ onRenameDraftChange,
747
+ onCommitRename,
748
+ onCancelRename,
749
+ onStartRename,
750
+ onSelect,
751
+ onToggleExpanded,
752
+ onToggleLocked,
753
+ onToggleHidden,
754
+ onHoverLayer,
755
+ onLeaveLayer,
756
+ onMoveLayer,
757
+ dropIndicator,
758
+ onDropIndicatorChange,
759
+ selectedIds,
760
+ }: {
761
+ row: FlatLayerRow;
762
+ labels: LayersPanelLabels;
763
+ isExpanded: boolean;
764
+ isSelected: boolean;
765
+ isInSelectedSubtree: boolean;
766
+ isActiveScreen: boolean;
767
+ isRenaming: boolean;
768
+ rowRef: (element: HTMLDivElement | null) => void;
769
+ renameDraft: string;
770
+ onRenameDraftChange: (value: string) => void;
771
+ onCommitRename: (id: string) => void;
772
+ onCancelRename: () => void;
773
+ onStartRename: (node: LayersPanelNode) => void;
774
+ onSelect: (
775
+ id: string,
776
+ options: {
777
+ additive: boolean;
778
+ range: boolean;
779
+ source: "keyboard" | "pointer";
780
+ },
781
+ ) => void;
782
+ onToggleExpanded: (expanded: boolean) => void;
783
+ onToggleLocked?: (id: string, locked: boolean) => void;
784
+ onToggleHidden?: (id: string, hidden: boolean) => void;
785
+ onHoverLayer?: (id: string) => void;
786
+ onLeaveLayer?: (id: string) => void;
787
+ onMoveLayer?: (intent: LayersPanelMoveIntent) => void;
788
+ dropIndicator: LayersPanelMoveIntent | null;
789
+ onDropIndicatorChange: (intent: LayersPanelMoveIntent | null) => void;
790
+ selectedIds: readonly string[];
791
+ }) {
792
+ const { node, depth, hasChildren } = row;
793
+ const selectable = node.selectable !== false;
794
+ const lockable = node.lockable !== false && Boolean(onToggleLocked);
795
+ const hideable = node.hideable !== false && Boolean(onToggleHidden);
796
+ const draggable = selectable && Boolean(onMoveLayer);
797
+ const activeDrop =
798
+ dropIndicator?.targetId === node.id ? dropIndicator.placement : null;
799
+
800
+ const handlePointerSelect = (event: MouseEvent<HTMLButtonElement>) => {
801
+ if (!selectable) return;
802
+ onSelect(node.id, {
803
+ additive: event.metaKey || event.ctrlKey,
804
+ range: event.shiftKey,
805
+ source: "pointer",
806
+ });
807
+ };
808
+
809
+ const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
810
+ if (event.key === "Enter") {
811
+ event.preventDefault();
812
+ onStartRename(node);
813
+ return;
814
+ }
815
+ if (event.key === "ArrowRight" && hasChildren && !isExpanded) {
816
+ event.preventDefault();
817
+ onToggleExpanded(true);
818
+ return;
819
+ }
820
+ if (event.key === "ArrowLeft" && hasChildren && isExpanded) {
821
+ event.preventDefault();
822
+ onToggleExpanded(false);
823
+ }
824
+ };
825
+
826
+ const handleDragStart = (event: DragEvent<HTMLDivElement>) => {
827
+ if (!draggable) {
828
+ event.preventDefault();
829
+ return;
830
+ }
831
+ const draggedIds = selectedIds.includes(node.id)
832
+ ? selectedIds.filter((id) => !id.startsWith("__"))
833
+ : [node.id];
834
+ event.dataTransfer.effectAllowed = "move";
835
+ event.dataTransfer.setData("application/x-design-layer-id", node.id);
836
+ event.dataTransfer.setData(
837
+ "application/x-design-layer-ids",
838
+ JSON.stringify(draggedIds),
839
+ );
840
+ };
841
+
842
+ const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
843
+ if (!onMoveLayer || !selectable) return;
844
+ const sourceId =
845
+ event.dataTransfer.getData("application/x-design-layer-id") || "";
846
+ if (sourceId === node.id) return;
847
+ const rawIds = event.dataTransfer.getData("application/x-design-layer-ids");
848
+ let draggedIds = [sourceId];
849
+ try {
850
+ const parsed = JSON.parse(rawIds);
851
+ if (Array.isArray(parsed)) {
852
+ draggedIds = parsed.filter(
853
+ (id): id is string => typeof id === "string",
854
+ );
855
+ }
856
+ } catch {
857
+ // Ignore malformed drag payloads and fall back to the primary id.
858
+ }
859
+ const cleanedIds = draggedIds.filter(
860
+ (id) => id && id !== node.id && !id.startsWith("__"),
861
+ );
862
+ if (cleanedIds.length === 0) return;
863
+ event.preventDefault();
864
+ event.dataTransfer.dropEffect = "move";
865
+ onDropIndicatorChange({
866
+ draggedIds: cleanedIds,
867
+ targetId: node.id,
868
+ placement: dropPlacementForEvent(event, hasChildren),
869
+ });
870
+ };
871
+
872
+ const handleDrop = (event: DragEvent<HTMLDivElement>) => {
873
+ if (
874
+ !onMoveLayer ||
875
+ !selectable ||
876
+ !dropIndicator ||
877
+ dropIndicator.targetId !== node.id
878
+ ) {
879
+ onDropIndicatorChange(null);
880
+ return;
881
+ }
882
+ event.preventDefault();
883
+ const rawIds = event.dataTransfer.getData("application/x-design-layer-ids");
884
+ let draggedIds = [
885
+ event.dataTransfer.getData("application/x-design-layer-id"),
886
+ ];
887
+ try {
888
+ const parsed = JSON.parse(rawIds);
889
+ if (Array.isArray(parsed)) {
890
+ draggedIds = parsed.filter(
891
+ (id): id is string => typeof id === "string",
892
+ );
893
+ }
894
+ } catch {
895
+ // Ignore malformed drag payloads and fall back to the primary id.
896
+ }
897
+ const cleanedIds = draggedIds.filter(
898
+ (id) => id && id !== node.id && !id.startsWith("__"),
899
+ );
900
+ if (cleanedIds.length > 0) {
901
+ onMoveLayer({
902
+ draggedIds: cleanedIds,
903
+ targetId: node.id,
904
+ placement: dropIndicator.placement,
905
+ });
906
+ }
907
+ onDropIndicatorChange(null);
908
+ };
909
+
910
+ return (
911
+ <div
912
+ ref={rowRef}
913
+ role="treeitem"
914
+ aria-expanded={hasChildren ? isExpanded : undefined}
915
+ aria-level={depth + 1}
916
+ aria-selected={selectable ? isSelected : undefined}
917
+ className="relative"
918
+ draggable={draggable}
919
+ onDragStart={handleDragStart}
920
+ onDragOver={handleDragOver}
921
+ onDragLeave={() => onDropIndicatorChange(null)}
922
+ onDrop={handleDrop}
923
+ onDragEnd={() => onDropIndicatorChange(null)}
924
+ onMouseEnter={() => onHoverLayer?.(node.id)}
925
+ onMouseLeave={() => onLeaveLayer?.(node.id)}
926
+ >
927
+ {activeDrop === "before" ? (
928
+ <span className="pointer-events-none absolute left-2 right-2 top-0 z-10 h-px bg-[var(--design-editor-accent-color)]" />
929
+ ) : null}
930
+ {activeDrop === "after" ? (
931
+ <span className="pointer-events-none absolute bottom-0 left-2 right-2 z-10 h-px bg-[var(--design-editor-accent-color)]" />
932
+ ) : null}
933
+ <div
934
+ className={cn(
935
+ "group flex h-8 items-center gap-1 rounded-[5px] pr-1 text-[12px]",
936
+ activeDrop === "inside" &&
937
+ "ring-1 ring-inset ring-[var(--design-editor-accent-color)]",
938
+ isSelected &&
939
+ "bg-[var(--design-editor-selection-color)] text-foreground",
940
+ !isSelected &&
941
+ isInSelectedSubtree &&
942
+ "bg-[var(--design-editor-selected-subtree-color)] text-foreground/95",
943
+ !isSelected &&
944
+ isActiveScreen &&
945
+ "bg-[var(--design-editor-active-row-color)] text-foreground hover:bg-[var(--design-editor-active-row-color)]",
946
+ !isSelected &&
947
+ !isInSelectedSubtree &&
948
+ !isActiveScreen &&
949
+ "text-foreground/90 hover:bg-[var(--design-editor-layer-hover-color)] hover:text-foreground",
950
+ node.hidden && "text-muted-foreground",
951
+ )}
952
+ style={{ paddingLeft: 4 + depth * 28 }}
953
+ >
954
+ {hasChildren ? (
955
+ <Button
956
+ type="button"
957
+ variant="ghost"
958
+ size="icon"
959
+ className="size-4 shrink-0 rounded-sm p-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
960
+ aria-label={isExpanded ? labels.collapse : labels.expand}
961
+ onClick={(event) => {
962
+ event.stopPropagation();
963
+ onToggleExpanded(!isExpanded);
964
+ }}
965
+ >
966
+ {isExpanded ? (
967
+ <IconChevronDown className="size-4" />
968
+ ) : (
969
+ <IconChevronRight className="size-4 rtl:-scale-x-100" />
970
+ )}
971
+ </Button>
972
+ ) : (
973
+ <span className="size-4 shrink-0" />
974
+ )}
975
+
976
+ <button
977
+ type="button"
978
+ disabled={!selectable}
979
+ className={cn(
980
+ "flex min-w-0 flex-1 items-center gap-2 overflow-hidden rounded-sm px-0.5 py-0 text-left outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]",
981
+ selectable ? "cursor-default" : "cursor-default opacity-80",
982
+ )}
983
+ onClick={handlePointerSelect}
984
+ onDoubleClick={() => onStartRename(node)}
985
+ onKeyDown={handleKeyDown}
986
+ >
987
+ <span
988
+ className={cn(
989
+ "shrink-0 text-muted-foreground",
990
+ (isSelected || isInSelectedSubtree) && "text-foreground",
991
+ )}
992
+ >
993
+ {node.icon ?? (
994
+ <LayerGlyph
995
+ node={node}
996
+ selected={isSelected}
997
+ inSelectedSubtree={isInSelectedSubtree}
998
+ />
999
+ )}
1000
+ </span>
1001
+ {isRenaming ? (
1002
+ <input
1003
+ autoFocus
1004
+ value={renameDraft}
1005
+ onClick={(event) => event.stopPropagation()}
1006
+ onChange={(event) => onRenameDraftChange(event.target.value)}
1007
+ onBlur={() => onCommitRename(node.id)}
1008
+ onKeyDown={(event) => {
1009
+ if (event.key === "Enter") {
1010
+ event.preventDefault();
1011
+ onCommitRename(node.id);
1012
+ } else if (event.key === "Escape") {
1013
+ event.preventDefault();
1014
+ onCancelRename();
1015
+ }
1016
+ }}
1017
+ className="h-6 min-w-0 flex-1 rounded-[4px] border border-[var(--design-editor-accent-color)] bg-[var(--design-editor-panel-bg)] px-1.5 text-[12px] text-foreground outline-none"
1018
+ aria-label={labels.rename}
1019
+ />
1020
+ ) : (
1021
+ <span
1022
+ className={cn(
1023
+ "min-w-0 flex-1 truncate font-medium leading-none",
1024
+ node.hidden && "line-through",
1025
+ )}
1026
+ >
1027
+ {node.name}
1028
+ </span>
1029
+ )}
1030
+ {!isRenaming &&
1031
+ layerCanShowBadge(node) &&
1032
+ node.badge !== null &&
1033
+ node.badge !== undefined ? (
1034
+ <span className="shrink-0 rounded-sm bg-muted px-1 text-[10px] text-muted-foreground">
1035
+ {node.badge}
1036
+ </span>
1037
+ ) : null}
1038
+ </button>
1039
+
1040
+ {lockable ? (
1041
+ <Tooltip>
1042
+ <TooltipTrigger asChild>
1043
+ <Button
1044
+ type="button"
1045
+ variant="ghost"
1046
+ size="icon"
1047
+ className={cn(
1048
+ "size-5 shrink-0 rounded-sm p-0 text-muted-foreground opacity-0 hover:bg-transparent hover:text-foreground group-hover:opacity-100 focus-visible:opacity-100",
1049
+ node.locked && "opacity-100",
1050
+ isSelected && "text-foreground",
1051
+ )}
1052
+ aria-label={node.locked ? labels.unlock : labels.lock}
1053
+ onClick={(event) => {
1054
+ event.stopPropagation();
1055
+ onToggleLocked?.(node.id, !node.locked);
1056
+ }}
1057
+ >
1058
+ {node.locked ? (
1059
+ <IconLock className="size-3" />
1060
+ ) : (
1061
+ <IconLockOpen className="size-3" />
1062
+ )}
1063
+ </Button>
1064
+ </TooltipTrigger>
1065
+ <TooltipContent>
1066
+ {node.locked ? labels.unlock : labels.lock}
1067
+ </TooltipContent>
1068
+ </Tooltip>
1069
+ ) : null}
1070
+
1071
+ {hideable ? (
1072
+ <Tooltip>
1073
+ <TooltipTrigger asChild>
1074
+ <Button
1075
+ type="button"
1076
+ variant="ghost"
1077
+ size="icon"
1078
+ className={cn(
1079
+ "size-5 shrink-0 rounded-sm p-0 text-muted-foreground opacity-0 hover:bg-transparent hover:text-foreground group-hover:opacity-100 focus-visible:opacity-100",
1080
+ node.hidden && "opacity-100",
1081
+ isSelected && "text-foreground",
1082
+ )}
1083
+ aria-label={node.hidden ? labels.show : labels.hide}
1084
+ onClick={(event) => {
1085
+ event.stopPropagation();
1086
+ onToggleHidden?.(node.id, !node.hidden);
1087
+ }}
1088
+ >
1089
+ {node.hidden ? (
1090
+ <IconEyeOff className="size-3" />
1091
+ ) : (
1092
+ <IconEye className="size-3" />
1093
+ )}
1094
+ </Button>
1095
+ </TooltipTrigger>
1096
+ <TooltipContent>
1097
+ {node.hidden ? labels.show : labels.hide}
1098
+ </TooltipContent>
1099
+ </Tooltip>
1100
+ ) : null}
1101
+ </div>
1102
+ </div>
1103
+ );
1104
+ }
1105
+
1106
+ function IconTooltipButton({
1107
+ label,
1108
+ onClick,
1109
+ disabled,
1110
+ children,
1111
+ }: {
1112
+ label: string;
1113
+ onClick?: () => void;
1114
+ disabled?: boolean;
1115
+ children: ReactNode;
1116
+ }) {
1117
+ return (
1118
+ <Tooltip>
1119
+ <TooltipTrigger asChild>
1120
+ <span className="inline-flex">
1121
+ <Button
1122
+ type="button"
1123
+ variant="ghost"
1124
+ size="icon"
1125
+ className="size-6 rounded-sm p-0 text-muted-foreground hover:bg-[var(--design-editor-layer-hover-color)] hover:text-foreground"
1126
+ aria-label={label}
1127
+ disabled={disabled}
1128
+ onClick={onClick}
1129
+ >
1130
+ {children}
1131
+ </Button>
1132
+ </span>
1133
+ </TooltipTrigger>
1134
+ <TooltipContent>{label}</TooltipContent>
1135
+ </Tooltip>
1136
+ );
1137
+ }
1138
+
1139
+ function LayerGlyph({
1140
+ node,
1141
+ selected,
1142
+ inSelectedSubtree,
1143
+ }: {
1144
+ node: Pick<LayersPanelNode, "type" | "layout">;
1145
+ selected?: boolean;
1146
+ inSelectedSubtree?: boolean;
1147
+ }) {
1148
+ const common = "size-4";
1149
+ const componentColor =
1150
+ selected || inSelectedSubtree
1151
+ ? "text-foreground"
1152
+ : "text-[var(--design-editor-accent-color)]";
1153
+ switch (node.type) {
1154
+ case "file":
1155
+ case "screen":
1156
+ return <PageLayerGlyph className={common} />;
1157
+ case "frame":
1158
+ return <LayoutLayerGlyph node={node} className={common} />;
1159
+ case "group":
1160
+ case "section":
1161
+ return <LayoutLayerGlyph node={node} className={common} />;
1162
+ case "component":
1163
+ case "instance":
1164
+ return <ComponentLayerGlyph className={cn(common, componentColor)} />;
1165
+ case "shape":
1166
+ case "rectangle":
1167
+ return <RectangleLayerGlyph className={common} />;
1168
+ case "text":
1169
+ return <TextLayerGlyph className={common} />;
1170
+ case "image":
1171
+ return <ImageLayerGlyph className={common} />;
1172
+ case "code":
1173
+ case "element":
1174
+ return node.layout?.isFlexContainer || node.layout?.isGridContainer ? (
1175
+ <LayoutLayerGlyph node={node} className={common} />
1176
+ ) : (
1177
+ <ElementLayerGlyph className={common} />
1178
+ );
1179
+ default:
1180
+ return <FrameLayerGlyph className={common} />;
1181
+ }
1182
+ }
1183
+
1184
+ function LayerOptionsGlyph({ className }: { className?: string }) {
1185
+ return (
1186
+ <svg
1187
+ viewBox="0 0 16 16"
1188
+ fill="none"
1189
+ stroke="currentColor"
1190
+ strokeWidth="1.4"
1191
+ strokeLinecap="round"
1192
+ strokeLinejoin="round"
1193
+ className={className}
1194
+ aria-hidden="true"
1195
+ >
1196
+ <path d="M3 4h6" />
1197
+ <path d="M3 8h8" />
1198
+ <path d="M3 12h5" />
1199
+ <path d="M12.5 4.5l1 1 1-1" />
1200
+ <path d="M12.5 11.5l1-1 1 1" />
1201
+ </svg>
1202
+ );
1203
+ }
1204
+
1205
+ function PageLayerGlyph({ className }: { className?: string }) {
1206
+ return (
1207
+ <svg
1208
+ viewBox="0 0 16 16"
1209
+ fill="none"
1210
+ stroke="currentColor"
1211
+ strokeWidth="1.45"
1212
+ strokeLinecap="round"
1213
+ strokeLinejoin="round"
1214
+ className={className}
1215
+ aria-hidden="true"
1216
+ >
1217
+ <path d="M4.5 2.5h5.1l2 2V13a.8.8 0 0 1-.8.8H4.5a.8.8 0 0 1-.8-.8V3.3a.8.8 0 0 1 .8-.8Z" />
1218
+ <path d="M9.6 2.6v2.1h2.1" />
1219
+ </svg>
1220
+ );
1221
+ }
1222
+
1223
+ function FrameLayerGlyph({ className }: { className?: string }) {
1224
+ return (
1225
+ <svg
1226
+ viewBox="0 0 16 16"
1227
+ fill="none"
1228
+ stroke="currentColor"
1229
+ strokeWidth="1.45"
1230
+ strokeLinecap="round"
1231
+ strokeLinejoin="round"
1232
+ className={className}
1233
+ aria-hidden="true"
1234
+ >
1235
+ <path d="M3.5 4.5h9" />
1236
+ <path d="M3.5 11.5h9" />
1237
+ <path d="M5.5 6.5h5" />
1238
+ <path d="M5.5 9.5h5" />
1239
+ </svg>
1240
+ );
1241
+ }
1242
+
1243
+ function LayoutLayerGlyph({
1244
+ node,
1245
+ className,
1246
+ }: {
1247
+ node: Pick<LayersPanelNode, "layout">;
1248
+ className?: string;
1249
+ }) {
1250
+ if (node.layout?.isGridContainer) {
1251
+ return (
1252
+ <svg
1253
+ viewBox="0 0 16 16"
1254
+ fill="none"
1255
+ stroke="currentColor"
1256
+ strokeWidth="1.35"
1257
+ className={className}
1258
+ aria-hidden="true"
1259
+ >
1260
+ <rect x="3" y="3" width="3.2" height="3.2" rx=".5" />
1261
+ <rect x="9.8" y="3" width="3.2" height="3.2" rx=".5" />
1262
+ <rect x="3" y="9.8" width="3.2" height="3.2" rx=".5" />
1263
+ <rect x="9.8" y="9.8" width="3.2" height="3.2" rx=".5" />
1264
+ </svg>
1265
+ );
1266
+ }
1267
+ if (node.layout?.isFlexContainer) {
1268
+ const isRow = node.layout.flexDirection?.startsWith("row");
1269
+ const align = node.layout.alignItems ?? "stretch";
1270
+ const justify = node.layout.justifyContent ?? "flex-start";
1271
+ return isRow ? (
1272
+ <HorizontalAutoLayoutGlyph
1273
+ align={align}
1274
+ justify={justify}
1275
+ className={className}
1276
+ />
1277
+ ) : (
1278
+ <VerticalAutoLayoutGlyph
1279
+ align={align}
1280
+ justify={justify}
1281
+ className={className}
1282
+ />
1283
+ );
1284
+ }
1285
+ return <FrameLayerGlyph className={className} />;
1286
+ }
1287
+
1288
+ function normalizedAlignment(value: string | undefined) {
1289
+ if (!value) return "start";
1290
+ if (value === "center") return "center";
1291
+ if (value === "flex-end" || value === "end") return "end";
1292
+ if (value === "space-between") return "space-between";
1293
+ if (value === "space-around" || value === "space-evenly")
1294
+ return "space-around";
1295
+ if (value === "stretch") return "stretch";
1296
+ return "start";
1297
+ }
1298
+
1299
+ function crossAxisOffset(align: string | undefined, axis: "x" | "y") {
1300
+ const normalized = normalizedAlignment(align);
1301
+ if (normalized === "center") return axis === "x" ? 5 : 5.5;
1302
+ if (normalized === "end") return axis === "x" ? 7 : 8;
1303
+ return axis === "x" ? 3 : 3;
1304
+ }
1305
+
1306
+ function mainAxisPositions(justify: string | undefined, axis: "x" | "y") {
1307
+ const normalized = normalizedAlignment(justify);
1308
+ if (axis === "x") {
1309
+ if (normalized === "center") return [3.6, 7.1, 10.6];
1310
+ if (normalized === "end") return [4.4, 7.8, 11.2];
1311
+ if (normalized === "space-between") return [2.6, 7.1, 11.6];
1312
+ if (normalized === "space-around") return [3.1, 7.1, 11.1];
1313
+ return [3, 6.6, 10.2];
1314
+ }
1315
+ if (normalized === "center") return [3.5, 7.1, 10.7];
1316
+ if (normalized === "end") return [4.2, 7.8, 11.4];
1317
+ if (normalized === "space-between") return [2.8, 7.1, 11.4];
1318
+ if (normalized === "space-around") return [3.2, 7.1, 11];
1319
+ return [3, 6.6, 10.2];
1320
+ }
1321
+
1322
+ function VerticalAutoLayoutGlyph({
1323
+ align,
1324
+ justify,
1325
+ className,
1326
+ }: {
1327
+ align?: string;
1328
+ justify?: string;
1329
+ className?: string;
1330
+ }) {
1331
+ const x = crossAxisOffset(align, "x");
1332
+ const yPositions = mainAxisPositions(justify, "y");
1333
+ return (
1334
+ <svg
1335
+ viewBox="0 0 16 16"
1336
+ fill="none"
1337
+ stroke="currentColor"
1338
+ strokeWidth="1.45"
1339
+ strokeLinejoin="round"
1340
+ className={className}
1341
+ aria-hidden="true"
1342
+ >
1343
+ <rect x={x} y={yPositions[0]} width="6" height="1.55" rx=".45" />
1344
+ <rect x={x} y={yPositions[1]} width="6" height="1.55" rx=".45" />
1345
+ <rect x={x} y={yPositions[2]} width="6" height="1.55" rx=".45" />
1346
+ </svg>
1347
+ );
1348
+ }
1349
+
1350
+ function HorizontalAutoLayoutGlyph({
1351
+ align,
1352
+ justify,
1353
+ className,
1354
+ }: {
1355
+ align?: string;
1356
+ justify?: string;
1357
+ className?: string;
1358
+ }) {
1359
+ const y = crossAxisOffset(align, "y");
1360
+ const xPositions = mainAxisPositions(justify, "x");
1361
+ return (
1362
+ <svg
1363
+ viewBox="0 0 16 16"
1364
+ fill="none"
1365
+ stroke="currentColor"
1366
+ strokeWidth="1.45"
1367
+ strokeLinejoin="round"
1368
+ className={className}
1369
+ aria-hidden="true"
1370
+ >
1371
+ <rect x={xPositions[0]} y={y} width="1.55" height="5" rx=".45" />
1372
+ <rect x={xPositions[1]} y={y} width="1.55" height="5" rx=".45" />
1373
+ <rect x={xPositions[2]} y={y} width="1.55" height="5" rx=".45" />
1374
+ </svg>
1375
+ );
1376
+ }
1377
+
1378
+ function ComponentLayerGlyph({ className }: { className?: string }) {
1379
+ return (
1380
+ <svg
1381
+ viewBox="0 0 16 16"
1382
+ fill="none"
1383
+ stroke="currentColor"
1384
+ strokeWidth="1.45"
1385
+ strokeLinejoin="round"
1386
+ className={className}
1387
+ aria-hidden="true"
1388
+ >
1389
+ <path d="m8 2.6 5.4 5.4L8 13.4 2.6 8 8 2.6Z" />
1390
+ </svg>
1391
+ );
1392
+ }
1393
+
1394
+ function RectangleLayerGlyph({ className }: { className?: string }) {
1395
+ return (
1396
+ <svg
1397
+ viewBox="0 0 16 16"
1398
+ fill="none"
1399
+ stroke="currentColor"
1400
+ strokeWidth="1.45"
1401
+ className={className}
1402
+ aria-hidden="true"
1403
+ >
1404
+ <rect x="3.2" y="4" width="9.6" height="8" rx="1" />
1405
+ </svg>
1406
+ );
1407
+ }
1408
+
1409
+ function TextLayerGlyph({ className }: { className?: string }) {
1410
+ return (
1411
+ <svg
1412
+ viewBox="0 0 16 16"
1413
+ fill="none"
1414
+ stroke="currentColor"
1415
+ strokeWidth="1.45"
1416
+ strokeLinecap="round"
1417
+ strokeLinejoin="round"
1418
+ className={className}
1419
+ aria-hidden="true"
1420
+ >
1421
+ <path d="M3.2 4h9.6" />
1422
+ <path d="M8 4v8.4" />
1423
+ </svg>
1424
+ );
1425
+ }
1426
+
1427
+ function ImageLayerGlyph({ className }: { className?: string }) {
1428
+ return (
1429
+ <svg
1430
+ viewBox="0 0 16 16"
1431
+ fill="none"
1432
+ stroke="currentColor"
1433
+ strokeWidth="1.35"
1434
+ strokeLinecap="round"
1435
+ strokeLinejoin="round"
1436
+ className={className}
1437
+ aria-hidden="true"
1438
+ >
1439
+ <rect x="3" y="3" width="10" height="10" rx="1.2" />
1440
+ <circle cx="6" cy="6" r="1" />
1441
+ <path d="m4.2 12 3.2-3.3 1.8 1.8 1.3-1.4 1.3 1.4" />
1442
+ </svg>
1443
+ );
1444
+ }
1445
+
1446
+ function ElementLayerGlyph({ className }: { className?: string }) {
1447
+ return (
1448
+ <svg
1449
+ viewBox="0 0 16 16"
1450
+ fill="none"
1451
+ stroke="currentColor"
1452
+ strokeWidth="1.45"
1453
+ strokeLinecap="round"
1454
+ strokeLinejoin="round"
1455
+ className={className}
1456
+ aria-hidden="true"
1457
+ >
1458
+ <path d="m6.4 4.2-3.2 3.8 3.2 3.8" />
1459
+ <path d="m9.6 4.2 3.2 3.8-3.2 3.8" />
1460
+ </svg>
1461
+ );
1462
+ }
1463
+
1464
+ function dropPlacementForEvent(
1465
+ event: DragEvent<HTMLDivElement>,
1466
+ canDropInside: boolean,
1467
+ ): LayersPanelMoveIntent["placement"] {
1468
+ const rect = event.currentTarget.getBoundingClientRect();
1469
+ const offset = event.clientY - rect.top;
1470
+ if (offset < rect.height * 0.3) return "before";
1471
+ if (offset > rect.height * 0.7) return "after";
1472
+ return canDropInside ? "inside" : "after";
1473
+ }