@classytic/fluid 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +21 -1
  2. package/dist/client/calendar.d.mts +1 -2
  3. package/dist/client/calendar.mjs +4 -4
  4. package/dist/client/color-picker.d.mts +94 -0
  5. package/dist/client/color-picker.mjs +392 -0
  6. package/dist/client/core.d.mts +243 -557
  7. package/dist/client/core.mjs +351 -1462
  8. package/dist/client/error.d.mts +41 -41
  9. package/dist/client/error.mjs +35 -35
  10. package/dist/client/gallery.d.mts +175 -0
  11. package/dist/client/gallery.mjs +546 -0
  12. package/dist/client/hooks.d.mts +57 -39
  13. package/dist/client/hooks.mjs +29 -7
  14. package/dist/client/spreadsheet.d.mts +30 -27
  15. package/dist/client/spreadsheet.mjs +80 -80
  16. package/dist/client/table.d.mts +66 -33
  17. package/dist/client/table.mjs +87 -54
  18. package/dist/client/theme.mjs +1 -1
  19. package/dist/command.d.mts +6 -4
  20. package/dist/command.mjs +3 -3
  21. package/dist/compact.d.mts +97 -95
  22. package/dist/compact.mjs +336 -322
  23. package/dist/dashboard.d.mts +614 -422
  24. package/dist/dashboard.mjs +1051 -762
  25. package/dist/{dropdown-wrapper-B86u9Fri.mjs → dropdown-wrapper-B9nRDUlz.mjs} +25 -35
  26. package/dist/forms.d.mts +1037 -972
  27. package/dist/forms.mjs +2849 -2721
  28. package/dist/index.d.mts +218 -152
  29. package/dist/index.mjs +357 -264
  30. package/dist/layouts.d.mts +94 -94
  31. package/dist/layouts.mjs +115 -110
  32. package/dist/phone-input-B9_XPNvv.mjs +429 -0
  33. package/dist/phone-input-CLH_UjQZ.d.mts +31 -0
  34. package/dist/{search-context-DR7DBs7S.mjs → search-context-1g3ZmOvx.mjs} +1 -1
  35. package/dist/search.d.mts +168 -164
  36. package/dist/search.mjs +305 -301
  37. package/dist/{sheet-wrapper-C13Y-Q6w.mjs → sheet-wrapper-B2uxookb.mjs} +1 -1
  38. package/dist/timeline-Bgu1mIe9.d.mts +373 -0
  39. package/dist/timeline-HJtWf4Op.mjs +804 -0
  40. package/dist/{use-base-search-BGgWnWaF.d.mts → use-base-search-DFC4QKYU.d.mts} +1 -1
  41. package/dist/{use-media-query-BnVNIKT4.mjs → use-media-query-ChLfFChU.mjs} +6 -7
  42. package/package.json +10 -2
  43. /package/dist/{api-pagination-CJ0vR_w6.d.mts → api-pagination-C30ser2L.d.mts} +0 -0
  44. /package/dist/{filter-utils-DqMmy_v-.mjs → filter-utils-BGIvtq1R.mjs} +0 -0
  45. /package/dist/{filter-utils-IZ0GtuPo.d.mts → filter-utils-DOFTBWm1.d.mts} +0 -0
  46. /package/dist/{use-debounce-xmZucz5e.mjs → use-debounce-BNoNiEon.mjs} +0 -0
  47. /package/dist/{use-keyboard-shortcut-Bl6YM5Q7.mjs → use-keyboard-shortcut-C_Vk-36P.mjs} +0 -0
  48. /package/dist/{use-keyboard-shortcut-_mRCh3QO.d.mts → use-keyboard-shortcut-Q4CSPzSI.d.mts} +0 -0
  49. /package/dist/{use-mobile-BX3SQVo2.mjs → use-mobile-CnEmFiQx.mjs} +0 -0
  50. /package/dist/{use-scroll-detection-CsgsQYvy.mjs → use-scroll-detection-BKfqkmEC.mjs} +0 -0
  51. /package/dist/{utils-CDue7cEt.d.mts → utils-rqvYP1by.d.mts} +0 -0
package/README.md CHANGED
@@ -75,11 +75,15 @@ import { CommandSearch, useKeyboardShortcut } from "@classytic/fluid/command";
75
75
  |----------|-----------|
76
76
  | **Layout** | Section, Container |
77
77
  | **Display** | DisplayHeading, SocialIcons (Facebook, Google, TwitterX, Instagram, WhatsApp) |
78
+ | **Presentation** | CardWrapper, DataCard, LoadingCard, DetailView, DetailItem, Timeline, Pill (+Avatar, Button, Status, Indicator, Delta), InfoRow, AccordionSection, FaqAccordion |
78
79
  | **States** | EmptyState, EmptyStateNoResults, EmptyStateNoData, EmptyStateNotFound |
80
+ | **Status** | StatusBadge, DetailField, StatsGrid |
79
81
  | **Loading** | LoadingState, LoadingOverlay |
80
82
  | **Skeletons** | SkeletonTable, SkeletonList, SkeletonCard, SkeletonGrid |
81
83
  | **Utilities** | cn, buildFilterParams, buildSearchParams, getApiParams |
82
84
 
85
+ > **v0.5.0:** CardWrapper, DetailView, Timeline, Pill, InfoRow, AccordionSection and more are now server-safe — import from the root entry for PPR/donut patterns in Next.js 16.
86
+
83
87
  ### `@classytic/fluid/client/core`
84
88
 
85
89
  | Category | Components |
@@ -119,7 +123,8 @@ EventCalendar, CalendarWithDetail, CalendarDayDetail
119
123
  | **Toggle** | CheckboxInput, RadioInput, SwitchInput |
120
124
  | **Date/Time** | DateInput, DateRangeInput, DateTimeInput, DateRangeFilter |
121
125
  | **Tags** | TagInput, TagChoiceInput |
122
- | **Special** | SlugField, OTPInput, FileUploadInput, PhoneInput (in client/core) |
126
+ | **Phone** | FormPhoneInput |
127
+ | **Special** | SlugField, OTPInput, FileUploadInput |
123
128
  | **Layout** | FormSection, FormGrid, FormFieldArray, FormFieldArrayItem |
124
129
  | **Feedback** | FormErrorSummary |
125
130
 
@@ -149,6 +154,7 @@ CommandSearch (+ Input, List, Group, Item, Empty, Separator), useCommandSearch,
149
154
  | `useDebouncedCallback` | client/hooks | Debounce a function callback |
150
155
  | `useCopyToClipboard` | client/hooks | Copy text with feedback state |
151
156
  | `useBaseSearch` | client/hooks | Full search state with URL sync |
157
+ | `createSearchHook` | client/hooks | Factory to pre-configure useBaseSearch |
152
158
  | `useScrollDetection` | client/hooks | Detect scroll position/direction |
153
159
  | `useLocalStorage` | client/hooks | Typed localStorage hook |
154
160
  | `useKeyboardShortcut` | client/hooks | Register keyboard shortcuts |
@@ -180,6 +186,19 @@ import "@classytic/fluid/styles.css";
180
186
  | `@tanstack/react-table` | DataTable component |
181
187
  | `date-fns` | DateInput, DateRangeInput, EventCalendar |
182
188
 
189
+ ## Key Features (v0.5.0)
190
+
191
+ - **Server-safe by default** — Presentation components (cards, timelines, pills, detail views) work in React Server Components for PPR/donut patterns
192
+ - **React 19** — ref-as-prop (no forwardRef), modern hooks
193
+ - **shadcn/ui + Base UI** — Uses `render` prop pattern (not Radix `asChild`), `@/components/ui/*` resolved at consumer runtime
194
+ - **Form integration** — Every form component works with or without react-hook-form via conditional Controller
195
+ - **`clearable` selects** — `SelectInput` and `CompactSelect` support `clearable` prop for auto-generated clear options
196
+ - **`createSearchHook` factory** — Pre-configure search with typed filter fields and URL sync
197
+ - **5 dashboard presets** — InsetSidebar, DualSidebar, FloatingSidebar, MiniSidebar, TopbarRail
198
+ - **Composable search** — `Search.Root/Input/TypeInput/Filters/ActiveFilters/Actions`
199
+ - **757 tests** across 50 test files
200
+ - **Biome** for linting/formatting, **Knip** for dead code detection
201
+
183
202
  ## Dev
184
203
 
185
204
  ```bash
@@ -187,6 +206,7 @@ npm run build # Build with tsdown
187
206
  npm run dev # Watch mode
188
207
  npm run typecheck # Type check
189
208
  npm run clean # Remove dist
209
+ npm test # Run vitest
190
210
  ```
191
211
 
192
212
  ## License
@@ -1,11 +1,10 @@
1
- import * as react_jsx_runtime0 from "react/jsx-runtime";
2
1
  import { ReactNode } from "react";
2
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
3
3
 
4
4
  //#region src/components/event-calendar.d.ts
5
5
  interface CalendarEvent {
6
6
  id: string;
7
7
  date: Date | string;
8
- [key: string]: any;
9
8
  }
10
9
  interface EventCalendarProps<T extends CalendarEvent> {
11
10
  /** Events to display on the calendar */
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
3
  import { t as cn } from "../utils-DQ5SCVoW.mjs";
4
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
4
  import { useCallback, useMemo, useState } from "react";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { ChevronLeft, ChevronRight } from "lucide-react";
7
7
  import { Button } from "@/components/ui/button";
8
8
  import { addMonths, eachDayOfInterval, endOfMonth, format, getDay, isSameDay, isSameMonth, startOfMonth, subMonths } from "date-fns";
@@ -131,7 +131,7 @@ function EventCalendar({ events = [], selectedDate: controlledSelectedDate, onDa
131
131
  className: cn("bg-card p-2 cursor-pointer transition-colors hover:bg-muted/50", isSelected && "ring-2 ring-primary ring-inset bg-primary/5", !isCurrentMonth && "opacity-50", isLoading && "pointer-events-none opacity-50"),
132
132
  style: { minHeight: minCellHeight },
133
133
  onClick: () => handleDateClick(day),
134
- children: renderDayContent ? renderDayContent(day, dayEvents, isSelected) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
134
+ children: renderDayContent ? renderDayContent(day, dayEvents, isSelected) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
135
135
  className: cn("flex h-7 w-7 items-center justify-center rounded-full text-sm", isToday && "bg-primary text-primary-foreground font-semibold"),
136
136
  children: format(day, "d")
137
137
  }), renderIndicators(dayEvents)] })
@@ -188,10 +188,10 @@ function CalendarWithDetail({ renderDetail, detailTitle, detailEmptyMessage, lay
188
188
  });
189
189
  return /* @__PURE__ */ jsx("div", {
190
190
  className: cn("gap-4", isHorizontal ? "flex" : "flex flex-col", isHorizontal && "items-start"),
191
- children: detailFirst ? /* @__PURE__ */ jsxs(Fragment, { children: [detailElement, /* @__PURE__ */ jsx("div", {
191
+ children: detailFirst ? /* @__PURE__ */ jsxs(Fragment$1, { children: [detailElement, /* @__PURE__ */ jsx("div", {
192
192
  className: "flex-1",
193
193
  children: calendarElement
194
- })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
194
+ })] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
195
195
  className: isHorizontal ? "flex-1" : "",
196
196
  children: calendarElement
197
197
  }), detailElement] })
@@ -0,0 +1,94 @@
1
+ import * as react from "react";
2
+ import { ReactNode } from "react";
3
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
4
+
5
+ //#region src/components/color-picker/types.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
+ interface ColorPickerProps {
20
+ /** Current color value (hex string, e.g. "#ff0000") */
21
+ value?: string;
22
+ /** Default color if value is not provided */
23
+ defaultValue?: string;
24
+ /** Callback with [R, G, B, Alpha(0-1)] when color changes */
25
+ onChange?: (color: RGBA) => void;
26
+ className?: string;
27
+ children?: ReactNode;
28
+ }
29
+ interface ColorPickerSelectionProps {
30
+ className?: string;
31
+ }
32
+ interface ColorPickerHueProps {
33
+ className?: string;
34
+ }
35
+ interface ColorPickerAlphaProps {
36
+ className?: string;
37
+ }
38
+ interface ColorPickerEyeDropperProps {
39
+ className?: string;
40
+ children?: ReactNode;
41
+ }
42
+ interface ColorPickerInputProps {
43
+ className?: string;
44
+ /** Show alpha percentage alongside hex */
45
+ showAlpha?: boolean;
46
+ }
47
+ interface ColorPickerPreviewProps {
48
+ className?: string;
49
+ }
50
+ //#endregion
51
+ //#region src/components/color-picker/color-picker.d.ts
52
+ declare function ColorPicker({
53
+ value,
54
+ defaultValue,
55
+ onChange,
56
+ className,
57
+ children
58
+ }: ColorPickerProps): react_jsx_runtime0.JSX.Element;
59
+ //#endregion
60
+ //#region src/components/color-picker/color-picker-alpha.d.ts
61
+ declare function ColorPickerAlpha({
62
+ className
63
+ }: ColorPickerAlphaProps): react_jsx_runtime0.JSX.Element;
64
+ //#endregion
65
+ //#region src/components/color-picker/color-picker-eye-dropper.d.ts
66
+ declare function ColorPickerEyeDropper({
67
+ className,
68
+ children
69
+ }: ColorPickerEyeDropperProps): react_jsx_runtime0.JSX.Element;
70
+ //#endregion
71
+ //#region src/components/color-picker/color-picker-hue.d.ts
72
+ declare function ColorPickerHue({
73
+ className
74
+ }: ColorPickerHueProps): react_jsx_runtime0.JSX.Element;
75
+ //#endregion
76
+ //#region src/components/color-picker/color-picker-input.d.ts
77
+ declare function ColorPickerInput({
78
+ className,
79
+ showAlpha
80
+ }: ColorPickerInputProps): react_jsx_runtime0.JSX.Element;
81
+ //#endregion
82
+ //#region src/components/color-picker/color-picker-preview.d.ts
83
+ declare function ColorPickerPreview({
84
+ className
85
+ }: ColorPickerPreviewProps): react_jsx_runtime0.JSX.Element;
86
+ //#endregion
87
+ //#region src/components/color-picker/color-picker-selection.d.ts
88
+ declare const ColorPickerSelection: react.NamedExoticComponent<ColorPickerSelectionProps>;
89
+ //#endregion
90
+ //#region src/components/color-picker/context.d.ts
91
+ declare function useColorPicker(): ColorPickerContextValue;
92
+ declare function useColorPickerHex(): string;
93
+ //#endregion
94
+ 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,392 @@
1
+ "use client";
2
+
3
+ import { t as cn } from "../utils-DQ5SCVoW.mjs";
4
+ import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
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/utils.ts
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
+
93
+ //#endregion
94
+ //#region src/components/color-picker/context.tsx
95
+ const ColorPickerContext = createContext(void 0);
96
+ function useColorPicker() {
97
+ const context = useContext(ColorPickerContext);
98
+ if (!context) throw new Error("useColorPicker must be used within a ColorPicker");
99
+ return context;
100
+ }
101
+ function useColorPickerHex() {
102
+ const { hue, saturation, lightness } = useColorPicker();
103
+ return hslToHex(hue, saturation, lightness);
104
+ }
105
+
106
+ //#endregion
107
+ //#region src/components/color-picker/color-picker.tsx
108
+ function ColorPicker({ value, defaultValue = "#000000", onChange, className, children }) {
109
+ const [ih, is, il] = hexToHsl(value || defaultValue);
110
+ const [hue, setHue] = useState(ih || 0);
111
+ const [saturation, setSaturation] = useState(is || 100);
112
+ const [lightness, setLightness] = useState(il || 50);
113
+ const [alpha, setAlpha] = useState(100);
114
+ const notify = useCallback((h, s, l, a) => {
115
+ if (!onChange) return;
116
+ const [r, g, b] = hslToRgb(h, s, l);
117
+ onChange([
118
+ r,
119
+ g,
120
+ b,
121
+ a / 100
122
+ ]);
123
+ }, [onChange]);
124
+ const setHueNotify = useCallback((v) => {
125
+ setHue(v);
126
+ notify(v, saturation, lightness, alpha);
127
+ }, [
128
+ saturation,
129
+ lightness,
130
+ alpha,
131
+ notify
132
+ ]);
133
+ const setSaturationNotify = useCallback((v) => {
134
+ setSaturation(v);
135
+ notify(hue, v, lightness, alpha);
136
+ }, [
137
+ hue,
138
+ lightness,
139
+ alpha,
140
+ notify
141
+ ]);
142
+ const setLightnessNotify = useCallback((v) => {
143
+ setLightness(v);
144
+ notify(hue, saturation, v, alpha);
145
+ }, [
146
+ hue,
147
+ saturation,
148
+ alpha,
149
+ notify
150
+ ]);
151
+ const setAlphaNotify = useCallback((v) => {
152
+ setAlpha(v);
153
+ notify(hue, saturation, lightness, v);
154
+ }, [
155
+ hue,
156
+ saturation,
157
+ lightness,
158
+ notify
159
+ ]);
160
+ const getCurrentColor = useCallback(() => {
161
+ const [r, g, b] = hslToRgb(hue, saturation, lightness);
162
+ return [
163
+ r,
164
+ g,
165
+ b,
166
+ alpha / 100
167
+ ];
168
+ }, [
169
+ hue,
170
+ saturation,
171
+ lightness,
172
+ alpha
173
+ ]);
174
+ const contextValue = useMemo(() => ({
175
+ hue,
176
+ saturation,
177
+ lightness,
178
+ alpha,
179
+ setHue: setHueNotify,
180
+ setSaturation: setSaturationNotify,
181
+ setLightness: setLightnessNotify,
182
+ setAlpha: setAlphaNotify,
183
+ getCurrentColor,
184
+ onChange
185
+ }), [
186
+ hue,
187
+ saturation,
188
+ lightness,
189
+ alpha,
190
+ setHueNotify,
191
+ setSaturationNotify,
192
+ setLightnessNotify,
193
+ setAlphaNotify,
194
+ getCurrentColor,
195
+ onChange
196
+ ]);
197
+ return /* @__PURE__ */ jsx(ColorPickerContext.Provider, {
198
+ value: contextValue,
199
+ children: /* @__PURE__ */ jsx("div", {
200
+ className: cn("flex size-full flex-col gap-4", className),
201
+ children
202
+ })
203
+ });
204
+ }
205
+
206
+ //#endregion
207
+ //#region src/components/color-picker/color-picker-slider.tsx
208
+ function ColorSlider({ value, max, onChange, trackClassName, trackStyle, children, className, ariaLabel }) {
209
+ const ref = useRef(null);
210
+ const [dragging, setDragging] = useState(false);
211
+ const update = useCallback((clientX) => {
212
+ if (!ref.current) return;
213
+ const rect = ref.current.getBoundingClientRect();
214
+ const x = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
215
+ onChange(Math.round(x * max));
216
+ }, [max, onChange]);
217
+ const handlePointerDown = useCallback((e) => {
218
+ e.preventDefault();
219
+ setDragging(true);
220
+ update(e.clientX);
221
+ }, [update]);
222
+ useEffect(() => {
223
+ if (!dragging) return;
224
+ const onMove = (e) => update(e.clientX);
225
+ const onUp = () => setDragging(false);
226
+ window.addEventListener("pointermove", onMove);
227
+ window.addEventListener("pointerup", onUp);
228
+ return () => {
229
+ window.removeEventListener("pointermove", onMove);
230
+ window.removeEventListener("pointerup", onUp);
231
+ };
232
+ }, [dragging, update]);
233
+ return /* @__PURE__ */ jsxs("div", {
234
+ ref,
235
+ className: cn("relative flex h-4 w-full touch-none items-center", className),
236
+ onPointerDown: handlePointerDown,
237
+ role: "slider",
238
+ "aria-label": ariaLabel,
239
+ "aria-valuemin": 0,
240
+ "aria-valuemax": max,
241
+ "aria-valuenow": value,
242
+ children: [/* @__PURE__ */ jsx("div", {
243
+ className: cn("relative h-3 w-full rounded-full", trackClassName),
244
+ style: trackStyle,
245
+ children
246
+ }), /* @__PURE__ */ jsx("div", {
247
+ className: "pointer-events-none absolute top-0 h-4 w-4 -translate-x-1/2 rounded-full border border-primary/50 bg-background shadow",
248
+ style: { left: `${value / max * 100}%` }
249
+ })]
250
+ });
251
+ }
252
+
253
+ //#endregion
254
+ //#region src/components/color-picker/color-picker-alpha.tsx
255
+ const CHECKER_BG = "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==')";
256
+ function ColorPickerAlpha({ className }) {
257
+ const { alpha, setAlpha } = useColorPicker();
258
+ return /* @__PURE__ */ jsx(ColorSlider, {
259
+ value: alpha,
260
+ max: 100,
261
+ onChange: setAlpha,
262
+ trackStyle: {
263
+ backgroundImage: CHECKER_BG,
264
+ backgroundSize: "contain"
265
+ },
266
+ className,
267
+ ariaLabel: "Opacity",
268
+ children: /* @__PURE__ */ jsx("div", { className: "absolute inset-0 rounded-full bg-gradient-to-r from-transparent to-black/50 dark:to-white/50" })
269
+ });
270
+ }
271
+
272
+ //#endregion
273
+ //#region src/components/color-picker/color-picker-eye-dropper.tsx
274
+ function ColorPickerEyeDropper({ className, children }) {
275
+ const { setHue, setSaturation, setLightness, setAlpha } = useColorPicker();
276
+ const handleEyeDropper = async () => {
277
+ try {
278
+ const [h, s, l] = hexToHsl((await new EyeDropper().open()).sRGBHex);
279
+ setHue(h);
280
+ setSaturation(s);
281
+ setLightness(l);
282
+ setAlpha(100);
283
+ } catch {}
284
+ };
285
+ return /* @__PURE__ */ jsx(Button, {
286
+ type: "button",
287
+ variant: "outline",
288
+ size: "icon",
289
+ className: cn("shrink-0", className),
290
+ onClick: handleEyeDropper,
291
+ children: children || /* @__PURE__ */ jsx(Pipette, { className: "h-4 w-4" })
292
+ });
293
+ }
294
+
295
+ //#endregion
296
+ //#region src/components/color-picker/color-picker-hue.tsx
297
+ const HUE_GRADIENT = "linear-gradient(90deg, #FF0000, #FFFF00, #00FF00, #00FFFF, #0000FF, #FF00FF, #FF0000)";
298
+ function ColorPickerHue({ className }) {
299
+ const { hue, setHue } = useColorPicker();
300
+ return /* @__PURE__ */ jsx(ColorSlider, {
301
+ value: hue,
302
+ max: 360,
303
+ onChange: setHue,
304
+ trackStyle: { background: HUE_GRADIENT },
305
+ className,
306
+ ariaLabel: "Hue"
307
+ });
308
+ }
309
+
310
+ //#endregion
311
+ //#region src/components/color-picker/color-picker-input.tsx
312
+ function ColorPickerInput({ className, showAlpha = false }) {
313
+ const { hue, saturation, lightness, alpha } = useColorPicker();
314
+ const hex = hslToHex(hue, saturation, lightness);
315
+ return /* @__PURE__ */ jsxs("div", {
316
+ className: cn("flex items-center gap-2", className),
317
+ children: [/* @__PURE__ */ jsx(Input, {
318
+ readOnly: true,
319
+ value: hex,
320
+ className: "flex-1"
321
+ }), showAlpha && /* @__PURE__ */ jsxs("span", {
322
+ className: "text-sm text-muted-foreground",
323
+ children: [Math.round(alpha), "%"]
324
+ })]
325
+ });
326
+ }
327
+
328
+ //#endregion
329
+ //#region src/components/color-picker/color-picker-preview.tsx
330
+ function ColorPickerPreview({ className }) {
331
+ const { hue, saturation, lightness, alpha } = useColorPicker();
332
+ const color = hslToHexa(hue, saturation, lightness, alpha / 100);
333
+ return /* @__PURE__ */ jsx("div", {
334
+ className: cn("h-10 w-10 rounded-md border border-input", className),
335
+ style: { backgroundColor: color }
336
+ });
337
+ }
338
+
339
+ //#endregion
340
+ //#region src/components/color-picker/color-picker-selection.tsx
341
+ const ColorPickerSelection = memo(function ColorPickerSelection({ className }) {
342
+ const containerRef = useRef(null);
343
+ const [isDragging, setIsDragging] = useState(false);
344
+ const { hue, saturation, lightness, setSaturation, setLightness } = useColorPicker();
345
+ 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]);
346
+ const positionX = saturation / 100;
347
+ const topLightness = positionX < .01 ? 100 : 50 + 50 * (1 - positionX);
348
+ const positionY = topLightness > 0 ? Math.max(0, Math.min(1, 1 - lightness / topLightness)) : 0;
349
+ const updateColor = useCallback((clientX, clientY) => {
350
+ if (!containerRef.current) return;
351
+ const rect = containerRef.current.getBoundingClientRect();
352
+ const x = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
353
+ const y = Math.max(0, Math.min(1, (clientY - rect.top) / rect.height));
354
+ setSaturation(x * 100);
355
+ setLightness((x < .01 ? 100 : 50 + 50 * (1 - x)) * (1 - y));
356
+ }, [setSaturation, setLightness]);
357
+ const handlePointerDown = useCallback((e) => {
358
+ e.preventDefault();
359
+ setIsDragging(true);
360
+ updateColor(e.clientX, e.clientY);
361
+ }, [updateColor]);
362
+ useEffect(() => {
363
+ if (!isDragging) return;
364
+ const onMove = (e) => updateColor(e.clientX, e.clientY);
365
+ const onUp = () => setIsDragging(false);
366
+ window.addEventListener("pointermove", onMove);
367
+ window.addEventListener("pointerup", onUp);
368
+ return () => {
369
+ window.removeEventListener("pointermove", onMove);
370
+ window.removeEventListener("pointerup", onUp);
371
+ };
372
+ }, [isDragging, updateColor]);
373
+ return /* @__PURE__ */ jsx("div", {
374
+ className: cn("relative size-full cursor-crosshair rounded", className),
375
+ onPointerDown: handlePointerDown,
376
+ ref: containerRef,
377
+ style: { background: backgroundGradient },
378
+ role: "application",
379
+ "aria-label": "Color saturation and lightness",
380
+ children: /* @__PURE__ */ jsx("div", {
381
+ className: "-translate-x-1/2 -translate-y-1/2 pointer-events-none absolute h-4 w-4 rounded-full border-2 border-white",
382
+ style: {
383
+ left: `${positionX * 100}%`,
384
+ top: `${positionY * 100}%`,
385
+ boxShadow: "0 0 0 1px rgba(0,0,0,0.5)"
386
+ }
387
+ })
388
+ });
389
+ });
390
+
391
+ //#endregion
392
+ export { ColorPicker, ColorPickerAlpha, ColorPickerEyeDropper, ColorPickerHue, ColorPickerInput, ColorPickerPreview, ColorPickerSelection, useColorPicker, useColorPickerHex };