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.
Files changed (84) hide show
  1. package/ColorPicker/ColorPicker.d.ts +3 -0
  2. package/ColorPicker/ColorPicker.js +116 -0
  3. package/ColorPicker/ColorPicker.module.css +22 -0
  4. package/ColorPicker/assets/none.png +0 -0
  5. package/ColorPicker/components/ColorPreview/ColorPreview.module.css +23 -0
  6. package/ColorPicker/components/ColorPreview/index.d.ts +10 -0
  7. package/ColorPicker/components/ColorPreview/index.js +29 -0
  8. package/ColorPicker/components/CustomColorModal/CustomColorModal.module.css +18 -0
  9. package/ColorPicker/components/CustomColorModal/index.d.ts +15 -0
  10. package/ColorPicker/components/CustomColorModal/index.js +107 -0
  11. package/ColorPicker/components/HueSlider/HueSlider.module.css +31 -0
  12. package/ColorPicker/components/HueSlider/index.d.ts +10 -0
  13. package/ColorPicker/components/HueSlider/index.js +73 -0
  14. package/ColorPicker/components/Palette/Palette.module.css +12 -0
  15. package/ColorPicker/components/Palette/index.d.ts +19 -0
  16. package/ColorPicker/components/Palette/index.js +99 -0
  17. package/ColorPicker/components/SaturationPanel/SaturationPanel.module.css +26 -0
  18. package/ColorPicker/components/SaturationPanel/index.d.ts +13 -0
  19. package/ColorPicker/components/SaturationPanel/index.js +86 -0
  20. package/ColorPicker/components/Swatch/Swatch.module.css +36 -0
  21. package/ColorPicker/components/Swatch/index.d.ts +14 -0
  22. package/ColorPicker/components/Swatch/index.js +47 -0
  23. package/ColorPicker/hooks/useEyeDropper.d.ts +8 -0
  24. package/ColorPicker/hooks/useEyeDropper.js +38 -0
  25. package/ColorPicker/hooks/useHexDraft.d.ts +19 -0
  26. package/ColorPicker/hooks/useHexDraft.js +49 -0
  27. package/ColorPicker/hooks/usePointerDrag.d.ts +20 -0
  28. package/ColorPicker/hooks/usePointerDrag.js +55 -0
  29. package/ColorPicker/index.d.ts +3 -0
  30. package/ColorPicker/index.js +9 -0
  31. package/ColorPicker/index.props.d.ts +43 -0
  32. package/ColorPicker/package.json +9 -0
  33. package/ColorPicker/presets.d.ts +3 -0
  34. package/ColorPicker/presets.js +92 -0
  35. package/ColorPicker/utils/color.d.ts +25 -0
  36. package/ColorPicker/utils/color.js +162 -0
  37. package/Icons/Pipette.d.ts +3 -0
  38. package/Icons/Pipette.js +35 -0
  39. package/Icons/index.d.ts +1 -0
  40. package/Icons/index.js +2 -0
  41. package/esm/ColorPicker/ColorPicker.d.ts +3 -0
  42. package/esm/ColorPicker/ColorPicker.js +108 -0
  43. package/esm/ColorPicker/ColorPicker.module.css +22 -0
  44. package/esm/ColorPicker/assets/none.png +0 -0
  45. package/esm/ColorPicker/components/ColorPreview/ColorPreview.module.css +23 -0
  46. package/esm/ColorPicker/components/ColorPreview/index.d.ts +10 -0
  47. package/esm/ColorPicker/components/ColorPreview/index.js +21 -0
  48. package/esm/ColorPicker/components/CustomColorModal/CustomColorModal.module.css +18 -0
  49. package/esm/ColorPicker/components/CustomColorModal/index.d.ts +15 -0
  50. package/esm/ColorPicker/components/CustomColorModal/index.js +100 -0
  51. package/esm/ColorPicker/components/HueSlider/HueSlider.module.css +31 -0
  52. package/esm/ColorPicker/components/HueSlider/index.d.ts +10 -0
  53. package/esm/ColorPicker/components/HueSlider/index.js +66 -0
  54. package/esm/ColorPicker/components/Palette/Palette.module.css +12 -0
  55. package/esm/ColorPicker/components/Palette/index.d.ts +19 -0
  56. package/esm/ColorPicker/components/Palette/index.js +92 -0
  57. package/esm/ColorPicker/components/SaturationPanel/SaturationPanel.module.css +26 -0
  58. package/esm/ColorPicker/components/SaturationPanel/index.d.ts +13 -0
  59. package/esm/ColorPicker/components/SaturationPanel/index.js +79 -0
  60. package/esm/ColorPicker/components/Swatch/Swatch.module.css +36 -0
  61. package/esm/ColorPicker/components/Swatch/index.d.ts +14 -0
  62. package/esm/ColorPicker/components/Swatch/index.js +38 -0
  63. package/esm/ColorPicker/hooks/useEyeDropper.d.ts +8 -0
  64. package/esm/ColorPicker/hooks/useEyeDropper.js +36 -0
  65. package/esm/ColorPicker/hooks/useHexDraft.d.ts +19 -0
  66. package/esm/ColorPicker/hooks/useHexDraft.js +47 -0
  67. package/esm/ColorPicker/hooks/usePointerDrag.d.ts +20 -0
  68. package/esm/ColorPicker/hooks/usePointerDrag.js +53 -0
  69. package/esm/ColorPicker/index.d.ts +3 -0
  70. package/esm/ColorPicker/index.js +2 -0
  71. package/esm/ColorPicker/index.props.d.ts +43 -0
  72. package/esm/ColorPicker/presets.d.ts +3 -0
  73. package/esm/ColorPicker/presets.js +89 -0
  74. package/esm/ColorPicker/utils/color.d.ts +25 -0
  75. package/esm/ColorPicker/utils/color.js +152 -0
  76. package/esm/Icons/Pipette.d.ts +3 -0
  77. package/esm/Icons/Pipette.js +29 -0
  78. package/esm/Icons/index.d.ts +1 -0
  79. package/esm/Icons/index.js +1 -0
  80. package/esm/index.d.ts +2 -0
  81. package/esm/index.js +3 -0
  82. package/index.d.ts +2 -0
  83. package/index.js +7 -0
  84. package/package.json +1 -1
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+
3
+ const clamp = (value, min = 0, max = 1) => Math.min(max, Math.max(min, value));
4
+ const round1 = value => Math.round(value * 10) / 10;
5
+ const hsv2rgb = ({
6
+ h,
7
+ s,
8
+ v
9
+ }) => {
10
+ const sat = s / 100;
11
+ const val = v / 100;
12
+ const channel = n => {
13
+ const k = (n + h / 60) % 6;
14
+ return Math.round((val - val * sat * Math.max(0, Math.min(k, 4 - k, 1))) * 255);
15
+ };
16
+ return {
17
+ r: channel(5),
18
+ g: channel(3),
19
+ b: channel(1)
20
+ };
21
+ };
22
+ // Not rounded, so repeated conversions do not accumulate errors.
23
+ const rgb2hsv = ({
24
+ r,
25
+ g,
26
+ b
27
+ }) => {
28
+ const rn = r / 255;
29
+ const gn = g / 255;
30
+ const bn = b / 255;
31
+ const max = Math.max(rn, gn, bn);
32
+ const min = Math.min(rn, gn, bn);
33
+ const delta = max - min;
34
+ let h = 0;
35
+ if (delta !== 0) {
36
+ if (max === rn) {
37
+ h = (gn - bn) / delta % 6;
38
+ } else if (max === gn) {
39
+ h = (bn - rn) / delta + 2;
40
+ } else {
41
+ h = (rn - gn) / delta + 4;
42
+ }
43
+ h *= 60;
44
+ if (h < 0) h += 360;
45
+ }
46
+ return {
47
+ h,
48
+ s: max === 0 ? 0 : delta / max * 100,
49
+ v: max * 100
50
+ };
51
+ };
52
+ /** HSV → `hsl(...)` for inline styles. */
53
+ const hsv2css = ({
54
+ h,
55
+ s,
56
+ v
57
+ }) => {
58
+ const sat = s / 100;
59
+ const val = v / 100;
60
+ const l = val * (1 - sat / 2);
61
+ const sl = l === 0 || l === 1 ? 0 : (val - l) / Math.min(l, 1 - l);
62
+ return `hsl(${round1(h)}, ${round1(sl * 100)}%, ${round1(l * 100)}%)`;
63
+ };
64
+ const HEX_RE = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
65
+ /** `#ABC`, `abc`, `#aabbcc` → `#aabbcc`. Invalid input → `null`. */
66
+ const normalizeHex = input => {
67
+ const match = HEX_RE.exec(input.trim());
68
+ if (!match) return null;
69
+ const digits = match[1].length === 3 ? match[1].replace(/./g, c => c + c) : match[1];
70
+ return `#${digits.toLowerCase()}`;
71
+ };
72
+ const hex2rgb = hex => {
73
+ const normalized = normalizeHex(hex);
74
+ if (!normalized) return null;
75
+ const int = parseInt(normalized.slice(1), 16);
76
+ return {
77
+ r: int >> 16 & 255,
78
+ g: int >> 8 & 255,
79
+ b: int & 255
80
+ };
81
+ };
82
+ const rgb2hex = ({
83
+ r,
84
+ g,
85
+ b
86
+ }) => `#${[r, g, b].map(c => Math.round(clamp(c, 0, 255)).toString(16).padStart(2, "0")).join("")}`;
87
+ const hsv2hex = hsv => rgb2hex(hsv2rgb(hsv));
88
+ const hex2hsv = hex => {
89
+ const rgb = hex2rgb(hex);
90
+ return rgb ? rgb2hsv(rgb) : null;
91
+ };
92
+ const RGB_RE = /^rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})/i;
93
+ /** HEX or `rgb()`/`rgba()` → `#rrggbb`. Invalid input → `null`. */
94
+ const toHex = input => {
95
+ const rgbMatch = RGB_RE.exec(input.trim());
96
+ if (rgbMatch) {
97
+ const [r, g, b] = rgbMatch.slice(1).map(Number);
98
+ return rgb2hex({
99
+ r,
100
+ g,
101
+ b
102
+ });
103
+ }
104
+ return normalizeHex(input);
105
+ };
106
+ /**
107
+ * HEX → HSV that keeps hue and saturation from `prev` where the color has
108
+ * none: grey has no hue, black has neither. Without this, typing `#000000`
109
+ * would jump the hue slider to 0 (red).
110
+ */
111
+ const toHsvPreservingHue = (hex, prev) => {
112
+ const next = hex2hsv(hex);
113
+ if (!next) return null;
114
+ if (next.v === 0) return {
115
+ h: prev.h,
116
+ s: prev.s,
117
+ v: 0
118
+ };
119
+ if (next.s === 0) return {
120
+ h: prev.h,
121
+ s: 0,
122
+ v: next.v
123
+ };
124
+ return next;
125
+ };
126
+ /** WCAG 2.x relative luminance, 0–1. */
127
+ const relativeLuminance = ({
128
+ r,
129
+ g,
130
+ b
131
+ }) => {
132
+ const [lr, lg, lb] = [r, g, b].map(c => {
133
+ const x = c / 255;
134
+ return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
135
+ });
136
+ return 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
137
+ };
138
+ const WHITE_LUMINANCE = 1;
139
+ const BLACK_LUMINANCE = 0;
140
+ const contrast = (a, b) => (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
141
+ /** Whether light text reads better than dark text on `background` (HEX). */
142
+ const prefersLightText = background => {
143
+ const rgb = hex2rgb(background);
144
+ if (!rgb) return false;
145
+ const luminance = relativeLuminance(rgb);
146
+ return contrast(luminance, WHITE_LUMINANCE) >= contrast(luminance, BLACK_LUMINANCE);
147
+ };
148
+ /** A light color that merges with a light surface and needs an outline. */
149
+ const isLightColor = (hex, threshold = 0.85) => {
150
+ const rgb = hex2rgb(hex);
151
+ return rgb ? relativeLuminance(rgb) >= threshold : false;
152
+ };
153
+
154
+ exports.clamp = clamp;
155
+ exports.hex2hsv = hex2hsv;
156
+ exports.hsv2css = hsv2css;
157
+ exports.hsv2hex = hsv2hex;
158
+ exports.isLightColor = isLightColor;
159
+ exports.normalizeHex = normalizeHex;
160
+ exports.prefersLightText = prefersLightText;
161
+ exports.toHex = toHex;
162
+ exports.toHsvPreservingHue = toHsvPreservingHue;
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import type { Props } from "./index.props";
3
+ export declare const Pipette: ({ fill, width, height, ref, ...props }: Props) => React.JSX.Element;
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ var tslib = require('tslib');
4
+ var React = require('react');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var React__default = /*#__PURE__*/_interopDefault(React);
9
+
10
+ const Pipette = _a => {
11
+ var {
12
+ fill = "currentColor",
13
+ width = 24,
14
+ height = 24,
15
+ ref
16
+ } = _a,
17
+ props = tslib.__rest(_a, ["fill", "width", "height", "ref"]);
18
+ return /*#__PURE__*/React__default.default.createElement("svg", Object.assign({
19
+ xmlns: "http://www.w3.org/2000/svg",
20
+ width: width,
21
+ height: height
22
+ }, props, {
23
+ ref: ref,
24
+ viewBox: "0 0 24 24",
25
+ fill: "none"
26
+ }), /*#__PURE__*/React__default.default.createElement("path", {
27
+ stroke: fill,
28
+ strokeWidth: "1.8",
29
+ strokeLinecap: "round",
30
+ strokeLinejoin: "round",
31
+ d: "M14.5 4.5l5 5M17 2.5a2.12 2.12 0 0 1 3 3L17.5 8l1 1-1.5 1.5-5-5L13.5 4l1 1L17 2.5zM12 7l-8 8v4h4l8-8"
32
+ }));
33
+ };
34
+
35
+ exports.Pipette = Pipette;
package/Icons/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export { Help } from "./Help";
14
14
  export { Info } from "./Info";
15
15
  export { Minus } from "./Minus";
16
16
  export { More } from "./More";
17
+ export { Pipette } from "./Pipette";
17
18
  export { Plus } from "./Plus";
18
19
  export { Setting } from "./Setting";
19
20
  export { Shield } from "./Shield";
package/Icons/index.js CHANGED
@@ -16,6 +16,7 @@ var Help = require('./Help.js');
16
16
  var Info = require('./Info.js');
17
17
  var Minus = require('./Minus.js');
18
18
  var More = require('./More.js');
19
+ var Pipette = require('./Pipette.js');
19
20
  var Plus = require('./Plus.js');
20
21
  var Setting = require('./Setting.js');
21
22
  var Shield = require('./Shield.js');
@@ -40,6 +41,7 @@ exports.Help = Help.Help;
40
41
  exports.Info = Info.Info;
41
42
  exports.Minus = Minus.Minus;
42
43
  exports.More = More.More;
44
+ exports.Pipette = Pipette.Pipette;
43
45
  exports.Plus = Plus.Plus;
44
46
  exports.Setting = Setting.Setting;
45
47
  exports.Shield = Shield.Shield;
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import type { ColorPickerProps } from "./index.props";
3
+ export declare const ColorPicker: ({ value, onChange, presets, columns, disabled, placement, zIndex, labels: labelsProp, children, }: ColorPickerProps) => React.JSX.Element;
@@ -0,0 +1,108 @@
1
+ import React, { useState, useRef, useMemo } from 'react';
2
+ import clsx from 'clsx';
3
+ import { Button } from '../Button/index.js';
4
+ import { Dropdown } from '../Dropdown/index.js';
5
+ import { CustomColorModal } from './components/CustomColorModal/index.js';
6
+ import { Palette } from './components/Palette/index.js';
7
+ import { DEFAULT_LABELS, COLOR_PICKER_PRESETS } from './presets.js';
8
+ import { normalizeHex } from './utils/color.js';
9
+ import styles from './ColorPicker.module.css';
10
+
11
+ const normalizePresets = presets => presets.flatMap(preset => {
12
+ const value = normalizeHex(preset.value);
13
+ return value ? [Object.assign(Object.assign({}, preset), {
14
+ value
15
+ })] : [];
16
+ });
17
+ const ColorPicker = ({
18
+ value,
19
+ onChange,
20
+ presets = COLOR_PICKER_PRESETS,
21
+ columns = 6,
22
+ disabled,
23
+ placement = "bottom-start",
24
+ zIndex,
25
+ labels: labelsProp,
26
+ children
27
+ }) => {
28
+ var _a, _b;
29
+ const [isOpen, setOpen] = useState(false);
30
+ const [isCustomOpen, setCustomOpen] = useState(false);
31
+ const rootRef = useRef(null);
32
+ const overlayRef = useRef(null);
33
+ const labels = Object.assign(Object.assign({}, DEFAULT_LABELS), labelsProp);
34
+ const current = value ? normalizeHex(value) : null;
35
+ const normalizedPresets = useMemo(() => normalizePresets(presets), [presets]);
36
+ // Selection, "+" and Escape remove the focused swatch together with the
37
+ // dropdown: return focus to the trigger. The custom color dialog then
38
+ // restores focus to the trigger as well.
39
+ const closeDropdown = () => {
40
+ var _a, _b;
41
+ const trigger = (_a = rootRef.current) === null || _a === void 0 ? void 0 : _a.firstElementChild;
42
+ if (((_b = overlayRef.current) === null || _b === void 0 ? void 0 : _b.contains(document.activeElement)) && trigger instanceof HTMLElement) {
43
+ trigger.focus();
44
+ }
45
+ setOpen(false);
46
+ };
47
+ const handleVisibleChange = visible => {
48
+ if (visible) setOpen(true);else closeDropdown();
49
+ };
50
+ const handleSelect = hex => {
51
+ closeDropdown();
52
+ onChange(hex);
53
+ };
54
+ const handleAddClick = () => {
55
+ closeDropdown();
56
+ setCustomOpen(true);
57
+ };
58
+ const handleSave = hex => {
59
+ setCustomOpen(false);
60
+ onChange(hex);
61
+ };
62
+ const trigger = children !== null && children !== void 0 ? children : (/*#__PURE__*/React.createElement(Button, {
63
+ variant: "outline",
64
+ className: clsx({
65
+ [styles.trigger_empty]: !current
66
+ }),
67
+ disabled: disabled,
68
+ "aria-label": labels.trigger,
69
+ icon: /*#__PURE__*/React.createElement("span", {
70
+ className: clsx(styles.dot, {
71
+ [styles.dot_hidden]: !current
72
+ }),
73
+ style: current ? {
74
+ backgroundColor: current
75
+ } : undefined
76
+ })
77
+ }));
78
+ return /*#__PURE__*/React.createElement("span", {
79
+ ref: rootRef,
80
+ className: styles.root
81
+ }, /*#__PURE__*/React.createElement(Dropdown, {
82
+ trigger: ["click"],
83
+ visible: isOpen,
84
+ onVisibleChange: handleVisibleChange,
85
+ preventAutoClose: true,
86
+ disabled: disabled,
87
+ placement: placement,
88
+ zIndex: zIndex,
89
+ overlay: /*#__PURE__*/React.createElement("div", {
90
+ ref: overlayRef
91
+ }, /*#__PURE__*/React.createElement(Palette, {
92
+ presets: normalizedPresets,
93
+ value: current,
94
+ columns: columns,
95
+ labels: labels,
96
+ onSelect: handleSelect,
97
+ onAddClick: handleAddClick,
98
+ onTabOut: closeDropdown
99
+ }))
100
+ }, trigger), isCustomOpen && (/*#__PURE__*/React.createElement(CustomColorModal, {
101
+ initialColor: (_b = current !== null && current !== void 0 ? current : (_a = normalizedPresets[0]) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : "#000000",
102
+ labels: labels,
103
+ onSave: handleSave,
104
+ onClose: () => setCustomOpen(false)
105
+ })));
106
+ };
107
+
108
+ export { ColorPicker };
@@ -0,0 +1,22 @@
1
+ .root {
2
+ display: contents;
3
+ }
4
+
5
+ .dot {
6
+ border-radius: var(--radius-full);
7
+ box-shadow: inset 0 0 0 var(--border-width-hairline) var(--color-border-default);
8
+ display: block;
9
+ height: var(--space-4);
10
+ width: var(--space-4);
11
+ }
12
+
13
+ .trigger_empty {
14
+ background-image: url("./assets/none.png");
15
+ background-position: center;
16
+ background-repeat: no-repeat;
17
+ background-size: cover;
18
+ }
19
+
20
+ .dot_hidden {
21
+ display: none;
22
+ }
Binary file
@@ -0,0 +1,23 @@
1
+ .root {
2
+ align-items: center;
3
+ border-radius: var(--radius-full);
4
+ box-shadow: inset 0 0 0 var(--border-width-hairline) var(--color-border-subtle);
5
+ display: inline-flex;
6
+ flex-shrink: 0;
7
+ font-family: var(--font-family-ui);
8
+ font-size: var(--font-size-heading-md);
9
+ font-weight: var(--font-weight-medium);
10
+ height: var(--space-12);
11
+ justify-content: center;
12
+ line-height: 1;
13
+ transition: background-color 0.1s ease, color 0.1s ease;
14
+ width: var(--space-12);
15
+ }
16
+
17
+ .text_light {
18
+ color: var(--color-neutral-0);
19
+ }
20
+
21
+ .text_dark {
22
+ color: var(--color-neutral-950);
23
+ }
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import type { ReactNode } from "react";
3
+ import type { HSV } from "../../utils/color";
4
+ interface ColorPreviewProps {
5
+ color: HSV;
6
+ children: ReactNode;
7
+ }
8
+ /** Circle with the color and a sample text in the automatically adjusted color. */
9
+ export declare const ColorPreview: ({ color, children }: ColorPreviewProps) => React.JSX.Element;
10
+ export {};
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import clsx from 'clsx';
3
+ import { hsv2hex, prefersLightText } from '../../utils/color.js';
4
+ import styles from './ColorPreview.module.css';
5
+
6
+ /** Circle with the color and a sample text in the automatically adjusted color. */
7
+ const ColorPreview = ({
8
+ color,
9
+ children
10
+ }) => {
11
+ const hex = hsv2hex(color);
12
+ return /*#__PURE__*/React.createElement("span", {
13
+ "aria-hidden": "true",
14
+ className: clsx(styles.root, prefersLightText(hex) ? styles.text_light : styles.text_dark),
15
+ style: {
16
+ backgroundColor: hex
17
+ }
18
+ }, children);
19
+ };
20
+
21
+ export { ColorPreview };
@@ -0,0 +1,18 @@
1
+ .picker {
2
+ align-items: start;
3
+ display: grid;
4
+ gap: var(--space-5) var(--space-4);
5
+ grid-template-columns: auto 1fr;
6
+ margin-top: var(--space-5);
7
+ }
8
+
9
+ .side {
10
+ align-items: center;
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: var(--space-4);
14
+ }
15
+
16
+ .full_row {
17
+ grid-column: 1/-1;
18
+ }
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import type { ColorPickerLabels } from "../../index.props";
3
+ interface CustomColorModalProps {
4
+ /** `#rrggbb` the picker starts from. */
5
+ initialColor: string;
6
+ labels: ColorPickerLabels;
7
+ onSave: (hex: string) => void;
8
+ onClose: () => void;
9
+ }
10
+ /**
11
+ * "Choose custom color" dialog. The draft is kept in HSV: HEX loses hue and
12
+ * saturation of grey and black, and the sliders would jump.
13
+ */
14
+ export declare const CustomColorModal: ({ initialColor, labels, onSave, onClose, }: CustomColorModalProps) => React.JSX.Element;
15
+ export {};
@@ -0,0 +1,100 @@
1
+ import React, { useState } from 'react';
2
+ import { Button } from '../../../Button/index.js';
3
+ import 'tslib';
4
+ import { Pipette } from '../../../Icons/Pipette.js';
5
+ import { Input } from '../../../Input/index.js';
6
+ import { Modal } from '../../../Modal/index.js';
7
+ import { Typography } from '../../../Typography/index.js';
8
+ import { useEyeDropper } from '../../hooks/useEyeDropper.js';
9
+ import { useHexDraft } from '../../hooks/useHexDraft.js';
10
+ import { hex2hsv, hsv2hex, toHsvPreservingHue } from '../../utils/color.js';
11
+ import { ColorPreview } from '../ColorPreview/index.js';
12
+ import { HueSlider } from '../HueSlider/index.js';
13
+ import { SaturationPanel } from '../SaturationPanel/index.js';
14
+ import styles from './CustomColorModal.module.css';
15
+
16
+ const BLACK = {
17
+ h: 0,
18
+ s: 0,
19
+ v: 0
20
+ };
21
+ /**
22
+ * "Choose custom color" dialog. The draft is kept in HSV: HEX loses hue and
23
+ * saturation of grey and black, and the sliders would jump.
24
+ */
25
+ const CustomColorModal = ({
26
+ initialColor,
27
+ labels,
28
+ onSave,
29
+ onClose
30
+ }) => {
31
+ const [color, setColor] = useState(() => {
32
+ var _a;
33
+ return (_a = hex2hsv(initialColor)) !== null && _a !== void 0 ? _a : BLACK;
34
+ });
35
+ const hex = hsv2hex(color);
36
+ const {
37
+ isSupported,
38
+ pick
39
+ } = useEyeDropper();
40
+ const setHex = next => setColor(prev => {
41
+ var _a;
42
+ return (_a = toHsvPreservingHue(next, prev)) !== null && _a !== void 0 ? _a : prev;
43
+ });
44
+ const {
45
+ invalid,
46
+ inputProps
47
+ } = useHexDraft(hex, setHex);
48
+ const handlePick = async () => {
49
+ const picked = await pick();
50
+ if (picked) setHex(picked);
51
+ };
52
+ return /*#__PURE__*/React.createElement(Modal, {
53
+ onClose: onClose
54
+ }, /*#__PURE__*/React.createElement(Modal.Header, null, /*#__PURE__*/React.createElement(Modal.Title, null, labels.customTitle)), /*#__PURE__*/React.createElement(Modal.Body, null, /*#__PURE__*/React.createElement(Typography, {
55
+ tag: "p",
56
+ variant: "secondary",
57
+ size: "text-sm"
58
+ }, labels.customDescription), /*#__PURE__*/React.createElement("div", {
59
+ className: styles.picker
60
+ }, /*#__PURE__*/React.createElement("div", {
61
+ className: styles.side
62
+ }, /*#__PURE__*/React.createElement(ColorPreview, {
63
+ color: color
64
+ }, labels.preview), isSupported && (/*#__PURE__*/React.createElement(Button, {
65
+ variant: "secondary",
66
+ size: "l",
67
+ icon: /*#__PURE__*/React.createElement(Pipette, {
68
+ width: 20,
69
+ height: 20,
70
+ "aria-hidden": "true"
71
+ }),
72
+ "aria-label": labels.eyeDropper,
73
+ title: labels.eyeDropper,
74
+ onClick: handlePick
75
+ }))), /*#__PURE__*/React.createElement(SaturationPanel, {
76
+ value: color,
77
+ onChange: setColor,
78
+ "aria-label": labels.saturation
79
+ }), /*#__PURE__*/React.createElement("div", {
80
+ className: styles.full_row
81
+ }, /*#__PURE__*/React.createElement(HueSlider, {
82
+ value: color,
83
+ onChange: setColor,
84
+ "aria-label": labels.hue
85
+ })), /*#__PURE__*/React.createElement(Input, Object.assign({
86
+ className: styles.full_row,
87
+ label: labels.hex,
88
+ hasError: invalid,
89
+ maxLength: 7,
90
+ spellCheck: false,
91
+ autoComplete: "off"
92
+ }, inputProps)))), /*#__PURE__*/React.createElement(Modal.Footer, null, /*#__PURE__*/React.createElement(Button, {
93
+ variant: "clear",
94
+ onClick: onClose
95
+ }, labels.cancel), /*#__PURE__*/React.createElement(Button, {
96
+ onClick: () => onSave(hex)
97
+ }, labels.save)));
98
+ };
99
+
100
+ export { CustomColorModal };
@@ -0,0 +1,31 @@
1
+ .root {
2
+ background: linear-gradient(to right, hsl(0, 100%, 50%), hsl(60, 100%, 50%), hsl(120, 100%, 50%), hsl(180, 100%, 50%), hsl(240, 100%, 50%), hsl(300, 100%, 50%), hsl(0, 100%, 50%));
3
+ border-radius: var(--radius-full);
4
+ cursor: pointer;
5
+ height: var(--space-3);
6
+ outline: none;
7
+ position: relative;
8
+ touch-action: none;
9
+ -webkit-user-select: none;
10
+ user-select: none;
11
+ }
12
+ .root::before {
13
+ content: "";
14
+ inset: calc(var(--space-2) * -1) 0;
15
+ position: absolute;
16
+ }
17
+ .root:focus-visible {
18
+ box-shadow: 0 0 0 var(--focus-ring-width) var(--color-focus-ring);
19
+ }
20
+
21
+ .thumb {
22
+ border: var(--focus-ring-width) solid var(--color-neutral-0);
23
+ border-radius: var(--radius-full);
24
+ box-shadow: var(--shadow-raised);
25
+ height: var(--space-5);
26
+ pointer-events: none;
27
+ position: absolute;
28
+ top: 50%;
29
+ transform: translate(-50%, -50%);
30
+ width: var(--space-5);
31
+ }
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import type { HSV } from "../../utils/color";
3
+ interface HueSliderProps {
4
+ value: HSV;
5
+ onChange: (value: HSV) => void;
6
+ "aria-label": string;
7
+ }
8
+ /** Hue bar. Arrow keys change by 1°, with Shift by 10°, Home/End go to the ends. */
9
+ export declare const HueSlider: ({ value, onChange, "aria-label": ariaLabel, }: HueSliderProps) => React.JSX.Element;
10
+ export {};
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { usePointerDrag } from '../../hooks/usePointerDrag.js';
3
+ import { hsv2css, clamp } from '../../utils/color.js';
4
+ import styles from './HueSlider.module.css';
5
+
6
+ const STEP = 1;
7
+ const BIG_STEP = 10;
8
+ /** Hue bar. Arrow keys change by 1°, with Shift by 10°, Home/End go to the ends. */
9
+ const HueSlider = ({
10
+ value,
11
+ onChange,
12
+ "aria-label": ariaLabel
13
+ }) => {
14
+ const {
15
+ h
16
+ } = value;
17
+ const {
18
+ ref,
19
+ handlers
20
+ } = usePointerDrag(({
21
+ x
22
+ }) => onChange(Object.assign(Object.assign({}, value), {
23
+ h: x * 360
24
+ })));
25
+ const handleKeyDown = event => {
26
+ const step = event.shiftKey ? BIG_STEP : STEP;
27
+ const next = {
28
+ ArrowLeft: h - step,
29
+ ArrowDown: h - step,
30
+ ArrowRight: h + step,
31
+ ArrowUp: h + step,
32
+ Home: 0,
33
+ End: 360
34
+ };
35
+ if (!(event.key in next)) return;
36
+ event.preventDefault();
37
+ onChange(Object.assign(Object.assign({}, value), {
38
+ h: clamp(next[event.key], 0, 360)
39
+ }));
40
+ };
41
+ return /*#__PURE__*/React.createElement("div", Object.assign({
42
+ ref: ref
43
+ }, handlers, {
44
+ role: "slider",
45
+ tabIndex: 0,
46
+ "aria-label": ariaLabel,
47
+ "aria-orientation": "horizontal",
48
+ "aria-valuemin": 0,
49
+ "aria-valuemax": 360,
50
+ "aria-valuenow": Math.round(h),
51
+ onKeyDown: handleKeyDown,
52
+ className: styles.root
53
+ }), /*#__PURE__*/React.createElement("div", {
54
+ className: styles.thumb,
55
+ style: {
56
+ left: `${h / 360 * 100}%`,
57
+ backgroundColor: hsv2css({
58
+ h,
59
+ s: 100,
60
+ v: 100
61
+ })
62
+ }
63
+ }));
64
+ };
65
+
66
+ export { HueSlider };
@@ -0,0 +1,12 @@
1
+ .root {
2
+ display: grid;
3
+ gap: var(--space-2);
4
+ padding: var(--space-3);
5
+ }
6
+
7
+ .add {
8
+ height: var(--space-6);
9
+ min-width: 0;
10
+ padding: 0;
11
+ width: var(--space-6);
12
+ }