@enigmax/primitives 0.24.0 → 0.25.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/dist/{chunk-QJJ34E4G.js → chunk-3MGBZOAU.js} +32 -15
- package/dist/chunk-KVLSOQTI.js +66 -0
- package/dist/{color-OPV3BJV6.js → color-T63FLJNH.js} +183 -120
- package/dist/{index-ZPvlf9vs.d.ts → index-qXOkYQCU.d.ts} +4 -0
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +2 -2
- package/dist/react/input.d.ts +1 -1
- package/dist/react/input.js +2 -2
- package/dist/react-router/index.d.ts +1 -1
- package/dist/react-router/index.js +2 -2
- package/package.json +1 -1
- package/recipes/color/styles.css +20 -0
- package/registry.json +12 -3
- package/src/react/input/color-styles.ts +20 -0
- package/src/react/input/color-swatch.tsx +96 -0
- package/src/react/input/color.tsx +84 -23
- package/src/react/input/index.tsx +27 -4
- package/src/react/input/types.ts +4 -0
- package/dist/chunk-S7EE57YB.js +0 -9
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeValue } from './chunk-
|
|
1
|
+
import { writeValue, ColorSwatch } from './chunk-KVLSOQTI.js';
|
|
2
2
|
import { INPUT_ICON_PATHS } from './chunk-F25CGNQC.js';
|
|
3
3
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
4
|
import { lazy, forwardRef, useRef, useState, useId, useCallback, useEffect, Suspense } from 'react';
|
|
@@ -25,7 +25,7 @@ var COLOR_FIELD_PROPS = {
|
|
|
25
25
|
};
|
|
26
26
|
var PasswordExtras = lazy(() => import('./password-3DRQYAYQ.js').then((module) => ({ default: module.PasswordExtras })));
|
|
27
27
|
var SearchExtras = lazy(() => import('./search-PBZORZ7P.js').then((module) => ({ default: module.SearchExtras })));
|
|
28
|
-
var ColorExtras = lazy(() => import('./color-
|
|
28
|
+
var ColorExtras = lazy(() => import('./color-T63FLJNH.js').then((module) => ({ default: module.ColorExtras })));
|
|
29
29
|
var Input = forwardRef(function Input2(props, forwardedRef) {
|
|
30
30
|
const {
|
|
31
31
|
reveal,
|
|
@@ -71,6 +71,7 @@ var Input = forwardRef(function Input2(props, forwardedRef) {
|
|
|
71
71
|
const innerRef = useRef(null);
|
|
72
72
|
const [element, setElement] = useState(null);
|
|
73
73
|
const [revealed, setRevealed] = useState(false);
|
|
74
|
+
const [colorWanted, setColorWanted] = useState(false);
|
|
74
75
|
const describedBy = useId();
|
|
75
76
|
const isPassword = type === "password";
|
|
76
77
|
const isSearch = type === "search";
|
|
@@ -200,21 +201,37 @@ var Input = forwardRef(function Input2(props, forwardedRef) {
|
|
|
200
201
|
"data-score": score ?? void 0,
|
|
201
202
|
children: [
|
|
202
203
|
/* @__PURE__ */ jsxs("div", { ...fieldProps, "data-enigma-input-field": "", children: [
|
|
203
|
-
isColor && /* @__PURE__ */ jsx(
|
|
204
|
-
|
|
204
|
+
isColor && /* @__PURE__ */ jsx(
|
|
205
|
+
Suspense,
|
|
205
206
|
{
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
207
|
+
fallback: /* @__PURE__ */ jsx(
|
|
208
|
+
ColorSwatch,
|
|
209
|
+
{
|
|
210
|
+
value,
|
|
211
|
+
locked,
|
|
212
|
+
baseline: styles !== false,
|
|
213
|
+
label: colorLabels?.open ?? "Pick a colour",
|
|
214
|
+
onPress: () => setColorWanted(true)
|
|
215
|
+
}
|
|
216
|
+
),
|
|
217
|
+
children: /* @__PURE__ */ jsx(
|
|
218
|
+
ColorExtras,
|
|
219
|
+
{
|
|
220
|
+
openOnMount: colorWanted,
|
|
221
|
+
input: element,
|
|
222
|
+
value,
|
|
223
|
+
format,
|
|
224
|
+
alpha,
|
|
225
|
+
swatches,
|
|
226
|
+
eyedropper,
|
|
227
|
+
placement,
|
|
228
|
+
styles,
|
|
229
|
+
locked,
|
|
230
|
+
labels: colorLabels
|
|
231
|
+
}
|
|
232
|
+
)
|
|
216
233
|
}
|
|
217
|
-
)
|
|
234
|
+
),
|
|
218
235
|
position === "start" && buttons,
|
|
219
236
|
/* @__PURE__ */ jsx(
|
|
220
237
|
"input",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
// src/react/input/color-swatch.tsx
|
|
4
|
+
var BASELINE = {
|
|
5
|
+
display: "block",
|
|
6
|
+
boxSizing: "border-box",
|
|
7
|
+
width: "1.25rem",
|
|
8
|
+
height: "1.25rem",
|
|
9
|
+
padding: 0,
|
|
10
|
+
background: "none",
|
|
11
|
+
border: "1px solid var(--enigma-color-border, #404040)",
|
|
12
|
+
borderRadius: "0.3125rem",
|
|
13
|
+
overflow: "hidden",
|
|
14
|
+
cursor: "pointer"
|
|
15
|
+
};
|
|
16
|
+
function ColorSwatch({ value, open = false, invalid = false, locked = false, label, anchorRef, buttonRef, onPress, baseline = false, children }) {
|
|
17
|
+
return /* @__PURE__ */ jsxs(
|
|
18
|
+
"span",
|
|
19
|
+
{
|
|
20
|
+
ref: anchorRef,
|
|
21
|
+
"data-enigma-color": "",
|
|
22
|
+
"data-open": open ? "" : void 0,
|
|
23
|
+
style: baseline ? { position: "relative", display: "inline-flex", flex: "none" } : void 0,
|
|
24
|
+
children: [
|
|
25
|
+
/* @__PURE__ */ jsx(
|
|
26
|
+
"button",
|
|
27
|
+
{
|
|
28
|
+
ref: buttonRef,
|
|
29
|
+
type: "button",
|
|
30
|
+
"data-enigma-color-swatch": "",
|
|
31
|
+
"data-enigma-color-checkers": "",
|
|
32
|
+
"data-invalid": invalid ? "" : void 0,
|
|
33
|
+
"aria-haspopup": "dialog",
|
|
34
|
+
"aria-expanded": open,
|
|
35
|
+
"aria-label": label,
|
|
36
|
+
title: label,
|
|
37
|
+
disabled: locked,
|
|
38
|
+
style: baseline ? BASELINE : void 0,
|
|
39
|
+
onClick: onPress,
|
|
40
|
+
children: /* @__PURE__ */ jsx(
|
|
41
|
+
"span",
|
|
42
|
+
{
|
|
43
|
+
"data-enigma-color-fill": "",
|
|
44
|
+
style: {
|
|
45
|
+
background: invalid ? void 0 : value,
|
|
46
|
+
...baseline ? { display: "block", width: "100%", height: "100%" } : null
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
),
|
|
52
|
+
children
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/react/input/write-value.ts
|
|
59
|
+
function writeValue(input, next) {
|
|
60
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
61
|
+
if (setter) setter.call(input, next);
|
|
62
|
+
else input.value = next;
|
|
63
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { ColorSwatch, writeValue };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseColor, rgbToHsv, colorEquals, hsvToRgb, formatColor, toHex } from './chunk-S2FPN5ID.js';
|
|
2
|
-
import { writeValue } from './chunk-
|
|
2
|
+
import { writeValue, ColorSwatch } from './chunk-KVLSOQTI.js';
|
|
3
3
|
import { useLayoutEffect, useRef, useState, useMemo, useEffect, useCallback } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
5
|
|
|
6
6
|
// src/react/input/color-styles.ts
|
|
7
7
|
var COLOR_STYLES = `
|
|
@@ -133,6 +133,26 @@ var COLOR_STYLES = `
|
|
|
133
133
|
[data-enigma-color-eyedropper]:hover { color: var(--enigma-color-text); }
|
|
134
134
|
[data-enigma-color-eyedropper]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 2px; }
|
|
135
135
|
|
|
136
|
+
/* The value as text, with the notation beside it. Monospace on purpose: a hex whose digits
|
|
137
|
+
shift width as you drag is a value nobody can read at a glance. */
|
|
138
|
+
[data-enigma-color-value] { display: flex; align-items: center; gap: 0.375rem; }
|
|
139
|
+
[data-enigma-color-format] {
|
|
140
|
+
flex: none; padding: 0.3125rem 0.375rem;
|
|
141
|
+
font: inherit; font-size: 0.625rem; line-height: 1; letter-spacing: 0.06em;
|
|
142
|
+
color: var(--enigma-color-muted); background: none;
|
|
143
|
+
border: 1px solid var(--enigma-color-border); border-radius: 0.25rem; cursor: pointer;
|
|
144
|
+
}
|
|
145
|
+
[data-enigma-color-format]:hover { color: var(--enigma-color-text); }
|
|
146
|
+
[data-enigma-color-input] {
|
|
147
|
+
flex: 1; min-width: 0; padding: 0.25rem 0.375rem;
|
|
148
|
+
font: inherit; font-family: var(--enigma-color-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
149
|
+
font-size: 0.75rem; line-height: 1.4;
|
|
150
|
+
color: var(--enigma-color-text); background: none;
|
|
151
|
+
border: 1px solid var(--enigma-color-border); border-radius: 0.25rem;
|
|
152
|
+
}
|
|
153
|
+
[data-enigma-color-format]:focus-visible,
|
|
154
|
+
[data-enigma-color-input]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 1px; }
|
|
155
|
+
|
|
136
156
|
[data-enigma-color-swatches] { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
|
137
157
|
[data-enigma-color-preset] {
|
|
138
158
|
width: 1.125rem; height: 1.125rem; padding: 0;
|
|
@@ -144,6 +164,8 @@ var COLOR_STYLES = `
|
|
|
144
164
|
var INITIAL = { h: 0, s: 0, v: 0, a: 1 };
|
|
145
165
|
var STEP = { fine: 0.01, coarse: 0.1, hue: 1, hueCoarse: 10 };
|
|
146
166
|
var MARGIN = 12;
|
|
167
|
+
var FORMATS = ["hex", "rgb", "hsl"];
|
|
168
|
+
var FORMAT_LABELS = { hex: "HEX", rgb: "RGB", hsl: "HSL" };
|
|
147
169
|
var injected = false;
|
|
148
170
|
function injectStyles() {
|
|
149
171
|
if (injected || typeof document === "undefined") return;
|
|
@@ -167,6 +189,7 @@ function ColorExtras({
|
|
|
167
189
|
placement = "auto",
|
|
168
190
|
styles = true,
|
|
169
191
|
locked = false,
|
|
192
|
+
openOnMount = false,
|
|
170
193
|
labels
|
|
171
194
|
}) {
|
|
172
195
|
useLayoutEffect(() => {
|
|
@@ -176,7 +199,7 @@ function ColorExtras({
|
|
|
176
199
|
const panelRef = useRef(null);
|
|
177
200
|
const swatchRef = useRef(null);
|
|
178
201
|
const areaRef = useRef(null);
|
|
179
|
-
const [open, setOpen] = useState(
|
|
202
|
+
const [open, setOpen] = useState(openOnMount && !locked);
|
|
180
203
|
const [side, setSide] = useState(placement === "top" ? "top" : "bottom");
|
|
181
204
|
const parsed = useMemo(() => parseColor(value), [value]);
|
|
182
205
|
const [hsv, setHsv] = useState(() => parsed ? rgbToHsv(parsed) : INITIAL);
|
|
@@ -305,138 +328,178 @@ function ColorExtras({
|
|
|
305
328
|
} catch {
|
|
306
329
|
}
|
|
307
330
|
}, [commit, hsv]);
|
|
331
|
+
const [shown, setShown] = useState(format);
|
|
332
|
+
useEffect(() => setShown(format), [format]);
|
|
333
|
+
const [draft, setDraft] = useState(null);
|
|
334
|
+
const readout = useMemo(() => formatColor(rgb, shown, { alpha }), [rgb, shown, alpha]);
|
|
335
|
+
const nextFormat = FORMATS[(FORMATS.indexOf(shown) + 1) % FORMATS.length];
|
|
336
|
+
const typed = useCallback((next) => {
|
|
337
|
+
setDraft(next);
|
|
338
|
+
const colour = parseColor(next);
|
|
339
|
+
if (colour) commit(rgbToHsv(alpha ? colour : { ...colour, a: 1 }, hsv.h));
|
|
340
|
+
}, [commit, alpha, hsv.h]);
|
|
308
341
|
const css = useMemo(() => formatColor(rgb, "hex", { alpha: true }), [rgb]);
|
|
309
342
|
const opaque = useMemo(() => toHex({ ...rgb, a: 1 }), [rgb]);
|
|
310
343
|
const text = labels ?? {};
|
|
311
344
|
const valueText = `${text.color ?? "Colour"} ${css}`;
|
|
312
345
|
const panelStyle = { "--enigma-color-hue": Math.round(hsv.h), "--enigma-color-opaque": opaque };
|
|
313
|
-
return /* @__PURE__ */
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (event.key !== "Escape") return;
|
|
342
|
-
event.stopPropagation();
|
|
343
|
-
close(true);
|
|
344
|
-
},
|
|
345
|
-
children: [
|
|
346
|
-
/* @__PURE__ */ jsx(
|
|
347
|
-
"div",
|
|
348
|
-
{
|
|
349
|
-
ref: areaRef,
|
|
350
|
-
"data-enigma-color-area": "",
|
|
351
|
-
role: "slider",
|
|
352
|
-
tabIndex: 0,
|
|
353
|
-
"aria-label": text.area ?? "Saturation and brightness",
|
|
354
|
-
"aria-valuemin": 0,
|
|
355
|
-
"aria-valuemax": 100,
|
|
356
|
-
"aria-valuenow": Math.round(hsv.s * 100),
|
|
357
|
-
"aria-valuetext": `${Math.round(hsv.s * 100)}% saturation, ${Math.round(hsv.v * 100)}% brightness, ${valueText}`,
|
|
358
|
-
onKeyDown: areaKeys,
|
|
359
|
-
onPointerDown: (event) => track(event, (x, y) => commit({ ...hsv, s: x, v: 1 - y })),
|
|
360
|
-
children: /* @__PURE__ */ jsx("span", { "data-enigma-color-thumb": "", style: { left: `${hsv.s * 100}%`, top: `${(1 - hsv.v) * 100}%` } })
|
|
361
|
-
}
|
|
362
|
-
),
|
|
363
|
-
/* @__PURE__ */ jsxs("div", { "data-enigma-color-controls": "", children: [
|
|
364
|
-
/* @__PURE__ */ jsx("span", { "data-enigma-color-preview": "", "data-enigma-color-checkers": "", children: /* @__PURE__ */ jsx("span", { "data-enigma-color-fill": "", style: { background: css } }) }),
|
|
365
|
-
dropper && /* @__PURE__ */ jsx(
|
|
366
|
-
"button",
|
|
346
|
+
return /* @__PURE__ */ jsx(
|
|
347
|
+
ColorSwatch,
|
|
348
|
+
{
|
|
349
|
+
anchorRef,
|
|
350
|
+
buttonRef: swatchRef,
|
|
351
|
+
value: css,
|
|
352
|
+
invalid: !parsed,
|
|
353
|
+
open,
|
|
354
|
+
locked,
|
|
355
|
+
label: text.open ?? "Pick a colour",
|
|
356
|
+
onPress: () => open ? close(true) : setOpenState(true),
|
|
357
|
+
children: open && !locked && /* @__PURE__ */ jsxs(
|
|
358
|
+
"div",
|
|
359
|
+
{
|
|
360
|
+
ref: panelRef,
|
|
361
|
+
"data-enigma-color-panel": "",
|
|
362
|
+
"data-side": side,
|
|
363
|
+
role: "dialog",
|
|
364
|
+
"aria-label": text.panel ?? "Colour picker",
|
|
365
|
+
style: panelStyle,
|
|
366
|
+
onKeyDown: (event) => {
|
|
367
|
+
if (event.key !== "Escape") return;
|
|
368
|
+
event.stopPropagation();
|
|
369
|
+
close(true);
|
|
370
|
+
},
|
|
371
|
+
children: [
|
|
372
|
+
/* @__PURE__ */ jsx(
|
|
373
|
+
"div",
|
|
367
374
|
{
|
|
368
|
-
|
|
369
|
-
"data-enigma-color-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
375
|
+
ref: areaRef,
|
|
376
|
+
"data-enigma-color-area": "",
|
|
377
|
+
role: "slider",
|
|
378
|
+
tabIndex: 0,
|
|
379
|
+
"aria-label": text.area ?? "Saturation and brightness",
|
|
380
|
+
"aria-valuemin": 0,
|
|
381
|
+
"aria-valuemax": 100,
|
|
382
|
+
"aria-valuenow": Math.round(hsv.s * 100),
|
|
383
|
+
"aria-valuetext": `${Math.round(hsv.s * 100)}% saturation, ${Math.round(hsv.v * 100)}% brightness, ${valueText}`,
|
|
384
|
+
onKeyDown: areaKeys,
|
|
385
|
+
onPointerDown: (event) => track(event, (x, y) => commit({ ...hsv, s: x, v: 1 - y })),
|
|
386
|
+
children: /* @__PURE__ */ jsx("span", { "data-enigma-color-thumb": "", style: { left: `${hsv.s * 100}%`, top: `${(1 - hsv.v) * 100}%` } })
|
|
376
387
|
}
|
|
377
388
|
),
|
|
378
|
-
/* @__PURE__ */ jsxs("div", { "data-enigma-color-
|
|
389
|
+
/* @__PURE__ */ jsxs("div", { "data-enigma-color-controls": "", children: [
|
|
390
|
+
/* @__PURE__ */ jsx("span", { "data-enigma-color-preview": "", "data-enigma-color-checkers": "", children: /* @__PURE__ */ jsx("span", { "data-enigma-color-fill": "", style: { background: css } }) }),
|
|
391
|
+
dropper && /* @__PURE__ */ jsx(
|
|
392
|
+
"button",
|
|
393
|
+
{
|
|
394
|
+
type: "button",
|
|
395
|
+
"data-enigma-color-eyedropper": "",
|
|
396
|
+
"aria-label": text.eyedropper ?? "Pick a colour from the screen",
|
|
397
|
+
title: text.eyedropper ?? "Pick a colour from the screen",
|
|
398
|
+
onClick: () => {
|
|
399
|
+
void pick();
|
|
400
|
+
},
|
|
401
|
+
children: /* @__PURE__ */ jsx(EyedropperIcon, {})
|
|
402
|
+
}
|
|
403
|
+
),
|
|
404
|
+
/* @__PURE__ */ jsxs("div", { "data-enigma-color-rails": "", children: [
|
|
405
|
+
/* @__PURE__ */ jsx(
|
|
406
|
+
"div",
|
|
407
|
+
{
|
|
408
|
+
"data-enigma-color-rail": "hue",
|
|
409
|
+
role: "slider",
|
|
410
|
+
tabIndex: 0,
|
|
411
|
+
"aria-label": text.hue ?? "Hue",
|
|
412
|
+
"aria-valuemin": 0,
|
|
413
|
+
"aria-valuemax": 360,
|
|
414
|
+
"aria-valuenow": Math.round(hsv.h),
|
|
415
|
+
"aria-valuetext": `${Math.round(hsv.h)} degrees`,
|
|
416
|
+
onKeyDown: (event) => railKeys(event, "hue"),
|
|
417
|
+
onPointerDown: (event) => track(event, (x) => commit({ ...hsv, h: x * 360 })),
|
|
418
|
+
children: /* @__PURE__ */ jsx("span", { "data-enigma-color-thumb": "", style: { left: `${hsv.h / 360 * 100}%` } })
|
|
419
|
+
}
|
|
420
|
+
),
|
|
421
|
+
alpha && /* @__PURE__ */ jsxs(
|
|
422
|
+
"div",
|
|
423
|
+
{
|
|
424
|
+
"data-enigma-color-rail": "alpha",
|
|
425
|
+
"data-enigma-color-checkers": "",
|
|
426
|
+
role: "slider",
|
|
427
|
+
tabIndex: 0,
|
|
428
|
+
"aria-label": text.alpha ?? "Opacity",
|
|
429
|
+
"aria-valuemin": 0,
|
|
430
|
+
"aria-valuemax": 100,
|
|
431
|
+
"aria-valuenow": Math.round(hsv.a * 100),
|
|
432
|
+
"aria-valuetext": `${Math.round(hsv.a * 100)}% opacity`,
|
|
433
|
+
onKeyDown: (event) => railKeys(event, "alpha"),
|
|
434
|
+
onPointerDown: (event) => track(event, (x) => commit({ ...hsv, a: x })),
|
|
435
|
+
children: [
|
|
436
|
+
/* @__PURE__ */ jsx("span", { "data-enigma-color-gradient": "" }),
|
|
437
|
+
/* @__PURE__ */ jsx("span", { "data-enigma-color-thumb": "", style: { left: `${hsv.a * 100}%` } })
|
|
438
|
+
]
|
|
439
|
+
}
|
|
440
|
+
)
|
|
441
|
+
] })
|
|
442
|
+
] }),
|
|
443
|
+
/* @__PURE__ */ jsxs("div", { "data-enigma-color-value": "", children: [
|
|
379
444
|
/* @__PURE__ */ jsx(
|
|
380
|
-
"
|
|
445
|
+
"button",
|
|
381
446
|
{
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
onPointerDown: (event) => track(event, (x) => commit({ ...hsv, h: x * 360 })),
|
|
392
|
-
children: /* @__PURE__ */ jsx("span", { "data-enigma-color-thumb": "", style: { left: `${hsv.h / 360 * 100}%` } })
|
|
447
|
+
type: "button",
|
|
448
|
+
"data-enigma-color-format": "",
|
|
449
|
+
"aria-label": `${text.formatAs ?? "Show as"} ${FORMAT_LABELS[nextFormat]}`,
|
|
450
|
+
title: `${text.formatAs ?? "Show as"} ${FORMAT_LABELS[nextFormat]}`,
|
|
451
|
+
onClick: () => {
|
|
452
|
+
setShown(nextFormat);
|
|
453
|
+
setDraft(null);
|
|
454
|
+
},
|
|
455
|
+
children: FORMAT_LABELS[shown]
|
|
393
456
|
}
|
|
394
457
|
),
|
|
395
|
-
|
|
396
|
-
"
|
|
458
|
+
/* @__PURE__ */ jsx(
|
|
459
|
+
"input",
|
|
397
460
|
{
|
|
398
|
-
|
|
399
|
-
"data-enigma-color-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
"
|
|
404
|
-
"
|
|
405
|
-
"
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
461
|
+
type: "text",
|
|
462
|
+
"data-enigma-color-input": "",
|
|
463
|
+
value: draft ?? readout,
|
|
464
|
+
"aria-label": text.value ?? "Colour value",
|
|
465
|
+
spellCheck: false,
|
|
466
|
+
autoComplete: "off",
|
|
467
|
+
autoCorrect: "off",
|
|
468
|
+
autoCapitalize: "off",
|
|
469
|
+
onChange: (event) => typed(event.target.value),
|
|
470
|
+
onBlur: () => setDraft(null),
|
|
471
|
+
onKeyDown: (event) => {
|
|
472
|
+
if (event.key !== "Enter") return;
|
|
473
|
+
event.preventDefault();
|
|
474
|
+
setDraft(null);
|
|
475
|
+
}
|
|
413
476
|
}
|
|
414
477
|
)
|
|
415
|
-
] })
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
478
|
+
] }),
|
|
479
|
+
swatches && swatches.length > 0 && /* @__PURE__ */ jsx("div", { "data-enigma-color-swatches": "", role: "group", "aria-label": text.swatches ?? "Preset colours", children: swatches.map((preset) => {
|
|
480
|
+
const colour = parseColor(preset);
|
|
481
|
+
return /* @__PURE__ */ jsx(
|
|
482
|
+
"button",
|
|
483
|
+
{
|
|
484
|
+
type: "button",
|
|
485
|
+
"data-enigma-color-preset": "",
|
|
486
|
+
"data-enigma-color-checkers": "",
|
|
487
|
+
style: { background: preset },
|
|
488
|
+
"aria-label": preset,
|
|
489
|
+
title: preset,
|
|
490
|
+
"aria-pressed": colourEqualsPreset(colour, css),
|
|
491
|
+
onClick: () => {
|
|
492
|
+
if (colour) commit(rgbToHsv(alpha ? colour : { ...colour, a: 1 }, hsv.h));
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
preset
|
|
496
|
+
);
|
|
497
|
+
}) })
|
|
498
|
+
]
|
|
499
|
+
}
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
);
|
|
440
503
|
}
|
|
441
504
|
function colourEqualsPreset(preset, current) {
|
|
442
505
|
return colorEquals(preset, parseColor(current));
|
|
@@ -145,6 +145,10 @@ interface ColorLabels {
|
|
|
145
145
|
alpha?: string;
|
|
146
146
|
eyedropper?: string;
|
|
147
147
|
swatches?: string;
|
|
148
|
+
/** The editable readout under the rails. Default "Colour value". */
|
|
149
|
+
value?: string;
|
|
150
|
+
/** The button that cycles the readout between HEX, RGB and HSL. Default "Show as". */
|
|
151
|
+
formatAs?: string;
|
|
148
152
|
/** The word before the colour in what a screen reader announces. Default "Colour". */
|
|
149
153
|
color?: string;
|
|
150
154
|
}
|
package/dist/next/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { UseSearchResult, useSearch } from '../react/search.js';
|
|
|
7
7
|
export { I as InputAction, b as InputIcon, d as InputOptions } from '../input-BwXjFenq.js';
|
|
8
8
|
export { F as FuseConstructor, a as FuseLike, S as SearchInstance, b as SearchMatch, c as SearchOptions, s as shortenQuery } from '../search-DYgqRp37.js';
|
|
9
9
|
export { c as ButtonOptions, d as ButtonState } from '../button-CaXaqG_K.js';
|
|
10
|
-
export { B as BreachChecker, a as BreachState, b as BreachStatus, C as ColorLabels, c as ColorOnlyProps, d as ColorPanelPlacement, F as FieldAction, I as Input, e as InputBaseProps, f as InputProps, g as InputType, P as PasswordOnlyProps, i as PlainOnlyProps, S as SearchOnlyProps } from '../index-
|
|
10
|
+
export { B as BreachChecker, a as BreachState, b as BreachStatus, C as ColorLabels, c as ColorOnlyProps, d as ColorPanelPlacement, F as FieldAction, I as Input, e as InputBaseProps, f as InputProps, g as InputType, P as PasswordOnlyProps, i as PlainOnlyProps, S as SearchOnlyProps } from '../index-qXOkYQCU.js';
|
|
11
11
|
export { C as ColorFormat, E as EstimateOptions, F as FormatColorOptions, G as GeneratePasswordOptions, H as Hsl, a as Hsv, b as PasswordScore, c as PasswordStrengthReport, R as Rgb, d as colorEquals, e as estimatePasswordStrength, f as formatColor, g as generatePassword, h as hslToRgb, i as hsvToRgb, p as parseColor, r as rgbToHsl, j as rgbToHsv, t as toHex } from '../color-D_rZ83Oc.js';
|
|
12
12
|
export { Slot, SlotProps, mergeSlotProps } from '../react/slot.js';
|
|
13
13
|
export { UseNotificationsResult, createNotificationQueue, defaultQueue, useNotifications } from '../react/notifications.js';
|
package/dist/next/index.js
CHANGED
|
@@ -23,8 +23,8 @@ export { configureFlags, flagAttributes, flagName, flagSrc, flagView, normalizeF
|
|
|
23
23
|
import '../chunk-SNYUBXWQ.js';
|
|
24
24
|
import '../chunk-UOSSNUSC.js';
|
|
25
25
|
import '../chunk-2QFTRNAZ.js';
|
|
26
|
-
export { Input } from '../chunk-
|
|
27
|
-
import '../chunk-
|
|
26
|
+
export { Input } from '../chunk-3MGBZOAU.js';
|
|
27
|
+
import '../chunk-KVLSOQTI.js';
|
|
28
28
|
import '../chunk-F25CGNQC.js';
|
|
29
29
|
export { PaletteContent, PaletteField, PaletteFooter, PaletteItem, PaletteList, PaletteRoot, PaletteTrigger, SearchPalette, usePaletteContext } from '../chunk-FGBIZDV2.js';
|
|
30
30
|
export { Select, SelectContent, SelectList, SelectOptionRow, SelectRoot, SelectSearch, SelectTrigger, SelectValue, useSelectContext } from '../chunk-VKL3DEIQ.js';
|
package/dist/react/index.d.ts
CHANGED
|
@@ -7,8 +7,8 @@ export { UseSearchResult, useSearch } from './search.js';
|
|
|
7
7
|
export { F as FuseConstructor, a as FuseLike, S as SearchInstance, b as SearchMatch, c as SearchOptions, s as shortenQuery } from '../search-DYgqRp37.js';
|
|
8
8
|
import { d as ButtonState, c as ButtonOptions } from '../button-CaXaqG_K.js';
|
|
9
9
|
export { Button, ButtonProps } from './button.js';
|
|
10
|
-
import { h as PasswordStrengthClassNames } from '../index-
|
|
11
|
-
export { B as BreachChecker, a as BreachState, b as BreachStatus, C as ColorLabels, c as ColorOnlyProps, d as ColorPanelPlacement, F as FieldAction, I as Input, e as InputBaseProps, f as InputProps, g as InputType, P as PasswordOnlyProps, i as PlainOnlyProps, S as SearchOnlyProps } from '../index-
|
|
10
|
+
import { h as PasswordStrengthClassNames } from '../index-qXOkYQCU.js';
|
|
11
|
+
export { B as BreachChecker, a as BreachState, b as BreachStatus, C as ColorLabels, c as ColorOnlyProps, d as ColorPanelPlacement, F as FieldAction, I as Input, e as InputBaseProps, f as InputProps, g as InputType, P as PasswordOnlyProps, i as PlainOnlyProps, S as SearchOnlyProps } from '../index-qXOkYQCU.js';
|
|
12
12
|
import { c as PasswordStrengthReport } from '../color-D_rZ83Oc.js';
|
|
13
13
|
export { C as ColorFormat, E as EstimateOptions, F as FormatColorOptions, G as GeneratePasswordOptions, H as Hsl, a as Hsv, b as PasswordScore, R as Rgb, d as colorEquals, e as estimatePasswordStrength, f as formatColor, g as generatePassword, h as hslToRgb, i as hsvToRgb, p as parseColor, r as rgbToHsl, j as rgbToHsv, t as toHex } from '../color-D_rZ83Oc.js';
|
|
14
14
|
export { Slot, SlotProps, mergeSlotProps } from './slot.js';
|
package/dist/react/index.js
CHANGED
|
@@ -22,8 +22,8 @@ export { configureFlags, flagAttributes, flagName, flagSrc, flagView, normalizeF
|
|
|
22
22
|
import '../chunk-SNYUBXWQ.js';
|
|
23
23
|
import '../chunk-UOSSNUSC.js';
|
|
24
24
|
import '../chunk-2QFTRNAZ.js';
|
|
25
|
-
export { Input } from '../chunk-
|
|
26
|
-
import '../chunk-
|
|
25
|
+
export { Input } from '../chunk-3MGBZOAU.js';
|
|
26
|
+
import '../chunk-KVLSOQTI.js';
|
|
27
27
|
import '../chunk-F25CGNQC.js';
|
|
28
28
|
export { PaletteContent, PaletteField, PaletteFooter, PaletteItem, PaletteList, PaletteRoot, PaletteTrigger, SearchPalette, usePaletteContext } from '../chunk-FGBIZDV2.js';
|
|
29
29
|
export { Select, SelectContent, SelectList, SelectOptionRow, SelectRoot, SelectSearch, SelectTrigger, SelectValue, useSelectContext } from '../chunk-VKL3DEIQ.js';
|
package/dist/react/input.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { B as BreachChecker, a as BreachState, b as BreachStatus, C as ColorLabels, c as ColorOnlyProps, d as ColorPanelPlacement, F as FieldAction, I as Input, e as InputBaseProps, f as InputProps, g as InputType, P as PasswordOnlyProps, i as PlainOnlyProps, S as SearchOnlyProps } from '../index-
|
|
1
|
+
export { B as BreachChecker, a as BreachState, b as BreachStatus, C as ColorLabels, c as ColorOnlyProps, d as ColorPanelPlacement, F as FieldAction, I as Input, e as InputBaseProps, f as InputProps, g as InputType, P as PasswordOnlyProps, i as PlainOnlyProps, S as SearchOnlyProps } from '../index-qXOkYQCU.js';
|
|
2
2
|
import 'react';
|
|
3
3
|
import '../color-D_rZ83Oc.js';
|
|
4
4
|
import '../search-DYgqRp37.js';
|
package/dist/react/input.js
CHANGED
|
@@ -7,7 +7,7 @@ export { UseSearchResult, useSearch } from '../react/search.js';
|
|
|
7
7
|
export { I as InputAction, b as InputIcon, d as InputOptions } from '../input-BwXjFenq.js';
|
|
8
8
|
export { F as FuseConstructor, a as FuseLike, S as SearchInstance, b as SearchMatch, c as SearchOptions, s as shortenQuery } from '../search-DYgqRp37.js';
|
|
9
9
|
export { c as ButtonOptions, d as ButtonState } from '../button-CaXaqG_K.js';
|
|
10
|
-
export { B as BreachChecker, a as BreachState, b as BreachStatus, C as ColorLabels, c as ColorOnlyProps, d as ColorPanelPlacement, F as FieldAction, I as Input, e as InputBaseProps, f as InputProps, g as InputType, P as PasswordOnlyProps, i as PlainOnlyProps, S as SearchOnlyProps } from '../index-
|
|
10
|
+
export { B as BreachChecker, a as BreachState, b as BreachStatus, C as ColorLabels, c as ColorOnlyProps, d as ColorPanelPlacement, F as FieldAction, I as Input, e as InputBaseProps, f as InputProps, g as InputType, P as PasswordOnlyProps, i as PlainOnlyProps, S as SearchOnlyProps } from '../index-qXOkYQCU.js';
|
|
11
11
|
export { C as ColorFormat, E as EstimateOptions, F as FormatColorOptions, G as GeneratePasswordOptions, H as Hsl, a as Hsv, b as PasswordScore, c as PasswordStrengthReport, R as Rgb, d as colorEquals, e as estimatePasswordStrength, f as formatColor, g as generatePassword, h as hslToRgb, i as hsvToRgb, p as parseColor, r as rgbToHsl, j as rgbToHsv, t as toHex } from '../color-D_rZ83Oc.js';
|
|
12
12
|
export { Slot, SlotProps, mergeSlotProps } from '../react/slot.js';
|
|
13
13
|
export { UseNotificationsResult, createNotificationQueue, defaultQueue, useNotifications } from '../react/notifications.js';
|
|
@@ -23,8 +23,8 @@ export { configureFlags, flagAttributes, flagName, flagSrc, flagView, normalizeF
|
|
|
23
23
|
import '../chunk-SNYUBXWQ.js';
|
|
24
24
|
import '../chunk-UOSSNUSC.js';
|
|
25
25
|
import '../chunk-2QFTRNAZ.js';
|
|
26
|
-
export { Input } from '../chunk-
|
|
27
|
-
import '../chunk-
|
|
26
|
+
export { Input } from '../chunk-3MGBZOAU.js';
|
|
27
|
+
import '../chunk-KVLSOQTI.js';
|
|
28
28
|
import '../chunk-F25CGNQC.js';
|
|
29
29
|
export { PaletteContent, PaletteField, PaletteFooter, PaletteItem, PaletteList, PaletteRoot, PaletteTrigger, SearchPalette, usePaletteContext } from '../chunk-FGBIZDV2.js';
|
|
30
30
|
export { Select, SelectContent, SelectList, SelectOptionRow, SelectRoot, SelectSearch, SelectTrigger, SelectValue, useSelectContext } from '../chunk-VKL3DEIQ.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Headless components: the behaviour, the timing and the accessibility, with no styles of their own. Framework-agnostic core plus thin adapters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
package/recipes/color/styles.css
CHANGED
|
@@ -134,6 +134,26 @@
|
|
|
134
134
|
[data-enigma-color-eyedropper]:hover { color: var(--enigma-color-text); }
|
|
135
135
|
[data-enigma-color-eyedropper]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 2px; }
|
|
136
136
|
|
|
137
|
+
/* The value as text, with the notation beside it. Monospace on purpose: a hex whose digits
|
|
138
|
+
shift width as you drag is a value nobody can read at a glance. */
|
|
139
|
+
[data-enigma-color-value] { display: flex; align-items: center; gap: 0.375rem; }
|
|
140
|
+
[data-enigma-color-format] {
|
|
141
|
+
flex: none; padding: 0.3125rem 0.375rem;
|
|
142
|
+
font: inherit; font-size: 0.625rem; line-height: 1; letter-spacing: 0.06em;
|
|
143
|
+
color: var(--enigma-color-muted); background: none;
|
|
144
|
+
border: 1px solid var(--enigma-color-border); border-radius: 0.25rem; cursor: pointer;
|
|
145
|
+
}
|
|
146
|
+
[data-enigma-color-format]:hover { color: var(--enigma-color-text); }
|
|
147
|
+
[data-enigma-color-input] {
|
|
148
|
+
flex: 1; min-width: 0; padding: 0.25rem 0.375rem;
|
|
149
|
+
font: inherit; font-family: var(--enigma-color-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
150
|
+
font-size: 0.75rem; line-height: 1.4;
|
|
151
|
+
color: var(--enigma-color-text); background: none;
|
|
152
|
+
border: 1px solid var(--enigma-color-border); border-radius: 0.25rem;
|
|
153
|
+
}
|
|
154
|
+
[data-enigma-color-format]:focus-visible,
|
|
155
|
+
[data-enigma-color-input]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 1px; }
|
|
156
|
+
|
|
137
157
|
[data-enigma-color-swatches] { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
|
138
158
|
[data-enigma-color-preset] {
|
|
139
159
|
width: 1.125rem; height: 1.125rem; padding: 0;
|
package/registry.json
CHANGED
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
{
|
|
157
157
|
"name": "input",
|
|
158
158
|
"title": "Input, one field for every type",
|
|
159
|
-
"description": "A field you pass props to, typed by its `type`: password gets the reveal toggle, a generator, a strength meter and a breach check; search gets debouncing, ranking and a clear button; color gets a swatch and a picker panel - saturation square, hue and alpha rails, presets and the screen eyedropper - instead of the operating system popup; everything else is a plain field with native props. What each type needs is loaded when that type is used, so a form of text fields ships none of it.",
|
|
159
|
+
"description": "A field you pass props to, typed by its `type`: password gets the reveal toggle, a generator, a strength meter and a breach check; search gets debouncing, ranking and a clear button; color gets a swatch and a picker panel - saturation square, hue and alpha rails, an editable readout that prints the value as hex, rgb() or hsl(), presets and the screen eyedropper - instead of the operating system popup, and the swatch is there from the first frame rather than when the panel's code lands; everything else is a plain field with native props. What each type needs is loaded when that type is used, so a form of text fields ships none of it.",
|
|
160
160
|
"targets": [
|
|
161
161
|
"vanilla",
|
|
162
162
|
"astro",
|
|
@@ -279,6 +279,13 @@
|
|
|
279
279
|
"react"
|
|
280
280
|
]
|
|
281
281
|
},
|
|
282
|
+
{
|
|
283
|
+
"path": "src/react/input/color-swatch.tsx",
|
|
284
|
+
"dest": "input/color-swatch.tsx",
|
|
285
|
+
"targets": [
|
|
286
|
+
"react"
|
|
287
|
+
]
|
|
288
|
+
},
|
|
282
289
|
{
|
|
283
290
|
"path": "src/react/input/color.tsx",
|
|
284
291
|
"dest": "input/color.tsx",
|
|
@@ -289,7 +296,8 @@
|
|
|
289
296
|
"@/core/color": "./color-core",
|
|
290
297
|
"@/react/input/write-value": "./write-value",
|
|
291
298
|
"@/react/input/color-styles": "./color-styles",
|
|
292
|
-
"@/react/input/types": "./types"
|
|
299
|
+
"@/react/input/types": "./types",
|
|
300
|
+
"@/react/input/color-swatch": "./color-swatch"
|
|
293
301
|
}
|
|
294
302
|
},
|
|
295
303
|
{
|
|
@@ -325,7 +333,8 @@
|
|
|
325
333
|
"@/react/input/password": "./password",
|
|
326
334
|
"@/react/input/search": "./search",
|
|
327
335
|
"@/core/password": "./password-core",
|
|
328
|
-
"@/react/input/color": "./color"
|
|
336
|
+
"@/react/input/color": "./color",
|
|
337
|
+
"@/react/input/color-swatch": "./color-swatch"
|
|
329
338
|
}
|
|
330
339
|
},
|
|
331
340
|
{
|
|
@@ -146,6 +146,26 @@ export const COLOR_STYLES = `
|
|
|
146
146
|
[data-enigma-color-eyedropper]:hover { color: var(--enigma-color-text); }
|
|
147
147
|
[data-enigma-color-eyedropper]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 2px; }
|
|
148
148
|
|
|
149
|
+
/* The value as text, with the notation beside it. Monospace on purpose: a hex whose digits
|
|
150
|
+
shift width as you drag is a value nobody can read at a glance. */
|
|
151
|
+
[data-enigma-color-value] { display: flex; align-items: center; gap: 0.375rem; }
|
|
152
|
+
[data-enigma-color-format] {
|
|
153
|
+
flex: none; padding: 0.3125rem 0.375rem;
|
|
154
|
+
font: inherit; font-size: 0.625rem; line-height: 1; letter-spacing: 0.06em;
|
|
155
|
+
color: var(--enigma-color-muted); background: none;
|
|
156
|
+
border: 1px solid var(--enigma-color-border); border-radius: 0.25rem; cursor: pointer;
|
|
157
|
+
}
|
|
158
|
+
[data-enigma-color-format]:hover { color: var(--enigma-color-text); }
|
|
159
|
+
[data-enigma-color-input] {
|
|
160
|
+
flex: 1; min-width: 0; padding: 0.25rem 0.375rem;
|
|
161
|
+
font: inherit; font-family: var(--enigma-color-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
162
|
+
font-size: 0.75rem; line-height: 1.4;
|
|
163
|
+
color: var(--enigma-color-text); background: none;
|
|
164
|
+
border: 1px solid var(--enigma-color-border); border-radius: 0.25rem;
|
|
165
|
+
}
|
|
166
|
+
[data-enigma-color-format]:focus-visible,
|
|
167
|
+
[data-enigma-color-input]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 1px; }
|
|
168
|
+
|
|
149
169
|
[data-enigma-color-swatches] { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
|
150
170
|
[data-enigma-color-preset] {
|
|
151
171
|
width: 1.125rem; height: 1.125rem; padding: 0;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties, ReactNode, Ref } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The swatch in the field, and the box the panel hangs from.
|
|
7
|
+
*
|
|
8
|
+
* Its own module, and deliberately NOT part of the picker's chunk. The picker is imported on
|
|
9
|
+
* demand, so a swatch that lived in there would not exist for as long as that request takes -
|
|
10
|
+
* the field renders as a bare text box, the affordance appears late and shifts the row, and a
|
|
11
|
+
* press in that window reaches nothing at all. On a fast connection that is a blink; on a
|
|
12
|
+
* cold cache it is the second or two somebody spends clicking a swatch that is not there yet.
|
|
13
|
+
*
|
|
14
|
+
* So the swatch ships with the field and the panel arrives behind it. It costs the markup
|
|
15
|
+
* below in the base chunk and no colour maths at all: the fill is handed to the browser, which
|
|
16
|
+
* is the only parser that already knows every notation a value could be written in.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Enough of a look to be a swatch, for the frames before the panel's stylesheet exists.
|
|
21
|
+
*
|
|
22
|
+
* The sheet ships with the picker, so the swatch rendered ahead of it would otherwise be a
|
|
23
|
+
* default browser button for as long as that request takes - which trades a missing control
|
|
24
|
+
* for an ugly one. Written against the same custom properties, so a project that themed the
|
|
25
|
+
* picker themes this too, and dropped entirely the moment the real swatch replaces it.
|
|
26
|
+
*/
|
|
27
|
+
const BASELINE: CSSProperties = {
|
|
28
|
+
display: "block",
|
|
29
|
+
boxSizing: "border-box",
|
|
30
|
+
width: "1.25rem",
|
|
31
|
+
height: "1.25rem",
|
|
32
|
+
padding: 0,
|
|
33
|
+
background: "none",
|
|
34
|
+
border: "1px solid var(--enigma-color-border, #404040)",
|
|
35
|
+
borderRadius: "0.3125rem",
|
|
36
|
+
overflow: "hidden",
|
|
37
|
+
cursor: "pointer"
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface ColorSwatchProps {
|
|
41
|
+
/** The value to paint, exactly as the field holds it. Anything unparseable paints nothing. */
|
|
42
|
+
value?: string;
|
|
43
|
+
/** Whether the panel is up, for `aria-expanded`. */
|
|
44
|
+
open?: boolean;
|
|
45
|
+
/** The value is not a colour: the swatch shows the chequerboard rather than a stale fill. */
|
|
46
|
+
invalid?: boolean;
|
|
47
|
+
/** The field is disabled or read-only. */
|
|
48
|
+
locked?: boolean;
|
|
49
|
+
label: string;
|
|
50
|
+
anchorRef?: Ref<HTMLSpanElement>;
|
|
51
|
+
buttonRef?: Ref<HTMLButtonElement>;
|
|
52
|
+
onPress?: () => void;
|
|
53
|
+
/** The picker's stylesheet has not arrived yet, so carry the few declarations it needs. */
|
|
54
|
+
baseline?: boolean;
|
|
55
|
+
/** The panel, once its code is here. */
|
|
56
|
+
children?: ReactNode;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function ColorSwatch({ value, open = false, invalid = false, locked = false, label, anchorRef, buttonRef, onPress, baseline = false, children }: ColorSwatchProps): ReactNode {
|
|
60
|
+
return (
|
|
61
|
+
<span
|
|
62
|
+
ref={anchorRef}
|
|
63
|
+
data-enigma-color=""
|
|
64
|
+
data-open={open ? "" : undefined}
|
|
65
|
+
style={baseline ? { position: "relative", display: "inline-flex", flex: "none" } : undefined}
|
|
66
|
+
>
|
|
67
|
+
<button
|
|
68
|
+
ref={buttonRef}
|
|
69
|
+
// Never a bare `<button>`: inside a form it would default to submit, so opening
|
|
70
|
+
// the picker would post the half-filled form. The same trap the reveal has.
|
|
71
|
+
type="button"
|
|
72
|
+
data-enigma-color-swatch=""
|
|
73
|
+
data-enigma-color-checkers=""
|
|
74
|
+
data-invalid={invalid ? "" : undefined}
|
|
75
|
+
aria-haspopup="dialog"
|
|
76
|
+
aria-expanded={open}
|
|
77
|
+
aria-label={label}
|
|
78
|
+
title={label}
|
|
79
|
+
disabled={locked}
|
|
80
|
+
style={baseline ? BASELINE : undefined}
|
|
81
|
+
onClick={onPress}
|
|
82
|
+
>
|
|
83
|
+
{/* The browser parses the value for us: an unparseable string simply applies
|
|
84
|
+
nothing, which is why the swatch needs no colour maths to fall back on. */}
|
|
85
|
+
<span
|
|
86
|
+
data-enigma-color-fill=""
|
|
87
|
+
style={{
|
|
88
|
+
background: invalid ? undefined : value,
|
|
89
|
+
...(baseline ? { display: "block", width: "100%", height: "100%" } : null)
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
</button>
|
|
93
|
+
{children}
|
|
94
|
+
</span>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { writeValue } from "@/react/input/write-value";
|
|
4
|
+
import { ColorSwatch } from "@/react/input/color-swatch";
|
|
4
5
|
import { COLOR_STYLES } from "@/react/input/color-styles";
|
|
5
6
|
import type { ColorLabels, ColorPanelPlacement } from "@/react/input/types";
|
|
6
7
|
import { parseColor, formatColor, rgbToHsv, hsvToRgb, toHex, colorEquals, type Hsv, type ColorFormat } from "@/core/color";
|
|
@@ -47,6 +48,10 @@ const STEP = { fine: 0.01, coarse: 0.1, hue: 1, hueCoarse: 10 };
|
|
|
47
48
|
/** Kept clear of the window edge when deciding which side to open on. */
|
|
48
49
|
const MARGIN = 12;
|
|
49
50
|
|
|
51
|
+
/** The notations the readout cycles through, in the order the button steps over them. */
|
|
52
|
+
const FORMATS: ColorFormat[] = ["hex", "rgb", "hsl"];
|
|
53
|
+
const FORMAT_LABELS: Record<ColorFormat, string> = { hex: "HEX", rgb: "RGB", hsl: "HSL" };
|
|
54
|
+
|
|
50
55
|
let injected = false;
|
|
51
56
|
|
|
52
57
|
function injectStyles(): void {
|
|
@@ -76,6 +81,11 @@ export interface ColorExtrasProps {
|
|
|
76
81
|
styles?: boolean;
|
|
77
82
|
/** The field is disabled or read-only: the swatch is inert and the panel never opens. */
|
|
78
83
|
locked?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* The swatch was already pressed, while this module was still being fetched. Opens the
|
|
86
|
+
* panel on the first render rather than asking for the press a second time.
|
|
87
|
+
*/
|
|
88
|
+
openOnMount?: boolean;
|
|
79
89
|
labels?: ColorLabels;
|
|
80
90
|
}
|
|
81
91
|
|
|
@@ -89,6 +99,7 @@ export function ColorExtras({
|
|
|
89
99
|
placement = "auto",
|
|
90
100
|
styles = true,
|
|
91
101
|
locked = false,
|
|
102
|
+
openOnMount = false,
|
|
92
103
|
labels
|
|
93
104
|
}: ColorExtrasProps): ReactNode {
|
|
94
105
|
// Before paint: a sheet applied after the first frame shows the panel unstyled first.
|
|
@@ -99,7 +110,7 @@ export function ColorExtras({
|
|
|
99
110
|
const swatchRef = useRef<HTMLButtonElement | null>(null);
|
|
100
111
|
const areaRef = useRef<HTMLDivElement | null>(null);
|
|
101
112
|
|
|
102
|
-
const [open, setOpen] = useState(
|
|
113
|
+
const [open, setOpen] = useState(openOnMount && !locked);
|
|
103
114
|
const [side, setSide] = useState<"top" | "bottom">(placement === "top" ? "top" : "bottom");
|
|
104
115
|
|
|
105
116
|
const parsed = useMemo(() => parseColor(value), [value]);
|
|
@@ -296,6 +307,33 @@ export function ColorExtras({
|
|
|
296
307
|
}
|
|
297
308
|
}, [commit, hsv]);
|
|
298
309
|
|
|
310
|
+
/* -------- the readout, which is the value in whichever notation you want to read it -------- */
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Which notation the panel PRINTS the colour in, and only that.
|
|
314
|
+
*
|
|
315
|
+
* `format` is the caller's contract - the spelling their column, their API or their CSS
|
|
316
|
+
* expects - so cycling this button never changes what is written into the field. It
|
|
317
|
+
* changes what you can read out of the panel and paste elsewhere, which is what the
|
|
318
|
+
* browser's own picker offers and what a picker showing no code at all cannot.
|
|
319
|
+
*/
|
|
320
|
+
const [shown, setShown] = useState<ColorFormat>(format);
|
|
321
|
+
useEffect(() => setShown(format), [format]);
|
|
322
|
+
|
|
323
|
+
/** What is being typed, while it is being typed. Null means "show the current colour". */
|
|
324
|
+
const [draft, setDraft] = useState<string | null>(null);
|
|
325
|
+
|
|
326
|
+
const readout = useMemo(() => formatColor(rgb, shown, { alpha }), [rgb, shown, alpha]);
|
|
327
|
+
const nextFormat = FORMATS[(FORMATS.indexOf(shown) + 1) % FORMATS.length] as ColorFormat;
|
|
328
|
+
|
|
329
|
+
const typed = useCallback((next: string) => {
|
|
330
|
+
setDraft(next);
|
|
331
|
+
const colour = parseColor(next);
|
|
332
|
+
// Applied as it becomes a colour, so the square and the rails follow a pasted value
|
|
333
|
+
// rather than waiting for a blur. Half-typed text simply is not one yet.
|
|
334
|
+
if (colour) commit(rgbToHsv(alpha ? colour : { ...colour, a: 1 }, hsv.h));
|
|
335
|
+
}, [commit, alpha, hsv.h]);
|
|
336
|
+
|
|
299
337
|
/* -------- rendering -------- */
|
|
300
338
|
|
|
301
339
|
const css = useMemo(() => formatColor(rgb, "hex", { alpha: true }), [rgb]);
|
|
@@ -306,27 +344,16 @@ export function ColorExtras({
|
|
|
306
344
|
const panelStyle = { "--enigma-color-hue": Math.round(hsv.h), "--enigma-color-opaque": opaque } as CSSProperties;
|
|
307
345
|
|
|
308
346
|
return (
|
|
309
|
-
<
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
aria-expanded={open}
|
|
320
|
-
aria-label={text.open ?? "Pick a colour"}
|
|
321
|
-
title={text.open ?? "Pick a colour"}
|
|
322
|
-
disabled={locked}
|
|
323
|
-
onClick={() => (open ? close(true) : setOpenState(true))}
|
|
324
|
-
>
|
|
325
|
-
{/* The browser parses the value for us: an unparseable string simply applies
|
|
326
|
-
nothing, which is why the swatch needs no colour maths to fall back on. */}
|
|
327
|
-
<span data-enigma-color-fill="" style={{ background: parsed ? css : undefined }} />
|
|
328
|
-
</button>
|
|
329
|
-
|
|
347
|
+
<ColorSwatch
|
|
348
|
+
anchorRef={anchorRef}
|
|
349
|
+
buttonRef={swatchRef}
|
|
350
|
+
value={css}
|
|
351
|
+
invalid={!parsed}
|
|
352
|
+
open={open}
|
|
353
|
+
locked={locked}
|
|
354
|
+
label={text.open ?? "Pick a colour"}
|
|
355
|
+
onPress={() => (open ? close(true) : setOpenState(true))}
|
|
356
|
+
>
|
|
330
357
|
{open && !locked && (
|
|
331
358
|
<div
|
|
332
359
|
ref={panelRef}
|
|
@@ -421,6 +448,40 @@ export function ColorExtras({
|
|
|
421
448
|
</div>
|
|
422
449
|
</div>
|
|
423
450
|
|
|
451
|
+
{/* The colour as text, editable, with the notation next to it. A picker
|
|
452
|
+
that shows no code is one you cannot read a value out of, cannot paste
|
|
453
|
+
one into, and cannot check against the hex somebody sent you. */}
|
|
454
|
+
<div data-enigma-color-value="">
|
|
455
|
+
<button
|
|
456
|
+
type="button"
|
|
457
|
+
data-enigma-color-format=""
|
|
458
|
+
aria-label={`${text.formatAs ?? "Show as"} ${FORMAT_LABELS[nextFormat]}`}
|
|
459
|
+
title={`${text.formatAs ?? "Show as"} ${FORMAT_LABELS[nextFormat]}`}
|
|
460
|
+
onClick={() => { setShown(nextFormat); setDraft(null); }}
|
|
461
|
+
>
|
|
462
|
+
{FORMAT_LABELS[shown]}
|
|
463
|
+
</button>
|
|
464
|
+
<input
|
|
465
|
+
type="text"
|
|
466
|
+
data-enigma-color-input=""
|
|
467
|
+
value={draft ?? readout}
|
|
468
|
+
aria-label={text.value ?? "Colour value"}
|
|
469
|
+
spellCheck={false}
|
|
470
|
+
autoComplete="off"
|
|
471
|
+
autoCorrect="off"
|
|
472
|
+
autoCapitalize="off"
|
|
473
|
+
onChange={(event) => typed(event.target.value)}
|
|
474
|
+
// What was typed goes back to the canonical spelling the moment the
|
|
475
|
+
// field is left, the way the input itself normalizes on blur.
|
|
476
|
+
onBlur={() => setDraft(null)}
|
|
477
|
+
onKeyDown={(event) => {
|
|
478
|
+
if (event.key !== "Enter") return;
|
|
479
|
+
event.preventDefault();
|
|
480
|
+
setDraft(null);
|
|
481
|
+
}}
|
|
482
|
+
/>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
424
485
|
{swatches && swatches.length > 0 && (
|
|
425
486
|
<div data-enigma-color-swatches="" role="group" aria-label={text.swatches ?? "Preset colours"}>
|
|
426
487
|
{swatches.map((preset) => {
|
|
@@ -445,7 +506,7 @@ export function ColorExtras({
|
|
|
445
506
|
)}
|
|
446
507
|
</div>
|
|
447
508
|
)}
|
|
448
|
-
</
|
|
509
|
+
</ColorSwatch>
|
|
449
510
|
);
|
|
450
511
|
}
|
|
451
512
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Icon } from "@/react/input/icon";
|
|
4
|
+
import { ColorSwatch } from "@/react/input/color-swatch";
|
|
4
5
|
import { INPUT_ICON_PATHS } from "@/core/input-icons";
|
|
5
6
|
import { writeValue } from "@/react/input/write-value";
|
|
6
7
|
import type { AnyInputProps, InputProps, FieldAction, BreachState } from "@/react/input/types";
|
|
@@ -96,6 +97,14 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(pro
|
|
|
96
97
|
const innerRef = useRef<HTMLInputElement | null>(null);
|
|
97
98
|
const [element, setElement] = useState<HTMLInputElement | null>(null);
|
|
98
99
|
const [revealed, setRevealed] = useState(false);
|
|
100
|
+
/**
|
|
101
|
+
* The swatch was pressed before the picker's code arrived.
|
|
102
|
+
*
|
|
103
|
+
* Remembered rather than dropped: the press was a request to open the panel, and a
|
|
104
|
+
* control that ignores one because a chunk was in flight is a control that feels broken
|
|
105
|
+
* for exactly as long as the network is slow.
|
|
106
|
+
*/
|
|
107
|
+
const [colorWanted, setColorWanted] = useState(false);
|
|
99
108
|
const describedBy = useId();
|
|
100
109
|
|
|
101
110
|
const isPassword = type === "password";
|
|
@@ -263,12 +272,26 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(pro
|
|
|
263
272
|
data-score={score ?? undefined}
|
|
264
273
|
>
|
|
265
274
|
<div {...fieldProps} data-enigma-input-field="">
|
|
266
|
-
{/*
|
|
267
|
-
|
|
268
|
-
|
|
275
|
+
{/* The swatch is NOT part of the picker's chunk, unlike the meter, which has
|
|
276
|
+
nothing to show until it has been asked a question. A colour field with no
|
|
277
|
+
swatch until a request lands is a field whose only affordance is missing,
|
|
278
|
+
and a press in that window reaches nothing: so the button is here from the
|
|
279
|
+
first frame, and a press before the panel's code arrives opens it as soon
|
|
280
|
+
as it does. */}
|
|
269
281
|
{isColor && (
|
|
270
|
-
<Suspense
|
|
282
|
+
<Suspense
|
|
283
|
+
fallback={
|
|
284
|
+
<ColorSwatch
|
|
285
|
+
value={value}
|
|
286
|
+
locked={locked}
|
|
287
|
+
baseline={styles !== false}
|
|
288
|
+
label={colorLabels?.open ?? "Pick a colour"}
|
|
289
|
+
onPress={() => setColorWanted(true)}
|
|
290
|
+
/>
|
|
291
|
+
}
|
|
292
|
+
>
|
|
271
293
|
<ColorExtras
|
|
294
|
+
openOnMount={colorWanted}
|
|
272
295
|
input={element}
|
|
273
296
|
value={value}
|
|
274
297
|
format={format}
|
package/src/react/input/types.ts
CHANGED
|
@@ -151,6 +151,10 @@ export interface ColorLabels {
|
|
|
151
151
|
alpha?: string;
|
|
152
152
|
eyedropper?: string;
|
|
153
153
|
swatches?: string;
|
|
154
|
+
/** The editable readout under the rails. Default "Colour value". */
|
|
155
|
+
value?: string;
|
|
156
|
+
/** The button that cycles the readout between HEX, RGB and HSL. Default "Show as". */
|
|
157
|
+
formatAs?: string;
|
|
154
158
|
/** The word before the colour in what a screen reader announces. Default "Colour". */
|
|
155
159
|
color?: string;
|
|
156
160
|
}
|
package/dist/chunk-S7EE57YB.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// src/react/input/write-value.ts
|
|
2
|
-
function writeValue(input, next) {
|
|
3
|
-
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
4
|
-
if (setter) setter.call(input, next);
|
|
5
|
-
else input.value = next;
|
|
6
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export { writeValue };
|