@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,10 +1,10 @@
1
1
  import { ReactNode, ComponentPropsWithoutRef } from 'react';
2
- import { b as ContextMenuNode, a as ContextMenuItem } from '../context-BfjYGPnH.js';
3
- export { C as ContextMenuContextValue, u as useContextMenuContext } from '../context-BfjYGPnH.js';
4
- import { c as ClipboardMenuOptions } from '../clipboard-menu-BvXJCSFN.js';
5
- export { C as CLIPBOARD_PREFIX, a as ClipboardAction, b as ClipboardMenuLabels, d as ClipboardTarget, e as clipboardAction, f as clipboardEntries, g as clipboardHasText, i as inspectClipboardTarget, p as performClipboardAction } from '../clipboard-menu-BvXJCSFN.js';
6
- import { g as ContextMenuOptions, b as ContextMenuEntry } from '../context-menu-D3FtTn7v.js';
7
- export { C as CONTEXT_MENU_SEARCH_KEYS, a as ContextMenuAction, c as ContextMenuInstance, d as ContextMenuLabel, e as ContextMenuLevel, f as ContextMenuMoveKey, h as ContextMenuPoint, i as ContextMenuSeparator, j as ContextMenuState, k as createContextMenu, l as isAction } from '../context-menu-D3FtTn7v.js';
2
+ import { b as ContextMenuNode, a as ContextMenuItem } from '../context-DR1dpO8b.js';
3
+ export { C as ContextMenuContextValue, u as useContextMenuContext } from '../context-DR1dpO8b.js';
4
+ import { c as ClipboardMenuOptions } from '../clipboard-menu-eemzEBqB.js';
5
+ export { C as CLIPBOARD_PREFIX, a as ClipboardAction, b as ClipboardMenuLabels, d as ClipboardTarget, e as clipboardAction, f as clipboardEntries, g as clipboardHasText, i as inspectClipboardTarget, p as performClipboardAction } from '../clipboard-menu-eemzEBqB.js';
6
+ import { g as ContextMenuOptions, b as ContextMenuEntry } from '../context-menu-qqpGBkny.js';
7
+ export { C as CONTEXT_MENU_SEARCH_KEYS, a as ContextMenuAction, c as ContextMenuInstance, d as ContextMenuLabel, e as ContextMenuLevel, f as ContextMenuMoveKey, h as ContextMenuPoint, i as ContextMenuSeparator, j as ContextMenuState, k as createContextMenu, l as isAction } from '../context-menu-qqpGBkny.js';
8
8
  export { S as Shortcut, b as ShortcutSpec, m as matchesShortcut, p as parseShortcut, e as shortcutText, f as shortcutTokens } from '../keys-D2zJs1uB.js';
9
9
  import '../search-DYgqRp37.js';
10
10
 
@@ -28,6 +28,12 @@ interface ContextMenuRootProps {
28
28
  * selection, Cut over a selection in something writable, Paste in anything writable.
29
29
  */
30
30
  clipboard?: boolean | ClipboardMenuOptions;
31
+ /**
32
+ * A filter field over the rows. `"auto"` (the default) grows one only once there are
33
+ * enough rows to be worth filtering; `true` asks for one whatever the count - which is
34
+ * what a menu whose rows are fetched wants - and `false` never draws one.
35
+ */
36
+ searchable?: boolean | "auto";
31
37
  /** Fuse.js's constructor, for fuzzy filtering. Omit it for the built-in matcher. */
32
38
  fuse?: ContextMenuOptions["fuse"];
33
39
  fuseOptions?: Record<string, unknown>;
@@ -124,7 +130,7 @@ declare function ContextMenuRow({ level, index, entry, children, ...props }: Con
124
130
  * is PREPENDED to `<head>`, so anything the document already has wins on source order at
125
131
  * equal specificity.
126
132
  */
127
- declare const CONTEXT_MENU_STYLES = "\n:root {\n --enigma-menu-bg: #1c1c1c;\n --enigma-menu-border: #333333;\n --enigma-menu-radius: 0.625rem;\n --enigma-menu-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);\n --enigma-menu-text: #f5f5f5;\n --enigma-menu-muted: #a3a3a3;\n --enigma-menu-font-size: 0.8125rem;\n --enigma-menu-min-width: 12rem;\n --enigma-menu-max-height: 20rem;\n\n --enigma-menu-active-bg: #2a2a2a;\n --enigma-menu-accent: #fbbf24;\n --enigma-menu-danger: #f87171;\n --enigma-menu-danger-bg: rgba(248, 113, 113, 0.12);\n}\n\n@media (prefers-color-scheme: light) {\n :root {\n --enigma-menu-bg: #ffffff;\n --enigma-menu-border: #e5e5e5;\n --enigma-menu-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);\n --enigma-menu-text: #171717;\n --enigma-menu-muted: #737373;\n --enigma-menu-active-bg: #f5f5f5;\n --enigma-menu-accent: #b45309;\n --enigma-menu-danger: #b91c1c;\n --enigma-menu-danger-bg: rgba(185, 28, 28, 0.08);\n }\n}\n\n/* Every panel is positioned in VIEWPORT coordinates, which is what a pointer event reports\n and what a menu opened at the pointer needs. Fixed rather than absolute so a scrolling\n ancestor cannot drag it away from the place it was opened. */\n[data-enigma-menu-panel] {\n position: fixed; z-index: 60;\n box-sizing: border-box;\n min-width: var(--enigma-menu-min-width);\n max-width: min(22rem, calc(100vw - 1rem));\n padding: 0.25rem;\n color: var(--enigma-menu-text);\n background: var(--enigma-menu-bg);\n border: 1px solid var(--enigma-menu-border);\n border-radius: var(--enigma-menu-radius);\n box-shadow: var(--enigma-menu-shadow);\n font-size: var(--enigma-menu-font-size);\n animation: enigma-menu-in 90ms ease-out;\n /* Measured before it is placed, and placed before it is seen: without this the panel is\n painted at 0,0 for one frame and jumps to the pointer on the next. */\n visibility: hidden;\n}\n[data-enigma-menu-panel][data-placed] { visibility: visible; }\n/* No focus ring around the panel. It takes focus so the keyboard has somewhere to be, not\n because it is a control: what MOVES is the highlighted row, drawn by [data-active] and\n announced through aria-activedescendant. A ring around the whole menu says the menu is the\n thing focused, which is not what is happening - and it appears the moment you press an\n arrow, over a surface that was already showing you where you are. Nothing is lost by\n removing it, because nothing inside is reachable by Tab: the menu owns that key. */\n[data-enigma-menu-panel]:focus,\n[data-enigma-menu-panel]:focus-visible { outline: none; }\n@keyframes enigma-menu-in {\n from { opacity: 0; transform: scale(0.98); }\n to { opacity: 1; transform: none; }\n}\n@media (prefers-reduced-motion: reduce) {\n [data-enigma-menu-panel] { animation: none; }\n}\n\n/* The heading: what the menu is acting ON. A long file name is cut rather than allowed to\n set the panel's width, and the full one stays reachable through its title attribute. */\n[data-enigma-menu-title] {\n margin: 0; padding: 0.375rem 0.5rem 0.25rem;\n font-size: 0.6875rem; font-weight: 600;\n color: var(--enigma-menu-muted);\n overflow: hidden; white-space: nowrap; text-overflow: ellipsis;\n}\n\n[data-enigma-menu-search] {\n width: 100%; box-sizing: border-box;\n padding: 0.375rem 0.5rem 0.5rem;\n font: inherit; font-size: var(--enigma-menu-font-size);\n color: var(--enigma-menu-text); background: transparent;\n border: 0; border-bottom: 1px solid var(--enigma-menu-border);\n outline: none;\n}\n[data-enigma-menu-search]::-webkit-search-cancel-button,\n[data-enigma-menu-search]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }\n\n[data-enigma-menu-list] {\n max-height: var(--enigma-menu-max-height);\n overflow-y: auto; overscroll-behavior: contain;\n}\n\n[data-enigma-menu-item] {\n display: flex; align-items: center; gap: 0.5rem;\n padding: 0.3125rem 0.5rem; border-radius: 0.375rem;\n line-height: 1.4;\n color: var(--enigma-menu-text);\n /* The hand, not the arrow. A native menu uses the arrow because the whole surface is\n drawn by the OS and nothing on a page looks like it; here the rows sit on a web page,\n where the hand is what says \"this is a thing you press\". */\n cursor: pointer; user-select: none;\n}\n[data-enigma-menu-item][data-active] { background: var(--enigma-menu-active-bg); }\n[data-enigma-menu-item][data-disabled] { opacity: 0.45; cursor: not-allowed; }\n[data-enigma-menu-item][data-destructive] { color: var(--enigma-menu-danger); }\n[data-enigma-menu-item][data-destructive][data-active] { background: var(--enigma-menu-danger-bg); }\n\n[data-enigma-menu-item-text] { display: grid; min-width: 0; }\n[data-enigma-menu-item-label] { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }\n[data-enigma-menu-item-description] {\n font-size: 0.6875rem; color: var(--enigma-menu-muted);\n overflow: hidden; white-space: nowrap; text-overflow: ellipsis;\n}\n\n[data-enigma-menu-icon] { display: inline-flex; align-items: center; flex: none; width: 1rem; max-height: 1.25em; line-height: 0; }\n\n/* The shortcut, on the right, the way every desktop menu prints it. margin-left: auto is\n what pushes it there, and flex: none is what stops a long label squashing it. */\n[data-enigma-menu-shortcut] {\n display: flex; align-items: center; gap: 0.125rem;\n flex: none; margin-left: auto; padding-left: 1.5rem;\n font-size: 0.6875rem; color: var(--enigma-menu-muted);\n font-variant-numeric: tabular-nums;\n}\n[data-enigma-menu-shortcut] kbd { font: inherit; }\n\n/* The submenu arrow shares the shortcut's slot, so a row never has both pushed apart. */\n[data-enigma-menu-arrow] {\n flex: none; margin-left: auto;\n width: 0.35rem; height: 0.35rem;\n border-right: 1.5px solid currentColor; border-top: 1.5px solid currentColor;\n transform: rotate(45deg);\n opacity: 0.6;\n}\n[data-enigma-menu-shortcut] + [data-enigma-menu-arrow] { margin-left: 0.5rem; }\n\n/* Check and radio: drawn rather than loaded, and reserving their space either way so\n choosing a row does not shift the label under the pointer. */\n[data-enigma-menu-check] {\n flex: none; width: 1rem; height: 1rem;\n display: grid; place-items: center;\n color: var(--enigma-menu-accent);\n}\n[data-enigma-menu-check]::after { content: \"\"; width: 0.35rem; height: 0.6rem; opacity: 0; }\n[data-enigma-menu-item][aria-checked=\"true\"] [data-enigma-menu-check]::after {\n opacity: 1;\n border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;\n transform: rotate(45deg) translate(-0.05rem, -0.05rem);\n}\n[data-enigma-menu-item][role=\"menuitemradio\"] [data-enigma-menu-check]::after {\n width: 0.4rem; height: 0.4rem; border-radius: 999px; background: currentColor;\n transform: none; border: 0;\n}\n\n[data-enigma-menu-separator] {\n height: 1px; margin: 0.25rem 0.25rem;\n background: var(--enigma-menu-border);\n}\n\n[data-enigma-menu-label] {\n margin: 0.375rem 0 0.125rem; padding: 0 0.5rem;\n font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;\n color: var(--enigma-menu-muted);\n}\n\n[data-enigma-menu-status] {\n margin: 0; padding: 0.625rem 0.5rem;\n font-size: var(--enigma-menu-font-size); color: var(--enigma-menu-muted);\n /* The quoted query is cut in the text as well, but a pasted string with no spaces has no\n break opportunity at all - so this is what stops it stretching the panel. */\n overflow-wrap: anywhere;\n}\n[data-enigma-menu-status][data-error] { color: var(--enigma-menu-danger); }\n\n/* The end-of-window marker: one pixel, so it has a box to be scrolled into. */\n[data-enigma-menu-more] { height: 1px; }\n";
133
+ declare const CONTEXT_MENU_STYLES = "\n:root {\n --enigma-menu-bg: #1c1c1c;\n --enigma-menu-border: #333333;\n --enigma-menu-radius: 0.625rem;\n --enigma-menu-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);\n --enigma-menu-text: #f5f5f5;\n --enigma-menu-muted: #a3a3a3;\n --enigma-menu-font-size: 0.8125rem;\n --enigma-menu-min-width: 12rem;\n --enigma-menu-max-height: 20rem;\n\n --enigma-menu-active-bg: #2a2a2a;\n --enigma-menu-accent: #fbbf24;\n --enigma-menu-danger: #f87171;\n --enigma-menu-danger-bg: rgba(248, 113, 113, 0.12);\n\n /* Above the lightbox, which sits at 9999, because a menu is opened FROM whatever is on\n top: the image viewer's own three-dot menu was portalled to the same body and painted\n underneath it, so its rows could be neither seen nor pressed. A menu is the topmost\n transient surface on a page, and this is the number that says so. It belongs here and\n not in the light-scheme block, or half the readers get an invalid z-index.\n --enigma-floating-z is the layer the select and the colour picker share, declared with\n the same value in their sheets, so overriding it once moves every popup together. */\n --enigma-floating-z: 10000;\n --enigma-menu-z: var(--enigma-floating-z);\n}\n\n@media (prefers-color-scheme: light) {\n :root {\n --enigma-menu-bg: #ffffff;\n --enigma-menu-border: #e5e5e5;\n --enigma-menu-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);\n --enigma-menu-text: #171717;\n --enigma-menu-muted: #737373;\n --enigma-menu-active-bg: #f5f5f5;\n --enigma-menu-accent: #b45309;\n --enigma-menu-danger: #b91c1c;\n --enigma-menu-danger-bg: rgba(185, 28, 28, 0.08);\n }\n}\n\n/* Every panel is positioned in VIEWPORT coordinates, which is what a pointer event reports\n and what a menu opened at the pointer needs. Fixed rather than absolute so a scrolling\n ancestor cannot drag it away from the place it was opened. */\n[data-enigma-menu-panel] {\n position: fixed; z-index: var(--enigma-menu-z);\n /* A column, so the max-height the placement writes in a short window shrinks the LIST,\n which scrolls, rather than letting rows spill past the panel's edge. */\n display: flex; flex-direction: column;\n box-sizing: border-box;\n min-width: var(--enigma-menu-min-width);\n max-width: min(22rem, calc(100vw - 1rem));\n padding: 0.25rem;\n color: var(--enigma-menu-text);\n background: var(--enigma-menu-bg);\n border: 1px solid var(--enigma-menu-border);\n border-radius: var(--enigma-menu-radius);\n box-shadow: var(--enigma-menu-shadow);\n font-size: var(--enigma-menu-font-size);\n animation: enigma-menu-in 90ms ease-out;\n /* Measured before it is placed, and placed before it is seen: without this the panel is\n painted at 0,0 for one frame and jumps to the pointer on the next. */\n visibility: hidden;\n}\n[data-enigma-menu-panel][data-placed] { visibility: visible; }\n/* No focus ring around the panel. It takes focus so the keyboard has somewhere to be, not\n because it is a control: what MOVES is the highlighted row, drawn by [data-active] and\n announced through aria-activedescendant. A ring around the whole menu says the menu is the\n thing focused, which is not what is happening - and it appears the moment you press an\n arrow, over a surface that was already showing you where you are. Nothing is lost by\n removing it, because nothing inside is reachable by Tab: the menu owns that key. */\n[data-enigma-menu-panel]:focus,\n[data-enigma-menu-panel]:focus-visible { outline: none; }\n@keyframes enigma-menu-in {\n from { opacity: 0; transform: scale(0.98); }\n to { opacity: 1; transform: none; }\n}\n@media (prefers-reduced-motion: reduce) {\n [data-enigma-menu-panel] { animation: none; }\n}\n\n/* The heading: what the menu is acting ON. A long file name is cut rather than allowed to\n set the panel's width, and the full one stays reachable through its title attribute. */\n[data-enigma-menu-title] {\n margin: 0; padding: 0.375rem 0.5rem 0.25rem;\n font-size: 0.6875rem; font-weight: 600;\n color: var(--enigma-menu-muted);\n overflow: hidden; white-space: nowrap; text-overflow: ellipsis;\n}\n\n[data-enigma-menu-search] {\n width: 100%; box-sizing: border-box;\n padding: 0.375rem 0.5rem 0.5rem;\n font: inherit; font-size: var(--enigma-menu-font-size);\n color: var(--enigma-menu-text); background: transparent;\n border: 0; border-bottom: 1px solid var(--enigma-menu-border);\n outline: none;\n}\n[data-enigma-menu-search]::-webkit-search-cancel-button,\n[data-enigma-menu-search]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }\n\n[data-enigma-menu-list] {\n max-height: var(--enigma-menu-max-height);\n min-height: 0; flex: 0 1 auto;\n overflow-y: auto; overscroll-behavior: contain;\n}\n\n[data-enigma-menu-item] {\n display: flex; align-items: center; gap: 0.5rem;\n padding: 0.3125rem 0.5rem; border-radius: 0.375rem;\n line-height: 1.4;\n color: var(--enigma-menu-text);\n /* The hand, not the arrow. A native menu uses the arrow because the whole surface is\n drawn by the OS and nothing on a page looks like it; here the rows sit on a web page,\n where the hand is what says \"this is a thing you press\". */\n cursor: pointer; user-select: none;\n}\n[data-enigma-menu-item][data-active] { background: var(--enigma-menu-active-bg); }\n[data-enigma-menu-item][data-disabled] { opacity: 0.45; cursor: not-allowed; }\n[data-enigma-menu-item][data-destructive] { color: var(--enigma-menu-danger); }\n[data-enigma-menu-item][data-destructive][data-active] { background: var(--enigma-menu-danger-bg); }\n\n[data-enigma-menu-item-text] { display: grid; min-width: 0; }\n[data-enigma-menu-item-label] { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }\n[data-enigma-menu-item-description] {\n font-size: 0.6875rem; color: var(--enigma-menu-muted);\n overflow: hidden; white-space: nowrap; text-overflow: ellipsis;\n}\n\n[data-enigma-menu-icon] { display: inline-flex; align-items: center; flex: none; width: 1rem; max-height: 1.25em; line-height: 0; }\n\n/* The shortcut, on the right, the way every desktop menu prints it. margin-left: auto is\n what pushes it there, and flex: none is what stops a long label squashing it. */\n[data-enigma-menu-shortcut] {\n display: flex; align-items: center; gap: 0.125rem;\n flex: none; margin-left: auto; padding-left: 1.5rem;\n font-size: 0.6875rem; color: var(--enigma-menu-muted);\n font-variant-numeric: tabular-nums;\n}\n[data-enigma-menu-shortcut] kbd { font: inherit; }\n\n/* The submenu arrow shares the shortcut's slot, so a row never has both pushed apart. */\n[data-enigma-menu-arrow] {\n flex: none; margin-left: auto;\n width: 0.35rem; height: 0.35rem;\n border-right: 1.5px solid currentColor; border-top: 1.5px solid currentColor;\n transform: rotate(45deg);\n opacity: 0.6;\n}\n[data-enigma-menu-shortcut] + [data-enigma-menu-arrow] { margin-left: 0.5rem; }\n\n/* Check and radio: drawn rather than loaded, and reserving their space either way so\n choosing a row does not shift the label under the pointer. */\n[data-enigma-menu-check] {\n flex: none; width: 1rem; height: 1rem;\n display: grid; place-items: center;\n color: var(--enigma-menu-accent);\n}\n[data-enigma-menu-check]::after { content: \"\"; width: 0.35rem; height: 0.6rem; opacity: 0; }\n[data-enigma-menu-item][aria-checked=\"true\"] [data-enigma-menu-check]::after {\n opacity: 1;\n border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;\n transform: rotate(45deg) translate(-0.05rem, -0.05rem);\n}\n[data-enigma-menu-item][role=\"menuitemradio\"] [data-enigma-menu-check]::after {\n width: 0.4rem; height: 0.4rem; border-radius: 999px; background: currentColor;\n transform: none; border: 0;\n}\n\n[data-enigma-menu-separator] {\n height: 1px; margin: 0.25rem 0.25rem;\n background: var(--enigma-menu-border);\n}\n\n[data-enigma-menu-label] {\n margin: 0.375rem 0 0.125rem; padding: 0 0.5rem;\n font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;\n color: var(--enigma-menu-muted);\n}\n\n[data-enigma-menu-status] {\n margin: 0; padding: 0.625rem 0.5rem;\n font-size: var(--enigma-menu-font-size); color: var(--enigma-menu-muted);\n /* The quoted query is cut in the text as well, but a pasted string with no spaces has no\n break opportunity at all - so this is what stops it stretching the panel. */\n overflow-wrap: anywhere;\n}\n[data-enigma-menu-status][data-error] { color: var(--enigma-menu-danger); }\n\n/* The end-of-window marker: one pixel, so it has a box to be scrolled into. */\n[data-enigma-menu-more] { height: 1px; }\n";
128
134
 
129
135
  type ContextMenuProps = ContextMenuRootProps & {
130
136
  /** How many rows to put in the document at once. `Infinity` renders the lot. */
@@ -1,6 +1,7 @@
1
1
  "use client";
2
- export { CONTEXT_MENU_STYLES, ContextMenu, ContextMenuContent, ContextMenuList, ContextMenuPanel, ContextMenuRoot, ContextMenuRow, ContextMenuSearch, ContextMenuTrigger, useContextMenuContext } from '../chunk-DZMHY3SU.js';
3
- export { CLIPBOARD_PREFIX, CONTEXT_MENU_SEARCH_KEYS, clipboardAction, clipboardEntries, clipboardHasText, createContextMenu, inspectClipboardTarget, isAction, performClipboardAction } from '../chunk-Z3VDE7OA.js';
2
+ export { CONTEXT_MENU_STYLES, ContextMenu, ContextMenuContent, ContextMenuList, ContextMenuPanel, ContextMenuRoot, ContextMenuRow, ContextMenuSearch, ContextMenuTrigger, useContextMenuContext } from '../chunk-RRPWJAIJ.js';
3
+ export { CLIPBOARD_PREFIX, CONTEXT_MENU_SEARCH_KEYS, clipboardAction, clipboardEntries, clipboardHasText, createContextMenu, inspectClipboardTarget, isAction, performClipboardAction } from '../chunk-YQV5XZIA.js';
4
+ import '../chunk-RXEO4WPI.js';
4
5
  export { matchesShortcut, parseShortcut, shortcutText, shortcutTokens } from '../chunk-4VUHQFAT.js';
5
6
  import '../chunk-HS3X3XCW.js';
6
7
  import '../chunk-4ZPMP47J.js';
@@ -1,6 +1,6 @@
1
1
  import { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- import { b as ContextMenuNode } from '../context-BfjYGPnH.js';
3
- import '../context-menu-D3FtTn7v.js';
2
+ import { b as ContextMenuNode } from '../context-DR1dpO8b.js';
3
+ import '../context-menu-qqpGBkny.js';
4
4
  import '../search-DYgqRp37.js';
5
5
 
6
6
  /**
@@ -38,6 +38,8 @@ interface ZoomOptions {
38
38
  interface ImageMenuOptions {
39
39
  /** The download row. On whenever the menu is. */
40
40
  download?: boolean;
41
+ /** The row that opens the picture on its own, in another tab. On whenever the menu is. */
42
+ newTab?: boolean;
41
43
  /** Rows of your own, after the built-in ones. */
42
44
  items?: readonly ContextMenuNode[];
43
45
  onSelect?: (id: string, item: ImageItem, index: number) => void;
@@ -53,6 +55,7 @@ interface ImageLabels {
53
55
  next?: string;
54
56
  menu?: string;
55
57
  download?: string;
58
+ newTab?: string;
56
59
  discard?: string;
57
60
  thumbnails?: string;
58
61
  /** The frame itself, announced as the dialog's name. Default "Image viewer". */
@@ -1,3 +1,3 @@
1
1
  "use client";
2
- export { Image } from '../chunk-4SZ5N7BA.js';
2
+ export { Image } from '../chunk-IQMHDRJF.js';
3
3
  import '../chunk-EYZ366LP.js';
@@ -26,14 +26,14 @@ export { R as RecentEntry, b as RecentStore, e as createRecentStore, g as groupR
26
26
  export { Select, SelectContent, SelectContextValue, SelectItem, SelectList, SelectMultipleProps, SelectOptionRow, SelectProps, SelectRoot, SelectRootProps, SelectSearch, SelectSingleProps, SelectTrigger, SelectValue, useSelectContext } from './select.js';
27
27
  export { a as SelectInstance, c as SelectOption, d as SelectOptions, e as SelectState, f as createSelect } from '../select-ClSy-J1f.js';
28
28
  export { ContextMenu, ContextMenuContent, ContextMenuContentProps, ContextMenuList, ContextMenuPanel, ContextMenuProps, ContextMenuRoot, ContextMenuRootProps, ContextMenuRow, ContextMenuSearch, ContextMenuTrigger, ContextMenuTriggerProps } from './context-menu.js';
29
- export { C as CLIPBOARD_PREFIX, a as ClipboardAction, b as ClipboardMenuLabels, c as ClipboardMenuOptions, d as ClipboardTarget, e as clipboardAction, f as clipboardEntries, g as clipboardHasText, i as inspectClipboardTarget, p as performClipboardAction } from '../clipboard-menu-BvXJCSFN.js';
30
- export { a as ContextMenuAction, b as ContextMenuEntry, c as ContextMenuInstance, g as ContextMenuOptions, j as ContextMenuState, k as createContextMenu, l as isAction } from '../context-menu-D3FtTn7v.js';
29
+ export { C as CLIPBOARD_PREFIX, a as ClipboardAction, b as ClipboardMenuLabels, c as ClipboardMenuOptions, d as ClipboardTarget, e as clipboardAction, f as clipboardEntries, g as clipboardHasText, i as inspectClipboardTarget, p as performClipboardAction } from '../clipboard-menu-eemzEBqB.js';
30
+ export { a as ContextMenuAction, b as ContextMenuEntry, c as ContextMenuInstance, g as ContextMenuOptions, j as ContextMenuState, k as createContextMenu, l as isAction } from '../context-menu-qqpGBkny.js';
31
31
  export { MarqueeRect, SelectionList, SelectionListProps, SelectionRenderer, SelectionRowRender, UseSelectionOptions, UseSelectionResult, useSelection } from './selection.js';
32
32
  export { D as DEFAULT_SELECTION_SHORTCUTS, a as SelectionCommand, b as SelectionCommandEvent, c as SelectionCommandName, d as SelectionInstance, e as SelectionOptions, f as SelectionShortcuts, g as SelectionState, h as createSelection } from '../selection-B_pmzHpy.js';
33
33
  export { S as Shortcut, b as ShortcutSpec, i as isApplePlatform, m as matchesShortcut, p as parseShortcut, e as shortcutText, f as shortcutTokens } from '../keys-D2zJs1uB.js';
34
34
  export { Image, ImageItem, ImageLabels, ImageMenuOptions, ImageProps, ImageSource, ZoomOptions } from './image.js';
35
35
  export { Video, VideoControls, VideoLabels, VideoProps, VideoSource, VideoTrack } from './video.js';
36
- export { C as ContextMenuContextValue, a as ContextMenuItem, b as ContextMenuNode, u as useContextMenuContext } from '../context-BfjYGPnH.js';
36
+ export { C as ContextMenuContextValue, a as ContextMenuItem, b as ContextMenuNode, u as useContextMenuContext } from '../context-DR1dpO8b.js';
37
37
 
38
38
  interface UseInputResult {
39
39
  /** Attach to the `<input>`. */
@@ -4,11 +4,11 @@ export { useSearch } from '../chunk-45UHLZYT.js';
4
4
  export { useNetworkState } from '../chunk-S653GLSF.js';
5
5
  export { RelativeTime } from '../chunk-OCMI7R6H.js';
6
6
  export { PasswordStrength } from '../chunk-XQHCZAPJ.js';
7
- export { ContextMenu, ContextMenuContent, ContextMenuList, ContextMenuPanel, ContextMenuRoot, ContextMenuRow, ContextMenuSearch, ContextMenuTrigger, useContextMenuContext } from '../chunk-DZMHY3SU.js';
7
+ export { ContextMenu, ContextMenuContent, ContextMenuList, ContextMenuPanel, ContextMenuRoot, ContextMenuRow, ContextMenuSearch, ContextMenuTrigger, useContextMenuContext } from '../chunk-RRPWJAIJ.js';
8
8
  export { SelectionList, useSelection } from '../chunk-N6PDHMAX.js';
9
- export { Image } from '../chunk-4SZ5N7BA.js';
9
+ export { Image } from '../chunk-IQMHDRJF.js';
10
10
  import '../chunk-EYZ366LP.js';
11
- export { Video } from '../chunk-JJVZYW5C.js';
11
+ export { Video } from '../chunk-JPBVKXOL.js';
12
12
  export { Button, getLinkComponent, setLinkComponent, useButton } from '../chunk-6BGBYUSZ.js';
13
13
  export { Flag } from '../chunk-NPHRS2Q7.js';
14
14
  export { Toaster, toast, useSonner } from '../chunk-WPTBURIC.js';
@@ -19,18 +19,19 @@ export { colorEquals, formatColor, hslToRgb, hsvToRgb, parseColor, rgbToHsl, rgb
19
19
  import '../chunk-2QFTRNAZ.js';
20
20
  import '../chunk-IXVMRVD4.js';
21
21
  export { estimatePasswordStrength, generatePassword } from '../chunk-HC2ME5PU.js';
22
- export { CLIPBOARD_PREFIX, clipboardAction, clipboardEntries, clipboardHasText, createContextMenu, inspectClipboardTarget, isAction, performClipboardAction } from '../chunk-Z3VDE7OA.js';
22
+ export { CLIPBOARD_PREFIX, clipboardAction, clipboardEntries, clipboardHasText, createContextMenu, inspectClipboardTarget, isAction, performClipboardAction } from '../chunk-YQV5XZIA.js';
23
23
  export { DEFAULT_SELECTION_SHORTCUTS, createSelection } from '../chunk-WSQC3PCC.js';
24
- import '../chunk-RUEQ3TGP.js';
24
+ import '../chunk-JLZDS6CV.js';
25
25
  import '../chunk-MMQPZGSU.js';
26
26
  export { configureFlags, flagAttributes, flagName, flagSrc, flagView, normalizeFlagCode } from '../chunk-LMZJBOZG.js';
27
27
  import '../chunk-SNYUBXWQ.js';
28
28
  import '../chunk-UOSSNUSC.js';
29
- export { Input } from '../chunk-RMNXVWQ4.js';
29
+ export { Input } from '../chunk-YHH5U2NW.js';
30
30
  import '../chunk-KVLSOQTI.js';
31
31
  import '../chunk-F25CGNQC.js';
32
32
  export { PaletteContent, PaletteField, PaletteFooter, PaletteItem, PaletteList, PaletteRoot, PaletteTrigger, SearchPalette, usePaletteContext } from '../chunk-FGBIZDV2.js';
33
- export { Select, SelectContent, SelectList, SelectOptionRow, SelectRoot, SelectSearch, SelectTrigger, SelectValue, useSelectContext } from '../chunk-VKL3DEIQ.js';
33
+ export { Select, SelectContent, SelectList, SelectOptionRow, SelectRoot, SelectSearch, SelectTrigger, SelectValue, useSelectContext } from '../chunk-6WIGVIQF.js';
34
+ import '../chunk-RXEO4WPI.js';
34
35
  export { createSelect } from '../chunk-NONGREXC.js';
35
36
  export { isApplePlatform, matchesShortcut, parseShortcut, shortcutText, shortcutTokens } from '../chunk-4VUHQFAT.js';
36
37
  export { Slot, mergeSlotProps } from '../chunk-HS3X3XCW.js';
@@ -1,4 +1,4 @@
1
1
  "use client";
2
- export { Input } from '../chunk-RMNXVWQ4.js';
2
+ export { Input } from '../chunk-YHH5U2NW.js';
3
3
  import '../chunk-KVLSOQTI.js';
4
4
  import '../chunk-F25CGNQC.js';
@@ -38,6 +38,11 @@ interface SelectContextValue {
38
38
  optionId: (index: number) => string;
39
39
  triggerRef: RefObject<HTMLButtonElement | null>;
40
40
  fieldRef: RefObject<HTMLInputElement | null>;
41
+ /**
42
+ * The panel. Portaled out of the root, so "inside the select" is the root OR this - a
43
+ * press or a focus check against the root alone would call every option outside.
44
+ */
45
+ contentRef: RefObject<HTMLDivElement | null>;
41
46
  /** Close and put focus back where it came from - the trigger, always. */
42
47
  close: () => void;
43
48
  /** The keys the list answers to, shared by the trigger and the search field. */
@@ -167,7 +172,7 @@ declare function SelectOptionRow({ option, index, children, ...props }: SelectOp
167
172
  * override a selector for the rest. The sheet is PREPENDED to `<head>`, so anything the
168
173
  * document already has wins on source order at equal specificity.
169
174
  */
170
- declare const SELECT_STYLES = "\n:root {\n --enigma-select-bg: #171717;\n --enigma-select-border: #404040;\n --enigma-select-border-focus: #a3a3a3;\n --enigma-select-text: #f5f5f5;\n --enigma-select-muted: #a3a3a3;\n --enigma-select-radius: 0.5rem;\n --enigma-select-font-size: 0.875rem;\n --enigma-select-padding: 0.5rem 0.75rem;\n\n --enigma-select-panel-bg: #1c1c1c;\n --enigma-select-panel-border: #333333;\n --enigma-select-panel-radius: 0.625rem;\n --enigma-select-panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);\n --enigma-select-panel-width: 100%;\n --enigma-select-list-height: 15rem;\n\n --enigma-select-active-bg: #2a2a2a;\n --enigma-select-accent: #fbbf24;\n --enigma-select-tag-bg: #2a2a2a;\n}\n\n@media (prefers-color-scheme: light) {\n :root {\n --enigma-select-bg: #ffffff;\n --enigma-select-border: #d4d4d4;\n --enigma-select-border-focus: #737373;\n --enigma-select-text: #171717;\n --enigma-select-muted: #737373;\n --enigma-select-panel-bg: #ffffff;\n --enigma-select-panel-border: #e5e5e5;\n --enigma-select-panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);\n --enigma-select-active-bg: #f5f5f5;\n --enigma-select-accent: #b45309;\n --enigma-select-tag-bg: #f0f0f0;\n }\n}\n\n[data-enigma-select-root] { position: relative; display: inline-block; min-width: 0; }\n\n[data-enigma-select-trigger] {\n display: flex; align-items: center; gap: 0.5rem;\n width: 100%; min-width: 0; box-sizing: border-box;\n padding: var(--enigma-select-padding);\n font: inherit; font-size: var(--enigma-select-font-size); text-align: left;\n color: var(--enigma-select-text); background: var(--enigma-select-bg);\n border: 1px solid var(--enigma-select-border); border-radius: var(--enigma-select-radius);\n cursor: pointer;\n}\n[data-enigma-select-trigger]:focus-visible { outline: 2px solid var(--enigma-select-border-focus); outline-offset: 1px; }\n[data-enigma-select-trigger][data-open] { border-color: var(--enigma-select-border-focus); }\n[data-enigma-select-trigger]:disabled,\n[data-enigma-select-trigger][aria-disabled=\"true\"] { opacity: 0.5; cursor: not-allowed; }\n/* Nothing to choose, or nothing yet: the control reads as unavailable and the caret fades,\n because a caret at full strength promises a panel that will never open. Not the disabled\n look, which would say the field is off rather than that the list is empty. */\n[data-enigma-select-trigger][data-empty],\n[data-enigma-select-trigger][data-loading] { opacity: 1; cursor: default; }\n[data-enigma-select-trigger][data-empty] [data-enigma-select-caret],\n[data-enigma-select-trigger][data-loading] [data-enigma-select-caret] { opacity: 0.25; }\n\n/* The indicator: one slot, holding the caret and the \u00D7 stacked in the same cell. Side by\n side they would be two targets a pixel apart - one of which throws away your choice - and\n the pair would change the trigger's width the moment a value appeared. */\n[data-enigma-select-indicator] {\n display: grid; place-items: center; flex: none;\n margin-left: auto; width: 1rem; height: 1rem;\n}\n[data-enigma-select-indicator] > * { grid-area: 1 / 1; }\n\n/* The caret, drawn rather than loaded: an icon font or an SVG file is a request, and this\n is two borders. */\n[data-enigma-select-caret] {\n width: 0.4rem; height: 0.4rem;\n /* Decoration, and it must not be the hit target: the rotation gives it a stacking\n context, which paints it OVER the \u00D7 sharing its cell and swallows the click. */\n pointer-events: none;\n border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;\n transform: translateY(-0.1rem) rotate(45deg);\n opacity: 0.6; transition: transform 120ms ease-out, opacity 120ms ease-out;\n}\n[data-enigma-select-trigger][data-open] [data-enigma-select-caret] { transform: translateY(0.1rem) rotate(225deg); }\n\n[data-enigma-select-value] {\n display: flex; align-items: center; gap: 0.375rem;\n /* min-width: 0 is what lets the ellipsis happen: a flex item's default minimum is its\n content, so without it a long label pushes the caret out of the button. */\n min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;\n}\n[data-enigma-select-value][data-placeholder] { color: var(--enigma-select-muted); }\n[data-enigma-select-value][data-tags] { flex-wrap: wrap; white-space: normal; }\n\n[data-enigma-select-tag] {\n display: inline-flex; align-items: center; gap: 0.25rem;\n padding: 0.125rem 0.25rem 0.125rem 0.5rem;\n font-size: 0.8125rem; line-height: 1.4;\n background: var(--enigma-select-tag-bg); border-radius: 999px;\n}\n[data-enigma-select-tag][data-rest] { padding: 0.125rem 0.5rem; color: var(--enigma-select-muted); }\n\n[data-enigma-select-tag-remove] {\n display: grid; place-items: center;\n width: 1rem; height: 1rem; border-radius: 999px;\n color: var(--enigma-select-muted); font-size: 0.875rem; line-height: 1; cursor: pointer;\n}\n[data-enigma-select-tag-remove]:hover { color: var(--enigma-select-text); background: var(--enigma-select-active-bg); }\n\n/* The \u00D7, in the caret's place and only while the control is under the pointer or holds\n focus. \":focus\" and not \":focus-visible\": a tap focuses the button, and on a touch screen\n there is no hover to reveal it with. */\n[data-enigma-select-clear] {\n display: grid; place-items: center;\n width: 1rem; height: 1rem; border-radius: 999px;\n color: var(--enigma-select-muted); font-size: 0.875rem; line-height: 1; cursor: pointer;\n opacity: 0; pointer-events: none; transition: opacity 120ms ease-out;\n}\n[data-enigma-select-trigger]:hover [data-enigma-select-clear],\n[data-enigma-select-trigger]:focus [data-enigma-select-clear] { opacity: 1; pointer-events: auto; }\n/* Scoped to a trigger that HAS a \u00D7 - otherwise hovering a select with nothing to clear\n would hide its caret and leave an empty slot. */\n[data-enigma-select-trigger][data-clearable]:hover [data-enigma-select-caret],\n[data-enigma-select-trigger][data-clearable]:focus [data-enigma-select-caret] { opacity: 0; }\n[data-enigma-select-clear]:hover { color: var(--enigma-select-text); background: var(--enigma-select-active-bg); }\n\n[data-enigma-select-content] {\n position: absolute; z-index: 50;\n top: calc(100% + 0.25rem); left: 0;\n min-width: var(--enigma-select-panel-width); box-sizing: border-box;\n padding: 0.25rem;\n background: var(--enigma-select-panel-bg);\n border: 1px solid var(--enigma-select-panel-border);\n border-radius: var(--enigma-select-panel-radius);\n box-shadow: var(--enigma-select-panel-shadow);\n animation: enigma-select-in 120ms ease-out;\n /* A bound, because the content is arbitrary: a long option label or a query typed into\n the filter would otherwise widen the panel with it. min-width still wins for a\n trigger wider than this, which is what it is for. */\n max-width: min(28rem, calc(100vw - 2rem));\n}\n/* Opening upwards is not a variant, it is the same panel measured against the window: near\n the bottom of the screen the list would otherwise be unreachable. */\n[data-enigma-select-content][data-side=\"top\"] { top: auto; bottom: calc(100% + 0.25rem); }\n[data-enigma-select-content][data-state=\"closed\"] { animation: enigma-select-out 120ms ease-in forwards; pointer-events: none; }\n\n@keyframes enigma-select-in {\n from { opacity: 0; transform: translateY(-0.25rem); }\n to { opacity: 1; transform: none; }\n}\n@keyframes enigma-select-out {\n from { opacity: 1; }\n to { opacity: 0; transform: translateY(-0.125rem); }\n}\n@media (prefers-reduced-motion: reduce) {\n [data-enigma-select-content],\n [data-enigma-select-content][data-state=\"closed\"] { animation: none; }\n [data-enigma-select-trigger]::after { transition: none; }\n}\n\n[data-enigma-select-search] {\n width: 100%; box-sizing: border-box;\n padding: 0.375rem 0.5rem 0.5rem;\n font: inherit; font-size: var(--enigma-select-font-size);\n color: var(--enigma-select-text); background: transparent;\n border: 0; border-bottom: 1px solid var(--enigma-select-panel-border);\n outline: none;\n}\n/* WebKit's own clear cross, gone: it is drawn on no other engine, so leaving it in gives\n half the visitors a control the other half never sees. */\n[data-enigma-select-search]::-webkit-search-cancel-button,\n[data-enigma-select-search]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }\n\n[data-enigma-select-list] {\n max-height: var(--enigma-select-list-height);\n margin-top: 0.25rem;\n overflow-y: auto; overscroll-behavior: contain;\n}\n\n[data-enigma-select-group-label] {\n margin: 0.375rem 0 0.125rem; padding: 0 0.5rem;\n font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;\n color: var(--enigma-select-muted);\n}\n\n[data-enigma-select-option] {\n display: flex; align-items: center; gap: 0.5rem;\n padding: 0.4375rem 0.5rem; border-radius: 0.375rem;\n font-size: var(--enigma-select-font-size); color: var(--enigma-select-text);\n cursor: pointer;\n}\n[data-enigma-select-option][data-active] { background: var(--enigma-select-active-bg); }\n[data-enigma-select-option][data-disabled] { opacity: 0.45; cursor: not-allowed; }\n\n[data-enigma-select-option-text] { display: grid; min-width: 0; }\n[data-enigma-select-option-label] { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }\n[data-enigma-select-option-description] {\n font-size: 0.75rem; color: var(--enigma-select-muted);\n overflow: hidden; white-space: nowrap; text-overflow: ellipsis;\n}\n\n[data-enigma-select-icon] { display: inline-flex; align-items: center; flex: none; max-height: 1.25em; line-height: 0; }\n\n/* The check, drawn with two borders and shown only where it belongs. Reserved space either\n way, so choosing a row does not shift the label under the pointer. */\n[data-enigma-select-check] {\n flex: none; margin-left: auto;\n width: 0.35rem; height: 0.6rem;\n border-right: 2px solid var(--enigma-select-accent); border-bottom: 2px solid var(--enigma-select-accent);\n transform: rotate(45deg) translate(-0.05rem, -0.05rem);\n opacity: 0;\n}\n[data-enigma-select-option][data-selected] [data-enigma-select-check] { opacity: 1; }\n\n/* The end-of-window marker: one pixel, so it has a box to be scrolled into. */\n[data-enigma-select-more] { height: 1px; }\n\n[data-enigma-select-empty] {\n margin: 0; padding: 0.75rem 0.5rem;\n font-size: var(--enigma-select-font-size); color: var(--enigma-select-muted);\n /* The quoted query is cut in the text as well, but a pasted string with no spaces has\n no break opportunity at all - so this is what stops it stretching the panel. */\n overflow-wrap: anywhere;\n}\n";
175
+ declare const SELECT_STYLES = "\n:root {\n --enigma-select-bg: #171717;\n --enigma-select-border: #404040;\n --enigma-select-border-focus: #a3a3a3;\n --enigma-select-text: #f5f5f5;\n --enigma-select-muted: #a3a3a3;\n --enigma-select-radius: 0.5rem;\n --enigma-select-font-size: 0.875rem;\n --enigma-select-padding: 0.5rem 0.75rem;\n\n --enigma-select-panel-bg: #1c1c1c;\n --enigma-select-panel-border: #333333;\n --enigma-select-panel-radius: 0.625rem;\n --enigma-select-panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);\n --enigma-select-list-height: 15rem;\n\n --enigma-select-active-bg: #2a2a2a;\n --enigma-select-accent: #fbbf24;\n --enigma-select-tag-bg: #2a2a2a;\n\n /* The layer every portaled popup shares: above the lightbox (9999), because a popup is\n opened FROM whatever is on top. The select, the colour picker and the context menu\n each declare it with the same value, so overriding it once moves all three. Here and\n not in the light-scheme block, or half the readers get an invalid z-index. */\n --enigma-floating-z: 10000;\n}\n\n@media (prefers-color-scheme: light) {\n :root {\n --enigma-select-bg: #ffffff;\n --enigma-select-border: #d4d4d4;\n --enigma-select-border-focus: #737373;\n --enigma-select-text: #171717;\n --enigma-select-muted: #737373;\n --enigma-select-panel-bg: #ffffff;\n --enigma-select-panel-border: #e5e5e5;\n --enigma-select-panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);\n --enigma-select-active-bg: #f5f5f5;\n --enigma-select-accent: #b45309;\n --enigma-select-tag-bg: #f0f0f0;\n }\n}\n\n[data-enigma-select-root] { position: relative; display: inline-block; min-width: 0; }\n\n[data-enigma-select-trigger] {\n display: flex; align-items: center; gap: 0.5rem;\n width: 100%; min-width: 0; box-sizing: border-box;\n padding: var(--enigma-select-padding);\n font: inherit; font-size: var(--enigma-select-font-size); text-align: left;\n color: var(--enigma-select-text); background: var(--enigma-select-bg);\n border: 1px solid var(--enigma-select-border); border-radius: var(--enigma-select-radius);\n cursor: pointer;\n}\n[data-enigma-select-trigger]:focus-visible { outline: 2px solid var(--enigma-select-border-focus); outline-offset: 1px; }\n[data-enigma-select-trigger][data-open] { border-color: var(--enigma-select-border-focus); }\n[data-enigma-select-trigger]:disabled,\n[data-enigma-select-trigger][aria-disabled=\"true\"] { opacity: 0.5; cursor: not-allowed; }\n/* Nothing to choose, or nothing yet: the control reads as unavailable and the caret fades,\n because a caret at full strength promises a panel that will never open. Not the disabled\n look, which would say the field is off rather than that the list is empty. */\n[data-enigma-select-trigger][data-empty],\n[data-enigma-select-trigger][data-loading] { opacity: 1; cursor: default; }\n[data-enigma-select-trigger][data-empty] [data-enigma-select-caret],\n[data-enigma-select-trigger][data-loading] [data-enigma-select-caret] { opacity: 0.25; }\n\n/* The indicator: one slot, holding the caret and the \u00D7 stacked in the same cell. Side by\n side they would be two targets a pixel apart - one of which throws away your choice - and\n the pair would change the trigger's width the moment a value appeared. */\n[data-enigma-select-indicator] {\n display: grid; place-items: center; flex: none;\n margin-left: auto; width: 1rem; height: 1rem;\n}\n[data-enigma-select-indicator] > * { grid-area: 1 / 1; }\n\n/* The caret, drawn rather than loaded: an icon font or an SVG file is a request, and this\n is two borders. */\n[data-enigma-select-caret] {\n width: 0.4rem; height: 0.4rem;\n /* Decoration, and it must not be the hit target: the rotation gives it a stacking\n context, which paints it OVER the \u00D7 sharing its cell and swallows the click. */\n pointer-events: none;\n border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;\n transform: translateY(-0.1rem) rotate(45deg);\n opacity: 0.6; transition: transform 120ms ease-out, opacity 120ms ease-out;\n}\n[data-enigma-select-trigger][data-open] [data-enigma-select-caret] { transform: translateY(0.1rem) rotate(225deg); }\n\n[data-enigma-select-value] {\n display: flex; align-items: center; gap: 0.375rem;\n /* min-width: 0 is what lets the ellipsis happen: a flex item's default minimum is its\n content, so without it a long label pushes the caret out of the button. */\n min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;\n}\n[data-enigma-select-value][data-placeholder] { color: var(--enigma-select-muted); }\n[data-enigma-select-value][data-tags] { flex-wrap: wrap; white-space: normal; }\n\n[data-enigma-select-tag] {\n display: inline-flex; align-items: center; gap: 0.25rem;\n padding: 0.125rem 0.25rem 0.125rem 0.5rem;\n font-size: 0.8125rem; line-height: 1.4;\n background: var(--enigma-select-tag-bg); border-radius: 999px;\n}\n[data-enigma-select-tag][data-rest] { padding: 0.125rem 0.5rem; color: var(--enigma-select-muted); }\n\n[data-enigma-select-tag-remove] {\n display: grid; place-items: center;\n width: 1rem; height: 1rem; border-radius: 999px;\n color: var(--enigma-select-muted); font-size: 0.875rem; line-height: 1; cursor: pointer;\n}\n[data-enigma-select-tag-remove]:hover { color: var(--enigma-select-text); background: var(--enigma-select-active-bg); }\n\n/* The \u00D7, in the caret's place and only while the control is under the pointer or holds\n focus. \":focus\" and not \":focus-visible\": a tap focuses the button, and on a touch screen\n there is no hover to reveal it with. */\n[data-enigma-select-clear] {\n display: grid; place-items: center;\n width: 1rem; height: 1rem; border-radius: 999px;\n color: var(--enigma-select-muted); font-size: 0.875rem; line-height: 1; cursor: pointer;\n opacity: 0; pointer-events: none; transition: opacity 120ms ease-out;\n}\n[data-enigma-select-trigger]:hover [data-enigma-select-clear],\n[data-enigma-select-trigger]:focus [data-enigma-select-clear] { opacity: 1; pointer-events: auto; }\n/* Scoped to a trigger that HAS a \u00D7 - otherwise hovering a select with nothing to clear\n would hide its caret and leave an empty slot. */\n[data-enigma-select-trigger][data-clearable]:hover [data-enigma-select-caret],\n[data-enigma-select-trigger][data-clearable]:focus [data-enigma-select-caret] { opacity: 0; }\n[data-enigma-select-clear]:hover { color: var(--enigma-select-text); background: var(--enigma-select-active-bg); }\n\n/* Portaled to <body> and placed in viewport coordinates against the trigger (see\n react/floating.ts), so no ancestor's overflow can clip it and no dialog can stack over it.\n left, top, max-height and data-side are written by the placement: below the trigger,\n flipped above when there is more room there, and capped to the room it has. The column\n is what makes the cap shrink the LIST, which scrolls, instead of cutting off the panel. */\n[data-enigma-select-content] {\n position: fixed; z-index: var(--enigma-floating-z);\n display: flex; flex-direction: column;\n /* As wide as the trigger, measured - 100% would now be a percentage of the window - and\n wider only for content that cannot shrink. min-content is what keeps the rest at the\n trigger's width: fixed, the panel's room is the whole window, and a long label or a\n long \"Nothing matches\" would otherwise widen it to max-width. --enigma-select-panel-width\n overrides the trigger's width. */\n width: min-content;\n min-width: var(--enigma-select-panel-width, var(--enigma-anchor-width, auto)); box-sizing: border-box;\n padding: 0.25rem;\n color: var(--enigma-select-text);\n font-size: var(--enigma-select-font-size);\n background: var(--enigma-select-panel-bg);\n border: 1px solid var(--enigma-select-panel-border);\n border-radius: var(--enigma-select-panel-radius);\n box-shadow: var(--enigma-select-panel-shadow);\n animation: enigma-select-in 120ms ease-out;\n /* A bound, because the content is arbitrary: a long option label or a query typed into\n the filter would otherwise widen the panel with it. min-width still wins for a\n trigger wider than this, which is what it is for. */\n max-width: min(28rem, calc(100vw - 2rem));\n}\n[data-enigma-select-content][data-state=\"closed\"] { animation: enigma-select-out 120ms ease-in forwards; pointer-events: none; }\n\n@keyframes enigma-select-in {\n from { opacity: 0; transform: translateY(-0.25rem); }\n to { opacity: 1; transform: none; }\n}\n@keyframes enigma-select-out {\n from { opacity: 1; }\n to { opacity: 0; transform: translateY(-0.125rem); }\n}\n@media (prefers-reduced-motion: reduce) {\n [data-enigma-select-content],\n [data-enigma-select-content][data-state=\"closed\"] { animation: none; }\n [data-enigma-select-trigger]::after { transition: none; }\n}\n\n[data-enigma-select-search] {\n width: 100%; box-sizing: border-box;\n padding: 0.375rem 0.5rem 0.5rem;\n font: inherit; font-size: var(--enigma-select-font-size);\n color: var(--enigma-select-text); background: transparent;\n border: 0; border-bottom: 1px solid var(--enigma-select-panel-border);\n outline: none;\n}\n/* WebKit's own clear cross, gone: it is drawn on no other engine, so leaving it in gives\n half the visitors a control the other half never sees. */\n[data-enigma-select-search]::-webkit-search-cancel-button,\n[data-enigma-select-search]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }\n\n[data-enigma-select-list] {\n max-height: var(--enigma-select-list-height);\n /* Allowed below its content, so the panel's placement cap shrinks it and it scrolls. */\n min-height: 0; flex: 0 1 auto;\n margin-top: 0.25rem;\n overflow-y: auto; overscroll-behavior: contain;\n}\n\n[data-enigma-select-group-label] {\n margin: 0.375rem 0 0.125rem; padding: 0 0.5rem;\n font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;\n color: var(--enigma-select-muted);\n}\n\n[data-enigma-select-option] {\n display: flex; align-items: center; gap: 0.5rem;\n padding: 0.4375rem 0.5rem; border-radius: 0.375rem;\n font-size: var(--enigma-select-font-size); color: var(--enigma-select-text);\n cursor: pointer;\n}\n[data-enigma-select-option][data-active] { background: var(--enigma-select-active-bg); }\n[data-enigma-select-option][data-disabled] { opacity: 0.45; cursor: not-allowed; }\n\n[data-enigma-select-option-text] { display: grid; min-width: 0; }\n[data-enigma-select-option-label] { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }\n[data-enigma-select-option-description] {\n font-size: 0.75rem; color: var(--enigma-select-muted);\n overflow: hidden; white-space: nowrap; text-overflow: ellipsis;\n}\n\n[data-enigma-select-icon] { display: inline-flex; align-items: center; flex: none; max-height: 1.25em; line-height: 0; }\n\n/* The check, drawn with two borders and shown only where it belongs. Reserved space either\n way, so choosing a row does not shift the label under the pointer. */\n[data-enigma-select-check] {\n flex: none; margin-left: auto;\n width: 0.35rem; height: 0.6rem;\n border-right: 2px solid var(--enigma-select-accent); border-bottom: 2px solid var(--enigma-select-accent);\n transform: rotate(45deg) translate(-0.05rem, -0.05rem);\n opacity: 0;\n}\n[data-enigma-select-option][data-selected] [data-enigma-select-check] { opacity: 1; }\n\n/* The end-of-window marker: one pixel, so it has a box to be scrolled into. */\n[data-enigma-select-more] { height: 1px; }\n\n[data-enigma-select-empty] {\n margin: 0; padding: 0.75rem 0.5rem;\n font-size: var(--enigma-select-font-size); color: var(--enigma-select-muted);\n /* The quoted query is cut in the text as well, but a pasted string with no spaces has\n no break opportunity at all - so this is what stops it stretching the panel. */\n overflow-wrap: anywhere;\n}\n";
171
176
 
172
177
  type SelectProps = SelectRootProps & {
173
178
  /** What the trigger says with nothing chosen. */
@@ -1,5 +1,6 @@
1
1
  "use client";
2
- export { SELECT_STYLES, Select, SelectContent, SelectList, SelectOptionRow, SelectRoot, SelectSearch, SelectTrigger, SelectValue, useSelectContext } from '../chunk-VKL3DEIQ.js';
2
+ export { SELECT_STYLES, Select, SelectContent, SelectList, SelectOptionRow, SelectRoot, SelectSearch, SelectTrigger, SelectValue, useSelectContext } from '../chunk-6WIGVIQF.js';
3
+ import '../chunk-RXEO4WPI.js';
3
4
  export { SELECT_SEARCH_KEYS, createSelect } from '../chunk-NONGREXC.js';
4
5
  import '../chunk-4VUHQFAT.js';
5
6
  import '../chunk-HS3X3XCW.js';
@@ -1,6 +1,6 @@
1
1
  import { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- import { b as ContextMenuNode, a as ContextMenuItem } from '../context-BfjYGPnH.js';
3
- import '../context-menu-D3FtTn7v.js';
2
+ import { b as ContextMenuNode, a as ContextMenuItem } from '../context-DR1dpO8b.js';
3
+ import '../context-menu-qqpGBkny.js';
4
4
  import '../search-DYgqRp37.js';
5
5
 
6
6
  /**
@@ -14,7 +14,13 @@ import '../search-DYgqRp37.js';
14
14
 
15
15
  interface VideoSource {
16
16
  src: string;
17
- /** `video/mp4`, `video/webm`. The browser picks the first one it can play. */
17
+ /**
18
+ * `video/mp4`, `video/webm`. The browser picks the first one it can play.
19
+ *
20
+ * Inferred from the file extension when it is left out, and left off entirely when the
21
+ * extension means nothing - the browser then fetches and sniffs, which always works. A
22
+ * WRONG type is the expensive mistake: the source is skipped without being tried.
23
+ */
18
24
  type?: string;
19
25
  }
20
26
  interface VideoTrack {
@@ -1,3 +1,3 @@
1
1
  "use client";
2
- export { Video } from '../chunk-JJVZYW5C.js';
3
- import '../chunk-RUEQ3TGP.js';
2
+ export { Video } from '../chunk-JPBVKXOL.js';
3
+ import '../chunk-JLZDS6CV.js';
@@ -24,14 +24,14 @@ export { R as RecentEntry, b as RecentStore, e as createRecentStore, g as groupR
24
24
  export { Select, SelectContent, SelectContextValue, SelectItem, SelectList, SelectMultipleProps, SelectOptionRow, SelectProps, SelectRoot, SelectRootProps, SelectSearch, SelectSingleProps, SelectTrigger, SelectValue, useSelectContext } from '../react/select.js';
25
25
  export { a as SelectInstance, c as SelectOption, d as SelectOptions, e as SelectState, f as createSelect } from '../select-ClSy-J1f.js';
26
26
  export { ContextMenu, ContextMenuContent, ContextMenuContentProps, ContextMenuList, ContextMenuPanel, ContextMenuProps, ContextMenuRoot, ContextMenuRootProps, ContextMenuRow, ContextMenuSearch, ContextMenuTrigger, ContextMenuTriggerProps } from '../react/context-menu.js';
27
- export { C as CLIPBOARD_PREFIX, a as ClipboardAction, b as ClipboardMenuLabels, c as ClipboardMenuOptions, d as ClipboardTarget, e as clipboardAction, f as clipboardEntries, g as clipboardHasText, i as inspectClipboardTarget, p as performClipboardAction } from '../clipboard-menu-BvXJCSFN.js';
28
- export { a as ContextMenuAction, b as ContextMenuEntry, c as ContextMenuInstance, g as ContextMenuOptions, j as ContextMenuState, k as createContextMenu, l as isAction } from '../context-menu-D3FtTn7v.js';
27
+ export { C as CLIPBOARD_PREFIX, a as ClipboardAction, b as ClipboardMenuLabels, c as ClipboardMenuOptions, d as ClipboardTarget, e as clipboardAction, f as clipboardEntries, g as clipboardHasText, i as inspectClipboardTarget, p as performClipboardAction } from '../clipboard-menu-eemzEBqB.js';
28
+ export { a as ContextMenuAction, b as ContextMenuEntry, c as ContextMenuInstance, g as ContextMenuOptions, j as ContextMenuState, k as createContextMenu, l as isAction } from '../context-menu-qqpGBkny.js';
29
29
  export { MarqueeRect, SelectionList, SelectionListProps, SelectionRenderer, SelectionRowRender, UseSelectionOptions, UseSelectionResult, useSelection } from '../react/selection.js';
30
30
  export { D as DEFAULT_SELECTION_SHORTCUTS, a as SelectionCommand, b as SelectionCommandEvent, c as SelectionCommandName, d as SelectionInstance, e as SelectionOptions, f as SelectionShortcuts, g as SelectionState, h as createSelection } from '../selection-B_pmzHpy.js';
31
31
  export { S as Shortcut, b as ShortcutSpec, i as isApplePlatform, m as matchesShortcut, p as parseShortcut, e as shortcutText, f as shortcutTokens } from '../keys-D2zJs1uB.js';
32
32
  export { Image, ImageItem, ImageLabels, ImageMenuOptions, ImageProps, ImageSource, ZoomOptions } from '../react/image.js';
33
33
  export { Video, VideoControls, VideoLabels, VideoProps, VideoSource, VideoTrack } from '../react/video.js';
34
- export { C as ContextMenuContextValue, a as ContextMenuItem, b as ContextMenuNode, u as useContextMenuContext } from '../context-BfjYGPnH.js';
34
+ export { C as ContextMenuContextValue, a as ContextMenuItem, b as ContextMenuNode, u as useContextMenuContext } from '../context-DR1dpO8b.js';
35
35
 
36
36
  declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLElement>>;
37
37
 
@@ -4,11 +4,11 @@ export { useSearch } from '../chunk-45UHLZYT.js';
4
4
  export { useNetworkState } from '../chunk-S653GLSF.js';
5
5
  export { RelativeTime } from '../chunk-OCMI7R6H.js';
6
6
  export { PasswordStrength } from '../chunk-XQHCZAPJ.js';
7
- export { ContextMenu, ContextMenuContent, ContextMenuList, ContextMenuPanel, ContextMenuRoot, ContextMenuRow, ContextMenuSearch, ContextMenuTrigger, useContextMenuContext } from '../chunk-DZMHY3SU.js';
7
+ export { ContextMenu, ContextMenuContent, ContextMenuList, ContextMenuPanel, ContextMenuRoot, ContextMenuRow, ContextMenuSearch, ContextMenuTrigger, useContextMenuContext } from '../chunk-RRPWJAIJ.js';
8
8
  export { SelectionList, useSelection } from '../chunk-N6PDHMAX.js';
9
- export { Image } from '../chunk-4SZ5N7BA.js';
9
+ export { Image } from '../chunk-IQMHDRJF.js';
10
10
  import '../chunk-EYZ366LP.js';
11
- export { Video } from '../chunk-JJVZYW5C.js';
11
+ export { Video } from '../chunk-JPBVKXOL.js';
12
12
  import { Button } from '../chunk-6BGBYUSZ.js';
13
13
  export { getLinkComponent, setLinkComponent, useButton } from '../chunk-6BGBYUSZ.js';
14
14
  export { Flag } from '../chunk-NPHRS2Q7.js';
@@ -20,18 +20,19 @@ export { colorEquals, formatColor, hslToRgb, hsvToRgb, parseColor, rgbToHsl, rgb
20
20
  import '../chunk-2QFTRNAZ.js';
21
21
  import '../chunk-IXVMRVD4.js';
22
22
  export { estimatePasswordStrength, generatePassword } from '../chunk-HC2ME5PU.js';
23
- export { CLIPBOARD_PREFIX, clipboardAction, clipboardEntries, clipboardHasText, createContextMenu, inspectClipboardTarget, isAction, performClipboardAction } from '../chunk-Z3VDE7OA.js';
23
+ export { CLIPBOARD_PREFIX, clipboardAction, clipboardEntries, clipboardHasText, createContextMenu, inspectClipboardTarget, isAction, performClipboardAction } from '../chunk-YQV5XZIA.js';
24
24
  export { DEFAULT_SELECTION_SHORTCUTS, createSelection } from '../chunk-WSQC3PCC.js';
25
- import '../chunk-RUEQ3TGP.js';
25
+ import '../chunk-JLZDS6CV.js';
26
26
  import '../chunk-MMQPZGSU.js';
27
27
  export { configureFlags, flagAttributes, flagName, flagSrc, flagView, normalizeFlagCode } from '../chunk-LMZJBOZG.js';
28
28
  import '../chunk-SNYUBXWQ.js';
29
29
  import '../chunk-UOSSNUSC.js';
30
- export { Input } from '../chunk-RMNXVWQ4.js';
30
+ export { Input } from '../chunk-YHH5U2NW.js';
31
31
  import '../chunk-KVLSOQTI.js';
32
32
  import '../chunk-F25CGNQC.js';
33
33
  export { PaletteContent, PaletteField, PaletteFooter, PaletteItem, PaletteList, PaletteRoot, PaletteTrigger, SearchPalette, usePaletteContext } from '../chunk-FGBIZDV2.js';
34
- export { Select, SelectContent, SelectList, SelectOptionRow, SelectRoot, SelectSearch, SelectTrigger, SelectValue, useSelectContext } from '../chunk-VKL3DEIQ.js';
34
+ export { Select, SelectContent, SelectList, SelectOptionRow, SelectRoot, SelectSearch, SelectTrigger, SelectValue, useSelectContext } from '../chunk-6WIGVIQF.js';
35
+ import '../chunk-RXEO4WPI.js';
35
36
  export { createSelect } from '../chunk-NONGREXC.js';
36
37
  export { isApplePlatform, matchesShortcut, parseShortcut, shortcutText, shortcutTokens } from '../chunk-4VUHQFAT.js';
37
38
  export { Slot, mergeSlotProps } from '../chunk-HS3X3XCW.js';
@@ -0,0 +1,3 @@
1
+ export { ImageViewer, MenuButton } from './chunk-CB7K6VW4.js';
2
+ import './chunk-HUUINHPR.js';
3
+ import './chunk-EYZ366LP.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/primitives",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Headless components: the behaviour, the timing and the accessibility, with no styles of their own. Framework-agnostic core plus thin adapters.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -24,6 +24,11 @@
24
24
  on, which is why only one colour is a property. */
25
25
  --enigma-color-checker: rgba(255, 255, 255, 0.22);
26
26
  --enigma-color-checker-size: 0.5rem;
27
+
28
+ /* The layer every portaled popup shares, declared with the same value by the select and
29
+ the context menu: above the lightbox (9999), because a popup is opened FROM whatever
30
+ is on top. Not in the light-scheme block, or half the readers get no z-index. */
31
+ --enigma-floating-z: 10000;
27
32
  }
28
33
 
29
34
  @media (prefers-color-scheme: light) {
@@ -40,9 +45,7 @@
40
45
  }
41
46
  }
42
47
 
43
- /* The anchor the panel hangs from. Inline so it sits in the field's own row, and the
44
- positioning context so the panel needs no portal and no scroll listener: it moves with
45
- the field because it is INSIDE it. */
48
+ /* The anchor the panel is placed against. Inline so it sits in the field's own row. */
46
49
  [data-enigma-color] { position: relative; display: inline-flex; flex: none; }
47
50
 
48
51
  [data-enigma-color-swatch] {
@@ -66,8 +69,13 @@
66
69
  rather than the last good colour: a stale swatch says the value is fine when it is not. */
67
70
  [data-enigma-color-swatch][data-invalid] [data-enigma-color-fill] { background: none; }
68
71
 
72
+ /* Portaled and placed in viewport coordinates against the swatch (see react/floating.ts),
73
+ so a dialog or a card with overflow: hidden cannot cut it off. left, top, max-height and
74
+ data-side are written by the placement: below, flipped above when there is more room
75
+ there, and capped to the room it has - scrolling inside rather than off the screen. */
69
76
  [data-enigma-color-panel] {
70
- position: absolute; z-index: 60; top: calc(100% + 0.375rem); left: 0;
77
+ position: fixed; z-index: var(--enigma-floating-z);
78
+ overflow-y: auto; overscroll-behavior: contain;
71
79
  display: grid; gap: 0.625rem;
72
80
  box-sizing: border-box; width: var(--enigma-color-panel-width); padding: 0.625rem;
73
81
  color: var(--enigma-color-text); background: var(--enigma-color-panel-bg);
@@ -75,9 +83,6 @@
75
83
  border-radius: var(--enigma-color-panel-radius);
76
84
  box-shadow: var(--enigma-color-panel-shadow);
77
85
  }
78
- /* Flipped when the field sits near the bottom of the window. Without it the panel opens off
79
- the screen and the controls are unreachable, which is the same defect the select fixes. */
80
- [data-enigma-color-panel][data-side="top"] { top: auto; bottom: calc(100% + 0.375rem); }
81
86
 
82
87
  [data-enigma-color-area] {
83
88
  position: relative; height: var(--enigma-color-area-height);
@@ -21,6 +21,16 @@
21
21
  --enigma-menu-accent: #fbbf24;
22
22
  --enigma-menu-danger: #f87171;
23
23
  --enigma-menu-danger-bg: rgba(248, 113, 113, 0.12);
24
+
25
+ /* Above the lightbox, which sits at 9999, because a menu is opened FROM whatever is on
26
+ top: the image viewer's own three-dot menu was portalled to the same body and painted
27
+ underneath it, so its rows could be neither seen nor pressed. A menu is the topmost
28
+ transient surface on a page, and this is the number that says so. It belongs here and
29
+ not in the light-scheme block, or half the readers get an invalid z-index.
30
+ --enigma-floating-z is the layer the select and the colour picker share, declared with
31
+ the same value in their sheets, so overriding it once moves every popup together. */
32
+ --enigma-floating-z: 10000;
33
+ --enigma-menu-z: var(--enigma-floating-z);
24
34
  }
25
35
 
26
36
  @media (prefers-color-scheme: light) {
@@ -41,7 +51,10 @@
41
51
  and what a menu opened at the pointer needs. Fixed rather than absolute so a scrolling
42
52
  ancestor cannot drag it away from the place it was opened. */
43
53
  [data-enigma-menu-panel] {
44
- position: fixed; z-index: 60;
54
+ position: fixed; z-index: var(--enigma-menu-z);
55
+ /* A column, so the max-height the placement writes in a short window shrinks the LIST,
56
+ which scrolls, rather than letting rows spill past the panel's edge. */
57
+ display: flex; flex-direction: column;
45
58
  box-sizing: border-box;
46
59
  min-width: var(--enigma-menu-min-width);
47
60
  max-width: min(22rem, calc(100vw - 1rem));
@@ -96,6 +109,7 @@
96
109
 
97
110
  [data-enigma-menu-list] {
98
111
  max-height: var(--enigma-menu-max-height);
112
+ min-height: 0; flex: 0 1 auto;
99
113
  overflow-y: auto; overscroll-behavior: contain;
100
114
  }
101
115
 
@@ -20,12 +20,17 @@
20
20
  --enigma-select-panel-border: #333333;
21
21
  --enigma-select-panel-radius: 0.625rem;
22
22
  --enigma-select-panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
23
- --enigma-select-panel-width: 100%;
24
23
  --enigma-select-list-height: 15rem;
25
24
 
26
25
  --enigma-select-active-bg: #2a2a2a;
27
26
  --enigma-select-accent: #fbbf24;
28
27
  --enigma-select-tag-bg: #2a2a2a;
28
+
29
+ /* The layer every portaled popup shares: above the lightbox (9999), because a popup is
30
+ opened FROM whatever is on top. The select, the colour picker and the context menu
31
+ each declare it with the same value, so overriding it once moves all three. Here and
32
+ not in the light-scheme block, or half the readers get an invalid z-index. */
33
+ --enigma-floating-z: 10000;
29
34
  }
30
35
 
31
36
  @media (prefers-color-scheme: light) {
@@ -130,11 +135,24 @@
130
135
  [data-enigma-select-trigger][data-clearable]:focus [data-enigma-select-caret] { opacity: 0; }
131
136
  [data-enigma-select-clear]:hover { color: var(--enigma-select-text); background: var(--enigma-select-active-bg); }
132
137
 
138
+ /* Portaled to <body> and placed in viewport coordinates against the trigger (see
139
+ react/floating.ts), so no ancestor's overflow can clip it and no dialog can stack over it.
140
+ left, top, max-height and data-side are written by the placement: below the trigger,
141
+ flipped above when there is more room there, and capped to the room it has. The column
142
+ is what makes the cap shrink the LIST, which scrolls, instead of cutting off the panel. */
133
143
  [data-enigma-select-content] {
134
- position: absolute; z-index: 50;
135
- top: calc(100% + 0.25rem); left: 0;
136
- min-width: var(--enigma-select-panel-width); box-sizing: border-box;
144
+ position: fixed; z-index: var(--enigma-floating-z);
145
+ display: flex; flex-direction: column;
146
+ /* As wide as the trigger, measured - 100% would now be a percentage of the window - and
147
+ wider only for content that cannot shrink. min-content is what keeps the rest at the
148
+ trigger's width: fixed, the panel's room is the whole window, and a long label or a
149
+ long "Nothing matches" would otherwise widen it to max-width. --enigma-select-panel-width
150
+ overrides the trigger's width. */
151
+ width: min-content;
152
+ min-width: var(--enigma-select-panel-width, var(--enigma-anchor-width, auto)); box-sizing: border-box;
137
153
  padding: 0.25rem;
154
+ color: var(--enigma-select-text);
155
+ font-size: var(--enigma-select-font-size);
138
156
  background: var(--enigma-select-panel-bg);
139
157
  border: 1px solid var(--enigma-select-panel-border);
140
158
  border-radius: var(--enigma-select-panel-radius);
@@ -145,9 +163,6 @@
145
163
  trigger wider than this, which is what it is for. */
146
164
  max-width: min(28rem, calc(100vw - 2rem));
147
165
  }
148
- /* Opening upwards is not a variant, it is the same panel measured against the window: near
149
- the bottom of the screen the list would otherwise be unreachable. */
150
- [data-enigma-select-content][data-side="top"] { top: auto; bottom: calc(100% + 0.25rem); }
151
166
  [data-enigma-select-content][data-state="closed"] { animation: enigma-select-out 120ms ease-in forwards; pointer-events: none; }
152
167
 
153
168
  @keyframes enigma-select-in {
@@ -179,6 +194,8 @@
179
194
 
180
195
  [data-enigma-select-list] {
181
196
  max-height: var(--enigma-select-list-height);
197
+ /* Allowed below its content, so the panel's placement cap shrinks it and it scrolls. */
198
+ min-height: 0; flex: 0 1 auto;
182
199
  margin-top: 0.25rem;
183
200
  overflow-y: auto; overscroll-behavior: contain;
184
201
  }