@charcuterie/ui 4.26.2 → 4.27.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/dist/ColorPicker/ColorPicker.d.ts +74 -0
- package/dist/ColorPicker/ColorPicker.d.ts.map +1 -0
- package/dist/ColorPicker/ColorPicker.js +133 -0
- package/dist/ColorPicker/ColorPicker.js.map +1 -0
- package/dist/ColorPicker/ColorPickerCustomPanel.d.ts +35 -0
- package/dist/ColorPicker/ColorPickerCustomPanel.d.ts.map +1 -0
- package/dist/ColorPicker/ColorPickerCustomPanel.js +93 -0
- package/dist/ColorPicker/ColorPickerCustomPanel.js.map +1 -0
- package/dist/ColorPicker/parseHexColor.d.ts +17 -0
- package/dist/ColorPicker/parseHexColor.d.ts.map +1 -0
- package/dist/ColorPicker/parseHexColor.js +31 -0
- package/dist/ColorPicker/parseHexColor.js.map +1 -0
- package/dist/SegmentedControl/SegmentedOption.js +1 -1
- package/dist/SegmentedControl/SegmentedOption.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ColorPicker/ColorPicker.mdx +70 -0
- package/src/ColorPicker/ColorPicker.stories.tsx +289 -0
- package/src/ColorPicker/ColorPicker.test.tsx +385 -0
- package/src/ColorPicker/ColorPicker.tsx +347 -0
- package/src/ColorPicker/ColorPickerCustomPanel.tsx +224 -0
- package/src/ColorPicker/parseHexColor.ts +37 -0
- package/src/SegmentedControl/SegmentedControl.stories.tsx +41 -0
- package/src/SegmentedControl/SegmentedControl.test.tsx +38 -3
- package/src/SegmentedControl/SegmentedOption.tsx +1 -1
- package/src/index.ts +6 -0
- package/src/sourceRules.test.ts +3 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks"
|
|
2
|
+
|
|
3
|
+
import * as ColorPickerStories from "./ColorPicker.stories.tsx"
|
|
4
|
+
|
|
5
|
+
<Meta of={ColorPickerStories} />
|
|
6
|
+
|
|
7
|
+
# ColorPicker
|
|
8
|
+
|
|
9
|
+
A named color from the app's list, or a color of the person's own. The presets are a
|
|
10
|
+
searchable `Combobox`, each row a `Swatch` and its name; an optional **Custom color…** row
|
|
11
|
+
opens a panel with the browser's color wheel and a hex field.
|
|
12
|
+
|
|
13
|
+
<Canvas of={ColorPickerStories.Playground} />
|
|
14
|
+
|
|
15
|
+
## Props
|
|
16
|
+
|
|
17
|
+
<Controls />
|
|
18
|
+
|
|
19
|
+
## When to use it, and when not
|
|
20
|
+
|
|
21
|
+
| You have | Use |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| A palette the **app** owns, maybe plus colors a person makes up | `ColorPicker` |
|
|
24
|
+
| A choice from the library's ten `categorical` hues | A row of `Swatch` buttons over `--color-categorical-<n>-solid`, as Docket's `CategoricalPicker` does |
|
|
25
|
+
| A color that arrived from the world and is only shown | `Swatch` |
|
|
26
|
+
|
|
27
|
+
The presets are data, so the result is a `Swatch`-class color, not a token: it does not
|
|
28
|
+
re-theme and the library cannot contrast-check it. That is why `getColorError` exists. The
|
|
29
|
+
app knows where the color is drawn and what it has to clear; the component only shows the
|
|
30
|
+
app's sentence and refuses to apply.
|
|
31
|
+
|
|
32
|
+
## Typing narrows the list
|
|
33
|
+
|
|
34
|
+
A palette worth offering is long, and siblings may all want a different pink. Typing a name
|
|
35
|
+
narrows the rows to that family; typing a hex code finds its preset. The **Custom color…**
|
|
36
|
+
row is always listed, whatever the search says, and a valid code in the search opens the
|
|
37
|
+
custom panel on that code.
|
|
38
|
+
|
|
39
|
+
<Canvas of={ColorPickerStories.Interactive} />
|
|
40
|
+
|
|
41
|
+
## The custom panel
|
|
42
|
+
|
|
43
|
+
The browser's `<input type="color">` is allowed here and only here. It is the custom
|
|
44
|
+
**mode**, inside a panel this component owns — never the whole picker. The wheel, the
|
|
45
|
+
eyedropper, and the sliders are worth keeping, and the presets are what the native control
|
|
46
|
+
alone could never offer.
|
|
47
|
+
|
|
48
|
+
Nothing is applied while a person drags the wheel. The panel holds a draft: **Use color** or
|
|
49
|
+
Enter applies it, and Escape or **Cancel** drops it. A custom color answers `#RRGGBB` in
|
|
50
|
+
upper case, so the app stores one spelling for one color.
|
|
51
|
+
|
|
52
|
+
<Canvas of={ColorPickerStories.CustomPanel} />
|
|
53
|
+
|
|
54
|
+
## The trigger says the name, or the code
|
|
55
|
+
|
|
56
|
+
`"Color: Hot Pink"` for a preset and `"Color: #C2185B"` for a custom color. The visible text
|
|
57
|
+
is in the accessible name (WCAG 2.5.3), as with `Picker`. Query it as:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
getByRole("button", { name: /^Color: / })
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
<Canvas of={ColorPickerStories.AllStates} />
|
|
64
|
+
|
|
65
|
+
## Inside a `Field`
|
|
66
|
+
|
|
67
|
+
`Field` clones its `id` and `aria-describedby` onto the trigger button, and the trigger
|
|
68
|
+
stretches to the container with the swatch and name at the start and the chevron at the end.
|
|
69
|
+
|
|
70
|
+
<Canvas of={ColorPickerStories.Responsive} />
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import type { ReactNode } from "react"
|
|
4
|
+
import { useState } from "react"
|
|
5
|
+
import { userEvent, within } from "storybook/test"
|
|
6
|
+
|
|
7
|
+
import { controlSizeArgType } from "../argTypes.storyHelpers.ts"
|
|
8
|
+
import {
|
|
9
|
+
ContainerBoard,
|
|
10
|
+
StoryRow,
|
|
11
|
+
StorySection,
|
|
12
|
+
} from "../board.storyHelpers.tsx"
|
|
13
|
+
import { Field } from "../Field/Field.tsx"
|
|
14
|
+
import type { ColorPreset } from "./ColorPicker.tsx"
|
|
15
|
+
import { ColorPicker } from "./ColorPicker.tsx"
|
|
16
|
+
import { parseHexColor } from "./parseHexColor.ts"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A palette with several shades of one hue on purpose: the case that
|
|
20
|
+
* made this a searchable list is a family where every child wants a
|
|
21
|
+
* different pink.
|
|
22
|
+
*/
|
|
23
|
+
const PRESETS: ColorPreset[] = [
|
|
24
|
+
{ name: "Red", value: "#E63946" },
|
|
25
|
+
{
|
|
26
|
+
keywords: ["pink", "red"],
|
|
27
|
+
name: "Raspberry",
|
|
28
|
+
value: "#DB2777",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
keywords: ["pink"],
|
|
32
|
+
name: "Hot Pink",
|
|
33
|
+
value: "#FF006E",
|
|
34
|
+
},
|
|
35
|
+
{ keywords: ["pink"], name: "Blush", value: "#FB7185" },
|
|
36
|
+
{
|
|
37
|
+
keywords: ["pink"],
|
|
38
|
+
name: "Bubblegum",
|
|
39
|
+
value: "#FF69B4",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
keywords: ["pink"],
|
|
43
|
+
name: "Cotton Candy",
|
|
44
|
+
value: "#F9A8D4",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
keywords: ["purple"],
|
|
48
|
+
name: "Lavender",
|
|
49
|
+
value: "#A78BFA",
|
|
50
|
+
},
|
|
51
|
+
{ name: "Purple", value: "#8338EC" },
|
|
52
|
+
{ name: "Blue", value: "#3A86FF" },
|
|
53
|
+
{
|
|
54
|
+
keywords: ["green", "blue"],
|
|
55
|
+
name: "Teal",
|
|
56
|
+
value: "#2A9D8F",
|
|
57
|
+
},
|
|
58
|
+
{ name: "Green", value: "#22C55E" },
|
|
59
|
+
{ name: "Yellow", value: "#FFBE0B" },
|
|
60
|
+
{ name: "Orange", value: "#FB5607" },
|
|
61
|
+
{ name: "Brown", value: "#B5651D" },
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A stand-in for an app's own rule. This one refuses grays and
|
|
66
|
+
* near-whites, the way a reader board that draws the color on a dark
|
|
67
|
+
* panel would.
|
|
68
|
+
*/
|
|
69
|
+
const getStoryColorError = (hex: string): null | string => {
|
|
70
|
+
const digits = (parseHexColor(hex) ?? "").slice(1)
|
|
71
|
+
const channels = [0, 2, 4].map(
|
|
72
|
+
(start) =>
|
|
73
|
+
Number.parseInt(digits.slice(start, start + 2), 16) /
|
|
74
|
+
255,
|
|
75
|
+
)
|
|
76
|
+
const highest = Math.max(...channels)
|
|
77
|
+
const lowest = Math.min(...channels)
|
|
78
|
+
|
|
79
|
+
if (highest - lowest < 0.2) {
|
|
80
|
+
return "Pick a color with more hue. Gray, white, and black do not show on the board."
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const meta = {
|
|
87
|
+
title: "Components/Controls/ColorPicker",
|
|
88
|
+
component: ColorPicker,
|
|
89
|
+
parameters: { layout: "padded" },
|
|
90
|
+
argTypes: {
|
|
91
|
+
size: controlSizeArgType,
|
|
92
|
+
},
|
|
93
|
+
args: {
|
|
94
|
+
isCustomAllowed: true,
|
|
95
|
+
label: "Color",
|
|
96
|
+
onChange: () => {},
|
|
97
|
+
presets: PRESETS,
|
|
98
|
+
},
|
|
99
|
+
} satisfies Meta<typeof ColorPicker>
|
|
100
|
+
|
|
101
|
+
export default meta
|
|
102
|
+
|
|
103
|
+
type Story = StoryObj<typeof meta>
|
|
104
|
+
|
|
105
|
+
const ColorPickerHarness = ({
|
|
106
|
+
hasColorRule = false,
|
|
107
|
+
initialValue,
|
|
108
|
+
isCustomAllowed = true,
|
|
109
|
+
label,
|
|
110
|
+
}: {
|
|
111
|
+
hasColorRule?: boolean
|
|
112
|
+
initialValue?: string
|
|
113
|
+
isCustomAllowed?: boolean
|
|
114
|
+
label: string
|
|
115
|
+
}): ReactNode => {
|
|
116
|
+
const [value, setValue] = useState(initialValue)
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="flex flex-col gap-2">
|
|
120
|
+
<ColorPicker
|
|
121
|
+
getColorError={
|
|
122
|
+
hasColorRule ? getStoryColorError : undefined
|
|
123
|
+
}
|
|
124
|
+
isCustomAllowed={isCustomAllowed}
|
|
125
|
+
label={label}
|
|
126
|
+
onChange={setValue}
|
|
127
|
+
presets={PRESETS}
|
|
128
|
+
value={value}
|
|
129
|
+
/>
|
|
130
|
+
<p className="text-content-muted text-xs">
|
|
131
|
+
Value: {value ?? "none"}
|
|
132
|
+
</p>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** A preset chosen, custom colors allowed, and an app rule. */
|
|
138
|
+
export const Playground: Story = {
|
|
139
|
+
parameters: playgroundParameters,
|
|
140
|
+
render: () => (
|
|
141
|
+
<ColorPickerHarness
|
|
142
|
+
hasColorRule
|
|
143
|
+
initialValue="#FF006E"
|
|
144
|
+
label="Color"
|
|
145
|
+
/>
|
|
146
|
+
),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The three sizes, each reading a density-aware control height. */
|
|
150
|
+
export const AllVariants: Story = {
|
|
151
|
+
render: () => (
|
|
152
|
+
<StorySection title="Sizes">
|
|
153
|
+
<StoryRow>
|
|
154
|
+
{(["sm", "md", "lg"] as const).map((size) => (
|
|
155
|
+
<ColorPicker
|
|
156
|
+
key={size}
|
|
157
|
+
label={`Color ${size}`}
|
|
158
|
+
onChange={() => {}}
|
|
159
|
+
presets={PRESETS}
|
|
160
|
+
size={size}
|
|
161
|
+
value="#3A86FF"
|
|
162
|
+
/>
|
|
163
|
+
))}
|
|
164
|
+
</StoryRow>
|
|
165
|
+
</StorySection>
|
|
166
|
+
),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A preset, a custom color (the trigger reads the code), nothing chosen
|
|
171
|
+
* yet, presets only, and disabled.
|
|
172
|
+
*/
|
|
173
|
+
export const AllStates: Story = {
|
|
174
|
+
render: () => (
|
|
175
|
+
<div className="flex flex-col gap-6">
|
|
176
|
+
<StorySection title="A preset">
|
|
177
|
+
<StoryRow>
|
|
178
|
+
<ColorPickerHarness
|
|
179
|
+
initialValue="#FF69B4"
|
|
180
|
+
label="Preset color"
|
|
181
|
+
/>
|
|
182
|
+
</StoryRow>
|
|
183
|
+
</StorySection>
|
|
184
|
+
|
|
185
|
+
<StorySection title="A custom color reads its code">
|
|
186
|
+
<StoryRow>
|
|
187
|
+
<ColorPickerHarness
|
|
188
|
+
initialValue="#C2185B"
|
|
189
|
+
label="Custom color"
|
|
190
|
+
/>
|
|
191
|
+
</StoryRow>
|
|
192
|
+
</StorySection>
|
|
193
|
+
|
|
194
|
+
<StorySection title="Nothing chosen">
|
|
195
|
+
<StoryRow>
|
|
196
|
+
<ColorPickerHarness label="Empty color" />
|
|
197
|
+
</StoryRow>
|
|
198
|
+
</StorySection>
|
|
199
|
+
|
|
200
|
+
<StorySection title="Presets only">
|
|
201
|
+
<StoryRow>
|
|
202
|
+
<ColorPickerHarness
|
|
203
|
+
initialValue="#22C55E"
|
|
204
|
+
isCustomAllowed={false}
|
|
205
|
+
label="Presets-only color"
|
|
206
|
+
/>
|
|
207
|
+
</StoryRow>
|
|
208
|
+
</StorySection>
|
|
209
|
+
|
|
210
|
+
<StorySection title="Disabled">
|
|
211
|
+
<StoryRow>
|
|
212
|
+
<ColorPicker
|
|
213
|
+
isDisabled
|
|
214
|
+
label="Disabled color"
|
|
215
|
+
onChange={() => {}}
|
|
216
|
+
presets={PRESETS}
|
|
217
|
+
value="#8338EC"
|
|
218
|
+
/>
|
|
219
|
+
</StoryRow>
|
|
220
|
+
</StorySection>
|
|
221
|
+
</div>
|
|
222
|
+
),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The custom panel, opened: the browser's color wheel beside a hex
|
|
227
|
+
* field, and an app rule refusing a gray.
|
|
228
|
+
*/
|
|
229
|
+
export const CustomPanel: Story = {
|
|
230
|
+
render: () => (
|
|
231
|
+
<ColorPickerHarness
|
|
232
|
+
hasColorRule
|
|
233
|
+
initialValue="#808080"
|
|
234
|
+
label="Rule color"
|
|
235
|
+
/>
|
|
236
|
+
),
|
|
237
|
+
play: async ({ canvasElement }) => {
|
|
238
|
+
const canvas = within(canvasElement)
|
|
239
|
+
|
|
240
|
+
await userEvent.click(
|
|
241
|
+
canvas.getByRole("button", {
|
|
242
|
+
name: "Rule color: #808080",
|
|
243
|
+
}),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
await userEvent.click(
|
|
247
|
+
within(canvasElement.ownerDocument.body).getByRole(
|
|
248
|
+
"option",
|
|
249
|
+
{ name: "Custom color…" },
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Drive it: open, type "pink" to narrow the list, choose one, then
|
|
257
|
+
* choose "Custom color…" and apply a code of your own.
|
|
258
|
+
*/
|
|
259
|
+
export const Interactive: Story = {
|
|
260
|
+
render: () => (
|
|
261
|
+
<ColorPickerHarness
|
|
262
|
+
hasColorRule
|
|
263
|
+
initialValue="#3A86FF"
|
|
264
|
+
label="Interactive color"
|
|
265
|
+
/>
|
|
266
|
+
),
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Inside a `Field` the trigger stretches to the container, and a long
|
|
271
|
+
* name truncates rather than pushing the chevron out.
|
|
272
|
+
*/
|
|
273
|
+
export const Responsive: Story = {
|
|
274
|
+
render: () => (
|
|
275
|
+
<ContainerBoard>
|
|
276
|
+
{(width) => (
|
|
277
|
+
<Field label={`Color at ${width}`}>
|
|
278
|
+
<ColorPicker
|
|
279
|
+
isCustomAllowed
|
|
280
|
+
label={`Color at ${width}`}
|
|
281
|
+
onChange={() => {}}
|
|
282
|
+
presets={PRESETS}
|
|
283
|
+
value="#F9A8D4"
|
|
284
|
+
/>
|
|
285
|
+
</Field>
|
|
286
|
+
)}
|
|
287
|
+
</ContainerBoard>
|
|
288
|
+
),
|
|
289
|
+
}
|