@classytic/fluid 0.4.1 → 0.4.2
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.
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
import * as react from "react";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/components/color-picker.d.ts
|
|
6
|
+
type RGBA = [number, number, number, number];
|
|
7
|
+
interface ColorPickerContextValue {
|
|
8
|
+
hue: number;
|
|
9
|
+
saturation: number;
|
|
10
|
+
lightness: number;
|
|
11
|
+
alpha: number;
|
|
12
|
+
setHue: (hue: number) => void;
|
|
13
|
+
setSaturation: (saturation: number) => void;
|
|
14
|
+
setLightness: (lightness: number) => void;
|
|
15
|
+
setAlpha: (alpha: number) => void;
|
|
16
|
+
getCurrentColor: () => RGBA;
|
|
17
|
+
onChange?: (color: RGBA) => void;
|
|
18
|
+
}
|
|
19
|
+
declare function useColorPicker(): ColorPickerContextValue;
|
|
20
|
+
declare function useColorPickerHex(): string;
|
|
21
|
+
interface ColorPickerProps {
|
|
22
|
+
/** Current color value (hex string, e.g. "#ff0000") */
|
|
23
|
+
value?: string;
|
|
24
|
+
/** Default color if value is not provided */
|
|
25
|
+
defaultValue?: string;
|
|
26
|
+
/** Callback with [R, G, B, Alpha(0-1)] when color changes */
|
|
27
|
+
onChange?: (color: RGBA) => void;
|
|
28
|
+
className?: string;
|
|
29
|
+
children?: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
declare function ColorPicker({
|
|
32
|
+
value,
|
|
33
|
+
defaultValue,
|
|
34
|
+
onChange,
|
|
35
|
+
className,
|
|
36
|
+
children
|
|
37
|
+
}: ColorPickerProps): react_jsx_runtime0.JSX.Element;
|
|
38
|
+
interface ColorPickerSelectionProps {
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
declare const ColorPickerSelection: react.NamedExoticComponent<ColorPickerSelectionProps>;
|
|
42
|
+
interface ColorPickerHueProps {
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
declare function ColorPickerHue({
|
|
46
|
+
className
|
|
47
|
+
}: ColorPickerHueProps): react_jsx_runtime0.JSX.Element;
|
|
48
|
+
interface ColorPickerAlphaProps {
|
|
49
|
+
className?: string;
|
|
50
|
+
}
|
|
51
|
+
declare function ColorPickerAlpha({
|
|
52
|
+
className
|
|
53
|
+
}: ColorPickerAlphaProps): react_jsx_runtime0.JSX.Element;
|
|
54
|
+
interface ColorPickerEyeDropperProps {
|
|
55
|
+
className?: string;
|
|
56
|
+
children?: ReactNode;
|
|
57
|
+
}
|
|
58
|
+
declare function ColorPickerEyeDropper({
|
|
59
|
+
className,
|
|
60
|
+
children
|
|
61
|
+
}: ColorPickerEyeDropperProps): react_jsx_runtime0.JSX.Element;
|
|
62
|
+
interface ColorPickerInputProps {
|
|
63
|
+
className?: string;
|
|
64
|
+
/** Show alpha percentage alongside hex */
|
|
65
|
+
showAlpha?: boolean;
|
|
66
|
+
}
|
|
67
|
+
declare function ColorPickerInput({
|
|
68
|
+
className,
|
|
69
|
+
showAlpha
|
|
70
|
+
}: ColorPickerInputProps): react_jsx_runtime0.JSX.Element;
|
|
71
|
+
interface ColorPickerPreviewProps {
|
|
72
|
+
className?: string;
|
|
73
|
+
}
|
|
74
|
+
declare function ColorPickerPreview({
|
|
75
|
+
className
|
|
76
|
+
}: ColorPickerPreviewProps): react_jsx_runtime0.JSX.Element;
|
|
77
|
+
//#endregion
|
|
78
|
+
export { ColorPicker, ColorPickerAlpha, type ColorPickerAlphaProps, ColorPickerEyeDropper, type ColorPickerEyeDropperProps, ColorPickerHue, type ColorPickerHueProps, ColorPickerInput, type ColorPickerInputProps, ColorPickerPreview, type ColorPickerPreviewProps, type ColorPickerProps, ColorPickerSelection, type ColorPickerSelectionProps, type RGBA, useColorPicker, useColorPickerHex };
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { t as cn } from "../utils-DQ5SCVoW.mjs";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { Pipette } from "lucide-react";
|
|
7
|
+
import { Button } from "@/components/ui/button";
|
|
8
|
+
import { Input } from "@/components/ui/input";
|
|
9
|
+
|
|
10
|
+
//#region src/components/color-picker.tsx
|
|
11
|
+
function hexToRgb(hex) {
|
|
12
|
+
const h = hex.replace("#", "");
|
|
13
|
+
const expanded = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
|
14
|
+
const n = parseInt(expanded.slice(0, 6), 16);
|
|
15
|
+
return [
|
|
16
|
+
n >> 16 & 255,
|
|
17
|
+
n >> 8 & 255,
|
|
18
|
+
n & 255
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
function rgbToHsl(r, g, b) {
|
|
22
|
+
r /= 255;
|
|
23
|
+
g /= 255;
|
|
24
|
+
b /= 255;
|
|
25
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
26
|
+
const l = (max + min) / 2;
|
|
27
|
+
if (max === min) return [
|
|
28
|
+
0,
|
|
29
|
+
0,
|
|
30
|
+
l * 100
|
|
31
|
+
];
|
|
32
|
+
const d = max - min;
|
|
33
|
+
const s = l > .5 ? d / (2 - max - min) : d / (max + min);
|
|
34
|
+
let h;
|
|
35
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
36
|
+
else if (max === g) h = ((b - r) / d + 2) / 6;
|
|
37
|
+
else h = ((r - g) / d + 4) / 6;
|
|
38
|
+
return [
|
|
39
|
+
h * 360,
|
|
40
|
+
s * 100,
|
|
41
|
+
l * 100
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function hslToRgb(h, s, l) {
|
|
45
|
+
h /= 360;
|
|
46
|
+
s /= 100;
|
|
47
|
+
l /= 100;
|
|
48
|
+
if (s === 0) {
|
|
49
|
+
const v = Math.round(l * 255);
|
|
50
|
+
return [
|
|
51
|
+
v,
|
|
52
|
+
v,
|
|
53
|
+
v
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
const hue2rgb = (p, q, t) => {
|
|
57
|
+
if (t < 0) t += 1;
|
|
58
|
+
if (t > 1) t -= 1;
|
|
59
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
60
|
+
if (t < 1 / 2) return q;
|
|
61
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
62
|
+
return p;
|
|
63
|
+
};
|
|
64
|
+
const q = l < .5 ? l * (1 + s) : l + s - l * s;
|
|
65
|
+
const p = 2 * l - q;
|
|
66
|
+
return [
|
|
67
|
+
Math.round(hue2rgb(p, q, h + 1 / 3) * 255),
|
|
68
|
+
Math.round(hue2rgb(p, q, h) * 255),
|
|
69
|
+
Math.round(hue2rgb(p, q, h - 1 / 3) * 255)
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
function hslToHex(h, s, l) {
|
|
73
|
+
const [r, g, b] = hslToRgb(h, s, l);
|
|
74
|
+
return "#" + [
|
|
75
|
+
r,
|
|
76
|
+
g,
|
|
77
|
+
b
|
|
78
|
+
].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
79
|
+
}
|
|
80
|
+
function hslToHexa(h, s, l, a) {
|
|
81
|
+
const [r, g, b] = hslToRgb(h, s, l);
|
|
82
|
+
return "#" + [
|
|
83
|
+
r,
|
|
84
|
+
g,
|
|
85
|
+
b,
|
|
86
|
+
Math.round(a * 255)
|
|
87
|
+
].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
88
|
+
}
|
|
89
|
+
function hexToHsl(hex) {
|
|
90
|
+
return rgbToHsl(...hexToRgb(hex));
|
|
91
|
+
}
|
|
92
|
+
const ColorPickerContext = createContext(void 0);
|
|
93
|
+
function useColorPicker() {
|
|
94
|
+
const context = useContext(ColorPickerContext);
|
|
95
|
+
if (!context) throw new Error("useColorPicker must be used within a ColorPicker");
|
|
96
|
+
return context;
|
|
97
|
+
}
|
|
98
|
+
function useColorPickerHex() {
|
|
99
|
+
const { hue, saturation, lightness } = useColorPicker();
|
|
100
|
+
return hslToHex(hue, saturation, lightness);
|
|
101
|
+
}
|
|
102
|
+
function ColorPicker({ value, defaultValue = "#000000", onChange, className, children }) {
|
|
103
|
+
const [ih, is, il] = hexToHsl(value || defaultValue);
|
|
104
|
+
const [hue, setHue] = useState(ih || 0);
|
|
105
|
+
const [saturation, setSaturation] = useState(is || 100);
|
|
106
|
+
const [lightness, setLightness] = useState(il || 50);
|
|
107
|
+
const [alpha, setAlpha] = useState(100);
|
|
108
|
+
const notify = useCallback((h, s, l, a) => {
|
|
109
|
+
if (!onChange) return;
|
|
110
|
+
const [r, g, b] = hslToRgb(h, s, l);
|
|
111
|
+
onChange([
|
|
112
|
+
r,
|
|
113
|
+
g,
|
|
114
|
+
b,
|
|
115
|
+
a / 100
|
|
116
|
+
]);
|
|
117
|
+
}, [onChange]);
|
|
118
|
+
const setHueNotify = useCallback((v) => {
|
|
119
|
+
setHue(v);
|
|
120
|
+
notify(v, saturation, lightness, alpha);
|
|
121
|
+
}, [
|
|
122
|
+
saturation,
|
|
123
|
+
lightness,
|
|
124
|
+
alpha,
|
|
125
|
+
notify
|
|
126
|
+
]);
|
|
127
|
+
const setSaturationNotify = useCallback((v) => {
|
|
128
|
+
setSaturation(v);
|
|
129
|
+
notify(hue, v, lightness, alpha);
|
|
130
|
+
}, [
|
|
131
|
+
hue,
|
|
132
|
+
lightness,
|
|
133
|
+
alpha,
|
|
134
|
+
notify
|
|
135
|
+
]);
|
|
136
|
+
const setLightnessNotify = useCallback((v) => {
|
|
137
|
+
setLightness(v);
|
|
138
|
+
notify(hue, saturation, v, alpha);
|
|
139
|
+
}, [
|
|
140
|
+
hue,
|
|
141
|
+
saturation,
|
|
142
|
+
alpha,
|
|
143
|
+
notify
|
|
144
|
+
]);
|
|
145
|
+
const setAlphaNotify = useCallback((v) => {
|
|
146
|
+
setAlpha(v);
|
|
147
|
+
notify(hue, saturation, lightness, v);
|
|
148
|
+
}, [
|
|
149
|
+
hue,
|
|
150
|
+
saturation,
|
|
151
|
+
lightness,
|
|
152
|
+
notify
|
|
153
|
+
]);
|
|
154
|
+
const getCurrentColor = useCallback(() => {
|
|
155
|
+
const [r, g, b] = hslToRgb(hue, saturation, lightness);
|
|
156
|
+
return [
|
|
157
|
+
r,
|
|
158
|
+
g,
|
|
159
|
+
b,
|
|
160
|
+
alpha / 100
|
|
161
|
+
];
|
|
162
|
+
}, [
|
|
163
|
+
hue,
|
|
164
|
+
saturation,
|
|
165
|
+
lightness,
|
|
166
|
+
alpha
|
|
167
|
+
]);
|
|
168
|
+
return /* @__PURE__ */ jsx(ColorPickerContext.Provider, {
|
|
169
|
+
value: {
|
|
170
|
+
hue,
|
|
171
|
+
saturation,
|
|
172
|
+
lightness,
|
|
173
|
+
alpha,
|
|
174
|
+
setHue: setHueNotify,
|
|
175
|
+
setSaturation: setSaturationNotify,
|
|
176
|
+
setLightness: setLightnessNotify,
|
|
177
|
+
setAlpha: setAlphaNotify,
|
|
178
|
+
getCurrentColor,
|
|
179
|
+
onChange
|
|
180
|
+
},
|
|
181
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
182
|
+
className: cn("flex size-full flex-col gap-4", className),
|
|
183
|
+
children
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const ColorPickerSelection = memo(function ColorPickerSelection({ className }) {
|
|
188
|
+
const containerRef = useRef(null);
|
|
189
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
190
|
+
const { hue, saturation, lightness, setSaturation, setLightness } = useColorPicker();
|
|
191
|
+
const backgroundGradient = useMemo(() => `linear-gradient(0deg, rgba(0,0,0,1), rgba(0,0,0,0)), linear-gradient(90deg, rgba(255,255,255,1), rgba(255,255,255,0)), hsl(${hue}, 100%, 50%)`, [hue]);
|
|
192
|
+
const positionX = saturation / 100;
|
|
193
|
+
const topLightness = positionX < .01 ? 100 : 50 + 50 * (1 - positionX);
|
|
194
|
+
const positionY = topLightness > 0 ? Math.max(0, Math.min(1, 1 - lightness / topLightness)) : 0;
|
|
195
|
+
const updateColor = useCallback((clientX, clientY) => {
|
|
196
|
+
if (!containerRef.current) return;
|
|
197
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
198
|
+
const x = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
199
|
+
const y = Math.max(0, Math.min(1, (clientY - rect.top) / rect.height));
|
|
200
|
+
setSaturation(x * 100);
|
|
201
|
+
setLightness((x < .01 ? 100 : 50 + 50 * (1 - x)) * (1 - y));
|
|
202
|
+
}, [setSaturation, setLightness]);
|
|
203
|
+
const handlePointerDown = useCallback((e) => {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
setIsDragging(true);
|
|
206
|
+
updateColor(e.clientX, e.clientY);
|
|
207
|
+
}, [updateColor]);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!isDragging) return;
|
|
210
|
+
const onMove = (e) => updateColor(e.clientX, e.clientY);
|
|
211
|
+
const onUp = () => setIsDragging(false);
|
|
212
|
+
window.addEventListener("pointermove", onMove);
|
|
213
|
+
window.addEventListener("pointerup", onUp);
|
|
214
|
+
return () => {
|
|
215
|
+
window.removeEventListener("pointermove", onMove);
|
|
216
|
+
window.removeEventListener("pointerup", onUp);
|
|
217
|
+
};
|
|
218
|
+
}, [isDragging, updateColor]);
|
|
219
|
+
return /* @__PURE__ */ jsx("div", {
|
|
220
|
+
className: cn("relative size-full cursor-crosshair rounded", className),
|
|
221
|
+
onPointerDown: handlePointerDown,
|
|
222
|
+
ref: containerRef,
|
|
223
|
+
style: { background: backgroundGradient },
|
|
224
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
225
|
+
className: "-translate-x-1/2 -translate-y-1/2 pointer-events-none absolute h-4 w-4 rounded-full border-2 border-white",
|
|
226
|
+
style: {
|
|
227
|
+
left: `${positionX * 100}%`,
|
|
228
|
+
top: `${positionY * 100}%`,
|
|
229
|
+
boxShadow: "0 0 0 1px rgba(0,0,0,0.5)"
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
function ColorSlider({ value, max, onChange, trackClassName, trackStyle, children, className }) {
|
|
235
|
+
const ref = useRef(null);
|
|
236
|
+
const [dragging, setDragging] = useState(false);
|
|
237
|
+
const update = useCallback((clientX) => {
|
|
238
|
+
if (!ref.current) return;
|
|
239
|
+
const rect = ref.current.getBoundingClientRect();
|
|
240
|
+
const x = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
241
|
+
onChange(Math.round(x * max));
|
|
242
|
+
}, [max, onChange]);
|
|
243
|
+
const handlePointerDown = useCallback((e) => {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
setDragging(true);
|
|
246
|
+
update(e.clientX);
|
|
247
|
+
}, [update]);
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (!dragging) return;
|
|
250
|
+
const onMove = (e) => update(e.clientX);
|
|
251
|
+
const onUp = () => setDragging(false);
|
|
252
|
+
window.addEventListener("pointermove", onMove);
|
|
253
|
+
window.addEventListener("pointerup", onUp);
|
|
254
|
+
return () => {
|
|
255
|
+
window.removeEventListener("pointermove", onMove);
|
|
256
|
+
window.removeEventListener("pointerup", onUp);
|
|
257
|
+
};
|
|
258
|
+
}, [dragging, update]);
|
|
259
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
260
|
+
ref,
|
|
261
|
+
className: cn("relative flex h-4 w-full touch-none items-center", className),
|
|
262
|
+
onPointerDown: handlePointerDown,
|
|
263
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
264
|
+
className: cn("relative h-3 w-full rounded-full", trackClassName),
|
|
265
|
+
style: trackStyle,
|
|
266
|
+
children
|
|
267
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
268
|
+
className: "pointer-events-none absolute top-0 h-4 w-4 -translate-x-1/2 rounded-full border border-primary/50 bg-background shadow",
|
|
269
|
+
style: { left: `${value / max * 100}%` }
|
|
270
|
+
})]
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const HUE_GRADIENT = "linear-gradient(90deg, #FF0000, #FFFF00, #00FF00, #00FFFF, #0000FF, #FF00FF, #FF0000)";
|
|
274
|
+
function ColorPickerHue({ className }) {
|
|
275
|
+
const { hue, setHue } = useColorPicker();
|
|
276
|
+
return /* @__PURE__ */ jsx(ColorSlider, {
|
|
277
|
+
value: hue,
|
|
278
|
+
max: 360,
|
|
279
|
+
onChange: setHue,
|
|
280
|
+
trackStyle: { background: HUE_GRADIENT },
|
|
281
|
+
className
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const CHECKER_BG = "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==')";
|
|
285
|
+
function ColorPickerAlpha({ className }) {
|
|
286
|
+
const { alpha, setAlpha } = useColorPicker();
|
|
287
|
+
return /* @__PURE__ */ jsx(ColorSlider, {
|
|
288
|
+
value: alpha,
|
|
289
|
+
max: 100,
|
|
290
|
+
onChange: setAlpha,
|
|
291
|
+
trackStyle: {
|
|
292
|
+
backgroundImage: CHECKER_BG,
|
|
293
|
+
backgroundSize: "contain"
|
|
294
|
+
},
|
|
295
|
+
className,
|
|
296
|
+
children: /* @__PURE__ */ jsx("div", { className: "absolute inset-0 rounded-full bg-gradient-to-r from-transparent to-black/50 dark:to-white/50" })
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function ColorPickerEyeDropper({ className, children }) {
|
|
300
|
+
const { setHue, setSaturation, setLightness, setAlpha } = useColorPicker();
|
|
301
|
+
const handleEyeDropper = async () => {
|
|
302
|
+
try {
|
|
303
|
+
const [h, s, l] = hexToHsl((await new EyeDropper().open()).sRGBHex);
|
|
304
|
+
setHue(h);
|
|
305
|
+
setSaturation(s);
|
|
306
|
+
setLightness(l);
|
|
307
|
+
setAlpha(100);
|
|
308
|
+
} catch {}
|
|
309
|
+
};
|
|
310
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
311
|
+
type: "button",
|
|
312
|
+
variant: "outline",
|
|
313
|
+
size: "icon",
|
|
314
|
+
className: cn("shrink-0", className),
|
|
315
|
+
onClick: handleEyeDropper,
|
|
316
|
+
children: children || /* @__PURE__ */ jsx(Pipette, { className: "h-4 w-4" })
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function ColorPickerInput({ className, showAlpha = false }) {
|
|
320
|
+
const { hue, saturation, lightness, alpha } = useColorPicker();
|
|
321
|
+
const hex = hslToHex(hue, saturation, lightness);
|
|
322
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
323
|
+
className: cn("flex items-center gap-2", className),
|
|
324
|
+
children: [/* @__PURE__ */ jsx(Input, {
|
|
325
|
+
readOnly: true,
|
|
326
|
+
value: hex,
|
|
327
|
+
className: "flex-1"
|
|
328
|
+
}), showAlpha && /* @__PURE__ */ jsxs("span", {
|
|
329
|
+
className: "text-sm text-muted-foreground",
|
|
330
|
+
children: [Math.round(alpha), "%"]
|
|
331
|
+
})]
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function ColorPickerPreview({ className }) {
|
|
335
|
+
const { hue, saturation, lightness, alpha } = useColorPicker();
|
|
336
|
+
const color = hslToHexa(hue, saturation, lightness, alpha / 100);
|
|
337
|
+
return /* @__PURE__ */ jsx("div", {
|
|
338
|
+
className: cn("h-10 w-10 rounded-md border border-input", className),
|
|
339
|
+
style: { backgroundColor: color }
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
//#endregion
|
|
344
|
+
export { ColorPicker, ColorPickerAlpha, ColorPickerEyeDropper, ColorPickerHue, ColorPickerInput, ColorPickerPreview, ColorPickerSelection, useColorPicker, useColorPickerHex };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/components/gallery/types.d.ts
|
|
5
|
+
interface GalleryImage {
|
|
6
|
+
src: string;
|
|
7
|
+
thumbnail?: string;
|
|
8
|
+
alt?: string;
|
|
9
|
+
}
|
|
10
|
+
interface GalleryBadge {
|
|
11
|
+
label: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
interface GalleryClassNames {
|
|
15
|
+
root?: string;
|
|
16
|
+
main?: string;
|
|
17
|
+
mainImage?: string;
|
|
18
|
+
slider?: string;
|
|
19
|
+
thumbnails?: string;
|
|
20
|
+
thumbnail?: string;
|
|
21
|
+
thumbnailActive?: string;
|
|
22
|
+
dots?: string;
|
|
23
|
+
dot?: string;
|
|
24
|
+
dotActive?: string;
|
|
25
|
+
nav?: string;
|
|
26
|
+
navPrev?: string;
|
|
27
|
+
navNext?: string;
|
|
28
|
+
badges?: string;
|
|
29
|
+
badge?: string;
|
|
30
|
+
lightbox?: string;
|
|
31
|
+
lightboxImage?: string;
|
|
32
|
+
lightboxHeader?: string;
|
|
33
|
+
lightboxFooter?: string;
|
|
34
|
+
}
|
|
35
|
+
interface GalleryContextValue {
|
|
36
|
+
images: GalleryImage[];
|
|
37
|
+
selectedIndex: number;
|
|
38
|
+
setSelectedIndex: (index: number) => void;
|
|
39
|
+
lightboxOpen: boolean;
|
|
40
|
+
setLightboxOpen: (open: boolean) => void;
|
|
41
|
+
goToNext: () => void;
|
|
42
|
+
goToPrevious: () => void;
|
|
43
|
+
classNames?: GalleryClassNames;
|
|
44
|
+
title?: string;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/components/gallery/gallery-main.d.ts
|
|
48
|
+
interface GalleryMainProps {
|
|
49
|
+
children?: ReactNode;
|
|
50
|
+
badges?: GalleryBadge[];
|
|
51
|
+
showNav?: boolean;
|
|
52
|
+
aspectRatio?: string;
|
|
53
|
+
className?: string;
|
|
54
|
+
}
|
|
55
|
+
declare function GalleryMain({
|
|
56
|
+
children,
|
|
57
|
+
badges,
|
|
58
|
+
showNav,
|
|
59
|
+
aspectRatio,
|
|
60
|
+
className
|
|
61
|
+
}: GalleryMainProps): react_jsx_runtime0.JSX.Element;
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/components/gallery/gallery-thumbnails.d.ts
|
|
64
|
+
interface GalleryThumbnailsProps {
|
|
65
|
+
/** Show on mobile (default: true) */
|
|
66
|
+
showOnMobile?: boolean;
|
|
67
|
+
/** Layout orientation (default: "horizontal") */
|
|
68
|
+
orientation?: "horizontal" | "vertical";
|
|
69
|
+
/** Thumbnail size class (default: "w-16 h-20 sm:w-20 sm:h-24") */
|
|
70
|
+
sizeClassName?: string;
|
|
71
|
+
className?: string;
|
|
72
|
+
}
|
|
73
|
+
declare function GalleryThumbnails({
|
|
74
|
+
showOnMobile,
|
|
75
|
+
orientation,
|
|
76
|
+
sizeClassName,
|
|
77
|
+
className
|
|
78
|
+
}: GalleryThumbnailsProps): react_jsx_runtime0.JSX.Element | null;
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/components/gallery/gallery-dots.d.ts
|
|
81
|
+
interface GalleryDotsProps {
|
|
82
|
+
/** Show on desktop (default: false — dots are usually mobile-only) */
|
|
83
|
+
showOnDesktop?: boolean;
|
|
84
|
+
className?: string;
|
|
85
|
+
}
|
|
86
|
+
declare function GalleryDots({
|
|
87
|
+
showOnDesktop,
|
|
88
|
+
className
|
|
89
|
+
}: GalleryDotsProps): react_jsx_runtime0.JSX.Element | null;
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/components/gallery/gallery-lightbox.d.ts
|
|
92
|
+
interface GalleryLightboxProps {
|
|
93
|
+
className?: string;
|
|
94
|
+
}
|
|
95
|
+
declare function GalleryLightbox({
|
|
96
|
+
className
|
|
97
|
+
}: GalleryLightboxProps): react_jsx_runtime0.JSX.Element | null;
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/components/gallery/gallery-nav.d.ts
|
|
100
|
+
interface GalleryNavProps {
|
|
101
|
+
/** Which direction this button navigates */
|
|
102
|
+
direction: "prev" | "next";
|
|
103
|
+
className?: string;
|
|
104
|
+
children?: React.ReactNode;
|
|
105
|
+
}
|
|
106
|
+
declare function GalleryNav({
|
|
107
|
+
direction,
|
|
108
|
+
className,
|
|
109
|
+
children
|
|
110
|
+
}: GalleryNavProps): react_jsx_runtime0.JSX.Element | null;
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/components/gallery/index.d.ts
|
|
113
|
+
interface ImageGalleryRootProps {
|
|
114
|
+
children: ReactNode;
|
|
115
|
+
images: GalleryImage[];
|
|
116
|
+
defaultIndex?: number;
|
|
117
|
+
classNames?: GalleryClassNames;
|
|
118
|
+
title?: string;
|
|
119
|
+
onIndexChange?: (index: number) => void;
|
|
120
|
+
className?: string;
|
|
121
|
+
}
|
|
122
|
+
declare function ImageGalleryRoot({
|
|
123
|
+
children,
|
|
124
|
+
images,
|
|
125
|
+
defaultIndex,
|
|
126
|
+
classNames,
|
|
127
|
+
title,
|
|
128
|
+
onIndexChange,
|
|
129
|
+
className
|
|
130
|
+
}: ImageGalleryRootProps): react_jsx_runtime0.JSX.Element;
|
|
131
|
+
interface ImageGalleryProps {
|
|
132
|
+
images: GalleryImage[];
|
|
133
|
+
title?: string;
|
|
134
|
+
badges?: GalleryBadge[];
|
|
135
|
+
/** Default selected index */
|
|
136
|
+
defaultIndex?: number;
|
|
137
|
+
/** Show thumbnails on mobile (default: true). If false, shows dots */
|
|
138
|
+
showMobileThumbnails?: boolean;
|
|
139
|
+
/** Main image aspect ratio */
|
|
140
|
+
aspectRatio?: string;
|
|
141
|
+
/** Show navigation arrows */
|
|
142
|
+
showNav?: boolean;
|
|
143
|
+
/** Custom class names for styling */
|
|
144
|
+
classNames?: GalleryClassNames;
|
|
145
|
+
/** Callback when index changes */
|
|
146
|
+
onIndexChange?: (index: number) => void;
|
|
147
|
+
/** Root className */
|
|
148
|
+
className?: string;
|
|
149
|
+
}
|
|
150
|
+
declare function ImageGallerySimple({
|
|
151
|
+
images,
|
|
152
|
+
title,
|
|
153
|
+
badges,
|
|
154
|
+
defaultIndex,
|
|
155
|
+
showMobileThumbnails,
|
|
156
|
+
aspectRatio,
|
|
157
|
+
showNav,
|
|
158
|
+
classNames,
|
|
159
|
+
onIndexChange,
|
|
160
|
+
className
|
|
161
|
+
}: ImageGalleryProps): react_jsx_runtime0.JSX.Element;
|
|
162
|
+
declare const ImageGallery: typeof ImageGallerySimple & {
|
|
163
|
+
Root: typeof ImageGalleryRoot;
|
|
164
|
+
Main: typeof GalleryMain;
|
|
165
|
+
Thumbnails: typeof GalleryThumbnails;
|
|
166
|
+
Dots: typeof GalleryDots;
|
|
167
|
+
Nav: typeof GalleryNav;
|
|
168
|
+
Lightbox: typeof GalleryLightbox;
|
|
169
|
+
};
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/components/gallery/gallery-context.d.ts
|
|
172
|
+
declare function useGallery(): GalleryContextValue;
|
|
173
|
+
declare function useGalleryOptional(): GalleryContextValue | null;
|
|
174
|
+
//#endregion
|
|
175
|
+
export { type GalleryBadge, type GalleryClassNames, type GalleryImage, ImageGallery, type ImageGalleryProps, useGallery, useGalleryOptional };
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { t as cn } from "../utils-DQ5SCVoW.mjs";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { ChevronLeft, ChevronRight, RotateCcw, X, ZoomIn, ZoomOut } from "lucide-react";
|
|
7
|
+
import Image from "next/image";
|
|
8
|
+
|
|
9
|
+
//#region src/components/gallery/gallery-context.tsx
|
|
10
|
+
const GalleryContext = createContext(null);
|
|
11
|
+
function useGallery() {
|
|
12
|
+
const context = useContext(GalleryContext);
|
|
13
|
+
if (!context) throw new Error("useGallery must be used within a GalleryProvider");
|
|
14
|
+
return context;
|
|
15
|
+
}
|
|
16
|
+
function useGalleryOptional() {
|
|
17
|
+
return useContext(GalleryContext);
|
|
18
|
+
}
|
|
19
|
+
function GalleryProvider({ children, images, defaultIndex = 0, classNames, title, onIndexChange }) {
|
|
20
|
+
const [selectedIndex, setSelectedIndexState] = useState(defaultIndex);
|
|
21
|
+
const [lightboxOpen, setLightboxOpen] = useState(false);
|
|
22
|
+
const setSelectedIndex = useCallback((index) => {
|
|
23
|
+
setSelectedIndexState(index);
|
|
24
|
+
onIndexChange?.(index);
|
|
25
|
+
}, [onIndexChange]);
|
|
26
|
+
const goToNext = useCallback(() => {
|
|
27
|
+
setSelectedIndex(selectedIndex === images.length - 1 ? 0 : selectedIndex + 1);
|
|
28
|
+
}, [
|
|
29
|
+
selectedIndex,
|
|
30
|
+
images.length,
|
|
31
|
+
setSelectedIndex
|
|
32
|
+
]);
|
|
33
|
+
const goToPrevious = useCallback(() => {
|
|
34
|
+
setSelectedIndex(selectedIndex === 0 ? images.length - 1 : selectedIndex - 1);
|
|
35
|
+
}, [
|
|
36
|
+
selectedIndex,
|
|
37
|
+
images.length,
|
|
38
|
+
setSelectedIndex
|
|
39
|
+
]);
|
|
40
|
+
const value = useMemo(() => ({
|
|
41
|
+
images,
|
|
42
|
+
selectedIndex,
|
|
43
|
+
setSelectedIndex,
|
|
44
|
+
lightboxOpen,
|
|
45
|
+
setLightboxOpen,
|
|
46
|
+
goToNext,
|
|
47
|
+
goToPrevious,
|
|
48
|
+
classNames,
|
|
49
|
+
title
|
|
50
|
+
}), [
|
|
51
|
+
images,
|
|
52
|
+
selectedIndex,
|
|
53
|
+
setSelectedIndex,
|
|
54
|
+
lightboxOpen,
|
|
55
|
+
goToNext,
|
|
56
|
+
goToPrevious,
|
|
57
|
+
classNames,
|
|
58
|
+
title
|
|
59
|
+
]);
|
|
60
|
+
return /* @__PURE__ */ jsx(GalleryContext.Provider, {
|
|
61
|
+
value,
|
|
62
|
+
children
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/components/gallery/gallery-main.tsx
|
|
68
|
+
function GalleryMain({ children, badges, showNav = true, aspectRatio = "aspect-[4/5]", className }) {
|
|
69
|
+
const { images, selectedIndex, setLightboxOpen, goToNext, goToPrevious, classNames, title } = useGallery();
|
|
70
|
+
const [imagesLoaded, setImagesLoaded] = useState(new Set([0]));
|
|
71
|
+
const [touchStart, setTouchStart] = useState(null);
|
|
72
|
+
const [touchEnd, setTouchEnd] = useState(null);
|
|
73
|
+
const sliderRef = useRef(null);
|
|
74
|
+
const minSwipeDistance = 50;
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
images.forEach((img, index) => {
|
|
77
|
+
if (index === 0) return;
|
|
78
|
+
const preloadImg = new window.Image();
|
|
79
|
+
preloadImg.src = img.src;
|
|
80
|
+
preloadImg.onload = () => {
|
|
81
|
+
setImagesLoaded((prev) => new Set([...prev, index]));
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}, [images]);
|
|
85
|
+
const onTouchStart = useCallback((e) => {
|
|
86
|
+
setTouchEnd(null);
|
|
87
|
+
setTouchStart(e.targetTouches[0].clientX);
|
|
88
|
+
}, []);
|
|
89
|
+
const onTouchMove = useCallback((e) => {
|
|
90
|
+
setTouchEnd(e.targetTouches[0].clientX);
|
|
91
|
+
}, []);
|
|
92
|
+
const onTouchEnd = useCallback(() => {
|
|
93
|
+
if (!touchStart || !touchEnd) return;
|
|
94
|
+
const distance = touchStart - touchEnd;
|
|
95
|
+
if (distance > minSwipeDistance) goToNext();
|
|
96
|
+
if (distance < -minSwipeDistance) goToPrevious();
|
|
97
|
+
}, [
|
|
98
|
+
touchStart,
|
|
99
|
+
touchEnd,
|
|
100
|
+
goToNext,
|
|
101
|
+
goToPrevious
|
|
102
|
+
]);
|
|
103
|
+
const handleImageLoad = useCallback((index) => {
|
|
104
|
+
setImagesLoaded((prev) => new Set([...prev, index]));
|
|
105
|
+
}, []);
|
|
106
|
+
return /* @__PURE__ */ jsx("div", {
|
|
107
|
+
className: cn("relative", className),
|
|
108
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
109
|
+
ref: sliderRef,
|
|
110
|
+
className: cn(aspectRatio, "bg-muted overflow-hidden relative rounded-lg", classNames?.main),
|
|
111
|
+
onTouchStart,
|
|
112
|
+
onTouchMove,
|
|
113
|
+
onTouchEnd,
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ jsx("div", {
|
|
116
|
+
className: cn("absolute inset-0 flex transition-transform duration-300 ease-out", classNames?.slider),
|
|
117
|
+
style: { transform: `translateX(-${selectedIndex * 100}%)` },
|
|
118
|
+
children: images.map((img, index) => /* @__PURE__ */ jsxs("div", {
|
|
119
|
+
className: "w-full h-full shrink-0 relative cursor-zoom-in",
|
|
120
|
+
onClick: () => setLightboxOpen(true),
|
|
121
|
+
children: [/* @__PURE__ */ jsx(Image, {
|
|
122
|
+
src: img.src,
|
|
123
|
+
alt: img.alt || `${title || "Gallery"} - Image ${index + 1}`,
|
|
124
|
+
fill: true,
|
|
125
|
+
className: cn("object-cover transition-opacity duration-300", imagesLoaded.has(index) ? "opacity-100" : "opacity-0", classNames?.mainImage),
|
|
126
|
+
sizes: "(max-width: 768px) 100vw, 50vw",
|
|
127
|
+
priority: index === 0,
|
|
128
|
+
onLoad: () => handleImageLoad(index)
|
|
129
|
+
}), !imagesLoaded.has(index) && /* @__PURE__ */ jsx("div", {
|
|
130
|
+
className: "absolute inset-0 flex items-center justify-center bg-muted",
|
|
131
|
+
children: /* @__PURE__ */ jsx("div", { className: "w-8 h-8 border-2 border-muted-foreground/20 border-t-muted-foreground rounded-full animate-spin" })
|
|
132
|
+
})]
|
|
133
|
+
}, index))
|
|
134
|
+
}),
|
|
135
|
+
badges && badges.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
136
|
+
className: cn("absolute top-4 left-4 flex flex-col gap-2 z-10", classNames?.badges),
|
|
137
|
+
children: badges.map((badge, index) => /* @__PURE__ */ jsx("span", {
|
|
138
|
+
className: cn("px-3 py-1 text-xs font-medium uppercase tracking-wider", classNames?.badge, badge.className),
|
|
139
|
+
children: badge.label
|
|
140
|
+
}, index))
|
|
141
|
+
}),
|
|
142
|
+
showNav && images.length > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
|
|
143
|
+
type: "button",
|
|
144
|
+
onClick: (e) => {
|
|
145
|
+
e.stopPropagation();
|
|
146
|
+
goToPrevious();
|
|
147
|
+
},
|
|
148
|
+
className: cn("hidden sm:flex absolute left-3 top-1/2 -translate-y-1/2 z-10", "w-10 h-10 items-center justify-center rounded-full", "bg-background/80 backdrop-blur-sm shadow-md hover:bg-background transition-colors", classNames?.nav, classNames?.navPrev),
|
|
149
|
+
"aria-label": "Previous image",
|
|
150
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-5 w-5" })
|
|
151
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
152
|
+
type: "button",
|
|
153
|
+
onClick: (e) => {
|
|
154
|
+
e.stopPropagation();
|
|
155
|
+
goToNext();
|
|
156
|
+
},
|
|
157
|
+
className: cn("hidden sm:flex absolute right-3 top-1/2 -translate-y-1/2 z-10", "w-10 h-10 items-center justify-center rounded-full", "bg-background/80 backdrop-blur-sm shadow-md hover:bg-background transition-colors", classNames?.nav, classNames?.navNext),
|
|
158
|
+
"aria-label": "Next image",
|
|
159
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-5 w-5" })
|
|
160
|
+
})] }),
|
|
161
|
+
children
|
|
162
|
+
]
|
|
163
|
+
})
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/components/gallery/gallery-thumbnails.tsx
|
|
169
|
+
function GalleryThumbnails({ showOnMobile = true, orientation = "horizontal", sizeClassName = "w-16 h-20 sm:w-20 sm:h-24", className }) {
|
|
170
|
+
const { images, selectedIndex, setSelectedIndex, classNames, title } = useGallery();
|
|
171
|
+
if (images.length <= 1) return null;
|
|
172
|
+
return /* @__PURE__ */ jsx("div", {
|
|
173
|
+
className: cn("flex gap-2 sm:gap-3 scrollbar-hide", orientation === "vertical" ? "flex-col overflow-y-auto pr-2" : "overflow-x-auto pb-2", !showOnMobile && "hidden sm:flex", classNames?.thumbnails, className),
|
|
174
|
+
children: images.map((img, index) => /* @__PURE__ */ jsx("button", {
|
|
175
|
+
type: "button",
|
|
176
|
+
onClick: () => setSelectedIndex(index),
|
|
177
|
+
className: cn("bg-muted overflow-hidden border-2 transition-all duration-200 shrink-0 rounded-md relative", sizeClassName, selectedIndex === index ? cn("border-foreground", classNames?.thumbnailActive) : cn("border-transparent opacity-60 hover:opacity-100", classNames?.thumbnail)),
|
|
178
|
+
"aria-label": `View image ${index + 1}`,
|
|
179
|
+
children: /* @__PURE__ */ jsx(Image, {
|
|
180
|
+
src: img.thumbnail || img.src,
|
|
181
|
+
alt: img.alt || `${title || "Gallery"} thumbnail ${index + 1}`,
|
|
182
|
+
fill: true,
|
|
183
|
+
className: "object-cover",
|
|
184
|
+
sizes: "80px"
|
|
185
|
+
})
|
|
186
|
+
}, index))
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/components/gallery/gallery-dots.tsx
|
|
192
|
+
function GalleryDots({ showOnDesktop = false, className }) {
|
|
193
|
+
const { images, selectedIndex, setSelectedIndex, classNames } = useGallery();
|
|
194
|
+
if (images.length <= 1) return null;
|
|
195
|
+
return /* @__PURE__ */ jsx("div", {
|
|
196
|
+
className: cn("flex justify-center gap-2 mt-3", !showOnDesktop && "sm:hidden", classNames?.dots, className),
|
|
197
|
+
children: images.map((_, index) => /* @__PURE__ */ jsx("button", {
|
|
198
|
+
type: "button",
|
|
199
|
+
onClick: () => setSelectedIndex(index),
|
|
200
|
+
className: cn("h-2 rounded-full transition-all duration-300", selectedIndex === index ? cn("bg-foreground w-6", classNames?.dotActive) : cn("bg-muted-foreground/30 hover:bg-muted-foreground/50 w-2", classNames?.dot)),
|
|
201
|
+
"aria-label": `Go to image ${index + 1}`
|
|
202
|
+
}, index))
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/components/gallery/gallery-lightbox.tsx
|
|
208
|
+
const MIN_ZOOM = 1;
|
|
209
|
+
const MAX_ZOOM = 4;
|
|
210
|
+
const ZOOM_STEP = .5;
|
|
211
|
+
function GalleryLightbox({ className }) {
|
|
212
|
+
const { images, selectedIndex, setSelectedIndex, lightboxOpen, setLightboxOpen, goToNext, goToPrevious, classNames, title } = useGallery();
|
|
213
|
+
const [zoom, setZoom] = useState(1);
|
|
214
|
+
const [position, setPosition] = useState({
|
|
215
|
+
x: 0,
|
|
216
|
+
y: 0
|
|
217
|
+
});
|
|
218
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
219
|
+
const [dragStart, setDragStart] = useState({
|
|
220
|
+
x: 0,
|
|
221
|
+
y: 0
|
|
222
|
+
});
|
|
223
|
+
const [imageLoaded, setImageLoaded] = useState(false);
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
setZoom(1);
|
|
226
|
+
setPosition({
|
|
227
|
+
x: 0,
|
|
228
|
+
y: 0
|
|
229
|
+
});
|
|
230
|
+
setImageLoaded(false);
|
|
231
|
+
}, [selectedIndex, lightboxOpen]);
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (!lightboxOpen) return;
|
|
234
|
+
const handleKeyDown = (e) => {
|
|
235
|
+
switch (e.key) {
|
|
236
|
+
case "Escape":
|
|
237
|
+
setLightboxOpen(false);
|
|
238
|
+
break;
|
|
239
|
+
case "ArrowLeft":
|
|
240
|
+
goToPrevious();
|
|
241
|
+
break;
|
|
242
|
+
case "ArrowRight":
|
|
243
|
+
goToNext();
|
|
244
|
+
break;
|
|
245
|
+
case "+":
|
|
246
|
+
case "=":
|
|
247
|
+
setZoom((prev) => Math.min(prev + ZOOM_STEP, MAX_ZOOM));
|
|
248
|
+
break;
|
|
249
|
+
case "-":
|
|
250
|
+
setZoom((prev) => {
|
|
251
|
+
const newZoom = Math.max(prev - ZOOM_STEP, MIN_ZOOM);
|
|
252
|
+
if (newZoom === 1) setPosition({
|
|
253
|
+
x: 0,
|
|
254
|
+
y: 0
|
|
255
|
+
});
|
|
256
|
+
return newZoom;
|
|
257
|
+
});
|
|
258
|
+
break;
|
|
259
|
+
case "0":
|
|
260
|
+
setZoom(1);
|
|
261
|
+
setPosition({
|
|
262
|
+
x: 0,
|
|
263
|
+
y: 0
|
|
264
|
+
});
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
269
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
270
|
+
}, [
|
|
271
|
+
lightboxOpen,
|
|
272
|
+
goToNext,
|
|
273
|
+
goToPrevious,
|
|
274
|
+
setLightboxOpen
|
|
275
|
+
]);
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
if (lightboxOpen) document.body.style.overflow = "hidden";
|
|
278
|
+
else document.body.style.overflow = "";
|
|
279
|
+
return () => {
|
|
280
|
+
document.body.style.overflow = "";
|
|
281
|
+
};
|
|
282
|
+
}, [lightboxOpen]);
|
|
283
|
+
const handleWheel = useCallback((e) => {
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
|
|
286
|
+
setZoom((prev) => {
|
|
287
|
+
const newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, prev + delta));
|
|
288
|
+
if (newZoom === 1) setPosition({
|
|
289
|
+
x: 0,
|
|
290
|
+
y: 0
|
|
291
|
+
});
|
|
292
|
+
return newZoom;
|
|
293
|
+
});
|
|
294
|
+
}, []);
|
|
295
|
+
const handleMouseDown = (e) => {
|
|
296
|
+
if (zoom > 1) {
|
|
297
|
+
setIsDragging(true);
|
|
298
|
+
setDragStart({
|
|
299
|
+
x: e.clientX - position.x,
|
|
300
|
+
y: e.clientY - position.y
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const handleMouseMove = (e) => {
|
|
305
|
+
if (isDragging && zoom > 1) setPosition({
|
|
306
|
+
x: e.clientX - dragStart.x,
|
|
307
|
+
y: e.clientY - dragStart.y
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
const handleMouseUp = () => setIsDragging(false);
|
|
311
|
+
const handleTouchStart = (e) => {
|
|
312
|
+
if (zoom > 1 && e.touches.length === 1) {
|
|
313
|
+
setIsDragging(true);
|
|
314
|
+
setDragStart({
|
|
315
|
+
x: e.touches[0].clientX - position.x,
|
|
316
|
+
y: e.touches[0].clientY - position.y
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
const handleTouchMove = (e) => {
|
|
321
|
+
if (isDragging && zoom > 1 && e.touches.length === 1) setPosition({
|
|
322
|
+
x: e.touches[0].clientX - dragStart.x,
|
|
323
|
+
y: e.touches[0].clientY - dragStart.y
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
const handleDoubleClick = () => {
|
|
327
|
+
if (zoom > 1) {
|
|
328
|
+
setZoom(1);
|
|
329
|
+
setPosition({
|
|
330
|
+
x: 0,
|
|
331
|
+
y: 0
|
|
332
|
+
});
|
|
333
|
+
} else setZoom(2);
|
|
334
|
+
};
|
|
335
|
+
if (!lightboxOpen) return null;
|
|
336
|
+
const currentImage = images[selectedIndex];
|
|
337
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
338
|
+
className: cn("fixed inset-0 z-50 bg-black", classNames?.lightbox, className),
|
|
339
|
+
role: "dialog",
|
|
340
|
+
"aria-modal": "true",
|
|
341
|
+
"aria-label": `${title || "Gallery"} - Image ${selectedIndex + 1} of ${images.length}`,
|
|
342
|
+
children: [
|
|
343
|
+
/* @__PURE__ */ jsxs("div", {
|
|
344
|
+
className: cn("absolute top-0 left-0 right-0 z-50", "flex items-center justify-between p-4", "bg-gradient-to-b from-black/60 to-transparent", classNames?.lightboxHeader),
|
|
345
|
+
children: [
|
|
346
|
+
/* @__PURE__ */ jsxs("span", {
|
|
347
|
+
className: "text-white text-sm font-medium",
|
|
348
|
+
children: [
|
|
349
|
+
selectedIndex + 1,
|
|
350
|
+
" / ",
|
|
351
|
+
images.length
|
|
352
|
+
]
|
|
353
|
+
}),
|
|
354
|
+
/* @__PURE__ */ jsxs("div", {
|
|
355
|
+
className: "flex items-center gap-2",
|
|
356
|
+
children: [
|
|
357
|
+
/* @__PURE__ */ jsx("button", {
|
|
358
|
+
type: "button",
|
|
359
|
+
onClick: () => setZoom((prev) => {
|
|
360
|
+
const newZoom = Math.max(prev - ZOOM_STEP, MIN_ZOOM);
|
|
361
|
+
if (newZoom === 1) setPosition({
|
|
362
|
+
x: 0,
|
|
363
|
+
y: 0
|
|
364
|
+
});
|
|
365
|
+
return newZoom;
|
|
366
|
+
}),
|
|
367
|
+
disabled: zoom <= MIN_ZOOM,
|
|
368
|
+
className: "p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors disabled:opacity-30",
|
|
369
|
+
"aria-label": "Zoom out",
|
|
370
|
+
children: /* @__PURE__ */ jsx(ZoomOut, { className: "h-5 w-5 text-white" })
|
|
371
|
+
}),
|
|
372
|
+
/* @__PURE__ */ jsxs("span", {
|
|
373
|
+
className: "text-white text-sm min-w-[3rem] text-center",
|
|
374
|
+
children: [Math.round(zoom * 100), "%"]
|
|
375
|
+
}),
|
|
376
|
+
/* @__PURE__ */ jsx("button", {
|
|
377
|
+
type: "button",
|
|
378
|
+
onClick: () => setZoom((prev) => Math.min(prev + ZOOM_STEP, MAX_ZOOM)),
|
|
379
|
+
disabled: zoom >= MAX_ZOOM,
|
|
380
|
+
className: "p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors disabled:opacity-30",
|
|
381
|
+
"aria-label": "Zoom in",
|
|
382
|
+
children: /* @__PURE__ */ jsx(ZoomIn, { className: "h-5 w-5 text-white" })
|
|
383
|
+
}),
|
|
384
|
+
/* @__PURE__ */ jsx("button", {
|
|
385
|
+
type: "button",
|
|
386
|
+
onClick: () => {
|
|
387
|
+
setZoom(1);
|
|
388
|
+
setPosition({
|
|
389
|
+
x: 0,
|
|
390
|
+
y: 0
|
|
391
|
+
});
|
|
392
|
+
},
|
|
393
|
+
disabled: zoom === 1,
|
|
394
|
+
className: "p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors disabled:opacity-30",
|
|
395
|
+
"aria-label": "Reset zoom",
|
|
396
|
+
children: /* @__PURE__ */ jsx(RotateCcw, { className: "h-5 w-5 text-white" })
|
|
397
|
+
})
|
|
398
|
+
]
|
|
399
|
+
}),
|
|
400
|
+
/* @__PURE__ */ jsx("button", {
|
|
401
|
+
type: "button",
|
|
402
|
+
onClick: () => setLightboxOpen(false),
|
|
403
|
+
className: "p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors",
|
|
404
|
+
"aria-label": "Close",
|
|
405
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-6 w-6 text-white" })
|
|
406
|
+
})
|
|
407
|
+
]
|
|
408
|
+
}),
|
|
409
|
+
/* @__PURE__ */ jsxs("div", {
|
|
410
|
+
className: cn("absolute inset-0 flex items-center justify-center", zoom > 1 ? "cursor-grab" : "cursor-zoom-in", isDragging && "cursor-grabbing"),
|
|
411
|
+
onWheel: handleWheel,
|
|
412
|
+
onMouseDown: handleMouseDown,
|
|
413
|
+
onMouseMove: handleMouseMove,
|
|
414
|
+
onMouseUp: handleMouseUp,
|
|
415
|
+
onMouseLeave: handleMouseUp,
|
|
416
|
+
onTouchStart: handleTouchStart,
|
|
417
|
+
onTouchMove: handleTouchMove,
|
|
418
|
+
onTouchEnd: handleMouseUp,
|
|
419
|
+
onDoubleClick: handleDoubleClick,
|
|
420
|
+
children: [!imageLoaded && /* @__PURE__ */ jsx("div", {
|
|
421
|
+
className: "absolute inset-0 flex items-center justify-center",
|
|
422
|
+
children: /* @__PURE__ */ jsx("div", { className: "w-12 h-12 border-4 border-white/20 border-t-white rounded-full animate-spin" })
|
|
423
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
424
|
+
className: "relative w-full h-full transition-transform duration-100",
|
|
425
|
+
style: { transform: `scale(${zoom}) translate(${position.x / zoom}px, ${position.y / zoom}px)` },
|
|
426
|
+
children: /* @__PURE__ */ jsx(Image, {
|
|
427
|
+
src: currentImage.src,
|
|
428
|
+
alt: currentImage.alt || `${title || "Gallery"} - Image ${selectedIndex + 1}`,
|
|
429
|
+
fill: true,
|
|
430
|
+
className: cn("object-contain transition-opacity duration-300", imageLoaded ? "opacity-100" : "opacity-0", classNames?.lightboxImage),
|
|
431
|
+
sizes: "100vw",
|
|
432
|
+
quality: 100,
|
|
433
|
+
priority: true,
|
|
434
|
+
unoptimized: true,
|
|
435
|
+
onLoad: () => setImageLoaded(true)
|
|
436
|
+
})
|
|
437
|
+
})]
|
|
438
|
+
}),
|
|
439
|
+
images.length > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
|
|
440
|
+
type: "button",
|
|
441
|
+
onClick: goToPrevious,
|
|
442
|
+
className: "absolute left-4 top-1/2 -translate-y-1/2 z-50 p-3 rounded-full bg-white/10 hover:bg-white/20 transition-colors",
|
|
443
|
+
"aria-label": "Previous",
|
|
444
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-8 w-8 text-white" })
|
|
445
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
446
|
+
type: "button",
|
|
447
|
+
onClick: goToNext,
|
|
448
|
+
className: "absolute right-4 top-1/2 -translate-y-1/2 z-50 p-3 rounded-full bg-white/10 hover:bg-white/20 transition-colors",
|
|
449
|
+
"aria-label": "Next",
|
|
450
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-8 w-8 text-white" })
|
|
451
|
+
})] }),
|
|
452
|
+
images.length > 1 && /* @__PURE__ */ jsxs("div", {
|
|
453
|
+
className: cn("absolute bottom-0 left-0 right-0 z-50 p-4", "bg-gradient-to-t from-black/60 to-transparent", classNames?.lightboxFooter),
|
|
454
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
455
|
+
className: "flex justify-center gap-2 overflow-x-auto max-w-full pb-2",
|
|
456
|
+
children: images.map((img, index) => /* @__PURE__ */ jsx("button", {
|
|
457
|
+
type: "button",
|
|
458
|
+
onClick: () => setSelectedIndex(index),
|
|
459
|
+
className: cn("w-16 h-16 md:w-20 md:h-20 overflow-hidden border-2 transition-all shrink-0 rounded-md relative", selectedIndex === index ? "border-white opacity-100" : "border-transparent opacity-50 hover:opacity-80"),
|
|
460
|
+
"aria-label": `View image ${index + 1}`,
|
|
461
|
+
children: /* @__PURE__ */ jsx(Image, {
|
|
462
|
+
src: img.thumbnail || img.src,
|
|
463
|
+
alt: img.alt || `Thumbnail ${index + 1}`,
|
|
464
|
+
fill: true,
|
|
465
|
+
className: "object-cover",
|
|
466
|
+
sizes: "80px"
|
|
467
|
+
})
|
|
468
|
+
}, index))
|
|
469
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
470
|
+
className: "text-white/50 text-xs text-center mt-2 hidden md:block",
|
|
471
|
+
children: "Scroll to zoom · Double-click to toggle · Drag to pan · Arrow keys to navigate"
|
|
472
|
+
})]
|
|
473
|
+
})
|
|
474
|
+
]
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/components/gallery/gallery-nav.tsx
|
|
480
|
+
function GalleryNav({ direction, className, children }) {
|
|
481
|
+
const { images, goToNext, goToPrevious, classNames } = useGallery();
|
|
482
|
+
if (images.length <= 1) return null;
|
|
483
|
+
const isPrev = direction === "prev";
|
|
484
|
+
return /* @__PURE__ */ jsx("button", {
|
|
485
|
+
type: "button",
|
|
486
|
+
onClick: isPrev ? goToPrevious : goToNext,
|
|
487
|
+
className: cn("flex items-center justify-center", "w-10 h-10 rounded-full", "bg-background/80 backdrop-blur-sm shadow-md hover:bg-background transition-colors", classNames?.nav, isPrev ? classNames?.navPrev : classNames?.navNext, className),
|
|
488
|
+
"aria-label": isPrev ? "Previous image" : "Next image",
|
|
489
|
+
children: children || (isPrev ? /* @__PURE__ */ jsx(ChevronLeft, { className: "h-5 w-5" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-5 w-5" }))
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/components/gallery/index.tsx
|
|
495
|
+
function ImageGalleryRoot({ children, images, defaultIndex = 0, classNames, title, onIndexChange, className }) {
|
|
496
|
+
return /* @__PURE__ */ jsx(GalleryProvider, {
|
|
497
|
+
images,
|
|
498
|
+
defaultIndex,
|
|
499
|
+
classNames,
|
|
500
|
+
title,
|
|
501
|
+
onIndexChange,
|
|
502
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
503
|
+
className: cn("space-y-4", classNames?.root, className),
|
|
504
|
+
children
|
|
505
|
+
})
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
function ImageGallerySimple({ images, title, badges, defaultIndex = 0, showMobileThumbnails = true, aspectRatio = "aspect-[4/5]", showNav = true, classNames, onIndexChange, className }) {
|
|
509
|
+
if (!images || images.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
510
|
+
className: cn(aspectRatio, "bg-muted flex items-center justify-center rounded-lg", className),
|
|
511
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
512
|
+
className: "text-muted-foreground",
|
|
513
|
+
children: "No image available"
|
|
514
|
+
})
|
|
515
|
+
});
|
|
516
|
+
return /* @__PURE__ */ jsxs(ImageGalleryRoot, {
|
|
517
|
+
images,
|
|
518
|
+
defaultIndex,
|
|
519
|
+
classNames,
|
|
520
|
+
title,
|
|
521
|
+
onIndexChange,
|
|
522
|
+
className,
|
|
523
|
+
children: [
|
|
524
|
+
/* @__PURE__ */ jsx(GalleryMain, {
|
|
525
|
+
badges,
|
|
526
|
+
showNav,
|
|
527
|
+
aspectRatio
|
|
528
|
+
}),
|
|
529
|
+
!showMobileThumbnails && /* @__PURE__ */ jsx(GalleryDots, {}),
|
|
530
|
+
/* @__PURE__ */ jsx(GalleryThumbnails, { showOnMobile: showMobileThumbnails }),
|
|
531
|
+
/* @__PURE__ */ jsx(GalleryLightbox, {})
|
|
532
|
+
]
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const ImageGallery = Object.assign(ImageGallerySimple, {
|
|
536
|
+
Root: ImageGalleryRoot,
|
|
537
|
+
Main: GalleryMain,
|
|
538
|
+
Thumbnails: GalleryThumbnails,
|
|
539
|
+
Dots: GalleryDots,
|
|
540
|
+
Nav: GalleryNav,
|
|
541
|
+
Lightbox: GalleryLightbox
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
//#endregion
|
|
545
|
+
export { ImageGallery, useGallery, useGalleryOptional };
|
|
@@ -76,6 +76,8 @@ interface SpreadsheetTableProps<TData = unknown> {
|
|
|
76
76
|
}) => ReactNode;
|
|
77
77
|
/** CSS class for the table container */
|
|
78
78
|
className?: string;
|
|
79
|
+
/** CSS class for the header row (overrides default bg-muted/50) */
|
|
80
|
+
headerClassName?: string;
|
|
79
81
|
/** Accessible label for the table */
|
|
80
82
|
ariaLabel?: string;
|
|
81
83
|
/** Called when a row is added (receives the ID of the row to insert after) */
|
|
@@ -124,6 +126,7 @@ declare function SpreadsheetTable<TData = unknown>({
|
|
|
124
126
|
mobileFooter,
|
|
125
127
|
rowActions,
|
|
126
128
|
className,
|
|
129
|
+
headerClassName,
|
|
127
130
|
ariaLabel,
|
|
128
131
|
onAddRow,
|
|
129
132
|
virtualize,
|
|
@@ -289,7 +289,7 @@ const VIRTUAL_ROW_BASE = {
|
|
|
289
289
|
width: "100%",
|
|
290
290
|
willChange: "transform"
|
|
291
291
|
};
|
|
292
|
-
function SpreadsheetTable({ columns, items, orderedIds, dispatch, isReadOnly = false, footer, mobileFooter, rowActions, className = "", ariaLabel = "Spreadsheet", onAddRow, virtualize = false, estimateRowHeight = 48 }) {
|
|
292
|
+
function SpreadsheetTable({ columns, items, orderedIds, dispatch, isReadOnly = false, footer, mobileFooter, rowActions, className = "", headerClassName, ariaLabel = "Spreadsheet", onAddRow, virtualize = false, estimateRowHeight = 48 }) {
|
|
293
293
|
const containerRef = useRef(null);
|
|
294
294
|
const tableContainerRef = useRef(null);
|
|
295
295
|
const mobileContainerRef = useRef(null);
|
|
@@ -343,9 +343,9 @@ function SpreadsheetTable({ columns, items, orderedIds, dispatch, isReadOnly = f
|
|
|
343
343
|
"aria-label": ariaLabel,
|
|
344
344
|
children: [
|
|
345
345
|
/* @__PURE__ */ jsx("thead", {
|
|
346
|
-
className:
|
|
346
|
+
className: `sticky top-0 z-10 ${headerClassName ?? "bg-muted/50"}`,
|
|
347
347
|
children: /* @__PURE__ */ jsxs("tr", {
|
|
348
|
-
className: "border-b
|
|
348
|
+
className: "border-b",
|
|
349
349
|
style: virtualize ? {
|
|
350
350
|
display: "flex",
|
|
351
351
|
width: "100%"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/fluid",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Fluid UI - Custom components built on shadcn/ui and base ui by Classytic",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
@@ -39,6 +39,14 @@
|
|
|
39
39
|
"types": "./dist/client/spreadsheet.d.mts",
|
|
40
40
|
"default": "./dist/client/spreadsheet.mjs"
|
|
41
41
|
},
|
|
42
|
+
"./client/color-picker": {
|
|
43
|
+
"types": "./dist/client/color-picker.d.mts",
|
|
44
|
+
"default": "./dist/client/color-picker.mjs"
|
|
45
|
+
},
|
|
46
|
+
"./client/gallery": {
|
|
47
|
+
"types": "./dist/client/gallery.d.mts",
|
|
48
|
+
"default": "./dist/client/gallery.mjs"
|
|
49
|
+
},
|
|
42
50
|
"./forms": {
|
|
43
51
|
"types": "./dist/forms.d.mts",
|
|
44
52
|
"default": "./dist/forms.mjs"
|