@cntyclub/ui-react 0.15.0 → 0.16.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/dist/index.d.ts +70 -1
- package/dist/index.js +322 -79
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/ui/color-picker.tsx +378 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cntyclub/ui-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "React component library for the Country Club UI Kit — Base UI primitives styled with the Country Club design system (Tailwind CSS v4)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"input-otp": "^1.4.2",
|
|
48
48
|
"lucide-react": "^0.561.0",
|
|
49
49
|
"qr-code-styling": "^1.9.2",
|
|
50
|
+
"react-aria-components": "^1.20.0",
|
|
50
51
|
"react-day-picker": "^9.14.0",
|
|
51
52
|
"react-dropzone": "^14.4.0",
|
|
52
53
|
"react-markdown": "^9.1.0",
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PipetteIcon, PlusIcon } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import {
|
|
6
|
+
ColorArea as AriaColorArea,
|
|
7
|
+
ColorField as AriaColorField,
|
|
8
|
+
ColorPicker as AriaColorPicker,
|
|
9
|
+
ColorSlider as AriaColorSlider,
|
|
10
|
+
ColorSwatch as AriaColorSwatch,
|
|
11
|
+
ColorSwatchPicker as AriaColorSwatchPicker,
|
|
12
|
+
ColorSwatchPickerItem as AriaColorSwatchPickerItem,
|
|
13
|
+
ColorThumb as AriaColorThumb,
|
|
14
|
+
ColorPickerStateContext,
|
|
15
|
+
Input as AriaInput,
|
|
16
|
+
SliderTrack as AriaSliderTrack,
|
|
17
|
+
getColorChannels,
|
|
18
|
+
parseColor,
|
|
19
|
+
} from "react-aria-components";
|
|
20
|
+
|
|
21
|
+
import { cn } from "../../lib/utils/css";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A colour picker, built on React Aria's colour primitives.
|
|
25
|
+
*
|
|
26
|
+
* The one part of this kit that is not Base UI, and deliberately: choosing a colour is
|
|
27
|
+
* a pointer-and-keyboard problem with a saturation plane, two gradient sliders, a
|
|
28
|
+
* parsed text field and a channel model behind them, and React Aria is the only
|
|
29
|
+
* accessible implementation of that we would not otherwise be writing ourselves.
|
|
30
|
+
* Everything visible is still this kit's tokens and `data-slot` conventions.
|
|
31
|
+
*
|
|
32
|
+
* **Compound, so a screen takes only the parts it needs.** The four-hex-field panel in
|
|
33
|
+
* the office needs a swatch and a hex box; a full editor wants the plane and both
|
|
34
|
+
* sliders. Both are the same state, so both read `ColorPicker.Provider` and neither
|
|
35
|
+
* has to lift anything.
|
|
36
|
+
*
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <ColorPicker.Provider defaultValue="#8c1515" onChange={(c) => save(c.toString("hex"))}>
|
|
39
|
+
* <ColorPicker.Area />
|
|
40
|
+
* <ColorPicker.HueSlider />
|
|
41
|
+
* <ColorPicker.ValueInput />
|
|
42
|
+
* </ColorPicker.Provider>
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* Or the whole thing, already composed:
|
|
46
|
+
*
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <ColorPickerPanel defaultValue="#8c1515" onChange={…} />
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
// The chequerboard behind anything that can be semi-transparent. Inline rather than a
|
|
53
|
+
// token because it is a texture, not a colour, and it must look identical in both
|
|
54
|
+
// themes — a transparency indicator that changes with the theme reads as part of the
|
|
55
|
+
// colour being shown.
|
|
56
|
+
const CHECKERBOARD =
|
|
57
|
+
"repeating-conic-gradient(rgba(0,0,0,.12) 0% 25%, transparent 0% 50%) 50% / 12px 12px";
|
|
58
|
+
|
|
59
|
+
function useColorPicker() {
|
|
60
|
+
const state = React.useContext(ColorPickerStateContext);
|
|
61
|
+
if (!state) {
|
|
62
|
+
throw new Error("useColorPicker must be used inside <ColorPicker.Provider>.");
|
|
63
|
+
}
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ColorPickerProvider(props: React.ComponentProps<typeof AriaColorPicker>) {
|
|
68
|
+
return <AriaColorPicker {...props} />;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ColorArea({ className, ...props }: React.ComponentProps<typeof AriaColorArea>) {
|
|
72
|
+
return (
|
|
73
|
+
<AriaColorArea
|
|
74
|
+
className={cn(
|
|
75
|
+
"h-40 w-full shrink-0 rounded-lg border border-input outline-none",
|
|
76
|
+
"data-[disabled]:opacity-64",
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
data-slot="color-picker-area"
|
|
80
|
+
{...props}
|
|
81
|
+
>
|
|
82
|
+
<ColorThumb />
|
|
83
|
+
</AriaColorArea>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function ColorThumb({ className, ...props }: React.ComponentProps<typeof AriaColorThumb>) {
|
|
88
|
+
return (
|
|
89
|
+
<AriaColorThumb
|
|
90
|
+
className={cn(
|
|
91
|
+
// `box-shadow` rather than a border, so the ring sits *outside* the swatch and
|
|
92
|
+
// the pixel under the thumb is the colour actually selected.
|
|
93
|
+
"size-4 rounded-full border-2 border-white shadow-[0_0_0_1px_rgba(0,0,0,.35)] outline-none",
|
|
94
|
+
"transition-[scale,box-shadow] duration-150 ease-out",
|
|
95
|
+
"data-[focus-visible]:ring-[3px] data-[focus-visible]:ring-ring/32 data-[dragging]:scale-115",
|
|
96
|
+
className,
|
|
97
|
+
)}
|
|
98
|
+
data-slot="color-picker-thumb"
|
|
99
|
+
{...props}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function ColorSliderBase({
|
|
105
|
+
className,
|
|
106
|
+
channel,
|
|
107
|
+
chequered,
|
|
108
|
+
...props
|
|
109
|
+
}: Omit<React.ComponentProps<typeof AriaColorSlider>, "channel"> & {
|
|
110
|
+
channel: "hue" | "alpha";
|
|
111
|
+
chequered?: boolean;
|
|
112
|
+
}) {
|
|
113
|
+
return (
|
|
114
|
+
<AriaColorSlider
|
|
115
|
+
channel={channel}
|
|
116
|
+
className={cn("w-full data-[disabled]:opacity-64", className)}
|
|
117
|
+
data-slot={`color-picker-${channel}-slider`}
|
|
118
|
+
orientation="horizontal"
|
|
119
|
+
{...props}
|
|
120
|
+
>
|
|
121
|
+
{/* The gradient is React Aria's, handed over as `defaultStyle` — it knows the
|
|
122
|
+
channel and the current colour, and recomputing it here would be a second
|
|
123
|
+
source of truth for the same thing. An alpha track additionally stacks the
|
|
124
|
+
chequerboard *under* it, which is the whole point of that track: the gradient
|
|
125
|
+
ends at transparent, so what shows through is what "transparent" looks like. */}
|
|
126
|
+
<AriaSliderTrack
|
|
127
|
+
className="relative h-3 w-full rounded-full border border-input"
|
|
128
|
+
style={({ defaultStyle }) => ({
|
|
129
|
+
background: chequered
|
|
130
|
+
? `${defaultStyle.background}, ${CHECKERBOARD}`
|
|
131
|
+
: defaultStyle.background,
|
|
132
|
+
})}
|
|
133
|
+
>
|
|
134
|
+
<ColorThumb className="top-1/2" />
|
|
135
|
+
</AriaSliderTrack>
|
|
136
|
+
</AriaColorSlider>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function ColorHueSlider(props: Omit<React.ComponentProps<typeof AriaColorSlider>, "channel">) {
|
|
141
|
+
return <ColorSliderBase channel="hue" {...props} />;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function ColorAlphaSlider(props: Omit<React.ComponentProps<typeof AriaColorSlider>, "channel">) {
|
|
145
|
+
return <ColorSliderBase channel="alpha" chequered {...props} />;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function ColorSwatch({ className, ...props }: React.ComponentProps<typeof AriaColorSwatch>) {
|
|
149
|
+
return (
|
|
150
|
+
<AriaColorSwatch
|
|
151
|
+
className={cn("size-8 shrink-0 rounded-md border border-input", className)}
|
|
152
|
+
data-slot="color-picker-swatch"
|
|
153
|
+
style={{ background: CHECKERBOARD }}
|
|
154
|
+
{...props}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ColorValueInput({
|
|
160
|
+
className,
|
|
161
|
+
...props
|
|
162
|
+
}: Omit<React.ComponentProps<typeof AriaColorField>, "children">) {
|
|
163
|
+
return (
|
|
164
|
+
<AriaColorField
|
|
165
|
+
className={cn("min-w-0 flex-1", className)}
|
|
166
|
+
data-slot="color-picker-value"
|
|
167
|
+
{...props}
|
|
168
|
+
>
|
|
169
|
+
<AriaInput
|
|
170
|
+
className={cn(
|
|
171
|
+
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-2.5 font-mono text-sm tabular-nums outline-none",
|
|
172
|
+
"placeholder:text-muted-foreground focus-visible:ring-[3px] focus-visible:ring-ring/24",
|
|
173
|
+
)}
|
|
174
|
+
/>
|
|
175
|
+
</AriaColorField>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A grid of colours somebody can pick from, and optionally add to.
|
|
181
|
+
*
|
|
182
|
+
* `onAdd` is what makes it "saved colours" rather than a fixed palette: the plus tile
|
|
183
|
+
* only appears when there is somewhere for the current colour to go.
|
|
184
|
+
*/
|
|
185
|
+
function ColorSavedColors({
|
|
186
|
+
className,
|
|
187
|
+
colors,
|
|
188
|
+
onSelect,
|
|
189
|
+
onAdd,
|
|
190
|
+
size = "md",
|
|
191
|
+
...props
|
|
192
|
+
}: Omit<React.ComponentProps<typeof AriaColorSwatchPicker>, "onChange" | "children"> & {
|
|
193
|
+
colors: string[];
|
|
194
|
+
onSelect?: (color: string) => void;
|
|
195
|
+
onAdd?: () => void;
|
|
196
|
+
size?: "sm" | "md";
|
|
197
|
+
}) {
|
|
198
|
+
const tile = size === "sm" ? "size-6" : "size-8";
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div className={cn("flex flex-wrap items-center gap-1.5", className)} data-slot="color-picker-saved">
|
|
202
|
+
<AriaColorSwatchPicker
|
|
203
|
+
className="flex flex-wrap items-center gap-1.5"
|
|
204
|
+
onChange={(color) => onSelect?.(color.toString("hex"))}
|
|
205
|
+
{...props}
|
|
206
|
+
>
|
|
207
|
+
{colors.map((color) => (
|
|
208
|
+
<AriaColorSwatchPickerItem
|
|
209
|
+
className={cn(
|
|
210
|
+
tile,
|
|
211
|
+
"rounded-md border border-input outline-none",
|
|
212
|
+
"data-[focus-visible]:ring-[3px] data-[focus-visible]:ring-ring/24",
|
|
213
|
+
"data-[selected]:ring-2 data-[selected]:ring-ring data-[selected]:ring-offset-2 data-[selected]:ring-offset-background",
|
|
214
|
+
)}
|
|
215
|
+
color={color}
|
|
216
|
+
key={color}
|
|
217
|
+
>
|
|
218
|
+
<AriaColorSwatch className="size-full rounded-[5px]" />
|
|
219
|
+
</AriaColorSwatchPickerItem>
|
|
220
|
+
))}
|
|
221
|
+
</AriaColorSwatchPicker>
|
|
222
|
+
{onAdd ? (
|
|
223
|
+
<button
|
|
224
|
+
aria-label="Save this colour"
|
|
225
|
+
className={cn(
|
|
226
|
+
tile,
|
|
227
|
+
"flex items-center justify-center rounded-md border border-input border-dashed text-muted-foreground outline-none",
|
|
228
|
+
"transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/24",
|
|
229
|
+
)}
|
|
230
|
+
onClick={onAdd}
|
|
231
|
+
type="button"
|
|
232
|
+
>
|
|
233
|
+
<PlusIcon className={size === "sm" ? "size-3" : "size-3.5"} strokeWidth={2} />
|
|
234
|
+
</button>
|
|
235
|
+
) : null}
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Pick a colour from anywhere on the screen.
|
|
242
|
+
*
|
|
243
|
+
* Renders nothing where the browser has no `EyeDropper` — Firefox and Safari do not,
|
|
244
|
+
* and a button that silently does nothing is worse than one that is not offered. The
|
|
245
|
+
* check runs in an effect rather than during render, because `window` is not there on
|
|
246
|
+
* the server and a hydration mismatch over a button is still a hydration mismatch.
|
|
247
|
+
*/
|
|
248
|
+
function ColorEyeDropper({ className, ...props }: React.ComponentProps<"button">) {
|
|
249
|
+
const state = useColorPicker();
|
|
250
|
+
const [supported, setSupported] = React.useState(false);
|
|
251
|
+
|
|
252
|
+
React.useEffect(() => {
|
|
253
|
+
setSupported(typeof window !== "undefined" && "EyeDropper" in window);
|
|
254
|
+
}, []);
|
|
255
|
+
|
|
256
|
+
if (!supported) return null;
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<button
|
|
260
|
+
aria-label="Pick a colour from the screen"
|
|
261
|
+
className={cn(
|
|
262
|
+
"flex size-9 shrink-0 items-center justify-center rounded-md border border-input text-muted-foreground outline-none",
|
|
263
|
+
"transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/24",
|
|
264
|
+
className,
|
|
265
|
+
)}
|
|
266
|
+
data-slot="color-picker-eyedropper"
|
|
267
|
+
onClick={async () => {
|
|
268
|
+
try {
|
|
269
|
+
// @ts-expect-error — EyeDropper is not in lib.dom yet.
|
|
270
|
+
const result = await new window.EyeDropper().open();
|
|
271
|
+
state.setColor(parseColor(result.sRGBHex));
|
|
272
|
+
} catch {
|
|
273
|
+
// The user dismissed it. Not an error, and nothing to say about it.
|
|
274
|
+
}
|
|
275
|
+
}}
|
|
276
|
+
type="button"
|
|
277
|
+
{...props}
|
|
278
|
+
>
|
|
279
|
+
<PipetteIcon className="size-4" strokeWidth={1.8} />
|
|
280
|
+
</button>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The current colour as a swatch — reads state, so it needs no props. */
|
|
285
|
+
function ColorPreview({ className }: { className?: string }) {
|
|
286
|
+
const state = useColorPicker();
|
|
287
|
+
return (
|
|
288
|
+
<AriaColorSwatch
|
|
289
|
+
className={cn("size-9 shrink-0 rounded-md border border-input", className)}
|
|
290
|
+
color={state.color}
|
|
291
|
+
data-slot="color-picker-preview"
|
|
292
|
+
style={{ background: CHECKERBOARD }}
|
|
293
|
+
/>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The whole picker, already composed — plane, hue, alpha, eyedropper and a hex box.
|
|
299
|
+
*
|
|
300
|
+
* What a screen reaches for when it just wants "let them choose a colour". Anything
|
|
301
|
+
* more specific composes the parts above instead.
|
|
302
|
+
*/
|
|
303
|
+
function ColorPickerPanel({
|
|
304
|
+
className,
|
|
305
|
+
withAlpha = false,
|
|
306
|
+
savedColors,
|
|
307
|
+
onSaveColor,
|
|
308
|
+
...props
|
|
309
|
+
}: Omit<React.ComponentProps<typeof AriaColorPicker>, "children"> & {
|
|
310
|
+
/** Wraps the panel, not the provider — the provider renders no element. */
|
|
311
|
+
className?: string;
|
|
312
|
+
withAlpha?: boolean;
|
|
313
|
+
savedColors?: string[];
|
|
314
|
+
onSaveColor?: () => void;
|
|
315
|
+
}) {
|
|
316
|
+
return (
|
|
317
|
+
<ColorPickerProvider {...props}>
|
|
318
|
+
<div
|
|
319
|
+
className={cn("flex w-64 flex-col gap-3 rounded-xl border border-input bg-popover p-3", className)}
|
|
320
|
+
data-slot="color-picker"
|
|
321
|
+
>
|
|
322
|
+
<ColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" />
|
|
323
|
+
<div className="flex items-center gap-2">
|
|
324
|
+
<ColorPreview />
|
|
325
|
+
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
|
326
|
+
<ColorHueSlider />
|
|
327
|
+
{withAlpha ? <ColorAlphaSlider /> : null}
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
<div className="flex items-center gap-2">
|
|
331
|
+
<ColorEyeDropper />
|
|
332
|
+
<ColorValueInput />
|
|
333
|
+
</div>
|
|
334
|
+
{savedColors?.length || onSaveColor ? (
|
|
335
|
+
<ColorSavedColors colors={savedColors ?? []} onAdd={onSaveColor} size="sm" />
|
|
336
|
+
) : null}
|
|
337
|
+
</div>
|
|
338
|
+
</ColorPickerProvider>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The compound namespace.
|
|
344
|
+
*
|
|
345
|
+
* Exported as one object rather than a dozen loose names because the parts are only
|
|
346
|
+
* meaningful together, and `ColorPicker.Area` says which state it reads in a way that
|
|
347
|
+
* `ColorArea` does not.
|
|
348
|
+
*/
|
|
349
|
+
const ColorPicker = Object.assign(ColorPickerPanel, {
|
|
350
|
+
Provider: ColorPickerProvider,
|
|
351
|
+
Area: ColorArea,
|
|
352
|
+
Thumb: ColorThumb,
|
|
353
|
+
HueSlider: ColorHueSlider,
|
|
354
|
+
AlphaSlider: ColorAlphaSlider,
|
|
355
|
+
Swatch: ColorSwatch,
|
|
356
|
+
Preview: ColorPreview,
|
|
357
|
+
ValueInput: ColorValueInput,
|
|
358
|
+
SavedColors: ColorSavedColors,
|
|
359
|
+
EyeDropper: ColorEyeDropper,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
export {
|
|
363
|
+
ColorPicker,
|
|
364
|
+
ColorPickerPanel,
|
|
365
|
+
ColorPickerProvider,
|
|
366
|
+
ColorArea,
|
|
367
|
+
ColorThumb,
|
|
368
|
+
ColorHueSlider,
|
|
369
|
+
ColorAlphaSlider,
|
|
370
|
+
ColorSwatch,
|
|
371
|
+
ColorPreview,
|
|
372
|
+
ColorValueInput,
|
|
373
|
+
ColorSavedColors,
|
|
374
|
+
ColorEyeDropper,
|
|
375
|
+
useColorPicker,
|
|
376
|
+
parseColor,
|
|
377
|
+
getColorChannels,
|
|
378
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export * from "./components/ui/chat-divider";
|
|
|
28
28
|
export * from "./components/ui/checkbox";
|
|
29
29
|
export * from "./components/ui/checkbox-group";
|
|
30
30
|
export * from "./components/ui/collapsible";
|
|
31
|
+
export * from "./components/ui/color-picker";
|
|
31
32
|
export * from "./components/ui/combobox";
|
|
32
33
|
export * from "./components/ui/command";
|
|
33
34
|
export * from "./components/ui/confetti";
|