@enigmax/primitives 0.23.0 → 0.24.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.
Files changed (39) hide show
  1. package/dist/{chunk-DSBYVA7V.js → chunk-DZMHY3SU.js} +73 -9
  2. package/dist/{chunk-KEVZ5XQV.js → chunk-QJJ34E4G.js} +33 -9
  3. package/dist/chunk-S2FPN5ID.js +115 -0
  4. package/dist/chunk-S7EE57YB.js +9 -0
  5. package/dist/{chunk-3BQVOOAM.js → chunk-Z3VDE7OA.js} +145 -1
  6. package/dist/{context-menu-D3FtTn7v.d.ts → clipboard-menu-B_ouitfS.d.ts} +91 -1
  7. package/dist/color-D_rZ83Oc.d.ts +152 -0
  8. package/dist/color-OPV3BJV6.js +452 -0
  9. package/dist/{index-DQNnohoo.d.ts → index-ZPvlf9vs.d.ts} +52 -4
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +2 -1
  12. package/dist/next/index.d.ts +3 -3
  13. package/dist/next/index.js +6 -4
  14. package/dist/react/context-menu.d.ts +23 -6
  15. package/dist/react/context-menu.js +2 -2
  16. package/dist/react/index.d.ts +5 -5
  17. package/dist/react/index.js +6 -4
  18. package/dist/react/input.d.ts +2 -2
  19. package/dist/react/input.js +2 -1
  20. package/dist/react-router/index.d.ts +3 -3
  21. package/dist/react-router/index.js +6 -4
  22. package/package.json +3 -1
  23. package/recipes/color/styles.css +143 -0
  24. package/recipes/context-menu/styles.css +8 -0
  25. package/registry.json +77 -7
  26. package/src/core/clipboard-menu.ts +270 -0
  27. package/src/core/color.ts +248 -0
  28. package/src/index.ts +32 -0
  29. package/src/react/context-menu/context.ts +9 -2
  30. package/src/react/context-menu/index.tsx +14 -0
  31. package/src/react/context-menu/root.tsx +132 -8
  32. package/src/react/context-menu/styles.ts +8 -0
  33. package/src/react/index.ts +32 -0
  34. package/src/react/input/color-styles.ts +156 -0
  35. package/src/react/input/color.tsx +466 -0
  36. package/src/react/input/index.tsx +54 -6
  37. package/src/react/input/types.ts +59 -3
  38. package/dist/password-C8lG4Zm9.d.ts +0 -71
  39. /package/dist/{chunk-3HDEZ2E7.js → chunk-MSOCCQGH.js} +0 -0
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Making a password, and judging one.
3
+ *
4
+ * Both are opt-in. A sign-in form wants neither: offering to generate a password where one
5
+ * already exists is noise, and scoring one the visitor cannot change is worse. They belong
6
+ * on a registration form and a change-password form, which is where an agent should switch
7
+ * them on.
8
+ */
9
+ /** Character classes a generated password can draw from. */
10
+ interface PasswordAlphabet {
11
+ lowercase?: boolean;
12
+ uppercase?: boolean;
13
+ digits?: boolean;
14
+ symbols?: boolean;
15
+ }
16
+ interface GeneratePasswordOptions extends PasswordAlphabet {
17
+ /** Default 20. Long beats clever: length is the only term that scales. */
18
+ length?: number;
19
+ /**
20
+ * Drop the characters that are read wrong off a screen or off paper - I l 1 O 0.
21
+ * Worth it when the password will be typed by hand, not worth the entropy otherwise.
22
+ */
23
+ excludeAmbiguous?: boolean;
24
+ /** Characters to remove from every class, e.g. ones your backend rejects. */
25
+ exclude?: string;
26
+ /**
27
+ * Guarantee at least one character from every class asked for. Most password policies
28
+ * demand it; it costs a little entropy, because it removes every password that happens
29
+ * to lack one.
30
+ */
31
+ requireEachClass?: boolean;
32
+ }
33
+ /**
34
+ * A random password from the classes asked for.
35
+ *
36
+ * @throws when the runtime has no CSPRNG, or when the options ask for something impossible
37
+ * (every class excluded, or a length too short to hold one of each).
38
+ */
39
+ declare function generatePassword(options?: GeneratePasswordOptions): string;
40
+ type PasswordScore = 0 | 1 | 2 | 3 | 4;
41
+ interface PasswordStrengthReport {
42
+ /** 0 worst, 4 best. What the bars under the field render. */
43
+ score: PasswordScore;
44
+ /** Estimated bits of entropy after the penalties below. */
45
+ bits: number;
46
+ /**
47
+ * Why it scored what it scored, worst first. Show the first one; showing all of them
48
+ * turns a hint into a lecture.
49
+ */
50
+ warnings: string[];
51
+ /** Empty field. Render nothing rather than a zero score, which reads as a failure. */
52
+ empty: boolean;
53
+ }
54
+ interface EstimateOptions {
55
+ /**
56
+ * Values the visitor has already typed elsewhere - email, name, company. A password
57
+ * containing one of them is guessable by anyone who has the sign-up form in front of
58
+ * them, and no character-class rule catches it.
59
+ */
60
+ userInputs?: string[];
61
+ }
62
+ /**
63
+ * Score a password.
64
+ *
65
+ * The bits are an estimate and the bands are a convention, not a measurement - they exist
66
+ * to move a bar, not to certify anything. Swap this out for zxcvbn where the number has to
67
+ * mean something, and check the breach corpus for the cases no estimator can see.
68
+ */
69
+ declare function estimatePasswordStrength(password: string, options?: EstimateOptions): PasswordStrengthReport;
70
+
71
+ /**
72
+ * Reading a colour, writing one, and converting between the two models a picker needs.
73
+ *
74
+ * Arithmetic only, like every other core here: no DOM, no framework. The picker drags a
75
+ * saturation/value square and a hue rail, which are HSV; a form field holds `#3b82f6`,
76
+ * `rgb(59, 130, 246)` or an `hsl()`, which is what a stylesheet and a database take. This
77
+ * module is the translation between them.
78
+ *
79
+ * Two things it does NOT do, both on purpose:
80
+ *
81
+ * - **Named colours.** `red`, `rebeccapurple` and the other 146 are a table nobody needs in
82
+ * a bundle to drag a square, and the DOM already resolves them for free (assign the name
83
+ * to `style.color` and read it back). The picker's canonical value is a hex string.
84
+ * - **Colour spaces past sRGB.** `oklch()` and `color()` describe colours a hex cannot, so
85
+ * accepting one here and handing back `#rrggbb` would silently clip it. Parsing returns
86
+ * null instead, which the field reports as unparseable rather than as a different colour.
87
+ */
88
+ /** sRGB, 0-255 per channel, with alpha 0-1. The transport shape everything converts through. */
89
+ interface Rgb {
90
+ r: number;
91
+ g: number;
92
+ b: number;
93
+ a: number;
94
+ }
95
+ /** Hue 0-360, saturation and value 0-1, alpha 0-1. What the picker's two controls move. */
96
+ interface Hsv {
97
+ h: number;
98
+ s: number;
99
+ v: number;
100
+ a: number;
101
+ }
102
+ /** Hue 0-360, saturation and lightness 0-1, alpha 0-1. Only used by `hsl()` in and out. */
103
+ interface Hsl {
104
+ h: number;
105
+ s: number;
106
+ l: number;
107
+ a: number;
108
+ }
109
+ /** How a colour is written back into the field. */
110
+ type ColorFormat = "hex" | "rgb" | "hsl";
111
+ interface FormatColorOptions {
112
+ /** Write the alpha channel. Off drops it, so a half-transparent colour becomes opaque. */
113
+ alpha?: boolean;
114
+ }
115
+ /**
116
+ * A colour string to sRGB, or null when it is not one.
117
+ *
118
+ * Null is the whole point of the return type: a field is unparseable for as long as someone
119
+ * is halfway through typing `#3b8`, and a picker that guesses at that moment fights the
120
+ * caret. Everything here is tolerant of what a person types - a missing `#`, upper case,
121
+ * stray spaces - and intolerant of what would be a guess.
122
+ */
123
+ declare function parseColor(input: string): Rgb | null;
124
+ /** `#rrggbb`, or `#rrggbbaa` when alpha is asked for and the colour is not opaque. */
125
+ declare function toHex(color: Rgb, options?: FormatColorOptions): string;
126
+ /**
127
+ * A colour back to a string.
128
+ *
129
+ * The legacy comma syntax for `rgb()` and `hsl()`, deliberately: this string is going into a
130
+ * field someone will paste into a stylesheet, a spreadsheet or an older toolchain, and the
131
+ * space-and-slash form is the one those still refuse.
132
+ */
133
+ declare function formatColor(color: Rgb, format?: ColorFormat, options?: FormatColorOptions): string;
134
+ /**
135
+ * sRGB to HSV, keeping a hue the arithmetic cannot see.
136
+ *
137
+ * THE colour picker bug, and it is in almost every hand-rolled one: hue is undefined at
138
+ * black, at white and at every grey, because those have no dominant channel. Recomputing it
139
+ * from the RGB therefore returns 0 - red - so dragging the square into its bottom-left
140
+ * corner and back out again resets a hue the visitor picked, and the rail jumps under their
141
+ * finger. `fallbackHue` is the hue they last chose, and it is what the picker keeps its own
142
+ * state for.
143
+ */
144
+ declare function rgbToHsv(color: Rgb, fallbackHue?: number): Hsv;
145
+ declare function hsvToRgb(color: Hsv): Rgb;
146
+ /** sRGB to HSL. Same undefined hue at the greys, same reason to pass the one being kept. */
147
+ declare function rgbToHsl(color: Rgb, fallbackHue?: number): Hsl;
148
+ declare function hslToRgb(color: Hsl): Rgb;
149
+ /** Same colour, to the byte. Used to tell a value the picker wrote from one someone typed. */
150
+ declare function colorEquals(a: Rgb | null, b: Rgb | null): boolean;
151
+
152
+ export { type ColorFormat as C, type EstimateOptions as E, type FormatColorOptions as F, type GeneratePasswordOptions as G, type Hsl as H, type PasswordAlphabet as P, type Rgb as R, type Hsv as a, type PasswordScore as b, type PasswordStrengthReport as c, colorEquals as d, estimatePasswordStrength as e, formatColor as f, generatePassword as g, hslToRgb as h, hsvToRgb as i, rgbToHsv as j, parseColor as p, rgbToHsl as r, toHex as t };
@@ -0,0 +1,452 @@
1
+ import { parseColor, rgbToHsv, colorEquals, hsvToRgb, formatColor, toHex } from './chunk-S2FPN5ID.js';
2
+ import { writeValue } from './chunk-S7EE57YB.js';
3
+ import { useLayoutEffect, useRef, useState, useMemo, useEffect, useCallback } from 'react';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
+
6
+ // src/react/input/color-styles.ts
7
+ var COLOR_STYLES = `
8
+ :root {
9
+ --enigma-color-panel-bg: #1c1c1c;
10
+ --enigma-color-panel-border: #333333;
11
+ --enigma-color-panel-radius: 0.625rem;
12
+ --enigma-color-panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
13
+ --enigma-color-panel-width: 13.5rem;
14
+ --enigma-color-text: #f5f5f5;
15
+ --enigma-color-muted: #a3a3a3;
16
+ --enigma-color-border: #404040;
17
+ --enigma-color-focus: #a3a3a3;
18
+ --enigma-color-area-height: 8rem;
19
+ --enigma-color-rail-height: 0.625rem;
20
+ --enigma-color-thumb: #ffffff;
21
+ --enigma-color-thumb-ring: rgba(0, 0, 0, 0.55);
22
+ /* The light square of the transparency chequerboard. The dark one is the surface it sits
23
+ on, which is why only one colour is a property. */
24
+ --enigma-color-checker: rgba(255, 255, 255, 0.22);
25
+ --enigma-color-checker-size: 0.5rem;
26
+ }
27
+
28
+ @media (prefers-color-scheme: light) {
29
+ :root {
30
+ --enigma-color-panel-bg: #ffffff;
31
+ --enigma-color-panel-border: #e5e5e5;
32
+ --enigma-color-panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
33
+ --enigma-color-text: #171717;
34
+ --enigma-color-muted: #737373;
35
+ --enigma-color-border: #d4d4d4;
36
+ --enigma-color-focus: #737373;
37
+ --enigma-color-thumb-ring: rgba(0, 0, 0, 0.35);
38
+ --enigma-color-checker: rgba(0, 0, 0, 0.14);
39
+ }
40
+ }
41
+
42
+ /* The anchor the panel hangs from. Inline so it sits in the field's own row, and the
43
+ positioning context so the panel needs no portal and no scroll listener: it moves with
44
+ the field because it is INSIDE it. */
45
+ [data-enigma-color] { position: relative; display: inline-flex; flex: none; }
46
+
47
+ [data-enigma-color-swatch] {
48
+ display: block; width: 1.25rem; height: 1.25rem; padding: 0;
49
+ background: none; border: 1px solid var(--enigma-color-border); border-radius: 0.3125rem;
50
+ cursor: pointer; overflow: hidden;
51
+ }
52
+ [data-enigma-color-swatch]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 2px; }
53
+ [data-enigma-color-swatch]:disabled { opacity: 0.5; cursor: not-allowed; }
54
+
55
+ /* The chequerboard, so a colour with alpha reads as transparent rather than as a lighter
56
+ colour - which is the whole difference between #ff000080 and #ff8080. */
57
+ [data-enigma-color-checkers] {
58
+ background-image:
59
+ conic-gradient(var(--enigma-color-checker) 0 25%, transparent 0 50%, var(--enigma-color-checker) 0 75%, transparent 0);
60
+ background-size: var(--enigma-color-checker-size) var(--enigma-color-checker-size);
61
+ }
62
+
63
+ [data-enigma-color-fill] { display: block; width: 100%; height: 100%; }
64
+ /* An unparseable field has no colour to show, so the swatch shows the chequerboard alone
65
+ rather than the last good colour: a stale swatch says the value is fine when it is not. */
66
+ [data-enigma-color-swatch][data-invalid] [data-enigma-color-fill] { background: none; }
67
+
68
+ [data-enigma-color-panel] {
69
+ position: absolute; z-index: 60; top: calc(100% + 0.375rem); left: 0;
70
+ display: grid; gap: 0.625rem;
71
+ box-sizing: border-box; width: var(--enigma-color-panel-width); padding: 0.625rem;
72
+ color: var(--enigma-color-text); background: var(--enigma-color-panel-bg);
73
+ border: 1px solid var(--enigma-color-panel-border);
74
+ border-radius: var(--enigma-color-panel-radius);
75
+ box-shadow: var(--enigma-color-panel-shadow);
76
+ }
77
+ /* Flipped when the field sits near the bottom of the window. Without it the panel opens off
78
+ the screen and the controls are unreachable, which is the same defect the select fixes. */
79
+ [data-enigma-color-panel][data-side="top"] { top: auto; bottom: calc(100% + 0.375rem); }
80
+
81
+ [data-enigma-color-area] {
82
+ position: relative; height: var(--enigma-color-area-height);
83
+ border-radius: 0.375rem; cursor: crosshair;
84
+ /* The drag IS the control here, so the page must not scroll under it. The marquee's
85
+ warning about touch-action is the opposite case: a full-width row still has to let the
86
+ page move. */
87
+ touch-action: none;
88
+ background:
89
+ linear-gradient(to top, #000000, rgba(0, 0, 0, 0)),
90
+ linear-gradient(to right, #ffffff, rgba(255, 255, 255, 0)),
91
+ hsl(var(--enigma-color-hue, 0), 100%, 50%);
92
+ }
93
+ [data-enigma-color-area]:focus-visible,
94
+ [data-enigma-color-rail]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 2px; }
95
+
96
+ [data-enigma-color-controls] { display: flex; align-items: center; gap: 0.5rem; }
97
+ [data-enigma-color-rails] { display: grid; gap: 0.5rem; flex: 1; min-width: 0; }
98
+
99
+ [data-enigma-color-rail] {
100
+ position: relative; height: var(--enigma-color-rail-height);
101
+ border-radius: 999px; cursor: pointer; touch-action: none;
102
+ }
103
+ [data-enigma-color-rail="hue"] {
104
+ background: linear-gradient(to right, #ff0000, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000);
105
+ }
106
+ [data-enigma-color-rail="alpha"] { background-image: none; }
107
+ [data-enigma-color-rail="alpha"] [data-enigma-color-gradient] {
108
+ position: absolute; inset: 0; border-radius: inherit;
109
+ background: linear-gradient(to right, transparent, var(--enigma-color-opaque, #000000));
110
+ }
111
+
112
+ /* One handle for the square and the rails. It must never take the pointer: a press that
113
+ lands on the handle instead of the surface under it would start no drag at all. */
114
+ [data-enigma-color-thumb] {
115
+ position: absolute; pointer-events: none;
116
+ width: 0.75rem; height: 0.75rem; margin: -0.375rem 0 0 -0.375rem;
117
+ border: 2px solid var(--enigma-color-thumb); border-radius: 999px;
118
+ box-shadow: 0 0 0 1px var(--enigma-color-thumb-ring), inset 0 0 0 1px var(--enigma-color-thumb-ring);
119
+ }
120
+ [data-enigma-color-rail] [data-enigma-color-thumb] { top: 50%; }
121
+
122
+ [data-enigma-color-preview] {
123
+ position: relative; flex: none; width: 1.75rem; height: 1.75rem;
124
+ border: 1px solid var(--enigma-color-border); border-radius: 999px; overflow: hidden;
125
+ }
126
+
127
+ [data-enigma-color-eyedropper] {
128
+ display: grid; place-items: center; flex: none;
129
+ width: 1.75rem; height: 1.75rem; padding: 0;
130
+ color: var(--enigma-color-muted); background: none;
131
+ border: 1px solid var(--enigma-color-border); border-radius: 0.375rem; cursor: pointer;
132
+ }
133
+ [data-enigma-color-eyedropper]:hover { color: var(--enigma-color-text); }
134
+ [data-enigma-color-eyedropper]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 2px; }
135
+
136
+ [data-enigma-color-swatches] { display: flex; flex-wrap: wrap; gap: 0.25rem; }
137
+ [data-enigma-color-preset] {
138
+ width: 1.125rem; height: 1.125rem; padding: 0;
139
+ border: 1px solid var(--enigma-color-border); border-radius: 0.25rem; cursor: pointer;
140
+ }
141
+ [data-enigma-color-preset]:focus-visible { outline: 2px solid var(--enigma-color-focus); outline-offset: 2px; }
142
+ [data-enigma-color-preset][aria-pressed="true"] { box-shadow: 0 0 0 2px var(--enigma-color-focus); }
143
+ `;
144
+ var INITIAL = { h: 0, s: 0, v: 0, a: 1 };
145
+ var STEP = { fine: 0.01, coarse: 0.1, hue: 1, hueCoarse: 10 };
146
+ var MARGIN = 12;
147
+ var injected = false;
148
+ function injectStyles() {
149
+ if (injected || typeof document === "undefined") return;
150
+ injected = true;
151
+ if (document.querySelector("[data-enigma-color-styles]")) return;
152
+ const element = document.createElement("style");
153
+ element.setAttribute("data-enigma-color-styles", "");
154
+ element.textContent = COLOR_STYLES;
155
+ document.head.prepend(element);
156
+ }
157
+ function clamp01(value) {
158
+ return value < 0 ? 0 : value > 1 ? 1 : value;
159
+ }
160
+ function ColorExtras({
161
+ input,
162
+ value,
163
+ format = "hex",
164
+ alpha = false,
165
+ swatches,
166
+ eyedropper = true,
167
+ placement = "auto",
168
+ styles = true,
169
+ locked = false,
170
+ labels
171
+ }) {
172
+ useLayoutEffect(() => {
173
+ if (styles) injectStyles();
174
+ }, [styles]);
175
+ const anchorRef = useRef(null);
176
+ const panelRef = useRef(null);
177
+ const swatchRef = useRef(null);
178
+ const areaRef = useRef(null);
179
+ const [open, setOpen] = useState(false);
180
+ const [side, setSide] = useState(placement === "top" ? "top" : "bottom");
181
+ const parsed = useMemo(() => parseColor(value), [value]);
182
+ const [hsv, setHsv] = useState(() => parsed ? rgbToHsv(parsed) : INITIAL);
183
+ useEffect(() => {
184
+ if (!parsed) return;
185
+ setHsv((current) => colorEquals(hsvToRgb(current), parsed) ? current : rgbToHsv(parsed, current.h));
186
+ }, [parsed]);
187
+ const rgb = useMemo(() => hsvToRgb(hsv), [hsv]);
188
+ const commit = useCallback((next) => {
189
+ setHsv(next);
190
+ if (input) writeValue(input, formatColor(hsvToRgb(next), format, { alpha }));
191
+ }, [input, format, alpha]);
192
+ useEffect(() => {
193
+ if (!input) return;
194
+ const onBlur = () => {
195
+ const colour = parseColor(input.value);
196
+ if (!colour) return;
197
+ const canonical = formatColor(colour, format, { alpha });
198
+ if (canonical !== input.value) writeValue(input, canonical);
199
+ };
200
+ input.addEventListener("blur", onBlur);
201
+ return () => input.removeEventListener("blur", onBlur);
202
+ }, [input, format, alpha]);
203
+ const setOpenState = useCallback((next) => {
204
+ setOpen((current) => current === next ? current : next);
205
+ }, []);
206
+ const close = useCallback((focusSwatch) => {
207
+ setOpenState(false);
208
+ if (focusSwatch) swatchRef.current?.focus();
209
+ }, [setOpenState]);
210
+ const place = useCallback(() => {
211
+ if (placement !== "auto") return setSide(placement);
212
+ const anchor = anchorRef.current?.getBoundingClientRect();
213
+ const panel = panelRef.current?.getBoundingClientRect();
214
+ if (!anchor || !panel) return;
215
+ const below = window.innerHeight - anchor.bottom;
216
+ setSide(below < panel.height + MARGIN && anchor.top > below ? "top" : "bottom");
217
+ }, [placement]);
218
+ useLayoutEffect(() => {
219
+ if (!open) return;
220
+ place();
221
+ }, [open, place]);
222
+ useEffect(() => {
223
+ if (!open) return;
224
+ areaRef.current?.focus();
225
+ const inside = (target) => Boolean(anchorRef.current?.contains(target));
226
+ const onPointerDown = (event) => {
227
+ if (!inside(event.target)) setOpenState(false);
228
+ };
229
+ const onFocusIn = (event) => {
230
+ if (event.target === document.body || inside(event.target)) return;
231
+ setOpenState(false);
232
+ };
233
+ const onResize = () => place();
234
+ document.addEventListener("pointerdown", onPointerDown, true);
235
+ document.addEventListener("focusin", onFocusIn, true);
236
+ window.addEventListener("resize", onResize);
237
+ return () => {
238
+ document.removeEventListener("pointerdown", onPointerDown, true);
239
+ document.removeEventListener("focusin", onFocusIn, true);
240
+ window.removeEventListener("resize", onResize);
241
+ };
242
+ }, [open, place, setOpenState]);
243
+ const track = useCallback((event, onMove) => {
244
+ if (event.button !== 0) return;
245
+ const element = event.currentTarget;
246
+ event.preventDefault();
247
+ element.focus();
248
+ const apply = (clientX, clientY) => {
249
+ const rect = element.getBoundingClientRect();
250
+ onMove(
251
+ rect.width ? clamp01((clientX - rect.left) / rect.width) : 0,
252
+ rect.height ? clamp01((clientY - rect.top) / rect.height) : 0
253
+ );
254
+ };
255
+ const move = (moved) => apply(moved.clientX, moved.clientY);
256
+ const stop = () => {
257
+ element.removeEventListener("pointermove", move);
258
+ element.removeEventListener("pointerup", stop);
259
+ element.removeEventListener("pointercancel", stop);
260
+ };
261
+ element.addEventListener("pointermove", move);
262
+ element.addEventListener("pointerup", stop);
263
+ element.addEventListener("pointercancel", stop);
264
+ try {
265
+ element.setPointerCapture(event.pointerId);
266
+ } catch {
267
+ }
268
+ apply(event.clientX, event.clientY);
269
+ }, []);
270
+ const areaKeys = useCallback((event) => {
271
+ const step = event.shiftKey ? STEP.coarse : STEP.fine;
272
+ const moves = {
273
+ ArrowLeft: [-step, 0],
274
+ ArrowRight: [step, 0],
275
+ ArrowUp: [0, step],
276
+ ArrowDown: [0, -step]
277
+ };
278
+ const move = moves[event.key];
279
+ if (!move) return;
280
+ event.preventDefault();
281
+ commit({ ...hsv, s: clamp01(hsv.s + move[0]), v: clamp01(hsv.v + move[1]) });
282
+ }, [commit, hsv]);
283
+ const railKeys = useCallback((event, kind) => {
284
+ const step = kind === "hue" ? event.shiftKey ? STEP.hueCoarse : STEP.hue : event.shiftKey ? STEP.coarse : STEP.fine;
285
+ const max = kind === "hue" ? 360 : 1;
286
+ const at = kind === "hue" ? hsv.h : hsv.a;
287
+ let next = at;
288
+ if (event.key === "ArrowLeft" || event.key === "ArrowDown") next = at - step;
289
+ else if (event.key === "ArrowRight" || event.key === "ArrowUp") next = at + step;
290
+ else if (event.key === "Home") next = 0;
291
+ else if (event.key === "End") next = max;
292
+ else return;
293
+ event.preventDefault();
294
+ const value2 = kind === "hue" ? (next % 360 + 360) % 360 : clamp01(next);
295
+ commit(kind === "hue" ? { ...hsv, h: value2 } : { ...hsv, a: value2 });
296
+ }, [commit, hsv]);
297
+ const dropper = eyedropper && typeof window !== "undefined" && "EyeDropper" in window;
298
+ const pick = useCallback(async () => {
299
+ const api = window.EyeDropper;
300
+ if (!api) return;
301
+ try {
302
+ const result = await new api().open();
303
+ const colour = parseColor(result.sRGBHex);
304
+ if (colour) commit(rgbToHsv({ ...colour, a: hsv.a }, hsv.h));
305
+ } catch {
306
+ }
307
+ }, [commit, hsv]);
308
+ const css = useMemo(() => formatColor(rgb, "hex", { alpha: true }), [rgb]);
309
+ const opaque = useMemo(() => toHex({ ...rgb, a: 1 }), [rgb]);
310
+ const text = labels ?? {};
311
+ const valueText = `${text.color ?? "Colour"} ${css}`;
312
+ const panelStyle = { "--enigma-color-hue": Math.round(hsv.h), "--enigma-color-opaque": opaque };
313
+ return /* @__PURE__ */ jsxs("span", { ref: anchorRef, "data-enigma-color": "", "data-open": open ? "" : void 0, children: [
314
+ /* @__PURE__ */ jsx(
315
+ "button",
316
+ {
317
+ ref: swatchRef,
318
+ type: "button",
319
+ "data-enigma-color-swatch": "",
320
+ "data-enigma-color-checkers": "",
321
+ "data-invalid": parsed ? void 0 : "",
322
+ "aria-haspopup": "dialog",
323
+ "aria-expanded": open,
324
+ "aria-label": text.open ?? "Pick a colour",
325
+ title: text.open ?? "Pick a colour",
326
+ disabled: locked,
327
+ onClick: () => open ? close(true) : setOpenState(true),
328
+ children: /* @__PURE__ */ jsx("span", { "data-enigma-color-fill": "", style: { background: parsed ? css : void 0 } })
329
+ }
330
+ ),
331
+ open && !locked && /* @__PURE__ */ jsxs(
332
+ "div",
333
+ {
334
+ ref: panelRef,
335
+ "data-enigma-color-panel": "",
336
+ "data-side": side,
337
+ role: "dialog",
338
+ "aria-label": text.panel ?? "Colour picker",
339
+ style: panelStyle,
340
+ onKeyDown: (event) => {
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",
367
+ {
368
+ type: "button",
369
+ "data-enigma-color-eyedropper": "",
370
+ "aria-label": text.eyedropper ?? "Pick a colour from the screen",
371
+ title: text.eyedropper ?? "Pick a colour from the screen",
372
+ onClick: () => {
373
+ void pick();
374
+ },
375
+ children: /* @__PURE__ */ jsx(EyedropperIcon, {})
376
+ }
377
+ ),
378
+ /* @__PURE__ */ jsxs("div", { "data-enigma-color-rails": "", children: [
379
+ /* @__PURE__ */ jsx(
380
+ "div",
381
+ {
382
+ "data-enigma-color-rail": "hue",
383
+ role: "slider",
384
+ tabIndex: 0,
385
+ "aria-label": text.hue ?? "Hue",
386
+ "aria-valuemin": 0,
387
+ "aria-valuemax": 360,
388
+ "aria-valuenow": Math.round(hsv.h),
389
+ "aria-valuetext": `${Math.round(hsv.h)} degrees`,
390
+ onKeyDown: (event) => railKeys(event, "hue"),
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}%` } })
393
+ }
394
+ ),
395
+ alpha && /* @__PURE__ */ jsxs(
396
+ "div",
397
+ {
398
+ "data-enigma-color-rail": "alpha",
399
+ "data-enigma-color-checkers": "",
400
+ role: "slider",
401
+ tabIndex: 0,
402
+ "aria-label": text.alpha ?? "Opacity",
403
+ "aria-valuemin": 0,
404
+ "aria-valuemax": 100,
405
+ "aria-valuenow": Math.round(hsv.a * 100),
406
+ "aria-valuetext": `${Math.round(hsv.a * 100)}% opacity`,
407
+ onKeyDown: (event) => railKeys(event, "alpha"),
408
+ onPointerDown: (event) => track(event, (x) => commit({ ...hsv, a: x })),
409
+ children: [
410
+ /* @__PURE__ */ jsx("span", { "data-enigma-color-gradient": "" }),
411
+ /* @__PURE__ */ jsx("span", { "data-enigma-color-thumb": "", style: { left: `${hsv.a * 100}%` } })
412
+ ]
413
+ }
414
+ )
415
+ ] })
416
+ ] }),
417
+ swatches && swatches.length > 0 && /* @__PURE__ */ jsx("div", { "data-enigma-color-swatches": "", role: "group", "aria-label": text.swatches ?? "Preset colours", children: swatches.map((preset) => {
418
+ const colour = parseColor(preset);
419
+ return /* @__PURE__ */ jsx(
420
+ "button",
421
+ {
422
+ type: "button",
423
+ "data-enigma-color-preset": "",
424
+ "data-enigma-color-checkers": "",
425
+ style: { background: preset },
426
+ "aria-label": preset,
427
+ title: preset,
428
+ "aria-pressed": colourEqualsPreset(colour, css),
429
+ onClick: () => {
430
+ if (colour) commit(rgbToHsv(alpha ? colour : { ...colour, a: 1 }, hsv.h));
431
+ }
432
+ },
433
+ preset
434
+ );
435
+ }) })
436
+ ]
437
+ }
438
+ )
439
+ ] });
440
+ }
441
+ function colourEqualsPreset(preset, current) {
442
+ return colorEquals(preset, parseColor(current));
443
+ }
444
+ function EyedropperIcon() {
445
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
446
+ /* @__PURE__ */ jsx("path", { d: "m2 22 1-1h3l9-9" }),
447
+ /* @__PURE__ */ jsx("path", { d: "M3 21v-3l9-9" }),
448
+ /* @__PURE__ */ jsx("path", { d: "m15 6 3-3a2.83 2.83 0 0 1 4 4l-3 3-1-1-4 4-3-3 4-4Z" })
449
+ ] });
450
+ }
451
+
452
+ export { ColorExtras };
@@ -1,6 +1,6 @@
1
+ import { C as ColorFormat, G as GeneratePasswordOptions, E as EstimateOptions, c as PasswordStrengthReport } from './color-D_rZ83Oc.js';
1
2
  import { ReactNode, ComponentPropsWithoutRef } from 'react';
2
3
  import { c as SearchOptions, F as FuseConstructor, b as SearchMatch } from './search-DYgqRp37.js';
3
- import { G as GeneratePasswordOptions, E as EstimateOptions, b as PasswordStrengthReport } from './password-C8lG4Zm9.js';
4
4
 
5
5
  /**
6
6
  * The prop shapes for `<Input>`, split from the component so the per-type chunks can import
@@ -131,14 +131,62 @@ interface SearchOnlyProps<Item = unknown> {
131
131
  clearable?: boolean;
132
132
  clearLabel?: string;
133
133
  }
134
+ /** Which side of the field the picker's panel opens on. `auto` measures the room there is. */
135
+ type ColorPanelPlacement = "auto" | "top" | "bottom";
136
+ /** The picker's accessible names, for a UI that is not in English. */
137
+ interface ColorLabels {
138
+ /** The swatch that opens the panel. */
139
+ open?: string;
140
+ /** The panel itself. */
141
+ panel?: string;
142
+ /** The saturation and brightness square. */
143
+ area?: string;
144
+ hue?: string;
145
+ alpha?: string;
146
+ eyedropper?: string;
147
+ swatches?: string;
148
+ /** The word before the colour in what a screen reader announces. Default "Colour". */
149
+ color?: string;
150
+ }
151
+ /** The half only a colour field has. */
152
+ interface ColorOnlyProps {
153
+ type: "color";
154
+ /**
155
+ * How the picker writes the value back: `#3b82f6`, `rgb(59, 130, 246)` or an `hsl()`.
156
+ * Default hex. Whatever the format, every one of them is READ - a pasted `rgb()` in a hex
157
+ * field is understood and normalized on blur rather than rejected.
158
+ */
159
+ format?: ColorFormat;
160
+ /**
161
+ * The opacity rail, and the alpha channel in the value. OFF by default: it turns
162
+ * `#3b82f6` into `#3b82f680`, which is not what a column typed as a 7-character hex, or a
163
+ * `<input type="color">` on the server side, is expecting.
164
+ */
165
+ alpha?: boolean;
166
+ /** Preset colours under the rails - a brand palette, or the last few that were used. */
167
+ swatches?: readonly string[];
168
+ /**
169
+ * The screen eyedropper, where the browser has one (Chromium today). Default true, and
170
+ * feature-detected: the button is not rendered at all where the API is missing.
171
+ */
172
+ eyedropper?: boolean;
173
+ /** Which side the panel opens on. Default `auto`, which flips near the bottom of the window. */
174
+ placement?: ColorPanelPlacement;
175
+ /**
176
+ * The panel's baseline stylesheet, injected once. Default true, and the one place this
177
+ * package ships a look - see `color-styles.ts` for why a picker cannot be unstyled.
178
+ */
179
+ styles?: boolean;
180
+ colorLabels?: ColorLabels;
181
+ }
134
182
  /** Everything else: one `<input>`, its native props, and no extras to bundle. */
135
183
  interface PlainOnlyProps {
136
- type?: Exclude<InputType, "password" | "search">;
184
+ type?: Exclude<InputType, "password" | "search" | "color">;
137
185
  }
138
- type InputProps<Item = unknown> = InputBaseProps & (PasswordOnlyProps | SearchOnlyProps<Item> | PlainOnlyProps);
186
+ type InputProps<Item = unknown> = InputBaseProps & (PasswordOnlyProps | SearchOnlyProps<Item> | ColorOnlyProps | PlainOnlyProps);
139
187
 
140
188
  declare const Input: <Item = unknown>(props: InputProps<Item> & {
141
189
  ref?: React.Ref<HTMLInputElement>;
142
190
  }) => ReactNode;
143
191
 
144
- export { type BreachChecker as B, type FieldAction as F, Input as I, type PasswordOnlyProps as P, type SearchOnlyProps as S, type BreachState as a, type BreachStatus as b, type InputBaseProps as c, type InputProps as d, type InputType as e, type PasswordStrengthClassNames as f, type PlainOnlyProps as g };
192
+ export { type BreachChecker as B, type ColorLabels as C, type FieldAction as F, Input as I, type PasswordOnlyProps as P, type SearchOnlyProps as S, type BreachState as a, type BreachStatus as b, type ColorOnlyProps as c, type ColorPanelPlacement as d, type InputBaseProps as e, type InputProps as f, type InputType as g, type PasswordStrengthClassNames as h, type PlainOnlyProps as i };