@agent-native/core 0.90.1 → 0.90.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AgentPanel.tsx +1 -1
- package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +11 -6
- package/corpus/core/src/client/use-action.ts +39 -2
- package/corpus/core/src/client/use-db-sync.ts +44 -18
- package/corpus/core/src/db/create-get-db.ts +32 -2
- package/corpus/core/src/server/framework-request-handler.ts +52 -4
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +5 -0
- package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +34 -6
- package/corpus/templates/assets/AGENTS.md +23 -4
- package/corpus/templates/assets/actions/_generation-preset-settings.ts +48 -0
- package/corpus/templates/assets/actions/_helpers.ts +8 -1
- package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +64 -0
- package/corpus/templates/assets/actions/create-generation-preset.ts +11 -3
- package/corpus/templates/assets/actions/generate-image-batch.ts +7 -2
- package/corpus/templates/assets/actions/generate-image.ts +342 -25
- package/corpus/templates/assets/actions/get-library.ts +4 -3
- package/corpus/templates/assets/actions/navigate.ts +2 -1
- package/corpus/templates/assets/actions/refresh-generation-run.ts +5 -1
- package/corpus/templates/assets/actions/update-asset.ts +1 -0
- package/corpus/templates/assets/actions/update-generation-preset.ts +8 -1
- package/corpus/templates/assets/actions/view-screen.ts +8 -1
- package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +9 -1
- package/corpus/templates/assets/app/hooks/use-image-model-menu.ts +1 -0
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +11 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +64 -0
- package/corpus/templates/assets/app/i18n-data.ts +650 -0
- package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -1
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +44 -33
- package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1474 -0
- package/corpus/templates/assets/app/routes/library.tsx +43 -1
- package/corpus/templates/assets/server/lib/generation.ts +366 -44
- package/corpus/templates/assets/server/lib/image-processing.ts +252 -0
- package/corpus/templates/assets/server/lib/preset-skeleton.ts +137 -0
- package/corpus/templates/assets/shared/api.ts +47 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +7 -2
- package/corpus/templates/clips/app/components/library/sort-menu.tsx +1 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +114 -33
- package/corpus/templates/clips/app/routes/_app.library._index.tsx +27 -1
- package/corpus/templates/clips/app/routes/record.tsx +157 -68
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +5 -5
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +20 -3
- package/corpus/templates/clips/shared/finalize-recovery.ts +23 -3
- package/corpus/templates/content/AGENTS.md +52 -52
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +188 -8
- package/corpus/templates/content/actions/_builder-docs-client.ts +41 -3
- package/corpus/templates/content/actions/_database-source-utils.ts +998 -160
- package/corpus/templates/content/actions/_database-utils.ts +44 -4
- package/corpus/templates/content/actions/_notion-action-utils.ts +6 -2
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +85 -12
- package/corpus/templates/content/actions/change-content-database-source-role.ts +9 -1
- package/corpus/templates/content/actions/delete-content-database.ts +1 -0
- package/corpus/templates/content/actions/get-document.ts +11 -2
- package/corpus/templates/content/actions/list-content-databases.ts +57 -9
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +23 -6
- package/corpus/templates/content/actions/refresh-content-database-source.ts +26 -3
- package/corpus/templates/content/actions/restore-content-database.ts +1 -0
- package/corpus/templates/content/actions/update-document.ts +43 -0
- package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +21 -0
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +0 -1
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +70 -28
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +69 -17
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +4 -4
- package/corpus/templates/content/app/components/editor/body-hydration.ts +112 -0
- package/corpus/templates/content/app/components/editor/builder-body-hydration-pump.ts +52 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +9076 -2876
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +41 -1
- package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +36 -11
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +21 -1
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +11 -1
- package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +11 -8
- package/corpus/templates/content/app/global.css +41 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +346 -42
- package/corpus/templates/content/app/hooks/use-db-sync.ts +9 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +47 -0
- package/corpus/templates/content/app/hooks/use-notion.ts +12 -10
- package/corpus/templates/content/app/i18n/zh-TW.ts +35 -0
- package/corpus/templates/content/app/i18n-data.ts +355 -0
- package/corpus/templates/content/app/root.tsx +22 -0
- package/corpus/templates/content/changelog/2026-07-01-builder-database-sources-keep-loading-rows-in-the-background.md +6 -0
- package/corpus/templates/content/changelog/2026-07-01-builder-source-attach-starts-with-usable-rows.md +6 -0
- package/corpus/templates/content/changelog/2026-07-01-builder-source-attachment-now-stays-connected-after-large.md +6 -0
- package/corpus/templates/content/changelog/2026-07-01-builder-source-refreshes-now-show-progress-for-large-docs-blog.md +6 -0
- package/corpus/templates/content/changelog/2026-07-01-database-pages-now-delete-to-trash-without-route-errors.md +6 -0
- package/corpus/templates/content/changelog/2026-07-02-builder-source-refreshes-recover-from-stalled-background-row.md +6 -0
- package/corpus/templates/content/changelog/2026-07-02-builder-sourced-rows-open-with-sync-protection.md +6 -0
- package/corpus/templates/content/changelog/2026-07-02-database-rows-and-pages-open-faster-from-recently-loaded-dat.md +6 -0
- package/corpus/templates/content/changelog/2026-07-02-preview-panels-close-reliably-narrow-sidebar-row-actions-sta.md +6 -0
- package/corpus/templates/content/changelog/2026-07-06-builder-sync-progress-no-longer-shows-continue.md +6 -0
- package/corpus/templates/content/shared/api.ts +13 -0
- package/corpus/templates/content/shared/builder-mdx.ts +16 -3
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +23 -13
- package/corpus/templates/design/.agents/skills/responsive-breakpoints/SKILL.md +98 -0
- package/corpus/templates/design/.agents/skills/shader-fills/SKILL.md +162 -0
- package/corpus/templates/design/AGENTS.md +9 -2
- package/corpus/templates/design/actions/add-breakpoint.ts +6 -3
- package/corpus/templates/design/actions/apply-a11y-fix.ts +57 -37
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +41 -20
- package/corpus/templates/design/actions/apply-motion-edit.ts +251 -38
- package/corpus/templates/design/actions/apply-shader-fill.ts +90 -66
- package/corpus/templates/design/actions/apply-visual-edit.ts +282 -55
- package/corpus/templates/design/actions/create-component.ts +29 -60
- package/corpus/templates/design/actions/create-file.ts +9 -2
- package/corpus/templates/design/actions/duplicate-design.ts +31 -2
- package/corpus/templates/design/actions/edit-design.ts +36 -38
- package/corpus/templates/design/actions/generate-design-title.ts +87 -0
- package/corpus/templates/design/actions/generate-design.ts +53 -18
- package/corpus/templates/design/actions/generate-screens.ts +108 -4
- package/corpus/templates/design/actions/get-motion-timeline.ts +35 -7
- package/corpus/templates/design/actions/insert-asset.ts +34 -29
- package/corpus/templates/design/actions/insert-design-native-asset.ts +129 -32
- package/corpus/templates/design/actions/insert-figma-library-asset.ts +35 -30
- package/corpus/templates/design/actions/present-design-variants.ts +7 -2
- package/corpus/templates/design/actions/remove-motion-timeline.ts +31 -36
- package/corpus/templates/design/actions/update-file.ts +190 -75
- package/corpus/templates/design/app/components/design/BreakpointBar.tsx +403 -0
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +469 -222
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +708 -53
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +83 -3
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1091 -6948
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +419 -85
- package/corpus/templates/design/app/components/design/MotionDock.tsx +1124 -103
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2610 -1398
- package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +22 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +2182 -235
- package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +187 -11
- package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +376 -1
- package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +746 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +17 -0
- package/corpus/templates/design/app/components/design/code-workbench/SideBar.tsx +1 -1
- package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +1 -3
- package/corpus/templates/design/app/components/design/code-workbench/quickinput/QuickInput.tsx +1 -1
- package/corpus/templates/design/app/components/design/edit-panel/appearance-properties.tsx +518 -0
- package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +562 -0
- package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +763 -0
- package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +94 -0
- package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +780 -0
- package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +446 -0
- package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +79 -0
- package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +315 -0
- package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +344 -0
- package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +594 -0
- package/corpus/templates/design/app/components/design/edit-panel/frame-presets-panel.tsx +119 -0
- package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +248 -0
- package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +45 -0
- package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +693 -0
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +724 -0
- package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +207 -0
- package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +628 -0
- package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +91 -0
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +649 -0
- package/corpus/templates/design/app/components/design/edit-panel/style-change-types.ts +118 -0
- package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +88 -0
- package/corpus/templates/design/app/components/design/edit-panel/transform-helpers.ts +100 -0
- package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +117 -0
- package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +501 -0
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +91 -40
- package/corpus/templates/design/app/components/design/inspector/BreakpointOverrideIndicator.tsx +82 -0
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +189 -87
- package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +1097 -0
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +44 -0
- package/corpus/templates/design/app/components/design/inspector/InteractionStatePanel.tsx +258 -0
- package/corpus/templates/design/app/components/design/inspector/MotionKeyframeDiamond.tsx +144 -0
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +99 -9
- package/corpus/templates/design/app/components/design/inspector/frame-size-presets.ts +116 -0
- package/corpus/templates/design/app/components/design/inspector/index.ts +26 -0
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +29 -0
- package/corpus/templates/design/app/components/design/multi-screen/alt-hover-measurement.ts +171 -0
- package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +220 -0
- package/corpus/templates/design/app/components/design/multi-screen/chrome-transitions.ts +35 -0
- package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +134 -0
- package/corpus/templates/design/app/components/design/multi-screen/culling.ts +185 -0
- package/corpus/templates/design/app/components/design/multi-screen/gradient-overlay-geometry.ts +120 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +892 -0
- package/corpus/templates/design/app/components/design/multi-screen/vector-edit-geometry.ts +21 -0
- package/corpus/templates/design/app/components/design/types.ts +15 -0
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +109 -5
- package/corpus/templates/design/app/global.css +8 -8
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +259 -2
- package/corpus/templates/design/app/i18n/zh-TW.ts +163 -9
- package/corpus/templates/design/app/i18n-breakpoints.ts +205 -0
- package/corpus/templates/design/app/i18n-data.ts +1664 -152
- package/corpus/templates/design/app/i18n-motion.ts +563 -0
- package/corpus/templates/design/app/lib/design-import.ts +73 -0
- package/corpus/templates/design/app/pages/Index.tsx +41 -22
- package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +1 -1
- package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +1 -1
- package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +1 -1
- package/corpus/templates/design/changelog/2026-06-30-multi-select-outlines-now-stay-consistent-across-all-screens.md +1 -1
- package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +1 -1
- package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +1 -1
- package/corpus/templates/design/changelog/2026-07-05-designs-get-ai-generated-names-from-your-prompt.md +6 -0
- package/corpus/templates/design/changelog/2026-07-05-local-code-screens-keep-their-connection-when-duplicated-and.md +6 -0
- package/corpus/templates/design/changelog/2026-07-05-press-a-to-draw-screens-on-the-canvas-inside-a-screen-it-dr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-3d-rotation-and-perspective-controls-on-every-element.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-ai-generated-and-duplicated-screens-now-get-their-editor-sel.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-applying-shaders-and-fills-in-quick-succession-can-no-longer.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-breakpoint-frames-in-the-overview-now-support-click-to-selec.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-breakpoint-segments-in-the-inspector-now-show-a-device-icon-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-breakpoint-targeting-now-lives-in-one-compact-control-in-the.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-canvas-edits-can-no-longer-be-overwritten-by-stale-collabora.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-canvas-edits-inside-screens-save-reliably-again.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-cleaner-inspector-removed-the-ask-ai-box-from-the-design-tab.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-clicking-empty-canvas-space-now-deselects-an-element-inside-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-dragging-an-element-onto-a-rectangle-now-nests-it-inside-wit.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-dragging-screens-and-objects-on-the-canvas-is-now-much-smoot.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-drawing-a-shape-or-text-on-the-canvas-no-longer-flashes-away.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-dropping-an-element-between-two-others-inside-a-screen-now-s.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-duplicating-a-screen-now-places-the-copy-exactly-where-you-d.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-edit-gradients-directly-on-elements-inside-screens-scale-str.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-edits-from-collaborators-and-the-agent-now-update-the-canvas.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-a-bug-where-most-style-position-and-text-edits-briefly.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-a-confusing-raw-error-message-when-a-file-upload-fails.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-a-rare-case-where-an-edit-could-be-silently-overwritte.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-inserted-assets-sometimes-disappearing-reappearing-or-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-inspector-value-scrubbing-producing-erratic-numbers-an.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-padding-gap-and-min-max-size-scrubbing-not-updating-th.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-text-stroke-color-turning-black-and-glyphs-disappearin.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-the-collapsed-effects-and-stroke-inspector-sections-sh.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-the-new-design-dialog-closing-unexpectedly-when-using-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fixed-the-typography-section-missing-when-selecting-text-nes.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-full-view-no-longer-flashes-back-to-the-overview-on-entry-no.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-keyframe-animation-timeline-per-property-keyframes-easing-cu.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-moving-restyling-and-scrubbing-values-on-elements-in-ai-gene.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-padding-and-corner-radius-value-scrubbing-now-snaps-to-whole.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-padding-handles-now-only-resize-on-the-handle-line-itself-sh.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-preview-and-edit-hover-focus-and-pressed-states-with-real-cs.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-promoting-an-element-to-a-component-now-works-correctly-for-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-reduced-url-history-churn-and-selection-overlay-flicker-whil.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-removed-the-decorative-dot-before-screen-names-in-the-overvi.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-responsive-breakpoints-edit-at-any-width-with-cascading-over.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-screen-labels-no-longer-turn-blue-when-you-hover-inside-the-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-scrolling-a-screen-with-an-element-selected-stays-smooth-on-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-selecting-multiple-elements-and-dragging-now-moves-the-whole.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-selection-borders-handles-and-value-readouts-now-stay-the-sa.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-selection-handles-and-editing-chrome-now-stay-a-constant-siz.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-shader-fills-and-effects-ai-authored-or-preset-glsl-shaders-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-text-drawn-on-the-canvas-now-defaults-to-white-and-inter-in-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-text-dropped-into-a-shape-now-stays-readable-instead-of-disa.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-the-add-tweak-controls-prompt-no-longer-jumps-to-the-corner-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-the-padding-value-readout-now-stays-visible-while-you-hover-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-the-selected-element-attachment-now-clears-from-the-chat-com.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-the-zoom-menu-is-more-compact-with-tight-rows-and-right-alig.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-undo-and-redo-no-longer-flash-the-screen-changes-now-apply-i.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-zoom-presets-like-zoom-to-50-now-stay-on-the-focused-screen-.md +6 -0
- package/corpus/templates/design/server/lib/import-design-files.ts +11 -2
- package/corpus/templates/design/server/source-workspace.ts +139 -59
- package/corpus/templates/design/shared/board-file.ts +11 -2
- package/corpus/templates/design/shared/breakpoint-media.ts +543 -0
- package/corpus/templates/design/shared/canvas-math.ts +354 -35
- package/corpus/templates/design/shared/code-layer.ts +277 -8
- package/corpus/templates/design/shared/design-state.ts +11 -4
- package/corpus/templates/design/shared/interaction-states.ts +548 -0
- package/corpus/templates/design/shared/motion-compiler.ts +186 -23
- package/corpus/templates/design/shared/motion-easing.ts +294 -0
- package/corpus/templates/design/shared/motion-timeline.ts +400 -34
- package/corpus/templates/design/shared/prompt-title.ts +95 -0
- package/corpus/templates/design/shared/responsive-classes.ts +448 -10
- package/corpus/templates/design/shared/screen-annotation.ts +46 -0
- package/corpus/templates/design/shared/shader-fills.ts +1009 -0
- package/corpus/templates/design/shared/shader-presets.ts +852 -4
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +15 -2
- package/corpus/templates/slides/app/components/editor/ImportButton.tsx +10 -1
- package/corpus/templates/slides/app/lib/upload-response.ts +71 -0
- package/dist/client/AgentPanel.js +1 -1
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.js +11 -6
- package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
- package/dist/client/use-action.d.ts +3 -0
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +34 -3
- package/dist/client/use-action.js.map +1 -1
- package/dist/client/use-db-sync.d.ts +4 -0
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +37 -18
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/create-get-db.d.ts +25 -0
- package/dist/db/create-get-db.d.ts.map +1 -1
- package/dist/db/create-get-db.js +25 -2
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/framework-request-handler.d.ts.map +1 -1
- package/dist/server/framework-request-handler.js +37 -1
- package/dist/server/framework-request-handler.js.map +1 -1
- package/package.json +1 -1
- package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +0 -153
- package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +0 -133
- package/corpus/templates/design/app/pages/DesignEditor.tsx +0 -22924
|
@@ -0,0 +1,1097 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GlslShaderPanel — the code-backed "Shader" paint-type / effect-type picker.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Figma UI3's Shader picker structure:
|
|
5
|
+
* • "Created by you" — shaders already saved in this design's screen HTML
|
|
6
|
+
* (parsed live from the source via shared/shader-fills.ts), plus the
|
|
7
|
+
* "Create new" AI tile that prefills the agent chat.
|
|
8
|
+
* • "Presets" — the curated GLSL preset library from
|
|
9
|
+
* shared/shader-presets.ts (our "By Figma" equivalent).
|
|
10
|
+
*
|
|
11
|
+
* Unlike the legacy ShaderFillsPanel (paper-design GPU presets persisted as
|
|
12
|
+
* CSS gradient approximations), applying a shader here persists the real GLSL
|
|
13
|
+
* fragment source + uniforms manifest into the screen HTML — readable and
|
|
14
|
+
* editable in the Code panel — and the embedded WebGL runtime renders it live
|
|
15
|
+
* in the editor and in exported standalone HTML.
|
|
16
|
+
*
|
|
17
|
+
* Persistence path (no new actions):
|
|
18
|
+
* read-source-file → applyShaderToHtml() → apply-source-edit(full-replace,
|
|
19
|
+
* expectedVersionHash) → onApplied(fileId, content) so the host editor can
|
|
20
|
+
* sync its local/collab state (same contract as component prop edits).
|
|
21
|
+
*
|
|
22
|
+
* Live preview path: postMessage broadcast to every screen iframe; the
|
|
23
|
+
* shader-fill-preview bridge forwards to the injected runtime, which only
|
|
24
|
+
* mounts on the iframe that actually contains the target node.
|
|
25
|
+
*
|
|
26
|
+
* See the "Cross-pipeline write-race guard" section below for why this
|
|
27
|
+
* picker's persist flow registers itself in a small exclusion registry that
|
|
28
|
+
* DesignEditor.tsx's commitVisualStyles checks before its own competing
|
|
29
|
+
* content write (Fill section Add layer / Remove layer, concurrent with an
|
|
30
|
+
* in-flight shader apply, otherwise corrupts the document).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
callAction,
|
|
35
|
+
useActionMutation,
|
|
36
|
+
useActionQuery,
|
|
37
|
+
useT,
|
|
38
|
+
} from "@agent-native/core/client";
|
|
39
|
+
import {
|
|
40
|
+
applyShaderToHtml,
|
|
41
|
+
annotateNodeWithShader,
|
|
42
|
+
defaultUniformValues,
|
|
43
|
+
listShaderMounts,
|
|
44
|
+
listShadersInHtml,
|
|
45
|
+
newShaderId,
|
|
46
|
+
removeShaderFromNode,
|
|
47
|
+
type GlslShaderDef,
|
|
48
|
+
type GlslShaderMode,
|
|
49
|
+
type GlslUniformValue,
|
|
50
|
+
} from "@shared/shader-fills";
|
|
51
|
+
import {
|
|
52
|
+
GLSL_SHADER_PRESETS,
|
|
53
|
+
type GlslShaderPreset,
|
|
54
|
+
type GlslShaderPresetCategory,
|
|
55
|
+
} from "@shared/shader-presets";
|
|
56
|
+
import {
|
|
57
|
+
IconArrowLeft,
|
|
58
|
+
IconCode,
|
|
59
|
+
IconPlus,
|
|
60
|
+
IconSearch,
|
|
61
|
+
IconTrash,
|
|
62
|
+
IconWaveSine,
|
|
63
|
+
IconX,
|
|
64
|
+
} from "@tabler/icons-react";
|
|
65
|
+
import { useMemo, useState } from "react";
|
|
66
|
+
import { toast } from "sonner";
|
|
67
|
+
|
|
68
|
+
import { Input } from "@/components/ui/input";
|
|
69
|
+
import {
|
|
70
|
+
Popover,
|
|
71
|
+
PopoverContent,
|
|
72
|
+
PopoverTrigger,
|
|
73
|
+
} from "@/components/ui/popover";
|
|
74
|
+
import {
|
|
75
|
+
Tooltip,
|
|
76
|
+
TooltipContent,
|
|
77
|
+
TooltipTrigger,
|
|
78
|
+
} from "@/components/ui/tooltip";
|
|
79
|
+
import { sendToDesignAgentChat } from "@/lib/agent-chat";
|
|
80
|
+
import { cn } from "@/lib/utils";
|
|
81
|
+
|
|
82
|
+
import { ScrubInput, type ScrubInputChangeMeta } from "./ScrubInput";
|
|
83
|
+
|
|
84
|
+
// ─── Cross-pipeline write-race guard ──────────────────────────────────────────
|
|
85
|
+
//
|
|
86
|
+
// This picker's persist flow (read-source-file GET -> pure transform ->
|
|
87
|
+
// apply-source-edit POST) is a SEPARATE round trip from the base Fill
|
|
88
|
+
// section's style commits (DesignEditor.tsx's commitVisualStyles ->
|
|
89
|
+
// update-file), and both ultimately feed the SAME per-file Yjs collab
|
|
90
|
+
// document — one via a diff-based server-side `applyText`, the other via the
|
|
91
|
+
// host's own synchronous, untracked full-document `ydoc.transact` rewrite
|
|
92
|
+
// (see DesignEditor.tsx's applyLocalContentUpdate/commitVisualStyles
|
|
93
|
+
// "Untracked full rewrite" comments). If a base style edit (e.g. Fill's Add
|
|
94
|
+
// layer / Remove layer) fires WHILE a shader apply/remove/knob-commit for the
|
|
95
|
+
// SAME file is still in flight, the two writes are computed from a common
|
|
96
|
+
// ancestor but never see each other before landing: the shader write's
|
|
97
|
+
// server-side diff and the style edit's own client-side full-document Y.Text
|
|
98
|
+
// rewrite merge as two divergent CRDT deltas, which do not converge to either
|
|
99
|
+
// intended document — verified to reproduce as a corrupted, doubled document
|
|
100
|
+
// (two concatenated <!DOCTYPE>...</html> copies) via the real
|
|
101
|
+
// applyShaderToHtml/applyVisualEdit/applyTextToYDoc functions.
|
|
102
|
+
//
|
|
103
|
+
// `withShaderWriteLock`/`isShaderWriteInFlight` below is a small, file-scoped
|
|
104
|
+
// exclusion registry (no new action, no new GlslShaderPanelContext field —
|
|
105
|
+
// EditPanel.tsx's context plumbing is unchanged) that DesignEditor.tsx's
|
|
106
|
+
// commitVisualStyles imports directly to defer its own competing write until
|
|
107
|
+
// this picker's in-flight persist for the same file has fully settled
|
|
108
|
+
// (including the onApplied host-sync), closing the race at its source
|
|
109
|
+
// instead of papering over the corrupted result afterward.
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Per-file registry of in-flight shader persist operations (read-source-file
|
|
113
|
+
* GET through apply-source-edit POST through the onApplied host-sync
|
|
114
|
+
* callback). Module-scoped rather than threaded through
|
|
115
|
+
* GlslShaderPanelContext so DesignEditor.tsx can await it without EditPanel.tsx
|
|
116
|
+
* needing to forward a new prop.
|
|
117
|
+
*/
|
|
118
|
+
const shaderWriteLocks = new Map<string, Promise<void>>();
|
|
119
|
+
|
|
120
|
+
/** True while a shader persist for this file id has not yet settled. */
|
|
121
|
+
export function isShaderWriteInFlight(fileId: string | undefined): boolean {
|
|
122
|
+
return !!fileId && shaderWriteLocks.has(fileId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolves once every shader persist registered for this file id (at the time
|
|
127
|
+
* of the call) has settled. Callers that are about to perform their own
|
|
128
|
+
* competing full-document content write for the same file should await this
|
|
129
|
+
* first so their write is always computed from — and lands after — the
|
|
130
|
+
* shader's already-settled result, instead of racing it.
|
|
131
|
+
*/
|
|
132
|
+
export async function waitForShaderWriteToSettle(
|
|
133
|
+
fileId: string | undefined,
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
if (!fileId) return;
|
|
136
|
+
const pending = shaderWriteLocks.get(fileId);
|
|
137
|
+
if (pending) await pending.catch(() => {});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Registers `fn` as the in-flight shader write for `fileId` until it settles,
|
|
142
|
+
* chaining behind any already-registered write for the same file so two
|
|
143
|
+
* shader operations on one file stay ordered too. Mirrors the identity-guard
|
|
144
|
+
* pattern DesignEditor.tsx's fileSaveChainsRef already uses for update-file
|
|
145
|
+
* saves: only clear the registry entry if nothing newer replaced it while
|
|
146
|
+
* this one was running.
|
|
147
|
+
*/
|
|
148
|
+
function withShaderWriteLock<T>(
|
|
149
|
+
fileId: string,
|
|
150
|
+
fn: () => Promise<T>,
|
|
151
|
+
): Promise<T> {
|
|
152
|
+
const previous = shaderWriteLocks.get(fileId) ?? Promise.resolve();
|
|
153
|
+
const run = previous.catch(() => {}).then(fn);
|
|
154
|
+
const settleMarker = run.then(
|
|
155
|
+
() => undefined,
|
|
156
|
+
() => undefined,
|
|
157
|
+
);
|
|
158
|
+
shaderWriteLocks.set(fileId, settleMarker);
|
|
159
|
+
void settleMarker.finally(() => {
|
|
160
|
+
if (shaderWriteLocks.get(fileId) === settleMarker) {
|
|
161
|
+
shaderWriteLocks.delete(fileId);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return run;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── Context contract ─────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
export interface GlslShaderPanelContext {
|
|
170
|
+
designId?: string;
|
|
171
|
+
fileId?: string;
|
|
172
|
+
/** Stable data-agent-native-node-id of the target element. */
|
|
173
|
+
nodeId?: string;
|
|
174
|
+
selector?: string;
|
|
175
|
+
/**
|
|
176
|
+
* Called after a persisted apply with the patched file content so the host
|
|
177
|
+
* editor can sync local/collab state (same contract as component prop
|
|
178
|
+
* edits' onComponentPropApplied).
|
|
179
|
+
*/
|
|
180
|
+
onApplied?: (fileId: string, content: string, updatedAt?: string) => void;
|
|
181
|
+
/**
|
|
182
|
+
* Optional: focus the Code panel on this screen's source so the user can
|
|
183
|
+
* edit the GLSL directly. When absent the Edit-code affordance still
|
|
184
|
+
* explains where the source lives.
|
|
185
|
+
*/
|
|
186
|
+
onEditCode?: (shaderId: string) => void;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Screen-source plumbing ───────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
interface SourceFileResult {
|
|
192
|
+
fileId?: string;
|
|
193
|
+
path?: string;
|
|
194
|
+
content?: string;
|
|
195
|
+
versionHash?: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Broadcast a bridge message to every screen iframe (runtime filters by target). */
|
|
199
|
+
export function broadcastShaderMessage(message: Record<string, unknown>): void {
|
|
200
|
+
if (typeof document === "undefined") return;
|
|
201
|
+
const frames = document.querySelectorAll<HTMLIFrameElement>("iframe");
|
|
202
|
+
frames.forEach((frame) => {
|
|
203
|
+
try {
|
|
204
|
+
frame.contentWindow?.postMessage(message, "*");
|
|
205
|
+
} catch {
|
|
206
|
+
/* inaccessible frame — ignore */
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Read the target screen's saved shaders + element mounts. Co-located data
|
|
213
|
+
* hook for the shader picker and the effects rows.
|
|
214
|
+
*/
|
|
215
|
+
export function useScreenGlslShaders(context: GlslShaderPanelContext) {
|
|
216
|
+
const enabled = Boolean(context.designId && context.fileId);
|
|
217
|
+
const query = useActionQuery<SourceFileResult>(
|
|
218
|
+
"read-source-file",
|
|
219
|
+
{ designId: context.designId ?? "", fileId: context.fileId ?? "" },
|
|
220
|
+
{ enabled },
|
|
221
|
+
);
|
|
222
|
+
const content = enabled ? (query.data?.content ?? "") : "";
|
|
223
|
+
const shaders = useMemo(() => listShadersInHtml(content), [content]);
|
|
224
|
+
const mounts = useMemo(() => listShaderMounts(content), [content]);
|
|
225
|
+
return { ...query, enabled, content, shaders, mounts };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* One persisted shader write: fresh read → pure HTML transform → guarded
|
|
230
|
+
* full-replace write → host sync + iframe rescan.
|
|
231
|
+
*/
|
|
232
|
+
export function usePersistShaderEdit(context: GlslShaderPanelContext) {
|
|
233
|
+
const t = useT();
|
|
234
|
+
const applyEdit = useActionMutation("apply-source-edit");
|
|
235
|
+
const [busy, setBusy] = useState(false);
|
|
236
|
+
|
|
237
|
+
const persist = async (
|
|
238
|
+
transform: (html: string) => { html: string; errors: string[] },
|
|
239
|
+
): Promise<boolean> => {
|
|
240
|
+
if (!context.designId || !context.fileId) {
|
|
241
|
+
toast.error(t("editPanel.shaders.selectElementFirst"));
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
const fileId = context.fileId;
|
|
245
|
+
const designId = context.designId;
|
|
246
|
+
setBusy(true);
|
|
247
|
+
try {
|
|
248
|
+
// Serialize against any other in-flight shader write for this file, AND
|
|
249
|
+
// register this operation in the shared registry so DesignEditor.tsx's
|
|
250
|
+
// commitVisualStyles (Add layer / Remove layer / any other base style
|
|
251
|
+
// commit) can await waitForShaderWriteToSettle(fileId) before doing its
|
|
252
|
+
// own competing full-document ydoc rewrite — see the module doc comment
|
|
253
|
+
// above for why an un-serialized race here corrupts the document.
|
|
254
|
+
return await withShaderWriteLock(fileId, async () => {
|
|
255
|
+
// read-source-file is a read-only GET action (see useScreenGlslShaders'
|
|
256
|
+
// useActionQuery above and the code-workbench inline provider's
|
|
257
|
+
// readFile) — it must be called imperatively via callAction with
|
|
258
|
+
// method: "GET", never useActionMutation (which always POSTs and the
|
|
259
|
+
// server rejects with "Method not allowed. Use GET.").
|
|
260
|
+
const source = await callAction<SourceFileResult>(
|
|
261
|
+
"read-source-file",
|
|
262
|
+
{ designId, fileId },
|
|
263
|
+
{ method: "GET" },
|
|
264
|
+
);
|
|
265
|
+
const baseHtml = source.content ?? "";
|
|
266
|
+
const transformed = transform(baseHtml);
|
|
267
|
+
if (transformed.errors.length > 0) {
|
|
268
|
+
toast.error(transformed.errors[0]);
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
if (transformed.html === baseHtml) return true;
|
|
272
|
+
const written = (await applyEdit.mutateAsync({
|
|
273
|
+
designId,
|
|
274
|
+
fileId,
|
|
275
|
+
edit: { kind: "full-replace", content: transformed.html },
|
|
276
|
+
...(source.versionHash
|
|
277
|
+
? { expectedVersionHash: source.versionHash }
|
|
278
|
+
: {}),
|
|
279
|
+
})) as { fileId?: string; updatedAt?: string };
|
|
280
|
+
context.onApplied?.(
|
|
281
|
+
written.fileId ?? fileId,
|
|
282
|
+
transformed.html,
|
|
283
|
+
typeof written.updatedAt === "string" ? written.updatedAt : undefined,
|
|
284
|
+
);
|
|
285
|
+
broadcastShaderMessage({ type: "glsl-shader-preview-clear" });
|
|
286
|
+
broadcastShaderMessage({ type: "glsl-shader-rescan" });
|
|
287
|
+
return true;
|
|
288
|
+
});
|
|
289
|
+
} catch (error) {
|
|
290
|
+
toast.error(
|
|
291
|
+
error instanceof Error
|
|
292
|
+
? error.message
|
|
293
|
+
: t("editPanel.shaders.saveFailed"),
|
|
294
|
+
);
|
|
295
|
+
return false;
|
|
296
|
+
} finally {
|
|
297
|
+
setBusy(false);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
return { persist, busy };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── Small building blocks ────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
function normalizeHex(value: string): string {
|
|
307
|
+
const hex = value.trim();
|
|
308
|
+
if (/^#[0-9a-fA-F]{3}$/.test(hex)) {
|
|
309
|
+
return (
|
|
310
|
+
"#" +
|
|
311
|
+
hex
|
|
312
|
+
.slice(1)
|
|
313
|
+
.split("")
|
|
314
|
+
.map((c) => c + c)
|
|
315
|
+
.join("")
|
|
316
|
+
).toLowerCase();
|
|
317
|
+
}
|
|
318
|
+
if (/^#[0-9a-fA-F]{6}$/.test(hex)) return hex.toLowerCase();
|
|
319
|
+
return "#808080";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function ColorKnob({
|
|
323
|
+
label,
|
|
324
|
+
value,
|
|
325
|
+
disabled,
|
|
326
|
+
onChange,
|
|
327
|
+
}: {
|
|
328
|
+
label: string;
|
|
329
|
+
value: string;
|
|
330
|
+
disabled?: boolean;
|
|
331
|
+
onChange: (next: string, phase: "preview" | "commit") => void;
|
|
332
|
+
}) {
|
|
333
|
+
const hex = normalizeHex(value);
|
|
334
|
+
return (
|
|
335
|
+
<div className="flex h-6 items-center gap-1.5">
|
|
336
|
+
<span className="w-20 shrink-0 truncate !text-[11px] text-muted-foreground">
|
|
337
|
+
{label}
|
|
338
|
+
</span>
|
|
339
|
+
<label
|
|
340
|
+
className={cn(
|
|
341
|
+
"relative size-4 shrink-0 cursor-pointer overflow-hidden rounded-[3px] border border-[var(--design-editor-control-border)]",
|
|
342
|
+
disabled && "pointer-events-none opacity-40",
|
|
343
|
+
)}
|
|
344
|
+
style={{ background: hex }}
|
|
345
|
+
>
|
|
346
|
+
<input
|
|
347
|
+
type="color"
|
|
348
|
+
value={hex}
|
|
349
|
+
disabled={disabled}
|
|
350
|
+
aria-label={label}
|
|
351
|
+
className="absolute inset-0 size-full cursor-pointer opacity-0"
|
|
352
|
+
onChange={(event) => onChange(event.target.value, "preview")}
|
|
353
|
+
onBlur={(event) => onChange(event.target.value, "commit")}
|
|
354
|
+
/>
|
|
355
|
+
</label>
|
|
356
|
+
<Input
|
|
357
|
+
value={hex.toUpperCase()}
|
|
358
|
+
disabled={disabled}
|
|
359
|
+
aria-label={`${label} hex`}
|
|
360
|
+
className="h-6 min-w-0 flex-1 border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 !text-[11px] uppercase shadow-none md:!text-[11px]"
|
|
361
|
+
onChange={(event) => {
|
|
362
|
+
const next = event.target.value;
|
|
363
|
+
if (
|
|
364
|
+
/^#[0-9a-fA-F]{6}$/.test(next) ||
|
|
365
|
+
/^#[0-9a-fA-F]{3}$/.test(next)
|
|
366
|
+
) {
|
|
367
|
+
onChange(normalizeHex(next), "commit");
|
|
368
|
+
}
|
|
369
|
+
}}
|
|
370
|
+
/>
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Localized preset-category labels. `shared/shader-presets.ts` only carries
|
|
377
|
+
* the stable English `GLSL_SHADER_PRESET_CATEGORY_LABELS` (shared code, not
|
|
378
|
+
* i18n-aware), so the picker maps category -> catalog key here instead.
|
|
379
|
+
*/
|
|
380
|
+
function useShaderPresetCategoryLabel() {
|
|
381
|
+
const t = useT();
|
|
382
|
+
return (category: GlslShaderPresetCategory): string => {
|
|
383
|
+
switch (category) {
|
|
384
|
+
case "gradient-flow":
|
|
385
|
+
return t("editPanel.shaders.categories.gradientFlow");
|
|
386
|
+
case "waves":
|
|
387
|
+
return t("editPanel.shaders.categories.waves");
|
|
388
|
+
case "noise":
|
|
389
|
+
return t("editPanel.shaders.categories.noise");
|
|
390
|
+
case "pattern":
|
|
391
|
+
return t("editPanel.shaders.categories.pattern");
|
|
392
|
+
case "texture":
|
|
393
|
+
return t("editPanel.shaders.categories.texture");
|
|
394
|
+
case "retro":
|
|
395
|
+
return t("editPanel.shaders.categories.retro");
|
|
396
|
+
default:
|
|
397
|
+
return category;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function PresetThumb({
|
|
403
|
+
preset,
|
|
404
|
+
disabled,
|
|
405
|
+
onPick,
|
|
406
|
+
}: {
|
|
407
|
+
preset: GlslShaderPreset;
|
|
408
|
+
disabled?: boolean;
|
|
409
|
+
onPick: (preset: GlslShaderPreset) => void;
|
|
410
|
+
}) {
|
|
411
|
+
const categoryLabel = useShaderPresetCategoryLabel();
|
|
412
|
+
return (
|
|
413
|
+
<Tooltip>
|
|
414
|
+
<TooltipTrigger asChild>
|
|
415
|
+
<button
|
|
416
|
+
type="button"
|
|
417
|
+
disabled={disabled}
|
|
418
|
+
aria-label={preset.label}
|
|
419
|
+
onClick={() => onPick(preset)}
|
|
420
|
+
className={cn(
|
|
421
|
+
"group flex flex-col gap-1 text-left focus-visible:outline-none",
|
|
422
|
+
disabled && "pointer-events-none opacity-40",
|
|
423
|
+
)}
|
|
424
|
+
>
|
|
425
|
+
<div
|
|
426
|
+
className="aspect-[4/3] w-full rounded-md border border-border/60 transition-colors group-hover:border-foreground/40"
|
|
427
|
+
style={{ background: preset.previewCss }}
|
|
428
|
+
/>
|
|
429
|
+
<span className="truncate text-[10px] text-muted-foreground group-hover:text-foreground">
|
|
430
|
+
{preset.label}
|
|
431
|
+
</span>
|
|
432
|
+
</button>
|
|
433
|
+
</TooltipTrigger>
|
|
434
|
+
<TooltipContent>
|
|
435
|
+
{categoryLabel(preset.category)} — {preset.description}
|
|
436
|
+
</TooltipContent>
|
|
437
|
+
</Tooltip>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ─── Uniform knobs (shared by fill picker + effect rows) ─────────────────────
|
|
442
|
+
|
|
443
|
+
export function GlslShaderKnobs({
|
|
444
|
+
def,
|
|
445
|
+
values,
|
|
446
|
+
disabled,
|
|
447
|
+
onValuesChange,
|
|
448
|
+
}: {
|
|
449
|
+
def: GlslShaderDef;
|
|
450
|
+
values: Record<string, GlslUniformValue>;
|
|
451
|
+
disabled?: boolean;
|
|
452
|
+
/**
|
|
453
|
+
* phase "preview" = live scrub tick (cheap, broadcast-only);
|
|
454
|
+
* phase "commit" = gesture end (persist).
|
|
455
|
+
*/
|
|
456
|
+
onValuesChange: (
|
|
457
|
+
next: Record<string, GlslUniformValue>,
|
|
458
|
+
changedName: string,
|
|
459
|
+
phase: "preview" | "commit",
|
|
460
|
+
) => void;
|
|
461
|
+
}) {
|
|
462
|
+
const t = useT();
|
|
463
|
+
const entries = Object.entries(def.uniforms);
|
|
464
|
+
if (entries.length === 0) {
|
|
465
|
+
return (
|
|
466
|
+
<p className="px-0.5 py-1 !text-[11px] text-muted-foreground">
|
|
467
|
+
{t("editPanel.shaders.noUniforms")}
|
|
468
|
+
</p>
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
const emit = (
|
|
472
|
+
name: string,
|
|
473
|
+
value: GlslUniformValue,
|
|
474
|
+
phase: "preview" | "commit",
|
|
475
|
+
) => {
|
|
476
|
+
onValuesChange({ ...values, [name]: value }, name, phase);
|
|
477
|
+
};
|
|
478
|
+
return (
|
|
479
|
+
<div className="space-y-1">
|
|
480
|
+
{entries.map(([name, u]) => {
|
|
481
|
+
const label = u.label ?? name.replace(/^u_/, "").replace(/_/g, " ");
|
|
482
|
+
const current = values[name] ?? u.value;
|
|
483
|
+
if (u.type === "color") {
|
|
484
|
+
return (
|
|
485
|
+
<ColorKnob
|
|
486
|
+
key={name}
|
|
487
|
+
label={label}
|
|
488
|
+
value={typeof current === "string" ? current : "#808080"}
|
|
489
|
+
disabled={disabled}
|
|
490
|
+
onChange={(next, phase) => emit(name, next, phase)}
|
|
491
|
+
/>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
if (u.type === "vec2") {
|
|
495
|
+
const pair = Array.isArray(current) ? current : [0, 0];
|
|
496
|
+
const emitAxis = (
|
|
497
|
+
axis: 0 | 1,
|
|
498
|
+
value: number,
|
|
499
|
+
meta: ScrubInputChangeMeta,
|
|
500
|
+
) => {
|
|
501
|
+
const next: [number, number] = [pair[0] ?? 0, pair[1] ?? 0];
|
|
502
|
+
next[axis] = value;
|
|
503
|
+
emit(name, next, meta.phase);
|
|
504
|
+
};
|
|
505
|
+
return (
|
|
506
|
+
<div key={name} className="flex h-6 items-center gap-1.5">
|
|
507
|
+
<span className="w-20 shrink-0 truncate !text-[11px] text-muted-foreground">
|
|
508
|
+
{label}
|
|
509
|
+
</span>
|
|
510
|
+
<ScrubInput
|
|
511
|
+
label="X"
|
|
512
|
+
value={Number(pair[0]) || 0}
|
|
513
|
+
step={0.01}
|
|
514
|
+
precision={2}
|
|
515
|
+
disabled={disabled}
|
|
516
|
+
onChange={(value, meta) => emitAxis(0, value, meta)}
|
|
517
|
+
labelClassName="w-3"
|
|
518
|
+
inputClassName="h-6"
|
|
519
|
+
className="min-w-0 flex-1"
|
|
520
|
+
/>
|
|
521
|
+
<ScrubInput
|
|
522
|
+
label="Y"
|
|
523
|
+
value={Number(pair[1]) || 0}
|
|
524
|
+
step={0.01}
|
|
525
|
+
precision={2}
|
|
526
|
+
disabled={disabled}
|
|
527
|
+
onChange={(value, meta) => emitAxis(1, value, meta)}
|
|
528
|
+
labelClassName="w-3"
|
|
529
|
+
inputClassName="h-6"
|
|
530
|
+
className="min-w-0 flex-1"
|
|
531
|
+
/>
|
|
532
|
+
</div>
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
return (
|
|
536
|
+
<ScrubInput
|
|
537
|
+
key={name}
|
|
538
|
+
label={label}
|
|
539
|
+
value={typeof current === "number" ? current : Number(current) || 0}
|
|
540
|
+
min={u.min}
|
|
541
|
+
max={u.max}
|
|
542
|
+
step={u.step ?? 0.01}
|
|
543
|
+
precision={u.step !== undefined && u.step >= 1 ? 0 : 2}
|
|
544
|
+
disabled={disabled}
|
|
545
|
+
onChange={(value, meta) => emit(name, value, meta.phase)}
|
|
546
|
+
labelClassName="w-20"
|
|
547
|
+
inputClassName="h-6"
|
|
548
|
+
/>
|
|
549
|
+
);
|
|
550
|
+
})}
|
|
551
|
+
</div>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ─── Main panel ───────────────────────────────────────────────────────────────
|
|
556
|
+
|
|
557
|
+
export interface GlslShaderPanelProps {
|
|
558
|
+
mode: GlslShaderMode;
|
|
559
|
+
context: GlslShaderPanelContext;
|
|
560
|
+
/**
|
|
561
|
+
* Return to the previous picker view (fill picker) or close the popover.
|
|
562
|
+
* `hasShader` reports whether the target element has a persisted shader of
|
|
563
|
+
* this panel's mode — callers use it to decide whether to revert the paint
|
|
564
|
+
* type (never revert when a shader is actually applied).
|
|
565
|
+
*/
|
|
566
|
+
onBack: (hasShader: boolean) => void;
|
|
567
|
+
disabled?: boolean;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export function GlslShaderPanel({
|
|
571
|
+
mode,
|
|
572
|
+
context,
|
|
573
|
+
onBack,
|
|
574
|
+
disabled = false,
|
|
575
|
+
}: GlslShaderPanelProps) {
|
|
576
|
+
const t = useT();
|
|
577
|
+
const categoryLabel = useShaderPresetCategoryLabel();
|
|
578
|
+
const [search, setSearch] = useState("");
|
|
579
|
+
// View state: detail view shows whenever the target node has a shader of
|
|
580
|
+
// this mode (or one was just applied) UNLESS the user explicitly backed
|
|
581
|
+
// out to browse for a replacement.
|
|
582
|
+
const [browsing, setBrowsing] = useState(false);
|
|
583
|
+
const [justAppliedId, setJustAppliedId] = useState<string | null>(null);
|
|
584
|
+
const screen = useScreenGlslShaders(context);
|
|
585
|
+
const { persist, busy } = usePersistShaderEdit(context);
|
|
586
|
+
|
|
587
|
+
const nodeId = context.nodeId;
|
|
588
|
+
const modeAttrTitle =
|
|
589
|
+
mode === "effect"
|
|
590
|
+
? t("editPanel.shaders.effectsTitle")
|
|
591
|
+
: t("editPanel.shaders.fillsTitle");
|
|
592
|
+
|
|
593
|
+
// The shader currently mounted on the target node (persisted state).
|
|
594
|
+
const nodeMount = useMemo(
|
|
595
|
+
() =>
|
|
596
|
+
screen.mounts.find(
|
|
597
|
+
(mount) => mount.nodeId === nodeId && mount.mode === mode,
|
|
598
|
+
),
|
|
599
|
+
[screen.mounts, nodeId, mode],
|
|
600
|
+
);
|
|
601
|
+
const activeId = browsing
|
|
602
|
+
? null
|
|
603
|
+
: (justAppliedId ?? nodeMount?.shaderId ?? null);
|
|
604
|
+
const activeDef = useMemo(
|
|
605
|
+
() => screen.shaders.find((shader) => shader.id === activeId) ?? null,
|
|
606
|
+
[screen.shaders, activeId],
|
|
607
|
+
);
|
|
608
|
+
const [draftValues, setDraftValues] = useState<Record<
|
|
609
|
+
string,
|
|
610
|
+
GlslUniformValue
|
|
611
|
+
> | null>(null);
|
|
612
|
+
const values = useMemo(() => {
|
|
613
|
+
if (!activeDef) return {};
|
|
614
|
+
return {
|
|
615
|
+
...defaultUniformValues(activeDef),
|
|
616
|
+
...(nodeMount?.shaderId === activeDef.id ? (nodeMount.values ?? {}) : {}),
|
|
617
|
+
...(draftValues ?? {}),
|
|
618
|
+
};
|
|
619
|
+
}, [activeDef, nodeMount, draftValues]);
|
|
620
|
+
|
|
621
|
+
const presets = useMemo(() => {
|
|
622
|
+
const byMode = GLSL_SHADER_PRESETS.filter((preset) => preset.mode === mode);
|
|
623
|
+
const query = search.trim().toLowerCase();
|
|
624
|
+
if (!query) return byMode;
|
|
625
|
+
return byMode.filter(
|
|
626
|
+
(preset) =>
|
|
627
|
+
preset.label.toLowerCase().includes(query) ||
|
|
628
|
+
preset.description.toLowerCase().includes(query) ||
|
|
629
|
+
categoryLabel(preset.category).toLowerCase().includes(query),
|
|
630
|
+
);
|
|
631
|
+
}, [mode, search, categoryLabel]);
|
|
632
|
+
|
|
633
|
+
const savedShaders = useMemo(() => {
|
|
634
|
+
const byMode = screen.shaders.filter((shader) => shader.mode === mode);
|
|
635
|
+
const query = search.trim().toLowerCase();
|
|
636
|
+
if (!query) return byMode;
|
|
637
|
+
return byMode.filter((shader) => shader.name.toLowerCase().includes(query));
|
|
638
|
+
}, [screen.shaders, mode, search]);
|
|
639
|
+
|
|
640
|
+
const fallbackFromDef = (def: GlslShaderDef): string | undefined => {
|
|
641
|
+
for (const u of Object.values(def.uniforms)) {
|
|
642
|
+
if (u.type === "color" && typeof u.value === "string") return u.value;
|
|
643
|
+
}
|
|
644
|
+
return undefined;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const applyDef = async (def: GlslShaderDef) => {
|
|
648
|
+
if (!nodeId) {
|
|
649
|
+
toast.error(t("editPanel.shaders.selectCanvasElementFirst"));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const ok = await persist((html) =>
|
|
653
|
+
applyShaderToHtml(html, {
|
|
654
|
+
nodeId,
|
|
655
|
+
def,
|
|
656
|
+
...(def.mode === "fill" ? { fallbackColor: fallbackFromDef(def) } : {}),
|
|
657
|
+
}),
|
|
658
|
+
);
|
|
659
|
+
if (ok) {
|
|
660
|
+
setJustAppliedId(def.id);
|
|
661
|
+
setBrowsing(false);
|
|
662
|
+
setDraftValues(null);
|
|
663
|
+
void screen.refetch();
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
const applyPreset = (preset: GlslShaderPreset) => {
|
|
668
|
+
void applyDef({
|
|
669
|
+
id: newShaderId(),
|
|
670
|
+
name: preset.label,
|
|
671
|
+
mode,
|
|
672
|
+
glsl: preset.glsl,
|
|
673
|
+
uniforms: preset.uniforms,
|
|
674
|
+
});
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const applySaved = (def: GlslShaderDef) => {
|
|
678
|
+
void applyDef(def);
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const removeFromNode = async () => {
|
|
682
|
+
if (!nodeId) return;
|
|
683
|
+
// Scope removal to this panel's mode — a fill and an effect can coexist
|
|
684
|
+
// on one node (see shared/shader-fills.ts), so clearing the fill picker
|
|
685
|
+
// must not also wipe a coexisting shader effect (and vice versa).
|
|
686
|
+
const ok = await persist((html) =>
|
|
687
|
+
removeShaderFromNode(html, nodeId, mode),
|
|
688
|
+
);
|
|
689
|
+
if (ok) {
|
|
690
|
+
setJustAppliedId(null);
|
|
691
|
+
setBrowsing(true);
|
|
692
|
+
setDraftValues(null);
|
|
693
|
+
void screen.refetch();
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
const createWithAi = () => {
|
|
698
|
+
sendToDesignAgentChat({
|
|
699
|
+
message:
|
|
700
|
+
mode === "effect"
|
|
701
|
+
? "Create a custom shader effect for the selected element."
|
|
702
|
+
: "Create a custom shader fill for the selected element.",
|
|
703
|
+
context: [
|
|
704
|
+
"Use the code-backed GLSL shader format from the shader-fills skill:",
|
|
705
|
+
'persist a <script type="application/x-agent-native-shader"> block',
|
|
706
|
+
"(uniforms manifest comment + GLSL fragment source) in the screen",
|
|
707
|
+
"HTML and reference it from the element.",
|
|
708
|
+
context.designId ? `designId: ${context.designId}` : "",
|
|
709
|
+
context.fileId ? `fileId: ${context.fileId}` : "",
|
|
710
|
+
nodeId ? `target nodeId (data-agent-native-node-id): ${nodeId}` : "",
|
|
711
|
+
`mode: ${mode}`,
|
|
712
|
+
]
|
|
713
|
+
.filter(Boolean)
|
|
714
|
+
.join("\n"),
|
|
715
|
+
submit: false,
|
|
716
|
+
});
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const handleValuesChange = (
|
|
720
|
+
next: Record<string, GlslUniformValue>,
|
|
721
|
+
changedName: string,
|
|
722
|
+
phase: "preview" | "commit",
|
|
723
|
+
) => {
|
|
724
|
+
if (!activeDef) return;
|
|
725
|
+
setDraftValues(next);
|
|
726
|
+
// Live-update the mounted shader in whichever iframe hosts the node.
|
|
727
|
+
broadcastShaderMessage({
|
|
728
|
+
type: "glsl-shader-set-uniform",
|
|
729
|
+
filter: { shaderId: activeDef.id, ...(nodeId ? { nodeId } : {}) },
|
|
730
|
+
name: changedName,
|
|
731
|
+
value: next[changedName],
|
|
732
|
+
});
|
|
733
|
+
if (phase === "commit" && nodeId) {
|
|
734
|
+
void persist((html) =>
|
|
735
|
+
annotateNodeWithShader(html, {
|
|
736
|
+
nodeId,
|
|
737
|
+
shaderId: activeDef.id,
|
|
738
|
+
mode,
|
|
739
|
+
values: next,
|
|
740
|
+
}),
|
|
741
|
+
).then((ok) => {
|
|
742
|
+
if (ok) void screen.refetch();
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
// ── Detail view — knobs for the applied shader ────────────────────────────
|
|
748
|
+
if (activeDef) {
|
|
749
|
+
return (
|
|
750
|
+
<div className="flex flex-col">
|
|
751
|
+
<div className="flex h-6 items-center gap-1.5 px-3">
|
|
752
|
+
<button
|
|
753
|
+
type="button"
|
|
754
|
+
aria-label={t("editPanel.shaders.backToBrowser")}
|
|
755
|
+
onClick={() => {
|
|
756
|
+
setBrowsing(true);
|
|
757
|
+
setJustAppliedId(null);
|
|
758
|
+
setDraftValues(null);
|
|
759
|
+
}}
|
|
760
|
+
className="flex size-5 items-center justify-center rounded text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
761
|
+
>
|
|
762
|
+
<IconArrowLeft className="size-3.5" />
|
|
763
|
+
</button>
|
|
764
|
+
<span className="flex-1 truncate !text-[11px] font-semibold text-foreground">
|
|
765
|
+
{activeDef.name}
|
|
766
|
+
</span>
|
|
767
|
+
{context.onEditCode ? (
|
|
768
|
+
<Tooltip>
|
|
769
|
+
<TooltipTrigger asChild>
|
|
770
|
+
<button
|
|
771
|
+
type="button"
|
|
772
|
+
aria-label={t("editPanel.shaders.editCode")}
|
|
773
|
+
onClick={() => context.onEditCode?.(activeDef.id)}
|
|
774
|
+
className="flex size-5 items-center justify-center rounded text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
775
|
+
>
|
|
776
|
+
<IconCode className="size-3.5" />
|
|
777
|
+
</button>
|
|
778
|
+
</TooltipTrigger>
|
|
779
|
+
<TooltipContent>{t("editPanel.shaders.editCode")}</TooltipContent>
|
|
780
|
+
</Tooltip>
|
|
781
|
+
) : null}
|
|
782
|
+
<Tooltip>
|
|
783
|
+
<TooltipTrigger asChild>
|
|
784
|
+
<button
|
|
785
|
+
type="button"
|
|
786
|
+
aria-label={t("editPanel.shaders.removeShader")}
|
|
787
|
+
disabled={disabled || busy || !nodeMount}
|
|
788
|
+
onClick={() => void removeFromNode()}
|
|
789
|
+
className="flex size-5 items-center justify-center rounded text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-destructive focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40"
|
|
790
|
+
>
|
|
791
|
+
<IconTrash className="size-3" />
|
|
792
|
+
</button>
|
|
793
|
+
</TooltipTrigger>
|
|
794
|
+
<TooltipContent>
|
|
795
|
+
{t("editPanel.shaders.removeShader")}
|
|
796
|
+
</TooltipContent>
|
|
797
|
+
</Tooltip>
|
|
798
|
+
<button
|
|
799
|
+
type="button"
|
|
800
|
+
aria-label={t("editPanel.shaders.closePanel")}
|
|
801
|
+
onClick={() => onBack(Boolean(nodeMount))}
|
|
802
|
+
className="flex size-5 items-center justify-center rounded text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
803
|
+
>
|
|
804
|
+
<IconX className="size-3" />
|
|
805
|
+
</button>
|
|
806
|
+
</div>
|
|
807
|
+
<div className="space-y-2 border-t border-border/70 p-2">
|
|
808
|
+
<GlslShaderKnobs
|
|
809
|
+
def={activeDef}
|
|
810
|
+
values={values}
|
|
811
|
+
disabled={disabled || busy}
|
|
812
|
+
onValuesChange={handleValuesChange}
|
|
813
|
+
/>
|
|
814
|
+
{!context.onEditCode ? (
|
|
815
|
+
<p className="px-0.5 !text-[10px] leading-snug text-muted-foreground">
|
|
816
|
+
{t("editPanel.shaders.codeHint")}
|
|
817
|
+
</p>
|
|
818
|
+
) : null}
|
|
819
|
+
</div>
|
|
820
|
+
</div>
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ── Browse view ────────────────────────────────────────────────────────────
|
|
825
|
+
return (
|
|
826
|
+
<div className="flex flex-col">
|
|
827
|
+
{/* Header */}
|
|
828
|
+
<div className="flex h-6 items-center gap-1 px-3">
|
|
829
|
+
<span className="flex-1 truncate !text-[11px] font-semibold text-foreground">
|
|
830
|
+
{modeAttrTitle}
|
|
831
|
+
</span>
|
|
832
|
+
<button
|
|
833
|
+
type="button"
|
|
834
|
+
aria-label={t("editPanel.shaders.closePanel")}
|
|
835
|
+
onClick={() => onBack(Boolean(nodeMount))}
|
|
836
|
+
className="flex size-5 items-center justify-center rounded text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
837
|
+
>
|
|
838
|
+
<IconX className="size-3" />
|
|
839
|
+
</button>
|
|
840
|
+
</div>
|
|
841
|
+
|
|
842
|
+
{/* Search */}
|
|
843
|
+
<div className="border-t border-border/70 px-3 py-2">
|
|
844
|
+
<div className="flex h-6 items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2">
|
|
845
|
+
<IconSearch className="size-3 shrink-0 text-muted-foreground" />
|
|
846
|
+
<Input
|
|
847
|
+
value={search}
|
|
848
|
+
disabled={disabled}
|
|
849
|
+
placeholder={"Search" /* i18n-ignore */}
|
|
850
|
+
aria-label={"Search shaders" /* i18n-ignore */}
|
|
851
|
+
className="h-full min-w-0 flex-1 border-0 bg-transparent p-0 !text-[11px] shadow-none focus-visible:ring-0 md:!text-[11px]"
|
|
852
|
+
onChange={(event) => setSearch(event.target.value)}
|
|
853
|
+
/>
|
|
854
|
+
{search && (
|
|
855
|
+
<button
|
|
856
|
+
type="button"
|
|
857
|
+
aria-label={"Clear search" /* i18n-ignore */}
|
|
858
|
+
onClick={() => setSearch("")}
|
|
859
|
+
className="flex size-4 items-center justify-center rounded text-muted-foreground hover:text-foreground"
|
|
860
|
+
>
|
|
861
|
+
<IconX className="size-3" />
|
|
862
|
+
</button>
|
|
863
|
+
)}
|
|
864
|
+
</div>
|
|
865
|
+
</div>
|
|
866
|
+
|
|
867
|
+
<div className="max-h-[380px] overflow-y-auto px-3 pb-3">
|
|
868
|
+
{/* Created by you */}
|
|
869
|
+
{!search && (
|
|
870
|
+
<section className="mb-3">
|
|
871
|
+
<p className="mb-1.5 text-[10px] font-semibold text-muted-foreground">
|
|
872
|
+
{t("editPanel.shaders.createdByYou")}
|
|
873
|
+
</p>
|
|
874
|
+
<div className="grid grid-cols-2 gap-2">
|
|
875
|
+
<button
|
|
876
|
+
type="button"
|
|
877
|
+
disabled={disabled}
|
|
878
|
+
onClick={createWithAi}
|
|
879
|
+
className={cn(
|
|
880
|
+
"group relative flex aspect-[4/3] w-full flex-col items-center justify-center gap-1 rounded-md border border-dashed border-[var(--design-editor-control-border)] text-muted-foreground transition-colors",
|
|
881
|
+
"hover:border-foreground/40 hover:text-foreground",
|
|
882
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
883
|
+
disabled && "pointer-events-none opacity-40",
|
|
884
|
+
)}
|
|
885
|
+
>
|
|
886
|
+
<span className="absolute right-1.5 top-1.5 rounded bg-[var(--design-editor-control-bg)] px-1 py-px text-[9px] font-semibold leading-none text-muted-foreground">
|
|
887
|
+
{t("editPanel.shaders.ai")}
|
|
888
|
+
</span>
|
|
889
|
+
<IconPlus className="size-4" />
|
|
890
|
+
<span className="text-[10px]">
|
|
891
|
+
{t("editPanel.shaders.createNew")}
|
|
892
|
+
</span>
|
|
893
|
+
</button>
|
|
894
|
+
{savedShaders.map((shader) => (
|
|
895
|
+
<Tooltip key={shader.id}>
|
|
896
|
+
<TooltipTrigger asChild>
|
|
897
|
+
<button
|
|
898
|
+
type="button"
|
|
899
|
+
disabled={disabled || busy}
|
|
900
|
+
aria-label={shader.name}
|
|
901
|
+
onClick={() => applySaved(shader)}
|
|
902
|
+
className={cn(
|
|
903
|
+
"group flex flex-col gap-1 text-left focus-visible:outline-none",
|
|
904
|
+
(disabled || busy) && "pointer-events-none opacity-40",
|
|
905
|
+
)}
|
|
906
|
+
>
|
|
907
|
+
<div className="flex aspect-[4/3] w-full items-center justify-center rounded-md border border-border/60 bg-[var(--design-editor-control-bg)] transition-colors group-hover:border-foreground/40">
|
|
908
|
+
<IconWaveSine className="size-4 text-muted-foreground" />
|
|
909
|
+
</div>
|
|
910
|
+
<span className="truncate text-[10px] text-muted-foreground group-hover:text-foreground">
|
|
911
|
+
{shader.name}
|
|
912
|
+
</span>
|
|
913
|
+
</button>
|
|
914
|
+
</TooltipTrigger>
|
|
915
|
+
<TooltipContent>
|
|
916
|
+
{t("editPanel.shaders.savedInThisDesign")}
|
|
917
|
+
</TooltipContent>
|
|
918
|
+
</Tooltip>
|
|
919
|
+
))}
|
|
920
|
+
</div>
|
|
921
|
+
</section>
|
|
922
|
+
)}
|
|
923
|
+
|
|
924
|
+
{/* Saved shaders matching a search */}
|
|
925
|
+
{search && savedShaders.length > 0 && (
|
|
926
|
+
<section className="mb-3">
|
|
927
|
+
<p className="mb-1.5 text-[10px] font-semibold text-muted-foreground">
|
|
928
|
+
{t("editPanel.shaders.createdByYou")}
|
|
929
|
+
</p>
|
|
930
|
+
<div className="grid grid-cols-2 gap-2">
|
|
931
|
+
{savedShaders.map((shader) => (
|
|
932
|
+
<button
|
|
933
|
+
key={shader.id}
|
|
934
|
+
type="button"
|
|
935
|
+
disabled={disabled || busy}
|
|
936
|
+
aria-label={shader.name}
|
|
937
|
+
onClick={() => applySaved(shader)}
|
|
938
|
+
className="group flex flex-col gap-1 text-left focus-visible:outline-none"
|
|
939
|
+
>
|
|
940
|
+
<div className="flex aspect-[4/3] w-full items-center justify-center rounded-md border border-border/60 bg-[var(--design-editor-control-bg)] transition-colors group-hover:border-foreground/40">
|
|
941
|
+
<IconWaveSine className="size-4 text-muted-foreground" />
|
|
942
|
+
</div>
|
|
943
|
+
<span className="truncate text-[10px] text-muted-foreground group-hover:text-foreground">
|
|
944
|
+
{shader.name}
|
|
945
|
+
</span>
|
|
946
|
+
</button>
|
|
947
|
+
))}
|
|
948
|
+
</div>
|
|
949
|
+
</section>
|
|
950
|
+
)}
|
|
951
|
+
|
|
952
|
+
{/* Presets */}
|
|
953
|
+
<section>
|
|
954
|
+
<p className="mb-1.5 text-[10px] font-semibold text-muted-foreground">
|
|
955
|
+
{t("editPanel.shaders.presets")}
|
|
956
|
+
</p>
|
|
957
|
+
{presets.length === 0 ? (
|
|
958
|
+
<p className="py-4 text-center !text-[11px] text-muted-foreground">
|
|
959
|
+
{t("editPanel.shaders.noMatches")}
|
|
960
|
+
</p>
|
|
961
|
+
) : (
|
|
962
|
+
<div className="grid grid-cols-2 gap-2">
|
|
963
|
+
{presets.map((preset) => (
|
|
964
|
+
<PresetThumb
|
|
965
|
+
key={preset.name}
|
|
966
|
+
preset={preset}
|
|
967
|
+
disabled={disabled || busy}
|
|
968
|
+
onPick={applyPreset}
|
|
969
|
+
/>
|
|
970
|
+
))}
|
|
971
|
+
</div>
|
|
972
|
+
)}
|
|
973
|
+
</section>
|
|
974
|
+
</div>
|
|
975
|
+
</div>
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// ─── Effects-section integration ─────────────────────────────────────────────
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Shader rows + picker for EditPanel's Effects section. Rendered alongside
|
|
983
|
+
* the shadow/blur rows; the "Add effect ▸ Shader" menu item controls
|
|
984
|
+
* `pickerOpen`. One shader effect per element (it can coexist with a shader
|
|
985
|
+
* fill — different attribute namespaces).
|
|
986
|
+
*/
|
|
987
|
+
export function GlslShaderEffectSection({
|
|
988
|
+
context,
|
|
989
|
+
pickerOpen,
|
|
990
|
+
onPickerOpenChange,
|
|
991
|
+
disabled = false,
|
|
992
|
+
}: {
|
|
993
|
+
context: GlslShaderPanelContext;
|
|
994
|
+
pickerOpen: boolean;
|
|
995
|
+
onPickerOpenChange: (open: boolean) => void;
|
|
996
|
+
disabled?: boolean;
|
|
997
|
+
}) {
|
|
998
|
+
const t = useT();
|
|
999
|
+
const screen = useScreenGlslShaders(context);
|
|
1000
|
+
const { persist, busy } = usePersistShaderEdit(context);
|
|
1001
|
+
const nodeId = context.nodeId;
|
|
1002
|
+
|
|
1003
|
+
const effectMount = useMemo(
|
|
1004
|
+
() =>
|
|
1005
|
+
screen.mounts.find(
|
|
1006
|
+
(mount) => mount.nodeId === nodeId && mount.mode === "effect",
|
|
1007
|
+
),
|
|
1008
|
+
[screen.mounts, nodeId],
|
|
1009
|
+
);
|
|
1010
|
+
const effectDef = useMemo(
|
|
1011
|
+
() =>
|
|
1012
|
+
effectMount
|
|
1013
|
+
? (screen.shaders.find(
|
|
1014
|
+
(shader) => shader.id === effectMount.shaderId,
|
|
1015
|
+
) ?? null)
|
|
1016
|
+
: null,
|
|
1017
|
+
[screen.shaders, effectMount],
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
const removeEffect = () => {
|
|
1021
|
+
if (!nodeId) return;
|
|
1022
|
+
void persist((html) => removeShaderFromNode(html, nodeId, "effect")).then(
|
|
1023
|
+
(ok) => {
|
|
1024
|
+
if (ok) void screen.refetch();
|
|
1025
|
+
},
|
|
1026
|
+
);
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
if (!effectMount && !pickerOpen) return null;
|
|
1030
|
+
|
|
1031
|
+
return (
|
|
1032
|
+
<>
|
|
1033
|
+
{effectDef && effectMount ? (
|
|
1034
|
+
/* design effect row: [name + knobs popover trigger (flex-1)] [remove] */
|
|
1035
|
+
<Popover>
|
|
1036
|
+
<div className="group flex items-center gap-1.5">
|
|
1037
|
+
<PopoverTrigger asChild>
|
|
1038
|
+
<button
|
|
1039
|
+
type="button"
|
|
1040
|
+
className="flex h-6 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-left !text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)]"
|
|
1041
|
+
>
|
|
1042
|
+
<IconWaveSine className="size-3.5 shrink-0 text-muted-foreground" />
|
|
1043
|
+
<span className="min-w-0 flex-1 truncate font-medium text-foreground">
|
|
1044
|
+
{effectDef.name}
|
|
1045
|
+
</span>
|
|
1046
|
+
<span className="shrink-0 text-muted-foreground">
|
|
1047
|
+
{t("editPanel.labels.shaderEffectType")}
|
|
1048
|
+
</span>
|
|
1049
|
+
</button>
|
|
1050
|
+
</PopoverTrigger>
|
|
1051
|
+
<Tooltip>
|
|
1052
|
+
<TooltipTrigger asChild>
|
|
1053
|
+
<button
|
|
1054
|
+
type="button"
|
|
1055
|
+
aria-label={t("editPanel.shaders.removeShaderEffect")}
|
|
1056
|
+
disabled={disabled || busy}
|
|
1057
|
+
onClick={removeEffect}
|
|
1058
|
+
className="flex size-5 items-center justify-center rounded text-muted-foreground opacity-0 transition-opacity hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100 disabled:pointer-events-none"
|
|
1059
|
+
>
|
|
1060
|
+
<IconTrash className="size-3" />
|
|
1061
|
+
</button>
|
|
1062
|
+
</TooltipTrigger>
|
|
1063
|
+
<TooltipContent>
|
|
1064
|
+
{t("editPanel.shaders.removeShaderEffect")}
|
|
1065
|
+
</TooltipContent>
|
|
1066
|
+
</Tooltip>
|
|
1067
|
+
</div>
|
|
1068
|
+
<PopoverContent
|
|
1069
|
+
side="left"
|
|
1070
|
+
align="start"
|
|
1071
|
+
sideOffset={8}
|
|
1072
|
+
className="z-[10000] w-[252px] p-0 shadow-xl"
|
|
1073
|
+
>
|
|
1074
|
+
<GlslShaderPanel
|
|
1075
|
+
mode="effect"
|
|
1076
|
+
context={context}
|
|
1077
|
+
disabled={disabled}
|
|
1078
|
+
onBack={() => onPickerOpenChange(false)}
|
|
1079
|
+
/>
|
|
1080
|
+
</PopoverContent>
|
|
1081
|
+
</Popover>
|
|
1082
|
+
) : null}
|
|
1083
|
+
|
|
1084
|
+
{pickerOpen && !effectMount ? (
|
|
1085
|
+
/* Inline browse panel while choosing the first shader effect. */
|
|
1086
|
+
<div className="rounded-md border border-[var(--design-editor-control-border)] bg-popover py-1.5">
|
|
1087
|
+
<GlslShaderPanel
|
|
1088
|
+
mode="effect"
|
|
1089
|
+
context={context}
|
|
1090
|
+
disabled={disabled}
|
|
1091
|
+
onBack={() => onPickerOpenChange(false)}
|
|
1092
|
+
/>
|
|
1093
|
+
</div>
|
|
1094
|
+
) : null}
|
|
1095
|
+
</>
|
|
1096
|
+
);
|
|
1097
|
+
}
|