@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,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useActionMutation,
|
|
3
|
+
useActionQuery,
|
|
4
|
+
useT,
|
|
5
|
+
} from "@agent-native/core/client";
|
|
2
6
|
import type { TweakDefinition } from "@shared/api";
|
|
3
7
|
import {
|
|
4
8
|
alphaToOpacity,
|
|
@@ -14,13 +18,15 @@ import {
|
|
|
14
18
|
IconAlignRight,
|
|
15
19
|
IconArrowAutofitHeight,
|
|
16
20
|
IconArrowAutofitWidth,
|
|
21
|
+
IconArrowRight,
|
|
17
22
|
IconBackground,
|
|
18
23
|
IconBlur,
|
|
19
24
|
IconBorderStyle,
|
|
20
25
|
IconBrush,
|
|
21
|
-
IconChevronDown,
|
|
22
26
|
IconCode,
|
|
23
27
|
IconComponents,
|
|
28
|
+
IconDeviceDesktop,
|
|
29
|
+
IconExternalLink,
|
|
24
30
|
IconDroplet,
|
|
25
31
|
IconEye,
|
|
26
32
|
IconEyeOff,
|
|
@@ -29,6 +35,8 @@ import {
|
|
|
29
35
|
IconFrame,
|
|
30
36
|
IconLayoutDistributeHorizontal,
|
|
31
37
|
IconLayoutGrid,
|
|
38
|
+
IconLoader2,
|
|
39
|
+
IconShieldCheck,
|
|
32
40
|
IconSlice,
|
|
33
41
|
IconLayoutAlignBottom,
|
|
34
42
|
IconLayoutAlignCenter,
|
|
@@ -40,6 +48,7 @@ import {
|
|
|
40
48
|
IconLetterSpacing,
|
|
41
49
|
IconLineHeight,
|
|
42
50
|
IconLink,
|
|
51
|
+
IconLock,
|
|
43
52
|
IconMaximize,
|
|
44
53
|
IconMinus,
|
|
45
54
|
IconPalette,
|
|
@@ -51,9 +60,25 @@ import {
|
|
|
51
60
|
IconUnlink,
|
|
52
61
|
IconVector,
|
|
53
62
|
} from "@tabler/icons-react";
|
|
54
|
-
import {
|
|
63
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
64
|
+
import {
|
|
65
|
+
useCallback,
|
|
66
|
+
useEffect,
|
|
67
|
+
useRef,
|
|
68
|
+
useState,
|
|
69
|
+
type ReactNode,
|
|
70
|
+
} from "react";
|
|
71
|
+
import { useParams } from "react-router";
|
|
55
72
|
|
|
56
73
|
import { Button } from "@/components/ui/button";
|
|
74
|
+
import {
|
|
75
|
+
Dialog,
|
|
76
|
+
DialogContent,
|
|
77
|
+
DialogDescription,
|
|
78
|
+
DialogFooter,
|
|
79
|
+
DialogHeader,
|
|
80
|
+
DialogTitle,
|
|
81
|
+
} from "@/components/ui/dialog";
|
|
57
82
|
import {
|
|
58
83
|
DropdownMenu,
|
|
59
84
|
DropdownMenuContent,
|
|
@@ -75,6 +100,7 @@ import {
|
|
|
75
100
|
SelectValue,
|
|
76
101
|
} from "@/components/ui/select";
|
|
77
102
|
import { Slider } from "@/components/ui/slider";
|
|
103
|
+
import { Switch } from "@/components/ui/switch";
|
|
78
104
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
79
105
|
import {
|
|
80
106
|
Tooltip,
|
|
@@ -100,18 +126,34 @@ import {
|
|
|
100
126
|
type AutoLayoutSizingAxis,
|
|
101
127
|
type ConstraintsValue,
|
|
102
128
|
type ExportSettingsValue,
|
|
129
|
+
imageFillToBackgroundStyles,
|
|
103
130
|
type DesignFillRow,
|
|
104
131
|
type DesignFillRowPatch,
|
|
105
132
|
type DesignGradientStop,
|
|
106
133
|
type DesignGradientStopPatch,
|
|
107
134
|
type DesignGradientType,
|
|
135
|
+
type ImageFillValue,
|
|
108
136
|
} from "./inspector";
|
|
137
|
+
import { IconLayoutSettings } from "./inspector/design-icons";
|
|
109
138
|
import type { DesignPaintType } from "./inspector/DesignColorPicker";
|
|
139
|
+
import { InspectorAiActions } from "./inspector/InspectorAiActions";
|
|
140
|
+
import { ReviewPanel } from "./ReviewPanel";
|
|
141
|
+
import type { ReviewPanelProps } from "./ReviewPanel";
|
|
142
|
+
import { StatesPanel } from "./StatesPanel";
|
|
143
|
+
import type { StatesPanelProps } from "./StatesPanel";
|
|
144
|
+
import { TokensPanel } from "./TokensPanel";
|
|
110
145
|
import { TweaksPanelContent } from "./TweaksPanel";
|
|
111
146
|
import type { ElementInfo } from "./types";
|
|
112
147
|
|
|
113
148
|
export type InspectorTab = "design" | "tweaks" | "extensions";
|
|
114
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Sub-tab within the Tweaks area: "tokens" = TokensPanel face (CSS-var
|
|
152
|
+
* editing through the Tweaks loop); "tweaks" = raw TweakDefinition controls.
|
|
153
|
+
* Defaults to "tokens" when a designId is provided, otherwise "tweaks".
|
|
154
|
+
*/
|
|
155
|
+
type TweaksSubTab = "tokens" | "tweaks";
|
|
156
|
+
|
|
115
157
|
interface EditPanelProps {
|
|
116
158
|
selectedElement: ElementInfo | null;
|
|
117
159
|
pageStyles?: Record<string, string>;
|
|
@@ -127,8 +169,84 @@ interface EditPanelProps {
|
|
|
127
169
|
onRequestTweaks?: (anchor: HTMLElement) => void;
|
|
128
170
|
onStyleChange: (property: string, value: string) => void;
|
|
129
171
|
onStylesChange?: (styles: Record<string, string>) => void;
|
|
172
|
+
onAutoLayoutConvert?: (
|
|
173
|
+
targetNodeId: string,
|
|
174
|
+
opts?: { direction?: "row" | "column"; gap?: string },
|
|
175
|
+
) => void;
|
|
130
176
|
onExport?: (settings: ExportSettingsValue[]) => void;
|
|
131
177
|
exporting?: boolean;
|
|
178
|
+
readOnly?: boolean;
|
|
179
|
+
/** Active file id — forwarded to InspectorAiActions for agent context. */
|
|
180
|
+
fileId?: string;
|
|
181
|
+
/** Active file name (e.g. "index.html") — forwarded to InspectorAiActions. */
|
|
182
|
+
filename?: string;
|
|
183
|
+
// -------------------------------------------------------------------------
|
|
184
|
+
// Design Studio panels (§6.2, §6.4, §6.5)
|
|
185
|
+
// Pass `designId` to unlock Tokens, States, and Review sections.
|
|
186
|
+
// -------------------------------------------------------------------------
|
|
187
|
+
/** The active design's id — required to mount Tokens / States / Review. */
|
|
188
|
+
designId?: string;
|
|
189
|
+
/**
|
|
190
|
+
* Called after a token edit is applied so the parent can push the resolved
|
|
191
|
+
* CSS-var map into the iframe via the tweak-values postMessage.
|
|
192
|
+
*/
|
|
193
|
+
onTokensApplied?: (resolvedCssVars: Record<string, string>) => void;
|
|
194
|
+
/** Props forwarded to the StatesPanel (§6.4). Requires `designId`. */
|
|
195
|
+
statesPanelProps?: Omit<StatesPanelProps, "designId">;
|
|
196
|
+
/** Props forwarded to the ReviewPanel (§6.5). */
|
|
197
|
+
reviewPanelProps?: Omit<ReviewPanelProps, "className">;
|
|
198
|
+
// -------------------------------------------------------------------------
|
|
199
|
+
// Component section (§6.1)
|
|
200
|
+
// When a component instance is selected, pass its node id here to unlock
|
|
201
|
+
// the contextual Component section at the top of the Design tab.
|
|
202
|
+
// -------------------------------------------------------------------------
|
|
203
|
+
/**
|
|
204
|
+
* The `data-agent-native-node-id` of the currently-selected component root
|
|
205
|
+
* element. When provided (along with `designId`), a Component section is
|
|
206
|
+
* shown at the top of the Design tab with name, source path, prop controls,
|
|
207
|
+
* and an Edit component action.
|
|
208
|
+
*/
|
|
209
|
+
componentNodeId?: string;
|
|
210
|
+
/**
|
|
211
|
+
* Source capabilities for the current design. Used to gate the Edit
|
|
212
|
+
* component / jump-to-source affordances. When absent all writes default
|
|
213
|
+
* to disabled (inline / Alpine tier behaviour).
|
|
214
|
+
*/
|
|
215
|
+
sourceCapabilities?: string[];
|
|
216
|
+
// -------------------------------------------------------------------------
|
|
217
|
+
// Selection header quick actions ("Create component" + "Inspect code")
|
|
218
|
+
// -------------------------------------------------------------------------
|
|
219
|
+
/**
|
|
220
|
+
* Promote the current selection into a reusable component. Receives the
|
|
221
|
+
* (already-normalized-by-the-action) component name the user typed. When
|
|
222
|
+
* omitted the "Create component" button is disabled.
|
|
223
|
+
*/
|
|
224
|
+
onCreateComponent?: (name: string) => void;
|
|
225
|
+
/** Suggested default name for the create-component dialog. */
|
|
226
|
+
defaultComponentName?: string;
|
|
227
|
+
/** Code-inspection data for the "Inspect code" popover. */
|
|
228
|
+
inspectCode?: InspectCodeData;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Data backing the "Inspect code" popover. The parent resolves the selected
|
|
233
|
+
* node's HTML and (for real-app sources) its source file location.
|
|
234
|
+
*/
|
|
235
|
+
export interface InspectCodeData {
|
|
236
|
+
/** Outer HTML of the selected element (inline / Alpine source). */
|
|
237
|
+
html?: string | null;
|
|
238
|
+
/**
|
|
239
|
+
* Resolved source file for real-app sources (localhost / fusion), when the
|
|
240
|
+
* resolveNodeToFile capability is available.
|
|
241
|
+
*/
|
|
242
|
+
sourceLocation?: {
|
|
243
|
+
/** Absolute path on disk — used to build the vscode:// deep link. */
|
|
244
|
+
absolutePath: string;
|
|
245
|
+
line?: number;
|
|
246
|
+
column?: number;
|
|
247
|
+
/** Optional snippet to show above the Open-in-VS-Code button. */
|
|
248
|
+
snippet?: string;
|
|
249
|
+
} | null;
|
|
132
250
|
}
|
|
133
251
|
|
|
134
252
|
/**
|
|
@@ -238,6 +356,7 @@ function ColorInput({
|
|
|
238
356
|
onChange,
|
|
239
357
|
backgroundImage,
|
|
240
358
|
onBackgroundImageChange,
|
|
359
|
+
onImageFillChange,
|
|
241
360
|
blendMode,
|
|
242
361
|
onBlendModeChange,
|
|
243
362
|
supportsLayeredFills = false,
|
|
@@ -249,6 +368,7 @@ function ColorInput({
|
|
|
249
368
|
onChange: (value: string) => void;
|
|
250
369
|
backgroundImage?: string;
|
|
251
370
|
onBackgroundImageChange?: (value: string) => void;
|
|
371
|
+
onImageFillChange?: (value: ImageFillValue) => void;
|
|
252
372
|
blendMode?: string;
|
|
253
373
|
onBlendModeChange?: (value: string) => void;
|
|
254
374
|
supportsLayeredFills?: boolean;
|
|
@@ -558,9 +678,10 @@ function ColorInput({
|
|
|
558
678
|
onPaintValueChange={
|
|
559
679
|
supportsLayeredFills ? handlePaintValueChange : undefined
|
|
560
680
|
}
|
|
681
|
+
onImageFillChange={onImageFillChange}
|
|
561
682
|
blendMode={blendMode}
|
|
562
683
|
onBlendModeChange={onBlendModeChange}
|
|
563
|
-
showBlendMode={
|
|
684
|
+
showBlendMode={Boolean(onBlendModeChange)}
|
|
564
685
|
fillRows={fillRows}
|
|
565
686
|
selectedFillId={selectedFillId}
|
|
566
687
|
onFillSelect={supportsLayeredFills ? setSelectedFillId : undefined}
|
|
@@ -912,8 +1033,6 @@ function PropSlider({
|
|
|
912
1033
|
);
|
|
913
1034
|
}
|
|
914
1035
|
|
|
915
|
-
const EmptyScrubIcon = () => null;
|
|
916
|
-
|
|
917
1036
|
/**
|
|
918
1037
|
* design-editor inspector section. Matches the design editor "Design" panel chrome:
|
|
919
1038
|
* - NO left collapse chevron (the design editor uses none).
|
|
@@ -1131,6 +1250,99 @@ const FONT_WEIGHT_OPTIONS = [
|
|
|
1131
1250
|
{ value: "900", key: "black" },
|
|
1132
1251
|
] as const;
|
|
1133
1252
|
|
|
1253
|
+
type TextResizeMode = "auto-width" | "auto-height" | "fixed";
|
|
1254
|
+
|
|
1255
|
+
function cleanFontFamilyName(value: string): string {
|
|
1256
|
+
const trimmed = value.trim();
|
|
1257
|
+
if (
|
|
1258
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
1259
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
1260
|
+
) {
|
|
1261
|
+
return trimmed.slice(1, -1).trim();
|
|
1262
|
+
}
|
|
1263
|
+
return trimmed;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function splitFontFamilyList(value: string | undefined): string[] {
|
|
1267
|
+
const raw = value?.trim();
|
|
1268
|
+
if (!raw) return [];
|
|
1269
|
+
|
|
1270
|
+
const families: string[] = [];
|
|
1271
|
+
let token = "";
|
|
1272
|
+
let quote: '"' | "'" | null = null;
|
|
1273
|
+
|
|
1274
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
1275
|
+
const char = raw[i];
|
|
1276
|
+
if ((char === '"' || char === "'") && raw[i - 1] !== "\\") {
|
|
1277
|
+
if (quote === char) quote = null;
|
|
1278
|
+
else if (!quote) quote = char;
|
|
1279
|
+
token += char;
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
if (char === "," && !quote) {
|
|
1283
|
+
const cleaned = cleanFontFamilyName(token);
|
|
1284
|
+
if (cleaned) families.push(cleaned);
|
|
1285
|
+
token = "";
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
token += char;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const cleaned = cleanFontFamilyName(token);
|
|
1292
|
+
if (cleaned) families.push(cleaned);
|
|
1293
|
+
return families;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function normalizeFontFamilyName(value: string): string {
|
|
1297
|
+
return cleanFontFamilyName(value).replace(/\s+/g, " ").toLowerCase();
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function normalizeFontFamilyStack(value: string): string {
|
|
1301
|
+
return splitFontFamilyList(value).map(normalizeFontFamilyName).join(",");
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function displayFontFamilyName(value: string | undefined): string {
|
|
1305
|
+
const first = splitFontFamilyList(value)[0];
|
|
1306
|
+
if (!first) return "Sans Serif"; // i18n-ignore design generic font label
|
|
1307
|
+
|
|
1308
|
+
const normalized = normalizeFontFamilyName(first);
|
|
1309
|
+
if (normalized === "sans-serif") {
|
|
1310
|
+
return "Sans Serif"; // i18n-ignore design generic font label
|
|
1311
|
+
}
|
|
1312
|
+
if (normalized === "serif") return "Serif"; // i18n-ignore design generic font label
|
|
1313
|
+
if (normalized === "monospace") {
|
|
1314
|
+
return "Monospace"; // i18n-ignore design generic font label
|
|
1315
|
+
}
|
|
1316
|
+
if (normalized === "system-ui" || normalized === "-apple-system") {
|
|
1317
|
+
return "System UI"; // i18n-ignore design generic font label
|
|
1318
|
+
}
|
|
1319
|
+
if (normalized === "blinkmacsystemfont") {
|
|
1320
|
+
return "Apple System"; // i18n-ignore design generic font label
|
|
1321
|
+
}
|
|
1322
|
+
return first;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function resolveFontFamilySelectValue(value: string | undefined): string {
|
|
1326
|
+
const raw = value?.trim();
|
|
1327
|
+
if (!raw) return "sans-serif";
|
|
1328
|
+
|
|
1329
|
+
const normalizedStack = normalizeFontFamilyStack(raw);
|
|
1330
|
+
const exactOption = FONT_FAMILY_OPTIONS.find(
|
|
1331
|
+
(option) => normalizeFontFamilyStack(option.value) === normalizedStack,
|
|
1332
|
+
);
|
|
1333
|
+
if (exactOption) return exactOption.value;
|
|
1334
|
+
|
|
1335
|
+
const firstFamily = normalizeFontFamilyName(
|
|
1336
|
+
splitFontFamilyList(raw)[0] ?? "",
|
|
1337
|
+
);
|
|
1338
|
+
const firstFamilyOption = FONT_FAMILY_OPTIONS.find(
|
|
1339
|
+
(option) =>
|
|
1340
|
+
normalizeFontFamilyName(splitFontFamilyList(option.value)[0] ?? "") ===
|
|
1341
|
+
firstFamily,
|
|
1342
|
+
);
|
|
1343
|
+
return firstFamilyOption?.value ?? raw;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1134
1346
|
const ALIGN_SELF_OPTIONS = [
|
|
1135
1347
|
{ value: "auto", key: "auto" },
|
|
1136
1348
|
{ value: "flex-start", key: "start" },
|
|
@@ -1282,6 +1494,8 @@ function ScrubStyleInput({
|
|
|
1282
1494
|
step = 1,
|
|
1283
1495
|
labelClassName,
|
|
1284
1496
|
inputClassName,
|
|
1497
|
+
ariaLabel,
|
|
1498
|
+
tooltipLabel,
|
|
1285
1499
|
hideIcon = true,
|
|
1286
1500
|
}: {
|
|
1287
1501
|
label: string;
|
|
@@ -1295,11 +1509,15 @@ function ScrubStyleInput({
|
|
|
1295
1509
|
labelClassName?: string;
|
|
1296
1510
|
inputClassName?: string;
|
|
1297
1511
|
hideIcon?: boolean;
|
|
1512
|
+
ariaLabel?: string;
|
|
1513
|
+
tooltipLabel?: string;
|
|
1298
1514
|
}) {
|
|
1299
1515
|
return (
|
|
1300
1516
|
<ScrubInput
|
|
1301
1517
|
label={label}
|
|
1302
|
-
|
|
1518
|
+
ariaLabel={ariaLabel}
|
|
1519
|
+
tooltipLabel={tooltipLabel}
|
|
1520
|
+
icon={hideIcon ? null : undefined}
|
|
1303
1521
|
value={value ? parseNumericValue(value) : (placeholder ?? 0)}
|
|
1304
1522
|
onChange={onChange}
|
|
1305
1523
|
unit={unit}
|
|
@@ -1650,6 +1868,33 @@ function inferElementSizing(
|
|
|
1650
1868
|
return "fixed";
|
|
1651
1869
|
}
|
|
1652
1870
|
|
|
1871
|
+
/**
|
|
1872
|
+
* Return the element's geometric dimension on the given axis in CSS pixels.
|
|
1873
|
+
*
|
|
1874
|
+
* `getComputedStyle().width/height` always resolves to a computed px value
|
|
1875
|
+
* (even for `width: auto` the browser returns e.g. "200px"). For rotated
|
|
1876
|
+
* elements this is the pre-rotation CSS box size — what Figma shows in the
|
|
1877
|
+
* inspector — while `getBoundingClientRect().width/height` would be the
|
|
1878
|
+
* axis-aligned bounding box which is inflated by the rotation.
|
|
1879
|
+
*
|
|
1880
|
+
* Falls back to the bounding-rect dimension only when the computed style is
|
|
1881
|
+
* missing or unparseable (e.g. the bridge hasn't populated it yet).
|
|
1882
|
+
*/
|
|
1883
|
+
function cssElementSize(
|
|
1884
|
+
element: ElementInfo,
|
|
1885
|
+
axis: AutoLayoutSizingAxis,
|
|
1886
|
+
): number {
|
|
1887
|
+
const isHorizontal = axis === "horizontal";
|
|
1888
|
+
const cssValue = isHorizontal
|
|
1889
|
+
? element.computedStyles.width
|
|
1890
|
+
: element.computedStyles.height;
|
|
1891
|
+
const parsed = parseFloat(cssValue || "");
|
|
1892
|
+
const fallback = isHorizontal
|
|
1893
|
+
? element.boundingRect.width
|
|
1894
|
+
: element.boundingRect.height;
|
|
1895
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1653
1898
|
function commitElementSizing(
|
|
1654
1899
|
element: ElementInfo,
|
|
1655
1900
|
axis: AutoLayoutSizingAxis,
|
|
@@ -1659,12 +1904,10 @@ function commitElementSizing(
|
|
|
1659
1904
|
) {
|
|
1660
1905
|
const isHorizontal = axis === "horizontal";
|
|
1661
1906
|
const sizeProperty = isHorizontal ? "width" : "height";
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
),
|
|
1667
|
-
);
|
|
1907
|
+
// Use CSS computed dimension (pre-rotation box size) as the seed for "fixed"
|
|
1908
|
+
// sizing so a rotated element is locked to its actual CSS width/height rather
|
|
1909
|
+
// than the inflated axis-aligned bounding rect.
|
|
1910
|
+
const resolvedSize = Math.max(1, Math.round(cssElementSize(element, axis)));
|
|
1668
1911
|
const parentDirection = parentFlexDirection(element);
|
|
1669
1912
|
const isFlex = isParentFlex(element);
|
|
1670
1913
|
const isGrid = isParentGrid(element);
|
|
@@ -1715,6 +1958,325 @@ function commitElementSizing(
|
|
|
1715
1958
|
commitStylePatch(patch, onStyleChange, onStylesChange);
|
|
1716
1959
|
}
|
|
1717
1960
|
|
|
1961
|
+
/**
|
|
1962
|
+
* Small modal that prompts for a component name, then promotes the current
|
|
1963
|
+
* selection into a reusable component via `onSubmit`.
|
|
1964
|
+
*/
|
|
1965
|
+
function CreateComponentDialog({
|
|
1966
|
+
open,
|
|
1967
|
+
onOpenChange,
|
|
1968
|
+
defaultName,
|
|
1969
|
+
onSubmit,
|
|
1970
|
+
}: {
|
|
1971
|
+
open: boolean;
|
|
1972
|
+
onOpenChange: (open: boolean) => void;
|
|
1973
|
+
defaultName: string;
|
|
1974
|
+
onSubmit: (name: string) => void;
|
|
1975
|
+
}) {
|
|
1976
|
+
const [name, setName] = useState(defaultName);
|
|
1977
|
+
|
|
1978
|
+
// Reset the field to the freshest default each time the dialog opens.
|
|
1979
|
+
useEffect(() => {
|
|
1980
|
+
if (open) setName(defaultName);
|
|
1981
|
+
}, [open, defaultName]);
|
|
1982
|
+
|
|
1983
|
+
const commit = () => {
|
|
1984
|
+
const trimmed = name.trim();
|
|
1985
|
+
if (!trimmed) return;
|
|
1986
|
+
onSubmit(trimmed);
|
|
1987
|
+
onOpenChange(false);
|
|
1988
|
+
};
|
|
1989
|
+
|
|
1990
|
+
return (
|
|
1991
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
1992
|
+
<DialogContent className="sm:max-w-sm">
|
|
1993
|
+
<DialogHeader>
|
|
1994
|
+
<DialogTitle>
|
|
1995
|
+
{"Create component" /* i18n-ignore design inspector action */}
|
|
1996
|
+
</DialogTitle>
|
|
1997
|
+
<DialogDescription>
|
|
1998
|
+
{
|
|
1999
|
+
"Name this element so it becomes a reusable component. The agent can then extract props and replace repeated instances." /* i18n-ignore design inspector copy */
|
|
2000
|
+
}
|
|
2001
|
+
</DialogDescription>
|
|
2002
|
+
</DialogHeader>
|
|
2003
|
+
<div className="space-y-1.5">
|
|
2004
|
+
<Label
|
|
2005
|
+
htmlFor="create-component-name"
|
|
2006
|
+
className="text-[11px] font-medium text-muted-foreground"
|
|
2007
|
+
>
|
|
2008
|
+
{"Component name" /* i18n-ignore design inspector label */}
|
|
2009
|
+
</Label>
|
|
2010
|
+
<Input
|
|
2011
|
+
id="create-component-name"
|
|
2012
|
+
autoFocus
|
|
2013
|
+
value={name}
|
|
2014
|
+
onChange={(e) => setName(e.target.value)}
|
|
2015
|
+
onKeyDown={(e) => {
|
|
2016
|
+
if (e.key === "Enter") {
|
|
2017
|
+
e.preventDefault();
|
|
2018
|
+
commit();
|
|
2019
|
+
}
|
|
2020
|
+
}}
|
|
2021
|
+
placeholder={
|
|
2022
|
+
"PrimaryButton" /* i18n-ignore design inspector placeholder */
|
|
2023
|
+
}
|
|
2024
|
+
/>
|
|
2025
|
+
</div>
|
|
2026
|
+
<DialogFooter>
|
|
2027
|
+
<Button
|
|
2028
|
+
type="button"
|
|
2029
|
+
variant="ghost"
|
|
2030
|
+
onClick={() => onOpenChange(false)}
|
|
2031
|
+
>
|
|
2032
|
+
{"Cancel" /* i18n-ignore design inspector action */}
|
|
2033
|
+
</Button>
|
|
2034
|
+
<Button type="button" onClick={commit} disabled={!name.trim()}>
|
|
2035
|
+
{"Create" /* i18n-ignore design inspector action */}
|
|
2036
|
+
</Button>
|
|
2037
|
+
</DialogFooter>
|
|
2038
|
+
</DialogContent>
|
|
2039
|
+
</Dialog>
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/** Build a `vscode://file/...` deep link for an absolute path + position. */
|
|
2044
|
+
function vscodeDeepLink(
|
|
2045
|
+
absolutePath: string,
|
|
2046
|
+
line?: number,
|
|
2047
|
+
column?: number,
|
|
2048
|
+
): string {
|
|
2049
|
+
const base = `vscode://file/${absolutePath}`;
|
|
2050
|
+
if (line == null) return base;
|
|
2051
|
+
return column == null ? `${base}:${line}` : `${base}:${line}:${column}`;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* Extract the *opening tag* from an element's outer HTML for an at-a-glance
|
|
2056
|
+
* summary (e.g. `<main class="hero" data-x="y">`). Self-closing tags keep
|
|
2057
|
+
* their `/>`. Returns `null` when no tag can be parsed.
|
|
2058
|
+
*
|
|
2059
|
+
* Pure — exported for tests.
|
|
2060
|
+
*/
|
|
2061
|
+
export function openingTagOf(html: string | null | undefined): string | null {
|
|
2062
|
+
if (!html) return null;
|
|
2063
|
+
const trimmed = html.trimStart();
|
|
2064
|
+
// Match the first `<tag ...>` (greedy up to the first unquoted `>`), allowing
|
|
2065
|
+
// quoted attribute values to contain `>`.
|
|
2066
|
+
const match = /^<([a-zA-Z][\w-]*)((?:"[^"]*"|'[^']*'|[^>])*?)\/?>/.exec(
|
|
2067
|
+
trimmed,
|
|
2068
|
+
);
|
|
2069
|
+
if (!match) return null;
|
|
2070
|
+
return match[0];
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/**
|
|
2074
|
+
* Collapse long attribute values in an opening tag so the at-a-glance summary
|
|
2075
|
+
* stays readable. Values longer than `max` chars are truncated with an
|
|
2076
|
+
* ellipsis (the surrounding quotes are preserved).
|
|
2077
|
+
*
|
|
2078
|
+
* Pure — exported for tests.
|
|
2079
|
+
*/
|
|
2080
|
+
export function truncateOpeningTag(openTag: string, max = 32): string {
|
|
2081
|
+
return openTag.replace(
|
|
2082
|
+
/("|')((?:\\.|(?!\1)[^\\])*)\1/g,
|
|
2083
|
+
(full, quote, value) => {
|
|
2084
|
+
if (typeof value !== "string" || value.length <= max) return full;
|
|
2085
|
+
return `${quote}${value.slice(0, max - 1)}…${quote}`;
|
|
2086
|
+
},
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
/**
|
|
2091
|
+
* Parse the top-level `key: value` pairs from an Alpine `x-data` object literal
|
|
2092
|
+
* (e.g. `{ variant: 'outline', size: 'lg', disabled: false }`).
|
|
2093
|
+
*
|
|
2094
|
+
* Best-effort: only handles a flat object of simple string / boolean / number
|
|
2095
|
+
* literals — exactly the shape used for component variant + state props. Nested
|
|
2096
|
+
* objects, methods, and computed expressions are ignored. Returns `null` when
|
|
2097
|
+
* the value is not a recognizable flat object literal.
|
|
2098
|
+
*
|
|
2099
|
+
* Pure — exported for tests.
|
|
2100
|
+
*/
|
|
2101
|
+
export function parseAlpineDataObject(
|
|
2102
|
+
xData: string | null | undefined,
|
|
2103
|
+
): Record<string, string> | null {
|
|
2104
|
+
if (!xData) return null;
|
|
2105
|
+
const trimmed = xData.trim();
|
|
2106
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
|
2107
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
2108
|
+
if (!inner) return {};
|
|
2109
|
+
|
|
2110
|
+
const out: Record<string, string> = {};
|
|
2111
|
+
// Split on top-level commas only (no nesting / quotes inside values here).
|
|
2112
|
+
const pairRe =
|
|
2113
|
+
/(?:^|,)\s*(?:'([^']+)'|"([^"]+)"|([A-Za-z_$][\w$]*))\s*:\s*('[^']*'|"[^"]*"|true|false|-?\d+(?:\.\d+)?)/g;
|
|
2114
|
+
let m: RegExpExecArray | null;
|
|
2115
|
+
let matched = false;
|
|
2116
|
+
while ((m = pairRe.exec(inner)) !== null) {
|
|
2117
|
+
matched = true;
|
|
2118
|
+
const key = m[1] ?? m[2] ?? m[3];
|
|
2119
|
+
let raw = m[4]!;
|
|
2120
|
+
// Unwrap quotes for string literals; keep booleans / numbers verbatim.
|
|
2121
|
+
if (
|
|
2122
|
+
(raw.startsWith("'") && raw.endsWith("'")) ||
|
|
2123
|
+
(raw.startsWith('"') && raw.endsWith('"'))
|
|
2124
|
+
) {
|
|
2125
|
+
raw = raw.slice(1, -1);
|
|
2126
|
+
}
|
|
2127
|
+
if (key) out[key] = raw;
|
|
2128
|
+
}
|
|
2129
|
+
// If there was content but nothing parsed, the shape is too complex to edit
|
|
2130
|
+
// safely — bail so the caller falls back to attribute-based prop edits.
|
|
2131
|
+
if (!matched) return null;
|
|
2132
|
+
return out;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
/**
|
|
2136
|
+
* Re-serialize a flat Alpine data object back into an `x-data` literal,
|
|
2137
|
+
* preserving boolean / number literals unquoted and single-quoting strings.
|
|
2138
|
+
*
|
|
2139
|
+
* Pure — exported for tests.
|
|
2140
|
+
*/
|
|
2141
|
+
export function serializeAlpineDataObject(obj: Record<string, string>): string {
|
|
2142
|
+
const parts = Object.entries(obj).map(([key, value]) => {
|
|
2143
|
+
const isBoolean = value === "true" || value === "false";
|
|
2144
|
+
const isNumber = /^-?\d+(\.\d+)?$/.test(value);
|
|
2145
|
+
const literal =
|
|
2146
|
+
isBoolean || isNumber ? value : `'${value.replace(/'/g, "\\'")}'`;
|
|
2147
|
+
return `${key}: ${literal}`;
|
|
2148
|
+
});
|
|
2149
|
+
return parts.length ? `{ ${parts.join(", ")} }` : "{}";
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
/** A boolean-ish prop value (`"true"` / `"false"`), case-insensitive. */
|
|
2153
|
+
export function isBooleanPropValue(value: string): boolean {
|
|
2154
|
+
const v = value.trim().toLowerCase();
|
|
2155
|
+
return v === "true" || v === "false";
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
/**
|
|
2159
|
+
* Popover anchored to the "Inspect code" button showing the selected node's
|
|
2160
|
+
* code. Inline/Alpine sources show the element's outer HTML with a Copy
|
|
2161
|
+
* button; real-app sources additionally render an "Open in VS Code" button.
|
|
2162
|
+
*
|
|
2163
|
+
* TODO: replace the read-only <pre> with an inline Monaco editor for richer
|
|
2164
|
+
* (editable) code inspection once the editor bundle is wired into the inspector.
|
|
2165
|
+
*/
|
|
2166
|
+
function InspectCodePopover({
|
|
2167
|
+
trigger,
|
|
2168
|
+
data,
|
|
2169
|
+
}: {
|
|
2170
|
+
trigger: ReactNode;
|
|
2171
|
+
data: InspectCodeData;
|
|
2172
|
+
}) {
|
|
2173
|
+
const [copied, setCopied] = useState(false);
|
|
2174
|
+
const html = data.html ?? "";
|
|
2175
|
+
const source = data.sourceLocation ?? null;
|
|
2176
|
+
const snippet = source?.snippet || html;
|
|
2177
|
+
// At-a-glance opening tag (tag name + attributes), truncated for readability.
|
|
2178
|
+
const openingTag = (() => {
|
|
2179
|
+
const raw = openingTagOf(html);
|
|
2180
|
+
return raw ? truncateOpeningTag(raw) : null;
|
|
2181
|
+
})();
|
|
2182
|
+
|
|
2183
|
+
const handleCopy = () => {
|
|
2184
|
+
if (!snippet) return;
|
|
2185
|
+
void navigator.clipboard
|
|
2186
|
+
?.writeText(snippet)
|
|
2187
|
+
.then(() => {
|
|
2188
|
+
setCopied(true);
|
|
2189
|
+
window.setTimeout(() => setCopied(false), 1200);
|
|
2190
|
+
})
|
|
2191
|
+
.catch(() => {
|
|
2192
|
+
/* clipboard may be unavailable; ignore */
|
|
2193
|
+
});
|
|
2194
|
+
};
|
|
2195
|
+
|
|
2196
|
+
return (
|
|
2197
|
+
<Popover>
|
|
2198
|
+
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
2199
|
+
<PopoverContent align="end" className="w-80 space-y-2 p-2 text-[11px]">
|
|
2200
|
+
<div className="flex items-center justify-between gap-2">
|
|
2201
|
+
<span className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">
|
|
2202
|
+
{"Inspect code" /* i18n-ignore design inspector label */}
|
|
2203
|
+
</span>
|
|
2204
|
+
<Button
|
|
2205
|
+
type="button"
|
|
2206
|
+
variant="ghost"
|
|
2207
|
+
size="sm"
|
|
2208
|
+
className="h-6 px-2 text-[10px]"
|
|
2209
|
+
onClick={handleCopy}
|
|
2210
|
+
disabled={!snippet}
|
|
2211
|
+
>
|
|
2212
|
+
{
|
|
2213
|
+
copied
|
|
2214
|
+
? "Copied" /* i18n-ignore design inspector action */
|
|
2215
|
+
: "Copy" /* i18n-ignore design inspector action */
|
|
2216
|
+
}
|
|
2217
|
+
</Button>
|
|
2218
|
+
</div>
|
|
2219
|
+
|
|
2220
|
+
{/* At-a-glance opening tag (tag name + attributes) above the full HTML. */}
|
|
2221
|
+
{openingTag && (
|
|
2222
|
+
<code
|
|
2223
|
+
className="block truncate rounded bg-[var(--design-editor-control-bg)] px-2 py-1 font-mono text-[10px] text-foreground"
|
|
2224
|
+
title={openingTag}
|
|
2225
|
+
>
|
|
2226
|
+
{openingTag}
|
|
2227
|
+
</code>
|
|
2228
|
+
)}
|
|
2229
|
+
|
|
2230
|
+
{source && (
|
|
2231
|
+
<div
|
|
2232
|
+
className="flex items-center gap-1 rounded bg-[var(--design-editor-control-bg)] px-2 py-1"
|
|
2233
|
+
title={source.absolutePath}
|
|
2234
|
+
>
|
|
2235
|
+
<IconCode className="size-3 shrink-0 text-muted-foreground/60" />
|
|
2236
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground">
|
|
2237
|
+
{source.absolutePath}
|
|
2238
|
+
{source.line != null ? `:${source.line}` : ""}
|
|
2239
|
+
</span>
|
|
2240
|
+
</div>
|
|
2241
|
+
)}
|
|
2242
|
+
|
|
2243
|
+
{snippet ? (
|
|
2244
|
+
<pre className="max-h-64 overflow-auto rounded bg-[var(--design-editor-control-bg)] p-2 font-mono text-[10px] leading-relaxed text-foreground">
|
|
2245
|
+
<code>{snippet}</code>
|
|
2246
|
+
</pre>
|
|
2247
|
+
) : (
|
|
2248
|
+
<p className="px-1 py-2 text-muted-foreground">
|
|
2249
|
+
{
|
|
2250
|
+
"No source available for this element." /* i18n-ignore design inspector empty */
|
|
2251
|
+
}
|
|
2252
|
+
</p>
|
|
2253
|
+
)}
|
|
2254
|
+
|
|
2255
|
+
{source && (
|
|
2256
|
+
<a
|
|
2257
|
+
href={vscodeDeepLink(
|
|
2258
|
+
source.absolutePath,
|
|
2259
|
+
source.line,
|
|
2260
|
+
source.column,
|
|
2261
|
+
)}
|
|
2262
|
+
className="block"
|
|
2263
|
+
>
|
|
2264
|
+
<Button
|
|
2265
|
+
type="button"
|
|
2266
|
+
variant="outline"
|
|
2267
|
+
size="sm"
|
|
2268
|
+
className="h-7 w-full gap-1.5 text-[11px]"
|
|
2269
|
+
>
|
|
2270
|
+
<IconExternalLink className="size-3.5" />
|
|
2271
|
+
{"Open in VS Code" /* i18n-ignore design inspector action */}
|
|
2272
|
+
</Button>
|
|
2273
|
+
</a>
|
|
2274
|
+
)}
|
|
2275
|
+
</PopoverContent>
|
|
2276
|
+
</Popover>
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
|
|
1718
2280
|
function elementTypeIcon(element: ElementInfo) {
|
|
1719
2281
|
const tag = element.tagName || "element";
|
|
1720
2282
|
if (TEXT_TAGS.has(tag)) return IconTypography;
|
|
@@ -1724,35 +2286,55 @@ function elementTypeIcon(element: ElementInfo) {
|
|
|
1724
2286
|
return IconFrame;
|
|
1725
2287
|
}
|
|
1726
2288
|
|
|
1727
|
-
function SelectionHeader({
|
|
2289
|
+
function SelectionHeader({
|
|
2290
|
+
element,
|
|
2291
|
+
onCreateComponent,
|
|
2292
|
+
inspectCodePopover,
|
|
2293
|
+
}: {
|
|
2294
|
+
element: ElementInfo | null;
|
|
2295
|
+
/** Promote the current selection into a reusable component. Omit/undefined to disable. */
|
|
2296
|
+
onCreateComponent?: () => void;
|
|
2297
|
+
/**
|
|
2298
|
+
* Render prop for the "Inspect code" affordance. Receives the trigger button
|
|
2299
|
+
* so the parent can anchor a Popover to it; returns the full popover element.
|
|
2300
|
+
* When omitted the button renders disabled.
|
|
2301
|
+
*/
|
|
2302
|
+
inspectCodePopover?: (trigger: ReactNode) => ReactNode;
|
|
2303
|
+
}) {
|
|
1728
2304
|
if (!element) return null;
|
|
1729
2305
|
|
|
1730
2306
|
const title = inspectorObjectTitle(element);
|
|
1731
2307
|
const TypeIcon = elementTypeIcon(element);
|
|
1732
2308
|
|
|
2309
|
+
const inspectTrigger = (
|
|
2310
|
+
<SectionIconButton
|
|
2311
|
+
label={"Inspect code" /* i18n-ignore design inspector action */}
|
|
2312
|
+
disabled={!inspectCodePopover}
|
|
2313
|
+
>
|
|
2314
|
+
<IconCode className="size-3.5" />
|
|
2315
|
+
</SectionIconButton>
|
|
2316
|
+
);
|
|
2317
|
+
|
|
1733
2318
|
return (
|
|
1734
2319
|
<div className="flex min-h-8 shrink-0 items-center justify-between gap-2 border-b border-border/90 px-3">
|
|
1735
|
-
{/*
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
className="flex min-w-0 items-center gap-1.5 bg-transparent text-left text-[13px] font-semibold text-foreground"
|
|
1739
|
-
>
|
|
2320
|
+
{/* Node-type label. Rename lives in the layers panel and device sizing
|
|
2321
|
+
lives elsewhere, so this is a plain non-interactive label. */}
|
|
2322
|
+
<div className="flex min-w-0 items-center gap-1.5 text-left text-[13px] font-semibold text-foreground">
|
|
1740
2323
|
<TypeIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
|
1741
2324
|
<span className="truncate">{title}</span>
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
{/* M3 · Right-aligned quick actions: create-component + dev inspect (</>) */}
|
|
2325
|
+
</div>
|
|
2326
|
+
{/* Right-aligned quick actions: create-component + dev inspect (</>) */}
|
|
1745
2327
|
<div className="flex shrink-0 items-center gap-0.5">
|
|
1746
2328
|
<SectionIconButton
|
|
1747
2329
|
label={"Create component" /* i18n-ignore design inspector action */}
|
|
2330
|
+
disabled={!onCreateComponent}
|
|
2331
|
+
onClick={onCreateComponent}
|
|
1748
2332
|
>
|
|
1749
2333
|
<IconComponents className="size-3.5" />
|
|
1750
2334
|
</SectionIconButton>
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
<IconCode className="size-3.5" />
|
|
1755
|
-
</SectionIconButton>
|
|
2335
|
+
{inspectCodePopover
|
|
2336
|
+
? inspectCodePopover(inspectTrigger)
|
|
2337
|
+
: inspectTrigger}
|
|
1756
2338
|
</div>
|
|
1757
2339
|
</div>
|
|
1758
2340
|
);
|
|
@@ -1770,7 +2352,7 @@ function InspectorTabsHeader({
|
|
|
1770
2352
|
const t = useT();
|
|
1771
2353
|
|
|
1772
2354
|
return (
|
|
1773
|
-
<div className="flex min-h-8 shrink-0 items-center justify-between gap-1 border-b border-border/90 px-2">
|
|
2355
|
+
<div className="flex min-h-8 shrink-0 items-center justify-between gap-1 border-b border-border/90 px-2 py-1">
|
|
1774
2356
|
<Tabs
|
|
1775
2357
|
value={activeTab}
|
|
1776
2358
|
onValueChange={(value) => onActiveTabChange(value as InspectorTab)}
|
|
@@ -1912,12 +2494,113 @@ function InspectorIconButton({
|
|
|
1912
2494
|
|
|
1913
2495
|
function InspectorSegment({ children }: { children: ReactNode }) {
|
|
1914
2496
|
return (
|
|
1915
|
-
<div className="flex min-w-0 overflow-hidden rounded-md bg-[var(--design-editor-control-bg)]">
|
|
2497
|
+
<div className="flex w-fit max-w-full min-w-0 overflow-hidden rounded-md bg-[var(--design-editor-control-bg)]">
|
|
1916
2498
|
{children}
|
|
1917
2499
|
</div>
|
|
1918
2500
|
);
|
|
1919
2501
|
}
|
|
1920
2502
|
|
|
2503
|
+
function TextResizeControls({
|
|
2504
|
+
resizeMode,
|
|
2505
|
+
onResizeModeChange,
|
|
2506
|
+
}: {
|
|
2507
|
+
resizeMode: TextResizeMode;
|
|
2508
|
+
onResizeModeChange: (mode: TextResizeMode) => void;
|
|
2509
|
+
}) {
|
|
2510
|
+
const t = useT();
|
|
2511
|
+
|
|
2512
|
+
return (
|
|
2513
|
+
<InspectorSegment>
|
|
2514
|
+
<InspectorIconButton
|
|
2515
|
+
label={t("editPanel.textResize.autoWidth")}
|
|
2516
|
+
active={resizeMode === "auto-width"}
|
|
2517
|
+
onClick={() => onResizeModeChange("auto-width")}
|
|
2518
|
+
>
|
|
2519
|
+
<IconArrowAutofitWidth className="size-3.5" />
|
|
2520
|
+
</InspectorIconButton>
|
|
2521
|
+
<InspectorIconButton
|
|
2522
|
+
label={t("editPanel.textResize.autoHeight")}
|
|
2523
|
+
active={resizeMode === "auto-height"}
|
|
2524
|
+
onClick={() => onResizeModeChange("auto-height")}
|
|
2525
|
+
>
|
|
2526
|
+
<IconArrowAutofitHeight className="size-3.5" />
|
|
2527
|
+
</InspectorIconButton>
|
|
2528
|
+
<InspectorIconButton
|
|
2529
|
+
label={t("editPanel.textResize.fixed")}
|
|
2530
|
+
active={resizeMode === "fixed"}
|
|
2531
|
+
onClick={() => onResizeModeChange("fixed")}
|
|
2532
|
+
>
|
|
2533
|
+
<IconSquare className="size-3.5" />
|
|
2534
|
+
</InspectorIconButton>
|
|
2535
|
+
</InspectorSegment>
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
function TypographyDetailsPopover({
|
|
2540
|
+
resizeMode,
|
|
2541
|
+
onResizeModeChange,
|
|
2542
|
+
}: {
|
|
2543
|
+
resizeMode: TextResizeMode;
|
|
2544
|
+
onResizeModeChange: (mode: TextResizeMode) => void;
|
|
2545
|
+
}) {
|
|
2546
|
+
const [open, setOpen] = useState(false);
|
|
2547
|
+
|
|
2548
|
+
return (
|
|
2549
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
2550
|
+
<PopoverTrigger asChild>
|
|
2551
|
+
<Button
|
|
2552
|
+
type="button"
|
|
2553
|
+
variant="ghost"
|
|
2554
|
+
size="icon"
|
|
2555
|
+
aria-label={"Typography details" /* i18n-ignore design action */}
|
|
2556
|
+
aria-pressed={open}
|
|
2557
|
+
className={cn(
|
|
2558
|
+
"h-6 min-w-6 cursor-pointer rounded-md text-muted-foreground hover:bg-[var(--design-editor-panel-raised-bg)] hover:text-foreground",
|
|
2559
|
+
open &&
|
|
2560
|
+
"bg-[var(--design-editor-accent-color)]/20 text-[var(--design-editor-accent-color)] hover:text-[var(--design-editor-accent-color)]",
|
|
2561
|
+
)}
|
|
2562
|
+
>
|
|
2563
|
+
<IconLayoutSettings className="size-3.5" />
|
|
2564
|
+
</Button>
|
|
2565
|
+
</PopoverTrigger>
|
|
2566
|
+
<PopoverContent
|
|
2567
|
+
side="left"
|
|
2568
|
+
align="end"
|
|
2569
|
+
sideOffset={8}
|
|
2570
|
+
className="z-[100010] w-[360px] rounded-xl border-[var(--design-editor-control-border)] bg-[var(--design-editor-panel-bg)] p-0 text-foreground shadow-2xl"
|
|
2571
|
+
>
|
|
2572
|
+
<div className="flex items-center gap-1 border-b border-[var(--design-editor-control-border)] p-2.5">
|
|
2573
|
+
<div className="flex rounded-md bg-[var(--design-editor-control-bg)] p-0.5">
|
|
2574
|
+
<span className="rounded bg-[var(--design-editor-panel-raised-bg)] px-2.5 py-1 text-[11px] font-semibold text-foreground">
|
|
2575
|
+
{"Basics" /* i18n-ignore design typography details tab */}
|
|
2576
|
+
</span>
|
|
2577
|
+
<span className="px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
|
|
2578
|
+
{"Details" /* i18n-ignore design typography details tab */}
|
|
2579
|
+
</span>
|
|
2580
|
+
<span className="px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
|
|
2581
|
+
{"Variable" /* i18n-ignore design typography details tab */}
|
|
2582
|
+
</span>
|
|
2583
|
+
</div>
|
|
2584
|
+
</div>
|
|
2585
|
+
<div className="space-y-3 p-4 text-[11px]">
|
|
2586
|
+
<div className="flex h-20 items-center justify-center rounded-md bg-[var(--design-editor-control-bg)] text-[18px] text-muted-foreground/80">
|
|
2587
|
+
{"Preview" /* i18n-ignore design typography details preview */}
|
|
2588
|
+
</div>
|
|
2589
|
+
<div className="flex items-center justify-between gap-3">
|
|
2590
|
+
<span className="text-[11px] font-medium text-muted-foreground">
|
|
2591
|
+
{"Text box" /* i18n-ignore design typography details label */}
|
|
2592
|
+
</span>
|
|
2593
|
+
<TextResizeControls
|
|
2594
|
+
resizeMode={resizeMode}
|
|
2595
|
+
onResizeModeChange={onResizeModeChange}
|
|
2596
|
+
/>
|
|
2597
|
+
</div>
|
|
2598
|
+
</div>
|
|
2599
|
+
</PopoverContent>
|
|
2600
|
+
</Popover>
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2603
|
+
|
|
1921
2604
|
function CornerRadiusControl({
|
|
1922
2605
|
styles,
|
|
1923
2606
|
onStyleChange,
|
|
@@ -2389,20 +3072,29 @@ function ShadowEffectRow({
|
|
|
2389
3072
|
function PageProperties({
|
|
2390
3073
|
styles,
|
|
2391
3074
|
onStyleChange,
|
|
3075
|
+
onStylesChange,
|
|
2392
3076
|
}: {
|
|
2393
3077
|
styles: Record<string, string>;
|
|
2394
3078
|
onStyleChange: (property: string, value: string) => void;
|
|
3079
|
+
onStylesChange?: (styles: Record<string, string>) => void;
|
|
2395
3080
|
}) {
|
|
2396
3081
|
const t = useT();
|
|
2397
|
-
const
|
|
3082
|
+
const baseFontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
|
|
2398
3083
|
value: option.value,
|
|
2399
3084
|
label: t(`editPanel.fontFamilies.${option.key}`),
|
|
2400
3085
|
}));
|
|
2401
|
-
const fontFamily =
|
|
2402
|
-
|
|
3086
|
+
const fontFamily = resolveFontFamilySelectValue(styles.fontFamily);
|
|
3087
|
+
const fontFamilyOptions = FONT_FAMILY_OPTIONS.some(
|
|
3088
|
+
(option) => option.value === fontFamily,
|
|
2403
3089
|
)
|
|
2404
|
-
?
|
|
2405
|
-
:
|
|
3090
|
+
? baseFontFamilyOptions
|
|
3091
|
+
: [
|
|
3092
|
+
{
|
|
3093
|
+
value: fontFamily,
|
|
3094
|
+
label: displayFontFamilyName(styles.fontFamily || fontFamily),
|
|
3095
|
+
},
|
|
3096
|
+
...baseFontFamilyOptions,
|
|
3097
|
+
];
|
|
2406
3098
|
|
|
2407
3099
|
return (
|
|
2408
3100
|
<div>
|
|
@@ -2413,6 +3105,13 @@ function PageProperties({
|
|
|
2413
3105
|
onChange={(v) => onStyleChange("backgroundColor", v)}
|
|
2414
3106
|
backgroundImage={styles.backgroundImage || ""}
|
|
2415
3107
|
onBackgroundImageChange={(v) => onStyleChange("backgroundImage", v)}
|
|
3108
|
+
onImageFillChange={(value) =>
|
|
3109
|
+
commitStylePatch(
|
|
3110
|
+
imageFillToBackgroundStyles(value),
|
|
3111
|
+
onStyleChange,
|
|
3112
|
+
onStylesChange,
|
|
3113
|
+
)
|
|
3114
|
+
}
|
|
2416
3115
|
blendMode={styles.backgroundBlendMode || "normal"}
|
|
2417
3116
|
onBlendModeChange={(v) => onStyleChange("backgroundBlendMode", v)}
|
|
2418
3117
|
supportsLayeredFills
|
|
@@ -2445,10 +3144,22 @@ function TypographyProperties({
|
|
|
2445
3144
|
}) {
|
|
2446
3145
|
const t = useT();
|
|
2447
3146
|
const styles = element.computedStyles;
|
|
2448
|
-
const
|
|
3147
|
+
const baseFontFamilyOptions = FONT_FAMILY_OPTIONS.map((option) => ({
|
|
2449
3148
|
value: option.value,
|
|
2450
3149
|
label: t(`editPanel.fontFamilies.${option.key}`),
|
|
2451
3150
|
}));
|
|
3151
|
+
const fontFamily = resolveFontFamilySelectValue(styles.fontFamily);
|
|
3152
|
+
const fontFamilyOptions = FONT_FAMILY_OPTIONS.some(
|
|
3153
|
+
(option) => option.value === fontFamily,
|
|
3154
|
+
)
|
|
3155
|
+
? baseFontFamilyOptions
|
|
3156
|
+
: [
|
|
3157
|
+
{
|
|
3158
|
+
value: fontFamily,
|
|
3159
|
+
label: displayFontFamilyName(styles.fontFamily || fontFamily),
|
|
3160
|
+
},
|
|
3161
|
+
...baseFontFamilyOptions,
|
|
3162
|
+
];
|
|
2452
3163
|
const fontWeightOptions = FONT_WEIGHT_OPTIONS.map((option) => ({
|
|
2453
3164
|
value: option.value,
|
|
2454
3165
|
label: t(`editPanel.fontWeights.${option.key}`),
|
|
@@ -2463,7 +3174,7 @@ function TypographyProperties({
|
|
|
2463
3174
|
!styles.width || styles.width === "auto" || styles.width === "max-content";
|
|
2464
3175
|
const heightIsAuto = !styles.height || styles.height === "auto";
|
|
2465
3176
|
const noWrap = styles.whiteSpace === "nowrap";
|
|
2466
|
-
const resizeMode:
|
|
3177
|
+
const resizeMode: TextResizeMode =
|
|
2467
3178
|
widthIsAuto && noWrap
|
|
2468
3179
|
? "auto-width"
|
|
2469
3180
|
: !heightIsAuto && !widthIsAuto
|
|
@@ -2471,7 +3182,7 @@ function TypographyProperties({
|
|
|
2471
3182
|
: "auto-height";
|
|
2472
3183
|
const currentWidth = styles.width && !widthIsAuto ? styles.width : "200px";
|
|
2473
3184
|
const currentHeight = styles.height && !heightIsAuto ? styles.height : "48px";
|
|
2474
|
-
const setResizeMode = (mode:
|
|
3185
|
+
const setResizeMode = (mode: TextResizeMode) => {
|
|
2475
3186
|
if (mode === "auto-width") {
|
|
2476
3187
|
onStyleChange("width", "auto");
|
|
2477
3188
|
onStyleChange("height", "auto");
|
|
@@ -2521,51 +3232,36 @@ function TypographyProperties({
|
|
|
2521
3232
|
|
|
2522
3233
|
return (
|
|
2523
3234
|
<PanelSection title={t("editPanel.sections.typography")}>
|
|
2524
|
-
{/* Row
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
label={"Auto height" /* i18n-ignore design text-resize mode */}
|
|
2535
|
-
active={resizeMode === "auto-height"}
|
|
2536
|
-
onClick={() => setResizeMode("auto-height")}
|
|
2537
|
-
>
|
|
2538
|
-
<IconArrowAutofitHeight className="size-3.5" />
|
|
2539
|
-
</InspectorIconButton>
|
|
2540
|
-
<InspectorIconButton
|
|
2541
|
-
label={"Fixed size" /* i18n-ignore design text-resize mode */}
|
|
2542
|
-
active={resizeMode === "fixed"}
|
|
2543
|
-
onClick={() => setResizeMode("fixed")}
|
|
3235
|
+
{/* Row 1: font family full-width.
|
|
3236
|
+
Wrapped in a height-constrained div so the SelectTrigger button's
|
|
3237
|
+
hit-target is exactly h-6 (24 px) and cannot visually or physically
|
|
3238
|
+
overlap the weight/size row below (bug: trigger extended ~12 px into
|
|
3239
|
+
the next row, causing clicks meant for the size input to open this
|
|
3240
|
+
dropdown instead). */}
|
|
3241
|
+
<div className="h-6 overflow-hidden">
|
|
3242
|
+
<Select
|
|
3243
|
+
value={fontFamily}
|
|
3244
|
+
onValueChange={(v) => onStyleChange("fontFamily", v)}
|
|
2544
3245
|
>
|
|
2545
|
-
<
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
{opt.label}
|
|
2565
|
-
</SelectItem>
|
|
2566
|
-
))}
|
|
2567
|
-
</SelectContent>
|
|
2568
|
-
</Select>
|
|
3246
|
+
<SelectTrigger
|
|
3247
|
+
aria-label={t("editPanel.labels.font")}
|
|
3248
|
+
className="h-6 w-full rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-[11px] shadow-none focus:ring-1 focus:ring-[var(--design-editor-accent-color)]"
|
|
3249
|
+
>
|
|
3250
|
+
<SelectValue />
|
|
3251
|
+
</SelectTrigger>
|
|
3252
|
+
<SelectContent>
|
|
3253
|
+
{fontFamilyOptions.map((opt) => (
|
|
3254
|
+
<SelectItem
|
|
3255
|
+
key={opt.value}
|
|
3256
|
+
value={opt.value}
|
|
3257
|
+
className="text-[11px]"
|
|
3258
|
+
>
|
|
3259
|
+
{opt.label}
|
|
3260
|
+
</SelectItem>
|
|
3261
|
+
))}
|
|
3262
|
+
</SelectContent>
|
|
3263
|
+
</Select>
|
|
3264
|
+
</div>
|
|
2569
3265
|
|
|
2570
3266
|
{/* Row 2: weight + size side by side */}
|
|
2571
3267
|
<div className="grid grid-cols-2 gap-1.5">
|
|
@@ -2639,7 +3335,7 @@ function TypographyProperties({
|
|
|
2639
3335
|
</div>
|
|
2640
3336
|
|
|
2641
3337
|
{/* Row 4: horizontal + vertical text alignment */}
|
|
2642
|
-
<div className="
|
|
3338
|
+
<div className="flex items-center gap-1.5">
|
|
2643
3339
|
<InspectorSegment>
|
|
2644
3340
|
<InspectorIconButton
|
|
2645
3341
|
label={t("editPanel.textAligns.left")}
|
|
@@ -2693,6 +3389,12 @@ function TypographyProperties({
|
|
|
2693
3389
|
<IconLayoutAlignBottom className="size-3.5" />
|
|
2694
3390
|
</InspectorIconButton>
|
|
2695
3391
|
</InspectorSegment>
|
|
3392
|
+
<div className="ml-auto shrink-0">
|
|
3393
|
+
<TypographyDetailsPopover
|
|
3394
|
+
resizeMode={resizeMode}
|
|
3395
|
+
onResizeModeChange={setResizeMode}
|
|
3396
|
+
/>
|
|
3397
|
+
</div>
|
|
2696
3398
|
</div>
|
|
2697
3399
|
</PanelSection>
|
|
2698
3400
|
);
|
|
@@ -2703,10 +3405,15 @@ function FlexContainerControls({
|
|
|
2703
3405
|
element,
|
|
2704
3406
|
onStyleChange,
|
|
2705
3407
|
onStylesChange,
|
|
3408
|
+
onAutoLayoutConvert,
|
|
2706
3409
|
}: {
|
|
2707
3410
|
element: ElementInfo;
|
|
2708
3411
|
onStyleChange: (property: string, value: string) => void;
|
|
2709
3412
|
onStylesChange?: (styles: Record<string, string>) => void;
|
|
3413
|
+
onAutoLayoutConvert?: (
|
|
3414
|
+
targetNodeId: string,
|
|
3415
|
+
opts?: { direction?: "row" | "column"; gap?: string },
|
|
3416
|
+
) => void;
|
|
2710
3417
|
}) {
|
|
2711
3418
|
const t = useT();
|
|
2712
3419
|
const styles = element.computedStyles;
|
|
@@ -2719,12 +3426,55 @@ function FlexContainerControls({
|
|
|
2719
3426
|
const isFlex = display.includes("flex");
|
|
2720
3427
|
const flexDirection: AutoLayoutMatrixValue["direction"] =
|
|
2721
3428
|
styles.flexDirection?.includes("column") ? "vertical" : "horizontal";
|
|
3429
|
+
const mainGapAxis =
|
|
3430
|
+
flexDirection === "horizontal" ? "horizontal" : "vertical";
|
|
2722
3431
|
// When the element is in normal flow (not flex yet), picking any flow option
|
|
2723
3432
|
// must first turn it into a flex container; otherwise setting flex-direction
|
|
2724
3433
|
// alone is a no-op against a block element.
|
|
2725
3434
|
const ensureFlex = () => {
|
|
2726
3435
|
if (!isFlex) onStyleChange("display", "flex");
|
|
2727
3436
|
};
|
|
3437
|
+
|
|
3438
|
+
/**
|
|
3439
|
+
* Handle the Flow control switching between flex and normal-flow (block).
|
|
3440
|
+
*
|
|
3441
|
+
* For 'flex': ensures display:flex is set (ensureFlex path).
|
|
3442
|
+
* For 'block': sets display:block and leaves children unchanged — mirrors
|
|
3443
|
+
* the { kind:"autoLayout", enabled:false } substrate intent exactly.
|
|
3444
|
+
*/
|
|
3445
|
+
const handleDisplayChange = (nextDisplay: "flex" | "block") => {
|
|
3446
|
+
if (nextDisplay === "flex") {
|
|
3447
|
+
ensureFlex();
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
// Turn auto-layout off: set display:block, leaving children unchanged.
|
|
3451
|
+
// This is the direct equivalent of the autoLayout substrate with enabled:false.
|
|
3452
|
+
onStyleChange("display", "block");
|
|
3453
|
+
};
|
|
3454
|
+
|
|
3455
|
+
/**
|
|
3456
|
+
* "Convert to auto layout" — enables flex on a non-flex container and strips
|
|
3457
|
+
* position:absolute from direct children (full reflow via the autoLayout
|
|
3458
|
+
* substrate intent). Falls back to a style-only patch when no substrate
|
|
3459
|
+
* handler is wired (e.g. read-only or non-editable contexts).
|
|
3460
|
+
*/
|
|
3461
|
+
const handleConvertToAutoLayout = () => {
|
|
3462
|
+
if (onAutoLayoutConvert && element.sourceId) {
|
|
3463
|
+
onAutoLayoutConvert(element.sourceId, { direction: "row", gap: "8px" });
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3466
|
+
// Fallback: set display:flex on the parent only (no child strip).
|
|
3467
|
+
commitStylePatch(
|
|
3468
|
+
{
|
|
3469
|
+
display: "flex",
|
|
3470
|
+
flexDirection: "row",
|
|
3471
|
+
gap: "8px",
|
|
3472
|
+
alignItems: "flex-start",
|
|
3473
|
+
},
|
|
3474
|
+
onStyleChange,
|
|
3475
|
+
onStylesChange,
|
|
3476
|
+
);
|
|
3477
|
+
};
|
|
2728
3478
|
const padding = {
|
|
2729
3479
|
top: parseNumericValue(styles.paddingTop || "0"),
|
|
2730
3480
|
right: parseNumericValue(styles.paddingRight || "0"),
|
|
@@ -2758,21 +3508,36 @@ function FlexContainerControls({
|
|
|
2758
3508
|
},
|
|
2759
3509
|
clipContent: styles.overflow === "hidden",
|
|
2760
3510
|
resolvedSize: {
|
|
2761
|
-
horizontal: element
|
|
2762
|
-
vertical: element
|
|
3511
|
+
horizontal: cssElementSize(element, "horizontal"),
|
|
3512
|
+
vertical: cssElementSize(element, "vertical"),
|
|
2763
3513
|
},
|
|
2764
3514
|
// Forward the raw CSS display so the matrix can render the correct Flow
|
|
2765
3515
|
// state (normal flow for block/grid, flex for flex/inline-flex). Added as an
|
|
2766
3516
|
// optional contract field consumed by AutoLayoutMatrix; harmless when the
|
|
2767
3517
|
// matrix ignores it.
|
|
2768
3518
|
...({ display } as Partial<AutoLayoutMatrixValue>),
|
|
3519
|
+
spaceBetween: styles.justifyContent === "space-between",
|
|
2769
3520
|
};
|
|
2770
3521
|
|
|
2771
3522
|
return (
|
|
2772
3523
|
<div className="space-y-2">
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
3524
|
+
{/* Convert to auto layout — shown when the container is not yet flex */}
|
|
3525
|
+
{!isFlex ? (
|
|
3526
|
+
<Button
|
|
3527
|
+
type="button"
|
|
3528
|
+
variant="outline"
|
|
3529
|
+
size="sm"
|
|
3530
|
+
className="h-7 w-full gap-1.5 text-[11px]"
|
|
3531
|
+
onClick={handleConvertToAutoLayout}
|
|
3532
|
+
>
|
|
3533
|
+
<IconLayoutSettings className="size-3.5 shrink-0" />
|
|
3534
|
+
{"Convert to auto layout" /* i18n-ignore design inspector action */}
|
|
3535
|
+
</Button>
|
|
3536
|
+
) : null}
|
|
3537
|
+
<AutoLayoutMatrix
|
|
3538
|
+
value={autoLayoutValue}
|
|
3539
|
+
onDisplayChange={handleDisplayChange}
|
|
3540
|
+
onDirectionChange={(direction) => {
|
|
2776
3541
|
ensureFlex();
|
|
2777
3542
|
onStyleChange(
|
|
2778
3543
|
"flexDirection",
|
|
@@ -2823,16 +3588,20 @@ function FlexContainerControls({
|
|
|
2823
3588
|
onStyleChange("overflow", clipContent ? "hidden" : "visible")
|
|
2824
3589
|
}
|
|
2825
3590
|
onDistribute={(axis) => {
|
|
2826
|
-
|
|
2827
|
-
autoLayoutValue.direction === "horizontal"
|
|
2828
|
-
? "horizontal"
|
|
2829
|
-
: "vertical";
|
|
2830
|
-
if (axis === mainAxis) {
|
|
3591
|
+
if (axis === mainGapAxis) {
|
|
2831
3592
|
onStyleChange("justifyContent", "space-between");
|
|
2832
3593
|
} else if (autoLayoutValue.wrap === "wrap") {
|
|
2833
3594
|
onStyleChange("alignContent", "space-between");
|
|
2834
3595
|
}
|
|
2835
3596
|
}}
|
|
3597
|
+
onGapModeChange={(gapMode, axis) => {
|
|
3598
|
+
if (axis !== mainGapAxis) return;
|
|
3599
|
+
ensureFlex();
|
|
3600
|
+
onStyleChange(
|
|
3601
|
+
"justifyContent",
|
|
3602
|
+
gapMode === "auto" ? "space-between" : "flex-start",
|
|
3603
|
+
);
|
|
3604
|
+
}}
|
|
2836
3605
|
availableChildSizing={availableSizingForElement(element)}
|
|
2837
3606
|
onChildSizingChange={(axis, sizing) => {
|
|
2838
3607
|
commitElementSizing(
|
|
@@ -2843,6 +3612,9 @@ function FlexContainerControls({
|
|
|
2843
3612
|
onStylesChange,
|
|
2844
3613
|
);
|
|
2845
3614
|
}}
|
|
3615
|
+
onChildSizeChange={(axis, px) =>
|
|
3616
|
+
onStyleChange(axis === "horizontal" ? "width" : "height", `${px}px`)
|
|
3617
|
+
}
|
|
2846
3618
|
onChildMinMaxChange={(axis, kind, val) =>
|
|
2847
3619
|
commitElementMinMax(axis, kind, val, onStyleChange)
|
|
2848
3620
|
}
|
|
@@ -2948,10 +3720,15 @@ function LayoutContextProperties({
|
|
|
2948
3720
|
element,
|
|
2949
3721
|
onStyleChange,
|
|
2950
3722
|
onStylesChange,
|
|
3723
|
+
onAutoLayoutConvert,
|
|
2951
3724
|
}: {
|
|
2952
3725
|
element: ElementInfo;
|
|
2953
3726
|
onStyleChange: (property: string, value: string) => void;
|
|
2954
3727
|
onStylesChange?: (styles: Record<string, string>) => void;
|
|
3728
|
+
onAutoLayoutConvert?: (
|
|
3729
|
+
targetNodeId: string,
|
|
3730
|
+
opts?: { direction?: "row" | "column"; gap?: string },
|
|
3731
|
+
) => void;
|
|
2955
3732
|
}) {
|
|
2956
3733
|
const t = useT();
|
|
2957
3734
|
const flexChild = isParentFlex(element);
|
|
@@ -2986,7 +3763,7 @@ function LayoutContextProperties({
|
|
|
2986
3763
|
axis="W"
|
|
2987
3764
|
sizingAxis="horizontal"
|
|
2988
3765
|
value={inferElementSizing(element, "horizontal")}
|
|
2989
|
-
resolvedSize={element
|
|
3766
|
+
resolvedSize={cssElementSize(element, "horizontal")}
|
|
2990
3767
|
minMax={readElementMinMax(element, "horizontal")}
|
|
2991
3768
|
options={availableSizing.horizontal ?? ["fixed"]}
|
|
2992
3769
|
disabled={false}
|
|
@@ -2999,6 +3776,7 @@ function LayoutContextProperties({
|
|
|
2999
3776
|
onStylesChange,
|
|
3000
3777
|
)
|
|
3001
3778
|
}
|
|
3779
|
+
onSizeChange={(px) => onStyleChange("width", `${px}px`)}
|
|
3002
3780
|
onMinMaxChange={(axis, kind, val) =>
|
|
3003
3781
|
commitElementMinMax(axis, kind, val, onStyleChange)
|
|
3004
3782
|
}
|
|
@@ -3007,7 +3785,7 @@ function LayoutContextProperties({
|
|
|
3007
3785
|
axis="H"
|
|
3008
3786
|
sizingAxis="vertical"
|
|
3009
3787
|
value={inferElementSizing(element, "vertical")}
|
|
3010
|
-
resolvedSize={element
|
|
3788
|
+
resolvedSize={cssElementSize(element, "vertical")}
|
|
3011
3789
|
minMax={readElementMinMax(element, "vertical")}
|
|
3012
3790
|
options={availableSizing.vertical ?? ["fixed"]}
|
|
3013
3791
|
disabled={false}
|
|
@@ -3020,6 +3798,7 @@ function LayoutContextProperties({
|
|
|
3020
3798
|
onStylesChange,
|
|
3021
3799
|
)
|
|
3022
3800
|
}
|
|
3801
|
+
onSizeChange={(px) => onStyleChange("height", `${px}px`)}
|
|
3023
3802
|
onMinMaxChange={(axis, kind, val) =>
|
|
3024
3803
|
commitElementMinMax(axis, kind, val, onStyleChange)
|
|
3025
3804
|
}
|
|
@@ -3042,6 +3821,7 @@ function LayoutContextProperties({
|
|
|
3042
3821
|
element={element}
|
|
3043
3822
|
onStyleChange={onStyleChange}
|
|
3044
3823
|
onStylesChange={onStylesChange}
|
|
3824
|
+
onAutoLayoutConvert={onAutoLayoutConvert}
|
|
3045
3825
|
/>
|
|
3046
3826
|
{childControls}
|
|
3047
3827
|
</PanelSection>
|
|
@@ -3372,6 +4152,8 @@ function PositionLayoutProperties({
|
|
|
3372
4152
|
<div className="grid grid-cols-2 gap-2">
|
|
3373
4153
|
<ScrubStyleInput
|
|
3374
4154
|
label="X"
|
|
4155
|
+
ariaLabel="X-position"
|
|
4156
|
+
tooltipLabel="X-position"
|
|
3375
4157
|
value={styles.left || ""}
|
|
3376
4158
|
placeholder={element.boundingRect.x}
|
|
3377
4159
|
inputClassName="h-6"
|
|
@@ -3379,6 +4161,8 @@ function PositionLayoutProperties({
|
|
|
3379
4161
|
/>
|
|
3380
4162
|
<ScrubStyleInput
|
|
3381
4163
|
label="Y"
|
|
4164
|
+
ariaLabel="Y-position"
|
|
4165
|
+
tooltipLabel="Y-position"
|
|
3382
4166
|
value={styles.top || ""}
|
|
3383
4167
|
placeholder={element.boundingRect.y}
|
|
3384
4168
|
inputClassName="h-6"
|
|
@@ -3474,7 +4258,7 @@ function FillProperties({
|
|
|
3474
4258
|
Record<string, string>
|
|
3475
4259
|
>({});
|
|
3476
4260
|
const stashKey = `${elementIdentityKey(element)}:${fillProperty}`;
|
|
3477
|
-
const isHidden = fillValue
|
|
4261
|
+
const isHidden = !colorHasVisibleAlpha(fillValue);
|
|
3478
4262
|
const handleFillVisibilityToggle = () => {
|
|
3479
4263
|
if (isHidden) {
|
|
3480
4264
|
// Restore the stashed color, or fall back to a sensible default.
|
|
@@ -3629,8 +4413,17 @@ function FillProperties({
|
|
|
3629
4413
|
const opacity = gradient
|
|
3630
4414
|
? averageGradientOpacity(gradient.stops)
|
|
3631
4415
|
: 100;
|
|
4416
|
+
const layerStashKey = `${elementIdentityKey(element)}:layer:${index}`;
|
|
4417
|
+
const stashedLayer = hiddenLayerStash[layerStashKey];
|
|
4418
|
+
const hiddenImagePlaceholder = Boolean(
|
|
4419
|
+
stashedLayer && gradient && opacity <= 0,
|
|
4420
|
+
);
|
|
3632
4421
|
const label = gradient
|
|
3633
|
-
?
|
|
4422
|
+
? hiddenImagePlaceholder
|
|
4423
|
+
? `${"Image" /* i18n-ignore design inspector paint row */} ${
|
|
4424
|
+
index + 1
|
|
4425
|
+
}`
|
|
4426
|
+
: `${gradientLabel(gradient.type)} ${index + 1}`
|
|
3634
4427
|
: `${"Image" /* i18n-ignore design inspector paint row */} ${
|
|
3635
4428
|
index + 1
|
|
3636
4429
|
}`;
|
|
@@ -3728,10 +4521,6 @@ function FillProperties({
|
|
|
3728
4521
|
: t("editPanel.labels.hideLayer")
|
|
3729
4522
|
}
|
|
3730
4523
|
onClick={() => {
|
|
3731
|
-
if (!gradient) return;
|
|
3732
|
-
const layerStashKey = `${elementIdentityKey(
|
|
3733
|
-
element,
|
|
3734
|
-
)}:layer:${index}`;
|
|
3735
4524
|
if (opacity <= 0) {
|
|
3736
4525
|
// Show: restore the exact pre-hide layer if stashed,
|
|
3737
4526
|
// otherwise fall back to forcing every stop opaque.
|
|
@@ -3743,7 +4532,7 @@ function FillProperties({
|
|
|
3743
4532
|
delete next[layerStashKey];
|
|
3744
4533
|
return next;
|
|
3745
4534
|
});
|
|
3746
|
-
} else {
|
|
4535
|
+
} else if (gradient) {
|
|
3747
4536
|
replaceLayer(
|
|
3748
4537
|
buildGradientLayer(
|
|
3749
4538
|
gradient.type,
|
|
@@ -3755,6 +4544,29 @@ function FillProperties({
|
|
|
3755
4544
|
),
|
|
3756
4545
|
);
|
|
3757
4546
|
}
|
|
4547
|
+
return;
|
|
4548
|
+
}
|
|
4549
|
+
if (!gradient) {
|
|
4550
|
+
setHiddenLayerStash((prev) => ({
|
|
4551
|
+
...prev,
|
|
4552
|
+
[layerStashKey]: layer,
|
|
4553
|
+
}));
|
|
4554
|
+
replaceLayer(
|
|
4555
|
+
buildGradientLayer("linear", [
|
|
4556
|
+
{
|
|
4557
|
+
id: "stop-0",
|
|
4558
|
+
color: "rgba(0, 0, 0, 0)",
|
|
4559
|
+
position: 0,
|
|
4560
|
+
opacity: 0,
|
|
4561
|
+
},
|
|
4562
|
+
{
|
|
4563
|
+
id: "stop-1",
|
|
4564
|
+
color: "rgba(0, 0, 0, 0)",
|
|
4565
|
+
position: 100,
|
|
4566
|
+
opacity: 0,
|
|
4567
|
+
},
|
|
4568
|
+
]),
|
|
4569
|
+
);
|
|
3758
4570
|
} else {
|
|
3759
4571
|
// Hide: stash the current layer (with its real per-stop
|
|
3760
4572
|
// opacities) before zeroing every stop's alpha.
|
|
@@ -4357,6 +5169,580 @@ const TEXT_TAGS = new Set([
|
|
|
4357
5169
|
"li",
|
|
4358
5170
|
]);
|
|
4359
5171
|
|
|
5172
|
+
// ─── Make it real — inline upgrade card (§3, §6.6) ──────────────────────────
|
|
5173
|
+
|
|
5174
|
+
/**
|
|
5175
|
+
* Payload shape returned by `connect-builder-app`. Only the fields used by
|
|
5176
|
+
* the card UI are typed here; the action may return additional fields.
|
|
5177
|
+
*/
|
|
5178
|
+
interface ConnectBuilderAppResult {
|
|
5179
|
+
connected: boolean;
|
|
5180
|
+
builderEnabled: boolean;
|
|
5181
|
+
connectUrl: string;
|
|
5182
|
+
appHost: string;
|
|
5183
|
+
branchProjectId?: string;
|
|
5184
|
+
cta: {
|
|
5185
|
+
kind: "connect-builder" | "configure-project";
|
|
5186
|
+
label: string;
|
|
5187
|
+
description: string;
|
|
5188
|
+
primaryAction: string;
|
|
5189
|
+
connectUrl: string;
|
|
5190
|
+
} | null;
|
|
5191
|
+
message: string;
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
/**
|
|
5195
|
+
* Inline "Make it real" upgrade card.
|
|
5196
|
+
*
|
|
5197
|
+
* Rendered wherever a real-app-only control is reached on an inline design
|
|
5198
|
+
* (Component source jump, token write-back, live captures, etc.). Queries
|
|
5199
|
+
* `connect-builder-app` to determine the current connection state, then
|
|
5200
|
+
* offers the appropriate CTA:
|
|
5201
|
+
*
|
|
5202
|
+
* - Not connected → "Connect Builder.io" button (opens connectUrl)
|
|
5203
|
+
* - Connected, no project → "Open Builder settings" (configure project ID)
|
|
5204
|
+
* - Fully enabled → "Make it real" button (calls migrate-inline-design-to-app)
|
|
5205
|
+
*
|
|
5206
|
+
* The card is progressively disclosed: it only mounts when a gated control is
|
|
5207
|
+
* actually reached, so it never appears for users who are already on a real-app
|
|
5208
|
+
* source (`localhost` / `fusion`) or whose `sourceCapabilities` already include
|
|
5209
|
+
* the needed capability.
|
|
5210
|
+
*
|
|
5211
|
+
* Matches the design-editor panel chrome: dashed-border, accent tint, small
|
|
5212
|
+
* text at 10px — same idiom as the existing `ctaRequired` block in
|
|
5213
|
+
* ComponentSection.
|
|
5214
|
+
*/
|
|
5215
|
+
function MakeItRealCard({
|
|
5216
|
+
designId,
|
|
5217
|
+
featureLabel,
|
|
5218
|
+
}: {
|
|
5219
|
+
/** The active design id — required to call connect-builder-app. */
|
|
5220
|
+
designId: string;
|
|
5221
|
+
/**
|
|
5222
|
+
* Short human-readable label for the gated feature (e.g. "token write-back",
|
|
5223
|
+
* "component source jump", "live captures"). Shown in the card body so the
|
|
5224
|
+
* user understands exactly what they're unlocking.
|
|
5225
|
+
*/
|
|
5226
|
+
featureLabel: string;
|
|
5227
|
+
}) {
|
|
5228
|
+
const { data, isLoading } = useActionQuery<ConnectBuilderAppResult>(
|
|
5229
|
+
"connect-builder-app",
|
|
5230
|
+
{ designId },
|
|
5231
|
+
);
|
|
5232
|
+
|
|
5233
|
+
const migrateMutation = useActionMutation("migrate-inline-design-to-app");
|
|
5234
|
+
|
|
5235
|
+
// While fetching status, show a muted placeholder that matches the card
|
|
5236
|
+
// height so the inspector doesn't jump when the data arrives.
|
|
5237
|
+
if (isLoading || !data) {
|
|
5238
|
+
return (
|
|
5239
|
+
<div className="rounded-md border border-dashed border-[var(--design-editor-accent-color)]/20 bg-[var(--design-editor-accent-color)]/3 px-2.5 py-2">
|
|
5240
|
+
<div className="h-3 w-32 animate-pulse rounded bg-muted/40" />
|
|
5241
|
+
</div>
|
|
5242
|
+
);
|
|
5243
|
+
}
|
|
5244
|
+
|
|
5245
|
+
// Determine which CTA to show.
|
|
5246
|
+
const cta = data.cta;
|
|
5247
|
+
|
|
5248
|
+
// Already fully enabled — no CTA needed (caller should already have gated
|
|
5249
|
+
// this component away, but guard here for safety).
|
|
5250
|
+
if (!cta) return null;
|
|
5251
|
+
|
|
5252
|
+
const isPending = migrateMutation.isPending;
|
|
5253
|
+
const migrateError = migrateMutation.error;
|
|
5254
|
+
|
|
5255
|
+
// "Make it real" primary action: open the connect URL or migrate.
|
|
5256
|
+
const handlePrimary = () => {
|
|
5257
|
+
if (cta.kind === "connect-builder") {
|
|
5258
|
+
// Open the Builder OAuth connect flow in a new tab. The user completes
|
|
5259
|
+
// it there and comes back; the card will re-query on next render.
|
|
5260
|
+
window.open(cta.connectUrl, "_blank", "noopener,noreferrer");
|
|
5261
|
+
return;
|
|
5262
|
+
}
|
|
5263
|
+
if (cta.kind === "configure-project") {
|
|
5264
|
+
window.open(cta.connectUrl, "_blank", "noopener,noreferrer");
|
|
5265
|
+
return;
|
|
5266
|
+
}
|
|
5267
|
+
};
|
|
5268
|
+
|
|
5269
|
+
const handleMigrate = () => {
|
|
5270
|
+
migrateMutation.mutate({ designId });
|
|
5271
|
+
};
|
|
5272
|
+
|
|
5273
|
+
// Migration result — show branch link.
|
|
5274
|
+
const migrateResult = migrateMutation.data as
|
|
5275
|
+
| {
|
|
5276
|
+
status: "processing";
|
|
5277
|
+
branchName?: string;
|
|
5278
|
+
url?: string;
|
|
5279
|
+
message?: string;
|
|
5280
|
+
}
|
|
5281
|
+
| undefined;
|
|
5282
|
+
|
|
5283
|
+
if (migrateResult?.status === "processing" && migrateResult.url) {
|
|
5284
|
+
return (
|
|
5285
|
+
<div className="rounded-md border border-[var(--design-editor-accent-color)]/30 bg-[var(--design-editor-accent-color)]/8 px-2.5 py-2 space-y-1.5">
|
|
5286
|
+
<p className="text-[10px] font-semibold text-[var(--design-editor-accent-color)]">
|
|
5287
|
+
{"Builder is working on it" /* i18n-ignore make-it-real card */}
|
|
5288
|
+
</p>
|
|
5289
|
+
<p className="text-[10px] leading-snug text-muted-foreground">
|
|
5290
|
+
{migrateResult.message ??
|
|
5291
|
+
`Branch "${migrateResult.branchName}" is being generated.`}
|
|
5292
|
+
</p>
|
|
5293
|
+
<a
|
|
5294
|
+
href={migrateResult.url}
|
|
5295
|
+
target="_blank"
|
|
5296
|
+
rel="noopener noreferrer"
|
|
5297
|
+
className="inline-flex items-center gap-1 text-[10px] font-medium text-[var(--design-editor-accent-color)] hover:underline"
|
|
5298
|
+
>
|
|
5299
|
+
{"Open in Builder" /* i18n-ignore make-it-real card */}
|
|
5300
|
+
<IconExternalLink className="size-2.5" />
|
|
5301
|
+
</a>
|
|
5302
|
+
</div>
|
|
5303
|
+
);
|
|
5304
|
+
}
|
|
5305
|
+
|
|
5306
|
+
return (
|
|
5307
|
+
<div className="rounded-md border border-dashed border-[var(--design-editor-accent-color)]/30 bg-[var(--design-editor-accent-color)]/5 px-2.5 py-2 space-y-1.5">
|
|
5308
|
+
{/* Header */}
|
|
5309
|
+
<p className="text-[10px] font-semibold text-foreground">
|
|
5310
|
+
{"Make this a real app" /* i18n-ignore make-it-real card */}
|
|
5311
|
+
</p>
|
|
5312
|
+
|
|
5313
|
+
{/* Body */}
|
|
5314
|
+
<p className="text-[10px] leading-snug text-muted-foreground">
|
|
5315
|
+
{cta.description}
|
|
5316
|
+
{featureLabel ? (
|
|
5317
|
+
<>
|
|
5318
|
+
{" "}
|
|
5319
|
+
<span className="italic">{featureLabel}</span>
|
|
5320
|
+
{" requires a real-app source."}
|
|
5321
|
+
</>
|
|
5322
|
+
) : null}
|
|
5323
|
+
</p>
|
|
5324
|
+
|
|
5325
|
+
{/* Error */}
|
|
5326
|
+
{migrateError ? (
|
|
5327
|
+
<p className="text-[10px] text-destructive">
|
|
5328
|
+
{migrateError instanceof Error
|
|
5329
|
+
? migrateError.message
|
|
5330
|
+
: "Migration failed. Please try again."}
|
|
5331
|
+
</p>
|
|
5332
|
+
) : null}
|
|
5333
|
+
|
|
5334
|
+
{/* Actions */}
|
|
5335
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
5336
|
+
<button
|
|
5337
|
+
type="button"
|
|
5338
|
+
onClick={handlePrimary}
|
|
5339
|
+
className="inline-flex items-center gap-1 rounded px-2 py-1 text-[10px] font-medium bg-[var(--design-editor-accent-color)] text-white hover:opacity-90 transition-opacity"
|
|
5340
|
+
>
|
|
5341
|
+
{cta.primaryAction}
|
|
5342
|
+
<IconArrowRight className="size-2.5" />
|
|
5343
|
+
</button>
|
|
5344
|
+
|
|
5345
|
+
{/* When Builder is fully connected, also offer direct migration */}
|
|
5346
|
+
{data.connected && data.builderEnabled && (
|
|
5347
|
+
<button
|
|
5348
|
+
type="button"
|
|
5349
|
+
onClick={handleMigrate}
|
|
5350
|
+
disabled={isPending}
|
|
5351
|
+
className="inline-flex items-center gap-1 rounded border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 py-1 text-[10px] font-medium text-muted-foreground hover:text-foreground transition-colors disabled:cursor-wait disabled:opacity-60"
|
|
5352
|
+
>
|
|
5353
|
+
{isPending ? (
|
|
5354
|
+
<>
|
|
5355
|
+
<IconLoader2 className="size-2.5 animate-spin" />
|
|
5356
|
+
{"Migrating…" /* i18n-ignore make-it-real card */}
|
|
5357
|
+
</>
|
|
5358
|
+
) : (
|
|
5359
|
+
<>{"Generate React app" /* i18n-ignore make-it-real card */}</>
|
|
5360
|
+
)}
|
|
5361
|
+
</button>
|
|
5362
|
+
)}
|
|
5363
|
+
</div>
|
|
5364
|
+
</div>
|
|
5365
|
+
);
|
|
5366
|
+
}
|
|
5367
|
+
|
|
5368
|
+
// ─── Component section (§6.1) ─────────────────────────────────────────────────
|
|
5369
|
+
|
|
5370
|
+
/**
|
|
5371
|
+
* Shape returned by `get-component-details`. Only the fields the UI needs are
|
|
5372
|
+
* typed here; the action may return additional fields.
|
|
5373
|
+
*/
|
|
5374
|
+
interface ComponentDetailsResult {
|
|
5375
|
+
name: string;
|
|
5376
|
+
sourceType: string;
|
|
5377
|
+
observedProps: Array<{ name: string; value: string }>;
|
|
5378
|
+
persistedVariants: Record<string, string[]>;
|
|
5379
|
+
sourceLocation?: { filePath: string; exportName?: string } | null;
|
|
5380
|
+
/** Component instance shape, including the Alpine `x-data` expression. */
|
|
5381
|
+
instance?: {
|
|
5382
|
+
alpineData?: string | null;
|
|
5383
|
+
nodeId?: string;
|
|
5384
|
+
selector?: string;
|
|
5385
|
+
} | null;
|
|
5386
|
+
capabilities: {
|
|
5387
|
+
canResolveToFile: boolean;
|
|
5388
|
+
hasFullIndex: boolean;
|
|
5389
|
+
canEditProps: boolean;
|
|
5390
|
+
ctaRequired: boolean;
|
|
5391
|
+
ctaMessage?: string;
|
|
5392
|
+
};
|
|
5393
|
+
}
|
|
5394
|
+
|
|
5395
|
+
/**
|
|
5396
|
+
* Contextual COMPONENT section rendered inside the Design tab when the
|
|
5397
|
+
* selected element is a component instance (carries
|
|
5398
|
+
* `data-agent-native-component`).
|
|
5399
|
+
*
|
|
5400
|
+
* Shows: component name, source path (when capability available), observed
|
|
5401
|
+
* prop values, variant/size/state controls from `get-component-details`, and
|
|
5402
|
+
* an "Edit component source" action. Real-app features are gated by the
|
|
5403
|
+
* capabilities returned by the action; Alpine gets a lightweight read-only
|
|
5404
|
+
* view plus a Connect-Builder CTA.
|
|
5405
|
+
*
|
|
5406
|
+
* Matches the workbench artboard spec in DESIGN-STUDIO-PLAN.md §6.1.
|
|
5407
|
+
*/
|
|
5408
|
+
function ComponentSection({
|
|
5409
|
+
designId,
|
|
5410
|
+
nodeId,
|
|
5411
|
+
sourceCapabilities = [],
|
|
5412
|
+
}: {
|
|
5413
|
+
designId: string;
|
|
5414
|
+
nodeId: string;
|
|
5415
|
+
/** Capability names advertised by the current source. */
|
|
5416
|
+
sourceCapabilities?: string[];
|
|
5417
|
+
}) {
|
|
5418
|
+
const queryClient = useQueryClient();
|
|
5419
|
+
const detailsKey = ["action", "get-component-details", { designId, nodeId }];
|
|
5420
|
+
|
|
5421
|
+
const { data, isLoading, error } = useActionQuery<ComponentDetailsResult>(
|
|
5422
|
+
"get-component-details",
|
|
5423
|
+
{ designId, nodeId },
|
|
5424
|
+
);
|
|
5425
|
+
|
|
5426
|
+
const openSourceMutation = useActionMutation("open-component-source");
|
|
5427
|
+
const applyPropMutation = useActionMutation("apply-component-prop-edit");
|
|
5428
|
+
|
|
5429
|
+
// Persist a single prop change through apply-component-prop-edit. The canvas
|
|
5430
|
+
// re-renders from the freshly written design content (get-design refetch),
|
|
5431
|
+
// so no postMessage into the iframe is needed. The get-component-details
|
|
5432
|
+
// cache is optimistically patched so the control reflects the new value
|
|
5433
|
+
// immediately, then reconciled with the server on settle.
|
|
5434
|
+
const persistPropEdit = (
|
|
5435
|
+
edit:
|
|
5436
|
+
| { kind: "alpineData"; value: string }
|
|
5437
|
+
| { kind: "attribute"; attribute: string; value: string },
|
|
5438
|
+
optimistic: (prev: ComponentDetailsResult) => ComponentDetailsResult,
|
|
5439
|
+
) => {
|
|
5440
|
+
queryClient.setQueryData<ComponentDetailsResult>(detailsKey, (prev) =>
|
|
5441
|
+
prev ? optimistic(prev) : prev,
|
|
5442
|
+
);
|
|
5443
|
+
applyPropMutation.mutate(
|
|
5444
|
+
{ designId, nodeId, edit },
|
|
5445
|
+
{
|
|
5446
|
+
onSettled: () => {
|
|
5447
|
+
// Re-render the canvas from the new content and re-read the props.
|
|
5448
|
+
void queryClient.invalidateQueries({
|
|
5449
|
+
queryKey: ["action", "get-design"],
|
|
5450
|
+
});
|
|
5451
|
+
void queryClient.invalidateQueries({ queryKey: detailsKey });
|
|
5452
|
+
},
|
|
5453
|
+
},
|
|
5454
|
+
);
|
|
5455
|
+
};
|
|
5456
|
+
|
|
5457
|
+
// While loading, show a compact skeleton that matches the section width.
|
|
5458
|
+
if (isLoading) {
|
|
5459
|
+
return (
|
|
5460
|
+
<section className="shrink-0 border-t border-[var(--design-editor-control-border)] first:border-t-0">
|
|
5461
|
+
<div className="flex min-h-9 items-center gap-2 px-3">
|
|
5462
|
+
<div className="h-3 w-24 animate-pulse rounded bg-muted/50" />
|
|
5463
|
+
</div>
|
|
5464
|
+
<div className="space-y-1.5 px-3 pb-3 pt-0.5">
|
|
5465
|
+
<div className="h-5 w-full animate-pulse rounded bg-muted/40" />
|
|
5466
|
+
<div className="h-5 w-3/4 animate-pulse rounded bg-muted/40" />
|
|
5467
|
+
</div>
|
|
5468
|
+
</section>
|
|
5469
|
+
);
|
|
5470
|
+
}
|
|
5471
|
+
|
|
5472
|
+
// Hard error (node not found, no access, etc.) — collapse silently so
|
|
5473
|
+
// the rest of the inspector is not disrupted.
|
|
5474
|
+
if (error || !data) return null;
|
|
5475
|
+
|
|
5476
|
+
const {
|
|
5477
|
+
name,
|
|
5478
|
+
sourceType,
|
|
5479
|
+
sourceLocation,
|
|
5480
|
+
observedProps,
|
|
5481
|
+
persistedVariants,
|
|
5482
|
+
instance,
|
|
5483
|
+
capabilities,
|
|
5484
|
+
} = data;
|
|
5485
|
+
|
|
5486
|
+
// ── Editable prop model ───────────────────────────────────────────────────
|
|
5487
|
+
// Inline/Alpine designs persist through apply-component-prop-edit. Two write
|
|
5488
|
+
// surfaces:
|
|
5489
|
+
// • x-data keys → kind "alpineData" (rewrites the whole object)
|
|
5490
|
+
// • data-prop-* attrs → kind "attribute" (data-agent-native-prop-<kebab>)
|
|
5491
|
+
// Real-app sources keep the deeper source-prop controls gated as-is, so for
|
|
5492
|
+
// non-inline sources the controls are read-only here.
|
|
5493
|
+
const isInline = sourceType === "inline";
|
|
5494
|
+
const editingEnabled = isInline; // gated; real-app stays read-only for now
|
|
5495
|
+
const alpineData = parseAlpineDataObject(instance?.alpineData);
|
|
5496
|
+
|
|
5497
|
+
// Each editable row: name + current value + how it persists + its options.
|
|
5498
|
+
type PropRow = {
|
|
5499
|
+
name: string;
|
|
5500
|
+
value: string;
|
|
5501
|
+
/** Variant/enum options when the prop is a known group. */
|
|
5502
|
+
options?: string[];
|
|
5503
|
+
/** Persist surface for this prop. */
|
|
5504
|
+
surface: "alpineData" | "attribute";
|
|
5505
|
+
};
|
|
5506
|
+
|
|
5507
|
+
const rows: PropRow[] = [];
|
|
5508
|
+
const seen = new Set<string>();
|
|
5509
|
+
|
|
5510
|
+
// 1) Alpine x-data keys come first — they drive the live variant/state.
|
|
5511
|
+
if (alpineData) {
|
|
5512
|
+
for (const [key, value] of Object.entries(alpineData)) {
|
|
5513
|
+
rows.push({
|
|
5514
|
+
name: key,
|
|
5515
|
+
value,
|
|
5516
|
+
options: persistedVariants[key],
|
|
5517
|
+
surface: "alpineData",
|
|
5518
|
+
});
|
|
5519
|
+
seen.add(key);
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5523
|
+
// 2) data-agent-native-prop-* attributes not already covered by x-data.
|
|
5524
|
+
for (const prop of observedProps) {
|
|
5525
|
+
if (seen.has(prop.name)) continue;
|
|
5526
|
+
rows.push({
|
|
5527
|
+
name: prop.name,
|
|
5528
|
+
value: prop.value,
|
|
5529
|
+
options: persistedVariants[prop.name],
|
|
5530
|
+
surface: "attribute",
|
|
5531
|
+
});
|
|
5532
|
+
seen.add(prop.name);
|
|
5533
|
+
}
|
|
5534
|
+
|
|
5535
|
+
// 3) persistedVariant groups with no observed value yet (default to first).
|
|
5536
|
+
for (const [group, options] of Object.entries(persistedVariants)) {
|
|
5537
|
+
if (seen.has(group)) continue;
|
|
5538
|
+
rows.push({
|
|
5539
|
+
name: group,
|
|
5540
|
+
value: options[0] ?? "",
|
|
5541
|
+
options,
|
|
5542
|
+
surface: alpineData ? "alpineData" : "attribute",
|
|
5543
|
+
});
|
|
5544
|
+
seen.add(group);
|
|
5545
|
+
}
|
|
5546
|
+
|
|
5547
|
+
const hasRows = rows.length > 0;
|
|
5548
|
+
|
|
5549
|
+
// Build the apply-component-prop-edit payload + optimistic cache patch for a
|
|
5550
|
+
// single prop change.
|
|
5551
|
+
const commitProp = (row: PropRow, nextValue: string) => {
|
|
5552
|
+
if (!editingEnabled || nextValue === row.value) return;
|
|
5553
|
+
|
|
5554
|
+
if (row.surface === "alpineData") {
|
|
5555
|
+
const nextData = { ...(alpineData ?? {}), [row.name]: nextValue };
|
|
5556
|
+
const serialized = serializeAlpineDataObject(nextData);
|
|
5557
|
+
persistPropEdit({ kind: "alpineData", value: serialized }, (prev) => ({
|
|
5558
|
+
...prev,
|
|
5559
|
+
instance: { ...(prev.instance ?? {}), alpineData: serialized },
|
|
5560
|
+
observedProps: prev.observedProps.map((p) =>
|
|
5561
|
+
p.name === row.name ? { ...p, value: nextValue } : p,
|
|
5562
|
+
),
|
|
5563
|
+
}));
|
|
5564
|
+
} else {
|
|
5565
|
+
// camelCase prop name → data-agent-native-prop-<kebab>
|
|
5566
|
+
const kebab = row.name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
5567
|
+
persistPropEdit(
|
|
5568
|
+
{
|
|
5569
|
+
kind: "attribute",
|
|
5570
|
+
attribute: `data-agent-native-prop-${kebab}`,
|
|
5571
|
+
value: nextValue,
|
|
5572
|
+
},
|
|
5573
|
+
(prev) => {
|
|
5574
|
+
const exists = prev.observedProps.some((p) => p.name === row.name);
|
|
5575
|
+
return {
|
|
5576
|
+
...prev,
|
|
5577
|
+
observedProps: exists
|
|
5578
|
+
? prev.observedProps.map((p) =>
|
|
5579
|
+
p.name === row.name ? { ...p, value: nextValue } : p,
|
|
5580
|
+
)
|
|
5581
|
+
: [...prev.observedProps, { name: row.name, value: nextValue }],
|
|
5582
|
+
};
|
|
5583
|
+
},
|
|
5584
|
+
);
|
|
5585
|
+
}
|
|
5586
|
+
};
|
|
5587
|
+
|
|
5588
|
+
// ── Capability gates ──
|
|
5589
|
+
const canJumpToSource =
|
|
5590
|
+
capabilities.canResolveToFile &&
|
|
5591
|
+
Boolean(sourceLocation?.filePath) &&
|
|
5592
|
+
sourceCapabilities.includes("resolveNodeToFile");
|
|
5593
|
+
|
|
5594
|
+
// ── Source chip text ──
|
|
5595
|
+
const sourceChip = sourceLocation?.exportName
|
|
5596
|
+
? `${sourceLocation.exportName} — ${sourceLocation.filePath}`
|
|
5597
|
+
: (sourceLocation?.filePath ?? null);
|
|
5598
|
+
|
|
5599
|
+
return (
|
|
5600
|
+
<section
|
|
5601
|
+
className="shrink-0 border-t border-[var(--design-editor-control-border)] first:border-t-0"
|
|
5602
|
+
data-testid="component-section"
|
|
5603
|
+
>
|
|
5604
|
+
{/* ── Section header ── */}
|
|
5605
|
+
<div className="flex min-h-9 items-center gap-2 px-3">
|
|
5606
|
+
{/* Accent diamond matching the workbench artboard component rows */}
|
|
5607
|
+
<span
|
|
5608
|
+
className="size-2 shrink-0 rotate-45 rounded-[2px] bg-[var(--design-editor-accent-color)]"
|
|
5609
|
+
aria-hidden="true"
|
|
5610
|
+
/>
|
|
5611
|
+
<h3 className="min-w-0 flex-1 truncate text-[11px] font-semibold text-foreground">
|
|
5612
|
+
{name}
|
|
5613
|
+
</h3>
|
|
5614
|
+
{/* Jump-to-source action */}
|
|
5615
|
+
<Tooltip>
|
|
5616
|
+
<TooltipTrigger asChild>
|
|
5617
|
+
<Button
|
|
5618
|
+
type="button"
|
|
5619
|
+
variant="ghost"
|
|
5620
|
+
size="icon"
|
|
5621
|
+
className="size-6 rounded-md text-muted-foreground hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
|
|
5622
|
+
disabled={!canJumpToSource}
|
|
5623
|
+
aria-label={
|
|
5624
|
+
"Edit component source" /* i18n-ignore design inspector action */
|
|
5625
|
+
}
|
|
5626
|
+
onClick={() => {
|
|
5627
|
+
openSourceMutation.mutate({ designId, nodeId });
|
|
5628
|
+
}}
|
|
5629
|
+
>
|
|
5630
|
+
<IconExternalLink className="size-3.5" />
|
|
5631
|
+
</Button>
|
|
5632
|
+
</TooltipTrigger>
|
|
5633
|
+
<TooltipContent>
|
|
5634
|
+
{
|
|
5635
|
+
canJumpToSource
|
|
5636
|
+
? "Edit component source" /* i18n-ignore design inspector action */
|
|
5637
|
+
: (capabilities.ctaMessage ??
|
|
5638
|
+
"Connect Builder to jump to component source") /* i18n-ignore design inspector tooltip */
|
|
5639
|
+
}
|
|
5640
|
+
</TooltipContent>
|
|
5641
|
+
</Tooltip>
|
|
5642
|
+
</div>
|
|
5643
|
+
|
|
5644
|
+
{/* ── Body ── */}
|
|
5645
|
+
<div className="space-y-1.5 px-3 pb-3 pt-0.5 text-[11px]">
|
|
5646
|
+
{/* Source path chip */}
|
|
5647
|
+
{sourceChip && (
|
|
5648
|
+
<div
|
|
5649
|
+
className="flex items-center gap-1 rounded bg-[var(--design-editor-control-bg)] px-2 py-1"
|
|
5650
|
+
title={sourceChip}
|
|
5651
|
+
>
|
|
5652
|
+
<IconCode className="size-3 shrink-0 text-muted-foreground/60" />
|
|
5653
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground">
|
|
5654
|
+
{sourceChip}
|
|
5655
|
+
</span>
|
|
5656
|
+
</div>
|
|
5657
|
+
)}
|
|
5658
|
+
|
|
5659
|
+
{/* Typed prop controls. Inline/Alpine designs are editable and persist
|
|
5660
|
+
through apply-component-prop-edit; real-app sources are read-only
|
|
5661
|
+
until the deeper source-prop controls land. */}
|
|
5662
|
+
{hasRows && (
|
|
5663
|
+
<div className="space-y-1">
|
|
5664
|
+
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">
|
|
5665
|
+
{"Props" /* i18n-ignore design inspector label */}
|
|
5666
|
+
</p>
|
|
5667
|
+
{rows.map((row) => {
|
|
5668
|
+
const hasOptions = (row.options?.length ?? 0) > 0;
|
|
5669
|
+
const isBoolean = !hasOptions && isBooleanPropValue(row.value);
|
|
5670
|
+
return (
|
|
5671
|
+
<div key={row.name} className="flex items-center gap-1.5">
|
|
5672
|
+
<Label className="w-[64px] shrink-0 truncate text-[11px] font-medium capitalize text-muted-foreground">
|
|
5673
|
+
{row.name}
|
|
5674
|
+
</Label>
|
|
5675
|
+
{hasOptions ? (
|
|
5676
|
+
// Dropdown for variant / enum groups.
|
|
5677
|
+
<Select
|
|
5678
|
+
value={row.value || row.options![0] || ""}
|
|
5679
|
+
onValueChange={(v) => commitProp(row, v)}
|
|
5680
|
+
disabled={!editingEnabled}
|
|
5681
|
+
>
|
|
5682
|
+
<SelectTrigger className="h-6 min-w-0 flex-1 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-[11px] shadow-none focus:ring-1 focus:ring-[var(--design-editor-accent-color)]">
|
|
5683
|
+
<SelectValue />
|
|
5684
|
+
</SelectTrigger>
|
|
5685
|
+
<SelectContent>
|
|
5686
|
+
{row.options!.map((opt) => (
|
|
5687
|
+
<SelectItem
|
|
5688
|
+
key={opt}
|
|
5689
|
+
value={opt}
|
|
5690
|
+
className="text-[11px]"
|
|
5691
|
+
>
|
|
5692
|
+
{opt}
|
|
5693
|
+
</SelectItem>
|
|
5694
|
+
))}
|
|
5695
|
+
</SelectContent>
|
|
5696
|
+
</Select>
|
|
5697
|
+
) : isBoolean ? (
|
|
5698
|
+
// Toggle for boolean props.
|
|
5699
|
+
<div className="flex min-w-0 flex-1 items-center">
|
|
5700
|
+
<Switch
|
|
5701
|
+
checked={row.value.trim().toLowerCase() === "true"}
|
|
5702
|
+
onCheckedChange={(checked) =>
|
|
5703
|
+
commitProp(row, checked ? "true" : "false")
|
|
5704
|
+
}
|
|
5705
|
+
disabled={!editingEnabled}
|
|
5706
|
+
className="h-4 w-7 [&>span]:size-3 [&>span]:data-[state=checked]:translate-x-3"
|
|
5707
|
+
aria-label={
|
|
5708
|
+
row.name /* i18n-ignore dynamic prop name */
|
|
5709
|
+
}
|
|
5710
|
+
/>
|
|
5711
|
+
</div>
|
|
5712
|
+
) : (
|
|
5713
|
+
// Text input for string props (e.g. a label).
|
|
5714
|
+
<Input
|
|
5715
|
+
defaultValue={row.value}
|
|
5716
|
+
key={`${row.name}:${row.value}`}
|
|
5717
|
+
disabled={!editingEnabled}
|
|
5718
|
+
onBlur={(e) => commitProp(row, e.target.value)}
|
|
5719
|
+
onKeyDown={(e) => {
|
|
5720
|
+
if (e.key === "Enter") {
|
|
5721
|
+
e.preventDefault();
|
|
5722
|
+
e.currentTarget.blur();
|
|
5723
|
+
}
|
|
5724
|
+
}}
|
|
5725
|
+
className="h-6 min-w-0 flex-1 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-[11px] shadow-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
|
|
5726
|
+
/>
|
|
5727
|
+
)}
|
|
5728
|
+
</div>
|
|
5729
|
+
);
|
|
5730
|
+
})}
|
|
5731
|
+
</div>
|
|
5732
|
+
)}
|
|
5733
|
+
|
|
5734
|
+
{/* Connect-Builder CTA (inline, only when real-app features are gated) */}
|
|
5735
|
+
{capabilities.ctaRequired && (
|
|
5736
|
+
<MakeItRealCard
|
|
5737
|
+
designId={designId}
|
|
5738
|
+
featureLabel="component source jump and full prop controls"
|
|
5739
|
+
/>
|
|
5740
|
+
)}
|
|
5741
|
+
</div>
|
|
5742
|
+
</section>
|
|
5743
|
+
);
|
|
5744
|
+
}
|
|
5745
|
+
|
|
4360
5746
|
export function EditPanel({
|
|
4361
5747
|
selectedElement,
|
|
4362
5748
|
pageStyles = {},
|
|
@@ -4371,14 +5757,35 @@ export function EditPanel({
|
|
|
4371
5757
|
onRequestTweaks,
|
|
4372
5758
|
onStyleChange,
|
|
4373
5759
|
onStylesChange,
|
|
5760
|
+
onAutoLayoutConvert,
|
|
4374
5761
|
onExport,
|
|
4375
5762
|
exporting = false,
|
|
5763
|
+
readOnly = false,
|
|
5764
|
+
fileId,
|
|
5765
|
+
filename,
|
|
5766
|
+
designId,
|
|
5767
|
+
onTokensApplied,
|
|
5768
|
+
statesPanelProps,
|
|
5769
|
+
reviewPanelProps,
|
|
5770
|
+
componentNodeId,
|
|
5771
|
+
sourceCapabilities = [],
|
|
5772
|
+
onCreateComponent,
|
|
5773
|
+
defaultComponentName = "Component",
|
|
5774
|
+
inspectCode,
|
|
4376
5775
|
}: EditPanelProps) {
|
|
4377
5776
|
const t = useT();
|
|
5777
|
+
const [createComponentOpen, setCreateComponentOpen] = useState(false);
|
|
4378
5778
|
const [exportSettings, setExportSettings] = useState<ExportSettingsValue>(
|
|
4379
5779
|
DEFAULT_EXPORT_SETTINGS,
|
|
4380
5780
|
);
|
|
4381
5781
|
const [showExportPreview, setShowExportPreview] = useState(false);
|
|
5782
|
+
// Sub-tab within the Tweaks area: "tokens" (TokensPanel face) or "tweaks"
|
|
5783
|
+
// (raw TweakDefinition controls). Always default to the existing "tweaks"
|
|
5784
|
+
// controls so opening Tweaks lands on the user's TweakDefinition controls
|
|
5785
|
+
// (and the + affordance) rather than auto-firing token indexing. "Tokens"
|
|
5786
|
+
// remains available as a sub-tab the user can click when designId is set.
|
|
5787
|
+
const [tweaksSubTab, setTweaksSubTab] = useState<TweaksSubTab>("tweaks");
|
|
5788
|
+
|
|
4382
5789
|
const selectedElementKey = selectedElement
|
|
4383
5790
|
? elementIdentityKey(selectedElement)
|
|
4384
5791
|
: "none";
|
|
@@ -4395,12 +5802,66 @@ export function EditPanel({
|
|
|
4395
5802
|
},
|
|
4396
5803
|
[onTweakChange],
|
|
4397
5804
|
);
|
|
5805
|
+
// The "Add Tweaks" (+) flow lives only on the "tweaks" sub-tab. Switch to it
|
|
5806
|
+
// before opening the prompt so the + button and the newly added control are
|
|
5807
|
+
// visible (avoids leaving the user on the Tokens sub-tab where + isn't shown).
|
|
5808
|
+
const handleRequestTweaks = useCallback(
|
|
5809
|
+
(anchor: HTMLElement) => {
|
|
5810
|
+
setTweaksSubTab("tweaks");
|
|
5811
|
+
onRequestTweaks?.(anchor);
|
|
5812
|
+
},
|
|
5813
|
+
[onRequestTweaks],
|
|
5814
|
+
);
|
|
4398
5815
|
|
|
4399
5816
|
useEffect(() => {
|
|
4400
5817
|
setExportSettings(DEFAULT_EXPORT_SETTINGS);
|
|
4401
5818
|
setShowExportPreview(false);
|
|
4402
5819
|
}, [selectedElementKey]);
|
|
4403
5820
|
|
|
5821
|
+
// Scroll guard: suppress the click that fires immediately after a scroll
|
|
5822
|
+
// gesture ends (rubber-band or normal scroll). Using onScroll instead of
|
|
5823
|
+
// onPointerDown avoids side-effects like Radix DismissableLayer detecting a
|
|
5824
|
+
// "pointerdown outside" and closing open popovers — which, during an
|
|
5825
|
+
// over-scroll bounce, could briefly un-shield the canvas and allow a stray
|
|
5826
|
+
// pointer event to deselect the selected canvas element (R3 regression).
|
|
5827
|
+
const scrolledRecentlyRef = useRef(false);
|
|
5828
|
+
const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
5829
|
+
|
|
5830
|
+
if (readOnly) {
|
|
5831
|
+
return (
|
|
5832
|
+
<div
|
|
5833
|
+
className={cn(
|
|
5834
|
+
"shrink-0 bg-[var(--design-editor-panel-bg)]",
|
|
5835
|
+
"flex h-full min-h-0 flex-col overflow-hidden",
|
|
5836
|
+
)}
|
|
5837
|
+
style={{ width }}
|
|
5838
|
+
>
|
|
5839
|
+
<div className="flex min-h-8 shrink-0 items-center border-b border-border/90 px-3">
|
|
5840
|
+
<h2 className="min-w-0 truncate text-[12px] font-semibold text-foreground">
|
|
5841
|
+
{t("editPanel.properties")}
|
|
5842
|
+
</h2>
|
|
5843
|
+
</div>
|
|
5844
|
+
<div className="flex min-h-0 flex-1 items-center justify-center px-5">
|
|
5845
|
+
<div
|
|
5846
|
+
className="max-w-[188px] text-center"
|
|
5847
|
+
aria-live="polite"
|
|
5848
|
+
aria-busy="true"
|
|
5849
|
+
>
|
|
5850
|
+
<div className="mx-auto mb-3 flex size-9 items-center justify-center rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-muted-foreground/60">
|
|
5851
|
+
<IconLock className="size-4" aria-hidden="true" />
|
|
5852
|
+
</div>
|
|
5853
|
+
<h3 className="text-[12px] font-semibold leading-snug text-foreground">
|
|
5854
|
+
{t("designEditor.inspectorLockedTitle")}
|
|
5855
|
+
</h3>
|
|
5856
|
+
<p className="mt-1.5 text-[11px] leading-snug text-muted-foreground/70">
|
|
5857
|
+
{t("designEditor.inspectorLockedDescription")}
|
|
5858
|
+
</p>
|
|
5859
|
+
</div>
|
|
5860
|
+
</div>
|
|
5861
|
+
</div>
|
|
5862
|
+
);
|
|
5863
|
+
}
|
|
5864
|
+
|
|
4404
5865
|
return (
|
|
4405
5866
|
<div
|
|
4406
5867
|
className={cn(
|
|
@@ -4417,13 +5878,109 @@ export function EditPanel({
|
|
|
4417
5878
|
|
|
4418
5879
|
{activeTab === "design" ? (
|
|
4419
5880
|
<>
|
|
4420
|
-
<SelectionHeader
|
|
5881
|
+
<SelectionHeader
|
|
5882
|
+
element={selectedElement}
|
|
5883
|
+
onCreateComponent={
|
|
5884
|
+
onCreateComponent && selectedElement
|
|
5885
|
+
? () => setCreateComponentOpen(true)
|
|
5886
|
+
: undefined
|
|
5887
|
+
}
|
|
5888
|
+
inspectCodePopover={
|
|
5889
|
+
inspectCode && selectedElement
|
|
5890
|
+
? (trigger) => (
|
|
5891
|
+
<InspectCodePopover trigger={trigger} data={inspectCode} />
|
|
5892
|
+
)
|
|
5893
|
+
: undefined
|
|
5894
|
+
}
|
|
5895
|
+
/>
|
|
5896
|
+
{onCreateComponent && (
|
|
5897
|
+
<CreateComponentDialog
|
|
5898
|
+
open={createComponentOpen}
|
|
5899
|
+
onOpenChange={setCreateComponentOpen}
|
|
5900
|
+
defaultName={defaultComponentName}
|
|
5901
|
+
onSubmit={onCreateComponent}
|
|
5902
|
+
/>
|
|
5903
|
+
)}
|
|
5904
|
+
|
|
5905
|
+
<div
|
|
5906
|
+
className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain"
|
|
5907
|
+
onScroll={() => {
|
|
5908
|
+
// Mark that a scroll just happened so the click that some
|
|
5909
|
+
// browsers fire at the end of a scroll gesture (or after an
|
|
5910
|
+
// overscroll/rubber-band bounce) is suppressed. Crucially this
|
|
5911
|
+
// runs on the scroll event — NOT on pointerdown — so it never
|
|
5912
|
+
// triggers Radix's DismissableLayer "outside pointerdown"
|
|
5913
|
+
// detection, which would close open inspector popovers and, once
|
|
5914
|
+
// the shield is removed, allow a stray canvas pointer event to
|
|
5915
|
+
// deselect the selected element (the R3 overscroll regression).
|
|
5916
|
+
scrolledRecentlyRef.current = true;
|
|
5917
|
+
if (scrollTimerRef.current !== null) {
|
|
5918
|
+
clearTimeout(scrollTimerRef.current);
|
|
5919
|
+
}
|
|
5920
|
+
scrollTimerRef.current = setTimeout(() => {
|
|
5921
|
+
scrolledRecentlyRef.current = false;
|
|
5922
|
+
scrollTimerRef.current = null;
|
|
5923
|
+
}, 300);
|
|
5924
|
+
}}
|
|
5925
|
+
onClickCapture={(e) => {
|
|
5926
|
+
// Suppress spurious clicks (e.g. color-picker opening) that
|
|
5927
|
+
// fire immediately after a scroll gesture ends. The 300ms
|
|
5928
|
+
// window from the last scroll event covers both the synchronous
|
|
5929
|
+
// scroll-end click and the delayed synthetic click that mobile
|
|
5930
|
+
// browsers generate after a touch-scroll ends.
|
|
5931
|
+
if (!scrolledRecentlyRef.current) return;
|
|
5932
|
+
scrolledRecentlyRef.current = false;
|
|
5933
|
+
if (scrollTimerRef.current !== null) {
|
|
5934
|
+
clearTimeout(scrollTimerRef.current);
|
|
5935
|
+
scrollTimerRef.current = null;
|
|
5936
|
+
}
|
|
5937
|
+
e.stopPropagation();
|
|
5938
|
+
e.preventDefault();
|
|
5939
|
+
}}
|
|
5940
|
+
onKeyDown={(e) => {
|
|
5941
|
+
// Trap Tab within the inspector panel so it never focuses the
|
|
5942
|
+
// canvas iframe. When the canvas iframe gains focus it forwards
|
|
5943
|
+
// a synthetic Tab keydown to the parent window, which is picked
|
|
5944
|
+
// up by the design-editor hotkey handler as "cycle file" and
|
|
5945
|
+
// causes apparent deselection / overview-mode switch (bug: Tab
|
|
5946
|
+
// in a numeric field deselected the canvas element).
|
|
5947
|
+
if (e.key !== "Tab") return;
|
|
5948
|
+
const panel = e.currentTarget;
|
|
5949
|
+
const focusable = Array.from(
|
|
5950
|
+
panel.querySelectorAll<HTMLElement>(
|
|
5951
|
+
'input, button, select, textarea, [tabindex]:not([tabindex="-1"])',
|
|
5952
|
+
),
|
|
5953
|
+
).filter(
|
|
5954
|
+
(el) =>
|
|
5955
|
+
!el.hasAttribute("disabled") &&
|
|
5956
|
+
el.tabIndex !== -1 &&
|
|
5957
|
+
!el.closest('[aria-hidden="true"]'),
|
|
5958
|
+
);
|
|
5959
|
+
if (focusable.length === 0) return;
|
|
5960
|
+
e.preventDefault();
|
|
5961
|
+
const current = document.activeElement as HTMLElement | null;
|
|
5962
|
+
const idx = current ? focusable.indexOf(current) : -1;
|
|
5963
|
+
const next = e.shiftKey
|
|
5964
|
+
? focusable[(idx - 1 + focusable.length) % focusable.length]
|
|
5965
|
+
: focusable[(idx + 1) % focusable.length];
|
|
5966
|
+
next?.focus();
|
|
5967
|
+
}}
|
|
5968
|
+
>
|
|
5969
|
+
{/* §6.1 Component section — shown at the top when a component
|
|
5970
|
+
instance is selected. Requires designId + componentNodeId. */}
|
|
5971
|
+
{designId && componentNodeId && (
|
|
5972
|
+
<ComponentSection
|
|
5973
|
+
designId={designId}
|
|
5974
|
+
nodeId={componentNodeId}
|
|
5975
|
+
sourceCapabilities={sourceCapabilities}
|
|
5976
|
+
/>
|
|
5977
|
+
)}
|
|
4421
5978
|
|
|
4422
|
-
<div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
|
4423
5979
|
{!selectedElement && (
|
|
4424
5980
|
<PageProperties
|
|
4425
5981
|
styles={pageStyles}
|
|
4426
5982
|
onStyleChange={onStyleChange}
|
|
5983
|
+
onStylesChange={onStylesChange}
|
|
4427
5984
|
/>
|
|
4428
5985
|
)}
|
|
4429
5986
|
|
|
@@ -4437,6 +5994,7 @@ export function EditPanel({
|
|
|
4437
5994
|
element={selectedElement}
|
|
4438
5995
|
onStyleChange={onStyleChange}
|
|
4439
5996
|
onStylesChange={onStylesChange}
|
|
5997
|
+
onAutoLayoutConvert={onAutoLayoutConvert}
|
|
4440
5998
|
/>
|
|
4441
5999
|
<AppearanceProperties
|
|
4442
6000
|
element={selectedElement}
|
|
@@ -4473,6 +6031,17 @@ export function EditPanel({
|
|
|
4473
6031
|
onStyleChange={onStyleChange}
|
|
4474
6032
|
/>
|
|
4475
6033
|
) : null}
|
|
6034
|
+
{/* AI edit request block — collapsible, collapsed by default */}
|
|
6035
|
+
<section className="shrink-0 border-t border-[var(--design-editor-control-border)]">
|
|
6036
|
+
<InspectorAiActions
|
|
6037
|
+
selector={selectedElement.selector}
|
|
6038
|
+
sourceId={selectedElement.sourceId}
|
|
6039
|
+
designId={designId}
|
|
6040
|
+
fileId={fileId}
|
|
6041
|
+
filename={filename}
|
|
6042
|
+
canEdit={!readOnly}
|
|
6043
|
+
/>
|
|
6044
|
+
</section>
|
|
4476
6045
|
</>
|
|
4477
6046
|
)}
|
|
4478
6047
|
{onExport ? (
|
|
@@ -4507,40 +6076,212 @@ export function EditPanel({
|
|
|
4507
6076
|
) : null}
|
|
4508
6077
|
</PanelSection>
|
|
4509
6078
|
) : null}
|
|
6079
|
+
|
|
6080
|
+
{/* §6.4 States & Responsive — contextual section in Design tab.
|
|
6081
|
+
Collapsed by default so existing Design content is not buried.
|
|
6082
|
+
Only mounts when designId is provided so there is something to
|
|
6083
|
+
load; without it the panel would fire an action with no id. */}
|
|
6084
|
+
{designId && statesPanelProps ? (
|
|
6085
|
+
<PanelSection
|
|
6086
|
+
title={"States" /* i18n-ignore design inspector section */}
|
|
6087
|
+
defaultCollapsed
|
|
6088
|
+
actions={
|
|
6089
|
+
<Tooltip>
|
|
6090
|
+
<TooltipTrigger asChild>
|
|
6091
|
+
<Button
|
|
6092
|
+
type="button"
|
|
6093
|
+
variant="ghost"
|
|
6094
|
+
size="icon"
|
|
6095
|
+
className="size-6 rounded-md text-muted-foreground hover:text-foreground"
|
|
6096
|
+
aria-label={
|
|
6097
|
+
"Breakpoints & states" /* i18n-ignore design inspector action */
|
|
6098
|
+
}
|
|
6099
|
+
>
|
|
6100
|
+
<IconDeviceDesktop className="size-3.5" />
|
|
6101
|
+
</Button>
|
|
6102
|
+
</TooltipTrigger>
|
|
6103
|
+
<TooltipContent>
|
|
6104
|
+
{
|
|
6105
|
+
"Breakpoints & states" /* i18n-ignore design inspector action */
|
|
6106
|
+
}
|
|
6107
|
+
</TooltipContent>
|
|
6108
|
+
</Tooltip>
|
|
6109
|
+
}
|
|
6110
|
+
>
|
|
6111
|
+
<StatesPanel designId={designId} {...statesPanelProps} />
|
|
6112
|
+
{/* §6.4 / §6.6 — live captures (real running-app data, route
|
|
6113
|
+
props, API responses) are a real-app capability. When
|
|
6114
|
+
canCapture is false (inline source) surface a compact
|
|
6115
|
+
"Make it real" CTA so the user knows it's one step away. */}
|
|
6116
|
+
{!statesPanelProps.canCapture && (
|
|
6117
|
+
<MakeItRealCard
|
|
6118
|
+
designId={designId}
|
|
6119
|
+
featureLabel="live data captures and real-app states"
|
|
6120
|
+
/>
|
|
6121
|
+
)}
|
|
6122
|
+
</PanelSection>
|
|
6123
|
+
) : null}
|
|
6124
|
+
|
|
6125
|
+
{/* §6.5 Review — contextual section in Design tab.
|
|
6126
|
+
Collapsed by default. Renders when reviewPanelProps is provided,
|
|
6127
|
+
no designId check needed since ReviewPanel is statically fed. */}
|
|
6128
|
+
{reviewPanelProps ? (
|
|
6129
|
+
<PanelSection
|
|
6130
|
+
title={"Review" /* i18n-ignore design inspector section */}
|
|
6131
|
+
defaultCollapsed
|
|
6132
|
+
actions={
|
|
6133
|
+
<Tooltip>
|
|
6134
|
+
<TooltipTrigger asChild>
|
|
6135
|
+
<Button
|
|
6136
|
+
type="button"
|
|
6137
|
+
variant="ghost"
|
|
6138
|
+
size="icon"
|
|
6139
|
+
className="size-6 rounded-md text-muted-foreground hover:text-foreground"
|
|
6140
|
+
aria-label={
|
|
6141
|
+
"Accessibility & visual diff" /* i18n-ignore design inspector action */
|
|
6142
|
+
}
|
|
6143
|
+
>
|
|
6144
|
+
<IconShieldCheck className="size-3.5" />
|
|
6145
|
+
</Button>
|
|
6146
|
+
</TooltipTrigger>
|
|
6147
|
+
<TooltipContent>
|
|
6148
|
+
{
|
|
6149
|
+
"Accessibility & visual diff" /* i18n-ignore design inspector action */
|
|
6150
|
+
}
|
|
6151
|
+
</TooltipContent>
|
|
6152
|
+
</Tooltip>
|
|
6153
|
+
}
|
|
6154
|
+
>
|
|
6155
|
+
{/* ReviewPanel manages its own scroll; no extra wrapper needed. */}
|
|
6156
|
+
<ReviewPanel {...reviewPanelProps} />
|
|
6157
|
+
</PanelSection>
|
|
6158
|
+
) : null}
|
|
4510
6159
|
</div>
|
|
4511
6160
|
</>
|
|
4512
6161
|
) : activeTab === "tweaks" ? (
|
|
4513
6162
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
</
|
|
4534
|
-
|
|
4535
|
-
|
|
6163
|
+
{/* Tweaks tab header: when designId is present show a Tokens | Tweaks
|
|
6164
|
+
sub-tab switcher so the TokensPanel is the first-class face of
|
|
6165
|
+
this area per §6.2. When there is no designId only the raw tweaks
|
|
6166
|
+
title is shown (original behaviour). */}
|
|
6167
|
+
{designId ? (
|
|
6168
|
+
<div className="flex h-9 shrink-0 items-center gap-0.5 border-b border-border/90 px-2">
|
|
6169
|
+
<Button
|
|
6170
|
+
type="button"
|
|
6171
|
+
variant="ghost"
|
|
6172
|
+
size="sm"
|
|
6173
|
+
className={cn(
|
|
6174
|
+
"h-6 rounded-md px-2 text-[11px] font-semibold text-muted-foreground transition-colors hover:text-foreground",
|
|
6175
|
+
tweaksSubTab === "tokens" &&
|
|
6176
|
+
"bg-[var(--design-editor-panel-raised-bg)] text-foreground",
|
|
6177
|
+
)}
|
|
6178
|
+
onClick={() => setTweaksSubTab("tokens")}
|
|
6179
|
+
>
|
|
6180
|
+
<IconPalette className="mr-1 size-3" />
|
|
6181
|
+
{"Tokens" /* i18n-ignore design inspector sub-tab */}
|
|
6182
|
+
</Button>
|
|
6183
|
+
<Button
|
|
6184
|
+
type="button"
|
|
6185
|
+
variant="ghost"
|
|
6186
|
+
size="sm"
|
|
6187
|
+
className={cn(
|
|
6188
|
+
"h-6 rounded-md px-2 text-[11px] font-semibold text-muted-foreground transition-colors hover:text-foreground",
|
|
6189
|
+
tweaksSubTab === "tweaks" &&
|
|
6190
|
+
"bg-[var(--design-editor-panel-raised-bg)] text-foreground",
|
|
6191
|
+
)}
|
|
6192
|
+
onClick={() => setTweaksSubTab("tweaks")}
|
|
6193
|
+
>
|
|
6194
|
+
{t("designEditor.tweaks")}
|
|
6195
|
+
</Button>
|
|
6196
|
+
</div>
|
|
6197
|
+
) : (
|
|
6198
|
+
<div className="flex h-10 shrink-0 items-center justify-between gap-2 border-b border-border/90 px-3">
|
|
6199
|
+
<h3 className="min-w-0 flex-1 truncate text-[13px] font-semibold text-foreground">
|
|
6200
|
+
{t("designEditor.tweaks")}
|
|
6201
|
+
</h3>
|
|
6202
|
+
{onRequestTweaks ? (
|
|
6203
|
+
<Tooltip>
|
|
6204
|
+
<TooltipTrigger asChild>
|
|
6205
|
+
<Button
|
|
6206
|
+
type="button"
|
|
6207
|
+
variant="ghost"
|
|
6208
|
+
size="icon"
|
|
6209
|
+
className="size-7 rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
6210
|
+
aria-label={t("designEditor.addTweaks")}
|
|
6211
|
+
onClick={(event) =>
|
|
6212
|
+
handleRequestTweaks(event.currentTarget)
|
|
6213
|
+
}
|
|
6214
|
+
>
|
|
6215
|
+
<IconPlus className="size-3.5" />
|
|
6216
|
+
</Button>
|
|
6217
|
+
</TooltipTrigger>
|
|
6218
|
+
<TooltipContent>{t("designEditor.addTweaks")}</TooltipContent>
|
|
6219
|
+
</Tooltip>
|
|
6220
|
+
) : null}
|
|
6221
|
+
</div>
|
|
6222
|
+
)}
|
|
6223
|
+
|
|
4536
6224
|
<div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
6225
|
+
{designId && tweaksSubTab === "tokens" ? (
|
|
6226
|
+
/* §6.2 Tokens face — first-class view of the Tweaks area.
|
|
6227
|
+
TokensPanel calls apply-design-token-edit which routes through
|
|
6228
|
+
the Tweaks loop (designs.data.tweakSelections persist). */
|
|
6229
|
+
<>
|
|
6230
|
+
<TokensPanel
|
|
6231
|
+
designId={designId}
|
|
6232
|
+
onTokensApplied={onTokensApplied}
|
|
6233
|
+
/>
|
|
6234
|
+
{/* §5 / §6.6 — token write-back to source is a real-app
|
|
6235
|
+
capability. On inline sources (sourceCapabilities lacks
|
|
6236
|
+
"writeFile") show a compact "Make it real" CTA after the
|
|
6237
|
+
token list so the user knows write-back is one step away. */}
|
|
6238
|
+
{!sourceCapabilities.includes("writeFile") && (
|
|
6239
|
+
<div className="px-3 pb-3 pt-1">
|
|
6240
|
+
<MakeItRealCard
|
|
6241
|
+
designId={designId}
|
|
6242
|
+
featureLabel="token write-back to source files"
|
|
6243
|
+
/>
|
|
6244
|
+
</div>
|
|
6245
|
+
)}
|
|
6246
|
+
</>
|
|
6247
|
+
) : (
|
|
6248
|
+
/* Raw TweakDefinition controls — shown when no designId, or when
|
|
6249
|
+
the user switches to the Tweaks sub-tab. */
|
|
6250
|
+
<>
|
|
6251
|
+
{designId && onRequestTweaks ? (
|
|
6252
|
+
/* When in sub-tab mode, show the add-tweaks action in the
|
|
6253
|
+
scrollable content area above the raw controls. */
|
|
6254
|
+
<div className="flex items-center justify-end px-3 pt-2">
|
|
6255
|
+
<Tooltip>
|
|
6256
|
+
<TooltipTrigger asChild>
|
|
6257
|
+
<Button
|
|
6258
|
+
type="button"
|
|
6259
|
+
variant="ghost"
|
|
6260
|
+
size="icon"
|
|
6261
|
+
className="size-7 rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
6262
|
+
aria-label={t("designEditor.addTweaks")}
|
|
6263
|
+
onClick={(event) =>
|
|
6264
|
+
handleRequestTweaks(event.currentTarget)
|
|
6265
|
+
}
|
|
6266
|
+
>
|
|
6267
|
+
<IconPlus className="size-3.5" />
|
|
6268
|
+
</Button>
|
|
6269
|
+
</TooltipTrigger>
|
|
6270
|
+
<TooltipContent>
|
|
6271
|
+
{t("designEditor.addTweaks")}
|
|
6272
|
+
</TooltipContent>
|
|
6273
|
+
</Tooltip>
|
|
6274
|
+
</div>
|
|
6275
|
+
) : null}
|
|
6276
|
+
<TweaksPanelContent
|
|
6277
|
+
tweaks={tweaks}
|
|
6278
|
+
values={tweakValues}
|
|
6279
|
+
onChange={handleTweakChange}
|
|
6280
|
+
onRequestTweaks={!designId ? handleRequestTweaks : undefined}
|
|
6281
|
+
className="px-3 py-3"
|
|
6282
|
+
/>
|
|
6283
|
+
</>
|
|
6284
|
+
)}
|
|
4544
6285
|
</div>
|
|
4545
6286
|
</div>
|
|
4546
6287
|
) : extensionContext ? (
|