@agent-native/core 0.79.27 → 0.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +26 -0
- package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +41 -0
- package/corpus/core/docs/content/locales/de-DE/template-design.mdx +51 -0
- package/corpus/core/docs/content/locales/es-ES/template-design.mdx +50 -0
- package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +49 -0
- package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +44 -0
- package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +46 -0
- package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +44 -0
- package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +48 -0
- package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +36 -0
- package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +38 -0
- package/corpus/core/docs/content/template-design.mdx +45 -0
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/agent/engine/index.ts +1 -0
- package/corpus/core/src/agent/engine/registry.ts +40 -0
- package/corpus/core/src/agent/production-agent.ts +6 -2
- package/corpus/core/src/cli/design-connect.ts +490 -0
- package/corpus/core/src/cli/index.ts +16 -0
- package/corpus/core/src/cli/skills.ts +126 -3
- package/corpus/core/src/client/AssistantChat.tsx +43 -7
- package/corpus/core/src/client/agent-chat.ts +27 -3
- package/corpus/core/src/client/blocks/library/MermaidBlock.tsx +1 -1
- package/corpus/core/src/client/blocks/library/diagram.tsx +2 -2
- package/corpus/core/src/client/blocks/library/wireframe.tsx +1 -1
- package/corpus/core/src/client/chat/message-components.tsx +1 -1
- package/corpus/core/src/client/chat-model-groups.ts +1 -2
- package/corpus/core/src/client/components/LiveCursorOverlay.tsx +33 -66
- package/corpus/core/src/client/composer/TiptapComposer.tsx +99 -24
- package/corpus/core/src/client/composer/useVoiceDictation.ts +429 -260
- package/corpus/core/src/client/frame-protocol.ts +17 -2
- package/corpus/core/src/client/rich-markdown-editor/RegistryBlockNode.tsx +2 -2
- package/corpus/core/src/client/sharing/ShareButton.tsx +20 -15
- package/corpus/core/src/client/use-agent-engine-configured.ts +80 -28
- package/corpus/core/src/server/transcribe-voice.ts +57 -22
- package/corpus/core/src/shared/mcp-embed-headers.ts +23 -1
- package/corpus/core/src/styles/agent-native.css +56 -7
- package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
- package/corpus/core/src/vite/client.ts +2 -0
- package/corpus/core/src/voice/index.ts +18 -0
- package/corpus/core/src/voice/voice-cleanup-prompt.ts +38 -0
- package/corpus/core/src/voice/voice-context.ts +217 -0
- package/corpus/core/src/voice/voice-replacements.ts +75 -0
- package/corpus/templates/analytics/AGENTS.md +4 -3
- package/corpus/templates/analytics/actions/list-session-recordings.ts +6 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +557 -400
- package/corpus/templates/analytics/app/global.css +0 -4
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +20 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +33 -8
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +7 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +7 -0
- package/corpus/templates/analytics/changelog/2026-06-28-ask-tab-now-matches-the-darker-app-background.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-28-dashboard-email-reports-now-use-cleaner-light-mode-snapshots.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-28-sessions-now-show-only-signed-in-recordings-with-playable-re.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +29 -7
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +5 -2
- package/corpus/templates/analytics/server/lib/session-replay.ts +24 -4
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -4
- package/corpus/templates/assets/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
- package/corpus/templates/brain/app/components/layout/Layout.tsx +29 -2
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +131 -42
- package/corpus/templates/brain/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/brain/app/i18n-data.ts +20 -0
- package/corpus/templates/brain/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +29 -1
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +417 -299
- package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/calendar/app/i18n-data.ts +20 -0
- package/corpus/templates/calendar/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +6 -19
- package/corpus/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
- package/corpus/templates/clips/AGENTS.md +4 -0
- package/corpus/templates/clips/actions/cleanup-transcript.ts +39 -9
- package/corpus/templates/clips/actions/finalize-meeting.ts +81 -12
- package/corpus/templates/clips/actions/lib/agents-md-context.ts +4 -1
- package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -3
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +1 -1
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +2 -2
- package/corpus/templates/clips/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
- package/corpus/templates/clips/desktop/src/lib/personal-vocabulary.ts +13 -8
- package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +60 -1
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -1
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -5
- package/corpus/templates/content/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +137 -0
- package/corpus/templates/design/AGENTS.md +95 -5
- package/corpus/templates/design/DESIGN.md +82 -0
- package/corpus/templates/design/DEVELOPING.md +43 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +426 -0
- package/corpus/templates/design/actions/apply-shader.ts +277 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +303 -0
- package/corpus/templates/design/actions/connect-localhost.ts +183 -0
- package/corpus/templates/design/actions/edit-design.ts +195 -42
- package/corpus/templates/design/actions/generate-design.ts +71 -1
- package/corpus/templates/design/actions/generate-screens.ts +193 -0
- package/corpus/templates/design/actions/get-code-layer-projection.ts +191 -0
- package/corpus/templates/design/actions/get-shader.ts +119 -0
- package/corpus/templates/design/actions/list-localhost-connections.ts +95 -0
- package/corpus/templates/design/actions/navigate.ts +55 -2
- package/corpus/templates/design/actions/view-screen.ts +68 -1
- package/corpus/templates/design/agent-native.app-skill.json +10 -0
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +761 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +2057 -103
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +74 -38
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +399 -0
- package/corpus/templates/design/app/components/design/EditPanel.tsx +3185 -439
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +1124 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +3116 -147
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +700 -13
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +107 -63
- package/corpus/templates/design/app/components/design/index.ts +0 -5
- package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +286 -0
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +699 -0
- package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +461 -0
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +166 -0
- package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +1420 -0
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +420 -0
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +215 -0
- package/corpus/templates/design/app/components/design/inspector/ShaderControls.stories.tsx +71 -0
- package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +518 -0
- package/corpus/templates/design/app/components/design/inspector/ShaderEffectRow.tsx +106 -0
- package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +361 -0
- package/corpus/templates/design/app/components/design/inspector/index.ts +60 -0
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +201 -0
- package/corpus/templates/design/app/components/design/types.ts +23 -0
- package/corpus/templates/design/app/components/layout/Layout.tsx +24 -13
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +4 -4
- package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +474 -36
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +22 -5
- package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +24 -11
- package/corpus/templates/design/app/global.css +121 -3
- package/corpus/templates/design/app/hooks/use-agent-generating.ts +4 -0
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +130 -8
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +329 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +264 -0
- package/corpus/templates/design/app/i18n-data.ts +2790 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +5108 -851
- package/corpus/templates/design/changelog/2026-06-28-annotate-mode-now-combines-drawing-and-comment-pins-in-one-c.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-design-editor-panel-borders-are-cleaner-so-the-canvas-edge-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-drawing-prompts-now-reliably-open-and-submit-to-the-design-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-editor-app-menus-now-use-an-outline-agent-native-mark-with-b.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-layer-selection-now-updates-chat-context-without-opening-the.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-layer-selections-now-preserve-hidden-and-locked-canvas-state.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-handles-zooming-drawing-opening-and-dele.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-selects-screens-on-click-and-opens-them-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-screen-overview-zooms-into-the-active-screen-with-smoother-e.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-selected-element-chips-in-chat-are-shorter-and-easier-to-sca.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-the-design-editor-toolbar-is-more-compact-and-the-app-menu-m.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-at-the-top-of-the-design-sidebar-instead-of-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-in-the-right-inspector-beside-the-design-tab.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-active-sidebar-rows-use-soft-highlights.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-agents-can-now-open-screen-overview-focus-screens-and-add.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-agents-now-edit-broad-copy-changes-more-reliably.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-editor-extensions-now-run-inline-beside-the-artboard.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-sidebars-use-darker-chrome-and-chat-warns-immediately.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-visual-edit-can-open-localhost-routes-as-url-backed-screens.md +6 -0
- package/corpus/templates/design/package.json +1 -0
- package/corpus/templates/design/server/db/schema.ts +26 -0
- package/corpus/templates/design/server/lib/design-snapshot.ts +1 -1
- package/corpus/templates/design/server/plugins/agent-chat.ts +3 -0
- package/corpus/templates/design/shared/api.ts +27 -0
- package/corpus/templates/design/shared/canvas-frames.ts +103 -0
- package/corpus/templates/design/shared/canvas-math.ts +1058 -0
- package/corpus/templates/design/shared/code-layer.ts +2212 -0
- package/corpus/templates/design/shared/color-utils.ts +180 -0
- package/corpus/templates/design/shared/generation-session.ts +61 -0
- package/corpus/templates/design/shared/shader-presets.ts +782 -0
- package/corpus/templates/design/shared/shader-safety.ts +137 -0
- package/corpus/templates/design/shared/source-mode.ts +237 -0
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +149 -6
- package/corpus/templates/forms/app/i18n/ar-SA.ts +2 -0
- package/corpus/templates/forms/app/i18n/de-DE.ts +2 -0
- package/corpus/templates/forms/app/i18n/en-US.ts +2 -0
- package/corpus/templates/forms/app/i18n/es-ES.ts +2 -0
- package/corpus/templates/forms/app/i18n/fr-FR.ts +2 -0
- package/corpus/templates/forms/app/i18n/hi-IN.ts +2 -0
- package/corpus/templates/forms/app/i18n/ja-JP.ts +2 -0
- package/corpus/templates/forms/app/i18n/ko-KR.ts +2 -0
- package/corpus/templates/forms/app/i18n/pt-BR.ts +2 -0
- package/corpus/templates/forms/app/i18n/zh-CN.ts +2 -0
- package/corpus/templates/forms/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/forms/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
- package/corpus/templates/macros/app/components/layout/AppLayout.tsx +2 -2
- package/corpus/templates/macros/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +408 -247
- package/corpus/templates/mail/app/i18n/ar-SA.ts +4 -0
- package/corpus/templates/mail/app/i18n/de-DE.ts +4 -0
- package/corpus/templates/mail/app/i18n/en-US.ts +4 -0
- package/corpus/templates/mail/app/i18n/es-ES.ts +4 -0
- package/corpus/templates/mail/app/i18n/fr-FR.ts +4 -0
- package/corpus/templates/mail/app/i18n/hi-IN.ts +4 -0
- package/corpus/templates/mail/app/i18n/ja-JP.ts +4 -0
- package/corpus/templates/mail/app/i18n/ko-KR.ts +4 -0
- package/corpus/templates/mail/app/i18n/pt-BR.ts +4 -0
- package/corpus/templates/mail/app/i18n/zh-CN.ts +4 -0
- package/corpus/templates/mail/app/i18n/zh-TW.ts +4 -0
- package/corpus/templates/mail/changelog/2026-06-28-pinned-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +6 -8
- package/corpus/templates/plan/app/components/plan/wireframe/Wireframe.tsx +1 -1
- package/corpus/templates/plan/app/global.css +61 -78
- package/corpus/templates/plan/app/pages/PlansPage.tsx +8 -0
- package/corpus/templates/plan/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
- package/corpus/templates/plan/changelog/2026-06-28-plans-use-the-shared-neutral-light-and-dark-theme.md +6 -0
- package/corpus/templates/plan/changelog/2026-06-28-sending-open-feedback-from-a-plan-now-opens-the-inline-plan-.md +6 -0
- package/corpus/templates/slides/app/components/layout/Sidebar.tsx +5 -5
- package/corpus/templates/slides/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
- package/corpus/templates/videos/app/components/Sidebar.tsx +2 -2
- package/corpus/templates/videos/app/components/layout/Layout.tsx +29 -2
- package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +113 -30
- package/corpus/templates/videos/app/i18n/ar-SA.ts +2 -0
- package/corpus/templates/videos/app/i18n/de-DE.ts +2 -0
- package/corpus/templates/videos/app/i18n/en-US.ts +2 -0
- package/corpus/templates/videos/app/i18n/es-ES.ts +2 -0
- package/corpus/templates/videos/app/i18n/fr-FR.ts +2 -0
- package/corpus/templates/videos/app/i18n/hi-IN.ts +2 -0
- package/corpus/templates/videos/app/i18n/ja-JP.ts +2 -0
- package/corpus/templates/videos/app/i18n/ko-KR.ts +2 -0
- package/corpus/templates/videos/app/i18n/pt-BR.ts +2 -0
- package/corpus/templates/videos/app/i18n/zh-CN.ts +2 -0
- package/corpus/templates/videos/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/videos/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
- package/dist/agent/engine/index.d.ts +1 -1
- package/dist/agent/engine/index.d.ts.map +1 -1
- package/dist/agent/engine/index.js +1 -1
- package/dist/agent/engine/index.js.map +1 -1
- package/dist/agent/engine/registry.d.ts +9 -0
- package/dist/agent/engine/registry.d.ts.map +1 -1
- package/dist/agent/engine/registry.js +37 -0
- package/dist/agent/engine/registry.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +4 -3
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/design-connect.d.ts +52 -0
- package/dist/cli/design-connect.d.ts.map +1 -0
- package/dist/cli/design-connect.js +409 -0
- package/dist/cli/design-connect.js.map +1 -0
- package/dist/cli/index.js +15 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/skills.d.ts +3 -0
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +121 -3
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +38 -8
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +6 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +17 -3
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/blocks/library/MermaidBlock.js +1 -1
- package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
- package/dist/client/blocks/library/diagram.js +1 -1
- package/dist/client/blocks/library/diagram.js.map +1 -1
- package/dist/client/blocks/library/wireframe.js +1 -1
- package/dist/client/blocks/library/wireframe.js.map +1 -1
- package/dist/client/chat/message-components.js +1 -1
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/chat-model-groups.d.ts.map +1 -1
- package/dist/client/chat-model-groups.js +1 -2
- package/dist/client/chat-model-groups.js.map +1 -1
- package/dist/client/components/LiveCursorOverlay.d.ts +1 -2
- package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -1
- package/dist/client/components/LiveCursorOverlay.js +19 -40
- package/dist/client/components/LiveCursorOverlay.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +3 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +86 -24
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/composer/useVoiceDictation.d.ts +9 -0
- package/dist/client/composer/useVoiceDictation.d.ts.map +1 -1
- package/dist/client/composer/useVoiceDictation.js +154 -32
- package/dist/client/composer/useVoiceDictation.js.map +1 -1
- package/dist/client/frame-protocol.d.ts +17 -2
- package/dist/client/frame-protocol.d.ts.map +1 -1
- package/dist/client/frame-protocol.js.map +1 -1
- package/dist/client/rich-markdown-editor/RegistryBlockNode.js +2 -2
- package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
- package/dist/client/sharing/ShareButton.d.ts +2 -0
- package/dist/client/sharing/ShareButton.d.ts.map +1 -1
- package/dist/client/sharing/ShareButton.js +7 -6
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/client/use-agent-engine-configured.d.ts +5 -0
- package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
- package/dist/client/use-agent-engine-configured.js +51 -25
- package/dist/client/use-agent-engine-configured.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +8 -8
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts.map +1 -1
- package/dist/server/transcribe-voice.js +40 -24
- package/dist/server/transcribe-voice.js.map +1 -1
- package/dist/shared/mcp-embed-headers.d.ts +1 -0
- package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
- package/dist/shared/mcp-embed-headers.js +20 -1
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/styles/agent-native.css +56 -7
- package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +2 -0
- package/dist/vite/client.js.map +1 -1
- package/dist/voice/index.d.ts +5 -0
- package/dist/voice/index.d.ts.map +1 -0
- package/dist/voice/index.js +4 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/voice-cleanup-prompt.d.ts +6 -0
- package/dist/voice/voice-cleanup-prompt.d.ts.map +1 -0
- package/dist/voice/voice-cleanup-prompt.js +26 -0
- package/dist/voice/voice-cleanup-prompt.js.map +1 -0
- package/dist/voice/voice-context.d.ts +25 -0
- package/dist/voice/voice-context.d.ts.map +1 -0
- package/dist/voice/voice-context.js +156 -0
- package/dist/voice/voice-context.js.map +1 -0
- package/dist/voice/voice-replacements.d.ts +4 -0
- package/dist/voice/voice-replacements.d.ts.map +1 -0
- package/dist/voice/voice-replacements.js +52 -0
- package/dist/voice/voice-replacements.js.map +1 -0
- package/docs/content/locales/ar-SA/template-design.mdx +41 -0
- package/docs/content/locales/de-DE/template-design.mdx +51 -0
- package/docs/content/locales/es-ES/template-design.mdx +50 -0
- package/docs/content/locales/fr-FR/template-design.mdx +49 -0
- package/docs/content/locales/hi-IN/template-design.mdx +44 -0
- package/docs/content/locales/ja-JP/template-design.mdx +46 -0
- package/docs/content/locales/ko-KR/template-design.mdx +44 -0
- package/docs/content/locales/pt-BR/template-design.mdx +48 -0
- package/docs/content/locales/zh-CN/template-design.mdx +36 -0
- package/docs/content/locales/zh-TW/template-design.mdx +38 -0
- package/docs/content/template-design.mdx +45 -0
- package/package.json +2 -1
- package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
- package/corpus/templates/design/app/components/design/DesignToolbar.tsx +0 -339
- package/corpus/templates/design/app/components/design/PresentMode.tsx +0 -68
- package/corpus/templates/design/app/components/design/ViewportTabs.tsx +0 -149
- package/corpus/templates/design/app/components/design/ZoomControls.tsx +0 -157
|
@@ -0,0 +1,1420 @@
|
|
|
1
|
+
import {
|
|
2
|
+
alphaToOpacity,
|
|
3
|
+
parseCssColor,
|
|
4
|
+
rgbaToCss,
|
|
5
|
+
rgbaToHex,
|
|
6
|
+
rgbaToHsl,
|
|
7
|
+
hslToRgba,
|
|
8
|
+
opacityToAlpha,
|
|
9
|
+
withColorOpacity,
|
|
10
|
+
type HslaColor,
|
|
11
|
+
type RgbaColor,
|
|
12
|
+
} from "@shared/color-utils";
|
|
13
|
+
import { IconChevronDown, IconColorPicker, IconX } from "@tabler/icons-react";
|
|
14
|
+
import {
|
|
15
|
+
useEffect,
|
|
16
|
+
useRef,
|
|
17
|
+
useState,
|
|
18
|
+
type JSX,
|
|
19
|
+
type KeyboardEvent,
|
|
20
|
+
type PointerEvent,
|
|
21
|
+
} from "react";
|
|
22
|
+
|
|
23
|
+
import { Input } from "@/components/ui/input";
|
|
24
|
+
import {
|
|
25
|
+
Popover,
|
|
26
|
+
PopoverContent,
|
|
27
|
+
PopoverTrigger,
|
|
28
|
+
} from "@/components/ui/popover";
|
|
29
|
+
import {
|
|
30
|
+
Select,
|
|
31
|
+
SelectContent,
|
|
32
|
+
SelectItem,
|
|
33
|
+
SelectTrigger,
|
|
34
|
+
SelectValue,
|
|
35
|
+
} from "@/components/ui/select";
|
|
36
|
+
import {
|
|
37
|
+
Tooltip,
|
|
38
|
+
TooltipContent,
|
|
39
|
+
TooltipTrigger,
|
|
40
|
+
} from "@/components/ui/tooltip";
|
|
41
|
+
import { cn } from "@/lib/utils";
|
|
42
|
+
|
|
43
|
+
// ─── Public types ──────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export type FigmaColorMode = "hex" | "rgb" | "hsl" | "hsb";
|
|
46
|
+
export type FigmaGradientType = "linear" | "radial" | "angular" | "diamond";
|
|
47
|
+
export type FigmaFillType = "solid" | "gradient" | "image";
|
|
48
|
+
export type FigmaPaintType =
|
|
49
|
+
| "solid"
|
|
50
|
+
| "linear"
|
|
51
|
+
| "radial"
|
|
52
|
+
| "angular"
|
|
53
|
+
| "diamond"
|
|
54
|
+
| "image"
|
|
55
|
+
| "video"
|
|
56
|
+
| "none";
|
|
57
|
+
|
|
58
|
+
// These interfaces remain so EditPanel's prop types don't break, even though
|
|
59
|
+
// the popover no longer renders the fills/gradient-stops list.
|
|
60
|
+
export interface FigmaFillRow {
|
|
61
|
+
id: string;
|
|
62
|
+
label: string;
|
|
63
|
+
value: string;
|
|
64
|
+
type: FigmaFillType;
|
|
65
|
+
opacity?: number;
|
|
66
|
+
swatch?: string;
|
|
67
|
+
selected?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface FigmaFillRowPatch {
|
|
71
|
+
value?: string;
|
|
72
|
+
opacity?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface FigmaGradientStop {
|
|
76
|
+
id: string;
|
|
77
|
+
color: string;
|
|
78
|
+
position: number;
|
|
79
|
+
opacity?: number;
|
|
80
|
+
label?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface FigmaGradientStopPatch {
|
|
84
|
+
color?: string;
|
|
85
|
+
position?: number;
|
|
86
|
+
opacity?: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface FigmaColorPickerLabels {
|
|
90
|
+
trigger: string;
|
|
91
|
+
hex: string;
|
|
92
|
+
red: string;
|
|
93
|
+
green: string;
|
|
94
|
+
blue: string;
|
|
95
|
+
hue: string;
|
|
96
|
+
saturation: string;
|
|
97
|
+
saturationBrightness: string;
|
|
98
|
+
lightness: string;
|
|
99
|
+
brightness: string;
|
|
100
|
+
opacity: string;
|
|
101
|
+
blendMode: string;
|
|
102
|
+
fills: string;
|
|
103
|
+
addFill: string;
|
|
104
|
+
removeFill: string;
|
|
105
|
+
gradientType: string;
|
|
106
|
+
gradientStops: string;
|
|
107
|
+
addStop: string;
|
|
108
|
+
removeStop: string;
|
|
109
|
+
stopPosition: string;
|
|
110
|
+
linear: string;
|
|
111
|
+
radial: string;
|
|
112
|
+
angular: string;
|
|
113
|
+
diamond: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface FigmaColorPickerProps {
|
|
117
|
+
value: string;
|
|
118
|
+
onChange: (value: string) => void;
|
|
119
|
+
label?: string;
|
|
120
|
+
opacity?: number;
|
|
121
|
+
onOpacityChange?: (opacity: number) => void;
|
|
122
|
+
blendMode?: string;
|
|
123
|
+
onBlendModeChange?: (mode: string) => void;
|
|
124
|
+
showBlendMode?: boolean;
|
|
125
|
+
// Accepted but unused in the popover — list management lives in the sidebar.
|
|
126
|
+
fillRows?: FigmaFillRow[];
|
|
127
|
+
selectedFillId?: string;
|
|
128
|
+
onFillSelect?: (id: string) => void;
|
|
129
|
+
onFillChange?: (id: string, patch: FigmaFillRowPatch) => void;
|
|
130
|
+
onAddFill?: () => void;
|
|
131
|
+
onRemoveFill?: (id: string) => void;
|
|
132
|
+
paintType?: FigmaPaintType;
|
|
133
|
+
onPaintTypeChange?: (type: FigmaPaintType) => void;
|
|
134
|
+
gradientType?: FigmaGradientType;
|
|
135
|
+
onGradientTypeChange?: (type: FigmaGradientType) => void;
|
|
136
|
+
// Accepted but unused in the popover — gradient stop handles belong on canvas.
|
|
137
|
+
gradientStops?: FigmaGradientStop[];
|
|
138
|
+
selectedStopId?: string;
|
|
139
|
+
onGradientStopSelect?: (id: string) => void;
|
|
140
|
+
onGradientStopChange?: (id: string, patch: FigmaGradientStopPatch) => void;
|
|
141
|
+
onAddGradientStop?: () => void;
|
|
142
|
+
onRemoveGradientStop?: (id: string) => void;
|
|
143
|
+
labels?: Partial<FigmaColorPickerLabels>;
|
|
144
|
+
disabled?: boolean;
|
|
145
|
+
className?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── Internal types ────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
interface HsvaColor {
|
|
151
|
+
h: number;
|
|
152
|
+
s: number;
|
|
153
|
+
v: number;
|
|
154
|
+
a: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Constants ─────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
const FALLBACK_COLOR: RgbaColor = { r: 0, g: 0, b: 0, a: 1 };
|
|
160
|
+
|
|
161
|
+
const DEFAULT_LABELS: FigmaColorPickerLabels = {
|
|
162
|
+
trigger: "Open color picker", // i18n-ignore fallback component label
|
|
163
|
+
hex: "Hex", // i18n-ignore fallback component label
|
|
164
|
+
red: "R", // i18n-ignore fallback component label
|
|
165
|
+
green: "G", // i18n-ignore fallback component label
|
|
166
|
+
blue: "B", // i18n-ignore fallback component label
|
|
167
|
+
hue: "H", // i18n-ignore fallback component label
|
|
168
|
+
saturation: "S", // i18n-ignore fallback component label
|
|
169
|
+
saturationBrightness: "Saturation and brightness", // i18n-ignore fallback component label
|
|
170
|
+
lightness: "L", // i18n-ignore fallback component label
|
|
171
|
+
brightness: "B", // i18n-ignore fallback component label
|
|
172
|
+
opacity: "Opacity", // i18n-ignore fallback component label
|
|
173
|
+
blendMode: "Blend", // i18n-ignore fallback component label
|
|
174
|
+
fills: "Fills", // i18n-ignore fallback component label
|
|
175
|
+
addFill: "Add fill", // i18n-ignore fallback component label
|
|
176
|
+
removeFill: "Remove fill", // i18n-ignore fallback component label
|
|
177
|
+
gradientType: "Type", // i18n-ignore fallback component label
|
|
178
|
+
gradientStops: "Gradient stops", // i18n-ignore fallback component label
|
|
179
|
+
addStop: "Add stop", // i18n-ignore fallback component label
|
|
180
|
+
removeStop: "Remove stop", // i18n-ignore fallback component label
|
|
181
|
+
stopPosition: "Position", // i18n-ignore fallback component label
|
|
182
|
+
linear: "Linear", // i18n-ignore fallback component label
|
|
183
|
+
radial: "Radial", // i18n-ignore fallback component label
|
|
184
|
+
angular: "Angular", // i18n-ignore fallback component label
|
|
185
|
+
diamond: "Diamond", // i18n-ignore fallback component label
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Figma-matching checkerboard: explicit light/dark tiles for legibility.
|
|
189
|
+
const CHECKER_A = "#d4d4d4";
|
|
190
|
+
const CHECKER_B = "#a3a3a3";
|
|
191
|
+
const CHECKERBOARD_IMAGE = `linear-gradient(45deg, ${CHECKER_A} 25%, transparent 25%), linear-gradient(-45deg, ${CHECKER_A} 25%, transparent 25%), linear-gradient(45deg, transparent 75%, ${CHECKER_A} 75%), linear-gradient(-45deg, transparent 75%, ${CHECKER_A} 75%)`;
|
|
192
|
+
|
|
193
|
+
// ─── Paint-type icon SVGs (Tabler style, distinct per type) ────────────────────
|
|
194
|
+
|
|
195
|
+
function IconSolid({ className }: { className?: string }) {
|
|
196
|
+
return (
|
|
197
|
+
<svg
|
|
198
|
+
viewBox="0 0 24 24"
|
|
199
|
+
fill="none"
|
|
200
|
+
stroke="currentColor"
|
|
201
|
+
strokeWidth={2}
|
|
202
|
+
strokeLinecap="round"
|
|
203
|
+
strokeLinejoin="round"
|
|
204
|
+
className={className}
|
|
205
|
+
>
|
|
206
|
+
<rect
|
|
207
|
+
x="4"
|
|
208
|
+
y="4"
|
|
209
|
+
width="16"
|
|
210
|
+
height="16"
|
|
211
|
+
rx="2"
|
|
212
|
+
fill="currentColor"
|
|
213
|
+
stroke="none"
|
|
214
|
+
/>
|
|
215
|
+
</svg>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function IconLinearGradient({ className }: { className?: string }) {
|
|
220
|
+
return (
|
|
221
|
+
<svg
|
|
222
|
+
viewBox="0 0 24 24"
|
|
223
|
+
fill="none"
|
|
224
|
+
stroke="currentColor"
|
|
225
|
+
strokeWidth={2}
|
|
226
|
+
strokeLinecap="round"
|
|
227
|
+
strokeLinejoin="round"
|
|
228
|
+
className={className}
|
|
229
|
+
>
|
|
230
|
+
<defs>
|
|
231
|
+
<linearGradient id="lg-ico" x1="0" y1="0" x2="1" y2="0">
|
|
232
|
+
<stop offset="0%" stopColor="currentColor" stopOpacity="0" />
|
|
233
|
+
<stop offset="100%" stopColor="currentColor" stopOpacity="1" />
|
|
234
|
+
</linearGradient>
|
|
235
|
+
</defs>
|
|
236
|
+
<rect
|
|
237
|
+
x="4"
|
|
238
|
+
y="4"
|
|
239
|
+
width="16"
|
|
240
|
+
height="16"
|
|
241
|
+
rx="2"
|
|
242
|
+
fill="url(#lg-ico)"
|
|
243
|
+
stroke="currentColor"
|
|
244
|
+
strokeOpacity="0.5"
|
|
245
|
+
/>
|
|
246
|
+
</svg>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function IconRadialGradient({ className }: { className?: string }) {
|
|
251
|
+
return (
|
|
252
|
+
<svg
|
|
253
|
+
viewBox="0 0 24 24"
|
|
254
|
+
fill="none"
|
|
255
|
+
stroke="currentColor"
|
|
256
|
+
strokeWidth={2}
|
|
257
|
+
strokeLinecap="round"
|
|
258
|
+
strokeLinejoin="round"
|
|
259
|
+
className={className}
|
|
260
|
+
>
|
|
261
|
+
<defs>
|
|
262
|
+
<radialGradient id="rg-ico" cx="50%" cy="50%" r="50%">
|
|
263
|
+
<stop offset="0%" stopColor="currentColor" stopOpacity="1" />
|
|
264
|
+
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
|
|
265
|
+
</radialGradient>
|
|
266
|
+
</defs>
|
|
267
|
+
<rect
|
|
268
|
+
x="4"
|
|
269
|
+
y="4"
|
|
270
|
+
width="16"
|
|
271
|
+
height="16"
|
|
272
|
+
rx="2"
|
|
273
|
+
fill="url(#rg-ico)"
|
|
274
|
+
stroke="currentColor"
|
|
275
|
+
strokeOpacity="0.5"
|
|
276
|
+
/>
|
|
277
|
+
</svg>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function IconAngularGradient({ className }: { className?: string }) {
|
|
282
|
+
return (
|
|
283
|
+
<svg
|
|
284
|
+
viewBox="0 0 24 24"
|
|
285
|
+
fill="none"
|
|
286
|
+
stroke="currentColor"
|
|
287
|
+
strokeWidth={2}
|
|
288
|
+
strokeLinecap="round"
|
|
289
|
+
strokeLinejoin="round"
|
|
290
|
+
className={className}
|
|
291
|
+
>
|
|
292
|
+
<defs>
|
|
293
|
+
<linearGradient id="ag-ico" x1="0" y1="0" x2="1" y2="1">
|
|
294
|
+
<stop offset="0%" stopColor="currentColor" stopOpacity="1" />
|
|
295
|
+
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
|
|
296
|
+
</linearGradient>
|
|
297
|
+
</defs>
|
|
298
|
+
<rect
|
|
299
|
+
x="4"
|
|
300
|
+
y="4"
|
|
301
|
+
width="16"
|
|
302
|
+
height="16"
|
|
303
|
+
rx="2"
|
|
304
|
+
fill="url(#ag-ico)"
|
|
305
|
+
stroke="currentColor"
|
|
306
|
+
strokeOpacity="0.5"
|
|
307
|
+
/>
|
|
308
|
+
<line
|
|
309
|
+
x1="12"
|
|
310
|
+
y1="4"
|
|
311
|
+
x2="12"
|
|
312
|
+
y2="20"
|
|
313
|
+
stroke="currentColor"
|
|
314
|
+
strokeOpacity="0.4"
|
|
315
|
+
strokeDasharray="2 2"
|
|
316
|
+
/>
|
|
317
|
+
<line
|
|
318
|
+
x1="4"
|
|
319
|
+
y1="12"
|
|
320
|
+
x2="20"
|
|
321
|
+
y2="12"
|
|
322
|
+
stroke="currentColor"
|
|
323
|
+
strokeOpacity="0.4"
|
|
324
|
+
strokeDasharray="2 2"
|
|
325
|
+
/>
|
|
326
|
+
</svg>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function IconDiamondGradient({ className }: { className?: string }) {
|
|
331
|
+
return (
|
|
332
|
+
<svg
|
|
333
|
+
viewBox="0 0 24 24"
|
|
334
|
+
fill="none"
|
|
335
|
+
stroke="currentColor"
|
|
336
|
+
strokeWidth={2}
|
|
337
|
+
strokeLinecap="round"
|
|
338
|
+
strokeLinejoin="round"
|
|
339
|
+
className={className}
|
|
340
|
+
>
|
|
341
|
+
<defs>
|
|
342
|
+
<radialGradient
|
|
343
|
+
id="dg-ico"
|
|
344
|
+
cx="50%"
|
|
345
|
+
cy="50%"
|
|
346
|
+
r="50%"
|
|
347
|
+
gradientTransform="scale(1, 1)"
|
|
348
|
+
>
|
|
349
|
+
<stop offset="0%" stopColor="currentColor" stopOpacity="1" />
|
|
350
|
+
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
|
|
351
|
+
</radialGradient>
|
|
352
|
+
</defs>
|
|
353
|
+
<polygon
|
|
354
|
+
points="12,4 20,12 12,20 4,12"
|
|
355
|
+
fill="url(#dg-ico)"
|
|
356
|
+
stroke="currentColor"
|
|
357
|
+
strokeOpacity="0.5"
|
|
358
|
+
/>
|
|
359
|
+
</svg>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function IconImageFill({ className }: { className?: string }) {
|
|
364
|
+
return (
|
|
365
|
+
<svg
|
|
366
|
+
viewBox="0 0 24 24"
|
|
367
|
+
fill="none"
|
|
368
|
+
stroke="currentColor"
|
|
369
|
+
strokeWidth={2}
|
|
370
|
+
strokeLinecap="round"
|
|
371
|
+
strokeLinejoin="round"
|
|
372
|
+
className={className}
|
|
373
|
+
>
|
|
374
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
375
|
+
<path d="m3 16 5-5 4 4 3-3 6 6" />
|
|
376
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
377
|
+
</svg>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function IconVideoFill({ className }: { className?: string }) {
|
|
382
|
+
return (
|
|
383
|
+
<svg
|
|
384
|
+
viewBox="0 0 24 24"
|
|
385
|
+
fill="none"
|
|
386
|
+
stroke="currentColor"
|
|
387
|
+
strokeWidth={2}
|
|
388
|
+
strokeLinecap="round"
|
|
389
|
+
strokeLinejoin="round"
|
|
390
|
+
className={className}
|
|
391
|
+
>
|
|
392
|
+
{/* Frame border */}
|
|
393
|
+
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
394
|
+
{/* Play triangle — filled, no stroke for clarity at small size */}
|
|
395
|
+
<polygon points="10,9 10,15 16,12" fill="currentColor" stroke="none" />
|
|
396
|
+
</svg>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function IconNoneFill({ className }: { className?: string }) {
|
|
401
|
+
return (
|
|
402
|
+
<svg
|
|
403
|
+
viewBox="0 0 24 24"
|
|
404
|
+
fill="none"
|
|
405
|
+
stroke="currentColor"
|
|
406
|
+
strokeWidth={2}
|
|
407
|
+
strokeLinecap="round"
|
|
408
|
+
strokeLinejoin="round"
|
|
409
|
+
className={className}
|
|
410
|
+
>
|
|
411
|
+
<circle cx="12" cy="12" r="9" />
|
|
412
|
+
<line x1="5.636" y1="5.636" x2="18.364" y2="18.364" />
|
|
413
|
+
</svg>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ─── Paint type definitions (only supported types rendered) ────────────────────
|
|
418
|
+
|
|
419
|
+
const PAINT_TYPES: Array<{
|
|
420
|
+
type: FigmaPaintType;
|
|
421
|
+
label: string;
|
|
422
|
+
Icon: (props: { className?: string }) => JSX.Element;
|
|
423
|
+
}> = [
|
|
424
|
+
{ type: "solid", label: "Solid", Icon: IconSolid }, // i18n-ignore paint type label
|
|
425
|
+
{ type: "linear", label: "Linear", Icon: IconLinearGradient }, // i18n-ignore paint type label
|
|
426
|
+
{ type: "radial", label: "Radial", Icon: IconRadialGradient }, // i18n-ignore paint type label
|
|
427
|
+
{ type: "angular", label: "Angular", Icon: IconAngularGradient }, // i18n-ignore paint type label
|
|
428
|
+
{ type: "diamond", label: "Diamond", Icon: IconDiamondGradient }, // i18n-ignore paint type label
|
|
429
|
+
{ type: "image", label: "Image", Icon: IconImageFill }, // i18n-ignore paint type label
|
|
430
|
+
{ type: "video", label: "Video", Icon: IconVideoFill }, // i18n-ignore paint type label
|
|
431
|
+
{ type: "none", label: "None", Icon: IconNoneFill }, // i18n-ignore paint type label
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
// ─── Document swatch placeholder (shown until real doc colors are provided) ────
|
|
435
|
+
|
|
436
|
+
const PLACEHOLDER_SWATCHES = [
|
|
437
|
+
"#1f2937",
|
|
438
|
+
"#6b7280",
|
|
439
|
+
"#ffffff",
|
|
440
|
+
"#a3a3a3",
|
|
441
|
+
"#f8fafc",
|
|
442
|
+
"#111111",
|
|
443
|
+
"#000000",
|
|
444
|
+
"#a8a29e",
|
|
445
|
+
"#22c1e8",
|
|
446
|
+
"#f08ce3",
|
|
447
|
+
"#737373",
|
|
448
|
+
"#050505",
|
|
449
|
+
"#23d4b5",
|
|
450
|
+
"#d9d9d2",
|
|
451
|
+
"#e6d05c",
|
|
452
|
+
"#9ca3af",
|
|
453
|
+
"#020617",
|
|
454
|
+
"#171717",
|
|
455
|
+
"#d4d4d8",
|
|
456
|
+
"#27272a",
|
|
457
|
+
];
|
|
458
|
+
|
|
459
|
+
// ─── Main component ────────────────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
export function FigmaColorPicker({
|
|
462
|
+
value,
|
|
463
|
+
onChange,
|
|
464
|
+
label: _label,
|
|
465
|
+
opacity,
|
|
466
|
+
onOpacityChange,
|
|
467
|
+
blendMode: _blendMode,
|
|
468
|
+
onBlendModeChange: _onBlendModeChange,
|
|
469
|
+
showBlendMode: _showBlendMode,
|
|
470
|
+
fillRows: _fillRows,
|
|
471
|
+
selectedFillId: _selectedFillId,
|
|
472
|
+
onFillSelect: _onFillSelect,
|
|
473
|
+
onFillChange: _onFillChange,
|
|
474
|
+
onAddFill: _onAddFill,
|
|
475
|
+
onRemoveFill: _onRemoveFill,
|
|
476
|
+
paintType,
|
|
477
|
+
onPaintTypeChange,
|
|
478
|
+
gradientType: _gradientType,
|
|
479
|
+
onGradientTypeChange: _onGradientTypeChange,
|
|
480
|
+
gradientStops: _gradientStops,
|
|
481
|
+
selectedStopId: _selectedStopId,
|
|
482
|
+
onGradientStopSelect: _onGradientStopSelect,
|
|
483
|
+
onGradientStopChange: _onGradientStopChange,
|
|
484
|
+
onAddGradientStop: _onAddGradientStop,
|
|
485
|
+
onRemoveGradientStop: _onRemoveGradientStop,
|
|
486
|
+
labels,
|
|
487
|
+
disabled = false,
|
|
488
|
+
className,
|
|
489
|
+
}: FigmaColorPickerProps) {
|
|
490
|
+
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
491
|
+
const color = parseCssColor(value) ?? FALLBACK_COLOR;
|
|
492
|
+
const hsv = rgbaToHsv(color);
|
|
493
|
+
const hsl = rgbaToHsl(color);
|
|
494
|
+
const effectiveOpacity = opacity ?? alphaToOpacity(color.a);
|
|
495
|
+
|
|
496
|
+
const [mode, setMode] = useState<FigmaColorMode>("hex");
|
|
497
|
+
const [hexDraft, setHexDraft] = useState(() => toDisplayHex(color));
|
|
498
|
+
const [open, setOpen] = useState(false);
|
|
499
|
+
const [sourceTab, setSourceTab] = useState<"custom" | "libraries">("custom");
|
|
500
|
+
const [picking, setPicking] = useState(false);
|
|
501
|
+
const skipNextHexBlurCommitRef = useRef(false);
|
|
502
|
+
|
|
503
|
+
const effectivePaintType =
|
|
504
|
+
paintType ?? inferPaintType(value, effectiveOpacity);
|
|
505
|
+
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
setHexDraft(toDisplayHex(color));
|
|
508
|
+
}, [color.r, color.g, color.b]);
|
|
509
|
+
|
|
510
|
+
// ── Emit helpers ────────────────────────────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
const emitColor = (nextColor: RgbaColor, nextOpacity = effectiveOpacity) => {
|
|
513
|
+
onChange(rgbaToCss(withColorOpacity(nextColor, nextOpacity)));
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const emitColorFromHsv = (nextHsv: HsvaColor) => {
|
|
517
|
+
emitColor(hsvToRgba({ ...nextHsv, a: opacityToAlpha(effectiveOpacity) }));
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const emitColorFromHsl = (nextHsl: HslaColor) => {
|
|
521
|
+
emitColor(hslToRgba({ ...nextHsl, a: opacityToAlpha(effectiveOpacity) }));
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const commitHex = () => {
|
|
525
|
+
const parsed = parseCssColor(`#${hexDraft.replace(/^#/, "")}`);
|
|
526
|
+
if (!parsed) {
|
|
527
|
+
setHexDraft(toDisplayHex(color));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const hexIncludesAlpha = hasHexAlpha(hexDraft);
|
|
531
|
+
const nextOpacity = hexIncludesAlpha
|
|
532
|
+
? alphaToOpacity(parsed.a)
|
|
533
|
+
: effectiveOpacity;
|
|
534
|
+
if (hexIncludesAlpha && onOpacityChange) onOpacityChange(nextOpacity);
|
|
535
|
+
emitColor(parsed, nextOpacity);
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const setOpacity = (nextOpacity: number) => {
|
|
539
|
+
if (onOpacityChange) onOpacityChange(nextOpacity);
|
|
540
|
+
else onChange(rgbaToCss(withColorOpacity(color, nextOpacity)));
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const setPaintType = (nextType: FigmaPaintType) => {
|
|
544
|
+
if (disabled) return;
|
|
545
|
+
if (onPaintTypeChange) {
|
|
546
|
+
onPaintTypeChange(nextType);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (nextType === "none") {
|
|
550
|
+
onChange("transparent");
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (nextType === "solid") {
|
|
554
|
+
emitColor(color, effectiveOpacity > 0 ? effectiveOpacity : 100);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const pickScreenColor = async () => {
|
|
560
|
+
const EyeDropperCtor = (
|
|
561
|
+
window as unknown as {
|
|
562
|
+
EyeDropper?: new () => { open: () => Promise<{ sRGBHex: string }> };
|
|
563
|
+
}
|
|
564
|
+
).EyeDropper;
|
|
565
|
+
if (!EyeDropperCtor || disabled) return;
|
|
566
|
+
setPicking(true);
|
|
567
|
+
try {
|
|
568
|
+
const result = await new EyeDropperCtor().open();
|
|
569
|
+
if (result.sRGBHex) onChange(result.sRGBHex);
|
|
570
|
+
} catch {
|
|
571
|
+
// Browser cancels are expected; keep the current color.
|
|
572
|
+
} finally {
|
|
573
|
+
setPicking(false);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const hasEyeDropper = typeof window !== "undefined" && "EyeDropper" in window;
|
|
578
|
+
|
|
579
|
+
// ── Value row inputs by mode ─────────────────────────────────────────────────
|
|
580
|
+
|
|
581
|
+
function renderValueInputs() {
|
|
582
|
+
if (mode === "hex") {
|
|
583
|
+
return (
|
|
584
|
+
<Input
|
|
585
|
+
value={hexDraft}
|
|
586
|
+
disabled={disabled}
|
|
587
|
+
aria-label={copy.hex}
|
|
588
|
+
spellCheck={false}
|
|
589
|
+
className="h-6 min-w-0 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-[11px] tabular-nums uppercase"
|
|
590
|
+
onChange={(e) => setHexDraft(e.target.value)}
|
|
591
|
+
onFocus={(e) => e.target.select()}
|
|
592
|
+
onKeyDown={(e) => {
|
|
593
|
+
if (e.key === "Enter") {
|
|
594
|
+
e.preventDefault();
|
|
595
|
+
commitHex();
|
|
596
|
+
skipNextHexBlurCommitRef.current = true;
|
|
597
|
+
e.currentTarget.blur();
|
|
598
|
+
}
|
|
599
|
+
if (e.key === "Escape") {
|
|
600
|
+
setHexDraft(toDisplayHex(color));
|
|
601
|
+
skipNextHexBlurCommitRef.current = true;
|
|
602
|
+
e.currentTarget.blur();
|
|
603
|
+
}
|
|
604
|
+
}}
|
|
605
|
+
onBlur={() => {
|
|
606
|
+
if (skipNextHexBlurCommitRef.current) {
|
|
607
|
+
skipNextHexBlurCommitRef.current = false;
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
commitHex();
|
|
611
|
+
}}
|
|
612
|
+
/>
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
if (mode === "rgb") {
|
|
616
|
+
return (
|
|
617
|
+
<div className="flex gap-1">
|
|
618
|
+
{(["r", "g", "b"] as const).map((ch) => (
|
|
619
|
+
<ScrubbyNumberInput
|
|
620
|
+
key={ch}
|
|
621
|
+
aria-label={ch.toUpperCase()}
|
|
622
|
+
value={color[ch]}
|
|
623
|
+
min={0}
|
|
624
|
+
max={255}
|
|
625
|
+
disabled={disabled}
|
|
626
|
+
onChange={(next) => emitColor({ ...color, [ch]: next })}
|
|
627
|
+
/>
|
|
628
|
+
))}
|
|
629
|
+
</div>
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
if (mode === "hsl") {
|
|
633
|
+
return (
|
|
634
|
+
<div className="flex gap-1">
|
|
635
|
+
<ScrubbyNumberInput
|
|
636
|
+
aria-label={copy.hue}
|
|
637
|
+
value={hsl.h}
|
|
638
|
+
min={0}
|
|
639
|
+
max={360}
|
|
640
|
+
disabled={disabled}
|
|
641
|
+
onChange={(h) => emitColorFromHsl({ ...hsl, h })}
|
|
642
|
+
/>
|
|
643
|
+
<ScrubbyNumberInput
|
|
644
|
+
aria-label={copy.saturation}
|
|
645
|
+
value={hsl.s}
|
|
646
|
+
min={0}
|
|
647
|
+
max={100}
|
|
648
|
+
disabled={disabled}
|
|
649
|
+
onChange={(s) => emitColorFromHsl({ ...hsl, s })}
|
|
650
|
+
/>
|
|
651
|
+
<ScrubbyNumberInput
|
|
652
|
+
aria-label={copy.lightness}
|
|
653
|
+
value={hsl.l}
|
|
654
|
+
min={0}
|
|
655
|
+
max={100}
|
|
656
|
+
disabled={disabled}
|
|
657
|
+
onChange={(l) => emitColorFromHsl({ ...hsl, l })}
|
|
658
|
+
/>
|
|
659
|
+
</div>
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
// hsb
|
|
663
|
+
return (
|
|
664
|
+
<div className="flex gap-1">
|
|
665
|
+
<ScrubbyNumberInput
|
|
666
|
+
aria-label={copy.hue}
|
|
667
|
+
value={hsv.h}
|
|
668
|
+
min={0}
|
|
669
|
+
max={360}
|
|
670
|
+
disabled={disabled}
|
|
671
|
+
onChange={(h) => emitColorFromHsv({ ...hsv, h })}
|
|
672
|
+
/>
|
|
673
|
+
<ScrubbyNumberInput
|
|
674
|
+
aria-label={copy.saturation}
|
|
675
|
+
value={hsv.s}
|
|
676
|
+
min={0}
|
|
677
|
+
max={100}
|
|
678
|
+
disabled={disabled}
|
|
679
|
+
onChange={(s) => emitColorFromHsv({ ...hsv, s })}
|
|
680
|
+
/>
|
|
681
|
+
<ScrubbyNumberInput
|
|
682
|
+
aria-label={copy.brightness}
|
|
683
|
+
value={hsv.v}
|
|
684
|
+
min={0}
|
|
685
|
+
max={100}
|
|
686
|
+
disabled={disabled}
|
|
687
|
+
onChange={(v) => emitColorFromHsv({ ...hsv, v })}
|
|
688
|
+
/>
|
|
689
|
+
</div>
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
694
|
+
|
|
695
|
+
return (
|
|
696
|
+
<div className={cn("space-y-1.5", className)}>
|
|
697
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
698
|
+
<PopoverTrigger asChild>
|
|
699
|
+
{/* Trigger: compact swatch + hex + opacity% — matches Figma's fill row */}
|
|
700
|
+
<button
|
|
701
|
+
type="button"
|
|
702
|
+
disabled={disabled}
|
|
703
|
+
aria-label={copy.trigger}
|
|
704
|
+
className={cn(
|
|
705
|
+
"flex h-6 w-full items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-[11px] shadow-none",
|
|
706
|
+
"hover:bg-[var(--design-editor-panel-raised-bg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
|
|
707
|
+
disabled && "pointer-events-none opacity-50",
|
|
708
|
+
)}
|
|
709
|
+
>
|
|
710
|
+
{/* Flat swatch chip — no shadow-inner (Figma is flat) */}
|
|
711
|
+
<span
|
|
712
|
+
className="size-4 shrink-0 rounded-[3px] border border-border/60"
|
|
713
|
+
style={swatchStyle(rgbaToCss(color))}
|
|
714
|
+
/>
|
|
715
|
+
<span className="min-w-0 flex-1 truncate text-left tabular-nums uppercase text-[11px]">
|
|
716
|
+
{toDisplayHex(color)}
|
|
717
|
+
</span>
|
|
718
|
+
<span className="tabular-nums text-muted-foreground text-[11px]">
|
|
719
|
+
{effectiveOpacity}%
|
|
720
|
+
</span>
|
|
721
|
+
</button>
|
|
722
|
+
</PopoverTrigger>
|
|
723
|
+
|
|
724
|
+
{/* Figma popover: ~240px wide, uniform 12px padding, tight controls */}
|
|
725
|
+
<PopoverContent
|
|
726
|
+
side="left"
|
|
727
|
+
align="start"
|
|
728
|
+
sideOffset={8}
|
|
729
|
+
className="z-[10000] w-[252px] p-0 shadow-xl"
|
|
730
|
+
>
|
|
731
|
+
<div className="overflow-hidden rounded-md bg-popover text-popover-foreground">
|
|
732
|
+
{/* ── Header: Custom | Libraries + ✕ ────────────────────────── */}
|
|
733
|
+
<div className="flex items-center justify-between px-3 py-1.5">
|
|
734
|
+
<div className="flex items-center gap-2">
|
|
735
|
+
{(["custom", "libraries"] as const).map((tab) => (
|
|
736
|
+
<button
|
|
737
|
+
key={tab}
|
|
738
|
+
type="button"
|
|
739
|
+
role="tab"
|
|
740
|
+
aria-selected={sourceTab === tab}
|
|
741
|
+
onClick={() => setSourceTab(tab)}
|
|
742
|
+
className={cn(
|
|
743
|
+
"cursor-pointer rounded px-1.5 py-0.5 text-[11px] font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
744
|
+
sourceTab === tab
|
|
745
|
+
? "bg-[var(--design-editor-control-bg)] text-foreground shadow-[inset_0_0_0_1px_var(--design-editor-control-border)]"
|
|
746
|
+
: "text-muted-foreground hover:text-foreground",
|
|
747
|
+
)}
|
|
748
|
+
>
|
|
749
|
+
{
|
|
750
|
+
tab === "custom"
|
|
751
|
+
? "Custom" /* i18n-ignore Figma picker tab */
|
|
752
|
+
: "Libraries" /* i18n-ignore Figma picker tab */
|
|
753
|
+
}
|
|
754
|
+
</button>
|
|
755
|
+
))}
|
|
756
|
+
</div>
|
|
757
|
+
<Tooltip>
|
|
758
|
+
<TooltipTrigger asChild>
|
|
759
|
+
<button
|
|
760
|
+
type="button"
|
|
761
|
+
aria-label={
|
|
762
|
+
"Close" /* i18n-ignore Figma picker close label */
|
|
763
|
+
}
|
|
764
|
+
onClick={() => setOpen(false)}
|
|
765
|
+
className="flex size-5 cursor-pointer 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"
|
|
766
|
+
>
|
|
767
|
+
<IconX className="size-3" />
|
|
768
|
+
</button>
|
|
769
|
+
</TooltipTrigger>
|
|
770
|
+
<TooltipContent>{"Close" /* i18n-ignore */}</TooltipContent>
|
|
771
|
+
</Tooltip>
|
|
772
|
+
</div>
|
|
773
|
+
|
|
774
|
+
{sourceTab === "custom" ? (
|
|
775
|
+
<>
|
|
776
|
+
{/* ── Paint-type icon row ──────────────────────────────────── */}
|
|
777
|
+
<div className="border-t border-border/70 px-2 py-1.5">
|
|
778
|
+
<div className="grid grid-cols-8 gap-px">
|
|
779
|
+
{PAINT_TYPES.map(({ type, label, Icon }) => {
|
|
780
|
+
const isActive = effectivePaintType === type;
|
|
781
|
+
const isGradientType =
|
|
782
|
+
type === "linear" ||
|
|
783
|
+
type === "radial" ||
|
|
784
|
+
type === "angular" ||
|
|
785
|
+
type === "diamond";
|
|
786
|
+
const gradientDisabled =
|
|
787
|
+
isGradientType &&
|
|
788
|
+
!onPaintTypeChange &&
|
|
789
|
+
!_onGradientTypeChange &&
|
|
790
|
+
!_onAddGradientStop;
|
|
791
|
+
const imageDisabled =
|
|
792
|
+
(type === "image" || type === "video") &&
|
|
793
|
+
!onPaintTypeChange;
|
|
794
|
+
const isDisabled =
|
|
795
|
+
disabled || gradientDisabled || imageDisabled;
|
|
796
|
+
return (
|
|
797
|
+
<Tooltip key={type}>
|
|
798
|
+
<TooltipTrigger asChild>
|
|
799
|
+
<button
|
|
800
|
+
type="button"
|
|
801
|
+
aria-label={label}
|
|
802
|
+
aria-pressed={isActive}
|
|
803
|
+
disabled={isDisabled}
|
|
804
|
+
onClick={() => setPaintType(type)}
|
|
805
|
+
className={cn(
|
|
806
|
+
"flex h-6 w-full cursor-pointer items-center justify-center rounded-sm transition-colors",
|
|
807
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
808
|
+
"active:scale-95",
|
|
809
|
+
isActive
|
|
810
|
+
? "bg-primary/10 text-primary shadow-[inset_0_0_0_1px_var(--primary)]"
|
|
811
|
+
: "text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground",
|
|
812
|
+
isDisabled && "pointer-events-none opacity-40",
|
|
813
|
+
)}
|
|
814
|
+
>
|
|
815
|
+
<Icon className="size-3.5" />
|
|
816
|
+
</button>
|
|
817
|
+
</TooltipTrigger>
|
|
818
|
+
<TooltipContent>{label}</TooltipContent>
|
|
819
|
+
</Tooltip>
|
|
820
|
+
);
|
|
821
|
+
})}
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
824
|
+
|
|
825
|
+
{/* ── 2D Saturation/Brightness field (edge-to-edge, no side padding) */}
|
|
826
|
+
<div className="border-t border-border/70">
|
|
827
|
+
<SaturationBrightnessField
|
|
828
|
+
hsv={hsv}
|
|
829
|
+
label={copy.saturationBrightness}
|
|
830
|
+
disabled={disabled}
|
|
831
|
+
onChange={emitColorFromHsv}
|
|
832
|
+
/>
|
|
833
|
+
</div>
|
|
834
|
+
|
|
835
|
+
{/* ── Eyedropper + Hue slider / Swatch + Alpha slider ─────── */}
|
|
836
|
+
{/* Left gutter spans both rows with dropper centered vertically */}
|
|
837
|
+
<div className="mt-2.5 px-3">
|
|
838
|
+
<div className="grid grid-cols-[1.5rem_1fr] items-center gap-x-2">
|
|
839
|
+
{/* Eyedropper centered across the two slider rows via row-span-2 */}
|
|
840
|
+
<div className="row-span-2 flex items-center justify-center">
|
|
841
|
+
<Tooltip>
|
|
842
|
+
<TooltipTrigger asChild>
|
|
843
|
+
<button
|
|
844
|
+
type="button"
|
|
845
|
+
aria-label={
|
|
846
|
+
"Pick color" /* i18n-ignore browser eyedropper label */
|
|
847
|
+
}
|
|
848
|
+
disabled={disabled || !hasEyeDropper}
|
|
849
|
+
onClick={() => void pickScreenColor()}
|
|
850
|
+
className={cn(
|
|
851
|
+
"flex size-6 cursor-pointer items-center justify-center rounded-sm transition-colors",
|
|
852
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
853
|
+
picking
|
|
854
|
+
? "bg-primary/10 text-primary ring-1 ring-primary/50"
|
|
855
|
+
: "text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground",
|
|
856
|
+
(disabled || !hasEyeDropper) &&
|
|
857
|
+
"pointer-events-none opacity-40",
|
|
858
|
+
)}
|
|
859
|
+
>
|
|
860
|
+
<IconColorPicker className="size-4" />
|
|
861
|
+
</button>
|
|
862
|
+
</TooltipTrigger>
|
|
863
|
+
<TooltipContent>
|
|
864
|
+
{
|
|
865
|
+
hasEyeDropper
|
|
866
|
+
? "Pick color" // i18n-ignore browser eyedropper label
|
|
867
|
+
: "Not supported in this browser" // i18n-ignore browser eyedropper disabled label
|
|
868
|
+
}
|
|
869
|
+
</TooltipContent>
|
|
870
|
+
</Tooltip>
|
|
871
|
+
</div>
|
|
872
|
+
|
|
873
|
+
{/* Hue track */}
|
|
874
|
+
<ColorTrack
|
|
875
|
+
label={copy.hue}
|
|
876
|
+
value={hsv.h}
|
|
877
|
+
min={0}
|
|
878
|
+
max={360}
|
|
879
|
+
disabled={disabled}
|
|
880
|
+
backgroundImage="linear-gradient(90deg, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)"
|
|
881
|
+
onChange={(next) =>
|
|
882
|
+
emitColorFromHsv({ ...hsv, h: next === 360 ? 0 : next })
|
|
883
|
+
}
|
|
884
|
+
/>
|
|
885
|
+
|
|
886
|
+
{/* Current-color swatch left of alpha (matches Figma's layout) */}
|
|
887
|
+
<div className="flex items-center gap-2">
|
|
888
|
+
<span
|
|
889
|
+
className="size-[18px] shrink-0 rounded-[3px] border border-border/60"
|
|
890
|
+
style={swatchStyle(rgbaToCss(color))}
|
|
891
|
+
/>
|
|
892
|
+
{/* Alpha track fills remaining width */}
|
|
893
|
+
<div className="flex-1">
|
|
894
|
+
<ColorTrack
|
|
895
|
+
label={copy.opacity}
|
|
896
|
+
value={effectiveOpacity}
|
|
897
|
+
min={0}
|
|
898
|
+
max={100}
|
|
899
|
+
disabled={disabled}
|
|
900
|
+
backgroundImage={alphaTrackBackground(color)}
|
|
901
|
+
backgroundSize="8px 8px, 8px 8px, 8px 8px, 8px 8px, 100% 100%"
|
|
902
|
+
backgroundPosition="0 0, 0 4px, 4px -4px, -4px 0, 0 0"
|
|
903
|
+
onChange={setOpacity}
|
|
904
|
+
/>
|
|
905
|
+
</div>
|
|
906
|
+
</div>
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
|
|
910
|
+
{/* ── Value row: [Hex ▾] [value input(s)] [opacity %] ─────── */}
|
|
911
|
+
<div className="mt-2.5 px-3 pb-3">
|
|
912
|
+
<div className="grid grid-cols-[4.5rem_1fr_3rem] items-center gap-1">
|
|
913
|
+
{/* Model dropdown */}
|
|
914
|
+
<Select
|
|
915
|
+
value={mode}
|
|
916
|
+
onValueChange={(v) => setMode(v as FigmaColorMode)}
|
|
917
|
+
disabled={disabled}
|
|
918
|
+
>
|
|
919
|
+
<SelectTrigger className="h-6 w-[4.5rem] rounded-md border-transparent bg-transparent px-1.5 text-[11px] font-semibold shadow-none hover:bg-[var(--design-editor-control-bg)] focus:ring-0 focus:ring-offset-0 focus-visible:ring-2 focus-visible:ring-ring [&>svg]:size-3 [&>svg]:shrink-0">
|
|
920
|
+
<SelectValue />
|
|
921
|
+
</SelectTrigger>
|
|
922
|
+
<SelectContent className="text-[11px]">
|
|
923
|
+
<SelectItem value="hex" className="text-[11px]">
|
|
924
|
+
Hex
|
|
925
|
+
</SelectItem>{" "}
|
|
926
|
+
{/* i18n-ignore color mode */}
|
|
927
|
+
<SelectItem value="rgb" className="text-[11px]">
|
|
928
|
+
RGB
|
|
929
|
+
</SelectItem>{" "}
|
|
930
|
+
{/* i18n-ignore color mode */}
|
|
931
|
+
<SelectItem value="hsl" className="text-[11px]">
|
|
932
|
+
HSL
|
|
933
|
+
</SelectItem>{" "}
|
|
934
|
+
{/* i18n-ignore color mode */}
|
|
935
|
+
<SelectItem value="hsb" className="text-[11px]">
|
|
936
|
+
HSB
|
|
937
|
+
</SelectItem>{" "}
|
|
938
|
+
{/* i18n-ignore color mode */}
|
|
939
|
+
</SelectContent>
|
|
940
|
+
</Select>
|
|
941
|
+
|
|
942
|
+
{/* Value field(s) — adapts to mode */}
|
|
943
|
+
{renderValueInputs()}
|
|
944
|
+
|
|
945
|
+
{/* Opacity % field */}
|
|
946
|
+
<div className="flex h-6 overflow-hidden rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)]">
|
|
947
|
+
<ScrubbyNumberInput
|
|
948
|
+
aria-label={copy.opacity}
|
|
949
|
+
value={effectiveOpacity}
|
|
950
|
+
min={0}
|
|
951
|
+
max={100}
|
|
952
|
+
disabled={disabled}
|
|
953
|
+
onChange={setOpacity}
|
|
954
|
+
className="h-full min-w-0 flex-1 rounded-none border-0 bg-transparent px-1 text-[11px] tabular-nums shadow-none focus-visible:ring-0"
|
|
955
|
+
compact
|
|
956
|
+
/>
|
|
957
|
+
<span className="flex w-4 shrink-0 items-center justify-center border-l border-border/60 text-[10px] text-muted-foreground">
|
|
958
|
+
%
|
|
959
|
+
</span>
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
</div>
|
|
963
|
+
|
|
964
|
+
{/* ── "On this page" source + swatches ────────────────────── */}
|
|
965
|
+
<div className="border-t border-border/70 px-3 py-2.5">
|
|
966
|
+
{/* Source selector styled as a full-width bordered dropdown */}
|
|
967
|
+
<button
|
|
968
|
+
type="button"
|
|
969
|
+
className="mb-2 flex h-6 w-full items-center justify-between rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-left text-[11px] text-foreground hover:bg-[var(--design-editor-panel-raised-bg)]"
|
|
970
|
+
>
|
|
971
|
+
{"On this page" /* i18n-ignore Figma picker source */}
|
|
972
|
+
<IconChevronDown className="size-3 text-muted-foreground" />
|
|
973
|
+
</button>
|
|
974
|
+
|
|
975
|
+
{/* Swatch grid: flat chips (no shadow-inner), 8 columns */}
|
|
976
|
+
<div className="grid grid-cols-8 gap-1">
|
|
977
|
+
{PLACEHOLDER_SWATCHES.map((swatch, index) => {
|
|
978
|
+
const isCurrentColor =
|
|
979
|
+
rgbaToHex(parseCssColor(swatch) ?? FALLBACK_COLOR) ===
|
|
980
|
+
rgbaToHex(color);
|
|
981
|
+
return (
|
|
982
|
+
<Tooltip key={`${swatch}-${index}`}>
|
|
983
|
+
<TooltipTrigger asChild>
|
|
984
|
+
<button
|
|
985
|
+
type="button"
|
|
986
|
+
disabled={disabled}
|
|
987
|
+
aria-label={swatch}
|
|
988
|
+
aria-pressed={isCurrentColor}
|
|
989
|
+
className={cn(
|
|
990
|
+
"size-5 rounded-sm border transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
991
|
+
isCurrentColor
|
|
992
|
+
? "border-primary ring-1 ring-primary"
|
|
993
|
+
: "border-border/60",
|
|
994
|
+
)}
|
|
995
|
+
style={swatchStyle(swatch)}
|
|
996
|
+
onClick={() =>
|
|
997
|
+
emitColor(parseCssColor(swatch) ?? color)
|
|
998
|
+
}
|
|
999
|
+
/>
|
|
1000
|
+
</TooltipTrigger>
|
|
1001
|
+
<TooltipContent>{swatch}</TooltipContent>
|
|
1002
|
+
</Tooltip>
|
|
1003
|
+
);
|
|
1004
|
+
})}
|
|
1005
|
+
</div>
|
|
1006
|
+
</div>
|
|
1007
|
+
</>
|
|
1008
|
+
) : (
|
|
1009
|
+
/* ── Libraries tab: honest empty state ────────────────────── */
|
|
1010
|
+
<div className="border-t border-border/70 p-3">
|
|
1011
|
+
<div className="rounded-md border border-dashed border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-3 py-5 text-center">
|
|
1012
|
+
<p className="text-[11px] font-medium text-foreground">
|
|
1013
|
+
{
|
|
1014
|
+
"No color libraries connected" /* i18n-ignore Figma picker library empty state */
|
|
1015
|
+
}
|
|
1016
|
+
</p>
|
|
1017
|
+
<p className="mt-1 text-[10px] text-muted-foreground">
|
|
1018
|
+
{
|
|
1019
|
+
"Connect a library in the Assets panel to browse colors here." /* i18n-ignore Figma picker library hint */
|
|
1020
|
+
}
|
|
1021
|
+
</p>
|
|
1022
|
+
</div>
|
|
1023
|
+
</div>
|
|
1024
|
+
)}
|
|
1025
|
+
</div>
|
|
1026
|
+
</PopoverContent>
|
|
1027
|
+
</Popover>
|
|
1028
|
+
</div>
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// ─── Sub-components ────────────────────────────────────────────────────────────
|
|
1033
|
+
|
|
1034
|
+
function SaturationBrightnessField({
|
|
1035
|
+
hsv,
|
|
1036
|
+
label,
|
|
1037
|
+
disabled,
|
|
1038
|
+
onChange,
|
|
1039
|
+
}: {
|
|
1040
|
+
hsv: HsvaColor;
|
|
1041
|
+
label: string;
|
|
1042
|
+
disabled: boolean;
|
|
1043
|
+
onChange: (color: HsvaColor) => void;
|
|
1044
|
+
}) {
|
|
1045
|
+
const fieldRef = useRef<HTMLDivElement>(null);
|
|
1046
|
+
const draggingRef = useRef(false);
|
|
1047
|
+
const hueColor = rgbaToCss(hsvToRgba({ h: hsv.h, s: 100, v: 100, a: 1 }));
|
|
1048
|
+
|
|
1049
|
+
const updateFromPointer = (event: PointerEvent<HTMLDivElement>) => {
|
|
1050
|
+
const rect = fieldRef.current?.getBoundingClientRect();
|
|
1051
|
+
if (!rect) return;
|
|
1052
|
+
const nextSaturation = ((event.clientX - rect.left) / rect.width) * 100;
|
|
1053
|
+
const nextBrightness =
|
|
1054
|
+
100 - ((event.clientY - rect.top) / rect.height) * 100;
|
|
1055
|
+
onChange({
|
|
1056
|
+
...hsv,
|
|
1057
|
+
s: clamp(nextSaturation, 0, 100),
|
|
1058
|
+
v: clamp(nextBrightness, 0, 100),
|
|
1059
|
+
});
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
const stepWithKeyboard = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
1063
|
+
if (disabled) return;
|
|
1064
|
+
const step = event.shiftKey ? 10 : 1;
|
|
1065
|
+
if (event.key === "ArrowRight") {
|
|
1066
|
+
event.preventDefault();
|
|
1067
|
+
onChange({ ...hsv, s: clamp(hsv.s + step, 0, 100) });
|
|
1068
|
+
}
|
|
1069
|
+
if (event.key === "ArrowLeft") {
|
|
1070
|
+
event.preventDefault();
|
|
1071
|
+
onChange({ ...hsv, s: clamp(hsv.s - step, 0, 100) });
|
|
1072
|
+
}
|
|
1073
|
+
if (event.key === "ArrowUp") {
|
|
1074
|
+
event.preventDefault();
|
|
1075
|
+
onChange({ ...hsv, v: clamp(hsv.v + step, 0, 100) });
|
|
1076
|
+
}
|
|
1077
|
+
if (event.key === "ArrowDown") {
|
|
1078
|
+
event.preventDefault();
|
|
1079
|
+
onChange({ ...hsv, v: clamp(hsv.v - step, 0, 100) });
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
return (
|
|
1084
|
+
<div
|
|
1085
|
+
ref={fieldRef}
|
|
1086
|
+
tabIndex={disabled ? -1 : 0}
|
|
1087
|
+
aria-label={label}
|
|
1088
|
+
aria-disabled={disabled}
|
|
1089
|
+
onPointerDown={(event) => {
|
|
1090
|
+
if (disabled) return;
|
|
1091
|
+
draggingRef.current = true;
|
|
1092
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1093
|
+
updateFromPointer(event);
|
|
1094
|
+
}}
|
|
1095
|
+
onPointerMove={(event) => {
|
|
1096
|
+
if (!draggingRef.current || disabled) return;
|
|
1097
|
+
updateFromPointer(event);
|
|
1098
|
+
}}
|
|
1099
|
+
onPointerUp={(event) => {
|
|
1100
|
+
draggingRef.current = false;
|
|
1101
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
1102
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
1103
|
+
}
|
|
1104
|
+
}}
|
|
1105
|
+
onPointerCancel={() => {
|
|
1106
|
+
draggingRef.current = false;
|
|
1107
|
+
}}
|
|
1108
|
+
onKeyDown={stepWithKeyboard}
|
|
1109
|
+
className={cn(
|
|
1110
|
+
"relative h-48 w-full cursor-crosshair overflow-hidden outline-none",
|
|
1111
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
|
|
1112
|
+
"active:cursor-grabbing",
|
|
1113
|
+
disabled && "cursor-not-allowed opacity-60",
|
|
1114
|
+
)}
|
|
1115
|
+
style={{
|
|
1116
|
+
backgroundImage: `linear-gradient(to top, #000 0%, transparent 100%), linear-gradient(to right, #fff 0%, ${hueColor} 100%)`,
|
|
1117
|
+
}}
|
|
1118
|
+
>
|
|
1119
|
+
{/* Handle: size-4, white ring, consistent foreground shadow */}
|
|
1120
|
+
<span
|
|
1121
|
+
className="pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-[0_0_0_1px_hsl(var(--foreground)/0.6)]"
|
|
1122
|
+
style={{
|
|
1123
|
+
left: `${hsv.s}%`,
|
|
1124
|
+
top: `${100 - hsv.v}%`,
|
|
1125
|
+
}}
|
|
1126
|
+
/>
|
|
1127
|
+
</div>
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function ColorTrack({
|
|
1132
|
+
label,
|
|
1133
|
+
value,
|
|
1134
|
+
min,
|
|
1135
|
+
max,
|
|
1136
|
+
disabled,
|
|
1137
|
+
backgroundImage,
|
|
1138
|
+
backgroundSize,
|
|
1139
|
+
backgroundPosition,
|
|
1140
|
+
onChange,
|
|
1141
|
+
}: {
|
|
1142
|
+
label: string;
|
|
1143
|
+
value: number;
|
|
1144
|
+
min: number;
|
|
1145
|
+
max: number;
|
|
1146
|
+
disabled: boolean;
|
|
1147
|
+
backgroundImage: string;
|
|
1148
|
+
backgroundSize?: string;
|
|
1149
|
+
backgroundPosition?: string;
|
|
1150
|
+
onChange: (value: number) => void;
|
|
1151
|
+
}) {
|
|
1152
|
+
const trackRef = useRef<HTMLDivElement>(null);
|
|
1153
|
+
const draggingRef = useRef(false);
|
|
1154
|
+
const percent = ((value - min) / (max - min)) * 100;
|
|
1155
|
+
|
|
1156
|
+
const updateFromPointer = (event: PointerEvent<HTMLDivElement>) => {
|
|
1157
|
+
const rect = trackRef.current?.getBoundingClientRect();
|
|
1158
|
+
if (!rect) return;
|
|
1159
|
+
const next = min + ((event.clientX - rect.left) / rect.width) * (max - min);
|
|
1160
|
+
onChange(clamp(next, min, max));
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
const stepWithKeyboard = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
1164
|
+
if (disabled) return;
|
|
1165
|
+
const step = event.shiftKey ? 10 : 1;
|
|
1166
|
+
if (event.key === "ArrowRight" || event.key === "ArrowUp") {
|
|
1167
|
+
event.preventDefault();
|
|
1168
|
+
onChange(clamp(value + step, min, max));
|
|
1169
|
+
}
|
|
1170
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowDown") {
|
|
1171
|
+
event.preventDefault();
|
|
1172
|
+
onChange(clamp(value - step, min, max));
|
|
1173
|
+
}
|
|
1174
|
+
if (event.key === "Home") {
|
|
1175
|
+
event.preventDefault();
|
|
1176
|
+
onChange(min);
|
|
1177
|
+
}
|
|
1178
|
+
if (event.key === "End") {
|
|
1179
|
+
event.preventDefault();
|
|
1180
|
+
onChange(max);
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
return (
|
|
1185
|
+
<div
|
|
1186
|
+
ref={trackRef}
|
|
1187
|
+
role="slider"
|
|
1188
|
+
tabIndex={disabled ? -1 : 0}
|
|
1189
|
+
aria-label={label}
|
|
1190
|
+
aria-valuemin={min}
|
|
1191
|
+
aria-valuemax={max}
|
|
1192
|
+
aria-valuenow={Math.round(value)}
|
|
1193
|
+
aria-disabled={disabled}
|
|
1194
|
+
onKeyDown={stepWithKeyboard}
|
|
1195
|
+
onPointerDown={(event) => {
|
|
1196
|
+
if (disabled) return;
|
|
1197
|
+
draggingRef.current = true;
|
|
1198
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1199
|
+
updateFromPointer(event);
|
|
1200
|
+
}}
|
|
1201
|
+
onPointerMove={(event) => {
|
|
1202
|
+
if (!draggingRef.current || disabled) return;
|
|
1203
|
+
updateFromPointer(event);
|
|
1204
|
+
}}
|
|
1205
|
+
onPointerUp={(event) => {
|
|
1206
|
+
draggingRef.current = false;
|
|
1207
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
1208
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
1209
|
+
}
|
|
1210
|
+
}}
|
|
1211
|
+
onPointerCancel={() => {
|
|
1212
|
+
draggingRef.current = false;
|
|
1213
|
+
}}
|
|
1214
|
+
className={cn(
|
|
1215
|
+
"relative h-3.5 cursor-pointer rounded-full border border-border/60 outline-none",
|
|
1216
|
+
"ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
1217
|
+
"active:cursor-grabbing",
|
|
1218
|
+
disabled && "cursor-not-allowed opacity-60",
|
|
1219
|
+
)}
|
|
1220
|
+
style={{ backgroundImage, backgroundSize, backgroundPosition }}
|
|
1221
|
+
>
|
|
1222
|
+
{/* Thumb overhangs the track slightly, matching Figma */}
|
|
1223
|
+
<span
|
|
1224
|
+
className="pointer-events-none absolute top-1/2 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-[0_0_0_1px_hsl(var(--foreground)/0.6)]"
|
|
1225
|
+
style={{ left: `${clamp(percent, 0, 100)}%` }}
|
|
1226
|
+
/>
|
|
1227
|
+
</div>
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* A number input with select-on-focus and Esc-to-revert.
|
|
1233
|
+
* The `compact` prop removes inner padding for use inside bordered wrappers.
|
|
1234
|
+
*/
|
|
1235
|
+
function ScrubbyNumberInput({
|
|
1236
|
+
"aria-label": ariaLabel,
|
|
1237
|
+
value,
|
|
1238
|
+
min,
|
|
1239
|
+
max,
|
|
1240
|
+
disabled,
|
|
1241
|
+
onChange,
|
|
1242
|
+
className,
|
|
1243
|
+
compact = false,
|
|
1244
|
+
}: {
|
|
1245
|
+
"aria-label": string;
|
|
1246
|
+
value: number;
|
|
1247
|
+
min: number;
|
|
1248
|
+
max: number;
|
|
1249
|
+
disabled: boolean;
|
|
1250
|
+
onChange: (value: number) => void;
|
|
1251
|
+
className?: string;
|
|
1252
|
+
compact?: boolean;
|
|
1253
|
+
}) {
|
|
1254
|
+
const [draft, setDraft] = useState<string>(() => String(value));
|
|
1255
|
+
const skipBlurRef = useRef(false);
|
|
1256
|
+
|
|
1257
|
+
useEffect(() => {
|
|
1258
|
+
setDraft(String(value));
|
|
1259
|
+
}, [value]);
|
|
1260
|
+
|
|
1261
|
+
const commit = () => {
|
|
1262
|
+
const parsed = Number(draft);
|
|
1263
|
+
if (!Number.isFinite(parsed)) {
|
|
1264
|
+
setDraft(String(value));
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
onChange(clamp(parsed, min, max));
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
return (
|
|
1271
|
+
<input
|
|
1272
|
+
type="number"
|
|
1273
|
+
aria-label={ariaLabel}
|
|
1274
|
+
value={draft}
|
|
1275
|
+
min={min}
|
|
1276
|
+
max={max}
|
|
1277
|
+
disabled={disabled}
|
|
1278
|
+
className={cn(
|
|
1279
|
+
"h-6 w-full rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] text-center text-[11px] tabular-nums",
|
|
1280
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
1281
|
+
compact && "border-0 shadow-none focus-visible:ring-0",
|
|
1282
|
+
className,
|
|
1283
|
+
)}
|
|
1284
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
1285
|
+
onFocus={(e) => e.target.select()}
|
|
1286
|
+
onKeyDown={(e) => {
|
|
1287
|
+
if (e.key === "Enter") {
|
|
1288
|
+
e.preventDefault();
|
|
1289
|
+
commit();
|
|
1290
|
+
skipBlurRef.current = true;
|
|
1291
|
+
e.currentTarget.blur();
|
|
1292
|
+
}
|
|
1293
|
+
if (e.key === "Escape") {
|
|
1294
|
+
setDraft(String(value));
|
|
1295
|
+
skipBlurRef.current = true;
|
|
1296
|
+
e.currentTarget.blur();
|
|
1297
|
+
}
|
|
1298
|
+
if (e.key === "ArrowUp") {
|
|
1299
|
+
e.preventDefault();
|
|
1300
|
+
const step = e.shiftKey ? 10 : 1;
|
|
1301
|
+
onChange(clamp(value + step, min, max));
|
|
1302
|
+
}
|
|
1303
|
+
if (e.key === "ArrowDown") {
|
|
1304
|
+
e.preventDefault();
|
|
1305
|
+
const step = e.shiftKey ? 10 : 1;
|
|
1306
|
+
onChange(clamp(value - step, min, max));
|
|
1307
|
+
}
|
|
1308
|
+
}}
|
|
1309
|
+
onBlur={() => {
|
|
1310
|
+
if (skipBlurRef.current) {
|
|
1311
|
+
skipBlurRef.current = false;
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
commit();
|
|
1315
|
+
}}
|
|
1316
|
+
/>
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// ─── Utilities ─────────────────────────────────────────────────────────────────
|
|
1321
|
+
|
|
1322
|
+
function inferPaintType(value: string, opacity: number): FigmaPaintType {
|
|
1323
|
+
const parsed = parseCssColor(value);
|
|
1324
|
+
if (opacity <= 0 || parsed?.a === 0 || value.trim() === "transparent") {
|
|
1325
|
+
return "none";
|
|
1326
|
+
}
|
|
1327
|
+
return "solid";
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/** Show hex without the leading # for the display field (matches Figma). */
|
|
1331
|
+
function toDisplayHex(color: RgbaColor): string {
|
|
1332
|
+
return rgbaToHex(color).replace(/^#/, "").toUpperCase();
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function swatchStyle(value: string): {
|
|
1336
|
+
backgroundColor?: string;
|
|
1337
|
+
backgroundImage?: string;
|
|
1338
|
+
backgroundSize?: string;
|
|
1339
|
+
backgroundPosition?: string;
|
|
1340
|
+
} {
|
|
1341
|
+
const parsed = parseCssColor(value);
|
|
1342
|
+
if (parsed && parsed.a < 1) {
|
|
1343
|
+
return {
|
|
1344
|
+
backgroundImage: `${CHECKERBOARD_IMAGE}, linear-gradient(${rgbaToCss(parsed)}, ${rgbaToCss(parsed)})`,
|
|
1345
|
+
backgroundSize: "8px 8px, 8px 8px, 8px 8px, 8px 8px, 100% 100%",
|
|
1346
|
+
backgroundPosition: "0 0, 0 4px, 4px -4px, -4px 0, 0 0",
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
if (parsed) return { backgroundColor: rgbaToCss(parsed) };
|
|
1350
|
+
return { backgroundImage: value || "none" };
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function alphaTrackBackground(color: RgbaColor): string {
|
|
1354
|
+
return `${CHECKERBOARD_IMAGE}, linear-gradient(90deg, rgba(${color.r}, ${color.g}, ${color.b}, 0), rgba(${color.r}, ${color.g}, ${color.b}, 1))`;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function rgbaToHsv(color: RgbaColor): HsvaColor {
|
|
1358
|
+
const r = clampFloat(color.r / 255, 0, 1);
|
|
1359
|
+
const g = clampFloat(color.g / 255, 0, 1);
|
|
1360
|
+
const b = clampFloat(color.b / 255, 0, 1);
|
|
1361
|
+
const max = Math.max(r, g, b);
|
|
1362
|
+
const min = Math.min(r, g, b);
|
|
1363
|
+
const delta = max - min;
|
|
1364
|
+
|
|
1365
|
+
let h = 0;
|
|
1366
|
+
if (delta !== 0) {
|
|
1367
|
+
if (max === r) h = ((g - b) / delta) % 6;
|
|
1368
|
+
else if (max === g) h = (b - r) / delta + 2;
|
|
1369
|
+
else h = (r - g) / delta + 4;
|
|
1370
|
+
h *= 60;
|
|
1371
|
+
if (h < 0) h += 360;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
return {
|
|
1375
|
+
h: Math.round(h),
|
|
1376
|
+
s: max === 0 ? 0 : Math.round((delta / max) * 100),
|
|
1377
|
+
v: Math.round(max * 100),
|
|
1378
|
+
a: color.a,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function hsvToRgba(color: HsvaColor): RgbaColor {
|
|
1383
|
+
const h = ((color.h % 360) + 360) % 360;
|
|
1384
|
+
const s = clampFloat(color.s, 0, 100) / 100;
|
|
1385
|
+
const v = clampFloat(color.v, 0, 100) / 100;
|
|
1386
|
+
const chroma = v * s;
|
|
1387
|
+
const x = chroma * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
1388
|
+
const m = v - chroma;
|
|
1389
|
+
|
|
1390
|
+
let r = 0,
|
|
1391
|
+
g = 0,
|
|
1392
|
+
b = 0;
|
|
1393
|
+
if (h < 60) [r, g, b] = [chroma, x, 0];
|
|
1394
|
+
else if (h < 120) [r, g, b] = [x, chroma, 0];
|
|
1395
|
+
else if (h < 180) [r, g, b] = [0, chroma, x];
|
|
1396
|
+
else if (h < 240) [r, g, b] = [0, x, chroma];
|
|
1397
|
+
else if (h < 300) [r, g, b] = [x, 0, chroma];
|
|
1398
|
+
else [r, g, b] = [chroma, 0, x];
|
|
1399
|
+
|
|
1400
|
+
return {
|
|
1401
|
+
r: clamp(Math.round((r + m) * 255), 0, 255),
|
|
1402
|
+
g: clamp(Math.round((g + m) * 255), 0, 255),
|
|
1403
|
+
b: clamp(Math.round((b + m) * 255), 0, 255),
|
|
1404
|
+
a: clampFloat(color.a, 0, 1),
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function clamp(value: number, min: number, max: number): number {
|
|
1409
|
+
if (!Number.isFinite(value)) return min;
|
|
1410
|
+
return Math.max(min, Math.min(max, Math.round(value)));
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function clampFloat(value: number, min: number, max: number): number {
|
|
1414
|
+
if (!Number.isFinite(value)) return min;
|
|
1415
|
+
return Math.max(min, Math.min(max, value));
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function hasHexAlpha(value: string): boolean {
|
|
1419
|
+
return /^#?(?:[0-9a-f]{4}|[0-9a-f]{8})$/i.test(value.trim());
|
|
1420
|
+
}
|