@enigmax/primitives 0.24.0 → 0.26.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 (59) hide show
  1. package/dist/chunk-4SZ5N7BA.js +121 -0
  2. package/dist/chunk-EYZ366LP.js +189 -0
  3. package/dist/chunk-ISXB5RUH.js +380 -0
  4. package/dist/chunk-JJVZYW5C.js +946 -0
  5. package/dist/chunk-KVLSOQTI.js +66 -0
  6. package/dist/chunk-QIIV2QSV.js +108 -0
  7. package/dist/{chunk-QJJ34E4G.js → chunk-RMNXVWQ4.js} +32 -15
  8. package/dist/chunk-RUEQ3TGP.js +155 -0
  9. package/dist/clipboard-menu-BvXJCSFN.d.ts +93 -0
  10. package/dist/{color-OPV3BJV6.js → color-T63FLJNH.js} +183 -120
  11. package/dist/context-BfjYGPnH.d.ts +54 -0
  12. package/dist/{clipboard-menu-B_ouitfS.d.ts → context-menu-D3FtTn7v.d.ts} +1 -91
  13. package/dist/{index-ZPvlf9vs.d.ts → index-qXOkYQCU.d.ts} +4 -0
  14. package/dist/index.d.ts +266 -2
  15. package/dist/index.js +4 -2
  16. package/dist/menu-MSQSE6U5.js +54 -0
  17. package/dist/menu-PNQRTRNV.js +33 -0
  18. package/dist/next/index.d.ts +7 -3
  19. package/dist/next/index.js +11 -7
  20. package/dist/react/context-menu.d.ts +8 -54
  21. package/dist/react/image.d.ts +109 -0
  22. package/dist/react/image.js +3 -0
  23. package/dist/react/index.d.ts +8 -4
  24. package/dist/react/index.js +11 -7
  25. package/dist/react/input.d.ts +1 -1
  26. package/dist/react/input.js +2 -2
  27. package/dist/react/video.d.ts +120 -0
  28. package/dist/react/video.js +3 -0
  29. package/dist/react-router/index.d.ts +7 -3
  30. package/dist/react-router/index.js +11 -7
  31. package/dist/viewer-73LSE6RR.js +3 -0
  32. package/package.json +15 -2
  33. package/recipes/color/styles.css +20 -0
  34. package/recipes/image/styles.css +182 -0
  35. package/recipes/video/styles.css +154 -0
  36. package/registry.json +477 -3
  37. package/src/core/image-viewer.ts +222 -0
  38. package/src/core/player.ts +303 -0
  39. package/src/index.ts +52 -0
  40. package/src/react/image/icons.tsx +55 -0
  41. package/src/react/image/index.tsx +185 -0
  42. package/src/react/image/menu.tsx +76 -0
  43. package/src/react/image/styles.ts +213 -0
  44. package/src/react/image/types.ts +113 -0
  45. package/src/react/image/viewer.tsx +571 -0
  46. package/src/react/index.ts +3 -0
  47. package/src/react/input/color-styles.ts +20 -0
  48. package/src/react/input/color-swatch.tsx +96 -0
  49. package/src/react/input/color.tsx +84 -23
  50. package/src/react/input/index.tsx +27 -4
  51. package/src/react/input/types.ts +4 -0
  52. package/src/react/video/icons.tsx +135 -0
  53. package/src/react/video/index.tsx +724 -0
  54. package/src/react/video/menu.tsx +66 -0
  55. package/src/react/video/rail.tsx +99 -0
  56. package/src/react/video/styles.ts +161 -0
  57. package/src/react/video/types.ts +135 -0
  58. package/dist/chunk-S7EE57YB.js +0 -9
  59. /package/dist/{chunk-MSOCCQGH.js → chunk-T7I6WJSN.js} +0 -0
@@ -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,108 @@
1
+ // src/core/image-viewer.ts
2
+ var IDENTITY = { scale: 1, x: 0, y: 0 };
3
+ var ZOOM_LIMITS = { min: 1, max: 8 };
4
+ var ZOOM_STEP = 1.35;
5
+ var ZOOM_DOUBLE = 2.5;
6
+ function clampScale(scale, limits = ZOOM_LIMITS) {
7
+ return Math.min(Math.max(scale, limits.min), limits.max);
8
+ }
9
+ function wheelFactor(deltaY, deltaMode = 0) {
10
+ const pixels = deltaMode === 1 ? deltaY * 16 : deltaMode === 2 ? deltaY * 400 : deltaY;
11
+ return Math.exp(-Math.min(Math.max(pixels, -240), 240) / 320);
12
+ }
13
+ function zoomAt(current, factor, point, box, limits = ZOOM_LIMITS) {
14
+ const scale = clampScale(current.scale * factor, limits);
15
+ if (scale === current.scale) return current;
16
+ const dx = point.x - (box.left + box.width / 2);
17
+ const dy = point.y - (box.top + box.height / 2);
18
+ const ratio = scale / current.scale;
19
+ return { scale, x: dx - (dx - current.x) * ratio, y: dy - (dy - current.y) * ratio };
20
+ }
21
+ function clampPan(transform, box, natural) {
22
+ const width = natural.width * transform.scale;
23
+ const height = natural.height * transform.scale;
24
+ const x = Math.max(0, (width - box.width) / 2);
25
+ const y = Math.max(0, (height - box.height) / 2);
26
+ return {
27
+ scale: transform.scale,
28
+ x: Math.min(Math.max(transform.x, -x), x),
29
+ y: Math.min(Math.max(transform.y, -y), y)
30
+ };
31
+ }
32
+ function fittedSize(natural, box) {
33
+ if (!natural.width || !natural.height) return { width: box.width, height: box.height };
34
+ const ratio = Math.min(box.width / natural.width, box.height / natural.height, 1);
35
+ return { width: natural.width * ratio, height: natural.height * ratio };
36
+ }
37
+ function pinchDistance(a, b) {
38
+ return Math.hypot(a.x - b.x, a.y - b.y);
39
+ }
40
+ function nextIndex(index, length, step, options = {}) {
41
+ const { loop = true, skip } = options;
42
+ if (length <= 0) return -1;
43
+ let next = index;
44
+ for (let moved = 0; moved < length; moved += 1) {
45
+ next += step;
46
+ if (next < 0 || next >= length) {
47
+ if (!loop) return -1;
48
+ next = (next % length + length) % length;
49
+ }
50
+ if (!skip?.has(next)) return next;
51
+ }
52
+ return -1;
53
+ }
54
+ function filenameFrom(url, fallback = "image") {
55
+ try {
56
+ const { pathname } = new URL(url, typeof location === "undefined" ? "https://localhost" : location.href);
57
+ const name = pathname.split("/").filter(Boolean).pop();
58
+ return name && name.includes(".") ? decodeURIComponent(name) : fallback;
59
+ } catch {
60
+ return fallback;
61
+ }
62
+ }
63
+ async function downloadFile(url, filename) {
64
+ const name = filename ?? filenameFrom(url);
65
+ try {
66
+ const response = await fetch(url, { mode: "cors", credentials: "omit" });
67
+ if (!response.ok) throw new Error(`the file answered ${response.status}`);
68
+ const blob = await response.blob();
69
+ const href = URL.createObjectURL(blob);
70
+ saveAs(href, name);
71
+ setTimeout(() => URL.revokeObjectURL(href), 1e4);
72
+ } catch {
73
+ saveAs(url, name, true);
74
+ }
75
+ }
76
+ function saveAs(href, name, external = false) {
77
+ if (typeof document === "undefined") return;
78
+ const anchor = document.createElement("a");
79
+ anchor.href = href;
80
+ anchor.download = name;
81
+ anchor.rel = "noopener";
82
+ if (external) anchor.target = "_blank";
83
+ document.body.append(anchor);
84
+ anchor.click();
85
+ anchor.remove();
86
+ }
87
+ var FLIGHT_MS = 300;
88
+ function flightMs(element) {
89
+ if (!element || typeof getComputedStyle === "undefined") return FLIGHT_MS;
90
+ const seconds = Number.parseFloat(getComputedStyle(element).transitionDuration);
91
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : FLIGHT_MS;
92
+ }
93
+ function flightFrom(from, to) {
94
+ if (!from.width || !from.height || !to.width || !to.height) return null;
95
+ return {
96
+ scale: from.width / to.width,
97
+ // Centre to centre: `translate(x, y) scale(s)` scales about the element's own centre
98
+ // first, so the offset that lands it on the thumbnail is the distance between them.
99
+ x: from.left + from.width / 2 - (to.left + to.width / 2),
100
+ y: from.top + from.height / 2 - (to.top + to.height / 2)
101
+ };
102
+ }
103
+ function prefersReducedMotion() {
104
+ if (typeof window === "undefined" || !window.matchMedia) return false;
105
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
106
+ }
107
+
108
+ export { FLIGHT_MS, IDENTITY, ZOOM_DOUBLE, ZOOM_LIMITS, ZOOM_STEP, clampPan, clampScale, downloadFile, filenameFrom, fittedSize, flightFrom, flightMs, nextIndex, pinchDistance, prefersReducedMotion, wheelFactor, zoomAt };
@@ -1,4 +1,4 @@
1
- import { writeValue } from './chunk-S7EE57YB.js';
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-OPV3BJV6.js').then((module) => ({ default: module.ColorExtras })));
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(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
204
- ColorExtras,
204
+ isColor && /* @__PURE__ */ jsx(
205
+ Suspense,
205
206
  {
206
- input: element,
207
- value,
208
- format,
209
- alpha,
210
- swatches,
211
- eyedropper,
212
- placement,
213
- styles,
214
- locked,
215
- labels: colorLabels
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,155 @@
1
+ // src/core/player.ts
2
+ var SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
3
+ var SEEK_STEP = 5;
4
+ var SEEK_JUMP = 10;
5
+ var VOLUME_STEP = 0.05;
6
+ function formatTime(seconds, duration = seconds) {
7
+ if (!Number.isFinite(seconds) || seconds < 0) seconds = 0;
8
+ const total = Math.floor(seconds);
9
+ const hours = Math.floor(total / 3600);
10
+ const minutes = Math.floor(total % 3600 / 60);
11
+ const rest = total % 60;
12
+ const withHours = Number.isFinite(duration) && duration >= 3600;
13
+ const pad = (value) => String(value).padStart(2, "0");
14
+ if (withHours || hours > 0) return `${hours}:${pad(minutes)}:${pad(rest)}`;
15
+ return `${minutes}:${pad(rest)}`;
16
+ }
17
+ function progress(time, duration) {
18
+ if (!Number.isFinite(duration) || duration <= 0) return 0;
19
+ return Math.min(Math.max(time / duration, 0), 1);
20
+ }
21
+ function fractionAt(box, clientX) {
22
+ if (!box.width) return 0;
23
+ return Math.min(Math.max((clientX - box.left) / box.width, 0), 1);
24
+ }
25
+ function bufferedAhead(ranges, time, duration) {
26
+ if (!ranges || !Number.isFinite(duration) || duration <= 0) return 0;
27
+ for (let at = 0; at < ranges.length; at += 1) {
28
+ if (ranges.start(at) <= time && ranges.end(at) >= time) return Math.min(ranges.end(at) / duration, 1);
29
+ }
30
+ return 0;
31
+ }
32
+ function nextSpeed(current, step = 1) {
33
+ const at = SPEEDS.indexOf(current);
34
+ const from = at === -1 ? SPEEDS.indexOf(1) : at;
35
+ return SPEEDS[(from + step + SPEEDS.length) % SPEEDS.length];
36
+ }
37
+ function commandFor(key) {
38
+ if (key === " " || key === "k" || key === "K") return { type: "toggle" };
39
+ if (key === "ArrowRight") return { type: "seek", by: SEEK_STEP };
40
+ if (key === "ArrowLeft") return { type: "seek", by: -SEEK_STEP };
41
+ if (key === "l" || key === "L") return { type: "seek", by: SEEK_JUMP };
42
+ if (key === "j" || key === "J") return { type: "seek", by: -SEEK_JUMP };
43
+ if (key === "ArrowUp") return { type: "volume", by: VOLUME_STEP };
44
+ if (key === "ArrowDown") return { type: "volume", by: -VOLUME_STEP };
45
+ if (key === "m" || key === "M") return { type: "mute" };
46
+ if (key === "f" || key === "F") return { type: "fullscreen" };
47
+ if (key === "c" || key === "C") return { type: "captions" };
48
+ if (key === "p" || key === "P") return { type: "pip" };
49
+ if (key >= "0" && key <= "9") return { type: "seekTo", fraction: Number(key) / 10 };
50
+ return null;
51
+ }
52
+ function isTypingTarget(target) {
53
+ const element = target;
54
+ if (!element || typeof element.tagName !== "string") return false;
55
+ if (element.isContentEditable) return true;
56
+ return ["INPUT", "TEXTAREA", "SELECT"].includes(element.tagName);
57
+ }
58
+ function fullscreenElement() {
59
+ if (typeof document === "undefined") return null;
60
+ const legacy = document;
61
+ return document.fullscreenElement ?? legacy.webkitFullscreenElement ?? null;
62
+ }
63
+ async function toggleFullscreen(container, video) {
64
+ const legacyDocument = document;
65
+ if (fullscreenElement()) {
66
+ await (document.exitFullscreen?.() ?? legacyDocument.webkitExitFullscreen?.());
67
+ return;
68
+ }
69
+ const legacyElement = container;
70
+ if (container.requestFullscreen) return void await container.requestFullscreen();
71
+ if (legacyElement.webkitRequestFullscreen) return void await legacyElement.webkitRequestFullscreen();
72
+ const iosVideo = video;
73
+ iosVideo?.webkitEnterFullscreen?.();
74
+ }
75
+ function supportsPip() {
76
+ return typeof document !== "undefined" && Boolean(document.pictureInPictureEnabled);
77
+ }
78
+ async function togglePip(video) {
79
+ if (document.pictureInPictureElement) return void await document.exitPictureInPicture();
80
+ await video.requestPictureInPicture();
81
+ }
82
+ function captionTracks(list) {
83
+ if (!list) return [];
84
+ const found = [];
85
+ for (let at = 0; at < list.length; at += 1) {
86
+ const track = list[at];
87
+ if (!track || track.kind !== "subtitles" && track.kind !== "captions") continue;
88
+ found.push({ index: at, label: track.label || track.language || `Track ${found.length + 1}`, language: track.language });
89
+ }
90
+ return found;
91
+ }
92
+ function activeCaption(list) {
93
+ if (!list) return -1;
94
+ for (let at = 0; at < list.length; at += 1) if (list[at]?.mode === "showing") return at;
95
+ return -1;
96
+ }
97
+ function showCaption(list, index) {
98
+ if (!list) return;
99
+ for (let at = 0; at < list.length; at += 1) {
100
+ const track = list[at];
101
+ if (track) track.mode = at === index ? "showing" : "disabled";
102
+ }
103
+ }
104
+ function supportsRemote(video) {
105
+ const element = video;
106
+ if (!element || element.disableRemotePlayback) return false;
107
+ return Boolean(element.remote?.prompt ?? element.webkitShowPlaybackTargetPicker);
108
+ }
109
+ function watchRemote(video, onChange) {
110
+ const element = video;
111
+ const state = { available: false, connected: false };
112
+ const publish = (next) => {
113
+ Object.assign(state, next);
114
+ onChange({ ...state });
115
+ };
116
+ if (element.remote) {
117
+ const remote = element.remote;
118
+ const onState = () => publish({ connected: remote.state === "connected" });
119
+ let watch = null;
120
+ let cancelled = false;
121
+ remote.watchAvailability((available) => {
122
+ if (!cancelled) publish({ available });
123
+ }).then((id) => {
124
+ if (cancelled) void remote.cancelWatchAvailability(id).catch(() => {
125
+ });
126
+ else watch = id;
127
+ }).catch(() => {
128
+ if (!cancelled) publish({ available: true });
129
+ });
130
+ for (const name of ["connect", "connecting", "disconnect"]) remote.addEventListener(name, onState);
131
+ onState();
132
+ return () => {
133
+ cancelled = true;
134
+ for (const name of ["connect", "connecting", "disconnect"]) remote.removeEventListener(name, onState);
135
+ if (watch !== null) void remote.cancelWatchAvailability(watch).catch(() => {
136
+ });
137
+ };
138
+ }
139
+ const onAvailability = (event) => publish({ available: event.availability === "available" });
140
+ const onWireless = () => publish({ connected: Boolean(element.webkitCurrentPlaybackTargetIsWireless) });
141
+ element.addEventListener("webkitplaybacktargetavailabilitychanged", onAvailability);
142
+ element.addEventListener("webkitcurrentplaybacktargetiswirelesschanged", onWireless);
143
+ onWireless();
144
+ return () => {
145
+ element.removeEventListener("webkitplaybacktargetavailabilitychanged", onAvailability);
146
+ element.removeEventListener("webkitcurrentplaybacktargetiswirelesschanged", onWireless);
147
+ };
148
+ }
149
+ async function promptRemote(video) {
150
+ const element = video;
151
+ if (element.remote?.prompt) return void await element.remote.prompt();
152
+ element.webkitShowPlaybackTargetPicker?.();
153
+ }
154
+
155
+ export { SEEK_JUMP, SEEK_STEP, SPEEDS, VOLUME_STEP, activeCaption, bufferedAhead, captionTracks, commandFor, formatTime, fractionAt, fullscreenElement, isTypingTarget, nextSpeed, progress, promptRemote, showCaption, supportsPip, supportsRemote, toggleFullscreen, togglePip, watchRemote };
@@ -0,0 +1,93 @@
1
+ import { b as ContextMenuEntry } from './context-menu-D3FtTn7v.js';
2
+
3
+ /**
4
+ * The rows every context menu on a desktop already has: Copy, Cut and Paste.
5
+ *
6
+ * A menu opened over a selection or over a field is expected to offer them - the browser's own
7
+ * does, and replacing that menu with a custom one takes them away without saying so. Which is
8
+ * why they are ON by default here and turned off with a prop, rather than the other way round:
9
+ * the defect is silent, so the default has to be the safe one.
10
+ *
11
+ * WHAT DECIDES THEY APPEAR is what was right-clicked, and it is read at the moment the menu
12
+ * opens, not from React state - by then the selection is settled (a `contextmenu` event fires
13
+ * after the press has adjusted it) and the caret is still where the visitor left it.
14
+ *
15
+ * - **Copy**, when there is selected text under the pointer.
16
+ * - **Cut**, when that selection is also in something writable.
17
+ * - **Paste**, in anything writable - and disabled when the clipboard is known to be empty.
18
+ *
19
+ * Not a React module: what a selection is, whether a field takes writes, and how text is put
20
+ * back into one are DOM questions, so a menu drawn by anything else gets the same rows.
21
+ */
22
+
23
+ /** The ids these rows carry. Namespaced, so they can never collide with a caller's own. */
24
+ declare const CLIPBOARD_PREFIX = "enigma:clipboard:";
25
+ type ClipboardAction = "copy" | "cut" | "paste";
26
+ /** What the menu was opened over, as far as the clipboard is concerned. */
27
+ interface ClipboardTarget {
28
+ /** The field or contenteditable under the pointer, or null when it is neither. */
29
+ editable: HTMLElement | null;
30
+ /** Whether that element takes writes: not disabled, not read-only. */
31
+ writable: boolean;
32
+ /** The selected text, from the field's own selection or the document's. */
33
+ selection: string;
34
+ /**
35
+ * Whether that text may be put on the clipboard.
36
+ *
37
+ * False for a password field. The clipboard is shared with every other application on the
38
+ * machine and is not cleared, so a menu that copies a password out of a masked field
39
+ * leaks it somewhere the visitor cannot see - and the browser's own menu refuses too.
40
+ */
41
+ copyable: boolean;
42
+ /** Where the selection was, so it can be put back after the menu has taken focus. */
43
+ range: {
44
+ start: number;
45
+ end: number;
46
+ } | null;
47
+ }
48
+ interface ClipboardMenuLabels {
49
+ copy?: string;
50
+ cut?: string;
51
+ paste?: string;
52
+ }
53
+ interface ClipboardMenuOptions {
54
+ copy?: boolean;
55
+ cut?: boolean;
56
+ paste?: boolean;
57
+ labels?: ClipboardMenuLabels;
58
+ /** Whatever the renderer draws icons with. `unknown`, because a core cannot know. */
59
+ icons?: {
60
+ copy?: unknown;
61
+ cut?: unknown;
62
+ paste?: unknown;
63
+ };
64
+ /** The clipboard is known to be empty, so Paste is listed and greyed rather than missing. */
65
+ clipboardEmpty?: boolean;
66
+ }
67
+ /** What was right-clicked, read the moment the menu opens. */
68
+ declare function inspectClipboardTarget(node: EventTarget | null): ClipboardTarget;
69
+ /**
70
+ * Whether the clipboard has text in it, or null when that cannot be known.
71
+ *
72
+ * Null is the common answer and not a failure: reading the clipboard needs permission, and
73
+ * asking for it puts a browser prompt on screen just to decide whether to grey out a row -
74
+ * which is a worse trade than showing an enabled Paste that turns out to do nothing. So the
75
+ * permission is only READ, never requested, and the clipboard is only opened where it has
76
+ * already been granted.
77
+ */
78
+ declare function clipboardHasText(): Promise<boolean | null>;
79
+ /** The rows for this target, in the order every desktop menu puts them. */
80
+ declare function clipboardEntries(target: ClipboardTarget, options?: ClipboardMenuOptions): ContextMenuEntry[];
81
+ /** Which clipboard row an id belongs to, or null for anything that is not one of ours. */
82
+ declare function clipboardAction(id: string): ClipboardAction | null;
83
+ /**
84
+ * Do what the row says.
85
+ *
86
+ * Called straight from the press that chose it, so the browser still counts it as a user
87
+ * gesture - which is what the clipboard API requires and what makes a paste possible at all.
88
+ * Returns whether the action happened: a refused permission and an empty clipboard are both
89
+ * "no", and neither is worth an exception the caller has to catch.
90
+ */
91
+ declare function performClipboardAction(action: ClipboardAction, target: ClipboardTarget): Promise<boolean>;
92
+
93
+ export { CLIPBOARD_PREFIX as C, type ClipboardAction as a, type ClipboardMenuLabels as b, type ClipboardMenuOptions as c, type ClipboardTarget as d, clipboardAction as e, clipboardEntries as f, clipboardHasText as g, inspectClipboardTarget as i, performClipboardAction as p };