colibri-ui 0.8.96 → 0.8.97
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/ColorPicker/ColorPicker.d.ts +3 -0
- package/ColorPicker/ColorPicker.js +116 -0
- package/ColorPicker/ColorPicker.module.css +22 -0
- package/ColorPicker/assets/none.png +0 -0
- package/ColorPicker/components/ColorPreview/ColorPreview.module.css +23 -0
- package/ColorPicker/components/ColorPreview/index.d.ts +10 -0
- package/ColorPicker/components/ColorPreview/index.js +29 -0
- package/ColorPicker/components/CustomColorModal/CustomColorModal.module.css +18 -0
- package/ColorPicker/components/CustomColorModal/index.d.ts +15 -0
- package/ColorPicker/components/CustomColorModal/index.js +107 -0
- package/ColorPicker/components/HueSlider/HueSlider.module.css +31 -0
- package/ColorPicker/components/HueSlider/index.d.ts +10 -0
- package/ColorPicker/components/HueSlider/index.js +73 -0
- package/ColorPicker/components/Palette/Palette.module.css +12 -0
- package/ColorPicker/components/Palette/index.d.ts +19 -0
- package/ColorPicker/components/Palette/index.js +99 -0
- package/ColorPicker/components/SaturationPanel/SaturationPanel.module.css +26 -0
- package/ColorPicker/components/SaturationPanel/index.d.ts +13 -0
- package/ColorPicker/components/SaturationPanel/index.js +86 -0
- package/ColorPicker/components/Swatch/Swatch.module.css +36 -0
- package/ColorPicker/components/Swatch/index.d.ts +14 -0
- package/ColorPicker/components/Swatch/index.js +47 -0
- package/ColorPicker/hooks/useEyeDropper.d.ts +8 -0
- package/ColorPicker/hooks/useEyeDropper.js +38 -0
- package/ColorPicker/hooks/useHexDraft.d.ts +19 -0
- package/ColorPicker/hooks/useHexDraft.js +49 -0
- package/ColorPicker/hooks/usePointerDrag.d.ts +20 -0
- package/ColorPicker/hooks/usePointerDrag.js +55 -0
- package/ColorPicker/index.d.ts +3 -0
- package/ColorPicker/index.js +9 -0
- package/ColorPicker/index.props.d.ts +43 -0
- package/ColorPicker/package.json +9 -0
- package/ColorPicker/presets.d.ts +3 -0
- package/ColorPicker/presets.js +92 -0
- package/ColorPicker/utils/color.d.ts +25 -0
- package/ColorPicker/utils/color.js +162 -0
- package/Icons/Pipette.d.ts +3 -0
- package/Icons/Pipette.js +35 -0
- package/Icons/index.d.ts +1 -0
- package/Icons/index.js +2 -0
- package/esm/ColorPicker/ColorPicker.d.ts +3 -0
- package/esm/ColorPicker/ColorPicker.js +108 -0
- package/esm/ColorPicker/ColorPicker.module.css +22 -0
- package/esm/ColorPicker/assets/none.png +0 -0
- package/esm/ColorPicker/components/ColorPreview/ColorPreview.module.css +23 -0
- package/esm/ColorPicker/components/ColorPreview/index.d.ts +10 -0
- package/esm/ColorPicker/components/ColorPreview/index.js +21 -0
- package/esm/ColorPicker/components/CustomColorModal/CustomColorModal.module.css +18 -0
- package/esm/ColorPicker/components/CustomColorModal/index.d.ts +15 -0
- package/esm/ColorPicker/components/CustomColorModal/index.js +100 -0
- package/esm/ColorPicker/components/HueSlider/HueSlider.module.css +31 -0
- package/esm/ColorPicker/components/HueSlider/index.d.ts +10 -0
- package/esm/ColorPicker/components/HueSlider/index.js +66 -0
- package/esm/ColorPicker/components/Palette/Palette.module.css +12 -0
- package/esm/ColorPicker/components/Palette/index.d.ts +19 -0
- package/esm/ColorPicker/components/Palette/index.js +92 -0
- package/esm/ColorPicker/components/SaturationPanel/SaturationPanel.module.css +26 -0
- package/esm/ColorPicker/components/SaturationPanel/index.d.ts +13 -0
- package/esm/ColorPicker/components/SaturationPanel/index.js +79 -0
- package/esm/ColorPicker/components/Swatch/Swatch.module.css +36 -0
- package/esm/ColorPicker/components/Swatch/index.d.ts +14 -0
- package/esm/ColorPicker/components/Swatch/index.js +38 -0
- package/esm/ColorPicker/hooks/useEyeDropper.d.ts +8 -0
- package/esm/ColorPicker/hooks/useEyeDropper.js +36 -0
- package/esm/ColorPicker/hooks/useHexDraft.d.ts +19 -0
- package/esm/ColorPicker/hooks/useHexDraft.js +47 -0
- package/esm/ColorPicker/hooks/usePointerDrag.d.ts +20 -0
- package/esm/ColorPicker/hooks/usePointerDrag.js +53 -0
- package/esm/ColorPicker/index.d.ts +3 -0
- package/esm/ColorPicker/index.js +2 -0
- package/esm/ColorPicker/index.props.d.ts +43 -0
- package/esm/ColorPicker/presets.d.ts +3 -0
- package/esm/ColorPicker/presets.js +89 -0
- package/esm/ColorPicker/utils/color.d.ts +25 -0
- package/esm/ColorPicker/utils/color.js +152 -0
- package/esm/Icons/Pipette.d.ts +3 -0
- package/esm/Icons/Pipette.js +29 -0
- package/esm/Icons/index.d.ts +1 -0
- package/esm/Icons/index.js +1 -0
- package/esm/index.d.ts +2 -0
- package/esm/index.js +3 -0
- package/index.d.ts +2 -0
- package/index.js +7 -0
- package/package.json +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ColorPickerLabels, ColorPickerPreset } from "../../index.props";
|
|
3
|
+
interface PaletteProps {
|
|
4
|
+
presets: ColorPickerPreset[];
|
|
5
|
+
/** Normalized `#rrggbb` of the current color. */
|
|
6
|
+
value: string | null;
|
|
7
|
+
columns: number;
|
|
8
|
+
labels: ColorPickerLabels;
|
|
9
|
+
onSelect: (hex: string) => void;
|
|
10
|
+
onAddClick: () => void;
|
|
11
|
+
/** Tab or Shift+Tab leaves the palette. */
|
|
12
|
+
onTabOut: () => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Preset grid with the "+" button. A current color missing from the presets
|
|
16
|
+
* is shown as an extra swatch before "+".
|
|
17
|
+
*/
|
|
18
|
+
export declare const Palette: ({ presets, value, columns, labels, onSelect, onAddClick, onTabOut, }: PaletteProps) => React.JSX.Element;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from 'react';
|
|
2
|
+
import { Button } from '../../../Button/index.js';
|
|
3
|
+
import 'tslib';
|
|
4
|
+
import { Plus } from '../../../Icons/Plus.js';
|
|
5
|
+
import { Swatch } from '../Swatch/index.js';
|
|
6
|
+
import styles from './Palette.module.css';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Preset grid with the "+" button. A current color missing from the presets
|
|
10
|
+
* is shown as an extra swatch before "+".
|
|
11
|
+
*/
|
|
12
|
+
const Palette = ({
|
|
13
|
+
presets,
|
|
14
|
+
value,
|
|
15
|
+
columns,
|
|
16
|
+
labels,
|
|
17
|
+
onSelect,
|
|
18
|
+
onAddClick,
|
|
19
|
+
onTabOut
|
|
20
|
+
}) => {
|
|
21
|
+
const rootRef = useRef(null);
|
|
22
|
+
const hasPreset = presets.some(preset => preset.value === value);
|
|
23
|
+
const colors = value && !hasPreset ? [...presets, {
|
|
24
|
+
value,
|
|
25
|
+
label: value
|
|
26
|
+
}] : presets;
|
|
27
|
+
// The dropdown is portaled to the end of the document: move focus into it,
|
|
28
|
+
// to the selected swatch or the first item ("+" when there are no colors).
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
var _a, _b;
|
|
31
|
+
const root = rootRef.current;
|
|
32
|
+
(_b = (_a = root === null || root === void 0 ? void 0 : root.querySelector('[aria-checked="true"]')) !== null && _a !== void 0 ? _a : root === null || root === void 0 ? void 0 : root.querySelector("button")) === null || _b === void 0 ? void 0 : _b.focus();
|
|
33
|
+
}, []);
|
|
34
|
+
// Arrow keys move focus across the grid, Home/End go to the first and the
|
|
35
|
+
// last item. Tab leaves the portaled palette through the trigger, so focus
|
|
36
|
+
// continues from it, also inside a parent focus trap.
|
|
37
|
+
const handleKeyDown = event => {
|
|
38
|
+
var _a;
|
|
39
|
+
if (event.key === "Tab") {
|
|
40
|
+
onTabOut();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const items = Array.from(event.currentTarget.querySelectorAll("button"));
|
|
44
|
+
const index = items.indexOf(event.target);
|
|
45
|
+
const offsets = {
|
|
46
|
+
ArrowLeft: -1,
|
|
47
|
+
ArrowRight: 1,
|
|
48
|
+
ArrowUp: -columns,
|
|
49
|
+
ArrowDown: columns,
|
|
50
|
+
Home: -index,
|
|
51
|
+
End: items.length - 1 - index
|
|
52
|
+
};
|
|
53
|
+
if (index === -1 || !(event.key in offsets)) return;
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
(_a = items[index + offsets[event.key]]) === null || _a === void 0 ? void 0 : _a.focus();
|
|
56
|
+
};
|
|
57
|
+
return (
|
|
58
|
+
/*#__PURE__*/
|
|
59
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- keys come from the focused items
|
|
60
|
+
React.createElement("div", {
|
|
61
|
+
ref: rootRef,
|
|
62
|
+
role: "group",
|
|
63
|
+
"aria-label": labels.palette,
|
|
64
|
+
className: styles.root,
|
|
65
|
+
onKeyDown: handleKeyDown,
|
|
66
|
+
style: {
|
|
67
|
+
gridTemplateColumns: `repeat(${columns}, auto)`
|
|
68
|
+
}
|
|
69
|
+
}, colors.map(color => (/*#__PURE__*/React.createElement(Swatch, {
|
|
70
|
+
key: color.value,
|
|
71
|
+
color: color.value,
|
|
72
|
+
label: color.label,
|
|
73
|
+
selected: color.value === value,
|
|
74
|
+
onClick: () => onSelect(color.value)
|
|
75
|
+
}))), /*#__PURE__*/React.createElement(Button, {
|
|
76
|
+
className: styles.add,
|
|
77
|
+
variant: "secondary",
|
|
78
|
+
size: "xs",
|
|
79
|
+
icon: /*#__PURE__*/React.createElement(Plus, {
|
|
80
|
+
width: 12,
|
|
81
|
+
height: 12,
|
|
82
|
+
"aria-hidden": "true"
|
|
83
|
+
}),
|
|
84
|
+
role: "menuitem",
|
|
85
|
+
"aria-label": labels.addCustom,
|
|
86
|
+
title: labels.addCustom,
|
|
87
|
+
onClick: onAddClick
|
|
88
|
+
}))
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export { Palette };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
background-image: linear-gradient(to top, black, transparent), linear-gradient(to right, white, transparent);
|
|
3
|
+
border-radius: var(--radius-sm);
|
|
4
|
+
cursor: crosshair;
|
|
5
|
+
height: var(--space-32);
|
|
6
|
+
outline: none;
|
|
7
|
+
position: relative;
|
|
8
|
+
touch-action: none;
|
|
9
|
+
-webkit-user-select: none;
|
|
10
|
+
user-select: none;
|
|
11
|
+
width: 100%;
|
|
12
|
+
}
|
|
13
|
+
.root:focus-visible {
|
|
14
|
+
box-shadow: 0 0 0 var(--focus-ring-width) var(--color-focus-ring);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.thumb {
|
|
18
|
+
border: var(--focus-ring-width) solid var(--color-neutral-0);
|
|
19
|
+
border-radius: var(--radius-full);
|
|
20
|
+
box-shadow: var(--shadow-raised);
|
|
21
|
+
height: var(--space-5);
|
|
22
|
+
pointer-events: none;
|
|
23
|
+
position: absolute;
|
|
24
|
+
transform: translate(-50%, -50%);
|
|
25
|
+
width: var(--space-5);
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { HSV } from "../../utils/color";
|
|
3
|
+
interface SaturationPanelProps {
|
|
4
|
+
value: HSV;
|
|
5
|
+
onChange: (value: HSV) => void;
|
|
6
|
+
"aria-label": string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Saturation (horizontal) and brightness (vertical) area. Arrow keys change
|
|
10
|
+
* by 1, with Shift by 10.
|
|
11
|
+
*/
|
|
12
|
+
export declare const SaturationPanel: ({ value, onChange, "aria-label": ariaLabel, }: SaturationPanelProps) => React.JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { usePointerDrag } from '../../hooks/usePointerDrag.js';
|
|
3
|
+
import { hsv2css, clamp } from '../../utils/color.js';
|
|
4
|
+
import styles from './SaturationPanel.module.css';
|
|
5
|
+
|
|
6
|
+
const STEP = 1;
|
|
7
|
+
const BIG_STEP = 10;
|
|
8
|
+
/**
|
|
9
|
+
* Saturation (horizontal) and brightness (vertical) area. Arrow keys change
|
|
10
|
+
* by 1, with Shift by 10.
|
|
11
|
+
*/
|
|
12
|
+
const SaturationPanel = ({
|
|
13
|
+
value,
|
|
14
|
+
onChange,
|
|
15
|
+
"aria-label": ariaLabel
|
|
16
|
+
}) => {
|
|
17
|
+
const {
|
|
18
|
+
h,
|
|
19
|
+
s,
|
|
20
|
+
v
|
|
21
|
+
} = value;
|
|
22
|
+
const {
|
|
23
|
+
ref,
|
|
24
|
+
handlers
|
|
25
|
+
} = usePointerDrag(({
|
|
26
|
+
x,
|
|
27
|
+
y
|
|
28
|
+
}) => onChange({
|
|
29
|
+
h,
|
|
30
|
+
s: x * 100,
|
|
31
|
+
v: (1 - y) * 100
|
|
32
|
+
}));
|
|
33
|
+
const handleKeyDown = event => {
|
|
34
|
+
const step = event.shiftKey ? BIG_STEP : STEP;
|
|
35
|
+
const deltas = {
|
|
36
|
+
ArrowLeft: [-step, 0],
|
|
37
|
+
ArrowRight: [step, 0],
|
|
38
|
+
ArrowUp: [0, step],
|
|
39
|
+
ArrowDown: [0, -step]
|
|
40
|
+
};
|
|
41
|
+
const delta = deltas[event.key];
|
|
42
|
+
if (!delta) return;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
onChange({
|
|
45
|
+
h,
|
|
46
|
+
s: clamp(s + delta[0], 0, 100),
|
|
47
|
+
v: clamp(v + delta[1], 0, 100)
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
return /*#__PURE__*/React.createElement("div", Object.assign({
|
|
51
|
+
ref: ref
|
|
52
|
+
}, handlers, {
|
|
53
|
+
role: "slider",
|
|
54
|
+
tabIndex: 0,
|
|
55
|
+
"aria-label": ariaLabel,
|
|
56
|
+
"aria-valuemin": 0,
|
|
57
|
+
"aria-valuemax": 100,
|
|
58
|
+
"aria-valuenow": Math.round(s),
|
|
59
|
+
"aria-valuetext": `${Math.round(s)}%, ${Math.round(v)}%`,
|
|
60
|
+
onKeyDown: handleKeyDown,
|
|
61
|
+
className: styles.root,
|
|
62
|
+
style: {
|
|
63
|
+
backgroundColor: hsv2css({
|
|
64
|
+
h,
|
|
65
|
+
s: 100,
|
|
66
|
+
v: 100
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}), /*#__PURE__*/React.createElement("div", {
|
|
70
|
+
className: styles.thumb,
|
|
71
|
+
style: {
|
|
72
|
+
left: `${s}%`,
|
|
73
|
+
top: `${100 - v}%`,
|
|
74
|
+
backgroundColor: hsv2css(value)
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export { SaturationPanel };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
align-items: center;
|
|
3
|
+
border-radius: var(--radius-full);
|
|
4
|
+
cursor: pointer;
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
flex-shrink: 0;
|
|
7
|
+
height: var(--space-6);
|
|
8
|
+
justify-content: center;
|
|
9
|
+
padding: 0;
|
|
10
|
+
transition: transform 0.1s ease;
|
|
11
|
+
width: var(--space-6);
|
|
12
|
+
}
|
|
13
|
+
.root:hover {
|
|
14
|
+
transform: scale(1.1);
|
|
15
|
+
}
|
|
16
|
+
.root:focus-visible {
|
|
17
|
+
outline: var(--focus-ring-width) solid var(--color-focus-ring);
|
|
18
|
+
outline-offset: var(--focus-ring-width);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.check_light {
|
|
22
|
+
color: var(--color-neutral-0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.check_dark {
|
|
26
|
+
color: var(--color-neutral-950);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.outlined {
|
|
30
|
+
box-shadow: inset 0 0 0 var(--border-width-hairline) var(--color-border-default);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.check {
|
|
34
|
+
height: var(--space-4);
|
|
35
|
+
width: var(--space-4);
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ButtonHTMLAttributes } from "react";
|
|
3
|
+
interface SwatchProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color" | "type"> {
|
|
4
|
+
/** `#rrggbb` */
|
|
5
|
+
color: string;
|
|
6
|
+
label: string;
|
|
7
|
+
selected: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Color circle button. The check mark adapts to the color, light colors get
|
|
11
|
+
* an outline.
|
|
12
|
+
*/
|
|
13
|
+
export declare const Swatch: ({ color, label, selected, className, ...props }: SwatchProps) => React.JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { __rest } from 'tslib';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { Check } from '../../../Icons/Check.js';
|
|
5
|
+
import { prefersLightText, isLightColor } from '../../utils/color.js';
|
|
6
|
+
import styles from './Swatch.module.css';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Color circle button. The check mark adapts to the color, light colors get
|
|
10
|
+
* an outline.
|
|
11
|
+
*/
|
|
12
|
+
const Swatch = _a => {
|
|
13
|
+
var {
|
|
14
|
+
color,
|
|
15
|
+
label,
|
|
16
|
+
selected,
|
|
17
|
+
className
|
|
18
|
+
} = _a,
|
|
19
|
+
props = __rest(_a, ["color", "label", "selected", "className"]);
|
|
20
|
+
return /*#__PURE__*/React.createElement("button", Object.assign({}, props, {
|
|
21
|
+
type: "button",
|
|
22
|
+
role: "menuitemradio",
|
|
23
|
+
"aria-checked": selected,
|
|
24
|
+
"aria-label": label,
|
|
25
|
+
title: label,
|
|
26
|
+
className: clsx(styles.root, prefersLightText(color) ? styles.check_light : styles.check_dark, {
|
|
27
|
+
[styles.outlined]: isLightColor(color)
|
|
28
|
+
}, className),
|
|
29
|
+
style: {
|
|
30
|
+
backgroundColor: color
|
|
31
|
+
}
|
|
32
|
+
}), selected && /*#__PURE__*/React.createElement(Check, {
|
|
33
|
+
className: styles.check,
|
|
34
|
+
"aria-hidden": "true"
|
|
35
|
+
}));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export { Swatch };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useSyncExternalStore, useCallback } from 'react';
|
|
2
|
+
import { toHex } from '../utils/color.js';
|
|
3
|
+
|
|
4
|
+
const subscribe = () => () => {};
|
|
5
|
+
const getSnapshot = () => "EyeDropper" in window;
|
|
6
|
+
// No API on the server, so hydration does not mismatch.
|
|
7
|
+
const getServerSnapshot = () => false;
|
|
8
|
+
/**
|
|
9
|
+
* Native browser eye dropper (EyeDropper API). Chromium only: in Safari and
|
|
10
|
+
* Firefox `isSupported` is `false`.
|
|
11
|
+
*/
|
|
12
|
+
const useEyeDropper = () => {
|
|
13
|
+
const isSupported = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
14
|
+
/** `#rrggbb`, or `null` when the user cancels with Escape. */
|
|
15
|
+
const pick = useCallback(async () => {
|
|
16
|
+
const {
|
|
17
|
+
EyeDropper
|
|
18
|
+
} = window;
|
|
19
|
+
if (!EyeDropper) return null;
|
|
20
|
+
try {
|
|
21
|
+
// eslint-disable-next-line compat/compat -- guarded by the feature check above
|
|
22
|
+
const {
|
|
23
|
+
sRGBHex
|
|
24
|
+
} = await new EyeDropper().open();
|
|
25
|
+
return toHex(sRGBHex);
|
|
26
|
+
} catch (_a) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}, []);
|
|
30
|
+
return {
|
|
31
|
+
isSupported,
|
|
32
|
+
pick
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export { useEyeDropper };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ChangeEvent, KeyboardEvent } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* HEX field logic for `Input`:
|
|
4
|
+
* - while focused, the field keeps a draft, so it can be cleared and retyped;
|
|
5
|
+
* - a full `#rrggbb` applies while typing;
|
|
6
|
+
* - a short `#abc` applies on blur or Enter (focus stays), so typing
|
|
7
|
+
* `#abcdef` does not flash `#aabbcc`;
|
|
8
|
+
* - invalid input reverts to the current value on blur.
|
|
9
|
+
*/
|
|
10
|
+
export declare const useHexDraft: (value: string, onChange: (hex: string) => void) => {
|
|
11
|
+
invalid: boolean;
|
|
12
|
+
inputProps: {
|
|
13
|
+
value: string;
|
|
14
|
+
onFocus: () => void;
|
|
15
|
+
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
16
|
+
onBlur: () => void;
|
|
17
|
+
onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => void;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { normalizeHex } from '../utils/color.js';
|
|
3
|
+
|
|
4
|
+
const FULL_HEX_RE = /^#?[0-9a-f]{6}$/i;
|
|
5
|
+
/**
|
|
6
|
+
* HEX field logic for `Input`:
|
|
7
|
+
* - while focused, the field keeps a draft, so it can be cleared and retyped;
|
|
8
|
+
* - a full `#rrggbb` applies while typing;
|
|
9
|
+
* - a short `#abc` applies on blur or Enter (focus stays), so typing
|
|
10
|
+
* `#abcdef` does not flash `#aabbcc`;
|
|
11
|
+
* - invalid input reverts to the current value on blur.
|
|
12
|
+
*/
|
|
13
|
+
const useHexDraft = (value, onChange) => {
|
|
14
|
+
// null: not editing, show `value`.
|
|
15
|
+
const [draft, setDraft] = useState(null);
|
|
16
|
+
const commit = text => {
|
|
17
|
+
const hex = normalizeHex(text);
|
|
18
|
+
if (hex && hex !== value) onChange(hex);
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
invalid: draft !== null && normalizeHex(draft) === null,
|
|
22
|
+
inputProps: {
|
|
23
|
+
value: draft !== null && draft !== void 0 ? draft : value,
|
|
24
|
+
onFocus: () => setDraft(value),
|
|
25
|
+
onChange: event => {
|
|
26
|
+
const text = event.target.value;
|
|
27
|
+
setDraft(text);
|
|
28
|
+
if (FULL_HEX_RE.test(text.trim())) commit(text);
|
|
29
|
+
},
|
|
30
|
+
onBlur: () => {
|
|
31
|
+
if (draft !== null) commit(draft);
|
|
32
|
+
setDraft(null);
|
|
33
|
+
},
|
|
34
|
+
// Applies a short HEX and keeps focus in the field.
|
|
35
|
+
onKeyDown: event => {
|
|
36
|
+
if (event.key !== "Enter" || draft === null) return;
|
|
37
|
+
const hex = normalizeHex(draft);
|
|
38
|
+
if (hex) {
|
|
39
|
+
commit(draft);
|
|
40
|
+
setDraft(hex);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export { useHexDraft };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PointerEvent as ReactPointerEvent } from "react";
|
|
2
|
+
/** Pointer position inside the element, 0–1 on each axis. */
|
|
3
|
+
export interface DragPosition {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Dragging inside an element with Pointer Events (mouse, touch, pen).
|
|
9
|
+
* Pointer capture keeps tracking outside the element without document
|
|
10
|
+
* listeners. The element needs `touch-action: none`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const usePointerDrag: <T extends HTMLElement>(onMove: (position: DragPosition) => void) => {
|
|
13
|
+
ref: import("react").RefObject<T | null>;
|
|
14
|
+
handlers: {
|
|
15
|
+
onPointerDown: (event: ReactPointerEvent<T>) => void;
|
|
16
|
+
onPointerMove: (event: ReactPointerEvent<T>) => void;
|
|
17
|
+
onPointerUp: (event: ReactPointerEvent<T>) => void;
|
|
18
|
+
onPointerCancel: (event: ReactPointerEvent<T>) => void;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import { clamp } from '../utils/color.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dragging inside an element with Pointer Events (mouse, touch, pen).
|
|
6
|
+
* Pointer capture keeps tracking outside the element without document
|
|
7
|
+
* listeners. The element needs `touch-action: none`.
|
|
8
|
+
*/
|
|
9
|
+
const usePointerDrag = onMove => {
|
|
10
|
+
const ref = useRef(null);
|
|
11
|
+
const onMoveRef = useRef(onMove);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
onMoveRef.current = onMove;
|
|
14
|
+
});
|
|
15
|
+
const move = useCallback(event => {
|
|
16
|
+
const element = ref.current;
|
|
17
|
+
if (!element) return;
|
|
18
|
+
const rect = element.getBoundingClientRect();
|
|
19
|
+
onMoveRef.current({
|
|
20
|
+
x: clamp((event.clientX - rect.left) / rect.width),
|
|
21
|
+
y: clamp((event.clientY - rect.top) / rect.height)
|
|
22
|
+
});
|
|
23
|
+
}, []);
|
|
24
|
+
const onPointerDown = useCallback(event => {
|
|
25
|
+
if (event.button !== 0 || !ref.current) return;
|
|
26
|
+
// preventDefault stops text selection but also focus, so focus manually.
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
ref.current.setPointerCapture(event.pointerId);
|
|
29
|
+
ref.current.focus();
|
|
30
|
+
move(event);
|
|
31
|
+
}, [move]);
|
|
32
|
+
const onPointerMove = useCallback(event => {
|
|
33
|
+
var _a;
|
|
34
|
+
if ((_a = ref.current) === null || _a === void 0 ? void 0 : _a.hasPointerCapture(event.pointerId)) move(event);
|
|
35
|
+
}, [move]);
|
|
36
|
+
const onPointerUp = useCallback(event => {
|
|
37
|
+
var _a;
|
|
38
|
+
if ((_a = ref.current) === null || _a === void 0 ? void 0 : _a.hasPointerCapture(event.pointerId)) {
|
|
39
|
+
ref.current.releasePointerCapture(event.pointerId);
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
42
|
+
return {
|
|
43
|
+
ref,
|
|
44
|
+
handlers: {
|
|
45
|
+
onPointerDown,
|
|
46
|
+
onPointerMove,
|
|
47
|
+
onPointerUp,
|
|
48
|
+
onPointerCancel: onPointerUp
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export { usePointerDrag };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { Placement } from "@floating-ui/react";
|
|
3
|
+
export interface ColorPickerPreset {
|
|
4
|
+
/** HEX color, for example `#d81b60`. */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Accessible name and tooltip of the swatch. */
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ColorPickerLabels {
|
|
10
|
+
/** Accessible name of the default trigger. */
|
|
11
|
+
trigger: string;
|
|
12
|
+
/** Accessible name of the preset grid. */
|
|
13
|
+
palette: string;
|
|
14
|
+
/** "+" button that opens the custom color dialog. */
|
|
15
|
+
addCustom: string;
|
|
16
|
+
customTitle: string;
|
|
17
|
+
customDescription: string;
|
|
18
|
+
/** Sample text in the preview circle. */
|
|
19
|
+
preview: string;
|
|
20
|
+
hex: string;
|
|
21
|
+
eyeDropper: string;
|
|
22
|
+
saturation: string;
|
|
23
|
+
hue: string;
|
|
24
|
+
cancel: string;
|
|
25
|
+
save: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ColorPickerProps {
|
|
28
|
+
/** Current color in HEX. A color missing from `presets` is shown as an extra swatch. */
|
|
29
|
+
value?: string;
|
|
30
|
+
/** Called with the chosen color as lowercase `#rrggbb`. */
|
|
31
|
+
onChange: (color: string) => void;
|
|
32
|
+
/** Preset colors. Defaults to `COLOR_PICKER_PRESETS`. */
|
|
33
|
+
presets?: ColorPickerPreset[];
|
|
34
|
+
/** Number of grid columns. */
|
|
35
|
+
columns?: number;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
placement?: Placement;
|
|
38
|
+
zIndex?: number;
|
|
39
|
+
/** Texts and accessible names. Missing keys use English defaults. */
|
|
40
|
+
labels?: Partial<ColorPickerLabels>;
|
|
41
|
+
/** Custom trigger element. Defaults to a round button with the current color. */
|
|
42
|
+
children?: ReactElement;
|
|
43
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const COLOR_PICKER_PRESETS = [{
|
|
2
|
+
value: "#ad1457",
|
|
3
|
+
label: "Radicchio"
|
|
4
|
+
}, {
|
|
5
|
+
value: "#f4511e",
|
|
6
|
+
label: "Tangerine"
|
|
7
|
+
}, {
|
|
8
|
+
value: "#e4c441",
|
|
9
|
+
label: "Citron"
|
|
10
|
+
}, {
|
|
11
|
+
value: "#0b8043",
|
|
12
|
+
label: "Basil"
|
|
13
|
+
}, {
|
|
14
|
+
value: "#3f51b5",
|
|
15
|
+
label: "Blueberry"
|
|
16
|
+
}, {
|
|
17
|
+
value: "#8e24aa",
|
|
18
|
+
label: "Grape"
|
|
19
|
+
}, {
|
|
20
|
+
value: "#d81b60",
|
|
21
|
+
label: "Cherry blossom"
|
|
22
|
+
}, {
|
|
23
|
+
value: "#ef6c00",
|
|
24
|
+
label: "Pumpkin"
|
|
25
|
+
}, {
|
|
26
|
+
value: "#c0ca33",
|
|
27
|
+
label: "Avocado"
|
|
28
|
+
}, {
|
|
29
|
+
value: "#009688",
|
|
30
|
+
label: "Eucalyptus"
|
|
31
|
+
}, {
|
|
32
|
+
value: "#7986cb",
|
|
33
|
+
label: "Lavender"
|
|
34
|
+
}, {
|
|
35
|
+
value: "#795548",
|
|
36
|
+
label: "Cocoa"
|
|
37
|
+
}, {
|
|
38
|
+
value: "#d50000",
|
|
39
|
+
label: "Tomato"
|
|
40
|
+
}, {
|
|
41
|
+
value: "#f09300",
|
|
42
|
+
label: "Mango"
|
|
43
|
+
}, {
|
|
44
|
+
value: "#7cb342",
|
|
45
|
+
label: "Pistachio"
|
|
46
|
+
}, {
|
|
47
|
+
value: "#039be5",
|
|
48
|
+
label: "Peacock"
|
|
49
|
+
}, {
|
|
50
|
+
value: "#b39ddb",
|
|
51
|
+
label: "Wisteria"
|
|
52
|
+
}, {
|
|
53
|
+
value: "#616161",
|
|
54
|
+
label: "Graphite"
|
|
55
|
+
}, {
|
|
56
|
+
value: "#e67c73",
|
|
57
|
+
label: "Flamingo"
|
|
58
|
+
}, {
|
|
59
|
+
value: "#f6bf26",
|
|
60
|
+
label: "Banana"
|
|
61
|
+
}, {
|
|
62
|
+
value: "#33b679",
|
|
63
|
+
label: "Sage"
|
|
64
|
+
}, {
|
|
65
|
+
value: "#4285f4",
|
|
66
|
+
label: "Cobalt"
|
|
67
|
+
}, {
|
|
68
|
+
value: "#9e69af",
|
|
69
|
+
label: "Amethyst"
|
|
70
|
+
}, {
|
|
71
|
+
value: "#a79b8e",
|
|
72
|
+
label: "Birch"
|
|
73
|
+
}];
|
|
74
|
+
const DEFAULT_LABELS = {
|
|
75
|
+
trigger: "Choose color",
|
|
76
|
+
palette: "Colors",
|
|
77
|
+
addCustom: "Add custom color",
|
|
78
|
+
customTitle: "Choose custom color",
|
|
79
|
+
customDescription: "Choose a background color. The text color will be adjusted automatically.",
|
|
80
|
+
preview: "A",
|
|
81
|
+
hex: "Hex",
|
|
82
|
+
eyeDropper: "Pick a color from the screen",
|
|
83
|
+
saturation: "Saturation and brightness",
|
|
84
|
+
hue: "Hue",
|
|
85
|
+
cancel: "Cancel",
|
|
86
|
+
save: "Save"
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export { COLOR_PICKER_PRESETS, DEFAULT_LABELS };
|