@agent-native/core 0.80.7 → 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 (302) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +18 -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/AssistantChat.tsx +70 -27
  18. package/corpus/core/src/client/analytics.ts +3 -1
  19. package/corpus/core/src/client/error-format.ts +25 -0
  20. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +13 -11
  21. package/corpus/core/src/client/guided-questions.tsx +32 -4
  22. package/corpus/core/src/client/onboarding/OnboardingPanel.tsx +33 -1
  23. package/corpus/core/src/client/resources/BuiltinCapabilityDetail.tsx +8 -0
  24. package/corpus/core/src/client/resources/use-builtin-capabilities.ts +4 -2
  25. package/corpus/core/src/client/session-replay.ts +34 -0
  26. package/corpus/core/src/client/sharing/ShareButton.tsx +106 -21
  27. package/corpus/core/src/coding-tools/index.ts +1 -1
  28. package/corpus/core/src/mcp/index.ts +2 -0
  29. package/corpus/core/src/mcp/screen-memory-stdio.ts +290 -0
  30. package/corpus/core/src/mcp-client/builtin-capabilities.ts +13 -1
  31. package/corpus/core/src/mcp-client/index.ts +9 -0
  32. package/corpus/core/src/mcp-client/screen-memory-local.ts +461 -0
  33. package/corpus/core/src/onboarding/types.ts +7 -0
  34. package/corpus/core/src/server/agent-chat-plugin.ts +25 -0
  35. package/corpus/core/src/server/agents-bundle.ts +35 -7
  36. package/corpus/core/src/styles/agent-native.css +5 -0
  37. package/corpus/core/src/templates/default/app/global.css +35 -35
  38. package/corpus/templates/analytics/AGENTS.md +6 -0
  39. package/corpus/templates/analytics/actions/github-repo-files.ts +9 -0
  40. package/corpus/templates/analytics/app/components/layout/Layout.tsx +4 -4
  41. package/corpus/templates/analytics/app/global.css +144 -40
  42. package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +1 -1
  43. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +1 -1
  44. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +1 -1
  45. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +4 -4
  46. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +9 -9
  47. package/corpus/templates/analytics/changelog/2026-06-29-agents-can-search-and-read-connected-github-repositories-whe.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-06-29-sessions-and-explorer-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  49. package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -1
  50. package/corpus/templates/assets/app/components/library/LibraryPresetGrid.tsx +1 -1
  51. package/corpus/templates/assets/app/global.css +99 -35
  52. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  53. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +4 -4
  54. package/corpus/templates/assets/app/routes/library.tsx +4 -4
  55. package/corpus/templates/assets/changelog/2026-06-29-asset-library-and-detail-layouts-adapt-when-the-agent-sidebar.md +6 -0
  56. package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
  57. package/corpus/templates/brain/app/global.css +63 -35
  58. package/corpus/templates/brain/app/routes/search.tsx +1 -1
  59. package/corpus/templates/brain/app/routes/settings.tsx +1 -1
  60. package/corpus/templates/brain/app/routes/sources.tsx +2 -2
  61. package/corpus/templates/brain/changelog/2026-06-29-search-settings-and-source-layouts-adapt-when-the-agent-sidebar.md +6 -0
  62. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +4 -4
  63. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +3 -2
  64. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -2
  65. package/corpus/templates/calendar/app/global.css +69 -35
  66. package/corpus/templates/calendar/changelog/2026-06-29-event-and-find-time-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  67. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  68. package/corpus/templates/chat/app/global.css +35 -35
  69. package/corpus/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  70. package/corpus/templates/clips/AGENTS.md +13 -0
  71. package/corpus/templates/clips/actions/get-screen-memory-status.ts +12 -0
  72. package/corpus/templates/clips/actions/query-screen-memory-context.ts +29 -0
  73. package/corpus/templates/clips/actions/view-screen.ts +2 -2
  74. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  75. package/corpus/templates/clips/app/global.css +47 -35
  76. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +3 -3
  77. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +45 -18
  78. package/corpus/templates/clips/changelog/2026-06-29-meeting-detail-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  79. package/corpus/templates/clips/changelog/2026-06-29-screen-memory-keeps-a-local-rolling-screen-buffer-for-agent-context.md +5 -0
  80. package/corpus/templates/clips/changelog/2026-06-29-video-chat-is-easier-to-find-on-recording-pages-and-s3-setup.md +6 -0
  81. package/corpus/templates/clips/chrome-extension/src/styles.css +39 -39
  82. package/corpus/templates/clips/desktop/src/app.tsx +290 -1
  83. package/corpus/templates/clips/desktop/src/shared/config.ts +83 -0
  84. package/corpus/templates/clips/desktop/src/styles.css +67 -52
  85. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +50 -0
  86. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +17 -0
  87. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +13 -13
  88. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +990 -0
  89. package/corpus/templates/clips/server/plugins/onboarding.ts +2 -0
  90. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +69 -16
  91. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +3 -3
  92. package/corpus/templates/content/app/components/layout/Layout.tsx +1 -1
  93. package/corpus/templates/content/app/global.css +29 -15
  94. package/corpus/templates/content/changelog/2026-06-29-database-gallery-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  95. package/corpus/templates/content/changelog/2026-06-29-read-only-pages-no-longer-try-to-autosave-or-open-editor-onl.md +6 -0
  96. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +25 -20
  97. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -0
  98. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +8 -0
  99. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +13 -0
  100. package/corpus/templates/design/AGENTS.md +24 -14
  101. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1054 -0
  102. package/corpus/templates/design/actions/apply-visual-edit.ts +24 -4
  103. package/corpus/templates/design/actions/edit-design.ts +6 -3
  104. package/corpus/templates/design/actions/export-coding-handoff.ts +12 -4
  105. package/corpus/templates/design/actions/generate-design.ts +6 -21
  106. package/corpus/templates/design/actions/generate-screens.ts +48 -5
  107. package/corpus/templates/design/actions/get-design.ts +1 -0
  108. package/corpus/templates/design/actions/present-design-variants.ts +355 -30
  109. package/corpus/templates/design/actions/update-design.ts +47 -10
  110. package/corpus/templates/design/actions/update-file.ts +33 -8
  111. package/corpus/templates/design/actions/view-screen.ts +89 -31
  112. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +389 -71
  113. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +1 -1
  114. package/corpus/templates/design/app/components/design/EditPanel.tsx +596 -95
  115. package/corpus/templates/design/app/components/design/LayersPanel.tsx +428 -221
  116. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1197 -228
  117. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +94 -217
  118. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +28 -14
  119. package/corpus/templates/design/app/components/design/index.ts +0 -1
  120. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +228 -170
  121. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +140 -13
  122. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +39 -8
  123. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +4 -2
  124. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +75 -5
  125. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +37 -12
  126. package/corpus/templates/design/app/components/design/inspector/index.ts +1 -0
  127. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +17 -4
  128. package/corpus/templates/design/app/components/design/types.ts +9 -0
  129. package/corpus/templates/design/app/components/layout/Layout.tsx +23 -5
  130. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +448 -259
  131. package/corpus/templates/design/app/global.css +77 -49
  132. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  133. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +8 -1
  134. package/corpus/templates/design/app/i18n/zh-TW.ts +4 -0
  135. package/corpus/templates/design/app/i18n-data.ts +55 -3
  136. package/corpus/templates/design/app/pages/DesignEditor.tsx +2783 -658
  137. package/corpus/templates/design/app/root.tsx +1 -1
  138. package/corpus/templates/design/changelog/2026-06-29-agent-edits-to-a-design-are-no-longer-occasionally-dropped-o.md +6 -0
  139. package/corpus/templates/design/changelog/2026-06-29-box-shadow-and-gradient-colors-keep-named-css-colors-like-re.md +6 -0
  140. package/corpus/templates/design/changelog/2026-06-29-containers-no-longer-block-selecting-nested-elements-and-nor.md +6 -0
  141. package/corpus/templates/design/changelog/2026-06-29-cut-cmd-ctrl-x-now-removes-the-selected-element-to-the-clipb.md +6 -0
  142. package/corpus/templates/design/changelog/2026-06-29-deleting-with-multiple-layers-selected-in-the-layers-panel-n.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-generate-and-refine-variants-from-stronger.md +6 -0
  144. package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +6 -0
  145. package/corpus/templates/design/changelog/2026-06-29-design-questions-now-appear-as-a-cleaner-canvas-intake-with-.md +6 -0
  146. package/corpus/templates/design/changelog/2026-06-29-design-variant-directions-now-appear-as-board-screens-with-chat-buttons.md +5 -0
  147. package/corpus/templates/design/changelog/2026-06-29-duplicating-a-screen-now-gives-the-copy-its-own-layer-ids-so.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-29-element-selection-no-longer-snaps-back-to-the-previous-layer.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-29-frame-resize-measurements-now-stay-beside-the-cursor-while-d.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-29-hiding-and-re-showing-a-gradient-fill-now-preserves-each-sto.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-29-inspector-number-fields-have-cleaner-tooltips-and-draggable-.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-29-layer-locking-layer-nesting-generated-question-intake-and-im.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-29-layer-names-in-the-sidebar-can-scroll-horizontally-instead-o.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-29-layer-nesting-in-the-sidebar-now-uses-tighter-spacing-so-nam.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-29-layers-no-longer-repeat-document-wrapper-rows-when-a-screen-.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-29-overview-previews-now-keep-element-hover-and-selection-tied-.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-29-picking-a-design-direction-now-submits-to-the-agent-right-aw.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-29-pressing-escape-now-cancels-a-stuck-screen-drag-in-the-overv.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-29-screen-overview-is-easier-to-find-and-named-screens-open-dir.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-29-screen-overview-only-highlights-screens-when-the-frame-or-ti.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-29-screen-overview-previews-now-resize-to-match-the-selected-de.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-now-uses-a-pointer-cursor-instead-.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-outlines-now-stay-locked-to-the-se.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zoom-now-reports-real-screen-scale-separatel.md +6 -0
  165. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-feels-smoother-and-selection-outline.md +6 -0
  166. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-now-stays-on-the-overview-canvas-ins.md +6 -0
  167. package/corpus/templates/design/changelog/2026-06-29-selecting-a-nested-layer-from-all-screens-now-keeps-you-in-a.md +6 -0
  168. package/corpus/templates/design/changelog/2026-06-29-share-now-groups-link-sharing-exports-and-agent-handoff.md +6 -0
  169. package/corpus/templates/design/changelog/2026-06-29-style-changes-that-set-several-properties-at-once-fixed-size.md +6 -0
  170. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-loading-skeleton-now-uses-a-softer-charcoa.md +6 -0
  171. package/corpus/templates/design/changelog/2026-06-29-the-device-preview-menu-now-sits-next-to-the-preview-button-.md +6 -0
  172. package/corpus/templates/design/changelog/2026-06-29-the-editor-no-longer-crashes-when-a-screen-ends-up-with-dupl.md +6 -0
  173. package/corpus/templates/design/changelog/2026-06-29-the-inspector-stays-read-only-while-an-empty-design-is-still.md +6 -0
  174. package/corpus/templates/design/changelog/2026-06-29-the-missing-design-view-now-matches-the-rest-of-the-editor-c.md +6 -0
  175. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-icon-and-tool-option-menus-better-match-the-edi.md +6 -0
  176. package/corpus/templates/design/changelog/2026-06-29-typography-controls-now-keep-the-font-name-visible-and-move-.md +6 -0
  177. package/corpus/templates/design/e2e/README.md +4 -3
  178. package/corpus/templates/design/e2e/global-setup.ts +24 -23
  179. package/corpus/templates/design/e2e/helpers.ts +122 -11
  180. package/corpus/templates/design/playwright.config.ts +19 -4
  181. package/corpus/templates/design/server/lib/coding-handoff.ts +126 -2
  182. package/corpus/templates/design/server/plugins/core-routes.ts +1 -2
  183. package/corpus/templates/design/server/routes/api/design-handoff/[id].zip.get.ts +86 -0
  184. package/corpus/templates/design/shared/code-layer.ts +125 -22
  185. package/corpus/templates/design/shared/color-utils.ts +159 -0
  186. package/corpus/templates/design/shared/generation-session.ts +42 -0
  187. package/corpus/templates/dispatch/app/global.css +79 -35
  188. package/corpus/templates/dispatch/app/routes/integrations.tsx +8 -7
  189. package/corpus/templates/dispatch/changelog/2026-06-29-integration-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  190. package/corpus/templates/forms/app/components/layout/Layout.tsx +6 -2
  191. package/corpus/templates/forms/app/global.css +64 -35
  192. package/corpus/templates/forms/app/pages/FormsListPage.tsx +2 -2
  193. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +3 -3
  194. package/corpus/templates/forms/changelog/2026-06-29-form-list-and-insight-layouts-adapt-when-the-agent-sidebar.md +6 -0
  195. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +3 -1
  196. package/corpus/templates/macros/app/global.css +25 -9
  197. package/corpus/templates/macros/changelog/2026-06-29-macro-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  198. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -2
  199. package/corpus/templates/mail/app/global.css +42 -36
  200. package/corpus/templates/mail/app/pages/InboxPage.tsx +1 -1
  201. package/corpus/templates/mail/changelog/2026-06-29-the-contact-panel-now-adapts-to-the-available-mail-pane-width.md +6 -0
  202. package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -1
  203. package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +6 -7
  204. package/corpus/templates/plan/app/global.css +74 -42
  205. package/corpus/templates/plan/changelog/2026-06-29-plan-document-blocks-adapt-to-the-available-document-width.md +6 -0
  206. package/corpus/templates/plan/changelog/2026-06-29-plan-titles-and-summaries-can-be-edited-inline-without-focus.md +6 -0
  207. package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -1
  208. package/corpus/templates/slides/app/global.css +49 -33
  209. package/corpus/templates/slides/changelog/2026-06-29-slide-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  210. package/corpus/templates/videos/app/components/layout/Layout.tsx +3 -1
  211. package/corpus/templates/videos/app/global.css +35 -35
  212. package/corpus/templates/videos/changelog/2026-06-29-video-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  213. package/dist/cli/mcp.d.ts.map +1 -1
  214. package/dist/cli/mcp.js +10 -0
  215. package/dist/cli/mcp.js.map +1 -1
  216. package/dist/cli/skills.d.ts.map +1 -1
  217. package/dist/cli/skills.js +58 -12
  218. package/dist/cli/skills.js.map +1 -1
  219. package/dist/client/AssistantChat.d.ts.map +1 -1
  220. package/dist/client/AssistantChat.js +54 -24
  221. package/dist/client/AssistantChat.js.map +1 -1
  222. package/dist/client/analytics.d.ts.map +1 -1
  223. package/dist/client/analytics.js +2 -1
  224. package/dist/client/analytics.js.map +1 -1
  225. package/dist/client/error-format.d.ts.map +1 -1
  226. package/dist/client/error-format.js +17 -0
  227. package/dist/client/error-format.js.map +1 -1
  228. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  229. package/dist/client/extensions/ExtensionViewer.js +3 -4
  230. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  231. package/dist/client/guided-questions.d.ts +4 -0
  232. package/dist/client/guided-questions.d.ts.map +1 -1
  233. package/dist/client/guided-questions.js +16 -7
  234. package/dist/client/guided-questions.js.map +1 -1
  235. package/dist/client/onboarding/OnboardingPanel.js +26 -1
  236. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  237. package/dist/client/resources/BuiltinCapabilityDetail.d.ts.map +1 -1
  238. package/dist/client/resources/BuiltinCapabilityDetail.js +1 -1
  239. package/dist/client/resources/BuiltinCapabilityDetail.js.map +1 -1
  240. package/dist/client/resources/use-builtin-capabilities.d.ts +1 -1
  241. package/dist/client/resources/use-builtin-capabilities.d.ts.map +1 -1
  242. package/dist/client/resources/use-builtin-capabilities.js +2 -1
  243. package/dist/client/resources/use-builtin-capabilities.js.map +1 -1
  244. package/dist/client/session-replay.d.ts.map +1 -1
  245. package/dist/client/session-replay.js +31 -0
  246. package/dist/client/session-replay.js.map +1 -1
  247. package/dist/client/sharing/ShareButton.d.ts +14 -0
  248. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  249. package/dist/client/sharing/ShareButton.js +36 -7
  250. package/dist/client/sharing/ShareButton.js.map +1 -1
  251. package/dist/coding-tools/index.js +1 -1
  252. package/dist/coding-tools/index.js.map +1 -1
  253. package/dist/collab/routes.d.ts +1 -1
  254. package/dist/mcp/index.d.ts +2 -0
  255. package/dist/mcp/index.d.ts.map +1 -1
  256. package/dist/mcp/index.js +1 -0
  257. package/dist/mcp/index.js.map +1 -1
  258. package/dist/mcp/screen-memory-stdio.d.ts +6 -0
  259. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -0
  260. package/dist/mcp/screen-memory-stdio.js +213 -0
  261. package/dist/mcp/screen-memory-stdio.js.map +1 -0
  262. package/dist/mcp-client/builtin-capabilities.d.ts +1 -1
  263. package/dist/mcp-client/builtin-capabilities.d.ts.map +1 -1
  264. package/dist/mcp-client/builtin-capabilities.js +10 -0
  265. package/dist/mcp-client/builtin-capabilities.js.map +1 -1
  266. package/dist/mcp-client/index.d.ts +1 -0
  267. package/dist/mcp-client/index.d.ts.map +1 -1
  268. package/dist/mcp-client/index.js +1 -0
  269. package/dist/mcp-client/index.js.map +1 -1
  270. package/dist/mcp-client/screen-memory-local.d.ts +61 -0
  271. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -0
  272. package/dist/mcp-client/screen-memory-local.js +336 -0
  273. package/dist/mcp-client/screen-memory-local.js.map +1 -0
  274. package/dist/onboarding/types.d.ts +7 -0
  275. package/dist/onboarding/types.d.ts.map +1 -1
  276. package/dist/onboarding/types.js.map +1 -1
  277. package/dist/resources/handlers.d.ts +2 -2
  278. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  279. package/dist/server/agent-chat-plugin.js +26 -0
  280. package/dist/server/agent-chat-plugin.js.map +1 -1
  281. package/dist/server/agents-bundle.d.ts.map +1 -1
  282. package/dist/server/agents-bundle.js +22 -7
  283. package/dist/server/agents-bundle.js.map +1 -1
  284. package/dist/server/transcribe-voice.d.ts +1 -1
  285. package/dist/styles/agent-native.css +5 -0
  286. package/dist/templates/default/app/global.css +35 -35
  287. package/docs/content/locales/ar-SA/template-design.mdx +6 -0
  288. package/docs/content/locales/de-DE/template-design.mdx +8 -0
  289. package/docs/content/locales/es-ES/template-design.mdx +10 -2
  290. package/docs/content/locales/fr-FR/template-design.mdx +10 -2
  291. package/docs/content/locales/hi-IN/template-design.mdx +7 -0
  292. package/docs/content/locales/ja-JP/template-design.mdx +7 -0
  293. package/docs/content/locales/ko-KR/template-design.mdx +7 -0
  294. package/docs/content/locales/pt-BR/template-design.mdx +9 -2
  295. package/docs/content/locales/zh-CN/template-design.mdx +6 -0
  296. package/docs/content/locales/zh-TW/template-design.mdx +6 -0
  297. package/docs/content/template-design.mdx +9 -2
  298. package/package.json +1 -1
  299. package/src/templates/default/app/global.css +35 -35
  300. package/corpus/templates/design/app/components/design/VariantGrid.tsx +0 -189
  301. package/corpus/templates/design/app/components/design/VariantHandoffCard.tsx +0 -115
  302. package/corpus/templates/design/app/hooks/use-variant-flow.ts +0 -258
@@ -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
  /**
@@ -137,10 +148,23 @@ interface EditPanelProps {
137
148
  * valid CSS length. Lets users type "32" and get the expected "32px" when
138
149
  * the field is committed.
139
150
  */
140
- function normalizeLengthValue(raw: string, defaultUnit: string): string {
151
+ function normalizeLengthValue(raw: string, defaultUnit: string): string | null {
141
152
  const trimmed = raw.trim();
142
- if (!trimmed) return "";
153
+ // Empty / invalid input returns null so the caller reverts the field instead
154
+ // of committing an empty or garbage CSS value (e.g. fontSize:"" or
155
+ // flexBasis:"abc") to the element's inline style.
156
+ if (!trimmed) return null;
143
157
  if (/^-?\d+(\.\d+)?$/.test(trimmed)) return `${trimmed}${defaultUnit}`;
158
+ // Validate free-form CSS so junk text never reaches the style. Fall back to
159
+ // accepting the value when CSS.supports is unavailable (SSR/tests) to keep
160
+ // prior behavior in non-DOM environments.
161
+ if (typeof CSS !== "undefined" && typeof CSS.supports === "function") {
162
+ const ok =
163
+ CSS.supports("width", trimmed) ||
164
+ CSS.supports("font-size", trimmed) ||
165
+ CSS.supports("flex-basis", trimmed);
166
+ return ok ? trimmed : null;
167
+ }
144
168
  return trimmed;
145
169
  }
146
170
 
@@ -180,6 +204,11 @@ function PropInput({
180
204
  return;
181
205
  }
182
206
  const next = normalizeLengthValue(draft, defaultUnit);
207
+ if (next === null) {
208
+ // Invalid or empty — revert the field to the last committed value.
209
+ setDraft(value);
210
+ return;
211
+ }
183
212
  if (next !== draft) setDraft(next);
184
213
  if (next !== value) onChange(next);
185
214
  };
@@ -220,6 +249,7 @@ function ColorInput({
220
249
  onChange,
221
250
  backgroundImage,
222
251
  onBackgroundImageChange,
252
+ onImageFillChange,
223
253
  blendMode,
224
254
  onBlendModeChange,
225
255
  supportsLayeredFills = false,
@@ -231,6 +261,7 @@ function ColorInput({
231
261
  onChange: (value: string) => void;
232
262
  backgroundImage?: string;
233
263
  onBackgroundImageChange?: (value: string) => void;
264
+ onImageFillChange?: (value: ImageFillValue) => void;
234
265
  blendMode?: string;
235
266
  onBlendModeChange?: (value: string) => void;
236
267
  supportsLayeredFills?: boolean;
@@ -540,6 +571,7 @@ function ColorInput({
540
571
  onPaintValueChange={
541
572
  supportsLayeredFills ? handlePaintValueChange : undefined
542
573
  }
574
+ onImageFillChange={onImageFillChange}
543
575
  blendMode={blendMode}
544
576
  onBlendModeChange={onBlendModeChange}
545
577
  showBlendMode={supportsLayeredFills}
@@ -745,7 +777,11 @@ function gradientTypeFromCss(
745
777
  layer: string,
746
778
  ): DesignGradientType {
747
779
  if (functionName.toLowerCase() === "conic") return "angular";
748
- if (/closest-corner/i.test(layer)) return "diamond";
780
+ // Recognize both diamond serializations — EditPanel's "closest-corner" and
781
+ // GradientEditor's "ellipse closest-side" — so a diamond authored in either
782
+ // place round-trips as diamond instead of flipping to radial.
783
+ if (/closest-corner/i.test(layer) || /ellipse\s+closest-side/i.test(layer))
784
+ return "diamond";
749
785
  if (functionName.toLowerCase() === "radial") return "radial";
750
786
  return "linear";
751
787
  }
@@ -890,8 +926,6 @@ function PropSlider({
890
926
  );
891
927
  }
892
928
 
893
- const EmptyScrubIcon = () => null;
894
-
895
929
  /**
896
930
  * design-editor inspector section. Matches the design editor "Design" panel chrome:
897
931
  * - NO left collapse chevron (the design editor uses none).
@@ -1109,6 +1143,99 @@ const FONT_WEIGHT_OPTIONS = [
1109
1143
  { value: "900", key: "black" },
1110
1144
  ] as const;
1111
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
+
1112
1239
  const ALIGN_SELF_OPTIONS = [
1113
1240
  { value: "auto", key: "auto" },
1114
1241
  { value: "flex-start", key: "start" },
@@ -1170,9 +1297,39 @@ function resolveLineHeight(
1170
1297
  return Number.isFinite(numeric) && numeric > 0 ? numeric : 1.2;
1171
1298
  }
1172
1299
 
1300
+ // Matches a 2D rotate()/rotateZ() with any CSS angle unit (not rotateX/Y/3d).
1301
+ const ROTATE_FN_PATTERN =
1302
+ /rotate[Zz]?\(\s*([+-]?[\d.]+(?:e[+-]?\d+)?)(deg|rad|turn|grad)?\s*\)/i;
1303
+
1173
1304
  function parseRotationValue(transform: string | undefined): number {
1174
- const match = transform?.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
1175
- return match ? Number(match[1]) : 0;
1305
+ if (!transform || transform === "none") return 0;
1306
+ const match = transform.match(ROTATE_FN_PATTERN);
1307
+ if (match) {
1308
+ const value = Number(match[1]);
1309
+ if (Number.isFinite(value)) {
1310
+ const unit = (match[2] || "deg").toLowerCase();
1311
+ const deg =
1312
+ unit === "rad"
1313
+ ? value * (180 / Math.PI)
1314
+ : unit === "turn"
1315
+ ? value * 360
1316
+ : unit === "grad"
1317
+ ? value * 0.9
1318
+ : value;
1319
+ return Math.round(deg * 10) / 10;
1320
+ }
1321
+ }
1322
+ // Fallback for rotate3d()/matrix()/skew composites: read the 2D rotation
1323
+ // component off the resolved matrix so the panel doesn't report 0.
1324
+ if (typeof DOMMatrixReadOnly !== "undefined") {
1325
+ try {
1326
+ const m = new DOMMatrixReadOnly(transform);
1327
+ return Math.round(((Math.atan2(m.b, m.a) * 180) / Math.PI) * 10) / 10;
1328
+ } catch {
1329
+ // Unparseable transform — fall through to 0.
1330
+ }
1331
+ }
1332
+ return 0;
1176
1333
  }
1177
1334
 
1178
1335
  /**
@@ -1191,8 +1348,10 @@ function parseScaleValue(value: string | undefined): [number, number] {
1191
1348
  function mergeRotationValue(transform: string | undefined, degrees: number) {
1192
1349
  const nextRotate = `rotate(${Math.round(degrees * 10) / 10}deg)`;
1193
1350
  if (!transform || transform === "none") return nextRotate;
1194
- if (/rotate\((-?\d+(?:\.\d+)?)deg\)/.test(transform)) {
1195
- return transform.replace(/rotate\((-?\d+(?:\.\d+)?)deg\)/, nextRotate);
1351
+ // Replace an existing rotate()/rotateZ() in ANY unit so we don't append a
1352
+ // second rotate() (which would compound, e.g. "rotate(0.5turn) rotate(30deg)").
1353
+ if (ROTATE_FN_PATTERN.test(transform)) {
1354
+ return transform.replace(ROTATE_FN_PATTERN, nextRotate);
1196
1355
  }
1197
1356
  return `${transform} ${nextRotate}`;
1198
1357
  }
@@ -1228,6 +1387,8 @@ function ScrubStyleInput({
1228
1387
  step = 1,
1229
1388
  labelClassName,
1230
1389
  inputClassName,
1390
+ ariaLabel,
1391
+ tooltipLabel,
1231
1392
  hideIcon = true,
1232
1393
  }: {
1233
1394
  label: string;
@@ -1241,11 +1402,15 @@ function ScrubStyleInput({
1241
1402
  labelClassName?: string;
1242
1403
  inputClassName?: string;
1243
1404
  hideIcon?: boolean;
1405
+ ariaLabel?: string;
1406
+ tooltipLabel?: string;
1244
1407
  }) {
1245
1408
  return (
1246
1409
  <ScrubInput
1247
1410
  label={label}
1248
- icon={hideIcon ? EmptyScrubIcon : undefined}
1411
+ ariaLabel={ariaLabel}
1412
+ tooltipLabel={tooltipLabel}
1413
+ icon={hideIcon ? null : undefined}
1249
1414
  value={value ? parseNumericValue(value) : (placeholder ?? 0)}
1250
1415
  onChange={onChange}
1251
1416
  unit={unit}
@@ -1596,6 +1761,33 @@ function inferElementSizing(
1596
1761
  return "fixed";
1597
1762
  }
1598
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
+
1599
1791
  function commitElementSizing(
1600
1792
  element: ElementInfo,
1601
1793
  axis: AutoLayoutSizingAxis,
@@ -1605,12 +1797,10 @@ function commitElementSizing(
1605
1797
  ) {
1606
1798
  const isHorizontal = axis === "horizontal";
1607
1799
  const sizeProperty = isHorizontal ? "width" : "height";
1608
- const resolvedSize = Math.max(
1609
- 1,
1610
- Math.round(
1611
- isHorizontal ? element.boundingRect.width : element.boundingRect.height,
1612
- ),
1613
- );
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)));
1614
1804
  const parentDirection = parentFlexDirection(element);
1615
1805
  const isFlex = isParentFlex(element);
1616
1806
  const isGrid = isParentGrid(element);
@@ -1716,7 +1906,7 @@ function InspectorTabsHeader({
1716
1906
  const t = useT();
1717
1907
 
1718
1908
  return (
1719
- <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">
1720
1910
  <Tabs
1721
1911
  value={activeTab}
1722
1912
  onValueChange={(value) => onActiveTabChange(value as InspectorTab)}
@@ -1858,12 +2048,113 @@ function InspectorIconButton({
1858
2048
 
1859
2049
  function InspectorSegment({ children }: { children: ReactNode }) {
1860
2050
  return (
1861
- <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)]">
1862
2052
  {children}
1863
2053
  </div>
1864
2054
  );
1865
2055
  }
1866
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
+
1867
2158
  function CornerRadiusControl({
1868
2159
  styles,
1869
2160
  onStyleChange,
@@ -2025,10 +2316,10 @@ function StrokeLayerControl({
2025
2316
  const nextPrefix = next === "outside" ? "outline" : "border";
2026
2317
  onStyleChange(`${nextPrefix}Color`, color);
2027
2318
  onStyleChange(`${nextPrefix}Width`, width || "1px");
2028
- onStyleChange(
2029
- `${nextPrefix}Style`,
2030
- styleValue === "none" ? "solid" : styleValue,
2031
- );
2319
+ // Preserve the original border-style so a hidden stroke (style:none, kept
2320
+ // visible as a row because width>0) stays hidden when its position moves
2321
+ // between inside/outside. Only default to solid when there's no style at all.
2322
+ onStyleChange(`${nextPrefix}Style`, styleValue || "solid");
2032
2323
  onRemove();
2033
2324
  };
2034
2325
 
@@ -2149,7 +2440,12 @@ function parseShadowLayer(layer: string, index: number): ShadowLayer {
2149
2440
  const tokens = splitCssTokens(layer);
2150
2441
  const inset = tokens.includes("inset");
2151
2442
  const colorToken =
2152
- tokens.find((token) => parseCssColor(token) || token === "transparent") ||
2443
+ tokens.find((token) => parseCssColor(token) || token === "transparent") ??
2444
+ // Preserve a color we don't parse into RGBA (currentColor, var(--x), or any
2445
+ // unrecognized keyword): the color is the non-inset token that doesn't look
2446
+ // like a numeric length. Without this, tweaking x/y/blur would reset it to
2447
+ // the hardcoded default below.
2448
+ tokens.find((token) => token !== "inset" && !/^[-+]?[\d.]/.test(token)) ??
2153
2449
  "rgba(0, 0, 0, 0.25)";
2154
2450
  const numericTokens = tokens
2155
2451
  .filter((token) => token !== "inset" && token !== colorToken)
@@ -2330,20 +2626,29 @@ function ShadowEffectRow({
2330
2626
  function PageProperties({
2331
2627
  styles,
2332
2628
  onStyleChange,
2629
+ onStylesChange,
2333
2630
  }: {
2334
2631
  styles: Record<string, string>;
2335
2632
  onStyleChange: (property: string, value: string) => void;
2633
+ onStylesChange?: (styles: Record<string, string>) => void;
2336
2634
  }) {
2337
2635
  const t = useT();
2338
- const fontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
2636
+ const baseFontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
2339
2637
  value: option.value,
2340
2638
  label: t(`editPanel.fontFamilies.${option.key}`),
2341
2639
  }));
2342
- const fontFamily = FONT_FAMILY_OPTIONS.some(
2343
- (option) => option.value === styles.fontFamily,
2640
+ const fontFamily = resolveFontFamilySelectValue(styles.fontFamily);
2641
+ const fontFamilyOptions = FONT_FAMILY_OPTIONS.some(
2642
+ (option) => option.value === fontFamily,
2344
2643
  )
2345
- ? styles.fontFamily
2346
- : "sans-serif";
2644
+ ? baseFontFamilyOptions
2645
+ : [
2646
+ {
2647
+ value: fontFamily,
2648
+ label: displayFontFamilyName(styles.fontFamily || fontFamily),
2649
+ },
2650
+ ...baseFontFamilyOptions,
2651
+ ];
2347
2652
 
2348
2653
  return (
2349
2654
  <div>
@@ -2354,6 +2659,13 @@ function PageProperties({
2354
2659
  onChange={(v) => onStyleChange("backgroundColor", v)}
2355
2660
  backgroundImage={styles.backgroundImage || ""}
2356
2661
  onBackgroundImageChange={(v) => onStyleChange("backgroundImage", v)}
2662
+ onImageFillChange={(value) =>
2663
+ commitStylePatch(
2664
+ imageFillToBackgroundStyles(value),
2665
+ onStyleChange,
2666
+ onStylesChange,
2667
+ )
2668
+ }
2357
2669
  blendMode={styles.backgroundBlendMode || "normal"}
2358
2670
  onBlendModeChange={(v) => onStyleChange("backgroundBlendMode", v)}
2359
2671
  supportsLayeredFills
@@ -2386,10 +2698,22 @@ function TypographyProperties({
2386
2698
  }) {
2387
2699
  const t = useT();
2388
2700
  const styles = element.computedStyles;
2389
- const fontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
2701
+ const baseFontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
2390
2702
  value: option.value,
2391
2703
  label: t(`editPanel.fontFamilies.${option.key}`),
2392
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
+ ];
2393
2717
  const fontWeightOptions = FONT_WEIGHT_OPTIONS.map((option) => ({
2394
2718
  value: option.value,
2395
2719
  label: t(`editPanel.fontWeights.${option.key}`),
@@ -2404,7 +2728,7 @@ function TypographyProperties({
2404
2728
  !styles.width || styles.width === "auto" || styles.width === "max-content";
2405
2729
  const heightIsAuto = !styles.height || styles.height === "auto";
2406
2730
  const noWrap = styles.whiteSpace === "nowrap";
2407
- const resizeMode: "auto-width" | "auto-height" | "fixed" =
2731
+ const resizeMode: TextResizeMode =
2408
2732
  widthIsAuto && noWrap
2409
2733
  ? "auto-width"
2410
2734
  : !heightIsAuto && !widthIsAuto
@@ -2412,7 +2736,7 @@ function TypographyProperties({
2412
2736
  : "auto-height";
2413
2737
  const currentWidth = styles.width && !widthIsAuto ? styles.width : "200px";
2414
2738
  const currentHeight = styles.height && !heightIsAuto ? styles.height : "48px";
2415
- const setResizeMode = (mode: "auto-width" | "auto-height" | "fixed") => {
2739
+ const setResizeMode = (mode: TextResizeMode) => {
2416
2740
  if (mode === "auto-width") {
2417
2741
  onStyleChange("width", "auto");
2418
2742
  onStyleChange("height", "auto");
@@ -2462,51 +2786,36 @@ function TypographyProperties({
2462
2786
 
2463
2787
  return (
2464
2788
  <PanelSection title={t("editPanel.sections.typography")}>
2465
- {/* Row 0: text resizing (auto-width / auto-height / fixed) */}
2466
- <InspectorSegment>
2467
- <InspectorIconButton
2468
- label={"Auto width" /* i18n-ignore design text-resize mode */}
2469
- active={resizeMode === "auto-width"}
2470
- onClick={() => setResizeMode("auto-width")}
2471
- >
2472
- <IconArrowAutofitWidth className="size-3.5" />
2473
- </InspectorIconButton>
2474
- <InspectorIconButton
2475
- label={"Auto height" /* i18n-ignore design text-resize mode */}
2476
- active={resizeMode === "auto-height"}
2477
- onClick={() => setResizeMode("auto-height")}
2478
- >
2479
- <IconArrowAutofitHeight className="size-3.5" />
2480
- </InspectorIconButton>
2481
- <InspectorIconButton
2482
- label={"Fixed size" /* i18n-ignore design text-resize mode */}
2483
- active={resizeMode === "fixed"}
2484
- 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)}
2485
2799
  >
2486
- <IconSquare className="size-3.5" />
2487
- </InspectorIconButton>
2488
- </InspectorSegment>
2489
-
2490
- {/* Row 1: font family full-width */}
2491
- <Select
2492
- value={styles.fontFamily || "sans-serif"}
2493
- onValueChange={(v) => onStyleChange("fontFamily", v)}
2494
- >
2495
- <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)]">
2496
- <SelectValue />
2497
- </SelectTrigger>
2498
- <SelectContent>
2499
- {fontFamilyOptions.map((opt) => (
2500
- <SelectItem
2501
- key={opt.value}
2502
- value={opt.value}
2503
- className="text-[11px]"
2504
- >
2505
- {opt.label}
2506
- </SelectItem>
2507
- ))}
2508
- </SelectContent>
2509
- </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>
2510
2819
 
2511
2820
  {/* Row 2: weight + size side by side */}
2512
2821
  <div className="grid grid-cols-2 gap-1.5">
@@ -2580,7 +2889,7 @@ function TypographyProperties({
2580
2889
  </div>
2581
2890
 
2582
2891
  {/* Row 4: horizontal + vertical text alignment */}
2583
- <div className="grid grid-cols-2 gap-1.5">
2892
+ <div className="flex items-center gap-1.5">
2584
2893
  <InspectorSegment>
2585
2894
  <InspectorIconButton
2586
2895
  label={t("editPanel.textAligns.left")}
@@ -2634,6 +2943,12 @@ function TypographyProperties({
2634
2943
  <IconLayoutAlignBottom className="size-3.5" />
2635
2944
  </InspectorIconButton>
2636
2945
  </InspectorSegment>
2946
+ <div className="ml-auto shrink-0">
2947
+ <TypographyDetailsPopover
2948
+ resizeMode={resizeMode}
2949
+ onResizeModeChange={setResizeMode}
2950
+ />
2951
+ </div>
2637
2952
  </div>
2638
2953
  </PanelSection>
2639
2954
  );
@@ -2699,8 +3014,8 @@ function FlexContainerControls({
2699
3014
  },
2700
3015
  clipContent: styles.overflow === "hidden",
2701
3016
  resolvedSize: {
2702
- horizontal: element.boundingRect.width,
2703
- vertical: element.boundingRect.height,
3017
+ horizontal: cssElementSize(element, "horizontal"),
3018
+ vertical: cssElementSize(element, "vertical"),
2704
3019
  },
2705
3020
  // Forward the raw CSS display so the matrix can render the correct Flow
2706
3021
  // state (normal flow for block/grid, flex for flex/inline-flex). Added as an
@@ -2784,6 +3099,9 @@ function FlexContainerControls({
2784
3099
  onStylesChange,
2785
3100
  );
2786
3101
  }}
3102
+ onChildSizeChange={(axis, px) =>
3103
+ onStyleChange(axis === "horizontal" ? "width" : "height", `${px}px`)
3104
+ }
2787
3105
  onChildMinMaxChange={(axis, kind, val) =>
2788
3106
  commitElementMinMax(axis, kind, val, onStyleChange)
2789
3107
  }
@@ -2927,7 +3245,7 @@ function LayoutContextProperties({
2927
3245
  axis="W"
2928
3246
  sizingAxis="horizontal"
2929
3247
  value={inferElementSizing(element, "horizontal")}
2930
- resolvedSize={element.boundingRect.width}
3248
+ resolvedSize={cssElementSize(element, "horizontal")}
2931
3249
  minMax={readElementMinMax(element, "horizontal")}
2932
3250
  options={availableSizing.horizontal ?? ["fixed"]}
2933
3251
  disabled={false}
@@ -2940,6 +3258,7 @@ function LayoutContextProperties({
2940
3258
  onStylesChange,
2941
3259
  )
2942
3260
  }
3261
+ onSizeChange={(px) => onStyleChange("width", `${px}px`)}
2943
3262
  onMinMaxChange={(axis, kind, val) =>
2944
3263
  commitElementMinMax(axis, kind, val, onStyleChange)
2945
3264
  }
@@ -2948,7 +3267,7 @@ function LayoutContextProperties({
2948
3267
  axis="H"
2949
3268
  sizingAxis="vertical"
2950
3269
  value={inferElementSizing(element, "vertical")}
2951
- resolvedSize={element.boundingRect.height}
3270
+ resolvedSize={cssElementSize(element, "vertical")}
2952
3271
  minMax={readElementMinMax(element, "vertical")}
2953
3272
  options={availableSizing.vertical ?? ["fixed"]}
2954
3273
  disabled={false}
@@ -2961,6 +3280,7 @@ function LayoutContextProperties({
2961
3280
  onStylesChange,
2962
3281
  )
2963
3282
  }
3283
+ onSizeChange={(px) => onStyleChange("height", `${px}px`)}
2964
3284
  onMinMaxChange={(axis, kind, val) =>
2965
3285
  commitElementMinMax(axis, kind, val, onStyleChange)
2966
3286
  }
@@ -3313,6 +3633,8 @@ function PositionLayoutProperties({
3313
3633
  <div className="grid grid-cols-2 gap-2">
3314
3634
  <ScrubStyleInput
3315
3635
  label="X"
3636
+ ariaLabel="X-position"
3637
+ tooltipLabel="X-position"
3316
3638
  value={styles.left || ""}
3317
3639
  placeholder={element.boundingRect.x}
3318
3640
  inputClassName="h-6"
@@ -3320,6 +3642,8 @@ function PositionLayoutProperties({
3320
3642
  />
3321
3643
  <ScrubStyleInput
3322
3644
  label="Y"
3645
+ ariaLabel="Y-position"
3646
+ tooltipLabel="Y-position"
3323
3647
  value={styles.top || ""}
3324
3648
  placeholder={element.boundingRect.y}
3325
3649
  inputClassName="h-6"
@@ -3408,8 +3732,14 @@ function FillProperties({
3408
3732
  const [hiddenFillStash, setHiddenFillStash] = useState<
3409
3733
  Record<string, string>
3410
3734
  >({});
3735
+ // Same non-destructive idea for background gradient/image layers: stash the
3736
+ // exact layer string on hide so per-stop opacity survives a hide→show toggle
3737
+ // instead of being flattened to all-0 then all-100.
3738
+ const [hiddenLayerStash, setHiddenLayerStash] = useState<
3739
+ Record<string, string>
3740
+ >({});
3411
3741
  const stashKey = `${elementIdentityKey(element)}:${fillProperty}`;
3412
- const isHidden = fillValue === "transparent" || fillValue === "";
3742
+ const isHidden = !colorHasVisibleAlpha(fillValue);
3413
3743
  const handleFillVisibilityToggle = () => {
3414
3744
  if (isHidden) {
3415
3745
  // Restore the stashed color, or fall back to a sensible default.
@@ -3564,8 +3894,17 @@ function FillProperties({
3564
3894
  const opacity = gradient
3565
3895
  ? averageGradientOpacity(gradient.stops)
3566
3896
  : 100;
3897
+ const layerStashKey = `${elementIdentityKey(element)}:layer:${index}`;
3898
+ const stashedLayer = hiddenLayerStash[layerStashKey];
3899
+ const hiddenImagePlaceholder = Boolean(
3900
+ stashedLayer && gradient && opacity <= 0,
3901
+ );
3567
3902
  const label = gradient
3568
- ? `${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}`
3569
3908
  : `${"Image" /* i18n-ignore design inspector paint row */} ${
3570
3909
  index + 1
3571
3910
  }`;
@@ -3663,17 +4002,70 @@ function FillProperties({
3663
4002
  : t("editPanel.labels.hideLayer")
3664
4003
  }
3665
4004
  onClick={() => {
3666
- if (!gradient) return;
3667
- replaceLayer(
3668
- buildGradientLayer(
3669
- gradient.type,
3670
- gradient.stops.map((stop) => ({
3671
- ...stop,
3672
- opacity: opacity <= 0 ? 100 : 0,
3673
- })),
3674
- gradient.prefix,
3675
- ),
3676
- );
4005
+ if (opacity <= 0) {
4006
+ // Show: restore the exact pre-hide layer if stashed,
4007
+ // otherwise fall back to forcing every stop opaque.
4008
+ const stashed = hiddenLayerStash[layerStashKey];
4009
+ if (stashed !== undefined) {
4010
+ replaceLayer(stashed);
4011
+ setHiddenLayerStash((prev) => {
4012
+ const next = { ...prev };
4013
+ delete next[layerStashKey];
4014
+ return next;
4015
+ });
4016
+ } else if (gradient) {
4017
+ replaceLayer(
4018
+ buildGradientLayer(
4019
+ gradient.type,
4020
+ gradient.stops.map((stop) => ({
4021
+ ...stop,
4022
+ opacity: 100,
4023
+ })),
4024
+ gradient.prefix,
4025
+ ),
4026
+ );
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
+ );
4051
+ } else {
4052
+ // Hide: stash the current layer (with its real per-stop
4053
+ // opacities) before zeroing every stop's alpha.
4054
+ setHiddenLayerStash((prev) => ({
4055
+ ...prev,
4056
+ [layerStashKey]: layer,
4057
+ }));
4058
+ replaceLayer(
4059
+ buildGradientLayer(
4060
+ gradient.type,
4061
+ gradient.stops.map((stop) => ({
4062
+ ...stop,
4063
+ opacity: 0,
4064
+ })),
4065
+ gradient.prefix,
4066
+ ),
4067
+ );
4068
+ }
3677
4069
  }}
3678
4070
  >
3679
4071
  {opacity <= 0 ? (
@@ -4274,6 +4666,7 @@ export function EditPanel({
4274
4666
  onStylesChange,
4275
4667
  onExport,
4276
4668
  exporting = false,
4669
+ readOnly = false,
4277
4670
  }: EditPanelProps) {
4278
4671
  const t = useT();
4279
4672
  const [exportSettings, setExportSettings] = useState<ExportSettingsValue>(
@@ -4302,6 +4695,50 @@ export function EditPanel({
4302
4695
  setShowExportPreview(false);
4303
4696
  }, [selectedElementKey]);
4304
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
+
4305
4742
  return (
4306
4743
  <div
4307
4744
  className={cn(
@@ -4320,11 +4757,75 @@ export function EditPanel({
4320
4757
  <>
4321
4758
  <SelectionHeader element={selectedElement} />
4322
4759
 
4323
- <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
+ >
4324
4824
  {!selectedElement && (
4325
4825
  <PageProperties
4326
4826
  styles={pageStyles}
4327
4827
  onStyleChange={onStyleChange}
4828
+ onStylesChange={onStylesChange}
4328
4829
  />
4329
4830
  )}
4330
4831