@enigmax/primitives 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-4SZ5N7BA.js → chunk-2RIO4VDV.js} +2 -2
- package/dist/{chunk-DZMHY3SU.js → chunk-CPJJ3FPW.js} +17 -7
- package/dist/{chunk-QIIV2QSV.js → chunk-HUUINHPR.js} +28 -1
- package/dist/{chunk-RUEQ3TGP.js → chunk-JLZDS6CV.js} +23 -1
- package/dist/{chunk-ISXB5RUH.js → chunk-O2UJ5ZIX.js} +10 -3
- package/dist/{chunk-JJVZYW5C.js → chunk-PVDU6AZU.js} +10 -3
- package/dist/{chunk-Z3VDE7OA.js → chunk-YQV5XZIA.js} +2 -2
- package/dist/{clipboard-menu-BvXJCSFN.d.ts → clipboard-menu-eemzEBqB.d.ts} +1 -1
- package/dist/{context-BfjYGPnH.d.ts → context-DR1dpO8b.d.ts} +1 -1
- package/dist/{context-menu-D3FtTn7v.d.ts → context-menu-qqpGBkny.d.ts} +6 -0
- package/dist/index.d.ts +29 -3
- package/dist/index.js +3 -3
- package/dist/{menu-PNQRTRNV.js → menu-52PXC5PL.js} +2 -2
- package/dist/{menu-MSQSE6U5.js → menu-YIGMPFUP.js} +8 -4
- package/dist/next/index.d.ts +3 -3
- package/dist/next/index.js +5 -5
- package/dist/react/context-menu.d.ts +13 -7
- package/dist/react/context-menu.js +2 -2
- package/dist/react/image.d.ts +5 -2
- package/dist/react/image.js +1 -1
- package/dist/react/index.d.ts +3 -3
- package/dist/react/index.js +5 -5
- package/dist/react/video.d.ts +9 -3
- package/dist/react/video.js +2 -2
- package/dist/react-router/index.d.ts +3 -3
- package/dist/react-router/index.js +5 -5
- package/dist/viewer-N5WLFB24.js +3 -0
- package/package.json +1 -1
- package/recipes/context-menu/styles.css +8 -1
- package/registry.json +21 -5
- package/src/core/context-menu.ts +22 -3
- package/src/core/image-viewer.ts +47 -0
- package/src/core/player.ts +42 -0
- package/src/index.ts +3 -0
- package/src/react/context-menu/root.tsx +27 -5
- package/src/react/context-menu/styles.ts +8 -1
- package/src/react/image/icons.tsx +4 -0
- package/src/react/image/menu.tsx +5 -1
- package/src/react/image/types.ts +3 -0
- package/src/react/video/index.tsx +8 -1
- package/src/react/video/types.ts +7 -1
- package/dist/viewer-73LSE6RR.js +0 -3
package/dist/react/index.js
CHANGED
|
@@ -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-
|
|
7
|
+
export { ContextMenu, ContextMenuContent, ContextMenuList, ContextMenuPanel, ContextMenuRoot, ContextMenuRow, ContextMenuSearch, ContextMenuTrigger, useContextMenuContext } from '../chunk-CPJJ3FPW.js';
|
|
8
8
|
export { SelectionList, useSelection } from '../chunk-N6PDHMAX.js';
|
|
9
|
-
export { Image } from '../chunk-
|
|
9
|
+
export { Image } from '../chunk-2RIO4VDV.js';
|
|
10
10
|
import '../chunk-EYZ366LP.js';
|
|
11
|
-
export { Video } from '../chunk-
|
|
11
|
+
export { Video } from '../chunk-PVDU6AZU.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,9 +19,9 @@ 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-
|
|
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-
|
|
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';
|
package/dist/react/video.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
2
|
-
import { b as ContextMenuNode, a as ContextMenuItem } from '../context-
|
|
3
|
-
import '../context-menu-
|
|
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
|
-
/**
|
|
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 {
|
package/dist/react/video.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
export { Video } from '../chunk-
|
|
3
|
-
import '../chunk-
|
|
2
|
+
export { Video } from '../chunk-PVDU6AZU.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-
|
|
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-
|
|
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-
|
|
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-
|
|
7
|
+
export { ContextMenu, ContextMenuContent, ContextMenuList, ContextMenuPanel, ContextMenuRoot, ContextMenuRow, ContextMenuSearch, ContextMenuTrigger, useContextMenuContext } from '../chunk-CPJJ3FPW.js';
|
|
8
8
|
export { SelectionList, useSelection } from '../chunk-N6PDHMAX.js';
|
|
9
|
-
export { Image } from '../chunk-
|
|
9
|
+
export { Image } from '../chunk-2RIO4VDV.js';
|
|
10
10
|
import '../chunk-EYZ366LP.js';
|
|
11
|
-
export { Video } from '../chunk-
|
|
11
|
+
export { Video } from '../chunk-PVDU6AZU.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,9 +20,9 @@ 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-
|
|
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-
|
|
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';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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": [
|
|
@@ -21,6 +21,13 @@
|
|
|
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-menu-z: 10000;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
@media (prefers-color-scheme: light) {
|
|
@@ -41,7 +48,7 @@
|
|
|
41
48
|
and what a menu opened at the pointer needs. Fixed rather than absolute so a scrolling
|
|
42
49
|
ancestor cannot drag it away from the place it was opened. */
|
|
43
50
|
[data-enigma-menu-panel] {
|
|
44
|
-
position: fixed; z-index:
|
|
51
|
+
position: fixed; z-index: var(--enigma-menu-z);
|
|
45
52
|
box-sizing: border-box;
|
|
46
53
|
min-width: var(--enigma-menu-min-width);
|
|
47
54
|
max-width: min(22rem, calc(100vw - 1rem));
|
package/registry.json
CHANGED
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
"[data-enigma-marquee-copy]"
|
|
87
87
|
],
|
|
88
88
|
"docs": "docs/notes/primitives.md#marquee",
|
|
89
|
+
"source": "src/core/marquee.ts",
|
|
89
90
|
"recipes": [
|
|
90
91
|
"tailwind",
|
|
91
92
|
"css"
|
|
@@ -151,7 +152,8 @@
|
|
|
151
152
|
".logo-marquee[data-grabbing]",
|
|
152
153
|
".logo-marquee__item"
|
|
153
154
|
],
|
|
154
|
-
"docs": "docs/notes/primitives.md#logo-marquee"
|
|
155
|
+
"docs": "docs/notes/primitives.md#logo-marquee",
|
|
156
|
+
"source": "recipes/logo-marquee/index.tsx"
|
|
155
157
|
},
|
|
156
158
|
{
|
|
157
159
|
"name": "input",
|
|
@@ -392,6 +394,7 @@
|
|
|
392
394
|
"[data-enigma-color-preset]"
|
|
393
395
|
],
|
|
394
396
|
"docs": "docs/notes/primitives.md#input",
|
|
397
|
+
"source": "src/core/input.ts",
|
|
395
398
|
"recipes": [
|
|
396
399
|
"tailwind",
|
|
397
400
|
"css"
|
|
@@ -460,6 +463,7 @@
|
|
|
460
463
|
"fuse.js": "Fuzzy matching. Omit it and a built-in substring matcher is used."
|
|
461
464
|
},
|
|
462
465
|
"docs": "docs/notes/primitives.md#search",
|
|
466
|
+
"source": "src/core/search.ts",
|
|
463
467
|
"recipes": [
|
|
464
468
|
"tailwind"
|
|
465
469
|
],
|
|
@@ -551,7 +555,8 @@
|
|
|
551
555
|
"[aria-busy=true]",
|
|
552
556
|
"[data-enigma-button-key]"
|
|
553
557
|
],
|
|
554
|
-
"docs": "docs/notes/primitives.md#button"
|
|
558
|
+
"docs": "docs/notes/primitives.md#button",
|
|
559
|
+
"source": "src/core/button.ts"
|
|
555
560
|
},
|
|
556
561
|
{
|
|
557
562
|
"name": "palette",
|
|
@@ -656,6 +661,7 @@
|
|
|
656
661
|
"[data-enigma-palette-hint]"
|
|
657
662
|
],
|
|
658
663
|
"docs": "docs/notes/primitives.md#palette",
|
|
664
|
+
"source": "src/core/palette.ts",
|
|
659
665
|
"recipes": [
|
|
660
666
|
"css"
|
|
661
667
|
]
|
|
@@ -799,6 +805,7 @@
|
|
|
799
805
|
"[data-enigma-select-empty]"
|
|
800
806
|
],
|
|
801
807
|
"docs": "docs/notes/primitives.md#select",
|
|
808
|
+
"source": "src/core/select.ts",
|
|
802
809
|
"recipes": [
|
|
803
810
|
"css"
|
|
804
811
|
]
|
|
@@ -946,6 +953,7 @@
|
|
|
946
953
|
"[data-enigma-menu-status][data-error]"
|
|
947
954
|
],
|
|
948
955
|
"docs": "docs/notes/primitives.md#context-menu",
|
|
956
|
+
"source": "src/core/context-menu.ts",
|
|
949
957
|
"recipes": [
|
|
950
958
|
"css"
|
|
951
959
|
]
|
|
@@ -1019,7 +1027,8 @@
|
|
|
1019
1027
|
"[data-enigma-selection-marquee]",
|
|
1020
1028
|
"[data-enigma-selection-empty]"
|
|
1021
1029
|
],
|
|
1022
|
-
"docs": "docs/notes/primitives.md#selection"
|
|
1030
|
+
"docs": "docs/notes/primitives.md#selection",
|
|
1031
|
+
"source": "src/core/selection.ts"
|
|
1023
1032
|
},
|
|
1024
1033
|
{
|
|
1025
1034
|
"name": "flags",
|
|
@@ -1081,7 +1090,8 @@
|
|
|
1081
1090
|
"[data-enigma-flag][data-flag-shape=circle]"
|
|
1082
1091
|
],
|
|
1083
1092
|
"assets": "flags",
|
|
1084
|
-
"docs": "docs/notes/primitives.md#flags"
|
|
1093
|
+
"docs": "docs/notes/primitives.md#flags",
|
|
1094
|
+
"source": "src/core/flags.ts"
|
|
1085
1095
|
},
|
|
1086
1096
|
{
|
|
1087
1097
|
"name": "notifications",
|
|
@@ -1140,6 +1150,7 @@
|
|
|
1140
1150
|
],
|
|
1141
1151
|
"styles": false,
|
|
1142
1152
|
"docs": "docs/notes/primitives.md#notifications",
|
|
1153
|
+
"source": "src/core/notifications.ts",
|
|
1143
1154
|
"recipes": [
|
|
1144
1155
|
"tailwind"
|
|
1145
1156
|
]
|
|
@@ -1278,6 +1289,7 @@
|
|
|
1278
1289
|
"[data-icon]"
|
|
1279
1290
|
],
|
|
1280
1291
|
"docs": "docs/notes/primitives.md#toast",
|
|
1292
|
+
"source": "src/react/toaster.tsx",
|
|
1281
1293
|
"recipes": [
|
|
1282
1294
|
"tailwind",
|
|
1283
1295
|
"css"
|
|
@@ -1331,7 +1343,8 @@
|
|
|
1331
1343
|
}
|
|
1332
1344
|
],
|
|
1333
1345
|
"styles": false,
|
|
1334
|
-
"docs": "docs/notes/primitives.md#network"
|
|
1346
|
+
"docs": "docs/notes/primitives.md#network",
|
|
1347
|
+
"source": "src/core/network.ts"
|
|
1335
1348
|
},
|
|
1336
1349
|
{
|
|
1337
1350
|
"name": "relative-time",
|
|
@@ -1399,6 +1412,7 @@
|
|
|
1399
1412
|
"[data-relative-time-capitalize-first-letter=true]"
|
|
1400
1413
|
],
|
|
1401
1414
|
"docs": "docs/notes/primitives.md#relative-time",
|
|
1415
|
+
"source": "src/core/relative-time.ts",
|
|
1402
1416
|
"dependencies": {
|
|
1403
1417
|
"@github/relative-time-element": "^4.4.0"
|
|
1404
1418
|
}
|
|
@@ -1632,6 +1646,7 @@
|
|
|
1632
1646
|
"[data-enigma-image-frame][data-flying]"
|
|
1633
1647
|
],
|
|
1634
1648
|
"docs": "docs/notes/primitives.md#image",
|
|
1649
|
+
"source": "src/core/image-viewer.ts",
|
|
1635
1650
|
"recipes": [
|
|
1636
1651
|
"css"
|
|
1637
1652
|
]
|
|
@@ -1864,6 +1879,7 @@
|
|
|
1864
1879
|
"[data-enigma-video][data-casting]"
|
|
1865
1880
|
],
|
|
1866
1881
|
"docs": "docs/notes/primitives.md#video",
|
|
1882
|
+
"source": "src/core/player.ts",
|
|
1867
1883
|
"recipes": [
|
|
1868
1884
|
"css"
|
|
1869
1885
|
]
|
package/src/core/context-menu.ts
CHANGED
|
@@ -82,8 +82,18 @@ export function isAction(entry: ContextMenuEntry): entry is ContextMenuAction {
|
|
|
82
82
|
/** The fields the filter reads when the caller does not say. */
|
|
83
83
|
export const CONTEXT_MENU_SEARCH_KEYS = ["label", "description", "group", "keywords"];
|
|
84
84
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
85
|
+
/**
|
|
86
|
+
* How many rows a level needs before it grows a filter on its own.
|
|
87
|
+
*
|
|
88
|
+
* Raised from 8, which turned out to be a number you can still READ. A menu of eight is
|
|
89
|
+
* scanned in one look, so the field was a row of chrome, a focus stop and a taller panel in
|
|
90
|
+
* exchange for nothing - the video player's own menu, at nine rows, is what showed it. Twelve
|
|
91
|
+
* is about where a list stops being a shape and starts being a list.
|
|
92
|
+
*
|
|
93
|
+
* It is only the DEFAULT. A menu whose rows arrive from somewhere - a list of files, a
|
|
94
|
+
* directory, anything fetched - says `searchable` and does not wait to grow into it.
|
|
95
|
+
*/
|
|
96
|
+
export const SEARCHABLE_FROM = 12;
|
|
87
97
|
|
|
88
98
|
/** Where a menu was opened. Viewport coordinates, which is what a pointer event reports. */
|
|
89
99
|
export interface ContextMenuPoint {
|
|
@@ -141,6 +151,12 @@ export interface ContextMenuOptions {
|
|
|
141
151
|
/** A row was invoked. The menu has already closed unless the row keeps it open. */
|
|
142
152
|
onSelect?: (item: ContextMenuAction, path: string[]) => void;
|
|
143
153
|
onOpenChange?: (open: boolean) => void;
|
|
154
|
+
/**
|
|
155
|
+
* Whether the ROOT level draws a filter. `"auto"` (the default) grows one from
|
|
156
|
+
* `SEARCHABLE_FROM` rows; `true` puts one there whatever the count, which is what a menu
|
|
157
|
+
* filling itself from somewhere wants; `false` never does.
|
|
158
|
+
*/
|
|
159
|
+
searchable?: boolean | "auto";
|
|
144
160
|
}
|
|
145
161
|
|
|
146
162
|
export interface ContextMenuInstance {
|
|
@@ -235,7 +251,10 @@ export function createContextMenu(options: ContextMenuOptions = {}): ContextMenu
|
|
|
235
251
|
}
|
|
236
252
|
|
|
237
253
|
function searchableFor(item: ContextMenuAction | null, entries: readonly ContextMenuEntry[]): boolean {
|
|
238
|
-
|
|
254
|
+
// The root has no item to carry the answer, so it reads the menu's own option - which
|
|
255
|
+
// is the only way a caller can turn the field on for rows they are about to fetch, or
|
|
256
|
+
// off for a menu they know is short.
|
|
257
|
+
const asked = (item ? item.searchable : opts.searchable) ?? "auto";
|
|
239
258
|
if (asked !== "auto") return asked;
|
|
240
259
|
return entries.filter(isAction).length >= SEARCHABLE_FROM;
|
|
241
260
|
}
|
package/src/core/image-viewer.ts
CHANGED
|
@@ -174,6 +174,53 @@ function saveAs(href: string, name: string, external = false): void {
|
|
|
174
174
|
anchor.remove();
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* A `data:` URL as the bytes it stands for.
|
|
179
|
+
*
|
|
180
|
+
* Synchronous on purpose: it is used to open a tab, and a window opened after an `await` is a
|
|
181
|
+
* popup with no gesture behind it, which the browser blocks.
|
|
182
|
+
*/
|
|
183
|
+
export function dataUrlToBlob(url: string): Blob {
|
|
184
|
+
const comma = url.indexOf(",");
|
|
185
|
+
if (!url.startsWith("data:") || comma === -1) throw new Error("not a data URL");
|
|
186
|
+
const head = url.slice("data:".length, comma);
|
|
187
|
+
const body = url.slice(comma + 1);
|
|
188
|
+
const base64 = head.endsWith(";base64");
|
|
189
|
+
const type = (base64 ? head.slice(0, -";base64".length) : head) || "text/plain";
|
|
190
|
+
if (!base64) return new Blob([decodeURIComponent(body)], { type });
|
|
191
|
+
|
|
192
|
+
const binary = atob(body);
|
|
193
|
+
const bytes = new Uint8Array(binary.length);
|
|
194
|
+
for (let at = 0; at < binary.length; at += 1) bytes[at] = binary.charCodeAt(at);
|
|
195
|
+
return new Blob([bytes], { type });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The picture on its own, in another tab.
|
|
200
|
+
*
|
|
201
|
+
* Chromium refuses a top-level navigation to a `data:` URL - it was a phishing vector - so a
|
|
202
|
+
* row that just called `window.open` on one did nothing at all, silently, which is how it
|
|
203
|
+
* shipped. Those are handed over as a blob instead; everything else opens as it is.
|
|
204
|
+
*
|
|
205
|
+
* `noopener` on both: the new tab must not get a handle on the page that opened it.
|
|
206
|
+
*/
|
|
207
|
+
export function openInNewTab(url: string): void {
|
|
208
|
+
if (typeof window === "undefined") return;
|
|
209
|
+
if (!url.startsWith("data:")) {
|
|
210
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const href = URL.createObjectURL(dataUrlToBlob(url));
|
|
215
|
+
window.open(href, "_blank", "noopener,noreferrer");
|
|
216
|
+
// Revoked later, not now: the tab has not finished reading it yet.
|
|
217
|
+
setTimeout(() => URL.revokeObjectURL(href), 60_000);
|
|
218
|
+
} catch {
|
|
219
|
+
// Undecodable, or a blocked popup. Better the browser's own refusal than a throw.
|
|
220
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
177
224
|
/* -------- the flight from the thumbnail into the viewer -------- */
|
|
178
225
|
|
|
179
226
|
/** How long the picture takes to fly, when the stylesheet does not say otherwise. */
|
package/src/core/player.ts
CHANGED
|
@@ -150,6 +150,48 @@ export async function togglePip(video: HTMLVideoElement): Promise<void> {
|
|
|
150
150
|
await video.requestPictureInPicture();
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/* -------- what a source IS -------- */
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The MIME types worth naming from a file extension, and nothing else.
|
|
157
|
+
*
|
|
158
|
+
* Deliberately short. A `<source>` with no `type` makes the browser fetch and sniff, which
|
|
159
|
+
* costs a request and always works; a `<source>` with the WRONG type is skipped outright, and
|
|
160
|
+
* the video then does not play at all. So a guess is more expensive than silence, and only
|
|
161
|
+
* extensions that mean one thing are in here.
|
|
162
|
+
*/
|
|
163
|
+
const SOURCE_TYPES: Record<string, string> = {
|
|
164
|
+
mp4: "video/mp4",
|
|
165
|
+
m4v: "video/mp4",
|
|
166
|
+
webm: "video/webm",
|
|
167
|
+
ogv: "video/ogg",
|
|
168
|
+
mov: "video/quicktime",
|
|
169
|
+
mkv: "video/x-matroska",
|
|
170
|
+
m3u8: "application/vnd.apple.mpegurl",
|
|
171
|
+
mpd: "application/dash+xml"
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The type a URL's extension implies, or undefined.
|
|
176
|
+
*
|
|
177
|
+
* Read from the PATH, so a query string or a fragment cannot be mistaken for one -
|
|
178
|
+
* `/clip.mp4?token=...` is still an mp4, and `/download?file=x.webm` is not a webm, because
|
|
179
|
+
* the extension has to be on the path's last segment. A `blob:` or `data:` URL has no
|
|
180
|
+
* extension to read and gets nothing, which is the right answer: the browser sniffs.
|
|
181
|
+
*/
|
|
182
|
+
export function sourceType(url: string): string | undefined {
|
|
183
|
+
let path = url;
|
|
184
|
+
try {
|
|
185
|
+
path = new URL(url, typeof location === "undefined" ? "https://localhost" : location.href).pathname;
|
|
186
|
+
} catch {
|
|
187
|
+
path = url.split("?")[0]!.split("#")[0]!;
|
|
188
|
+
}
|
|
189
|
+
const name = path.split("/").pop() ?? "";
|
|
190
|
+
const dot = name.lastIndexOf(".");
|
|
191
|
+
if (dot <= 0) return undefined;
|
|
192
|
+
return SOURCE_TYPES[name.slice(dot + 1).toLowerCase()];
|
|
193
|
+
}
|
|
194
|
+
|
|
153
195
|
/* -------- captions -------- */
|
|
154
196
|
|
|
155
197
|
/** One subtitle track, as the menu lists it. */
|
package/src/index.ts
CHANGED
|
@@ -167,6 +167,8 @@ export {
|
|
|
167
167
|
downloadFile,
|
|
168
168
|
flightFrom,
|
|
169
169
|
flightMs,
|
|
170
|
+
openInNewTab,
|
|
171
|
+
dataUrlToBlob,
|
|
170
172
|
prefersReducedMotion,
|
|
171
173
|
FLIGHT_MS,
|
|
172
174
|
IDENTITY,
|
|
@@ -190,6 +192,7 @@ export {
|
|
|
190
192
|
toggleFullscreen,
|
|
191
193
|
supportsPip,
|
|
192
194
|
togglePip,
|
|
195
|
+
sourceType,
|
|
193
196
|
captionTracks,
|
|
194
197
|
activeCaption,
|
|
195
198
|
showCaption,
|
|
@@ -146,6 +146,12 @@ export interface ContextMenuRootProps {
|
|
|
146
146
|
* selection, Cut over a selection in something writable, Paste in anything writable.
|
|
147
147
|
*/
|
|
148
148
|
clipboard?: boolean | ClipboardMenuOptions;
|
|
149
|
+
/**
|
|
150
|
+
* A filter field over the rows. `"auto"` (the default) grows one only once there are
|
|
151
|
+
* enough rows to be worth filtering; `true` asks for one whatever the count - which is
|
|
152
|
+
* what a menu whose rows are fetched wants - and `false` never draws one.
|
|
153
|
+
*/
|
|
154
|
+
searchable?: boolean | "auto";
|
|
149
155
|
/** Fuse.js's constructor, for fuzzy filtering. Omit it for the built-in matcher. */
|
|
150
156
|
fuse?: ContextMenuOptions["fuse"];
|
|
151
157
|
fuseOptions?: Record<string, unknown>;
|
|
@@ -169,6 +175,7 @@ export function ContextMenuRoot(props: ContextMenuRootProps): ReactNode {
|
|
|
169
175
|
const {
|
|
170
176
|
items,
|
|
171
177
|
title,
|
|
178
|
+
searchable,
|
|
172
179
|
disabled = false,
|
|
173
180
|
fuse,
|
|
174
181
|
fuseOptions,
|
|
@@ -208,6 +215,7 @@ export function ContextMenuRoot(props: ContextMenuRootProps): ReactNode {
|
|
|
208
215
|
const instance = useMemo<ContextMenuInstance>(() => createContextMenu({
|
|
209
216
|
items: items as readonly ContextMenuEntry[],
|
|
210
217
|
title,
|
|
218
|
+
searchable,
|
|
211
219
|
fuse,
|
|
212
220
|
fuseOptions,
|
|
213
221
|
matcher,
|
|
@@ -307,8 +315,8 @@ export function ContextMenuRoot(props: ContextMenuRootProps): ReactNode {
|
|
|
307
315
|
}, []);
|
|
308
316
|
|
|
309
317
|
useEffect(() => {
|
|
310
|
-
instance.update({ items: withClipboard(currentItems.current), title });
|
|
311
|
-
}, [instance, signature, title, withClipboard]);
|
|
318
|
+
instance.update({ items: withClipboard(currentItems.current), title, searchable });
|
|
319
|
+
}, [instance, signature, title, searchable, withClipboard]);
|
|
312
320
|
|
|
313
321
|
const cancelClose = useCallback(() => { window.clearTimeout(closing.current); }, []);
|
|
314
322
|
|
|
@@ -883,6 +891,18 @@ export function ContextMenuRow({ level, index, entry, children, ...props }: Cont
|
|
|
883
891
|
return <p {...props} data-enigma-menu-label="" aria-hidden="true">{entry.label}</p>;
|
|
884
892
|
}
|
|
885
893
|
|
|
894
|
+
/**
|
|
895
|
+
* Whether the level RESERVES the tick column, which is not the same as this row having a
|
|
896
|
+
* tick.
|
|
897
|
+
*
|
|
898
|
+
* A checkable row carries a check and, usually, an icon; its neighbours carry only the
|
|
899
|
+
* icon. Drawn per row, the ticked one's label therefore sat a column further right than
|
|
900
|
+
* everyone else's and the menu read as broken. Every desktop menu reserves the column for
|
|
901
|
+
* the whole level instead, so this asks the LEVEL. Read from `entries` rather than from
|
|
902
|
+
* what is visible: filtering a menu must not shift its rows sideways.
|
|
903
|
+
*/
|
|
904
|
+
const reserveCheck = state?.entries.some((entry) => isAction(entry) && (entry as ContextMenuItem).checked !== undefined) ?? false;
|
|
905
|
+
|
|
886
906
|
const checkable = item.checked !== undefined;
|
|
887
907
|
// A checkable row inside a group is a radio: choosing one is choosing INSTEAD of its
|
|
888
908
|
// siblings, and a screen reader announces the two differently.
|
|
@@ -942,16 +962,18 @@ export function ContextMenuRow({ level, index, entry, children, ...props }: Cont
|
|
|
942
962
|
menu.instance.select(level, index);
|
|
943
963
|
}}
|
|
944
964
|
>
|
|
945
|
-
{children ?? menu.renderItem?.(item, level, index) ?? <ContextMenuRowContent item={item} submenu={submenu} />}
|
|
965
|
+
{children ?? menu.renderItem?.(item, level, index) ?? <ContextMenuRowContent item={item} submenu={submenu} reserveCheck={reserveCheck} />}
|
|
946
966
|
</div>
|
|
947
967
|
);
|
|
948
968
|
}
|
|
949
969
|
|
|
950
970
|
/** The default row: what a desktop menu draws, in the order it draws it. */
|
|
951
|
-
function ContextMenuRowContent({ item, submenu }: { item: ContextMenuItem; submenu: boolean; }): ReactNode {
|
|
971
|
+
function ContextMenuRowContent({ item, submenu, reserveCheck }: { item: ContextMenuItem; submenu: boolean; reserveCheck: boolean; }): ReactNode {
|
|
952
972
|
return (
|
|
953
973
|
<>
|
|
954
|
-
{
|
|
974
|
+
{/* Empty when this row is not checkable and something else in the level is: the
|
|
975
|
+
column is the level's, not the row's. */}
|
|
976
|
+
{(item.checked !== undefined || reserveCheck) && <span data-enigma-menu-check="" aria-hidden="true" />}
|
|
955
977
|
{item.icon ? <span data-enigma-menu-icon="">{item.icon}</span> : null}
|
|
956
978
|
<span data-enigma-menu-item-text="">
|
|
957
979
|
<span data-enigma-menu-item-label="">{item.label}</span>
|
|
@@ -29,6 +29,13 @@ export const CONTEXT_MENU_STYLES = `
|
|
|
29
29
|
--enigma-menu-accent: #fbbf24;
|
|
30
30
|
--enigma-menu-danger: #f87171;
|
|
31
31
|
--enigma-menu-danger-bg: rgba(248, 113, 113, 0.12);
|
|
32
|
+
|
|
33
|
+
/* Above the lightbox, which sits at 9999, because a menu is opened FROM whatever is on
|
|
34
|
+
top: the image viewer's own three-dot menu was portalled to the same body and painted
|
|
35
|
+
underneath it, so its rows could be neither seen nor pressed. A menu is the topmost
|
|
36
|
+
transient surface on a page, and this is the number that says so. It belongs here and
|
|
37
|
+
not in the light-scheme block, or half the readers get an invalid z-index. */
|
|
38
|
+
--enigma-menu-z: 10000;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
@media (prefers-color-scheme: light) {
|
|
@@ -49,7 +56,7 @@ export const CONTEXT_MENU_STYLES = `
|
|
|
49
56
|
and what a menu opened at the pointer needs. Fixed rather than absolute so a scrolling
|
|
50
57
|
ancestor cannot drag it away from the place it was opened. */
|
|
51
58
|
[data-enigma-menu-panel] {
|
|
52
|
-
position: fixed; z-index:
|
|
59
|
+
position: fixed; z-index: var(--enigma-menu-z);
|
|
53
60
|
box-sizing: border-box;
|
|
54
61
|
min-width: var(--enigma-menu-min-width);
|
|
55
62
|
max-width: min(22rem, calc(100vw - 1rem));
|
|
@@ -53,3 +53,7 @@ export function Dots(): ReactNode {
|
|
|
53
53
|
export function Download(): ReactNode {
|
|
54
54
|
return <Glyph><path d="M12 3v12M7 11l5 5 5-5M4 20h16" /></Glyph>;
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
export function NewTab(): ReactNode {
|
|
58
|
+
return <Glyph><path d="M14 4h6v6" /><path d="M20 4 11 13" /><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" /></Glyph>;
|
|
59
|
+
}
|
package/src/react/image/menu.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as icons from "@/react/image/icons";
|
|
4
4
|
import { useRef, type ReactNode } from "react";
|
|
5
5
|
import { MenuButton } from "@/react/image/viewer";
|
|
6
|
-
import { downloadFile } from "@/core/image-viewer";
|
|
6
|
+
import { downloadFile, openInNewTab } from "@/core/image-viewer";
|
|
7
7
|
import type { ImageItem, ImageLabels, ImageMenuOptions } from "@/react/image/types";
|
|
8
8
|
import { ContextMenu, useContextMenuContext, type ContextMenuNode } from "@/react/context-menu";
|
|
9
9
|
|
|
@@ -31,6 +31,9 @@ export function ImageMenu({ item, index, options, labels }: ImageMenuProps): Rea
|
|
|
31
31
|
if (options.download !== false) {
|
|
32
32
|
rows.push({ id: "download", label: labels.download ?? "Download", icon: <icons.Download /> });
|
|
33
33
|
}
|
|
34
|
+
if (options.newTab !== false) {
|
|
35
|
+
rows.push({ id: "new-tab", label: labels.newTab ?? "Open in a new tab", icon: <icons.NewTab /> });
|
|
36
|
+
}
|
|
34
37
|
if (options.items?.length) {
|
|
35
38
|
if (rows.length > 0) rows.push({ type: "separator" });
|
|
36
39
|
rows.push(...options.items);
|
|
@@ -44,6 +47,7 @@ export function ImageMenu({ item, index, options, labels }: ImageMenuProps): Rea
|
|
|
44
47
|
clipboard={false}
|
|
45
48
|
onSelect={(row) => {
|
|
46
49
|
if (row.id === "download") void downloadFile(item.download ?? item.src, item.filename);
|
|
50
|
+
if (row.id === "new-tab") openInNewTab(item.src);
|
|
47
51
|
options.onSelect?.(row.id, item, index);
|
|
48
52
|
}}
|
|
49
53
|
>
|
package/src/react/image/types.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface ZoomOptions {
|
|
|
39
39
|
export interface ImageMenuOptions {
|
|
40
40
|
/** The download row. On whenever the menu is. */
|
|
41
41
|
download?: boolean;
|
|
42
|
+
/** The row that opens the picture on its own, in another tab. On whenever the menu is. */
|
|
43
|
+
newTab?: boolean;
|
|
42
44
|
/** Rows of your own, after the built-in ones. */
|
|
43
45
|
items?: readonly ContextMenuNode[];
|
|
44
46
|
onSelect?: (id: string, item: ImageItem, index: number) => void;
|
|
@@ -55,6 +57,7 @@ export interface ImageLabels {
|
|
|
55
57
|
next?: string;
|
|
56
58
|
menu?: string;
|
|
57
59
|
download?: string;
|
|
60
|
+
newTab?: string;
|
|
58
61
|
discard?: string;
|
|
59
62
|
thumbnails?: string;
|
|
60
63
|
/** The frame itself, announced as the dialog's name. Default "Image viewer". */
|
|
@@ -531,7 +531,14 @@ export function Video(props: VideoProps): ReactNode {
|
|
|
531
531
|
playsInline={rest.playsInline ?? true}
|
|
532
532
|
onClick={() => { if (clickToPlay) toggle(); }}
|
|
533
533
|
>
|
|
534
|
-
{typeof src !== "string" && sources.map((source) =>
|
|
534
|
+
{typeof src !== "string" && sources.map((source) => (
|
|
535
|
+
// The type is what lets the browser SKIP a file it cannot play without
|
|
536
|
+
// fetching it, which is the only reason to list several. Read from the
|
|
537
|
+
// extension when it was not given, and left off when the extension means
|
|
538
|
+
// nothing - a wrong type is worse than none, because the source is then
|
|
539
|
+
// skipped and the video does not play at all.
|
|
540
|
+
<source key={source.src} src={source.src} type={source.type ?? player.sourceType(source.src)} />
|
|
541
|
+
))}
|
|
535
542
|
{tracks?.map((track) => (
|
|
536
543
|
<track
|
|
537
544
|
key={track.src}
|
package/src/react/video/types.ts
CHANGED
|
@@ -12,7 +12,13 @@ import type { ContextMenuItem, ContextMenuNode } from "@/react/context-menu/cont
|
|
|
12
12
|
|
|
13
13
|
export interface VideoSource {
|
|
14
14
|
src: string;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* `video/mp4`, `video/webm`. The browser picks the first one it can play.
|
|
17
|
+
*
|
|
18
|
+
* Inferred from the file extension when it is left out, and left off entirely when the
|
|
19
|
+
* extension means nothing - the browser then fetches and sniffs, which always works. A
|
|
20
|
+
* WRONG type is the expensive mistake: the source is skipped without being tried.
|
|
21
|
+
*/
|
|
16
22
|
type?: string;
|
|
17
23
|
}
|
|
18
24
|
|
package/dist/viewer-73LSE6RR.js
DELETED