@enigmax/primitives 0.23.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-KEVZ5XQV.js → chunk-3MGBZOAU.js} +50 -9
- package/dist/{chunk-DSBYVA7V.js → chunk-DZMHY3SU.js} +73 -9
- package/dist/chunk-KVLSOQTI.js +66 -0
- package/dist/chunk-S2FPN5ID.js +115 -0
- package/dist/{chunk-3BQVOOAM.js → chunk-Z3VDE7OA.js} +145 -1
- package/dist/{context-menu-D3FtTn7v.d.ts → clipboard-menu-B_ouitfS.d.ts} +91 -1
- package/dist/color-D_rZ83Oc.d.ts +152 -0
- package/dist/color-T63FLJNH.js +515 -0
- package/dist/{index-DQNnohoo.d.ts → index-qXOkYQCU.d.ts} +56 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/next/index.d.ts +3 -3
- package/dist/next/index.js +6 -4
- package/dist/react/context-menu.d.ts +23 -6
- package/dist/react/context-menu.js +2 -2
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.js +6 -4
- package/dist/react/input.d.ts +2 -2
- package/dist/react/input.js +2 -1
- package/dist/react-router/index.d.ts +3 -3
- package/dist/react-router/index.js +6 -4
- package/package.json +3 -1
- package/recipes/color/styles.css +163 -0
- package/recipes/context-menu/styles.css +8 -0
- package/registry.json +86 -7
- package/src/core/clipboard-menu.ts +270 -0
- package/src/core/color.ts +248 -0
- package/src/index.ts +32 -0
- package/src/react/context-menu/context.ts +9 -2
- package/src/react/context-menu/index.tsx +14 -0
- package/src/react/context-menu/root.tsx +132 -8
- package/src/react/context-menu/styles.ts +8 -0
- package/src/react/index.ts +32 -0
- package/src/react/input/color-styles.ts +176 -0
- package/src/react/input/color-swatch.tsx +96 -0
- package/src/react/input/color.tsx +527 -0
- package/src/react/input/index.tsx +77 -6
- package/src/react/input/types.ts +63 -3
- package/dist/password-C8lG4Zm9.d.ts +0 -71
- /package/dist/{chunk-3HDEZ2E7.js → chunk-MSOCCQGH.js} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { writeValue, ColorSwatch } from './chunk-KVLSOQTI.js';
|
|
1
2
|
import { INPUT_ICON_PATHS } from './chunk-F25CGNQC.js';
|
|
2
3
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
4
|
import { lazy, forwardRef, useRef, useState, useId, useCallback, useEffect, Suspense } from 'react';
|
|
@@ -16,16 +17,15 @@ var ICON_PROPS = {
|
|
|
16
17
|
function Icon({ paths }) {
|
|
17
18
|
return /* @__PURE__ */ jsx("svg", { ...ICON_PROPS, children: paths.map((path) => /* @__PURE__ */ jsx("path", { d: path }, path)) });
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
26
|
-
}
|
|
20
|
+
var COLOR_FIELD_PROPS = {
|
|
21
|
+
autoComplete: "off",
|
|
22
|
+
autoCorrect: "off",
|
|
23
|
+
autoCapitalize: "off",
|
|
24
|
+
spellCheck: false
|
|
25
|
+
};
|
|
27
26
|
var PasswordExtras = lazy(() => import('./password-3DRQYAYQ.js').then((module) => ({ default: module.PasswordExtras })));
|
|
28
27
|
var SearchExtras = lazy(() => import('./search-PBZORZ7P.js').then((module) => ({ default: module.SearchExtras })));
|
|
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,
|
|
@@ -53,6 +53,13 @@ var Input = forwardRef(function Input2(props, forwardedRef) {
|
|
|
53
53
|
renderResults,
|
|
54
54
|
clearable,
|
|
55
55
|
clearLabel = "Clear",
|
|
56
|
+
format,
|
|
57
|
+
alpha,
|
|
58
|
+
swatches,
|
|
59
|
+
eyedropper,
|
|
60
|
+
placement,
|
|
61
|
+
styles,
|
|
62
|
+
colorLabels,
|
|
56
63
|
wrapperProps,
|
|
57
64
|
fieldProps,
|
|
58
65
|
classNames,
|
|
@@ -64,9 +71,11 @@ var Input = forwardRef(function Input2(props, forwardedRef) {
|
|
|
64
71
|
const innerRef = useRef(null);
|
|
65
72
|
const [element, setElement] = useState(null);
|
|
66
73
|
const [revealed, setRevealed] = useState(false);
|
|
74
|
+
const [colorWanted, setColorWanted] = useState(false);
|
|
67
75
|
const describedBy = useId();
|
|
68
76
|
const isPassword = type === "password";
|
|
69
77
|
const isSearch = type === "search";
|
|
78
|
+
const isColor = type === "color";
|
|
70
79
|
const showReveal = reveal ?? isPassword;
|
|
71
80
|
const showGenerate = generate !== false && isPassword;
|
|
72
81
|
const showClear = (clearable ?? isSearch) && isSearch;
|
|
@@ -192,10 +201,42 @@ var Input = forwardRef(function Input2(props, forwardedRef) {
|
|
|
192
201
|
"data-score": score ?? void 0,
|
|
193
202
|
children: [
|
|
194
203
|
/* @__PURE__ */ jsxs("div", { ...fieldProps, "data-enigma-input-field": "", children: [
|
|
204
|
+
isColor && /* @__PURE__ */ jsx(
|
|
205
|
+
Suspense,
|
|
206
|
+
{
|
|
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
|
+
)
|
|
233
|
+
}
|
|
234
|
+
),
|
|
195
235
|
position === "start" && buttons,
|
|
196
236
|
/* @__PURE__ */ jsx(
|
|
197
237
|
"input",
|
|
198
238
|
{
|
|
239
|
+
...isColor ? COLOR_FIELD_PROPS : null,
|
|
199
240
|
...rest,
|
|
200
241
|
ref: (node) => {
|
|
201
242
|
innerRef.current = node;
|
|
@@ -203,7 +244,7 @@ var Input = forwardRef(function Input2(props, forwardedRef) {
|
|
|
203
244
|
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
204
245
|
else if (forwardedRef) forwardedRef.current = node;
|
|
205
246
|
},
|
|
206
|
-
type: revealed && isPassword ? "text" : type,
|
|
247
|
+
type: isColor ? "text" : revealed && isPassword ? "text" : type,
|
|
207
248
|
onChange: handleChange,
|
|
208
249
|
"data-enigma-input": "",
|
|
209
250
|
"aria-describedby": score !== null ? describedBy : rest["aria-describedby"]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContextMenu, isAction } from './chunk-
|
|
1
|
+
import { createContextMenu, clipboardAction, performClipboardAction, inspectClipboardTarget, CLIPBOARD_PREFIX, clipboardEntries, isAction, clipboardHasText } from './chunk-Z3VDE7OA.js';
|
|
2
2
|
import { shortcutTokens } from './chunk-4VUHQFAT.js';
|
|
3
3
|
import { Slot } from './chunk-HS3X3XCW.js';
|
|
4
4
|
import { shortenQuery } from './chunk-4ZPMP47J.js';
|
|
@@ -60,6 +60,14 @@ var CONTEXT_MENU_STYLES = `
|
|
|
60
60
|
visibility: hidden;
|
|
61
61
|
}
|
|
62
62
|
[data-enigma-menu-panel][data-placed] { visibility: visible; }
|
|
63
|
+
/* No focus ring around the panel. It takes focus so the keyboard has somewhere to be, not
|
|
64
|
+
because it is a control: what MOVES is the highlighted row, drawn by [data-active] and
|
|
65
|
+
announced through aria-activedescendant. A ring around the whole menu says the menu is the
|
|
66
|
+
thing focused, which is not what is happening - and it appears the moment you press an
|
|
67
|
+
arrow, over a surface that was already showing you where you are. Nothing is lost by
|
|
68
|
+
removing it, because nothing inside is reachable by Tab: the menu owns that key. */
|
|
69
|
+
[data-enigma-menu-panel]:focus,
|
|
70
|
+
[data-enigma-menu-panel]:focus-visible { outline: none; }
|
|
63
71
|
@keyframes enigma-menu-in {
|
|
64
72
|
from { opacity: 0; transform: scale(0.98); }
|
|
65
73
|
to { opacity: 1; transform: none; }
|
|
@@ -184,6 +192,32 @@ function useContextMenuContext(part) {
|
|
|
184
192
|
if (!value) throw new Error(`<${part}> must be rendered inside <ContextMenu.Root>.`);
|
|
185
193
|
return value;
|
|
186
194
|
}
|
|
195
|
+
var CLIPBOARD_ICON = {
|
|
196
|
+
viewBox: "0 0 24 24",
|
|
197
|
+
width: "1em",
|
|
198
|
+
height: "1em",
|
|
199
|
+
fill: "none",
|
|
200
|
+
stroke: "currentColor",
|
|
201
|
+
strokeWidth: 2,
|
|
202
|
+
strokeLinecap: "round",
|
|
203
|
+
strokeLinejoin: "round",
|
|
204
|
+
"aria-hidden": true
|
|
205
|
+
};
|
|
206
|
+
var CLIPBOARD_ICONS = {
|
|
207
|
+
copy: /* @__PURE__ */ jsxs("svg", { ...CLIPBOARD_ICON, children: [
|
|
208
|
+
/* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "12", height: "12", rx: "2" }),
|
|
209
|
+
/* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
|
|
210
|
+
] }),
|
|
211
|
+
cut: /* @__PURE__ */ jsxs("svg", { ...CLIPBOARD_ICON, children: [
|
|
212
|
+
/* @__PURE__ */ jsx("circle", { cx: "6", cy: "6", r: "3" }),
|
|
213
|
+
/* @__PURE__ */ jsx("circle", { cx: "6", cy: "18", r: "3" }),
|
|
214
|
+
/* @__PURE__ */ jsx("path", { d: "M20 4 8.12 15.88M14.47 14.48 20 20M8.12 8.12 12 12" })
|
|
215
|
+
] }),
|
|
216
|
+
paste: /* @__PURE__ */ jsxs("svg", { ...CLIPBOARD_ICON, children: [
|
|
217
|
+
/* @__PURE__ */ jsx("path", { d: "M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" }),
|
|
218
|
+
/* @__PURE__ */ jsx("rect", { x: "8", y: "2", width: "8", height: "4", rx: "1" })
|
|
219
|
+
] })
|
|
220
|
+
};
|
|
187
221
|
var injected = false;
|
|
188
222
|
function injectStyles() {
|
|
189
223
|
if (injected || typeof document === "undefined") return;
|
|
@@ -223,6 +257,7 @@ function ContextMenuRoot(props) {
|
|
|
223
257
|
loadingLabel = "Loading...",
|
|
224
258
|
emptyLabel = "Nothing here.",
|
|
225
259
|
styles = true,
|
|
260
|
+
clipboard = true,
|
|
226
261
|
children
|
|
227
262
|
} = props;
|
|
228
263
|
useLayoutEffect(() => {
|
|
@@ -242,7 +277,11 @@ function ContextMenuRoot(props) {
|
|
|
242
277
|
matcher,
|
|
243
278
|
searchKeys,
|
|
244
279
|
cacheMs,
|
|
245
|
-
onSelect: (item, path) =>
|
|
280
|
+
onSelect: (item, path) => {
|
|
281
|
+
const action = clipboardAction(item.id);
|
|
282
|
+
if (action) void performClipboardAction(action, clipboardTarget.current);
|
|
283
|
+
latest.current.onSelect?.(item, path);
|
|
284
|
+
},
|
|
246
285
|
onOpenChange: (open2) => latest.current.onOpenChange?.(open2)
|
|
247
286
|
// Built once: rebuilding it would drop the open branch and the highlight on every
|
|
248
287
|
// render. Every option below is pushed in through update().
|
|
@@ -276,9 +315,21 @@ function ContextMenuRoot(props) {
|
|
|
276
315
|
}, [items]);
|
|
277
316
|
const currentItems = useRef(items);
|
|
278
317
|
currentItems.current = items;
|
|
318
|
+
const clipboardTarget = useRef(inspectClipboardTarget(null));
|
|
319
|
+
const clipboardRows = useRef([]);
|
|
320
|
+
const clipboardOptions = useMemo(() => {
|
|
321
|
+
if (clipboard === false) return null;
|
|
322
|
+
return { icons: CLIPBOARD_ICONS, ...clipboard === true ? {} : clipboard };
|
|
323
|
+
}, [clipboard]);
|
|
324
|
+
const withClipboard = useCallback((rows) => {
|
|
325
|
+
const clip = clipboardRows.current;
|
|
326
|
+
if (clip.length === 0) return rows;
|
|
327
|
+
if (rows.length === 0) return clip;
|
|
328
|
+
return [...clip, { type: "separator", id: `${CLIPBOARD_PREFIX}rule` }, ...rows];
|
|
329
|
+
}, []);
|
|
279
330
|
useEffect(() => {
|
|
280
|
-
instance.update({ items: currentItems.current, title });
|
|
281
|
-
}, [instance, signature, title]);
|
|
331
|
+
instance.update({ items: withClipboard(currentItems.current), title });
|
|
332
|
+
}, [instance, signature, title, withClipboard]);
|
|
282
333
|
const cancelClose = useCallback(() => {
|
|
283
334
|
window.clearTimeout(closing.current);
|
|
284
335
|
}, []);
|
|
@@ -287,11 +338,23 @@ function ContextMenuRoot(props) {
|
|
|
287
338
|
closing.current = window.setTimeout(() => instance.closeBelow(level), closeDelay);
|
|
288
339
|
}, [instance, closeDelay]);
|
|
289
340
|
useEffect(() => cancelClose, [cancelClose]);
|
|
290
|
-
const open = useCallback((point) => {
|
|
341
|
+
const open = useCallback((point, target) => {
|
|
291
342
|
if (disabled) return false;
|
|
292
343
|
cancelClose();
|
|
344
|
+
if (clipboardOptions) {
|
|
345
|
+
clipboardTarget.current = inspectClipboardTarget(target ?? null);
|
|
346
|
+
clipboardRows.current = clipboardEntries(clipboardTarget.current, clipboardOptions);
|
|
347
|
+
instance.update({ items: withClipboard(currentItems.current) });
|
|
348
|
+
if (clipboardRows.current.some((entry) => isAction(entry) && clipboardAction(entry.id) === "paste")) {
|
|
349
|
+
void clipboardHasText().then((has) => {
|
|
350
|
+
if (has !== false || !instance.state.open) return;
|
|
351
|
+
clipboardRows.current = clipboardRows.current.map((entry) => isAction(entry) && clipboardAction(entry.id) === "paste" ? { ...entry, disabled: true } : entry);
|
|
352
|
+
instance.update({ items: withClipboard(currentItems.current) });
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
293
356
|
return instance.open(point);
|
|
294
|
-
}, [instance, disabled, cancelClose]);
|
|
357
|
+
}, [instance, disabled, cancelClose, clipboardOptions, withClipboard]);
|
|
295
358
|
const close = useCallback(() => {
|
|
296
359
|
cancelClose();
|
|
297
360
|
instance.close();
|
|
@@ -414,16 +477,17 @@ function ContextMenuTrigger({ asChild = false, longPress = true, children, ...pr
|
|
|
414
477
|
onContextMenu: (event) => {
|
|
415
478
|
props.onContextMenu?.(event);
|
|
416
479
|
if (event.defaultPrevented) return;
|
|
417
|
-
if (menu.open({ x: event.clientX, y: event.clientY })) event.preventDefault();
|
|
480
|
+
if (menu.open({ x: event.clientX, y: event.clientY }, event.target)) event.preventDefault();
|
|
418
481
|
},
|
|
419
482
|
onPointerDown: (event) => {
|
|
420
483
|
props.onPointerDown?.(event);
|
|
421
484
|
if (event.defaultPrevented || !longPress || event.pointerType !== "touch") return;
|
|
422
485
|
const { clientX: x, clientY: y } = event;
|
|
423
486
|
cancelPress();
|
|
487
|
+
const target = event.target;
|
|
424
488
|
press.current = { x, y, timer: window.setTimeout(() => {
|
|
425
489
|
press.current = null;
|
|
426
|
-
menu.open({ x, y });
|
|
490
|
+
menu.open({ x, y }, target);
|
|
427
491
|
}, LONG_PRESS_MS) };
|
|
428
492
|
},
|
|
429
493
|
onPointerMove: (event) => {
|
|
@@ -445,7 +509,7 @@ function ContextMenuTrigger({ asChild = false, longPress = true, children, ...pr
|
|
|
445
509
|
const wanted = event.key === "ContextMenu" || event.key === "F10" && event.shiftKey;
|
|
446
510
|
if (!wanted) return;
|
|
447
511
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
448
|
-
if (menu.open({ x: rect.left + 8, y: rect.top + 8 })) event.preventDefault();
|
|
512
|
+
if (menu.open({ x: rect.left + 8, y: rect.top + 8 }, document.activeElement)) event.preventDefault();
|
|
449
513
|
},
|
|
450
514
|
children
|
|
451
515
|
}
|
|
@@ -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 };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/core/color.ts
|
|
2
|
+
function clamp(value, min, max) {
|
|
3
|
+
return value < min ? min : value > max ? max : value;
|
|
4
|
+
}
|
|
5
|
+
function clampAlpha(value) {
|
|
6
|
+
return Number.isFinite(value) ? clamp(value, 0, 1) : 1;
|
|
7
|
+
}
|
|
8
|
+
function channel(value) {
|
|
9
|
+
return Math.round(clamp(value, 0, 255));
|
|
10
|
+
}
|
|
11
|
+
function ratio(token, scale) {
|
|
12
|
+
const text = token.trim();
|
|
13
|
+
const value = Number.parseFloat(text);
|
|
14
|
+
if (!Number.isFinite(value)) return Number.NaN;
|
|
15
|
+
return text.endsWith("%") ? value / 100 * scale : value;
|
|
16
|
+
}
|
|
17
|
+
function components(body) {
|
|
18
|
+
return body.split(/[\s,/]+/).map((part) => part.trim()).filter(Boolean);
|
|
19
|
+
}
|
|
20
|
+
function parseColor(input) {
|
|
21
|
+
const text = input.trim().toLowerCase();
|
|
22
|
+
if (!text) return null;
|
|
23
|
+
if (text === "transparent") return { r: 0, g: 0, b: 0, a: 0 };
|
|
24
|
+
const hex = /^#?([0-9a-f]+)$/.exec(text);
|
|
25
|
+
if (hex) return fromHex(hex[1]);
|
|
26
|
+
const functional = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(text);
|
|
27
|
+
if (!functional) return null;
|
|
28
|
+
const parts = components(functional[2]);
|
|
29
|
+
if (parts.length < 3 || parts.length > 4) return null;
|
|
30
|
+
const alpha = parts.length === 4 ? clampAlpha(ratio(parts[3], 1)) : 1;
|
|
31
|
+
if (functional[1].startsWith("rgb")) {
|
|
32
|
+
const values = parts.slice(0, 3).map((part) => ratio(part, 255));
|
|
33
|
+
if (values.some((value) => !Number.isFinite(value))) return null;
|
|
34
|
+
return { r: channel(values[0]), g: channel(values[1]), b: channel(values[2]), a: alpha };
|
|
35
|
+
}
|
|
36
|
+
const hue = Number.parseFloat(parts[0]);
|
|
37
|
+
const saturation = ratio(parts[1], 1);
|
|
38
|
+
const lightness = ratio(parts[2], 1);
|
|
39
|
+
if (!Number.isFinite(hue) || !Number.isFinite(saturation) || !Number.isFinite(lightness)) return null;
|
|
40
|
+
return hslToRgb({ h: hue, s: clamp(saturation, 0, 1), l: clamp(lightness, 0, 1), a: alpha });
|
|
41
|
+
}
|
|
42
|
+
function fromHex(digits) {
|
|
43
|
+
const size = digits.length;
|
|
44
|
+
if (size !== 3 && size !== 4 && size !== 6 && size !== 8) return null;
|
|
45
|
+
const short = size <= 4;
|
|
46
|
+
const at = (index) => {
|
|
47
|
+
const slice = short ? digits[index].repeat(2) : digits.slice(index * 2, index * 2 + 2);
|
|
48
|
+
return Number.parseInt(slice, 16);
|
|
49
|
+
};
|
|
50
|
+
const alpha = size === 4 || size === 8 ? at(3) / 255 : 1;
|
|
51
|
+
return { r: at(0), g: at(1), b: at(2), a: clampAlpha(alpha) };
|
|
52
|
+
}
|
|
53
|
+
function toHex(color, options = {}) {
|
|
54
|
+
const pair = (value) => channel(value).toString(16).padStart(2, "0");
|
|
55
|
+
const alpha = clampAlpha(color.a);
|
|
56
|
+
const suffix = options.alpha && alpha < 1 ? pair(Math.round(alpha * 255)) : "";
|
|
57
|
+
return `#${pair(color.r)}${pair(color.g)}${pair(color.b)}${suffix}`;
|
|
58
|
+
}
|
|
59
|
+
function formatColor(color, format = "hex", options = {}) {
|
|
60
|
+
const alpha = clampAlpha(color.a);
|
|
61
|
+
const opaque = !options.alpha || alpha >= 1;
|
|
62
|
+
const printed = Number.parseFloat(alpha.toFixed(3));
|
|
63
|
+
if (format === "hex") return toHex(color, options);
|
|
64
|
+
if (format === "rgb") {
|
|
65
|
+
const body2 = `${channel(color.r)}, ${channel(color.g)}, ${channel(color.b)}`;
|
|
66
|
+
return opaque ? `rgb(${body2})` : `rgba(${body2}, ${printed})`;
|
|
67
|
+
}
|
|
68
|
+
const hsl = rgbToHsl(color);
|
|
69
|
+
const body = `${Math.round(hsl.h)}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%`;
|
|
70
|
+
return opaque ? `hsl(${body})` : `hsla(${body}, ${printed})`;
|
|
71
|
+
}
|
|
72
|
+
function rgbToHsv(color, fallbackHue = 0) {
|
|
73
|
+
const r = clamp(color.r, 0, 255) / 255;
|
|
74
|
+
const g = clamp(color.g, 0, 255) / 255;
|
|
75
|
+
const b = clamp(color.b, 0, 255) / 255;
|
|
76
|
+
const max = Math.max(r, g, b);
|
|
77
|
+
const span = max - Math.min(r, g, b);
|
|
78
|
+
let h = fallbackHue;
|
|
79
|
+
if (span > 0) {
|
|
80
|
+
if (max === r) h = (g - b) / span % 6;
|
|
81
|
+
else if (max === g) h = (b - r) / span + 2;
|
|
82
|
+
else h = (r - g) / span + 4;
|
|
83
|
+
h *= 60;
|
|
84
|
+
if (h < 0) h += 360;
|
|
85
|
+
}
|
|
86
|
+
return { h, s: max === 0 ? 0 : span / max, v: max, a: clampAlpha(color.a) };
|
|
87
|
+
}
|
|
88
|
+
function hsvToRgb(color) {
|
|
89
|
+
const h = (color.h % 360 + 360) % 360;
|
|
90
|
+
const s = clamp(color.s, 0, 1);
|
|
91
|
+
const v = clamp(color.v, 0, 1);
|
|
92
|
+
const c = v * s;
|
|
93
|
+
const x = c * (1 - Math.abs(h / 60 % 2 - 1));
|
|
94
|
+
const m = v - c;
|
|
95
|
+
const [r, g, b] = h < 60 ? [c, x, 0] : h < 120 ? [x, c, 0] : h < 180 ? [0, c, x] : h < 240 ? [0, x, c] : h < 300 ? [x, 0, c] : [c, 0, x];
|
|
96
|
+
return { r: channel((r + m) * 255), g: channel((g + m) * 255), b: channel((b + m) * 255), a: clampAlpha(color.a) };
|
|
97
|
+
}
|
|
98
|
+
function rgbToHsl(color, fallbackHue = 0) {
|
|
99
|
+
const hsv = rgbToHsv(color, fallbackHue);
|
|
100
|
+
const l = hsv.v * (1 - hsv.s / 2);
|
|
101
|
+
const s = l === 0 || l === 1 ? 0 : (hsv.v - l) / Math.min(l, 1 - l);
|
|
102
|
+
return { h: hsv.h, s: clamp(s, 0, 1), l, a: hsv.a };
|
|
103
|
+
}
|
|
104
|
+
function hslToRgb(color) {
|
|
105
|
+
const s = clamp(color.s, 0, 1);
|
|
106
|
+
const l = clamp(color.l, 0, 1);
|
|
107
|
+
const v = l + s * Math.min(l, 1 - l);
|
|
108
|
+
return hsvToRgb({ h: color.h, s: v === 0 ? 0 : 2 * (1 - l / v), v, a: color.a });
|
|
109
|
+
}
|
|
110
|
+
function colorEquals(a, b) {
|
|
111
|
+
if (!a || !b) return a === b;
|
|
112
|
+
return channel(a.r) === channel(b.r) && channel(a.g) === channel(b.g) && channel(a.b) === channel(b.b) && Math.round(clampAlpha(a.a) * 255) === Math.round(clampAlpha(b.a) * 255);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { colorEquals, formatColor, hslToRgb, hsvToRgb, parseColor, rgbToHsl, rgbToHsv, toHex };
|
|
@@ -385,4 +385,148 @@ function createContextMenu(options = {}) {
|
|
|
385
385
|
};
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
-
|
|
388
|
+
// src/core/clipboard-menu.ts
|
|
389
|
+
var CLIPBOARD_PREFIX = "enigma:clipboard:";
|
|
390
|
+
var SELECTABLE_TYPES = /* @__PURE__ */ new Set(["text", "search", "url", "tel", "password"]);
|
|
391
|
+
var EMPTY = { editable: null, writable: false, selection: "", copyable: true, range: null };
|
|
392
|
+
function isEditableElement(node) {
|
|
393
|
+
return Boolean(node && node.isContentEditable);
|
|
394
|
+
}
|
|
395
|
+
function documentSelection(element) {
|
|
396
|
+
const selection = typeof window === "undefined" ? null : window.getSelection();
|
|
397
|
+
if (!selection || selection.isCollapsed || selection.rangeCount === 0) return "";
|
|
398
|
+
const range = selection.getRangeAt(0);
|
|
399
|
+
if (element && !range.intersectsNode(element)) return "";
|
|
400
|
+
return selection.toString();
|
|
401
|
+
}
|
|
402
|
+
function inspectClipboardTarget(node) {
|
|
403
|
+
if (typeof document === "undefined") return EMPTY;
|
|
404
|
+
const element = node instanceof Element ? node : null;
|
|
405
|
+
const field = element?.closest?.("input, textarea");
|
|
406
|
+
if (field) {
|
|
407
|
+
const password = field instanceof HTMLInputElement && field.type === "password";
|
|
408
|
+
const selectable = field instanceof HTMLTextAreaElement || SELECTABLE_TYPES.has(field.type);
|
|
409
|
+
const start = selectable ? field.selectionStart ?? 0 : 0;
|
|
410
|
+
const end = selectable ? field.selectionEnd ?? 0 : 0;
|
|
411
|
+
return {
|
|
412
|
+
editable: field,
|
|
413
|
+
writable: !field.disabled && !field.readOnly,
|
|
414
|
+
selection: selectable ? field.value.slice(start, end) : "",
|
|
415
|
+
copyable: !password,
|
|
416
|
+
range: selectable ? { start, end } : null
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const editable = isEditableElement(element) ? element.closest("[contenteditable]") ?? element : null;
|
|
420
|
+
return {
|
|
421
|
+
editable,
|
|
422
|
+
writable: Boolean(editable),
|
|
423
|
+
selection: documentSelection(element),
|
|
424
|
+
copyable: true,
|
|
425
|
+
// A contenteditable's selection is a live DOM Range the browser keeps for us; there is
|
|
426
|
+
// no pair of offsets to restore, and re-focusing the element puts the caret back.
|
|
427
|
+
range: null
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async function clipboardHasText() {
|
|
431
|
+
if (typeof navigator === "undefined" || !navigator.clipboard?.readText) return null;
|
|
432
|
+
try {
|
|
433
|
+
const status = await navigator.permissions?.query({ name: "clipboard-read" });
|
|
434
|
+
if (status?.state !== "granted") return null;
|
|
435
|
+
return (await navigator.clipboard.readText()).length > 0;
|
|
436
|
+
} catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function clipboardEntries(target, options = {}) {
|
|
441
|
+
const { copy = true, cut = true, paste = true, labels = {}, icons = {}, clipboardEmpty = false } = options;
|
|
442
|
+
const entries = [];
|
|
443
|
+
const selected = target.selection.length > 0 && target.copyable;
|
|
444
|
+
const canPaste = typeof navigator !== "undefined" && Boolean(navigator.clipboard?.readText);
|
|
445
|
+
if (copy && selected) {
|
|
446
|
+
entries.push({ id: `${CLIPBOARD_PREFIX}copy`, label: labels.copy ?? "Copy", shortcut: "Mod+C", icon: icons.copy });
|
|
447
|
+
}
|
|
448
|
+
if (cut && selected && target.writable) {
|
|
449
|
+
entries.push({ id: `${CLIPBOARD_PREFIX}cut`, label: labels.cut ?? "Cut", shortcut: "Mod+X", icon: icons.cut });
|
|
450
|
+
}
|
|
451
|
+
if (paste && target.writable && canPaste) {
|
|
452
|
+
entries.push({
|
|
453
|
+
id: `${CLIPBOARD_PREFIX}paste`,
|
|
454
|
+
label: labels.paste ?? "Paste",
|
|
455
|
+
shortcut: "Mod+V",
|
|
456
|
+
icon: icons.paste,
|
|
457
|
+
// Listed and greyed rather than dropped: a row that disappears between two opens
|
|
458
|
+
// is read as the menu being unreliable, and every desktop menu greys this one.
|
|
459
|
+
disabled: clipboardEmpty
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
return entries;
|
|
463
|
+
}
|
|
464
|
+
function clipboardAction(id) {
|
|
465
|
+
if (!id.startsWith(CLIPBOARD_PREFIX)) return null;
|
|
466
|
+
const action = id.slice(CLIPBOARD_PREFIX.length);
|
|
467
|
+
return action === "copy" || action === "cut" || action === "paste" ? action : null;
|
|
468
|
+
}
|
|
469
|
+
function restore(target) {
|
|
470
|
+
const element = target.editable;
|
|
471
|
+
if (!element) return;
|
|
472
|
+
element.focus({ preventScroll: true });
|
|
473
|
+
if (!target.range) return;
|
|
474
|
+
const field = element;
|
|
475
|
+
try {
|
|
476
|
+
field.setSelectionRange(target.range.start, target.range.end);
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function writeFieldValue(field, next) {
|
|
481
|
+
const prototype = field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
482
|
+
const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
|
|
483
|
+
if (setter) setter.call(field, next);
|
|
484
|
+
else field.value = next;
|
|
485
|
+
field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
486
|
+
}
|
|
487
|
+
function replaceSelection(target, text) {
|
|
488
|
+
const element = target.editable;
|
|
489
|
+
if (!element) return;
|
|
490
|
+
try {
|
|
491
|
+
if (document.execCommand("insertText", false, text)) return;
|
|
492
|
+
} catch {
|
|
493
|
+
}
|
|
494
|
+
const field = element;
|
|
495
|
+
if (typeof field.setSelectionRange !== "function" || target.range === null) return;
|
|
496
|
+
const start = field.selectionStart ?? target.range.start;
|
|
497
|
+
const end = field.selectionEnd ?? target.range.end;
|
|
498
|
+
writeFieldValue(field, field.value.slice(0, start) + text + field.value.slice(end));
|
|
499
|
+
const caret = start + text.length;
|
|
500
|
+
try {
|
|
501
|
+
field.setSelectionRange(caret, caret);
|
|
502
|
+
} catch {
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function performClipboardAction(action, target) {
|
|
506
|
+
restore(target);
|
|
507
|
+
if (action === "copy" || action === "cut") {
|
|
508
|
+
if (!target.selection || !target.copyable) return false;
|
|
509
|
+
try {
|
|
510
|
+
await navigator.clipboard.writeText(target.selection);
|
|
511
|
+
} catch {
|
|
512
|
+
try {
|
|
513
|
+
if (!document.execCommand("copy")) return false;
|
|
514
|
+
} catch {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (action === "cut" && target.writable) replaceSelection(target, "");
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
if (!target.writable) return false;
|
|
522
|
+
try {
|
|
523
|
+
const text = await navigator.clipboard.readText();
|
|
524
|
+
if (!text) return false;
|
|
525
|
+
replaceSelection(target, text);
|
|
526
|
+
return true;
|
|
527
|
+
} catch {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export { CLIPBOARD_PREFIX, CONTEXT_MENU_SEARCH_KEYS, clipboardAction, clipboardEntries, clipboardHasText, createContextMenu, inspectClipboardTarget, isAction, performClipboardAction };
|