@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.
- package/LICENSE +21 -0
- package/README.md +41 -0
- package/package.json +106 -0
- package/src/design/ColorPalette/ColorPalette.tsx +129 -0
- package/src/design/ColorPalette/README.md +34 -0
- package/src/design/ColorPalette/color-palette.stories.tsx +95 -0
- package/src/design/ColorPalette/components/Swatch.tsx +102 -0
- package/src/design/ColorPalette/hooks/useCopyToClipboard.ts +41 -0
- package/src/design/ColorPalette/index.ts +12 -0
- package/src/design/ColorPalette/lazy.tsx +21 -0
- package/src/design/ColorPalette/types.ts +63 -0
- package/src/design/ColorPalette/utils.ts +114 -0
- package/src/design/ColorPicker/ColorPicker.tsx +257 -0
- package/src/design/ColorPicker/color-picker.stories.tsx +113 -0
- package/src/design/ColorPicker/context/ColorPickerContext.tsx +28 -0
- package/src/design/ColorPicker/context/ColorPickerStore.tsx +166 -0
- package/src/design/ColorPicker/context/index.ts +2 -0
- package/src/design/ColorPicker/index.ts +15 -0
- package/src/design/ColorPicker/lazy.tsx +24 -0
- package/src/design/ColorPicker/lib/color-utils.ts +323 -0
- package/src/design/ColorPicker/parts/ColorPickerAlphaSlider.tsx +72 -0
- package/src/design/ColorPicker/parts/ColorPickerArea.tsx +155 -0
- package/src/design/ColorPicker/parts/ColorPickerEyeDropper.tsx +73 -0
- package/src/design/ColorPicker/parts/ColorPickerFormatSelect.tsx +64 -0
- package/src/design/ColorPicker/parts/ColorPickerHueSlider.tsx +64 -0
- package/src/design/ColorPicker/parts/ColorPickerInput.tsx +512 -0
- package/src/design/ColorPicker/parts/ColorPickerSwatch.tsx +63 -0
- package/src/design/ColorPicker/parts/index.ts +7 -0
- package/src/design/ColorPicker/types.ts +77 -0
- package/src/gallery/components/Gallery.tsx +182 -0
- package/src/gallery/components/compact/GalleryCompact.tsx +396 -0
- package/src/gallery/components/compact/index.ts +1 -0
- package/src/gallery/components/index.ts +21 -0
- package/src/gallery/components/lightbox/GalleryLightbox.tsx +426 -0
- package/src/gallery/components/lightbox/index.ts +1 -0
- package/src/gallery/components/media/GalleryImage.tsx +105 -0
- package/src/gallery/components/media/GalleryMedia.tsx +70 -0
- package/src/gallery/components/media/GalleryVideo.tsx +241 -0
- package/src/gallery/components/media/index.ts +3 -0
- package/src/gallery/components/preview/GalleryCarousel.tsx +374 -0
- package/src/gallery/components/preview/GalleryGrid.tsx +519 -0
- package/src/gallery/components/preview/index.ts +2 -0
- package/src/gallery/components/shared/ImageSpinner.tsx +37 -0
- package/src/gallery/components/shared/index.ts +1 -0
- package/src/gallery/components/thumbnails/GalleryThumbnails.tsx +198 -0
- package/src/gallery/components/thumbnails/GalleryThumbnailsVirtual.tsx +164 -0
- package/src/gallery/components/thumbnails/index.ts +2 -0
- package/src/gallery/gallery.stories.tsx +182 -0
- package/src/gallery/hooks/index.ts +23 -0
- package/src/gallery/hooks/useGallery.ts +138 -0
- package/src/gallery/hooks/useImageDimensions.ts +224 -0
- package/src/gallery/hooks/usePinchZoom.ts +239 -0
- package/src/gallery/hooks/usePreloadImages.ts +119 -0
- package/src/gallery/hooks/useSwipe.ts +87 -0
- package/src/gallery/hooks/useVirtualList.ts +129 -0
- package/src/gallery/hooks/useZoom.ts +321 -0
- package/src/gallery/index.ts +66 -0
- package/src/gallery/thumbnails.stories.tsx +127 -0
- package/src/gallery/types.ts +185 -0
- package/src/gallery/utils/imageAnalysis.ts +52 -0
- package/src/gallery/utils/index.ts +16 -0
- package/src/gallery/utils/normalizeUrl.ts +31 -0
- package/src/index.ts +18 -0
- package/src/lottie/LottiePlayer.client.tsx +319 -0
- package/src/lottie/index.tsx +66 -0
- package/src/lottie/lazy.tsx +65 -0
- package/src/lottie/lottie-player.stories.tsx +173 -0
- package/src/lottie/types.ts +138 -0
- package/src/lottie/useLottie.ts +187 -0
- package/src/lottie/usePrefersReducedMotion.ts +46 -0
- package/src/marquee/Marquee.tsx +637 -0
- package/src/marquee/index.ts +7 -0
- package/src/marquee/lazy.tsx +14 -0
- package/src/marquee/marquee.stories.tsx +118 -0
- package/src/marquee/types.ts +40 -0
- package/src/qrcode/QRCode.tsx +468 -0
- package/src/qrcode/index.ts +10 -0
- package/src/qrcode/lazy.tsx +19 -0
- package/src/qrcode/qr-code.stories.tsx +102 -0
- package/src/qrcode/types.ts +57 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createLazyComponent } from '@djangocfg/widget-kit/lazy';
|
|
4
|
+
import type { ColorPickerProps } from './types';
|
|
5
|
+
|
|
6
|
+
export const LazyColorPicker = createLazyComponent<ColorPickerProps>(
|
|
7
|
+
() => import('./ColorPicker').then((mod) => ({ default: mod.ColorPicker })),
|
|
8
|
+
{
|
|
9
|
+
displayName: 'LazyColorPicker',
|
|
10
|
+
fallback: (
|
|
11
|
+
<div className="flex w-[340px] flex-col gap-4 p-4">
|
|
12
|
+
<div className="h-40 w-full animate-pulse rounded-sm bg-muted" />
|
|
13
|
+
<div className="h-3 w-full animate-pulse rounded-full bg-muted" />
|
|
14
|
+
<div className="h-3 w-full animate-pulse rounded-full bg-muted" />
|
|
15
|
+
<div className="flex gap-2">
|
|
16
|
+
<div className="h-8 w-8 animate-pulse rounded-sm bg-muted" />
|
|
17
|
+
<div className="h-8 flex-1 animate-pulse rounded-sm bg-muted" />
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
),
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export type { ColorPickerProps } from './types';
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import type { ColorValue, HSVColorValue, ColorFormat } from '../types';
|
|
2
|
+
|
|
3
|
+
export function hexToRgb(hex: string, alpha?: number): ColorValue {
|
|
4
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
5
|
+
return result
|
|
6
|
+
? {
|
|
7
|
+
r: Number.parseInt(result[1] ?? '0', 16),
|
|
8
|
+
g: Number.parseInt(result[2] ?? '0', 16),
|
|
9
|
+
b: Number.parseInt(result[3] ?? '0', 16),
|
|
10
|
+
a: alpha ?? 1,
|
|
11
|
+
}
|
|
12
|
+
: { r: 0, g: 0, b: 0, a: alpha ?? 1 };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function rgbToHex(color: ColorValue): string {
|
|
16
|
+
const toHex = (n: number) => {
|
|
17
|
+
const hex = Math.round(n).toString(16);
|
|
18
|
+
return hex.length === 1 ? `0${hex}` : hex;
|
|
19
|
+
};
|
|
20
|
+
return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function rgbToHsv(color: ColorValue): HSVColorValue {
|
|
24
|
+
const r = color.r / 255;
|
|
25
|
+
const g = color.g / 255;
|
|
26
|
+
const b = color.b / 255;
|
|
27
|
+
|
|
28
|
+
const max = Math.max(r, g, b);
|
|
29
|
+
const min = Math.min(r, g, b);
|
|
30
|
+
const diff = max - min;
|
|
31
|
+
|
|
32
|
+
let h = 0;
|
|
33
|
+
if (diff !== 0) {
|
|
34
|
+
switch (max) {
|
|
35
|
+
case r:
|
|
36
|
+
h = ((g - b) / diff) % 6;
|
|
37
|
+
break;
|
|
38
|
+
case g:
|
|
39
|
+
h = (b - r) / diff + 2;
|
|
40
|
+
break;
|
|
41
|
+
case b:
|
|
42
|
+
h = (r - g) / diff + 4;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
h = Math.round(h * 60);
|
|
47
|
+
if (h < 0) h += 360;
|
|
48
|
+
|
|
49
|
+
const s = max === 0 ? 0 : diff / max;
|
|
50
|
+
const v = max;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
h,
|
|
54
|
+
s: Math.round(s * 100),
|
|
55
|
+
v: Math.round(v * 100),
|
|
56
|
+
a: color.a,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function hsvToRgb(hsv: HSVColorValue): ColorValue {
|
|
61
|
+
const h = hsv.h / 360;
|
|
62
|
+
const s = hsv.s / 100;
|
|
63
|
+
const v = hsv.v / 100;
|
|
64
|
+
|
|
65
|
+
const i = Math.floor(h * 6);
|
|
66
|
+
const f = h * 6 - i;
|
|
67
|
+
const p = v * (1 - s);
|
|
68
|
+
const q = v * (1 - f * s);
|
|
69
|
+
const t = v * (1 - (1 - f) * s);
|
|
70
|
+
|
|
71
|
+
let r: number;
|
|
72
|
+
let g: number;
|
|
73
|
+
let b: number;
|
|
74
|
+
|
|
75
|
+
switch (i % 6) {
|
|
76
|
+
case 0: {
|
|
77
|
+
r = v;
|
|
78
|
+
g = t;
|
|
79
|
+
b = p;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 1: {
|
|
83
|
+
r = q;
|
|
84
|
+
g = v;
|
|
85
|
+
b = p;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case 2: {
|
|
89
|
+
r = p;
|
|
90
|
+
g = v;
|
|
91
|
+
b = t;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case 3: {
|
|
95
|
+
r = p;
|
|
96
|
+
g = q;
|
|
97
|
+
b = v;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case 4: {
|
|
101
|
+
r = t;
|
|
102
|
+
g = p;
|
|
103
|
+
b = v;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 5: {
|
|
107
|
+
r = v;
|
|
108
|
+
g = p;
|
|
109
|
+
b = q;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
default: {
|
|
113
|
+
r = 0;
|
|
114
|
+
g = 0;
|
|
115
|
+
b = 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
r: Math.round(r * 255),
|
|
121
|
+
g: Math.round(g * 255),
|
|
122
|
+
b: Math.round(b * 255),
|
|
123
|
+
a: hsv.a,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function colorToString(color: ColorValue, format: ColorFormat = 'hex'): string {
|
|
128
|
+
switch (format) {
|
|
129
|
+
case 'hex':
|
|
130
|
+
return rgbToHex(color);
|
|
131
|
+
case 'rgb':
|
|
132
|
+
return color.a < 1
|
|
133
|
+
? `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`
|
|
134
|
+
: `rgb(${color.r}, ${color.g}, ${color.b})`;
|
|
135
|
+
case 'hsl': {
|
|
136
|
+
const hsl = rgbToHsl(color);
|
|
137
|
+
return color.a < 1
|
|
138
|
+
? `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${color.a})`
|
|
139
|
+
: `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`;
|
|
140
|
+
}
|
|
141
|
+
case 'hsb': {
|
|
142
|
+
const hsv = rgbToHsv(color);
|
|
143
|
+
return color.a < 1
|
|
144
|
+
? `hsba(${hsv.h}, ${hsv.s}%, ${hsv.v}%, ${color.a})`
|
|
145
|
+
: `hsb(${hsv.h}, ${hsv.s}%, ${hsv.v}%)`;
|
|
146
|
+
}
|
|
147
|
+
default:
|
|
148
|
+
return rgbToHex(color);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function rgbToHsl(color: ColorValue) {
|
|
153
|
+
const r = color.r / 255;
|
|
154
|
+
const g = color.g / 255;
|
|
155
|
+
const b = color.b / 255;
|
|
156
|
+
|
|
157
|
+
const max = Math.max(r, g, b);
|
|
158
|
+
const min = Math.min(r, g, b);
|
|
159
|
+
const diff = max - min;
|
|
160
|
+
const sum = max + min;
|
|
161
|
+
|
|
162
|
+
const l = sum / 2;
|
|
163
|
+
|
|
164
|
+
let h = 0;
|
|
165
|
+
let s = 0;
|
|
166
|
+
|
|
167
|
+
if (diff !== 0) {
|
|
168
|
+
s = l > 0.5 ? diff / (2 - sum) : diff / sum;
|
|
169
|
+
|
|
170
|
+
if (max === r) {
|
|
171
|
+
h = (g - b) / diff + (g < b ? 6 : 0);
|
|
172
|
+
} else if (max === g) {
|
|
173
|
+
h = (b - r) / diff + 2;
|
|
174
|
+
} else if (max === b) {
|
|
175
|
+
h = (r - g) / diff + 4;
|
|
176
|
+
}
|
|
177
|
+
h /= 6;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
h: Math.round(h * 360),
|
|
182
|
+
s: Math.round(s * 100),
|
|
183
|
+
l: Math.round(l * 100),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function hslToRgb(
|
|
188
|
+
hsl: { h: number; s: number; l: number },
|
|
189
|
+
alpha = 1,
|
|
190
|
+
): ColorValue {
|
|
191
|
+
const h = hsl.h / 360;
|
|
192
|
+
const s = hsl.s / 100;
|
|
193
|
+
const l = hsl.l / 100;
|
|
194
|
+
|
|
195
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
196
|
+
const x = c * (1 - Math.abs(((h * 6) % 2) - 1));
|
|
197
|
+
const m = l - c / 2;
|
|
198
|
+
|
|
199
|
+
let r = 0;
|
|
200
|
+
let g = 0;
|
|
201
|
+
let b = 0;
|
|
202
|
+
|
|
203
|
+
if (h >= 0 && h < 1 / 6) {
|
|
204
|
+
r = c;
|
|
205
|
+
g = x;
|
|
206
|
+
b = 0;
|
|
207
|
+
} else if (h >= 1 / 6 && h < 2 / 6) {
|
|
208
|
+
r = x;
|
|
209
|
+
g = c;
|
|
210
|
+
b = 0;
|
|
211
|
+
} else if (h >= 2 / 6 && h < 3 / 6) {
|
|
212
|
+
r = 0;
|
|
213
|
+
g = c;
|
|
214
|
+
b = x;
|
|
215
|
+
} else if (h >= 3 / 6 && h < 4 / 6) {
|
|
216
|
+
r = 0;
|
|
217
|
+
g = x;
|
|
218
|
+
b = c;
|
|
219
|
+
} else if (h >= 4 / 6 && h < 5 / 6) {
|
|
220
|
+
r = x;
|
|
221
|
+
g = 0;
|
|
222
|
+
b = c;
|
|
223
|
+
} else if (h >= 5 / 6 && h < 1) {
|
|
224
|
+
r = c;
|
|
225
|
+
g = 0;
|
|
226
|
+
b = x;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
r: Math.round((r + m) * 255),
|
|
231
|
+
g: Math.round((g + m) * 255),
|
|
232
|
+
b: Math.round((b + m) * 255),
|
|
233
|
+
a: alpha,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function parseColorString(value: string): ColorValue | null {
|
|
238
|
+
const trimmed = value.trim();
|
|
239
|
+
|
|
240
|
+
if (trimmed.startsWith('#')) {
|
|
241
|
+
const hexMatch = trimmed.match(/^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$/);
|
|
242
|
+
if (hexMatch) {
|
|
243
|
+
return hexToRgb(trimmed);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const rgbMatch = trimmed.match(
|
|
248
|
+
/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)$/,
|
|
249
|
+
);
|
|
250
|
+
if (rgbMatch) {
|
|
251
|
+
return {
|
|
252
|
+
r: Number.parseInt(rgbMatch[1] ?? '0', 10),
|
|
253
|
+
g: Number.parseInt(rgbMatch[2] ?? '0', 10),
|
|
254
|
+
b: Number.parseInt(rgbMatch[3] ?? '0', 10),
|
|
255
|
+
a: rgbMatch[4] ? Number.parseFloat(rgbMatch[4]) : 1,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const hslMatch = trimmed.match(
|
|
260
|
+
/^hsla?\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*(?:,\s*([\d.]+))?\s*\)$/,
|
|
261
|
+
);
|
|
262
|
+
if (hslMatch) {
|
|
263
|
+
const h = Number.parseInt(hslMatch[1] ?? '0', 10);
|
|
264
|
+
const s = Number.parseInt(hslMatch[2] ?? '0', 10) / 100;
|
|
265
|
+
const l = Number.parseInt(hslMatch[3] ?? '0', 10) / 100;
|
|
266
|
+
const a = hslMatch[4] ? Number.parseFloat(hslMatch[4]) : 1;
|
|
267
|
+
|
|
268
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
269
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
270
|
+
const m = l - c / 2;
|
|
271
|
+
|
|
272
|
+
let r = 0;
|
|
273
|
+
let g = 0;
|
|
274
|
+
let b = 0;
|
|
275
|
+
|
|
276
|
+
if (h >= 0 && h < 60) {
|
|
277
|
+
r = c;
|
|
278
|
+
g = x;
|
|
279
|
+
b = 0;
|
|
280
|
+
} else if (h >= 60 && h < 120) {
|
|
281
|
+
r = x;
|
|
282
|
+
g = c;
|
|
283
|
+
b = 0;
|
|
284
|
+
} else if (h >= 120 && h < 180) {
|
|
285
|
+
r = 0;
|
|
286
|
+
g = c;
|
|
287
|
+
b = x;
|
|
288
|
+
} else if (h >= 180 && h < 240) {
|
|
289
|
+
r = 0;
|
|
290
|
+
g = x;
|
|
291
|
+
b = c;
|
|
292
|
+
} else if (h >= 240 && h < 300) {
|
|
293
|
+
r = x;
|
|
294
|
+
g = 0;
|
|
295
|
+
b = c;
|
|
296
|
+
} else if (h >= 300 && h < 360) {
|
|
297
|
+
r = c;
|
|
298
|
+
g = 0;
|
|
299
|
+
b = x;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
r: Math.round((r + m) * 255),
|
|
304
|
+
g: Math.round((g + m) * 255),
|
|
305
|
+
b: Math.round((b + m) * 255),
|
|
306
|
+
a,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const hsbMatch = trimmed.match(
|
|
311
|
+
/^hsba?\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*(?:,\s*([\d.]+))?\s*\)$/,
|
|
312
|
+
);
|
|
313
|
+
if (hsbMatch) {
|
|
314
|
+
const h = Number.parseInt(hsbMatch[1] ?? '0', 10);
|
|
315
|
+
const s = Number.parseInt(hsbMatch[2] ?? '0', 10);
|
|
316
|
+
const v = Number.parseInt(hsbMatch[3] ?? '0', 10);
|
|
317
|
+
const a = hsbMatch[4] ? Number.parseFloat(hsbMatch[4]) : 1;
|
|
318
|
+
|
|
319
|
+
return hsvToRgb({ h, s, v, a });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
|
|
9
|
+
const ALPHA_SLIDER_NAME = 'ColorPickerAlphaSlider';
|
|
10
|
+
|
|
11
|
+
export function ColorPickerAlphaSlider(props: React.ComponentProps<typeof Slider>) {
|
|
12
|
+
const { className, ...sliderProps } = props;
|
|
13
|
+
|
|
14
|
+
const context = useColorPickerContext(ALPHA_SLIDER_NAME);
|
|
15
|
+
const store = useStoreContext(ALPHA_SLIDER_NAME);
|
|
16
|
+
|
|
17
|
+
const color = useStore((state) => state.color);
|
|
18
|
+
const hsv = useStore((state) => state.hsv);
|
|
19
|
+
|
|
20
|
+
const onValueChange = React.useCallback(
|
|
21
|
+
(values: number[]) => {
|
|
22
|
+
const alpha = (values[0] ?? 0) / 100;
|
|
23
|
+
const newColor = { ...color, a: alpha };
|
|
24
|
+
const newHsv = { ...hsv, a: alpha };
|
|
25
|
+
store.setColor(newColor);
|
|
26
|
+
store.setHsv(newHsv);
|
|
27
|
+
},
|
|
28
|
+
[color, hsv, store],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const opaqueColor = `rgb(${color?.r ?? 0}, ${color?.g ?? 0}, ${color?.b ?? 0})`;
|
|
32
|
+
const transparentColor = `rgba(${color?.r ?? 0}, ${color?.g ?? 0}, ${color?.b ?? 0}, 0)`;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
className={cn(
|
|
37
|
+
'relative flex w-full touch-none select-none items-center',
|
|
38
|
+
className,
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
{/* Checkerboard background */}
|
|
42
|
+
<div
|
|
43
|
+
className="pointer-events-none absolute inset-x-0 h-3 rounded-full"
|
|
44
|
+
style={{
|
|
45
|
+
background:
|
|
46
|
+
'linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%)',
|
|
47
|
+
backgroundSize: '8px 8px',
|
|
48
|
+
backgroundPosition: '0 0, 0 4px, 4px -4px, -4px 0px',
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
{/* Alpha gradient overlay */}
|
|
52
|
+
<div
|
|
53
|
+
className="pointer-events-none absolute inset-x-0 h-3 rounded-full"
|
|
54
|
+
style={{
|
|
55
|
+
background: `linear-gradient(to right, ${transparentColor}, ${opaqueColor})`,
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
<Slider
|
|
59
|
+
data-slot="color-picker-alpha-slider"
|
|
60
|
+
{...sliderProps}
|
|
61
|
+
max={100}
|
|
62
|
+
step={1}
|
|
63
|
+
disabled={context.disabled}
|
|
64
|
+
className="relative z-10 w-full"
|
|
65
|
+
value={[Math.round((color?.a ?? 1) * 100)]}
|
|
66
|
+
onValueChange={onValueChange}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ColorPickerAlphaSlider.displayName = ALPHA_SLIDER_NAME;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { useComposedRefs } from '@djangocfg/ui-core/lib';
|
|
5
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
6
|
+
import { useColorPickerContext } from '../context/ColorPickerContext';
|
|
7
|
+
import { useStoreContext } from '../context/ColorPickerStore';
|
|
8
|
+
import { useStore } from '../context/ColorPickerStore';
|
|
9
|
+
import { hsvToRgb } from '../lib/color-utils';
|
|
10
|
+
import type { HSVColorValue } from '../types';
|
|
11
|
+
|
|
12
|
+
const AREA_NAME = 'ColorPickerArea';
|
|
13
|
+
|
|
14
|
+
export function ColorPickerArea(props: React.ComponentProps<'div'> & { asChild?: boolean }) {
|
|
15
|
+
const {
|
|
16
|
+
asChild,
|
|
17
|
+
onPointerDown: onPointerDownProp,
|
|
18
|
+
onPointerMove: onPointerMoveProp,
|
|
19
|
+
onPointerUp: onPointerUpProp,
|
|
20
|
+
className,
|
|
21
|
+
ref,
|
|
22
|
+
...areaProps
|
|
23
|
+
} = props;
|
|
24
|
+
|
|
25
|
+
const propsRef = React.useRef({
|
|
26
|
+
onPointerDown: onPointerDownProp,
|
|
27
|
+
onPointerMove: onPointerMoveProp,
|
|
28
|
+
onPointerUp: onPointerUpProp,
|
|
29
|
+
});
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
propsRef.current = {
|
|
32
|
+
onPointerDown: onPointerDownProp,
|
|
33
|
+
onPointerMove: onPointerMoveProp,
|
|
34
|
+
onPointerUp: onPointerUpProp,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const context = useColorPickerContext(AREA_NAME);
|
|
39
|
+
const store = useStoreContext(AREA_NAME);
|
|
40
|
+
|
|
41
|
+
const hsv = useStore((state) => state.hsv);
|
|
42
|
+
|
|
43
|
+
const isDraggingRef = React.useRef(false);
|
|
44
|
+
const areaRef = React.useRef<HTMLDivElement>(null);
|
|
45
|
+
const composedRef = useComposedRefs(ref, areaRef);
|
|
46
|
+
|
|
47
|
+
const updateColorFromPosition = React.useCallback(
|
|
48
|
+
(clientX: number, clientY: number) => {
|
|
49
|
+
if (!areaRef.current) return;
|
|
50
|
+
|
|
51
|
+
const rect = areaRef.current.getBoundingClientRect();
|
|
52
|
+
const x = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
53
|
+
const y = Math.max(
|
|
54
|
+
0,
|
|
55
|
+
Math.min(1, 1 - (clientY - rect.top) / rect.height),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const newHsv: HSVColorValue = {
|
|
59
|
+
h: hsv?.h ?? 0,
|
|
60
|
+
s: Math.round(x * 100),
|
|
61
|
+
v: Math.round(y * 100),
|
|
62
|
+
a: hsv?.a ?? 1,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
store.setHsv(newHsv);
|
|
66
|
+
store.setColor(hsvToRgb(newHsv));
|
|
67
|
+
},
|
|
68
|
+
[hsv, store],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const onPointerDown = React.useCallback(
|
|
72
|
+
(event: React.PointerEvent<HTMLDivElement>) => {
|
|
73
|
+
if (context.disabled) return;
|
|
74
|
+
propsRef.current.onPointerDown?.(event);
|
|
75
|
+
if (event.defaultPrevented) return;
|
|
76
|
+
|
|
77
|
+
isDraggingRef.current = true;
|
|
78
|
+
areaRef.current?.setPointerCapture(event.pointerId);
|
|
79
|
+
updateColorFromPosition(event.clientX, event.clientY);
|
|
80
|
+
},
|
|
81
|
+
[context.disabled, updateColorFromPosition],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const onPointerMove = React.useCallback(
|
|
85
|
+
(event: React.PointerEvent<HTMLDivElement>) => {
|
|
86
|
+
propsRef.current.onPointerMove?.(event);
|
|
87
|
+
if (event.defaultPrevented) return;
|
|
88
|
+
|
|
89
|
+
if (isDraggingRef.current) {
|
|
90
|
+
updateColorFromPosition(event.clientX, event.clientY);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[updateColorFromPosition],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const onPointerUp = React.useCallback(
|
|
97
|
+
(event: React.PointerEvent<HTMLDivElement>) => {
|
|
98
|
+
propsRef.current.onPointerUp?.(event);
|
|
99
|
+
if (event.defaultPrevented) return;
|
|
100
|
+
|
|
101
|
+
isDraggingRef.current = false;
|
|
102
|
+
areaRef.current?.releasePointerCapture(event.pointerId);
|
|
103
|
+
},
|
|
104
|
+
[],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const hue = hsv?.h ?? 0;
|
|
108
|
+
const backgroundHue = hsvToRgb({ h: hue, s: 100, v: 100, a: 1 });
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
data-slot="color-picker-area"
|
|
113
|
+
{...areaProps}
|
|
114
|
+
className={cn(
|
|
115
|
+
'relative h-40 w-full cursor-crosshair touch-none rounded-sm border',
|
|
116
|
+
context.disabled && 'pointer-events-none opacity-50',
|
|
117
|
+
className,
|
|
118
|
+
)}
|
|
119
|
+
ref={composedRef}
|
|
120
|
+
onPointerDown={onPointerDown}
|
|
121
|
+
onPointerMove={onPointerMove}
|
|
122
|
+
onPointerUp={onPointerUp}
|
|
123
|
+
>
|
|
124
|
+
<div className="absolute inset-0 overflow-hidden rounded-sm">
|
|
125
|
+
<div
|
|
126
|
+
className="absolute inset-0"
|
|
127
|
+
style={{
|
|
128
|
+
backgroundColor: `rgb(${backgroundHue.r}, ${backgroundHue.g}, ${backgroundHue.b})`,
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
<div
|
|
132
|
+
className="absolute inset-0"
|
|
133
|
+
style={{
|
|
134
|
+
background: 'linear-gradient(to right, #fff, transparent)',
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
<div
|
|
138
|
+
className="absolute inset-0"
|
|
139
|
+
style={{
|
|
140
|
+
background: 'linear-gradient(to bottom, transparent, #000)',
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
<div
|
|
145
|
+
className="absolute size-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-sm"
|
|
146
|
+
style={{
|
|
147
|
+
left: `${hsv?.s ?? 0}%`,
|
|
148
|
+
top: `${100 - (hsv?.v ?? 0)}%`,
|
|
149
|
+
}}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
ColorPickerArea.displayName = AREA_NAME;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { PipetteIcon } from 'lucide-react';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { Button } from '@djangocfg/ui-core/components';
|
|
6
|
+
import { useColorPickerContext } from '../context/ColorPickerContext';
|
|
7
|
+
import { useStoreContext, useStore } from '../context/ColorPickerStore';
|
|
8
|
+
import { hexToRgb, rgbToHsv } from '../lib/color-utils';
|
|
9
|
+
|
|
10
|
+
const EYE_DROPPER_NAME = 'ColorPickerEyeDropper';
|
|
11
|
+
|
|
12
|
+
interface EyeDropper {
|
|
13
|
+
open: (options?: { signal?: AbortSignal }) => Promise<{ sRGBHex: string }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
interface Window {
|
|
18
|
+
EyeDropper?: {
|
|
19
|
+
new (): EyeDropper;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ColorPickerEyeDropper(props: React.ComponentProps<typeof Button>) {
|
|
25
|
+
const { size: sizeProp, children, disabled, ...buttonProps } = props;
|
|
26
|
+
|
|
27
|
+
const context = useColorPickerContext(EYE_DROPPER_NAME);
|
|
28
|
+
const store = useStoreContext(EYE_DROPPER_NAME);
|
|
29
|
+
|
|
30
|
+
const color = useStore((state) => state.color);
|
|
31
|
+
|
|
32
|
+
const isDisabled = disabled || context.disabled;
|
|
33
|
+
|
|
34
|
+
const onEyeDropper = React.useCallback(async () => {
|
|
35
|
+
if (!window.EyeDropper) return;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const eyeDropper = new window.EyeDropper();
|
|
39
|
+
const result = await eyeDropper.open();
|
|
40
|
+
|
|
41
|
+
if (result.sRGBHex) {
|
|
42
|
+
const currentAlpha = color?.a ?? 1;
|
|
43
|
+
const newColor = hexToRgb(result.sRGBHex, currentAlpha);
|
|
44
|
+
const newHsv = rgbToHsv(newColor);
|
|
45
|
+
store.setColor(newColor);
|
|
46
|
+
store.setHsv(newHsv);
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn('EyeDropper error:', error);
|
|
50
|
+
}
|
|
51
|
+
}, [color, store]);
|
|
52
|
+
|
|
53
|
+
const hasEyeDropper = typeof window !== 'undefined' && !!window.EyeDropper;
|
|
54
|
+
|
|
55
|
+
if (!hasEyeDropper) return null;
|
|
56
|
+
|
|
57
|
+
const size = sizeProp ?? (children ? 'default' : 'icon');
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Button
|
|
61
|
+
data-slot="color-picker-eye-dropper"
|
|
62
|
+
{...buttonProps}
|
|
63
|
+
variant="outline"
|
|
64
|
+
size={size}
|
|
65
|
+
onClick={onEyeDropper}
|
|
66
|
+
disabled={isDisabled}
|
|
67
|
+
>
|
|
68
|
+
{children ?? <PipetteIcon />}
|
|
69
|
+
</Button>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ColorPickerEyeDropper.displayName = EYE_DROPPER_NAME;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Select,
|
|
6
|
+
SelectContent,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectTrigger,
|
|
9
|
+
SelectValue,
|
|
10
|
+
} from '@djangocfg/ui-core/components';
|
|
11
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
12
|
+
import { useColorPickerContext } from '../context/ColorPickerContext';
|
|
13
|
+
import { useStoreContext, useStore } from '../context/ColorPickerStore';
|
|
14
|
+
import { colorFormats } from '../types';
|
|
15
|
+
import type { ColorFormat } from '../types';
|
|
16
|
+
|
|
17
|
+
const FORMAT_SELECT_NAME = 'ColorPickerFormatSelect';
|
|
18
|
+
|
|
19
|
+
export function ColorPickerFormatSelect(
|
|
20
|
+
props: Omit<React.ComponentProps<typeof Select>, 'value' | 'onValueChange'> & {
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
) {
|
|
24
|
+
const { disabled, className, ...selectProps } = props;
|
|
25
|
+
|
|
26
|
+
const context = useColorPickerContext(FORMAT_SELECT_NAME);
|
|
27
|
+
const store = useStoreContext(FORMAT_SELECT_NAME);
|
|
28
|
+
const isDisabled = disabled || context.disabled;
|
|
29
|
+
|
|
30
|
+
const format = useStore((state) => state.format);
|
|
31
|
+
|
|
32
|
+
const onFormatChange = React.useCallback(
|
|
33
|
+
(value: string) => {
|
|
34
|
+
store.setFormat(value as ColorFormat);
|
|
35
|
+
},
|
|
36
|
+
[store],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Select
|
|
41
|
+
data-slot="color-picker-format-select"
|
|
42
|
+
{...selectProps}
|
|
43
|
+
value={format}
|
|
44
|
+
onValueChange={onFormatChange}
|
|
45
|
+
disabled={isDisabled}
|
|
46
|
+
>
|
|
47
|
+
<SelectTrigger
|
|
48
|
+
data-slot="color-picker-format-select-trigger"
|
|
49
|
+
className={cn('h-8 text-xs', className)}
|
|
50
|
+
>
|
|
51
|
+
<SelectValue />
|
|
52
|
+
</SelectTrigger>
|
|
53
|
+
<SelectContent>
|
|
54
|
+
{colorFormats.map((format) => (
|
|
55
|
+
<SelectItem key={format} value={format}>
|
|
56
|
+
{format.toUpperCase()}
|
|
57
|
+
</SelectItem>
|
|
58
|
+
))}
|
|
59
|
+
</SelectContent>
|
|
60
|
+
</Select>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ColorPickerFormatSelect.displayName = FORMAT_SELECT_NAME;
|