@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
@@ -40,6 +40,7 @@ import {
40
40
  IconLetterSpacing,
41
41
  IconLineHeight,
42
42
  IconLink,
43
+ IconLock,
43
44
  IconMaximize,
44
45
  IconMinus,
45
46
  IconPalette,
@@ -51,7 +52,13 @@ import {
51
52
  IconUnlink,
52
53
  IconVector,
53
54
  } from "@tabler/icons-react";
54
- import { useCallback, useEffect, useState, type ReactNode } from "react";
55
+ import {
56
+ useCallback,
57
+ useEffect,
58
+ useRef,
59
+ useState,
60
+ type ReactNode,
61
+ } from "react";
55
62
 
56
63
  import { Button } from "@/components/ui/button";
57
64
  import {
@@ -100,12 +107,15 @@ import {
100
107
  type AutoLayoutSizingAxis,
101
108
  type ConstraintsValue,
102
109
  type ExportSettingsValue,
110
+ imageFillToBackgroundStyles,
103
111
  type DesignFillRow,
104
112
  type DesignFillRowPatch,
105
113
  type DesignGradientStop,
106
114
  type DesignGradientStopPatch,
107
115
  type DesignGradientType,
116
+ type ImageFillValue,
108
117
  } from "./inspector";
118
+ import { IconLayoutSettings } from "./inspector/design-icons";
109
119
  import type { DesignPaintType } from "./inspector/DesignColorPicker";
110
120
  import { TweaksPanelContent } from "./TweaksPanel";
111
121
  import type { ElementInfo } from "./types";
@@ -129,6 +139,7 @@ interface EditPanelProps {
129
139
  onStylesChange?: (styles: Record<string, string>) => void;
130
140
  onExport?: (settings: ExportSettingsValue[]) => void;
131
141
  exporting?: boolean;
142
+ readOnly?: boolean;
132
143
  }
133
144
 
134
145
  /**
@@ -238,6 +249,7 @@ function ColorInput({
238
249
  onChange,
239
250
  backgroundImage,
240
251
  onBackgroundImageChange,
252
+ onImageFillChange,
241
253
  blendMode,
242
254
  onBlendModeChange,
243
255
  supportsLayeredFills = false,
@@ -249,6 +261,7 @@ function ColorInput({
249
261
  onChange: (value: string) => void;
250
262
  backgroundImage?: string;
251
263
  onBackgroundImageChange?: (value: string) => void;
264
+ onImageFillChange?: (value: ImageFillValue) => void;
252
265
  blendMode?: string;
253
266
  onBlendModeChange?: (value: string) => void;
254
267
  supportsLayeredFills?: boolean;
@@ -558,6 +571,7 @@ function ColorInput({
558
571
  onPaintValueChange={
559
572
  supportsLayeredFills ? handlePaintValueChange : undefined
560
573
  }
574
+ onImageFillChange={onImageFillChange}
561
575
  blendMode={blendMode}
562
576
  onBlendModeChange={onBlendModeChange}
563
577
  showBlendMode={supportsLayeredFills}
@@ -912,8 +926,6 @@ function PropSlider({
912
926
  );
913
927
  }
914
928
 
915
- const EmptyScrubIcon = () => null;
916
-
917
929
  /**
918
930
  * design-editor inspector section. Matches the design editor "Design" panel chrome:
919
931
  * - NO left collapse chevron (the design editor uses none).
@@ -1131,6 +1143,99 @@ const FONT_WEIGHT_OPTIONS = [
1131
1143
  { value: "900", key: "black" },
1132
1144
  ] as const;
1133
1145
 
1146
+ type TextResizeMode = "auto-width" | "auto-height" | "fixed";
1147
+
1148
+ function cleanFontFamilyName(value: string): string {
1149
+ const trimmed = value.trim();
1150
+ if (
1151
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
1152
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
1153
+ ) {
1154
+ return trimmed.slice(1, -1).trim();
1155
+ }
1156
+ return trimmed;
1157
+ }
1158
+
1159
+ function splitFontFamilyList(value: string | undefined): string[] {
1160
+ const raw = value?.trim();
1161
+ if (!raw) return [];
1162
+
1163
+ const families: string[] = [];
1164
+ let token = "";
1165
+ let quote: '"' | "'" | null = null;
1166
+
1167
+ for (let i = 0; i < raw.length; i += 1) {
1168
+ const char = raw[i];
1169
+ if ((char === '"' || char === "'") && raw[i - 1] !== "\\") {
1170
+ if (quote === char) quote = null;
1171
+ else if (!quote) quote = char;
1172
+ token += char;
1173
+ continue;
1174
+ }
1175
+ if (char === "," && !quote) {
1176
+ const cleaned = cleanFontFamilyName(token);
1177
+ if (cleaned) families.push(cleaned);
1178
+ token = "";
1179
+ continue;
1180
+ }
1181
+ token += char;
1182
+ }
1183
+
1184
+ const cleaned = cleanFontFamilyName(token);
1185
+ if (cleaned) families.push(cleaned);
1186
+ return families;
1187
+ }
1188
+
1189
+ function normalizeFontFamilyName(value: string): string {
1190
+ return cleanFontFamilyName(value).replace(/\s+/g, " ").toLowerCase();
1191
+ }
1192
+
1193
+ function normalizeFontFamilyStack(value: string): string {
1194
+ return splitFontFamilyList(value).map(normalizeFontFamilyName).join(",");
1195
+ }
1196
+
1197
+ function displayFontFamilyName(value: string | undefined): string {
1198
+ const first = splitFontFamilyList(value)[0];
1199
+ if (!first) return "Sans Serif"; // i18n-ignore design generic font label
1200
+
1201
+ const normalized = normalizeFontFamilyName(first);
1202
+ if (normalized === "sans-serif") {
1203
+ return "Sans Serif"; // i18n-ignore design generic font label
1204
+ }
1205
+ if (normalized === "serif") return "Serif"; // i18n-ignore design generic font label
1206
+ if (normalized === "monospace") {
1207
+ return "Monospace"; // i18n-ignore design generic font label
1208
+ }
1209
+ if (normalized === "system-ui" || normalized === "-apple-system") {
1210
+ return "System UI"; // i18n-ignore design generic font label
1211
+ }
1212
+ if (normalized === "blinkmacsystemfont") {
1213
+ return "Apple System"; // i18n-ignore design generic font label
1214
+ }
1215
+ return first;
1216
+ }
1217
+
1218
+ function resolveFontFamilySelectValue(value: string | undefined): string {
1219
+ const raw = value?.trim();
1220
+ if (!raw) return "sans-serif";
1221
+
1222
+ const normalizedStack = normalizeFontFamilyStack(raw);
1223
+ const exactOption = FONT_FAMILY_OPTIONS.find(
1224
+ (option) => normalizeFontFamilyStack(option.value) === normalizedStack,
1225
+ );
1226
+ if (exactOption) return exactOption.value;
1227
+
1228
+ const firstFamily = normalizeFontFamilyName(
1229
+ splitFontFamilyList(raw)[0] ?? "",
1230
+ );
1231
+ const firstFamilyOption = FONT_FAMILY_OPTIONS.find(
1232
+ (option) =>
1233
+ normalizeFontFamilyName(splitFontFamilyList(option.value)[0] ?? "") ===
1234
+ firstFamily,
1235
+ );
1236
+ return firstFamilyOption?.value ?? raw;
1237
+ }
1238
+
1134
1239
  const ALIGN_SELF_OPTIONS = [
1135
1240
  { value: "auto", key: "auto" },
1136
1241
  { value: "flex-start", key: "start" },
@@ -1282,6 +1387,8 @@ function ScrubStyleInput({
1282
1387
  step = 1,
1283
1388
  labelClassName,
1284
1389
  inputClassName,
1390
+ ariaLabel,
1391
+ tooltipLabel,
1285
1392
  hideIcon = true,
1286
1393
  }: {
1287
1394
  label: string;
@@ -1295,11 +1402,15 @@ function ScrubStyleInput({
1295
1402
  labelClassName?: string;
1296
1403
  inputClassName?: string;
1297
1404
  hideIcon?: boolean;
1405
+ ariaLabel?: string;
1406
+ tooltipLabel?: string;
1298
1407
  }) {
1299
1408
  return (
1300
1409
  <ScrubInput
1301
1410
  label={label}
1302
- icon={hideIcon ? EmptyScrubIcon : undefined}
1411
+ ariaLabel={ariaLabel}
1412
+ tooltipLabel={tooltipLabel}
1413
+ icon={hideIcon ? null : undefined}
1303
1414
  value={value ? parseNumericValue(value) : (placeholder ?? 0)}
1304
1415
  onChange={onChange}
1305
1416
  unit={unit}
@@ -1650,6 +1761,33 @@ function inferElementSizing(
1650
1761
  return "fixed";
1651
1762
  }
1652
1763
 
1764
+ /**
1765
+ * Return the element's geometric dimension on the given axis in CSS pixels.
1766
+ *
1767
+ * `getComputedStyle().width/height` always resolves to a computed px value
1768
+ * (even for `width: auto` the browser returns e.g. "200px"). For rotated
1769
+ * elements this is the pre-rotation CSS box size — what Figma shows in the
1770
+ * inspector — while `getBoundingClientRect().width/height` would be the
1771
+ * axis-aligned bounding box which is inflated by the rotation.
1772
+ *
1773
+ * Falls back to the bounding-rect dimension only when the computed style is
1774
+ * missing or unparseable (e.g. the bridge hasn't populated it yet).
1775
+ */
1776
+ function cssElementSize(
1777
+ element: ElementInfo,
1778
+ axis: AutoLayoutSizingAxis,
1779
+ ): number {
1780
+ const isHorizontal = axis === "horizontal";
1781
+ const cssValue = isHorizontal
1782
+ ? element.computedStyles.width
1783
+ : element.computedStyles.height;
1784
+ const parsed = parseFloat(cssValue || "");
1785
+ const fallback = isHorizontal
1786
+ ? element.boundingRect.width
1787
+ : element.boundingRect.height;
1788
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
1789
+ }
1790
+
1653
1791
  function commitElementSizing(
1654
1792
  element: ElementInfo,
1655
1793
  axis: AutoLayoutSizingAxis,
@@ -1659,12 +1797,10 @@ function commitElementSizing(
1659
1797
  ) {
1660
1798
  const isHorizontal = axis === "horizontal";
1661
1799
  const sizeProperty = isHorizontal ? "width" : "height";
1662
- const resolvedSize = Math.max(
1663
- 1,
1664
- Math.round(
1665
- isHorizontal ? element.boundingRect.width : element.boundingRect.height,
1666
- ),
1667
- );
1800
+ // Use CSS computed dimension (pre-rotation box size) as the seed for "fixed"
1801
+ // sizing so a rotated element is locked to its actual CSS width/height rather
1802
+ // than the inflated axis-aligned bounding rect.
1803
+ const resolvedSize = Math.max(1, Math.round(cssElementSize(element, axis)));
1668
1804
  const parentDirection = parentFlexDirection(element);
1669
1805
  const isFlex = isParentFlex(element);
1670
1806
  const isGrid = isParentGrid(element);
@@ -1770,7 +1906,7 @@ function InspectorTabsHeader({
1770
1906
  const t = useT();
1771
1907
 
1772
1908
  return (
1773
- <div className="flex min-h-8 shrink-0 items-center justify-between gap-1 border-b border-border/90 px-2">
1909
+ <div className="flex min-h-8 shrink-0 items-center justify-between gap-1 border-b border-border/90 px-2 py-1">
1774
1910
  <Tabs
1775
1911
  value={activeTab}
1776
1912
  onValueChange={(value) => onActiveTabChange(value as InspectorTab)}
@@ -1912,12 +2048,113 @@ function InspectorIconButton({
1912
2048
 
1913
2049
  function InspectorSegment({ children }: { children: ReactNode }) {
1914
2050
  return (
1915
- <div className="flex min-w-0 overflow-hidden rounded-md bg-[var(--design-editor-control-bg)]">
2051
+ <div className="flex w-fit max-w-full min-w-0 overflow-hidden rounded-md bg-[var(--design-editor-control-bg)]">
1916
2052
  {children}
1917
2053
  </div>
1918
2054
  );
1919
2055
  }
1920
2056
 
2057
+ function TextResizeControls({
2058
+ resizeMode,
2059
+ onResizeModeChange,
2060
+ }: {
2061
+ resizeMode: TextResizeMode;
2062
+ onResizeModeChange: (mode: TextResizeMode) => void;
2063
+ }) {
2064
+ const t = useT();
2065
+
2066
+ return (
2067
+ <InspectorSegment>
2068
+ <InspectorIconButton
2069
+ label={t("editPanel.textResize.autoWidth")}
2070
+ active={resizeMode === "auto-width"}
2071
+ onClick={() => onResizeModeChange("auto-width")}
2072
+ >
2073
+ <IconArrowAutofitWidth className="size-3.5" />
2074
+ </InspectorIconButton>
2075
+ <InspectorIconButton
2076
+ label={t("editPanel.textResize.autoHeight")}
2077
+ active={resizeMode === "auto-height"}
2078
+ onClick={() => onResizeModeChange("auto-height")}
2079
+ >
2080
+ <IconArrowAutofitHeight className="size-3.5" />
2081
+ </InspectorIconButton>
2082
+ <InspectorIconButton
2083
+ label={t("editPanel.textResize.fixed")}
2084
+ active={resizeMode === "fixed"}
2085
+ onClick={() => onResizeModeChange("fixed")}
2086
+ >
2087
+ <IconSquare className="size-3.5" />
2088
+ </InspectorIconButton>
2089
+ </InspectorSegment>
2090
+ );
2091
+ }
2092
+
2093
+ function TypographyDetailsPopover({
2094
+ resizeMode,
2095
+ onResizeModeChange,
2096
+ }: {
2097
+ resizeMode: TextResizeMode;
2098
+ onResizeModeChange: (mode: TextResizeMode) => void;
2099
+ }) {
2100
+ const [open, setOpen] = useState(false);
2101
+
2102
+ return (
2103
+ <Popover open={open} onOpenChange={setOpen}>
2104
+ <PopoverTrigger asChild>
2105
+ <Button
2106
+ type="button"
2107
+ variant="ghost"
2108
+ size="icon"
2109
+ aria-label={"Typography details" /* i18n-ignore design action */}
2110
+ aria-pressed={open}
2111
+ className={cn(
2112
+ "h-6 min-w-6 cursor-pointer rounded-md text-muted-foreground hover:bg-[var(--design-editor-panel-raised-bg)] hover:text-foreground",
2113
+ open &&
2114
+ "bg-[var(--design-editor-accent-color)]/20 text-[var(--design-editor-accent-color)] hover:text-[var(--design-editor-accent-color)]",
2115
+ )}
2116
+ >
2117
+ <IconLayoutSettings className="size-3.5" />
2118
+ </Button>
2119
+ </PopoverTrigger>
2120
+ <PopoverContent
2121
+ side="left"
2122
+ align="end"
2123
+ sideOffset={8}
2124
+ className="z-[100010] w-[360px] rounded-xl border-[var(--design-editor-control-border)] bg-[var(--design-editor-panel-bg)] p-0 text-foreground shadow-2xl"
2125
+ >
2126
+ <div className="flex items-center gap-1 border-b border-[var(--design-editor-control-border)] p-2.5">
2127
+ <div className="flex rounded-md bg-[var(--design-editor-control-bg)] p-0.5">
2128
+ <span className="rounded bg-[var(--design-editor-panel-raised-bg)] px-2.5 py-1 text-[11px] font-semibold text-foreground">
2129
+ {"Basics" /* i18n-ignore design typography details tab */}
2130
+ </span>
2131
+ <span className="px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
2132
+ {"Details" /* i18n-ignore design typography details tab */}
2133
+ </span>
2134
+ <span className="px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
2135
+ {"Variable" /* i18n-ignore design typography details tab */}
2136
+ </span>
2137
+ </div>
2138
+ </div>
2139
+ <div className="space-y-3 p-4 text-[11px]">
2140
+ <div className="flex h-20 items-center justify-center rounded-md bg-[var(--design-editor-control-bg)] text-[18px] text-muted-foreground/80">
2141
+ {"Preview" /* i18n-ignore design typography details preview */}
2142
+ </div>
2143
+ <div className="flex items-center justify-between gap-3">
2144
+ <span className="text-[11px] font-medium text-muted-foreground">
2145
+ {"Text box" /* i18n-ignore design typography details label */}
2146
+ </span>
2147
+ <TextResizeControls
2148
+ resizeMode={resizeMode}
2149
+ onResizeModeChange={onResizeModeChange}
2150
+ />
2151
+ </div>
2152
+ </div>
2153
+ </PopoverContent>
2154
+ </Popover>
2155
+ );
2156
+ }
2157
+
1921
2158
  function CornerRadiusControl({
1922
2159
  styles,
1923
2160
  onStyleChange,
@@ -2389,20 +2626,29 @@ function ShadowEffectRow({
2389
2626
  function PageProperties({
2390
2627
  styles,
2391
2628
  onStyleChange,
2629
+ onStylesChange,
2392
2630
  }: {
2393
2631
  styles: Record<string, string>;
2394
2632
  onStyleChange: (property: string, value: string) => void;
2633
+ onStylesChange?: (styles: Record<string, string>) => void;
2395
2634
  }) {
2396
2635
  const t = useT();
2397
- const fontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
2636
+ const baseFontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
2398
2637
  value: option.value,
2399
2638
  label: t(`editPanel.fontFamilies.${option.key}`),
2400
2639
  }));
2401
- const fontFamily = FONT_FAMILY_OPTIONS.some(
2402
- (option) => option.value === styles.fontFamily,
2640
+ const fontFamily = resolveFontFamilySelectValue(styles.fontFamily);
2641
+ const fontFamilyOptions = FONT_FAMILY_OPTIONS.some(
2642
+ (option) => option.value === fontFamily,
2403
2643
  )
2404
- ? styles.fontFamily
2405
- : "sans-serif";
2644
+ ? baseFontFamilyOptions
2645
+ : [
2646
+ {
2647
+ value: fontFamily,
2648
+ label: displayFontFamilyName(styles.fontFamily || fontFamily),
2649
+ },
2650
+ ...baseFontFamilyOptions,
2651
+ ];
2406
2652
 
2407
2653
  return (
2408
2654
  <div>
@@ -2413,6 +2659,13 @@ function PageProperties({
2413
2659
  onChange={(v) => onStyleChange("backgroundColor", v)}
2414
2660
  backgroundImage={styles.backgroundImage || ""}
2415
2661
  onBackgroundImageChange={(v) => onStyleChange("backgroundImage", v)}
2662
+ onImageFillChange={(value) =>
2663
+ commitStylePatch(
2664
+ imageFillToBackgroundStyles(value),
2665
+ onStyleChange,
2666
+ onStylesChange,
2667
+ )
2668
+ }
2416
2669
  blendMode={styles.backgroundBlendMode || "normal"}
2417
2670
  onBlendModeChange={(v) => onStyleChange("backgroundBlendMode", v)}
2418
2671
  supportsLayeredFills
@@ -2445,10 +2698,22 @@ function TypographyProperties({
2445
2698
  }) {
2446
2699
  const t = useT();
2447
2700
  const styles = element.computedStyles;
2448
- const fontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
2701
+ const baseFontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
2449
2702
  value: option.value,
2450
2703
  label: t(`editPanel.fontFamilies.${option.key}`),
2451
2704
  }));
2705
+ const fontFamily = resolveFontFamilySelectValue(styles.fontFamily);
2706
+ const fontFamilyOptions = FONT_FAMILY_OPTIONS.some(
2707
+ (option) => option.value === fontFamily,
2708
+ )
2709
+ ? baseFontFamilyOptions
2710
+ : [
2711
+ {
2712
+ value: fontFamily,
2713
+ label: displayFontFamilyName(styles.fontFamily || fontFamily),
2714
+ },
2715
+ ...baseFontFamilyOptions,
2716
+ ];
2452
2717
  const fontWeightOptions = FONT_WEIGHT_OPTIONS.map((option) => ({
2453
2718
  value: option.value,
2454
2719
  label: t(`editPanel.fontWeights.${option.key}`),
@@ -2463,7 +2728,7 @@ function TypographyProperties({
2463
2728
  !styles.width || styles.width === "auto" || styles.width === "max-content";
2464
2729
  const heightIsAuto = !styles.height || styles.height === "auto";
2465
2730
  const noWrap = styles.whiteSpace === "nowrap";
2466
- const resizeMode: "auto-width" | "auto-height" | "fixed" =
2731
+ const resizeMode: TextResizeMode =
2467
2732
  widthIsAuto && noWrap
2468
2733
  ? "auto-width"
2469
2734
  : !heightIsAuto && !widthIsAuto
@@ -2471,7 +2736,7 @@ function TypographyProperties({
2471
2736
  : "auto-height";
2472
2737
  const currentWidth = styles.width && !widthIsAuto ? styles.width : "200px";
2473
2738
  const currentHeight = styles.height && !heightIsAuto ? styles.height : "48px";
2474
- const setResizeMode = (mode: "auto-width" | "auto-height" | "fixed") => {
2739
+ const setResizeMode = (mode: TextResizeMode) => {
2475
2740
  if (mode === "auto-width") {
2476
2741
  onStyleChange("width", "auto");
2477
2742
  onStyleChange("height", "auto");
@@ -2521,51 +2786,36 @@ function TypographyProperties({
2521
2786
 
2522
2787
  return (
2523
2788
  <PanelSection title={t("editPanel.sections.typography")}>
2524
- {/* Row 0: text resizing (auto-width / auto-height / fixed) */}
2525
- <InspectorSegment>
2526
- <InspectorIconButton
2527
- label={"Auto width" /* i18n-ignore design text-resize mode */}
2528
- active={resizeMode === "auto-width"}
2529
- onClick={() => setResizeMode("auto-width")}
2530
- >
2531
- <IconArrowAutofitWidth className="size-3.5" />
2532
- </InspectorIconButton>
2533
- <InspectorIconButton
2534
- label={"Auto height" /* i18n-ignore design text-resize mode */}
2535
- active={resizeMode === "auto-height"}
2536
- onClick={() => setResizeMode("auto-height")}
2537
- >
2538
- <IconArrowAutofitHeight className="size-3.5" />
2539
- </InspectorIconButton>
2540
- <InspectorIconButton
2541
- label={"Fixed size" /* i18n-ignore design text-resize mode */}
2542
- active={resizeMode === "fixed"}
2543
- onClick={() => setResizeMode("fixed")}
2789
+ {/* Row 1: font family full-width.
2790
+ Wrapped in a height-constrained div so the SelectTrigger button's
2791
+ hit-target is exactly h-6 (24 px) and cannot visually or physically
2792
+ overlap the weight/size row below (bug: trigger extended ~12 px into
2793
+ the next row, causing clicks meant for the size input to open this
2794
+ dropdown instead). */}
2795
+ <div className="h-6 overflow-hidden">
2796
+ <Select
2797
+ value={fontFamily}
2798
+ onValueChange={(v) => onStyleChange("fontFamily", v)}
2544
2799
  >
2545
- <IconSquare className="size-3.5" />
2546
- </InspectorIconButton>
2547
- </InspectorSegment>
2548
-
2549
- {/* Row 1: font family full-width */}
2550
- <Select
2551
- value={styles.fontFamily || "sans-serif"}
2552
- onValueChange={(v) => onStyleChange("fontFamily", v)}
2553
- >
2554
- <SelectTrigger className="h-6 w-full rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-[11px] shadow-none focus:ring-1 focus:ring-[var(--design-editor-accent-color)]">
2555
- <SelectValue />
2556
- </SelectTrigger>
2557
- <SelectContent>
2558
- {fontFamilyOptions.map((opt) => (
2559
- <SelectItem
2560
- key={opt.value}
2561
- value={opt.value}
2562
- className="text-[11px]"
2563
- >
2564
- {opt.label}
2565
- </SelectItem>
2566
- ))}
2567
- </SelectContent>
2568
- </Select>
2800
+ <SelectTrigger
2801
+ aria-label={t("editPanel.labels.font")}
2802
+ className="h-6 w-full rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-[11px] shadow-none focus:ring-1 focus:ring-[var(--design-editor-accent-color)]"
2803
+ >
2804
+ <SelectValue />
2805
+ </SelectTrigger>
2806
+ <SelectContent>
2807
+ {fontFamilyOptions.map((opt) => (
2808
+ <SelectItem
2809
+ key={opt.value}
2810
+ value={opt.value}
2811
+ className="text-[11px]"
2812
+ >
2813
+ {opt.label}
2814
+ </SelectItem>
2815
+ ))}
2816
+ </SelectContent>
2817
+ </Select>
2818
+ </div>
2569
2819
 
2570
2820
  {/* Row 2: weight + size side by side */}
2571
2821
  <div className="grid grid-cols-2 gap-1.5">
@@ -2639,7 +2889,7 @@ function TypographyProperties({
2639
2889
  </div>
2640
2890
 
2641
2891
  {/* Row 4: horizontal + vertical text alignment */}
2642
- <div className="grid grid-cols-2 gap-1.5">
2892
+ <div className="flex items-center gap-1.5">
2643
2893
  <InspectorSegment>
2644
2894
  <InspectorIconButton
2645
2895
  label={t("editPanel.textAligns.left")}
@@ -2693,6 +2943,12 @@ function TypographyProperties({
2693
2943
  <IconLayoutAlignBottom className="size-3.5" />
2694
2944
  </InspectorIconButton>
2695
2945
  </InspectorSegment>
2946
+ <div className="ml-auto shrink-0">
2947
+ <TypographyDetailsPopover
2948
+ resizeMode={resizeMode}
2949
+ onResizeModeChange={setResizeMode}
2950
+ />
2951
+ </div>
2696
2952
  </div>
2697
2953
  </PanelSection>
2698
2954
  );
@@ -2758,8 +3014,8 @@ function FlexContainerControls({
2758
3014
  },
2759
3015
  clipContent: styles.overflow === "hidden",
2760
3016
  resolvedSize: {
2761
- horizontal: element.boundingRect.width,
2762
- vertical: element.boundingRect.height,
3017
+ horizontal: cssElementSize(element, "horizontal"),
3018
+ vertical: cssElementSize(element, "vertical"),
2763
3019
  },
2764
3020
  // Forward the raw CSS display so the matrix can render the correct Flow
2765
3021
  // state (normal flow for block/grid, flex for flex/inline-flex). Added as an
@@ -2843,6 +3099,9 @@ function FlexContainerControls({
2843
3099
  onStylesChange,
2844
3100
  );
2845
3101
  }}
3102
+ onChildSizeChange={(axis, px) =>
3103
+ onStyleChange(axis === "horizontal" ? "width" : "height", `${px}px`)
3104
+ }
2846
3105
  onChildMinMaxChange={(axis, kind, val) =>
2847
3106
  commitElementMinMax(axis, kind, val, onStyleChange)
2848
3107
  }
@@ -2986,7 +3245,7 @@ function LayoutContextProperties({
2986
3245
  axis="W"
2987
3246
  sizingAxis="horizontal"
2988
3247
  value={inferElementSizing(element, "horizontal")}
2989
- resolvedSize={element.boundingRect.width}
3248
+ resolvedSize={cssElementSize(element, "horizontal")}
2990
3249
  minMax={readElementMinMax(element, "horizontal")}
2991
3250
  options={availableSizing.horizontal ?? ["fixed"]}
2992
3251
  disabled={false}
@@ -2999,6 +3258,7 @@ function LayoutContextProperties({
2999
3258
  onStylesChange,
3000
3259
  )
3001
3260
  }
3261
+ onSizeChange={(px) => onStyleChange("width", `${px}px`)}
3002
3262
  onMinMaxChange={(axis, kind, val) =>
3003
3263
  commitElementMinMax(axis, kind, val, onStyleChange)
3004
3264
  }
@@ -3007,7 +3267,7 @@ function LayoutContextProperties({
3007
3267
  axis="H"
3008
3268
  sizingAxis="vertical"
3009
3269
  value={inferElementSizing(element, "vertical")}
3010
- resolvedSize={element.boundingRect.height}
3270
+ resolvedSize={cssElementSize(element, "vertical")}
3011
3271
  minMax={readElementMinMax(element, "vertical")}
3012
3272
  options={availableSizing.vertical ?? ["fixed"]}
3013
3273
  disabled={false}
@@ -3020,6 +3280,7 @@ function LayoutContextProperties({
3020
3280
  onStylesChange,
3021
3281
  )
3022
3282
  }
3283
+ onSizeChange={(px) => onStyleChange("height", `${px}px`)}
3023
3284
  onMinMaxChange={(axis, kind, val) =>
3024
3285
  commitElementMinMax(axis, kind, val, onStyleChange)
3025
3286
  }
@@ -3372,6 +3633,8 @@ function PositionLayoutProperties({
3372
3633
  <div className="grid grid-cols-2 gap-2">
3373
3634
  <ScrubStyleInput
3374
3635
  label="X"
3636
+ ariaLabel="X-position"
3637
+ tooltipLabel="X-position"
3375
3638
  value={styles.left || ""}
3376
3639
  placeholder={element.boundingRect.x}
3377
3640
  inputClassName="h-6"
@@ -3379,6 +3642,8 @@ function PositionLayoutProperties({
3379
3642
  />
3380
3643
  <ScrubStyleInput
3381
3644
  label="Y"
3645
+ ariaLabel="Y-position"
3646
+ tooltipLabel="Y-position"
3382
3647
  value={styles.top || ""}
3383
3648
  placeholder={element.boundingRect.y}
3384
3649
  inputClassName="h-6"
@@ -3474,7 +3739,7 @@ function FillProperties({
3474
3739
  Record<string, string>
3475
3740
  >({});
3476
3741
  const stashKey = `${elementIdentityKey(element)}:${fillProperty}`;
3477
- const isHidden = fillValue === "transparent" || fillValue === "";
3742
+ const isHidden = !colorHasVisibleAlpha(fillValue);
3478
3743
  const handleFillVisibilityToggle = () => {
3479
3744
  if (isHidden) {
3480
3745
  // Restore the stashed color, or fall back to a sensible default.
@@ -3629,8 +3894,17 @@ function FillProperties({
3629
3894
  const opacity = gradient
3630
3895
  ? averageGradientOpacity(gradient.stops)
3631
3896
  : 100;
3897
+ const layerStashKey = `${elementIdentityKey(element)}:layer:${index}`;
3898
+ const stashedLayer = hiddenLayerStash[layerStashKey];
3899
+ const hiddenImagePlaceholder = Boolean(
3900
+ stashedLayer && gradient && opacity <= 0,
3901
+ );
3632
3902
  const label = gradient
3633
- ? `${gradientLabel(gradient.type)} ${index + 1}`
3903
+ ? hiddenImagePlaceholder
3904
+ ? `${"Image" /* i18n-ignore design inspector paint row */} ${
3905
+ index + 1
3906
+ }`
3907
+ : `${gradientLabel(gradient.type)} ${index + 1}`
3634
3908
  : `${"Image" /* i18n-ignore design inspector paint row */} ${
3635
3909
  index + 1
3636
3910
  }`;
@@ -3728,10 +4002,6 @@ function FillProperties({
3728
4002
  : t("editPanel.labels.hideLayer")
3729
4003
  }
3730
4004
  onClick={() => {
3731
- if (!gradient) return;
3732
- const layerStashKey = `${elementIdentityKey(
3733
- element,
3734
- )}:layer:${index}`;
3735
4005
  if (opacity <= 0) {
3736
4006
  // Show: restore the exact pre-hide layer if stashed,
3737
4007
  // otherwise fall back to forcing every stop opaque.
@@ -3743,7 +4013,7 @@ function FillProperties({
3743
4013
  delete next[layerStashKey];
3744
4014
  return next;
3745
4015
  });
3746
- } else {
4016
+ } else if (gradient) {
3747
4017
  replaceLayer(
3748
4018
  buildGradientLayer(
3749
4019
  gradient.type,
@@ -3755,6 +4025,29 @@ function FillProperties({
3755
4025
  ),
3756
4026
  );
3757
4027
  }
4028
+ return;
4029
+ }
4030
+ if (!gradient) {
4031
+ setHiddenLayerStash((prev) => ({
4032
+ ...prev,
4033
+ [layerStashKey]: layer,
4034
+ }));
4035
+ replaceLayer(
4036
+ buildGradientLayer("linear", [
4037
+ {
4038
+ id: "stop-0",
4039
+ color: "rgba(0, 0, 0, 0)",
4040
+ position: 0,
4041
+ opacity: 0,
4042
+ },
4043
+ {
4044
+ id: "stop-1",
4045
+ color: "rgba(0, 0, 0, 0)",
4046
+ position: 100,
4047
+ opacity: 0,
4048
+ },
4049
+ ]),
4050
+ );
3758
4051
  } else {
3759
4052
  // Hide: stash the current layer (with its real per-stop
3760
4053
  // opacities) before zeroing every stop's alpha.
@@ -4373,6 +4666,7 @@ export function EditPanel({
4373
4666
  onStylesChange,
4374
4667
  onExport,
4375
4668
  exporting = false,
4669
+ readOnly = false,
4376
4670
  }: EditPanelProps) {
4377
4671
  const t = useT();
4378
4672
  const [exportSettings, setExportSettings] = useState<ExportSettingsValue>(
@@ -4401,6 +4695,50 @@ export function EditPanel({
4401
4695
  setShowExportPreview(false);
4402
4696
  }, [selectedElementKey]);
4403
4697
 
4698
+ // Scroll guard: suppress the click that fires immediately after a scroll
4699
+ // gesture ends (rubber-band or normal scroll). Using onScroll instead of
4700
+ // onPointerDown avoids side-effects like Radix DismissableLayer detecting a
4701
+ // "pointerdown outside" and closing open popovers — which, during an
4702
+ // over-scroll bounce, could briefly un-shield the canvas and allow a stray
4703
+ // pointer event to deselect the selected canvas element (R3 regression).
4704
+ const scrolledRecentlyRef = useRef(false);
4705
+ const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
4706
+
4707
+ if (readOnly) {
4708
+ return (
4709
+ <div
4710
+ className={cn(
4711
+ "shrink-0 bg-[var(--design-editor-panel-bg)]",
4712
+ "flex h-full min-h-0 flex-col overflow-hidden",
4713
+ )}
4714
+ style={{ width }}
4715
+ >
4716
+ <div className="flex min-h-8 shrink-0 items-center border-b border-border/90 px-3">
4717
+ <h2 className="min-w-0 truncate text-[12px] font-semibold text-foreground">
4718
+ {t("editPanel.properties")}
4719
+ </h2>
4720
+ </div>
4721
+ <div className="flex min-h-0 flex-1 items-center justify-center px-5">
4722
+ <div
4723
+ className="max-w-[188px] text-center"
4724
+ aria-live="polite"
4725
+ aria-busy="true"
4726
+ >
4727
+ <div className="mx-auto mb-3 flex size-9 items-center justify-center rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-muted-foreground/60">
4728
+ <IconLock className="size-4" aria-hidden="true" />
4729
+ </div>
4730
+ <h3 className="text-[12px] font-semibold leading-snug text-foreground">
4731
+ {t("designEditor.inspectorLockedTitle")}
4732
+ </h3>
4733
+ <p className="mt-1.5 text-[11px] leading-snug text-muted-foreground/70">
4734
+ {t("designEditor.inspectorLockedDescription")}
4735
+ </p>
4736
+ </div>
4737
+ </div>
4738
+ </div>
4739
+ );
4740
+ }
4741
+
4404
4742
  return (
4405
4743
  <div
4406
4744
  className={cn(
@@ -4419,11 +4757,75 @@ export function EditPanel({
4419
4757
  <>
4420
4758
  <SelectionHeader element={selectedElement} />
4421
4759
 
4422
- <div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain">
4760
+ <div
4761
+ className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain"
4762
+ onScroll={() => {
4763
+ // Mark that a scroll just happened so the click that some
4764
+ // browsers fire at the end of a scroll gesture (or after an
4765
+ // overscroll/rubber-band bounce) is suppressed. Crucially this
4766
+ // runs on the scroll event — NOT on pointerdown — so it never
4767
+ // triggers Radix's DismissableLayer "outside pointerdown"
4768
+ // detection, which would close open inspector popovers and, once
4769
+ // the shield is removed, allow a stray canvas pointer event to
4770
+ // deselect the selected element (the R3 overscroll regression).
4771
+ scrolledRecentlyRef.current = true;
4772
+ if (scrollTimerRef.current !== null) {
4773
+ clearTimeout(scrollTimerRef.current);
4774
+ }
4775
+ scrollTimerRef.current = setTimeout(() => {
4776
+ scrolledRecentlyRef.current = false;
4777
+ scrollTimerRef.current = null;
4778
+ }, 300);
4779
+ }}
4780
+ onClickCapture={(e) => {
4781
+ // Suppress spurious clicks (e.g. color-picker opening) that
4782
+ // fire immediately after a scroll gesture ends. The 300ms
4783
+ // window from the last scroll event covers both the synchronous
4784
+ // scroll-end click and the delayed synthetic click that mobile
4785
+ // browsers generate after a touch-scroll ends.
4786
+ if (!scrolledRecentlyRef.current) return;
4787
+ scrolledRecentlyRef.current = false;
4788
+ if (scrollTimerRef.current !== null) {
4789
+ clearTimeout(scrollTimerRef.current);
4790
+ scrollTimerRef.current = null;
4791
+ }
4792
+ e.stopPropagation();
4793
+ e.preventDefault();
4794
+ }}
4795
+ onKeyDown={(e) => {
4796
+ // Trap Tab within the inspector panel so it never focuses the
4797
+ // canvas iframe. When the canvas iframe gains focus it forwards
4798
+ // a synthetic Tab keydown to the parent window, which is picked
4799
+ // up by the design-editor hotkey handler as "cycle file" and
4800
+ // causes apparent deselection / overview-mode switch (bug: Tab
4801
+ // in a numeric field deselected the canvas element).
4802
+ if (e.key !== "Tab") return;
4803
+ const panel = e.currentTarget;
4804
+ const focusable = Array.from(
4805
+ panel.querySelectorAll<HTMLElement>(
4806
+ 'input, button, select, textarea, [tabindex]:not([tabindex="-1"])',
4807
+ ),
4808
+ ).filter(
4809
+ (el) =>
4810
+ !el.hasAttribute("disabled") &&
4811
+ el.tabIndex !== -1 &&
4812
+ !el.closest('[aria-hidden="true"]'),
4813
+ );
4814
+ if (focusable.length === 0) return;
4815
+ e.preventDefault();
4816
+ const current = document.activeElement as HTMLElement | null;
4817
+ const idx = current ? focusable.indexOf(current) : -1;
4818
+ const next = e.shiftKey
4819
+ ? focusable[(idx - 1 + focusable.length) % focusable.length]
4820
+ : focusable[(idx + 1) % focusable.length];
4821
+ next?.focus();
4822
+ }}
4823
+ >
4423
4824
  {!selectedElement && (
4424
4825
  <PageProperties
4425
4826
  styles={pageStyles}
4426
4827
  onStyleChange={onStyleChange}
4828
+ onStylesChange={onStylesChange}
4427
4829
  />
4428
4830
  )}
4429
4831