@enigmax/primitives 0.26.0 → 0.28.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 (58) hide show
  1. package/dist/{chunk-VKL3DEIQ.js → chunk-6WIGVIQF.js} +58 -37
  2. package/dist/{chunk-ISXB5RUH.js → chunk-CB7K6VW4.js} +10 -3
  3. package/dist/{chunk-QIIV2QSV.js → chunk-HUUINHPR.js} +28 -1
  4. package/dist/{chunk-4SZ5N7BA.js → chunk-IQMHDRJF.js} +2 -2
  5. package/dist/{chunk-RUEQ3TGP.js → chunk-JLZDS6CV.js} +23 -1
  6. package/dist/{chunk-JJVZYW5C.js → chunk-JPBVKXOL.js} +10 -3
  7. package/dist/{chunk-DZMHY3SU.js → chunk-RRPWJAIJ.js} +41 -17
  8. package/dist/chunk-RXEO4WPI.js +111 -0
  9. package/dist/{chunk-RMNXVWQ4.js → chunk-YHH5U2NW.js} +1 -1
  10. package/dist/{chunk-Z3VDE7OA.js → chunk-YQV5XZIA.js} +2 -2
  11. package/dist/{clipboard-menu-BvXJCSFN.d.ts → clipboard-menu-eemzEBqB.d.ts} +1 -1
  12. package/dist/{color-T63FLJNH.js → color-WBNVPFT5.js} +157 -158
  13. package/dist/{context-BfjYGPnH.d.ts → context-DR1dpO8b.d.ts} +1 -1
  14. package/dist/{context-menu-D3FtTn7v.d.ts → context-menu-qqpGBkny.d.ts} +6 -0
  15. package/dist/index.d.ts +29 -3
  16. package/dist/index.js +3 -3
  17. package/dist/{menu-PNQRTRNV.js → menu-5JZMGNOT.js} +3 -2
  18. package/dist/{menu-MSQSE6U5.js → menu-O3G4LM5O.js} +9 -4
  19. package/dist/next/index.d.ts +3 -3
  20. package/dist/next/index.js +8 -7
  21. package/dist/react/context-menu.d.ts +13 -7
  22. package/dist/react/context-menu.js +3 -2
  23. package/dist/react/image.d.ts +5 -2
  24. package/dist/react/image.js +1 -1
  25. package/dist/react/index.d.ts +3 -3
  26. package/dist/react/index.js +8 -7
  27. package/dist/react/input.js +1 -1
  28. package/dist/react/select.d.ts +6 -1
  29. package/dist/react/select.js +2 -1
  30. package/dist/react/video.d.ts +9 -3
  31. package/dist/react/video.js +2 -2
  32. package/dist/react-router/index.d.ts +3 -3
  33. package/dist/react-router/index.js +8 -7
  34. package/dist/viewer-C323RYNZ.js +3 -0
  35. package/package.json +1 -1
  36. package/recipes/color/styles.css +12 -7
  37. package/recipes/context-menu/styles.css +15 -1
  38. package/recipes/select/styles.css +24 -7
  39. package/registry.json +119 -10
  40. package/src/core/context-menu.ts +22 -3
  41. package/src/core/floating.ts +71 -0
  42. package/src/core/image-viewer.ts +47 -0
  43. package/src/core/player.ts +42 -0
  44. package/src/index.ts +3 -0
  45. package/src/react/context-menu/root.tsx +57 -20
  46. package/src/react/context-menu/styles.ts +15 -1
  47. package/src/react/floating.ts +185 -0
  48. package/src/react/image/icons.tsx +4 -0
  49. package/src/react/image/menu.tsx +5 -1
  50. package/src/react/image/types.ts +3 -0
  51. package/src/react/input/color-styles.ts +12 -7
  52. package/src/react/input/color.tsx +30 -27
  53. package/src/react/select/context.ts +5 -0
  54. package/src/react/select/root.tsx +33 -21
  55. package/src/react/select/styles.ts +24 -7
  56. package/src/react/video/index.tsx +8 -1
  57. package/src/react/video/types.ts +7 -1
  58. package/dist/viewer-73LSE6RR.js +0 -3
@@ -1,8 +1,10 @@
1
+ import { tabOut, useFloating } from './chunk-RXEO4WPI.js';
1
2
  import { createSelect } from './chunk-NONGREXC.js';
2
3
  import { Slot } from './chunk-HS3X3XCW.js';
3
4
  import { groupRows } from './chunk-DTWZDONY.js';
4
5
  import { shortenQuery } from './chunk-4ZPMP47J.js';
5
- import { createContext, useContext, useLayoutEffect, useId, useMemo, useRef, useState, useEffect, useCallback } from 'react';
6
+ import { createPortal } from 'react-dom';
7
+ import { createContext, useContext, useLayoutEffect, useId, useMemo, useRef, useCallback, useState, useEffect } from 'react';
6
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
9
 
8
10
  // src/react/select/styles.ts
@@ -21,12 +23,17 @@ var SELECT_STYLES = `
21
23
  --enigma-select-panel-border: #333333;
22
24
  --enigma-select-panel-radius: 0.625rem;
23
25
  --enigma-select-panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
24
- --enigma-select-panel-width: 100%;
25
26
  --enigma-select-list-height: 15rem;
26
27
 
27
28
  --enigma-select-active-bg: #2a2a2a;
28
29
  --enigma-select-accent: #fbbf24;
29
30
  --enigma-select-tag-bg: #2a2a2a;
31
+
32
+ /* The layer every portaled popup shares: above the lightbox (9999), because a popup is
33
+ opened FROM whatever is on top. The select, the colour picker and the context menu
34
+ each declare it with the same value, so overriding it once moves all three. Here and
35
+ not in the light-scheme block, or half the readers get an invalid z-index. */
36
+ --enigma-floating-z: 10000;
30
37
  }
31
38
 
32
39
  @media (prefers-color-scheme: light) {
@@ -131,11 +138,24 @@ var SELECT_STYLES = `
131
138
  [data-enigma-select-trigger][data-clearable]:focus [data-enigma-select-caret] { opacity: 0; }
132
139
  [data-enigma-select-clear]:hover { color: var(--enigma-select-text); background: var(--enigma-select-active-bg); }
133
140
 
141
+ /* Portaled to <body> and placed in viewport coordinates against the trigger (see
142
+ react/floating.ts), so no ancestor's overflow can clip it and no dialog can stack over it.
143
+ left, top, max-height and data-side are written by the placement: below the trigger,
144
+ flipped above when there is more room there, and capped to the room it has. The column
145
+ is what makes the cap shrink the LIST, which scrolls, instead of cutting off the panel. */
134
146
  [data-enigma-select-content] {
135
- position: absolute; z-index: 50;
136
- top: calc(100% + 0.25rem); left: 0;
137
- min-width: var(--enigma-select-panel-width); box-sizing: border-box;
147
+ position: fixed; z-index: var(--enigma-floating-z);
148
+ display: flex; flex-direction: column;
149
+ /* As wide as the trigger, measured - 100% would now be a percentage of the window - and
150
+ wider only for content that cannot shrink. min-content is what keeps the rest at the
151
+ trigger's width: fixed, the panel's room is the whole window, and a long label or a
152
+ long "Nothing matches" would otherwise widen it to max-width. --enigma-select-panel-width
153
+ overrides the trigger's width. */
154
+ width: min-content;
155
+ min-width: var(--enigma-select-panel-width, var(--enigma-anchor-width, auto)); box-sizing: border-box;
138
156
  padding: 0.25rem;
157
+ color: var(--enigma-select-text);
158
+ font-size: var(--enigma-select-font-size);
139
159
  background: var(--enigma-select-panel-bg);
140
160
  border: 1px solid var(--enigma-select-panel-border);
141
161
  border-radius: var(--enigma-select-panel-radius);
@@ -146,9 +166,6 @@ var SELECT_STYLES = `
146
166
  trigger wider than this, which is what it is for. */
147
167
  max-width: min(28rem, calc(100vw - 2rem));
148
168
  }
149
- /* Opening upwards is not a variant, it is the same panel measured against the window: near
150
- the bottom of the screen the list would otherwise be unreachable. */
151
- [data-enigma-select-content][data-side="top"] { top: auto; bottom: calc(100% + 0.25rem); }
152
169
  [data-enigma-select-content][data-state="closed"] { animation: enigma-select-out 120ms ease-in forwards; pointer-events: none; }
153
170
 
154
171
  @keyframes enigma-select-in {
@@ -180,6 +197,8 @@ var SELECT_STYLES = `
180
197
 
181
198
  [data-enigma-select-list] {
182
199
  max-height: var(--enigma-select-list-height);
200
+ /* Allowed below its content, so the panel's placement cap shrinks it and it scrolls. */
201
+ min-height: 0; flex: 0 1 auto;
183
202
  margin-top: 0.25rem;
184
203
  overflow-y: auto; overscroll-behavior: contain;
185
204
  }
@@ -295,6 +314,8 @@ function SelectRoot(props) {
295
314
  const triggerRef = useRef(null);
296
315
  const fieldRef = useRef(null);
297
316
  const rootRef = useRef(null);
317
+ const contentRef = useRef(null);
318
+ const owns = useCallback((node) => Boolean(node && (rootRef.current?.contains(node) || contentRef.current?.contains(node))), []);
298
319
  const latest = useRef(props);
299
320
  latest.current = props;
300
321
  const instance = useMemo(() => createSelect({
@@ -367,10 +388,10 @@ function SelectRoot(props) {
367
388
  const closedItself = wasOpen.current && !state.open;
368
389
  wasOpen.current = state.open;
369
390
  if (!closedItself || !open) return;
370
- const held = rootRef.current?.contains(document.activeElement);
391
+ const held = owns(document.activeElement);
371
392
  setOpen(false);
372
393
  if (held) triggerRef.current?.focus();
373
- }, [state.open, open, setOpen]);
394
+ }, [state.open, open, setOpen, owns]);
374
395
  const close = useCallback(() => {
375
396
  setOpen(false);
376
397
  triggerRef.current?.focus();
@@ -378,12 +399,12 @@ function SelectRoot(props) {
378
399
  useEffect(() => {
379
400
  if (!open) return;
380
401
  const onPointerDown = (event) => {
381
- if (rootRef.current?.contains(event.target)) return;
402
+ if (owns(event.target)) return;
382
403
  setOpen(false);
383
404
  };
384
405
  document.addEventListener("pointerdown", onPointerDown, true);
385
406
  return () => document.removeEventListener("pointerdown", onPointerDown, true);
386
- }, [open, setOpen]);
407
+ }, [open, setOpen, owns]);
387
408
  const onListKeyDown = useCallback((event) => {
388
409
  const move = MOVE_KEYS[event.key];
389
410
  if (move) {
@@ -413,6 +434,7 @@ function SelectRoot(props) {
413
434
  return;
414
435
  }
415
436
  if (event.key === "Tab" && open) {
437
+ if (contentRef.current?.contains(event.target)) tabOut(event, contentRef.current, triggerRef.current);
416
438
  setOpen(false);
417
439
  return;
418
440
  }
@@ -450,6 +472,7 @@ function SelectRoot(props) {
450
472
  optionId: (index) => `${id}-option-${index}`,
451
473
  triggerRef,
452
474
  fieldRef,
475
+ contentRef,
453
476
  close,
454
477
  onListKeyDown
455
478
  }), [instance, state, setOpen, disabled, clearable, multiple, isSearchable, isEmpty, loading, emptyLabel, loadingLabel, ids, id, close, onListKeyDown]);
@@ -608,9 +631,7 @@ function SelectValue({ placeholder = "Select", tags, maxTags = 3, children, ...p
608
631
  }
609
632
  function SelectContent({ closeDuration = 120, children, ...props }) {
610
633
  const select = useSelectContext("Select.Content");
611
- const ref = useRef(null);
612
634
  const [mounted, setMounted] = useState(select.state.open);
613
- const [side, setSide] = useState("bottom");
614
635
  useEffect(() => {
615
636
  if (select.state.open) {
616
637
  setMounted(true);
@@ -619,29 +640,29 @@ function SelectContent({ closeDuration = 120, children, ...props }) {
619
640
  const timer = setTimeout(() => setMounted(false), closeDuration);
620
641
  return () => clearTimeout(timer);
621
642
  }, [select.state.open, closeDuration]);
622
- useLayoutEffect(() => {
623
- if (!select.state.open || !ref.current) return;
624
- const trigger = select.triggerRef.current?.getBoundingClientRect();
625
- if (!trigger) return;
626
- const height = ref.current.offsetHeight;
627
- const below = window.innerHeight - trigger.bottom;
628
- setSide(below < height && trigger.top > below ? "top" : "bottom");
629
- }, [select.state.open, select.state.visible.length, select.triggerRef]);
630
- if (!mounted) return null;
631
- return /* @__PURE__ */ jsx(
632
- "div",
633
- {
634
- ...props,
635
- ref,
636
- "data-enigma-select-content": "",
637
- "data-state": select.state.open ? "open" : "closed",
638
- "data-side": side,
639
- onKeyDown: (event) => {
640
- props.onKeyDown?.(event);
641
- if (!event.defaultPrevented) select.onListKeyDown(event);
642
- },
643
- children
644
- }
643
+ const container = useFloating({
644
+ open: mounted && select.state.open,
645
+ anchorRef: select.triggerRef,
646
+ panelRef: select.contentRef,
647
+ deps: [select.state.visible.length]
648
+ });
649
+ if (!mounted || !container) return null;
650
+ return createPortal(
651
+ /* @__PURE__ */ jsx(
652
+ "div",
653
+ {
654
+ ...props,
655
+ ref: select.contentRef,
656
+ "data-enigma-select-content": "",
657
+ "data-state": select.state.open ? "open" : "closed",
658
+ onKeyDown: (event) => {
659
+ props.onKeyDown?.(event);
660
+ if (!event.defaultPrevented) select.onListKeyDown(event);
661
+ },
662
+ children
663
+ }
664
+ ),
665
+ container
645
666
  );
646
667
  }
647
668
  function SelectSearch({ placeholder = "Search", onKeyDown, ...props }) {
@@ -1,4 +1,4 @@
1
- import { IDENTITY, prefersReducedMotion, ZOOM_LIMITS, flightFrom, nextIndex, fittedSize, clampPan, zoomAt, pinchDistance, flightMs, ZOOM_STEP, ZOOM_DOUBLE, wheelFactor } from './chunk-QIIV2QSV.js';
1
+ import { IDENTITY, prefersReducedMotion, ZOOM_LIMITS, flightFrom, nextIndex, fittedSize, clampPan, zoomAt, pinchDistance, flightMs, ZOOM_STEP, ZOOM_DOUBLE, wheelFactor } from './chunk-HUUINHPR.js';
2
2
  import { injectImageStyles } from './chunk-EYZ366LP.js';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -35,7 +35,14 @@ function Dots() {
35
35
  function Download() {
36
36
  return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M12 3v12M7 11l5 5 5-5M4 20h16" }) });
37
37
  }
38
- var ImageMenu = lazy(() => import('./menu-MSQSE6U5.js').then((module) => ({ default: module.ImageMenu })));
38
+ function NewTab() {
39
+ return /* @__PURE__ */ jsxs(Glyph, { children: [
40
+ /* @__PURE__ */ jsx("path", { d: "M14 4h6v6" }),
41
+ /* @__PURE__ */ jsx("path", { d: "M20 4 11 13" }),
42
+ /* @__PURE__ */ jsx("path", { d: "M18 14v5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 4 19V7.5A1.5 1.5 0 0 1 5.5 6H10" })
43
+ ] });
44
+ }
45
+ var ImageMenu = lazy(() => import('./menu-O3G4LM5O.js').then((module) => ({ default: module.ImageMenu })));
39
46
  function ImageViewer({
40
47
  items,
41
48
  index,
@@ -377,4 +384,4 @@ function MenuButton({ label, onPress, expanded }) {
377
384
  );
378
385
  }
379
386
 
380
- export { Download, ImageViewer, MenuButton };
387
+ export { Download, ImageViewer, MenuButton, NewTab };
@@ -84,6 +84,33 @@ function saveAs(href, name, external = false) {
84
84
  anchor.click();
85
85
  anchor.remove();
86
86
  }
87
+ function dataUrlToBlob(url) {
88
+ const comma = url.indexOf(",");
89
+ if (!url.startsWith("data:") || comma === -1) throw new Error("not a data URL");
90
+ const head = url.slice("data:".length, comma);
91
+ const body = url.slice(comma + 1);
92
+ const base64 = head.endsWith(";base64");
93
+ const type = (base64 ? head.slice(0, -";base64".length) : head) || "text/plain";
94
+ if (!base64) return new Blob([decodeURIComponent(body)], { type });
95
+ const binary = atob(body);
96
+ const bytes = new Uint8Array(binary.length);
97
+ for (let at = 0; at < binary.length; at += 1) bytes[at] = binary.charCodeAt(at);
98
+ return new Blob([bytes], { type });
99
+ }
100
+ function openInNewTab(url) {
101
+ if (typeof window === "undefined") return;
102
+ if (!url.startsWith("data:")) {
103
+ window.open(url, "_blank", "noopener,noreferrer");
104
+ return;
105
+ }
106
+ try {
107
+ const href = URL.createObjectURL(dataUrlToBlob(url));
108
+ window.open(href, "_blank", "noopener,noreferrer");
109
+ setTimeout(() => URL.revokeObjectURL(href), 6e4);
110
+ } catch {
111
+ window.open(url, "_blank", "noopener,noreferrer");
112
+ }
113
+ }
87
114
  var FLIGHT_MS = 300;
88
115
  function flightMs(element) {
89
116
  if (!element || typeof getComputedStyle === "undefined") return FLIGHT_MS;
@@ -105,4 +132,4 @@ function prefersReducedMotion() {
105
132
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
106
133
  }
107
134
 
108
- export { FLIGHT_MS, IDENTITY, ZOOM_DOUBLE, ZOOM_LIMITS, ZOOM_STEP, clampPan, clampScale, downloadFile, filenameFrom, fittedSize, flightFrom, flightMs, nextIndex, pinchDistance, prefersReducedMotion, wheelFactor, zoomAt };
135
+ export { FLIGHT_MS, IDENTITY, ZOOM_DOUBLE, ZOOM_LIMITS, ZOOM_STEP, clampPan, clampScale, dataUrlToBlob, downloadFile, filenameFrom, fittedSize, flightFrom, flightMs, nextIndex, openInNewTab, pinchDistance, prefersReducedMotion, wheelFactor, zoomAt };
@@ -6,12 +6,12 @@ import { jsx, jsxs } from 'react/jsx-runtime';
6
6
  function toItem(source) {
7
7
  return typeof source === "string" ? { src: source } : source;
8
8
  }
9
- var ImageViewer = lazy(() => import('./viewer-73LSE6RR.js').then((module) => ({ default: module.ImageViewer })));
9
+ var ImageViewer = lazy(() => import('./viewer-C323RYNZ.js').then((module) => ({ default: module.ImageViewer })));
10
10
  var prefetched = false;
11
11
  function prefetchViewer() {
12
12
  if (prefetched) return;
13
13
  prefetched = true;
14
- void import('./viewer-73LSE6RR.js');
14
+ void import('./viewer-C323RYNZ.js');
15
15
  }
16
16
  var ZOOM_DEFAULTS = { min: 1, max: 8, wheel: true, doubleClick: true };
17
17
  function Image(props) {
@@ -79,6 +79,28 @@ async function togglePip(video) {
79
79
  if (document.pictureInPictureElement) return void await document.exitPictureInPicture();
80
80
  await video.requestPictureInPicture();
81
81
  }
82
+ var SOURCE_TYPES = {
83
+ mp4: "video/mp4",
84
+ m4v: "video/mp4",
85
+ webm: "video/webm",
86
+ ogv: "video/ogg",
87
+ mov: "video/quicktime",
88
+ mkv: "video/x-matroska",
89
+ m3u8: "application/vnd.apple.mpegurl",
90
+ mpd: "application/dash+xml"
91
+ };
92
+ function sourceType(url) {
93
+ let path = url;
94
+ try {
95
+ path = new URL(url, typeof location === "undefined" ? "https://localhost" : location.href).pathname;
96
+ } catch {
97
+ path = url.split("?")[0].split("#")[0];
98
+ }
99
+ const name = path.split("/").pop() ?? "";
100
+ const dot = name.lastIndexOf(".");
101
+ if (dot <= 0) return void 0;
102
+ return SOURCE_TYPES[name.slice(dot + 1).toLowerCase()];
103
+ }
82
104
  function captionTracks(list) {
83
105
  if (!list) return [];
84
106
  const found = [];
@@ -152,4 +174,4 @@ async function promptRemote(video) {
152
174
  element.webkitShowPlaybackTargetPicker?.();
153
175
  }
154
176
 
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 };
177
+ export { SEEK_JUMP, SEEK_STEP, SPEEDS, VOLUME_STEP, activeCaption, bufferedAhead, captionTracks, commandFor, formatTime, fractionAt, fullscreenElement, isTypingTarget, nextSpeed, progress, promptRemote, showCaption, sourceType, supportsPip, supportsRemote, toggleFullscreen, togglePip, watchRemote };
@@ -1,4 +1,4 @@
1
- import { SPEEDS, supportsPip, supportsRemote, watchRemote, showCaption, captionTracks, activeCaption, toggleFullscreen, togglePip, promptRemote, isTypingTarget, commandFor, progress, formatTime, bufferedAhead, fullscreenElement, fractionAt } from './chunk-RUEQ3TGP.js';
1
+ import { SPEEDS, supportsPip, supportsRemote, watchRemote, showCaption, captionTracks, activeCaption, toggleFullscreen, togglePip, promptRemote, isTypingTarget, commandFor, progress, sourceType, formatTime, bufferedAhead, fullscreenElement, fractionAt } from './chunk-JLZDS6CV.js';
2
2
  import { lazy, useLayoutEffect, useRef, useState, useMemo, useEffect, useCallback, Suspense } from 'react';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
 
@@ -351,7 +351,7 @@ var CONTROL_DEFAULTS = {
351
351
  fullscreen: true,
352
352
  download: false
353
353
  };
354
- var VideoMenu = lazy(() => import('./menu-PNQRTRNV.js').then((module) => ({ default: module.VideoMenu })));
354
+ var VideoMenu = lazy(() => import('./menu-5JZMGNOT.js').then((module) => ({ default: module.VideoMenu })));
355
355
  var injected = false;
356
356
  function injectStyles() {
357
357
  if (injected || typeof document === "undefined") return;
@@ -765,7 +765,14 @@ function Video(props) {
765
765
  if (clickToPlay) toggle();
766
766
  },
767
767
  children: [
768
- typeof src !== "string" && sources.map((source) => /* @__PURE__ */ jsx("source", { src: source.src, type: source.type }, source.src)),
768
+ typeof src !== "string" && sources.map((source) => (
769
+ // The type is what lets the browser SKIP a file it cannot play without
770
+ // fetching it, which is the only reason to list several. Read from the
771
+ // extension when it was not given, and left off when the extension means
772
+ // nothing - a wrong type is worse than none, because the source is then
773
+ // skipped and the video does not play at all.
774
+ /* @__PURE__ */ jsx("source", { src: source.src, type: source.type ?? sourceType(source.src) }, source.src)
775
+ )),
769
776
  tracks?.map((track) => /* @__PURE__ */ jsx(
770
777
  "track",
771
778
  {
@@ -1,4 +1,5 @@
1
- import { createContextMenu, clipboardAction, performClipboardAction, inspectClipboardTarget, CLIPBOARD_PREFIX, clipboardEntries, isAction, clipboardHasText } from './chunk-Z3VDE7OA.js';
1
+ import { createContextMenu, clipboardAction, performClipboardAction, inspectClipboardTarget, CLIPBOARD_PREFIX, clipboardEntries, isAction, clipboardHasText } from './chunk-YQV5XZIA.js';
2
+ import { floatingContainer, measureFrame, placeFloating } from './chunk-RXEO4WPI.js';
2
3
  import { shortcutTokens } from './chunk-4VUHQFAT.js';
3
4
  import { Slot } from './chunk-HS3X3XCW.js';
4
5
  import { shortenQuery } from './chunk-4ZPMP47J.js';
@@ -23,6 +24,16 @@ var CONTEXT_MENU_STYLES = `
23
24
  --enigma-menu-accent: #fbbf24;
24
25
  --enigma-menu-danger: #f87171;
25
26
  --enigma-menu-danger-bg: rgba(248, 113, 113, 0.12);
27
+
28
+ /* Above the lightbox, which sits at 9999, because a menu is opened FROM whatever is on
29
+ top: the image viewer's own three-dot menu was portalled to the same body and painted
30
+ underneath it, so its rows could be neither seen nor pressed. A menu is the topmost
31
+ transient surface on a page, and this is the number that says so. It belongs here and
32
+ not in the light-scheme block, or half the readers get an invalid z-index.
33
+ --enigma-floating-z is the layer the select and the colour picker share, declared with
34
+ the same value in their sheets, so overriding it once moves every popup together. */
35
+ --enigma-floating-z: 10000;
36
+ --enigma-menu-z: var(--enigma-floating-z);
26
37
  }
27
38
 
28
39
  @media (prefers-color-scheme: light) {
@@ -43,7 +54,10 @@ var CONTEXT_MENU_STYLES = `
43
54
  and what a menu opened at the pointer needs. Fixed rather than absolute so a scrolling
44
55
  ancestor cannot drag it away from the place it was opened. */
45
56
  [data-enigma-menu-panel] {
46
- position: fixed; z-index: 60;
57
+ position: fixed; z-index: var(--enigma-menu-z);
58
+ /* A column, so the max-height the placement writes in a short window shrinks the LIST,
59
+ which scrolls, rather than letting rows spill past the panel's edge. */
60
+ display: flex; flex-direction: column;
47
61
  box-sizing: border-box;
48
62
  min-width: var(--enigma-menu-min-width);
49
63
  max-width: min(22rem, calc(100vw - 1rem));
@@ -98,6 +112,7 @@ var CONTEXT_MENU_STYLES = `
98
112
 
99
113
  [data-enigma-menu-list] {
100
114
  max-height: var(--enigma-menu-max-height);
115
+ min-height: 0; flex: 0 1 auto;
101
116
  overflow-y: auto; overscroll-behavior: contain;
102
117
  }
103
118
 
@@ -245,6 +260,7 @@ function ContextMenuRoot(props) {
245
260
  const {
246
261
  items,
247
262
  title,
263
+ searchable,
248
264
  disabled = false,
249
265
  fuse,
250
266
  fuseOptions,
@@ -272,6 +288,7 @@ function ContextMenuRoot(props) {
272
288
  const instance = useMemo(() => createContextMenu({
273
289
  items,
274
290
  title,
291
+ searchable,
275
292
  fuse,
276
293
  fuseOptions,
277
294
  matcher,
@@ -328,8 +345,8 @@ function ContextMenuRoot(props) {
328
345
  return [...clip, { type: "separator", id: `${CLIPBOARD_PREFIX}rule` }, ...rows];
329
346
  }, []);
330
347
  useEffect(() => {
331
- instance.update({ items: withClipboard(currentItems.current), title });
332
- }, [instance, signature, title, withClipboard]);
348
+ instance.update({ items: withClipboard(currentItems.current), title, searchable });
349
+ }, [instance, signature, title, searchable, withClipboard]);
333
350
  const cancelClose = useCallback(() => {
334
351
  window.clearTimeout(closing.current);
335
352
  }, []);
@@ -525,7 +542,7 @@ function ContextMenuContent({ chunk = 40, portal = true, ...props }) {
525
542
  const panels = /* @__PURE__ */ jsx(Fragment, { children: menu.state.levels.map((_, level) => /* @__PURE__ */ jsx(ContextMenuPanel, { level, chunk, ...props }, level)) });
526
543
  if (!portal) return panels;
527
544
  if (!mounted || typeof document === "undefined") return null;
528
- return createPortal(panels, document.body);
545
+ return createPortal(panels, floatingContainer(menu.triggerRef.current) ?? document.body);
529
546
  }
530
547
  function ContextMenuPanel({ level, chunk = 40, ...props }) {
531
548
  const menu = useContextMenuContext("ContextMenu.Panel");
@@ -538,27 +555,33 @@ function ContextMenuPanel({ level, chunk = 40, ...props }) {
538
555
  useLayoutEffect(() => {
539
556
  const panel = ref.current;
540
557
  if (!panel) return;
558
+ panel.style.maxHeight = "";
541
559
  const width = panel.offsetWidth;
542
560
  const height = panel.offsetHeight;
543
- const vw = window.innerWidth;
544
- const vh = window.innerHeight;
561
+ const frame = measureFrame(panel);
562
+ const { boundary } = frame;
545
563
  let left;
546
564
  let top;
547
565
  if (level === 0) {
548
- left = point?.x ?? MARGIN;
549
- top = point?.y ?? MARGIN;
550
- if (left + width > vw - MARGIN) left = Math.max(MARGIN, left - width);
551
- if (top + height > vh - MARGIN) top = Math.max(MARGIN, top - height);
566
+ const x = point?.x ?? MARGIN;
567
+ const y = point?.y ?? MARGIN;
568
+ const placedAt = placeFloating({ left: x, right: x, top: y, bottom: y }, { width, height }, { boundary, margin: MARGIN });
569
+ left = placedAt.left;
570
+ top = placedAt.top;
571
+ panel.style.maxHeight = `${placedAt.maxHeight}px`;
552
572
  } else {
553
573
  const parent = document.getElementById(menu.panelId(level - 1))?.getBoundingClientRect();
554
574
  const row = document.getElementById(menu.itemId(level - 1, menu.state.levels[level - 1]?.active ?? -1))?.getBoundingClientRect();
555
575
  const anchor = parent ?? { left: point?.x ?? MARGIN, right: point?.x ?? MARGIN, top: point?.y ?? MARGIN };
556
576
  left = anchor.right - 2;
557
- if (left + width > vw - MARGIN) left = Math.max(MARGIN, anchor.left - width + 2);
577
+ if (left + width > boundary.right - MARGIN) left = Math.max(boundary.left + MARGIN, anchor.left - width + 2);
558
578
  top = (row?.top ?? anchor.top) - 4;
559
- if (top + height > vh - MARGIN) top = Math.max(MARGIN, vh - MARGIN - height);
579
+ if (top + height > boundary.bottom - MARGIN) top = Math.max(boundary.top + MARGIN, boundary.bottom - MARGIN - height);
560
580
  }
561
- setPlaced({ left: Math.max(MARGIN, Math.round(left)), top: Math.max(MARGIN, Math.round(top)) });
581
+ setPlaced({
582
+ left: Math.max(boundary.left + MARGIN, Math.round(left)) - frame.x,
583
+ top: Math.max(boundary.top + MARGIN, Math.round(top)) - frame.y
584
+ });
562
585
  }, [level, point?.x, point?.y, rows, loading, menu, menu.state.levels.length]);
563
586
  const isPlaced = placed !== null;
564
587
  const isDeepest = menu.state.levels.length - 1 === level;
@@ -699,6 +722,7 @@ function ContextMenuRow({ level, index, entry, children, ...props }) {
699
722
  if (entry.type === "separator") return /* @__PURE__ */ jsx("div", { ...props, role: "separator", "data-enigma-menu-separator": "" });
700
723
  return /* @__PURE__ */ jsx("p", { ...props, "data-enigma-menu-label": "", "aria-hidden": "true", children: entry.label });
701
724
  }
725
+ const reserveCheck = state?.entries.some((entry2) => isAction(entry2) && entry2.checked !== void 0) ?? false;
702
726
  const checkable = item.checked !== void 0;
703
727
  const role = checkable ? item.group ? "menuitemradio" : "menuitemcheckbox" : "menuitem";
704
728
  return /* @__PURE__ */ jsx(
@@ -741,13 +765,13 @@ function ContextMenuRow({ level, index, entry, children, ...props }) {
741
765
  if (event.defaultPrevented || item.disabled || event.detail !== 0) return;
742
766
  menu.instance.select(level, index);
743
767
  },
744
- children: children ?? menu.renderItem?.(item, level, index) ?? /* @__PURE__ */ jsx(ContextMenuRowContent, { item, submenu })
768
+ children: children ?? menu.renderItem?.(item, level, index) ?? /* @__PURE__ */ jsx(ContextMenuRowContent, { item, submenu, reserveCheck })
745
769
  }
746
770
  );
747
771
  }
748
- function ContextMenuRowContent({ item, submenu }) {
772
+ function ContextMenuRowContent({ item, submenu, reserveCheck }) {
749
773
  return /* @__PURE__ */ jsxs(Fragment, { children: [
750
- item.checked !== void 0 && /* @__PURE__ */ jsx("span", { "data-enigma-menu-check": "", "aria-hidden": "true" }),
774
+ (item.checked !== void 0 || reserveCheck) && /* @__PURE__ */ jsx("span", { "data-enigma-menu-check": "", "aria-hidden": "true" }),
751
775
  item.icon ? /* @__PURE__ */ jsx("span", { "data-enigma-menu-icon": "", children: item.icon }) : null,
752
776
  /* @__PURE__ */ jsxs("span", { "data-enigma-menu-item-text": "", children: [
753
777
  /* @__PURE__ */ jsx("span", { "data-enigma-menu-item-label": "", children: item.label }),
@@ -0,0 +1,111 @@
1
+ import { useState, useLayoutEffect, useCallback, useEffect } from 'react';
2
+
3
+ // src/core/floating.ts
4
+ function placeFloating(anchor, size, options) {
5
+ const { boundary, margin = 8, gap = 0, side = "auto" } = options;
6
+ const below = boundary.bottom - margin - anchor.bottom - gap;
7
+ const above = anchor.top - gap - boundary.top - margin;
8
+ const chosen = side !== "auto" ? side : size.height <= below || below >= above ? "bottom" : "top";
9
+ const maxHeight = Math.max(0, Math.floor(chosen === "bottom" ? below : above));
10
+ const height = Math.min(size.height, maxHeight);
11
+ const top = chosen === "bottom" ? anchor.bottom + gap : anchor.top - gap - height;
12
+ const maxLeft = boundary.right - margin - size.width;
13
+ let left = anchor.left;
14
+ if (left > maxLeft) left = anchor.right - size.width;
15
+ left = Math.max(boundary.left + margin, Math.min(left, maxLeft));
16
+ return { left: Math.round(left), top: Math.round(top), side: chosen, maxHeight };
17
+ }
18
+ function floatingContainer(anchor) {
19
+ if (typeof document === "undefined") return null;
20
+ const host = anchor?.closest("dialog[open], [popover], [role=dialog], [role=alertdialog], [aria-modal=true]");
21
+ return host ?? document.body;
22
+ }
23
+ function measureFrame(panel) {
24
+ const root = document.documentElement;
25
+ const viewport = { left: 0, top: 0, right: root.clientWidth || window.innerWidth, bottom: root.clientHeight || window.innerHeight };
26
+ const host = panel.parentElement;
27
+ if (!host || host === document.body) return { x: 0, y: 0, boundary: viewport };
28
+ const probe = document.createElement("div");
29
+ probe.style.cssText = "position:fixed;left:0;top:0;width:0;height:0;visibility:hidden;pointer-events:none";
30
+ host.appendChild(probe);
31
+ const origin = probe.getBoundingClientRect();
32
+ probe.remove();
33
+ const box = host.getBoundingClientRect();
34
+ const isBlock = Math.abs(origin.left - (box.left + host.clientLeft)) < 1 && Math.abs(origin.top - (box.top + host.clientTop)) < 1;
35
+ const style = getComputedStyle(host);
36
+ const clips = style.overflowX !== "visible" || style.overflowY !== "visible";
37
+ const boundary = isBlock && clips ? {
38
+ left: Math.max(viewport.left, box.left),
39
+ top: Math.max(viewport.top, box.top),
40
+ right: Math.min(viewport.right, box.right),
41
+ bottom: Math.min(viewport.bottom, box.bottom)
42
+ } : viewport;
43
+ return { x: origin.left, y: origin.top, boundary };
44
+ }
45
+ function useFloating({ open, anchorRef, panelRef, gap = 4, margin = 8, side = "auto", deps = [] }) {
46
+ const [container, setContainer] = useState(null);
47
+ useLayoutEffect(() => {
48
+ if (open) setContainer(floatingContainer(anchorRef.current));
49
+ }, [open, anchorRef]);
50
+ const place = useCallback(() => {
51
+ const anchor = anchorRef.current;
52
+ const panel = panelRef.current;
53
+ if (!anchor || !panel) return;
54
+ const rect = anchor.getBoundingClientRect();
55
+ panel.style.setProperty("--enigma-anchor-width", `${rect.width}px`);
56
+ panel.style.maxHeight = "";
57
+ const frame = measureFrame(panel);
58
+ const placed = placeFloating(rect, { width: panel.offsetWidth, height: panel.offsetHeight }, { boundary: frame.boundary, margin, gap, side });
59
+ panel.style.left = `${placed.left - frame.x}px`;
60
+ panel.style.top = `${placed.top - frame.y}px`;
61
+ panel.style.maxHeight = `${placed.maxHeight}px`;
62
+ panel.setAttribute("data-side", placed.side);
63
+ }, [anchorRef, panelRef, margin, gap, side]);
64
+ useLayoutEffect(() => {
65
+ if (open) place();
66
+ }, [open, container, place, ...deps]);
67
+ useEffect(() => {
68
+ if (!open || !container) return;
69
+ let frame = 0;
70
+ const schedule = () => {
71
+ if (frame) return;
72
+ frame = requestAnimationFrame(() => {
73
+ frame = 0;
74
+ place();
75
+ });
76
+ };
77
+ const onScroll = (event) => {
78
+ if (panelRef.current?.contains(event.target)) return;
79
+ schedule();
80
+ };
81
+ document.addEventListener("scroll", onScroll, true);
82
+ window.addEventListener("resize", schedule);
83
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(schedule);
84
+ if (anchorRef.current) observer?.observe(anchorRef.current);
85
+ if (panelRef.current) observer?.observe(panelRef.current);
86
+ return () => {
87
+ cancelAnimationFrame(frame);
88
+ document.removeEventListener("scroll", onScroll, true);
89
+ window.removeEventListener("resize", schedule);
90
+ observer?.disconnect();
91
+ };
92
+ }, [open, container, place, anchorRef, panelRef]);
93
+ return container;
94
+ }
95
+ var TABBABLE = "a[href], button:not(:disabled), input:not(:disabled):not([type=hidden]), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex='-1'])";
96
+ function tabOut(event, panel, trigger) {
97
+ if (event.key !== "Tab" || !panel || !trigger) return false;
98
+ const stops = Array.from(panel.querySelectorAll(TABBABLE));
99
+ const at = stops.indexOf(document.activeElement);
100
+ if (event.shiftKey) {
101
+ if (at > 0) return false;
102
+ event.preventDefault();
103
+ trigger.focus();
104
+ return true;
105
+ }
106
+ if (at !== -1 && at < stops.length - 1) return false;
107
+ trigger.focus();
108
+ return true;
109
+ }
110
+
111
+ export { floatingContainer, measureFrame, placeFloating, tabOut, useFloating };
@@ -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-T63FLJNH.js').then((module) => ({ default: module.ColorExtras })));
28
+ var ColorExtras = lazy(() => import('./color-WBNVPFT5.js').then((module) => ({ default: module.ColorExtras })));
29
29
  var Input = forwardRef(function Input2(props, forwardedRef) {
30
30
  const {
31
31
  reveal,
@@ -6,7 +6,7 @@ function isAction(entry) {
6
6
  return (entry.type ?? "item") === "item";
7
7
  }
8
8
  var CONTEXT_MENU_SEARCH_KEYS = ["label", "description", "group", "keywords"];
9
- var SEARCHABLE_FROM = 8;
9
+ var SEARCHABLE_FROM = 12;
10
10
  var PAGE = 5;
11
11
  var DEFAULT_CACHE_MS = 5 * 60 * 1e3;
12
12
  function fold(value) {
@@ -32,7 +32,7 @@ function createContextMenu(options = {}) {
32
32
  return path.join("\0");
33
33
  }
34
34
  function searchableFor(item, entries) {
35
- const asked = item?.searchable ?? "auto";
35
+ const asked = (item ? item.searchable : opts.searchable) ?? "auto";
36
36
  if (asked !== "auto") return asked;
37
37
  return entries.filter(isAction).length >= SEARCHABLE_FROM;
38
38
  }
@@ -1,4 +1,4 @@
1
- import { b as ContextMenuEntry } from './context-menu-D3FtTn7v.js';
1
+ import { b as ContextMenuEntry } from './context-menu-qqpGBkny.js';
2
2
 
3
3
  /**
4
4
  * The rows every context menu on a desktop already has: Copy, Cut and Paste.