@agent-native/core 0.79.27 → 0.80.0

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 (346) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +26 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +41 -0
  4. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +51 -0
  5. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +50 -0
  6. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +49 -0
  7. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +44 -0
  8. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +46 -0
  9. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +44 -0
  10. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +48 -0
  11. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +36 -0
  12. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +38 -0
  13. package/corpus/core/docs/content/template-design.mdx +45 -0
  14. package/corpus/core/package.json +2 -1
  15. package/corpus/core/src/agent/engine/index.ts +1 -0
  16. package/corpus/core/src/agent/engine/registry.ts +40 -0
  17. package/corpus/core/src/agent/production-agent.ts +6 -2
  18. package/corpus/core/src/cli/design-connect.ts +490 -0
  19. package/corpus/core/src/cli/index.ts +16 -0
  20. package/corpus/core/src/cli/skills.ts +126 -3
  21. package/corpus/core/src/client/AssistantChat.tsx +43 -7
  22. package/corpus/core/src/client/agent-chat.ts +27 -3
  23. package/corpus/core/src/client/blocks/library/MermaidBlock.tsx +1 -1
  24. package/corpus/core/src/client/blocks/library/diagram.tsx +2 -2
  25. package/corpus/core/src/client/blocks/library/wireframe.tsx +1 -1
  26. package/corpus/core/src/client/chat/message-components.tsx +1 -1
  27. package/corpus/core/src/client/chat-model-groups.ts +1 -2
  28. package/corpus/core/src/client/components/LiveCursorOverlay.tsx +33 -66
  29. package/corpus/core/src/client/composer/TiptapComposer.tsx +99 -24
  30. package/corpus/core/src/client/composer/useVoiceDictation.ts +429 -260
  31. package/corpus/core/src/client/frame-protocol.ts +17 -2
  32. package/corpus/core/src/client/rich-markdown-editor/RegistryBlockNode.tsx +2 -2
  33. package/corpus/core/src/client/sharing/ShareButton.tsx +20 -15
  34. package/corpus/core/src/client/use-agent-engine-configured.ts +80 -28
  35. package/corpus/core/src/server/transcribe-voice.ts +57 -22
  36. package/corpus/core/src/shared/mcp-embed-headers.ts +23 -1
  37. package/corpus/core/src/styles/agent-native.css +56 -7
  38. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  39. package/corpus/core/src/vite/client.ts +2 -0
  40. package/corpus/core/src/voice/index.ts +18 -0
  41. package/corpus/core/src/voice/voice-cleanup-prompt.ts +38 -0
  42. package/corpus/core/src/voice/voice-context.ts +217 -0
  43. package/corpus/core/src/voice/voice-replacements.ts +75 -0
  44. package/corpus/templates/analytics/AGENTS.md +4 -3
  45. package/corpus/templates/analytics/actions/list-session-recordings.ts +6 -1
  46. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +557 -400
  47. package/corpus/templates/analytics/app/global.css +0 -4
  48. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  49. package/corpus/templates/analytics/app/i18n-data.ts +20 -0
  50. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +33 -8
  51. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +7 -0
  52. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +7 -0
  53. package/corpus/templates/analytics/changelog/2026-06-28-ask-tab-now-matches-the-darker-app-background.md +6 -0
  54. package/corpus/templates/analytics/changelog/2026-06-28-dashboard-email-reports-now-use-cleaner-light-mode-snapshots.md +6 -0
  55. package/corpus/templates/analytics/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  56. package/corpus/templates/analytics/changelog/2026-06-28-sessions-now-show-only-signed-in-recordings-with-playable-re.md +6 -0
  57. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  58. package/corpus/templates/analytics/server/lib/dashboard-report.ts +29 -7
  59. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +5 -2
  60. package/corpus/templates/analytics/server/lib/session-replay.ts +24 -4
  61. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -4
  62. package/corpus/templates/assets/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  63. package/corpus/templates/brain/app/components/layout/Layout.tsx +29 -2
  64. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +131 -42
  65. package/corpus/templates/brain/app/i18n/zh-TW.ts +2 -0
  66. package/corpus/templates/brain/app/i18n-data.ts +20 -0
  67. package/corpus/templates/brain/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  68. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +29 -1
  69. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +417 -299
  70. package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
  71. package/corpus/templates/calendar/app/i18n-data.ts +20 -0
  72. package/corpus/templates/calendar/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  73. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +6 -19
  74. package/corpus/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  75. package/corpus/templates/clips/AGENTS.md +4 -0
  76. package/corpus/templates/clips/actions/cleanup-transcript.ts +39 -9
  77. package/corpus/templates/clips/actions/finalize-meeting.ts +81 -12
  78. package/corpus/templates/clips/actions/lib/agents-md-context.ts +4 -1
  79. package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -3
  80. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +1 -1
  81. package/corpus/templates/clips/app/components/player/share-dialog.tsx +2 -2
  82. package/corpus/templates/clips/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  83. package/corpus/templates/clips/desktop/src/lib/personal-vocabulary.ts +13 -8
  84. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +60 -1
  85. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1 -1
  86. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -1
  87. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -5
  88. package/corpus/templates/content/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  89. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +137 -0
  90. package/corpus/templates/design/AGENTS.md +95 -5
  91. package/corpus/templates/design/DESIGN.md +82 -0
  92. package/corpus/templates/design/DEVELOPING.md +43 -0
  93. package/corpus/templates/design/actions/add-localhost-screens.ts +426 -0
  94. package/corpus/templates/design/actions/apply-shader.ts +277 -0
  95. package/corpus/templates/design/actions/apply-visual-edit.ts +303 -0
  96. package/corpus/templates/design/actions/connect-localhost.ts +183 -0
  97. package/corpus/templates/design/actions/edit-design.ts +195 -42
  98. package/corpus/templates/design/actions/generate-design.ts +71 -1
  99. package/corpus/templates/design/actions/generate-screens.ts +193 -0
  100. package/corpus/templates/design/actions/get-code-layer-projection.ts +191 -0
  101. package/corpus/templates/design/actions/get-shader.ts +119 -0
  102. package/corpus/templates/design/actions/list-localhost-connections.ts +95 -0
  103. package/corpus/templates/design/actions/navigate.ts +55 -2
  104. package/corpus/templates/design/actions/view-screen.ts +68 -1
  105. package/corpus/templates/design/agent-native.app-skill.json +10 -0
  106. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +761 -0
  107. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +2057 -103
  108. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +74 -38
  109. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +399 -0
  110. package/corpus/templates/design/app/components/design/EditPanel.tsx +3185 -439
  111. package/corpus/templates/design/app/components/design/LayersPanel.tsx +1124 -0
  112. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +3116 -147
  113. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +700 -13
  114. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +107 -63
  115. package/corpus/templates/design/app/components/design/index.ts +0 -5
  116. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +286 -0
  117. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +699 -0
  118. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +461 -0
  119. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +166 -0
  120. package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +1420 -0
  121. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +420 -0
  122. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +215 -0
  123. package/corpus/templates/design/app/components/design/inspector/ShaderControls.stories.tsx +71 -0
  124. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +518 -0
  125. package/corpus/templates/design/app/components/design/inspector/ShaderEffectRow.tsx +106 -0
  126. package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +361 -0
  127. package/corpus/templates/design/app/components/design/inspector/index.ts +60 -0
  128. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +201 -0
  129. package/corpus/templates/design/app/components/design/types.ts +23 -0
  130. package/corpus/templates/design/app/components/layout/Layout.tsx +24 -13
  131. package/corpus/templates/design/app/components/layout/Sidebar.tsx +4 -4
  132. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +474 -36
  133. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +22 -5
  134. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +24 -11
  135. package/corpus/templates/design/app/global.css +121 -3
  136. package/corpus/templates/design/app/hooks/use-agent-generating.ts +4 -0
  137. package/corpus/templates/design/app/hooks/use-navigation-state.ts +130 -8
  138. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +329 -0
  139. package/corpus/templates/design/app/i18n/zh-TW.ts +264 -0
  140. package/corpus/templates/design/app/i18n-data.ts +2790 -1
  141. package/corpus/templates/design/app/pages/DesignEditor.tsx +5108 -851
  142. package/corpus/templates/design/changelog/2026-06-28-annotate-mode-now-combines-drawing-and-comment-pins-in-one-c.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-28-design-editor-panel-borders-are-cleaner-so-the-canvas-edge-s.md +6 -0
  144. package/corpus/templates/design/changelog/2026-06-28-drawing-prompts-now-reliably-open-and-submit-to-the-design-a.md +6 -0
  145. package/corpus/templates/design/changelog/2026-06-28-editor-app-menus-now-use-an-outline-agent-native-mark-with-b.md +6 -0
  146. package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-28-layer-selection-now-updates-chat-context-without-opening-the.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-28-layer-selections-now-preserve-hidden-and-locked-canvas-state.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-handles-zooming-drawing-opening-and-dele.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-selects-screens-on-click-and-opens-them-.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-28-screen-overview-zooms-into-the-active-screen-with-smoother-e.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-28-selected-element-chips-in-chat-are-shorter-and-easier-to-sca.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-28-the-design-editor-toolbar-is-more-compact-and-the-app-menu-m.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-at-the-top-of-the-design-sidebar-instead-of-.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-in-the-right-inspector-beside-the-design-tab.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-29-active-sidebar-rows-use-soft-highlights.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-29-agents-can-now-open-screen-overview-focus-screens-and-add.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-edit-broad-copy-changes-more-reliably.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-29-design-editor-extensions-now-run-inline-beside-the-artboard.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-29-sidebars-use-darker-chrome-and-chat-warns-immediately.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-29-visual-edit-can-open-localhost-routes-as-url-backed-screens.md +6 -0
  165. package/corpus/templates/design/package.json +1 -0
  166. package/corpus/templates/design/server/db/schema.ts +26 -0
  167. package/corpus/templates/design/server/lib/design-snapshot.ts +1 -1
  168. package/corpus/templates/design/server/plugins/agent-chat.ts +3 -0
  169. package/corpus/templates/design/shared/api.ts +27 -0
  170. package/corpus/templates/design/shared/canvas-frames.ts +103 -0
  171. package/corpus/templates/design/shared/canvas-math.ts +1058 -0
  172. package/corpus/templates/design/shared/code-layer.ts +2212 -0
  173. package/corpus/templates/design/shared/color-utils.ts +180 -0
  174. package/corpus/templates/design/shared/generation-session.ts +61 -0
  175. package/corpus/templates/design/shared/shader-presets.ts +782 -0
  176. package/corpus/templates/design/shared/shader-safety.ts +137 -0
  177. package/corpus/templates/design/shared/source-mode.ts +237 -0
  178. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +149 -6
  179. package/corpus/templates/forms/app/i18n/ar-SA.ts +2 -0
  180. package/corpus/templates/forms/app/i18n/de-DE.ts +2 -0
  181. package/corpus/templates/forms/app/i18n/en-US.ts +2 -0
  182. package/corpus/templates/forms/app/i18n/es-ES.ts +2 -0
  183. package/corpus/templates/forms/app/i18n/fr-FR.ts +2 -0
  184. package/corpus/templates/forms/app/i18n/hi-IN.ts +2 -0
  185. package/corpus/templates/forms/app/i18n/ja-JP.ts +2 -0
  186. package/corpus/templates/forms/app/i18n/ko-KR.ts +2 -0
  187. package/corpus/templates/forms/app/i18n/pt-BR.ts +2 -0
  188. package/corpus/templates/forms/app/i18n/zh-CN.ts +2 -0
  189. package/corpus/templates/forms/app/i18n/zh-TW.ts +2 -0
  190. package/corpus/templates/forms/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  191. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +2 -2
  192. package/corpus/templates/macros/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  193. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +408 -247
  194. package/corpus/templates/mail/app/i18n/ar-SA.ts +4 -0
  195. package/corpus/templates/mail/app/i18n/de-DE.ts +4 -0
  196. package/corpus/templates/mail/app/i18n/en-US.ts +4 -0
  197. package/corpus/templates/mail/app/i18n/es-ES.ts +4 -0
  198. package/corpus/templates/mail/app/i18n/fr-FR.ts +4 -0
  199. package/corpus/templates/mail/app/i18n/hi-IN.ts +4 -0
  200. package/corpus/templates/mail/app/i18n/ja-JP.ts +4 -0
  201. package/corpus/templates/mail/app/i18n/ko-KR.ts +4 -0
  202. package/corpus/templates/mail/app/i18n/pt-BR.ts +4 -0
  203. package/corpus/templates/mail/app/i18n/zh-CN.ts +4 -0
  204. package/corpus/templates/mail/app/i18n/zh-TW.ts +4 -0
  205. package/corpus/templates/mail/changelog/2026-06-28-pinned-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  206. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +6 -8
  207. package/corpus/templates/plan/app/components/plan/wireframe/Wireframe.tsx +1 -1
  208. package/corpus/templates/plan/app/global.css +61 -78
  209. package/corpus/templates/plan/app/pages/PlansPage.tsx +8 -0
  210. package/corpus/templates/plan/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  211. package/corpus/templates/plan/changelog/2026-06-28-plans-use-the-shared-neutral-light-and-dark-theme.md +6 -0
  212. package/corpus/templates/plan/changelog/2026-06-28-sending-open-feedback-from-a-plan-now-opens-the-inline-plan-.md +6 -0
  213. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +5 -5
  214. package/corpus/templates/slides/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  215. package/corpus/templates/videos/app/components/Sidebar.tsx +2 -2
  216. package/corpus/templates/videos/app/components/layout/Layout.tsx +29 -2
  217. package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +113 -30
  218. package/corpus/templates/videos/app/i18n/ar-SA.ts +2 -0
  219. package/corpus/templates/videos/app/i18n/de-DE.ts +2 -0
  220. package/corpus/templates/videos/app/i18n/en-US.ts +2 -0
  221. package/corpus/templates/videos/app/i18n/es-ES.ts +2 -0
  222. package/corpus/templates/videos/app/i18n/fr-FR.ts +2 -0
  223. package/corpus/templates/videos/app/i18n/hi-IN.ts +2 -0
  224. package/corpus/templates/videos/app/i18n/ja-JP.ts +2 -0
  225. package/corpus/templates/videos/app/i18n/ko-KR.ts +2 -0
  226. package/corpus/templates/videos/app/i18n/pt-BR.ts +2 -0
  227. package/corpus/templates/videos/app/i18n/zh-CN.ts +2 -0
  228. package/corpus/templates/videos/app/i18n/zh-TW.ts +2 -0
  229. package/corpus/templates/videos/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  230. package/dist/agent/engine/index.d.ts +1 -1
  231. package/dist/agent/engine/index.d.ts.map +1 -1
  232. package/dist/agent/engine/index.js +1 -1
  233. package/dist/agent/engine/index.js.map +1 -1
  234. package/dist/agent/engine/registry.d.ts +9 -0
  235. package/dist/agent/engine/registry.d.ts.map +1 -1
  236. package/dist/agent/engine/registry.js +37 -0
  237. package/dist/agent/engine/registry.js.map +1 -1
  238. package/dist/agent/production-agent.d.ts.map +1 -1
  239. package/dist/agent/production-agent.js +4 -3
  240. package/dist/agent/production-agent.js.map +1 -1
  241. package/dist/cli/design-connect.d.ts +52 -0
  242. package/dist/cli/design-connect.d.ts.map +1 -0
  243. package/dist/cli/design-connect.js +409 -0
  244. package/dist/cli/design-connect.js.map +1 -0
  245. package/dist/cli/index.js +15 -0
  246. package/dist/cli/index.js.map +1 -1
  247. package/dist/cli/skills.d.ts +3 -0
  248. package/dist/cli/skills.d.ts.map +1 -1
  249. package/dist/cli/skills.js +121 -3
  250. package/dist/cli/skills.js.map +1 -1
  251. package/dist/client/AssistantChat.d.ts.map +1 -1
  252. package/dist/client/AssistantChat.js +38 -8
  253. package/dist/client/AssistantChat.js.map +1 -1
  254. package/dist/client/agent-chat.d.ts +6 -0
  255. package/dist/client/agent-chat.d.ts.map +1 -1
  256. package/dist/client/agent-chat.js +17 -3
  257. package/dist/client/agent-chat.js.map +1 -1
  258. package/dist/client/blocks/library/MermaidBlock.js +1 -1
  259. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  260. package/dist/client/blocks/library/diagram.js +1 -1
  261. package/dist/client/blocks/library/diagram.js.map +1 -1
  262. package/dist/client/blocks/library/wireframe.js +1 -1
  263. package/dist/client/blocks/library/wireframe.js.map +1 -1
  264. package/dist/client/chat/message-components.js +1 -1
  265. package/dist/client/chat/message-components.js.map +1 -1
  266. package/dist/client/chat-model-groups.d.ts.map +1 -1
  267. package/dist/client/chat-model-groups.js +1 -2
  268. package/dist/client/chat-model-groups.js.map +1 -1
  269. package/dist/client/components/LiveCursorOverlay.d.ts +1 -2
  270. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -1
  271. package/dist/client/components/LiveCursorOverlay.js +19 -40
  272. package/dist/client/components/LiveCursorOverlay.js.map +1 -1
  273. package/dist/client/composer/TiptapComposer.d.ts +3 -1
  274. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  275. package/dist/client/composer/TiptapComposer.js +86 -24
  276. package/dist/client/composer/TiptapComposer.js.map +1 -1
  277. package/dist/client/composer/useVoiceDictation.d.ts +9 -0
  278. package/dist/client/composer/useVoiceDictation.d.ts.map +1 -1
  279. package/dist/client/composer/useVoiceDictation.js +154 -32
  280. package/dist/client/composer/useVoiceDictation.js.map +1 -1
  281. package/dist/client/frame-protocol.d.ts +17 -2
  282. package/dist/client/frame-protocol.d.ts.map +1 -1
  283. package/dist/client/frame-protocol.js.map +1 -1
  284. package/dist/client/rich-markdown-editor/RegistryBlockNode.js +2 -2
  285. package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
  286. package/dist/client/sharing/ShareButton.d.ts +2 -0
  287. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  288. package/dist/client/sharing/ShareButton.js +7 -6
  289. package/dist/client/sharing/ShareButton.js.map +1 -1
  290. package/dist/client/use-agent-engine-configured.d.ts +5 -0
  291. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  292. package/dist/client/use-agent-engine-configured.js +51 -25
  293. package/dist/client/use-agent-engine-configured.js.map +1 -1
  294. package/dist/collab/awareness.d.ts +2 -2
  295. package/dist/collab/awareness.d.ts.map +1 -1
  296. package/dist/collab/routes.d.ts +1 -1
  297. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  298. package/dist/notifications/routes.d.ts +3 -3
  299. package/dist/observability/routes.d.ts +8 -8
  300. package/dist/progress/routes.d.ts +1 -1
  301. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  302. package/dist/server/transcribe-voice.d.ts.map +1 -1
  303. package/dist/server/transcribe-voice.js +40 -24
  304. package/dist/server/transcribe-voice.js.map +1 -1
  305. package/dist/shared/mcp-embed-headers.d.ts +1 -0
  306. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  307. package/dist/shared/mcp-embed-headers.js +20 -1
  308. package/dist/shared/mcp-embed-headers.js.map +1 -1
  309. package/dist/styles/agent-native.css +56 -7
  310. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  311. package/dist/vite/client.d.ts.map +1 -1
  312. package/dist/vite/client.js +2 -0
  313. package/dist/vite/client.js.map +1 -1
  314. package/dist/voice/index.d.ts +5 -0
  315. package/dist/voice/index.d.ts.map +1 -0
  316. package/dist/voice/index.js +4 -0
  317. package/dist/voice/index.js.map +1 -0
  318. package/dist/voice/voice-cleanup-prompt.d.ts +6 -0
  319. package/dist/voice/voice-cleanup-prompt.d.ts.map +1 -0
  320. package/dist/voice/voice-cleanup-prompt.js +26 -0
  321. package/dist/voice/voice-cleanup-prompt.js.map +1 -0
  322. package/dist/voice/voice-context.d.ts +25 -0
  323. package/dist/voice/voice-context.d.ts.map +1 -0
  324. package/dist/voice/voice-context.js +156 -0
  325. package/dist/voice/voice-context.js.map +1 -0
  326. package/dist/voice/voice-replacements.d.ts +4 -0
  327. package/dist/voice/voice-replacements.d.ts.map +1 -0
  328. package/dist/voice/voice-replacements.js +52 -0
  329. package/dist/voice/voice-replacements.js.map +1 -0
  330. package/docs/content/locales/ar-SA/template-design.mdx +41 -0
  331. package/docs/content/locales/de-DE/template-design.mdx +51 -0
  332. package/docs/content/locales/es-ES/template-design.mdx +50 -0
  333. package/docs/content/locales/fr-FR/template-design.mdx +49 -0
  334. package/docs/content/locales/hi-IN/template-design.mdx +44 -0
  335. package/docs/content/locales/ja-JP/template-design.mdx +46 -0
  336. package/docs/content/locales/ko-KR/template-design.mdx +44 -0
  337. package/docs/content/locales/pt-BR/template-design.mdx +48 -0
  338. package/docs/content/locales/zh-CN/template-design.mdx +36 -0
  339. package/docs/content/locales/zh-TW/template-design.mdx +38 -0
  340. package/docs/content/template-design.mdx +45 -0
  341. package/package.json +2 -1
  342. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  343. package/corpus/templates/design/app/components/design/DesignToolbar.tsx +0 -339
  344. package/corpus/templates/design/app/components/design/PresentMode.tsx +0 -68
  345. package/corpus/templates/design/app/components/design/ViewportTabs.tsx +0 -149
  346. package/corpus/templates/design/app/components/design/ZoomControls.tsx +0 -157
@@ -0,0 +1,2212 @@
1
+ import type { DesignSourceType } from "./source-mode";
2
+
3
+ export type CodeLayerSourceKind =
4
+ | "design-file"
5
+ | "inline-html"
6
+ | "local-file"
7
+ | "remote-url";
8
+
9
+ export interface CodeLayerSource {
10
+ kind: CodeLayerSourceKind;
11
+ sourceType?: DesignSourceType;
12
+ designId?: string;
13
+ fileId?: string;
14
+ filename?: string;
15
+ path?: string;
16
+ url?: string;
17
+ connectionId?: string;
18
+ routeId?: string;
19
+ artboardId?: string;
20
+ bridgeUrl?: string;
21
+ revision?: string;
22
+ }
23
+
24
+ export interface CodeLayerSourceSpan {
25
+ start: number;
26
+ end: number;
27
+ openStart: number;
28
+ openEnd: number;
29
+ contentStart?: number;
30
+ contentEnd?: number;
31
+ closeStart?: number;
32
+ closeEnd?: number;
33
+ }
34
+
35
+ export type VisualStyleProperty =
36
+ | "width"
37
+ | "height"
38
+ | "min-width"
39
+ | "max-width"
40
+ | "min-height"
41
+ | "max-height"
42
+ | "left"
43
+ | "top"
44
+ | "right"
45
+ | "bottom"
46
+ | "inset"
47
+ | "position"
48
+ | "display"
49
+ | "color"
50
+ | "background"
51
+ | "background-color"
52
+ | "background-image"
53
+ | "background-blend-mode"
54
+ | "fill"
55
+ | "fill-opacity"
56
+ | "opacity"
57
+ | "mix-blend-mode"
58
+ | "font-size"
59
+ | "font-weight"
60
+ | "font-family"
61
+ | "font-style"
62
+ | "letter-spacing"
63
+ | "line-height"
64
+ | "text-align"
65
+ | "text-decoration"
66
+ | "text-transform"
67
+ | "white-space"
68
+ | "overflow"
69
+ | "overflow-x"
70
+ | "overflow-y"
71
+ | "text-overflow"
72
+ | "border"
73
+ | "border-width"
74
+ | "border-style"
75
+ | "border-color"
76
+ | "border-radius"
77
+ | "border-top-left-radius"
78
+ | "border-top-right-radius"
79
+ | "border-bottom-left-radius"
80
+ | "border-bottom-right-radius"
81
+ | "stroke"
82
+ | "stroke-width"
83
+ | "stroke-opacity"
84
+ | "stroke-dasharray"
85
+ | "stroke-linecap"
86
+ | "stroke-linejoin"
87
+ | "outline"
88
+ | "outline-width"
89
+ | "outline-style"
90
+ | "outline-color"
91
+ | "outline-offset"
92
+ | "box-shadow"
93
+ | "text-shadow"
94
+ | "filter"
95
+ | "backdrop-filter"
96
+ | "transform"
97
+ | "transform-origin"
98
+ | "rotate"
99
+ | "scale"
100
+ | "translate"
101
+ | "padding"
102
+ | "padding-top"
103
+ | "padding-right"
104
+ | "padding-bottom"
105
+ | "padding-left"
106
+ | "margin"
107
+ | "margin-top"
108
+ | "margin-right"
109
+ | "margin-bottom"
110
+ | "margin-left"
111
+ | "gap"
112
+ | "row-gap"
113
+ | "column-gap"
114
+ | "flex"
115
+ | "flex-direction"
116
+ | "flex-wrap"
117
+ | "flex-grow"
118
+ | "flex-shrink"
119
+ | "flex-basis"
120
+ | "order"
121
+ | "align-self"
122
+ | "align-items"
123
+ | "align-content"
124
+ | "justify-content"
125
+ | "justify-items"
126
+ | "justify-self"
127
+ | "grid-column"
128
+ | "grid-row"
129
+ | "grid-template-columns"
130
+ | "grid-template-rows"
131
+ | "grid-auto-flow"
132
+ | "grid-auto-columns"
133
+ | "grid-auto-rows"
134
+ | "box-sizing"
135
+ | "aspect-ratio"
136
+ | "z-index";
137
+
138
+ export interface StyleToken {
139
+ property: VisualStyleProperty;
140
+ value: string;
141
+ token: string;
142
+ source: "inline-style" | "class";
143
+ confidence: number;
144
+ }
145
+
146
+ export interface LayoutContext {
147
+ parentId?: string;
148
+ parentSelector?: string;
149
+ siblingIndex: number;
150
+ nthOfType: number;
151
+ display?: string;
152
+ position?: string;
153
+ width?: string;
154
+ height?: string;
155
+ flexDirection?: string;
156
+ gap?: string;
157
+ padding?: string;
158
+ parentDisplay?: string;
159
+ parentFlexDirection?: string;
160
+ parentGap?: string;
161
+ isFlexContainer: boolean;
162
+ isGridContainer: boolean;
163
+ }
164
+
165
+ export type EditCapability =
166
+ | {
167
+ kind: "style";
168
+ properties: VisualStyleProperty[];
169
+ confidence: number;
170
+ reason?: string;
171
+ }
172
+ | {
173
+ kind: "class";
174
+ operations: Array<"add" | "remove" | "replace" | "set">;
175
+ confidence: number;
176
+ reason?: string;
177
+ }
178
+ | {
179
+ kind: "text";
180
+ operations: Array<"setTextContent">;
181
+ confidence: number;
182
+ reason?: string;
183
+ }
184
+ | {
185
+ kind: "structure";
186
+ operations: Array<"moveNode">;
187
+ confidence: number;
188
+ reason?: string;
189
+ };
190
+
191
+ export interface CodeLayerNode {
192
+ id: string;
193
+ tag: string;
194
+ layerName: string;
195
+ layerNameSource: "attribute" | "semantic" | "text" | "selector" | "tag";
196
+ layerNameAttribute?: string;
197
+ selector: string;
198
+ selectors: string[];
199
+ path: string;
200
+ attributes: Record<string, string | true>;
201
+ dataAttributes: Record<string, string>;
202
+ classes: string[];
203
+ textSnippet: string | null;
204
+ style: Partial<Record<VisualStyleProperty | string, string>>;
205
+ styleTokens: StyleToken[];
206
+ parentId?: string;
207
+ children: string[];
208
+ layout: LayoutContext;
209
+ capabilities: EditCapability[];
210
+ confidence: number;
211
+ source: CodeLayerSourceSpan | null;
212
+ }
213
+
214
+ export interface ProjectionDiagnostic {
215
+ severity: "info" | "warning";
216
+ code: string;
217
+ message: string;
218
+ span?: { start: number; end: number };
219
+ }
220
+
221
+ export interface CodeLayerProjection {
222
+ version: 1;
223
+ projectionId: string;
224
+ source: CodeLayerSource;
225
+ rootNodeIds: string[];
226
+ nodes: CodeLayerNode[];
227
+ diagnostics: ProjectionDiagnostic[];
228
+ }
229
+
230
+ export type CodeLayerTreeNodeType =
231
+ | "frame"
232
+ | "group"
233
+ | "component"
234
+ | "shape"
235
+ | "text"
236
+ | "image"
237
+ | "element";
238
+
239
+ export interface CodeLayerTreeNode {
240
+ id: string;
241
+ name: string;
242
+ type: CodeLayerTreeNodeType;
243
+ tag: string;
244
+ selector: string;
245
+ detail: string;
246
+ badge?: string;
247
+ renamable: boolean;
248
+ children: CodeLayerTreeNode[];
249
+ }
250
+
251
+ export interface PreviewBridgeProjectionPayload {
252
+ type: "code-layer-projection";
253
+ projection: CodeLayerProjection;
254
+ }
255
+
256
+ export interface PreviewBridgeSelectionPayload {
257
+ type: "code-layer-selection";
258
+ source: CodeLayerSource;
259
+ nodeId?: string;
260
+ selector?: string;
261
+ bounds?: {
262
+ x: number;
263
+ y: number;
264
+ width: number;
265
+ height: number;
266
+ };
267
+ }
268
+
269
+ export interface PreviewBridgeEditPayload {
270
+ type: "code-layer-edit-intent";
271
+ source: CodeLayerSource;
272
+ intent: EditIntent;
273
+ }
274
+
275
+ export type PreviewBridgePayload =
276
+ | PreviewBridgeProjectionPayload
277
+ | PreviewBridgeSelectionPayload
278
+ | PreviewBridgeEditPayload;
279
+
280
+ export interface EditIntentTarget {
281
+ nodeId?: string;
282
+ selector?: string;
283
+ }
284
+
285
+ export interface StyleEditIntent {
286
+ kind: "style";
287
+ target: EditIntentTarget;
288
+ property: VisualStyleProperty | string;
289
+ value: string;
290
+ }
291
+
292
+ export interface ClassEditIntent {
293
+ kind: "class";
294
+ target: EditIntentTarget;
295
+ operation: "add" | "remove" | "replace" | "set";
296
+ className?: string;
297
+ classNames?: string[];
298
+ from?: string;
299
+ to?: string;
300
+ }
301
+
302
+ export interface TextEditIntent {
303
+ kind: "textContent";
304
+ target: EditIntentTarget;
305
+ value: string;
306
+ html?: string;
307
+ }
308
+
309
+ export interface MoveNodeEditIntent {
310
+ kind: "moveNode";
311
+ target: EditIntentTarget;
312
+ anchor: EditIntentTarget;
313
+ placement: "before" | "after" | "inside";
314
+ }
315
+
316
+ export type EditIntent =
317
+ | StyleEditIntent
318
+ | ClassEditIntent
319
+ | TextEditIntent
320
+ | MoveNodeEditIntent;
321
+
322
+ export interface EditIntentResolution {
323
+ status: "resolved" | "conflict" | "unsupported";
324
+ node?: CodeLayerNode;
325
+ message?: string;
326
+ }
327
+
328
+ export interface EditIntentResolver {
329
+ resolve(
330
+ intent: EditIntent,
331
+ projection: CodeLayerProjection,
332
+ ): EditIntentResolution | Promise<EditIntentResolution>;
333
+ }
334
+
335
+ export type PatchResultStatus =
336
+ | "applied"
337
+ | "needsAgent"
338
+ | "conflict"
339
+ | "unsupported";
340
+
341
+ export interface PatchNodeSummary {
342
+ nodeId: string;
343
+ selector: string;
344
+ tag: string;
345
+ classes: string[];
346
+ style: Partial<Record<VisualStyleProperty | string, string>>;
347
+ textSnippet: string | null;
348
+ }
349
+
350
+ export interface PatchResult {
351
+ status: PatchResultStatus;
352
+ source: CodeLayerSource;
353
+ intent: EditIntent;
354
+ target?: {
355
+ nodeId: string;
356
+ selector: string;
357
+ tag: string;
358
+ };
359
+ capability?: EditCapability;
360
+ before?: PatchNodeSummary;
361
+ after?: PatchNodeSummary;
362
+ changed: boolean;
363
+ message?: string;
364
+ }
365
+
366
+ export interface ApplyVisualEditResult {
367
+ content: string;
368
+ projection: CodeLayerProjection;
369
+ result: PatchResult;
370
+ }
371
+
372
+ interface ParsedAttribute {
373
+ name: string;
374
+ lowerName: string;
375
+ value: string | true;
376
+ start: number;
377
+ end: number;
378
+ }
379
+
380
+ interface ParsedElement {
381
+ index: number;
382
+ tag: string;
383
+ start: number;
384
+ openEnd: number;
385
+ end: number;
386
+ contentStart: number;
387
+ contentEnd: number;
388
+ closeStart?: number;
389
+ closeEnd?: number;
390
+ selfClosing: boolean;
391
+ attributes: ParsedAttribute[];
392
+ parentIndex?: number;
393
+ childIndexes: number[];
394
+ siblingIndex: number;
395
+ nthOfType: number;
396
+ }
397
+
398
+ interface ProjectionBuild {
399
+ projection: CodeLayerProjection;
400
+ elementByNodeId: Map<string, ParsedElement>;
401
+ }
402
+
403
+ const STYLE_PROPERTIES = [
404
+ "width",
405
+ "height",
406
+ "min-width",
407
+ "max-width",
408
+ "min-height",
409
+ "max-height",
410
+ "left",
411
+ "top",
412
+ "right",
413
+ "bottom",
414
+ "inset",
415
+ "position",
416
+ "display",
417
+ "color",
418
+ "background",
419
+ "background-color",
420
+ "background-image",
421
+ "background-blend-mode",
422
+ "fill",
423
+ "fill-opacity",
424
+ "opacity",
425
+ "mix-blend-mode",
426
+ "font-size",
427
+ "font-weight",
428
+ "font-family",
429
+ "font-style",
430
+ "letter-spacing",
431
+ "line-height",
432
+ "text-align",
433
+ "text-decoration",
434
+ "text-transform",
435
+ "white-space",
436
+ "overflow",
437
+ "overflow-x",
438
+ "overflow-y",
439
+ "text-overflow",
440
+ "border",
441
+ "border-width",
442
+ "border-style",
443
+ "border-color",
444
+ "border-radius",
445
+ "border-top-left-radius",
446
+ "border-top-right-radius",
447
+ "border-bottom-left-radius",
448
+ "border-bottom-right-radius",
449
+ "stroke",
450
+ "stroke-width",
451
+ "stroke-opacity",
452
+ "stroke-dasharray",
453
+ "stroke-linecap",
454
+ "stroke-linejoin",
455
+ "outline",
456
+ "outline-width",
457
+ "outline-style",
458
+ "outline-color",
459
+ "outline-offset",
460
+ "box-shadow",
461
+ "text-shadow",
462
+ "filter",
463
+ "backdrop-filter",
464
+ "transform",
465
+ "transform-origin",
466
+ "rotate",
467
+ "scale",
468
+ "translate",
469
+ "padding",
470
+ "padding-top",
471
+ "padding-right",
472
+ "padding-bottom",
473
+ "padding-left",
474
+ "margin",
475
+ "margin-top",
476
+ "margin-right",
477
+ "margin-bottom",
478
+ "margin-left",
479
+ "gap",
480
+ "row-gap",
481
+ "column-gap",
482
+ "flex",
483
+ "flex-direction",
484
+ "flex-wrap",
485
+ "flex-grow",
486
+ "flex-shrink",
487
+ "flex-basis",
488
+ "order",
489
+ "align-self",
490
+ "align-items",
491
+ "align-content",
492
+ "justify-content",
493
+ "justify-items",
494
+ "justify-self",
495
+ "grid-column",
496
+ "grid-row",
497
+ "grid-template-columns",
498
+ "grid-template-rows",
499
+ "grid-auto-flow",
500
+ "grid-auto-columns",
501
+ "grid-auto-rows",
502
+ "box-sizing",
503
+ "aspect-ratio",
504
+ "z-index",
505
+ ] as const satisfies readonly VisualStyleProperty[];
506
+
507
+ const STYLE_PROPERTY_SET = new Set<string>(STYLE_PROPERTIES);
508
+
509
+ const STYLE_PROPERTY_ALIASES: Record<string, VisualStyleProperty> = {
510
+ backgroundColor: "background-color",
511
+ bg: "background",
512
+ cornerRadius: "border-radius",
513
+ dropShadow: "box-shadow",
514
+ radius: "border-radius",
515
+ rotation: "rotate",
516
+ shadow: "box-shadow",
517
+ };
518
+
519
+ const VOID_TAGS = new Set([
520
+ "area",
521
+ "base",
522
+ "br",
523
+ "col",
524
+ "embed",
525
+ "hr",
526
+ "img",
527
+ "input",
528
+ "link",
529
+ "meta",
530
+ "param",
531
+ "source",
532
+ "track",
533
+ "wbr",
534
+ ]);
535
+
536
+ const NON_VISUAL_TAGS = new Set([
537
+ "head",
538
+ "script",
539
+ "style",
540
+ "meta",
541
+ "link",
542
+ "title",
543
+ "template",
544
+ "noscript",
545
+ ]);
546
+
547
+ const DATA_SELECTOR_PRIORITY = [
548
+ "data-agent-native-node-id",
549
+ "data-code-layer-id",
550
+ "data-layer-id",
551
+ "data-builder-id",
552
+ "data-loc",
553
+ "data-testid",
554
+ "data-test-id",
555
+ "data-component",
556
+ "data-name",
557
+ "data-screen",
558
+ ];
559
+
560
+ const STABLE_NODE_ID_ATTRIBUTES = [
561
+ "data-agent-native-node-id",
562
+ "data-code-layer-id",
563
+ "data-layer-id",
564
+ "data-builder-id",
565
+ "data-loc",
566
+ ] as const;
567
+
568
+ const LAYER_NAME_ATTRIBUTE_PRIORITY = [
569
+ "data-agent-native-layer-name",
570
+ "data-layer-name",
571
+ ] as const;
572
+
573
+ const SEMANTIC_LABEL_ATTRIBUTE_PRIORITY = [
574
+ "aria-label",
575
+ "title",
576
+ "data-code-layer-id",
577
+ "data-layer-id",
578
+ "data-name",
579
+ "data-component",
580
+ "data-screen",
581
+ "data-testid",
582
+ "data-test-id",
583
+ ] as const;
584
+
585
+ const TEXT_LAYER_TAGS = new Set([
586
+ "a",
587
+ "button",
588
+ "em",
589
+ "h1",
590
+ "h2",
591
+ "h3",
592
+ "h4",
593
+ "h5",
594
+ "h6",
595
+ "label",
596
+ "li",
597
+ "p",
598
+ "span",
599
+ "strong",
600
+ ]);
601
+
602
+ const IMAGE_LAYER_TAGS = new Set(["canvas", "figure", "img", "picture"]);
603
+ const SHAPE_LAYER_TAGS = new Set([
604
+ "circle",
605
+ "line",
606
+ "path",
607
+ "polygon",
608
+ "rect",
609
+ "svg",
610
+ ]);
611
+ const COMPONENT_LAYER_TAGS = new Set(["button", "input", "select", "textarea"]);
612
+
613
+ function hashStable(value: string): string {
614
+ let hash = 0x811c9dc5;
615
+ for (let i = 0; i < value.length; i += 1) {
616
+ hash ^= value.charCodeAt(i);
617
+ hash = Math.imul(hash, 0x01000193);
618
+ }
619
+ return (hash >>> 0).toString(36);
620
+ }
621
+
622
+ function stableAttributeValueForNode(node: CodeLayerNode): string {
623
+ const basis = [
624
+ node.id,
625
+ node.tag,
626
+ node.path,
627
+ node.source?.openStart ?? 0,
628
+ node.source?.openEnd ?? 0,
629
+ ].join(":");
630
+ return `an-${hashStable(basis)}`;
631
+ }
632
+
633
+ function collapseWhitespace(value: string): string {
634
+ return value.replace(/\s+/g, " ").trim();
635
+ }
636
+
637
+ function cssEscape(value: string): string {
638
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
639
+ }
640
+
641
+ function cssIdent(value: string): string | null {
642
+ if (/^-?[A-Za-z_][A-Za-z0-9_-]*$/.test(value)) return value;
643
+ return null;
644
+ }
645
+
646
+ function unquoteHtmlAttributeValue(value: string): string {
647
+ const trimmed = value.trim();
648
+ if (
649
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
650
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
651
+ ) {
652
+ return trimmed.slice(1, -1);
653
+ }
654
+ return trimmed;
655
+ }
656
+
657
+ function escapeHtmlAttribute(value: string): string {
658
+ return value
659
+ .replace(/&/g, "&amp;")
660
+ .replace(/"/g, "&quot;")
661
+ .replace(/</g, "&lt;")
662
+ .replace(/>/g, "&gt;");
663
+ }
664
+
665
+ function escapeHtmlText(value: string): string {
666
+ return value
667
+ .replace(/&/g, "&amp;")
668
+ .replace(/</g, "&lt;")
669
+ .replace(/>/g, "&gt;");
670
+ }
671
+
672
+ function decodeBasicHtmlEntities(value: string): string {
673
+ return value
674
+ .replace(/&nbsp;/g, " ")
675
+ .replace(/&amp;/g, "&")
676
+ .replace(/&lt;/g, "<")
677
+ .replace(/&gt;/g, ">")
678
+ .replace(/&quot;/g, '"')
679
+ .replace(/&#39;/g, "'");
680
+ }
681
+
682
+ function truncateLayerName(value: string): string {
683
+ const normalized = collapseWhitespace(decodeBasicHtmlEntities(value));
684
+ if (normalized.length <= 72) return normalized;
685
+ return `${normalized.slice(0, 69)}...`;
686
+ }
687
+
688
+ function prettifyIdentifier(value: string): string {
689
+ return collapseWhitespace(
690
+ value
691
+ .replace(/[_-]+/g, " ")
692
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
693
+ .toLowerCase()
694
+ .replace(/\b\w/g, (char) => char.toUpperCase()),
695
+ );
696
+ }
697
+
698
+ function stripTags(value: string): string {
699
+ return value.replace(/<[^>]*>/g, " ");
700
+ }
701
+
702
+ function getAttribute(
703
+ element: ParsedElement,
704
+ name: string,
705
+ ): ParsedAttribute | undefined {
706
+ const lowerName = name.toLowerCase();
707
+ return element.attributes.find((attr) => attr.lowerName === lowerName);
708
+ }
709
+
710
+ function attributeValue(element: ParsedElement, name: string): string | null {
711
+ const value = getAttribute(element, name)?.value;
712
+ if (typeof value === "string") return value;
713
+ if (value === true) return "";
714
+ return null;
715
+ }
716
+
717
+ function explicitLayerNameFor(element: ParsedElement): {
718
+ name: string;
719
+ source: CodeLayerNode["layerNameSource"];
720
+ attribute?: string;
721
+ } | null {
722
+ for (const attribute of LAYER_NAME_ATTRIBUTE_PRIORITY) {
723
+ const value = attributeValue(element, attribute);
724
+ if (value) {
725
+ const name = truncateLayerName(value);
726
+ if (name) return { name, source: "attribute", attribute };
727
+ }
728
+ }
729
+ return null;
730
+ }
731
+
732
+ function semanticLayerNameFor(element: ParsedElement): {
733
+ name: string;
734
+ source: CodeLayerNode["layerNameSource"];
735
+ attribute?: string;
736
+ } | null {
737
+ for (const attribute of SEMANTIC_LABEL_ATTRIBUTE_PRIORITY) {
738
+ const value = attributeValue(element, attribute);
739
+ if (value) {
740
+ const name =
741
+ attribute === "aria-label" || attribute === "title"
742
+ ? truncateLayerName(value)
743
+ : prettifyIdentifier(value);
744
+ if (name) return { name, source: "semantic", attribute };
745
+ }
746
+ }
747
+
748
+ const id = attributeValue(element, "id");
749
+ if (id) {
750
+ return {
751
+ name: prettifyIdentifier(id),
752
+ source: "selector",
753
+ attribute: "id",
754
+ };
755
+ }
756
+
757
+ const meaningfulClass = classList(element).find(
758
+ (token) =>
759
+ !/^(flex|grid|block|inline|hidden|relative|absolute|fixed|sticky)$/.test(
760
+ token,
761
+ ) &&
762
+ !/^(sm|md|lg|xl|2xl):/.test(token) &&
763
+ !/^(p|px|py|pt|pr|pb|pl|m|mx|my|mt|mr|mb|ml|w|h|min|max|text|bg|border|rounded|shadow|gap)-/.test(
764
+ token,
765
+ ),
766
+ );
767
+ if (meaningfulClass) {
768
+ return { name: prettifyIdentifier(meaningfulClass), source: "selector" };
769
+ }
770
+
771
+ return null;
772
+ }
773
+
774
+ function fallbackTagLayerName(tag: string): string {
775
+ switch (tag) {
776
+ case "article":
777
+ return "Article";
778
+ case "aside":
779
+ return "Aside";
780
+ case "body":
781
+ return "Body";
782
+ case "button":
783
+ return "Button";
784
+ case "div":
785
+ return "Frame";
786
+ case "footer":
787
+ return "Footer";
788
+ case "form":
789
+ return "Form";
790
+ case "header":
791
+ return "Header";
792
+ case "img":
793
+ case "picture":
794
+ return "Image";
795
+ case "main":
796
+ return "Main";
797
+ case "nav":
798
+ return "Navigation";
799
+ case "section":
800
+ return "Section";
801
+ case "svg":
802
+ return "Vector";
803
+ case "ul":
804
+ case "ol":
805
+ return "List";
806
+ case "li":
807
+ return "List item";
808
+ default:
809
+ if (TEXT_LAYER_TAGS.has(tag)) return "Text";
810
+ return tag.toUpperCase();
811
+ }
812
+ }
813
+
814
+ function attributeRecord(
815
+ element: ParsedElement,
816
+ ): Record<string, string | true> {
817
+ const record: Record<string, string | true> = {};
818
+ for (const attr of element.attributes) {
819
+ record[attr.lowerName] = attr.value;
820
+ }
821
+ return record;
822
+ }
823
+
824
+ function dataAttributeRecord(element: ParsedElement): Record<string, string> {
825
+ const record: Record<string, string> = {};
826
+ for (const attr of element.attributes) {
827
+ if (attr.lowerName.startsWith("data-") && typeof attr.value === "string") {
828
+ record[attr.lowerName] = attr.value;
829
+ }
830
+ }
831
+ return record;
832
+ }
833
+
834
+ function classList(element: ParsedElement): string[] {
835
+ return collapseWhitespace(attributeValue(element, "class") ?? "")
836
+ .split(" ")
837
+ .filter(Boolean);
838
+ }
839
+
840
+ function parseStyle(value: string | null): Record<string, string> {
841
+ const style: Record<string, string> = {};
842
+ if (!value) return style;
843
+ for (const part of value.split(";")) {
844
+ const index = part.indexOf(":");
845
+ if (index === -1) continue;
846
+ const property = part.slice(0, index).trim().toLowerCase();
847
+ const propertyValue = part.slice(index + 1).trim();
848
+ if (property && propertyValue) style[property] = propertyValue;
849
+ }
850
+ return style;
851
+ }
852
+
853
+ function parseStyleDeclarations(value: string | null): Array<{
854
+ property: string;
855
+ value: string;
856
+ }> {
857
+ if (!value) return [];
858
+ return value
859
+ .split(";")
860
+ .map((part) => {
861
+ const index = part.indexOf(":");
862
+ if (index === -1) return null;
863
+ const property = part.slice(0, index).trim().toLowerCase();
864
+ const propertyValue = part.slice(index + 1).trim();
865
+ if (!property || !propertyValue) return null;
866
+ return { property, value: propertyValue };
867
+ })
868
+ .filter((part): part is { property: string; value: string } =>
869
+ Boolean(part),
870
+ );
871
+ }
872
+
873
+ function serializeStyleDeclarations(
874
+ declarations: Array<{ property: string; value: string }>,
875
+ ): string {
876
+ return declarations
877
+ .map((item) => `${item.property}: ${item.value}`)
878
+ .join("; ");
879
+ }
880
+
881
+ function normalizeStyleProperty(property: string): VisualStyleProperty | null {
882
+ const normalized =
883
+ STYLE_PROPERTY_ALIASES[property] ??
884
+ property
885
+ .replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)
886
+ .toLowerCase();
887
+ if (!STYLE_PROPERTY_SET.has(normalized)) return null;
888
+ return normalized as VisualStyleProperty;
889
+ }
890
+
891
+ function isSafeStyleValue(
892
+ property: VisualStyleProperty,
893
+ value: string,
894
+ ): boolean {
895
+ const trimmed = value.trim();
896
+ if (!trimmed) return false;
897
+ if (/[<>{};]/.test(trimmed)) return false;
898
+ if (/expression\s*\(/i.test(trimmed)) return false;
899
+ if (/javascript\s*:/i.test(trimmed)) return false;
900
+ if (/url\s*\(/i.test(trimmed)) return false;
901
+ if (property === "display") {
902
+ return [
903
+ "block",
904
+ "inline",
905
+ "inline-block",
906
+ "flex",
907
+ "inline-flex",
908
+ "grid",
909
+ "inline-grid",
910
+ "none",
911
+ "contents",
912
+ ].includes(trimmed);
913
+ }
914
+ return true;
915
+ }
916
+
917
+ function isSafeClassToken(value: string): boolean {
918
+ return value.length > 0 && !/[\s"'<>`=]/.test(value);
919
+ }
920
+
921
+ function classTokensFromIntent(intent: ClassEditIntent): string[] {
922
+ if (intent.classNames) return intent.classNames;
923
+ if (intent.className) return [intent.className];
924
+ return [];
925
+ }
926
+
927
+ function parseAttributes(rawTag: string, tagStart: number): ParsedAttribute[] {
928
+ const nameMatch = rawTag.match(/^<\s*\/?\s*([A-Za-z][A-Za-z0-9:-]*)/);
929
+ if (!nameMatch?.[0]) return [];
930
+ const attrTextStart = nameMatch[0].length;
931
+ const attrTextEnd = rawTag.endsWith(">") ? rawTag.length - 1 : rawTag.length;
932
+ const attrText = rawTag.slice(attrTextStart, attrTextEnd);
933
+ const attrOffset = tagStart + attrTextStart;
934
+ const attrs: ParsedAttribute[] = [];
935
+ const attrRe =
936
+ /([:@A-Za-z_][A-Za-z0-9_:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
937
+ let match: RegExpExecArray | null;
938
+ while ((match = attrRe.exec(attrText))) {
939
+ const name = match[1];
940
+ if (!name || name === "/") continue;
941
+ const value = match[2] ?? match[3] ?? match[4] ?? true;
942
+ attrs.push({
943
+ name,
944
+ lowerName: name.toLowerCase(),
945
+ value,
946
+ start: attrOffset + match.index,
947
+ end: attrOffset + match.index + match[0].length,
948
+ });
949
+ }
950
+ return attrs;
951
+ }
952
+
953
+ function findHtmlTagEnd(html: string, start: number): number {
954
+ let quote: '"' | "'" | null = null;
955
+ for (let index = start; index < html.length; index += 1) {
956
+ const char = html[index];
957
+ if (quote) {
958
+ if (char === quote) quote = null;
959
+ continue;
960
+ }
961
+ if (char === '"' || char === "'") {
962
+ quote = char;
963
+ continue;
964
+ }
965
+ if (char === ">") return index + 1;
966
+ }
967
+ return html.length;
968
+ }
969
+
970
+ function parseHtmlElements(html: string): ParsedElement[] {
971
+ const elements: ParsedElement[] = [];
972
+ const stack: number[] = [];
973
+ const sameTypeCounts = new Map<string, number>();
974
+ const tagRe =
975
+ /<!--[\s\S]*?-->|<![A-Za-z][^>]*>|<\/?\s*([A-Za-z][A-Za-z0-9:-]*)\b/g;
976
+ let match: RegExpExecArray | null;
977
+
978
+ while ((match = tagRe.exec(html))) {
979
+ const raw =
980
+ match[0].startsWith("<!--") || match[0].startsWith("<!")
981
+ ? match[0]
982
+ : html.slice(match.index, findHtmlTagEnd(html, match.index));
983
+ tagRe.lastIndex = match.index + raw.length;
984
+ const tag = match[1]?.toLowerCase();
985
+ if (!tag || raw.startsWith("<!--") || raw.startsWith("<!")) continue;
986
+
987
+ if (raw.startsWith("</")) {
988
+ for (let i = stack.length - 1; i >= 0; i -= 1) {
989
+ const element = elements[stack[i]];
990
+ if (!element) continue;
991
+ stack.pop();
992
+ element.closeStart = match.index;
993
+ element.closeEnd = match.index + raw.length;
994
+ element.contentEnd = match.index;
995
+ element.end = match.index + raw.length;
996
+ if (element.tag === tag) break;
997
+ }
998
+ continue;
999
+ }
1000
+
1001
+ const parentIndex = stack.length > 0 ? stack[stack.length - 1] : undefined;
1002
+ const parentKey = `${parentIndex ?? "root"}:${tag}`;
1003
+ const nthOfType = (sameTypeCounts.get(parentKey) ?? 0) + 1;
1004
+ sameTypeCounts.set(parentKey, nthOfType);
1005
+ const selfClosing = raw.endsWith("/>") || VOID_TAGS.has(tag);
1006
+ const index = elements.length;
1007
+ const element: ParsedElement = {
1008
+ index,
1009
+ tag,
1010
+ start: match.index,
1011
+ openEnd: match.index + raw.length,
1012
+ end: selfClosing ? match.index + raw.length : html.length,
1013
+ contentStart: match.index + raw.length,
1014
+ contentEnd: selfClosing ? match.index + raw.length : html.length,
1015
+ selfClosing,
1016
+ attributes: parseAttributes(raw, match.index),
1017
+ parentIndex,
1018
+ childIndexes: [],
1019
+ siblingIndex:
1020
+ parentIndex === undefined
1021
+ ? elements.filter((item) => item.parentIndex === undefined).length
1022
+ : (elements[parentIndex]?.childIndexes.length ?? 0),
1023
+ nthOfType,
1024
+ };
1025
+ elements.push(element);
1026
+ if (parentIndex !== undefined) {
1027
+ elements[parentIndex]?.childIndexes.push(index);
1028
+ }
1029
+ if (!selfClosing) stack.push(index);
1030
+ }
1031
+
1032
+ return elements;
1033
+ }
1034
+
1035
+ function candidateDataSelector(
1036
+ element: ParsedElement,
1037
+ ): { selector: string; confidence: number } | null {
1038
+ const data = dataAttributeRecord(element);
1039
+ for (const name of DATA_SELECTOR_PRIORITY) {
1040
+ const value = data[name];
1041
+ if (value) {
1042
+ return {
1043
+ selector: `[${name}="${cssEscape(value)}"]`,
1044
+ confidence: name === "data-code-layer-id" ? 0.95 : 0.86,
1045
+ };
1046
+ }
1047
+ }
1048
+ const [firstName, firstValue] = Object.entries(data)[0] ?? [];
1049
+ if (firstName && firstValue) {
1050
+ return {
1051
+ selector: `[${firstName}="${cssEscape(firstValue)}"]`,
1052
+ confidence: 0.78,
1053
+ };
1054
+ }
1055
+ return null;
1056
+ }
1057
+
1058
+ function selectorPart(element: ParsedElement): string {
1059
+ const dataSelector = candidateDataSelector(element);
1060
+ if (dataSelector) return `${element.tag}${dataSelector.selector}`;
1061
+
1062
+ const id = attributeValue(element, "id");
1063
+ const escapedId = id ? cssIdent(id) : null;
1064
+ if (escapedId) return `#${escapedId}`;
1065
+
1066
+ const safeClasses = classList(element)
1067
+ .map(cssIdent)
1068
+ .filter((value): value is string => Boolean(value))
1069
+ .slice(0, 2);
1070
+ const classes = safeClasses.map((value) => `.${value}`).join("");
1071
+ const nth = element.nthOfType > 1 ? `:nth-of-type(${element.nthOfType})` : "";
1072
+ return `${element.tag}${classes}${nth}`;
1073
+ }
1074
+
1075
+ function pathSelector(
1076
+ element: ParsedElement,
1077
+ elements: ParsedElement[],
1078
+ ): string {
1079
+ const parts: string[] = [];
1080
+ let current: ParsedElement | undefined = element;
1081
+ while (current) {
1082
+ parts.unshift(selectorPart(current));
1083
+ current =
1084
+ current.parentIndex === undefined
1085
+ ? undefined
1086
+ : elements[current.parentIndex];
1087
+ }
1088
+ return parts.slice(-5).join(" > ");
1089
+ }
1090
+
1091
+ function primarySelector(
1092
+ element: ParsedElement,
1093
+ elements: ParsedElement[],
1094
+ ): { selector: string; confidence: number } {
1095
+ const dataSelector = candidateDataSelector(element);
1096
+ if (dataSelector) return dataSelector;
1097
+
1098
+ const id = attributeValue(element, "id");
1099
+ const escapedId = id ? cssIdent(id) : null;
1100
+ if (escapedId) return { selector: `#${escapedId}`, confidence: 0.96 };
1101
+
1102
+ const safeClasses = classList(element)
1103
+ .map(cssIdent)
1104
+ .filter((value): value is string => Boolean(value))
1105
+ .slice(0, 3);
1106
+ if (safeClasses.length > 0) {
1107
+ return {
1108
+ selector: `${element.tag}${safeClasses.map((item) => `.${item}`).join("")}`,
1109
+ confidence: 0.72,
1110
+ };
1111
+ }
1112
+
1113
+ return { selector: pathSelector(element, elements), confidence: 0.58 };
1114
+ }
1115
+
1116
+ function nodeIdFor(
1117
+ element: ParsedElement,
1118
+ elements: ParsedElement[],
1119
+ source: CodeLayerSource,
1120
+ ): string {
1121
+ const sourceKey =
1122
+ source.fileId ??
1123
+ source.filename ??
1124
+ source.path ??
1125
+ source.url ??
1126
+ source.kind;
1127
+ const codeLayerId = stableSourceIdForElement(element);
1128
+ if (codeLayerId) {
1129
+ return `html:${hashStable(`${sourceKey}:data:${codeLayerId}`)}`;
1130
+ }
1131
+ const id = attributeValue(element, "id");
1132
+ if (id) return `html:${hashStable(`${sourceKey}:id:${id}`)}`;
1133
+ const path = pathSelector(element, elements);
1134
+ return `html:${hashStable(`${sourceKey}:${path}:${element.start}`)}`;
1135
+ }
1136
+
1137
+ function stableSourceIdForElement(element: ParsedElement): string | null {
1138
+ for (const attribute of STABLE_NODE_ID_ATTRIBUTES) {
1139
+ const value = attributeValue(element, attribute);
1140
+ if (value) return value;
1141
+ }
1142
+ return null;
1143
+ }
1144
+
1145
+ function styleTokensFor(element: ParsedElement): StyleToken[] {
1146
+ const tokens: StyleToken[] = [];
1147
+ for (const declaration of parseStyleDeclarations(
1148
+ attributeValue(element, "style"),
1149
+ )) {
1150
+ const property = normalizeStyleProperty(declaration.property);
1151
+ if (!property) continue;
1152
+ tokens.push({
1153
+ property,
1154
+ value: declaration.value,
1155
+ token: `${declaration.property}: ${declaration.value}`,
1156
+ source: "inline-style",
1157
+ confidence: 0.95,
1158
+ });
1159
+ }
1160
+
1161
+ for (const token of classList(element)) {
1162
+ const classStyle = classStyleToken(token);
1163
+ if (classStyle) tokens.push(classStyle);
1164
+ }
1165
+
1166
+ return tokens;
1167
+ }
1168
+
1169
+ function classStyleToken(token: string): StyleToken | null {
1170
+ const normalized = token.replace(/^[a-z]+:/, "");
1171
+ if (/^w-/.test(normalized)) {
1172
+ return {
1173
+ property: "width",
1174
+ value: token,
1175
+ token,
1176
+ source: "class",
1177
+ confidence: 0.64,
1178
+ };
1179
+ }
1180
+ if (/^h-/.test(normalized)) {
1181
+ return {
1182
+ property: "height",
1183
+ value: token,
1184
+ token,
1185
+ source: "class",
1186
+ confidence: 0.64,
1187
+ };
1188
+ }
1189
+ if (/^bg-/.test(normalized)) {
1190
+ return {
1191
+ property: "background",
1192
+ value: token,
1193
+ token,
1194
+ source: "class",
1195
+ confidence: 0.6,
1196
+ };
1197
+ }
1198
+ if (/^(p|px|py|pt|pr|pb|pl)-/.test(normalized)) {
1199
+ return {
1200
+ property: "padding",
1201
+ value: token,
1202
+ token,
1203
+ source: "class",
1204
+ confidence: 0.62,
1205
+ };
1206
+ }
1207
+ if (/^gap-/.test(normalized)) {
1208
+ return {
1209
+ property: "gap",
1210
+ value: token,
1211
+ token,
1212
+ source: "class",
1213
+ confidence: 0.62,
1214
+ };
1215
+ }
1216
+ if (
1217
+ [
1218
+ "block",
1219
+ "inline",
1220
+ "inline-block",
1221
+ "flex",
1222
+ "inline-flex",
1223
+ "grid",
1224
+ "inline-grid",
1225
+ "hidden",
1226
+ ].includes(normalized)
1227
+ ) {
1228
+ return {
1229
+ property: "display",
1230
+ value: normalized === "hidden" ? "none" : normalized,
1231
+ token,
1232
+ source: "class",
1233
+ confidence: 0.68,
1234
+ };
1235
+ }
1236
+ if (/^text-/.test(normalized)) {
1237
+ return {
1238
+ property: "color",
1239
+ value: token,
1240
+ token,
1241
+ source: "class",
1242
+ confidence: 0.45,
1243
+ };
1244
+ }
1245
+ return null;
1246
+ }
1247
+
1248
+ function layoutFor(
1249
+ element: ParsedElement,
1250
+ parent: ParsedElement | undefined,
1251
+ ): Omit<LayoutContext, "parentId" | "parentSelector"> {
1252
+ const style = parseStyle(attributeValue(element, "style"));
1253
+ const parentStyle = parent
1254
+ ? parseStyle(attributeValue(parent, "style"))
1255
+ : undefined;
1256
+ const classes = new Set(classList(element));
1257
+ const parentClasses = parent ? new Set(classList(parent)) : undefined;
1258
+ const display =
1259
+ style.display ??
1260
+ (classes.has("flex")
1261
+ ? "flex"
1262
+ : classes.has("grid")
1263
+ ? "grid"
1264
+ : classes.has("hidden")
1265
+ ? "none"
1266
+ : classes.has("block")
1267
+ ? "block"
1268
+ : classes.has("inline-block")
1269
+ ? "inline-block"
1270
+ : undefined);
1271
+ const parentDisplay =
1272
+ parentStyle?.display ??
1273
+ (parentClasses?.has("flex")
1274
+ ? "flex"
1275
+ : parentClasses?.has("grid")
1276
+ ? "grid"
1277
+ : parentClasses?.has("hidden")
1278
+ ? "none"
1279
+ : undefined);
1280
+ const flexDirection =
1281
+ style["flex-direction"] ??
1282
+ (classes.has("flex-col")
1283
+ ? "column"
1284
+ : classes.has("flex-row")
1285
+ ? "row"
1286
+ : undefined);
1287
+ const parentFlexDirection =
1288
+ parentStyle?.["flex-direction"] ??
1289
+ (parentClasses?.has("flex-col")
1290
+ ? "column"
1291
+ : parentClasses?.has("flex-row")
1292
+ ? "row"
1293
+ : undefined);
1294
+
1295
+ return {
1296
+ siblingIndex: element.siblingIndex,
1297
+ nthOfType: element.nthOfType,
1298
+ display,
1299
+ position: style.position,
1300
+ width: style.width,
1301
+ height: style.height,
1302
+ flexDirection,
1303
+ gap: style.gap,
1304
+ padding: style.padding,
1305
+ parentDisplay,
1306
+ parentFlexDirection,
1307
+ parentGap: parentStyle?.gap,
1308
+ isFlexContainer: display === "flex" || display === "inline-flex",
1309
+ isGridContainer: display === "grid" || display === "inline-grid",
1310
+ };
1311
+ }
1312
+
1313
+ function textSnippetFor(html: string, element: ParsedElement): string | null {
1314
+ if (element.selfClosing) return null;
1315
+ const inner = html.slice(element.contentStart, element.contentEnd);
1316
+ const text = collapseWhitespace(decodeBasicHtmlEntities(stripTags(inner)));
1317
+ if (!text) return null;
1318
+ return text.length > 160 ? `${text.slice(0, 157)}...` : text;
1319
+ }
1320
+
1321
+ function layerNameFor(
1322
+ html: string,
1323
+ element: ParsedElement,
1324
+ ): {
1325
+ name: string;
1326
+ source: CodeLayerNode["layerNameSource"];
1327
+ attribute?: string;
1328
+ } {
1329
+ const explicit = explicitLayerNameFor(element);
1330
+ if (explicit) return explicit;
1331
+
1332
+ const semantic = semanticLayerNameFor(element);
1333
+ if (semantic) return semantic;
1334
+
1335
+ if (TEXT_LAYER_TAGS.has(element.tag)) {
1336
+ const text = textSnippetFor(html, element);
1337
+ if (text) return { name: truncateLayerName(text), source: "text" };
1338
+ }
1339
+
1340
+ return { name: fallbackTagLayerName(element.tag), source: "tag" };
1341
+ }
1342
+
1343
+ function treeTypeForNode(node: CodeLayerNode): CodeLayerTreeNodeType {
1344
+ if (TEXT_LAYER_TAGS.has(node.tag)) return "text";
1345
+ if (IMAGE_LAYER_TAGS.has(node.tag)) return "image";
1346
+ if (SHAPE_LAYER_TAGS.has(node.tag)) return "shape";
1347
+ if (
1348
+ COMPONENT_LAYER_TAGS.has(node.tag) ||
1349
+ node.classes.some((item) => /component|card|button|control/.test(item))
1350
+ ) {
1351
+ return "component";
1352
+ }
1353
+ if (node.layout.isFlexContainer || node.layout.isGridContainer) {
1354
+ return "frame";
1355
+ }
1356
+ if (node.children.length > 0) return "group";
1357
+ return "element";
1358
+ }
1359
+
1360
+ function capabilitiesFor(element: ParsedElement): EditCapability[] {
1361
+ const capabilities: EditCapability[] = [
1362
+ {
1363
+ kind: "style",
1364
+ properties: [...STYLE_PROPERTIES],
1365
+ confidence: 0.9,
1366
+ },
1367
+ {
1368
+ kind: "class",
1369
+ operations: ["add", "remove", "replace", "set"],
1370
+ confidence: 0.88,
1371
+ },
1372
+ ];
1373
+
1374
+ if (!element.selfClosing) {
1375
+ capabilities.push({
1376
+ kind: "text",
1377
+ operations: ["setTextContent"],
1378
+ confidence: element.childIndexes.length === 0 ? 0.82 : 0.35,
1379
+ reason:
1380
+ element.childIndexes.length === 0
1381
+ ? undefined
1382
+ : "Text edits on mixed-content elements should be escalated.",
1383
+ });
1384
+ }
1385
+
1386
+ return capabilities;
1387
+ }
1388
+
1389
+ function buildProjection(
1390
+ html: string,
1391
+ source: CodeLayerSource,
1392
+ ): ProjectionBuild {
1393
+ const elements = parseHtmlElements(html);
1394
+ const nodeIdByElementIndex = new Map<number, string>();
1395
+ const nodes: CodeLayerNode[] = [];
1396
+ const diagnostics: ProjectionDiagnostic[] = [];
1397
+
1398
+ for (const element of elements) {
1399
+ if (NON_VISUAL_TAGS.has(element.tag)) continue;
1400
+ const nodeId = nodeIdFor(element, elements, source);
1401
+ nodeIdByElementIndex.set(element.index, nodeId);
1402
+ }
1403
+
1404
+ const elementByNodeId = new Map<string, ParsedElement>();
1405
+
1406
+ for (const element of elements) {
1407
+ const nodeId = nodeIdByElementIndex.get(element.index);
1408
+ if (!nodeId) continue;
1409
+
1410
+ const parent =
1411
+ element.parentIndex === undefined
1412
+ ? undefined
1413
+ : elements[element.parentIndex];
1414
+ const parentId =
1415
+ element.parentIndex === undefined
1416
+ ? undefined
1417
+ : nodeIdByElementIndex.get(element.parentIndex);
1418
+ const selector = primarySelector(element, elements);
1419
+ const path = pathSelector(element, elements);
1420
+ const classes = classList(element);
1421
+ const style = parseStyle(attributeValue(element, "style"));
1422
+ const dataAttributes = dataAttributeRecord(element);
1423
+ const layerName = layerNameFor(html, element);
1424
+ const selectors = Array.from(
1425
+ new Set([
1426
+ selector.selector,
1427
+ path,
1428
+ ...Object.entries(dataAttributes).map(
1429
+ ([name, value]) => `[${name}="${cssEscape(value)}"]`,
1430
+ ),
1431
+ ]),
1432
+ );
1433
+
1434
+ nodes.push({
1435
+ id: nodeId,
1436
+ tag: element.tag,
1437
+ layerName: layerName.name,
1438
+ layerNameSource: layerName.source,
1439
+ layerNameAttribute: layerName.attribute,
1440
+ selector: selector.selector,
1441
+ selectors,
1442
+ path,
1443
+ attributes: attributeRecord(element),
1444
+ dataAttributes,
1445
+ classes,
1446
+ textSnippet: textSnippetFor(html, element),
1447
+ style,
1448
+ styleTokens: styleTokensFor(element),
1449
+ parentId,
1450
+ children: element.childIndexes
1451
+ .map((index) => nodeIdByElementIndex.get(index))
1452
+ .filter((id): id is string => Boolean(id)),
1453
+ layout: {
1454
+ parentId,
1455
+ parentSelector: parent
1456
+ ? primarySelector(parent, elements).selector
1457
+ : undefined,
1458
+ ...layoutFor(element, parent),
1459
+ },
1460
+ capabilities: capabilitiesFor(element),
1461
+ confidence: selector.confidence,
1462
+ source: {
1463
+ start: element.start,
1464
+ end: element.end,
1465
+ openStart: element.start,
1466
+ openEnd: element.openEnd,
1467
+ contentStart: element.selfClosing ? undefined : element.contentStart,
1468
+ contentEnd: element.selfClosing ? undefined : element.contentEnd,
1469
+ closeStart: element.closeStart,
1470
+ closeEnd: element.closeEnd,
1471
+ },
1472
+ });
1473
+ elementByNodeId.set(nodeId, element);
1474
+ }
1475
+
1476
+ if (nodes.length === 0 && html.trim()) {
1477
+ diagnostics.push({
1478
+ severity: "warning",
1479
+ code: "no-projectable-elements",
1480
+ message: "No visual HTML elements were found in this source.",
1481
+ });
1482
+ }
1483
+
1484
+ return {
1485
+ projection: {
1486
+ version: 1,
1487
+ projectionId: `clp_${hashStable(`${source.kind}:${source.fileId ?? ""}:${source.filename ?? ""}:${html}`)}`,
1488
+ source,
1489
+ rootNodeIds: nodes
1490
+ .filter((node) => !node.parentId)
1491
+ .map((node) => node.id),
1492
+ nodes,
1493
+ diagnostics,
1494
+ },
1495
+ elementByNodeId,
1496
+ };
1497
+ }
1498
+
1499
+ export function buildCodeLayerProjection(
1500
+ html: string,
1501
+ options: { source?: CodeLayerSource } = {},
1502
+ ): CodeLayerProjection {
1503
+ return buildProjection(html, options.source ?? { kind: "inline-html" })
1504
+ .projection;
1505
+ }
1506
+
1507
+ export function ensureCodeLayerNodeIdsInHtml(
1508
+ html: string,
1509
+ options: { source?: CodeLayerSource } = {},
1510
+ ): { content: string; changed: boolean; stamped: number } {
1511
+ const projection = buildCodeLayerProjection(html, options);
1512
+ const usedIds = new Set<string>();
1513
+ const edits: Array<{ start: number; end: number; value: string }> = [];
1514
+
1515
+ const uniqueValueFor = (base: string) => {
1516
+ let value = base;
1517
+ let suffix = 1;
1518
+ while (usedIds.has(value)) {
1519
+ value = `an-${hashStable(`${base}:${suffix}`)}`;
1520
+ suffix += 1;
1521
+ }
1522
+ usedIds.add(value);
1523
+ return value;
1524
+ };
1525
+
1526
+ for (const node of projection.nodes) {
1527
+ if (
1528
+ !node.source ||
1529
+ node.source.openEnd <= node.source.openStart ||
1530
+ !node.source
1531
+ ) {
1532
+ continue;
1533
+ }
1534
+ const source = node.source;
1535
+ const existing = node.dataAttributes["data-agent-native-node-id"]?.trim();
1536
+ const openTag = html.slice(source.openStart, source.openEnd);
1537
+ const stableIdMatches = Array.from(
1538
+ openTag.matchAll(
1539
+ /\sdata-agent-native-node-id\s*=\s*(?:"[^"]*"|'[^']*'|[^\s/>]+)/gi,
1540
+ ),
1541
+ );
1542
+ const hasSingleCleanStableId =
1543
+ existing && stableIdMatches.length === 1 && !usedIds.has(existing);
1544
+ if (hasSingleCleanStableId) {
1545
+ usedIds.add(existing);
1546
+ continue;
1547
+ }
1548
+
1549
+ const nextValue = uniqueValueFor(
1550
+ existing
1551
+ ? `an-${hashStable(
1552
+ `${existing}:${node.id}:${source.openStart}:${source.openEnd}`,
1553
+ )}`
1554
+ : stableAttributeValueForNode(node),
1555
+ );
1556
+ if (stableIdMatches.length > 0) {
1557
+ const [firstMatch, ...duplicateMatches] = stableIdMatches;
1558
+ if (!firstMatch || firstMatch.index === undefined) continue;
1559
+ edits.push({
1560
+ start: source.openStart + firstMatch.index,
1561
+ end: source.openStart + firstMatch.index + firstMatch[0].length,
1562
+ value: ` data-agent-native-node-id="${escapeHtmlAttribute(nextValue)}"`,
1563
+ });
1564
+ for (const duplicate of duplicateMatches) {
1565
+ if (duplicate.index === undefined) continue;
1566
+ edits.push({
1567
+ start: source.openStart + duplicate.index,
1568
+ end: source.openStart + duplicate.index + duplicate[0].length,
1569
+ value: "",
1570
+ });
1571
+ }
1572
+ continue;
1573
+ }
1574
+
1575
+ const insertAt = source.openEnd - (openTag.endsWith("/>") ? 2 : 1);
1576
+ if (insertAt <= 0 || insertAt > html.length) continue;
1577
+ edits.push({
1578
+ start: insertAt,
1579
+ end: insertAt,
1580
+ value: ` data-agent-native-node-id="${escapeHtmlAttribute(nextValue)}"`,
1581
+ });
1582
+ }
1583
+
1584
+ const orderedEdits = edits.sort((a, b) => b.start - a.start);
1585
+
1586
+ if (orderedEdits.length === 0) {
1587
+ return { content: html, changed: false, stamped: 0 };
1588
+ }
1589
+
1590
+ let content = html;
1591
+ for (const edit of orderedEdits) {
1592
+ content = `${content.slice(0, edit.start)}${edit.value}${content.slice(edit.end)}`;
1593
+ }
1594
+ return { content, changed: true, stamped: orderedEdits.length };
1595
+ }
1596
+
1597
+ export function removeCodeLayerNodeFromHtml(
1598
+ html: string,
1599
+ node: CodeLayerNode,
1600
+ ): string | null {
1601
+ if (!node.source) return null;
1602
+ if (node.tag === "html" || node.tag === "body") return null;
1603
+ const start = node.source.start;
1604
+ const end = node.source.end;
1605
+ if (start < 0 || end <= start || end > html.length) return null;
1606
+ return `${html.slice(0, start)}${html.slice(end)}`;
1607
+ }
1608
+
1609
+ export function buildCodeLayerTree(
1610
+ projection: CodeLayerProjection,
1611
+ ): CodeLayerTreeNode[] {
1612
+ const nodesById = new Map(projection.nodes.map((node) => [node.id, node]));
1613
+ const treeById = new Map<string, CodeLayerTreeNode>();
1614
+
1615
+ for (const node of projection.nodes) {
1616
+ treeById.set(node.id, {
1617
+ id: node.id,
1618
+ name: node.layerName,
1619
+ type: treeTypeForNode(node),
1620
+ tag: node.tag,
1621
+ selector: node.selector,
1622
+ detail: `<${node.tag}>`,
1623
+ badge:
1624
+ node.layerNameSource === "attribute" && node.layerNameAttribute
1625
+ ? node.layerNameAttribute
1626
+ : undefined,
1627
+ // Safe rename persistence belongs in the caller's edit action. The
1628
+ // preferred write target is data-agent-native-layer-name; projection is
1629
+ // intentionally read-only and never mutates source by itself.
1630
+ renamable: node.source != null,
1631
+ children: [],
1632
+ });
1633
+ }
1634
+
1635
+ for (const node of projection.nodes) {
1636
+ const parent =
1637
+ node.parentId && nodesById.has(node.parentId)
1638
+ ? treeById.get(node.parentId)
1639
+ : undefined;
1640
+ const treeNode = treeById.get(node.id);
1641
+ if (parent && treeNode) parent.children.push(treeNode);
1642
+ }
1643
+
1644
+ const roots = projection.rootNodeIds
1645
+ .map((id) => treeById.get(id))
1646
+ .filter((node): node is CodeLayerTreeNode => Boolean(node));
1647
+ const rootIds = new Set(roots.map((node) => node.id));
1648
+ for (const node of projection.nodes) {
1649
+ if (!node.parentId && !rootIds.has(node.id)) {
1650
+ const treeNode = treeById.get(node.id);
1651
+ if (treeNode) roots.push(treeNode);
1652
+ }
1653
+ }
1654
+ return roots;
1655
+ }
1656
+
1657
+ function normalizeSelectorForMatch(selector: string): string {
1658
+ return selector
1659
+ .trim()
1660
+ .replace(/\s*>\s*/g, " > ")
1661
+ .replace(/\s+/g, " ");
1662
+ }
1663
+
1664
+ function lastSelectorPart(selector: string): string {
1665
+ const parts = normalizeSelectorForMatch(selector).split(" > ");
1666
+ return parts[parts.length - 1] ?? selector;
1667
+ }
1668
+
1669
+ function simpleSelectorMatches(node: CodeLayerNode, selector: string): boolean {
1670
+ if (!selector) return false;
1671
+ if (selector.startsWith("#")) {
1672
+ return node.attributes.id === selector.slice(1);
1673
+ }
1674
+ const tagIdMatch = selector.match(/^([A-Za-z][A-Za-z0-9:-]*)#(.+)$/);
1675
+ if (tagIdMatch?.[1]) {
1676
+ return (
1677
+ node.tag === tagIdMatch[1].toLowerCase() &&
1678
+ node.attributes.id === tagIdMatch[2]
1679
+ );
1680
+ }
1681
+ if (selector.startsWith(".")) {
1682
+ const required = selector
1683
+ .split(".")
1684
+ .map((item) => item.trim())
1685
+ .filter(Boolean);
1686
+ return required.every((item) => node.classes.includes(item));
1687
+ }
1688
+ const dataMatch = selector.match(
1689
+ /^(?:([A-Za-z][A-Za-z0-9:-]*)?)?\[([A-Za-z_][A-Za-z0-9_:.-]*)=(?:"([^"]*)"|'([^']*)')\]$/,
1690
+ );
1691
+ if (dataMatch?.[2]) {
1692
+ const tag = dataMatch[1]?.toLowerCase();
1693
+ const attribute = dataMatch[2].toLowerCase();
1694
+ const expected = dataMatch[3] ?? dataMatch[4] ?? "";
1695
+ const actual = attribute.startsWith("data-")
1696
+ ? node.dataAttributes[attribute]
1697
+ : node.attributes[attribute];
1698
+ return (!tag || node.tag === tag) && actual === expected;
1699
+ }
1700
+ const tagClassMatch = selector.match(
1701
+ /^([A-Za-z][A-Za-z0-9:-]*)(\.[A-Za-z0-9_-]+)+$/,
1702
+ );
1703
+ if (tagClassMatch?.[1]) {
1704
+ const tag = tagClassMatch[1].toLowerCase();
1705
+ const required = selector.slice(tag.length).split(".").filter(Boolean);
1706
+ return (
1707
+ node.tag === tag && required.every((item) => node.classes.includes(item))
1708
+ );
1709
+ }
1710
+ const nthMatch = selector.match(
1711
+ /^([A-Za-z][A-Za-z0-9:-]*)(?::nth-of-type\((\d+)\))$/,
1712
+ );
1713
+ if (nthMatch?.[1]) {
1714
+ return (
1715
+ node.tag === nthMatch[1].toLowerCase() &&
1716
+ lastSelectorPart(node.path).endsWith(`:nth-of-type(${nthMatch[2]})`)
1717
+ );
1718
+ }
1719
+ return node.tag === selector.toLowerCase();
1720
+ }
1721
+
1722
+ function nodeMatchesStableSourceId(
1723
+ node: CodeLayerNode,
1724
+ sourceId: string,
1725
+ ): boolean {
1726
+ if (!sourceId) return false;
1727
+ if (node.id === sourceId) return true;
1728
+ for (const attribute of STABLE_NODE_ID_ATTRIBUTES) {
1729
+ if (node.dataAttributes[attribute] === sourceId) return true;
1730
+ }
1731
+ return node.attributes.id === sourceId;
1732
+ }
1733
+
1734
+ function selectorMatches(node: CodeLayerNode, selector: string): boolean {
1735
+ const normalizedSelector = normalizeSelectorForMatch(selector);
1736
+ const normalizedNodeSelectors = [
1737
+ node.selector,
1738
+ node.path,
1739
+ ...node.selectors,
1740
+ ].map(normalizeSelectorForMatch);
1741
+ if (normalizedNodeSelectors.includes(normalizedSelector)) return true;
1742
+ if (
1743
+ normalizedSelector.includes(" > ") &&
1744
+ normalizedNodeSelectors.some(
1745
+ (candidate) =>
1746
+ candidate.endsWith(` > ${normalizedSelector}`) ||
1747
+ normalizedSelector.endsWith(` > ${candidate}`),
1748
+ )
1749
+ ) {
1750
+ return true;
1751
+ }
1752
+ if (simpleSelectorMatches(node, normalizedSelector)) return true;
1753
+ const lastPart = lastSelectorPart(normalizedSelector);
1754
+ return (
1755
+ lastPart !== normalizedSelector && simpleSelectorMatches(node, lastPart)
1756
+ );
1757
+ }
1758
+
1759
+ function resolveTarget(
1760
+ projection: CodeLayerProjection,
1761
+ target: EditIntentTarget,
1762
+ ): EditIntentResolution {
1763
+ if (target.nodeId) {
1764
+ const matches = projection.nodes.filter((candidate) =>
1765
+ nodeMatchesStableSourceId(candidate, target.nodeId ?? ""),
1766
+ );
1767
+ if (matches.length === 1 && matches[0]) {
1768
+ return { status: "resolved", node: matches[0] };
1769
+ }
1770
+ if (matches.length > 1) {
1771
+ return {
1772
+ status: "conflict",
1773
+ message: `Node id "${target.nodeId}" matched ${matches.length} code layer nodes.`,
1774
+ };
1775
+ }
1776
+ if (!target.selector) {
1777
+ return {
1778
+ status: "conflict",
1779
+ message: `No code layer node exists for nodeId "${target.nodeId}".`,
1780
+ };
1781
+ }
1782
+ }
1783
+
1784
+ if (!target.selector) {
1785
+ return {
1786
+ status: "conflict",
1787
+ message:
1788
+ "Edit intent must include either target.nodeId or target.selector.",
1789
+ };
1790
+ }
1791
+
1792
+ const matches = projection.nodes.filter((node) =>
1793
+ selectorMatches(node, target.selector ?? ""),
1794
+ );
1795
+ if (matches.length === 1 && matches[0]) {
1796
+ return { status: "resolved", node: matches[0] };
1797
+ }
1798
+ if (matches.length > 1) {
1799
+ return {
1800
+ status: "conflict",
1801
+ message: `Selector "${target.selector}" matched ${matches.length} code layer nodes.`,
1802
+ };
1803
+ }
1804
+ return {
1805
+ status: "conflict",
1806
+ message: `Selector "${target.selector}" did not match a code layer node.`,
1807
+ };
1808
+ }
1809
+
1810
+ function summarizeNode(node: CodeLayerNode): PatchNodeSummary {
1811
+ return {
1812
+ nodeId: node.id,
1813
+ selector: node.selector,
1814
+ tag: node.tag,
1815
+ classes: [...node.classes],
1816
+ style: { ...node.style },
1817
+ textSnippet: node.textSnippet,
1818
+ };
1819
+ }
1820
+
1821
+ function patchResult(
1822
+ status: PatchResultStatus,
1823
+ source: CodeLayerSource,
1824
+ intent: EditIntent,
1825
+ changed: boolean,
1826
+ message: string,
1827
+ node?: CodeLayerNode,
1828
+ capability?: EditCapability,
1829
+ before?: PatchNodeSummary,
1830
+ after?: PatchNodeSummary,
1831
+ ): PatchResult {
1832
+ return {
1833
+ status,
1834
+ source,
1835
+ intent,
1836
+ target: node
1837
+ ? { nodeId: node.id, selector: node.selector, tag: node.tag }
1838
+ : undefined,
1839
+ capability,
1840
+ before,
1841
+ after,
1842
+ changed,
1843
+ message,
1844
+ };
1845
+ }
1846
+
1847
+ function replaceOrInsertAttribute(
1848
+ html: string,
1849
+ element: ParsedElement,
1850
+ name: string,
1851
+ value: string,
1852
+ ): string {
1853
+ const escaped = escapeHtmlAttribute(value);
1854
+ const existing = getAttribute(element, name);
1855
+ if (existing) {
1856
+ return `${html.slice(0, existing.start)}${existing.name}="${escaped}"${html.slice(existing.end)}`;
1857
+ }
1858
+
1859
+ const rawOpen = html.slice(element.start, element.openEnd);
1860
+ const closeIndex = element.openEnd - 1;
1861
+ const slashIndex = rawOpen.trimEnd().endsWith("/>")
1862
+ ? html.lastIndexOf("/", closeIndex)
1863
+ : -1;
1864
+ const insertAt = slashIndex > element.start ? slashIndex : closeIndex;
1865
+ return `${html.slice(0, insertAt)} ${name}="${escaped}"${html.slice(insertAt)}`;
1866
+ }
1867
+
1868
+ function setStyleValue(
1869
+ currentStyle: string | null,
1870
+ property: VisualStyleProperty,
1871
+ value: string,
1872
+ ): string {
1873
+ const declarations = parseStyleDeclarations(currentStyle);
1874
+ const existing = declarations.find((item) => item.property === property);
1875
+ if (existing) {
1876
+ existing.value = value;
1877
+ } else {
1878
+ declarations.push({ property, value });
1879
+ }
1880
+ return serializeStyleDeclarations(declarations);
1881
+ }
1882
+
1883
+ function applyStyleEdit(
1884
+ html: string,
1885
+ element: ParsedElement,
1886
+ intent: StyleEditIntent,
1887
+ ): { content: string; capability: EditCapability } | PatchResultStatus {
1888
+ const property = normalizeStyleProperty(intent.property);
1889
+ if (!property || !isSafeStyleValue(property, intent.value))
1890
+ return "unsupported";
1891
+ const nextStyle = setStyleValue(
1892
+ attributeValue(element, "style"),
1893
+ property,
1894
+ intent.value.trim(),
1895
+ );
1896
+ return {
1897
+ content: replaceOrInsertAttribute(html, element, "style", nextStyle),
1898
+ capability: {
1899
+ kind: "style",
1900
+ properties: [property],
1901
+ confidence: 0.9,
1902
+ },
1903
+ };
1904
+ }
1905
+
1906
+ function applyClassEdit(
1907
+ html: string,
1908
+ element: ParsedElement,
1909
+ intent: ClassEditIntent,
1910
+ ): { content: string; capability: EditCapability } | PatchResultStatus {
1911
+ const classes = classList(element);
1912
+ let nextClasses = [...classes];
1913
+
1914
+ if (intent.operation === "add") {
1915
+ const additions = classTokensFromIntent(intent);
1916
+ if (
1917
+ additions.length === 0 ||
1918
+ additions.some((token) => !isSafeClassToken(token))
1919
+ ) {
1920
+ return "unsupported";
1921
+ }
1922
+ nextClasses = Array.from(new Set([...classes, ...additions]));
1923
+ } else if (intent.operation === "remove") {
1924
+ const removals = classTokensFromIntent(intent);
1925
+ if (
1926
+ removals.length === 0 ||
1927
+ removals.some((token) => !isSafeClassToken(token))
1928
+ ) {
1929
+ return "unsupported";
1930
+ }
1931
+ nextClasses = classes.filter((token) => !removals.includes(token));
1932
+ } else if (intent.operation === "replace") {
1933
+ if (
1934
+ !intent.from ||
1935
+ !intent.to ||
1936
+ !isSafeClassToken(intent.from) ||
1937
+ !isSafeClassToken(intent.to)
1938
+ ) {
1939
+ return "unsupported";
1940
+ }
1941
+ if (!classes.includes(intent.from)) return "conflict";
1942
+ nextClasses = classes.map((token) =>
1943
+ token === intent.from ? (intent.to ?? token) : token,
1944
+ );
1945
+ } else {
1946
+ const replacement = classTokensFromIntent(intent);
1947
+ if (
1948
+ replacement.length === 0 ||
1949
+ replacement.some((token) => !isSafeClassToken(token))
1950
+ ) {
1951
+ return "unsupported";
1952
+ }
1953
+ nextClasses = replacement;
1954
+ }
1955
+
1956
+ return {
1957
+ content: replaceOrInsertAttribute(
1958
+ html,
1959
+ element,
1960
+ "class",
1961
+ nextClasses.join(" "),
1962
+ ),
1963
+ capability: {
1964
+ kind: "class",
1965
+ operations: [intent.operation],
1966
+ confidence: 0.88,
1967
+ },
1968
+ };
1969
+ }
1970
+
1971
+ function sanitizeTextEditHtml(html: string): string {
1972
+ return html
1973
+ .replace(
1974
+ /<\s*(script|style|iframe|object|embed|link|meta|base)\b[\s\S]*?<\s*\/\s*\1\s*>/gi,
1975
+ "",
1976
+ )
1977
+ .replace(
1978
+ /<\s*(script|style|iframe|object|embed|link|meta|base)\b[^>]*\/?\s*>/gi,
1979
+ "",
1980
+ )
1981
+ .replace(/\s+on[A-Za-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/g, "")
1982
+ .replace(
1983
+ /\s+(href|src|xlink:href)\s*=\s*(["'])\s*javascript:[\s\S]*?\2/gi,
1984
+ "",
1985
+ );
1986
+ }
1987
+
1988
+ function applyTextEdit(
1989
+ html: string,
1990
+ element: ParsedElement,
1991
+ intent: TextEditIntent,
1992
+ ): { content: string; capability: EditCapability } | PatchResultStatus {
1993
+ if (element.selfClosing || element.contentStart > element.contentEnd) {
1994
+ return "unsupported";
1995
+ }
1996
+ if (element.childIndexes.length > 0 && intent.html === undefined) {
1997
+ return "needsAgent";
1998
+ }
1999
+ const replacement =
2000
+ intent.html !== undefined
2001
+ ? sanitizeTextEditHtml(intent.html)
2002
+ : escapeHtmlText(intent.value);
2003
+ return {
2004
+ content: `${html.slice(0, element.contentStart)}${replacement}${html.slice(element.contentEnd)}`,
2005
+ capability: {
2006
+ kind: "text",
2007
+ operations: ["setTextContent"],
2008
+ confidence: 0.82,
2009
+ },
2010
+ };
2011
+ }
2012
+
2013
+ function applyMoveNodeEdit(
2014
+ html: string,
2015
+ element: ParsedElement,
2016
+ anchor: ParsedElement,
2017
+ intent: MoveNodeEditIntent,
2018
+ ): { content: string; capability: EditCapability } | PatchResultStatus {
2019
+ if (element.index === anchor.index) return "conflict";
2020
+ if (anchor.start >= element.start && anchor.end <= element.end) {
2021
+ return "conflict";
2022
+ }
2023
+ if (intent.placement === "inside" && anchor.selfClosing) {
2024
+ return "unsupported";
2025
+ }
2026
+
2027
+ const fragment = html.slice(element.start, element.end);
2028
+ const withoutTarget = `${html.slice(0, element.start)}${html.slice(
2029
+ element.end,
2030
+ )}`;
2031
+ const removedLength = element.end - element.start;
2032
+ const rawInsertAt =
2033
+ intent.placement === "before"
2034
+ ? anchor.start
2035
+ : intent.placement === "after"
2036
+ ? anchor.end
2037
+ : anchor.contentEnd;
2038
+ const insertAt =
2039
+ element.start < rawInsertAt ? rawInsertAt - removedLength : rawInsertAt;
2040
+
2041
+ if (insertAt < 0 || insertAt > withoutTarget.length) return "conflict";
2042
+
2043
+ return {
2044
+ content: `${withoutTarget.slice(0, insertAt)}${fragment}${withoutTarget.slice(insertAt)}`,
2045
+ capability: {
2046
+ kind: "structure",
2047
+ operations: ["moveNode"],
2048
+ confidence: 0.78,
2049
+ },
2050
+ };
2051
+ }
2052
+
2053
+ function findAfterNode(
2054
+ projection: CodeLayerProjection,
2055
+ before: CodeLayerNode,
2056
+ ): CodeLayerNode | undefined {
2057
+ return (
2058
+ projection.nodes.find((node) => node.id === before.id) ??
2059
+ projection.nodes.find(
2060
+ (node) =>
2061
+ node.tag === before.tag &&
2062
+ node.source?.openStart === before.source?.openStart,
2063
+ )
2064
+ );
2065
+ }
2066
+
2067
+ export function applyVisualEdit(
2068
+ html: string,
2069
+ intent: EditIntent,
2070
+ options: { source?: CodeLayerSource } = {},
2071
+ ): ApplyVisualEditResult {
2072
+ const source = options.source ?? { kind: "inline-html" };
2073
+ if (source.kind !== "inline-html" && source.kind !== "design-file") {
2074
+ const projection = buildCodeLayerProjection(html, { source });
2075
+ return {
2076
+ content: html,
2077
+ projection,
2078
+ result: patchResult(
2079
+ "unsupported",
2080
+ source,
2081
+ intent,
2082
+ false,
2083
+ `Source kind "${source.kind}" is not supported by the deterministic HTML editor yet.`,
2084
+ ),
2085
+ };
2086
+ }
2087
+
2088
+ const initial = buildProjection(html, source);
2089
+ const resolution = resolveTarget(initial.projection, intent.target);
2090
+ if (resolution.status !== "resolved" || !resolution.node) {
2091
+ return {
2092
+ content: html,
2093
+ projection: initial.projection,
2094
+ result: patchResult(
2095
+ "conflict",
2096
+ source,
2097
+ intent,
2098
+ false,
2099
+ resolution.message ?? "Could not resolve the edit target.",
2100
+ ),
2101
+ };
2102
+ }
2103
+
2104
+ const beforeNode = resolution.node;
2105
+ const before = summarizeNode(beforeNode);
2106
+ const element = initial.elementByNodeId.get(beforeNode.id);
2107
+ if (!element || !beforeNode.source) {
2108
+ return {
2109
+ content: html,
2110
+ projection: initial.projection,
2111
+ result: patchResult(
2112
+ "needsAgent",
2113
+ source,
2114
+ intent,
2115
+ false,
2116
+ "The target node does not have editable source spans.",
2117
+ beforeNode,
2118
+ undefined,
2119
+ before,
2120
+ ),
2121
+ };
2122
+ }
2123
+
2124
+ let edit: { content: string; capability: EditCapability } | PatchResultStatus;
2125
+ if (intent.kind === "style") {
2126
+ edit = applyStyleEdit(html, element, intent);
2127
+ } else if (intent.kind === "class") {
2128
+ edit = applyClassEdit(html, element, intent);
2129
+ } else if (intent.kind === "textContent") {
2130
+ edit = applyTextEdit(html, element, intent);
2131
+ } else {
2132
+ const anchorResolution = resolveTarget(initial.projection, intent.anchor);
2133
+ if (anchorResolution.status !== "resolved" || !anchorResolution.node) {
2134
+ return {
2135
+ content: html,
2136
+ projection: initial.projection,
2137
+ result: patchResult(
2138
+ "conflict",
2139
+ source,
2140
+ intent,
2141
+ false,
2142
+ anchorResolution.message ?? "Could not resolve the move anchor.",
2143
+ beforeNode,
2144
+ undefined,
2145
+ before,
2146
+ ),
2147
+ };
2148
+ }
2149
+ const anchorElement = initial.elementByNodeId.get(anchorResolution.node.id);
2150
+ if (!anchorElement || !anchorResolution.node.source) {
2151
+ return {
2152
+ content: html,
2153
+ projection: initial.projection,
2154
+ result: patchResult(
2155
+ "needsAgent",
2156
+ source,
2157
+ intent,
2158
+ false,
2159
+ "The move anchor does not have editable source spans.",
2160
+ beforeNode,
2161
+ undefined,
2162
+ before,
2163
+ ),
2164
+ };
2165
+ }
2166
+ edit = applyMoveNodeEdit(html, element, anchorElement, intent);
2167
+ }
2168
+
2169
+ if (typeof edit === "string") {
2170
+ const status = edit;
2171
+ return {
2172
+ content: html,
2173
+ projection: initial.projection,
2174
+ result: patchResult(
2175
+ status,
2176
+ source,
2177
+ intent,
2178
+ false,
2179
+ status === "conflict"
2180
+ ? "The requested edit conflicts with the current source."
2181
+ : status === "needsAgent"
2182
+ ? "The requested edit needs agent-level source rewriting."
2183
+ : "The requested edit is not supported by the deterministic editor.",
2184
+ beforeNode,
2185
+ undefined,
2186
+ before,
2187
+ ),
2188
+ };
2189
+ }
2190
+
2191
+ const nextProjection = buildCodeLayerProjection(edit.content, { source });
2192
+ const afterNode = findAfterNode(nextProjection, beforeNode);
2193
+ const after = afterNode ? summarizeNode(afterNode) : undefined;
2194
+
2195
+ return {
2196
+ content: edit.content,
2197
+ projection: nextProjection,
2198
+ result: patchResult(
2199
+ "applied",
2200
+ source,
2201
+ intent,
2202
+ edit.content !== html,
2203
+ edit.content === html
2204
+ ? "No source change was needed."
2205
+ : "Visual edit applied.",
2206
+ beforeNode,
2207
+ edit.capability,
2208
+ before,
2209
+ after,
2210
+ ),
2211
+ };
2212
+ }