@enigmax/primitives 0.25.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.
Files changed (55) hide show
  1. package/dist/chunk-2RIO4VDV.js +121 -0
  2. package/dist/{chunk-DZMHY3SU.js → chunk-CPJJ3FPW.js} +17 -7
  3. package/dist/chunk-EYZ366LP.js +189 -0
  4. package/dist/chunk-HUUINHPR.js +135 -0
  5. package/dist/chunk-JLZDS6CV.js +177 -0
  6. package/dist/chunk-O2UJ5ZIX.js +387 -0
  7. package/dist/chunk-PVDU6AZU.js +953 -0
  8. package/dist/{chunk-Z3VDE7OA.js → chunk-YQV5XZIA.js} +2 -2
  9. package/dist/clipboard-menu-eemzEBqB.d.ts +93 -0
  10. package/dist/context-DR1dpO8b.d.ts +54 -0
  11. package/dist/{clipboard-menu-B_ouitfS.d.ts → context-menu-qqpGBkny.d.ts} +7 -91
  12. package/dist/index.d.ts +292 -2
  13. package/dist/index.js +5 -3
  14. package/dist/menu-52PXC5PL.js +33 -0
  15. package/dist/menu-YIGMPFUP.js +58 -0
  16. package/dist/next/index.d.ts +6 -2
  17. package/dist/next/index.js +12 -8
  18. package/dist/react/context-menu.d.ts +15 -55
  19. package/dist/react/context-menu.js +2 -2
  20. package/dist/react/image.d.ts +112 -0
  21. package/dist/react/image.js +3 -0
  22. package/dist/react/index.d.ts +6 -2
  23. package/dist/react/index.js +12 -8
  24. package/dist/react/input.js +1 -1
  25. package/dist/react/video.d.ts +126 -0
  26. package/dist/react/video.js +3 -0
  27. package/dist/react-router/index.d.ts +6 -2
  28. package/dist/react-router/index.js +12 -8
  29. package/dist/viewer-N5WLFB24.js +3 -0
  30. package/package.json +15 -2
  31. package/recipes/context-menu/styles.css +8 -1
  32. package/recipes/image/styles.css +182 -0
  33. package/recipes/video/styles.css +154 -0
  34. package/registry.json +486 -5
  35. package/src/core/context-menu.ts +22 -3
  36. package/src/core/image-viewer.ts +269 -0
  37. package/src/core/player.ts +345 -0
  38. package/src/index.ts +55 -0
  39. package/src/react/context-menu/root.tsx +27 -5
  40. package/src/react/context-menu/styles.ts +8 -1
  41. package/src/react/image/icons.tsx +59 -0
  42. package/src/react/image/index.tsx +185 -0
  43. package/src/react/image/menu.tsx +80 -0
  44. package/src/react/image/styles.ts +213 -0
  45. package/src/react/image/types.ts +116 -0
  46. package/src/react/image/viewer.tsx +571 -0
  47. package/src/react/index.ts +3 -0
  48. package/src/react/video/icons.tsx +135 -0
  49. package/src/react/video/index.tsx +731 -0
  50. package/src/react/video/menu.tsx +66 -0
  51. package/src/react/video/rail.tsx +99 -0
  52. package/src/react/video/styles.ts +161 -0
  53. package/src/react/video/types.ts +141 -0
  54. /package/dist/{chunk-3MGBZOAU.js → chunk-RMNXVWQ4.js} +0 -0
  55. /package/dist/{chunk-MSOCCQGH.js → chunk-T7I6WJSN.js} +0 -0
@@ -6,7 +6,7 @@ function isAction(entry) {
6
6
  return (entry.type ?? "item") === "item";
7
7
  }
8
8
  var CONTEXT_MENU_SEARCH_KEYS = ["label", "description", "group", "keywords"];
9
- var SEARCHABLE_FROM = 8;
9
+ var SEARCHABLE_FROM = 12;
10
10
  var PAGE = 5;
11
11
  var DEFAULT_CACHE_MS = 5 * 60 * 1e3;
12
12
  function fold(value) {
@@ -32,7 +32,7 @@ function createContextMenu(options = {}) {
32
32
  return path.join("\0");
33
33
  }
34
34
  function searchableFor(item, entries) {
35
- const asked = item?.searchable ?? "auto";
35
+ const asked = (item ? item.searchable : opts.searchable) ?? "auto";
36
36
  if (asked !== "auto") return asked;
37
37
  return entries.filter(isAction).length >= SEARCHABLE_FROM;
38
38
  }
@@ -0,0 +1,93 @@
1
+ import { b as ContextMenuEntry } from './context-menu-qqpGBkny.js';
2
+
3
+ /**
4
+ * The rows every context menu on a desktop already has: Copy, Cut and Paste.
5
+ *
6
+ * A menu opened over a selection or over a field is expected to offer them - the browser's own
7
+ * does, and replacing that menu with a custom one takes them away without saying so. Which is
8
+ * why they are ON by default here and turned off with a prop, rather than the other way round:
9
+ * the defect is silent, so the default has to be the safe one.
10
+ *
11
+ * WHAT DECIDES THEY APPEAR is what was right-clicked, and it is read at the moment the menu
12
+ * opens, not from React state - by then the selection is settled (a `contextmenu` event fires
13
+ * after the press has adjusted it) and the caret is still where the visitor left it.
14
+ *
15
+ * - **Copy**, when there is selected text under the pointer.
16
+ * - **Cut**, when that selection is also in something writable.
17
+ * - **Paste**, in anything writable - and disabled when the clipboard is known to be empty.
18
+ *
19
+ * Not a React module: what a selection is, whether a field takes writes, and how text is put
20
+ * back into one are DOM questions, so a menu drawn by anything else gets the same rows.
21
+ */
22
+
23
+ /** The ids these rows carry. Namespaced, so they can never collide with a caller's own. */
24
+ declare const CLIPBOARD_PREFIX = "enigma:clipboard:";
25
+ type ClipboardAction = "copy" | "cut" | "paste";
26
+ /** What the menu was opened over, as far as the clipboard is concerned. */
27
+ interface ClipboardTarget {
28
+ /** The field or contenteditable under the pointer, or null when it is neither. */
29
+ editable: HTMLElement | null;
30
+ /** Whether that element takes writes: not disabled, not read-only. */
31
+ writable: boolean;
32
+ /** The selected text, from the field's own selection or the document's. */
33
+ selection: string;
34
+ /**
35
+ * Whether that text may be put on the clipboard.
36
+ *
37
+ * False for a password field. The clipboard is shared with every other application on the
38
+ * machine and is not cleared, so a menu that copies a password out of a masked field
39
+ * leaks it somewhere the visitor cannot see - and the browser's own menu refuses too.
40
+ */
41
+ copyable: boolean;
42
+ /** Where the selection was, so it can be put back after the menu has taken focus. */
43
+ range: {
44
+ start: number;
45
+ end: number;
46
+ } | null;
47
+ }
48
+ interface ClipboardMenuLabels {
49
+ copy?: string;
50
+ cut?: string;
51
+ paste?: string;
52
+ }
53
+ interface ClipboardMenuOptions {
54
+ copy?: boolean;
55
+ cut?: boolean;
56
+ paste?: boolean;
57
+ labels?: ClipboardMenuLabels;
58
+ /** Whatever the renderer draws icons with. `unknown`, because a core cannot know. */
59
+ icons?: {
60
+ copy?: unknown;
61
+ cut?: unknown;
62
+ paste?: unknown;
63
+ };
64
+ /** The clipboard is known to be empty, so Paste is listed and greyed rather than missing. */
65
+ clipboardEmpty?: boolean;
66
+ }
67
+ /** What was right-clicked, read the moment the menu opens. */
68
+ declare function inspectClipboardTarget(node: EventTarget | null): ClipboardTarget;
69
+ /**
70
+ * Whether the clipboard has text in it, or null when that cannot be known.
71
+ *
72
+ * Null is the common answer and not a failure: reading the clipboard needs permission, and
73
+ * asking for it puts a browser prompt on screen just to decide whether to grey out a row -
74
+ * which is a worse trade than showing an enabled Paste that turns out to do nothing. So the
75
+ * permission is only READ, never requested, and the clipboard is only opened where it has
76
+ * already been granted.
77
+ */
78
+ declare function clipboardHasText(): Promise<boolean | null>;
79
+ /** The rows for this target, in the order every desktop menu puts them. */
80
+ declare function clipboardEntries(target: ClipboardTarget, options?: ClipboardMenuOptions): ContextMenuEntry[];
81
+ /** Which clipboard row an id belongs to, or null for anything that is not one of ours. */
82
+ declare function clipboardAction(id: string): ClipboardAction | null;
83
+ /**
84
+ * Do what the row says.
85
+ *
86
+ * Called straight from the press that chose it, so the browser still counts it as a user
87
+ * gesture - which is what the clipboard API requires and what makes a paste possible at all.
88
+ * Returns whether the action happened: a refused permission and an empty clipboard are both
89
+ * "no", and neither is worth an exception the caller has to catch.
90
+ */
91
+ declare function performClipboardAction(action: ClipboardAction, target: ClipboardTarget): Promise<boolean>;
92
+
93
+ export { CLIPBOARD_PREFIX as C, type ClipboardAction as a, type ClipboardMenuLabels as b, type ClipboardMenuOptions as c, type ClipboardTarget as d, clipboardAction as e, clipboardEntries as f, clipboardHasText as g, inspectClipboardTarget as i, performClipboardAction as p };
@@ -0,0 +1,54 @@
1
+ import { ReactNode, RefObject } from 'react';
2
+ import { a as ContextMenuAction, b as ContextMenuEntry, c as ContextMenuInstance, j as ContextMenuState, h as ContextMenuPoint } from './context-menu-qqpGBkny.js';
3
+
4
+ /**
5
+ * One row, as React draws it. The core carries `icon` as `unknown` because it cannot know
6
+ * what a node is; here it is one.
7
+ */
8
+ interface ContextMenuItem extends Omit<ContextMenuAction, "icon" | "items"> {
9
+ icon?: ReactNode;
10
+ items?: readonly ContextMenuNode[];
11
+ }
12
+ /** Anything that can appear in a menu, with React's icon type. */
13
+ type ContextMenuNode = ContextMenuItem | Exclude<ContextMenuEntry, ContextMenuAction>;
14
+ interface ContextMenuContextValue {
15
+ instance: ContextMenuInstance;
16
+ state: ContextMenuState;
17
+ /**
18
+ * Open at a point, over what the press landed on. False when there was nothing to show,
19
+ * so the press is left alone.
20
+ *
21
+ * `target` is what the clipboard rows are built from - the field or the selection under
22
+ * the pointer - and a caller that leaves it out gets a menu with none of them, which is
23
+ * the right answer for a menu opened from somewhere that is not a press.
24
+ */
25
+ open: (point: ContextMenuPoint, target?: EventTarget | null) => boolean;
26
+ close: () => void;
27
+ /** ids the parts need to point at each other. */
28
+ ids: {
29
+ trigger: string;
30
+ };
31
+ panelId: (level: number) => string;
32
+ itemId: (level: number, index: number) => string;
33
+ triggerRef: RefObject<HTMLElement | null>;
34
+ /** Which panel the keyboard is in. Always the deepest open one. */
35
+ onMenuKeyDown: (event: React.KeyboardEvent) => void;
36
+ /** How long the pointer must rest on a row before its submenu opens, and before one closes. */
37
+ delays: {
38
+ open: number;
39
+ close: number;
40
+ };
41
+ /** Drop the pending close, wherever it was scheduled. Called wherever the pointer lands. */
42
+ cancelClose: () => void;
43
+ /** Close everything below `level`, after the beat - replacing any close already waiting. */
44
+ scheduleClose: (level: number) => void;
45
+ /** Draw a row instead of the default icon / label / shortcut. */
46
+ renderItem?: (item: ContextMenuItem, level: number, index: number) => ReactNode;
47
+ /** Shown while a fetched submenu has not answered. */
48
+ loadingLabel: ReactNode;
49
+ /** Shown when a filter matched nothing, or a fetched submenu came back with nothing. */
50
+ emptyLabel: ReactNode;
51
+ }
52
+ declare function useContextMenuContext(part: string): ContextMenuContextValue;
53
+
54
+ export { type ContextMenuContextValue as C, type ContextMenuItem as a, type ContextMenuNode as b, useContextMenuContext as u };
@@ -126,6 +126,12 @@ interface ContextMenuOptions {
126
126
  /** A row was invoked. The menu has already closed unless the row keeps it open. */
127
127
  onSelect?: (item: ContextMenuAction, path: string[]) => void;
128
128
  onOpenChange?: (open: boolean) => void;
129
+ /**
130
+ * Whether the ROOT level draws a filter. `"auto"` (the default) grows one from
131
+ * `SEARCHABLE_FROM` rows; `true` puts one there whatever the count, which is what a menu
132
+ * filling itself from somewhere wants; `false` never does.
133
+ */
134
+ searchable?: boolean | "auto";
129
135
  }
130
136
  interface ContextMenuInstance {
131
137
  readonly state: ContextMenuState;
@@ -171,94 +177,4 @@ interface ContextMenuInstance {
171
177
  type ContextMenuMoveKey = "ArrowDown" | "ArrowUp" | "Home" | "End" | "PageDown" | "PageUp";
172
178
  declare function createContextMenu(options?: ContextMenuOptions): ContextMenuInstance;
173
179
 
174
- /**
175
- * The rows every context menu on a desktop already has: Copy, Cut and Paste.
176
- *
177
- * A menu opened over a selection or over a field is expected to offer them - the browser's own
178
- * does, and replacing that menu with a custom one takes them away without saying so. Which is
179
- * why they are ON by default here and turned off with a prop, rather than the other way round:
180
- * the defect is silent, so the default has to be the safe one.
181
- *
182
- * WHAT DECIDES THEY APPEAR is what was right-clicked, and it is read at the moment the menu
183
- * opens, not from React state - by then the selection is settled (a `contextmenu` event fires
184
- * after the press has adjusted it) and the caret is still where the visitor left it.
185
- *
186
- * - **Copy**, when there is selected text under the pointer.
187
- * - **Cut**, when that selection is also in something writable.
188
- * - **Paste**, in anything writable - and disabled when the clipboard is known to be empty.
189
- *
190
- * Not a React module: what a selection is, whether a field takes writes, and how text is put
191
- * back into one are DOM questions, so a menu drawn by anything else gets the same rows.
192
- */
193
-
194
- /** The ids these rows carry. Namespaced, so they can never collide with a caller's own. */
195
- declare const CLIPBOARD_PREFIX = "enigma:clipboard:";
196
- type ClipboardAction = "copy" | "cut" | "paste";
197
- /** What the menu was opened over, as far as the clipboard is concerned. */
198
- interface ClipboardTarget {
199
- /** The field or contenteditable under the pointer, or null when it is neither. */
200
- editable: HTMLElement | null;
201
- /** Whether that element takes writes: not disabled, not read-only. */
202
- writable: boolean;
203
- /** The selected text, from the field's own selection or the document's. */
204
- selection: string;
205
- /**
206
- * Whether that text may be put on the clipboard.
207
- *
208
- * False for a password field. The clipboard is shared with every other application on the
209
- * machine and is not cleared, so a menu that copies a password out of a masked field
210
- * leaks it somewhere the visitor cannot see - and the browser's own menu refuses too.
211
- */
212
- copyable: boolean;
213
- /** Where the selection was, so it can be put back after the menu has taken focus. */
214
- range: {
215
- start: number;
216
- end: number;
217
- } | null;
218
- }
219
- interface ClipboardMenuLabels {
220
- copy?: string;
221
- cut?: string;
222
- paste?: string;
223
- }
224
- interface ClipboardMenuOptions {
225
- copy?: boolean;
226
- cut?: boolean;
227
- paste?: boolean;
228
- labels?: ClipboardMenuLabels;
229
- /** Whatever the renderer draws icons with. `unknown`, because a core cannot know. */
230
- icons?: {
231
- copy?: unknown;
232
- cut?: unknown;
233
- paste?: unknown;
234
- };
235
- /** The clipboard is known to be empty, so Paste is listed and greyed rather than missing. */
236
- clipboardEmpty?: boolean;
237
- }
238
- /** What was right-clicked, read the moment the menu opens. */
239
- declare function inspectClipboardTarget(node: EventTarget | null): ClipboardTarget;
240
- /**
241
- * Whether the clipboard has text in it, or null when that cannot be known.
242
- *
243
- * Null is the common answer and not a failure: reading the clipboard needs permission, and
244
- * asking for it puts a browser prompt on screen just to decide whether to grey out a row -
245
- * which is a worse trade than showing an enabled Paste that turns out to do nothing. So the
246
- * permission is only READ, never requested, and the clipboard is only opened where it has
247
- * already been granted.
248
- */
249
- declare function clipboardHasText(): Promise<boolean | null>;
250
- /** The rows for this target, in the order every desktop menu puts them. */
251
- declare function clipboardEntries(target: ClipboardTarget, options?: ClipboardMenuOptions): ContextMenuEntry[];
252
- /** Which clipboard row an id belongs to, or null for anything that is not one of ours. */
253
- declare function clipboardAction(id: string): ClipboardAction | null;
254
- /**
255
- * Do what the row says.
256
- *
257
- * Called straight from the press that chose it, so the browser still counts it as a user
258
- * gesture - which is what the clipboard API requires and what makes a paste possible at all.
259
- * Returns whether the action happened: a refused permission and an empty clipboard are both
260
- * "no", and neither is worth an exception the caller has to catch.
261
- */
262
- declare function performClipboardAction(action: ClipboardAction, target: ClipboardTarget): Promise<boolean>;
263
-
264
- export { CLIPBOARD_PREFIX as C, CONTEXT_MENU_SEARCH_KEYS as a, type ClipboardAction as b, type ClipboardMenuLabels as c, type ClipboardMenuOptions as d, type ClipboardTarget as e, type ContextMenuAction as f, type ContextMenuEntry as g, type ContextMenuInstance as h, type ContextMenuLabel as i, type ContextMenuLevel as j, type ContextMenuMoveKey as k, type ContextMenuOptions as l, type ContextMenuPoint as m, type ContextMenuSeparator as n, type ContextMenuState as o, clipboardAction as p, clipboardEntries as q, clipboardHasText as r, createContextMenu as s, inspectClipboardTarget as t, isAction as u, performClipboardAction as v };
180
+ export { CONTEXT_MENU_SEARCH_KEYS as C, type ContextMenuAction as a, type ContextMenuEntry as b, type ContextMenuInstance as c, type ContextMenuLabel as d, type ContextMenuLevel as e, type ContextMenuMoveKey as f, type ContextMenuOptions as g, type ContextMenuPoint as h, type ContextMenuSeparator as i, type ContextMenuState as j, createContextMenu as k, isAction as l };
package/dist/index.d.ts CHANGED
@@ -9,7 +9,8 @@ export { R as RelativeTimeFormat, a as RelativeTimeOptions, b as RelativeTimePre
9
9
  export { F as FLAG_CDN, a as FLAG_RATIO, b as FlagConfig, c as FlagFormat, d as FlagOptions, e as FlagShape, f as FlagSource, g as FlagView, h as configureFlags, i as flagAttributes, j as flagConfig, k as flagName, l as flagSrc, m as flagView, n as normalizeFlagCode, r as resetFlagConfig } from './flags-BQU1V9rE.js';
10
10
  export { P as PaletteKey, a as PositionedRow, R as RecentEntry, b as RecentStore, c as RecentStoreOptions, d as RowGroup, e as createRecentStore, g as groupRows, i as isPaletteShortcut, m as moveActive, r as recentKey, s as shortcutLabel } from './palette-D7iuQh_T.js';
11
11
  export { S as SELECT_SEARCH_KEYS, a as SelectInstance, b as SelectMoveKey, c as SelectOption, d as SelectOptions, e as SelectState, f as createSelect } from './select-ClSy-J1f.js';
12
- export { C as CLIPBOARD_PREFIX, a as CONTEXT_MENU_SEARCH_KEYS, b as ClipboardAction, c as ClipboardMenuLabels, d as ClipboardMenuOptions, e as ClipboardTarget, f as ContextMenuAction, g as ContextMenuEntry, h as ContextMenuInstance, i as ContextMenuLabel, j as ContextMenuLevel, k as ContextMenuMoveKey, l as ContextMenuOptions, m as ContextMenuPoint, n as ContextMenuSeparator, o as ContextMenuState, p as clipboardAction, q as clipboardEntries, r as clipboardHasText, s as createContextMenu, t as inspectClipboardTarget, u as isAction, v as performClipboardAction } from './clipboard-menu-B_ouitfS.js';
12
+ export { C as CONTEXT_MENU_SEARCH_KEYS, a as ContextMenuAction, b as ContextMenuEntry, c as ContextMenuInstance, d as ContextMenuLabel, e as ContextMenuLevel, f as ContextMenuMoveKey, g as ContextMenuOptions, h as ContextMenuPoint, i as ContextMenuSeparator, j as ContextMenuState, k as createContextMenu, l as isAction } from './context-menu-qqpGBkny.js';
13
+ 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';
13
14
  export { D as DEFAULT_SELECTION_SHORTCUTS, S as SelectionClickModifiers, 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';
14
15
  export { S as Shortcut, a as ShortcutEvent, b as ShortcutSpec, T as TYPEAHEAD_MS, c as TypeaheadState, d as TypeaheadStep, i as isApplePlatform, m as matchesShortcut, p as parseShortcut, s as shortcutList, e as shortcutText, f as shortcutTokens, t as typeaheadStep } from './keys-D2zJs1uB.js';
15
16
 
@@ -39,4 +40,293 @@ declare const INPUT_ICON_PATHS: {
39
40
  /** The same shapes as a standalone SVG string, for the DOM renderer below. */
40
41
  declare function iconMarkup(paths: readonly string[]): string;
41
42
 
42
- export { INPUT_ICON_PATHS, iconMarkup };
43
+ /**
44
+ * The arithmetic behind the image viewer: zoom around a point, panning that cannot lose the
45
+ * image, moving through a set, and getting a file onto the disk.
46
+ *
47
+ * Not a React module, and deliberately: none of this is rendering. A page drawing its own
48
+ * viewer, or a test, gets the same maths - and the parts that are easy to get wrong (zoom
49
+ * that drifts away from the cursor, a pan that strands the image off screen, a download that
50
+ * silently opens a tab instead) are then testable without a browser.
51
+ */
52
+ /** How far the image is scaled, and where its centre sits relative to the frame's, in px. */
53
+ interface Transform {
54
+ scale: number;
55
+ x: number;
56
+ y: number;
57
+ }
58
+ interface ZoomLimits {
59
+ min: number;
60
+ max: number;
61
+ }
62
+ /** A box in viewport coordinates - the frame the image is being viewed in. */
63
+ interface Box {
64
+ left: number;
65
+ top: number;
66
+ width: number;
67
+ height: number;
68
+ }
69
+ declare const IDENTITY: Transform;
70
+ declare const ZOOM_LIMITS: ZoomLimits;
71
+ /** What a keyboard `+`/`-` step multiplies by, and the ratio a double press toggles to. */
72
+ declare const ZOOM_STEP = 1.35;
73
+ declare const ZOOM_DOUBLE = 2.5;
74
+ declare function clampScale(scale: number, limits?: ZoomLimits): number;
75
+ /**
76
+ * A wheel notch to a zoom factor.
77
+ *
78
+ * `deltaMode` matters: a mouse reports pixels, but Firefox reports LINES (mode 1) and a page
79
+ * scroll reports pages (mode 2), so treating every delta as pixels makes the same gesture
80
+ * zoom a hundred times harder in one browser than another. The exponential keeps a notch
81
+ * proportional - zooming out from 8x and in from 1x take the same number of turns.
82
+ */
83
+ declare function wheelFactor(deltaY: number, deltaMode?: number): number;
84
+ /**
85
+ * Zoom, keeping the point under the pointer under the pointer.
86
+ *
87
+ * The defect this exists for: scaling around the frame's centre slides whatever the visitor
88
+ * was pointing at out from under them, so zooming into a face means zooming and then hunting
89
+ * for it again. The offset is corrected by how much that point moved when the scale changed.
90
+ */
91
+ declare function zoomAt(current: Transform, factor: number, point: {
92
+ x: number;
93
+ y: number;
94
+ }, box: Box, limits?: ZoomLimits): Transform;
95
+ /**
96
+ * Keep the image over the frame.
97
+ *
98
+ * At 1x it is centred and there is nothing to pan. Zoomed, the offset is bounded by the half
99
+ * of the image that hangs outside the frame - without it a drag can throw the picture off the
100
+ * screen entirely, and the only way back is to close the viewer.
101
+ */
102
+ declare function clampPan(transform: Transform, box: Box, natural: {
103
+ width: number;
104
+ height: number;
105
+ }): Transform;
106
+ /** The size an image is DRAWN at inside a frame it is fitted to, which is what pan is bounded by. */
107
+ declare function fittedSize(natural: {
108
+ width: number;
109
+ height: number;
110
+ }, box: Box): {
111
+ width: number;
112
+ height: number;
113
+ };
114
+ /** The distance between two pointers, for a pinch. */
115
+ declare function pinchDistance(a: {
116
+ x: number;
117
+ y: number;
118
+ }, b: {
119
+ x: number;
120
+ y: number;
121
+ }): number;
122
+ /**
123
+ * The next index in a set, skipping what has been discarded.
124
+ *
125
+ * Returns -1 when there is nothing left to show, which is the viewer's cue to close rather
126
+ * than to sit on an empty frame.
127
+ */
128
+ declare function nextIndex(index: number, length: number, step: number, options?: {
129
+ loop?: boolean;
130
+ skip?: ReadonlySet<number>;
131
+ }): number;
132
+ /** The file name to save as: whatever the caller asked for, else the one in the URL. */
133
+ declare function filenameFrom(url: string, fallback?: string): string;
134
+ /**
135
+ * Save the file, rather than navigate to it.
136
+ *
137
+ * `<a download>` is honoured only for a same-origin URL; on a CDN the browser ignores the
138
+ * attribute and opens the image in a tab instead, which is not what the row said it would do.
139
+ * So the bytes are fetched and handed over as a blob, and the plain anchor is the fallback for
140
+ * when that is refused (no CORS header) - a tab is still better than nothing happening.
141
+ */
142
+ declare function downloadFile(url: string, filename?: string): Promise<void>;
143
+ /**
144
+ * A `data:` URL as the bytes it stands for.
145
+ *
146
+ * Synchronous on purpose: it is used to open a tab, and a window opened after an `await` is a
147
+ * popup with no gesture behind it, which the browser blocks.
148
+ */
149
+ declare function dataUrlToBlob(url: string): Blob;
150
+ /**
151
+ * The picture on its own, in another tab.
152
+ *
153
+ * Chromium refuses a top-level navigation to a `data:` URL - it was a phishing vector - so a
154
+ * row that just called `window.open` on one did nothing at all, silently, which is how it
155
+ * shipped. Those are handed over as a blob instead; everything else opens as it is.
156
+ *
157
+ * `noopener` on both: the new tab must not get a handle on the page that opened it.
158
+ */
159
+ declare function openInNewTab(url: string): void;
160
+ /** How long the picture takes to fly, when the stylesheet does not say otherwise. */
161
+ declare const FLIGHT_MS = 300;
162
+ /**
163
+ * How long the flight ACTUALLY lasts, read off the element.
164
+ *
165
+ * The duration lives in the stylesheet (`--enigma-image-flight`), so a project that slows it
166
+ * down would otherwise have the viewer give up waiting halfway through and close mid-flight.
167
+ * The constant above is only the answer for an element with no transition on it at all.
168
+ */
169
+ declare function flightMs(element: Element | null): number;
170
+ /**
171
+ * The transform that puts a picture exactly over another box.
172
+ *
173
+ * This is the FLIP the lightbox opens with: the full-size image is laid out where it belongs,
174
+ * measured, and then drawn back ON the thumbnail with this transform - so releasing it to the
175
+ * identity transform is the picture growing out of the one in the page rather than a dialog
176
+ * appearing over it. Closing is the same transform applied in the other direction.
177
+ *
178
+ * Both boxes are the same picture, so one scale is enough; the width is used because a
179
+ * thumbnail is nearly always constrained by it. Null when either box has not been laid out
180
+ * yet, which is the caller's cue to skip the animation rather than to divide by zero.
181
+ */
182
+ declare function flightFrom(from: Box, to: Box): Transform | null;
183
+ /** Whether the reader has asked for less movement. Every animation here is skipped when they have. */
184
+ declare function prefersReducedMotion(): boolean;
185
+
186
+ /**
187
+ * The arithmetic behind the video player: times, seeking, buffering, volume and the keys.
188
+ *
189
+ * None of it is rendering, so it is testable without a browser and reusable by a page that
190
+ * draws its own controls - the same split the colour maths and the image viewer's are in.
191
+ */
192
+ /** The playback rates the settings menu offers, in the order it lists them. */
193
+ declare const SPEEDS: readonly [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
194
+ /** How far the arrows seek, in seconds, and what a bigger jump moves by. */
195
+ declare const SEEK_STEP = 5;
196
+ declare const SEEK_JUMP = 10;
197
+ declare const VOLUME_STEP = 0.05;
198
+ /**
199
+ * Seconds as a clock, sized by the LONGEST time it will have to show.
200
+ *
201
+ * `duration` is what stops the label changing width mid-playback: a 1h04m video passing 10:00
202
+ * would otherwise go from `9:59` to `10:00` to `1:00:00`, and every control after it moves.
203
+ */
204
+ declare function formatTime(seconds: number, duration?: number): string;
205
+ /** A time as a fraction of the duration, safe before the metadata says what that is. */
206
+ declare function progress(time: number, duration: number): number;
207
+ /** Where a press on a horizontal rail lands, as a fraction of it. */
208
+ declare function fractionAt(box: {
209
+ left: number;
210
+ width: number;
211
+ }, clientX: number): number;
212
+ /**
213
+ * How much is buffered AHEAD of where playback is.
214
+ *
215
+ * The range under the playhead, not the largest one: a viewer who seeks past the buffer is
216
+ * looking at a bar that says nothing is loaded, which is true of the part they are watching.
217
+ */
218
+ declare function bufferedAhead(ranges: TimeRanges | null, time: number, duration: number): number;
219
+ /** The next rate in the list, wrapping - what a repeated press on the speed row does. */
220
+ declare function nextSpeed(current: number, step?: number): number;
221
+ /** What a key press means, or null when the player should leave it to the page. */
222
+ type PlayerCommand = {
223
+ type: "toggle";
224
+ } | {
225
+ type: "seek";
226
+ by: number;
227
+ } | {
228
+ type: "seekTo";
229
+ fraction: number;
230
+ } | {
231
+ type: "volume";
232
+ by: number;
233
+ } | {
234
+ type: "mute";
235
+ } | {
236
+ type: "fullscreen";
237
+ } | {
238
+ type: "captions";
239
+ } | {
240
+ type: "pip";
241
+ };
242
+ /**
243
+ * The shortcuts, as the platform's video players define them.
244
+ *
245
+ * Space and K play, J and L jump ten, the arrows nudge five and the volume, M mutes, F is
246
+ * fullscreen, C captions, and a digit seeks to that tenth of the video.
247
+ */
248
+ declare function commandFor(key: string): PlayerCommand | null;
249
+ /**
250
+ * Whether a key press belongs to the page rather than to the player.
251
+ *
252
+ * A shortcut that fires while somebody is typing turns their space bar into a pause, so the
253
+ * player stands down inside anything that takes text.
254
+ */
255
+ declare function isTypingTarget(target: EventTarget | null): boolean;
256
+ /** Whether the document is showing something fullscreen, across the two spellings of it. */
257
+ declare function fullscreenElement(): Element | null;
258
+ /**
259
+ * Fullscreen, including the one platform that will not give it to a container.
260
+ *
261
+ * iOS Safari has no Fullscreen API on an arbitrary element: the only thing that can fill the
262
+ * screen is the video itself, through `webkitEnterFullscreen`. Without that branch the button
263
+ * does nothing at all on an iPhone, which is where most video is watched.
264
+ */
265
+ declare function toggleFullscreen(container: HTMLElement, video: HTMLVideoElement | null): Promise<void>;
266
+ /** Picture in picture, where the browser has it. Firefox has its own button and no API. */
267
+ declare function supportsPip(): boolean;
268
+ declare function togglePip(video: HTMLVideoElement): Promise<void>;
269
+ /**
270
+ * The type a URL's extension implies, or undefined.
271
+ *
272
+ * Read from the PATH, so a query string or a fragment cannot be mistaken for one -
273
+ * `/clip.mp4?token=...` is still an mp4, and `/download?file=x.webm` is not a webm, because
274
+ * the extension has to be on the path's last segment. A `blob:` or `data:` URL has no
275
+ * extension to read and gets nothing, which is the right answer: the browser sniffs.
276
+ */
277
+ declare function sourceType(url: string): string | undefined;
278
+ /** One subtitle track, as the menu lists it. */
279
+ interface CaptionTrack {
280
+ /** Where it sits in the element's own `textTracks`, which is what turns it on. */
281
+ index: number;
282
+ label: string;
283
+ language: string;
284
+ }
285
+ /** Which of the element's text tracks are subtitles a viewer would choose between. */
286
+ declare function captionTracks(list: TextTrackList | null | undefined): CaptionTrack[];
287
+ /**
288
+ * The track the element is SHOWING, or -1.
289
+ *
290
+ * Read rather than remembered: a `default` track is showing before any button was pressed, and
291
+ * a player that assumed "off" would need two presses to turn something off.
292
+ */
293
+ declare function activeCaption(list: TextTrackList | null | undefined): number;
294
+ /**
295
+ * Show one track and disable the rest. -1 turns them all off.
296
+ *
297
+ * "disabled" rather than "hidden": a hidden track still fires its cues, so a page listening to
298
+ * `cuechange` for its own transcript would keep receiving a language nobody asked for.
299
+ */
300
+ declare function showCaption(list: TextTrackList | null | undefined, index: number): void;
301
+ /** What the cast control knows about the world. */
302
+ interface RemoteState {
303
+ /** There is somewhere to cast TO. Before the browser has looked, this is optimistic. */
304
+ available: boolean;
305
+ /** Playing on that screen right now. */
306
+ connected: boolean;
307
+ }
308
+ /**
309
+ * Whether this element can be cast at all.
310
+ *
311
+ * `disableRemotePlayback` is honoured: a page that asked for the button to be gone does not
312
+ * get one drawn over its video by a component.
313
+ */
314
+ declare function supportsRemote(video: HTMLVideoElement | null): boolean;
315
+ /**
316
+ * Watch for a screen to cast to, and for the connection to it.
317
+ *
318
+ * WHY AVAILABILITY IS OPTIMISTIC WHEN THE WATCH FAILS. `watchAvailability` rejects with
319
+ * NotSupportedError on the platforms that can still `prompt()` - Safari, and Chromium for some
320
+ * sources - so treating a rejection as "no devices" hides a control that works. A button that
321
+ * opens an empty picker is a smaller loss than a missing one.
322
+ */
323
+ declare function watchRemote(video: HTMLVideoElement, onChange: (state: RemoteState) => void): () => void;
324
+ /**
325
+ * Open the browser's own device picker.
326
+ *
327
+ * There is no list to draw ourselves: both APIs hand the choice to the platform, which is the
328
+ * only thing that can enumerate the screens on the network.
329
+ */
330
+ declare function promptRemote(video: HTMLVideoElement): Promise<void>;
331
+
332
+ export { type Box, type CaptionTrack, FLIGHT_MS, IDENTITY, INPUT_ICON_PATHS, type PlayerCommand, type RemoteState, SEEK_JUMP, SEEK_STEP, SPEEDS, type Transform, VOLUME_STEP, ZOOM_DOUBLE, ZOOM_LIMITS, ZOOM_STEP, type ZoomLimits, activeCaption, bufferedAhead, captionTracks, clampPan, clampScale, commandFor, dataUrlToBlob, downloadFile, filenameFrom, fittedSize, flightFrom, flightMs, formatTime, fractionAt, fullscreenElement, iconMarkup, isTypingTarget, nextIndex, nextSpeed, openInNewTab, pinchDistance, prefersReducedMotion, progress, promptRemote, showCaption, sourceType, supportsPip, supportsRemote, toggleFullscreen, togglePip, watchRemote, wheelFactor, zoomAt };
package/dist/index.js CHANGED
@@ -1,14 +1,16 @@
1
+ export { FLIGHT_MS, IDENTITY, ZOOM_DOUBLE, ZOOM_LIMITS, ZOOM_STEP, clampPan, clampScale, dataUrlToBlob, downloadFile, filenameFrom, fittedSize, flightFrom, flightMs, nextIndex, openInNewTab, pinchDistance, prefersReducedMotion, wheelFactor, zoomAt } from './chunk-HUUINHPR.js';
1
2
  export { createInput } from './chunk-KJINGUQN.js';
3
+ export { colorEquals, formatColor, hslToRgb, hsvToRgb, parseColor, rgbToHsl, rgbToHsv, toHex } from './chunk-S2FPN5ID.js';
4
+ export { SERVER_NETWORK_STATE, createNetworkMonitor } from './chunk-2QFTRNAZ.js';
2
5
  export { ensureZone, normalizeDate, parseDuration, relativeTimeAttributes, relativeTimeView } from './chunk-IXVMRVD4.js';
3
6
  export { estimatePasswordStrength, generatePassword } from './chunk-HC2ME5PU.js';
4
- export { colorEquals, formatColor, hslToRgb, hsvToRgb, parseColor, rgbToHsl, rgbToHsv, toHex } from './chunk-S2FPN5ID.js';
5
- export { CLIPBOARD_PREFIX, CONTEXT_MENU_SEARCH_KEYS, clipboardAction, clipboardEntries, clipboardHasText, createContextMenu, inspectClipboardTarget, isAction, performClipboardAction } from './chunk-Z3VDE7OA.js';
7
+ export { CLIPBOARD_PREFIX, CONTEXT_MENU_SEARCH_KEYS, clipboardAction, clipboardEntries, clipboardHasText, createContextMenu, inspectClipboardTarget, isAction, performClipboardAction } from './chunk-YQV5XZIA.js';
6
8
  export { DEFAULT_SELECTION_SHORTCUTS, createSelection } from './chunk-WSQC3PCC.js';
9
+ export { SEEK_JUMP, SEEK_STEP, SPEEDS, VOLUME_STEP, activeCaption, bufferedAhead, captionTracks, commandFor, formatTime, fractionAt, fullscreenElement, isTypingTarget, nextSpeed, progress, promptRemote, showCaption, sourceType, supportsPip, supportsRemote, toggleFullscreen, togglePip, watchRemote } from './chunk-JLZDS6CV.js';
7
10
  export { createButton } from './chunk-MMQPZGSU.js';
8
11
  export { FLAG_CDN, FLAG_RATIO, configureFlags, flagAttributes, flagConfig, flagName, flagSrc, flagView, normalizeFlagCode, resetFlagConfig } from './chunk-LMZJBOZG.js';
9
12
  export { createNotifications } from './chunk-SNYUBXWQ.js';
10
13
  export { createMarquee } from './chunk-UOSSNUSC.js';
11
- export { SERVER_NETWORK_STATE, createNetworkMonitor } from './chunk-2QFTRNAZ.js';
12
14
  export { INPUT_ICON_PATHS, iconMarkup } from './chunk-F25CGNQC.js';
13
15
  export { SELECT_SEARCH_KEYS, createSelect } from './chunk-NONGREXC.js';
14
16
  export { TYPEAHEAD_MS, isApplePlatform, matchesShortcut, parseShortcut, shortcutList, shortcutText, shortcutTokens, typeaheadStep } from './chunk-4VUHQFAT.js';
@@ -0,0 +1,33 @@
1
+ import { ContextMenu, useContextMenuContext } from './chunk-CPJJ3FPW.js';
2
+ import './chunk-YQV5XZIA.js';
3
+ import './chunk-4VUHQFAT.js';
4
+ import './chunk-HS3X3XCW.js';
5
+ import './chunk-4ZPMP47J.js';
6
+ import { useEffect } from 'react';
7
+ import { jsxs, jsx } from 'react/jsx-runtime';
8
+
9
+ function VideoMenu({ rows, title, onSelect, onReady }) {
10
+ return /* @__PURE__ */ jsxs(
11
+ ContextMenu.Root,
12
+ {
13
+ items: rows,
14
+ title,
15
+ clipboard: false,
16
+ onSelect,
17
+ children: [
18
+ /* @__PURE__ */ jsx(Publish, { onReady }),
19
+ /* @__PURE__ */ jsx(ContextMenu.Content, {})
20
+ ]
21
+ }
22
+ );
23
+ }
24
+ function Publish({ onReady }) {
25
+ const menu = useContextMenuContext("Video.Menu");
26
+ useEffect(() => {
27
+ onReady(menu.open);
28
+ return () => onReady(null);
29
+ }, [menu.open, onReady]);
30
+ return null;
31
+ }
32
+
33
+ export { VideoMenu };