colibri-ui 0.8.95 → 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/Modal/Modal.module.css +10 -0
- package/Modal/index.js +4 -2
- package/Modal/index.props.d.ts +6 -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/Modal/Modal.module.css +10 -0
- package/esm/Modal/index.js +4 -2
- package/esm/Modal/index.props.d.ts +6 -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,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;
|
package/Icons/Pipette.js
ADDED
|
@@ -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;
|
package/Modal/Modal.module.css
CHANGED
|
@@ -61,4 +61,14 @@
|
|
|
61
61
|
max-height: calc(100% - var(--space-8));
|
|
62
62
|
max-width: calc(100% - var(--space-8));
|
|
63
63
|
min-width: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.modal-wrapper_contained.modal-wrapper_fill {
|
|
67
|
+
border: 0;
|
|
68
|
+
border-radius: 0;
|
|
69
|
+
height: 100%;
|
|
70
|
+
max-height: none;
|
|
71
|
+
max-width: none;
|
|
72
|
+
min-width: 0;
|
|
73
|
+
width: 100%;
|
|
64
74
|
}
|
package/Modal/index.js
CHANGED
|
@@ -30,7 +30,8 @@ const ModalRoot = ({
|
|
|
30
30
|
className,
|
|
31
31
|
onClose,
|
|
32
32
|
title,
|
|
33
|
-
withinParent = false
|
|
33
|
+
withinParent = false,
|
|
34
|
+
fillParent = false
|
|
34
35
|
}) => {
|
|
35
36
|
const [dialog, setDialog] = React.useState(null);
|
|
36
37
|
const [container, setContainer] = React.useState(null);
|
|
@@ -108,7 +109,8 @@ const ModalRoot = ({
|
|
|
108
109
|
className: clsx__default.default(styles__default.default["modal-wrapper"], {
|
|
109
110
|
[styles__default.default["modal-wrapper_desktop"]]: isDesktop,
|
|
110
111
|
[styles__default.default["modal-wrapper_mobile"]]: !isDesktop,
|
|
111
|
-
[styles__default.default["modal-wrapper_contained"]]: isContained
|
|
112
|
+
[styles__default.default["modal-wrapper_contained"]]: isContained,
|
|
113
|
+
[styles__default.default["modal-wrapper_fill"]]: isContained && fillParent
|
|
112
114
|
}, className),
|
|
113
115
|
ref: setContainer
|
|
114
116
|
}, content))));
|
package/Modal/index.props.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export type ModalProps = {
|
|
|
13
13
|
* rendered inside another `Modal`.
|
|
14
14
|
*/
|
|
15
15
|
withinParent?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Makes a `withinParent` modal take the whole parent window, with no
|
|
18
|
+
* offset. The parent border and radius form the edges. Has no effect
|
|
19
|
+
* without `withinParent` or a parent modal.
|
|
20
|
+
*/
|
|
21
|
+
fillParent?: boolean;
|
|
16
22
|
};
|
|
17
23
|
export interface ModalHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
18
24
|
/**
|
|
@@ -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 {};
|