@agent-native/core 0.80.8 → 0.80.9

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 (281) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +6 -0
  4. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +8 -0
  5. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +10 -2
  6. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +10 -2
  7. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +7 -0
  8. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +7 -0
  9. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +7 -0
  10. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +9 -2
  11. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +6 -0
  12. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +6 -0
  13. package/corpus/core/docs/content/template-design.mdx +9 -2
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/cli/mcp.ts +10 -0
  16. package/corpus/core/src/cli/skills.ts +58 -12
  17. package/corpus/core/src/client/error-format.ts +25 -0
  18. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +13 -11
  19. package/corpus/core/src/client/guided-questions.tsx +32 -4
  20. package/corpus/core/src/client/onboarding/OnboardingPanel.tsx +33 -1
  21. package/corpus/core/src/client/resources/BuiltinCapabilityDetail.tsx +8 -0
  22. package/corpus/core/src/client/resources/use-builtin-capabilities.ts +4 -2
  23. package/corpus/core/src/client/session-replay.ts +34 -0
  24. package/corpus/core/src/client/sharing/ShareButton.tsx +106 -21
  25. package/corpus/core/src/coding-tools/index.ts +1 -1
  26. package/corpus/core/src/mcp/index.ts +2 -0
  27. package/corpus/core/src/mcp/screen-memory-stdio.ts +290 -0
  28. package/corpus/core/src/mcp-client/builtin-capabilities.ts +13 -1
  29. package/corpus/core/src/mcp-client/index.ts +9 -0
  30. package/corpus/core/src/mcp-client/screen-memory-local.ts +461 -0
  31. package/corpus/core/src/onboarding/types.ts +7 -0
  32. package/corpus/core/src/server/agent-chat-plugin.ts +25 -0
  33. package/corpus/core/src/server/agents-bundle.ts +35 -7
  34. package/corpus/core/src/styles/agent-native.css +5 -0
  35. package/corpus/core/src/templates/default/app/global.css +35 -35
  36. package/corpus/templates/analytics/AGENTS.md +6 -0
  37. package/corpus/templates/analytics/actions/github-repo-files.ts +9 -0
  38. package/corpus/templates/analytics/app/components/layout/Layout.tsx +4 -4
  39. package/corpus/templates/analytics/app/global.css +144 -40
  40. package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +1 -1
  41. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +1 -1
  42. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +1 -1
  43. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +4 -4
  44. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +9 -9
  45. package/corpus/templates/analytics/changelog/2026-06-29-agents-can-search-and-read-connected-github-repositories-whe.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-06-29-sessions-and-explorer-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  47. package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -1
  48. package/corpus/templates/assets/app/components/library/LibraryPresetGrid.tsx +1 -1
  49. package/corpus/templates/assets/app/global.css +99 -35
  50. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  51. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +4 -4
  52. package/corpus/templates/assets/app/routes/library.tsx +4 -4
  53. package/corpus/templates/assets/changelog/2026-06-29-asset-library-and-detail-layouts-adapt-when-the-agent-sidebar.md +6 -0
  54. package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
  55. package/corpus/templates/brain/app/global.css +63 -35
  56. package/corpus/templates/brain/app/routes/search.tsx +1 -1
  57. package/corpus/templates/brain/app/routes/settings.tsx +1 -1
  58. package/corpus/templates/brain/app/routes/sources.tsx +2 -2
  59. package/corpus/templates/brain/changelog/2026-06-29-search-settings-and-source-layouts-adapt-when-the-agent-sidebar.md +6 -0
  60. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +4 -4
  61. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +3 -2
  62. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -2
  63. package/corpus/templates/calendar/app/global.css +69 -35
  64. package/corpus/templates/calendar/changelog/2026-06-29-event-and-find-time-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  65. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  66. package/corpus/templates/chat/app/global.css +35 -35
  67. package/corpus/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  68. package/corpus/templates/clips/AGENTS.md +13 -0
  69. package/corpus/templates/clips/actions/get-screen-memory-status.ts +12 -0
  70. package/corpus/templates/clips/actions/query-screen-memory-context.ts +29 -0
  71. package/corpus/templates/clips/actions/view-screen.ts +2 -2
  72. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  73. package/corpus/templates/clips/app/global.css +47 -35
  74. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +3 -3
  75. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +45 -18
  76. package/corpus/templates/clips/changelog/2026-06-29-meeting-detail-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  77. package/corpus/templates/clips/changelog/2026-06-29-screen-memory-keeps-a-local-rolling-screen-buffer-for-agent-context.md +5 -0
  78. package/corpus/templates/clips/changelog/2026-06-29-video-chat-is-easier-to-find-on-recording-pages-and-s3-setup.md +6 -0
  79. package/corpus/templates/clips/chrome-extension/src/styles.css +39 -39
  80. package/corpus/templates/clips/desktop/src/app.tsx +290 -1
  81. package/corpus/templates/clips/desktop/src/shared/config.ts +83 -0
  82. package/corpus/templates/clips/desktop/src/styles.css +67 -52
  83. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +50 -0
  84. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +17 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +13 -13
  86. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +990 -0
  87. package/corpus/templates/clips/server/plugins/onboarding.ts +2 -0
  88. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +3 -3
  89. package/corpus/templates/content/app/components/layout/Layout.tsx +1 -1
  90. package/corpus/templates/content/app/global.css +29 -15
  91. package/corpus/templates/content/changelog/2026-06-29-database-gallery-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  92. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +25 -20
  93. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -0
  94. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +8 -0
  95. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +13 -0
  96. package/corpus/templates/design/AGENTS.md +20 -12
  97. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1054 -0
  98. package/corpus/templates/design/actions/apply-visual-edit.ts +24 -4
  99. package/corpus/templates/design/actions/export-coding-handoff.ts +12 -4
  100. package/corpus/templates/design/actions/generate-design.ts +6 -21
  101. package/corpus/templates/design/actions/generate-screens.ts +43 -2
  102. package/corpus/templates/design/actions/get-design.ts +1 -0
  103. package/corpus/templates/design/actions/present-design-variants.ts +355 -30
  104. package/corpus/templates/design/actions/view-screen.ts +75 -31
  105. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +329 -69
  106. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +1 -1
  107. package/corpus/templates/design/app/components/design/EditPanel.tsx +479 -77
  108. package/corpus/templates/design/app/components/design/LayersPanel.tsx +380 -216
  109. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1050 -196
  110. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +83 -206
  111. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +1 -1
  112. package/corpus/templates/design/app/components/design/index.ts +0 -1
  113. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +225 -133
  114. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +122 -11
  115. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +24 -5
  116. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +57 -1
  117. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +37 -12
  118. package/corpus/templates/design/app/components/design/inspector/index.ts +1 -0
  119. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +12 -5
  120. package/corpus/templates/design/app/components/design/types.ts +9 -0
  121. package/corpus/templates/design/app/components/layout/Layout.tsx +23 -5
  122. package/corpus/templates/design/app/global.css +77 -49
  123. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  124. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +8 -1
  125. package/corpus/templates/design/app/i18n/zh-TW.ts +3 -0
  126. package/corpus/templates/design/app/i18n-data.ts +45 -3
  127. package/corpus/templates/design/app/pages/DesignEditor.tsx +2602 -657
  128. package/corpus/templates/design/app/root.tsx +1 -1
  129. package/corpus/templates/design/changelog/2026-06-29-containers-no-longer-block-selecting-nested-elements-and-nor.md +6 -0
  130. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-generate-and-refine-variants-from-stronger.md +6 -0
  131. package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +6 -0
  132. package/corpus/templates/design/changelog/2026-06-29-design-questions-now-appear-as-a-cleaner-canvas-intake-with-.md +6 -0
  133. package/corpus/templates/design/changelog/2026-06-29-design-variant-directions-now-appear-as-board-screens-with-chat-buttons.md +5 -0
  134. package/corpus/templates/design/changelog/2026-06-29-element-selection-no-longer-snaps-back-to-the-previous-layer.md +6 -0
  135. package/corpus/templates/design/changelog/2026-06-29-frame-resize-measurements-now-stay-beside-the-cursor-while-d.md +6 -0
  136. package/corpus/templates/design/changelog/2026-06-29-inspector-number-fields-have-cleaner-tooltips-and-draggable-.md +6 -0
  137. package/corpus/templates/design/changelog/2026-06-29-layer-locking-layer-nesting-generated-question-intake-and-im.md +6 -0
  138. package/corpus/templates/design/changelog/2026-06-29-layer-names-in-the-sidebar-can-scroll-horizontally-instead-o.md +6 -0
  139. package/corpus/templates/design/changelog/2026-06-29-layer-nesting-in-the-sidebar-now-uses-tighter-spacing-so-nam.md +6 -0
  140. package/corpus/templates/design/changelog/2026-06-29-layers-no-longer-repeat-document-wrapper-rows-when-a-screen-.md +6 -0
  141. package/corpus/templates/design/changelog/2026-06-29-overview-previews-now-keep-element-hover-and-selection-tied-.md +6 -0
  142. package/corpus/templates/design/changelog/2026-06-29-pressing-escape-now-cancels-a-stuck-screen-drag-in-the-overv.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-29-screen-overview-is-easier-to-find-and-named-screens-open-dir.md +6 -0
  144. package/corpus/templates/design/changelog/2026-06-29-screen-overview-only-highlights-screens-when-the-frame-or-ti.md +6 -0
  145. package/corpus/templates/design/changelog/2026-06-29-screen-overview-previews-now-resize-to-match-the-selected-de.md +6 -0
  146. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-now-uses-a-pointer-cursor-instead-.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-outlines-now-stay-locked-to-the-se.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zoom-now-reports-real-screen-scale-separatel.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-feels-smoother-and-selection-outline.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-now-stays-on-the-overview-canvas-ins.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-29-selecting-a-nested-layer-from-all-screens-now-keeps-you-in-a.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-29-share-now-groups-link-sharing-exports-and-agent-handoff.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-loading-skeleton-now-uses-a-softer-charcoa.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-29-the-device-preview-menu-now-sits-next-to-the-preview-button-.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-29-the-inspector-stays-read-only-while-an-empty-design-is-still.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-29-the-missing-design-view-now-matches-the-rest-of-the-editor-c.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-icon-and-tool-option-menus-better-match-the-edi.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-29-typography-controls-now-keep-the-font-name-visible-and-move-.md +6 -0
  159. package/corpus/templates/design/e2e/README.md +3 -3
  160. package/corpus/templates/design/e2e/global-setup.ts +11 -22
  161. package/corpus/templates/design/e2e/helpers.ts +122 -11
  162. package/corpus/templates/design/playwright.config.ts +9 -3
  163. package/corpus/templates/design/server/lib/coding-handoff.ts +126 -2
  164. package/corpus/templates/design/server/plugins/core-routes.ts +1 -2
  165. package/corpus/templates/design/server/routes/api/design-handoff/[id].zip.get.ts +86 -0
  166. package/corpus/templates/design/shared/code-layer.ts +125 -22
  167. package/corpus/templates/design/shared/generation-session.ts +41 -0
  168. package/corpus/templates/dispatch/app/global.css +79 -35
  169. package/corpus/templates/dispatch/app/routes/integrations.tsx +8 -7
  170. package/corpus/templates/dispatch/changelog/2026-06-29-integration-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  171. package/corpus/templates/forms/app/components/layout/Layout.tsx +6 -2
  172. package/corpus/templates/forms/app/global.css +64 -35
  173. package/corpus/templates/forms/app/pages/FormsListPage.tsx +2 -2
  174. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +3 -3
  175. package/corpus/templates/forms/changelog/2026-06-29-form-list-and-insight-layouts-adapt-when-the-agent-sidebar.md +6 -0
  176. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +3 -1
  177. package/corpus/templates/macros/app/global.css +25 -9
  178. package/corpus/templates/macros/changelog/2026-06-29-macro-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  179. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -2
  180. package/corpus/templates/mail/app/global.css +42 -36
  181. package/corpus/templates/mail/app/pages/InboxPage.tsx +1 -1
  182. package/corpus/templates/mail/changelog/2026-06-29-the-contact-panel-now-adapts-to-the-available-mail-pane-width.md +6 -0
  183. package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -1
  184. package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +6 -7
  185. package/corpus/templates/plan/app/global.css +74 -42
  186. package/corpus/templates/plan/changelog/2026-06-29-plan-document-blocks-adapt-to-the-available-document-width.md +6 -0
  187. package/corpus/templates/plan/changelog/2026-06-29-plan-titles-and-summaries-can-be-edited-inline-without-focus.md +6 -0
  188. package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -1
  189. package/corpus/templates/slides/app/global.css +49 -33
  190. package/corpus/templates/slides/changelog/2026-06-29-slide-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  191. package/corpus/templates/videos/app/components/layout/Layout.tsx +3 -1
  192. package/corpus/templates/videos/app/global.css +35 -35
  193. package/corpus/templates/videos/changelog/2026-06-29-video-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  194. package/dist/cli/mcp.d.ts.map +1 -1
  195. package/dist/cli/mcp.js +10 -0
  196. package/dist/cli/mcp.js.map +1 -1
  197. package/dist/cli/skills.d.ts.map +1 -1
  198. package/dist/cli/skills.js +58 -12
  199. package/dist/cli/skills.js.map +1 -1
  200. package/dist/client/error-format.d.ts.map +1 -1
  201. package/dist/client/error-format.js +17 -0
  202. package/dist/client/error-format.js.map +1 -1
  203. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  204. package/dist/client/extensions/ExtensionViewer.js +3 -4
  205. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  206. package/dist/client/guided-questions.d.ts +4 -0
  207. package/dist/client/guided-questions.d.ts.map +1 -1
  208. package/dist/client/guided-questions.js +16 -7
  209. package/dist/client/guided-questions.js.map +1 -1
  210. package/dist/client/onboarding/OnboardingPanel.js +26 -1
  211. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  212. package/dist/client/resources/BuiltinCapabilityDetail.d.ts.map +1 -1
  213. package/dist/client/resources/BuiltinCapabilityDetail.js +1 -1
  214. package/dist/client/resources/BuiltinCapabilityDetail.js.map +1 -1
  215. package/dist/client/resources/use-builtin-capabilities.d.ts +1 -1
  216. package/dist/client/resources/use-builtin-capabilities.d.ts.map +1 -1
  217. package/dist/client/resources/use-builtin-capabilities.js +2 -1
  218. package/dist/client/resources/use-builtin-capabilities.js.map +1 -1
  219. package/dist/client/session-replay.d.ts.map +1 -1
  220. package/dist/client/session-replay.js +31 -0
  221. package/dist/client/session-replay.js.map +1 -1
  222. package/dist/client/sharing/ShareButton.d.ts +14 -0
  223. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  224. package/dist/client/sharing/ShareButton.js +36 -7
  225. package/dist/client/sharing/ShareButton.js.map +1 -1
  226. package/dist/coding-tools/index.js +1 -1
  227. package/dist/coding-tools/index.js.map +1 -1
  228. package/dist/collab/routes.d.ts +1 -1
  229. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  230. package/dist/mcp/index.d.ts +2 -0
  231. package/dist/mcp/index.d.ts.map +1 -1
  232. package/dist/mcp/index.js +1 -0
  233. package/dist/mcp/index.js.map +1 -1
  234. package/dist/mcp/screen-memory-stdio.d.ts +6 -0
  235. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -0
  236. package/dist/mcp/screen-memory-stdio.js +213 -0
  237. package/dist/mcp/screen-memory-stdio.js.map +1 -0
  238. package/dist/mcp-client/builtin-capabilities.d.ts +1 -1
  239. package/dist/mcp-client/builtin-capabilities.d.ts.map +1 -1
  240. package/dist/mcp-client/builtin-capabilities.js +10 -0
  241. package/dist/mcp-client/builtin-capabilities.js.map +1 -1
  242. package/dist/mcp-client/index.d.ts +1 -0
  243. package/dist/mcp-client/index.d.ts.map +1 -1
  244. package/dist/mcp-client/index.js +1 -0
  245. package/dist/mcp-client/index.js.map +1 -1
  246. package/dist/mcp-client/screen-memory-local.d.ts +61 -0
  247. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -0
  248. package/dist/mcp-client/screen-memory-local.js +336 -0
  249. package/dist/mcp-client/screen-memory-local.js.map +1 -0
  250. package/dist/notifications/routes.d.ts +2 -2
  251. package/dist/observability/routes.d.ts +8 -8
  252. package/dist/onboarding/types.d.ts +7 -0
  253. package/dist/onboarding/types.d.ts.map +1 -1
  254. package/dist/onboarding/types.js.map +1 -1
  255. package/dist/resources/handlers.d.ts +2 -2
  256. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  257. package/dist/server/agent-chat-plugin.js +26 -0
  258. package/dist/server/agent-chat-plugin.js.map +1 -1
  259. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  260. package/dist/server/agents-bundle.d.ts.map +1 -1
  261. package/dist/server/agents-bundle.js +22 -7
  262. package/dist/server/agents-bundle.js.map +1 -1
  263. package/dist/server/transcribe-voice.d.ts +1 -1
  264. package/dist/styles/agent-native.css +5 -0
  265. package/dist/templates/default/app/global.css +35 -35
  266. package/docs/content/locales/ar-SA/template-design.mdx +6 -0
  267. package/docs/content/locales/de-DE/template-design.mdx +8 -0
  268. package/docs/content/locales/es-ES/template-design.mdx +10 -2
  269. package/docs/content/locales/fr-FR/template-design.mdx +10 -2
  270. package/docs/content/locales/hi-IN/template-design.mdx +7 -0
  271. package/docs/content/locales/ja-JP/template-design.mdx +7 -0
  272. package/docs/content/locales/ko-KR/template-design.mdx +7 -0
  273. package/docs/content/locales/pt-BR/template-design.mdx +9 -2
  274. package/docs/content/locales/zh-CN/template-design.mdx +6 -0
  275. package/docs/content/locales/zh-TW/template-design.mdx +6 -0
  276. package/docs/content/template-design.mdx +9 -2
  277. package/package.json +1 -1
  278. package/src/templates/default/app/global.css +35 -35
  279. package/corpus/templates/design/app/components/design/VariantGrid.tsx +0 -189
  280. package/corpus/templates/design/app/components/design/VariantHandoffCard.tsx +0 -115
  281. package/corpus/templates/design/app/hooks/use-variant-flow.ts +0 -342
@@ -7,12 +7,14 @@ import {
7
7
  IconLock,
8
8
  IconLockOpen,
9
9
  IconLayoutGrid,
10
+ IconPencil,
10
11
  IconPlus,
11
12
  IconSearch,
12
13
  } from "@tabler/icons-react";
13
14
  import {
14
15
  useCallback,
15
16
  useEffect,
17
+ useLayoutEffect,
16
18
  useMemo,
17
19
  useRef,
18
20
  useState,
@@ -23,6 +25,13 @@ import {
23
25
  } from "react";
24
26
 
25
27
  import { Button } from "@/components/ui/button";
28
+ import {
29
+ ContextMenu,
30
+ ContextMenuContent,
31
+ ContextMenuItem,
32
+ ContextMenuSeparator,
33
+ ContextMenuTrigger,
34
+ } from "@/components/ui/context-menu";
26
35
  import { Input } from "@/components/ui/input";
27
36
  import {
28
37
  Tooltip,
@@ -31,6 +40,9 @@ import {
31
40
  } from "@/components/ui/tooltip";
32
41
  import { cn } from "@/lib/utils";
33
42
 
43
+ const LAYER_ROW_BASE_INDENT = 4;
44
+ const LAYER_ROW_DEPTH_INDENT = 18;
45
+
34
46
  export type LayersPanelNodeType =
35
47
  | "file"
36
48
  | "screen"
@@ -94,6 +106,7 @@ export interface LayersPanelSelectionIntent {
94
106
  id: string;
95
107
  selectedIds: string[];
96
108
  additive: boolean;
109
+ currentSelectedIds?: string[];
97
110
  range: boolean;
98
111
  source: "keyboard" | "pointer";
99
112
  }
@@ -107,6 +120,7 @@ export interface LayersPanelMoveIntent {
107
120
  export interface LayersPanelLabels {
108
121
  title: string;
109
122
  screens: string;
123
+ allScreens: string;
110
124
  screenOverview: string;
111
125
  addScreen: string;
112
126
  searchPlaceholder: string;
@@ -128,6 +142,7 @@ export interface LayersPanelLabels {
128
142
  export interface LayersPanelProps {
129
143
  screens?: LayersPanelFile[];
130
144
  activeScreenId?: string;
145
+ screenOverviewActive?: boolean;
131
146
  files?: LayersPanelFile[];
132
147
  layers?: LayersPanelNode[];
133
148
  codeLayers?: LayersPanelNode[];
@@ -185,6 +200,7 @@ const SECTION_ELEMENT_ID = "__design_layers_elements__";
185
200
  // Module-level drag state: dataTransfer.getData() returns "" during dragover
186
201
  // per spec; the source row stores the drag payload here on dragstart instead.
187
202
  let activeDragState: { sourceId: string; draggedIds: string[] } | null = null;
203
+ let activeDropIntent: LayersPanelMoveIntent | null = null;
188
204
 
189
205
  const ROW_BASE_INDENT = 4;
190
206
  const ROW_INDENT_STEP = 28;
@@ -198,6 +214,7 @@ function defaultLabels(t: ReturnType<typeof useT>): LayersPanelLabels {
198
214
  return {
199
215
  title: t("layersPanel.title"),
200
216
  screens: t("layersPanel.screens"),
217
+ allScreens: t("layersPanel.allScreens"),
201
218
  screenOverview: t("designEditor.screenOverview"),
202
219
  addScreen: t("layersPanel.addScreen"),
203
220
  searchPlaceholder: t("layersPanel.searchPlaceholder"),
@@ -406,6 +423,7 @@ function layerCanShowBadge(node: LayersPanelNode) {
406
423
  export function LayersPanel({
407
424
  screens,
408
425
  activeScreenId,
426
+ screenOverviewActive = false,
409
427
  files,
410
428
  layers,
411
429
  codeLayers,
@@ -432,6 +450,7 @@ export function LayersPanel({
432
450
  const t = useT();
433
451
  const labels = useMemo(() => mergeLabels(labelsProp, t), [labelsProp, t]);
434
452
  const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]);
453
+ const selectedIdsRef = useRef<readonly string[]>(selectedIds);
435
454
  const expandedIdSet = useMemo(() => new Set(expandedIds), [expandedIds]);
436
455
  const lastSelectionAnchorRef = useRef<string | null>(selectedIds[0] ?? null);
437
456
  const searchInputRef = useRef<HTMLInputElement>(null);
@@ -467,6 +486,10 @@ export function LayersPanel({
467
486
  [roots, selectedIdSet],
468
487
  );
469
488
 
489
+ useLayoutEffect(() => {
490
+ selectedIdsRef.current = selectedIds;
491
+ }, [selectedIds]);
492
+
470
493
  useEffect(() => {
471
494
  if (selectedAncestorIds.length === 0) return;
472
495
  const next = new Set(expandedIds);
@@ -513,10 +536,14 @@ export function LayersPanel({
513
536
  id: string,
514
537
  options: {
515
538
  additive: boolean;
539
+ currentSelectedIds?: string[];
516
540
  range: boolean;
517
541
  source: "keyboard" | "pointer";
518
542
  },
519
543
  ) => {
544
+ const currentSelectedIds =
545
+ options.currentSelectedIds ?? selectedIdsRef.current;
546
+ const currentSelectedIdSet = new Set(currentSelectedIds);
520
547
  let nextIds: string[];
521
548
  if (options.range && lastSelectionAnchorRef.current) {
522
549
  let anchor = lastSelectionAnchorRef.current;
@@ -538,19 +565,18 @@ export function LayersPanel({
538
565
  const [start, end] = from < to ? [from, to] : [to, from];
539
566
  const rangeIds = selectableVisibleIds.slice(start, end + 1);
540
567
  nextIds = options.additive
541
- ? Array.from(new Set([...selectedIds, ...rangeIds]))
568
+ ? Array.from(new Set([...currentSelectedIds, ...rangeIds]))
542
569
  : rangeIds;
543
570
  } else {
544
571
  nextIds = [id];
545
572
  }
546
573
  } else if (options.additive) {
547
- nextIds = selectedIdSet.has(id)
548
- ? selectedIds.filter((selectedId) => selectedId !== id)
549
- : [...selectedIds, id];
574
+ nextIds = currentSelectedIdSet.has(id)
575
+ ? currentSelectedIds.filter((selectedId) => selectedId !== id)
576
+ : [...currentSelectedIds, id];
550
577
  } else {
551
578
  nextIds = [id];
552
579
  }
553
-
554
580
  // Only advance the anchor on plain clicks; Shift+clicks extend from the
555
581
  // existing anchor so the pivot stays fixed across consecutive range clicks.
556
582
  if (!options.range) {
@@ -558,7 +584,7 @@ export function LayersPanel({
558
584
  }
559
585
  onSelectionChange(nextIds, { id, selectedIds: nextIds, ...options });
560
586
  },
561
- [onSelectionChange, selectableVisibleIds, selectedIdSet, selectedIds],
587
+ [onSelectionChange, selectableVisibleIds],
562
588
  );
563
589
 
564
590
  const commitRename = useCallback(
@@ -642,12 +668,6 @@ export function LayersPanel({
642
668
  {labels.screens}
643
669
  </h2>
644
670
  <div className="flex items-center gap-0.5 text-muted-foreground">
645
- <IconTooltipButton
646
- label={labels.screenOverview}
647
- onClick={onScreenOverview}
648
- >
649
- <IconLayoutGrid className="size-3.5" />
650
- </IconTooltipButton>
651
671
  <IconTooltipButton
652
672
  label={labels.searchPlaceholder}
653
673
  onClick={openSearch}
@@ -663,9 +683,30 @@ export function LayersPanel({
663
683
  </IconTooltipButton>
664
684
  </div>
665
685
  </div>
686
+ <div className="px-2">
687
+ <button
688
+ type="button"
689
+ className={cn(
690
+ "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)]",
691
+ screenOverviewActive
692
+ ? "bg-[var(--design-editor-active-row-color)] text-foreground"
693
+ : "text-foreground/85 hover:bg-[var(--design-editor-active-row-color)] hover:text-foreground",
694
+ )}
695
+ aria-current={screenOverviewActive ? "page" : undefined}
696
+ onClick={() => onScreenOverview?.()}
697
+ title={labels.allScreens}
698
+ >
699
+ <IconLayoutGrid className="size-4 shrink-0" />
700
+ <span className="min-w-0 flex-1 truncate">
701
+ {labels.allScreens}
702
+ </span>
703
+ </button>
704
+ </div>
705
+ <div className="mx-3 my-2 border-t border-[var(--design-editor-panel-divider-color)]" />
666
706
  <div className="space-y-0.5 px-2">
667
707
  {screenRows.map((screen) => {
668
- const isActive = screen.id === activeScreenId;
708
+ const isActive =
709
+ !screenOverviewActive && screen.id === activeScreenId;
669
710
  return (
670
711
  <button
671
712
  key={screen.id}
@@ -676,6 +717,7 @@ export function LayersPanel({
676
717
  ? "bg-[var(--design-editor-active-row-color)] text-foreground"
677
718
  : "text-foreground/85 hover:bg-[var(--design-editor-active-row-color)] hover:text-foreground",
678
719
  )}
720
+ aria-current={isActive ? "page" : undefined}
679
721
  onClick={() => onScreenSelect?.(screen.id)}
680
722
  title={screen.filename ?? screen.name}
681
723
  >
@@ -740,9 +782,13 @@ export function LayersPanel({
740
782
  </div>
741
783
  ) : null}
742
784
 
743
- <div className="min-h-0 flex-1 overflow-y-auto py-2">
785
+ <div className="min-h-0 flex-1 overflow-auto overscroll-contain py-2">
744
786
  {visibleRows.length ? (
745
- <div className="px-2" role="tree" aria-label={labels.title}>
787
+ <div
788
+ className="w-max min-w-full px-2"
789
+ role="tree"
790
+ aria-label={labels.title}
791
+ >
746
792
  {visibleRows.map((row) => (
747
793
  <LayerRow
748
794
  key={row.rowKey}
@@ -766,6 +812,7 @@ export function LayersPanel({
766
812
  onCommitRename={commitRename}
767
813
  onCancelRename={() => setRenamingId(null)}
768
814
  onStartRename={startRename}
815
+ onRename={onRename}
769
816
  onSelect={selectNode}
770
817
  onToggleExpanded={(expanded) =>
771
818
  onExpandedIdsChange(
@@ -809,6 +856,7 @@ function LayerRow({
809
856
  onCommitRename,
810
857
  onCancelRename,
811
858
  onStartRename,
859
+ onRename,
812
860
  onSelect,
813
861
  onToggleExpanded,
814
862
  onToggleLocked,
@@ -835,10 +883,12 @@ function LayerRow({
835
883
  onCommitRename: (id: string) => void;
836
884
  onCancelRename: () => void;
837
885
  onStartRename: (node: LayersPanelNode) => void;
886
+ onRename?: (id: string, name: string) => void;
838
887
  onSelect: (
839
888
  id: string,
840
889
  options: {
841
890
  additive: boolean;
891
+ currentSelectedIds?: string[];
842
892
  range: boolean;
843
893
  source: "keyboard" | "pointer";
844
894
  },
@@ -860,16 +910,37 @@ function LayerRow({
860
910
  const lockable = node.lockable !== false && Boolean(onToggleLocked);
861
911
  const hideable = node.hideable !== false && Boolean(onToggleHidden);
862
912
  const draggable = selectable && Boolean(onMoveLayer);
913
+ const canDropInside = layerCanDropInside(node, hasChildren);
863
914
  const activeDrop =
864
915
  dropIndicator?.targetId === node.id ? dropIndicator.placement : null;
865
916
  // Tracks whether the user pressed Escape to cancel rename so that the
866
917
  // subsequent blur event does not commit the edit.
867
918
  const renameCancelledRef = useRef(false);
868
919
 
920
+ const readSelectedIdsFromTree = (target: HTMLElement): string[] => {
921
+ const tree = target.closest('[role="tree"]');
922
+ if (!tree) return selectedIds.filter((id) => !id.startsWith("__"));
923
+ return Array.from(
924
+ tree.querySelectorAll<HTMLElement>(
925
+ '[role="treeitem"][aria-selected="true"] [data-layer-row-button][data-layer-node-id]',
926
+ ),
927
+ )
928
+ .map((button) => button.dataset.layerNodeId)
929
+ .filter((id): id is string => Boolean(id && !id.startsWith("__")));
930
+ };
931
+
869
932
  const handlePointerSelect = (event: MouseEvent<HTMLButtonElement>) => {
870
933
  if (!selectable) return;
934
+ if (event.detail === 0) return;
935
+ const nativeEvent = event.nativeEvent;
936
+ const additive =
937
+ event.metaKey ||
938
+ event.ctrlKey ||
939
+ nativeEvent.metaKey ||
940
+ nativeEvent.ctrlKey;
871
941
  onSelect(node.id, {
872
- additive: event.metaKey || event.ctrlKey,
942
+ additive,
943
+ currentSelectedIds: readSelectedIdsFromTree(event.currentTarget),
873
944
  range: event.shiftKey,
874
945
  source: "pointer",
875
946
  });
@@ -900,11 +971,12 @@ function LayerRow({
900
971
  (visibleRow) => visibleRow.rowKey === row.rowKey,
901
972
  );
902
973
 
903
- if (event.key === "Enter" || event.key === " ") {
974
+ if (event.key === "Enter" || event.key === " " || event.key === "Space") {
904
975
  event.preventDefault();
905
976
  if (!selectable) return;
906
977
  onSelect(node.id, {
907
978
  additive: event.metaKey || event.ctrlKey,
979
+ currentSelectedIds: readSelectedIdsFromTree(event.currentTarget),
908
980
  range: event.shiftKey,
909
981
  source: "keyboard",
910
982
  });
@@ -1002,11 +1074,12 @@ function LayerRow({
1002
1074
  const intent = {
1003
1075
  draggedIds: cleanedIds,
1004
1076
  targetId: node.id,
1005
- placement: dropPlacementForEvent(event, canAcceptChildren),
1077
+ placement: dropPlacementForEvent(event, canDropInside),
1006
1078
  } satisfies LayersPanelMoveIntent;
1007
1079
  if (canMoveLayer && !canMoveLayer(intent)) return;
1008
1080
  event.preventDefault();
1009
1081
  event.dataTransfer.dropEffect = "move";
1082
+ activeDropIntent = intent;
1010
1083
  onDropIndicatorChange(intent);
1011
1084
  };
1012
1085
 
@@ -1035,233 +1108,312 @@ function LayerRow({
1035
1108
  (id) => id && id !== node.id && !id.startsWith("__"),
1036
1109
  );
1037
1110
  if (cleanedIds.length > 0) {
1038
- // Recompute placement from the drop event position rather than relying on
1039
- // React state (dropIndicator) which may be stale or null.
1040
- const intent = {
1041
- draggedIds: cleanedIds,
1042
- targetId: node.id,
1043
- placement: dropPlacementForEvent(event, canAcceptChildren),
1044
- } satisfies LayersPanelMoveIntent;
1111
+ const storedIntent =
1112
+ activeDropIntent?.targetId === node.id
1113
+ ? {
1114
+ ...activeDropIntent,
1115
+ draggedIds: activeDropIntent.draggedIds.filter((id) =>
1116
+ cleanedIds.includes(id),
1117
+ ),
1118
+ }
1119
+ : null;
1120
+ const intent =
1121
+ storedIntent && storedIntent.draggedIds.length > 0
1122
+ ? storedIntent
1123
+ : ({
1124
+ draggedIds: cleanedIds,
1125
+ targetId: node.id,
1126
+ placement: dropPlacementForEvent(event, canDropInside),
1127
+ } satisfies LayersPanelMoveIntent);
1045
1128
  if (!canMoveLayer || canMoveLayer(intent)) {
1046
1129
  onMoveLayer(intent);
1047
1130
  }
1048
1131
  }
1132
+ activeDropIntent = null;
1049
1133
  onDropIndicatorChange(null);
1050
1134
  };
1051
1135
 
1052
- return (
1053
- <div
1054
- ref={rowRef}
1055
- role="treeitem"
1056
- aria-expanded={hasChildren ? isExpanded : undefined}
1057
- aria-level={depth + 1}
1058
- aria-selected={selectable ? isSelected : undefined}
1059
- className="relative"
1060
- draggable={draggable}
1061
- onDragStart={handleDragStart}
1062
- onDragOver={handleDragOver}
1063
- onDragLeave={(event) => {
1064
- // Only clear the indicator when the pointer truly leaves this row.
1065
- // Moving into a child element fires dragleave on the outer div too, so
1066
- // we suppress the clear when relatedTarget is still within this row.
1067
- if (event.currentTarget.contains(event.relatedTarget as Node | null))
1068
- return;
1069
- onDropIndicatorChange(null);
1070
- }}
1071
- onDrop={handleDrop}
1072
- onDragEnd={() => {
1073
- activeDragState = null;
1074
- onDropIndicatorChange(null);
1075
- }}
1076
- onMouseEnter={() => onHoverLayer?.(node.id)}
1077
- onMouseLeave={() => onLeaveLayer?.(node.id)}
1078
- >
1079
- {activeDrop === "before" ? (
1080
- <span className="pointer-events-none absolute left-2 right-2 top-0 z-10 h-px bg-[var(--design-editor-accent-color)]" />
1081
- ) : null}
1082
- {activeDrop === "after" ? (
1083
- <span className="pointer-events-none absolute bottom-0 left-2 right-2 z-10 h-px bg-[var(--design-editor-accent-color)]" />
1084
- ) : null}
1085
- <div
1086
- className={cn(
1087
- "group flex h-8 items-center gap-1 rounded-[5px] pr-1 text-[12px]",
1088
- activeDrop === "inside" &&
1089
- "ring-1 ring-inset ring-[var(--design-editor-accent-color)]",
1090
- isSelected &&
1091
- "bg-[var(--design-editor-selection-color)] text-foreground",
1092
- !isSelected &&
1093
- isInSelectedSubtree &&
1094
- "bg-[var(--design-editor-selected-subtree-color)] text-foreground/95",
1095
- !isSelected &&
1096
- isActiveScreen &&
1097
- "bg-[var(--design-editor-active-row-color)] text-foreground hover:bg-[var(--design-editor-active-row-color)]",
1098
- !isSelected &&
1099
- !isInSelectedSubtree &&
1100
- !isActiveScreen &&
1101
- "text-foreground/90 hover:bg-[var(--design-editor-layer-hover-color)] hover:text-foreground",
1102
- node.hidden && "text-muted-foreground",
1103
- )}
1104
- style={{ paddingLeft: rowIndent(depth) }}
1105
- >
1106
- {hasChildren ? (
1107
- <Button
1108
- type="button"
1109
- variant="ghost"
1110
- size="icon"
1111
- className="size-4 shrink-0 rounded-sm p-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
1112
- aria-label={isExpanded ? labels.collapse : labels.expand}
1113
- onClick={(event) => {
1114
- event.stopPropagation();
1115
- onToggleExpanded(!isExpanded);
1116
- }}
1117
- >
1118
- {isExpanded ? (
1119
- <IconChevronDown className="size-4" />
1120
- ) : (
1121
- <IconChevronRight className="size-4 rtl:-scale-x-100" />
1122
- )}
1123
- </Button>
1124
- ) : (
1125
- <span className="size-4 shrink-0" />
1126
- )}
1136
+ const showContextMenu =
1137
+ selectable &&
1138
+ (Boolean(onRename && node.renamable !== false) || lockable || hideable);
1127
1139
 
1128
- <button
1129
- type="button"
1130
- disabled={!selectable}
1131
- data-layer-row-button
1132
- data-layer-node-id={node.id}
1133
- className={cn(
1134
- "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)]",
1135
- selectable ? "cursor-default" : "cursor-default opacity-80",
1136
- )}
1137
- onClick={handlePointerSelect}
1138
- onDoubleClick={() => onStartRename(node)}
1139
- onKeyDown={handleKeyDown}
1140
+ return (
1141
+ <ContextMenu>
1142
+ <ContextMenuTrigger asChild disabled={!showContextMenu}>
1143
+ <div
1144
+ ref={rowRef}
1145
+ role="treeitem"
1146
+ aria-expanded={hasChildren ? isExpanded : undefined}
1147
+ aria-level={depth + 1}
1148
+ aria-selected={selectable ? isSelected : undefined}
1149
+ className="relative min-w-full"
1150
+ draggable={draggable}
1151
+ onDragStart={handleDragStart}
1152
+ onDragOver={handleDragOver}
1153
+ onDragLeave={(event) => {
1154
+ // Only clear the indicator when the pointer truly leaves this row.
1155
+ // Moving into a child element fires dragleave on the outer div too, so
1156
+ // we suppress the clear when relatedTarget is still within this row.
1157
+ if (
1158
+ event.currentTarget.contains(event.relatedTarget as Node | null)
1159
+ )
1160
+ return;
1161
+ if (activeDropIntent?.targetId === node.id) activeDropIntent = null;
1162
+ onDropIndicatorChange(null);
1163
+ }}
1164
+ onDrop={handleDrop}
1165
+ onDragEnd={() => {
1166
+ activeDragState = null;
1167
+ activeDropIntent = null;
1168
+ onDropIndicatorChange(null);
1169
+ }}
1170
+ onMouseEnter={() => onHoverLayer?.(node.id)}
1171
+ onMouseLeave={() => onLeaveLayer?.(node.id)}
1140
1172
  >
1141
- <span
1173
+ {activeDrop === "before" ? (
1174
+ <span className="pointer-events-none absolute left-2 right-2 top-0 z-10 h-px bg-[var(--design-editor-accent-color)]" />
1175
+ ) : null}
1176
+ {activeDrop === "after" ? (
1177
+ <span className="pointer-events-none absolute bottom-0 left-2 right-2 z-10 h-px bg-[var(--design-editor-accent-color)]" />
1178
+ ) : null}
1179
+ <div
1142
1180
  className={cn(
1143
- "shrink-0 text-muted-foreground",
1144
- (isSelected || isInSelectedSubtree) && "text-foreground",
1181
+ "group flex h-8 min-w-full items-center gap-1 rounded-[5px] pr-1 text-[12px]",
1182
+ activeDrop === "inside" &&
1183
+ "ring-1 ring-inset ring-[var(--design-editor-accent-color)]",
1184
+ isSelected &&
1185
+ "bg-[var(--design-editor-selection-color)] text-foreground",
1186
+ !isSelected &&
1187
+ isInSelectedSubtree &&
1188
+ "bg-[var(--design-editor-selected-subtree-color)] text-foreground/95",
1189
+ !isSelected &&
1190
+ isActiveScreen &&
1191
+ "bg-[var(--design-editor-active-row-color)] text-foreground hover:bg-[var(--design-editor-active-row-color)]",
1192
+ !isSelected &&
1193
+ !isInSelectedSubtree &&
1194
+ !isActiveScreen &&
1195
+ "text-foreground/90 hover:bg-[var(--design-editor-layer-hover-color)] hover:text-foreground",
1196
+ node.hidden && "text-muted-foreground",
1145
1197
  )}
1198
+ style={{ paddingLeft: rowIndent(depth) }}
1146
1199
  >
1147
- {node.icon ?? (
1148
- <LayerGlyph
1149
- node={node}
1150
- selected={isSelected}
1151
- inSelectedSubtree={isInSelectedSubtree}
1152
- />
1153
- )}
1154
- </span>
1155
- {isRenaming ? (
1156
- <input
1157
- autoFocus
1158
- value={renameDraft}
1159
- onClick={(event) => event.stopPropagation()}
1160
- onChange={(event) => onRenameDraftChange(event.target.value)}
1161
- onBlur={() => {
1162
- // Escape sets renameCancelledRef before blur fires; skip commit.
1163
- if (renameCancelledRef.current) {
1164
- renameCancelledRef.current = false;
1165
- return;
1166
- }
1167
- onCommitRename(node.id);
1168
- }}
1169
- onKeyDown={(event) => {
1170
- if (event.key === "Enter") {
1171
- event.preventDefault();
1172
- onCommitRename(node.id);
1173
- } else if (event.key === "Escape") {
1174
- event.preventDefault();
1175
- renameCancelledRef.current = true;
1176
- onCancelRename();
1177
- }
1178
- }}
1179
- 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"
1180
- aria-label={labels.rename}
1181
- />
1182
- ) : (
1183
- <span
1184
- className={cn(
1185
- "min-w-0 flex-1 truncate font-medium leading-none",
1186
- node.hidden && "line-through",
1187
- )}
1188
- >
1189
- {node.name}
1190
- </span>
1191
- )}
1192
- {!isRenaming &&
1193
- layerCanShowBadge(node) &&
1194
- node.badge !== null &&
1195
- node.badge !== undefined ? (
1196
- <span className="shrink-0 rounded-sm bg-muted px-1 text-[10px] text-muted-foreground">
1197
- {node.badge}
1198
- </span>
1199
- ) : null}
1200
- </button>
1201
-
1202
- {lockable ? (
1203
- <Tooltip>
1204
- <TooltipTrigger asChild>
1200
+ {hasChildren ? (
1205
1201
  <Button
1206
1202
  type="button"
1207
1203
  variant="ghost"
1208
1204
  size="icon"
1209
- className={cn(
1210
- "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",
1211
- node.locked && "opacity-100",
1212
- isSelected && "text-foreground",
1213
- )}
1214
- aria-label={node.locked ? labels.unlock : labels.lock}
1205
+ className="size-4 shrink-0 rounded-sm p-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
1206
+ aria-label={isExpanded ? labels.collapse : labels.expand}
1215
1207
  onClick={(event) => {
1216
1208
  event.stopPropagation();
1217
- onToggleLocked?.(node.id, !node.locked);
1209
+ onToggleExpanded(!isExpanded);
1218
1210
  }}
1219
1211
  >
1220
- {node.locked ? (
1221
- <IconLock className="size-3" />
1212
+ {isExpanded ? (
1213
+ <IconChevronDown className="size-4" />
1222
1214
  ) : (
1223
- <IconLockOpen className="size-3" />
1215
+ <IconChevronRight className="size-4 rtl:-scale-x-100" />
1224
1216
  )}
1225
1217
  </Button>
1226
- </TooltipTrigger>
1227
- <TooltipContent>
1228
- {node.locked ? labels.unlock : labels.lock}
1229
- </TooltipContent>
1230
- </Tooltip>
1231
- ) : null}
1218
+ ) : (
1219
+ <span className="size-4 shrink-0" />
1220
+ )}
1232
1221
 
1233
- {hideable ? (
1234
- <Tooltip>
1235
- <TooltipTrigger asChild>
1236
- <Button
1237
- type="button"
1238
- variant="ghost"
1239
- size="icon"
1222
+ <button
1223
+ type="button"
1224
+ disabled={!selectable}
1225
+ data-layer-row-button
1226
+ data-layer-node-id={node.id}
1227
+ className={cn(
1228
+ "flex min-w-0 flex-1 items-center gap-2 rounded-sm px-0.5 py-0 text-left outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]",
1229
+ selectable ? "cursor-default" : "cursor-default opacity-80",
1230
+ )}
1231
+ onClick={handlePointerSelect}
1232
+ onDoubleClick={() => onStartRename(node)}
1233
+ onKeyDown={handleKeyDown}
1234
+ >
1235
+ <span
1240
1236
  className={cn(
1241
- "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",
1242
- node.hidden && "opacity-100",
1243
- isSelected && "text-foreground",
1237
+ "shrink-0 text-muted-foreground",
1238
+ (isSelected || isInSelectedSubtree) && "text-foreground",
1244
1239
  )}
1245
- aria-label={node.hidden ? labels.show : labels.hide}
1246
- onClick={(event) => {
1247
- event.stopPropagation();
1248
- onToggleHidden?.(node.id, !node.hidden);
1249
- }}
1250
1240
  >
1251
- {node.hidden ? (
1252
- <IconEyeOff className="size-3" />
1253
- ) : (
1254
- <IconEye className="size-3" />
1241
+ {node.icon ?? (
1242
+ <LayerGlyph
1243
+ node={node}
1244
+ selected={isSelected}
1245
+ inSelectedSubtree={isInSelectedSubtree}
1246
+ />
1255
1247
  )}
1256
- </Button>
1257
- </TooltipTrigger>
1258
- <TooltipContent>
1248
+ </span>
1249
+ {isRenaming ? (
1250
+ <input
1251
+ autoFocus
1252
+ value={renameDraft}
1253
+ onClick={(event) => event.stopPropagation()}
1254
+ onChange={(event) => onRenameDraftChange(event.target.value)}
1255
+ onBlur={() => {
1256
+ // Escape sets renameCancelledRef before blur fires; skip commit.
1257
+ if (renameCancelledRef.current) {
1258
+ renameCancelledRef.current = false;
1259
+ return;
1260
+ }
1261
+ onCommitRename(node.id);
1262
+ }}
1263
+ onKeyDown={(event) => {
1264
+ if (event.key === "Enter") {
1265
+ event.preventDefault();
1266
+ // Stop propagation so the keydown does not bubble up to the
1267
+ // parent row <button>'s handleKeyDown, which would fire
1268
+ // onSelect and potentially trigger canvas-level side effects
1269
+ // (e.g. switching to overview mode or selecting a wrong layer).
1270
+ event.stopPropagation();
1271
+ onCommitRename(node.id);
1272
+ } else if (event.key === "Tab") {
1273
+ // Commit the rename on Tab (Figma behavior) and prevent the
1274
+ // keydown from reaching the global design hotkeys handler which
1275
+ // would cycle the active file when Tab fires outside an input.
1276
+ event.preventDefault();
1277
+ event.stopPropagation();
1278
+ onCommitRename(node.id);
1279
+ } else if (event.key === "Escape") {
1280
+ event.preventDefault();
1281
+ event.stopPropagation();
1282
+ renameCancelledRef.current = true;
1283
+ onCancelRename();
1284
+ }
1285
+ }}
1286
+ 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"
1287
+ aria-label={labels.rename}
1288
+ />
1289
+ ) : (
1290
+ <span
1291
+ className={cn(
1292
+ "min-w-0 flex-1 truncate font-medium leading-none",
1293
+ node.hidden && "line-through",
1294
+ )}
1295
+ title={node.name}
1296
+ >
1297
+ {node.name}
1298
+ </span>
1299
+ )}
1300
+ {!isRenaming &&
1301
+ layerCanShowBadge(node) &&
1302
+ node.badge !== null &&
1303
+ node.badge !== undefined ? (
1304
+ <span className="shrink-0 rounded-sm bg-muted px-1 text-[10px] text-muted-foreground">
1305
+ {node.badge}
1306
+ </span>
1307
+ ) : null}
1308
+ </button>
1309
+
1310
+ {lockable ? (
1311
+ <Tooltip>
1312
+ <TooltipTrigger asChild>
1313
+ <Button
1314
+ type="button"
1315
+ variant="ghost"
1316
+ size="icon"
1317
+ className={cn(
1318
+ "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",
1319
+ node.locked && "opacity-100",
1320
+ isSelected && "text-foreground",
1321
+ )}
1322
+ aria-label={node.locked ? labels.unlock : labels.lock}
1323
+ onClick={(event) => {
1324
+ event.stopPropagation();
1325
+ onToggleLocked?.(node.id, !node.locked);
1326
+ }}
1327
+ >
1328
+ {node.locked ? (
1329
+ <IconLock className="size-3" />
1330
+ ) : (
1331
+ <IconLockOpen className="size-3" />
1332
+ )}
1333
+ </Button>
1334
+ </TooltipTrigger>
1335
+ <TooltipContent>
1336
+ {node.locked ? labels.unlock : labels.lock}
1337
+ </TooltipContent>
1338
+ </Tooltip>
1339
+ ) : null}
1340
+
1341
+ {hideable ? (
1342
+ <Tooltip>
1343
+ <TooltipTrigger asChild>
1344
+ <Button
1345
+ type="button"
1346
+ variant="ghost"
1347
+ size="icon"
1348
+ className={cn(
1349
+ "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",
1350
+ node.hidden && "opacity-100",
1351
+ isSelected && "text-foreground",
1352
+ )}
1353
+ aria-label={node.hidden ? labels.show : labels.hide}
1354
+ onClick={(event) => {
1355
+ event.stopPropagation();
1356
+ onToggleHidden?.(node.id, !node.hidden);
1357
+ }}
1358
+ >
1359
+ {node.hidden ? (
1360
+ <IconEyeOff className="size-3" />
1361
+ ) : (
1362
+ <IconEye className="size-3" />
1363
+ )}
1364
+ </Button>
1365
+ </TooltipTrigger>
1366
+ <TooltipContent>
1367
+ {node.hidden ? labels.show : labels.hide}
1368
+ </TooltipContent>
1369
+ </Tooltip>
1370
+ ) : null}
1371
+ </div>
1372
+ </div>
1373
+ </ContextMenuTrigger>
1374
+ {showContextMenu ? (
1375
+ <ContextMenuContent className="z-[300] min-w-[160px] text-[12px]">
1376
+ {onRename && node.renamable !== false ? (
1377
+ <ContextMenuItem
1378
+ className="gap-2 text-[12px]"
1379
+ onSelect={() => onStartRename(node)}
1380
+ >
1381
+ <IconPencil className="size-3.5 text-muted-foreground" />
1382
+ {labels.rename}
1383
+ </ContextMenuItem>
1384
+ ) : null}
1385
+ {onRename && node.renamable !== false && (lockable || hideable) ? (
1386
+ <ContextMenuSeparator />
1387
+ ) : null}
1388
+ {lockable ? (
1389
+ <ContextMenuItem
1390
+ className="gap-2 text-[12px]"
1391
+ onSelect={() => onToggleLocked?.(node.id, !node.locked)}
1392
+ >
1393
+ {node.locked ? (
1394
+ <IconLockOpen className="size-3.5 text-muted-foreground" />
1395
+ ) : (
1396
+ <IconLock className="size-3.5 text-muted-foreground" />
1397
+ )}
1398
+ {node.locked ? labels.unlock : labels.lock}
1399
+ </ContextMenuItem>
1400
+ ) : null}
1401
+ {hideable ? (
1402
+ <ContextMenuItem
1403
+ className="gap-2 text-[12px]"
1404
+ onSelect={() => onToggleHidden?.(node.id, !node.hidden)}
1405
+ >
1406
+ {node.hidden ? (
1407
+ <IconEye className="size-3.5 text-muted-foreground" />
1408
+ ) : (
1409
+ <IconEyeOff className="size-3.5 text-muted-foreground" />
1410
+ )}
1259
1411
  {node.hidden ? labels.show : labels.hide}
1260
- </TooltipContent>
1261
- </Tooltip>
1262
- ) : null}
1263
- </div>
1264
- </div>
1412
+ </ContextMenuItem>
1413
+ ) : null}
1414
+ </ContextMenuContent>
1415
+ ) : null}
1416
+ </ContextMenu>
1265
1417
  );
1266
1418
  }
1267
1419
 
@@ -1623,6 +1775,18 @@ function ElementLayerGlyph({ className }: { className?: string }) {
1623
1775
  );
1624
1776
  }
1625
1777
 
1778
+ function layerCanDropInside(node: LayersPanelNode, hasChildren: boolean) {
1779
+ return (
1780
+ hasChildren ||
1781
+ Boolean(node.layout?.isFlexContainer || node.layout?.isGridContainer) ||
1782
+ node.type === "file" ||
1783
+ node.type === "screen" ||
1784
+ node.type === "frame" ||
1785
+ node.type === "group" ||
1786
+ node.type === "section"
1787
+ );
1788
+ }
1789
+
1626
1790
  function dropPlacementForEvent(
1627
1791
  event: DragEvent<HTMLDivElement>,
1628
1792
  canDropInside: boolean,