@agent-native/core 0.90.1 → 0.90.2

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 (295) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AgentPanel.tsx +1 -1
  5. package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +11 -6
  6. package/corpus/core/src/client/use-action.ts +39 -2
  7. package/corpus/core/src/client/use-db-sync.ts +44 -18
  8. package/corpus/core/src/db/create-get-db.ts +32 -2
  9. package/corpus/core/src/server/framework-request-handler.ts +52 -4
  10. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +5 -0
  11. package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +34 -6
  12. package/corpus/templates/assets/AGENTS.md +23 -4
  13. package/corpus/templates/assets/actions/_generation-preset-settings.ts +48 -0
  14. package/corpus/templates/assets/actions/_helpers.ts +8 -1
  15. package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +64 -0
  16. package/corpus/templates/assets/actions/create-generation-preset.ts +11 -3
  17. package/corpus/templates/assets/actions/generate-image-batch.ts +7 -2
  18. package/corpus/templates/assets/actions/generate-image.ts +342 -25
  19. package/corpus/templates/assets/actions/get-library.ts +4 -3
  20. package/corpus/templates/assets/actions/navigate.ts +2 -1
  21. package/corpus/templates/assets/actions/refresh-generation-run.ts +5 -1
  22. package/corpus/templates/assets/actions/update-asset.ts +1 -0
  23. package/corpus/templates/assets/actions/update-generation-preset.ts +8 -1
  24. package/corpus/templates/assets/actions/view-screen.ts +8 -1
  25. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +9 -1
  26. package/corpus/templates/assets/app/hooks/use-image-model-menu.ts +1 -0
  27. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +11 -0
  28. package/corpus/templates/assets/app/i18n/zh-TW.ts +64 -0
  29. package/corpus/templates/assets/app/i18n-data.ts +650 -0
  30. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -1
  31. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +44 -33
  32. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1474 -0
  33. package/corpus/templates/assets/app/routes/library.tsx +43 -1
  34. package/corpus/templates/assets/server/lib/generation.ts +366 -44
  35. package/corpus/templates/assets/server/lib/image-processing.ts +252 -0
  36. package/corpus/templates/assets/server/lib/preset-skeleton.ts +137 -0
  37. package/corpus/templates/assets/shared/api.ts +47 -0
  38. package/corpus/templates/clips/app/components/library/library-layout.tsx +7 -2
  39. package/corpus/templates/clips/app/components/library/sort-menu.tsx +1 -1
  40. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +114 -33
  41. package/corpus/templates/clips/app/routes/_app.library._index.tsx +27 -1
  42. package/corpus/templates/clips/app/routes/record.tsx +157 -68
  43. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +5 -5
  44. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +20 -3
  45. package/corpus/templates/clips/shared/finalize-recovery.ts +23 -3
  46. package/corpus/templates/content/AGENTS.md +52 -52
  47. package/corpus/templates/content/actions/_builder-cms-read-client.ts +188 -8
  48. package/corpus/templates/content/actions/_builder-docs-client.ts +41 -3
  49. package/corpus/templates/content/actions/_database-source-utils.ts +998 -160
  50. package/corpus/templates/content/actions/_database-utils.ts +44 -4
  51. package/corpus/templates/content/actions/_notion-action-utils.ts +6 -2
  52. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -1
  53. package/corpus/templates/content/actions/attach-content-database-source.ts +85 -12
  54. package/corpus/templates/content/actions/change-content-database-source-role.ts +9 -1
  55. package/corpus/templates/content/actions/delete-content-database.ts +1 -0
  56. package/corpus/templates/content/actions/get-document.ts +11 -2
  57. package/corpus/templates/content/actions/list-content-databases.ts +57 -9
  58. package/corpus/templates/content/actions/prepare-builder-source-review.ts +23 -6
  59. package/corpus/templates/content/actions/refresh-content-database-source.ts +26 -3
  60. package/corpus/templates/content/actions/restore-content-database.ts +1 -0
  61. package/corpus/templates/content/actions/update-document.ts +43 -0
  62. package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +21 -0
  63. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +0 -1
  64. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +70 -28
  65. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +69 -17
  66. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +4 -4
  67. package/corpus/templates/content/app/components/editor/body-hydration.ts +112 -0
  68. package/corpus/templates/content/app/components/editor/builder-body-hydration-pump.ts +52 -0
  69. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +9076 -2876
  70. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +41 -1
  71. package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +36 -11
  72. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +21 -1
  73. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +11 -1
  74. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +11 -8
  75. package/corpus/templates/content/app/global.css +41 -0
  76. package/corpus/templates/content/app/hooks/use-content-database.ts +346 -42
  77. package/corpus/templates/content/app/hooks/use-db-sync.ts +9 -0
  78. package/corpus/templates/content/app/hooks/use-documents.ts +47 -0
  79. package/corpus/templates/content/app/hooks/use-notion.ts +12 -10
  80. package/corpus/templates/content/app/i18n/zh-TW.ts +35 -0
  81. package/corpus/templates/content/app/i18n-data.ts +355 -0
  82. package/corpus/templates/content/app/root.tsx +22 -0
  83. package/corpus/templates/content/changelog/2026-07-01-builder-database-sources-keep-loading-rows-in-the-background.md +6 -0
  84. package/corpus/templates/content/changelog/2026-07-01-builder-source-attach-starts-with-usable-rows.md +6 -0
  85. package/corpus/templates/content/changelog/2026-07-01-builder-source-attachment-now-stays-connected-after-large.md +6 -0
  86. package/corpus/templates/content/changelog/2026-07-01-builder-source-refreshes-now-show-progress-for-large-docs-blog.md +6 -0
  87. package/corpus/templates/content/changelog/2026-07-01-database-pages-now-delete-to-trash-without-route-errors.md +6 -0
  88. package/corpus/templates/content/changelog/2026-07-02-builder-source-refreshes-recover-from-stalled-background-row.md +6 -0
  89. package/corpus/templates/content/changelog/2026-07-02-builder-sourced-rows-open-with-sync-protection.md +6 -0
  90. package/corpus/templates/content/changelog/2026-07-02-database-rows-and-pages-open-faster-from-recently-loaded-dat.md +6 -0
  91. package/corpus/templates/content/changelog/2026-07-02-preview-panels-close-reliably-narrow-sidebar-row-actions-sta.md +6 -0
  92. package/corpus/templates/content/changelog/2026-07-06-builder-sync-progress-no-longer-shows-continue.md +6 -0
  93. package/corpus/templates/content/shared/api.ts +13 -0
  94. package/corpus/templates/content/shared/builder-mdx.ts +16 -3
  95. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +23 -13
  96. package/corpus/templates/design/.agents/skills/responsive-breakpoints/SKILL.md +98 -0
  97. package/corpus/templates/design/.agents/skills/shader-fills/SKILL.md +162 -0
  98. package/corpus/templates/design/AGENTS.md +9 -2
  99. package/corpus/templates/design/actions/add-breakpoint.ts +6 -3
  100. package/corpus/templates/design/actions/apply-a11y-fix.ts +57 -37
  101. package/corpus/templates/design/actions/apply-component-prop-edit.ts +41 -20
  102. package/corpus/templates/design/actions/apply-motion-edit.ts +251 -38
  103. package/corpus/templates/design/actions/apply-shader-fill.ts +90 -66
  104. package/corpus/templates/design/actions/apply-visual-edit.ts +282 -55
  105. package/corpus/templates/design/actions/create-component.ts +29 -60
  106. package/corpus/templates/design/actions/create-file.ts +9 -2
  107. package/corpus/templates/design/actions/duplicate-design.ts +31 -2
  108. package/corpus/templates/design/actions/edit-design.ts +36 -38
  109. package/corpus/templates/design/actions/generate-design-title.ts +87 -0
  110. package/corpus/templates/design/actions/generate-design.ts +53 -18
  111. package/corpus/templates/design/actions/generate-screens.ts +108 -4
  112. package/corpus/templates/design/actions/get-motion-timeline.ts +35 -7
  113. package/corpus/templates/design/actions/insert-asset.ts +34 -29
  114. package/corpus/templates/design/actions/insert-design-native-asset.ts +129 -32
  115. package/corpus/templates/design/actions/insert-figma-library-asset.ts +35 -30
  116. package/corpus/templates/design/actions/present-design-variants.ts +7 -2
  117. package/corpus/templates/design/actions/remove-motion-timeline.ts +31 -36
  118. package/corpus/templates/design/actions/update-file.ts +190 -75
  119. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +403 -0
  120. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +469 -222
  121. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +708 -53
  122. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +83 -3
  123. package/corpus/templates/design/app/components/design/EditPanel.tsx +1091 -6948
  124. package/corpus/templates/design/app/components/design/LayersPanel.tsx +419 -85
  125. package/corpus/templates/design/app/components/design/MotionDock.tsx +1124 -103
  126. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2610 -1398
  127. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +22 -0
  128. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +2182 -235
  129. package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +187 -11
  130. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +376 -1
  131. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +746 -0
  132. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +17 -0
  133. package/corpus/templates/design/app/components/design/code-workbench/SideBar.tsx +1 -1
  134. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +1 -3
  135. package/corpus/templates/design/app/components/design/code-workbench/quickinput/QuickInput.tsx +1 -1
  136. package/corpus/templates/design/app/components/design/edit-panel/appearance-properties.tsx +518 -0
  137. package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +562 -0
  138. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +763 -0
  139. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +94 -0
  140. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +780 -0
  141. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +446 -0
  142. package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +79 -0
  143. package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +315 -0
  144. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +344 -0
  145. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +594 -0
  146. package/corpus/templates/design/app/components/design/edit-panel/frame-presets-panel.tsx +119 -0
  147. package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +248 -0
  148. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +45 -0
  149. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +693 -0
  150. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +724 -0
  151. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +207 -0
  152. package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +628 -0
  153. package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +91 -0
  154. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +649 -0
  155. package/corpus/templates/design/app/components/design/edit-panel/style-change-types.ts +118 -0
  156. package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +88 -0
  157. package/corpus/templates/design/app/components/design/edit-panel/transform-helpers.ts +100 -0
  158. package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +117 -0
  159. package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +501 -0
  160. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +91 -40
  161. package/corpus/templates/design/app/components/design/inspector/BreakpointOverrideIndicator.tsx +82 -0
  162. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +189 -87
  163. package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +1097 -0
  164. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +44 -0
  165. package/corpus/templates/design/app/components/design/inspector/InteractionStatePanel.tsx +258 -0
  166. package/corpus/templates/design/app/components/design/inspector/MotionKeyframeDiamond.tsx +144 -0
  167. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +99 -9
  168. package/corpus/templates/design/app/components/design/inspector/frame-size-presets.ts +116 -0
  169. package/corpus/templates/design/app/components/design/inspector/index.ts +26 -0
  170. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +29 -0
  171. package/corpus/templates/design/app/components/design/multi-screen/alt-hover-measurement.ts +171 -0
  172. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +220 -0
  173. package/corpus/templates/design/app/components/design/multi-screen/chrome-transitions.ts +35 -0
  174. package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +134 -0
  175. package/corpus/templates/design/app/components/design/multi-screen/culling.ts +185 -0
  176. package/corpus/templates/design/app/components/design/multi-screen/gradient-overlay-geometry.ts +120 -0
  177. package/corpus/templates/design/app/components/design/multi-screen/types.ts +892 -0
  178. package/corpus/templates/design/app/components/design/multi-screen/vector-edit-geometry.ts +21 -0
  179. package/corpus/templates/design/app/components/design/types.ts +15 -0
  180. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +109 -5
  181. package/corpus/templates/design/app/global.css +8 -8
  182. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +259 -2
  183. package/corpus/templates/design/app/i18n/zh-TW.ts +163 -9
  184. package/corpus/templates/design/app/i18n-breakpoints.ts +205 -0
  185. package/corpus/templates/design/app/i18n-data.ts +1664 -152
  186. package/corpus/templates/design/app/i18n-motion.ts +563 -0
  187. package/corpus/templates/design/app/lib/design-import.ts +73 -0
  188. package/corpus/templates/design/app/pages/Index.tsx +41 -22
  189. package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +1 -1
  190. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +1 -1
  191. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +1 -1
  192. package/corpus/templates/design/changelog/2026-06-30-multi-select-outlines-now-stay-consistent-across-all-screens.md +1 -1
  193. package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +1 -1
  194. package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +1 -1
  195. package/corpus/templates/design/changelog/2026-07-05-designs-get-ai-generated-names-from-your-prompt.md +6 -0
  196. package/corpus/templates/design/changelog/2026-07-05-local-code-screens-keep-their-connection-when-duplicated-and.md +6 -0
  197. package/corpus/templates/design/changelog/2026-07-05-press-a-to-draw-screens-on-the-canvas-inside-a-screen-it-dr.md +6 -0
  198. package/corpus/templates/design/changelog/2026-07-06-3d-rotation-and-perspective-controls-on-every-element.md +6 -0
  199. package/corpus/templates/design/changelog/2026-07-06-ai-generated-and-duplicated-screens-now-get-their-editor-sel.md +6 -0
  200. package/corpus/templates/design/changelog/2026-07-06-applying-shaders-and-fills-in-quick-succession-can-no-longer.md +6 -0
  201. package/corpus/templates/design/changelog/2026-07-06-breakpoint-frames-in-the-overview-now-support-click-to-selec.md +6 -0
  202. package/corpus/templates/design/changelog/2026-07-06-breakpoint-segments-in-the-inspector-now-show-a-device-icon-.md +6 -0
  203. package/corpus/templates/design/changelog/2026-07-06-breakpoint-targeting-now-lives-in-one-compact-control-in-the.md +6 -0
  204. package/corpus/templates/design/changelog/2026-07-06-canvas-edits-can-no-longer-be-overwritten-by-stale-collabora.md +6 -0
  205. package/corpus/templates/design/changelog/2026-07-06-canvas-edits-inside-screens-save-reliably-again.md +6 -0
  206. package/corpus/templates/design/changelog/2026-07-06-cleaner-inspector-removed-the-ask-ai-box-from-the-design-tab.md +6 -0
  207. package/corpus/templates/design/changelog/2026-07-06-clicking-empty-canvas-space-now-deselects-an-element-inside-.md +6 -0
  208. package/corpus/templates/design/changelog/2026-07-06-dragging-an-element-onto-a-rectangle-now-nests-it-inside-wit.md +6 -0
  209. package/corpus/templates/design/changelog/2026-07-06-dragging-screens-and-objects-on-the-canvas-is-now-much-smoot.md +6 -0
  210. package/corpus/templates/design/changelog/2026-07-06-drawing-a-shape-or-text-on-the-canvas-no-longer-flashes-away.md +6 -0
  211. package/corpus/templates/design/changelog/2026-07-06-dropping-an-element-between-two-others-inside-a-screen-now-s.md +6 -0
  212. package/corpus/templates/design/changelog/2026-07-06-duplicating-a-screen-now-places-the-copy-exactly-where-you-d.md +6 -0
  213. package/corpus/templates/design/changelog/2026-07-06-edit-gradients-directly-on-elements-inside-screens-scale-str.md +6 -0
  214. package/corpus/templates/design/changelog/2026-07-06-edits-from-collaborators-and-the-agent-now-update-the-canvas.md +6 -0
  215. package/corpus/templates/design/changelog/2026-07-06-fixed-a-bug-where-most-style-position-and-text-edits-briefly.md +6 -0
  216. package/corpus/templates/design/changelog/2026-07-06-fixed-a-confusing-raw-error-message-when-a-file-upload-fails.md +6 -0
  217. package/corpus/templates/design/changelog/2026-07-06-fixed-a-rare-case-where-an-edit-could-be-silently-overwritte.md +6 -0
  218. package/corpus/templates/design/changelog/2026-07-06-fixed-inserted-assets-sometimes-disappearing-reappearing-or-.md +6 -0
  219. package/corpus/templates/design/changelog/2026-07-06-fixed-inspector-value-scrubbing-producing-erratic-numbers-an.md +6 -0
  220. package/corpus/templates/design/changelog/2026-07-06-fixed-padding-gap-and-min-max-size-scrubbing-not-updating-th.md +6 -0
  221. package/corpus/templates/design/changelog/2026-07-06-fixed-text-stroke-color-turning-black-and-glyphs-disappearin.md +6 -0
  222. package/corpus/templates/design/changelog/2026-07-06-fixed-the-collapsed-effects-and-stroke-inspector-sections-sh.md +6 -0
  223. package/corpus/templates/design/changelog/2026-07-06-fixed-the-new-design-dialog-closing-unexpectedly-when-using-.md +6 -0
  224. package/corpus/templates/design/changelog/2026-07-06-fixed-the-typography-section-missing-when-selecting-text-nes.md +6 -0
  225. package/corpus/templates/design/changelog/2026-07-06-full-view-no-longer-flashes-back-to-the-overview-on-entry-no.md +6 -0
  226. package/corpus/templates/design/changelog/2026-07-06-keyframe-animation-timeline-per-property-keyframes-easing-cu.md +6 -0
  227. package/corpus/templates/design/changelog/2026-07-06-moving-restyling-and-scrubbing-values-on-elements-in-ai-gene.md +6 -0
  228. package/corpus/templates/design/changelog/2026-07-06-padding-and-corner-radius-value-scrubbing-now-snaps-to-whole.md +6 -0
  229. package/corpus/templates/design/changelog/2026-07-06-padding-handles-now-only-resize-on-the-handle-line-itself-sh.md +6 -0
  230. package/corpus/templates/design/changelog/2026-07-06-preview-and-edit-hover-focus-and-pressed-states-with-real-cs.md +6 -0
  231. package/corpus/templates/design/changelog/2026-07-06-promoting-an-element-to-a-component-now-works-correctly-for-.md +6 -0
  232. package/corpus/templates/design/changelog/2026-07-06-reduced-url-history-churn-and-selection-overlay-flicker-whil.md +6 -0
  233. package/corpus/templates/design/changelog/2026-07-06-removed-the-decorative-dot-before-screen-names-in-the-overvi.md +6 -0
  234. package/corpus/templates/design/changelog/2026-07-06-responsive-breakpoints-edit-at-any-width-with-cascading-over.md +6 -0
  235. package/corpus/templates/design/changelog/2026-07-06-screen-labels-no-longer-turn-blue-when-you-hover-inside-the-.md +6 -0
  236. package/corpus/templates/design/changelog/2026-07-06-scrolling-a-screen-with-an-element-selected-stays-smooth-on-.md +6 -0
  237. package/corpus/templates/design/changelog/2026-07-06-selecting-multiple-elements-and-dragging-now-moves-the-whole.md +6 -0
  238. package/corpus/templates/design/changelog/2026-07-06-selection-borders-handles-and-value-readouts-now-stay-the-sa.md +6 -0
  239. package/corpus/templates/design/changelog/2026-07-06-selection-handles-and-editing-chrome-now-stay-a-constant-siz.md +6 -0
  240. package/corpus/templates/design/changelog/2026-07-06-shader-fills-and-effects-ai-authored-or-preset-glsl-shaders-.md +6 -0
  241. package/corpus/templates/design/changelog/2026-07-06-text-drawn-on-the-canvas-now-defaults-to-white-and-inter-in-.md +6 -0
  242. package/corpus/templates/design/changelog/2026-07-06-text-dropped-into-a-shape-now-stays-readable-instead-of-disa.md +6 -0
  243. package/corpus/templates/design/changelog/2026-07-06-the-add-tweak-controls-prompt-no-longer-jumps-to-the-corner-.md +6 -0
  244. package/corpus/templates/design/changelog/2026-07-06-the-padding-value-readout-now-stays-visible-while-you-hover-.md +6 -0
  245. package/corpus/templates/design/changelog/2026-07-06-the-selected-element-attachment-now-clears-from-the-chat-com.md +6 -0
  246. package/corpus/templates/design/changelog/2026-07-06-the-zoom-menu-is-more-compact-with-tight-rows-and-right-alig.md +6 -0
  247. package/corpus/templates/design/changelog/2026-07-06-undo-and-redo-no-longer-flash-the-screen-changes-now-apply-i.md +6 -0
  248. package/corpus/templates/design/changelog/2026-07-06-zoom-presets-like-zoom-to-50-now-stay-on-the-focused-screen-.md +6 -0
  249. package/corpus/templates/design/server/lib/import-design-files.ts +11 -2
  250. package/corpus/templates/design/server/source-workspace.ts +139 -59
  251. package/corpus/templates/design/shared/board-file.ts +11 -2
  252. package/corpus/templates/design/shared/breakpoint-media.ts +543 -0
  253. package/corpus/templates/design/shared/canvas-math.ts +354 -35
  254. package/corpus/templates/design/shared/code-layer.ts +277 -8
  255. package/corpus/templates/design/shared/design-state.ts +11 -4
  256. package/corpus/templates/design/shared/interaction-states.ts +548 -0
  257. package/corpus/templates/design/shared/motion-compiler.ts +186 -23
  258. package/corpus/templates/design/shared/motion-easing.ts +294 -0
  259. package/corpus/templates/design/shared/motion-timeline.ts +400 -34
  260. package/corpus/templates/design/shared/prompt-title.ts +95 -0
  261. package/corpus/templates/design/shared/responsive-classes.ts +448 -10
  262. package/corpus/templates/design/shared/screen-annotation.ts +46 -0
  263. package/corpus/templates/design/shared/shader-fills.ts +1009 -0
  264. package/corpus/templates/design/shared/shader-presets.ts +852 -4
  265. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +15 -2
  266. package/corpus/templates/slides/app/components/editor/ImportButton.tsx +10 -1
  267. package/corpus/templates/slides/app/lib/upload-response.ts +71 -0
  268. package/dist/client/AgentPanel.js +1 -1
  269. package/dist/client/AgentPanel.js.map +1 -1
  270. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  271. package/dist/client/rich-markdown-editor/useCollabReconcile.js +11 -6
  272. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  273. package/dist/client/use-action.d.ts +3 -0
  274. package/dist/client/use-action.d.ts.map +1 -1
  275. package/dist/client/use-action.js +34 -3
  276. package/dist/client/use-action.js.map +1 -1
  277. package/dist/client/use-db-sync.d.ts +4 -0
  278. package/dist/client/use-db-sync.d.ts.map +1 -1
  279. package/dist/client/use-db-sync.js +37 -18
  280. package/dist/client/use-db-sync.js.map +1 -1
  281. package/dist/collab/routes.d.ts +1 -1
  282. package/dist/collab/struct-routes.d.ts +1 -1
  283. package/dist/db/create-get-db.d.ts +25 -0
  284. package/dist/db/create-get-db.d.ts.map +1 -1
  285. package/dist/db/create-get-db.js +25 -2
  286. package/dist/db/create-get-db.js.map +1 -1
  287. package/dist/observability/routes.d.ts +3 -3
  288. package/dist/secrets/routes.d.ts +9 -9
  289. package/dist/server/framework-request-handler.d.ts.map +1 -1
  290. package/dist/server/framework-request-handler.js +37 -1
  291. package/dist/server/framework-request-handler.js.map +1 -1
  292. package/package.json +1 -1
  293. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +0 -153
  294. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +0 -133
  295. package/corpus/templates/design/app/pages/DesignEditor.tsx +0 -22924
@@ -29,6 +29,50 @@ export interface GradientValue {
29
29
  stops: GradientStopValue[];
30
30
  }
31
31
 
32
+ /**
33
+ * Contract for "a gradient editing session is active for element X with
34
+ * value V" (Figma-parity on-canvas handles, IP21 follow-up). This popover
35
+ * component stays the source of truth for parsing/serializing the CSS string
36
+ * (`parseGradientCss`/`gradientToCss` below) and for the ramp-bar UI; a
37
+ * canvas-side overlay (see `MultiScreenCanvas`'s `gradientEditTarget` prop)
38
+ * is a *second*, purely-visual view of the same session, so the two must
39
+ * agree on one shared shape rather than inventing parallel state.
40
+ *
41
+ * A caller that wants on-canvas handles alongside this popover should:
42
+ * 1. Keep the "which element/selection is being fill-edited" id it already
43
+ * has to open this popover (e.g. a selected draft primitive id or a
44
+ * selected screen/frame id).
45
+ * 2. Track the *current* `GradientValue` for that element the same way this
46
+ * component's `value` prop is already threaded (parsed once via
47
+ * `parseGradientCss`, then serialized back to CSS via `gradientToCss` on
48
+ * every change — exactly what this component's own `onChange` callers do
49
+ * today).
50
+ * 3. Build a `GradientEditSessionTarget` from those two pieces and pass it
51
+ * to `MultiScreenCanvas`'s `gradientEditTarget` prop whenever this
52
+ * popover is open (gated on `showGradientEditor` /
53
+ * `GRADIENT_PAINT_TYPES.has(effectivePaintType)`) and the target is a
54
+ * board/draft primitive or screen frame that canvas can draw chrome for;
55
+ * pass `null`/`undefined` otherwise (popover closed, non-canvas target,
56
+ * or non-linear kind — see that prop's doc for the current linear-only
57
+ * scope).
58
+ */
59
+ export interface GradientEditSessionTarget {
60
+ /** Id of the draft primitive or screen/frame the gradient applies to. */
61
+ frameOrDraftId: string;
62
+ /** The live CSS gradient string, e.g. what `gradientToCss` produces. */
63
+ cssValue: string;
64
+ /**
65
+ * Called by the canvas overlay when the user drags an on-canvas handle.
66
+ * `nextCss` is a full replacement gradient CSS string (round-trippable
67
+ * through `parseGradientCss`). `phase` mirrors the gesture-coalescing
68
+ * convention used elsewhere in this popover (see `onChangeComplete` on
69
+ * `DesignColorPickerProps`): "preview" fires on every drag tick for live
70
+ * feedback, "commit" fires once on pointerup with the final value so undo
71
+ * history only gets one entry per drag.
72
+ */
73
+ onChange: (nextCss: string, meta?: { phase: "preview" | "commit" }) => void;
74
+ }
75
+
32
76
  // ─── Checkerboard (matches DesignColorPicker) ───────────────────────────────────
33
77
 
34
78
  const CHECKER_A = "#d4d4d4";
@@ -0,0 +1,258 @@
1
+ /**
2
+ * InteractionStatePanel — the Webflow-style element interaction-state
3
+ * selector (Default / Hover / Focus / Focus-visible / Active / Disabled).
4
+ *
5
+ * Rendered at the top of the style sections when exactly one element is
6
+ * selected (see `EditPanel`'s mount site). Selecting a non-default state:
7
+ * 1. Visually marks the control as "editing <State>" so users can't
8
+ * forget they're no longer editing the element's base styling
9
+ * (Webflow/Framer convention — a persistent, unmissable accent, not a
10
+ * transient toast).
11
+ * 2. Tells the parent via `onInteractionStateChange` so DesignEditor can
12
+ * force the canvas preview (phase 2 — see `shared/interaction-states.ts`
13
+ * module doc for the forced-preview attribute mechanism this drives).
14
+ * 3. EditPanel then threads the active state through `StyleChangeMeta`
15
+ * (see EditPanel.tsx) so every style commit made while a state is
16
+ * active targets that state's managed CSS rule instead of the
17
+ * element's inline style.
18
+ *
19
+ * This component owns ONLY the selector UI — it has no opinion on how
20
+ * styles are persisted. It reads `hasOverride` (per state) from the caller
21
+ * so the row for a state that already has authored overrides can carry a
22
+ * small accent dot, matching the rest of the app's "this control has a
23
+ * non-default value" convention (see `StatesPanel`'s active-row dot and the
24
+ * breakpoint override dot conventions).
25
+ */
26
+
27
+ import { useT } from "@agent-native/core/client";
28
+ import {
29
+ IconChevronDown,
30
+ IconPointer,
31
+ IconFocus2,
32
+ IconHandClick,
33
+ IconBan,
34
+ } from "@tabler/icons-react";
35
+ import { Fragment } from "react";
36
+
37
+ import {
38
+ DropdownMenu,
39
+ DropdownMenuContent,
40
+ DropdownMenuItem,
41
+ DropdownMenuTrigger,
42
+ } from "@/components/ui/dropdown-menu";
43
+ import {
44
+ Tooltip,
45
+ TooltipContent,
46
+ TooltipTrigger,
47
+ } from "@/components/ui/tooltip";
48
+ import { cn } from "@/lib/utils";
49
+
50
+ import type { InteractionState } from "../../../../shared/interaction-states";
51
+
52
+ export type { InteractionState };
53
+
54
+ /** `null` (or omitted) means the Default (base) state is active. */
55
+ export type ActiveInteractionState = InteractionState | null;
56
+
57
+ export interface InteractionStatePanelProps {
58
+ /** `null` = Default. */
59
+ activeState: ActiveInteractionState;
60
+ onActiveStateChange: (state: ActiveInteractionState) => void;
61
+ /**
62
+ * Which non-default states are actually applicable to the selected
63
+ * element. `disabled` only makes sense on form controls / buttons, for
64
+ * example. Defaults to all five supported states when omitted so callers
65
+ * that haven't wired element-kind detection yet still get a working
66
+ * selector.
67
+ */
68
+ availableStates?: readonly InteractionState[];
69
+ /**
70
+ * States that already have at least one authored override, for the
71
+ * dropdown's per-row accent dot. Does not affect availability.
72
+ */
73
+ statesWithOverrides?: ReadonlySet<InteractionState>;
74
+ }
75
+
76
+ const STATE_ICONS: Record<InteractionState, typeof IconPointer> = {
77
+ hover: IconPointer,
78
+ focus: IconFocus2,
79
+ "focus-visible": IconFocus2,
80
+ active: IconHandClick,
81
+ disabled: IconBan,
82
+ };
83
+
84
+ function stateLabel(
85
+ t: ReturnType<typeof useT>,
86
+ state: ActiveInteractionState,
87
+ ): string {
88
+ switch (state) {
89
+ case "hover":
90
+ return t("editPanel.interactionStates.hover");
91
+ case "focus":
92
+ return t("editPanel.interactionStates.focus");
93
+ case "focus-visible":
94
+ return t("editPanel.interactionStates.focusVisible");
95
+ case "active":
96
+ return t("editPanel.interactionStates.active");
97
+ case "disabled":
98
+ return t("editPanel.interactionStates.disabled");
99
+ default:
100
+ return t("editPanel.interactionStates.default");
101
+ }
102
+ }
103
+
104
+ const DEFAULT_AVAILABLE_STATES: readonly InteractionState[] = [
105
+ "hover",
106
+ "focus",
107
+ "focus-visible",
108
+ "active",
109
+ "disabled",
110
+ ];
111
+
112
+ export function InteractionStatePanel({
113
+ activeState,
114
+ onActiveStateChange,
115
+ availableStates = DEFAULT_AVAILABLE_STATES,
116
+ statesWithOverrides,
117
+ }: InteractionStatePanelProps) {
118
+ const t = useT();
119
+ const isNonDefault = activeState !== null;
120
+
121
+ return (
122
+ <div className="border-b border-[var(--design-editor-control-border)] px-2 py-1.5">
123
+ <Tooltip>
124
+ <TooltipTrigger asChild>
125
+ <DropdownMenu>
126
+ <DropdownMenuTrigger asChild>
127
+ <button
128
+ type="button"
129
+ className={cn(
130
+ "flex h-7 w-full cursor-pointer items-center gap-1.5 rounded-md px-2 text-left text-[12px] font-semibold outline-none transition-colors",
131
+ isNonDefault
132
+ ? "bg-[var(--design-editor-accent-color)] text-white shadow-[0_0_0_1px_var(--design-editor-accent-color)]"
133
+ : "bg-[var(--design-editor-control-bg)] text-foreground hover:bg-[var(--design-editor-panel-raised-bg)]",
134
+ )}
135
+ aria-label={t("editPanel.interactionStates.selectorLabel")}
136
+ >
137
+ {isNonDefault ? (
138
+ <span
139
+ className="size-1.5 shrink-0 rounded-full bg-white"
140
+ aria-hidden="true"
141
+ />
142
+ ) : null}
143
+ <span className="min-w-0 flex-1 truncate">
144
+ {isNonDefault
145
+ ? t("editPanel.interactionStates.editingState", {
146
+ state: stateLabel(t, activeState),
147
+ })
148
+ : stateLabel(t, null)}
149
+ </span>
150
+ <IconChevronDown className="size-3.5 shrink-0 opacity-70" />
151
+ </button>
152
+ </DropdownMenuTrigger>
153
+ <DropdownMenuContent align="start" className="w-48">
154
+ <DropdownMenuItem
155
+ onClick={() => onActiveStateChange(null)}
156
+ className={cn(activeState === null && "font-semibold")}
157
+ >
158
+ <span className="flex-1">{stateLabel(t, null)}</span>
159
+ {activeState === null && (
160
+ <span className="size-1.5 rounded-full bg-primary" />
161
+ )}
162
+ </DropdownMenuItem>
163
+ {availableStates.map((state) => {
164
+ const Icon = STATE_ICONS[state];
165
+ const hasOverride = statesWithOverrides?.has(state) ?? false;
166
+ return (
167
+ <DropdownMenuItem
168
+ key={state}
169
+ onClick={() => onActiveStateChange(state)}
170
+ className={cn(activeState === state && "font-semibold")}
171
+ >
172
+ <Icon className="mr-2 size-3.5 shrink-0" />
173
+ <span className="flex-1">{stateLabel(t, state)}</span>
174
+ {hasOverride && (
175
+ <span
176
+ className={cn(
177
+ "size-1.5 shrink-0 rounded-full",
178
+ activeState === state
179
+ ? "bg-primary"
180
+ : "bg-[var(--design-editor-accent-color)]",
181
+ )}
182
+ aria-label={t(
183
+ "editPanel.interactionStates.hasOverrideIndicator",
184
+ )}
185
+ />
186
+ )}
187
+ </DropdownMenuItem>
188
+ );
189
+ })}
190
+ </DropdownMenuContent>
191
+ </DropdownMenu>
192
+ </TooltipTrigger>
193
+ <TooltipContent>
194
+ {isNonDefault
195
+ ? t("editPanel.interactionStates.editingStateTooltip", {
196
+ state: stateLabel(t, activeState),
197
+ })
198
+ : t("editPanel.interactionStates.selectorTooltip")}
199
+ </TooltipContent>
200
+ </Tooltip>
201
+ </div>
202
+ );
203
+ }
204
+
205
+ /**
206
+ * PropertyRow reset/override indicator — a small accent dot placed next to
207
+ * any style-section field row that has a declared override in the active
208
+ * interaction state, plus a reset button that clears just that property.
209
+ * Follows the same "dot + reset" shape as the breakpoint-override indicator
210
+ * convention (`getBreakpointOverrideState` callers elsewhere in EditPanel).
211
+ *
212
+ * Usage: wrap or place inline next to a field's label when
213
+ * `hasOverride` is true. `onReset` should call the shared
214
+ * `removeStateProperty` helper (via the parent's style-change plumbing) for
215
+ * that property/state.
216
+ */
217
+ export function InteractionStateOverrideIndicator({
218
+ hasOverride,
219
+ onReset,
220
+ }: {
221
+ hasOverride: boolean;
222
+ onReset?: () => void;
223
+ }) {
224
+ const t = useT();
225
+ if (!hasOverride) return null;
226
+ return (
227
+ <Fragment>
228
+ <Tooltip>
229
+ <TooltipTrigger asChild>
230
+ <span
231
+ className="ml-1 inline-block size-1.5 shrink-0 rounded-full bg-[var(--design-editor-accent-color)]"
232
+ aria-hidden="true"
233
+ />
234
+ </TooltipTrigger>
235
+ <TooltipContent>
236
+ {t("editPanel.interactionStates.hasOverrideIndicator")}
237
+ </TooltipContent>
238
+ </Tooltip>
239
+ {onReset ? (
240
+ <Tooltip>
241
+ <TooltipTrigger asChild>
242
+ <button
243
+ type="button"
244
+ onClick={onReset}
245
+ className="ml-0.5 cursor-pointer text-[10px] font-medium text-muted-foreground hover:text-foreground"
246
+ aria-label={t("editPanel.interactionStates.resetOverride")}
247
+ >
248
+ {t("editPanel.interactionStates.reset")}
249
+ </button>
250
+ </TooltipTrigger>
251
+ <TooltipContent>
252
+ {t("editPanel.interactionStates.resetOverrideTooltip")}
253
+ </TooltipContent>
254
+ </Tooltip>
255
+ ) : null}
256
+ </Fragment>
257
+ );
258
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * MotionKeyframeDiamond — the small ◆ affordance Figma places to the right
3
+ * of a keyframeable property's field/label once a timeline exists on the
4
+ * layer. Two states:
5
+ * - outline (muted): the property has a timeline but no keyframes yet for
6
+ * it — clicking adds one at the current playhead position.
7
+ * - filled accent: the property already has one or more keyframes —
8
+ * rendered solid so a glance across the panel shows which fields are
9
+ * currently animated.
10
+ *
11
+ * This component owns ONLY the affordance's rendering + click wiring. It has
12
+ * no opinion on how keyframes are stored or how the timeline UI reacts to a
13
+ * toggle — see `EditPanel`'s `motionKeyframeState`/`onToggleMotionKeyframe`
14
+ * props for that contract. The CSS property name passed in `cssProperty`
15
+ * must be one of the identifiers `MOTION_PROPERTY_PRESETS` in
16
+ * `shared/motion-timeline.ts` already uses (`translate` / `scale` /
17
+ * `rotate` / `opacity` / `border-radius` / `background-color` /
18
+ * `border-color` / `border-width` / `box-shadow`) so the motion machinery
19
+ * that owns the timeline can resolve the click to the right track.
20
+ *
21
+ * Visibility: hidden entirely when there's no timeline for the selected
22
+ * layer (`hasTimeline: false` or the state prop is omitted) — Figma only
23
+ * shows the diamond rail once a layer has been added to a timeline/scroll
24
+ * animation. When a timeline exists, the diamond is always in the DOM but
25
+ * stays visually muted (low-opacity outline) until the row is hovered, or is
26
+ * always visible once the property already carries a keyframe (filled),
27
+ * matching Figma's "quiet until relevant" treatment.
28
+ */
29
+
30
+ import { useT } from "@agent-native/core/client";
31
+
32
+ import {
33
+ Tooltip,
34
+ TooltipContent,
35
+ TooltipTrigger,
36
+ } from "@/components/ui/tooltip";
37
+ import { cn } from "@/lib/utils";
38
+
39
+ /**
40
+ * The exact CSS property identifiers the motion catalog
41
+ * (`MOTION_PROPERTY_PRESETS` in `shared/motion-timeline.ts`) tracks.
42
+ * `EditPanel` fields must emit one of these when calling
43
+ * `onToggleMotionKeyframe` so the caller can resolve the click to a motion
44
+ * track without guessing at a mapping.
45
+ */
46
+ export type MotionKeyframeCssProperty =
47
+ | "translate"
48
+ | "scale"
49
+ | "rotate"
50
+ | "opacity"
51
+ | "border-radius"
52
+ | "background-color"
53
+ | "border-color"
54
+ | "border-width"
55
+ | "box-shadow";
56
+
57
+ export interface MotionKeyframeDiamondProps {
58
+ /** One of the motion catalog's tracked CSS properties — see module doc. */
59
+ cssProperty: MotionKeyframeCssProperty;
60
+ /** True when this property already has at least one authored keyframe. */
61
+ hasKeyframe: boolean;
62
+ onToggle: () => void;
63
+ className?: string;
64
+ }
65
+
66
+ /**
67
+ * Small ◆ glyph, 8x8, drawn with currentColor so it inherits the button's
68
+ * text color for the outline/filled/hover states below.
69
+ */
70
+ function DiamondGlyph({ filled }: { filled: boolean }) {
71
+ return (
72
+ <svg
73
+ viewBox="0 0 8 8"
74
+ width="8"
75
+ height="8"
76
+ aria-hidden="true"
77
+ className="shrink-0"
78
+ >
79
+ <rect
80
+ x="1"
81
+ y="1"
82
+ width="6"
83
+ height="6"
84
+ transform="rotate(45 4 4)"
85
+ fill={filled ? "currentColor" : "none"}
86
+ stroke="currentColor"
87
+ strokeWidth={filled ? 0 : 1.25}
88
+ />
89
+ </svg>
90
+ );
91
+ }
92
+
93
+ export function MotionKeyframeDiamond({
94
+ cssProperty,
95
+ hasKeyframe,
96
+ onToggle,
97
+ className,
98
+ }: MotionKeyframeDiamondProps) {
99
+ const t = useT();
100
+ const label = hasKeyframe
101
+ ? t("editPanel.motionKeyframe.removeTooltip")
102
+ : t("editPanel.motionKeyframe.addTooltip");
103
+ return (
104
+ <Tooltip>
105
+ <TooltipTrigger asChild>
106
+ <button
107
+ type="button"
108
+ onClick={onToggle}
109
+ aria-pressed={hasKeyframe}
110
+ aria-label={label}
111
+ data-motion-css-property={cssProperty}
112
+ className={cn(
113
+ // Hover/focus-reveal for the muted outline (not-yet-keyframed)
114
+ // state is the caller's responsibility (see `FieldTrailer`'s
115
+ // wrapper, which fades this whole affordance in on field
116
+ // hover) — this component itself always renders at full
117
+ // opacity so a filled (keyframed) diamond never gets hidden by
118
+ // an ancestor hover state it doesn't control.
119
+ "flex size-4 shrink-0 items-center justify-center rounded-sm text-muted-foreground/50 transition-colors",
120
+ "hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]",
121
+ hasKeyframe &&
122
+ "text-[var(--design-editor-accent-color)] hover:text-[var(--design-editor-accent-color)]",
123
+ className,
124
+ )}
125
+ >
126
+ <DiamondGlyph filled={hasKeyframe} />
127
+ </button>
128
+ </TooltipTrigger>
129
+ <TooltipContent>{label}</TooltipContent>
130
+ </Tooltip>
131
+ );
132
+ }
133
+
134
+ /**
135
+ * Per-selection lookup helper: does `cssProperty` already have a keyframe,
136
+ * per the `motionKeyframeState.keyframedProperties` list threaded down from
137
+ * DesignEditor. Pure/cheap — safe to call inline in render.
138
+ */
139
+ export function motionPropertyHasKeyframe(
140
+ keyframedProperties: readonly string[] | undefined,
141
+ cssProperty: MotionKeyframeCssProperty,
142
+ ): boolean {
143
+ return keyframedProperties?.includes(cssProperty) ?? false;
144
+ }
@@ -23,6 +23,7 @@ import {
23
23
  getScrubStepFromEvent,
24
24
  normalizeScrubNumber,
25
25
  parseScrubExpression,
26
+ roundScrubDragValue,
26
27
  startScrubDrag,
27
28
  updateScrubDrag,
28
29
  type ScrubExpressionOptions,
@@ -47,6 +48,17 @@ export interface ScrubInputChangeMeta {
47
48
  * change, since those are already discrete, complete edits.
48
49
  */
49
50
  phase: "preview" | "commit";
51
+ /**
52
+ * Set when an arrow-key nudge fires on a `mixed` selection (see
53
+ * `handleKeyDown`): there is no single current value to step from across a
54
+ * mixed selection, so `onChange`'s `value` arg is the step delta itself
55
+ * (not a new absolute value) and consumers that support per-target relative
56
+ * application should add this delta to each selected target's own current
57
+ * value instead of overwriting every target with `value`. Omitted for
58
+ * every other change — existing consumers that don't check for it keep
59
+ * receiving absolute values exactly as before.
60
+ */
61
+ relativeDelta?: number;
50
62
  }
51
63
 
52
64
  export interface ScrubInputProps extends ScrubExpressionOptions {
@@ -107,8 +119,30 @@ export function ScrubInput({
107
119
  // can re-emit it once as the gesture's authoritative "commit" — without
108
120
  // recomputing from stale pointer deltas after pointer capture is released.
109
121
  const lastScrubValueRef = useRef(value);
122
+ // The most recent value THIS input committed (typed Enter/blur, keyboard
123
+ // nudge, or scrub release) that the host hasn't echoed back yet. While this
124
+ // is set, the resync effect below must hold the optimistic committed
125
+ // display instead of snapping back to the still-stale incoming `value`
126
+ // prop — otherwise a round-trip slower than one React render (host commit
127
+ // -> computedStyles update -> fresh `value` prop) clobbers the just-typed
128
+ // value back to the old one the instant focus leaves the input, which
129
+ // reads as "Enter resets to the old value". Cleared as soon as a fresh
130
+ // `value` prop confirms (or supersedes) the pending commit.
131
+ const pendingCommitRef = useRef<number | null>(null);
132
+ const options = { unit, min, max, precision };
110
133
 
111
134
  useEffect(() => {
135
+ if (pendingCommitRef.current !== null) {
136
+ if (normalizeScrubNumber(value, options) === pendingCommitRef.current) {
137
+ // The host echoed back exactly what we committed — resolved, safe to
138
+ // resume syncing from the prop again.
139
+ pendingCommitRef.current = null;
140
+ } else {
141
+ // Still waiting for the round-trip (or the host is reporting a
142
+ // different in-flight value) — don't stomp the optimistic draft.
143
+ return;
144
+ }
145
+ }
112
146
  if (!focused) {
113
147
  const formatted = mixed
114
148
  ? "Mixed"
@@ -116,13 +150,19 @@ export function ScrubInput({
116
150
  draftRef.current = formatted;
117
151
  setDraft(formatted);
118
152
  }
153
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- `options` is a fresh object every render; the individual fields it's built from (unit/min/max/precision) are already listed below.
119
154
  }, [focused, mixed, precision, unit, value]);
120
155
 
121
- const options = { unit, min, max, precision };
122
156
  const resolvedTooltipLabel = tooltipLabel ?? ariaLabel ?? label;
123
157
 
124
158
  const setNextValue = (nextValue: number, meta: ScrubInputChangeMeta) => {
125
159
  const normalized = normalizeScrubNumber(nextValue, options);
160
+ // Mark commit-phase writes as pending confirmation so the resync effect
161
+ // holds this optimistic display instead of reverting to a stale `value`
162
+ // prop before the host's round-trip lands (see pendingCommitRef above).
163
+ // Preview ticks don't need this: they're expected to be superseded by
164
+ // the next tick or the gesture's own final commit almost immediately.
165
+ if (meta.phase === "commit") pendingCommitRef.current = normalized;
126
166
  onChange(normalized, meta);
127
167
  const formatted = formatScrubValue(normalized, options);
128
168
  draftRef.current = formatted;
@@ -150,6 +190,9 @@ export function ScrubInput({
150
190
  // when it equals the placeholder `value` prop (e.g. typing "0"): the
151
191
  // selected objects hold differing values, so "no change" is meaningless.
152
192
  if (parsed.value !== value || mixed) {
193
+ // See setNextValue's pendingCommitRef comment — this text-commit path
194
+ // (Enter/blur) bypasses setNextValue, so mark it pending here too.
195
+ pendingCommitRef.current = parsed.value;
153
196
  onChange(parsed.value, {
154
197
  source: "commit",
155
198
  expression: currentDraft,
@@ -161,16 +204,33 @@ export function ScrubInput({
161
204
  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
162
205
  if (event.key === "ArrowUp" || event.key === "ArrowDown") {
163
206
  event.preventDefault();
164
- // Mixed selection: the `value` prop is only a placeholder (typically 0),
165
- // so stepping from it would snap every selected object to a value the
166
- // user never chose. Require an explicit typed value first — typing then
167
- // committing applies to all, which is the design-editor convention.
168
- if (mixed) return;
169
207
  const direction = event.key === "ArrowUp" ? 1 : -1;
170
208
  // getScrubStepFromEvent handles shiftKey (×10) and altKey (÷10).
171
209
  // Cmd (metaKey) mirrors Shift for ×10 — editor convention on macOS.
172
210
  const baseStep = getScrubStepFromEvent(event, step);
173
211
  const cmdMultiplier = event.metaKey && !event.shiftKey ? 10 : 1;
212
+ const delta = direction * baseStep * cmdMultiplier;
213
+ // Mixed selection: the `value` prop is only a placeholder (typically 0)
214
+ // — there's no single current value to step from, and there's no
215
+ // typed draft either (mixed keeps the draft as the literal "Mixed"
216
+ // string, see commitDraft's guard). Figma's behavior here is a
217
+ // *relative* nudge: apply the same +/-delta to each selected object's
218
+ // own value rather than snapping every object to one absolute number.
219
+ // ScrubInput itself can't resolve each target's individual value, so
220
+ // emit the delta via `onChange` (as both `value` and
221
+ // `meta.relativeDelta`) and let the consumer apply it per-target. Do
222
+ // NOT route through setNextValue: that formats/displays one absolute
223
+ // number in the draft, which would incorrectly replace the "Mixed"
224
+ // placeholder text with a single value that was never actually common
225
+ // to the whole selection.
226
+ if (mixed) {
227
+ onChange(delta, {
228
+ source: "keyboard",
229
+ phase: "commit",
230
+ relativeDelta: delta,
231
+ });
232
+ return;
233
+ }
174
234
  // Step from the currently typed draft, not the last-committed `value`
175
235
  // prop — otherwise an in-progress, uncommitted edit (typed but not yet
176
236
  // blurred/entered) is silently discarded the moment an arrow key is
@@ -182,7 +242,7 @@ export function ScrubInput({
182
242
  options,
183
243
  );
184
244
  const base = draftParsed ? draftParsed.value : value;
185
- setNextValue(base + direction * baseStep * cmdMultiplier, {
245
+ setNextValue(base + delta, {
186
246
  source: "keyboard",
187
247
  phase: "commit",
188
248
  });
@@ -214,6 +274,13 @@ export function ScrubInput({
214
274
  pointerId: event.pointerId,
215
275
  drag: startScrubDrag(event.clientX),
216
276
  };
277
+ // Re-seed the gesture's running base from the current prop right as the
278
+ // drag starts. Without this, a stale `lastScrubValueRef` left over from a
279
+ // previous gesture (or from an out-of-band prop update that arrived while
280
+ // not dragging) would silently become this gesture's starting point
281
+ // instead of the value actually displayed when the user grabbed the
282
+ // control.
283
+ lastScrubValueRef.current = value;
217
284
  event.currentTarget.setPointerCapture(event.pointerId);
218
285
  setDragging(true);
219
286
  };
@@ -235,14 +302,32 @@ export function ScrubInput({
235
302
  // Use incremental deltas from the last move so that clamped/rounded values
236
303
  // committed by onChange are respected. A total-delta approach would create
237
304
  // a dead zone equal to the amount dragged past the clamp boundary.
305
+ //
306
+ // Accumulate from this gesture's OWN last emitted value
307
+ // (lastScrubValueRef), not the `value` prop. The prop only reflects
308
+ // whatever the host last echoed back through computedStyles — a "preview"
309
+ // phase commit is not guaranteed to round-trip before the next
310
+ // pointermove tick fires (the host may debounce/throttle/skip preview
311
+ // writes), so re-reading `value` here would recompute every tick from a
312
+ // stale, pre-drag base plus one tiny incremental delta: the displayed
313
+ // number barely creeps from the original value instead of following the
314
+ // cursor, which reads as jittery/near-random rather than a smooth
315
+ // continuum. The gesture's own running total is always current because
316
+ // this component sets it itself on every tick below.
238
317
  const next =
239
- value +
318
+ lastScrubValueRef.current +
240
319
  tick.deltaX *
241
320
  getScrubStepFromEvent(
242
321
  { altKey: event.altKey, shiftKey: event.shiftKey },
243
322
  step,
244
323
  );
245
- lastScrubValueRef.current = setNextValue(next, {
324
+ // Px-type fields snap to whole numbers while scrubbing (see
325
+ // roundScrubDragValue) even though `precision` — which also governs typed
326
+ // input and keyboard nudges — allows a decimal. Rounding here, before
327
+ // setNextValue's own normalizeScrubNumber pass, keeps every subsequent
328
+ // incremental delta measured from an already-whole value instead of
329
+ // drifting on fractional leftovers.
330
+ lastScrubValueRef.current = setNextValue(roundScrubDragValue(next, unit), {
246
331
  source: "scrub",
247
332
  phase: "preview",
248
333
  });
@@ -259,6 +344,11 @@ export function ScrubInput({
259
344
  // dragging" — without this, the last preview tick would be the only
260
345
  // signal, and a consumer that ignores preview ticks would never commit.
261
346
  if (wasDrag && !mixed) {
347
+ // See setNextValue's pendingCommitRef comment — mark this gesture's
348
+ // authoritative value as pending confirmation so releasing the drag
349
+ // can't be clobbered back to the pre-drag value by a slow host
350
+ // round-trip (same class of bug as the Enter/blur text-commit case).
351
+ pendingCommitRef.current = lastScrubValueRef.current;
262
352
  onChange(lastScrubValueRef.current, {
263
353
  source: "scrub",
264
354
  phase: "commit",