@cladd-ui/react 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/calendar/Calendar.d.ts +21 -0
- package/calendar/Calendar.d.ts.map +1 -0
- package/calendar/Calendar.js +158 -0
- package/calendar/Calendar.js.map +1 -0
- package/calendar/CalendarIcon.d.ts +2 -0
- package/calendar/CalendarIcon.d.ts.map +1 -0
- package/calendar/CalendarIcon.js +5 -0
- package/calendar/CalendarIcon.js.map +1 -0
- package/calendar/DatePicker.d.ts +91 -0
- package/calendar/DatePicker.d.ts.map +1 -0
- package/calendar/DatePicker.js +79 -0
- package/calendar/DatePicker.js.map +1 -0
- package/calendar/index.d.ts +4 -0
- package/calendar/index.d.ts.map +1 -0
- package/calendar/index.js +3 -0
- package/calendar/index.js.map +1 -0
- package/components/AccordionContext.d.ts +22 -0
- package/components/AccordionContext.d.ts.map +1 -0
- package/components/AccordionContext.js +14 -0
- package/components/AccordionContext.js.map +1 -0
- package/components/AccordionItem.d.ts +27 -0
- package/components/AccordionItem.d.ts.map +1 -0
- package/components/AccordionItem.js +36 -0
- package/components/AccordionItem.js.map +1 -0
- package/components/AccordionRoot.d.ts +47 -0
- package/components/AccordionRoot.d.ts.map +1 -0
- package/components/AccordionRoot.js +53 -0
- package/components/AccordionRoot.js.map +1 -0
- package/components/Button.js +2 -2
- package/components/Button.js.map +1 -1
- package/components/Checkbox.js +2 -2
- package/components/Checkbox.js.map +1 -1
- package/components/CollapsibleContext.d.ts +36 -0
- package/components/CollapsibleContext.d.ts.map +1 -0
- package/components/CollapsibleContext.js +24 -0
- package/components/CollapsibleContext.js.map +1 -0
- package/components/CollapsibleIndicator.d.ts +45 -0
- package/components/CollapsibleIndicator.d.ts.map +1 -0
- package/components/CollapsibleIndicator.js +34 -0
- package/components/CollapsibleIndicator.js.map +1 -0
- package/components/CollapsiblePanel.d.ts +35 -0
- package/components/CollapsiblePanel.d.ts.map +1 -0
- package/components/CollapsiblePanel.js +104 -0
- package/components/CollapsiblePanel.js.map +1 -0
- package/components/CollapsibleRoot.d.ts +35 -0
- package/components/CollapsibleRoot.d.ts.map +1 -0
- package/components/CollapsibleRoot.js +43 -0
- package/components/CollapsibleRoot.js.map +1 -0
- package/components/CollapsibleTrigger.d.ts +21 -0
- package/components/CollapsibleTrigger.d.ts.map +1 -0
- package/components/CollapsibleTrigger.js +36 -0
- package/components/CollapsibleTrigger.js.map +1 -0
- package/components/ColorEditor.d.ts +84 -0
- package/components/ColorEditor.d.ts.map +1 -0
- package/components/ColorEditor.js +517 -0
- package/components/ColorEditor.js.map +1 -0
- package/components/ColorPicker.d.ts +127 -0
- package/components/ColorPicker.d.ts.map +1 -0
- package/components/ColorPicker.js +136 -0
- package/components/ColorPicker.js.map +1 -0
- package/components/{FocusableLayer.d.ts → FocusRing.d.ts} +2 -2
- package/components/FocusRing.d.ts.map +1 -0
- package/components/{FocusableLayer.js → FocusRing.js} +2 -2
- package/components/FocusRing.js.map +1 -0
- package/components/Input.d.ts +2 -0
- package/components/Input.d.ts.map +1 -1
- package/components/Input.js +4 -4
- package/components/Input.js.map +1 -1
- package/components/Link.js +2 -2
- package/components/Link.js.map +1 -1
- package/components/NumberScrubber.d.ts +2 -0
- package/components/NumberScrubber.d.ts.map +1 -1
- package/components/NumberScrubber.js +3 -3
- package/components/NumberScrubber.js.map +1 -1
- package/components/OTPField.d.ts.map +1 -1
- package/components/OTPField.js +2 -2
- package/components/OTPField.js.map +1 -1
- package/components/Popover.d.ts.map +1 -1
- package/components/Popover.js +7 -1
- package/components/Popover.js.map +1 -1
- package/components/Radio.js +2 -2
- package/components/Radio.js.map +1 -1
- package/components/Segmented.js +2 -2
- package/components/Segmented.js.map +1 -1
- package/components/Select.d.ts +2 -0
- package/components/Select.d.ts.map +1 -1
- package/components/Select.js +5 -5
- package/components/Select.js.map +1 -1
- package/components/Slider.d.ts +34 -3
- package/components/Slider.d.ts.map +1 -1
- package/components/Slider.js +27 -14
- package/components/Slider.js.map +1 -1
- package/components/Switch.js +2 -2
- package/components/Switch.js.map +1 -1
- package/components/Tab.d.ts +19 -0
- package/components/Tab.d.ts.map +1 -0
- package/components/Tab.js +22 -0
- package/components/Tab.js.map +1 -0
- package/components/TabPanel.d.ts +31 -0
- package/components/TabPanel.d.ts.map +1 -0
- package/components/TabPanel.js +19 -0
- package/components/TabPanel.js.map +1 -0
- package/components/Tabs.d.ts +33 -0
- package/components/Tabs.d.ts.map +1 -0
- package/components/Tabs.js +36 -0
- package/components/Tabs.js.map +1 -0
- package/components/TabsContext.d.ts +25 -0
- package/components/TabsContext.d.ts.map +1 -0
- package/components/TabsContext.js +18 -0
- package/components/TabsContext.js.map +1 -0
- package/components/TabsList.d.ts +12 -0
- package/components/TabsList.d.ts.map +1 -0
- package/components/TabsList.js +52 -0
- package/components/TabsList.js.map +1 -0
- package/components/Textarea.d.ts +2 -0
- package/components/Textarea.d.ts.map +1 -1
- package/components/Textarea.js +3 -3
- package/components/Textarea.js.map +1 -1
- package/components/ThemeContext.d.ts +26 -0
- package/components/ThemeContext.d.ts.map +1 -1
- package/components/ThemeContext.js.map +1 -1
- package/components/ToggleButton.d.ts +44 -0
- package/components/ToggleButton.d.ts.map +1 -0
- package/components/ToggleButton.js +66 -0
- package/components/ToggleButton.js.map +1 -0
- package/components/ToggleGroup.d.ts +50 -0
- package/components/ToggleGroup.d.ts.map +1 -0
- package/components/ToggleGroup.js +57 -0
- package/components/ToggleGroup.js.map +1 -0
- package/components/ToggleGroupContext.d.ts +31 -0
- package/components/ToggleGroupContext.d.ts.map +1 -0
- package/components/ToggleGroupContext.js +14 -0
- package/components/ToggleGroupContext.js.map +1 -0
- package/components/icons/ArrowLeftIcon.d.ts +2 -0
- package/components/icons/ArrowLeftIcon.d.ts.map +1 -0
- package/components/icons/ArrowLeftIcon.js +5 -0
- package/components/icons/ArrowLeftIcon.js.map +1 -0
- package/components/icons/ChevronLeftIcon.d.ts +2 -0
- package/components/icons/ChevronLeftIcon.d.ts.map +1 -0
- package/components/icons/ChevronLeftIcon.js +5 -0
- package/components/icons/ChevronLeftIcon.js.map +1 -0
- package/components/icons/FlipIcon.d.ts +2 -0
- package/components/icons/FlipIcon.d.ts.map +1 -0
- package/components/icons/FlipIcon.js +5 -0
- package/components/icons/FlipIcon.js.map +1 -0
- package/components/icons/GradientColorIcon.d.ts +2 -0
- package/components/icons/GradientColorIcon.d.ts.map +1 -0
- package/components/icons/GradientColorIcon.js +5 -0
- package/components/icons/GradientColorIcon.js.map +1 -0
- package/components/icons/SolidColorIcon.d.ts +2 -0
- package/components/icons/SolidColorIcon.d.ts.map +1 -0
- package/components/icons/SolidColorIcon.js +5 -0
- package/components/icons/SolidColorIcon.js.map +1 -0
- package/index.d.ts +20 -1
- package/index.d.ts.map +1 -1
- package/index.js +18 -0
- package/index.js.map +1 -1
- package/package.json +11 -1
- package/shared/color.d.ts +122 -0
- package/shared/color.d.ts.map +1 -0
- package/shared/color.js +319 -0
- package/shared/color.js.map +1 -0
- package/styles/colors.css +4 -4
- package/components/FocusableLayer.d.ts.map +0 -1
- package/components/FocusableLayer.js.map +0 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { cloneElement } from 'react';
|
|
2
|
+
import { useCollapsibleContext } from './CollapsibleContext.js';
|
|
3
|
+
/**
|
|
4
|
+
* Wraps a single child element to act as the disclosure trigger. **Clones** the
|
|
5
|
+
* child (Radix-style `asChild`) to attach the toggle `onClick` plus the aria
|
|
6
|
+
* wiring (`aria-expanded`, `aria-controls`, `id`) and a `data-open` attribute -
|
|
7
|
+
* so you bring your own markup and the kit injects only behavior and state.
|
|
8
|
+
*
|
|
9
|
+
* Because the cloned child carries `data-open`, rotating or swapping your own
|
|
10
|
+
* chevron is pure CSS: add `group` to the child and
|
|
11
|
+
* `group-data-[open]:rotate-90` to the icon. For state in JS, drop a
|
|
12
|
+
* `CollapsibleIndicator` inside or call `useCollapsibleContext()`.
|
|
13
|
+
*
|
|
14
|
+
* Composes with any existing `onClick` on the child, and no-ops the toggle while
|
|
15
|
+
* the disclosure is `disabled`.
|
|
16
|
+
*/
|
|
17
|
+
export const CollapsibleTrigger = ({ children }) => {
|
|
18
|
+
const { open, toggle, disabled, triggerId, panelId } = useCollapsibleContext();
|
|
19
|
+
const child = children;
|
|
20
|
+
const originalOnClick = child?.props?.onClick;
|
|
21
|
+
const onClick = (e) => {
|
|
22
|
+
originalOnClick?.(e);
|
|
23
|
+
if (!disabled)
|
|
24
|
+
toggle();
|
|
25
|
+
};
|
|
26
|
+
return cloneElement(child, {
|
|
27
|
+
id: triggerId,
|
|
28
|
+
onClick,
|
|
29
|
+
'aria-expanded': open,
|
|
30
|
+
'aria-controls': panelId,
|
|
31
|
+
'aria-disabled': disabled || undefined,
|
|
32
|
+
'data-open': open || undefined,
|
|
33
|
+
'data-disabled': disabled || undefined,
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=CollapsibleTrigger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CollapsibleTrigger.js","sourceRoot":"","sources":["../../../src/components/CollapsibleTrigger.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuC,MAAM,OAAO,CAAC;AAE1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAO7D;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,EAAE,QAAQ,EAA2B,EAAE,EAAE;IAC1E,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,GAClD,qBAAqB,EAAE,CAAC;IAE1B,MAAM,KAAK,GAAG,QAA6B,CAAC;IAC5C,MAAM,eAAe,GAAG,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC;IAE9C,MAAM,OAAO,GAAG,CAAC,CAAa,EAAE,EAAE;QAChC,eAAe,EAAE,CAAC,CAAC,CAAC,CAAC;QACrB,IAAI,CAAC,QAAQ;YAAE,MAAM,EAAE,CAAC;IAC1B,CAAC,CAAC;IAEF,OAAO,YAAY,CAAC,KAAK,EAAE;QACzB,EAAE,EAAE,SAAS;QACb,OAAO;QACP,eAAe,EAAE,IAAI;QACrB,eAAe,EAAE,OAAO;QACxB,eAAe,EAAE,QAAQ,IAAI,SAAS;QACtC,WAAW,EAAE,IAAI,IAAI,SAAS;QAC9B,eAAe,EAAE,QAAQ,IAAI,SAAS;KACvC,CAAC,CAAC;AACL,CAAC,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Ref } from 'react';
|
|
2
|
+
import { ColorEditorValue, ColorInput, ColorValue, GradientInput } from '../shared/color.js';
|
|
3
|
+
/** Size token for the inner controls (scrubbers, hex input, gradient buttons). */
|
|
4
|
+
export type ColorEditorControlSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
5
|
+
/** Which channel scrubbers the inputs row shows. Does not affect the emitted value. */
|
|
6
|
+
export type ColorEditorFormat = 'rgb' | 'hsl' | 'hsb';
|
|
7
|
+
interface ColorEditorBaseProps {
|
|
8
|
+
/** Size of the inner controls — scrubbers, hex input, gradient buttons. Default `'md'`.
|
|
9
|
+
*
|
|
10
|
+
* The Solid/Gradient toolbar is rendered one step smaller.
|
|
11
|
+
*/
|
|
12
|
+
controlSize?: ColorEditorControlSize;
|
|
13
|
+
/** Render the surface outline on the inner controls — channel/alpha scrubbers, hex input, and the Solid/Gradient toolbar.
|
|
14
|
+
*
|
|
15
|
+
* The gradient bar's flip/angle controls stay ghost regardless. Default `true`.
|
|
16
|
+
*/
|
|
17
|
+
controlOutline?: boolean;
|
|
18
|
+
/** Show the alpha slider and the alpha scrubber. Default `true`. */
|
|
19
|
+
alpha?: boolean;
|
|
20
|
+
/** Show the channel-scrubber row. Default `true`. */
|
|
21
|
+
inputs?: boolean;
|
|
22
|
+
/** Which channels the scrubber row shows. Default `'rgb'`. */
|
|
23
|
+
format?: ColorEditorFormat;
|
|
24
|
+
/** Show the hex input. Default `true`. */
|
|
25
|
+
hexInput?: boolean;
|
|
26
|
+
/** Angle control in gradient mode: a 45°-step button, or a degree scrubber. Default `'scrubber'`. */
|
|
27
|
+
angleControl?: 'button' | 'scrubber';
|
|
28
|
+
/** Preset colors rendered as a row of thumbs. Clicking one applies it to the color (or the selected gradient stop).
|
|
29
|
+
*
|
|
30
|
+
* Solid colors only.
|
|
31
|
+
*/
|
|
32
|
+
swatches?: ColorInput[];
|
|
33
|
+
/** Dim the panel and block interaction. */
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/** Block interaction without the dimmed treatment. */
|
|
36
|
+
readOnly?: boolean;
|
|
37
|
+
/** Debounce `onChange` calls in ms. Fires once after changes stop for N ms. Defaults to `0` (immediate). */
|
|
38
|
+
debounce?: number;
|
|
39
|
+
/** Throttle `onChange` calls in ms. Fires immediately, then at most once per N ms while changing, with a trailing call for the final value. Defaults to `0` (immediate).
|
|
40
|
+
*
|
|
41
|
+
* Takes precedence over `debounce`.
|
|
42
|
+
*/
|
|
43
|
+
throttle?: number;
|
|
44
|
+
/** Content rendered above the panel, before the controls.
|
|
45
|
+
*
|
|
46
|
+
* Stays interactive when `disabled` (only the panel below dims).
|
|
47
|
+
*
|
|
48
|
+
* For inherit toggles, titles, eyedroppers, etc.
|
|
49
|
+
* */
|
|
50
|
+
header?: React.ReactNode;
|
|
51
|
+
/** Content rendered below the panel, after the swatches. Stays interactive when `disabled`. */
|
|
52
|
+
footer?: React.ReactNode;
|
|
53
|
+
/** Extra classes for the panel root. The panel is full-width — size it via its container. */
|
|
54
|
+
className?: string;
|
|
55
|
+
/** Extra classes for the saturation/brightness area (e.g. to set its height). */
|
|
56
|
+
areaClassName?: string;
|
|
57
|
+
/** Forwarded to the panel root element. */
|
|
58
|
+
ref?: Ref<HTMLDivElement>;
|
|
59
|
+
}
|
|
60
|
+
type SolidColorEditorProps = ColorEditorBaseProps & {
|
|
61
|
+
/** Enable the Solid/Gradient switch and gradient editing. Default `false`. */
|
|
62
|
+
gradient?: false;
|
|
63
|
+
/** Controlled value. A CSS color string or any one channel set. */
|
|
64
|
+
value?: ColorInput;
|
|
65
|
+
/** Initial value (uncontrolled). */
|
|
66
|
+
defaultValue?: ColorInput;
|
|
67
|
+
/** Fires on every change with the full color. */
|
|
68
|
+
onChange?: (value: ColorValue) => void;
|
|
69
|
+
};
|
|
70
|
+
type GradientColorEditorProps = ColorEditorBaseProps & {
|
|
71
|
+
gradient: true;
|
|
72
|
+
/** Controlled value. A CSS color/gradient string, a channel set, or a gradient object. */
|
|
73
|
+
value?: ColorInput | GradientInput;
|
|
74
|
+
/** Initial value (uncontrolled). */
|
|
75
|
+
defaultValue?: ColorInput | GradientInput;
|
|
76
|
+
/** Fires on every change with a discriminated `solid` / `linear` value. */
|
|
77
|
+
onChange?: (value: ColorEditorValue) => void;
|
|
78
|
+
};
|
|
79
|
+
export type ColorEditorProps = SolidColorEditorProps | GradientColorEditorProps;
|
|
80
|
+
/** Shape of `ColorEditor` defaults that can be supplied via `CladdProvider`'s `defaults` prop. */
|
|
81
|
+
export type ColorEditorDefaultProps = Partial<Omit<ColorEditorBaseProps, 'ref'>>;
|
|
82
|
+
export declare const ColorEditor: (props: ColorEditorProps) => import("react/jsx-runtime").JSX.Element;
|
|
83
|
+
export {};
|
|
84
|
+
//# sourceMappingURL=ColorEditor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ColorEditor.d.ts","sourceRoot":"","sources":["../../../src/components/ColorEditor.tsx"],"names":[],"mappings":"AAAA,OAAO,EAGL,GAAG,EAIJ,MAAM,OAAO,CAAC;AAIf,OAAO,EAGL,gBAAgB,EAChB,UAAU,EACV,UAAU,EAEV,aAAa,EAYd,MAAM,iBAAiB,CAAC;AAYzB,kFAAkF;AAClF,MAAM,MAAM,sBAAsB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,CAAC;AAEvE,uFAAuF;AACvF,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAEtD,UAAU,oBAAoB;IAC5B;;;OAGG;IACH,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qGAAqG;IACrG,YAAY,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;IACxB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4GAA4G;IAC5G,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;SAKK;IACL,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,6FAA6F;IAC7F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2CAA2C;IAC3C,GAAG,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;CAC3B;AAED,KAAK,qBAAqB,GAAG,oBAAoB,GAAG;IAClD,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,mEAAmE;IACnE,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,oCAAoC;IACpC,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,iDAAiD;IACjD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACxC,CAAC;AAEF,KAAK,wBAAwB,GAAG,oBAAoB,GAAG;IACrD,QAAQ,EAAE,IAAI,CAAC;IACf,0FAA0F;IAC1F,KAAK,CAAC,EAAE,UAAU,GAAG,aAAa,CAAC;IACnC,oCAAoC;IACpC,YAAY,CAAC,EAAE,UAAU,GAAG,aAAa,CAAC;IAC1C,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,qBAAqB,GAAG,wBAAwB,CAAC;AAEhF,kGAAkG;AAClG,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAC3C,IAAI,CAAC,oBAAoB,EAAE,KAAK,CAAC,CAClC,CAAC;AAkFF,eAAO,MAAM,WAAW,GAAI,OAAO,gBAAgB,4CA83BlD,CAAC"}
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState, } from 'react';
|
|
3
|
+
import { useComponentDefaults } from '../hooks/use-component-defaults.js';
|
|
4
|
+
import { cn } from '../shared/cn.js';
|
|
5
|
+
import { buildColorValue, CHECKER, colorSignature, gradientCss, gradientPreviewCss, hslToHsv, hsvToHsl, hsvToRgb, isGradientInput, parseColor, parseGradient, rgbToHsv, } from '../shared/color.js';
|
|
6
|
+
import { Button } from './Button.js';
|
|
7
|
+
import { ArrowLeftIcon } from './icons/ArrowLeftIcon.js';
|
|
8
|
+
import { FlipIcon } from './icons/FlipIcon.js';
|
|
9
|
+
import { GradientColorIcon } from './icons/GradientColorIcon.js';
|
|
10
|
+
import { SolidColorIcon } from './icons/SolidColorIcon.js';
|
|
11
|
+
import { Input } from './Input.js';
|
|
12
|
+
import { NumberScrubber } from './NumberScrubber.js';
|
|
13
|
+
import { Segmented } from './Segmented.js';
|
|
14
|
+
import { SegmentedButton } from './SegmentedButton.js';
|
|
15
|
+
import { Toolbar } from './Toolbar.js';
|
|
16
|
+
// Fixed dimensions — customize the area via `areaClassName`, the panel via `className`.
|
|
17
|
+
const AREA_H = 'h-36';
|
|
18
|
+
const BAR_H = 'h-3';
|
|
19
|
+
const THUMB = 'size-4';
|
|
20
|
+
const THUMB_PX = 16;
|
|
21
|
+
const SWATCH = 'size-4';
|
|
22
|
+
/** The Solid/Gradient toolbar renders one step below `controlSize`. */
|
|
23
|
+
const TOOLBAR_SIZE = {
|
|
24
|
+
sm: 'xs',
|
|
25
|
+
md: 'sm',
|
|
26
|
+
lg: 'md',
|
|
27
|
+
xl: 'lg',
|
|
28
|
+
'2xl': 'xl',
|
|
29
|
+
};
|
|
30
|
+
const clamp = (n, min, max) => Math.min(max, Math.max(min, n));
|
|
31
|
+
const wrap360 = (n) => ((n % 360) + 360) % 360;
|
|
32
|
+
const r0 = (n) => Math.round(n);
|
|
33
|
+
/** Center-position a thumb so it never overshoots the track ends. */
|
|
34
|
+
const thumbLeft = (fraction) => `calc(${fraction} * (100% - ${THUMB_PX}px) + ${THUMB_PX / 2}px)`;
|
|
35
|
+
const THUMB_CLASS = 'absolute rounded-full border-2 border-white shadow-[0_0_0_1px_rgba(0,0,0,0.2),0_1px_3px_rgba(0,0,0,0.45)] outline-none focus-visible:ring-2 focus-visible:ring-cladd-primary';
|
|
36
|
+
const HUE_TRACK = 'linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)';
|
|
37
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
38
|
+
const cloneInternal = (d) => ({
|
|
39
|
+
mode: d.mode,
|
|
40
|
+
solid: { ...d.solid },
|
|
41
|
+
angle: d.angle,
|
|
42
|
+
stops: [
|
|
43
|
+
{ hsva: { ...d.stops[0].hsva }, position: d.stops[0].position },
|
|
44
|
+
{ hsva: { ...d.stops[1].hsva }, position: d.stops[1].position },
|
|
45
|
+
],
|
|
46
|
+
active: d.active,
|
|
47
|
+
});
|
|
48
|
+
const currentOf = (d) => d.mode === 'linear' ? d.stops[d.active].hsva : d.solid;
|
|
49
|
+
const sortedStops = (d) => d.stops
|
|
50
|
+
.map((s) => ({ color: buildColorValue(s.hsva), position: s.position }))
|
|
51
|
+
.sort((a, b) => a.position - b.position);
|
|
52
|
+
const isValidHex = (text) => /^[0-9a-fA-F]{3,8}$/.test(text) && [3, 4, 6, 8].includes(text.length);
|
|
53
|
+
const LetterIcon = (props) => {
|
|
54
|
+
return (_jsx("span", { className: "flex size-4 items-center justify-center text-cladd-xs font-medium text-cladd-fg-soft", children: props.children }));
|
|
55
|
+
};
|
|
56
|
+
export const ColorEditor = (props) => {
|
|
57
|
+
const resolved = useComponentDefaults('ColorEditor', props);
|
|
58
|
+
const { controlSize = 'md', controlOutline = true, alpha = true, inputs = true, format = 'rgb', hexInput = true, angleControl = 'scrubber', swatches, gradient = false, disabled = false, readOnly = false, debounce = 0, throttle = 0, header, footer, value, defaultValue, onChange, className, areaClassName, ref, } = resolved;
|
|
59
|
+
const interactive = !disabled && !readOnly;
|
|
60
|
+
const toolbarSize = TOOLBAR_SIZE[controlSize];
|
|
61
|
+
// The alpha % scrubber rides along with the hex input — show it only when
|
|
62
|
+
// alpha, the input row, and the hex input are all on. On its own the alpha
|
|
63
|
+
// *slider* already covers transparency, so a lone scrubber adds nothing.
|
|
64
|
+
const alphaInput = alpha && inputs && hexInput;
|
|
65
|
+
const seedFrom = (input) => {
|
|
66
|
+
if (gradient && isGradientInput(input)) {
|
|
67
|
+
const parsed = parseGradient(input);
|
|
68
|
+
const list = parsed?.stops ?? [];
|
|
69
|
+
const s0 = list[0] ?? { hsva: { h: 0, s: 0, v: 0, a: 1 }, position: 0 };
|
|
70
|
+
const s1 = list[list.length - 1] ?? {
|
|
71
|
+
hsva: { h: 0, s: 0, v: 100, a: 1 },
|
|
72
|
+
position: 100,
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
mode: 'linear',
|
|
76
|
+
solid: { ...s0.hsva },
|
|
77
|
+
angle: parsed?.angle ?? 90,
|
|
78
|
+
stops: [
|
|
79
|
+
{ hsva: { ...s0.hsva }, position: s0.position },
|
|
80
|
+
{ hsva: { ...s1.hsva }, position: s1.position },
|
|
81
|
+
],
|
|
82
|
+
active: 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const hsva = parseColor(input);
|
|
86
|
+
return {
|
|
87
|
+
mode: 'solid',
|
|
88
|
+
solid: hsva,
|
|
89
|
+
angle: 90,
|
|
90
|
+
stops: [
|
|
91
|
+
{ hsva: { ...hsva }, position: 0 },
|
|
92
|
+
{ hsva: { ...hsva, a: 0 }, position: 100 },
|
|
93
|
+
],
|
|
94
|
+
active: 0,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
const buildOutput = (d) => {
|
|
98
|
+
if (!gradient)
|
|
99
|
+
return buildColorValue(d.solid);
|
|
100
|
+
if (d.mode === 'solid') {
|
|
101
|
+
return { type: 'solid', ...buildColorValue(d.solid) };
|
|
102
|
+
}
|
|
103
|
+
const stops = sortedStops(d);
|
|
104
|
+
return {
|
|
105
|
+
type: 'linear',
|
|
106
|
+
angle: d.angle,
|
|
107
|
+
stops,
|
|
108
|
+
css: gradientCss(d.angle, stops),
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
const signatureOf = (d) => {
|
|
112
|
+
if (!gradient)
|
|
113
|
+
return colorSignature(d.solid);
|
|
114
|
+
if (d.mode === 'solid')
|
|
115
|
+
return `solid|${colorSignature(d.solid)}`;
|
|
116
|
+
return `linear|${d.angle}|${sortedStops(d)
|
|
117
|
+
.map((s) => `${s.position}:${s.color.hex}`)
|
|
118
|
+
.join('|')}`;
|
|
119
|
+
};
|
|
120
|
+
const isControlled = value !== undefined;
|
|
121
|
+
const internalRef = useRef(seedFrom(value ?? defaultValue));
|
|
122
|
+
const [internal, setInternal] = useState(internalRef.current);
|
|
123
|
+
const lastSigRef = useRef(signatureOf(internalRef.current));
|
|
124
|
+
const interactingRef = useRef(false);
|
|
125
|
+
// Rate-limit only the outward `onChange`; internal state always updates
|
|
126
|
+
// immediately so the panel stays responsive. Mirrors Slider's debounce /
|
|
127
|
+
// throttle semantics. The trailing call guarantees the final value is emitted.
|
|
128
|
+
const debounceTimer = useRef(null);
|
|
129
|
+
const throttleTimer = useRef(null);
|
|
130
|
+
const throttleLastFire = useRef(0);
|
|
131
|
+
const throttlePending = useRef(null);
|
|
132
|
+
const emitChange = (out) => {
|
|
133
|
+
if (!onChange)
|
|
134
|
+
return;
|
|
135
|
+
if (throttle > 0) {
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
const elapsed = now - throttleLastFire.current;
|
|
138
|
+
if (elapsed >= throttle) {
|
|
139
|
+
throttleLastFire.current = now;
|
|
140
|
+
throttlePending.current = null;
|
|
141
|
+
if (throttleTimer.current) {
|
|
142
|
+
clearTimeout(throttleTimer.current);
|
|
143
|
+
throttleTimer.current = null;
|
|
144
|
+
}
|
|
145
|
+
onChange(out);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
throttlePending.current = out;
|
|
149
|
+
if (!throttleTimer.current) {
|
|
150
|
+
throttleTimer.current = setTimeout(() => {
|
|
151
|
+
throttleTimer.current = null;
|
|
152
|
+
if (throttlePending.current !== null) {
|
|
153
|
+
const pending = throttlePending.current;
|
|
154
|
+
throttlePending.current = null;
|
|
155
|
+
throttleLastFire.current = Date.now();
|
|
156
|
+
onChange(pending);
|
|
157
|
+
}
|
|
158
|
+
}, throttle - elapsed);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (debounce > 0) {
|
|
163
|
+
if (debounceTimer.current)
|
|
164
|
+
clearTimeout(debounceTimer.current);
|
|
165
|
+
debounceTimer.current = setTimeout(() => onChange(out), debounce);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
onChange(out);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
return () => {
|
|
173
|
+
if (debounceTimer.current)
|
|
174
|
+
clearTimeout(debounceTimer.current);
|
|
175
|
+
if (throttleTimer.current)
|
|
176
|
+
clearTimeout(throttleTimer.current);
|
|
177
|
+
};
|
|
178
|
+
}, []);
|
|
179
|
+
// Sync external (controlled) value changes. Skipped while the user is
|
|
180
|
+
// interacting — mid-drag the component is the source of truth, and our own
|
|
181
|
+
// rapid onChange echoes would otherwise race back in and clobber state.
|
|
182
|
+
// The signature guard ignores the (non-interacting) echo of what we emitted;
|
|
183
|
+
// `active` is UI-only, so it's always preserved across a reseed.
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!isControlled || interactingRef.current)
|
|
186
|
+
return;
|
|
187
|
+
const seeded = seedFrom(value);
|
|
188
|
+
const sig = signatureOf(seeded);
|
|
189
|
+
if (sig !== lastSigRef.current) {
|
|
190
|
+
seeded.active = internalRef.current.active;
|
|
191
|
+
lastSigRef.current = sig;
|
|
192
|
+
internalRef.current = seeded;
|
|
193
|
+
setInternal(seeded);
|
|
194
|
+
}
|
|
195
|
+
// Only `value` — the helpers close over render-stable props.
|
|
196
|
+
}, [value]);
|
|
197
|
+
const patch = (mutate, emit = true) => {
|
|
198
|
+
const next = cloneInternal(internalRef.current);
|
|
199
|
+
mutate(next);
|
|
200
|
+
internalRef.current = next;
|
|
201
|
+
setInternal(next);
|
|
202
|
+
if (emit) {
|
|
203
|
+
lastSigRef.current = signatureOf(next);
|
|
204
|
+
emitChange(buildOutput(next));
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const cur = currentOf(internal);
|
|
208
|
+
const opaque = buildColorValue({ ...cur, a: 1 }).hex;
|
|
209
|
+
const curValue = buildColorValue(cur);
|
|
210
|
+
const rgb = curValue.rgb;
|
|
211
|
+
const hsl = curValue.hsl;
|
|
212
|
+
// ── pointer dragging ────────────────────────────────────────────────────────
|
|
213
|
+
const areaElRef = useRef(null);
|
|
214
|
+
const hueElRef = useRef(null);
|
|
215
|
+
const alphaElRef = useRef(null);
|
|
216
|
+
const gradientBarRef = useRef(null);
|
|
217
|
+
const drag = (el, e, onPos, padX = 0) => {
|
|
218
|
+
if (!interactive || !el || e.button !== 0)
|
|
219
|
+
return;
|
|
220
|
+
interactingRef.current = true;
|
|
221
|
+
const rect = el.getBoundingClientRect();
|
|
222
|
+
const travel = rect.width - 2 * padX;
|
|
223
|
+
const apply = (clientX, clientY) => {
|
|
224
|
+
onPos(clamp((clientX - rect.left - padX) / travel, 0, 1), clamp((clientY - rect.top) / rect.height, 0, 1));
|
|
225
|
+
};
|
|
226
|
+
apply(e.clientX, e.clientY);
|
|
227
|
+
const onMove = (ev) => {
|
|
228
|
+
ev.preventDefault();
|
|
229
|
+
apply(ev.clientX, ev.clientY);
|
|
230
|
+
};
|
|
231
|
+
const onUp = () => {
|
|
232
|
+
interactingRef.current = false;
|
|
233
|
+
document.removeEventListener('pointermove', onMove);
|
|
234
|
+
document.removeEventListener('pointerup', onUp);
|
|
235
|
+
};
|
|
236
|
+
document.addEventListener('pointermove', onMove);
|
|
237
|
+
document.addEventListener('pointerup', onUp);
|
|
238
|
+
};
|
|
239
|
+
const onAreaDown = (e) => drag(areaElRef.current, e, (x, y) => patch((d) => {
|
|
240
|
+
const c = currentOf(d);
|
|
241
|
+
c.s = x * 100;
|
|
242
|
+
c.v = (1 - y) * 100;
|
|
243
|
+
}));
|
|
244
|
+
const onHueDown = (e) => drag(hueElRef.current, e, (x) => patch((d) => {
|
|
245
|
+
currentOf(d).h = x * 360;
|
|
246
|
+
}), THUMB_PX / 2);
|
|
247
|
+
const onAlphaDown = (e) => drag(alphaElRef.current, e, (x) => patch((d) => {
|
|
248
|
+
currentOf(d).a = Math.round(x * 100) / 100;
|
|
249
|
+
}), THUMB_PX / 2);
|
|
250
|
+
// ── keyboard ────────────────────────────────────────────────────────────────
|
|
251
|
+
const kstep = (e) => (e.shiftKey ? 10 : 1);
|
|
252
|
+
const onAreaKey = (e) => {
|
|
253
|
+
if (!interactive)
|
|
254
|
+
return;
|
|
255
|
+
const d = kstep(e);
|
|
256
|
+
const map = {
|
|
257
|
+
ArrowLeft: () => patch((x) => (currentOf(x).s = clamp(cur.s - d, 0, 100))),
|
|
258
|
+
ArrowRight: () => patch((x) => (currentOf(x).s = clamp(cur.s + d, 0, 100))),
|
|
259
|
+
ArrowUp: () => patch((x) => (currentOf(x).v = clamp(cur.v + d, 0, 100))),
|
|
260
|
+
ArrowDown: () => patch((x) => (currentOf(x).v = clamp(cur.v - d, 0, 100))),
|
|
261
|
+
};
|
|
262
|
+
if (map[e.key]) {
|
|
263
|
+
e.preventDefault();
|
|
264
|
+
map[e.key]();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const onHueKey = (e) => {
|
|
268
|
+
if (!interactive)
|
|
269
|
+
return;
|
|
270
|
+
const d = kstep(e);
|
|
271
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
272
|
+
e.preventDefault();
|
|
273
|
+
patch((x) => (currentOf(x).h = wrap360(cur.h + (e.key === 'ArrowRight' ? d : -d))));
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
const onAlphaKey = (e) => {
|
|
277
|
+
if (!interactive)
|
|
278
|
+
return;
|
|
279
|
+
const d = (e.shiftKey ? 10 : 1) / 100;
|
|
280
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
281
|
+
e.preventDefault();
|
|
282
|
+
patch((x) => (currentOf(x).a = clamp(Math.round((cur.a + (e.key === 'ArrowRight' ? d : -d)) * 100) / 100, 0, 1)));
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
// ── channel scrubbers ─────────────────────────────────────────────────────────
|
|
286
|
+
const setRgb = (key, v) => patch((d) => {
|
|
287
|
+
const c = currentOf(d);
|
|
288
|
+
const next = { ...hsvToRgb(c) };
|
|
289
|
+
next[key] = clamp(r0(v), 0, 255);
|
|
290
|
+
const h = rgbToHsv(next);
|
|
291
|
+
if (h.s !== 0)
|
|
292
|
+
c.h = h.h;
|
|
293
|
+
if (h.v !== 0)
|
|
294
|
+
c.s = h.s;
|
|
295
|
+
c.v = h.v;
|
|
296
|
+
});
|
|
297
|
+
const setHsl = (key, v) => patch((d) => {
|
|
298
|
+
const c = currentOf(d);
|
|
299
|
+
const next = hsvToHsl(c);
|
|
300
|
+
next[key] = key === 'h' ? wrap360(v) : clamp(v, 0, 100);
|
|
301
|
+
const h = hslToHsv(next.h, next.s, next.l, c.a);
|
|
302
|
+
c.h = next.h;
|
|
303
|
+
c.s = h.s;
|
|
304
|
+
c.v = h.v;
|
|
305
|
+
});
|
|
306
|
+
const setHsb = (key, v) => patch((d) => {
|
|
307
|
+
const c = currentOf(d);
|
|
308
|
+
if (key === 'h')
|
|
309
|
+
c.h = wrap360(v);
|
|
310
|
+
else if (key === 's')
|
|
311
|
+
c.s = clamp(v, 0, 100);
|
|
312
|
+
else
|
|
313
|
+
c.v = clamp(v, 0, 100);
|
|
314
|
+
});
|
|
315
|
+
const channels = format === 'rgb'
|
|
316
|
+
? [
|
|
317
|
+
{
|
|
318
|
+
label: 'R',
|
|
319
|
+
value: rgb.r,
|
|
320
|
+
min: 0,
|
|
321
|
+
max: 255,
|
|
322
|
+
set: (v) => setRgb('r', v),
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
label: 'G',
|
|
326
|
+
value: rgb.g,
|
|
327
|
+
min: 0,
|
|
328
|
+
max: 255,
|
|
329
|
+
set: (v) => setRgb('g', v),
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
label: 'B',
|
|
333
|
+
value: rgb.b,
|
|
334
|
+
min: 0,
|
|
335
|
+
max: 255,
|
|
336
|
+
set: (v) => setRgb('b', v),
|
|
337
|
+
},
|
|
338
|
+
]
|
|
339
|
+
: format === 'hsl'
|
|
340
|
+
? [
|
|
341
|
+
{
|
|
342
|
+
label: 'H',
|
|
343
|
+
value: r0(hsl.h),
|
|
344
|
+
min: 0,
|
|
345
|
+
max: 360,
|
|
346
|
+
set: (v) => setHsl('h', v),
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
label: 'S',
|
|
350
|
+
value: r0(hsl.s),
|
|
351
|
+
min: 0,
|
|
352
|
+
max: 100,
|
|
353
|
+
set: (v) => setHsl('s', v),
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
label: 'L',
|
|
357
|
+
value: r0(hsl.l),
|
|
358
|
+
min: 0,
|
|
359
|
+
max: 100,
|
|
360
|
+
set: (v) => setHsl('l', v),
|
|
361
|
+
},
|
|
362
|
+
]
|
|
363
|
+
: [
|
|
364
|
+
{
|
|
365
|
+
label: 'H',
|
|
366
|
+
value: r0(cur.h),
|
|
367
|
+
min: 0,
|
|
368
|
+
max: 360,
|
|
369
|
+
set: (v) => setHsb('h', v),
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
label: 'S',
|
|
373
|
+
value: r0(cur.s),
|
|
374
|
+
min: 0,
|
|
375
|
+
max: 100,
|
|
376
|
+
set: (v) => setHsb('s', v),
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
label: 'B',
|
|
380
|
+
value: r0(cur.v),
|
|
381
|
+
min: 0,
|
|
382
|
+
max: 100,
|
|
383
|
+
set: (v) => setHsb('b', v),
|
|
384
|
+
},
|
|
385
|
+
];
|
|
386
|
+
// ── hex input ──────────────────────────────────────────────────────────────────
|
|
387
|
+
const hex6 = buildColorValue({ ...cur, a: 1 })
|
|
388
|
+
.hex.slice(1)
|
|
389
|
+
.toUpperCase();
|
|
390
|
+
const [hexText, setHexText] = useState(hex6);
|
|
391
|
+
const [hexFocused, setHexFocused] = useState(false);
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
if (!hexFocused)
|
|
394
|
+
setHexText(hex6);
|
|
395
|
+
}, [hex6, hexFocused]);
|
|
396
|
+
const commitHex = () => {
|
|
397
|
+
setHexFocused(false);
|
|
398
|
+
if (isValidHex(hexText)) {
|
|
399
|
+
const parsed = parseColor(`#${hexText}`);
|
|
400
|
+
patch((d) => {
|
|
401
|
+
const c = currentOf(d);
|
|
402
|
+
c.h = parsed.h;
|
|
403
|
+
c.s = parsed.s;
|
|
404
|
+
c.v = parsed.v;
|
|
405
|
+
if (hexText.length === 4 || hexText.length === 8)
|
|
406
|
+
c.a = parsed.a;
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
setHexText(hex6);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
// ── gradient ───────────────────────────────────────────────────────────────────
|
|
414
|
+
const setMode = (mode) => {
|
|
415
|
+
if (mode === internal.mode)
|
|
416
|
+
return;
|
|
417
|
+
patch((d) => {
|
|
418
|
+
d.mode = mode;
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
const setActive = (i) => patch((d) => (d.active = i), false);
|
|
422
|
+
const flip = () =>
|
|
423
|
+
// Reverse the colors; positions stay put (a true direction flip for 2 stops).
|
|
424
|
+
patch((d) => {
|
|
425
|
+
const a = d.stops[0].hsva;
|
|
426
|
+
d.stops[0].hsva = d.stops[1].hsva;
|
|
427
|
+
d.stops[1].hsva = a;
|
|
428
|
+
});
|
|
429
|
+
const setAngle = (v) => patch((d) => {
|
|
430
|
+
d.angle = wrap360(v);
|
|
431
|
+
});
|
|
432
|
+
// Select on press; reposition only on drag (so a click just selects the stop).
|
|
433
|
+
const onStopDown = (i, e) => {
|
|
434
|
+
if (!interactive || e.button !== 0)
|
|
435
|
+
return;
|
|
436
|
+
interactingRef.current = true;
|
|
437
|
+
if (internal.active !== i)
|
|
438
|
+
setActive(i);
|
|
439
|
+
const el = gradientBarRef.current;
|
|
440
|
+
if (!el) {
|
|
441
|
+
interactingRef.current = false;
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const rect = el.getBoundingClientRect();
|
|
445
|
+
const pad = THUMB_PX / 2;
|
|
446
|
+
const move = (clientX) => patch((d) => {
|
|
447
|
+
d.stops[i].position = r0(clamp((clientX - rect.left - pad) / (rect.width - 2 * pad), 0, 1) *
|
|
448
|
+
100);
|
|
449
|
+
});
|
|
450
|
+
const onMove = (ev) => {
|
|
451
|
+
ev.preventDefault();
|
|
452
|
+
move(ev.clientX);
|
|
453
|
+
};
|
|
454
|
+
const onUp = () => {
|
|
455
|
+
interactingRef.current = false;
|
|
456
|
+
document.removeEventListener('pointermove', onMove);
|
|
457
|
+
document.removeEventListener('pointerup', onUp);
|
|
458
|
+
};
|
|
459
|
+
document.addEventListener('pointermove', onMove);
|
|
460
|
+
document.addEventListener('pointerup', onUp);
|
|
461
|
+
};
|
|
462
|
+
const onStopKey = (i, e) => {
|
|
463
|
+
if (!interactive)
|
|
464
|
+
return;
|
|
465
|
+
const d = kstep(e);
|
|
466
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
467
|
+
e.preventDefault();
|
|
468
|
+
patch((x) => {
|
|
469
|
+
x.stops[i].position = clamp(x.stops[i].position + (e.key === 'ArrowRight' ? d : -d), 0, 100);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
const showGradientBar = gradient && internal.mode === 'linear';
|
|
474
|
+
const stopThumbs = internal.stops.map((s, i) => ({
|
|
475
|
+
color: buildColorValue(s.hsva),
|
|
476
|
+
position: s.position,
|
|
477
|
+
index: i,
|
|
478
|
+
}));
|
|
479
|
+
const previewStops = [...stopThumbs]
|
|
480
|
+
.map((s) => ({ color: s.color, position: s.position }))
|
|
481
|
+
.sort((a, b) => a.position - b.position);
|
|
482
|
+
// ── render ───────────────────────────────────────────────────────────────────
|
|
483
|
+
const sliderThumb = (handlers, fraction, aria, fill) => (_jsx("div", { role: "slider", "aria-label": aria.label, "aria-valuemin": 0, "aria-valuemax": aria.max, "aria-valuenow": aria.now, tabIndex: interactive ? 0 : undefined, onKeyDown: handlers.onKeyDown, className: cn(THUMB_CLASS, 'pointer-events-none top-1/2', THUMB), style: {
|
|
484
|
+
left: thumbLeft(fraction),
|
|
485
|
+
transform: 'translate(-50%, -50%)',
|
|
486
|
+
backgroundColor: fill,
|
|
487
|
+
} }));
|
|
488
|
+
return (_jsxs("div", { ref: ref, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, className: cn('cladd-coloreditor flex w-full flex-col gap-2.5', className), children: [header, _jsxs("div", { "data-part": "body", className: cn('flex w-full flex-col gap-2.5', disabled && 'pointer-events-none opacity-50'), children: [gradient && (_jsx(Toolbar, { "data-part": "toolbar", size: toolbarSize, outline: controlOutline, className: "mx-auto w-32", contentClassName: "w-full", children: _jsxs(Segmented, { className: "w-full", activeOutline: controlOutline, children: [_jsx(SegmentedButton, { className: "flex-1", active: internal.mode === 'solid', onClick: () => setMode('solid'), "aria-label": "Solid", children: _jsx(SolidColorIcon, {}) }), _jsx(SegmentedButton, { className: "flex-1", active: internal.mode === 'linear', onClick: () => setMode('linear'), "aria-label": "Gradient", children: _jsx(GradientColorIcon, {}) })] }) })), showGradientBar && (_jsxs("div", { className: "flex w-full items-center gap-2.5", children: [_jsx(Button, { square: true, size: controlSize, variant: "transparent", outline: false, disabled: !interactive, onClick: flip, "aria-label": "Flip gradient", children: _jsx(FlipIcon, { className: "text-cladd-fg-soft" }) }), _jsxs("div", { ref: gradientBarRef, className: cn('relative flex-1 touch-none rounded-full', BAR_H), style: CHECKER, children: [_jsx("div", { className: "absolute inset-0 rounded-full", style: { background: gradientPreviewCss(previewStops) } }), stopThumbs.map((s) => (_jsx("div", { role: "slider", "aria-label": `Gradient stop ${s.index + 1}`, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": s.position, tabIndex: interactive ? 0 : undefined, onPointerDown: (e) => onStopDown(s.index, e), onKeyDown: (e) => onStopKey(s.index, e), className: cn(THUMB_CLASS, 'top-1/2', THUMB, internal.active === s.index &&
|
|
489
|
+
'z-10 ring-2 ring-cladd-primary ring-offset-1'), style: {
|
|
490
|
+
left: thumbLeft(s.position / 100),
|
|
491
|
+
transform: 'translate(-50%, -50%)',
|
|
492
|
+
backgroundColor: s.color.css,
|
|
493
|
+
} }, s.index)))] }), angleControl === 'button' ? (_jsx(Button, { square: true, size: controlSize, variant: "transparent", outline: false, disabled: !interactive, onClick: () => setAngle(internal.angle + 45), "aria-label": "Rotate 45\u00B0", children: _jsx(ArrowLeftIcon, { className: "text-cladd-fg-soft transition-transform duration-200", style: { transform: `rotate(${internal.angle + 90}deg)` } }) })) : (_jsx(NumberScrubber, { className: "w-16 shrink-0", contentClassName: "pl-1.5 gap-1", inputClassName: "text-right pl-5", iconClassName: "left-1.5", size: controlSize, variant: "transparent", outline: false, scrubberIcon: false, icon: _jsx(ArrowLeftIcon, { style: { transform: `rotate(${internal.angle + 90}deg)` } }), min: 0, max: 360, step: 1, value: r0(internal.angle), displayValue: (v) => `${v}°`, disabled: !interactive, onChange: setAngle, onTemporaryChange: setAngle }))] })), _jsx("div", { ref: areaElRef, "data-part": "area", className: cn('relative w-full touch-none rounded-cladd-lg select-none', AREA_H, interactive && 'cursor-crosshair', areaClassName), style: {
|
|
494
|
+
background: `linear-gradient(to top, #000 0%, rgba(0,0,0,0) 100%), linear-gradient(to right, #fff 0%, rgba(255,255,255,0) 100%), hsl(${cur.h}, 100%, 50%)`,
|
|
495
|
+
}, onPointerDown: onAreaDown, children: _jsx("div", { "data-part": "area-thumb", role: "slider", "aria-label": "Saturation and brightness", "aria-valuetext": `S ${r0(cur.s)}%, B ${r0(cur.v)}%`, tabIndex: interactive ? 0 : undefined, onKeyDown: onAreaKey, className: cn(THUMB_CLASS, 'pointer-events-none', THUMB), style: {
|
|
496
|
+
left: `${cur.s}%`,
|
|
497
|
+
top: `${100 - cur.v}%`,
|
|
498
|
+
transform: 'translate(-50%, -50%)',
|
|
499
|
+
backgroundColor: opaque,
|
|
500
|
+
} }) }), _jsx("div", { ref: hueElRef, "data-part": "hue", className: cn('relative w-full touch-none rounded-full select-none', BAR_H), style: { background: HUE_TRACK }, onPointerDown: onHueDown, children: sliderThumb({ onKeyDown: onHueKey }, cur.h / 360, { label: 'Hue', now: r0(cur.h), max: 360 }, `hsl(${cur.h}, 100%, 50%)`) }), alpha && (_jsxs("div", { ref: alphaElRef, "data-part": "alpha", className: cn('relative w-full touch-none rounded-full select-none', BAR_H), style: CHECKER, onPointerDown: onAlphaDown, children: [_jsx("div", { className: "absolute inset-0 rounded-full", style: {
|
|
501
|
+
background: `linear-gradient(to right, rgba(${rgb.r},${rgb.g},${rgb.b},0) 0%, rgba(${rgb.r},${rgb.g},${rgb.b},1) 100%)`,
|
|
502
|
+
} }), sliderThumb({ onKeyDown: onAlphaKey }, cur.a, { label: 'Alpha', now: r0(cur.a * 100), max: 100 }, opaque)] })), inputs && (_jsx("div", { className: "flex w-full items-start gap-1.5", children: channels.map((ch) => (_jsx("div", { className: "flex min-w-0 flex-1 flex-col items-center gap-1", children: _jsx(NumberScrubber, { className: "w-full", contentClassName: "justify-between pl-1.5 gap-1", inputClassName: "text-right pl-5", iconClassName: "left-1.5", icon: _jsx(LetterIcon, { children: ch.label }), size: controlSize, outline: controlOutline, scrubberIcon: false, min: ch.min, max: ch.max, step: 1, value: ch.value, disabled: !interactive, onChange: ch.set, onTemporaryChange: ch.set }) }, ch.label))) })), hexInput && (_jsxs("div", { className: "grid w-full grid-cols-3 items-center gap-1.5", children: [_jsx(Input, { className: cn('min-w-0', alphaInput ? 'col-span-2' : 'col-span-3'), size: controlSize, outline: controlOutline, color: "", icon: _jsx(LetterIcon, { children: "#" }), iconClassName: "left-1.5", inputClassName: "uppercase tracking-wide pl-6", value: hexText, maxLength: 8, readOnly: !interactive, onFocus: () => setHexFocused(true), onBlur: commitHex, onChange: (v) => setHexText(v.replace(/[^0-9a-fA-F]/g, '')), onKeyDown: (e) => {
|
|
503
|
+
if (e.key === 'Enter')
|
|
504
|
+
e.target.blur();
|
|
505
|
+
} }), alphaInput && (_jsx(NumberScrubber, { className: "w-full", contentClassName: "justify-between pl-1.5 gap-1", inputClassName: "text-right pl-5", iconClassName: "left-1.5", icon: _jsx(LetterIcon, { children: "A" }), size: controlSize, outline: controlOutline, scrubberIcon: false, min: 0, max: 100, step: 1, value: r0(cur.a * 100), displayValue: (v) => `${v}%`, disabled: !interactive, onChange: (v) => patch((d) => (currentOf(d).a = clamp(v / 100, 0, 1))), onTemporaryChange: (v) => patch((d) => (currentOf(d).a = clamp(v / 100, 0, 1))) }))] })), swatches && swatches.length > 0 && (_jsx("div", { "data-part": "swatches", className: "flex w-full flex-wrap gap-1.5", children: swatches.map((sw, i) => {
|
|
506
|
+
const swatch = buildColorValue(parseColor(sw));
|
|
507
|
+
return (_jsx("button", { type: "button", "aria-label": swatch.hex, disabled: !interactive, onClick: () => patch((d) => {
|
|
508
|
+
const c = currentOf(d);
|
|
509
|
+
const p = parseColor(sw);
|
|
510
|
+
c.h = p.h;
|
|
511
|
+
c.s = p.s;
|
|
512
|
+
c.v = p.v;
|
|
513
|
+
c.a = p.a;
|
|
514
|
+
}), className: cn('relative shrink-0 overflow-hidden rounded-cladd-2xs shadow-[inset_0_0_0_1px_rgba(0,0,0,0.15)] transition-transform hover:scale-110', SWATCH), style: CHECKER, children: _jsx("span", { className: "absolute inset-0", style: { background: swatch.css } }) }, i));
|
|
515
|
+
}) }))] }), footer] }));
|
|
516
|
+
};
|
|
517
|
+
//# sourceMappingURL=ColorEditor.js.map
|