@agent-native/core 0.80.0 → 0.80.1
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 +8 -0
- package/corpus/core/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/de-DE/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/de-DE/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/es-ES/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/es-ES/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +16 -1
- package/corpus/core/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +14 -1
- package/corpus/core/docs/content/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/template-design.mdx +14 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/application-state/handlers.ts +1 -1
- package/corpus/core/src/brand-kit/index.ts +1 -1
- package/corpus/core/src/cli/skills.ts +18 -0
- package/corpus/core/src/client/AgentPanel.tsx +1 -1
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
- package/corpus/core/src/client/extensions/portable-extension.ts +1 -1
- package/corpus/core/src/code-agents/transcript-normalizer.ts +1 -1
- package/corpus/core/src/extensions/path.ts +1 -4
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +1 -1
- package/corpus/core/src/mcp/build-server.ts +1 -1
- package/corpus/core/src/org/handlers.ts +5 -5
- package/corpus/core/src/org/plugin.ts +2 -2
- package/corpus/core/src/scripts/call-agent.ts +1 -1
- package/corpus/core/src/scripts/db/safety.ts +1 -1
- package/corpus/core/src/server/auth.ts +3 -0
- package/corpus/core/src/server/builder-design-systems.ts +308 -0
- package/corpus/core/src/server/core-routes-plugin.ts +29 -3
- package/corpus/core/src/server/index.ts +8 -0
- package/corpus/core/src/styles/agent-native.css +14 -7
- package/corpus/core/src/usage/store.ts +2 -2
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +298 -15
- package/corpus/templates/analytics/actions/get-sql-dashboard.ts +9 -4
- package/corpus/templates/analytics/actions/mutate-dashboard.ts +18 -2
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +12 -2
- package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +66 -0
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +37 -0
- package/corpus/templates/analytics/app/i18n-data.ts +404 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +333 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +121 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +49 -26
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +137 -36
- package/corpus/templates/analytics/changelog/2026-06-29-analytics-agents-can-now-place-new-charts-into-requested-das.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-analytics-queries-now-report-failed-source-query-errors-inst.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-charts-can-now-be-dropped-between-full-dashboard-rows-to-cre.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-dashboard-email-reports-include-the-full-dashboard-image-wit.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-main-surface-border-reaches-the-top.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-session-replay-now-records-playable-sessions-in-production-a.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-set-up-session-replay-storage-from-settings-connect-builder-.md +6 -0
- package/corpus/templates/analytics/scripts/cleanup-empty-session-recordings.ts +102 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +49 -26
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +81 -20
- package/corpus/templates/analytics/server/lib/replay-storage.ts +79 -0
- package/corpus/templates/analytics/server/lib/s3-upload-provider.ts +255 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +29 -8
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -2
- package/corpus/templates/analytics/server/plugins/onboarding.ts +97 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +24 -0
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +105 -5
- package/corpus/templates/content/actions/set-image-alt-text.ts +1 -1
- package/corpus/templates/content/shared/document-export.ts +1 -1
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -18
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +17 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +32 -5
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +259 -0
- package/corpus/templates/design/actions/update-file.ts +9 -2
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +33 -75
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +72 -9
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +41 -39
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1029 -249
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +450 -101
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +978 -154
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +7 -9
- package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +133 -66
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +1234 -344
- package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +144 -94
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +301 -72
- package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +876 -286
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +601 -0
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +242 -0
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +30 -7
- package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +212 -155
- package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +423 -0
- package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +487 -107
- package/corpus/templates/design/app/components/design/inspector/index.ts +27 -0
- package/corpus/templates/design/app/global.css +29 -3
- package/corpus/templates/design/app/i18n/zh-TW.ts +13 -0
- package/corpus/templates/design/app/i18n-data.ts +239 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +1023 -353
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +57 -221
- package/corpus/templates/design/changelog/2026-06-29-canvas-selection-no-longer-flashes-broken-prototype-html-whe.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-command-menu-selections-are-clearer-in-dark-mode.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-element-selection-now-stays-on-the-intended-layer-after-sele.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-full-view-controls-now-sit-above-frames-inst.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-tools-can-draw-shapes-create-screens-and-kee.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-now-creates-anchor-based-bezier-vectors-with-ha.md +6 -0
- package/corpus/templates/design/server/handlers/import-figma-system.ts +38 -85
- package/corpus/templates/design/server/lib/design-snapshot.ts +9 -1
- package/corpus/templates/design/shared/code-layer.ts +209 -9
- package/corpus/templates/design/shared/html-content.ts +21 -0
- package/corpus/templates/design/shared/pen-path.ts +207 -0
- package/corpus/templates/mail/app/lib/utils.ts +2 -2
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +9 -10
- package/corpus/templates/slides/AGENTS.md +3 -2
- package/corpus/templates/slides/actions/import-file.ts +26 -17
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +259 -0
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +35 -167
- package/corpus/templates/slides/app/components/design-system/fig-import-response.ts +9 -13
- package/corpus/templates/slides/app/context/DeckContext.tsx +1 -1
- package/corpus/templates/slides/app/i18n/ar-SA.ts +9 -37
- package/corpus/templates/slides/app/i18n/de-DE.ts +10 -25
- package/corpus/templates/slides/app/i18n/en-US.ts +9 -26
- package/corpus/templates/slides/app/i18n/es-ES.ts +10 -28
- package/corpus/templates/slides/app/i18n/fr-FR.ts +10 -28
- package/corpus/templates/slides/app/i18n/hi-IN.ts +9 -26
- package/corpus/templates/slides/app/i18n/ja-JP.ts +10 -22
- package/corpus/templates/slides/app/i18n/ko-KR.ts +9 -23
- package/corpus/templates/slides/app/i18n/pt-BR.ts +10 -28
- package/corpus/templates/slides/app/i18n/zh-CN.ts +9 -22
- package/corpus/templates/slides/app/i18n/zh-TW.ts +9 -22
- package/corpus/templates/slides/server/handlers/import-figma-system.ts +44 -12
- package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
- package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +1 -1
- package/dist/application-state/handlers.js +1 -1
- package/dist/application-state/handlers.js.map +1 -1
- package/dist/brand-kit/index.d.ts +1 -1
- package/dist/brand-kit/index.js +1 -1
- package/dist/brand-kit/index.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +18 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.js +1 -1
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +1 -1
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/extensions/portable-extension.d.ts +1 -1
- package/dist/client/extensions/portable-extension.d.ts.map +1 -1
- package/dist/client/extensions/portable-extension.js.map +1 -1
- package/dist/code-agents/transcript-normalizer.js +1 -1
- package/dist/code-agents/transcript-normalizer.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/extensions/path.d.ts +1 -1
- package/dist/extensions/path.d.ts.map +1 -1
- package/dist/extensions/path.js.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +1 -1
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/mcp/build-server.d.ts +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/handlers.js +5 -5
- package/dist/org/handlers.js.map +1 -1
- package/dist/org/plugin.js +2 -2
- package/dist/org/plugin.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/scripts/call-agent.js +1 -1
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/scripts/db/safety.js +1 -1
- package/dist/scripts/db/safety.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +3 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-design-systems.d.ts +28 -0
- package/dist/server/builder-design-systems.d.ts.map +1 -0
- package/dist/server/builder-design-systems.js +217 -0
- package/dist/server/builder-design-systems.js.map +1 -0
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +20 -3
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +14 -7
- package/dist/usage/store.js +2 -2
- package/dist/usage/store.js.map +1 -1
- package/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
- package/docs/content/locales/ar-SA/template-design.mdx +14 -1
- package/docs/content/locales/de-DE/skills-guide.mdx +5 -0
- package/docs/content/locales/de-DE/template-design.mdx +15 -1
- package/docs/content/locales/es-ES/skills-guide.mdx +5 -0
- package/docs/content/locales/es-ES/template-design.mdx +15 -1
- package/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
- package/docs/content/locales/fr-FR/template-design.mdx +15 -1
- package/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
- package/docs/content/locales/hi-IN/template-design.mdx +15 -1
- package/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
- package/docs/content/locales/ja-JP/template-design.mdx +16 -1
- package/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
- package/docs/content/locales/ko-KR/template-design.mdx +14 -1
- package/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
- package/docs/content/locales/pt-BR/template-design.mdx +14 -1
- package/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
- package/docs/content/locales/zh-CN/template-design.mdx +14 -1
- package/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
- package/docs/content/locales/zh-TW/template-design.mdx +14 -1
- package/docs/content/skills-guide.mdx +5 -0
- package/docs/content/template-design.mdx +14 -1
- package/package.json +1 -1
- package/corpus/templates/design/actions/import-figma-file.ts +0 -166
- package/corpus/templates/slides/server/lib/fig-design-system.ts +0 -110
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
type HslaColor,
|
|
11
11
|
type RgbaColor,
|
|
12
12
|
} from "@shared/color-utils";
|
|
13
|
-
import {
|
|
13
|
+
import type { ShaderDescriptor } from "@shared/shader-presets";
|
|
14
|
+
import { IconChevronDown, IconColorPicker } from "@tabler/icons-react";
|
|
14
15
|
import {
|
|
15
16
|
useEffect,
|
|
16
17
|
useRef,
|
|
@@ -26,13 +27,6 @@ import {
|
|
|
26
27
|
PopoverContent,
|
|
27
28
|
PopoverTrigger,
|
|
28
29
|
} from "@/components/ui/popover";
|
|
29
|
-
import {
|
|
30
|
-
Select,
|
|
31
|
-
SelectContent,
|
|
32
|
-
SelectItem,
|
|
33
|
-
SelectTrigger,
|
|
34
|
-
SelectValue,
|
|
35
|
-
} from "@/components/ui/select";
|
|
36
30
|
import {
|
|
37
31
|
Tooltip,
|
|
38
32
|
TooltipContent,
|
|
@@ -40,6 +34,22 @@ import {
|
|
|
40
34
|
} from "@/components/ui/tooltip";
|
|
41
35
|
import { cn } from "@/lib/utils";
|
|
42
36
|
|
|
37
|
+
import {
|
|
38
|
+
GradientEditor,
|
|
39
|
+
defaultGradient,
|
|
40
|
+
gradientToCss,
|
|
41
|
+
parseGradientCss,
|
|
42
|
+
type GradientKind,
|
|
43
|
+
type GradientValue,
|
|
44
|
+
} from "./GradientEditor";
|
|
45
|
+
import {
|
|
46
|
+
ImageFillControls,
|
|
47
|
+
imageFillToCss,
|
|
48
|
+
parseImageFillCss,
|
|
49
|
+
type ImageFillValue,
|
|
50
|
+
} from "./ImageFillControls";
|
|
51
|
+
import { ShaderFillsPanel, shaderDescriptorToCss } from "./ShaderFillsPanel";
|
|
52
|
+
|
|
43
53
|
// ─── Public types ──────────────────────────────────────────────────────────────
|
|
44
54
|
|
|
45
55
|
export type FigmaColorMode = "hex" | "rgb" | "hsl" | "hsb";
|
|
@@ -53,6 +63,9 @@ export type FigmaPaintType =
|
|
|
53
63
|
| "diamond"
|
|
54
64
|
| "image"
|
|
55
65
|
| "video"
|
|
66
|
+
| "shader"
|
|
67
|
+
| "noise"
|
|
68
|
+
| "pattern"
|
|
56
69
|
| "none";
|
|
57
70
|
|
|
58
71
|
// These interfaces remain so EditPanel's prop types don't break, even though
|
|
@@ -116,6 +129,7 @@ export interface FigmaColorPickerLabels {
|
|
|
116
129
|
export interface FigmaColorPickerProps {
|
|
117
130
|
value: string;
|
|
118
131
|
onChange: (value: string) => void;
|
|
132
|
+
onPaintValueChange?: (value: string) => void;
|
|
119
133
|
label?: string;
|
|
120
134
|
opacity?: number;
|
|
121
135
|
onOpacityChange?: (opacity: number) => void;
|
|
@@ -140,6 +154,25 @@ export interface FigmaColorPickerProps {
|
|
|
140
154
|
onGradientStopChange?: (id: string, patch: FigmaGradientStopPatch) => void;
|
|
141
155
|
onAddGradientStop?: () => void;
|
|
142
156
|
onRemoveGradientStop?: (id: string) => void;
|
|
157
|
+
/**
|
|
158
|
+
* Colors already present in the document (e.g. unique hex values collected
|
|
159
|
+
* from the current selection or page). Rendered as a swatch grid under the
|
|
160
|
+
* "Document colors" heading — click any swatch to apply it. Deduplicated and
|
|
161
|
+
* limited in the caller; the component renders whatever is passed.
|
|
162
|
+
*/
|
|
163
|
+
documentColors?: string[];
|
|
164
|
+
/**
|
|
165
|
+
* Optional design context forwarded to the apply-shader action when a shader
|
|
166
|
+
* fill is selected, so the agent can write real shader code for the target.
|
|
167
|
+
*/
|
|
168
|
+
shaderContext?: {
|
|
169
|
+
designId?: string;
|
|
170
|
+
fileId?: string;
|
|
171
|
+
nodeId?: string;
|
|
172
|
+
selector?: string;
|
|
173
|
+
};
|
|
174
|
+
/** Notified when a shader fill is applied/tuned (descriptor + CSS fallback). */
|
|
175
|
+
onShaderChange?: (descriptor: ShaderDescriptor, css: string) => void;
|
|
143
176
|
labels?: Partial<FigmaColorPickerLabels>;
|
|
144
177
|
disabled?: boolean;
|
|
145
178
|
className?: string;
|
|
@@ -414,6 +447,90 @@ function IconNoneFill({ className }: { className?: string }) {
|
|
|
414
447
|
);
|
|
415
448
|
}
|
|
416
449
|
|
|
450
|
+
function IconShaderFill({ className }: { className?: string }) {
|
|
451
|
+
// Droplet — Figma uses a teardrop for shader/blur-type fills.
|
|
452
|
+
return (
|
|
453
|
+
<svg
|
|
454
|
+
viewBox="0 0 24 24"
|
|
455
|
+
fill="none"
|
|
456
|
+
stroke="currentColor"
|
|
457
|
+
strokeWidth={2}
|
|
458
|
+
strokeLinecap="round"
|
|
459
|
+
strokeLinejoin="round"
|
|
460
|
+
className={className}
|
|
461
|
+
>
|
|
462
|
+
<defs>
|
|
463
|
+
<linearGradient id="shader-ico" x1="0" y1="0" x2="1" y2="1">
|
|
464
|
+
<stop offset="0%" stopColor="currentColor" stopOpacity="0.9" />
|
|
465
|
+
<stop offset="100%" stopColor="currentColor" stopOpacity="0.25" />
|
|
466
|
+
</linearGradient>
|
|
467
|
+
</defs>
|
|
468
|
+
<path
|
|
469
|
+
d="M12 3c3.5 4 6 7 6 10a6 6 0 0 1-12 0c0-3 2.5-6 6-10z"
|
|
470
|
+
fill="url(#shader-ico)"
|
|
471
|
+
stroke="currentColor"
|
|
472
|
+
strokeOpacity="0.7"
|
|
473
|
+
/>
|
|
474
|
+
</svg>
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function IconNoiseFill({ className }: { className?: string }) {
|
|
479
|
+
return (
|
|
480
|
+
<svg
|
|
481
|
+
viewBox="0 0 24 24"
|
|
482
|
+
fill="currentColor"
|
|
483
|
+
stroke="none"
|
|
484
|
+
className={className}
|
|
485
|
+
>
|
|
486
|
+
<rect
|
|
487
|
+
x="3.5"
|
|
488
|
+
y="3.5"
|
|
489
|
+
width="17"
|
|
490
|
+
height="17"
|
|
491
|
+
rx="2"
|
|
492
|
+
fill="none"
|
|
493
|
+
stroke="currentColor"
|
|
494
|
+
strokeWidth={1.6}
|
|
495
|
+
strokeOpacity="0.6"
|
|
496
|
+
/>
|
|
497
|
+
{[
|
|
498
|
+
[7, 7],
|
|
499
|
+
[11, 6.5],
|
|
500
|
+
[15, 8],
|
|
501
|
+
[8, 10.5],
|
|
502
|
+
[13, 11],
|
|
503
|
+
[16.5, 11.5],
|
|
504
|
+
[6.5, 13],
|
|
505
|
+
[10, 14],
|
|
506
|
+
[14, 13.5],
|
|
507
|
+
[9, 16.5],
|
|
508
|
+
[13, 16.5],
|
|
509
|
+
[16, 15.5],
|
|
510
|
+
].map(([cx, cy], index) => (
|
|
511
|
+
<circle key={index} cx={cx} cy={cy} r="0.9" />
|
|
512
|
+
))}
|
|
513
|
+
</svg>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function IconPatternFill({ className }: { className?: string }) {
|
|
518
|
+
return (
|
|
519
|
+
<svg
|
|
520
|
+
viewBox="0 0 24 24"
|
|
521
|
+
fill="none"
|
|
522
|
+
stroke="currentColor"
|
|
523
|
+
strokeWidth={1.7}
|
|
524
|
+
strokeLinecap="round"
|
|
525
|
+
strokeLinejoin="round"
|
|
526
|
+
className={className}
|
|
527
|
+
>
|
|
528
|
+
<rect x="3.5" y="3.5" width="17" height="17" rx="2" strokeOpacity="0.6" />
|
|
529
|
+
<path d="M3.5 9h17M3.5 15h17M9 3.5v17M15 3.5v17" strokeOpacity="0.85" />
|
|
530
|
+
</svg>
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
417
534
|
// ─── Paint type definitions (only supported types rendered) ────────────────────
|
|
418
535
|
|
|
419
536
|
const PAINT_TYPES: Array<{
|
|
@@ -428,39 +545,36 @@ const PAINT_TYPES: Array<{
|
|
|
428
545
|
{ type: "diamond", label: "Diamond", Icon: IconDiamondGradient }, // i18n-ignore paint type label
|
|
429
546
|
{ type: "image", label: "Image", Icon: IconImageFill }, // i18n-ignore paint type label
|
|
430
547
|
{ type: "video", label: "Video", Icon: IconVideoFill }, // i18n-ignore paint type label
|
|
548
|
+
{ type: "shader", label: "Shader", Icon: IconShaderFill }, // i18n-ignore paint type label
|
|
549
|
+
{ type: "noise", label: "Noise", Icon: IconNoiseFill }, // i18n-ignore paint type label
|
|
550
|
+
{ type: "pattern", label: "Pattern", Icon: IconPatternFill }, // i18n-ignore paint type label
|
|
431
551
|
{ type: "none", label: "None", Icon: IconNoneFill }, // i18n-ignore paint type label
|
|
432
552
|
];
|
|
433
553
|
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
"
|
|
440
|
-
"
|
|
441
|
-
"
|
|
442
|
-
"
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
"#
|
|
449
|
-
|
|
450
|
-
"#
|
|
451
|
-
"#e6d05c",
|
|
452
|
-
"#9ca3af",
|
|
453
|
-
"#020617",
|
|
454
|
-
"#171717",
|
|
455
|
-
"#d4d4d8",
|
|
456
|
-
"#27272a",
|
|
457
|
-
];
|
|
554
|
+
// Alias used internally before the exported constant is defined below.
|
|
555
|
+
// Both point at the same member set — keep reads using this name so the
|
|
556
|
+
// component body compiles even though the exported constant is declared
|
|
557
|
+
// at the bottom of the file (hoisting doesn't apply to const).
|
|
558
|
+
const GRADIENT_TYPES = new Set<FigmaPaintType>([
|
|
559
|
+
"linear",
|
|
560
|
+
"radial",
|
|
561
|
+
"angular",
|
|
562
|
+
"diamond",
|
|
563
|
+
]);
|
|
564
|
+
|
|
565
|
+
// Static fallback fills for the "functional but minimal" paint types so the
|
|
566
|
+
// element fill always reflects the selected mode even without bespoke editors.
|
|
567
|
+
const NOISE_FALLBACK_CSS =
|
|
568
|
+
"repeating-conic-gradient(#0000 0% 25%, #00000010 0% 50%) 0 0 / 6px 6px, #8a8a8a";
|
|
569
|
+
const PATTERN_FALLBACK_CSS =
|
|
570
|
+
"repeating-linear-gradient(45deg, #00000014 0 6px, #ffffff14 6px 12px), #9aa0a6";
|
|
458
571
|
|
|
459
572
|
// ─── Main component ────────────────────────────────────────────────────────────
|
|
460
573
|
|
|
461
574
|
export function FigmaColorPicker({
|
|
462
575
|
value,
|
|
463
576
|
onChange,
|
|
577
|
+
onPaintValueChange,
|
|
464
578
|
label: _label,
|
|
465
579
|
opacity,
|
|
466
580
|
onOpacityChange,
|
|
@@ -475,14 +589,17 @@ export function FigmaColorPicker({
|
|
|
475
589
|
onRemoveFill: _onRemoveFill,
|
|
476
590
|
paintType,
|
|
477
591
|
onPaintTypeChange,
|
|
478
|
-
gradientType
|
|
479
|
-
onGradientTypeChange
|
|
592
|
+
gradientType,
|
|
593
|
+
onGradientTypeChange,
|
|
480
594
|
gradientStops: _gradientStops,
|
|
481
595
|
selectedStopId: _selectedStopId,
|
|
482
596
|
onGradientStopSelect: _onGradientStopSelect,
|
|
483
597
|
onGradientStopChange: _onGradientStopChange,
|
|
484
598
|
onAddGradientStop: _onAddGradientStop,
|
|
485
599
|
onRemoveGradientStop: _onRemoveGradientStop,
|
|
600
|
+
documentColors,
|
|
601
|
+
shaderContext,
|
|
602
|
+
onShaderChange,
|
|
486
603
|
labels,
|
|
487
604
|
disabled = false,
|
|
488
605
|
className,
|
|
@@ -496,23 +613,95 @@ export function FigmaColorPicker({
|
|
|
496
613
|
const [mode, setMode] = useState<FigmaColorMode>("hex");
|
|
497
614
|
const [hexDraft, setHexDraft] = useState(() => toDisplayHex(color));
|
|
498
615
|
const [open, setOpen] = useState(false);
|
|
499
|
-
const [sourceTab, setSourceTab] = useState<"custom" | "libraries">("custom");
|
|
500
616
|
const [picking, setPicking] = useState(false);
|
|
501
617
|
const skipNextHexBlurCommitRef = useRef(false);
|
|
502
618
|
|
|
503
|
-
|
|
504
|
-
|
|
619
|
+
// Whole-popover view: the standard picker, or the Figma "Shader fills" panel.
|
|
620
|
+
const [view, setView] = useState<"picker" | "shader">("picker");
|
|
621
|
+
|
|
622
|
+
// Self-managed paint-type fallback for when EditPanel doesn't drive it.
|
|
623
|
+
const [localPaintType, setLocalPaintType] = useState<FigmaPaintType | null>(
|
|
624
|
+
null,
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
// Locally-managed gradient/image/shader state, seeded from the current value
|
|
628
|
+
// so the editors round-trip when EditPanel passes the CSS back through value.
|
|
629
|
+
const [localGradient, setLocalGradient] = useState<GradientValue | null>(
|
|
630
|
+
null,
|
|
631
|
+
);
|
|
632
|
+
const [selectedStopId, setSelectedStopId] = useState<string>("");
|
|
633
|
+
const [imageFill, setImageFill] = useState<ImageFillValue>({
|
|
634
|
+
url: "",
|
|
635
|
+
fit: "fill",
|
|
636
|
+
});
|
|
637
|
+
const [shaderDescriptor, setShaderDescriptor] =
|
|
638
|
+
useState<ShaderDescriptor | null>(null);
|
|
639
|
+
|
|
640
|
+
// The user's explicit paint-type click (localPaintType) wins over the
|
|
641
|
+
// EditPanel-driven `paintType` prop so selecting a gradient/image/shader
|
|
642
|
+
// engages its editor even when EditPanel doesn't complete the structural
|
|
643
|
+
// fill switch. localPaintType is reset below when the prop changes (i.e. a
|
|
644
|
+
// different element/fill is selected) so the picker still follows selection.
|
|
645
|
+
const effectivePaintType: FigmaPaintType =
|
|
646
|
+
localPaintType ?? paintType ?? inferPaintType(value, effectiveOpacity);
|
|
647
|
+
|
|
648
|
+
// Resolve the active gradient: prefer EditPanel-driven props; otherwise parse
|
|
649
|
+
// the live CSS value, falling back to local edit state.
|
|
650
|
+
const parsedGradient = parseGradientCss(value, gradientType ?? "linear");
|
|
651
|
+
const activeGradient: GradientValue | null = GRADIENT_TYPES.has(
|
|
652
|
+
effectivePaintType,
|
|
653
|
+
)
|
|
654
|
+
? (localGradient ??
|
|
655
|
+
parsedGradient ??
|
|
656
|
+
defaultGradient(
|
|
657
|
+
effectivePaintType as GradientKind,
|
|
658
|
+
toCssColor(color) || "#000000",
|
|
659
|
+
))
|
|
660
|
+
: null;
|
|
505
661
|
|
|
506
662
|
useEffect(() => {
|
|
507
663
|
setHexDraft(toDisplayHex(color));
|
|
508
664
|
}, [color.r, color.g, color.b]);
|
|
509
665
|
|
|
666
|
+
// The local override (the user's explicit paint-type click) persists for the
|
|
667
|
+
// life of the open popover so EditPanel bouncing `paintType` back to solid
|
|
668
|
+
// can't wipe a just-selected gradient/image/shader. A new element selection
|
|
669
|
+
// remounts this popover content, which resets the local state naturally.
|
|
670
|
+
|
|
671
|
+
// Keep image-fill state synced when the incoming value is an image fill.
|
|
672
|
+
useEffect(() => {
|
|
673
|
+
if (effectivePaintType !== "image") return;
|
|
674
|
+
const parsed = parseImageFillCss(value);
|
|
675
|
+
if (
|
|
676
|
+
parsed &&
|
|
677
|
+
(parsed.url !== imageFill.url || parsed.fit !== imageFill.fit)
|
|
678
|
+
) {
|
|
679
|
+
setImageFill(parsed);
|
|
680
|
+
}
|
|
681
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
682
|
+
}, [value, effectivePaintType]);
|
|
683
|
+
|
|
684
|
+
// Ensure a selected stop id exists whenever a gradient is active.
|
|
685
|
+
useEffect(() => {
|
|
686
|
+
if (!activeGradient) return;
|
|
687
|
+
const ids = activeGradient.stops.map((s) => s.id);
|
|
688
|
+
if (!ids.includes(selectedStopId)) {
|
|
689
|
+
setSelectedStopId(activeGradient.stops[0]?.id ?? "");
|
|
690
|
+
}
|
|
691
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
692
|
+
}, [activeGradient?.stops.map((s) => s.id).join(",")]);
|
|
693
|
+
|
|
510
694
|
// ── Emit helpers ────────────────────────────────────────────────────────────
|
|
511
695
|
|
|
512
696
|
const emitColor = (nextColor: RgbaColor, nextOpacity = effectiveOpacity) => {
|
|
513
697
|
onChange(rgbaToCss(withColorOpacity(nextColor, nextOpacity)));
|
|
514
698
|
};
|
|
515
699
|
|
|
700
|
+
const emitPaintValue = (nextValue: string) => {
|
|
701
|
+
if (onPaintValueChange) onPaintValueChange(nextValue);
|
|
702
|
+
else onChange(nextValue);
|
|
703
|
+
};
|
|
704
|
+
|
|
516
705
|
const emitColorFromHsv = (nextHsv: HsvaColor) => {
|
|
517
706
|
emitColor(hsvToRgba({ ...nextHsv, a: opacityToAlpha(effectiveOpacity) }));
|
|
518
707
|
};
|
|
@@ -524,7 +713,12 @@ export function FigmaColorPicker({
|
|
|
524
713
|
const commitHex = () => {
|
|
525
714
|
const parsed = parseCssColor(`#${hexDraft.replace(/^#/, "")}`);
|
|
526
715
|
if (!parsed) {
|
|
527
|
-
setHexDraft(toDisplayHex(color));
|
|
716
|
+
setHexDraft(toDisplayHex(activeGradient ? fieldColor : color));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (activeGradient) {
|
|
720
|
+
const hexIncludesAlpha = hasHexAlpha(hexDraft);
|
|
721
|
+
emitStopColor(hexIncludesAlpha ? parsed : { ...parsed, a: fieldColor.a });
|
|
528
722
|
return;
|
|
529
723
|
}
|
|
530
724
|
const hexIncludesAlpha = hasHexAlpha(hexDraft);
|
|
@@ -540,12 +734,99 @@ export function FigmaColorPicker({
|
|
|
540
734
|
else onChange(rgbaToCss(withColorOpacity(color, nextOpacity)));
|
|
541
735
|
};
|
|
542
736
|
|
|
737
|
+
// ── Gradient editing ─────────────────────────────────────────────────────────
|
|
738
|
+
|
|
739
|
+
const emitGradient = (next: GradientValue) => {
|
|
740
|
+
setLocalGradient(next);
|
|
741
|
+
if (onGradientTypeChange && next.kind !== gradientType) {
|
|
742
|
+
onGradientTypeChange(next.kind as FigmaGradientType);
|
|
743
|
+
}
|
|
744
|
+
emitPaintValue(gradientToCss(next));
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
const selectedStop =
|
|
748
|
+
activeGradient?.stops.find((s) => s.id === selectedStopId) ??
|
|
749
|
+
activeGradient?.stops[0];
|
|
750
|
+
|
|
751
|
+
// The 2D field edits the selected gradient stop's color when in gradient mode.
|
|
752
|
+
const fieldColor: RgbaColor = activeGradient
|
|
753
|
+
? (parseCssColor(selectedStop?.color ?? "#000000") ?? FALLBACK_COLOR)
|
|
754
|
+
: color;
|
|
755
|
+
const fieldHsv = rgbaToHsv(fieldColor);
|
|
756
|
+
const fieldHsl = rgbaToHsl(fieldColor);
|
|
757
|
+
|
|
758
|
+
// In gradient mode, mirror the selected stop's color into the hex draft.
|
|
759
|
+
const selectedStopColor = selectedStop?.color;
|
|
760
|
+
useEffect(() => {
|
|
761
|
+
if (!activeGradient || !selectedStopColor) return;
|
|
762
|
+
const parsed = parseCssColor(selectedStopColor);
|
|
763
|
+
if (parsed) setHexDraft(toDisplayHex(parsed));
|
|
764
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
765
|
+
}, [selectedStopColor, selectedStopId]);
|
|
766
|
+
|
|
767
|
+
const emitStopColor = (nextColor: RgbaColor) => {
|
|
768
|
+
if (!activeGradient || !selectedStop) return;
|
|
769
|
+
emitGradient({
|
|
770
|
+
...activeGradient,
|
|
771
|
+
stops: activeGradient.stops.map((stop) =>
|
|
772
|
+
stop.id === selectedStop.id
|
|
773
|
+
? { ...stop, color: rgbaToCss(nextColor) }
|
|
774
|
+
: stop,
|
|
775
|
+
),
|
|
776
|
+
});
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// Value-row emit helpers: route to the selected stop in gradient mode,
|
|
780
|
+
// else to the solid color (preserving the existing solid behavior).
|
|
781
|
+
const emitFieldColor = (next: RgbaColor) => {
|
|
782
|
+
if (activeGradient) emitStopColor({ ...next, a: fieldColor.a });
|
|
783
|
+
else emitColor(next);
|
|
784
|
+
};
|
|
785
|
+
const emitFieldHsl = (next: HslaColor) => {
|
|
786
|
+
if (activeGradient) emitStopColor(hslToRgba({ ...next, a: fieldColor.a }));
|
|
787
|
+
else emitColorFromHsl(next);
|
|
788
|
+
};
|
|
789
|
+
const emitFieldHsv = (next: HsvaColor) => {
|
|
790
|
+
if (activeGradient) emitStopColor(hsvToRgba({ ...next, a: fieldColor.a }));
|
|
791
|
+
else emitColorFromHsv(next);
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
// ── Image editing ─────────────────────────────────────────────────────────────
|
|
795
|
+
|
|
796
|
+
const emitImageFill = (next: ImageFillValue) => {
|
|
797
|
+
setImageFill(next);
|
|
798
|
+
emitPaintValue(imageFillToCss(next));
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// ── Shader editing ──────────────────────────────────────────────────────────────
|
|
802
|
+
|
|
803
|
+
const emitShader = (descriptor: ShaderDescriptor, css: string) => {
|
|
804
|
+
setShaderDescriptor(descriptor);
|
|
805
|
+
onShaderChange?.(descriptor, css);
|
|
806
|
+
emitPaintValue(css);
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
// ── Paint-type switching (does real work for every type) ──────────────────────
|
|
810
|
+
|
|
543
811
|
const setPaintType = (nextType: FigmaPaintType) => {
|
|
544
812
|
if (disabled) return;
|
|
813
|
+
|
|
814
|
+
// Shader opens the dedicated Figma "Shader fills" panel.
|
|
815
|
+
if (nextType === "shader") {
|
|
816
|
+
setLocalPaintType("shader");
|
|
817
|
+
setView("shader");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Defer structural fill changes to EditPanel when it manages layered fills.
|
|
545
822
|
if (onPaintTypeChange) {
|
|
823
|
+
setLocalPaintType(nextType);
|
|
546
824
|
onPaintTypeChange(nextType);
|
|
547
825
|
return;
|
|
548
826
|
}
|
|
827
|
+
|
|
828
|
+
setLocalPaintType(nextType);
|
|
829
|
+
|
|
549
830
|
if (nextType === "none") {
|
|
550
831
|
onChange("transparent");
|
|
551
832
|
return;
|
|
@@ -554,6 +835,36 @@ export function FigmaColorPicker({
|
|
|
554
835
|
emitColor(color, effectiveOpacity > 0 ? effectiveOpacity : 100);
|
|
555
836
|
return;
|
|
556
837
|
}
|
|
838
|
+
if (GRADIENT_TYPES.has(nextType)) {
|
|
839
|
+
const base =
|
|
840
|
+
activeGradient ??
|
|
841
|
+
defaultGradient(
|
|
842
|
+
nextType as GradientKind,
|
|
843
|
+
toCssColor(color) || "#000000",
|
|
844
|
+
);
|
|
845
|
+
const next: GradientValue = { ...base, kind: nextType as GradientKind };
|
|
846
|
+
setSelectedStopId(next.stops[0]?.id ?? "");
|
|
847
|
+
emitGradient(next);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (nextType === "image") {
|
|
851
|
+
emitPaintValue(imageFill.url ? imageFillToCss(imageFill) : "transparent");
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (nextType === "video") {
|
|
855
|
+
// No standalone CSS for video; mark the fill type and keep a checker fill
|
|
856
|
+
// until a source is wired. The agent can replace it with a <video> layer.
|
|
857
|
+
emitPaintValue("transparent");
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (nextType === "noise") {
|
|
861
|
+
emitPaintValue(NOISE_FALLBACK_CSS);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (nextType === "pattern") {
|
|
865
|
+
emitPaintValue(PATTERN_FALLBACK_CSS);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
557
868
|
};
|
|
558
869
|
|
|
559
870
|
const pickScreenColor = async () => {
|
|
@@ -619,11 +930,11 @@ export function FigmaColorPicker({
|
|
|
619
930
|
<ScrubbyNumberInput
|
|
620
931
|
key={ch}
|
|
621
932
|
aria-label={ch.toUpperCase()}
|
|
622
|
-
value={
|
|
933
|
+
value={fieldColor[ch]}
|
|
623
934
|
min={0}
|
|
624
935
|
max={255}
|
|
625
936
|
disabled={disabled}
|
|
626
|
-
onChange={(next) =>
|
|
937
|
+
onChange={(next) => emitFieldColor({ ...fieldColor, [ch]: next })}
|
|
627
938
|
/>
|
|
628
939
|
))}
|
|
629
940
|
</div>
|
|
@@ -634,27 +945,27 @@ export function FigmaColorPicker({
|
|
|
634
945
|
<div className="flex gap-1">
|
|
635
946
|
<ScrubbyNumberInput
|
|
636
947
|
aria-label={copy.hue}
|
|
637
|
-
value={
|
|
948
|
+
value={fieldHsl.h}
|
|
638
949
|
min={0}
|
|
639
950
|
max={360}
|
|
640
951
|
disabled={disabled}
|
|
641
|
-
onChange={(h) =>
|
|
952
|
+
onChange={(h) => emitFieldHsl({ ...fieldHsl, h })}
|
|
642
953
|
/>
|
|
643
954
|
<ScrubbyNumberInput
|
|
644
955
|
aria-label={copy.saturation}
|
|
645
|
-
value={
|
|
956
|
+
value={fieldHsl.s}
|
|
646
957
|
min={0}
|
|
647
958
|
max={100}
|
|
648
959
|
disabled={disabled}
|
|
649
|
-
onChange={(s) =>
|
|
960
|
+
onChange={(s) => emitFieldHsl({ ...fieldHsl, s })}
|
|
650
961
|
/>
|
|
651
962
|
<ScrubbyNumberInput
|
|
652
963
|
aria-label={copy.lightness}
|
|
653
|
-
value={
|
|
964
|
+
value={fieldHsl.l}
|
|
654
965
|
min={0}
|
|
655
966
|
max={100}
|
|
656
967
|
disabled={disabled}
|
|
657
|
-
onChange={(l) =>
|
|
968
|
+
onChange={(l) => emitFieldHsl({ ...fieldHsl, l })}
|
|
658
969
|
/>
|
|
659
970
|
</div>
|
|
660
971
|
);
|
|
@@ -664,27 +975,27 @@ export function FigmaColorPicker({
|
|
|
664
975
|
<div className="flex gap-1">
|
|
665
976
|
<ScrubbyNumberInput
|
|
666
977
|
aria-label={copy.hue}
|
|
667
|
-
value={
|
|
978
|
+
value={fieldHsv.h}
|
|
668
979
|
min={0}
|
|
669
980
|
max={360}
|
|
670
981
|
disabled={disabled}
|
|
671
|
-
onChange={(h) =>
|
|
982
|
+
onChange={(h) => emitFieldHsv({ ...fieldHsv, h })}
|
|
672
983
|
/>
|
|
673
984
|
<ScrubbyNumberInput
|
|
674
985
|
aria-label={copy.saturation}
|
|
675
|
-
value={
|
|
986
|
+
value={fieldHsv.s}
|
|
676
987
|
min={0}
|
|
677
988
|
max={100}
|
|
678
989
|
disabled={disabled}
|
|
679
|
-
onChange={(s) =>
|
|
990
|
+
onChange={(s) => emitFieldHsv({ ...fieldHsv, s })}
|
|
680
991
|
/>
|
|
681
992
|
<ScrubbyNumberInput
|
|
682
993
|
aria-label={copy.brightness}
|
|
683
|
-
value={
|
|
994
|
+
value={fieldHsv.v}
|
|
684
995
|
min={0}
|
|
685
996
|
max={100}
|
|
686
997
|
disabled={disabled}
|
|
687
|
-
onChange={(v) =>
|
|
998
|
+
onChange={(v) => emitFieldHsv({ ...fieldHsv, v })}
|
|
688
999
|
/>
|
|
689
1000
|
</div>
|
|
690
1001
|
);
|
|
@@ -710,10 +1021,10 @@ export function FigmaColorPicker({
|
|
|
710
1021
|
{/* Flat swatch chip — no shadow-inner (Figma is flat) */}
|
|
711
1022
|
<span
|
|
712
1023
|
className="size-4 shrink-0 rounded-[3px] border border-border/60"
|
|
713
|
-
style={
|
|
1024
|
+
style={triggerSwatchStyle(value, color)}
|
|
714
1025
|
/>
|
|
715
1026
|
<span className="min-w-0 flex-1 truncate text-left tabular-nums uppercase text-[11px]">
|
|
716
|
-
{
|
|
1027
|
+
{triggerLabel(effectivePaintType, color)}
|
|
717
1028
|
</span>
|
|
718
1029
|
<span className="tabular-nums text-muted-foreground text-[11px]">
|
|
719
1030
|
{effectiveOpacity}%
|
|
@@ -729,70 +1040,31 @@ export function FigmaColorPicker({
|
|
|
729
1040
|
className="z-[10000] w-[252px] p-0 shadow-xl"
|
|
730
1041
|
>
|
|
731
1042
|
<div className="overflow-hidden rounded-md bg-popover text-popover-foreground">
|
|
732
|
-
{
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
{
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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" ? (
|
|
1043
|
+
{view === "shader" ? (
|
|
1044
|
+
<ShaderFillsPanel
|
|
1045
|
+
descriptor={shaderDescriptor ?? undefined}
|
|
1046
|
+
applyContext={shaderContext}
|
|
1047
|
+
disabled={disabled}
|
|
1048
|
+
onApply={emitShader}
|
|
1049
|
+
onBack={() => {
|
|
1050
|
+
setView("picker");
|
|
1051
|
+
if (effectivePaintType === "shader") {
|
|
1052
|
+
// No shader chosen — revert to a solid so the row isn't dead.
|
|
1053
|
+
if (!shaderDescriptor) setPaintType("solid");
|
|
1054
|
+
}
|
|
1055
|
+
}}
|
|
1056
|
+
/>
|
|
1057
|
+
) : (
|
|
775
1058
|
<>
|
|
776
|
-
{/* ── Paint-type icon row
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1059
|
+
{/* ── Paint-type icon row (Figma-style, full-width tabs) ─── */}
|
|
1060
|
+
{/* 11 types → two rows: first 6, then 5. Each icon is a
|
|
1061
|
+
clearly-hittable 36×32px target with a distinct active
|
|
1062
|
+
accent so the selected mode is immediately obvious. */}
|
|
1063
|
+
<div className="border-b border-border/70 px-2 pt-2 pb-1.5">
|
|
1064
|
+
{/* Row 1: Solid · Linear · Radial · Angular · Diamond · Image */}
|
|
1065
|
+
<div className="mb-1 grid grid-cols-6 gap-1">
|
|
1066
|
+
{PAINT_TYPES.slice(0, 6).map(({ type, label, Icon }) => {
|
|
780
1067
|
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
1068
|
return (
|
|
797
1069
|
<Tooltip key={type}>
|
|
798
1070
|
<TooltipTrigger asChild>
|
|
@@ -800,227 +1072,358 @@ export function FigmaColorPicker({
|
|
|
800
1072
|
type="button"
|
|
801
1073
|
aria-label={label}
|
|
802
1074
|
aria-pressed={isActive}
|
|
803
|
-
disabled={
|
|
1075
|
+
disabled={disabled}
|
|
804
1076
|
onClick={() => setPaintType(type)}
|
|
805
1077
|
className={cn(
|
|
806
|
-
"flex h-
|
|
1078
|
+
"flex h-8 w-full cursor-pointer flex-col items-center justify-center gap-0.5 rounded transition-colors",
|
|
807
1079
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
808
1080
|
"active:scale-95",
|
|
809
1081
|
isActive
|
|
810
|
-
? "bg-
|
|
1082
|
+
? "bg-accent text-accent-foreground ring-1 ring-primary/60"
|
|
811
1083
|
: "text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground",
|
|
812
|
-
|
|
1084
|
+
disabled && "pointer-events-none opacity-40",
|
|
813
1085
|
)}
|
|
814
1086
|
>
|
|
815
|
-
<Icon className="size-
|
|
1087
|
+
<Icon className="size-4" />
|
|
816
1088
|
</button>
|
|
817
1089
|
</TooltipTrigger>
|
|
818
|
-
<TooltipContent>
|
|
1090
|
+
<TooltipContent side="bottom" className="text-[10px]">
|
|
1091
|
+
{label}
|
|
1092
|
+
</TooltipContent>
|
|
819
1093
|
</Tooltip>
|
|
820
1094
|
);
|
|
821
1095
|
})}
|
|
822
1096
|
</div>
|
|
1097
|
+
{/* Row 2: Video · Shader · Noise · Pattern · None */}
|
|
1098
|
+
<div className="grid grid-cols-5 gap-1">
|
|
1099
|
+
{PAINT_TYPES.slice(6).map(({ type, label, Icon }) => {
|
|
1100
|
+
const isActive = effectivePaintType === type;
|
|
1101
|
+
return (
|
|
1102
|
+
<Tooltip key={type}>
|
|
1103
|
+
<TooltipTrigger asChild>
|
|
1104
|
+
<button
|
|
1105
|
+
type="button"
|
|
1106
|
+
aria-label={label}
|
|
1107
|
+
aria-pressed={isActive}
|
|
1108
|
+
disabled={disabled}
|
|
1109
|
+
onClick={() => setPaintType(type)}
|
|
1110
|
+
className={cn(
|
|
1111
|
+
"flex h-8 w-full cursor-pointer flex-col items-center justify-center gap-0.5 rounded transition-colors",
|
|
1112
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
1113
|
+
"active:scale-95",
|
|
1114
|
+
isActive
|
|
1115
|
+
? "bg-accent text-accent-foreground ring-1 ring-primary/60"
|
|
1116
|
+
: "text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground",
|
|
1117
|
+
disabled && "pointer-events-none opacity-40",
|
|
1118
|
+
)}
|
|
1119
|
+
>
|
|
1120
|
+
<Icon className="size-4" />
|
|
1121
|
+
</button>
|
|
1122
|
+
</TooltipTrigger>
|
|
1123
|
+
<TooltipContent side="bottom" className="text-[10px]">
|
|
1124
|
+
{label}
|
|
1125
|
+
</TooltipContent>
|
|
1126
|
+
</Tooltip>
|
|
1127
|
+
);
|
|
1128
|
+
})}
|
|
1129
|
+
</div>
|
|
1130
|
+
{/* Active-type label — shows which mode is selected */}
|
|
1131
|
+
<p className="mt-1 text-center text-[10px] font-medium text-muted-foreground">
|
|
1132
|
+
{PAINT_TYPES.find((p) => p.type === effectivePaintType)
|
|
1133
|
+
?.label ?? effectivePaintType}
|
|
1134
|
+
</p>
|
|
823
1135
|
</div>
|
|
824
1136
|
|
|
825
|
-
{/* ──
|
|
826
|
-
|
|
827
|
-
<
|
|
828
|
-
|
|
829
|
-
|
|
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}
|
|
1137
|
+
{/* ── Image fill controls ─────────────────────────────────── */}
|
|
1138
|
+
{effectivePaintType === "image" && (
|
|
1139
|
+
<div>
|
|
1140
|
+
<ImageFillControls
|
|
1141
|
+
value={imageFill}
|
|
879
1142
|
disabled={disabled}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1143
|
+
onChange={emitImageFill}
|
|
1144
|
+
/>
|
|
1145
|
+
</div>
|
|
1146
|
+
)}
|
|
1147
|
+
|
|
1148
|
+
{/* ── Video fill: source field ────────────────────────────── */}
|
|
1149
|
+
{effectivePaintType === "video" && (
|
|
1150
|
+
<div className="px-3 py-2">
|
|
1151
|
+
<p className="mb-1.5 text-[10px] text-muted-foreground">
|
|
1152
|
+
{
|
|
1153
|
+
"Paste a video URL to use as the fill." /* i18n-ignore */
|
|
883
1154
|
}
|
|
1155
|
+
</p>
|
|
1156
|
+
<Input
|
|
1157
|
+
defaultValue=""
|
|
1158
|
+
disabled={disabled}
|
|
1159
|
+
placeholder={"Video URL (mp4, webm)" /* i18n-ignore */}
|
|
1160
|
+
aria-label={"Video URL" /* i18n-ignore */}
|
|
1161
|
+
spellCheck={false}
|
|
1162
|
+
className="h-6 w-full rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-[11px]"
|
|
1163
|
+
onKeyDown={(e) => {
|
|
1164
|
+
if (e.key === "Enter") {
|
|
1165
|
+
e.preventDefault();
|
|
1166
|
+
const url = e.currentTarget.value.trim();
|
|
1167
|
+
if (url)
|
|
1168
|
+
emitPaintValue(
|
|
1169
|
+
`url("${url}") center / cover no-repeat`,
|
|
1170
|
+
);
|
|
1171
|
+
e.currentTarget.blur();
|
|
1172
|
+
}
|
|
1173
|
+
}}
|
|
1174
|
+
onBlur={(e) => {
|
|
1175
|
+
const url = e.currentTarget.value.trim();
|
|
1176
|
+
if (url)
|
|
1177
|
+
emitPaintValue(
|
|
1178
|
+
`url("${url}") center / cover no-repeat`,
|
|
1179
|
+
);
|
|
1180
|
+
}}
|
|
1181
|
+
/>
|
|
1182
|
+
</div>
|
|
1183
|
+
)}
|
|
1184
|
+
|
|
1185
|
+
{/* ── Gradient editor (linear / radial / angular / diamond) ── */}
|
|
1186
|
+
{activeGradient && (
|
|
1187
|
+
<div>
|
|
1188
|
+
<GradientEditor
|
|
1189
|
+
value={activeGradient}
|
|
1190
|
+
selectedStopId={selectedStopId}
|
|
1191
|
+
disabled={disabled}
|
|
1192
|
+
onSelectStop={setSelectedStopId}
|
|
1193
|
+
onChange={emitGradient}
|
|
884
1194
|
/>
|
|
1195
|
+
</div>
|
|
1196
|
+
)}
|
|
1197
|
+
|
|
1198
|
+
{/* ── 2D Saturation/Brightness field ──────────────────────── */}
|
|
1199
|
+
{/* Hidden for non-color fills (image/video/noise/pattern). */}
|
|
1200
|
+
{(effectivePaintType === "solid" ||
|
|
1201
|
+
effectivePaintType === "none" ||
|
|
1202
|
+
activeGradient) && (
|
|
1203
|
+
<div className="border-t border-border/70">
|
|
1204
|
+
<SaturationBrightnessField
|
|
1205
|
+
hsv={fieldHsv}
|
|
1206
|
+
label={copy.saturationBrightness}
|
|
1207
|
+
disabled={disabled}
|
|
1208
|
+
onChange={(nextHsv) => {
|
|
1209
|
+
if (activeGradient) {
|
|
1210
|
+
emitStopColor(
|
|
1211
|
+
hsvToRgba({
|
|
1212
|
+
...nextHsv,
|
|
1213
|
+
a: opacityToAlpha(effectiveOpacity),
|
|
1214
|
+
}),
|
|
1215
|
+
);
|
|
1216
|
+
} else {
|
|
1217
|
+
emitColorFromHsv(nextHsv);
|
|
1218
|
+
}
|
|
1219
|
+
}}
|
|
1220
|
+
/>
|
|
1221
|
+
</div>
|
|
1222
|
+
)}
|
|
1223
|
+
|
|
1224
|
+
{/* ── Eyedropper + Hue slider / Swatch + Alpha slider ─────── */}
|
|
1225
|
+
{/* Color sliders only apply to color-based fills. */}
|
|
1226
|
+
{(effectivePaintType === "solid" ||
|
|
1227
|
+
effectivePaintType === "none" ||
|
|
1228
|
+
activeGradient) && (
|
|
1229
|
+
<div className="mt-2.5 px-3">
|
|
1230
|
+
<div className="grid grid-cols-[1.5rem_1fr] items-center gap-x-2">
|
|
1231
|
+
{/* Eyedropper centered across the two slider rows via row-span-2 */}
|
|
1232
|
+
<div className="row-span-2 flex items-center justify-center">
|
|
1233
|
+
<Tooltip>
|
|
1234
|
+
<TooltipTrigger asChild>
|
|
1235
|
+
<button
|
|
1236
|
+
type="button"
|
|
1237
|
+
aria-label={
|
|
1238
|
+
"Pick color" /* i18n-ignore browser eyedropper label */
|
|
1239
|
+
}
|
|
1240
|
+
disabled={disabled || !hasEyeDropper}
|
|
1241
|
+
onClick={() => void pickScreenColor()}
|
|
1242
|
+
className={cn(
|
|
1243
|
+
"flex size-6 cursor-pointer items-center justify-center rounded-sm transition-colors",
|
|
1244
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
1245
|
+
picking
|
|
1246
|
+
? "bg-primary/10 text-primary ring-1 ring-primary/50"
|
|
1247
|
+
: "text-muted-foreground hover:bg-[var(--design-editor-control-bg)] hover:text-foreground",
|
|
1248
|
+
(disabled || !hasEyeDropper) &&
|
|
1249
|
+
"pointer-events-none opacity-40",
|
|
1250
|
+
)}
|
|
1251
|
+
>
|
|
1252
|
+
<IconColorPicker className="size-4" />
|
|
1253
|
+
</button>
|
|
1254
|
+
</TooltipTrigger>
|
|
1255
|
+
<TooltipContent>
|
|
1256
|
+
{
|
|
1257
|
+
hasEyeDropper
|
|
1258
|
+
? "Pick color" // i18n-ignore browser eyedropper label
|
|
1259
|
+
: "Not supported in this browser" // i18n-ignore browser eyedropper disabled label
|
|
1260
|
+
}
|
|
1261
|
+
</TooltipContent>
|
|
1262
|
+
</Tooltip>
|
|
1263
|
+
</div>
|
|
885
1264
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1265
|
+
{/* Hue track */}
|
|
1266
|
+
<ColorTrack
|
|
1267
|
+
label={copy.hue}
|
|
1268
|
+
value={fieldHsv.h}
|
|
1269
|
+
min={0}
|
|
1270
|
+
max={360}
|
|
1271
|
+
disabled={disabled}
|
|
1272
|
+
backgroundImage="linear-gradient(90deg, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)"
|
|
1273
|
+
onChange={(next) => {
|
|
1274
|
+
const h = next === 360 ? 0 : next;
|
|
1275
|
+
if (activeGradient) {
|
|
1276
|
+
emitStopColor(
|
|
1277
|
+
hsvToRgba({
|
|
1278
|
+
...fieldHsv,
|
|
1279
|
+
h,
|
|
1280
|
+
a: fieldColor.a,
|
|
1281
|
+
}),
|
|
1282
|
+
);
|
|
1283
|
+
} else {
|
|
1284
|
+
emitColorFromHsv({ ...hsv, h });
|
|
1285
|
+
}
|
|
1286
|
+
}}
|
|
891
1287
|
/>
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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}
|
|
1288
|
+
|
|
1289
|
+
{/* Current-color swatch left of alpha (matches Figma's layout) */}
|
|
1290
|
+
<div className="flex items-center gap-2">
|
|
1291
|
+
<span
|
|
1292
|
+
className="size-[18px] shrink-0 rounded-[3px] border border-border/60"
|
|
1293
|
+
style={swatchStyle(rgbaToCss(fieldColor))}
|
|
904
1294
|
/>
|
|
1295
|
+
{/* Alpha track fills remaining width */}
|
|
1296
|
+
<div className="flex-1">
|
|
1297
|
+
<ColorTrack
|
|
1298
|
+
label={copy.opacity}
|
|
1299
|
+
value={
|
|
1300
|
+
activeGradient
|
|
1301
|
+
? alphaToOpacity(fieldColor.a)
|
|
1302
|
+
: effectiveOpacity
|
|
1303
|
+
}
|
|
1304
|
+
min={0}
|
|
1305
|
+
max={100}
|
|
1306
|
+
disabled={disabled}
|
|
1307
|
+
backgroundImage={alphaTrackBackground(fieldColor)}
|
|
1308
|
+
backgroundSize="8px 8px, 8px 8px, 8px 8px, 8px 8px, 100% 100%"
|
|
1309
|
+
backgroundPosition="0 0, 0 4px, 4px -4px, -4px 0, 0 0"
|
|
1310
|
+
onChange={(next) => {
|
|
1311
|
+
if (activeGradient) {
|
|
1312
|
+
emitStopColor({
|
|
1313
|
+
...fieldColor,
|
|
1314
|
+
a: opacityToAlpha(next),
|
|
1315
|
+
});
|
|
1316
|
+
} else {
|
|
1317
|
+
setOpacity(next);
|
|
1318
|
+
}
|
|
1319
|
+
}}
|
|
1320
|
+
/>
|
|
1321
|
+
</div>
|
|
905
1322
|
</div>
|
|
906
1323
|
</div>
|
|
907
1324
|
</div>
|
|
908
|
-
|
|
1325
|
+
)}
|
|
909
1326
|
|
|
910
1327
|
{/* ── Value row: [Hex ▾] [value input(s)] [opacity %] ─────── */}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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}
|
|
1328
|
+
{(effectivePaintType === "solid" ||
|
|
1329
|
+
effectivePaintType === "none" ||
|
|
1330
|
+
activeGradient) && (
|
|
1331
|
+
<div className="mt-2.5 px-3 pb-3">
|
|
1332
|
+
<div className="grid grid-cols-[4.5rem_1fr_3rem] items-center gap-1">
|
|
1333
|
+
{/* Model pill — bare text+chevron, no border or bg box (Figma-style) */}
|
|
1334
|
+
<ColorModelPill
|
|
1335
|
+
value={mode}
|
|
952
1336
|
disabled={disabled}
|
|
953
|
-
onChange={
|
|
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
|
|
1337
|
+
onChange={(v) => setMode(v as FigmaColorMode)}
|
|
956
1338
|
/>
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1339
|
+
|
|
1340
|
+
{/* Value field(s) — adapts to mode */}
|
|
1341
|
+
{renderValueInputs()}
|
|
1342
|
+
|
|
1343
|
+
{/* Opacity % field */}
|
|
1344
|
+
<div className="flex h-6 overflow-hidden rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)]">
|
|
1345
|
+
<ScrubbyNumberInput
|
|
1346
|
+
aria-label={copy.opacity}
|
|
1347
|
+
value={
|
|
1348
|
+
activeGradient
|
|
1349
|
+
? alphaToOpacity(fieldColor.a)
|
|
1350
|
+
: effectiveOpacity
|
|
1351
|
+
}
|
|
1352
|
+
min={0}
|
|
1353
|
+
max={100}
|
|
1354
|
+
disabled={disabled}
|
|
1355
|
+
onChange={(next) => {
|
|
1356
|
+
if (activeGradient) {
|
|
1357
|
+
emitStopColor({
|
|
1358
|
+
...fieldColor,
|
|
1359
|
+
a: opacityToAlpha(next),
|
|
1360
|
+
});
|
|
1361
|
+
} else {
|
|
1362
|
+
setOpacity(next);
|
|
1363
|
+
}
|
|
1364
|
+
}}
|
|
1365
|
+
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"
|
|
1366
|
+
compact
|
|
1367
|
+
/>
|
|
1368
|
+
<span className="flex w-4 shrink-0 items-center justify-center border-l border-border/60 text-[10px] text-muted-foreground">
|
|
1369
|
+
%
|
|
1370
|
+
</span>
|
|
1371
|
+
</div>
|
|
960
1372
|
</div>
|
|
961
1373
|
</div>
|
|
962
|
-
|
|
1374
|
+
)}
|
|
963
1375
|
|
|
964
|
-
{/* ──
|
|
1376
|
+
{/* ── Document colors ──────────────────────────────────────── */}
|
|
1377
|
+
{/* Renders the palette of colors already used in the design.
|
|
1378
|
+
When `documentColors` is provided, those swatches are shown;
|
|
1379
|
+
otherwise falls back to the single current color so the
|
|
1380
|
+
section is never empty. */}
|
|
965
1381
|
<div className="border-t border-border/70 px-3 py-2.5">
|
|
966
|
-
{/* Source
|
|
967
|
-
<
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
<IconChevronDown className="size-3 text-muted-foreground" />
|
|
973
|
-
</button>
|
|
974
|
-
|
|
975
|
-
{/* Swatch grid: flat chips (no shadow-inner), 8 columns */}
|
|
1382
|
+
{/* Source label — matches Figma layout */}
|
|
1383
|
+
<div className="mb-2 flex h-6 w-full items-center justify-between px-0.5 text-[11px] text-muted-foreground">
|
|
1384
|
+
{"Document colors" /* i18n-ignore Figma picker source */}
|
|
1385
|
+
</div>
|
|
1386
|
+
|
|
1387
|
+
{/* Swatch grid: document palette when available, else current color */}
|
|
976
1388
|
<div className="grid grid-cols-8 gap-1">
|
|
977
|
-
{
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1389
|
+
{(documentColors && documentColors.length > 0
|
|
1390
|
+
? documentColors
|
|
1391
|
+
: [rgbaToCss(color)]
|
|
1392
|
+
).map((docColor) => {
|
|
1393
|
+
const currentHex = rgbaToHex(
|
|
1394
|
+
parseCssColor(docColor) ?? color,
|
|
1395
|
+
);
|
|
1396
|
+
const isActive =
|
|
1397
|
+
rgbaToHex(color) === currentHex && !activeGradient;
|
|
981
1398
|
return (
|
|
982
|
-
<Tooltip key={
|
|
1399
|
+
<Tooltip key={docColor}>
|
|
983
1400
|
<TooltipTrigger asChild>
|
|
984
1401
|
<button
|
|
985
1402
|
type="button"
|
|
986
1403
|
disabled={disabled}
|
|
987
|
-
aria-label={
|
|
988
|
-
aria-pressed={
|
|
1404
|
+
aria-label={currentHex}
|
|
1405
|
+
aria-pressed={isActive}
|
|
989
1406
|
className={cn(
|
|
990
1407
|
"size-5 rounded-sm border transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
991
|
-
|
|
1408
|
+
isActive
|
|
992
1409
|
? "border-primary ring-1 ring-primary"
|
|
993
1410
|
: "border-border/60",
|
|
994
1411
|
)}
|
|
995
|
-
style={swatchStyle(
|
|
996
|
-
onClick={() =>
|
|
997
|
-
|
|
998
|
-
|
|
1412
|
+
style={swatchStyle(docColor)}
|
|
1413
|
+
onClick={() => {
|
|
1414
|
+
const parsed = parseCssColor(docColor) ?? color;
|
|
1415
|
+
if (activeGradient) emitStopColor(parsed);
|
|
1416
|
+
else emitColor(parsed);
|
|
1417
|
+
}}
|
|
999
1418
|
/>
|
|
1000
1419
|
</TooltipTrigger>
|
|
1001
|
-
<TooltipContent>{
|
|
1420
|
+
<TooltipContent>{currentHex}</TooltipContent>
|
|
1002
1421
|
</Tooltip>
|
|
1003
1422
|
);
|
|
1004
1423
|
})}
|
|
1005
1424
|
</div>
|
|
1006
1425
|
</div>
|
|
1007
1426
|
</>
|
|
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
1427
|
)}
|
|
1025
1428
|
</div>
|
|
1026
1429
|
</PopoverContent>
|
|
@@ -1031,6 +1434,104 @@ export function FigmaColorPicker({
|
|
|
1031
1434
|
|
|
1032
1435
|
// ─── Sub-components ────────────────────────────────────────────────────────────
|
|
1033
1436
|
|
|
1437
|
+
const COLOR_MODES: Array<{ value: FigmaColorMode; label: string }> = [
|
|
1438
|
+
{ value: "hex", label: "Hex" }, // i18n-ignore color mode
|
|
1439
|
+
{ value: "rgb", label: "RGB" }, // i18n-ignore color mode
|
|
1440
|
+
{ value: "hsl", label: "HSL" }, // i18n-ignore color mode
|
|
1441
|
+
{ value: "hsb", label: "HSB" }, // i18n-ignore color mode
|
|
1442
|
+
];
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Figma-style bare color-model selector: text + small chevron, no border/bg box.
|
|
1446
|
+
* Hover reveals a subtle bg tint; active mode is font-semibold.
|
|
1447
|
+
* Renders its own lightweight dropdown (no Radix Select overhead).
|
|
1448
|
+
*/
|
|
1449
|
+
function ColorModelPill({
|
|
1450
|
+
value,
|
|
1451
|
+
disabled,
|
|
1452
|
+
onChange,
|
|
1453
|
+
}: {
|
|
1454
|
+
value: FigmaColorMode;
|
|
1455
|
+
disabled: boolean;
|
|
1456
|
+
onChange: (mode: FigmaColorMode) => void;
|
|
1457
|
+
}) {
|
|
1458
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
1459
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
1460
|
+
const label =
|
|
1461
|
+
COLOR_MODES.find((m) => m.value === value)?.label ?? value.toUpperCase();
|
|
1462
|
+
|
|
1463
|
+
// Close on outside click.
|
|
1464
|
+
useEffect(() => {
|
|
1465
|
+
if (!menuOpen) return;
|
|
1466
|
+
const handler = (e: MouseEvent) => {
|
|
1467
|
+
if (!containerRef.current?.contains(e.target as Node)) {
|
|
1468
|
+
setMenuOpen(false);
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
document.addEventListener("mousedown", handler);
|
|
1472
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
1473
|
+
}, [menuOpen]);
|
|
1474
|
+
|
|
1475
|
+
return (
|
|
1476
|
+
<div ref={containerRef} className="relative">
|
|
1477
|
+
<button
|
|
1478
|
+
type="button"
|
|
1479
|
+
disabled={disabled}
|
|
1480
|
+
aria-haspopup="listbox"
|
|
1481
|
+
aria-expanded={menuOpen}
|
|
1482
|
+
onClick={() => setMenuOpen((o) => !o)}
|
|
1483
|
+
className={cn(
|
|
1484
|
+
"flex h-6 w-[4.5rem] items-center gap-0.5 rounded px-1.5",
|
|
1485
|
+
"text-[11px] font-semibold text-foreground",
|
|
1486
|
+
"bg-transparent border-0 shadow-none",
|
|
1487
|
+
"hover:bg-[var(--design-editor-control-bg)]",
|
|
1488
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
1489
|
+
"transition-colors",
|
|
1490
|
+
disabled && "pointer-events-none opacity-50",
|
|
1491
|
+
)}
|
|
1492
|
+
>
|
|
1493
|
+
<span className="flex-1 text-left">{label}</span>
|
|
1494
|
+
<IconChevronDown className="size-3 shrink-0 text-muted-foreground" />
|
|
1495
|
+
</button>
|
|
1496
|
+
|
|
1497
|
+
{menuOpen && (
|
|
1498
|
+
<div
|
|
1499
|
+
role="listbox"
|
|
1500
|
+
aria-label="Color model" // i18n-ignore aria label
|
|
1501
|
+
className={cn(
|
|
1502
|
+
"absolute left-0 top-full z-[10001] mt-0.5 min-w-[4.5rem]",
|
|
1503
|
+
"rounded-md border border-border bg-popover shadow-lg",
|
|
1504
|
+
"overflow-hidden py-0.5",
|
|
1505
|
+
)}
|
|
1506
|
+
>
|
|
1507
|
+
{COLOR_MODES.map((m) => (
|
|
1508
|
+
<button
|
|
1509
|
+
key={m.value}
|
|
1510
|
+
type="button"
|
|
1511
|
+
role="option"
|
|
1512
|
+
aria-selected={m.value === value}
|
|
1513
|
+
onClick={() => {
|
|
1514
|
+
onChange(m.value);
|
|
1515
|
+
setMenuOpen(false);
|
|
1516
|
+
}}
|
|
1517
|
+
className={cn(
|
|
1518
|
+
"flex w-full items-center px-2 py-1 text-[11px]",
|
|
1519
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
1520
|
+
"focus-visible:outline-none focus-visible:bg-accent",
|
|
1521
|
+
m.value === value
|
|
1522
|
+
? "font-semibold text-foreground"
|
|
1523
|
+
: "font-normal text-foreground/80",
|
|
1524
|
+
)}
|
|
1525
|
+
>
|
|
1526
|
+
{m.label}
|
|
1527
|
+
</button>
|
|
1528
|
+
))}
|
|
1529
|
+
</div>
|
|
1530
|
+
)}
|
|
1531
|
+
</div>
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1034
1535
|
function SaturationBrightnessField({
|
|
1035
1536
|
hsv,
|
|
1036
1537
|
label,
|
|
@@ -1319,7 +1820,14 @@ function ScrubbyNumberInput({
|
|
|
1319
1820
|
|
|
1320
1821
|
// ─── Utilities ─────────────────────────────────────────────────────────────────
|
|
1321
1822
|
|
|
1322
|
-
function inferPaintType(value: string, opacity: number): FigmaPaintType {
|
|
1823
|
+
export function inferPaintType(value: string, opacity: number): FigmaPaintType {
|
|
1824
|
+
const lower = value.trim().toLowerCase();
|
|
1825
|
+
if (lower.includes("gradient(")) {
|
|
1826
|
+
if (lower.startsWith("radial-gradient")) return "radial";
|
|
1827
|
+
if (lower.startsWith("conic-gradient")) return "angular";
|
|
1828
|
+
return "linear";
|
|
1829
|
+
}
|
|
1830
|
+
if (lower.startsWith("url(")) return "image";
|
|
1323
1831
|
const parsed = parseCssColor(value);
|
|
1324
1832
|
if (opacity <= 0 || parsed?.a === 0 || value.trim() === "transparent") {
|
|
1325
1833
|
return "none";
|
|
@@ -1327,11 +1835,93 @@ function inferPaintType(value: string, opacity: number): FigmaPaintType {
|
|
|
1327
1835
|
return "solid";
|
|
1328
1836
|
}
|
|
1329
1837
|
|
|
1838
|
+
/** The set of paint types that render a gradient editor. */
|
|
1839
|
+
export const GRADIENT_PAINT_TYPES: ReadonlySet<FigmaPaintType> = new Set([
|
|
1840
|
+
"linear",
|
|
1841
|
+
"radial",
|
|
1842
|
+
"angular",
|
|
1843
|
+
"diamond",
|
|
1844
|
+
]);
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* Pure helper: resolves the effective paint type and which editor panel should
|
|
1848
|
+
* be visible given the three-level precedence:
|
|
1849
|
+
*
|
|
1850
|
+
* localPaintType (user's explicit click this session)
|
|
1851
|
+
* ?? paintType prop (EditPanel-driven structural type)
|
|
1852
|
+
* ?? inferred from the CSS value string
|
|
1853
|
+
*
|
|
1854
|
+
* The shader panel is a view-level switch (not just a paint-type), so
|
|
1855
|
+
* `showShaderPanel` is derived directly from the effective type.
|
|
1856
|
+
*
|
|
1857
|
+
* @param paintType The `paintType` prop from the parent (or undefined).
|
|
1858
|
+
* @param localPaintType The user's explicit in-session selection (or null).
|
|
1859
|
+
* @param value The current CSS fill value string.
|
|
1860
|
+
* @param opacity The current opacity (0–100).
|
|
1861
|
+
*/
|
|
1862
|
+
export function resolveActivePaint(
|
|
1863
|
+
paintType: FigmaPaintType | undefined,
|
|
1864
|
+
localPaintType: FigmaPaintType | null,
|
|
1865
|
+
value: string,
|
|
1866
|
+
opacity: number,
|
|
1867
|
+
): {
|
|
1868
|
+
effectivePaintType: FigmaPaintType;
|
|
1869
|
+
showGradientEditor: boolean;
|
|
1870
|
+
showImageControls: boolean;
|
|
1871
|
+
showShaderPanel: boolean;
|
|
1872
|
+
} {
|
|
1873
|
+
const effectivePaintType: FigmaPaintType =
|
|
1874
|
+
localPaintType ?? paintType ?? inferPaintType(value, opacity);
|
|
1875
|
+
return {
|
|
1876
|
+
effectivePaintType,
|
|
1877
|
+
showGradientEditor: GRADIENT_PAINT_TYPES.has(effectivePaintType),
|
|
1878
|
+
showImageControls: effectivePaintType === "image",
|
|
1879
|
+
showShaderPanel: effectivePaintType === "shader",
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
function toCssColor(color: RgbaColor): string {
|
|
1884
|
+
return rgbaToCss(color);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1330
1887
|
/** Show hex without the leading # for the display field (matches Figma). */
|
|
1331
1888
|
function toDisplayHex(color: RgbaColor): string {
|
|
1332
1889
|
return rgbaToHex(color).replace(/^#/, "").toUpperCase();
|
|
1333
1890
|
}
|
|
1334
1891
|
|
|
1892
|
+
function triggerLabel(type: FigmaPaintType, color: RgbaColor): string {
|
|
1893
|
+
if (type === "solid") return toDisplayHex(color);
|
|
1894
|
+
if (type === "none") return "None";
|
|
1895
|
+
if (type === "image") return "Image";
|
|
1896
|
+
if (type === "video") return "Video";
|
|
1897
|
+
if (type === "shader") return "Shader";
|
|
1898
|
+
if (type === "noise") return "Noise";
|
|
1899
|
+
if (type === "pattern") return "Pattern";
|
|
1900
|
+
return `${type[0].toUpperCase()}${type.slice(1)} gradient`;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function triggerSwatchStyle(
|
|
1904
|
+
value: string,
|
|
1905
|
+
color: RgbaColor,
|
|
1906
|
+
): {
|
|
1907
|
+
backgroundColor?: string;
|
|
1908
|
+
backgroundImage?: string;
|
|
1909
|
+
backgroundSize?: string;
|
|
1910
|
+
backgroundPosition?: string;
|
|
1911
|
+
} {
|
|
1912
|
+
const lower = value.trim().toLowerCase();
|
|
1913
|
+
if (!lower || lower === "transparent") {
|
|
1914
|
+
return {
|
|
1915
|
+
backgroundImage: CHECKERBOARD_IMAGE,
|
|
1916
|
+
backgroundSize: "8px 8px",
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
if (lower.includes("gradient(") || lower.startsWith("url(")) {
|
|
1920
|
+
return swatchStyle(value);
|
|
1921
|
+
}
|
|
1922
|
+
return swatchStyle(rgbaToCss(color));
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1335
1925
|
function swatchStyle(value: string): {
|
|
1336
1926
|
backgroundColor?: string;
|
|
1337
1927
|
backgroundImage?: string;
|