@varialkit/colorpicker 0.1.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/docs.md ADDED
@@ -0,0 +1,124 @@
1
+ # ColorPicker
2
+
3
+ The ColorPicker combines user-defined presets, a format-aware value editor, and optional advanced controls for hue,
4
+ opacity, and freeform spectrum selection. The content (`ColorPickerContent`) can be used inline or embedded inside
5
+ another surface, while the wrapper (`ColorPicker`) handles the menu trigger. The palette (`ColorPickerPalette`) and
6
+ swatch (`ColorPickerSwatch`) are independent so the preset grid can be embedded in menus, popovers, or custom
7
+ surfaces.
8
+
9
+ ## Exports
10
+
11
+ - `ColorPicker`
12
+ - `ColorPickerContent`
13
+ - `ColorPickerPalette`
14
+ - `ColorPickerSwatch`
15
+ - `ColorPreview`
16
+
17
+ ## How It Works
18
+
19
+ The ColorPicker package is intentionally split into small building blocks so you can assemble a color selection
20
+ experience that fits the surface you are working with (menu, popover, inline panel, etc.).
21
+
22
+ **Composition flow**
23
+
24
+ 1. `ColorPicker` is the wrapper that wires a trigger to a dropdown `Menu`.
25
+ 2. Inside the menu, it renders `ColorPickerContent` to handle advanced controls + presets + value input.
26
+ 3. `ColorPickerContent` renders the optional spectrum / sliders / eyedropper row, the optional value editor row, and
27
+ a `ColorPickerPalette` for presets.
28
+ 4. `ColorPickerPalette` renders a grid of `ColorPickerSwatch` instances.
29
+ 5. `ColorPreview` is a separate, reusable indicator for the currently selected color and can be used as a trigger.
30
+
31
+ **Data flow**
32
+
33
+ - `value` can be `hex`, `rgba`, or `hsla`; the picker normalizes it into an internal HSVA model so presets, sliders,
34
+ opacity, and typed values stay in sync.
35
+ - `onChange` fires when a preset, spectrum, slider, eyedropper, or valid typed value updates the selection.
36
+ - `onSelectColor` still fires for preset selections only (useful when you want to differentiate menu-click events).
37
+ - `presets` is the preferred way to provide user-controlled menu colors. `colors` remains supported as a legacy alias.
38
+ - `presets` / `colors` can be an array of strings or structured palette entries. Structured entries allow per-swatch
39
+ labels, overlay content, and disabled state.
40
+ - `valueFormat` controls what `onChange` emits (`hex`, `rgba`, or `hsla`).
41
+ - `defaultInputFormat`, `inputFormats`, and `showFormatSelector` control the value editor row.
42
+ - `menuWidth` and `menuHeight` let the wrapper size the dropdown for larger editing surfaces.
43
+ - `swatchRadius` and `swatchContent` let you tune the swatch shape and overlay without replacing the palette
44
+ component.
45
+
46
+ **Menu behavior**
47
+
48
+ - `ColorPicker` uses `MenuDropdown` and disables auto-close so the content can handle selection.
49
+ - `closeOnSelect` enables closing the dropdown after a swatch is clicked while keeping hex input changes inline.
50
+
51
+ ## Subcomponents
52
+
53
+ **`ColorPicker`**
54
+
55
+ Use this when you want a trigger + dropdown. It provides the default `ColorPreview` trigger and passes through
56
+ menu placement props. It also owns the open state (controlled or uncontrolled).
57
+
58
+ **`ColorPickerContent`**
59
+
60
+ Use this when you want to render the picker inline inside another surface (menu, popover, panel). It handles
61
+ value normalization, preset selection, optional advanced controls, and optional typed value editing.
62
+
63
+ **`ColorPickerPalette`**
64
+
65
+ Renders the grid of swatches. It is fully independent and can be used directly. The `colors` prop supports:
66
+
67
+ - Hex strings: `["#ef4444", "#22c55e"]`
68
+ - Objects: `{ value, label, content, disabled }`
69
+
70
+ When you need overlay content or a different radius for all swatches, prefer `swatchContent` and `swatchRadius`
71
+ instead of re-implementing the grid.
72
+
73
+ ## Advanced Controls
74
+
75
+ `ColorPickerContent` stays palette-first by default, but you can opt into richer menu controls:
76
+
77
+ - `showSpectrum`: adds the full-width saturation / brightness field.
78
+ - `showHueSlider`: adds the horizontal hue slider.
79
+ - `showOpacitySlider`: adds the horizontal opacity slider.
80
+ - `showEyedropper`: adds the native `EyeDropper` button when supported by the browser.
81
+ - `showValueInput`: shows the typed value editor row. `showHexInput` still works as a legacy alias.
82
+ - `showFormatSelector`: adds the value-type dropdown (`hex`, `rgba`, `hsla` by default).
83
+ - `showOpacityInput`: adds the alpha / opacity text field on the right side of the value row.
84
+
85
+ These controls render above the presets so you can progressively enhance the menu without replacing the existing swatch
86
+ grid behavior.
87
+
88
+ When the preset list is long, the preset section becomes scrollable so the advanced controls and typed input stay
89
+ visible.
90
+
91
+ ## Presets
92
+
93
+ Treat the swatch grid as a preset section controlled by the component consumer:
94
+
95
+ ```tsx
96
+ <ColorPicker
97
+ value={color}
98
+ onChange={setColor}
99
+ presets={[
100
+ { value: "#FF4D4F", label: "Error" },
101
+ { value: "#1677FF", label: "Primary" },
102
+ { value: "#111827", label: "Ink" },
103
+ ]}
104
+ />
105
+ ```
106
+
107
+ `colors` still works, but new usage should prefer `presets` because it better reflects that these entries are
108
+ consumer-defined menu content.
109
+
110
+ **`ColorPickerSwatch`**
111
+
112
+ The individual cell used by the palette. This is useful when you want to build a custom grid or layout while
113
+ preserving the built-in focus/selection styles. It accepts `radius`, `size`, and optional overlay `children`.
114
+
115
+ **`ColorPreview`**
116
+
117
+ A small, reusable color indicator. It can be used as a trigger or a static label in forms, tables, or lists.
118
+
119
+ ## Notes
120
+
121
+ - `ColorPicker` uses `MenuDropdown` and disables menu auto-close so advanced controls and typed input can stay inline.
122
+ - Use `ColorPreview` on its own when you only need a swatch indicator.
123
+ - `ColorPickerPalette` accepts `colors` as either strings or objects with `{ value, label, content, disabled }`.
124
+ - Use `swatchRadius` and `swatchContent` (or `ColorPickerSwatch`) when you need custom corner rounding or overlay content.
package/examples.tsx ADDED
@@ -0,0 +1,376 @@
1
+ import React, { useState } from "react";
2
+ import { ColorPicker, ColorPickerContent, ColorPickerPalette, ColorPickerSwatch, ColorPreview } from "./src";
3
+ import { Menu } from "@solara/menu";
4
+
5
+ export const stories = {
6
+ triggered: {
7
+ title: "Triggered picker",
8
+ description: "A menu-triggered color picker using the default preview trigger.",
9
+ showProps: false,
10
+ render: () => <TriggeredExample />,
11
+ code: `import React from "react";
12
+ import { ColorPicker } from "@solara/colorpicker";
13
+
14
+ export function Example() {
15
+ const [color, setColor] = React.useState("#3b82f6");
16
+ return <ColorPicker value={color} onChange={setColor} />;
17
+ }
18
+ `,
19
+ },
20
+ advancedMenu: {
21
+ title: "Advanced menu controls",
22
+ description: "Opt into spectrum, hue, opacity, eyedropper, and format-aware value editing above presets.",
23
+ showProps: false,
24
+ render: () => <AdvancedMenuExample />,
25
+ code: `import React from "react";
26
+ import { ColorPicker } from "@solara/colorpicker";
27
+
28
+ export function Example() {
29
+ const [color, setColor] = React.useState("#C94B4B");
30
+
31
+ return (
32
+ <ColorPicker
33
+ value={color}
34
+ onChange={setColor}
35
+ menuWidth={340}
36
+ menuHeight={520}
37
+ presets={[
38
+ { value: "#C94B4B", label: "Brand red" },
39
+ { value: "#E35D5B", label: "Rose clay" },
40
+ { value: "#F97316", label: "Orange" },
41
+ { value: "#FB923C", label: "Apricot" },
42
+ { value: "#EAB308", label: "Gold" },
43
+ { value: "#FACC15", label: "Sun" },
44
+ { value: "#22C55E", label: "Green" },
45
+ { value: "#4ADE80", label: "Mint" },
46
+ { value: "#14B8A6", label: "Teal" },
47
+ { value: "#06B6D4", label: "Cyan" },
48
+ { value: "#3B82F6", label: "Blue" },
49
+ { value: "#6366F1", label: "Indigo" },
50
+ { value: "#8B5CF6", label: "Violet" },
51
+ { value: "#A855F7", label: "Purple" },
52
+ { value: "#EC4899", label: "Pink" },
53
+ { value: "#F43F5E", label: "Crimson" },
54
+ { value: "#6B7280", label: "Slate" },
55
+ { value: "#111827", label: "Ink" },
56
+ { value: "#000000", label: "Black" },
57
+ { value: "#FFFFFF", label: "White" },
58
+ ]}
59
+ showSpectrum
60
+ showHueSlider
61
+ showOpacitySlider
62
+ showEyedropper
63
+ showFormatSelector
64
+ showOpacityInput
65
+ valueFormat="rgba"
66
+ triggerLabel="Brand color"
67
+ />
68
+ );
69
+ }
70
+ `,
71
+ },
72
+ customPalette: {
73
+ title: "Custom palette",
74
+ description: "Supply a custom preset section and hide the value input row.",
75
+ showProps: false,
76
+ render: () => <CustomPaletteExample />,
77
+ code: `import React from "react";
78
+ import { ColorPicker } from "@solara/colorpicker";
79
+
80
+ export function Example() {
81
+ const [color, setColor] = React.useState("#ff6b6b");
82
+
83
+ return (
84
+ <ColorPicker
85
+ value={color}
86
+ onChange={setColor}
87
+ colors={["#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4", "#ffeaa7", "#dda0dd"]}
88
+ showHexInput={false}
89
+ triggerLabel="Accent"
90
+ />
91
+ );
92
+ }
93
+ `,
94
+ },
95
+ inline: {
96
+ title: "Inline content",
97
+ description: "Use ColorPickerContent directly inside a surface.",
98
+ showProps: false,
99
+ render: () => <InlineExample />,
100
+ code: `import React from "react";
101
+ import { ColorPickerContent } from "@solara/colorpicker";
102
+ import { Menu } from "@solara/menu";
103
+
104
+ export function Example() {
105
+ const [color, setColor] = React.useState("#22c55e");
106
+
107
+ return (
108
+ <Menu style={{ width: 280 }}>
109
+ <ColorPickerContent value={color} onChange={setColor} />
110
+ </Menu>
111
+ );
112
+ }
113
+ `,
114
+ },
115
+ preview: {
116
+ title: "ColorPreview",
117
+ description: "The preview component can be used as a standalone swatch.",
118
+ showProps: false,
119
+ render: () => (
120
+ <div style={{ display: "flex", gap: 12, alignItems: "center" }}>
121
+ <ColorPreview color="#ef4444" size="lg" label="Primary" />
122
+ <ColorPreview color="#000000" size="lg" shape="circle" label="Ink" />
123
+ </div>
124
+ ),
125
+ code: `import { ColorPreview } from "@solara/colorpicker";
126
+
127
+ export function Example() {
128
+ return (
129
+ <div style={{ display: "flex", gap: 12, alignItems: "center" }}>
130
+ <ColorPreview color="#ef4444" size="lg" label="Primary" />
131
+ <ColorPreview color="#000000" size="lg" shape="circle" label="Ink" />
132
+ </div>
133
+ );
134
+ }
135
+ `,
136
+ },
137
+ previewVariants: {
138
+ title: "ColorPreview variants",
139
+ description: "Label vs no label, plus size and radius adjustments.",
140
+ showProps: false,
141
+ render: () => (
142
+ <div style={{ display: "flex", gap: 16, alignItems: "center", flexWrap: "wrap" }}>
143
+ <ColorPreview color="#3b82f6" size="sm" label="Small" />
144
+ <ColorPreview color="#22c55e" size="md" label="Medium" />
145
+ <ColorPreview color="#f59e0b" size="lg" label="Large" />
146
+ <ColorPreview color="#111827" size="md" />
147
+ <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
148
+ <ColorPreview
149
+ color="#ec4899"
150
+ size="md"
151
+ label="Rounded"
152
+ style={{ ["--colorpreview-swatch-size" as const]: "32px" } as React.CSSProperties}
153
+ />
154
+ <span
155
+ style={{
156
+ width: 28,
157
+ height: 28,
158
+ borderRadius: 999,
159
+ background: "#ec4899",
160
+ boxShadow: "inset 0 0 2px rgba(0,0,0,0.4)",
161
+ }}
162
+ />
163
+ </div>
164
+ </div>
165
+ ),
166
+ code: `import { ColorPreview } from "@solara/colorpicker";
167
+
168
+ export function Example() {
169
+ return (
170
+ <div style={{ display: "flex", gap: 16, alignItems: "center", flexWrap: "wrap" }}>
171
+ <ColorPreview color="#3b82f6" size="sm" label="Small" />
172
+ <ColorPreview color="#22c55e" size="md" label="Medium" />
173
+ <ColorPreview color="#f59e0b" size="lg" label="Large" />
174
+ <ColorPreview color="#111827" size="md" />
175
+ <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
176
+ <ColorPreview
177
+ color="#ec4899"
178
+ size="md"
179
+ label="Rounded"
180
+ style={{ ["--colorpreview-swatch-size" as const]: "32px" }}
181
+ />
182
+ <span
183
+ style={{
184
+ width: 28,
185
+ height: 28,
186
+ borderRadius: 999,
187
+ background: "#ec4899",
188
+ boxShadow: "inset 0 0 2px rgba(0,0,0,0.4)",
189
+ }}
190
+ />
191
+ </div>
192
+ </div>
193
+ );
194
+ }
195
+ `,
196
+ },
197
+ paletteSwatches: {
198
+ title: "Palette swatches",
199
+ description: "Use swatches directly with custom radius and overlay content.",
200
+ showProps: false,
201
+ render: () => (
202
+ <div
203
+ style={{
204
+ display: "grid",
205
+ gridTemplateColumns: "repeat(6, 1fr)",
206
+ gap: 8,
207
+ padding: 12,
208
+ background: "var(--color-surface-100)",
209
+ borderRadius: 8,
210
+ }}>
211
+ {["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#6366f1", "#ec4899"].map(
212
+ (color) => (
213
+ <ColorPickerSwatch
214
+ key={color}
215
+ color={color}
216
+ size="md"
217
+ radius="12px"
218
+ aria-label={`Swatch ${color}`}
219
+ />
220
+ )
221
+ )}
222
+ <ColorPickerSwatch color="#111827" size="md" radius="6px">
223
+ A
224
+ </ColorPickerSwatch>
225
+ <ColorPickerSwatch color="#000000" size="md" radius="4px">
226
+ B
227
+ </ColorPickerSwatch>
228
+ </div>
229
+ ),
230
+ code: `import { ColorPickerSwatch } from "@solara/colorpicker";
231
+
232
+ export function Example() {
233
+ return (
234
+ <div
235
+ style={{
236
+ display: "grid",
237
+ gridTemplateColumns: "repeat(6, 1fr)",
238
+ gap: 8,
239
+ padding: 12,
240
+ background: "var(--color-surface-100)",
241
+ borderRadius: 8,
242
+ }}
243
+ >
244
+ {["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#6366f1", "#ec4899"].map(
245
+ (color) => (
246
+ <ColorPickerSwatch
247
+ key={color}
248
+ color={color}
249
+ size="md"
250
+ radius="12px"
251
+ aria-label={\`Swatch \${color}\`}
252
+ />
253
+ )
254
+ )}
255
+ <ColorPickerSwatch color="#111827" size="md" radius="6px">
256
+ A
257
+ </ColorPickerSwatch>
258
+ <ColorPickerSwatch color="#000000" size="md" radius="4px">
259
+ B
260
+ </ColorPickerSwatch>
261
+ </div>
262
+ );
263
+ }
264
+ `,
265
+ },
266
+ paletteComponent: {
267
+ title: "ColorPickerPalette",
268
+ description: "Palette component with per-swatch content and radius overrides.",
269
+ showProps: false,
270
+ render: () => (
271
+ <ColorPickerPalette
272
+ value="#3b82f6"
273
+ size="sm"
274
+ swatchRadius="10px"
275
+ colors={[
276
+ { value: "#ef4444", label: "Red", content: "R" },
277
+ { value: "#f59e0b", label: "Amber", content: "A" },
278
+ { value: "#22c55e", label: "Green", content: "G" },
279
+ { value: "#3b82f6", label: "Blue", content: "B" },
280
+ { value: "#6366f1", label: "Indigo", content: "I" },
281
+ { value: "#ec4899", label: "Pink", content: "P" },
282
+ ]}
283
+ />
284
+ ),
285
+ code: `import { ColorPickerPalette } from "@solara/colorpicker";
286
+
287
+ export function Example() {
288
+ return (
289
+ <ColorPickerPalette
290
+ value="#3b82f6"
291
+ size="sm"
292
+ swatchRadius="10px"
293
+ colors={[
294
+ { value: "#ef4444", label: "Red", content: "R" },
295
+ { value: "#f59e0b", label: "Amber", content: "A" },
296
+ { value: "#22c55e", label: "Green", content: "G" },
297
+ { value: "#3b82f6", label: "Blue", content: "B" },
298
+ { value: "#6366f1", label: "Indigo", content: "I" },
299
+ { value: "#ec4899", label: "Pink", content: "P" },
300
+ ]}
301
+ />
302
+ );
303
+ }
304
+ `,
305
+ },
306
+ };
307
+
308
+ function TriggeredExample() {
309
+ const [color, setColor] = useState("#3b82f6");
310
+ return <ColorPicker value={color} onChange={setColor} />;
311
+ }
312
+
313
+ function AdvancedMenuExample() {
314
+ const [color, setColor] = useState("#C94B4B");
315
+
316
+ return (
317
+ <ColorPicker
318
+ value={color}
319
+ onChange={setColor}
320
+ menuWidth={340}
321
+ menuHeight={520}
322
+ presets={[
323
+ { value: "#C94B4B", label: "Brand red" },
324
+ { value: "#E35D5B", label: "Rose clay" },
325
+ { value: "#F97316", label: "Orange" },
326
+ { value: "#FB923C", label: "Apricot" },
327
+ { value: "#EAB308", label: "Gold" },
328
+ { value: "#FACC15", label: "Sun" },
329
+ { value: "#22C55E", label: "Green" },
330
+ { value: "#4ADE80", label: "Mint" },
331
+ { value: "#14B8A6", label: "Teal" },
332
+ { value: "#06B6D4", label: "Cyan" },
333
+ { value: "#3B82F6", label: "Blue" },
334
+ { value: "#6366F1", label: "Indigo" },
335
+ { value: "#8B5CF6", label: "Violet" },
336
+ { value: "#A855F7", label: "Purple" },
337
+ { value: "#EC4899", label: "Pink" },
338
+ { value: "#F43F5E", label: "Crimson" },
339
+ { value: "#6B7280", label: "Slate" },
340
+ { value: "#111827", label: "Ink" },
341
+ { value: "#000000", label: "Black" },
342
+ { value: "#FFFFFF", label: "White" },
343
+ ]}
344
+ showSpectrum
345
+ showHueSlider
346
+ showOpacitySlider
347
+ showEyedropper
348
+ showFormatSelector
349
+ showOpacityInput
350
+ valueFormat="rgba"
351
+ triggerLabel="Brand color"
352
+ />
353
+ );
354
+ }
355
+
356
+ function CustomPaletteExample() {
357
+ const [color, setColor] = useState("#ff6b6b");
358
+ return (
359
+ <ColorPicker
360
+ value={color}
361
+ onChange={setColor}
362
+ presets={["#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4", "#ffeaa7", "#dda0dd"]}
363
+ showValueInput={false}
364
+ triggerLabel="Accent"
365
+ />
366
+ );
367
+ }
368
+
369
+ function InlineExample() {
370
+ const [color, setColor] = useState("#22c55e");
371
+ return (
372
+ <Menu style={{ width: 280 }}>
373
+ <ColorPickerContent value={color} onChange={setColor} />
374
+ </Menu>
375
+ );
376
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@varialkit/colorpicker",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./examples": "./examples.tsx"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "docs.md",
14
+ "examples.tsx"
15
+ ],
16
+ "peerDependencies": {
17
+ "react": "^19.0.0"
18
+ },
19
+ "dependencies": {
20
+ "@varialkit/textfield": "0.1.0",
21
+ "@varialkit/menu": "0.1.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/react": "19.0.10",
25
+ "react": "19.0.0"
26
+ }
27
+ }