@fuzdev/fuz_ui 0.202.0 → 0.203.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 (68) hide show
  1. package/dist/ApiIndex.svelte +6 -3
  2. package/dist/ApiIndex.svelte.d.ts +1 -1
  3. package/dist/ApiIndex.svelte.d.ts.map +1 -1
  4. package/dist/ApiModule.svelte +72 -21
  5. package/dist/ApiModule.svelte.d.ts +1 -1
  6. package/dist/ApiModule.svelte.d.ts.map +1 -1
  7. package/dist/ContextmenuEntry.svelte +11 -10
  8. package/dist/ContextmenuEntry.svelte.d.ts +1 -1
  9. package/dist/ContextmenuEntry.svelte.d.ts.map +1 -1
  10. package/dist/ContextmenuLinkEntry.svelte +30 -12
  11. package/dist/ContextmenuLinkEntry.svelte.d.ts +2 -1
  12. package/dist/ContextmenuLinkEntry.svelte.d.ts.map +1 -1
  13. package/dist/ContextmenuMenu.svelte +207 -0
  14. package/dist/ContextmenuMenu.svelte.d.ts +41 -0
  15. package/dist/ContextmenuMenu.svelte.d.ts.map +1 -0
  16. package/dist/ContextmenuRoot.svelte +58 -260
  17. package/dist/ContextmenuRoot.svelte.d.ts +2 -60
  18. package/dist/ContextmenuRoot.svelte.d.ts.map +1 -1
  19. package/dist/ContextmenuRootForSafariCompatibility.svelte +72 -283
  20. package/dist/ContextmenuRootForSafariCompatibility.svelte.d.ts +2 -58
  21. package/dist/ContextmenuRootForSafariCompatibility.svelte.d.ts.map +1 -1
  22. package/dist/ContextmenuSeparator.svelte +2 -1
  23. package/dist/ContextmenuSeparator.svelte.d.ts.map +1 -1
  24. package/dist/ContextmenuSubmenu.svelte +27 -23
  25. package/dist/ContextmenuSubmenu.svelte.d.ts +1 -1
  26. package/dist/ContextmenuSubmenu.svelte.d.ts.map +1 -1
  27. package/dist/ContextmenuTextEntry.svelte +7 -3
  28. package/dist/ContextmenuTextEntry.svelte.d.ts +2 -1
  29. package/dist/ContextmenuTextEntry.svelte.d.ts.map +1 -1
  30. package/dist/DeclarationLink.svelte +15 -2
  31. package/dist/DeclarationLink.svelte.d.ts +7 -0
  32. package/dist/DeclarationLink.svelte.d.ts.map +1 -1
  33. package/dist/DocsLink.svelte +3 -1
  34. package/dist/DocsLink.svelte.d.ts.map +1 -1
  35. package/dist/DocsTertiaryNav.svelte +2 -1
  36. package/dist/DocsTertiaryNav.svelte.d.ts.map +1 -1
  37. package/dist/LibraryDetail.svelte +9 -3
  38. package/dist/LibraryDetail.svelte.d.ts +1 -1
  39. package/dist/LibraryDetail.svelte.d.ts.map +1 -1
  40. package/dist/ModuleLink.svelte +2 -1
  41. package/dist/ModuleLink.svelte.d.ts.map +1 -1
  42. package/dist/TypeLink.svelte +2 -1
  43. package/dist/TypeLink.svelte.d.ts.map +1 -1
  44. package/dist/contextmenu_helpers.d.ts +324 -1
  45. package/dist/contextmenu_helpers.d.ts.map +1 -1
  46. package/dist/contextmenu_helpers.js +403 -2
  47. package/dist/contextmenu_state.svelte.d.ts +61 -15
  48. package/dist/contextmenu_state.svelte.d.ts.map +1 -1
  49. package/dist/contextmenu_state.svelte.js +206 -119
  50. package/dist/declaration.svelte.d.ts +1 -0
  51. package/dist/declaration.svelte.d.ts.map +1 -1
  52. package/dist/declaration.svelte.js +2 -1
  53. package/dist/icons.d.ts +15 -3
  54. package/dist/icons.d.ts.map +1 -1
  55. package/dist/icons.js +17 -3
  56. package/dist/library.svelte.d.ts +30 -3
  57. package/dist/library.svelte.d.ts.map +1 -1
  58. package/dist/library.svelte.js +38 -0
  59. package/dist/module.svelte.d.ts +62 -1
  60. package/dist/module.svelte.d.ts.map +1 -1
  61. package/dist/module.svelte.js +71 -1
  62. package/package.json +2 -2
  63. package/src/lib/contextmenu_helpers.ts +546 -3
  64. package/src/lib/contextmenu_state.svelte.ts +219 -119
  65. package/src/lib/declaration.svelte.ts +2 -1
  66. package/src/lib/icons.ts +19 -3
  67. package/src/lib/library.svelte.ts +43 -1
  68. package/src/lib/module.svelte.ts +97 -2
@@ -18,20 +18,19 @@
18
18
  * and relies on the standard `contextmenu` event.
19
19
  */
20
20
  import {swallow} from '@fuzdev/fuz_util/dom.js';
21
- import {DEV} from 'esm-env';
22
21
  import {on} from 'svelte/events';
23
- import type {ComponentProps, Snippet} from 'svelte';
24
22
 
25
23
  import {
26
24
  contextmenu_context,
27
- contextmenu_dimensions_context,
28
25
  ContextmenuState,
29
26
  contextmenu_open,
30
27
  contextmenu_check_global_root,
31
28
  } from './contextmenu_state.svelte.js';
32
- import ContextmenuLinkEntry from './ContextmenuLinkEntry.svelte';
33
- import ContextmenuTextEntry from './ContextmenuTextEntry.svelte';
34
- import ContextmenuSeparator from './ContextmenuSeparator.svelte';
29
+ import ContextmenuMenu, {
30
+ link_entry_default,
31
+ text_entry_default,
32
+ separator_entry_default,
33
+ } from './ContextmenuMenu.svelte';
35
34
  import {
36
35
  CONTEXTMENU_DEFAULT_OPEN_OFFSET_X,
37
36
  CONTEXTMENU_DEFAULT_OPEN_OFFSET_Y,
@@ -40,10 +39,10 @@
40
39
  CONTEXTMENU_DEFAULT_LONGPRESS_DURATION,
41
40
  CONTEXTMENU_DEFAULT_LONGPRESS_MOVE_TOLERANCE,
42
41
  contextmenu_is_valid_target,
43
- contextmenu_create_keyboard_handlers,
44
- contextmenu_create_keydown_handler,
45
- contextmenu_calculate_constrained_x,
46
- contextmenu_calculate_constrained_y,
42
+ contextmenu_resolve_contextmenu_event,
43
+ ContextmenuBypassTracker,
44
+ ContextmenuOpenGuard,
45
+ type ContextmenuRootBaseProps,
47
46
  } from './contextmenu_helpers.js';
48
47
 
49
48
  const {
@@ -60,8 +59,7 @@
60
59
  text_entry = text_entry_default,
61
60
  separator_entry = separator_entry_default,
62
61
  children,
63
- }: {
64
- contextmenu?: ContextmenuState;
62
+ }: ContextmenuRootBaseProps & {
65
63
  /**
66
64
  * The number of pixels the pointer can be moved without canceling `longpress`.
67
65
  */
@@ -70,93 +68,25 @@
70
68
  * The number of milliseconds after a touch starts before opening the Fuz contextmenu.
71
69
  */
72
70
  longpress_duration?: number;
73
- /**
74
- * Whether to detect tap-then-longpress to bypass the Fuz contextmenu.
75
- * This allows access to the system contextmenu by tapping once then long-pressing.
76
- * Setting to `false` disables the gesture.
77
- */
78
- bypass_with_tap_then_longpress?: boolean;
79
- /**
80
- * The number of milliseconds between taps to detect a gesture that bypasses the Fuz contextmenu.
81
- * Used only when `bypass_with_tap_then_longpress` is true.
82
- * If the duration is too long, it'll detect more false positives and interrupt normal usage,
83
- * but too short and some people will have difficulty performing the gesture.
84
- */
85
- bypass_window?: number;
86
- /**
87
- * The number of pixels the pointer can be moved between taps to detect a tap-then-longpress.
88
- * Used only when `bypass_with_tap_then_longpress` is true.
89
- */
90
- bypass_move_tolerance?: number;
91
- /**
92
- * The number of pixels to offset from the pointer X position when opened.
93
- * Useful to ensure the first menu item is immediately under the pointer.
94
- */
95
- open_offset_x?: number;
96
- /**
97
- * The number of pixels to offset from the pointer Y position when opened.
98
- * Useful to ensure the first menu item is immediately under the pointer.
99
- */
100
- open_offset_y?: number;
101
- /**
102
- * If `true`, wraps `children` with a div and listens to events on it instead of the window.
103
- */
104
- scoped?: boolean;
105
- /**
106
- * Snippet for rendering link entries.
107
- * Set to `null` to disable automatic link detection.
108
- * Defaults to `link_entry_default` which renders `ContextmenuLinkEntry`.
109
- */
110
- link_entry?: Snippet<[ComponentProps<typeof ContextmenuLinkEntry>]> | null;
111
- /**
112
- * Snippet for rendering copy text entries.
113
- * Set to `null` to disable automatic copy text detection.
114
- * Defaults to `text_entry_default` which renders `ContextmenuTextEntry`.
115
- */
116
- text_entry?: Snippet<[ComponentProps<typeof ContextmenuTextEntry>]> | null;
117
- /**
118
- * Snippet for rendering separator entries.
119
- * Set to `null` to disable automatic separator rendering.
120
- * Defaults to `separator_entry_default` which renders `ContextmenuSeparator`.
121
- */
122
- separator_entry?: Snippet<[ComponentProps<typeof ContextmenuSeparator>]> | null;
123
- children: Snippet;
124
71
  } = $props();
125
72
 
126
73
  contextmenu_context.set(() => contextmenu);
127
74
 
128
- if (DEV) contextmenu_check_global_root(() => scoped); // TODO @many is this import tree-shaken?
75
+ contextmenu_check_global_root(() => scoped); // DEV-only internally, eliminated from production bundles
129
76
 
77
+ // The menu element while opened, bound from `ContextmenuMenu.svelte`.
130
78
  let el: HTMLElement | undefined = $state.raw();
131
79
 
132
- const {layout} = $derived(contextmenu);
133
-
134
- const dimensions = contextmenu_dimensions_context.set();
135
-
136
- const x = $derived(
137
- contextmenu_calculate_constrained_x(contextmenu.x, dimensions.width, layout.width),
138
- );
139
- const y = $derived(
140
- contextmenu_calculate_constrained_y(contextmenu.y, dimensions.height, layout.height),
141
- );
142
-
143
80
  // TODO maybe show an indicator fade in at these coordinates
144
81
 
145
- // State for tap-then-longpress bypass detection.
146
- // These values are `undefined` when unused, and `null` after being reset.
147
- let touch_x: number | undefined | null = $state.raw();
148
- let touch_y: number | undefined | null = $state.raw();
149
- let first_tap_time: number | undefined | null = $state.raw();
150
- let longpress_timeout: NodeJS.Timeout | undefined | null = $state.raw();
151
- let longpress_opened: boolean | undefined = $state.raw();
152
- let longpress_bypass: boolean | undefined = $state.raw();
153
- let tap_tracking_timeout: NodeJS.Timeout | undefined | null = $state.raw();
82
+ // Longpress tracking state - plain non-reactive variables, only used inside event handlers.
83
+ let touch_x = 0;
84
+ let touch_y = 0;
85
+ let longpress_timeout: NodeJS.Timeout | null = null;
86
+ let longpress_opened = false;
154
87
 
155
- /**
156
- * Blocks the next click event. Set to true when a longpress completes to prevent
157
- * iOS's synthesized click from activating the first menu item.
158
- */
159
- let block_next_click = $state.raw(false);
88
+ const bypass_tracker = new ContextmenuBypassTracker();
89
+ const open_guard = new ContextmenuOpenGuard();
160
90
 
161
91
  /**
162
92
  * Adds contextmenu-pending class to body during longpress tracking.
@@ -180,7 +110,7 @@
180
110
  */
181
111
  const reset_longpress_timeout = (): void => {
182
112
  longpress_opened = false;
183
- if (longpress_timeout != null) {
113
+ if (longpress_timeout !== null) {
184
114
  clearTimeout(longpress_timeout);
185
115
  longpress_timeout = null;
186
116
  }
@@ -188,69 +118,45 @@
188
118
  // Don't clear tap tracking state here - we need it for tap-then-longpress detection
189
119
  };
190
120
 
191
- /**
192
- * Clears tap tracking state and bypass flag used for bypass detection.
193
- */
194
- const reset_tap_tracking = (): void => {
195
- first_tap_time = null;
196
- touch_x = null;
197
- touch_y = null;
198
- longpress_bypass = false;
199
- if (tap_tracking_timeout != null) {
200
- clearTimeout(tap_tracking_timeout);
201
- tap_tracking_timeout = null;
202
- }
203
- };
204
-
205
121
  /**
206
122
  * Resets all state - both longpress and tap tracking.
207
123
  */
208
124
  const reset_all = (): void => {
209
125
  reset_longpress_timeout(); // Also removes contextmenu-pending class
210
- reset_tap_tracking();
126
+ bypass_tracker.reset();
211
127
  };
212
128
 
129
+ const open_options = $derived({
130
+ open_offset_x,
131
+ open_offset_y,
132
+ link_enabled: link_entry !== null,
133
+ text_enabled: text_entry !== null,
134
+ separator_enabled: separator_entry !== null,
135
+ });
136
+
213
137
  const on_window_contextmenu = (e: MouseEvent) => {
214
- // handle the tap-then-longpress bypass gesture
215
- if (longpress_bypass) {
216
- reset_tap_tracking(); // Clear bypass state after using it
217
- return;
218
- }
219
- const {target} = e;
138
+ // let the system contextmenu through after a tap-then-longpress bypass gesture
139
+ if (bypass_tracker.consume()) return;
220
140
  // handle touch devices that trigger `'contextmenu'` slower than our longpress
221
141
  if (longpress_opened) {
222
142
  // Don't prevent contextmenu events on elements inside our own contextmenu
223
143
  // This allows the browser's native contextmenu (useful for dev tools, inspecting elements, etc.)
224
- if (el?.contains(target as Node)) {
144
+ if (el?.contains(e.target as Node)) {
225
145
  return; // Let the event pass through
226
146
  }
227
147
  reset_all();
228
148
  swallow(e);
229
149
  return;
230
150
  }
231
- if (!contextmenu_is_valid_target(target, e.shiftKey)) {
232
- return;
233
- }
234
- // Don't open contextmenu when clicking on the menu itself
235
- if (el?.contains(target)) {
236
- return;
237
- }
238
- if (
239
- contextmenu_open(target, e.clientX + open_offset_x, e.clientY + open_offset_y, contextmenu, {
240
- link_enabled: link_entry !== null,
241
- text_enabled: text_entry !== null,
242
- separator_enabled: separator_entry !== null,
243
- })
244
- ) {
245
- swallow(e);
151
+ if (contextmenu_resolve_contextmenu_event(e, contextmenu, el, open_guard, open_options)) {
246
152
  reset_all(); // handle touch devices that trigger `'contextmenu'` faster than our longpress
247
153
  }
248
154
  };
249
155
 
250
- // Needed for the iOS workaround. Registered with { passive: false } via $effect (window) or attachment (scoped).
156
+ // Needed for the iOS workaround. Registered non-passively via `touch_event_attachment` (window or scoped element).
251
157
  const touchstart = (e: TouchEvent): void => {
252
158
  longpress_opened = false;
253
- block_next_click = false; // Clear any stale click blocking flag
159
+ open_guard.touchstart(); // begins a gesture, clearing stale flags
254
160
  const {touches, target} = e;
255
161
  if (
256
162
  contextmenu.opened ||
@@ -264,39 +170,13 @@
264
170
 
265
171
  const {clientX, clientY} = touches[0]!;
266
172
 
267
- // Bypass the contextmenu behavior in certain conditions including a tap-and-longpress gesture.
268
- // To handle tap-then-longpress we need to see if `first_tap_time`
269
- // is less than `bypass_window`, and also allow a small amount
270
- // of pointer movement, `bypass_move_tolerance`.
271
173
  // The builtin `'contextmenu'` event will still fire for non-iOS browsers,
272
- // so `longpress_bypass` is used to tell the handler `on_window_contextmenu` to exit early.
273
- if (bypass_with_tap_then_longpress) {
274
- if (
275
- first_tap_time != null &&
276
- performance.now() - first_tap_time < bypass_window &&
277
- Math.hypot(clientX - touch_x!, clientY - touch_y!) < bypass_move_tolerance
278
- ) {
279
- // Tap-then-longpress detected! Set bypass and clear tap tracking state.
280
- // Must manually clear state (not call reset_tap_tracking) to preserve bypass flag.
281
- longpress_bypass = true;
282
- first_tap_time = null;
283
- touch_x = null;
284
- touch_y = null;
285
- if (tap_tracking_timeout != null) {
286
- clearTimeout(tap_tracking_timeout); // Prevent timeout from clearing bypass
287
- tap_tracking_timeout = null;
288
- }
289
- return;
290
- }
291
- // Record this tap for potential future bypass detection
292
- first_tap_time = performance.now();
293
- // Set timeout to clear stale tap tracking after the detection window expires
294
- if (tap_tracking_timeout != null) {
295
- clearTimeout(tap_tracking_timeout);
296
- }
297
- tap_tracking_timeout = setTimeout(() => {
298
- reset_tap_tracking();
299
- }, bypass_window);
174
+ // so the tracker's `bypassed` flag tells `on_window_contextmenu` to exit early.
175
+ if (
176
+ bypass_with_tap_then_longpress &&
177
+ bypass_tracker.track(clientX, clientY, bypass_window, bypass_move_tolerance)
178
+ ) {
179
+ return;
300
180
  }
301
181
 
302
182
  touch_x = clientX;
@@ -305,26 +185,33 @@
305
185
  // Add pending class to enable aggressive iOS blocking via CSS during tracking
306
186
  add_contextmenu_pending_class();
307
187
 
308
- if (longpress_timeout != null) reset_longpress_timeout();
188
+ if (longpress_timeout !== null) reset_longpress_timeout();
309
189
  longpress_timeout = setTimeout(() => {
310
190
  longpress_opened = true;
311
191
  remove_contextmenu_pending_class(); // Tracking complete, menu opening
312
- contextmenu_open(target, touch_x! + open_offset_x, touch_y! + open_offset_y, contextmenu, {
313
- link_enabled: link_entry !== null,
314
- text_enabled: text_entry !== null,
315
- separator_enabled: separator_entry !== null,
316
- });
192
+ if (
193
+ contextmenu_open(
194
+ target,
195
+ touch_x + open_offset_x,
196
+ touch_y + open_offset_y,
197
+ contextmenu,
198
+ open_options,
199
+ )
200
+ ) {
201
+ // guard the release of this gesture from interacting with the menu
202
+ open_guard.opened();
203
+ }
317
204
  }, longpress_duration);
318
205
  };
319
206
 
320
- // Needed for the iOS workaround. Registered with { passive: false } via $effect (window) or attachment (scoped).
207
+ // Needed for the iOS workaround. Registered non-passively via `touch_event_attachment` (window or scoped element).
321
208
  const touchmove = (e: TouchEvent): void => {
322
209
  // Exit early if no pending longpress or menu is already open
323
- if (longpress_timeout == null || contextmenu.opened) return;
210
+ if (longpress_timeout === null || contextmenu.opened) return;
324
211
  const {touches} = e;
325
212
  if (touches.length !== 1) return;
326
213
  const {clientX, clientY} = touches[0]!;
327
- const distance = Math.hypot(clientX - touch_x!, clientY - touch_y!);
214
+ const distance = Math.hypot(clientX - touch_x, clientY - touch_y);
328
215
  if (distance > longpress_move_tolerance) {
329
216
  // User is scrolling - cancel longpress but DON'T preventDefault
330
217
  reset_longpress_timeout();
@@ -334,43 +221,30 @@
334
221
  // CRITICAL: Prevent iOS from showing magnifier, text selection, and link callouts
335
222
  e.preventDefault();
336
223
  };
337
- // Needed for the iOS workaround. Registered with { passive: false } via $effect.
224
+ // Needed for the iOS workaround. Registered non-passively via `touch_event_attachment` (window or scoped element).
338
225
  const touchend = (e: TouchEvent): void => {
226
+ // Swallow the release of a gesture that opened the menu (custom longpress or
227
+ // native `contextmenu`), stopping the browser from synthesizing mouse events at
228
+ // the touch point that would activate the first item, and arming the click
229
+ // blocker for iOS, which can synthesize the click regardless.
230
+ open_guard.touchend(e);
339
231
  // Clear longpress timeout if it exists
340
- if (longpress_timeout != null) {
341
- if (longpress_opened) {
342
- swallow(e);
343
- // Block the next click to prevent iOS's synthesized click from activating the first menu item
344
- block_next_click = true;
345
- }
232
+ if (longpress_timeout !== null) {
346
233
  reset_longpress_timeout();
347
234
  }
348
- // Clear bypass flag if it was set but the contextmenu event hasn't fired yet
349
- // This handles the case where bypass is detected but user lifts finger before native menu opens
350
- if (longpress_bypass) {
351
- reset_tap_tracking();
352
- }
353
- // Note: We don't clear tap tracking state here - we preserve it for tap-then-longpress detection
235
+ // Clear a pending bypass when the user lifts before the native contextmenu opens.
236
+ // Tap tracking is otherwise preserved for tap-then-longpress detection.
237
+ bypass_tracker.consume();
354
238
  };
355
239
 
356
240
  /**
357
241
  * Handle touchcancel - this should reset all state since the gesture was interrupted.
358
242
  */
359
243
  const touchcancel = (): void => {
244
+ open_guard.reset();
360
245
  reset_all();
361
246
  };
362
247
 
363
- // Passive listener that runs during the event's `capture` phase
364
- // so that things like the Dialog don't eat the events and prevent the contextmenu from closing.
365
- const mousedown = (e: MouseEvent) => {
366
- if (el && !el.contains(e.target as any)) {
367
- contextmenu.close();
368
- }
369
- };
370
-
371
- const keyboard_handlers = $derived(contextmenu_create_keyboard_handlers(contextmenu));
372
- const keydown = $derived(contextmenu_create_keydown_handler(keyboard_handlers));
373
-
374
248
  /**
375
249
  * Creates an attachment that registers touch event listeners with { passive: false }
376
250
  * to enable preventDefault() on iOS Safari. Works for both window (non-scoped mode) and
@@ -412,15 +286,15 @@
412
286
 
413
287
  <svelte:window
414
288
  oncontextmenu={scoped ? undefined : on_window_contextmenu}
415
- onmousedown={!contextmenu.opened ? undefined : mousedown}
416
- onkeydown={!contextmenu.opened ? undefined : keydown}
417
289
  {@attach scoped ? undefined : touch_event_attachment}
418
290
  />
419
291
 
420
292
  {#if scoped}
293
+ <!-- A transparent (`display: contents`) event-delegation wrapper. `role="group"` rather
294
+ than a landmark role - it satisfies the a11y lint without requiring an accessible name. -->
421
295
  <div
422
296
  class="contextmenu-root"
423
- role="region"
297
+ role="group"
424
298
  oncontextmenu={on_window_contextmenu}
425
299
  {@attach touch_event_attachment}
426
300
  >
@@ -430,61 +304,7 @@
430
304
  {@render children()}
431
305
  {/if}
432
306
 
433
- {#if !contextmenu.has_custom_layout}
434
- <div
435
- class="contextmenu-layout"
436
- bind:clientWidth={layout.width}
437
- bind:clientHeight={layout.height}
438
- aria-hidden="true"
439
- ></div>
440
- {/if}
441
-
442
- <!-- TODO Maybe animate a subtle highlight around the contextmenu as it appears? -->
443
- {#if contextmenu.opened}
444
- <ul
445
- class="contextmenu unstyled pane"
446
- role="menu"
447
- aria-label="context menu"
448
- tabindex="-1"
449
- bind:this={el}
450
- bind:offsetWidth={dimensions.width}
451
- bind:offsetHeight={dimensions.height}
452
- style:transform="translate3d({x}px, {y}px, 0)"
453
- onclickcapture={block_next_click
454
- ? (e) => {
455
- // iOS synthesizes a click after touchend which
456
- // can unintentionally activate the first menu item. This blocks it.
457
- block_next_click = false;
458
- swallow(e);
459
- }
460
- : undefined}
461
- >
462
- <!-- TODO maybe this should be generic? -->
463
- {#each contextmenu.params as p (p)}
464
- {#if typeof p === 'function'}
465
- {@render p()}
466
- {:else if p.snippet === 'link'}
467
- {@render link_entry?.(p.props)}
468
- {:else if p.snippet === 'text'}
469
- {@render text_entry?.(p.props)}
470
- {:else if p.snippet === 'separator'}
471
- {@render separator_entry?.(p.props)}
472
- {/if}
473
- {/each}
474
- </ul>
475
- {/if}
476
-
477
- {#snippet link_entry_default(props: ComponentProps<typeof ContextmenuLinkEntry>)}
478
- <ContextmenuLinkEntry {...props} />
479
- {/snippet}
480
-
481
- {#snippet text_entry_default(props: ComponentProps<typeof ContextmenuTextEntry>)}
482
- <ContextmenuTextEntry {...props} />
483
- {/snippet}
484
-
485
- {#snippet separator_entry_default(props: ComponentProps<typeof ContextmenuSeparator>)}
486
- <ContextmenuSeparator {...props} />
487
- {/snippet}
307
+ <ContextmenuMenu bind:el {contextmenu} {open_guard} {link_entry} {text_entry} {separator_entry} />
488
308
 
489
309
  <style>
490
310
  :global(body.contextmenu-pending) {
@@ -499,35 +319,4 @@
499
319
  .contextmenu-root {
500
320
  display: contents;
501
321
  }
502
- .contextmenu {
503
- --icon_size: var(--icon_size_xs);
504
- /* TODO maybe make this responsive or a max of the page width
505
- minus some space to tap items covered by the menu on the side,
506
- or consider a totally different design for small screens (more dialog-like) */
507
- --contextmenu_width: 320px;
508
- position: fixed;
509
- left: 0;
510
- top: 0;
511
- z-index: var(--contextmenu_z_index, 200);
512
- max-width: var(--contextmenu_width);
513
- width: 100%;
514
- /* Re-enable callouts on the menu itself to allow native contextmenu (for dev tools).
515
- Resets the global body blocking. Prevents the menu from being selected. */
516
- -webkit-touch-callout: initial !important;
517
- }
518
- /* TODO hacky */
519
- .contextmenu,
520
- .contextmenu :global(menu.pane) {
521
- border: var(--contextmenu_border_width, var(--border_width))
522
- var(--contextmenu_border_style, var(--border_style))
523
- var(--contextmenu_border_color, var(--border_color));
524
- border-radius: var(--contextmenu_border_radius, var(--border_radius_xs));
525
- }
526
-
527
- /* TODO better way to do this? */
528
- .contextmenu-layout {
529
- z-index: -200;
530
- position: fixed;
531
- inset: 0;
532
- }
533
322
  </style>
@@ -1,10 +1,5 @@
1
- import type { ComponentProps, Snippet } from 'svelte';
2
- import { ContextmenuState } from './contextmenu_state.svelte.js';
3
- import ContextmenuLinkEntry from './ContextmenuLinkEntry.svelte';
4
- import ContextmenuTextEntry from './ContextmenuTextEntry.svelte';
5
- import ContextmenuSeparator from './ContextmenuSeparator.svelte';
6
- type $$ComponentProps = {
7
- contextmenu?: ContextmenuState;
1
+ import { type ContextmenuRootBaseProps } from './contextmenu_helpers.js';
2
+ type $$ComponentProps = ContextmenuRootBaseProps & {
8
3
  /**
9
4
  * The number of pixels the pointer can be moved without canceling `longpress`.
10
5
  */
@@ -13,57 +8,6 @@ type $$ComponentProps = {
13
8
  * The number of milliseconds after a touch starts before opening the Fuz contextmenu.
14
9
  */
15
10
  longpress_duration?: number;
16
- /**
17
- * Whether to detect tap-then-longpress to bypass the Fuz contextmenu.
18
- * This allows access to the system contextmenu by tapping once then long-pressing.
19
- * Setting to `false` disables the gesture.
20
- */
21
- bypass_with_tap_then_longpress?: boolean;
22
- /**
23
- * The number of milliseconds between taps to detect a gesture that bypasses the Fuz contextmenu.
24
- * Used only when `bypass_with_tap_then_longpress` is true.
25
- * If the duration is too long, it'll detect more false positives and interrupt normal usage,
26
- * but too short and some people will have difficulty performing the gesture.
27
- */
28
- bypass_window?: number;
29
- /**
30
- * The number of pixels the pointer can be moved between taps to detect a tap-then-longpress.
31
- * Used only when `bypass_with_tap_then_longpress` is true.
32
- */
33
- bypass_move_tolerance?: number;
34
- /**
35
- * The number of pixels to offset from the pointer X position when opened.
36
- * Useful to ensure the first menu item is immediately under the pointer.
37
- */
38
- open_offset_x?: number;
39
- /**
40
- * The number of pixels to offset from the pointer Y position when opened.
41
- * Useful to ensure the first menu item is immediately under the pointer.
42
- */
43
- open_offset_y?: number;
44
- /**
45
- * If `true`, wraps `children` with a div and listens to events on it instead of the window.
46
- */
47
- scoped?: boolean;
48
- /**
49
- * Snippet for rendering link entries.
50
- * Set to `null` to disable automatic link detection.
51
- * Defaults to `link_entry_default` which renders `ContextmenuLinkEntry`.
52
- */
53
- link_entry?: Snippet<[ComponentProps<typeof ContextmenuLinkEntry>]> | null;
54
- /**
55
- * Snippet for rendering copy text entries.
56
- * Set to `null` to disable automatic copy text detection.
57
- * Defaults to `text_entry_default` which renders `ContextmenuTextEntry`.
58
- */
59
- text_entry?: Snippet<[ComponentProps<typeof ContextmenuTextEntry>]> | null;
60
- /**
61
- * Snippet for rendering separator entries.
62
- * Set to `null` to disable automatic separator rendering.
63
- * Defaults to `separator_entry_default` which renders `ContextmenuSeparator`.
64
- */
65
- separator_entry?: Snippet<[ComponentProps<typeof ContextmenuSeparator>]> | null;
66
- children: Snippet;
67
11
  };
68
12
  declare const ContextmenuRootForSafariCompatibility: import("svelte").Component<$$ComponentProps, {}, "">;
69
13
  type ContextmenuRootForSafariCompatibility = ReturnType<typeof ContextmenuRootForSafariCompatibility>;
@@ -1 +1 @@
1
- {"version":3,"file":"ContextmenuRootForSafariCompatibility.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuRootForSafariCompatibility.svelte"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAC,cAAc,EAAE,OAAO,EAAC,MAAM,QAAQ,CAAC;AAEpD,OAAO,EAGL,gBAAgB,EAGhB,MAAM,+BAA+B,CAAC;AACxC,OAAO,oBAAoB,MAAM,+BAA+B,CAAC;AACjE,OAAO,oBAAoB,MAAM,+BAA+B,CAAC;AACjE,OAAO,oBAAoB,MAAM,+BAA+B,CAAC;AAehE,KAAK,gBAAgB,GAAI;IACxB,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B;;OAEG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC;;OAEG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,8BAA8B,CAAC,EAAE,OAAO,CAAC;IACzC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC,cAAc,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAC3E;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC,cAAc,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAC3E;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC,CAAC,cAAc,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAChF,QAAQ,EAAE,OAAO,CAAC;CAClB,CAAC;AAoXH,QAAA,MAAM,qCAAqC,sDAAwC,CAAC;AACpF,KAAK,qCAAqC,GAAG,UAAU,CAAC,OAAO,qCAAqC,CAAC,CAAC;AACtG,eAAe,qCAAqC,CAAC"}
1
+ {"version":3,"file":"ContextmenuRootForSafariCompatibility.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuRootForSafariCompatibility.svelte"],"names":[],"mappings":"AAmCA,OAAO,EAWL,KAAK,wBAAwB,EAC7B,MAAM,0BAA0B,CAAC;AAElC,KAAK,gBAAgB,GAAI,wBAAwB,GAAG;IACnD;;OAEG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC;;OAEG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAiQH,QAAA,MAAM,qCAAqC,sDAAwC,CAAC;AACpF,KAAK,qCAAqC,GAAG,UAAU,CAAC,OAAO,qCAAqC,CAAC,CAAC;AACtG,eAAe,qCAAqC,CAAC"}
@@ -4,7 +4,8 @@
4
4
  const {...rest}: SvelteHTMLElements['li'] = $props();
5
5
  </script>
6
6
 
7
- <li role="separator" aria-orientation="vertical" {...rest} class:contextmenu-separator={true}></li>
7
+ <!-- `aria-orientation` is omitted - it defaults to horizontal, correct for a vertical menu's rule -->
8
+ <li role="separator" {...rest} class:contextmenu-separator={true}></li>
8
9
 
9
10
  <style>
10
11
  .contextmenu-separator {
@@ -1 +1 @@
1
- {"version":3,"file":"ContextmenuSeparator.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuSeparator.svelte"],"names":[],"mappings":"AAkBA,QAAA,MAAM,oBAAoB,gFAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"ContextmenuSeparator.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuSeparator.svelte"],"names":[],"mappings":"AAmBA,QAAA,MAAM,oBAAoB,gFAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}