@agent-native/core 0.79.27 → 0.80.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 (346) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +26 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +41 -0
  4. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +51 -0
  5. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +50 -0
  6. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +49 -0
  7. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +44 -0
  8. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +46 -0
  9. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +44 -0
  10. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +48 -0
  11. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +36 -0
  12. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +38 -0
  13. package/corpus/core/docs/content/template-design.mdx +45 -0
  14. package/corpus/core/package.json +2 -1
  15. package/corpus/core/src/agent/engine/index.ts +1 -0
  16. package/corpus/core/src/agent/engine/registry.ts +40 -0
  17. package/corpus/core/src/agent/production-agent.ts +6 -2
  18. package/corpus/core/src/cli/design-connect.ts +490 -0
  19. package/corpus/core/src/cli/index.ts +16 -0
  20. package/corpus/core/src/cli/skills.ts +126 -3
  21. package/corpus/core/src/client/AssistantChat.tsx +43 -7
  22. package/corpus/core/src/client/agent-chat.ts +27 -3
  23. package/corpus/core/src/client/blocks/library/MermaidBlock.tsx +1 -1
  24. package/corpus/core/src/client/blocks/library/diagram.tsx +2 -2
  25. package/corpus/core/src/client/blocks/library/wireframe.tsx +1 -1
  26. package/corpus/core/src/client/chat/message-components.tsx +1 -1
  27. package/corpus/core/src/client/chat-model-groups.ts +1 -2
  28. package/corpus/core/src/client/components/LiveCursorOverlay.tsx +33 -66
  29. package/corpus/core/src/client/composer/TiptapComposer.tsx +99 -24
  30. package/corpus/core/src/client/composer/useVoiceDictation.ts +429 -260
  31. package/corpus/core/src/client/frame-protocol.ts +17 -2
  32. package/corpus/core/src/client/rich-markdown-editor/RegistryBlockNode.tsx +2 -2
  33. package/corpus/core/src/client/sharing/ShareButton.tsx +20 -15
  34. package/corpus/core/src/client/use-agent-engine-configured.ts +80 -28
  35. package/corpus/core/src/server/transcribe-voice.ts +57 -22
  36. package/corpus/core/src/shared/mcp-embed-headers.ts +23 -1
  37. package/corpus/core/src/styles/agent-native.css +56 -7
  38. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  39. package/corpus/core/src/vite/client.ts +2 -0
  40. package/corpus/core/src/voice/index.ts +18 -0
  41. package/corpus/core/src/voice/voice-cleanup-prompt.ts +38 -0
  42. package/corpus/core/src/voice/voice-context.ts +217 -0
  43. package/corpus/core/src/voice/voice-replacements.ts +75 -0
  44. package/corpus/templates/analytics/AGENTS.md +4 -3
  45. package/corpus/templates/analytics/actions/list-session-recordings.ts +6 -1
  46. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +557 -400
  47. package/corpus/templates/analytics/app/global.css +0 -4
  48. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  49. package/corpus/templates/analytics/app/i18n-data.ts +20 -0
  50. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +33 -8
  51. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +7 -0
  52. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +7 -0
  53. package/corpus/templates/analytics/changelog/2026-06-28-ask-tab-now-matches-the-darker-app-background.md +6 -0
  54. package/corpus/templates/analytics/changelog/2026-06-28-dashboard-email-reports-now-use-cleaner-light-mode-snapshots.md +6 -0
  55. package/corpus/templates/analytics/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  56. package/corpus/templates/analytics/changelog/2026-06-28-sessions-now-show-only-signed-in-recordings-with-playable-re.md +6 -0
  57. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  58. package/corpus/templates/analytics/server/lib/dashboard-report.ts +29 -7
  59. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +5 -2
  60. package/corpus/templates/analytics/server/lib/session-replay.ts +24 -4
  61. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -4
  62. package/corpus/templates/assets/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  63. package/corpus/templates/brain/app/components/layout/Layout.tsx +29 -2
  64. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +131 -42
  65. package/corpus/templates/brain/app/i18n/zh-TW.ts +2 -0
  66. package/corpus/templates/brain/app/i18n-data.ts +20 -0
  67. package/corpus/templates/brain/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  68. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +29 -1
  69. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +417 -299
  70. package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
  71. package/corpus/templates/calendar/app/i18n-data.ts +20 -0
  72. package/corpus/templates/calendar/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  73. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +6 -19
  74. package/corpus/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  75. package/corpus/templates/clips/AGENTS.md +4 -0
  76. package/corpus/templates/clips/actions/cleanup-transcript.ts +39 -9
  77. package/corpus/templates/clips/actions/finalize-meeting.ts +81 -12
  78. package/corpus/templates/clips/actions/lib/agents-md-context.ts +4 -1
  79. package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -3
  80. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +1 -1
  81. package/corpus/templates/clips/app/components/player/share-dialog.tsx +2 -2
  82. package/corpus/templates/clips/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  83. package/corpus/templates/clips/desktop/src/lib/personal-vocabulary.ts +13 -8
  84. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +60 -1
  85. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1 -1
  86. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -1
  87. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -5
  88. package/corpus/templates/content/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  89. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +137 -0
  90. package/corpus/templates/design/AGENTS.md +95 -5
  91. package/corpus/templates/design/DESIGN.md +82 -0
  92. package/corpus/templates/design/DEVELOPING.md +43 -0
  93. package/corpus/templates/design/actions/add-localhost-screens.ts +426 -0
  94. package/corpus/templates/design/actions/apply-shader.ts +277 -0
  95. package/corpus/templates/design/actions/apply-visual-edit.ts +303 -0
  96. package/corpus/templates/design/actions/connect-localhost.ts +183 -0
  97. package/corpus/templates/design/actions/edit-design.ts +195 -42
  98. package/corpus/templates/design/actions/generate-design.ts +71 -1
  99. package/corpus/templates/design/actions/generate-screens.ts +193 -0
  100. package/corpus/templates/design/actions/get-code-layer-projection.ts +191 -0
  101. package/corpus/templates/design/actions/get-shader.ts +119 -0
  102. package/corpus/templates/design/actions/list-localhost-connections.ts +95 -0
  103. package/corpus/templates/design/actions/navigate.ts +55 -2
  104. package/corpus/templates/design/actions/view-screen.ts +68 -1
  105. package/corpus/templates/design/agent-native.app-skill.json +10 -0
  106. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +761 -0
  107. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +2057 -103
  108. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +74 -38
  109. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +399 -0
  110. package/corpus/templates/design/app/components/design/EditPanel.tsx +3185 -439
  111. package/corpus/templates/design/app/components/design/LayersPanel.tsx +1124 -0
  112. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +3116 -147
  113. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +700 -13
  114. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +107 -63
  115. package/corpus/templates/design/app/components/design/index.ts +0 -5
  116. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +286 -0
  117. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +699 -0
  118. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +461 -0
  119. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +166 -0
  120. package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +1420 -0
  121. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +420 -0
  122. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +215 -0
  123. package/corpus/templates/design/app/components/design/inspector/ShaderControls.stories.tsx +71 -0
  124. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +518 -0
  125. package/corpus/templates/design/app/components/design/inspector/ShaderEffectRow.tsx +106 -0
  126. package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +361 -0
  127. package/corpus/templates/design/app/components/design/inspector/index.ts +60 -0
  128. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +201 -0
  129. package/corpus/templates/design/app/components/design/types.ts +23 -0
  130. package/corpus/templates/design/app/components/layout/Layout.tsx +24 -13
  131. package/corpus/templates/design/app/components/layout/Sidebar.tsx +4 -4
  132. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +474 -36
  133. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +22 -5
  134. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +24 -11
  135. package/corpus/templates/design/app/global.css +121 -3
  136. package/corpus/templates/design/app/hooks/use-agent-generating.ts +4 -0
  137. package/corpus/templates/design/app/hooks/use-navigation-state.ts +130 -8
  138. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +329 -0
  139. package/corpus/templates/design/app/i18n/zh-TW.ts +264 -0
  140. package/corpus/templates/design/app/i18n-data.ts +2790 -1
  141. package/corpus/templates/design/app/pages/DesignEditor.tsx +5108 -851
  142. package/corpus/templates/design/changelog/2026-06-28-annotate-mode-now-combines-drawing-and-comment-pins-in-one-c.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-28-design-editor-panel-borders-are-cleaner-so-the-canvas-edge-s.md +6 -0
  144. package/corpus/templates/design/changelog/2026-06-28-drawing-prompts-now-reliably-open-and-submit-to-the-design-a.md +6 -0
  145. package/corpus/templates/design/changelog/2026-06-28-editor-app-menus-now-use-an-outline-agent-native-mark-with-b.md +6 -0
  146. package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-28-layer-selection-now-updates-chat-context-without-opening-the.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-28-layer-selections-now-preserve-hidden-and-locked-canvas-state.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-handles-zooming-drawing-opening-and-dele.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-selects-screens-on-click-and-opens-them-.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-28-screen-overview-zooms-into-the-active-screen-with-smoother-e.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-28-selected-element-chips-in-chat-are-shorter-and-easier-to-sca.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-28-the-design-editor-toolbar-is-more-compact-and-the-app-menu-m.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-at-the-top-of-the-design-sidebar-instead-of-.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-in-the-right-inspector-beside-the-design-tab.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-29-active-sidebar-rows-use-soft-highlights.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-29-agents-can-now-open-screen-overview-focus-screens-and-add.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-edit-broad-copy-changes-more-reliably.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-29-design-editor-extensions-now-run-inline-beside-the-artboard.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-29-sidebars-use-darker-chrome-and-chat-warns-immediately.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-29-visual-edit-can-open-localhost-routes-as-url-backed-screens.md +6 -0
  165. package/corpus/templates/design/package.json +1 -0
  166. package/corpus/templates/design/server/db/schema.ts +26 -0
  167. package/corpus/templates/design/server/lib/design-snapshot.ts +1 -1
  168. package/corpus/templates/design/server/plugins/agent-chat.ts +3 -0
  169. package/corpus/templates/design/shared/api.ts +27 -0
  170. package/corpus/templates/design/shared/canvas-frames.ts +103 -0
  171. package/corpus/templates/design/shared/canvas-math.ts +1058 -0
  172. package/corpus/templates/design/shared/code-layer.ts +2212 -0
  173. package/corpus/templates/design/shared/color-utils.ts +180 -0
  174. package/corpus/templates/design/shared/generation-session.ts +61 -0
  175. package/corpus/templates/design/shared/shader-presets.ts +782 -0
  176. package/corpus/templates/design/shared/shader-safety.ts +137 -0
  177. package/corpus/templates/design/shared/source-mode.ts +237 -0
  178. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +149 -6
  179. package/corpus/templates/forms/app/i18n/ar-SA.ts +2 -0
  180. package/corpus/templates/forms/app/i18n/de-DE.ts +2 -0
  181. package/corpus/templates/forms/app/i18n/en-US.ts +2 -0
  182. package/corpus/templates/forms/app/i18n/es-ES.ts +2 -0
  183. package/corpus/templates/forms/app/i18n/fr-FR.ts +2 -0
  184. package/corpus/templates/forms/app/i18n/hi-IN.ts +2 -0
  185. package/corpus/templates/forms/app/i18n/ja-JP.ts +2 -0
  186. package/corpus/templates/forms/app/i18n/ko-KR.ts +2 -0
  187. package/corpus/templates/forms/app/i18n/pt-BR.ts +2 -0
  188. package/corpus/templates/forms/app/i18n/zh-CN.ts +2 -0
  189. package/corpus/templates/forms/app/i18n/zh-TW.ts +2 -0
  190. package/corpus/templates/forms/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  191. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +2 -2
  192. package/corpus/templates/macros/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  193. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +408 -247
  194. package/corpus/templates/mail/app/i18n/ar-SA.ts +4 -0
  195. package/corpus/templates/mail/app/i18n/de-DE.ts +4 -0
  196. package/corpus/templates/mail/app/i18n/en-US.ts +4 -0
  197. package/corpus/templates/mail/app/i18n/es-ES.ts +4 -0
  198. package/corpus/templates/mail/app/i18n/fr-FR.ts +4 -0
  199. package/corpus/templates/mail/app/i18n/hi-IN.ts +4 -0
  200. package/corpus/templates/mail/app/i18n/ja-JP.ts +4 -0
  201. package/corpus/templates/mail/app/i18n/ko-KR.ts +4 -0
  202. package/corpus/templates/mail/app/i18n/pt-BR.ts +4 -0
  203. package/corpus/templates/mail/app/i18n/zh-CN.ts +4 -0
  204. package/corpus/templates/mail/app/i18n/zh-TW.ts +4 -0
  205. package/corpus/templates/mail/changelog/2026-06-28-pinned-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  206. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +6 -8
  207. package/corpus/templates/plan/app/components/plan/wireframe/Wireframe.tsx +1 -1
  208. package/corpus/templates/plan/app/global.css +61 -78
  209. package/corpus/templates/plan/app/pages/PlansPage.tsx +8 -0
  210. package/corpus/templates/plan/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  211. package/corpus/templates/plan/changelog/2026-06-28-plans-use-the-shared-neutral-light-and-dark-theme.md +6 -0
  212. package/corpus/templates/plan/changelog/2026-06-28-sending-open-feedback-from-a-plan-now-opens-the-inline-plan-.md +6 -0
  213. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +5 -5
  214. package/corpus/templates/slides/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  215. package/corpus/templates/videos/app/components/Sidebar.tsx +2 -2
  216. package/corpus/templates/videos/app/components/layout/Layout.tsx +29 -2
  217. package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +113 -30
  218. package/corpus/templates/videos/app/i18n/ar-SA.ts +2 -0
  219. package/corpus/templates/videos/app/i18n/de-DE.ts +2 -0
  220. package/corpus/templates/videos/app/i18n/en-US.ts +2 -0
  221. package/corpus/templates/videos/app/i18n/es-ES.ts +2 -0
  222. package/corpus/templates/videos/app/i18n/fr-FR.ts +2 -0
  223. package/corpus/templates/videos/app/i18n/hi-IN.ts +2 -0
  224. package/corpus/templates/videos/app/i18n/ja-JP.ts +2 -0
  225. package/corpus/templates/videos/app/i18n/ko-KR.ts +2 -0
  226. package/corpus/templates/videos/app/i18n/pt-BR.ts +2 -0
  227. package/corpus/templates/videos/app/i18n/zh-CN.ts +2 -0
  228. package/corpus/templates/videos/app/i18n/zh-TW.ts +2 -0
  229. package/corpus/templates/videos/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  230. package/dist/agent/engine/index.d.ts +1 -1
  231. package/dist/agent/engine/index.d.ts.map +1 -1
  232. package/dist/agent/engine/index.js +1 -1
  233. package/dist/agent/engine/index.js.map +1 -1
  234. package/dist/agent/engine/registry.d.ts +9 -0
  235. package/dist/agent/engine/registry.d.ts.map +1 -1
  236. package/dist/agent/engine/registry.js +37 -0
  237. package/dist/agent/engine/registry.js.map +1 -1
  238. package/dist/agent/production-agent.d.ts.map +1 -1
  239. package/dist/agent/production-agent.js +4 -3
  240. package/dist/agent/production-agent.js.map +1 -1
  241. package/dist/cli/design-connect.d.ts +52 -0
  242. package/dist/cli/design-connect.d.ts.map +1 -0
  243. package/dist/cli/design-connect.js +409 -0
  244. package/dist/cli/design-connect.js.map +1 -0
  245. package/dist/cli/index.js +15 -0
  246. package/dist/cli/index.js.map +1 -1
  247. package/dist/cli/skills.d.ts +3 -0
  248. package/dist/cli/skills.d.ts.map +1 -1
  249. package/dist/cli/skills.js +121 -3
  250. package/dist/cli/skills.js.map +1 -1
  251. package/dist/client/AssistantChat.d.ts.map +1 -1
  252. package/dist/client/AssistantChat.js +38 -8
  253. package/dist/client/AssistantChat.js.map +1 -1
  254. package/dist/client/agent-chat.d.ts +6 -0
  255. package/dist/client/agent-chat.d.ts.map +1 -1
  256. package/dist/client/agent-chat.js +17 -3
  257. package/dist/client/agent-chat.js.map +1 -1
  258. package/dist/client/blocks/library/MermaidBlock.js +1 -1
  259. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  260. package/dist/client/blocks/library/diagram.js +1 -1
  261. package/dist/client/blocks/library/diagram.js.map +1 -1
  262. package/dist/client/blocks/library/wireframe.js +1 -1
  263. package/dist/client/blocks/library/wireframe.js.map +1 -1
  264. package/dist/client/chat/message-components.js +1 -1
  265. package/dist/client/chat/message-components.js.map +1 -1
  266. package/dist/client/chat-model-groups.d.ts.map +1 -1
  267. package/dist/client/chat-model-groups.js +1 -2
  268. package/dist/client/chat-model-groups.js.map +1 -1
  269. package/dist/client/components/LiveCursorOverlay.d.ts +1 -2
  270. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -1
  271. package/dist/client/components/LiveCursorOverlay.js +19 -40
  272. package/dist/client/components/LiveCursorOverlay.js.map +1 -1
  273. package/dist/client/composer/TiptapComposer.d.ts +3 -1
  274. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  275. package/dist/client/composer/TiptapComposer.js +86 -24
  276. package/dist/client/composer/TiptapComposer.js.map +1 -1
  277. package/dist/client/composer/useVoiceDictation.d.ts +9 -0
  278. package/dist/client/composer/useVoiceDictation.d.ts.map +1 -1
  279. package/dist/client/composer/useVoiceDictation.js +154 -32
  280. package/dist/client/composer/useVoiceDictation.js.map +1 -1
  281. package/dist/client/frame-protocol.d.ts +17 -2
  282. package/dist/client/frame-protocol.d.ts.map +1 -1
  283. package/dist/client/frame-protocol.js.map +1 -1
  284. package/dist/client/rich-markdown-editor/RegistryBlockNode.js +2 -2
  285. package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
  286. package/dist/client/sharing/ShareButton.d.ts +2 -0
  287. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  288. package/dist/client/sharing/ShareButton.js +7 -6
  289. package/dist/client/sharing/ShareButton.js.map +1 -1
  290. package/dist/client/use-agent-engine-configured.d.ts +5 -0
  291. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  292. package/dist/client/use-agent-engine-configured.js +51 -25
  293. package/dist/client/use-agent-engine-configured.js.map +1 -1
  294. package/dist/collab/awareness.d.ts +2 -2
  295. package/dist/collab/awareness.d.ts.map +1 -1
  296. package/dist/collab/routes.d.ts +1 -1
  297. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  298. package/dist/notifications/routes.d.ts +3 -3
  299. package/dist/observability/routes.d.ts +8 -8
  300. package/dist/progress/routes.d.ts +1 -1
  301. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  302. package/dist/server/transcribe-voice.d.ts.map +1 -1
  303. package/dist/server/transcribe-voice.js +40 -24
  304. package/dist/server/transcribe-voice.js.map +1 -1
  305. package/dist/shared/mcp-embed-headers.d.ts +1 -0
  306. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  307. package/dist/shared/mcp-embed-headers.js +20 -1
  308. package/dist/shared/mcp-embed-headers.js.map +1 -1
  309. package/dist/styles/agent-native.css +56 -7
  310. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  311. package/dist/vite/client.d.ts.map +1 -1
  312. package/dist/vite/client.js +2 -0
  313. package/dist/vite/client.js.map +1 -1
  314. package/dist/voice/index.d.ts +5 -0
  315. package/dist/voice/index.d.ts.map +1 -0
  316. package/dist/voice/index.js +4 -0
  317. package/dist/voice/index.js.map +1 -0
  318. package/dist/voice/voice-cleanup-prompt.d.ts +6 -0
  319. package/dist/voice/voice-cleanup-prompt.d.ts.map +1 -0
  320. package/dist/voice/voice-cleanup-prompt.js +26 -0
  321. package/dist/voice/voice-cleanup-prompt.js.map +1 -0
  322. package/dist/voice/voice-context.d.ts +25 -0
  323. package/dist/voice/voice-context.d.ts.map +1 -0
  324. package/dist/voice/voice-context.js +156 -0
  325. package/dist/voice/voice-context.js.map +1 -0
  326. package/dist/voice/voice-replacements.d.ts +4 -0
  327. package/dist/voice/voice-replacements.d.ts.map +1 -0
  328. package/dist/voice/voice-replacements.js +52 -0
  329. package/dist/voice/voice-replacements.js.map +1 -0
  330. package/docs/content/locales/ar-SA/template-design.mdx +41 -0
  331. package/docs/content/locales/de-DE/template-design.mdx +51 -0
  332. package/docs/content/locales/es-ES/template-design.mdx +50 -0
  333. package/docs/content/locales/fr-FR/template-design.mdx +49 -0
  334. package/docs/content/locales/hi-IN/template-design.mdx +44 -0
  335. package/docs/content/locales/ja-JP/template-design.mdx +46 -0
  336. package/docs/content/locales/ko-KR/template-design.mdx +44 -0
  337. package/docs/content/locales/pt-BR/template-design.mdx +48 -0
  338. package/docs/content/locales/zh-CN/template-design.mdx +36 -0
  339. package/docs/content/locales/zh-TW/template-design.mdx +38 -0
  340. package/docs/content/template-design.mdx +45 -0
  341. package/package.json +2 -1
  342. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  343. package/corpus/templates/design/app/components/design/DesignToolbar.tsx +0 -339
  344. package/corpus/templates/design/app/components/design/PresentMode.tsx +0 -68
  345. package/corpus/templates/design/app/components/design/ViewportTabs.tsx +0 -149
  346. package/corpus/templates/design/app/components/design/ZoomControls.tsx +0 -157
@@ -0,0 +1,1124 @@
1
+ import { useT } from "@agent-native/core/client";
2
+ import {
3
+ IconChevronDown,
4
+ IconChevronRight,
5
+ IconCode,
6
+ IconComponents,
7
+ IconEye,
8
+ IconEyeOff,
9
+ IconFile,
10
+ IconFileCode,
11
+ IconFolder,
12
+ IconFrame,
13
+ IconHierarchy,
14
+ IconLayersIntersect,
15
+ IconLock,
16
+ IconLockOpen,
17
+ IconLayoutGrid,
18
+ IconPlus,
19
+ IconPhoto,
20
+ IconRectangle,
21
+ IconSearch,
22
+ IconSquare,
23
+ IconStack2,
24
+ IconTypography,
25
+ type Icon,
26
+ } from "@tabler/icons-react";
27
+ import {
28
+ useCallback,
29
+ useEffect,
30
+ useMemo,
31
+ useRef,
32
+ useState,
33
+ type KeyboardEvent,
34
+ type DragEvent,
35
+ type MouseEvent,
36
+ type ReactNode,
37
+ } from "react";
38
+
39
+ import { Button } from "@/components/ui/button";
40
+ import { Input } from "@/components/ui/input";
41
+ import {
42
+ Tooltip,
43
+ TooltipContent,
44
+ TooltipTrigger,
45
+ } from "@/components/ui/tooltip";
46
+ import { cn } from "@/lib/utils";
47
+
48
+ export type LayersPanelNodeType =
49
+ | "file"
50
+ | "screen"
51
+ | "frame"
52
+ | "group"
53
+ | "component"
54
+ | "instance"
55
+ | "section"
56
+ | "shape"
57
+ | "rectangle"
58
+ | "text"
59
+ | "image"
60
+ | "code"
61
+ | "element"
62
+ | "unknown";
63
+
64
+ export interface LayersPanelNode {
65
+ id: string;
66
+ name: string;
67
+ type?: LayersPanelNodeType;
68
+ children?: LayersPanelNode[];
69
+ detail?: string;
70
+ badge?: string | number;
71
+ hidden?: boolean;
72
+ locked?: boolean;
73
+ selectable?: boolean;
74
+ renamable?: boolean;
75
+ lockable?: boolean;
76
+ hideable?: boolean;
77
+ icon?: ReactNode;
78
+ }
79
+
80
+ export interface LayersPanelScreen extends Omit<
81
+ LayersPanelNode,
82
+ "children" | "type"
83
+ > {
84
+ type?: "screen" | "frame";
85
+ layers?: LayersPanelNode[];
86
+ }
87
+
88
+ export interface LayersPanelFile extends Omit<
89
+ LayersPanelNode,
90
+ "children" | "type"
91
+ > {
92
+ type?: "file";
93
+ filename?: string;
94
+ fileType?: string;
95
+ screens?: LayersPanelScreen[];
96
+ layers?: LayersPanelNode[];
97
+ }
98
+
99
+ export interface LayersPanelSelectionIntent {
100
+ id: string;
101
+ selectedIds: string[];
102
+ additive: boolean;
103
+ range: boolean;
104
+ source: "keyboard" | "pointer";
105
+ }
106
+
107
+ export interface LayersPanelMoveIntent {
108
+ draggedIds: string[];
109
+ targetId: string;
110
+ placement: "before" | "after" | "inside";
111
+ }
112
+
113
+ export interface LayersPanelLabels {
114
+ title: string;
115
+ screens: string;
116
+ screenOverview: string;
117
+ addScreen: string;
118
+ searchPlaceholder: string;
119
+ empty: string;
120
+ noMatches: string;
121
+ designLayers: string;
122
+ codeLayers: string;
123
+ elementLayers: string;
124
+ collapse: string;
125
+ expand: string;
126
+ lock: string;
127
+ unlock: string;
128
+ hide: string;
129
+ show: string;
130
+ rename: string;
131
+ selected: (count: number) => string;
132
+ }
133
+
134
+ export interface LayersPanelProps {
135
+ screens?: LayersPanelFile[];
136
+ activeScreenId?: string;
137
+ files?: LayersPanelFile[];
138
+ layers?: LayersPanelNode[];
139
+ codeLayers?: LayersPanelNode[];
140
+ elementLayers?: LayersPanelNode[];
141
+ selectedIds: readonly string[];
142
+ expandedIds: readonly string[];
143
+ searchQuery: string;
144
+ className?: string;
145
+ labels?: Partial<LayersPanelLabels>;
146
+ onSearchQueryChange: (query: string) => void;
147
+ onScreenSelect?: (id: string) => void;
148
+ onScreenOverview?: () => void;
149
+ onAddScreen?: () => void;
150
+ onExpandedIdsChange: (ids: string[]) => void;
151
+ onSelectionChange: (
152
+ ids: string[],
153
+ intent: LayersPanelSelectionIntent,
154
+ ) => void;
155
+ onRename?: (id: string, name: string) => void;
156
+ onToggleLocked?: (id: string, locked: boolean) => void;
157
+ onToggleHidden?: (id: string, hidden: boolean) => void;
158
+ onHoverLayer?: (id: string) => void;
159
+ onLeaveLayer?: (id: string) => void;
160
+ onMoveLayer?: (intent: LayersPanelMoveIntent) => void;
161
+ }
162
+
163
+ interface FlatLayerRow {
164
+ node: LayersPanelNode;
165
+ rowKey: string;
166
+ depth: number;
167
+ hasChildren: boolean;
168
+ }
169
+
170
+ const SECTION_CODE_ID = "__design_layers_code__";
171
+ const SECTION_ELEMENT_ID = "__design_layers_elements__";
172
+
173
+ function defaultLabels(t: ReturnType<typeof useT>): LayersPanelLabels {
174
+ return {
175
+ title: t("layersPanel.title"),
176
+ screens: t("layersPanel.screens"),
177
+ screenOverview: t("designEditor.screenOverview"),
178
+ addScreen: t("layersPanel.addScreen"),
179
+ searchPlaceholder: t("layersPanel.searchPlaceholder"),
180
+ empty: t("layersPanel.empty"),
181
+ noMatches: t("layersPanel.noMatches"),
182
+ designLayers: t("layersPanel.designLayers"),
183
+ codeLayers: t("layersPanel.codeLayers"),
184
+ elementLayers: t("layersPanel.elementLayers"),
185
+ collapse: t("layersPanel.collapse"),
186
+ expand: t("layersPanel.expand"),
187
+ lock: t("layersPanel.lock"),
188
+ unlock: t("layersPanel.unlock"),
189
+ hide: t("layersPanel.hide"),
190
+ show: t("layersPanel.show"),
191
+ rename: t("layersPanel.rename"),
192
+ selected: (count) => t("layersPanel.selected", { count }),
193
+ };
194
+ }
195
+
196
+ function mergeLabels(
197
+ labels: LayersPanelProps["labels"],
198
+ t: ReturnType<typeof useT>,
199
+ ): LayersPanelLabels {
200
+ return { ...defaultLabels(t), ...labels };
201
+ }
202
+
203
+ function asFileNode(file: LayersPanelFile): LayersPanelNode {
204
+ const screens = file.screens?.map(asScreenNode) ?? [];
205
+ return {
206
+ ...file,
207
+ type: "file",
208
+ name: file.name || file.filename || "Untitled file",
209
+ detail: file.detail ?? file.fileType,
210
+ children: [...screens, ...(file.layers ?? [])],
211
+ };
212
+ }
213
+
214
+ function asScreenNode(screen: LayersPanelScreen): LayersPanelNode {
215
+ return {
216
+ ...screen,
217
+ type: screen.type ?? "screen",
218
+ children: screen.layers ?? [],
219
+ };
220
+ }
221
+
222
+ function sectionNode(
223
+ id: string,
224
+ name: string,
225
+ children: LayersPanelNode[] | undefined,
226
+ ): LayersPanelNode | null {
227
+ if (!children?.length) return null;
228
+ return {
229
+ id,
230
+ name,
231
+ type: "section",
232
+ selectable: false,
233
+ renamable: false,
234
+ lockable: false,
235
+ hideable: false,
236
+ children,
237
+ };
238
+ }
239
+
240
+ function buildRootNodes({
241
+ files,
242
+ layers,
243
+ codeLayers,
244
+ elementLayers,
245
+ labels,
246
+ }: Pick<
247
+ LayersPanelProps,
248
+ "files" | "layers" | "codeLayers" | "elementLayers"
249
+ > & {
250
+ labels: LayersPanelLabels;
251
+ }) {
252
+ const roots: LayersPanelNode[] = [
253
+ ...(files?.map(asFileNode) ?? []),
254
+ ...(layers ?? []),
255
+ ];
256
+ const codeSection = sectionNode(
257
+ SECTION_CODE_ID,
258
+ labels.codeLayers,
259
+ codeLayers,
260
+ );
261
+ const elementSection = sectionNode(
262
+ SECTION_ELEMENT_ID,
263
+ labels.elementLayers,
264
+ elementLayers,
265
+ );
266
+
267
+ if (codeSection) roots.push(codeSection);
268
+ if (elementSection) roots.push(elementSection);
269
+ return roots;
270
+ }
271
+
272
+ function nodeMatches(node: LayersPanelNode, query: string) {
273
+ if (!query) return true;
274
+ const haystack = [node.name, node.detail, node.type, node.badge]
275
+ .filter((value) => value !== null && value !== undefined)
276
+ .join(" ")
277
+ .toLowerCase();
278
+ return haystack.includes(query);
279
+ }
280
+
281
+ function filterNode(
282
+ node: LayersPanelNode,
283
+ query: string,
284
+ ): LayersPanelNode | null {
285
+ const normalized = query.trim().toLowerCase();
286
+ if (!normalized) return node;
287
+
288
+ const children = node.children
289
+ ?.map((child) => filterNode(child, normalized))
290
+ .filter((child): child is LayersPanelNode => Boolean(child));
291
+
292
+ if (nodeMatches(node, normalized) || children?.length) {
293
+ return { ...node, children };
294
+ }
295
+ return null;
296
+ }
297
+
298
+ function flattenRows(
299
+ nodes: LayersPanelNode[],
300
+ expandedIds: ReadonlySet<string>,
301
+ forceExpanded: boolean,
302
+ depth = 0,
303
+ parentKey = "root",
304
+ rows: FlatLayerRow[] = [],
305
+ ) {
306
+ nodes.forEach((node, index) => {
307
+ const children = node.children ?? [];
308
+ const hasChildren = children.length > 0;
309
+ const rowKey = `${parentKey}/${node.id}:${index}`;
310
+ rows.push({ node, rowKey, depth, hasChildren });
311
+ if (hasChildren && (forceExpanded || expandedIds.has(node.id))) {
312
+ flattenRows(
313
+ children,
314
+ expandedIds,
315
+ forceExpanded,
316
+ depth + 1,
317
+ rowKey,
318
+ rows,
319
+ );
320
+ }
321
+ });
322
+ return rows;
323
+ }
324
+
325
+ function nextExpandedIds(
326
+ ids: readonly string[],
327
+ nodeId: string,
328
+ expanded: boolean,
329
+ ) {
330
+ const next = new Set(ids);
331
+ if (expanded) {
332
+ next.add(nodeId);
333
+ } else {
334
+ next.delete(nodeId);
335
+ }
336
+ return Array.from(next);
337
+ }
338
+
339
+ function collectAncestorIds(
340
+ nodes: LayersPanelNode[],
341
+ targetIds: ReadonlySet<string>,
342
+ ): string[] {
343
+ const ancestors = new Set<string>();
344
+
345
+ function visit(node: LayersPanelNode, path: string[]): boolean {
346
+ const children = node.children ?? [];
347
+ let containsSelectedChild = false;
348
+ children.forEach((child) => {
349
+ if (visit(child, [...path, node.id])) {
350
+ containsSelectedChild = true;
351
+ }
352
+ });
353
+ const containsSelected = targetIds.has(node.id) || containsSelectedChild;
354
+ if (containsSelected) {
355
+ path.forEach((id) => ancestors.add(id));
356
+ }
357
+ return containsSelected;
358
+ }
359
+
360
+ nodes.forEach((node) => visit(node, []));
361
+ return Array.from(ancestors);
362
+ }
363
+
364
+ function layerTypeIcon(type: LayersPanelNodeType | undefined): Icon {
365
+ switch (type) {
366
+ case "file":
367
+ return IconFile;
368
+ case "screen":
369
+ return IconFrame;
370
+ case "frame":
371
+ return IconFrame;
372
+ case "group":
373
+ return IconHierarchy;
374
+ case "component":
375
+ case "instance":
376
+ return IconComponents;
377
+ case "section":
378
+ return IconFolder;
379
+ case "shape":
380
+ return IconSquare;
381
+ case "rectangle":
382
+ return IconRectangle;
383
+ case "text":
384
+ return IconTypography;
385
+ case "image":
386
+ return IconPhoto;
387
+ case "code":
388
+ return IconFileCode;
389
+ case "element":
390
+ return IconCode;
391
+ default:
392
+ return IconLayersIntersect;
393
+ }
394
+ }
395
+
396
+ export function LayersPanel({
397
+ screens,
398
+ activeScreenId,
399
+ files,
400
+ layers,
401
+ codeLayers,
402
+ elementLayers,
403
+ selectedIds,
404
+ expandedIds,
405
+ searchQuery,
406
+ className,
407
+ labels: labelsProp,
408
+ onSearchQueryChange,
409
+ onScreenSelect,
410
+ onScreenOverview,
411
+ onAddScreen,
412
+ onExpandedIdsChange,
413
+ onSelectionChange,
414
+ onRename,
415
+ onToggleLocked,
416
+ onToggleHidden,
417
+ onHoverLayer,
418
+ onLeaveLayer,
419
+ onMoveLayer,
420
+ }: LayersPanelProps) {
421
+ const t = useT();
422
+ const labels = useMemo(() => mergeLabels(labelsProp, t), [labelsProp, t]);
423
+ const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]);
424
+ const expandedIdSet = useMemo(() => new Set(expandedIds), [expandedIds]);
425
+ const lastSelectionAnchorRef = useRef<string | null>(selectedIds[0] ?? null);
426
+ const searchInputRef = useRef<HTMLInputElement>(null);
427
+ const rowElementRefs = useRef(new Map<string, HTMLDivElement>());
428
+ const [renamingId, setRenamingId] = useState<string | null>(null);
429
+ const [renameDraft, setRenameDraft] = useState("");
430
+ const [dropIndicator, setDropIndicator] =
431
+ useState<LayersPanelMoveIntent | null>(null);
432
+
433
+ const roots = useMemo(
434
+ () =>
435
+ buildRootNodes({
436
+ files,
437
+ layers,
438
+ codeLayers,
439
+ elementLayers,
440
+ labels,
441
+ }),
442
+ [codeLayers, elementLayers, files, labels, layers],
443
+ );
444
+
445
+ const visibleRows = useMemo(() => {
446
+ const filtered = roots
447
+ .map((node) => filterNode(node, searchQuery))
448
+ .filter((node): node is LayersPanelNode => Boolean(node));
449
+ return flattenRows(filtered, expandedIdSet, Boolean(searchQuery.trim()));
450
+ }, [expandedIdSet, roots, searchQuery]);
451
+
452
+ const selectedAncestorIds = useMemo(
453
+ () => collectAncestorIds(roots, selectedIdSet),
454
+ [roots, selectedIdSet],
455
+ );
456
+
457
+ useEffect(() => {
458
+ if (selectedAncestorIds.length === 0) return;
459
+ const next = new Set(expandedIds);
460
+ let changed = false;
461
+ selectedAncestorIds.forEach((id) => {
462
+ if (!next.has(id)) {
463
+ next.add(id);
464
+ changed = true;
465
+ }
466
+ });
467
+ if (changed) onExpandedIdsChange(Array.from(next));
468
+ }, [expandedIds, onExpandedIdsChange, selectedAncestorIds]);
469
+
470
+ const selectedScrollId = selectedIds[selectedIds.length - 1] ?? null;
471
+ const selectedScrollRowKey = useMemo(() => {
472
+ if (!selectedScrollId) return null;
473
+ return (
474
+ visibleRows.find((row) => row.node.id === selectedScrollId)?.rowKey ??
475
+ null
476
+ );
477
+ }, [selectedScrollId, visibleRows]);
478
+
479
+ useEffect(() => {
480
+ if (!selectedScrollRowKey) return;
481
+ const frame = window.requestAnimationFrame(() => {
482
+ rowElementRefs.current.get(selectedScrollRowKey)?.scrollIntoView({
483
+ block: "nearest",
484
+ });
485
+ });
486
+ return () => window.cancelAnimationFrame(frame);
487
+ }, [selectedScrollRowKey]);
488
+
489
+ const selectableVisibleIds = useMemo(
490
+ () =>
491
+ visibleRows
492
+ .map(({ node }) => node)
493
+ .filter((node) => node.selectable !== false)
494
+ .map((node) => node.id),
495
+ [visibleRows],
496
+ );
497
+
498
+ const selectNode = useCallback(
499
+ (
500
+ id: string,
501
+ options: {
502
+ additive: boolean;
503
+ range: boolean;
504
+ source: "keyboard" | "pointer";
505
+ },
506
+ ) => {
507
+ let nextIds: string[];
508
+ if (options.range && lastSelectionAnchorRef.current) {
509
+ const from = selectableVisibleIds.indexOf(
510
+ lastSelectionAnchorRef.current,
511
+ );
512
+ const to = selectableVisibleIds.indexOf(id);
513
+ if (from >= 0 && to >= 0) {
514
+ const [start, end] = from < to ? [from, to] : [to, from];
515
+ const rangeIds = selectableVisibleIds.slice(start, end + 1);
516
+ nextIds = options.additive
517
+ ? Array.from(new Set([...selectedIds, ...rangeIds]))
518
+ : rangeIds;
519
+ } else {
520
+ nextIds = [id];
521
+ }
522
+ } else if (options.additive) {
523
+ nextIds = selectedIdSet.has(id)
524
+ ? selectedIds.filter((selectedId) => selectedId !== id)
525
+ : [...selectedIds, id];
526
+ } else {
527
+ nextIds = [id];
528
+ }
529
+
530
+ lastSelectionAnchorRef.current = id;
531
+ onSelectionChange(nextIds, { id, selectedIds: nextIds, ...options });
532
+ },
533
+ [onSelectionChange, selectableVisibleIds, selectedIdSet, selectedIds],
534
+ );
535
+
536
+ const commitRename = useCallback(
537
+ (id: string) => {
538
+ const nextName = renameDraft.trim();
539
+ // The panel only emits rename intent. Code-backed DOM layer renames must
540
+ // persist through a safe source edit that updates data-agent-native-layer-name.
541
+ if (nextName) onRename?.(id, nextName);
542
+ setRenamingId(null);
543
+ setRenameDraft("");
544
+ },
545
+ [onRename, renameDraft],
546
+ );
547
+
548
+ const startRename = useCallback(
549
+ (node: LayersPanelNode) => {
550
+ if (!onRename || node.renamable === false) return;
551
+ setRenamingId(node.id);
552
+ setRenameDraft(node.name);
553
+ },
554
+ [onRename],
555
+ );
556
+
557
+ const registerRowElement = useCallback(
558
+ (rowKey: string, element: HTMLDivElement | null) => {
559
+ if (element) {
560
+ rowElementRefs.current.set(rowKey, element);
561
+ } else {
562
+ rowElementRefs.current.delete(rowKey);
563
+ }
564
+ },
565
+ [],
566
+ );
567
+
568
+ const hasAnyRows = roots.length > 0;
569
+ const selectedCount = selectedIds.length;
570
+ const screenRows = screens ?? files ?? [];
571
+
572
+ return (
573
+ <aside
574
+ className={cn(
575
+ "flex h-full min-h-0 w-full flex-col overflow-hidden bg-[var(--design-editor-panel-bg)] text-[11px] text-foreground",
576
+ className,
577
+ )}
578
+ aria-label={labels.title}
579
+ >
580
+ {screenRows.length > 0 ? (
581
+ <div className="shrink-0 border-b border-border pb-2">
582
+ <div className="flex h-9 items-center justify-between px-2.5">
583
+ <h2 className="truncate text-[11px] font-semibold text-foreground">
584
+ {labels.screens}
585
+ </h2>
586
+ <div className="flex items-center gap-0.5 text-muted-foreground">
587
+ <IconTooltipButton
588
+ label={labels.screenOverview}
589
+ onClick={onScreenOverview}
590
+ >
591
+ <IconLayoutGrid className="size-3.5" />
592
+ </IconTooltipButton>
593
+ <IconTooltipButton
594
+ label={labels.searchPlaceholder}
595
+ onClick={() => searchInputRef.current?.focus()}
596
+ >
597
+ <IconSearch className="size-3.5" />
598
+ </IconTooltipButton>
599
+ <IconTooltipButton
600
+ label={labels.addScreen}
601
+ disabled={!onAddScreen}
602
+ onClick={onAddScreen}
603
+ >
604
+ <IconPlus className="size-3.5" />
605
+ </IconTooltipButton>
606
+ </div>
607
+ </div>
608
+ <div className="space-y-0.5 px-1">
609
+ {screenRows.map((screen) => {
610
+ const isActive = screen.id === activeScreenId;
611
+ return (
612
+ <button
613
+ key={screen.id}
614
+ type="button"
615
+ className={cn(
616
+ "flex h-7 w-full cursor-default items-center gap-2 rounded-sm px-2 text-left text-[12px] font-medium outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]",
617
+ isActive
618
+ ? "bg-[var(--design-editor-active-row-color)] text-foreground"
619
+ : "text-foreground/85 hover:bg-[var(--design-editor-active-row-color)] hover:text-foreground",
620
+ )}
621
+ onClick={() => onScreenSelect?.(screen.id)}
622
+ title={screen.filename ?? screen.name}
623
+ >
624
+ <IconFile className="size-3.5 shrink-0 text-muted-foreground" />
625
+ <span className="min-w-0 flex-1 truncate">{screen.name}</span>
626
+ {screen.badge ? (
627
+ <span className="rounded-sm bg-muted px-1 text-[10px] font-normal text-muted-foreground">
628
+ {screen.badge}
629
+ </span>
630
+ ) : null}
631
+ </button>
632
+ );
633
+ })}
634
+ </div>
635
+ </div>
636
+ ) : null}
637
+
638
+ <div className="flex h-9 shrink-0 items-center justify-between border-b border-border px-2.5">
639
+ <div className="min-w-0">
640
+ <h2 className="truncate text-[11px] font-semibold text-foreground">
641
+ {labels.title}
642
+ </h2>
643
+ {selectedCount > 1 ? (
644
+ <p className="truncate text-[10px] text-muted-foreground">
645
+ {labels.selected(selectedCount)}
646
+ </p>
647
+ ) : null}
648
+ </div>
649
+ <IconStack2 className="size-3.5 shrink-0 text-muted-foreground" />
650
+ </div>
651
+
652
+ <div className="shrink-0 border-b border-border p-1.5">
653
+ <div className="relative">
654
+ <IconSearch className="pointer-events-none absolute left-2 top-1/2 size-3 -translate-y-1/2 text-muted-foreground" />
655
+ <Input
656
+ ref={searchInputRef}
657
+ value={searchQuery}
658
+ onChange={(event) => onSearchQueryChange(event.target.value)}
659
+ placeholder={labels.searchPlaceholder}
660
+ className="h-7 rounded-sm border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] pl-6 text-[11px] shadow-none placeholder:text-muted-foreground/70 focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
661
+ />
662
+ </div>
663
+ </div>
664
+
665
+ <div className="min-h-0 flex-1 overflow-y-auto py-1">
666
+ {visibleRows.length ? (
667
+ <div className="px-1" role="tree" aria-label={labels.title}>
668
+ {visibleRows.map((row) => (
669
+ <LayerRow
670
+ key={row.rowKey}
671
+ row={row}
672
+ labels={labels}
673
+ isExpanded={expandedIdSet.has(row.node.id)}
674
+ isSelected={selectedIdSet.has(row.node.id)}
675
+ isActiveScreen={
676
+ row.node.id === activeScreenId &&
677
+ (row.node.type === "file" ||
678
+ row.node.type === "screen" ||
679
+ row.node.type === "frame")
680
+ }
681
+ isRenaming={renamingId === row.node.id}
682
+ rowRef={(element) => registerRowElement(row.rowKey, element)}
683
+ renameDraft={renameDraft}
684
+ onRenameDraftChange={setRenameDraft}
685
+ onCommitRename={commitRename}
686
+ onCancelRename={() => setRenamingId(null)}
687
+ onStartRename={startRename}
688
+ onSelect={selectNode}
689
+ onToggleExpanded={(expanded) =>
690
+ onExpandedIdsChange(
691
+ nextExpandedIds(expandedIds, row.node.id, expanded),
692
+ )
693
+ }
694
+ onToggleLocked={onToggleLocked}
695
+ onToggleHidden={onToggleHidden}
696
+ onHoverLayer={onHoverLayer}
697
+ onLeaveLayer={onLeaveLayer}
698
+ onMoveLayer={onMoveLayer}
699
+ dropIndicator={dropIndicator}
700
+ onDropIndicatorChange={setDropIndicator}
701
+ selectedIds={selectedIds}
702
+ />
703
+ ))}
704
+ </div>
705
+ ) : (
706
+ <div className="px-3 py-8 text-center text-[11px] text-muted-foreground">
707
+ {hasAnyRows ? labels.noMatches : labels.empty}
708
+ </div>
709
+ )}
710
+ </div>
711
+ </aside>
712
+ );
713
+ }
714
+
715
+ function LayerRow({
716
+ row,
717
+ labels,
718
+ isExpanded,
719
+ isSelected,
720
+ isActiveScreen,
721
+ isRenaming,
722
+ rowRef,
723
+ renameDraft,
724
+ onRenameDraftChange,
725
+ onCommitRename,
726
+ onCancelRename,
727
+ onStartRename,
728
+ onSelect,
729
+ onToggleExpanded,
730
+ onToggleLocked,
731
+ onToggleHidden,
732
+ onHoverLayer,
733
+ onLeaveLayer,
734
+ onMoveLayer,
735
+ dropIndicator,
736
+ onDropIndicatorChange,
737
+ selectedIds,
738
+ }: {
739
+ row: FlatLayerRow;
740
+ labels: LayersPanelLabels;
741
+ isExpanded: boolean;
742
+ isSelected: boolean;
743
+ isActiveScreen: boolean;
744
+ isRenaming: boolean;
745
+ rowRef: (element: HTMLDivElement | null) => void;
746
+ renameDraft: string;
747
+ onRenameDraftChange: (value: string) => void;
748
+ onCommitRename: (id: string) => void;
749
+ onCancelRename: () => void;
750
+ onStartRename: (node: LayersPanelNode) => void;
751
+ onSelect: (
752
+ id: string,
753
+ options: {
754
+ additive: boolean;
755
+ range: boolean;
756
+ source: "keyboard" | "pointer";
757
+ },
758
+ ) => void;
759
+ onToggleExpanded: (expanded: boolean) => void;
760
+ onToggleLocked?: (id: string, locked: boolean) => void;
761
+ onToggleHidden?: (id: string, hidden: boolean) => void;
762
+ onHoverLayer?: (id: string) => void;
763
+ onLeaveLayer?: (id: string) => void;
764
+ onMoveLayer?: (intent: LayersPanelMoveIntent) => void;
765
+ dropIndicator: LayersPanelMoveIntent | null;
766
+ onDropIndicatorChange: (intent: LayersPanelMoveIntent | null) => void;
767
+ selectedIds: readonly string[];
768
+ }) {
769
+ const { node, depth, hasChildren } = row;
770
+ const Icon = layerTypeIcon(node.type);
771
+ const selectable = node.selectable !== false;
772
+ const lockable = node.lockable !== false && Boolean(onToggleLocked);
773
+ const hideable = node.hideable !== false && Boolean(onToggleHidden);
774
+ const draggable = selectable && Boolean(onMoveLayer);
775
+ const activeDrop =
776
+ dropIndicator?.targetId === node.id ? dropIndicator.placement : null;
777
+
778
+ const handlePointerSelect = (event: MouseEvent<HTMLButtonElement>) => {
779
+ if (!selectable) return;
780
+ onSelect(node.id, {
781
+ additive: event.metaKey || event.ctrlKey,
782
+ range: event.shiftKey,
783
+ source: "pointer",
784
+ });
785
+ };
786
+
787
+ const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
788
+ if (event.key === "Enter") {
789
+ event.preventDefault();
790
+ onStartRename(node);
791
+ return;
792
+ }
793
+ if (event.key === "ArrowRight" && hasChildren && !isExpanded) {
794
+ event.preventDefault();
795
+ onToggleExpanded(true);
796
+ return;
797
+ }
798
+ if (event.key === "ArrowLeft" && hasChildren && isExpanded) {
799
+ event.preventDefault();
800
+ onToggleExpanded(false);
801
+ }
802
+ };
803
+
804
+ const handleDragStart = (event: DragEvent<HTMLDivElement>) => {
805
+ if (!draggable) {
806
+ event.preventDefault();
807
+ return;
808
+ }
809
+ const draggedIds = selectedIds.includes(node.id)
810
+ ? selectedIds.filter((id) => !id.startsWith("__"))
811
+ : [node.id];
812
+ event.dataTransfer.effectAllowed = "move";
813
+ event.dataTransfer.setData("application/x-design-layer-id", node.id);
814
+ event.dataTransfer.setData(
815
+ "application/x-design-layer-ids",
816
+ JSON.stringify(draggedIds),
817
+ );
818
+ };
819
+
820
+ const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
821
+ if (!onMoveLayer || !selectable) return;
822
+ const sourceId =
823
+ event.dataTransfer.getData("application/x-design-layer-id") || "";
824
+ if (sourceId === node.id) return;
825
+ const rawIds = event.dataTransfer.getData("application/x-design-layer-ids");
826
+ let draggedIds = [sourceId];
827
+ try {
828
+ const parsed = JSON.parse(rawIds);
829
+ if (Array.isArray(parsed)) {
830
+ draggedIds = parsed.filter(
831
+ (id): id is string => typeof id === "string",
832
+ );
833
+ }
834
+ } catch {
835
+ // Ignore malformed drag payloads and fall back to the primary id.
836
+ }
837
+ const cleanedIds = draggedIds.filter(
838
+ (id) => id && id !== node.id && !id.startsWith("__"),
839
+ );
840
+ if (cleanedIds.length === 0) return;
841
+ event.preventDefault();
842
+ event.dataTransfer.dropEffect = "move";
843
+ onDropIndicatorChange({
844
+ draggedIds: cleanedIds,
845
+ targetId: node.id,
846
+ placement: dropPlacementForEvent(event, hasChildren),
847
+ });
848
+ };
849
+
850
+ const handleDrop = (event: DragEvent<HTMLDivElement>) => {
851
+ if (
852
+ !onMoveLayer ||
853
+ !selectable ||
854
+ !dropIndicator ||
855
+ dropIndicator.targetId !== node.id
856
+ ) {
857
+ onDropIndicatorChange(null);
858
+ return;
859
+ }
860
+ event.preventDefault();
861
+ const rawIds = event.dataTransfer.getData("application/x-design-layer-ids");
862
+ let draggedIds = [
863
+ event.dataTransfer.getData("application/x-design-layer-id"),
864
+ ];
865
+ try {
866
+ const parsed = JSON.parse(rawIds);
867
+ if (Array.isArray(parsed)) {
868
+ draggedIds = parsed.filter(
869
+ (id): id is string => typeof id === "string",
870
+ );
871
+ }
872
+ } catch {
873
+ // Ignore malformed drag payloads and fall back to the primary id.
874
+ }
875
+ const cleanedIds = draggedIds.filter(
876
+ (id) => id && id !== node.id && !id.startsWith("__"),
877
+ );
878
+ if (cleanedIds.length > 0) {
879
+ onMoveLayer({
880
+ draggedIds: cleanedIds,
881
+ targetId: node.id,
882
+ placement: dropIndicator.placement,
883
+ });
884
+ }
885
+ onDropIndicatorChange(null);
886
+ };
887
+
888
+ return (
889
+ <div
890
+ ref={rowRef}
891
+ role="treeitem"
892
+ aria-expanded={hasChildren ? isExpanded : undefined}
893
+ aria-level={depth + 1}
894
+ aria-selected={selectable ? isSelected : undefined}
895
+ className="relative"
896
+ draggable={draggable}
897
+ onDragStart={handleDragStart}
898
+ onDragOver={handleDragOver}
899
+ onDragLeave={() => onDropIndicatorChange(null)}
900
+ onDrop={handleDrop}
901
+ onDragEnd={() => onDropIndicatorChange(null)}
902
+ onMouseEnter={() => onHoverLayer?.(node.id)}
903
+ onMouseLeave={() => onLeaveLayer?.(node.id)}
904
+ >
905
+ {activeDrop === "before" ? (
906
+ <span className="pointer-events-none absolute left-2 right-2 top-0 z-10 h-px bg-[var(--design-editor-accent-color)]" />
907
+ ) : null}
908
+ {activeDrop === "after" ? (
909
+ <span className="pointer-events-none absolute bottom-0 left-2 right-2 z-10 h-px bg-[var(--design-editor-accent-color)]" />
910
+ ) : null}
911
+ {Array.from({ length: depth }).map((_, index) => (
912
+ <span
913
+ key={index}
914
+ className="pointer-events-none absolute bottom-0 top-0 w-px bg-border/70"
915
+ style={{ left: 15 + index * 12 }}
916
+ />
917
+ ))}
918
+ <div
919
+ className={cn(
920
+ "group flex h-6 items-center gap-0.5 rounded-sm pr-0.5 text-[11px]",
921
+ activeDrop === "inside" &&
922
+ "ring-1 ring-inset ring-[var(--design-editor-accent-color)]",
923
+ isSelected &&
924
+ !isActiveScreen &&
925
+ "bg-[var(--design-editor-selection-color)] text-foreground",
926
+ isActiveScreen &&
927
+ "bg-[var(--design-editor-active-row-color)] text-foreground hover:bg-[var(--design-editor-active-row-color)]",
928
+ !isSelected &&
929
+ !isActiveScreen &&
930
+ "text-foreground/90 hover:bg-accent/70 hover:text-foreground",
931
+ node.hidden && "text-muted-foreground",
932
+ )}
933
+ style={{ paddingLeft: 2 + depth * 12 }}
934
+ >
935
+ {hasChildren ? (
936
+ <Button
937
+ type="button"
938
+ variant="ghost"
939
+ size="icon"
940
+ className="size-4 shrink-0 rounded-sm p-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
941
+ aria-label={isExpanded ? labels.collapse : labels.expand}
942
+ onClick={(event) => {
943
+ event.stopPropagation();
944
+ onToggleExpanded(!isExpanded);
945
+ }}
946
+ >
947
+ {isExpanded ? (
948
+ <IconChevronDown className="size-3" />
949
+ ) : (
950
+ <IconChevronRight className="size-3 rtl:-scale-x-100" />
951
+ )}
952
+ </Button>
953
+ ) : (
954
+ <span className="size-4 shrink-0" />
955
+ )}
956
+
957
+ <button
958
+ type="button"
959
+ disabled={!selectable}
960
+ className={cn(
961
+ "flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden rounded-sm px-1 py-0.5 text-left outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]",
962
+ selectable ? "cursor-default" : "cursor-default opacity-80",
963
+ )}
964
+ onClick={handlePointerSelect}
965
+ onDoubleClick={() => onStartRename(node)}
966
+ onKeyDown={handleKeyDown}
967
+ title={node.name}
968
+ >
969
+ <span
970
+ className={cn(
971
+ "shrink-0 text-muted-foreground",
972
+ isSelected && "text-[var(--design-editor-accent-strong-color)]",
973
+ )}
974
+ >
975
+ {node.icon ?? <Icon className="size-3.5" />}
976
+ </span>
977
+ {isRenaming ? (
978
+ <input
979
+ autoFocus
980
+ value={renameDraft}
981
+ onClick={(event) => event.stopPropagation()}
982
+ onChange={(event) => onRenameDraftChange(event.target.value)}
983
+ onBlur={() => onCommitRename(node.id)}
984
+ onKeyDown={(event) => {
985
+ if (event.key === "Enter") {
986
+ event.preventDefault();
987
+ onCommitRename(node.id);
988
+ } else if (event.key === "Escape") {
989
+ event.preventDefault();
990
+ onCancelRename();
991
+ }
992
+ }}
993
+ className="h-5 min-w-0 flex-1 rounded-sm border border-[var(--design-editor-accent-color)] bg-[var(--design-editor-panel-bg)] px-1 text-[11px] text-foreground outline-none"
994
+ aria-label={labels.rename}
995
+ />
996
+ ) : (
997
+ <span
998
+ className={cn(
999
+ "min-w-0 flex-1 truncate",
1000
+ node.hidden && "line-through",
1001
+ )}
1002
+ >
1003
+ {node.name}
1004
+ </span>
1005
+ )}
1006
+ {!isRenaming && node.detail ? (
1007
+ <span className="hidden max-w-[72px] shrink truncate text-[10px] text-muted-foreground group-hover:inline">
1008
+ {node.detail}
1009
+ </span>
1010
+ ) : null}
1011
+ {!isRenaming && node.badge !== null && node.badge !== undefined ? (
1012
+ <span className="shrink-0 rounded-sm bg-muted px-1 text-[10px] text-muted-foreground">
1013
+ {node.badge}
1014
+ </span>
1015
+ ) : null}
1016
+ </button>
1017
+
1018
+ {lockable ? (
1019
+ <Tooltip>
1020
+ <TooltipTrigger asChild>
1021
+ <Button
1022
+ type="button"
1023
+ variant="ghost"
1024
+ size="icon"
1025
+ className={cn(
1026
+ "size-4 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",
1027
+ node.locked && "opacity-100",
1028
+ )}
1029
+ aria-label={node.locked ? labels.unlock : labels.lock}
1030
+ onClick={(event) => {
1031
+ event.stopPropagation();
1032
+ onToggleLocked?.(node.id, !node.locked);
1033
+ }}
1034
+ >
1035
+ {node.locked ? (
1036
+ <IconLock className="size-3" />
1037
+ ) : (
1038
+ <IconLockOpen className="size-3" />
1039
+ )}
1040
+ </Button>
1041
+ </TooltipTrigger>
1042
+ <TooltipContent>
1043
+ {node.locked ? labels.unlock : labels.lock}
1044
+ </TooltipContent>
1045
+ </Tooltip>
1046
+ ) : null}
1047
+
1048
+ {hideable ? (
1049
+ <Tooltip>
1050
+ <TooltipTrigger asChild>
1051
+ <Button
1052
+ type="button"
1053
+ variant="ghost"
1054
+ size="icon"
1055
+ className={cn(
1056
+ "size-4 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",
1057
+ node.hidden && "opacity-100",
1058
+ )}
1059
+ aria-label={node.hidden ? labels.show : labels.hide}
1060
+ onClick={(event) => {
1061
+ event.stopPropagation();
1062
+ onToggleHidden?.(node.id, !node.hidden);
1063
+ }}
1064
+ >
1065
+ {node.hidden ? (
1066
+ <IconEyeOff className="size-3" />
1067
+ ) : (
1068
+ <IconEye className="size-3" />
1069
+ )}
1070
+ </Button>
1071
+ </TooltipTrigger>
1072
+ <TooltipContent>
1073
+ {node.hidden ? labels.show : labels.hide}
1074
+ </TooltipContent>
1075
+ </Tooltip>
1076
+ ) : null}
1077
+ </div>
1078
+ </div>
1079
+ );
1080
+ }
1081
+
1082
+ function IconTooltipButton({
1083
+ label,
1084
+ onClick,
1085
+ disabled,
1086
+ children,
1087
+ }: {
1088
+ label: string;
1089
+ onClick?: () => void;
1090
+ disabled?: boolean;
1091
+ children: ReactNode;
1092
+ }) {
1093
+ return (
1094
+ <Tooltip>
1095
+ <TooltipTrigger asChild>
1096
+ <span className="inline-flex">
1097
+ <Button
1098
+ type="button"
1099
+ variant="ghost"
1100
+ size="icon"
1101
+ className="size-6 rounded-sm p-0 hover:bg-accent hover:text-foreground"
1102
+ aria-label={label}
1103
+ disabled={disabled}
1104
+ onClick={onClick}
1105
+ >
1106
+ {children}
1107
+ </Button>
1108
+ </span>
1109
+ </TooltipTrigger>
1110
+ <TooltipContent>{label}</TooltipContent>
1111
+ </Tooltip>
1112
+ );
1113
+ }
1114
+
1115
+ function dropPlacementForEvent(
1116
+ event: DragEvent<HTMLDivElement>,
1117
+ canDropInside: boolean,
1118
+ ): LayersPanelMoveIntent["placement"] {
1119
+ const rect = event.currentTarget.getBoundingClientRect();
1120
+ const offset = event.clientY - rect.top;
1121
+ if (offset < rect.height * 0.3) return "before";
1122
+ if (offset > rect.height * 0.7) return "after";
1123
+ return canDropInside ? "inside" : "after";
1124
+ }