@djangocfg/widget-visual 0.1.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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -0
  3. package/package.json +106 -0
  4. package/src/design/ColorPalette/ColorPalette.tsx +129 -0
  5. package/src/design/ColorPalette/README.md +34 -0
  6. package/src/design/ColorPalette/color-palette.stories.tsx +95 -0
  7. package/src/design/ColorPalette/components/Swatch.tsx +102 -0
  8. package/src/design/ColorPalette/hooks/useCopyToClipboard.ts +41 -0
  9. package/src/design/ColorPalette/index.ts +12 -0
  10. package/src/design/ColorPalette/lazy.tsx +21 -0
  11. package/src/design/ColorPalette/types.ts +63 -0
  12. package/src/design/ColorPalette/utils.ts +114 -0
  13. package/src/design/ColorPicker/ColorPicker.tsx +257 -0
  14. package/src/design/ColorPicker/color-picker.stories.tsx +113 -0
  15. package/src/design/ColorPicker/context/ColorPickerContext.tsx +28 -0
  16. package/src/design/ColorPicker/context/ColorPickerStore.tsx +166 -0
  17. package/src/design/ColorPicker/context/index.ts +2 -0
  18. package/src/design/ColorPicker/index.ts +15 -0
  19. package/src/design/ColorPicker/lazy.tsx +24 -0
  20. package/src/design/ColorPicker/lib/color-utils.ts +323 -0
  21. package/src/design/ColorPicker/parts/ColorPickerAlphaSlider.tsx +72 -0
  22. package/src/design/ColorPicker/parts/ColorPickerArea.tsx +155 -0
  23. package/src/design/ColorPicker/parts/ColorPickerEyeDropper.tsx +73 -0
  24. package/src/design/ColorPicker/parts/ColorPickerFormatSelect.tsx +64 -0
  25. package/src/design/ColorPicker/parts/ColorPickerHueSlider.tsx +64 -0
  26. package/src/design/ColorPicker/parts/ColorPickerInput.tsx +512 -0
  27. package/src/design/ColorPicker/parts/ColorPickerSwatch.tsx +63 -0
  28. package/src/design/ColorPicker/parts/index.ts +7 -0
  29. package/src/design/ColorPicker/types.ts +77 -0
  30. package/src/gallery/components/Gallery.tsx +182 -0
  31. package/src/gallery/components/compact/GalleryCompact.tsx +396 -0
  32. package/src/gallery/components/compact/index.ts +1 -0
  33. package/src/gallery/components/index.ts +21 -0
  34. package/src/gallery/components/lightbox/GalleryLightbox.tsx +426 -0
  35. package/src/gallery/components/lightbox/index.ts +1 -0
  36. package/src/gallery/components/media/GalleryImage.tsx +105 -0
  37. package/src/gallery/components/media/GalleryMedia.tsx +70 -0
  38. package/src/gallery/components/media/GalleryVideo.tsx +241 -0
  39. package/src/gallery/components/media/index.ts +3 -0
  40. package/src/gallery/components/preview/GalleryCarousel.tsx +374 -0
  41. package/src/gallery/components/preview/GalleryGrid.tsx +519 -0
  42. package/src/gallery/components/preview/index.ts +2 -0
  43. package/src/gallery/components/shared/ImageSpinner.tsx +37 -0
  44. package/src/gallery/components/shared/index.ts +1 -0
  45. package/src/gallery/components/thumbnails/GalleryThumbnails.tsx +198 -0
  46. package/src/gallery/components/thumbnails/GalleryThumbnailsVirtual.tsx +164 -0
  47. package/src/gallery/components/thumbnails/index.ts +2 -0
  48. package/src/gallery/gallery.stories.tsx +182 -0
  49. package/src/gallery/hooks/index.ts +23 -0
  50. package/src/gallery/hooks/useGallery.ts +138 -0
  51. package/src/gallery/hooks/useImageDimensions.ts +224 -0
  52. package/src/gallery/hooks/usePinchZoom.ts +239 -0
  53. package/src/gallery/hooks/usePreloadImages.ts +119 -0
  54. package/src/gallery/hooks/useSwipe.ts +87 -0
  55. package/src/gallery/hooks/useVirtualList.ts +129 -0
  56. package/src/gallery/hooks/useZoom.ts +321 -0
  57. package/src/gallery/index.ts +66 -0
  58. package/src/gallery/thumbnails.stories.tsx +127 -0
  59. package/src/gallery/types.ts +185 -0
  60. package/src/gallery/utils/imageAnalysis.ts +52 -0
  61. package/src/gallery/utils/index.ts +16 -0
  62. package/src/gallery/utils/normalizeUrl.ts +31 -0
  63. package/src/index.ts +18 -0
  64. package/src/lottie/LottiePlayer.client.tsx +319 -0
  65. package/src/lottie/index.tsx +66 -0
  66. package/src/lottie/lazy.tsx +65 -0
  67. package/src/lottie/lottie-player.stories.tsx +173 -0
  68. package/src/lottie/types.ts +138 -0
  69. package/src/lottie/useLottie.ts +187 -0
  70. package/src/lottie/usePrefersReducedMotion.ts +46 -0
  71. package/src/marquee/Marquee.tsx +637 -0
  72. package/src/marquee/index.ts +7 -0
  73. package/src/marquee/lazy.tsx +14 -0
  74. package/src/marquee/marquee.stories.tsx +118 -0
  75. package/src/marquee/types.ts +40 -0
  76. package/src/qrcode/QRCode.tsx +468 -0
  77. package/src/qrcode/index.ts +10 -0
  78. package/src/qrcode/lazy.tsx +19 -0
  79. package/src/qrcode/qr-code.stories.tsx +102 -0
  80. package/src/qrcode/types.ts +57 -0
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Slider } from '@djangocfg/ui-core/components';
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+ import { useColorPickerContext } from '../context/ColorPickerContext';
7
+ import { useStoreContext, useStore } from '../context/ColorPickerStore';
8
+ import { hsvToRgb } from '../lib/color-utils';
9
+ import type { HSVColorValue } from '../types';
10
+
11
+ const HUE_SLIDER_NAME = 'ColorPickerHueSlider';
12
+
13
+ export function ColorPickerHueSlider(props: React.ComponentProps<typeof Slider>) {
14
+ const { className, ...sliderProps } = props;
15
+
16
+ const context = useColorPickerContext(HUE_SLIDER_NAME);
17
+ const store = useStoreContext(HUE_SLIDER_NAME);
18
+
19
+ const hsv = useStore((state) => state.hsv);
20
+
21
+ const onValueChange = React.useCallback(
22
+ (values: number[]) => {
23
+ const newHsv: HSVColorValue = {
24
+ h: values[0] ?? 0,
25
+ s: hsv?.s ?? 0,
26
+ v: hsv?.v ?? 0,
27
+ a: hsv?.a ?? 1,
28
+ };
29
+ store.setHsv(newHsv);
30
+ store.setColor(hsvToRgb(newHsv));
31
+ },
32
+ [hsv, store],
33
+ );
34
+
35
+ return (
36
+ <div
37
+ className={cn(
38
+ 'relative flex w-full touch-none select-none items-center',
39
+ className,
40
+ )}
41
+ >
42
+ {/* Hue gradient background */}
43
+ <div
44
+ className="pointer-events-none absolute inset-x-0 h-3 rounded-full"
45
+ style={{
46
+ background:
47
+ 'linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)',
48
+ }}
49
+ />
50
+ <Slider
51
+ data-slot="color-picker-hue-slider"
52
+ {...sliderProps}
53
+ max={360}
54
+ step={1}
55
+ className="relative z-10 w-full"
56
+ value={[hsv?.h ?? 0]}
57
+ onValueChange={onValueChange}
58
+ disabled={context.disabled}
59
+ />
60
+ </div>
61
+ );
62
+ }
63
+
64
+ ColorPickerHueSlider.displayName = HUE_SLIDER_NAME;
@@ -0,0 +1,512 @@
1
+ 'use client';
2
+
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+ import * as React from 'react';
5
+ import { Input } from '@djangocfg/ui-core/components';
6
+ import { cn } from '@djangocfg/ui-core/lib';
7
+ import { useColorPickerContext } from '../context/ColorPickerContext';
8
+ import { useStoreContext, useStore } from '../context/ColorPickerStore';
9
+ import {
10
+ rgbToHex,
11
+ rgbToHsv,
12
+ rgbToHsl,
13
+ hslToRgb,
14
+ parseColorString,
15
+ } from '../lib/color-utils';
16
+ import type { ColorValue, ColorFormat, ColorPickerInputProps } from '../types';
17
+
18
+ const INPUT_NAME = 'ColorPickerInput';
19
+
20
+ const inputGroupItemVariants = cva(
21
+ 'h-8 [-moz-appearance:textfield] focus-visible:z-10 focus-visible:ring-1 [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none',
22
+ {
23
+ variants: {
24
+ position: {
25
+ first: 'rounded-e-none',
26
+ middle: '-ms-px rounded-none border-l-0',
27
+ last: '-ms-px rounded-s-none border-l-0',
28
+ isolated: '',
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ position: 'isolated',
33
+ },
34
+ }
35
+ );
36
+
37
+ function InputGroupItem({
38
+ className,
39
+ position,
40
+ ...props
41
+ }: React.ComponentProps<typeof Input> & VariantProps<typeof inputGroupItemVariants>) {
42
+ return (
43
+ <Input
44
+ data-slot="color-picker-input"
45
+ className={cn(inputGroupItemVariants({ position, className }))}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ export function ColorPickerInput(props: ColorPickerInputProps) {
52
+ const store = useStoreContext(INPUT_NAME);
53
+ const context = useColorPickerContext(INPUT_NAME);
54
+
55
+ const color = useStore((state) => state.color);
56
+ const format = useStore((state) => state.format);
57
+ const hsv = useStore((state) => state.hsv);
58
+
59
+ const onColorChange = React.useCallback(
60
+ (newColor: ColorValue) => {
61
+ const newHsv = rgbToHsv(newColor);
62
+ store.setColor(newColor);
63
+ store.setHsv(newHsv);
64
+ },
65
+ [store],
66
+ );
67
+
68
+ if (format === 'hex') {
69
+ return (
70
+ <HexInput
71
+ color={color}
72
+ onColorChange={onColorChange}
73
+ context={context}
74
+ {...props}
75
+ />
76
+ );
77
+ }
78
+
79
+ if (format === 'rgb') {
80
+ return (
81
+ <RgbInput
82
+ color={color}
83
+ onColorChange={onColorChange}
84
+ context={context}
85
+ {...props}
86
+ />
87
+ );
88
+ }
89
+
90
+ if (format === 'hsl') {
91
+ return (
92
+ <HslInput
93
+ color={color}
94
+ onColorChange={onColorChange}
95
+ context={context}
96
+ {...props}
97
+ />
98
+ );
99
+ }
100
+
101
+ if (format === 'hsb') {
102
+ return (
103
+ <HsbInput
104
+ hsv={hsv}
105
+ onColorChange={onColorChange}
106
+ context={context}
107
+ {...props}
108
+ />
109
+ );
110
+ }
111
+
112
+ return null;
113
+ }
114
+
115
+ ColorPickerInput.displayName = INPUT_NAME;
116
+
117
+ interface FormatInputProps extends ColorPickerInputProps {
118
+ color: ColorValue;
119
+ onColorChange: (color: ColorValue) => void;
120
+ context: ReturnType<typeof useColorPickerContext>;
121
+ }
122
+
123
+ function HexInput(props: FormatInputProps) {
124
+ const {
125
+ color,
126
+ onColorChange,
127
+ context,
128
+ withoutAlpha,
129
+ className,
130
+ ...inputProps
131
+ } = props;
132
+
133
+ const hexValue = rgbToHex(color);
134
+ const alphaValue = Math.round((color?.a ?? 1) * 100);
135
+
136
+ const onHexChange = React.useCallback(
137
+ (event: React.ChangeEvent<HTMLInputElement>) => {
138
+ const value = event.target.value;
139
+ const parsedColor = parseColorString(value);
140
+ if (parsedColor) {
141
+ onColorChange({ ...parsedColor, a: color?.a ?? 1 });
142
+ }
143
+ },
144
+ [color, onColorChange],
145
+ );
146
+
147
+ const onAlphaChange = React.useCallback(
148
+ (event: React.ChangeEvent<HTMLInputElement>) => {
149
+ const value = Number.parseInt(event.target.value, 10);
150
+ if (!Number.isNaN(value) && value >= 0 && value <= 100) {
151
+ onColorChange({ ...color, a: value / 100 });
152
+ }
153
+ },
154
+ [color, onColorChange],
155
+ );
156
+
157
+ if (withoutAlpha) {
158
+ return (
159
+ <InputGroupItem
160
+ aria-label="Hex color value"
161
+ position="isolated"
162
+ {...inputProps}
163
+ placeholder="#000000"
164
+ className={cn('font-mono', className)}
165
+ value={hexValue}
166
+ onChange={onHexChange}
167
+ disabled={context.disabled}
168
+ />
169
+ );
170
+ }
171
+
172
+ return (
173
+ <div
174
+ data-slot="color-picker-input-wrapper"
175
+ className={cn('flex items-center', className)}
176
+ >
177
+ <InputGroupItem
178
+ aria-label="Hex color value"
179
+ position="first"
180
+ {...inputProps}
181
+ placeholder="#000000"
182
+ className="flex-1 font-mono"
183
+ value={hexValue}
184
+ onChange={onHexChange}
185
+ disabled={context.disabled}
186
+ />
187
+ <InputGroupItem
188
+ aria-label="Alpha transparency percentage"
189
+ position="last"
190
+ {...inputProps}
191
+ placeholder="100"
192
+ inputMode="numeric"
193
+ pattern="[0-9]*"
194
+ min="0"
195
+ max="100"
196
+ className="w-14"
197
+ value={alphaValue}
198
+ onChange={onAlphaChange}
199
+ disabled={context.disabled}
200
+ />
201
+ </div>
202
+ );
203
+ }
204
+
205
+ function RgbInput(props: FormatInputProps) {
206
+ const {
207
+ color,
208
+ onColorChange,
209
+ context,
210
+ withoutAlpha,
211
+ className,
212
+ ...inputProps
213
+ } = props;
214
+
215
+ const rValue = Math.round(color?.r ?? 0);
216
+ const gValue = Math.round(color?.g ?? 0);
217
+ const bValue = Math.round(color?.b ?? 0);
218
+ const alphaValue = Math.round((color?.a ?? 1) * 100);
219
+
220
+ const onChannelChange = React.useCallback(
221
+ (channel: 'r' | 'g' | 'b' | 'a', max: number, isAlpha = false) =>
222
+ (event: React.ChangeEvent<HTMLInputElement>) => {
223
+ const value = Number.parseInt(event.target.value, 10);
224
+ if (!Number.isNaN(value) && value >= 0 && value <= max) {
225
+ const newValue = isAlpha ? value / 100 : value;
226
+ onColorChange({ ...color, [channel]: newValue });
227
+ }
228
+ },
229
+ [color, onColorChange],
230
+ );
231
+
232
+ return (
233
+ <div
234
+ data-slot="color-picker-input-wrapper"
235
+ className={cn('flex items-center', className)}
236
+ >
237
+ <InputGroupItem
238
+ aria-label="Red color component (0-255)"
239
+ position="first"
240
+ {...inputProps}
241
+ placeholder="0"
242
+ inputMode="numeric"
243
+ pattern="[0-9]*"
244
+ min="0"
245
+ max="255"
246
+ className="w-14"
247
+ value={rValue}
248
+ onChange={onChannelChange('r', 255)}
249
+ disabled={context.disabled}
250
+ />
251
+ <InputGroupItem
252
+ aria-label="Green color component (0-255)"
253
+ position="middle"
254
+ {...inputProps}
255
+ placeholder="0"
256
+ inputMode="numeric"
257
+ pattern="[0-9]*"
258
+ min="0"
259
+ max="255"
260
+ className="w-14"
261
+ value={gValue}
262
+ onChange={onChannelChange('g', 255)}
263
+ disabled={context.disabled}
264
+ />
265
+ <InputGroupItem
266
+ aria-label="Blue color component (0-255)"
267
+ position={withoutAlpha ? 'last' : 'middle'}
268
+ {...inputProps}
269
+ placeholder="0"
270
+ inputMode="numeric"
271
+ pattern="[0-9]*"
272
+ min="0"
273
+ max="255"
274
+ className="w-14"
275
+ value={bValue}
276
+ onChange={onChannelChange('b', 255)}
277
+ disabled={context.disabled}
278
+ />
279
+ {!withoutAlpha && (
280
+ <InputGroupItem
281
+ aria-label="Alpha transparency percentage"
282
+ position="last"
283
+ {...inputProps}
284
+ placeholder="100"
285
+ inputMode="numeric"
286
+ pattern="[0-9]*"
287
+ min="0"
288
+ max="100"
289
+ className="w-14"
290
+ value={alphaValue}
291
+ onChange={onChannelChange('a', 100, true)}
292
+ disabled={context.disabled}
293
+ />
294
+ )}
295
+ </div>
296
+ );
297
+ }
298
+
299
+ function HslInput(props: FormatInputProps) {
300
+ const {
301
+ color,
302
+ onColorChange,
303
+ context,
304
+ withoutAlpha,
305
+ className,
306
+ ...inputProps
307
+ } = props;
308
+
309
+ const hsl = React.useMemo(() => rgbToHsl(color), [color]);
310
+ const alphaValue = Math.round((color?.a ?? 1) * 100);
311
+
312
+ const onHslChannelChange = React.useCallback(
313
+ (channel: 'h' | 's' | 'l', max: number) =>
314
+ (event: React.ChangeEvent<HTMLInputElement>) => {
315
+ const value = Number.parseInt(event.target.value, 10);
316
+ if (!Number.isNaN(value) && value >= 0 && value <= max) {
317
+ const newHsl = { ...hsl, [channel]: value };
318
+ const newColor = hslToRgb(newHsl, color?.a ?? 1);
319
+ onColorChange(newColor);
320
+ }
321
+ },
322
+ [hsl, color, onColorChange],
323
+ );
324
+
325
+ const onAlphaChange = React.useCallback(
326
+ (event: React.ChangeEvent<HTMLInputElement>) => {
327
+ const value = Number.parseInt(event.target.value, 10);
328
+ if (!Number.isNaN(value) && value >= 0 && value <= 100) {
329
+ onColorChange({ ...color, a: value / 100 });
330
+ }
331
+ },
332
+ [color, onColorChange],
333
+ );
334
+
335
+ return (
336
+ <div
337
+ data-slot="color-picker-input-wrapper"
338
+ className={cn('flex items-center', className)}
339
+ >
340
+ <InputGroupItem
341
+ aria-label="Hue degree (0-360)"
342
+ position="first"
343
+ {...inputProps}
344
+ placeholder="0"
345
+ inputMode="numeric"
346
+ pattern="[0-9]*"
347
+ min="0"
348
+ max="360"
349
+ className="w-14"
350
+ value={hsl.h}
351
+ onChange={onHslChannelChange('h', 360)}
352
+ disabled={context.disabled}
353
+ />
354
+ <InputGroupItem
355
+ aria-label="Saturation percentage (0-100)"
356
+ position="middle"
357
+ {...inputProps}
358
+ placeholder="0"
359
+ inputMode="numeric"
360
+ pattern="[0-9]*"
361
+ min="0"
362
+ max="100"
363
+ className="w-14"
364
+ value={hsl.s}
365
+ onChange={onHslChannelChange('s', 100)}
366
+ disabled={context.disabled}
367
+ />
368
+ <InputGroupItem
369
+ aria-label="Lightness percentage (0-100)"
370
+ position={withoutAlpha ? 'last' : 'middle'}
371
+ {...inputProps}
372
+ placeholder="0"
373
+ inputMode="numeric"
374
+ pattern="[0-9]*"
375
+ min="0"
376
+ max="100"
377
+ className="w-14"
378
+ value={hsl.l}
379
+ onChange={onHslChannelChange('l', 100)}
380
+ disabled={context.disabled}
381
+ />
382
+ {!withoutAlpha && (
383
+ <InputGroupItem
384
+ aria-label="Alpha transparency percentage"
385
+ position="last"
386
+ {...inputProps}
387
+ placeholder="100"
388
+ inputMode="numeric"
389
+ pattern="[0-9]*"
390
+ min="0"
391
+ max="100"
392
+ className="w-14"
393
+ value={alphaValue}
394
+ onChange={onAlphaChange}
395
+ disabled={context.disabled}
396
+ />
397
+ )}
398
+ </div>
399
+ );
400
+ }
401
+
402
+ interface HsbInputProps extends Omit<FormatInputProps, 'color'> {
403
+ hsv: import('../types').HSVColorValue;
404
+ }
405
+
406
+ function HsbInput(props: HsbInputProps) {
407
+ const {
408
+ hsv,
409
+ onColorChange,
410
+ context,
411
+ withoutAlpha,
412
+ className,
413
+ ...inputProps
414
+ } = props;
415
+
416
+ const alphaValue = Math.round((hsv?.a ?? 1) * 100);
417
+
418
+ const onHsvChannelChange = React.useCallback(
419
+ (channel: 'h' | 's' | 'v', max: number) =>
420
+ (event: React.ChangeEvent<HTMLInputElement>) => {
421
+ const value = Number.parseInt(event.target.value, 10);
422
+ if (!Number.isNaN(value) && value >= 0 && value <= max) {
423
+ const newHsv = { ...hsv, [channel]: value };
424
+ const newColor = hsvToRgb(newHsv);
425
+ onColorChange(newColor);
426
+ }
427
+ },
428
+ [hsv, onColorChange],
429
+ );
430
+
431
+ const onAlphaChange = React.useCallback(
432
+ (event: React.ChangeEvent<HTMLInputElement>) => {
433
+ const value = Number.parseInt(event.target.value, 10);
434
+ if (!Number.isNaN(value) && value >= 0 && value <= 100) {
435
+ const currentColor = hsvToRgb(hsv);
436
+ onColorChange({ ...currentColor, a: value / 100 });
437
+ }
438
+ },
439
+ [hsv, onColorChange],
440
+ );
441
+
442
+ return (
443
+ <div
444
+ data-slot="color-picker-input-wrapper"
445
+ className={cn('flex items-center', className)}
446
+ >
447
+ <InputGroupItem
448
+ aria-label="Hue degree (0-360)"
449
+ position="first"
450
+ {...inputProps}
451
+ placeholder="0"
452
+ inputMode="numeric"
453
+ pattern="[0-9]*"
454
+ min="0"
455
+ max="360"
456
+ className="w-14"
457
+ value={hsv?.h ?? 0}
458
+ onChange={onHsvChannelChange('h', 360)}
459
+ disabled={context.disabled}
460
+ />
461
+ <InputGroupItem
462
+ aria-label="Saturation percentage (0-100)"
463
+ position="middle"
464
+ {...inputProps}
465
+ placeholder="0"
466
+ inputMode="numeric"
467
+ pattern="[0-9]*"
468
+ min="0"
469
+ max="100"
470
+ className="w-14"
471
+ value={hsv?.s ?? 0}
472
+ onChange={onHsvChannelChange('s', 100)}
473
+ disabled={context.disabled}
474
+ />
475
+ <InputGroupItem
476
+ aria-label="Brightness percentage (0-100)"
477
+ position={withoutAlpha ? 'last' : 'middle'}
478
+ {...inputProps}
479
+ placeholder="0"
480
+ inputMode="numeric"
481
+ pattern="[0-9]*"
482
+ min="0"
483
+ max="100"
484
+ className="w-14"
485
+ value={hsv?.v ?? 0}
486
+ onChange={onHsvChannelChange('v', 100)}
487
+ disabled={context.disabled}
488
+ />
489
+ {!withoutAlpha && (
490
+ <InputGroupItem
491
+ aria-label="Alpha transparency percentage"
492
+ position="last"
493
+ {...inputProps}
494
+ placeholder="100"
495
+ inputMode="numeric"
496
+ pattern="[0-9]*"
497
+ min="0"
498
+ max="100"
499
+ className="w-14"
500
+ value={alphaValue}
501
+ onChange={onAlphaChange}
502
+ disabled={context.disabled}
503
+ />
504
+ )}
505
+ </div>
506
+ );
507
+ }
508
+
509
+ function hsvToRgb(hsv: import('../types').HSVColorValue): ColorValue {
510
+ const { hsvToRgb: fn } = require('../lib/color-utils');
511
+ return fn(hsv);
512
+ }
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { cn } from '@djangocfg/ui-core/lib';
5
+ import { useColorPickerContext } from '../context/ColorPickerContext';
6
+ import { useStore } from '../context/ColorPickerStore';
7
+ import { colorToString } from '../lib/color-utils';
8
+
9
+ const SWATCH_NAME = 'ColorPickerSwatch';
10
+
11
+ export function ColorPickerSwatch(props: React.ComponentProps<'div'> & { asChild?: boolean }) {
12
+ const { className, ...swatchProps } = props;
13
+
14
+ const context = useColorPickerContext(SWATCH_NAME);
15
+
16
+ const color = useStore((state) => state.color);
17
+ const format = useStore((state) => state.format);
18
+
19
+ const backgroundStyle = React.useMemo(() => {
20
+ if (!color) {
21
+ return {
22
+ background:
23
+ 'linear-gradient(to bottom right, transparent calc(50% - 1px), hsl(var(--destructive)) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) no-repeat',
24
+ };
25
+ }
26
+
27
+ const colorString = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
28
+
29
+ if (color.a < 1) {
30
+ return {
31
+ background: `linear-gradient(${colorString}, ${colorString}), repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0% 50% / 8px 8px`,
32
+ };
33
+ }
34
+
35
+ return {
36
+ backgroundColor: colorString,
37
+ };
38
+ }, [color]);
39
+
40
+ const ariaLabel = !color
41
+ ? 'No color selected'
42
+ : `Current color: ${colorToString(color, format)}`;
43
+
44
+ return (
45
+ <div
46
+ role="img"
47
+ aria-label={ariaLabel}
48
+ data-slot="color-picker-swatch"
49
+ {...swatchProps}
50
+ className={cn(
51
+ 'box-border size-8 rounded-sm border shadow-sm',
52
+ context.disabled && 'opacity-50',
53
+ className,
54
+ )}
55
+ style={{
56
+ ...backgroundStyle,
57
+ forcedColorAdjust: 'none',
58
+ }}
59
+ />
60
+ );
61
+ }
62
+
63
+ ColorPickerSwatch.displayName = SWATCH_NAME;
@@ -0,0 +1,7 @@
1
+ export { ColorPickerArea } from './ColorPickerArea';
2
+ export { ColorPickerHueSlider } from './ColorPickerHueSlider';
3
+ export { ColorPickerAlphaSlider } from './ColorPickerAlphaSlider';
4
+ export { ColorPickerSwatch } from './ColorPickerSwatch';
5
+ export { ColorPickerEyeDropper } from './ColorPickerEyeDropper';
6
+ export { ColorPickerFormatSelect } from './ColorPickerFormatSelect';
7
+ export { ColorPickerInput } from './ColorPickerInput';