@agent-native/core 0.80.8 → 0.80.10
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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +19 -0
- package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +6 -0
- package/corpus/core/docs/content/locales/de-DE/template-design.mdx +8 -0
- package/corpus/core/docs/content/locales/es-ES/template-design.mdx +10 -2
- package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +10 -2
- package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +7 -0
- package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +7 -0
- package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +7 -0
- package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +9 -2
- package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +6 -0
- package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +6 -0
- package/corpus/core/docs/content/template-design.mdx +9 -2
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/design-connect.ts +28 -2
- package/corpus/core/src/cli/mcp.ts +10 -0
- package/corpus/core/src/cli/skills.ts +103 -36
- package/corpus/core/src/client/error-format.ts +25 -0
- package/corpus/core/src/client/extensions/ExtensionViewer.tsx +13 -11
- package/corpus/core/src/client/guided-questions.tsx +32 -4
- package/corpus/core/src/client/onboarding/OnboardingPanel.tsx +33 -1
- package/corpus/core/src/client/resources/BuiltinCapabilityDetail.tsx +8 -0
- package/corpus/core/src/client/resources/use-builtin-capabilities.ts +4 -2
- package/corpus/core/src/client/session-replay.ts +34 -0
- package/corpus/core/src/client/sharing/ShareButton.tsx +106 -21
- package/corpus/core/src/coding-tools/index.ts +1 -1
- package/corpus/core/src/mcp/index.ts +2 -0
- package/corpus/core/src/mcp/screen-memory-stdio.ts +290 -0
- package/corpus/core/src/mcp-client/builtin-capabilities.ts +13 -1
- package/corpus/core/src/mcp-client/index.ts +9 -0
- package/corpus/core/src/mcp-client/screen-memory-local.ts +461 -0
- package/corpus/core/src/onboarding/types.ts +7 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +25 -0
- package/corpus/core/src/server/agents-bundle.ts +35 -7
- package/corpus/core/src/styles/agent-native.css +5 -0
- package/corpus/core/src/templates/default/app/global.css +35 -35
- package/corpus/templates/analytics/AGENTS.md +6 -0
- package/corpus/templates/analytics/actions/github-repo-files.ts +9 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +4 -4
- package/corpus/templates/analytics/app/global.css +144 -40
- package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +1 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +4 -4
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +9 -9
- package/corpus/templates/analytics/changelog/2026-06-29-agents-can-search-and-read-connected-github-repositories-whe.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-sessions-and-explorer-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
- package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -1
- package/corpus/templates/assets/app/components/library/LibraryPresetGrid.tsx +1 -1
- package/corpus/templates/assets/app/global.css +99 -35
- package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +4 -4
- package/corpus/templates/assets/app/routes/library.tsx +4 -4
- package/corpus/templates/assets/changelog/2026-06-29-asset-library-and-detail-layouts-adapt-when-the-agent-sidebar.md +6 -0
- package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/brain/app/global.css +63 -35
- package/corpus/templates/brain/app/routes/search.tsx +1 -1
- package/corpus/templates/brain/app/routes/settings.tsx +1 -1
- package/corpus/templates/brain/app/routes/sources.tsx +2 -2
- package/corpus/templates/brain/changelog/2026-06-29-search-settings-and-source-layouts-adapt-when-the-agent-sidebar.md +6 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +4 -4
- package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +3 -2
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -2
- package/corpus/templates/calendar/app/global.css +69 -35
- package/corpus/templates/calendar/changelog/2026-06-29-event-and-find-time-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
- package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/chat/app/global.css +35 -35
- package/corpus/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/clips/AGENTS.md +13 -0
- package/corpus/templates/clips/actions/get-screen-memory-status.ts +12 -0
- package/corpus/templates/clips/actions/query-screen-memory-context.ts +29 -0
- package/corpus/templates/clips/actions/view-screen.ts +2 -2
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
- package/corpus/templates/clips/app/global.css +47 -35
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +3 -3
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +45 -18
- package/corpus/templates/clips/changelog/2026-06-29-meeting-detail-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-29-screen-memory-keeps-a-local-rolling-screen-buffer-for-agent-context.md +5 -0
- package/corpus/templates/clips/changelog/2026-06-29-video-chat-is-easier-to-find-on-recording-pages-and-s3-setup.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/styles.css +39 -39
- package/corpus/templates/clips/desktop/src/app.tsx +290 -1
- package/corpus/templates/clips/desktop/src/shared/config.ts +83 -0
- package/corpus/templates/clips/desktop/src/styles.css +67 -52
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +50 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +17 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +13 -13
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +990 -0
- package/corpus/templates/clips/server/plugins/onboarding.ts +2 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +3 -3
- package/corpus/templates/content/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/content/app/global.css +29 -15
- package/corpus/templates/content/changelog/2026-06-29-database-gallery-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +25 -20
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -0
- package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +8 -0
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +13 -0
- package/corpus/templates/design/AGENTS.md +20 -12
- package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1054 -0
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
- package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
- package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
- package/corpus/templates/design/actions/apply-design-state.ts +200 -0
- package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
- package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
- package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +214 -8
- package/corpus/templates/design/actions/capture-design-state.ts +224 -0
- package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
- package/corpus/templates/design/actions/create-component.ts +447 -0
- package/corpus/templates/design/actions/create-design-branch.ts +263 -0
- package/corpus/templates/design/actions/create-design-state.ts +162 -0
- package/corpus/templates/design/actions/delete-design-state.ts +55 -0
- package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
- package/corpus/templates/design/actions/export-coding-handoff.ts +12 -4
- package/corpus/templates/design/actions/generate-design.ts +6 -21
- package/corpus/templates/design/actions/generate-screens.ts +43 -2
- package/corpus/templates/design/actions/get-component-details.ts +242 -0
- package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
- package/corpus/templates/design/actions/get-design-review.ts +314 -0
- package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
- package/corpus/templates/design/actions/get-design.ts +1 -0
- package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
- package/corpus/templates/design/actions/index-components.ts +258 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
- package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
- package/corpus/templates/design/actions/list-design-states.ts +72 -0
- package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
- package/corpus/templates/design/actions/open-component-source.ts +237 -0
- package/corpus/templates/design/actions/present-design-variants.ts +355 -30
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
- package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
- package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
- package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
- package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
- package/corpus/templates/design/actions/run-design-audit.ts +421 -0
- package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
- package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
- package/corpus/templates/design/actions/view-screen.ts +75 -31
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1738 -135
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +1 -1
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1873 -132
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +384 -219
- package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2103 -295
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +83 -206
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +1 -1
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
- package/corpus/templates/design/app/components/design/index.ts +16 -1
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +238 -137
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +188 -14
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +24 -5
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +57 -1
- package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +37 -12
- package/corpus/templates/design/app/components/design/inspector/index.ts +1 -0
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +12 -5
- package/corpus/templates/design/app/components/design/types.ts +9 -0
- package/corpus/templates/design/app/components/layout/Layout.tsx +23 -5
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/global.css +77 -49
- package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
- package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +11 -2
- package/corpus/templates/design/app/i18n/zh-TW.ts +34 -0
- package/corpus/templates/design/app/i18n-data.ts +314 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +4929 -892
- package/corpus/templates/design/app/root.tsx +1 -1
- package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-containers-no-longer-block-selecting-nested-elements-and-nor.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-agents-now-generate-and-refine-variants-from-stronger.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-questions-now-appear-as-a-cleaner-canvas-intake-with-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-variant-directions-now-appear-as-board-screens-with-chat-buttons.md +5 -0
- package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-element-selection-no-longer-snaps-back-to-the-previous-layer.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-frame-resize-measurements-now-stay-beside-the-cursor-while-d.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-inspector-number-fields-have-cleaner-tooltips-and-draggable-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-locking-layer-nesting-generated-question-intake-and-im.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-names-in-the-sidebar-can-scroll-horizontally-instead-o.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-nesting-in-the-sidebar-now-uses-tighter-spacing-so-nam.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layers-no-longer-repeat-document-wrapper-rows-when-a-screen-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-overview-previews-now-keep-element-hover-and-selection-tied-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-pressing-escape-now-cancels-a-stuck-screen-drag-in-the-overv.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-is-easier-to-find-and-named-screens-open-dir.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-only-highlights-screens-when-the-frame-or-ti.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-previews-now-resize-to-match-the-selected-de.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-now-uses-a-pointer-cursor-instead-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-outlines-now-stay-locked-to-the-se.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-zoom-now-reports-real-screen-scale-separatel.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-feels-smoother-and-selection-outline.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-now-stays-on-the-overview-canvas-ins.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-selecting-a-nested-layer-from-all-screens-now-keeps-you-in-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-share-now-groups-link-sharing-exports-and-agent-handoff.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-design-editor-loading-skeleton-now-uses-a-softer-charcoa.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-device-preview-menu-now-sits-next-to-the-preview-button-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-inspector-stays-read-only-while-an-empty-design-is-still.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-missing-design-view-now-matches-the-rest-of-the-editor-c.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-icon-and-tool-option-menus-better-match-the-edi.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-typography-controls-now-keep-the-font-name-visible-and-move-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
- package/corpus/templates/design/e2e/README.md +3 -3
- package/corpus/templates/design/e2e/global-setup.ts +11 -22
- package/corpus/templates/design/e2e/helpers.ts +128 -11
- package/corpus/templates/design/playwright.config.ts +9 -3
- package/corpus/templates/design/server/db/schema.ts +123 -0
- package/corpus/templates/design/server/lib/coding-handoff.ts +126 -2
- package/corpus/templates/design/server/plugins/core-routes.ts +1 -2
- package/corpus/templates/design/server/plugins/db.ts +90 -0
- package/corpus/templates/design/server/routes/api/design-handoff/[id].zip.get.ts +86 -0
- package/corpus/templates/design/shared/builder-app.ts +297 -0
- package/corpus/templates/design/shared/capability-resolver.ts +123 -0
- package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
- package/corpus/templates/design/shared/code-layer.ts +1141 -93
- package/corpus/templates/design/shared/component-model.ts +239 -0
- package/corpus/templates/design/shared/design-review.ts +275 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
- package/corpus/templates/design/shared/design-state.ts +112 -0
- package/corpus/templates/design/shared/design-surface-index.ts +258 -0
- package/corpus/templates/design/shared/generation-session.ts +41 -0
- package/corpus/templates/design/shared/motion-compiler.ts +278 -0
- package/corpus/templates/design/shared/motion-timeline.ts +193 -0
- package/corpus/templates/design/shared/responsive-classes.ts +452 -0
- package/corpus/templates/design/shared/shader-fill.ts +323 -0
- package/corpus/templates/design/shared/source-mode.ts +245 -0
- package/corpus/templates/dispatch/app/global.css +79 -35
- package/corpus/templates/dispatch/app/routes/integrations.tsx +8 -7
- package/corpus/templates/dispatch/changelog/2026-06-29-integration-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/forms/app/components/layout/Layout.tsx +6 -2
- package/corpus/templates/forms/app/global.css +64 -35
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +2 -2
- package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +3 -3
- package/corpus/templates/forms/changelog/2026-06-29-form-list-and-insight-layouts-adapt-when-the-agent-sidebar.md +6 -0
- package/corpus/templates/macros/app/components/layout/AppLayout.tsx +3 -1
- package/corpus/templates/macros/app/global.css +25 -9
- package/corpus/templates/macros/changelog/2026-06-29-macro-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -2
- package/corpus/templates/mail/app/global.css +42 -36
- package/corpus/templates/mail/app/pages/InboxPage.tsx +1 -1
- package/corpus/templates/mail/changelog/2026-06-29-the-contact-panel-now-adapts-to-the-available-mail-pane-width.md +6 -0
- package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -1
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
- package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +6 -7
- package/corpus/templates/plan/app/global.css +74 -42
- package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
- package/corpus/templates/plan/changelog/2026-06-29-plan-document-blocks-adapt-to-the-available-document-width.md +6 -0
- package/corpus/templates/plan/changelog/2026-06-29-plan-titles-and-summaries-can-be-edited-inline-without-focus.md +6 -0
- package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -1
- package/corpus/templates/slides/app/global.css +49 -33
- package/corpus/templates/slides/changelog/2026-06-29-slide-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/videos/app/components/layout/Layout.tsx +3 -1
- package/corpus/templates/videos/app/global.css +35 -35
- package/corpus/templates/videos/changelog/2026-06-29-video-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +28 -2
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +10 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +95 -33
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +17 -0
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionViewer.js +3 -4
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/guided-questions.d.ts +4 -0
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +16 -7
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/client/onboarding/OnboardingPanel.js +26 -1
- package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
- package/dist/client/resources/BuiltinCapabilityDetail.d.ts.map +1 -1
- package/dist/client/resources/BuiltinCapabilityDetail.js +1 -1
- package/dist/client/resources/BuiltinCapabilityDetail.js.map +1 -1
- package/dist/client/resources/use-builtin-capabilities.d.ts +1 -1
- package/dist/client/resources/use-builtin-capabilities.d.ts.map +1 -1
- package/dist/client/resources/use-builtin-capabilities.js +2 -1
- package/dist/client/resources/use-builtin-capabilities.js.map +1 -1
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +31 -0
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/sharing/ShareButton.d.ts +14 -0
- package/dist/client/sharing/ShareButton.d.ts.map +1 -1
- package/dist/client/sharing/ShareButton.js +36 -7
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/coding-tools/index.js +1 -1
- package/dist/coding-tools/index.js.map +1 -1
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +6 -0
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -0
- package/dist/mcp/screen-memory-stdio.js +213 -0
- package/dist/mcp/screen-memory-stdio.js.map +1 -0
- package/dist/mcp-client/builtin-capabilities.d.ts +1 -1
- package/dist/mcp-client/builtin-capabilities.d.ts.map +1 -1
- package/dist/mcp-client/builtin-capabilities.js +10 -0
- package/dist/mcp-client/builtin-capabilities.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -0
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -0
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/screen-memory-local.d.ts +61 -0
- package/dist/mcp-client/screen-memory-local.d.ts.map +1 -0
- package/dist/mcp-client/screen-memory-local.js +336 -0
- package/dist/mcp-client/screen-memory-local.js.map +1 -0
- package/dist/observability/routes.d.ts +3 -3
- package/dist/onboarding/types.d.ts +7 -0
- package/dist/onboarding/types.d.ts.map +1 -1
- package/dist/onboarding/types.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +26 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agents-bundle.d.ts.map +1 -1
- package/dist/server/agents-bundle.js +22 -7
- package/dist/server/agents-bundle.js.map +1 -1
- package/dist/styles/agent-native.css +5 -0
- package/dist/templates/default/app/global.css +35 -35
- package/docs/content/locales/ar-SA/template-design.mdx +6 -0
- package/docs/content/locales/de-DE/template-design.mdx +8 -0
- package/docs/content/locales/es-ES/template-design.mdx +10 -2
- package/docs/content/locales/fr-FR/template-design.mdx +10 -2
- package/docs/content/locales/hi-IN/template-design.mdx +7 -0
- package/docs/content/locales/ja-JP/template-design.mdx +7 -0
- package/docs/content/locales/ko-KR/template-design.mdx +7 -0
- package/docs/content/locales/pt-BR/template-design.mdx +9 -2
- package/docs/content/locales/zh-CN/template-design.mdx +6 -0
- package/docs/content/locales/zh-TW/template-design.mdx +6 -0
- package/docs/content/template-design.mdx +9 -2
- package/package.json +1 -1
- package/src/templates/default/app/global.css +35 -35
- package/corpus/templates/design/app/components/design/VariantGrid.tsx +0 -189
- package/corpus/templates/design/app/components/design/VariantHandoffCard.tsx +0 -115
- package/corpus/templates/design/app/hooks/use-variant-flow.ts +0 -342
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isComponentInstance,
|
|
3
|
+
instanceFromNode,
|
|
4
|
+
type ComponentInstance,
|
|
5
|
+
} from "./component-model";
|
|
6
|
+
import type { TailwindBreakpointPrefix } from "./design-state.js";
|
|
7
|
+
import {
|
|
8
|
+
parseClassGroups,
|
|
9
|
+
parseClassToken,
|
|
10
|
+
setPropertyClass,
|
|
11
|
+
removePropertyClass,
|
|
12
|
+
} from "./responsive-classes.js";
|
|
1
13
|
import type { DesignSourceType } from "./source-mode";
|
|
2
14
|
|
|
3
15
|
export type CodeLayerSourceKind =
|
|
@@ -137,10 +149,32 @@ export type VisualStyleProperty =
|
|
|
137
149
|
|
|
138
150
|
export interface StyleToken {
|
|
139
151
|
property: VisualStyleProperty;
|
|
152
|
+
/**
|
|
153
|
+
* The resolved value at the *base* breakpoint (unprefixed), or the inline
|
|
154
|
+
* style value. For class-sourced tokens this is the utility string of the
|
|
155
|
+
* base class (e.g. `"text-sm"`). Use `breakpointValues` to inspect how the
|
|
156
|
+
* value differs across responsive prefixes.
|
|
157
|
+
*/
|
|
140
158
|
value: string;
|
|
141
159
|
token: string;
|
|
142
160
|
source: "inline-style" | "class";
|
|
143
161
|
confidence: number;
|
|
162
|
+
/**
|
|
163
|
+
* For class-sourced tokens only: the resolved utility string per responsive
|
|
164
|
+
* prefix. Only prefixes that have an explicit class token are included.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* // className="text-sm md:text-base lg:text-lg"
|
|
168
|
+
* // styleToken for "color" property would have:
|
|
169
|
+
* // breakpointValues = { base: "text-sm", md: "text-base", lg: "text-lg" }
|
|
170
|
+
*/
|
|
171
|
+
breakpointValues?: Partial<Record<TailwindBreakpointPrefix, string>>;
|
|
172
|
+
/**
|
|
173
|
+
* For class-sourced tokens only: the prefixes (other than `"base"`) at which
|
|
174
|
+
* this property has a responsive override in the class list. Populated when
|
|
175
|
+
* `breakpointValues` has keys other than `"base"`.
|
|
176
|
+
*/
|
|
177
|
+
overriddenAtPrefixes?: TailwindBreakpointPrefix[];
|
|
144
178
|
}
|
|
145
179
|
|
|
146
180
|
export interface LayoutContext {
|
|
@@ -177,6 +211,28 @@ export type EditCapability =
|
|
|
177
211
|
confidence: number;
|
|
178
212
|
reason?: string;
|
|
179
213
|
}
|
|
214
|
+
| {
|
|
215
|
+
/**
|
|
216
|
+
* Responsive-class editing — adds, replaces, or removes a Tailwind
|
|
217
|
+
* utility at a specific breakpoint prefix without touching other
|
|
218
|
+
* breakpoints. Uses the helpers in `responsive-classes.ts`
|
|
219
|
+
* (setPropertyClass / removePropertyClass) and the same deterministic
|
|
220
|
+
* HTML-patch path as `class` edits.
|
|
221
|
+
*
|
|
222
|
+
* The `prefix` field indicates the active breakpoint scope for which
|
|
223
|
+
* this capability was computed (derived from the canvas frame width via
|
|
224
|
+
* `widthToPrefix`). Callers may target any prefix — `prefix` is
|
|
225
|
+
* informational, not a constraint.
|
|
226
|
+
*/
|
|
227
|
+
kind: "responsive-class";
|
|
228
|
+
/** Active breakpoint scope (informational). */
|
|
229
|
+
prefix: TailwindBreakpointPrefix;
|
|
230
|
+
operations: Array<"add" | "remove" | "replace">;
|
|
231
|
+
/** Properties that currently have per-breakpoint overrides (non-empty means overrides exist). */
|
|
232
|
+
overriddenProperties: string[];
|
|
233
|
+
confidence: number;
|
|
234
|
+
reason?: string;
|
|
235
|
+
}
|
|
180
236
|
| {
|
|
181
237
|
kind: "text";
|
|
182
238
|
operations: Array<"setTextContent">;
|
|
@@ -211,6 +267,15 @@ export interface CodeLayerNode {
|
|
|
211
267
|
capabilities: EditCapability[];
|
|
212
268
|
confidence: number;
|
|
213
269
|
source: CodeLayerSourceSpan | null;
|
|
270
|
+
/**
|
|
271
|
+
* Present when the node is the root of a component instance — i.e. it
|
|
272
|
+
* carries a `data-agent-native-component` attribute (Alpine-annotated or
|
|
273
|
+
* build-time-instrumented). The canvas uses this to draw the component
|
|
274
|
+
* outline and the inspector uses it to surface component-level controls.
|
|
275
|
+
*
|
|
276
|
+
* `undefined` when the node is not a component root.
|
|
277
|
+
*/
|
|
278
|
+
componentInstance?: ComponentInstance;
|
|
214
279
|
}
|
|
215
280
|
|
|
216
281
|
export interface ProjectionDiagnostic {
|
|
@@ -324,11 +389,91 @@ export interface MoveNodeEditIntent {
|
|
|
324
389
|
placement: "before" | "after" | "inside";
|
|
325
390
|
}
|
|
326
391
|
|
|
392
|
+
/**
|
|
393
|
+
* GROUP: wrap sibling nodes sharing a parent inside a new <div> wrapper.
|
|
394
|
+
* The wrapper is inserted at the position of the first target; targets are
|
|
395
|
+
* reparented into it in source order. The new wrapper gets a fresh
|
|
396
|
+
* data-agent-native-node-id and data-agent-native-layer-name="Group".
|
|
397
|
+
*
|
|
398
|
+
* When autoLayout is true the wrapper also receives
|
|
399
|
+
* `display:flex; flex-direction:column; gap:8px` and
|
|
400
|
+
* position/left/top/right/bottom are stripped from each wrapped child.
|
|
401
|
+
*
|
|
402
|
+
* Returns "unsupported" if the targets don't share a common parent.
|
|
403
|
+
*/
|
|
404
|
+
export interface WrapNodesEditIntent {
|
|
405
|
+
kind: "wrapNodes";
|
|
406
|
+
targetIds: string[];
|
|
407
|
+
autoLayout?: boolean;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* UNGROUP: replace the wrapper node with its children, spliced into the
|
|
412
|
+
* wrapper's parent at the wrapper's position, then remove the wrapper.
|
|
413
|
+
*/
|
|
414
|
+
export interface UnwrapEditIntent {
|
|
415
|
+
kind: "unwrap";
|
|
416
|
+
targetId: string;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* CONVERT an existing container to/from auto-layout.
|
|
421
|
+
* When enabled, sets display:flex (+ flex-direction, gap) on the target and
|
|
422
|
+
* strips position:absolute/left/top/right/bottom from its DIRECT children.
|
|
423
|
+
* When !enabled, sets display:block (turns auto-layout off).
|
|
424
|
+
*/
|
|
425
|
+
export interface AutoLayoutEditIntent {
|
|
426
|
+
kind: "autoLayout";
|
|
427
|
+
targetId: string;
|
|
428
|
+
enabled: boolean;
|
|
429
|
+
direction?: "row" | "column";
|
|
430
|
+
gap?: string;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Responsive-class edit intent — adds, replaces, or removes a single Tailwind
|
|
435
|
+
* utility at the given `prefix` (breakpoint scope) without touching classes at
|
|
436
|
+
* other breakpoints.
|
|
437
|
+
*
|
|
438
|
+
* Examples:
|
|
439
|
+
* - Add `text-base` at `md:` on a node that already has `text-sm` base:
|
|
440
|
+
* `{ kind: "responsive-class", target, prefix: "md", operation: "add", utility: "text-base" }`
|
|
441
|
+
*
|
|
442
|
+
* - Replace whatever `text-*` class currently lives at `lg:` with `text-xl`:
|
|
443
|
+
* `{ kind: "responsive-class", target, prefix: "lg", operation: "replace", utility: "text-xl" }`
|
|
444
|
+
*
|
|
445
|
+
* - Remove the `md:` override for the `text` stem, falling back to the base:
|
|
446
|
+
* `{ kind: "responsive-class", target, prefix: "md", operation: "remove", stem: "text" }`
|
|
447
|
+
*
|
|
448
|
+
* `utility` should be the bare utility without its prefix (e.g. `"text-lg"`,
|
|
449
|
+
* not `"md:text-lg"`). The prefix is applied automatically.
|
|
450
|
+
* `stem` is required only for `"remove"` operations.
|
|
451
|
+
*/
|
|
452
|
+
export interface ResponsiveClassEditIntent {
|
|
453
|
+
kind: "responsive-class";
|
|
454
|
+
target: EditIntentTarget;
|
|
455
|
+
/** Target breakpoint prefix. Use `"base"` to edit the unprefixed class. */
|
|
456
|
+
prefix: TailwindBreakpointPrefix;
|
|
457
|
+
operation: "add" | "remove" | "replace";
|
|
458
|
+
/** The bare utility to add or replace (without prefix). Required for add/replace. */
|
|
459
|
+
utility?: string;
|
|
460
|
+
/**
|
|
461
|
+
* The CSS-property stem to remove (e.g. `"text"`, `"bg"`, `"p"`).
|
|
462
|
+
* Required for `"remove"` operations; ignored for add/replace (the stem is
|
|
463
|
+
* derived from `utility` instead).
|
|
464
|
+
*/
|
|
465
|
+
stem?: string;
|
|
466
|
+
}
|
|
467
|
+
|
|
327
468
|
export type EditIntent =
|
|
328
469
|
| StyleEditIntent
|
|
329
470
|
| ClassEditIntent
|
|
330
471
|
| TextEditIntent
|
|
331
|
-
| MoveNodeEditIntent
|
|
472
|
+
| MoveNodeEditIntent
|
|
473
|
+
| WrapNodesEditIntent
|
|
474
|
+
| UnwrapEditIntent
|
|
475
|
+
| AutoLayoutEditIntent
|
|
476
|
+
| ResponsiveClassEditIntent;
|
|
332
477
|
|
|
333
478
|
export interface EditIntentResolution {
|
|
334
479
|
status: "resolved" | "conflict" | "unsupported";
|
|
@@ -372,6 +517,8 @@ export interface PatchResult {
|
|
|
372
517
|
after?: PatchNodeSummary;
|
|
373
518
|
changed: boolean;
|
|
374
519
|
message?: string;
|
|
520
|
+
/** For wrapNodes: the data-agent-native-node-id of the newly created wrapper. */
|
|
521
|
+
wrapperNodeId?: string;
|
|
375
522
|
}
|
|
376
523
|
|
|
377
524
|
export interface ApplyVisualEditResult {
|
|
@@ -1234,6 +1381,8 @@ function stableSourceIdForElement(element: ParsedElement): string | null {
|
|
|
1234
1381
|
|
|
1235
1382
|
function styleTokensFor(element: ParsedElement): StyleToken[] {
|
|
1236
1383
|
const tokens: StyleToken[] = [];
|
|
1384
|
+
|
|
1385
|
+
// --- Inline styles (no breakpoint concept) ---
|
|
1237
1386
|
for (const declaration of parseStyleDeclarations(
|
|
1238
1387
|
attributeValue(element, "style"),
|
|
1239
1388
|
)) {
|
|
@@ -1248,61 +1397,104 @@ function styleTokensFor(element: ParsedElement): StyleToken[] {
|
|
|
1248
1397
|
});
|
|
1249
1398
|
}
|
|
1250
1399
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1400
|
+
// --- Class tokens — responsive-aware ---
|
|
1401
|
+
// Group all class tokens by breakpoint prefix so we can build per-property
|
|
1402
|
+
// breakpointValues maps and detect overrides.
|
|
1403
|
+
const classValue = attributeValue(element, "class") ?? "";
|
|
1404
|
+
const groups = parseClassGroups(classValue);
|
|
1255
1405
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1406
|
+
// For each property, collect the utility value at every prefix that has one.
|
|
1407
|
+
// We key by property so we emit one token per property, not one per class.
|
|
1408
|
+
const propertyMap = new Map<
|
|
1409
|
+
VisualStyleProperty,
|
|
1410
|
+
{
|
|
1411
|
+
property: VisualStyleProperty;
|
|
1412
|
+
confidence: number;
|
|
1413
|
+
breakpointValues: Partial<Record<TailwindBreakpointPrefix, string>>;
|
|
1414
|
+
/** The raw token for the base occurrence (for backward-compat `token` field). */
|
|
1415
|
+
baseToken: string;
|
|
1416
|
+
}
|
|
1417
|
+
>();
|
|
1418
|
+
|
|
1419
|
+
const allPrefixes: ReadonlyArray<TailwindBreakpointPrefix> = [
|
|
1420
|
+
"base",
|
|
1421
|
+
"sm",
|
|
1422
|
+
"md",
|
|
1423
|
+
"lg",
|
|
1424
|
+
"xl",
|
|
1425
|
+
"2xl",
|
|
1426
|
+
];
|
|
1258
1427
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
if (/^(p|px|py|pt|pr|pb|pl)-/.test(normalized)) {
|
|
1289
|
-
return {
|
|
1290
|
-
property: "padding",
|
|
1291
|
-
value: token,
|
|
1292
|
-
token,
|
|
1293
|
-
source: "class",
|
|
1294
|
-
confidence: 0.62,
|
|
1295
|
-
};
|
|
1428
|
+
for (const prefix of allPrefixes) {
|
|
1429
|
+
for (const rawToken of groups[prefix]) {
|
|
1430
|
+
const parsed = parseClassToken(rawToken);
|
|
1431
|
+
const mapped = utilityToStyleProperty(parsed.utility);
|
|
1432
|
+
if (!mapped) continue;
|
|
1433
|
+
const { property, confidence } = mapped;
|
|
1434
|
+
|
|
1435
|
+
// Resolve the display value the same way classStyleToken does.
|
|
1436
|
+
const resolvedValue =
|
|
1437
|
+
property === "display"
|
|
1438
|
+
? parsed.utility === "hidden"
|
|
1439
|
+
? "none"
|
|
1440
|
+
: parsed.utility
|
|
1441
|
+
: parsed.utility; // utility without prefix, e.g. "text-sm"
|
|
1442
|
+
|
|
1443
|
+
const existing = propertyMap.get(property);
|
|
1444
|
+
if (existing) {
|
|
1445
|
+
existing.breakpointValues[prefix] = resolvedValue;
|
|
1446
|
+
if (existing.confidence < confidence) existing.confidence = confidence;
|
|
1447
|
+
} else {
|
|
1448
|
+
propertyMap.set(property, {
|
|
1449
|
+
property,
|
|
1450
|
+
confidence,
|
|
1451
|
+
breakpointValues: { [prefix]: resolvedValue },
|
|
1452
|
+
baseToken: rawToken,
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1296
1456
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1457
|
+
|
|
1458
|
+
for (const entry of propertyMap.values()) {
|
|
1459
|
+
const baseValue = entry.breakpointValues["base"] ?? "";
|
|
1460
|
+
// The full original token for the base occurrence (for backward compat).
|
|
1461
|
+
const rawBaseToken = entry.baseToken;
|
|
1462
|
+
const overriddenAt = Object.keys(entry.breakpointValues).filter(
|
|
1463
|
+
(p) => p !== "base",
|
|
1464
|
+
) as TailwindBreakpointPrefix[];
|
|
1465
|
+
|
|
1466
|
+
tokens.push({
|
|
1467
|
+
property: entry.property,
|
|
1468
|
+
// `value` = the base utility string (backward-compatible).
|
|
1469
|
+
value: baseValue || rawBaseToken,
|
|
1470
|
+
token: rawBaseToken,
|
|
1302
1471
|
source: "class",
|
|
1303
|
-
confidence:
|
|
1304
|
-
|
|
1472
|
+
confidence: entry.confidence,
|
|
1473
|
+
breakpointValues: { ...entry.breakpointValues },
|
|
1474
|
+
overriddenAtPrefixes: overriddenAt.length > 0 ? overriddenAt : undefined,
|
|
1475
|
+
});
|
|
1305
1476
|
}
|
|
1477
|
+
|
|
1478
|
+
return tokens;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Map a bare Tailwind utility (without any responsive prefix, e.g. `"text-sm"`
|
|
1483
|
+
* not `"md:text-sm"`) to the `VisualStyleProperty` it most likely controls.
|
|
1484
|
+
* Returns `null` when the utility is not recognised.
|
|
1485
|
+
*
|
|
1486
|
+
* This is called by both the legacy `classStyleToken` path and the new
|
|
1487
|
+
* responsive-aware `styleTokensFor` implementation.
|
|
1488
|
+
*/
|
|
1489
|
+
function utilityToStyleProperty(
|
|
1490
|
+
utility: string,
|
|
1491
|
+
): { property: VisualStyleProperty; confidence: number } | null {
|
|
1492
|
+
if (/^w-/.test(utility)) return { property: "width", confidence: 0.64 };
|
|
1493
|
+
if (/^h-/.test(utility)) return { property: "height", confidence: 0.64 };
|
|
1494
|
+
if (/^bg-/.test(utility)) return { property: "background", confidence: 0.6 };
|
|
1495
|
+
if (/^(p|px|py|pt|pr|pb|pl)-/.test(utility))
|
|
1496
|
+
return { property: "padding", confidence: 0.62 };
|
|
1497
|
+
if (/^gap-/.test(utility)) return { property: "gap", confidence: 0.62 };
|
|
1306
1498
|
if (
|
|
1307
1499
|
[
|
|
1308
1500
|
"block",
|
|
@@ -1313,28 +1505,26 @@ function classStyleToken(token: string): StyleToken | null {
|
|
|
1313
1505
|
"grid",
|
|
1314
1506
|
"inline-grid",
|
|
1315
1507
|
"hidden",
|
|
1316
|
-
].includes(
|
|
1508
|
+
].includes(utility)
|
|
1317
1509
|
) {
|
|
1318
|
-
return {
|
|
1319
|
-
property: "display",
|
|
1320
|
-
value: normalized === "hidden" ? "none" : normalized,
|
|
1321
|
-
token,
|
|
1322
|
-
source: "class",
|
|
1323
|
-
confidence: 0.68,
|
|
1324
|
-
};
|
|
1325
|
-
}
|
|
1326
|
-
if (/^text-/.test(normalized)) {
|
|
1327
|
-
return {
|
|
1328
|
-
property: "color",
|
|
1329
|
-
value: token,
|
|
1330
|
-
token,
|
|
1331
|
-
source: "class",
|
|
1332
|
-
confidence: 0.45,
|
|
1333
|
-
};
|
|
1510
|
+
return { property: "display", confidence: 0.68 };
|
|
1334
1511
|
}
|
|
1512
|
+
if (/^text-/.test(utility)) return { property: "color", confidence: 0.45 };
|
|
1335
1513
|
return null;
|
|
1336
1514
|
}
|
|
1337
1515
|
|
|
1516
|
+
function classStyleToken(token: string): StyleToken | null {
|
|
1517
|
+
const { utility } = parseClassToken(token);
|
|
1518
|
+
const mapped = utilityToStyleProperty(utility);
|
|
1519
|
+
if (!mapped) return null;
|
|
1520
|
+
const { property, confidence } = mapped;
|
|
1521
|
+
// The legacy value field preserves the original full token (including prefix)
|
|
1522
|
+
// for backward compatibility. The responsive path uses `breakpointValues`.
|
|
1523
|
+
const value =
|
|
1524
|
+
property === "display" ? (utility === "hidden" ? "none" : utility) : token;
|
|
1525
|
+
return { property, value, token, source: "class", confidence };
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1338
1528
|
function layoutFor(
|
|
1339
1529
|
element: ParsedElement,
|
|
1340
1530
|
parent: ParsedElement | undefined,
|
|
@@ -1472,6 +1662,9 @@ function treeTypeForNode(node: CodeLayerNode): CodeLayerTreeNodeType {
|
|
|
1472
1662
|
if (TEXT_LAYER_TAGS.has(node.tag)) return "text";
|
|
1473
1663
|
if (IMAGE_LAYER_TAGS.has(node.tag)) return "image";
|
|
1474
1664
|
if (SHAPE_LAYER_TAGS.has(node.tag)) return "shape";
|
|
1665
|
+
// A node annotated as a component instance is always classified as "component"
|
|
1666
|
+
// regardless of its tag — this is the canonical detection path.
|
|
1667
|
+
if (node.componentInstance) return "component";
|
|
1475
1668
|
if (
|
|
1476
1669
|
COMPONENT_LAYER_TAGS.has(node.tag) ||
|
|
1477
1670
|
node.classes.some((item) => /component|card|button|control/.test(item))
|
|
@@ -1485,6 +1678,49 @@ function treeTypeForNode(node: CodeLayerNode): CodeLayerTreeNodeType {
|
|
|
1485
1678
|
return "element";
|
|
1486
1679
|
}
|
|
1487
1680
|
|
|
1681
|
+
function isCollapsibleDocumentShellNode(
|
|
1682
|
+
node: CodeLayerTreeNode,
|
|
1683
|
+
nodesById: Map<string, CodeLayerNode>,
|
|
1684
|
+
): boolean {
|
|
1685
|
+
if (node.tag !== "html" && node.tag !== "body") return false;
|
|
1686
|
+
return nodesById.get(node.id)?.layerNameSource === "tag";
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function compactCodeLayerTreeNodes(
|
|
1690
|
+
nodes: CodeLayerTreeNode[],
|
|
1691
|
+
nodesById: Map<string, CodeLayerNode>,
|
|
1692
|
+
ancestors: Set<string> = new Set(),
|
|
1693
|
+
): CodeLayerTreeNode[] {
|
|
1694
|
+
const compacted: CodeLayerTreeNode[] = [];
|
|
1695
|
+
const siblingIds = new Set<string>();
|
|
1696
|
+
|
|
1697
|
+
for (const node of nodes) {
|
|
1698
|
+
if (ancestors.has(node.id)) continue;
|
|
1699
|
+
|
|
1700
|
+
const nextAncestors = new Set(ancestors);
|
|
1701
|
+
nextAncestors.add(node.id);
|
|
1702
|
+
const children = compactCodeLayerTreeNodes(
|
|
1703
|
+
node.children,
|
|
1704
|
+
nodesById,
|
|
1705
|
+
nextAncestors,
|
|
1706
|
+
);
|
|
1707
|
+
const compactedNode: CodeLayerTreeNode = { ...node, children };
|
|
1708
|
+
const promotedNodes =
|
|
1709
|
+
isCollapsibleDocumentShellNode(compactedNode, nodesById) &&
|
|
1710
|
+
children.length > 0
|
|
1711
|
+
? children
|
|
1712
|
+
: [compactedNode];
|
|
1713
|
+
|
|
1714
|
+
for (const promotedNode of promotedNodes) {
|
|
1715
|
+
if (siblingIds.has(promotedNode.id)) continue;
|
|
1716
|
+
siblingIds.add(promotedNode.id);
|
|
1717
|
+
compacted.push(promotedNode);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
return compacted;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1488
1724
|
function capabilitiesFor(element: ParsedElement): EditCapability[] {
|
|
1489
1725
|
const capabilities: EditCapability[] = [
|
|
1490
1726
|
{
|
|
@@ -1499,6 +1735,39 @@ function capabilitiesFor(element: ParsedElement): EditCapability[] {
|
|
|
1499
1735
|
},
|
|
1500
1736
|
];
|
|
1501
1737
|
|
|
1738
|
+
// Responsive-class capability — detect which properties already carry
|
|
1739
|
+
// breakpoint overrides so the inspector can surface override indicators.
|
|
1740
|
+
const classValue = attributeValue(element, "class") ?? "";
|
|
1741
|
+
const groups = parseClassGroups(classValue);
|
|
1742
|
+
const overriddenProps: string[] = [];
|
|
1743
|
+
const responsivePrefixes: ReadonlyArray<TailwindBreakpointPrefix> = [
|
|
1744
|
+
"sm",
|
|
1745
|
+
"md",
|
|
1746
|
+
"lg",
|
|
1747
|
+
"xl",
|
|
1748
|
+
"2xl",
|
|
1749
|
+
];
|
|
1750
|
+
for (const prefix of responsivePrefixes) {
|
|
1751
|
+
for (const rawToken of groups[prefix]) {
|
|
1752
|
+
const { utility } = parseClassToken(rawToken);
|
|
1753
|
+
// Derive a property stem to use as the override indicator label.
|
|
1754
|
+
const stemPart = utility.split("-")[0];
|
|
1755
|
+
if (stemPart && !overriddenProps.includes(stemPart)) {
|
|
1756
|
+
overriddenProps.push(stemPart);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
// The prefix here is "base" as the default; callers that know the active
|
|
1761
|
+
// frame width should use widthToPrefix() from responsive-classes.ts to
|
|
1762
|
+
// determine the appropriate editing prefix.
|
|
1763
|
+
capabilities.push({
|
|
1764
|
+
kind: "responsive-class",
|
|
1765
|
+
prefix: "base",
|
|
1766
|
+
operations: ["add", "remove", "replace"],
|
|
1767
|
+
overriddenProperties: overriddenProps,
|
|
1768
|
+
confidence: 0.87,
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1502
1771
|
if (!element.selfClosing) {
|
|
1503
1772
|
capabilities.push({
|
|
1504
1773
|
kind: "text",
|
|
@@ -1518,6 +1787,9 @@ function buildProjection(
|
|
|
1518
1787
|
html: string,
|
|
1519
1788
|
source: CodeLayerSource,
|
|
1520
1789
|
): ProjectionBuild {
|
|
1790
|
+
// Tolerate non-string input from any caller (e.g. content not yet loaded):
|
|
1791
|
+
// an empty projection is correct; crashing the editor is not.
|
|
1792
|
+
if (typeof html !== "string") html = "";
|
|
1521
1793
|
const elements = parseHtmlElements(html);
|
|
1522
1794
|
const nodeIdByElementIndex = new Map<number, string>();
|
|
1523
1795
|
const nodes: CodeLayerNode[] = [];
|
|
@@ -1559,7 +1831,7 @@ function buildProjection(
|
|
|
1559
1831
|
]),
|
|
1560
1832
|
);
|
|
1561
1833
|
|
|
1562
|
-
|
|
1834
|
+
const node: CodeLayerNode = {
|
|
1563
1835
|
id: nodeId,
|
|
1564
1836
|
tag: element.tag,
|
|
1565
1837
|
layerName: layerName.name,
|
|
@@ -1597,7 +1869,17 @@ function buildProjection(
|
|
|
1597
1869
|
closeStart: element.closeStart,
|
|
1598
1870
|
closeEnd: element.closeEnd,
|
|
1599
1871
|
},
|
|
1600
|
-
}
|
|
1872
|
+
};
|
|
1873
|
+
|
|
1874
|
+
// Detect component instances — nodes that carry data-agent-native-component.
|
|
1875
|
+
// Populate the metadata so the canvas can outline component roots and the
|
|
1876
|
+
// inspector can surface component-level controls.
|
|
1877
|
+
if (isComponentInstance(node)) {
|
|
1878
|
+
const instance = instanceFromNode(node);
|
|
1879
|
+
if (instance) node.componentInstance = instance;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
nodes.push(node);
|
|
1601
1883
|
elementByNodeId.set(nodeId, element);
|
|
1602
1884
|
}
|
|
1603
1885
|
|
|
@@ -1628,7 +1910,11 @@ export function buildCodeLayerProjection(
|
|
|
1628
1910
|
html: string,
|
|
1629
1911
|
options: { source?: CodeLayerSource } = {},
|
|
1630
1912
|
): CodeLayerProjection {
|
|
1631
|
-
|
|
1913
|
+
// Defensive: callers (memos/effects) may project before content has loaded
|
|
1914
|
+
// (e.g. `activeContent` is briefly undefined on first render). Projecting a
|
|
1915
|
+
// non-string must yield an empty projection, never crash the editor.
|
|
1916
|
+
const safeHtml = typeof html === "string" ? html : "";
|
|
1917
|
+
return buildProjection(safeHtml, options.source ?? { kind: "inline-html" })
|
|
1632
1918
|
.projection;
|
|
1633
1919
|
}
|
|
1634
1920
|
|
|
@@ -1768,26 +2054,36 @@ export function buildCodeLayerTree(
|
|
|
1768
2054
|
});
|
|
1769
2055
|
}
|
|
1770
2056
|
|
|
2057
|
+
const childIdsByParentId = new Map<string, Set<string>>();
|
|
1771
2058
|
for (const node of projection.nodes) {
|
|
1772
2059
|
const parent =
|
|
1773
2060
|
node.parentId && nodesById.has(node.parentId)
|
|
1774
2061
|
? treeById.get(node.parentId)
|
|
1775
2062
|
: undefined;
|
|
1776
2063
|
const treeNode = treeById.get(node.id);
|
|
1777
|
-
if (parent
|
|
1778
|
-
|
|
2064
|
+
if (!parent || !treeNode || parent.id === treeNode.id) continue;
|
|
2065
|
+
const childIds = childIdsByParentId.get(parent.id) ?? new Set<string>();
|
|
2066
|
+
if (childIds.has(treeNode.id)) continue;
|
|
2067
|
+
childIds.add(treeNode.id);
|
|
2068
|
+
childIdsByParentId.set(parent.id, childIds);
|
|
2069
|
+
parent.children.push(treeNode);
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
const roots: CodeLayerTreeNode[] = [];
|
|
2073
|
+
const rootIds = new Set<string>();
|
|
2074
|
+
const appendRoot = (id: string) => {
|
|
2075
|
+
if (rootIds.has(id)) return;
|
|
2076
|
+
const treeNode = treeById.get(id);
|
|
2077
|
+
if (!treeNode) return;
|
|
2078
|
+
rootIds.add(id);
|
|
2079
|
+
roots.push(treeNode);
|
|
2080
|
+
};
|
|
1779
2081
|
|
|
1780
|
-
|
|
1781
|
-
.map((id) => treeById.get(id))
|
|
1782
|
-
.filter((node): node is CodeLayerTreeNode => Boolean(node));
|
|
1783
|
-
const rootIds = new Set(roots.map((node) => node.id));
|
|
2082
|
+
projection.rootNodeIds.forEach(appendRoot);
|
|
1784
2083
|
for (const node of projection.nodes) {
|
|
1785
|
-
if (!node.parentId
|
|
1786
|
-
const treeNode = treeById.get(node.id);
|
|
1787
|
-
if (treeNode) roots.push(treeNode);
|
|
1788
|
-
}
|
|
2084
|
+
if (!node.parentId) appendRoot(node.id);
|
|
1789
2085
|
}
|
|
1790
|
-
return roots;
|
|
2086
|
+
return compactCodeLayerTreeNodes(roots, nodesById);
|
|
1791
2087
|
}
|
|
1792
2088
|
|
|
1793
2089
|
function normalizeSelectorForMatch(selector: string): string {
|
|
@@ -1797,6 +2093,26 @@ function normalizeSelectorForMatch(selector: string): string {
|
|
|
1797
2093
|
.replace(/\s+/g, " ");
|
|
1798
2094
|
}
|
|
1799
2095
|
|
|
2096
|
+
function selectorPartTag(selectorPart: string): string | null {
|
|
2097
|
+
const match = selectorPart.trim().match(/^([A-Za-z][A-Za-z0-9:-]*)/);
|
|
2098
|
+
return match?.[1]?.toLowerCase() ?? null;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
function isDocumentRootSelectorPart(selectorPart: string): boolean {
|
|
2102
|
+
const tag = selectorPartTag(selectorPart);
|
|
2103
|
+
return tag === "html" || tag === "body";
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// Removes positional `:nth-of-type(n)` suffixes from a selector. Runtime
|
|
2107
|
+
// bridge selectors fall back to `:nth-of-type` for elements without a durable
|
|
2108
|
+
// id, and that position is computed against the live (possibly Alpine-mutated)
|
|
2109
|
+
// DOM. When the stored source order differs, the positional index no longer
|
|
2110
|
+
// lines up. Dropping the suffix lets resolution fall back to the element's
|
|
2111
|
+
// stable signal (tag, classes, attributes, ancestor path).
|
|
2112
|
+
function stripPositionalNthOfType(selector: string): string {
|
|
2113
|
+
return selector.replace(/:nth-of-type\(\d+\)/g, "");
|
|
2114
|
+
}
|
|
2115
|
+
|
|
1800
2116
|
function lastSelectorPart(selector: string): string {
|
|
1801
2117
|
const parts = normalizeSelectorForMatch(selector).split(" > ");
|
|
1802
2118
|
return parts[parts.length - 1] ?? selector;
|
|
@@ -1926,7 +2242,11 @@ function selectorPathMatchesElement(
|
|
|
1926
2242
|
let current: ParsedElement | undefined = element;
|
|
1927
2243
|
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
|
1928
2244
|
const selectorPart = parts[index];
|
|
1929
|
-
if (!
|
|
2245
|
+
if (!selectorPart) return false;
|
|
2246
|
+
if (!current) {
|
|
2247
|
+
if (isDocumentRootSelectorPart(selectorPart)) continue;
|
|
2248
|
+
return false;
|
|
2249
|
+
}
|
|
1930
2250
|
if (!simpleSelectorMatchesElement(current, selectorPart)) return false;
|
|
1931
2251
|
current =
|
|
1932
2252
|
current.parentIndex === undefined
|
|
@@ -2024,26 +2344,52 @@ function resolveTarget(
|
|
|
2024
2344
|
};
|
|
2025
2345
|
}
|
|
2026
2346
|
|
|
2027
|
-
const
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2347
|
+
const selectorValue = target.selector ?? "";
|
|
2348
|
+
const matchesForSelector = (value: string): CodeLayerNode[] =>
|
|
2349
|
+
projection.nodes.filter((node) =>
|
|
2350
|
+
selectorMatches(
|
|
2351
|
+
node,
|
|
2352
|
+
value,
|
|
2353
|
+
elementByNodeId.get(node.id),
|
|
2354
|
+
elementByIndex,
|
|
2355
|
+
),
|
|
2356
|
+
);
|
|
2357
|
+
|
|
2358
|
+
const matches = matchesForSelector(selectorValue);
|
|
2035
2359
|
if (matches.length === 1 && matches[0]) {
|
|
2036
2360
|
return { status: "resolved", node: matches[0] };
|
|
2037
2361
|
}
|
|
2038
2362
|
if (matches.length > 1) {
|
|
2039
2363
|
return {
|
|
2040
2364
|
status: "conflict",
|
|
2041
|
-
message: `Selector "${
|
|
2365
|
+
message: `Selector "${selectorValue}" matched ${matches.length} code layer nodes.`,
|
|
2042
2366
|
};
|
|
2043
2367
|
}
|
|
2368
|
+
|
|
2369
|
+
// Strict matching found nothing. Runtime selectors anchor unstamped elements
|
|
2370
|
+
// with positional `:nth-of-type(n)` parts, which drift when the live DOM
|
|
2371
|
+
// order differs from the stored source (reordered or runtime-inserted nodes).
|
|
2372
|
+
// Retry once with the positional suffixes dropped so an element that is still
|
|
2373
|
+
// unique by its tag/classes/attributes resolves instead of surfacing a hard
|
|
2374
|
+
// "did not match" error. Genuinely ambiguous results stay a conflict rather
|
|
2375
|
+
// than silently editing the wrong node.
|
|
2376
|
+
const positionTolerantSelector = stripPositionalNthOfType(selectorValue);
|
|
2377
|
+
if (positionTolerantSelector && positionTolerantSelector !== selectorValue) {
|
|
2378
|
+
const tolerantMatches = matchesForSelector(positionTolerantSelector);
|
|
2379
|
+
if (tolerantMatches.length === 1 && tolerantMatches[0]) {
|
|
2380
|
+
return { status: "resolved", node: tolerantMatches[0] };
|
|
2381
|
+
}
|
|
2382
|
+
if (tolerantMatches.length > 1) {
|
|
2383
|
+
return {
|
|
2384
|
+
status: "conflict",
|
|
2385
|
+
message: `Selector "${selectorValue}" matched ${tolerantMatches.length} code layer nodes after ignoring positional :nth-of-type (the element may have been reordered or added at runtime). Re-select the element to refresh its id.`,
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2044
2390
|
return {
|
|
2045
2391
|
status: "conflict",
|
|
2046
|
-
message: `Selector "${
|
|
2392
|
+
message: `Selector "${selectorValue}" did not match a code layer node.`,
|
|
2047
2393
|
};
|
|
2048
2394
|
}
|
|
2049
2395
|
|
|
@@ -2250,6 +2596,71 @@ function applyTextEdit(
|
|
|
2250
2596
|
};
|
|
2251
2597
|
}
|
|
2252
2598
|
|
|
2599
|
+
/**
|
|
2600
|
+
* Apply a responsive-class edit intent to the HTML source.
|
|
2601
|
+
*
|
|
2602
|
+
* - `"add"`: calls `setPropertyClass(className, prefix, utility)` — adds a
|
|
2603
|
+
* new token at the target prefix (or replaces the existing one for
|
|
2604
|
+
* the same stem).
|
|
2605
|
+
* - `"replace"`: same as `"add"` — `setPropertyClass` already handles the
|
|
2606
|
+
* replace-if-same-stem semantics.
|
|
2607
|
+
* - `"remove"`: calls `removePropertyClass(className, prefix, stem)` — strips
|
|
2608
|
+
* all tokens with the given property stem at the target prefix,
|
|
2609
|
+
* falling back to the base value (Tailwind cascade).
|
|
2610
|
+
*
|
|
2611
|
+
* Uses the helpers from `responsive-classes.ts` so the logic is shared with
|
|
2612
|
+
* the StatesPanel / inspector UI.
|
|
2613
|
+
*/
|
|
2614
|
+
function applyResponsiveClassEdit(
|
|
2615
|
+
html: string,
|
|
2616
|
+
element: ParsedElement,
|
|
2617
|
+
intent: ResponsiveClassEditIntent,
|
|
2618
|
+
): { content: string; capability: EditCapability } | PatchResultStatus {
|
|
2619
|
+
const currentClass = attributeValue(element, "class") ?? "";
|
|
2620
|
+
|
|
2621
|
+
let nextClass: string;
|
|
2622
|
+
if (intent.operation === "remove") {
|
|
2623
|
+
if (!intent.stem) {
|
|
2624
|
+
// stem is required for remove
|
|
2625
|
+
return "unsupported";
|
|
2626
|
+
}
|
|
2627
|
+
if (!isSafeClassToken(intent.stem)) return "unsupported";
|
|
2628
|
+
nextClass = removePropertyClass(currentClass, intent.prefix, intent.stem);
|
|
2629
|
+
} else {
|
|
2630
|
+
// "add" and "replace" both use setPropertyClass
|
|
2631
|
+
if (!intent.utility) return "unsupported";
|
|
2632
|
+
if (!isSafeClassToken(intent.utility)) return "unsupported";
|
|
2633
|
+
nextClass = setPropertyClass(currentClass, intent.prefix, intent.utility);
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
if (nextClass === currentClass) {
|
|
2637
|
+
// No-op — nothing to patch.
|
|
2638
|
+
return {
|
|
2639
|
+
content: html,
|
|
2640
|
+
capability: {
|
|
2641
|
+
kind: "responsive-class",
|
|
2642
|
+
prefix: intent.prefix,
|
|
2643
|
+
operations: [intent.operation],
|
|
2644
|
+
overriddenProperties: [],
|
|
2645
|
+
confidence: 0.87,
|
|
2646
|
+
},
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
return {
|
|
2651
|
+
content: replaceOrInsertAttribute(html, element, "class", nextClass),
|
|
2652
|
+
capability: {
|
|
2653
|
+
kind: "responsive-class",
|
|
2654
|
+
prefix: intent.prefix,
|
|
2655
|
+
operations: [intent.operation],
|
|
2656
|
+
overriddenProperties: intent.utility
|
|
2657
|
+
? [intent.utility.split("-")[0] ?? ""]
|
|
2658
|
+
: [],
|
|
2659
|
+
confidence: 0.87,
|
|
2660
|
+
},
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2253
2664
|
function applyMoveNodeEdit(
|
|
2254
2665
|
html: string,
|
|
2255
2666
|
element: ParsedElement,
|
|
@@ -2290,6 +2701,379 @@ function applyMoveNodeEdit(
|
|
|
2290
2701
|
};
|
|
2291
2702
|
}
|
|
2292
2703
|
|
|
2704
|
+
/** Generate a fresh unique data-agent-native-node-id value not already in the set. */
|
|
2705
|
+
function freshNodeId(usedIds: Set<string>, basis: string): string {
|
|
2706
|
+
const base = `an-${hashStable(basis)}`;
|
|
2707
|
+
let value = base;
|
|
2708
|
+
let suffix = 1;
|
|
2709
|
+
while (usedIds.has(value)) {
|
|
2710
|
+
value = `an-${hashStable(`${base}:${suffix}`)}`;
|
|
2711
|
+
suffix += 1;
|
|
2712
|
+
}
|
|
2713
|
+
usedIds.add(value);
|
|
2714
|
+
return value;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
/**
|
|
2718
|
+
* Strip the given CSS property names from an inline style attribute value.
|
|
2719
|
+
* Returns the new style string (may be empty if all declarations were removed).
|
|
2720
|
+
*/
|
|
2721
|
+
function stripStyleProperties(
|
|
2722
|
+
styleValue: string | null,
|
|
2723
|
+
propertiesToRemove: string[],
|
|
2724
|
+
): string {
|
|
2725
|
+
if (!styleValue) return "";
|
|
2726
|
+
const toRemove = new Set(propertiesToRemove.map((p) => p.toLowerCase()));
|
|
2727
|
+
const remaining = parseStyleDeclarations(styleValue).filter(
|
|
2728
|
+
(decl) => !toRemove.has(decl.property),
|
|
2729
|
+
);
|
|
2730
|
+
return serializeStyleDeclarations(remaining);
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
/** Absolute-positioning properties stripped when converting a child to auto-layout flow. */
|
|
2734
|
+
const AUTO_LAYOUT_STRIP_PROPS = [
|
|
2735
|
+
"position",
|
|
2736
|
+
"left",
|
|
2737
|
+
"top",
|
|
2738
|
+
"right",
|
|
2739
|
+
"bottom",
|
|
2740
|
+
"inset",
|
|
2741
|
+
] as const;
|
|
2742
|
+
|
|
2743
|
+
/**
|
|
2744
|
+
* Apply display:flex + direction + gap to a raw open-tag string and return it.
|
|
2745
|
+
* Only touches the style attribute.
|
|
2746
|
+
*/
|
|
2747
|
+
function addAutoLayoutStyleToOpenTag(
|
|
2748
|
+
openTag: string,
|
|
2749
|
+
element: ParsedElement,
|
|
2750
|
+
html: string,
|
|
2751
|
+
direction: "row" | "column",
|
|
2752
|
+
gap: string,
|
|
2753
|
+
): string {
|
|
2754
|
+
const currentStyle = attributeValue(element, "style");
|
|
2755
|
+
let declarations = parseStyleDeclarations(currentStyle);
|
|
2756
|
+
const setOrReplace = (prop: string, val: string) => {
|
|
2757
|
+
const existing = declarations.find((d) => d.property === prop);
|
|
2758
|
+
if (existing) {
|
|
2759
|
+
existing.value = val;
|
|
2760
|
+
} else {
|
|
2761
|
+
declarations.push({ property: prop, value: val });
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
setOrReplace("display", "flex");
|
|
2765
|
+
setOrReplace("flex-direction", direction);
|
|
2766
|
+
setOrReplace("gap", gap);
|
|
2767
|
+
const nextStyle = serializeStyleDeclarations(declarations);
|
|
2768
|
+
// We work on a scratch copy relative to element boundaries
|
|
2769
|
+
const attr = getAttribute(element, "style");
|
|
2770
|
+
if (attr) {
|
|
2771
|
+
return `${openTag.slice(0, attr.start - element.start)}${attr.name}="${escapeHtmlAttribute(nextStyle)}"${openTag.slice(attr.end - element.start)}`;
|
|
2772
|
+
}
|
|
2773
|
+
// Insert before closing >
|
|
2774
|
+
const closeChar = openTag.endsWith("/>")
|
|
2775
|
+
? openTag.length - 2
|
|
2776
|
+
: openTag.length - 1;
|
|
2777
|
+
return `${openTag.slice(0, closeChar)} style="${escapeHtmlAttribute(nextStyle)}"${openTag.slice(closeChar)}`;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
/**
|
|
2781
|
+
* Strip absolute-positioning properties from a child's inline style, applying
|
|
2782
|
+
* the edit directly to the html string at the child element's source spans.
|
|
2783
|
+
* Returns the updated html string.
|
|
2784
|
+
*/
|
|
2785
|
+
function stripAbsolutePositioningFromChild(
|
|
2786
|
+
html: string,
|
|
2787
|
+
child: ParsedElement,
|
|
2788
|
+
): string {
|
|
2789
|
+
const currentStyle = attributeValue(child, "style");
|
|
2790
|
+
if (!currentStyle) return html;
|
|
2791
|
+
const nextStyle = stripStyleProperties(currentStyle, [
|
|
2792
|
+
...AUTO_LAYOUT_STRIP_PROPS,
|
|
2793
|
+
]);
|
|
2794
|
+
return replaceOrInsertAttribute(html, child, "style", nextStyle);
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
/**
|
|
2798
|
+
* GROUP: wrap targetted sibling elements (sharing a common parent) in a new
|
|
2799
|
+
* <div> wrapper. Targets must all share the same parent element.
|
|
2800
|
+
*/
|
|
2801
|
+
function applyWrapNodes(
|
|
2802
|
+
html: string,
|
|
2803
|
+
build: ProjectionBuild,
|
|
2804
|
+
intent: WrapNodesEditIntent,
|
|
2805
|
+
):
|
|
2806
|
+
| { content: string; capability: EditCapability; wrapperNodeId: string }
|
|
2807
|
+
| PatchResultStatus {
|
|
2808
|
+
const { targetIds, autoLayout = false } = intent;
|
|
2809
|
+
if (targetIds.length === 0) return "unsupported";
|
|
2810
|
+
|
|
2811
|
+
// Resolve all target nodes via nodeId attribute matching.
|
|
2812
|
+
const targetElements: ParsedElement[] = [];
|
|
2813
|
+
for (const id of targetIds) {
|
|
2814
|
+
const node = build.projection.nodes.find(
|
|
2815
|
+
(n) =>
|
|
2816
|
+
n.dataAttributes["data-agent-native-node-id"] === id || n.id === id,
|
|
2817
|
+
);
|
|
2818
|
+
if (!node) return "conflict";
|
|
2819
|
+
const el = build.elementByNodeId.get(node.id);
|
|
2820
|
+
if (!el) return "conflict";
|
|
2821
|
+
targetElements.push(el);
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
// All targets must share the same parent.
|
|
2825
|
+
const parentIndexes = new Set(targetElements.map((el) => el.parentIndex));
|
|
2826
|
+
if (parentIndexes.size !== 1) return "unsupported";
|
|
2827
|
+
|
|
2828
|
+
// Sort targets by their source position (ascending).
|
|
2829
|
+
targetElements.sort((a, b) => a.start - b.start);
|
|
2830
|
+
|
|
2831
|
+
const siblingIndexes = targetElements
|
|
2832
|
+
.map((el) => el.siblingIndex)
|
|
2833
|
+
.sort((a, b) => a - b);
|
|
2834
|
+
const firstSiblingIndex = siblingIndexes[0];
|
|
2835
|
+
if (firstSiblingIndex === undefined) return "unsupported";
|
|
2836
|
+
const targetsAreContiguous = siblingIndexes.every(
|
|
2837
|
+
(siblingIndex, index) => siblingIndex === firstSiblingIndex + index,
|
|
2838
|
+
);
|
|
2839
|
+
if (!targetsAreContiguous) return "unsupported";
|
|
2840
|
+
|
|
2841
|
+
// Collect existing node ids so we can generate a unique one.
|
|
2842
|
+
const usedIds = new Set(
|
|
2843
|
+
build.projection.nodes.flatMap((n) => {
|
|
2844
|
+
const id = n.dataAttributes["data-agent-native-node-id"];
|
|
2845
|
+
return id ? [id] : [];
|
|
2846
|
+
}),
|
|
2847
|
+
);
|
|
2848
|
+
|
|
2849
|
+
const wrapperNodeId = freshNodeId(
|
|
2850
|
+
usedIds,
|
|
2851
|
+
`wrap:${targetElements.map((el) => el.start).join(":")}`,
|
|
2852
|
+
);
|
|
2853
|
+
|
|
2854
|
+
// Collect the source fragments for all targets.
|
|
2855
|
+
const fragments = targetElements.map((el) => {
|
|
2856
|
+
let frag = html.slice(el.start, el.end);
|
|
2857
|
+
// If autoLayout, strip positioning from each wrapped child.
|
|
2858
|
+
if (autoLayout) {
|
|
2859
|
+
// We need a ParsedElement that reflects the fragment's own positions.
|
|
2860
|
+
// Re-parse the fragment to find the root element and strip its style.
|
|
2861
|
+
const fragElements = parseHtmlElements(frag);
|
|
2862
|
+
const root = fragElements.find((fe) => fe.parentIndex === undefined);
|
|
2863
|
+
if (root) {
|
|
2864
|
+
frag = stripAbsolutePositioningFromChild(frag, root);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
return frag;
|
|
2868
|
+
});
|
|
2869
|
+
|
|
2870
|
+
const autoLayoutStyle = autoLayout
|
|
2871
|
+
? ` style="display: flex; flex-direction: column; gap: 8px"`
|
|
2872
|
+
: "";
|
|
2873
|
+
const wrapperOpen = `<div data-agent-native-node-id="${escapeHtmlAttribute(wrapperNodeId)}" data-agent-native-layer-name="Group"${autoLayoutStyle}>`;
|
|
2874
|
+
const wrapperClose = `</div>`;
|
|
2875
|
+
const wrapperContent = `${wrapperOpen}${fragments.join("")}${wrapperClose}`;
|
|
2876
|
+
|
|
2877
|
+
// Build the replacement: remove all targets from html (back to front) then
|
|
2878
|
+
// insert the wrapper at the first target's position.
|
|
2879
|
+
// Sort by position descending to remove safely.
|
|
2880
|
+
const sorted = [...targetElements].sort((a, b) => b.start - a.start);
|
|
2881
|
+
|
|
2882
|
+
// Remove all targets from the html (back to front).
|
|
2883
|
+
let result = html;
|
|
2884
|
+
let firstTargetStart = targetElements[0]!.start;
|
|
2885
|
+
|
|
2886
|
+
for (const el of sorted) {
|
|
2887
|
+
const start = el.start;
|
|
2888
|
+
const end = el.end;
|
|
2889
|
+
if (start < firstTargetStart) {
|
|
2890
|
+
firstTargetStart = start;
|
|
2891
|
+
}
|
|
2892
|
+
result = `${result.slice(0, start)}${result.slice(end)}`;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
// Re-compute firstTargetStart relative to the modified string: all removals
|
|
2896
|
+
// before it shift it. Count how many bytes were removed before firstTargetStart.
|
|
2897
|
+
let bytesRemovedBefore = 0;
|
|
2898
|
+
for (const el of targetElements) {
|
|
2899
|
+
if (el.start < targetElements[0]!.start) {
|
|
2900
|
+
bytesRemovedBefore += el.end - el.start;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
const insertAt = firstTargetStart - bytesRemovedBefore;
|
|
2904
|
+
|
|
2905
|
+
result = `${result.slice(0, insertAt)}${wrapperContent}${result.slice(insertAt)}`;
|
|
2906
|
+
|
|
2907
|
+
return {
|
|
2908
|
+
content: result,
|
|
2909
|
+
capability: {
|
|
2910
|
+
kind: "structure",
|
|
2911
|
+
operations: ["moveNode"],
|
|
2912
|
+
confidence: 0.82,
|
|
2913
|
+
},
|
|
2914
|
+
wrapperNodeId,
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
/**
|
|
2919
|
+
* UNGROUP: replace the wrapper node with its children, spliced into the
|
|
2920
|
+
* wrapper's parent at the wrapper's position, then remove the wrapper.
|
|
2921
|
+
*/
|
|
2922
|
+
function applyUnwrap(
|
|
2923
|
+
html: string,
|
|
2924
|
+
build: ProjectionBuild,
|
|
2925
|
+
intent: UnwrapEditIntent,
|
|
2926
|
+
): { content: string; capability: EditCapability } | PatchResultStatus {
|
|
2927
|
+
const { targetId } = intent;
|
|
2928
|
+
const node = build.projection.nodes.find(
|
|
2929
|
+
(n) =>
|
|
2930
|
+
n.dataAttributes["data-agent-native-node-id"] === targetId ||
|
|
2931
|
+
n.id === targetId,
|
|
2932
|
+
);
|
|
2933
|
+
if (!node) return "conflict";
|
|
2934
|
+
if (!node.source) return "needsAgent";
|
|
2935
|
+
|
|
2936
|
+
const element = build.elementByNodeId.get(node.id);
|
|
2937
|
+
if (!element) return "conflict";
|
|
2938
|
+
if (element.selfClosing || element.contentStart >= element.contentEnd) {
|
|
2939
|
+
// Nothing to unwrap from an empty/void element.
|
|
2940
|
+
return "unsupported";
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
// The children's source content is the element's inner HTML.
|
|
2944
|
+
const innerContent = html.slice(element.contentStart, element.contentEnd);
|
|
2945
|
+
|
|
2946
|
+
// Replace the whole element (start..end) with its inner content.
|
|
2947
|
+
const result = `${html.slice(0, element.start)}${innerContent}${html.slice(element.end)}`;
|
|
2948
|
+
|
|
2949
|
+
return {
|
|
2950
|
+
content: result,
|
|
2951
|
+
capability: {
|
|
2952
|
+
kind: "structure",
|
|
2953
|
+
operations: ["moveNode"],
|
|
2954
|
+
confidence: 0.82,
|
|
2955
|
+
},
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
/**
|
|
2960
|
+
* CONVERT: toggle auto-layout (display:flex) on an existing container.
|
|
2961
|
+
*/
|
|
2962
|
+
function applyAutoLayout(
|
|
2963
|
+
html: string,
|
|
2964
|
+
build: ProjectionBuild,
|
|
2965
|
+
intent: AutoLayoutEditIntent,
|
|
2966
|
+
): { content: string; capability: EditCapability } | PatchResultStatus {
|
|
2967
|
+
const { targetId, enabled, direction = "column", gap = "8px" } = intent;
|
|
2968
|
+
const node = build.projection.nodes.find(
|
|
2969
|
+
(n) =>
|
|
2970
|
+
n.dataAttributes["data-agent-native-node-id"] === targetId ||
|
|
2971
|
+
n.id === targetId,
|
|
2972
|
+
);
|
|
2973
|
+
if (!node) return "conflict";
|
|
2974
|
+
if (!node.source) return "needsAgent";
|
|
2975
|
+
|
|
2976
|
+
const element = build.elementByNodeId.get(node.id);
|
|
2977
|
+
if (!element) return "conflict";
|
|
2978
|
+
|
|
2979
|
+
if (!enabled) {
|
|
2980
|
+
// Turn off auto-layout: set display:block.
|
|
2981
|
+
const currentStyle = attributeValue(element, "style");
|
|
2982
|
+
let declarations = parseStyleDeclarations(currentStyle);
|
|
2983
|
+
const displayDecl = declarations.find((d) => d.property === "display");
|
|
2984
|
+
if (displayDecl) {
|
|
2985
|
+
displayDecl.value = "block";
|
|
2986
|
+
} else {
|
|
2987
|
+
declarations.push({ property: "display", value: "block" });
|
|
2988
|
+
}
|
|
2989
|
+
const nextStyle = serializeStyleDeclarations(declarations);
|
|
2990
|
+
return {
|
|
2991
|
+
content: replaceOrInsertAttribute(html, element, "style", nextStyle),
|
|
2992
|
+
capability: {
|
|
2993
|
+
kind: "style",
|
|
2994
|
+
properties: ["display"],
|
|
2995
|
+
confidence: 0.9,
|
|
2996
|
+
},
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
// Enable auto-layout: set display:flex + direction + gap on the container.
|
|
3001
|
+
// Apply all three style properties in a single mutation against the already-
|
|
3002
|
+
// resolved element so that elements with no stable data attributes or HTML id
|
|
3003
|
+
// are handled correctly. Re-parsing after each individual property write
|
|
3004
|
+
// caused a silent no-op for those elements because the re-parse-based element
|
|
3005
|
+
// finder could not locate them after the first write changed the style attr.
|
|
3006
|
+
const currentStyle = attributeValue(element, "style");
|
|
3007
|
+
let declarations = parseStyleDeclarations(currentStyle);
|
|
3008
|
+
const setOrReplace = (prop: string, val: string) => {
|
|
3009
|
+
const existing = declarations.find((d) => d.property === prop);
|
|
3010
|
+
if (existing) {
|
|
3011
|
+
existing.value = val;
|
|
3012
|
+
} else {
|
|
3013
|
+
declarations.push({ property: prop, value: val });
|
|
3014
|
+
}
|
|
3015
|
+
};
|
|
3016
|
+
setOrReplace("display", "flex");
|
|
3017
|
+
setOrReplace("flex-direction", direction);
|
|
3018
|
+
setOrReplace("gap", gap);
|
|
3019
|
+
let result = replaceOrInsertAttribute(
|
|
3020
|
+
html,
|
|
3021
|
+
element,
|
|
3022
|
+
"style",
|
|
3023
|
+
serializeStyleDeclarations(declarations),
|
|
3024
|
+
);
|
|
3025
|
+
|
|
3026
|
+
// Strip absolute positioning from direct children.
|
|
3027
|
+
// Re-parse to get up-to-date child element positions after the style mutation.
|
|
3028
|
+
const updatedElements = parseHtmlElements(result);
|
|
3029
|
+
// Locate the target element in the updated parse. Prefer stable data
|
|
3030
|
+
// attributes and HTML id; fall back to matching by original source position
|
|
3031
|
+
// (safe because the container open-tag length only changed by the style attr
|
|
3032
|
+
// rewrite, which shifts nothing before element.start).
|
|
3033
|
+
const stableAttrPairs: Array<[string, string]> = [];
|
|
3034
|
+
for (const attrName of STABLE_NODE_ID_ATTRIBUTES) {
|
|
3035
|
+
const v = attributeValue(element, attrName);
|
|
3036
|
+
if (v) stableAttrPairs.push([attrName, v]);
|
|
3037
|
+
}
|
|
3038
|
+
const htmlIdValue = attributeValue(element, "id");
|
|
3039
|
+
const findElementInParsed = (
|
|
3040
|
+
elements: ParsedElement[],
|
|
3041
|
+
): ParsedElement | undefined => {
|
|
3042
|
+
for (const attrPair of stableAttrPairs) {
|
|
3043
|
+
const found = elements.find(
|
|
3044
|
+
(fe) => attributeValue(fe, attrPair[0]) === attrPair[1],
|
|
3045
|
+
);
|
|
3046
|
+
if (found) return found;
|
|
3047
|
+
}
|
|
3048
|
+
if (htmlIdValue) {
|
|
3049
|
+
return elements.find((fe) => attributeValue(fe, "id") === htmlIdValue);
|
|
3050
|
+
}
|
|
3051
|
+
// Fallback: match by original start position. The container's start offset
|
|
3052
|
+
// is unchanged because only its open-tag content (style attr) was modified.
|
|
3053
|
+
return elements.find((fe) => fe.start === element.start);
|
|
3054
|
+
};
|
|
3055
|
+
const updatedTarget = findElementInParsed(updatedElements);
|
|
3056
|
+
|
|
3057
|
+
if (updatedTarget) {
|
|
3058
|
+
// Process children in reverse order so offsets stay valid.
|
|
3059
|
+
const childIndexes = [...updatedTarget.childIndexes].reverse();
|
|
3060
|
+
for (const childIndex of childIndexes) {
|
|
3061
|
+
const child = updatedElements[childIndex];
|
|
3062
|
+
if (!child) continue;
|
|
3063
|
+
result = stripAbsolutePositioningFromChild(result, child);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
return {
|
|
3068
|
+
content: result,
|
|
3069
|
+
capability: {
|
|
3070
|
+
kind: "style",
|
|
3071
|
+
properties: ["display", "flex-direction", "gap"],
|
|
3072
|
+
confidence: 0.88,
|
|
3073
|
+
},
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
3076
|
+
|
|
2293
3077
|
function findAfterNode(
|
|
2294
3078
|
projection: CodeLayerProjection,
|
|
2295
3079
|
before: CodeLayerNode,
|
|
@@ -2333,6 +3117,121 @@ export function applyVisualEdit(
|
|
|
2333
3117
|
}
|
|
2334
3118
|
|
|
2335
3119
|
const initial = buildProjection(html, source);
|
|
3120
|
+
|
|
3121
|
+
// --- Structural intents that don't resolve a single target node ---
|
|
3122
|
+
|
|
3123
|
+
if (intent.kind === "wrapNodes") {
|
|
3124
|
+
const wrapEdit = applyWrapNodes(html, initial, intent);
|
|
3125
|
+
if (typeof wrapEdit === "string") {
|
|
3126
|
+
return {
|
|
3127
|
+
content: html,
|
|
3128
|
+
projection: initial.projection,
|
|
3129
|
+
result: {
|
|
3130
|
+
...patchResult(
|
|
3131
|
+
wrapEdit,
|
|
3132
|
+
source,
|
|
3133
|
+
intent,
|
|
3134
|
+
false,
|
|
3135
|
+
wrapEdit === "unsupported"
|
|
3136
|
+
? "wrapNodes requires all targets to share a common parent element."
|
|
3137
|
+
: "Could not resolve one or more target nodes for wrapNodes.",
|
|
3138
|
+
),
|
|
3139
|
+
},
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
const nextProjection = buildCodeLayerProjection(wrapEdit.content, {
|
|
3143
|
+
source,
|
|
3144
|
+
});
|
|
3145
|
+
return {
|
|
3146
|
+
content: wrapEdit.content,
|
|
3147
|
+
projection: nextProjection,
|
|
3148
|
+
result: {
|
|
3149
|
+
...patchResult(
|
|
3150
|
+
"applied",
|
|
3151
|
+
source,
|
|
3152
|
+
intent,
|
|
3153
|
+
wrapEdit.content !== html,
|
|
3154
|
+
wrapEdit.content === html
|
|
3155
|
+
? "No source change was needed."
|
|
3156
|
+
: "Nodes wrapped.",
|
|
3157
|
+
),
|
|
3158
|
+
wrapperNodeId: wrapEdit.wrapperNodeId,
|
|
3159
|
+
},
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
if (intent.kind === "unwrap") {
|
|
3164
|
+
const unwrapEdit = applyUnwrap(html, initial, intent);
|
|
3165
|
+
if (typeof unwrapEdit === "string") {
|
|
3166
|
+
return {
|
|
3167
|
+
content: html,
|
|
3168
|
+
projection: initial.projection,
|
|
3169
|
+
result: patchResult(
|
|
3170
|
+
unwrapEdit,
|
|
3171
|
+
source,
|
|
3172
|
+
intent,
|
|
3173
|
+
false,
|
|
3174
|
+
unwrapEdit === "conflict"
|
|
3175
|
+
? `Could not resolve unwrap target "${intent.targetId}".`
|
|
3176
|
+
: "Cannot unwrap a self-closing or empty element.",
|
|
3177
|
+
),
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
const nextProjection = buildCodeLayerProjection(unwrapEdit.content, {
|
|
3181
|
+
source,
|
|
3182
|
+
});
|
|
3183
|
+
return {
|
|
3184
|
+
content: unwrapEdit.content,
|
|
3185
|
+
projection: nextProjection,
|
|
3186
|
+
result: patchResult(
|
|
3187
|
+
"applied",
|
|
3188
|
+
source,
|
|
3189
|
+
intent,
|
|
3190
|
+
unwrapEdit.content !== html,
|
|
3191
|
+
unwrapEdit.content === html
|
|
3192
|
+
? "No source change was needed."
|
|
3193
|
+
: "Node unwrapped.",
|
|
3194
|
+
),
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
if (intent.kind === "autoLayout") {
|
|
3199
|
+
const alEdit = applyAutoLayout(html, initial, intent);
|
|
3200
|
+
if (typeof alEdit === "string") {
|
|
3201
|
+
return {
|
|
3202
|
+
content: html,
|
|
3203
|
+
projection: initial.projection,
|
|
3204
|
+
result: patchResult(
|
|
3205
|
+
alEdit,
|
|
3206
|
+
source,
|
|
3207
|
+
intent,
|
|
3208
|
+
false,
|
|
3209
|
+
alEdit === "conflict"
|
|
3210
|
+
? `Could not resolve autoLayout target "${intent.targetId}".`
|
|
3211
|
+
: "Cannot apply autoLayout to this element.",
|
|
3212
|
+
),
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
3215
|
+
const nextProjection = buildCodeLayerProjection(alEdit.content, { source });
|
|
3216
|
+
return {
|
|
3217
|
+
content: alEdit.content,
|
|
3218
|
+
projection: nextProjection,
|
|
3219
|
+
result: patchResult(
|
|
3220
|
+
"applied",
|
|
3221
|
+
source,
|
|
3222
|
+
intent,
|
|
3223
|
+
alEdit.content !== html,
|
|
3224
|
+
alEdit.content === html
|
|
3225
|
+
? "No source change was needed."
|
|
3226
|
+
: intent.enabled
|
|
3227
|
+
? "Auto-layout enabled."
|
|
3228
|
+
: "Auto-layout disabled.",
|
|
3229
|
+
),
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
// --- Target-resolved intents (style / class / textContent / moveNode) ---
|
|
3234
|
+
|
|
2336
3235
|
const resolution = resolveTarget(initial, intent.target);
|
|
2337
3236
|
if (resolution.status !== "resolved" || !resolution.node) {
|
|
2338
3237
|
return {
|
|
@@ -2376,6 +3275,8 @@ export function applyVisualEdit(
|
|
|
2376
3275
|
edit = applyClassEdit(html, element, intent);
|
|
2377
3276
|
} else if (intent.kind === "textContent") {
|
|
2378
3277
|
edit = applyTextEdit(html, element, intent);
|
|
3278
|
+
} else if (intent.kind === "responsive-class") {
|
|
3279
|
+
edit = applyResponsiveClassEdit(html, element, intent);
|
|
2379
3280
|
} else {
|
|
2380
3281
|
const anchorResolution = resolveTarget(initial, intent.anchor);
|
|
2381
3282
|
if (anchorResolution.status !== "resolved" || !anchorResolution.node) {
|
|
@@ -2469,3 +3370,150 @@ export function applyVisualEdit(
|
|
|
2469
3370
|
),
|
|
2470
3371
|
};
|
|
2471
3372
|
}
|
|
3373
|
+
|
|
3374
|
+
export interface MoveNodeBetweenDocumentsOptions {
|
|
3375
|
+
nodeId: string;
|
|
3376
|
+
anchorNodeId?: string;
|
|
3377
|
+
placement?: "before" | "after" | "inside";
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
export interface MoveNodeBetweenDocumentsResult {
|
|
3381
|
+
sourceHtml: string;
|
|
3382
|
+
destHtml: string;
|
|
3383
|
+
status: "applied" | "unsupported";
|
|
3384
|
+
message?: string;
|
|
3385
|
+
/**
|
|
3386
|
+
* The data-agent-native-node-id of the moved node in destHtml.
|
|
3387
|
+
* May differ from the original nodeId when a collision caused a re-stamp.
|
|
3388
|
+
* Only present when status is "applied".
|
|
3389
|
+
*/
|
|
3390
|
+
movedNodeId?: string;
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
/**
|
|
3394
|
+
* Move a node (by data-agent-native-node-id) from sourceHtml into destHtml.
|
|
3395
|
+
* The node's serialized subtree is removed from sourceHtml and inserted into
|
|
3396
|
+
* destHtml relative to anchorNodeId (default: append to <body> or end of doc).
|
|
3397
|
+
* Any node ids in the moved subtree that already exist in destHtml are
|
|
3398
|
+
* re-stamped to stay unique. No external dependencies.
|
|
3399
|
+
*/
|
|
3400
|
+
export function moveNodeBetweenDocuments(
|
|
3401
|
+
sourceHtml: string,
|
|
3402
|
+
destHtml: string,
|
|
3403
|
+
opts: MoveNodeBetweenDocumentsOptions,
|
|
3404
|
+
): MoveNodeBetweenDocumentsResult {
|
|
3405
|
+
const { nodeId, anchorNodeId, placement = "inside" } = opts;
|
|
3406
|
+
|
|
3407
|
+
// --- Locate the node in sourceHtml ---
|
|
3408
|
+
const sourceElements = parseHtmlElements(sourceHtml);
|
|
3409
|
+
const sourceTarget = sourceElements.find(
|
|
3410
|
+
(el) => attributeValue(el, "data-agent-native-node-id") === nodeId,
|
|
3411
|
+
);
|
|
3412
|
+
if (!sourceTarget) {
|
|
3413
|
+
return {
|
|
3414
|
+
sourceHtml,
|
|
3415
|
+
destHtml,
|
|
3416
|
+
status: "unsupported",
|
|
3417
|
+
message: `Node with data-agent-native-node-id="${nodeId}" not found in sourceHtml.`,
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
// --- Extract the subtree fragment ---
|
|
3422
|
+
let fragment = sourceHtml.slice(sourceTarget.start, sourceTarget.end);
|
|
3423
|
+
|
|
3424
|
+
// --- Collect all existing node ids in destHtml to avoid collisions ---
|
|
3425
|
+
const destElements = parseHtmlElements(destHtml);
|
|
3426
|
+
const destUsedIds = new Set<string>(
|
|
3427
|
+
destElements
|
|
3428
|
+
.map((el) => attributeValue(el, "data-agent-native-node-id"))
|
|
3429
|
+
.filter((v): v is string => v !== null),
|
|
3430
|
+
);
|
|
3431
|
+
|
|
3432
|
+
// --- Re-stamp any colliding node ids in the fragment ---
|
|
3433
|
+
// We do this by parsing the fragment as its own HTML and replacing ids.
|
|
3434
|
+
const fragElements = parseHtmlElements(fragment);
|
|
3435
|
+
// Build replacement map: old id → new id
|
|
3436
|
+
const idRemap = new Map<string, string>();
|
|
3437
|
+
for (const fragEl of fragElements) {
|
|
3438
|
+
const existingId = attributeValue(fragEl, "data-agent-native-node-id");
|
|
3439
|
+
if (!existingId) continue;
|
|
3440
|
+
if (destUsedIds.has(existingId)) {
|
|
3441
|
+
const newId = freshNodeId(
|
|
3442
|
+
destUsedIds,
|
|
3443
|
+
`moved:${existingId}:${fragEl.start}`,
|
|
3444
|
+
);
|
|
3445
|
+
idRemap.set(existingId, newId);
|
|
3446
|
+
} else {
|
|
3447
|
+
destUsedIds.add(existingId);
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
// Apply id remaps to the fragment (back to front by attribute position).
|
|
3452
|
+
if (idRemap.size > 0) {
|
|
3453
|
+
const remapEdits: Array<{ start: number; end: number; value: string }> = [];
|
|
3454
|
+
for (const fragEl of fragElements) {
|
|
3455
|
+
const attr = getAttribute(fragEl, "data-agent-native-node-id");
|
|
3456
|
+
if (!attr || typeof attr.value !== "string") continue;
|
|
3457
|
+
const newId = idRemap.get(attr.value);
|
|
3458
|
+
if (!newId) continue;
|
|
3459
|
+
remapEdits.push({
|
|
3460
|
+
start: attr.start,
|
|
3461
|
+
end: attr.end,
|
|
3462
|
+
value: `data-agent-native-node-id="${escapeHtmlAttribute(newId)}"`,
|
|
3463
|
+
});
|
|
3464
|
+
}
|
|
3465
|
+
// Apply back to front.
|
|
3466
|
+
remapEdits.sort((a, b) => b.start - a.start);
|
|
3467
|
+
for (const edit of remapEdits) {
|
|
3468
|
+
fragment = `${fragment.slice(0, edit.start)}${edit.value}${fragment.slice(edit.end)}`;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
// --- Remove node from sourceHtml ---
|
|
3473
|
+
const nextSourceHtml = `${sourceHtml.slice(0, sourceTarget.start)}${sourceHtml.slice(sourceTarget.end)}`;
|
|
3474
|
+
|
|
3475
|
+
// --- Insert fragment into destHtml ---
|
|
3476
|
+
let nextDestHtml: string;
|
|
3477
|
+
|
|
3478
|
+
if (anchorNodeId) {
|
|
3479
|
+
const anchor = destElements.find(
|
|
3480
|
+
(el) => attributeValue(el, "data-agent-native-node-id") === anchorNodeId,
|
|
3481
|
+
);
|
|
3482
|
+
if (!anchor) {
|
|
3483
|
+
return {
|
|
3484
|
+
sourceHtml,
|
|
3485
|
+
destHtml,
|
|
3486
|
+
status: "unsupported",
|
|
3487
|
+
message: `Anchor node with data-agent-native-node-id="${anchorNodeId}" not found in destHtml.`,
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
const insertAt =
|
|
3491
|
+
placement === "before"
|
|
3492
|
+
? anchor.start
|
|
3493
|
+
: placement === "after"
|
|
3494
|
+
? anchor.end
|
|
3495
|
+
: anchor.selfClosing
|
|
3496
|
+
? anchor.end
|
|
3497
|
+
: anchor.contentEnd;
|
|
3498
|
+
nextDestHtml = `${destHtml.slice(0, insertAt)}${fragment}${destHtml.slice(insertAt)}`;
|
|
3499
|
+
} else {
|
|
3500
|
+
// Default: find <body> and append inside it, or append at end of doc.
|
|
3501
|
+
const bodyEl = destElements.find((el) => el.tag === "body");
|
|
3502
|
+
const insertAt = bodyEl
|
|
3503
|
+
? bodyEl.selfClosing
|
|
3504
|
+
? bodyEl.end
|
|
3505
|
+
: bodyEl.contentEnd
|
|
3506
|
+
: destHtml.length;
|
|
3507
|
+
nextDestHtml = `${destHtml.slice(0, insertAt)}${fragment}${destHtml.slice(insertAt)}`;
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
// The moved node keeps its original id unless it collided and was re-stamped.
|
|
3511
|
+
const movedNodeId = idRemap.get(nodeId) ?? nodeId;
|
|
3512
|
+
|
|
3513
|
+
return {
|
|
3514
|
+
sourceHtml: nextSourceHtml,
|
|
3515
|
+
destHtml: nextDestHtml,
|
|
3516
|
+
status: "applied",
|
|
3517
|
+
movedNodeId,
|
|
3518
|
+
};
|
|
3519
|
+
}
|