@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,1009 @@
1
+ /**
2
+ * Code-backed GLSL shader fills + effects — canonical persisted format.
3
+ *
4
+ * Unlike the preset-only `shader-presets.ts` catalog (which approximates
5
+ * shaders as CSS gradients when persisting), this module defines a fully
6
+ * code-backed representation: the GLSL fragment source lives IN the screen
7
+ * HTML, readable and editable in the Code panel, and a small self-contained
8
+ * WebGL runtime (embedded in the same HTML) renders it live — in the editor,
9
+ * in shared links, and in exported standalone HTML.
10
+ *
11
+ * ── Persisted format (v1) ────────────────────────────────────────────────
12
+ *
13
+ * 1. One definition block per shader, anywhere in the document (by
14
+ * convention just before `</body>`):
15
+ *
16
+ * <script type="application/x-agent-native-shader"
17
+ * data-shader-id="an-shader-x1y2z3"
18
+ * data-shader-name="Water Caustics"
19
+ * data-shader-mode="fill">
20
+ * (leading GLSL block comment opened with slash-star-bang)
21
+ * an-shader v1
22
+ * { "uniforms": { "u_speed": { "type": "float", "value": 1,
23
+ * "min": 0, "max": 4, "step": 0.01, "label": "Speed" } } }
24
+ * (star-slash closes the comment)
25
+ * precision highp float;
26
+ * uniform vec2 u_resolution;
27
+ * uniform float u_time;
28
+ * uniform float u_speed;
29
+ * void main() { ... gl_FragColor = vec4(...); }
30
+ * </script>
31
+ *
32
+ * The uniforms manifest rides inside a leading GLSL block comment — valid
33
+ * GLSL, so the whole block body is directly compilable, and the Code panel
34
+ * shows one readable, self-documenting artifact.
35
+ *
36
+ * 2. Elements reference a shader by id:
37
+ * data-an-shader-fill="an-shader-x1y2z3" (canvas behind content)
38
+ * data-an-shader-effect="an-shader-x1y2z3" (overlay above content)
39
+ * Optional per-element uniform overrides (Figma's paint-instance
40
+ * properties): data-an-shader-uniforms='{"u_speed":2}'
41
+ * For fills, the element's inline `background` holds a static fallback
42
+ * color so the artifact still renders without JS/WebGL.
43
+ *
44
+ * 3. The runtime is embedded once per document:
45
+ * <script data-agent-native-shader-runtime data-runtime-version="1">…
46
+ * (source of truth: app/components/design/bridge/shader-runtime.bridge.ts,
47
+ * compiled by bridge/codegen.ts — `ensureShaderRuntime()` injects/upgrades
48
+ * it.)
49
+ *
50
+ * Built-in uniforms every shader may declare (auto-driven by the runtime):
51
+ * uniform float u_time; // seconds, 0 under prefers-reduced-motion
52
+ * uniform vec2 u_resolution; // canvas size in physical pixels
53
+ *
54
+ * Keep the manifest parsing in shader-runtime.bridge.ts in sync with this
55
+ * module.
56
+ */
57
+
58
+ import { shaderRuntimeBridgeScript } from "../.generated/bridge/shader-runtime.generated";
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Types
62
+ // ---------------------------------------------------------------------------
63
+
64
+ export type GlslUniformType = "float" | "vec2" | "color";
65
+
66
+ export type GlslUniformValue = number | [number, number] | string;
67
+
68
+ export interface GlslUniformDef {
69
+ type: GlslUniformType;
70
+ /** float → number; vec2 → [x, y]; color → "#rrggbb". */
71
+ value: GlslUniformValue;
72
+ /** Knob range — only for type "float". */
73
+ min?: number;
74
+ max?: number;
75
+ step?: number;
76
+ /** Human-readable knob label shown in the inspector. */
77
+ label?: string;
78
+ }
79
+
80
+ export type GlslUniformManifest = Record<string, GlslUniformDef>;
81
+
82
+ export type GlslShaderMode = "fill" | "effect";
83
+
84
+ /** One code-backed shader as persisted in the screen HTML. */
85
+ export interface GlslShaderDef {
86
+ id: string;
87
+ name: string;
88
+ mode: GlslShaderMode;
89
+ glsl: string;
90
+ uniforms: GlslUniformManifest;
91
+ }
92
+
93
+ /** One element ↔ shader reference parsed back out of the HTML. */
94
+ export interface GlslShaderMountRef {
95
+ nodeId: string | null;
96
+ shaderId: string;
97
+ mode: GlslShaderMode;
98
+ /** Per-element uniform value overrides, when present. */
99
+ values?: Record<string, GlslUniformValue>;
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Constants
104
+ // ---------------------------------------------------------------------------
105
+
106
+ export const SHADER_SCRIPT_TYPE = "application/x-agent-native-shader";
107
+ export const SHADER_FILL_ATTR = "data-an-shader-fill";
108
+ export const SHADER_EFFECT_ATTR = "data-an-shader-effect";
109
+ export const SHADER_UNIFORMS_ATTR = "data-an-shader-uniforms";
110
+ /**
111
+ * Per-element uniform overrides for the EFFECT mount. Fills and effects can
112
+ * coexist on one element (Figma stacks them), so each mode gets its own
113
+ * override attribute.
114
+ */
115
+ export const SHADER_EFFECT_UNIFORMS_ATTR = "data-an-shader-effect-uniforms";
116
+ export const SHADER_RUNTIME_ATTR = "data-agent-native-shader-runtime";
117
+ export const SHADER_RUNTIME_VERSION = "1";
118
+ export const SHADER_MANIFEST_VERSION = 1;
119
+
120
+ /** Uniform names the runtime provides automatically — reserved. */
121
+ export const SHADER_BUILTIN_UNIFORMS = ["u_time", "u_resolution"] as const;
122
+
123
+ const MAX_GLSL_LENGTH = 20000;
124
+ const ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
125
+ const UNIFORM_NAME_RE = /^u_[A-Za-z0-9_]{1,48}$/;
126
+ const VOID_TAGS = new Set([
127
+ "area",
128
+ "base",
129
+ "br",
130
+ "col",
131
+ "embed",
132
+ "hr",
133
+ "img",
134
+ "input",
135
+ "link",
136
+ "meta",
137
+ "param",
138
+ "source",
139
+ "track",
140
+ "wbr",
141
+ ]);
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Small utilities
145
+ // ---------------------------------------------------------------------------
146
+
147
+ function escapeAttr(value: string): string {
148
+ return value
149
+ .replace(/&/g, "&amp;")
150
+ .replace(/"/g, "&quot;")
151
+ .replace(/'/g, "&#39;")
152
+ .replace(/</g, "&lt;")
153
+ .replace(/>/g, "&gt;");
154
+ }
155
+
156
+ function unescapeAttr(value: string): string {
157
+ return value
158
+ .replace(/&quot;/g, '"')
159
+ .replace(/&#39;/g, "'")
160
+ .replace(/&lt;/g, "<")
161
+ .replace(/&gt;/g, ">")
162
+ .replace(/&amp;/g, "&");
163
+ }
164
+
165
+ function escapeRegExp(value: string): string {
166
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
167
+ }
168
+
169
+ /** Generate a fresh shader id: an-shader-<8 base36 chars>. */
170
+ export function newShaderId(): string {
171
+ let suffix = "";
172
+ while (suffix.length < 8) {
173
+ suffix += Math.random().toString(36).slice(2);
174
+ }
175
+ return "an-shader-" + suffix.slice(0, 8);
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Validation
180
+ // ---------------------------------------------------------------------------
181
+
182
+ const GLSL_TYPE_FOR_UNIFORM: Record<GlslUniformType, string> = {
183
+ float: "float",
184
+ vec2: "vec2",
185
+ color: "vec3",
186
+ };
187
+
188
+ const SAFE_COLOR_RE =
189
+ /^(#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgba?\([\d.,\s%/]*\)|hsla?\([\d.,\s%/deg]*\)|oklch\([\d.,\s%/deg]*\)|[a-zA-Z]{3,25})$/;
190
+
191
+ /** True when a CSS color literal is safe to embed in an inline style. */
192
+ export function isSafeShaderFallbackColor(value: string): boolean {
193
+ const trimmed = value.trim();
194
+ if (trimmed.length === 0 || trimmed.length > 64) return false;
195
+ if (/[;{}<>]|url\s*\(/i.test(trimmed)) return false;
196
+ return SAFE_COLOR_RE.test(trimmed);
197
+ }
198
+
199
+ /** Neutralize an unsafe fallback color instead of failing the whole apply. */
200
+ export function sanitizeShaderFallbackColor(value: string | undefined): string {
201
+ if (value && isSafeShaderFallbackColor(value)) return value.trim();
202
+ return "#808080";
203
+ }
204
+
205
+ export interface ShaderValidationResult {
206
+ valid: boolean;
207
+ errors: string[];
208
+ }
209
+
210
+ /** String-level sanity checks on a GLSL fragment source. */
211
+ export function validateGlslSource(glsl: string): ShaderValidationResult {
212
+ const errors: string[] = [];
213
+ if (typeof glsl !== "string" || glsl.trim().length === 0) {
214
+ return { valid: false, errors: ["GLSL source is empty"] };
215
+ }
216
+ if (glsl.length > MAX_GLSL_LENGTH) {
217
+ errors.push(
218
+ `GLSL source is ${glsl.length} chars — max is ${MAX_GLSL_LENGTH}`,
219
+ );
220
+ }
221
+ if (!/void\s+main\s*\(/.test(glsl)) {
222
+ errors.push("GLSL source must define void main()");
223
+ }
224
+ if (!/gl_FragColor/.test(glsl)) {
225
+ errors.push("GLSL source must write gl_FragColor");
226
+ }
227
+ // An opening `<script` tag is a real injection vector (it starts a brand
228
+ // new, arbitrary script rather than merely closing the current block), so
229
+ // it stays a hard rejection. A closing `</script` on its own — e.g. inside
230
+ // a GLSL line comment describing script-tag breakouts — is no longer
231
+ // rejected here: serializeShaderScriptBlock escapes it
232
+ // (escapeShaderScriptBreakout) before embedding, and parseShaderBlockBody
233
+ // reverses that escape, so the literal sequence never reaches the emitted
234
+ // HTML unescaped.
235
+ if (/<script/i.test(glsl)) {
236
+ errors.push("GLSL source must not contain an opening script tag");
237
+ }
238
+ // eslint-disable-next-line no-control-regex
239
+ if (/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/.test(glsl)) {
240
+ errors.push("GLSL source contains control characters");
241
+ }
242
+ return { valid: errors.length === 0, errors };
243
+ }
244
+
245
+ /** Validate a uniforms manifest (names, types, values, ranges). */
246
+ export function validateUniformManifest(
247
+ uniforms: GlslUniformManifest,
248
+ ): ShaderValidationResult {
249
+ const errors: string[] = [];
250
+ if (!uniforms || typeof uniforms !== "object") {
251
+ return { valid: false, errors: ["uniforms manifest must be an object"] };
252
+ }
253
+ const names = Object.keys(uniforms);
254
+ if (names.length > 16) {
255
+ errors.push(`too many uniforms (${names.length}) — max is 16`);
256
+ }
257
+ for (const name of names) {
258
+ const def = uniforms[name];
259
+ if (!UNIFORM_NAME_RE.test(name)) {
260
+ errors.push(
261
+ `uniform "${name}" must match u_[A-Za-z0-9_]+ (max 50 chars)`,
262
+ );
263
+ continue;
264
+ }
265
+ if ((SHADER_BUILTIN_UNIFORMS as readonly string[]).includes(name)) {
266
+ errors.push(
267
+ `uniform "${name}" is a built-in provided by the runtime — ` +
268
+ "do not declare it in the manifest",
269
+ );
270
+ continue;
271
+ }
272
+ if (!def || typeof def !== "object") {
273
+ errors.push(`uniform "${name}" definition must be an object`);
274
+ continue;
275
+ }
276
+ if (def.type === "float") {
277
+ if (typeof def.value !== "number" || !isFinite(def.value)) {
278
+ errors.push(`uniform "${name}" value must be a finite number`);
279
+ } else {
280
+ if (def.min !== undefined && def.value < def.min) {
281
+ errors.push(`uniform "${name}" value is below its min`);
282
+ }
283
+ if (def.max !== undefined && def.value > def.max) {
284
+ errors.push(`uniform "${name}" value is above its max`);
285
+ }
286
+ }
287
+ if (
288
+ def.min !== undefined &&
289
+ def.max !== undefined &&
290
+ !(def.min < def.max)
291
+ ) {
292
+ errors.push(`uniform "${name}" needs min < max`);
293
+ }
294
+ if (def.step !== undefined && !(def.step > 0)) {
295
+ errors.push(`uniform "${name}" step must be > 0`);
296
+ }
297
+ } else if (def.type === "vec2") {
298
+ const v = def.value;
299
+ if (
300
+ !Array.isArray(v) ||
301
+ v.length !== 2 ||
302
+ typeof v[0] !== "number" ||
303
+ typeof v[1] !== "number" ||
304
+ !isFinite(v[0]) ||
305
+ !isFinite(v[1])
306
+ ) {
307
+ errors.push(`uniform "${name}" value must be [x, y] finite numbers`);
308
+ }
309
+ } else if (def.type === "color") {
310
+ if (
311
+ typeof def.value !== "string" ||
312
+ !/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(def.value)
313
+ ) {
314
+ errors.push(
315
+ `uniform "${name}" value must be a hex color like "#1a2b3c"`,
316
+ );
317
+ }
318
+ } else {
319
+ errors.push(
320
+ `uniform "${name}" has unknown type "${String(
321
+ (def as { type?: unknown }).type,
322
+ )}" — use float, vec2, or color`,
323
+ );
324
+ }
325
+ if (def.label !== undefined) {
326
+ if (typeof def.label !== "string" || def.label.length > 40) {
327
+ errors.push(`uniform "${name}" label must be a string ≤ 40 chars`);
328
+ } else if (def.label.includes("*/") || /<\/?script/i.test(def.label)) {
329
+ errors.push(`uniform "${name}" label contains forbidden sequences`);
330
+ }
331
+ }
332
+ }
333
+ return { valid: errors.length === 0, errors };
334
+ }
335
+
336
+ /**
337
+ * Validate a full shader definition, including the cross-check that every
338
+ * manifest uniform is actually declared in the GLSL with the matching type
339
+ * (float → float, vec2 → vec2, color → vec3) so every knob provably drives
340
+ * the shader.
341
+ */
342
+ export function validateShaderDef(def: GlslShaderDef): ShaderValidationResult {
343
+ const errors: string[] = [];
344
+ if (!def || typeof def !== "object") {
345
+ return { valid: false, errors: ["shader definition must be an object"] };
346
+ }
347
+ if (typeof def.id !== "string" || !ID_RE.test(def.id)) {
348
+ errors.push(
349
+ 'shader id must match [A-Za-z0-9_-]{1,64} (e.g. "an-shader-x1y2z3")',
350
+ );
351
+ }
352
+ if (
353
+ typeof def.name !== "string" ||
354
+ def.name.trim().length === 0 ||
355
+ def.name.length > 80
356
+ ) {
357
+ errors.push("shader name must be a non-empty string ≤ 80 chars");
358
+ } else if (def.name.includes("*/") || /<\/?script/i.test(def.name)) {
359
+ errors.push("shader name contains forbidden sequences");
360
+ }
361
+ if (def.mode !== "fill" && def.mode !== "effect") {
362
+ errors.push('shader mode must be "fill" or "effect"');
363
+ }
364
+ const glslResult = validateGlslSource(def.glsl);
365
+ errors.push(...glslResult.errors);
366
+ const manifestResult = validateUniformManifest(def.uniforms ?? {});
367
+ errors.push(...manifestResult.errors);
368
+
369
+ if (glslResult.valid && manifestResult.valid) {
370
+ for (const [name, u] of Object.entries(def.uniforms ?? {})) {
371
+ const glslType = GLSL_TYPE_FOR_UNIFORM[u.type];
372
+ const declRe = new RegExp(
373
+ "uniform\\s+(?:lowp\\s+|mediump\\s+|highp\\s+)?" +
374
+ glslType +
375
+ "\\s+" +
376
+ escapeRegExp(name) +
377
+ "\\s*;",
378
+ );
379
+ if (!declRe.test(def.glsl)) {
380
+ errors.push(
381
+ `uniform "${name}" (${u.type}) is not declared in the GLSL as ` +
382
+ `"uniform ${glslType} ${name};" — every knob must drive the shader`,
383
+ );
384
+ }
385
+ }
386
+ }
387
+ return { valid: errors.length === 0, errors };
388
+ }
389
+
390
+ // ---------------------------------------------------------------------------
391
+ // Manifest comment + script block serialization
392
+ // ---------------------------------------------------------------------------
393
+
394
+ const MANIFEST_COMMENT_RE = /^\s*\/\*!\s*an-shader\s+v(\d+)\s*([\s\S]*?)\*\//;
395
+
396
+ /** Serialize the manifest as the leading GLSL block comment. */
397
+ export function serializeManifestComment(
398
+ uniforms: GlslUniformManifest,
399
+ ): string {
400
+ const json = JSON.stringify({ uniforms }, null, 2);
401
+ if (json.includes("*/")) {
402
+ throw new Error("uniforms manifest must not contain */");
403
+ }
404
+ return "/*! an-shader v" + SHADER_MANIFEST_VERSION + "\n" + json + "\n*/";
405
+ }
406
+
407
+ /**
408
+ * Escape/unescape pair guarding against a `</script` breakout inside a
409
+ * serialized GLSL body.
410
+ *
411
+ * `def.glsl` is caller-controlled (agent/user-edited GLSL source) and is
412
+ * embedded RAW inside a `<script type="application/x-agent-native-shader">`
413
+ * block. `validateGlslSource` already rejects a literal `</script` at the
414
+ * point a shader definition is validated/applied — but `serializeShaderScriptBlock`
415
+ * is also reachable with defs that were constructed without going through
416
+ * that validation (e.g. directly in tests, or future callers), and stored
417
+ * legacy content may already contain the substring. Escaping here is
418
+ * defense in depth: even if an unescaped `</script` reaches this function,
419
+ * the emitted HTML never contains a literal closing-script-tag sequence
420
+ * inside the block body, so the surrounding document can't be corrupted.
421
+ *
422
+ * Approach: replace `</script` (case-insensitive) with `<\/script` — the
423
+ * same backslash-escape idiom used for embedding JS strings inside
424
+ * `<script>` blocks. It's a minimal, transform-invariant edit (inserts one
425
+ * `\` character) that is trivial to reverse exactly on parse.
426
+ */
427
+ export function escapeShaderScriptBreakout(glsl: string): string {
428
+ return glsl.replace(
429
+ /<\/script/gi,
430
+ (match) => match.slice(0, 1) + "\\" + match.slice(1),
431
+ );
432
+ }
433
+
434
+ /** Reverse {@link escapeShaderScriptBreakout} — restores the original GLSL. */
435
+ export function unescapeShaderScriptBreakout(glsl: string): string {
436
+ return glsl.replace(
437
+ /<\\\/script/gi,
438
+ (match) => match.slice(0, 1) + match.slice(2),
439
+ );
440
+ }
441
+
442
+ /**
443
+ * Split a script-block body into { uniforms, glsl }. Missing/malformed
444
+ * manifests degrade to an empty manifest with the full body as GLSL — the
445
+ * shader still renders, it just has no knobs.
446
+ *
447
+ * The raw captured body may contain an escaped `</script` breakout marker
448
+ * (see {@link escapeShaderScriptBreakout}); it is unescaped here, after
449
+ * manifest-comment splitting, so callers always get back the original,
450
+ * editable GLSL source.
451
+ */
452
+ export function parseShaderBlockBody(body: string): {
453
+ uniforms: GlslUniformManifest;
454
+ glsl: string;
455
+ } {
456
+ const match = MANIFEST_COMMENT_RE.exec(body);
457
+ if (!match) {
458
+ return {
459
+ uniforms: {},
460
+ glsl: unescapeShaderScriptBreakout(
461
+ body.replace(/^[ \t]*\r?\n/, "").trim(),
462
+ ),
463
+ };
464
+ }
465
+ let uniforms: GlslUniformManifest = {};
466
+ try {
467
+ const meta = JSON.parse(match[2]) as { uniforms?: GlslUniformManifest };
468
+ if (meta && typeof meta === "object" && meta.uniforms) {
469
+ uniforms = meta.uniforms;
470
+ }
471
+ } catch {
472
+ uniforms = {};
473
+ }
474
+ const glsl = unescapeShaderScriptBreakout(
475
+ body
476
+ .slice(match.index + match[0].length)
477
+ .replace(/^[ \t]*\r?\n/, "")
478
+ .trim(),
479
+ );
480
+ return { uniforms, glsl };
481
+ }
482
+
483
+ /** Serialize a full `<script type="application/x-agent-native-shader">` block. */
484
+ export function serializeShaderScriptBlock(def: GlslShaderDef): string {
485
+ const validation = validateShaderDef(def);
486
+ if (!validation.valid) {
487
+ throw new Error(
488
+ "Invalid shader definition: " + validation.errors.join("; "),
489
+ );
490
+ }
491
+ return (
492
+ `<script type="${SHADER_SCRIPT_TYPE}"` +
493
+ ` data-shader-id="${escapeAttr(def.id)}"` +
494
+ ` data-shader-name="${escapeAttr(def.name)}"` +
495
+ ` data-shader-mode="${def.mode}">\n` +
496
+ serializeManifestComment(def.uniforms) +
497
+ "\n" +
498
+ escapeShaderScriptBreakout(def.glsl.trim()) +
499
+ "\n</script>"
500
+ );
501
+ }
502
+
503
+ const SHADER_BLOCK_RE = new RegExp(
504
+ '<script\\b([^>]*type\\s*=\\s*"' +
505
+ escapeRegExp(SHADER_SCRIPT_TYPE) +
506
+ '"[^>]*)>([\\s\\S]*?)<\\/script\\s*>',
507
+ "gi",
508
+ );
509
+
510
+ function readAttrFrom(attrs: string, name: string): string | null {
511
+ const match = new RegExp(name + '\\s*=\\s*"([^"]*)"', "i").exec(attrs);
512
+ return match ? unescapeAttr(match[1]) : null;
513
+ }
514
+
515
+ /** Parse every shader definition block out of a screen HTML document. */
516
+ export function listShadersInHtml(html: string): GlslShaderDef[] {
517
+ const defs: GlslShaderDef[] = [];
518
+ if (typeof html !== "string" || html.length === 0) return defs;
519
+ SHADER_BLOCK_RE.lastIndex = 0;
520
+ let match: RegExpExecArray | null;
521
+ while ((match = SHADER_BLOCK_RE.exec(html))) {
522
+ const attrs = match[1];
523
+ const id = readAttrFrom(attrs, "data-shader-id");
524
+ if (!id || !ID_RE.test(id)) continue;
525
+ const body = parseShaderBlockBody(match[2]);
526
+ defs.push({
527
+ id,
528
+ name: readAttrFrom(attrs, "data-shader-name") || id,
529
+ mode:
530
+ readAttrFrom(attrs, "data-shader-mode") === "effect"
531
+ ? "effect"
532
+ : "fill",
533
+ glsl: body.glsl,
534
+ uniforms: body.uniforms,
535
+ });
536
+ }
537
+ return defs;
538
+ }
539
+
540
+ /** Find one shader definition by id. */
541
+ export function getShaderFromHtml(
542
+ html: string,
543
+ id: string,
544
+ ): GlslShaderDef | undefined {
545
+ return listShadersInHtml(html).find((def) => def.id === id);
546
+ }
547
+
548
+ function findShaderBlockSpan(
549
+ html: string,
550
+ id: string,
551
+ ): { start: number; end: number } | null {
552
+ SHADER_BLOCK_RE.lastIndex = 0;
553
+ let match: RegExpExecArray | null;
554
+ while ((match = SHADER_BLOCK_RE.exec(html))) {
555
+ if (readAttrFrom(match[1], "data-shader-id") === id) {
556
+ return { start: match.index, end: match.index + match[0].length };
557
+ }
558
+ }
559
+ return null;
560
+ }
561
+
562
+ /** Insert or replace a shader definition block in the document. */
563
+ export function upsertShaderInHtml(html: string, def: GlslShaderDef): string {
564
+ const block = serializeShaderScriptBlock(def);
565
+ const existing = findShaderBlockSpan(html, def.id);
566
+ if (existing) {
567
+ return html.slice(0, existing.start) + block + html.slice(existing.end);
568
+ }
569
+ const bodyClose = html.toLowerCase().lastIndexOf("</body>");
570
+ if (bodyClose !== -1) {
571
+ return html.slice(0, bodyClose) + block + "\n" + html.slice(bodyClose);
572
+ }
573
+ const htmlClose = html.toLowerCase().lastIndexOf("</html>");
574
+ if (htmlClose !== -1) {
575
+ return html.slice(0, htmlClose) + block + "\n" + html.slice(htmlClose);
576
+ }
577
+ return html + "\n" + block + "\n";
578
+ }
579
+
580
+ /** Remove a shader definition block (references are left untouched). */
581
+ export function removeShaderFromHtml(html: string, id: string): string {
582
+ const span = findShaderBlockSpan(html, id);
583
+ if (!span) return html;
584
+ let start = span.start;
585
+ // Also consume the preceding line's indentation/newline for tidiness.
586
+ while (start > 0 && (html[start - 1] === " " || html[start - 1] === "\t")) {
587
+ start--;
588
+ }
589
+ let end = span.end;
590
+ if (html[end] === "\r") end++;
591
+ if (html[end] === "\n") end++;
592
+ return html.slice(0, start) + html.slice(end);
593
+ }
594
+
595
+ // ---------------------------------------------------------------------------
596
+ // Runtime embedding
597
+ // ---------------------------------------------------------------------------
598
+
599
+ /** The compiled, self-contained WebGL runtime (see shader-runtime.bridge.ts). */
600
+ export const SHADER_RUNTIME_SOURCE: string = shaderRuntimeBridgeScript;
601
+
602
+ export function buildShaderRuntimeScriptTag(): string {
603
+ return (
604
+ `<script ${SHADER_RUNTIME_ATTR} data-runtime-version="${SHADER_RUNTIME_VERSION}">\n` +
605
+ SHADER_RUNTIME_SOURCE +
606
+ "\n</script>"
607
+ );
608
+ }
609
+
610
+ const RUNTIME_BLOCK_RE = new RegExp(
611
+ "<script\\s+" + SHADER_RUNTIME_ATTR + "[^>]*>[\\s\\S]*?<\\/script\\s*>",
612
+ "i",
613
+ );
614
+
615
+ /**
616
+ * Ensure the document embeds the current shader runtime exactly once,
617
+ * upgrading any previously-embedded copy in place. Idempotent.
618
+ */
619
+ export function ensureShaderRuntime(html: string): string {
620
+ const tag = buildShaderRuntimeScriptTag();
621
+ const existing = RUNTIME_BLOCK_RE.exec(html);
622
+ if (existing) {
623
+ if (existing[0] === tag) return html;
624
+ return (
625
+ html.slice(0, existing.index) +
626
+ tag +
627
+ html.slice(existing.index + existing[0].length)
628
+ );
629
+ }
630
+ const bodyClose = html.toLowerCase().lastIndexOf("</body>");
631
+ if (bodyClose !== -1) {
632
+ return html.slice(0, bodyClose) + tag + "\n" + html.slice(bodyClose);
633
+ }
634
+ const htmlClose = html.toLowerCase().lastIndexOf("</html>");
635
+ if (htmlClose !== -1) {
636
+ return html.slice(0, htmlClose) + tag + "\n" + html.slice(htmlClose);
637
+ }
638
+ return html + "\n" + tag + "\n";
639
+ }
640
+
641
+ /** True when the document has no shader references left. */
642
+ export function htmlHasShaderReferences(html: string): boolean {
643
+ return (
644
+ html.includes(SHADER_FILL_ATTR + '="') ||
645
+ html.includes(SHADER_EFFECT_ATTR + '="')
646
+ );
647
+ }
648
+
649
+ /** Remove definition blocks that no element references any more. */
650
+ export function pruneUnusedShaders(html: string): string {
651
+ let out = html;
652
+ for (const def of listShadersInHtml(html)) {
653
+ const fillRef = `${SHADER_FILL_ATTR}="${escapeAttr(def.id)}"`;
654
+ const effectRef = `${SHADER_EFFECT_ATTR}="${escapeAttr(def.id)}"`;
655
+ if (!out.includes(fillRef) && !out.includes(effectRef)) {
656
+ out = removeShaderFromHtml(out, def.id);
657
+ }
658
+ }
659
+ return out;
660
+ }
661
+
662
+ // ---------------------------------------------------------------------------
663
+ // Element annotation (pure string transforms, no DOM)
664
+ // ---------------------------------------------------------------------------
665
+
666
+ /**
667
+ * Locate the bounds of the open tag containing the given index. Scans
668
+ * backward for "<" and forward for ">" while respecting quoted attribute
669
+ * values. Returns null when the index isn't inside a plausible tag.
670
+ */
671
+ function findTagBounds(
672
+ html: string,
673
+ indexInsideTag: number,
674
+ ): { start: number; end: number } | null {
675
+ let start = -1;
676
+ for (let i = indexInsideTag; i >= 0; i--) {
677
+ const ch = html[i];
678
+ if (ch === "<") {
679
+ start = i;
680
+ break;
681
+ }
682
+ if (ch === ">") return null;
683
+ }
684
+ if (start === -1) return null;
685
+ if (!/[a-zA-Z]/.test(html[start + 1] ?? "")) return null;
686
+ let quote: string | null = null;
687
+ for (let i = start + 1; i < html.length; i++) {
688
+ const ch = html[i];
689
+ if (quote) {
690
+ if (ch === quote) quote = null;
691
+ continue;
692
+ }
693
+ if (ch === '"' || ch === "'") {
694
+ quote = ch;
695
+ continue;
696
+ }
697
+ if (ch === ">") return { start, end: i + 1 };
698
+ }
699
+ return null;
700
+ }
701
+
702
+ function tagNameOf(tagText: string): string {
703
+ const match = /^<\s*([a-zA-Z][a-zA-Z0-9-]*)/.exec(tagText);
704
+ return match ? match[1].toLowerCase() : "";
705
+ }
706
+
707
+ function removeAttr(tagText: string, name: string): string {
708
+ return tagText.replace(
709
+ new RegExp("\\s+" + name + "\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s/>]+)", "gi"),
710
+ "",
711
+ );
712
+ }
713
+
714
+ /** Upsert one declaration inside an inline style attribute. */
715
+ function upsertStyleProperty(
716
+ tagText: string,
717
+ property: string,
718
+ value: string,
719
+ ): string {
720
+ const styleRe = /(\sstyle\s*=\s*")([^"]*)(")/i;
721
+ const styleMatch = styleRe.exec(tagText);
722
+ const decl = `${property}: ${value}`;
723
+ if (!styleMatch) {
724
+ const insertAt = tagText.endsWith("/>")
725
+ ? tagText.length - 2
726
+ : tagText.length - 1;
727
+ return (
728
+ tagText.slice(0, insertAt) +
729
+ ` style="${escapeAttr(decl)}"` +
730
+ tagText.slice(insertAt)
731
+ );
732
+ }
733
+ const existing = unescapeAttr(styleMatch[2]);
734
+ const kept = existing
735
+ .split(";")
736
+ .map((part) => part.trim())
737
+ .filter(
738
+ (part) =>
739
+ part.length > 0 &&
740
+ !new RegExp("^" + escapeRegExp(property) + "\\s*:", "i").test(part),
741
+ );
742
+ kept.push(decl);
743
+ const next = kept.join("; ");
744
+ return (
745
+ tagText.slice(0, styleMatch.index) +
746
+ styleMatch[1] +
747
+ escapeAttr(next) +
748
+ styleMatch[3] +
749
+ tagText.slice(styleMatch.index + styleMatch[0].length)
750
+ );
751
+ }
752
+
753
+ function locateNodeTag(
754
+ html: string,
755
+ nodeId: string,
756
+ ): { start: number; end: number; tagText: string } | null {
757
+ const marker = new RegExp(
758
+ 'data-agent-native-node-id\\s*=\\s*"' + escapeRegExp(nodeId) + '"',
759
+ ).exec(html);
760
+ if (!marker) return null;
761
+ const bounds = findTagBounds(html, marker.index);
762
+ if (!bounds) return null;
763
+ return {
764
+ start: bounds.start,
765
+ end: bounds.end,
766
+ tagText: html.slice(bounds.start, bounds.end),
767
+ };
768
+ }
769
+
770
+ export interface AnnotateShaderNodeOptions {
771
+ nodeId: string;
772
+ shaderId: string;
773
+ mode: GlslShaderMode;
774
+ /** Per-element uniform overrides serialized to data-an-shader-uniforms. */
775
+ values?: Record<string, GlslUniformValue>;
776
+ /**
777
+ * Static fallback background written to the element's inline style
778
+ * (fill mode only). Unsafe values are neutralized to #808080. When
779
+ * omitted, the element's existing background is left untouched — pass it
780
+ * on the initial apply, omit it for knob-value-only updates.
781
+ */
782
+ fallbackColor?: string;
783
+ }
784
+
785
+ export interface HtmlTransformResult {
786
+ html: string;
787
+ changed: boolean;
788
+ errors: string[];
789
+ }
790
+
791
+ /**
792
+ * Point an element (located by its stable data-agent-native-node-id) at a
793
+ * shader definition. Replaces any previous shader annotation OF THE SAME
794
+ * MODE on the node — a fill and an effect can coexist on one element,
795
+ * mirroring Figma's paint/effect stacking.
796
+ */
797
+ export function annotateNodeWithShader(
798
+ html: string,
799
+ options: AnnotateShaderNodeOptions,
800
+ ): HtmlTransformResult {
801
+ const errors: string[] = [];
802
+ if (!ID_RE.test(options.shaderId)) {
803
+ return { html, changed: false, errors: ["invalid shader id"] };
804
+ }
805
+ const located = locateNodeTag(html, options.nodeId);
806
+ if (!located) {
807
+ return {
808
+ html,
809
+ changed: false,
810
+ errors: [
811
+ `no element with data-agent-native-node-id="${options.nodeId}" found`,
812
+ ],
813
+ };
814
+ }
815
+ const tagName = tagNameOf(located.tagText);
816
+ if (VOID_TAGS.has(tagName)) {
817
+ return {
818
+ html,
819
+ changed: false,
820
+ errors: [
821
+ `<${tagName}> cannot host a shader canvas — wrap it in a container ` +
822
+ "element and apply the shader there",
823
+ ],
824
+ };
825
+ }
826
+
827
+ const isEffect = options.mode === "effect";
828
+ const refAttr = isEffect ? SHADER_EFFECT_ATTR : SHADER_FILL_ATTR;
829
+ const valuesAttr = isEffect
830
+ ? SHADER_EFFECT_UNIFORMS_ATTR
831
+ : SHADER_UNIFORMS_ATTR;
832
+
833
+ let tagText = located.tagText;
834
+ tagText = removeAttr(tagText, refAttr);
835
+ tagText = removeAttr(tagText, valuesAttr);
836
+
837
+ let insertion = ` ${refAttr}="${escapeAttr(options.shaderId)}"`;
838
+ if (options.values && Object.keys(options.values).length > 0) {
839
+ const json = JSON.stringify(options.values);
840
+ insertion += ` ${valuesAttr}="${escapeAttr(json)}"`;
841
+ }
842
+
843
+ const selfClosing = tagText.endsWith("/>");
844
+ const insertAt = selfClosing ? tagText.length - 2 : tagText.length - 1;
845
+ tagText = tagText.slice(0, insertAt) + insertion + tagText.slice(insertAt);
846
+
847
+ if (options.mode === "fill" && options.fallbackColor !== undefined) {
848
+ tagText = upsertStyleProperty(
849
+ tagText,
850
+ "background",
851
+ sanitizeShaderFallbackColor(options.fallbackColor),
852
+ );
853
+ }
854
+
855
+ const nextHtml =
856
+ html.slice(0, located.start) + tagText + html.slice(located.end);
857
+ return { html: nextHtml, changed: nextHtml !== html, errors };
858
+ }
859
+
860
+ /**
861
+ * Remove shader annotations from an element (fallback background stays).
862
+ * Pass `mode` to clear only the fill or only the effect; omit to clear both.
863
+ */
864
+ export function clearNodeShader(
865
+ html: string,
866
+ nodeId: string,
867
+ mode?: GlslShaderMode,
868
+ ): HtmlTransformResult {
869
+ const located = locateNodeTag(html, nodeId);
870
+ if (!located) {
871
+ return {
872
+ html,
873
+ changed: false,
874
+ errors: [`no element with data-agent-native-node-id="${nodeId}" found`],
875
+ };
876
+ }
877
+ let tagText = located.tagText;
878
+ if (mode !== "effect") {
879
+ tagText = removeAttr(tagText, SHADER_FILL_ATTR);
880
+ tagText = removeAttr(tagText, SHADER_UNIFORMS_ATTR);
881
+ }
882
+ if (mode !== "fill") {
883
+ tagText = removeAttr(tagText, SHADER_EFFECT_ATTR);
884
+ tagText = removeAttr(tagText, SHADER_EFFECT_UNIFORMS_ATTR);
885
+ }
886
+ const nextHtml =
887
+ html.slice(0, located.start) + tagText + html.slice(located.end);
888
+ return { html: nextHtml, changed: nextHtml !== html, errors: [] };
889
+ }
890
+
891
+ /** Parse every element ↔ shader reference out of the document. */
892
+ export function listShaderMounts(html: string): GlslShaderMountRef[] {
893
+ const mounts: GlslShaderMountRef[] = [];
894
+ if (typeof html !== "string" || html.length === 0) return mounts;
895
+ const refRe = new RegExp(
896
+ "\\s(" +
897
+ SHADER_FILL_ATTR +
898
+ "|" +
899
+ SHADER_EFFECT_ATTR +
900
+ ')\\s*=\\s*"([^"]*)"',
901
+ "g",
902
+ );
903
+ let match: RegExpExecArray | null;
904
+ while ((match = refRe.exec(html))) {
905
+ const bounds = findTagBounds(html, match.index + 1);
906
+ if (!bounds) continue;
907
+ const tagText = html.slice(bounds.start, bounds.end);
908
+ const shaderId = unescapeAttr(match[2]);
909
+ if (!ID_RE.test(shaderId)) continue;
910
+ const mode: GlslShaderMode =
911
+ match[1] === SHADER_EFFECT_ATTR ? "effect" : "fill";
912
+ const valuesRaw = readAttrFrom(
913
+ tagText,
914
+ mode === "effect" ? SHADER_EFFECT_UNIFORMS_ATTR : SHADER_UNIFORMS_ATTR,
915
+ );
916
+ let values: Record<string, GlslUniformValue> | undefined;
917
+ if (valuesRaw) {
918
+ try {
919
+ const parsed = JSON.parse(valuesRaw) as Record<
920
+ string,
921
+ GlslUniformValue
922
+ >;
923
+ if (parsed && typeof parsed === "object") values = parsed;
924
+ } catch {
925
+ values = undefined;
926
+ }
927
+ }
928
+ mounts.push({
929
+ nodeId: readAttrFrom(tagText, "data-agent-native-node-id"),
930
+ shaderId,
931
+ mode,
932
+ values,
933
+ });
934
+ }
935
+ return mounts;
936
+ }
937
+
938
+ // ---------------------------------------------------------------------------
939
+ // High-level apply (the one-call transform the inspector + agent use)
940
+ // ---------------------------------------------------------------------------
941
+
942
+ export interface ApplyShaderOptions {
943
+ nodeId: string;
944
+ def: GlslShaderDef;
945
+ /** Per-element uniform value overrides. */
946
+ values?: Record<string, GlslUniformValue>;
947
+ /** Fill-mode static fallback background color. */
948
+ fallbackColor?: string;
949
+ }
950
+
951
+ /**
952
+ * The complete persistence transform: upsert the definition block, embed or
953
+ * upgrade the runtime, and annotate the target element. Pure string → string;
954
+ * callers persist the result through their normal write path
955
+ * (apply-source-edit / edit-design / collab).
956
+ */
957
+ export function applyShaderToHtml(
958
+ html: string,
959
+ options: ApplyShaderOptions,
960
+ ): HtmlTransformResult {
961
+ const validation = validateShaderDef(options.def);
962
+ if (!validation.valid) {
963
+ return { html, changed: false, errors: validation.errors };
964
+ }
965
+ let next = upsertShaderInHtml(html, options.def);
966
+ next = ensureShaderRuntime(next);
967
+ const annotated = annotateNodeWithShader(next, {
968
+ nodeId: options.nodeId,
969
+ shaderId: options.def.id,
970
+ mode: options.def.mode,
971
+ values: options.values,
972
+ fallbackColor: options.fallbackColor,
973
+ });
974
+ if (annotated.errors.length > 0) {
975
+ return { html, changed: false, errors: annotated.errors };
976
+ }
977
+ return {
978
+ html: annotated.html,
979
+ changed: annotated.html !== html,
980
+ errors: [],
981
+ };
982
+ }
983
+
984
+ /**
985
+ * Remove a shader from an element and garbage-collect any now-unreferenced
986
+ * definition blocks (the runtime tag stays — it is inert without mounts).
987
+ * Pass `mode` to remove only the fill or only the effect; omit for both.
988
+ */
989
+ export function removeShaderFromNode(
990
+ html: string,
991
+ nodeId: string,
992
+ mode?: GlslShaderMode,
993
+ ): HtmlTransformResult {
994
+ const cleared = clearNodeShader(html, nodeId, mode);
995
+ if (cleared.errors.length > 0) return cleared;
996
+ const pruned = pruneUnusedShaders(cleared.html);
997
+ return { html: pruned, changed: pruned !== html, errors: [] };
998
+ }
999
+
1000
+ /** Default uniform values (manifest values) for fresh knob state. */
1001
+ export function defaultUniformValues(
1002
+ def: GlslShaderDef,
1003
+ ): Record<string, GlslUniformValue> {
1004
+ const out: Record<string, GlslUniformValue> = {};
1005
+ for (const [name, u] of Object.entries(def.uniforms)) {
1006
+ out[name] = u.value;
1007
+ }
1008
+ return out;
1009
+ }