@enigmax/primitives 0.22.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 (79) hide show
  1. package/dist/{chunk-QQFNAKMY.js → chunk-45UHLZYT.js} +1 -1
  2. package/dist/chunk-4VUHQFAT.js +130 -0
  3. package/dist/{chunk-D5A2ZMAG.js → chunk-4ZPMP47J.js} +6 -1
  4. package/dist/chunk-DZMHY3SU.js +777 -0
  5. package/dist/{chunk-FWVWX67R.js → chunk-FGBIZDV2.js} +3 -3
  6. package/dist/chunk-N6PDHMAX.js +213 -0
  7. package/dist/chunk-NONGREXC.js +248 -0
  8. package/dist/{chunk-R4ZAEE7V.js → chunk-QJJ34E4G.js} +34 -10
  9. package/dist/chunk-S2FPN5ID.js +115 -0
  10. package/dist/chunk-S7EE57YB.js +9 -0
  11. package/dist/chunk-VKL3DEIQ.js +804 -0
  12. package/dist/chunk-WSQC3PCC.js +466 -0
  13. package/dist/chunk-Z3VDE7OA.js +532 -0
  14. package/dist/clipboard-menu-B_ouitfS.d.ts +264 -0
  15. package/dist/color-D_rZ83Oc.d.ts +152 -0
  16. package/dist/color-OPV3BJV6.js +452 -0
  17. package/dist/{index-BHpOZncw.d.ts → index-ZPvlf9vs.d.ts} +53 -5
  18. package/dist/index.d.ts +6 -2
  19. package/dist/index.js +9 -4
  20. package/dist/keys-D2zJs1uB.d.ts +100 -0
  21. package/dist/next/index.d.ts +10 -3
  22. package/dist/next/index.js +22 -13
  23. package/dist/react/context-menu.d.ts +219 -0
  24. package/dist/react/context-menu.js +6 -0
  25. package/dist/react/index.d.ts +12 -5
  26. package/dist/react/index.js +21 -12
  27. package/dist/react/input.d.ts +3 -3
  28. package/dist/react/input.js +2 -1
  29. package/dist/react/palette.d.ts +1 -1
  30. package/dist/react/palette.js +3 -3
  31. package/dist/react/search.d.ts +1 -1
  32. package/dist/react/search.js +2 -2
  33. package/dist/react/select.d.ts +217 -0
  34. package/dist/react/select.js +7 -0
  35. package/dist/react/selection.d.ts +104 -0
  36. package/dist/react/selection.js +4 -0
  37. package/dist/react-router/index.d.ts +10 -3
  38. package/dist/react-router/index.js +22 -13
  39. package/dist/search/index.d.ts +2 -2
  40. package/dist/search/index.js +1 -1
  41. package/dist/{search-BD9-5O5U.d.ts → search-DYgqRp37.d.ts} +13 -1
  42. package/dist/{search-UQEXAPQB.js → search-PBZORZ7P.js} +1 -1
  43. package/dist/select-ClSy-J1f.d.ts +100 -0
  44. package/dist/selection-B_pmzHpy.d.ts +150 -0
  45. package/package.json +23 -2
  46. package/recipes/color/styles.css +143 -0
  47. package/recipes/context-menu/styles.css +185 -0
  48. package/recipes/input/styles.css +9 -0
  49. package/recipes/palette/styles.css +3 -0
  50. package/recipes/search/tailwind.tsx +3 -2
  51. package/recipes/select/styles.css +230 -0
  52. package/recipes/toast/styles.css +1 -1
  53. package/registry.json +432 -5
  54. package/src/core/clipboard-menu.ts +270 -0
  55. package/src/core/color.ts +248 -0
  56. package/src/core/context-menu.ts +694 -0
  57. package/src/core/keys.ts +264 -0
  58. package/src/core/search.ts +19 -0
  59. package/src/core/select.ts +404 -0
  60. package/src/core/selection.ts +648 -0
  61. package/src/index.ts +92 -1
  62. package/src/react/context-menu/context.ts +64 -0
  63. package/src/react/context-menu/index.tsx +108 -0
  64. package/src/react/context-menu/root.tsx +970 -0
  65. package/src/react/context-menu/styles.ts +194 -0
  66. package/src/react/index.ts +100 -1
  67. package/src/react/input/color-styles.ts +156 -0
  68. package/src/react/input/color.tsx +466 -0
  69. package/src/react/input/index.tsx +54 -6
  70. package/src/react/input/types.ts +59 -3
  71. package/src/react/palette/root.tsx +2 -2
  72. package/src/react/select/context.ts +56 -0
  73. package/src/react/select/index.tsx +96 -0
  74. package/src/react/select/root.tsx +839 -0
  75. package/src/react/select/styles.ts +238 -0
  76. package/src/react/selection/index.tsx +115 -0
  77. package/src/react/selection/use-selection.ts +260 -0
  78. package/dist/password-C8lG4Zm9.d.ts +0 -71
  79. /package/dist/{chunk-U3V4EHOB.js → chunk-MSOCCQGH.js} +0 -0
@@ -0,0 +1,532 @@
1
+ import { typeaheadStep } from './chunk-4VUHQFAT.js';
2
+ import { createSearch } from './chunk-4ZPMP47J.js';
3
+
4
+ // src/core/context-menu.ts
5
+ function isAction(entry) {
6
+ return (entry.type ?? "item") === "item";
7
+ }
8
+ var CONTEXT_MENU_SEARCH_KEYS = ["label", "description", "group", "keywords"];
9
+ var SEARCHABLE_FROM = 8;
10
+ var PAGE = 5;
11
+ var DEFAULT_CACHE_MS = 5 * 60 * 1e3;
12
+ function fold(value) {
13
+ return value.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
14
+ }
15
+ function createContextMenu(options = {}) {
16
+ let opts = { ...options };
17
+ let root = opts.items ?? [];
18
+ let open = false;
19
+ let point = null;
20
+ let levels = [];
21
+ let typed = "";
22
+ let typedAt = 0;
23
+ let destroyed = false;
24
+ const listeners = /* @__PURE__ */ new Set();
25
+ const cache = /* @__PURE__ */ new Map();
26
+ const wantsFocus = /* @__PURE__ */ new Set();
27
+ const engines = /* @__PURE__ */ new Map();
28
+ function cacheMs() {
29
+ return opts.cacheMs ?? DEFAULT_CACHE_MS;
30
+ }
31
+ function keyOf(path) {
32
+ return path.join("\0");
33
+ }
34
+ function searchableFor(item, entries) {
35
+ const asked = item?.searchable ?? "auto";
36
+ if (asked !== "auto") return asked;
37
+ return entries.filter(isAction).length >= SEARCHABLE_FROM;
38
+ }
39
+ function filtered(level) {
40
+ if (!level.searchable || !level.query.trim()) return [...level.entries];
41
+ const actions = level.entries.filter(isAction);
42
+ const matched = new Set(engineFor(level, actions).searchNow(level.query).map((match) => match.item));
43
+ return trimFurniture(level.entries.filter((entry) => !isAction(entry) || matched.has(entry)));
44
+ }
45
+ function engineFor(level, actions) {
46
+ const id = keyOf(level.path);
47
+ const held = engines.get(id);
48
+ if (held && held.items.length === actions.length && held.items.every((item, index) => item === actions[index])) return held.engine;
49
+ held?.engine.destroy();
50
+ const engine = createSearch({
51
+ items: actions,
52
+ keys: opts.searchKeys ?? CONTEXT_MENU_SEARCH_KEYS,
53
+ fuse: opts.fuse,
54
+ fuseOptions: opts.fuseOptions,
55
+ matcher: opts.matcher,
56
+ // A menu filters as you type: the rows are in memory, and a delay is felt as the
57
+ // panel lagging behind the field.
58
+ debounce: 0,
59
+ empty: "all"
60
+ });
61
+ engines.set(id, { items: actions, engine });
62
+ return engine;
63
+ }
64
+ function trimFurniture(entries) {
65
+ const result = [];
66
+ let separator = null;
67
+ let caption = null;
68
+ for (const entry of entries) {
69
+ if (!isAction(entry)) {
70
+ if (entry.type === "separator") separator = entry;
71
+ else caption = entry;
72
+ continue;
73
+ }
74
+ if (separator && result.length > 0) result.push(separator);
75
+ if (caption) result.push(caption);
76
+ separator = null;
77
+ caption = null;
78
+ result.push(entry);
79
+ }
80
+ return result;
81
+ }
82
+ function wrap(index, length) {
83
+ return (index % length + length) % length;
84
+ }
85
+ function enabledIndex(level, from, step) {
86
+ const length = level.visible.length;
87
+ if (length === 0) return -1;
88
+ for (let attempt = 0; attempt < length; attempt++) {
89
+ const index = wrap(from + step * attempt, length);
90
+ const entry = level.visible[index];
91
+ if (entry && isAction(entry) && !entry.disabled) return index;
92
+ }
93
+ return -1;
94
+ }
95
+ function makeLevel(path, item, entries, loading = false, error = null) {
96
+ const level = {
97
+ path,
98
+ title: (item ? item.title : opts.title) ?? null,
99
+ entries: [...entries],
100
+ visible: [],
101
+ active: -1,
102
+ query: "",
103
+ searchable: searchableFor(item, entries),
104
+ loading,
105
+ error
106
+ };
107
+ level.visible = filtered(level);
108
+ return level;
109
+ }
110
+ function snapshot() {
111
+ return {
112
+ open,
113
+ point,
114
+ // Copied one level deep: a subscriber that keeps a snapshot must not see the next
115
+ // state mutate the one it is holding.
116
+ levels: levels.map((level) => ({ ...level, path: [...level.path], entries: [...level.entries], visible: [...level.visible] }))
117
+ };
118
+ }
119
+ function emit() {
120
+ const state = snapshot();
121
+ opts.onChange?.(state);
122
+ for (const listener of listeners) listener(state);
123
+ }
124
+ function deepest() {
125
+ return levels[levels.length - 1];
126
+ }
127
+ function actionAt(level, index) {
128
+ const entry = level?.visible[index];
129
+ return entry && isAction(entry) ? entry : null;
130
+ }
131
+ function hasSubmenu(item) {
132
+ return Boolean(item.loadItems || item.items && item.items.some(isAction));
133
+ }
134
+ function hasActions(entries) {
135
+ return entries.some(isAction);
136
+ }
137
+ function pushSubmenu(parent, index, focus) {
138
+ const level = levels[parent];
139
+ const item = actionAt(level, index);
140
+ if (!item || item.disabled || !hasSubmenu(item)) return;
141
+ const path = [...level.path, item.id];
142
+ const id = keyOf(path);
143
+ const shown = levels[parent + 1];
144
+ if (shown && keyOf(shown.path) === id) {
145
+ level.active = index;
146
+ if (focus) {
147
+ wantsFocus.add(id);
148
+ if (shown.active < 0) focusFirst(parent + 1);
149
+ }
150
+ emit();
151
+ return;
152
+ }
153
+ levels = levels.slice(0, parent + 1);
154
+ level.active = index;
155
+ if (focus) wantsFocus.add(id);
156
+ else wantsFocus.delete(id);
157
+ if (item.items) {
158
+ levels.push(makeLevel(path, item, item.items));
159
+ if (focus) focusFirst(levels.length - 1);
160
+ emit();
161
+ return;
162
+ }
163
+ const entry = cache.get(id);
164
+ const fresh = entry?.entries && (cacheMs() <= 0 ? false : Date.now() - entry.at < cacheMs());
165
+ if (fresh && entry?.entries) {
166
+ levels.push(makeLevel(path, item, entry.entries));
167
+ if (focus) focusFirst(levels.length - 1);
168
+ emit();
169
+ return;
170
+ }
171
+ levels.push(makeLevel(path, item, [], true));
172
+ emit();
173
+ const request = entry?.pending ?? Promise.resolve().then(() => item.loadItems());
174
+ cache.set(id, { at: entry?.at ?? Date.now(), entries: entry?.entries, pending: request });
175
+ request.then(
176
+ (entries) => {
177
+ cache.set(id, { at: Date.now(), entries });
178
+ settle(id, path, item, entries, null);
179
+ },
180
+ (reason) => {
181
+ cache.delete(id);
182
+ settle(id, path, item, [], reason instanceof Error ? reason : new Error(String(reason)));
183
+ }
184
+ );
185
+ }
186
+ function settle(id, path, item, entries, error) {
187
+ if (destroyed) return;
188
+ const index = levels.findIndex((level) => keyOf(level.path) === id);
189
+ if (index === -1) return;
190
+ levels = levels.slice(0, index);
191
+ levels.push(makeLevel(path, item, entries, false, error));
192
+ if (wantsFocus.has(id)) focusFirst(index);
193
+ emit();
194
+ }
195
+ function focusFirst(index) {
196
+ const level = levels[index];
197
+ if (!level) return;
198
+ level.active = enabledIndex(level, 0, 1);
199
+ }
200
+ function refilter(level) {
201
+ const current = level.active >= 0 ? level.visible[level.active] : void 0;
202
+ level.visible = filtered(level);
203
+ const kept = current ? level.visible.indexOf(current) : -1;
204
+ const entry = kept >= 0 ? level.visible[kept] : void 0;
205
+ level.active = entry && isAction(entry) && !entry.disabled ? kept : enabledIndex(level, 0, 1);
206
+ }
207
+ return {
208
+ get state() {
209
+ return snapshot();
210
+ },
211
+ open(next) {
212
+ if (destroyed) return false;
213
+ if (!hasActions(root)) return false;
214
+ const wasOpen = open;
215
+ open = true;
216
+ point = { x: next.x, y: next.y };
217
+ levels = [makeLevel([], null, root)];
218
+ typed = "";
219
+ wantsFocus.clear();
220
+ if (!wasOpen) opts.onOpenChange?.(true);
221
+ emit();
222
+ return true;
223
+ },
224
+ close() {
225
+ if (destroyed || !open) return;
226
+ open = false;
227
+ point = null;
228
+ levels = [];
229
+ typed = "";
230
+ wantsFocus.clear();
231
+ opts.onOpenChange?.(false);
232
+ emit();
233
+ },
234
+ move(key) {
235
+ if (destroyed) return;
236
+ const level = deepest();
237
+ if (!level || level.visible.length === 0) return;
238
+ const from = level.active < 0 ? -1 : level.active;
239
+ switch (key) {
240
+ // Wrapping, because a menu is a short list and the row after the last one is
241
+ // the first: clamping leaves the arrow key doing nothing, which reads as a
242
+ // frozen panel.
243
+ case "ArrowDown":
244
+ level.active = enabledIndex(level, from + 1, 1);
245
+ break;
246
+ // From the LAST row when nothing is highlighted, which is how a menu opened at
247
+ // the pointer starts: `from - 1` there is the row before the last one.
248
+ case "ArrowUp":
249
+ level.active = enabledIndex(level, from < 0 ? level.visible.length - 1 : from - 1 + level.visible.length, -1);
250
+ break;
251
+ case "Home":
252
+ level.active = enabledIndex(level, 0, 1);
253
+ break;
254
+ case "End":
255
+ level.active = enabledIndex(level, level.visible.length - 1, -1);
256
+ break;
257
+ case "PageDown":
258
+ level.active = enabledIndex(level, Math.min(level.visible.length - 1, from + PAGE), 1);
259
+ break;
260
+ case "PageUp":
261
+ level.active = enabledIndex(level, Math.max(0, from - PAGE), -1);
262
+ break;
263
+ }
264
+ emit();
265
+ },
266
+ setActive(levelIndex, index) {
267
+ const level = levels[levelIndex];
268
+ if (destroyed || !level || level.active === index) return;
269
+ if (index < 0 || index >= level.visible.length) return;
270
+ const item = actionAt(level, index);
271
+ if (!item || item.disabled) return;
272
+ level.active = index;
273
+ emit();
274
+ },
275
+ setQuery(levelIndex, query) {
276
+ const level = levels[levelIndex];
277
+ if (destroyed || !level) return;
278
+ level.query = query;
279
+ levels = levels.slice(0, levelIndex + 1);
280
+ refilter(level);
281
+ emit();
282
+ },
283
+ openSubmenu(levelIndex, index, focus = false) {
284
+ if (destroyed) return;
285
+ pushSubmenu(levelIndex, index, focus);
286
+ },
287
+ closeBelow(levelIndex) {
288
+ if (destroyed || levels.length <= levelIndex + 1) return;
289
+ levels = levels.slice(0, levelIndex + 1);
290
+ emit();
291
+ },
292
+ select(levelIndex, index) {
293
+ if (destroyed) return;
294
+ const level = levels[levelIndex];
295
+ const item = actionAt(level, index);
296
+ if (!item || item.disabled) return;
297
+ if (hasSubmenu(item)) {
298
+ pushSubmenu(levelIndex, index, true);
299
+ return;
300
+ }
301
+ const path = [...level?.path ?? [], item.id];
302
+ open = false;
303
+ point = null;
304
+ levels = [];
305
+ typed = "";
306
+ wantsFocus.clear();
307
+ opts.onOpenChange?.(false);
308
+ emit();
309
+ opts.onSelect?.(item, path);
310
+ },
311
+ selectActive() {
312
+ const index = levels.length - 1;
313
+ const level = levels[index];
314
+ if (level && level.active >= 0) this.select(index, level.active);
315
+ },
316
+ enterSubmenu() {
317
+ const index = levels.length - 1;
318
+ const level = levels[index];
319
+ if (!level || level.active < 0) return;
320
+ const item = actionAt(level, level.active);
321
+ if (item && hasSubmenu(item)) pushSubmenu(index, level.active, true);
322
+ },
323
+ leaveSubmenu() {
324
+ if (destroyed || levels.length <= 1) return;
325
+ levels = levels.slice(0, levels.length - 1);
326
+ emit();
327
+ },
328
+ typeahead(character) {
329
+ if (destroyed || character.length !== 1) return;
330
+ const level = deepest();
331
+ if (!level || level.visible.length === 0) return;
332
+ const step = typeaheadStep({ typed, at: typedAt }, character);
333
+ typed = step.typed;
334
+ typedAt = step.at;
335
+ const needle = fold(step.needle);
336
+ const from = step.cycle && level.active >= 0 ? 1 : 0;
337
+ for (let step2 = from; step2 < level.visible.length + from; step2++) {
338
+ const index = wrap(Math.max(level.active, 0) + step2, level.visible.length);
339
+ const entry = level.visible[index];
340
+ if (!entry || !isAction(entry) || entry.disabled) continue;
341
+ if (fold(entry.label).startsWith(needle)) {
342
+ level.active = index;
343
+ emit();
344
+ return;
345
+ }
346
+ }
347
+ },
348
+ invalidate(path) {
349
+ if (!path) {
350
+ cache.clear();
351
+ return;
352
+ }
353
+ cache.delete(keyOf(path));
354
+ },
355
+ update(next) {
356
+ if (destroyed) return;
357
+ const hadItems = next.items !== void 0;
358
+ opts = { ...opts, ...next };
359
+ if (hadItems) {
360
+ root = next.items ?? [];
361
+ const level = levels[0];
362
+ if (level) {
363
+ level.entries = [...root];
364
+ level.title = opts.title ?? null;
365
+ level.searchable = searchableFor(null, root);
366
+ refilter(level);
367
+ emit();
368
+ }
369
+ }
370
+ },
371
+ subscribe(listener) {
372
+ listeners.add(listener);
373
+ return () => {
374
+ listeners.delete(listener);
375
+ };
376
+ },
377
+ destroy() {
378
+ destroyed = true;
379
+ listeners.clear();
380
+ cache.clear();
381
+ wantsFocus.clear();
382
+ for (const held of engines.values()) held.engine.destroy();
383
+ engines.clear();
384
+ }
385
+ };
386
+ }
387
+
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 };