@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
@@ -0,0 +1,315 @@
1
+ import { useT } from "@agent-native/core/client";
2
+ import { IconLink, IconUnlink } from "@tabler/icons-react";
3
+ import { useState, type ReactNode } from "react";
4
+
5
+ import { Button } from "@/components/ui/button";
6
+ import { Label } from "@/components/ui/label";
7
+ import {
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipTrigger,
11
+ } from "@/components/ui/tooltip";
12
+ import { cn } from "@/lib/utils";
13
+
14
+ import {
15
+ BreakpointOverrideIndicator,
16
+ MotionKeyframeDiamond,
17
+ motionPropertyHasKeyframe,
18
+ ScrubInput,
19
+ type MotionKeyframeCssProperty,
20
+ type ScrubInputChangeMeta,
21
+ } from "../inspector";
22
+ import type { ElementInfo } from "../types";
23
+ import { isMixedValue } from "./selection-helpers";
24
+ import {
25
+ resolveBreakpointOverride,
26
+ type BreakpointOverrideFieldContext,
27
+ type MotionKeyframeFieldContext,
28
+ type StyleChangeHandler,
29
+ type StylesChangeHandler,
30
+ } from "./style-change-types";
31
+ import { parseNumericValue, sidesAreLinked } from "./style-options";
32
+
33
+ export function DesignSpacingControl({
34
+ label,
35
+ values,
36
+ onChange,
37
+ }: {
38
+ label: string;
39
+ values: { top: string; right: string; bottom: string; left: string };
40
+ onChange: (side: string, value: string) => void;
41
+ }) {
42
+ const t = useT();
43
+ const [linked, setLinked] = useState(() => sidesAreLinked(values));
44
+ const numeric = {
45
+ top: parseNumericValue(values.top || "0"),
46
+ right: parseNumericValue(values.right || "0"),
47
+ bottom: parseNumericValue(values.bottom || "0"),
48
+ left: parseNumericValue(values.left || "0"),
49
+ };
50
+ const linkedValue = Math.round(
51
+ (numeric.top + numeric.right + numeric.bottom + numeric.left) / 4,
52
+ );
53
+ const setSide = (
54
+ side: "Top" | "Right" | "Bottom" | "Left",
55
+ value: number,
56
+ ) => {
57
+ onChange(side, `${Math.round(value)}px`);
58
+ };
59
+ const setAll = (value: number) => {
60
+ (["Top", "Right", "Bottom", "Left"] as const).forEach((side) =>
61
+ setSide(side, value),
62
+ );
63
+ };
64
+ const linkedLabel = linked
65
+ ? t("editPanel.labels.unlinkSides")
66
+ : t("editPanel.labels.linkSides");
67
+
68
+ return (
69
+ <div className="space-y-1.5">
70
+ <div className="flex items-center justify-between gap-1.5">
71
+ <Label className="!text-[11px] font-medium text-muted-foreground">
72
+ {label}
73
+ </Label>
74
+ <Tooltip>
75
+ <TooltipTrigger asChild>
76
+ <Button
77
+ type="button"
78
+ variant="ghost"
79
+ size="icon"
80
+ className="size-6 rounded-md text-muted-foreground hover:text-foreground"
81
+ onClick={() => setLinked((current) => !current)}
82
+ aria-label={linkedLabel}
83
+ >
84
+ {linked ? (
85
+ <IconLink className="size-3.5" />
86
+ ) : (
87
+ <IconUnlink className="size-3.5" />
88
+ )}
89
+ </Button>
90
+ </TooltipTrigger>
91
+ <TooltipContent>{linkedLabel}</TooltipContent>
92
+ </Tooltip>
93
+ </div>
94
+ {linked ? (
95
+ <ScrubInput
96
+ label={t("editPanel.labels.allSides")}
97
+ value={linkedValue}
98
+ onChange={setAll}
99
+ unit="px"
100
+ min={0}
101
+ labelClassName="w-16"
102
+ inputClassName="h-6"
103
+ />
104
+ ) : (
105
+ <div className="grid grid-cols-2 gap-1.5">
106
+ <ScrubInput
107
+ label={t("editPanel.sidePlaceholders.top")}
108
+ value={numeric.top}
109
+ onChange={(value) => setSide("Top", value)}
110
+ unit="px"
111
+ min={0}
112
+ precision={1}
113
+ inputClassName="h-6"
114
+ />
115
+ <ScrubInput
116
+ label={t("editPanel.sidePlaceholders.right")}
117
+ value={numeric.right}
118
+ onChange={(value) => setSide("Right", value)}
119
+ unit="px"
120
+ min={0}
121
+ precision={1}
122
+ inputClassName="h-6"
123
+ />
124
+ <ScrubInput
125
+ label={t("editPanel.sidePlaceholders.bottom")}
126
+ value={numeric.bottom}
127
+ onChange={(value) => setSide("Bottom", value)}
128
+ unit="px"
129
+ min={0}
130
+ precision={1}
131
+ inputClassName="h-6"
132
+ />
133
+ <ScrubInput
134
+ label={t("editPanel.sidePlaceholders.left")}
135
+ value={numeric.left}
136
+ onChange={(value) => setSide("Left", value)}
137
+ unit="px"
138
+ min={0}
139
+ precision={1}
140
+ inputClassName="h-6"
141
+ />
142
+ </div>
143
+ )}
144
+ </div>
145
+ );
146
+ }
147
+
148
+ /**
149
+ * FieldTrailer — composes the motion keyframe diamond and the breakpoint
150
+ * override indicator/reset for one field, in the Figma-parity order (diamond
151
+ * first, then the override dot). Renders `null` when neither affordance
152
+ * applies, so call sites can drop it in unconditionally next to any
153
+ * keyframeable/overridable field without their own presence checks.
154
+ *
155
+ * `motionCssProperty` drives the keyframe diamond (omit to skip it — e.g.
156
+ * for fields with no motion-catalog equivalent); `overrideProperty` drives
157
+ * the breakpoint override indicator (defaults to `motionCssProperty` when
158
+ * omitted, since most fields use the same identifier for both — pass it
159
+ * explicitly when a field's CSS property differs from its motion-catalog
160
+ * name, e.g. corner radius's independent-corner longhands).
161
+ */
162
+ export function FieldTrailer({
163
+ element,
164
+ motionCssProperty,
165
+ overrideProperty,
166
+ motionKeyframeContext,
167
+ breakpointOverrideContext,
168
+ hoverRevealClassName,
169
+ className,
170
+ }: {
171
+ element: ElementInfo;
172
+ motionCssProperty?: MotionKeyframeCssProperty;
173
+ overrideProperty?: string;
174
+ motionKeyframeContext?: MotionKeyframeFieldContext;
175
+ breakpointOverrideContext?: BreakpointOverrideFieldContext;
176
+ /**
177
+ * Applied ONLY to the keyframe diamond, and only while it's in its muted
178
+ * outline (not-yet-keyframed) state — e.g. `"opacity-0
179
+ * group-hover/field:opacity-100"` to hide it until the field is hovered.
180
+ * A filled (already-keyframed) diamond, and the breakpoint override dot,
181
+ * always render regardless of this class since both convey real state
182
+ * rather than a quiet affordance.
183
+ */
184
+ hoverRevealClassName?: string;
185
+ className?: string;
186
+ }) {
187
+ const showDiamond =
188
+ motionCssProperty != null && motionKeyframeContext?.hasTimeline === true;
189
+ const hasKeyframe = showDiamond
190
+ ? motionPropertyHasKeyframe(
191
+ motionKeyframeContext?.keyframedProperties,
192
+ motionCssProperty!,
193
+ )
194
+ : false;
195
+ const resolvedOverrideProperty = overrideProperty ?? motionCssProperty;
196
+ const overrideState = resolvedOverrideProperty
197
+ ? resolveBreakpointOverride(
198
+ breakpointOverrideContext,
199
+ element.classes.join(" "),
200
+ resolvedOverrideProperty,
201
+ )
202
+ : undefined;
203
+
204
+ if (!showDiamond && !overrideState?.overriddenAtActive) return null;
205
+
206
+ return (
207
+ <span
208
+ className={cn(
209
+ "group/trailer inline-flex items-center gap-0.5",
210
+ className,
211
+ )}
212
+ >
213
+ {showDiamond ? (
214
+ <MotionKeyframeDiamond
215
+ cssProperty={motionCssProperty!}
216
+ hasKeyframe={hasKeyframe}
217
+ onToggle={() => motionKeyframeContext?.onToggle?.(motionCssProperty!)}
218
+ className={hasKeyframe ? undefined : hoverRevealClassName}
219
+ />
220
+ ) : null}
221
+ {overrideState?.overriddenAtActive && resolvedOverrideProperty ? (
222
+ <BreakpointOverrideIndicator
223
+ overridden
224
+ maxWidthPx={overrideState.activeUpperBoundPx}
225
+ onReset={
226
+ breakpointOverrideContext &&
227
+ overrideState.activeUpperBoundPx != null
228
+ ? () =>
229
+ breakpointOverrideContext.onReset(
230
+ resolvedOverrideProperty,
231
+ overrideState.activeUpperBoundPx!,
232
+ )
233
+ : undefined
234
+ }
235
+ />
236
+ ) : null}
237
+ </span>
238
+ );
239
+ }
240
+
241
+ export function ScrubStyleInput({
242
+ label,
243
+ value,
244
+ placeholder,
245
+ onChange,
246
+ unit = "px",
247
+ min,
248
+ max,
249
+ step = 1,
250
+ labelClassName,
251
+ inputClassName,
252
+ ariaLabel,
253
+ tooltipLabel,
254
+ hideIcon = true,
255
+ icon,
256
+ disabled = false,
257
+ }: {
258
+ label: string;
259
+ value: string;
260
+ placeholder?: number;
261
+ onChange: (value: number, meta?: ScrubInputChangeMeta) => void;
262
+ unit?: string;
263
+ min?: number;
264
+ max?: number;
265
+ step?: number;
266
+ labelClassName?: string;
267
+ inputClassName?: string;
268
+ hideIcon?: boolean;
269
+ ariaLabel?: string;
270
+ tooltipLabel?: string;
271
+ icon?: (props: { className?: string }) => ReactNode;
272
+ disabled?: boolean;
273
+ }) {
274
+ const mixed = isMixedValue(value);
275
+ return (
276
+ <ScrubInput
277
+ label={label}
278
+ ariaLabel={ariaLabel}
279
+ tooltipLabel={tooltipLabel}
280
+ icon={hideIcon ? null : icon}
281
+ value={mixed ? 0 : value ? parseNumericValue(value) : (placeholder ?? 0)}
282
+ onChange={onChange}
283
+ mixed={mixed}
284
+ unit={unit}
285
+ min={min}
286
+ max={max}
287
+ step={step}
288
+ precision={1}
289
+ disabled={disabled}
290
+ className="gap-0"
291
+ labelClassName={cn(
292
+ "h-6 w-7 justify-center gap-0 rounded-l-md rounded-r-none border border-r-0 border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] !text-[11px] tabular-nums",
293
+ labelClassName,
294
+ )}
295
+ inputClassName={cn(
296
+ "h-6 rounded-l-none rounded-r-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] shadow-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]",
297
+ inputClassName,
298
+ )}
299
+ />
300
+ );
301
+ }
302
+
303
+ export function commitStylePatch(
304
+ styles: Record<string, string>,
305
+ onStyleChange: StyleChangeHandler,
306
+ onStylesChange?: StylesChangeHandler,
307
+ ) {
308
+ if (onStylesChange) {
309
+ onStylesChange(styles);
310
+ return;
311
+ }
312
+ Object.entries(styles).forEach(([property, value]) => {
313
+ onStyleChange(property, value);
314
+ });
315
+ }
@@ -0,0 +1,344 @@
1
+ import {
2
+ alphaToOpacity,
3
+ parseCssColor,
4
+ rgbaToCss,
5
+ withColorOpacity,
6
+ } from "@shared/color-utils";
7
+
8
+ import {
9
+ type DesignFillRow,
10
+ type DesignGradientStop,
11
+ type DesignGradientType,
12
+ type ExportSettingsValue,
13
+ } from "../inspector";
14
+ import { cssColorOrFallback } from "./position-helpers";
15
+
16
+ export const SOLID_FILL_ID = "solid";
17
+ export const FILL_LAYER_PREFIX = "layer:";
18
+
19
+ interface ParsedGradientLayer {
20
+ type: DesignGradientType;
21
+ prefix?: string;
22
+ stops: DesignGradientStop[];
23
+ }
24
+
25
+ export const DEFAULT_EXPORT_SETTINGS: ExportSettingsValue = {
26
+ scale: 1,
27
+ format: "png",
28
+ suffix: "",
29
+ };
30
+
31
+ export function clampNumber(value: number, min: number, max: number): number {
32
+ if (!Number.isFinite(value)) return min;
33
+ return Math.max(min, Math.min(max, Math.round(value)));
34
+ }
35
+
36
+ export function fillLayerId(index: number): string {
37
+ return `${FILL_LAYER_PREFIX}${index}`;
38
+ }
39
+
40
+ export function fillLayerIndex(id: string): number | null {
41
+ if (!id.startsWith(FILL_LAYER_PREFIX)) return null;
42
+ const index = Number(id.slice(FILL_LAYER_PREFIX.length));
43
+ return Number.isInteger(index) && index >= 0 ? index : null;
44
+ }
45
+
46
+ export function buildFillRows(
47
+ colorValue: string,
48
+ backgroundLayers: string[],
49
+ selectedFillId: string,
50
+ ): DesignFillRow[] {
51
+ const solid = parseCssColor(colorValue);
52
+ const rows: DesignFillRow[] = [
53
+ {
54
+ id: SOLID_FILL_ID,
55
+ label: "Solid", // i18n-ignore inspector fallback label
56
+ type: "solid",
57
+ value: colorValue,
58
+ swatch: colorValue,
59
+ opacity: solid ? alphaToOpacity(solid.a) : 100,
60
+ selected: selectedFillId === SOLID_FILL_ID,
61
+ },
62
+ ];
63
+
64
+ backgroundLayers.forEach((layer, index) => {
65
+ const gradient = parseGradientLayer(layer);
66
+ rows.push({
67
+ id: fillLayerId(index),
68
+ label: gradient
69
+ ? `Gradient ${index + 1}` // i18n-ignore inspector fallback label
70
+ : `Image ${index + 1}`, // i18n-ignore inspector fallback label
71
+ type: gradient ? "gradient" : "image",
72
+ value: layer,
73
+ swatch: layer,
74
+ opacity: gradient ? averageGradientOpacity(gradient.stops) : 100,
75
+ selected: selectedFillId === fillLayerId(index),
76
+ });
77
+ });
78
+
79
+ return rows;
80
+ }
81
+
82
+ export function averageGradientOpacity(stops: DesignGradientStop[]): number {
83
+ if (!stops.length) return 100;
84
+ const total = stops.reduce((sum, stop) => {
85
+ const parsed = parseCssColor(stop.color);
86
+ return sum + (stop.opacity ?? (parsed ? alphaToOpacity(parsed.a) : 100));
87
+ }, 0);
88
+ return Math.round(total / stops.length);
89
+ }
90
+
91
+ /**
92
+ * Marker used to non-destructively hide a single backgroundImage layer
93
+ * (gradient or image). CSS strips comments from computed style values — a
94
+ * trailing comment appended to a backgroundImage layer does not survive
95
+ * getComputedStyle (verified: browsers normalize/serialize computed values
96
+ * without their source comments) — so we can't tag the layer text itself.
97
+ * Instead we pair the untouched original layer with a zero-size
98
+ * background-size entry at the same index: `background-size: 0px 0px` makes
99
+ * that layer render nothing while backgroundImage keeps the exact original
100
+ * CSS text. Both backgroundImage and backgroundSize are real, valid,
101
+ * positionally-paired CSS lists that DO round-trip through computed style,
102
+ * so hiding survives reselect/reload with no React state stash required.
103
+ */
104
+ const HIDDEN_LAYER_SIZE_MARKER = "0px 0px";
105
+
106
+ export function isLayerHiddenBySize(sizeEntry: string | undefined): boolean {
107
+ return (
108
+ (sizeEntry ?? "").trim().replace(/\s+/g, " ") === HIDDEN_LAYER_SIZE_MARKER
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Rewrites the background-size list so `index` is hidden/shown via the
114
+ * zero-size marker, padding shorter lists with "auto" (the CSS default) so
115
+ * every other layer keeps rendering at its current/default size.
116
+ */
117
+ export function withLayerSizeMarker(
118
+ sizeLayers: string[],
119
+ layerCount: number,
120
+ index: number,
121
+ hidden: boolean,
122
+ ): string {
123
+ const next = Array.from(
124
+ { length: layerCount },
125
+ (_, i) => sizeLayers[i] || "auto",
126
+ );
127
+ next[index] = hidden ? HIDDEN_LAYER_SIZE_MARKER : "auto";
128
+ return joinCssLayers(next);
129
+ }
130
+
131
+ export function splitCssLayers(value: string): string[] {
132
+ const trimmed = value.trim();
133
+ if (!trimmed || trimmed === "none") return [];
134
+ const layers: string[] = [];
135
+ let depth = 0;
136
+ let start = 0;
137
+
138
+ for (let index = 0; index < trimmed.length; index += 1) {
139
+ const char = trimmed[index];
140
+ if (char === "(") depth += 1;
141
+ if (char === ")") depth = Math.max(0, depth - 1);
142
+ if (char === "," && depth === 0) {
143
+ const layer = trimmed.slice(start, index).trim();
144
+ if (layer) layers.push(layer);
145
+ start = index + 1;
146
+ }
147
+ }
148
+
149
+ const finalLayer = trimmed.slice(start).trim();
150
+ if (finalLayer) layers.push(finalLayer);
151
+ return layers;
152
+ }
153
+
154
+ export function joinCssLayers(layers: string[]): string {
155
+ const cleaned = layers.map((layer) => layer.trim()).filter(Boolean);
156
+ return cleaned.length ? cleaned.join(", ") : "none";
157
+ }
158
+
159
+ /** One fill layer's index-aligned parallel CSS values. */
160
+ export interface FillLayerArrays {
161
+ backgroundImage: string[];
162
+ backgroundSize: string[];
163
+ backgroundRepeat: string[];
164
+ backgroundPosition: string[];
165
+ }
166
+
167
+ /**
168
+ * Removes the layer at `index` from all four index-aligned parallel fill
169
+ * arrays (image/size/repeat/position) together, returning a single patch of
170
+ * joined CSS layer-list strings ready to commit as one atomic style change.
171
+ *
172
+ * Splicing only `backgroundImage`/`backgroundSize` (as a previous version of
173
+ * `removeLayer` did) and leaving `backgroundRepeat`/`backgroundPosition`
174
+ * untouched shifts every remaining layer's index relative to those two
175
+ * arrays, silently re-pairing each of them with the *next* layer's original
176
+ * repeat/position. Splicing all four together — the same pattern
177
+ * `reorderFillLayers` already uses for permutation — keeps every remaining
178
+ * layer's size/repeat/position aligned with its own image after the removal.
179
+ */
180
+ export function removeFillLayerAtIndex(
181
+ layers: FillLayerArrays,
182
+ index: number,
183
+ ): Record<
184
+ | "backgroundImage"
185
+ | "backgroundSize"
186
+ | "backgroundRepeat"
187
+ | "backgroundPosition",
188
+ string
189
+ > {
190
+ const withoutIndex = (values: string[]) =>
191
+ values.filter((_, layerIndex) => layerIndex !== index);
192
+ return {
193
+ backgroundImage: joinCssLayers(withoutIndex(layers.backgroundImage)),
194
+ backgroundSize: joinCssLayers(withoutIndex(layers.backgroundSize)),
195
+ backgroundRepeat: joinCssLayers(withoutIndex(layers.backgroundRepeat)),
196
+ backgroundPosition: joinCssLayers(withoutIndex(layers.backgroundPosition)),
197
+ };
198
+ }
199
+
200
+ export function parseGradientLayer(layer: string): ParsedGradientLayer | null {
201
+ const match = layer.trim().match(/^(linear|radial|conic)-gradient\((.*)\)$/i);
202
+ if (!match) return null;
203
+
204
+ const parts = splitCssLayers(match[2] || "");
205
+ const type = gradientTypeFromCss(match[1] || "", layer);
206
+ const firstStop = parseGradientStop(parts[0] || "", 0, parts.length);
207
+ const prefix = firstStop ? undefined : parts[0]?.trim();
208
+ const stopParts = firstStop ? parts : parts.slice(1);
209
+ const stops = stopParts
210
+ .map((part, index) => parseGradientStop(part, index, stopParts.length))
211
+ .filter((stop): stop is DesignGradientStop => Boolean(stop));
212
+
213
+ if (!stops.length) return null;
214
+ return { type, prefix, stops };
215
+ }
216
+
217
+ function parseGradientStop(
218
+ part: string,
219
+ index: number,
220
+ total: number,
221
+ ): DesignGradientStop | null {
222
+ const color = readLeadingColor(part);
223
+ if (!color) return null;
224
+ const parsed = parseCssColor(color.value);
225
+ const remaining = part.slice(color.raw.length);
226
+ const positionMatch = remaining.match(/(-?\d+(?:\.\d+)?)%/);
227
+ const position = positionMatch
228
+ ? clampNumber(Number(positionMatch[1]), 0, 100)
229
+ : total <= 1
230
+ ? 0
231
+ : Math.round((index / (total - 1)) * 100);
232
+
233
+ return {
234
+ id: `stop-${index}`,
235
+ color: parsed ? rgbaToCss(parsed) : color.value,
236
+ position,
237
+ opacity: parsed ? alphaToOpacity(parsed.a) : 100,
238
+ };
239
+ }
240
+
241
+ function readLeadingColor(part: string): { raw: string; value: string } | null {
242
+ const trimmed = part.trim();
243
+ const hex = trimmed.match(/^#[0-9a-f]{3,8}\b/i);
244
+ if (hex) return { raw: hex[0], value: hex[0] };
245
+ const transparent = trimmed.match(/^transparent\b/i);
246
+ if (transparent) {
247
+ return { raw: transparent[0], value: "rgba(0, 0, 0, 0)" };
248
+ }
249
+ const functionName = trimmed.match(/^[a-z][a-z0-9-]*\(/i);
250
+ if (!functionName) return null;
251
+ let depth = 0;
252
+ for (let index = 0; index < trimmed.length; index += 1) {
253
+ const char = trimmed[index];
254
+ if (char === "(") depth += 1;
255
+ if (char === ")") {
256
+ depth -= 1;
257
+ if (depth === 0) {
258
+ const raw = trimmed.slice(0, index + 1);
259
+ return { raw, value: raw };
260
+ }
261
+ }
262
+ }
263
+ return null;
264
+ }
265
+
266
+ function gradientTypeFromCss(
267
+ functionName: string,
268
+ layer: string,
269
+ ): DesignGradientType {
270
+ if (functionName.toLowerCase() === "conic") return "angular";
271
+ // Recognize both diamond serializations — EditPanel's "closest-corner" and
272
+ // GradientEditor's "ellipse closest-side" — so a diamond authored in either
273
+ // place round-trips as diamond instead of flipping to radial.
274
+ if (/closest-corner/i.test(layer) || /ellipse\s+closest-side/i.test(layer))
275
+ return "diamond";
276
+ if (functionName.toLowerCase() === "radial") return "radial";
277
+ return "linear";
278
+ }
279
+
280
+ export function gradientLabel(type: DesignGradientType): string {
281
+ if (type === "radial") {
282
+ return "Radial gradient"; // i18n-ignore design inspector paint row
283
+ }
284
+ if (type === "angular") {
285
+ return "Angular gradient"; // i18n-ignore design inspector paint row
286
+ }
287
+ if (type === "diamond") {
288
+ return "Diamond gradient"; // i18n-ignore design inspector paint row
289
+ }
290
+ return "Linear gradient"; // i18n-ignore design inspector paint row
291
+ }
292
+
293
+ function defaultGradientPrefix(type: DesignGradientType): string {
294
+ if (type === "radial") return "circle at 50% 50%";
295
+ if (type === "angular") return "from 0deg at 50% 50%";
296
+ if (type === "diamond") return "closest-corner at 50% 50%";
297
+ return "90deg";
298
+ }
299
+
300
+ export function buildGradientLayer(
301
+ type: DesignGradientType,
302
+ stops: DesignGradientStop[],
303
+ prefix = defaultGradientPrefix(type),
304
+ ): string {
305
+ const stopList = [...stops]
306
+ .sort((a, b) => a.position - b.position)
307
+ .map((stop) => {
308
+ const parsed = parseCssColor(stop.color);
309
+ const opacity = stop.opacity ?? (parsed ? alphaToOpacity(parsed.a) : 100);
310
+ const color = parsed
311
+ ? rgbaToCss(withColorOpacity(parsed, opacity))
312
+ : stop.color;
313
+ return `${color} ${clampNumber(stop.position, 0, 100)}%`;
314
+ })
315
+ .join(", ");
316
+
317
+ if (type === "radial" || type === "diamond") {
318
+ return `radial-gradient(${prefix}, ${stopList})`;
319
+ }
320
+ if (type === "angular") return `conic-gradient(${prefix}, ${stopList})`;
321
+ return `linear-gradient(${prefix}, ${stopList})`;
322
+ }
323
+
324
+ export function defaultGradientStops(colorValue: string): DesignGradientStop[] {
325
+ const parsed =
326
+ parseCssColor(cssColorOrFallback(colorValue, "#000000")) ??
327
+ parseCssColor("#000000");
328
+ const start = parsed ? rgbaToCss(withColorOpacity(parsed, 100)) : "#000000";
329
+ const end = parsed
330
+ ? rgbaToCss(withColorOpacity(parsed, 0))
331
+ : "rgba(0, 0, 0, 0)";
332
+
333
+ return [
334
+ { id: "stop-0", color: start, position: 0, opacity: 100 },
335
+ { id: "stop-1", color: end, position: 100, opacity: 0 },
336
+ ];
337
+ }
338
+
339
+ export function defaultGradientLayer(
340
+ type: DesignGradientType,
341
+ colorValue: string,
342
+ ) {
343
+ return buildGradientLayer(type, defaultGradientStops(colorValue));
344
+ }