svelte-multiselect 11.5.1 → 11.5.2

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.
@@ -1,42 +1,43 @@
1
1
  import type { Page } from '@sveltejs/kit';
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
- declare function $$render<Route extends string | [string, string] | [string, string[]]>(): {
5
- props: {
6
- routes: Route[];
7
- children?: Snippet<[{
8
- is_open: boolean;
9
- panel_id: string;
10
- routes: Route[];
11
- }]>;
12
- link?: Snippet<[{
13
- href: string;
14
- label: string;
15
- }]>;
16
- menu_props?: HTMLAttributes<HTMLDivElement>;
17
- link_props?: HTMLAttributes<HTMLAnchorElement>;
18
- page?: Page;
19
- labels?: Record<string, string>;
20
- } & Omit<HTMLAttributes<HTMLElement>, "children">;
21
- exports: {};
22
- bindings: "";
23
- slots: {};
24
- events: {};
25
- };
26
- declare class __sveltets_Render<Route extends string | [string, string] | [string, string[]]> {
27
- props(): ReturnType<typeof $$render<Route>>['props'];
28
- events(): ReturnType<typeof $$render<Route>>['events'];
29
- slots(): ReturnType<typeof $$render<Route>>['slots'];
30
- bindings(): "";
31
- exports(): {};
4
+ import { type TooltipOptions } from './attachments';
5
+ import type { NavRoute, NavRouteObject } from './types';
6
+ interface ItemSnippetParams {
7
+ route: NavRouteObject;
8
+ href: string;
9
+ label: string;
10
+ is_active: boolean;
11
+ is_dropdown: boolean;
12
+ render_default: Snippet;
32
13
  }
33
- interface $$IsomorphicComponent {
34
- new <Route extends string | [string, string] | [string, string[]]>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Route>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Route>['props']>, ReturnType<__sveltets_Render<Route>['events']>, ReturnType<__sveltets_Render<Route>['slots']>> & {
35
- $$bindings?: ReturnType<__sveltets_Render<Route>['bindings']>;
36
- } & ReturnType<__sveltets_Render<Route>['exports']>;
37
- <Route extends string | [string, string] | [string, string[]]>(internal: unknown, props: ReturnType<__sveltets_Render<Route>['props']> & {}): ReturnType<__sveltets_Render<Route>['exports']>;
38
- z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
39
- }
40
- declare const Nav: $$IsomorphicComponent;
41
- type Nav<Route extends string | [string, string] | [string, string[]]> = InstanceType<typeof Nav<Route>>;
14
+ type $$ComponentProps = {
15
+ routes: NavRoute[];
16
+ children?: Snippet<[{
17
+ is_open: boolean;
18
+ panel_id: string;
19
+ routes: NavRoute[];
20
+ }]>;
21
+ item?: Snippet<[ItemSnippetParams]>;
22
+ link?: Snippet<[{
23
+ href: string;
24
+ label: string;
25
+ }]>;
26
+ menu_props?: HTMLAttributes<HTMLDivElement>;
27
+ link_props?: HTMLAttributes<HTMLAnchorElement>;
28
+ page?: Page;
29
+ labels?: Record<string, string>;
30
+ tooltips?: Record<string, string | Omit<TooltipOptions, `disabled`>>;
31
+ tooltip_options?: Omit<TooltipOptions, `content`>;
32
+ breakpoint?: number;
33
+ onnavigate?: (data: {
34
+ href: string;
35
+ event: MouseEvent;
36
+ route: NavRouteObject;
37
+ }) => void | false;
38
+ onopen?: () => void;
39
+ onclose?: () => void;
40
+ } & Omit<HTMLAttributes<HTMLElementTagNameMap[`nav`]>, `children`>;
41
+ declare const Nav: import("svelte").Component<$$ComponentProps, {}, "">;
42
+ type Nav = ReturnType<typeof Nav>;
42
43
  export default Nav;
@@ -1,6 +1,11 @@
1
1
  <script lang="ts">import { Spring } from 'svelte/motion';
2
- let { wiggle = $bindable(false), angle = 0, scale = 1, dx = 0, dy = 0, duration = 200, stiffness = 0.05, damping = 0.1, children, } = $props();
3
- const store = Spring.of(() => (wiggle ? { scale, angle, dx, dy } : { angle: 0, scale: 1, dx: 0, dy: 0 }), { stiffness, damping });
2
+ let { wiggle = $bindable(false), angle = 0, scale = 1, dx = 0, dy = 0, duration = 200, spring_options = $bindable({ stiffness: 0.05, damping: 0.1 }), children, ...rest } = $props();
3
+ const store = Spring.of(() => (wiggle ? { scale, angle, dx, dy } : { angle: 0, scale: 1, dx: 0, dy: 0 }), spring_options);
4
+ // update spring physics when options change
5
+ $effect(() => {
6
+ store.stiffness = spring_options.stiffness;
7
+ store.damping = spring_options.damping;
8
+ });
4
9
  $effect.pre(() => {
5
10
  if (wiggle)
6
11
  setTimeout(() => (wiggle = false), duration);
@@ -8,6 +13,8 @@ $effect.pre(() => {
8
13
  </script>
9
14
 
10
15
  <span
16
+ {...rest}
17
+ style:display="inline-block"
11
18
  style:transform="rotate({store.current.angle}deg) scale({store.current.scale})
12
19
  translate({store.current.dx}px, {store.current.dy}px)"
13
20
  >
@@ -1,15 +1,18 @@
1
1
  import type { Snippet } from 'svelte';
2
- type $$ComponentProps = {
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type $$ComponentProps = HTMLAttributes<HTMLSpanElement> & {
3
4
  wiggle?: boolean;
4
5
  angle?: number;
5
6
  scale?: number;
6
7
  dx?: number;
7
8
  dy?: number;
8
9
  duration?: number;
9
- stiffness?: number;
10
- damping?: number;
10
+ spring_options?: {
11
+ stiffness: number;
12
+ damping: number;
13
+ };
11
14
  children?: Snippet;
12
15
  };
13
- declare const Wiggle: import("svelte").Component<$$ComponentProps, {}, "wiggle">;
16
+ declare const Wiggle: import("svelte").Component<$$ComponentProps, {}, "wiggle" | "spring_options">;
14
17
  type Wiggle = ReturnType<typeof Wiggle>;
15
18
  export default Wiggle;
@@ -56,8 +56,12 @@ export interface TooltipOptions {
56
56
  content?: string;
57
57
  placement?: `top` | `bottom` | `left` | `right`;
58
58
  delay?: number;
59
- disabled?: boolean;
59
+ hide_delay?: number;
60
+ disabled?: boolean | `touch-devices`;
60
61
  style?: string;
62
+ show_arrow?: boolean;
63
+ offset?: number;
64
+ allow_html?: boolean;
61
65
  }
62
66
  export declare const tooltip: (options?: TooltipOptions) => Attachment;
63
67
  export type ClickOutsideConfig<T extends HTMLElement> = {
@@ -358,35 +358,46 @@ function clear_tooltip() {
358
358
  if (hide_timeout)
359
359
  clearTimeout(hide_timeout);
360
360
  if (current_tooltip) {
361
+ // Remove aria-describedby from the trigger element
362
+ current_tooltip._owner?.removeAttribute(`aria-describedby`);
361
363
  current_tooltip.remove();
362
364
  current_tooltip = null;
363
365
  }
364
366
  }
365
367
  export const tooltip = (options = {}) => (node) => {
366
- // Handle null/undefined elements
367
- if (!node || !(node instanceof HTMLElement))
368
+ // SSR guard + element validation
369
+ if (typeof document === `undefined` || !(node instanceof HTMLElement))
368
370
  return;
369
- // Handle null/undefined options
370
- const safe_options = options || {};
371
371
  const cleanup_functions = [];
372
+ // Handle disabled option
373
+ if (options.disabled === true)
374
+ return;
375
+ // Track current input method for 'touch-devices' option (runtime detection, not capability sniffing)
376
+ // This allows tooltips on hybrid devices (Surface, iPad with mouse) when using mouse/stylus
377
+ let last_pointer_type = `mouse`;
378
+ const track_pointer = (event) => {
379
+ last_pointer_type = event.pointerType;
380
+ };
381
+ if (options.disabled === `touch-devices`) {
382
+ document.addEventListener(`pointerdown`, track_pointer, true);
383
+ cleanup_functions.push(() => document.removeEventListener(`pointerdown`, track_pointer, true));
384
+ }
372
385
  function setup_tooltip(element) {
373
- if (!element || safe_options.disabled)
374
- return;
375
386
  // Use let so content can be updated reactively
376
- let content = safe_options.content || element.title ||
387
+ let content = options.content || element.title ||
377
388
  element.getAttribute(`aria-label`) || element.getAttribute(`data-title`);
378
389
  if (!content)
379
390
  return;
380
391
  // Store original title and remove it to prevent default tooltip
381
392
  // Only store title if we're not using custom content
382
- if (element.title && !safe_options.content) {
393
+ if (element.title && !options.content) {
383
394
  element.setAttribute(`data-original-title`, element.title);
384
395
  element.removeAttribute(`title`);
385
396
  }
386
397
  // Reactively update content when tooltip attributes change
387
398
  const tooltip_attrs = [`title`, `aria-label`, `data-title`];
388
399
  const observer = new MutationObserver((mutations) => {
389
- if (safe_options.content)
400
+ if (options.content)
390
401
  return; // custom content takes precedence
391
402
  for (const { type, attributeName } of mutations) {
392
403
  if (type !== `attributes` || !attributeName)
@@ -408,20 +419,33 @@ export const tooltip = (options = {}) => (node) => {
408
419
  content = new_content;
409
420
  // Only update tooltip if this element owns it
410
421
  if (current_tooltip?._owner === element) {
411
- current_tooltip.innerHTML = content.replace(/\r/g, `<br/>`);
422
+ if (options.allow_html !== false) {
423
+ current_tooltip.innerHTML = content.replace(/\r/g, `<br/>`);
424
+ }
425
+ else {
426
+ current_tooltip.textContent = content;
427
+ }
412
428
  }
413
429
  }
414
430
  });
415
431
  observer.observe(element, { attributes: true, attributeFilter: tooltip_attrs });
416
432
  function show_tooltip() {
433
+ // Skip tooltip on touch input when 'touch-devices' option is set
434
+ if (options.disabled === `touch-devices` && last_pointer_type === `touch`)
435
+ return;
417
436
  clear_tooltip();
418
437
  show_timeout = setTimeout(() => {
419
- const tooltip = document.createElement(`div`);
420
- tooltip.className = `custom-tooltip`;
421
- const placement = safe_options.placement || `bottom`;
422
- tooltip.setAttribute(`data-placement`, placement);
438
+ const tooltip_el = document.createElement(`div`);
439
+ tooltip_el.className = `custom-tooltip`;
440
+ const placement = options.placement || `bottom`;
441
+ tooltip_el.setAttribute(`data-placement`, placement);
442
+ // Accessibility: link tooltip to trigger element
443
+ const tooltip_id = `tooltip-${crypto.randomUUID()}`;
444
+ tooltip_el.id = tooltip_id;
445
+ tooltip_el.setAttribute(`role`, `tooltip`);
446
+ element.setAttribute(`aria-describedby`, tooltip_id);
423
447
  // Apply base styles
424
- tooltip.style.cssText = `
448
+ tooltip_el.style.cssText = `
425
449
  position: absolute; z-index: 9999; opacity: 0;
426
450
  background: var(--tooltip-bg, #333); color: var(--text-color, white); border: var(--tooltip-border, none);
427
451
  padding: var(--tooltip-padding, 6px 10px); border-radius: var(--tooltip-radius, 6px); font-size: var(--tooltip-font-size, 13px); line-height: 1.4;
@@ -429,16 +453,22 @@ export const tooltip = (options = {}) => (node) => {
429
453
  filter: var(--tooltip-shadow, drop-shadow(0 2px 8px rgba(0,0,0,0.25))); transition: opacity 0.15s ease-out;
430
454
  `;
431
455
  // Apply custom styles if provided (these will override base styles due to CSS specificity)
432
- if (safe_options.style) {
456
+ if (options.style) {
433
457
  // Parse and apply custom styles as individual properties for better control
434
- const custom_styles = safe_options.style.split(`;`).filter((style) => style.trim());
458
+ const custom_styles = options.style.split(`;`).filter((style) => style.trim());
435
459
  custom_styles.forEach((style) => {
436
460
  const [property, value] = style.split(`:`).map((s) => s.trim());
437
461
  if (property && value)
438
- tooltip.style.setProperty(property, value);
462
+ tooltip_el.style.setProperty(property, value);
439
463
  });
440
464
  }
441
- tooltip.innerHTML = content?.replace(/\r/g, `<br/>`) ?? ``;
465
+ // Security: allow_html defaults to true; set to false for plain text rendering
466
+ if (options.allow_html !== false) {
467
+ tooltip_el.innerHTML = content?.replace(/\r/g, `<br/>`) ?? ``;
468
+ }
469
+ else {
470
+ tooltip_el.textContent = content ?? ``;
471
+ }
442
472
  // Mirror CSS custom properties from the trigger node onto the tooltip element
443
473
  const trigger_styles = getComputedStyle(element);
444
474
  [
@@ -455,97 +485,100 @@ export const tooltip = (options = {}) => (node) => {
455
485
  ].forEach((name) => {
456
486
  const value = trigger_styles.getPropertyValue(name).trim();
457
487
  if (value)
458
- tooltip.style.setProperty(name, value);
488
+ tooltip_el.style.setProperty(name, value);
459
489
  });
460
490
  // Append early so we can read computed border styles for arrow border
461
- document.body.appendChild(tooltip);
462
- // Arrow elements: optional border triangle behind fill triangle
463
- const tooltip_styles = getComputedStyle(tooltip);
464
- const arrow = document.createElement(`div`);
465
- arrow.className = `custom-tooltip-arrow`;
466
- arrow.style.cssText =
467
- `position: absolute; width: 0; height: 0; pointer-events: none;`;
468
- const arrow_size_raw = trigger_styles.getPropertyValue(`--tooltip-arrow-size`)
469
- .trim();
470
- const arrow_size_num = Number.parseInt(arrow_size_raw || ``, 10);
471
- const arrow_px = Number.isFinite(arrow_size_num) ? arrow_size_num : 6;
472
- const border_color = tooltip_styles.borderTopColor;
473
- const border_width_num = Number.parseFloat(tooltip_styles.borderTopWidth || `0`);
474
- const has_border = !!border_color && border_color !== `rgba(0, 0, 0, 0)` &&
475
- border_width_num > 0;
476
- const maybe_append_border_arrow = () => {
477
- if (!has_border)
478
- return;
479
- const border_arrow = document.createElement(`div`);
480
- border_arrow.className = `custom-tooltip-arrow-border`;
481
- border_arrow.style.cssText =
491
+ document.body.appendChild(tooltip_el);
492
+ // Create arrow elements only if show_arrow is not false
493
+ if (options.show_arrow !== false) {
494
+ const tooltip_styles = getComputedStyle(tooltip_el);
495
+ const arrow = document.createElement(`div`);
496
+ arrow.className = `custom-tooltip-arrow`;
497
+ arrow.style.cssText =
482
498
  `position: absolute; width: 0; height: 0; pointer-events: none;`;
483
- const border_size = arrow_px + (border_width_num * 1.4);
499
+ const arrow_size_raw = trigger_styles.getPropertyValue(`--tooltip-arrow-size`)
500
+ .trim();
501
+ const arrow_size_num = Number.parseInt(arrow_size_raw || ``, 10);
502
+ const arrow_px = Number.isFinite(arrow_size_num) ? arrow_size_num : 6;
503
+ const border_color = tooltip_styles.borderTopColor;
504
+ const border_width_num = Number.parseFloat(tooltip_styles.borderTopWidth || `0`);
505
+ const has_border = !!border_color && border_color !== `rgba(0, 0, 0, 0)` &&
506
+ border_width_num > 0;
507
+ // Helper to create border arrow behind fill arrow
508
+ const append_border_arrow = () => {
509
+ if (!has_border)
510
+ return;
511
+ const border_arrow = document.createElement(`div`);
512
+ border_arrow.className = `custom-tooltip-arrow-border`;
513
+ border_arrow.style.cssText =
514
+ `position: absolute; width: 0; height: 0; pointer-events: none;`;
515
+ const border_size = arrow_px + (border_width_num * 1.4);
516
+ if (placement === `top`) {
517
+ border_arrow.style.left = `calc(50% - ${border_size}px)`;
518
+ border_arrow.style.bottom = `-${border_size}px`;
519
+ border_arrow.style.borderLeft = `${border_size}px solid transparent`;
520
+ border_arrow.style.borderRight = `${border_size}px solid transparent`;
521
+ border_arrow.style.borderTop = `${border_size}px solid ${border_color}`;
522
+ }
523
+ else if (placement === `left`) {
524
+ border_arrow.style.top = `calc(50% - ${border_size}px)`;
525
+ border_arrow.style.right = `-${border_size}px`;
526
+ border_arrow.style.borderTop = `${border_size}px solid transparent`;
527
+ border_arrow.style.borderBottom = `${border_size}px solid transparent`;
528
+ border_arrow.style.borderLeft = `${border_size}px solid ${border_color}`;
529
+ }
530
+ else if (placement === `right`) {
531
+ border_arrow.style.top = `calc(50% - ${border_size}px)`;
532
+ border_arrow.style.left = `-${border_size}px`;
533
+ border_arrow.style.borderTop = `${border_size}px solid transparent`;
534
+ border_arrow.style.borderBottom = `${border_size}px solid transparent`;
535
+ border_arrow.style.borderRight = `${border_size}px solid ${border_color}`;
536
+ }
537
+ else { // bottom
538
+ border_arrow.style.left = `calc(50% - ${border_size}px)`;
539
+ border_arrow.style.top = `-${border_size}px`;
540
+ border_arrow.style.borderLeft = `${border_size}px solid transparent`;
541
+ border_arrow.style.borderRight = `${border_size}px solid transparent`;
542
+ border_arrow.style.borderBottom = `${border_size}px solid ${border_color}`;
543
+ }
544
+ tooltip_el.appendChild(border_arrow);
545
+ };
546
+ // Style and position fill arrow
484
547
  if (placement === `top`) {
485
- border_arrow.style.left = `calc(50% - ${border_size}px)`;
486
- border_arrow.style.bottom = `-${border_size}px`;
487
- border_arrow.style.borderLeft = `${border_size}px solid transparent`;
488
- border_arrow.style.borderRight = `${border_size}px solid transparent`;
489
- border_arrow.style.borderTop = `${border_size}px solid ${border_color}`;
548
+ arrow.style.left = `calc(50% - ${arrow_px}px)`;
549
+ arrow.style.bottom = `-${arrow_px}px`;
550
+ arrow.style.borderLeft = `${arrow_px}px solid transparent`;
551
+ arrow.style.borderRight = `${arrow_px}px solid transparent`;
552
+ arrow.style.borderTop = `${arrow_px}px solid var(--tooltip-bg, #333)`;
490
553
  }
491
554
  else if (placement === `left`) {
492
- border_arrow.style.top = `calc(50% - ${border_size}px)`;
493
- border_arrow.style.right = `-${border_size}px`;
494
- border_arrow.style.borderTop = `${border_size}px solid transparent`;
495
- border_arrow.style.borderBottom = `${border_size}px solid transparent`;
496
- border_arrow.style.borderLeft = `${border_size}px solid ${border_color}`;
555
+ arrow.style.top = `calc(50% - ${arrow_px}px)`;
556
+ arrow.style.right = `-${arrow_px}px`;
557
+ arrow.style.borderTop = `${arrow_px}px solid transparent`;
558
+ arrow.style.borderBottom = `${arrow_px}px solid transparent`;
559
+ arrow.style.borderLeft = `${arrow_px}px solid var(--tooltip-bg, #333)`;
497
560
  }
498
561
  else if (placement === `right`) {
499
- border_arrow.style.top = `calc(50% - ${border_size}px)`;
500
- border_arrow.style.left = `-${border_size}px`;
501
- border_arrow.style.borderTop = `${border_size}px solid transparent`;
502
- border_arrow.style.borderBottom = `${border_size}px solid transparent`;
503
- border_arrow.style.borderRight = `${border_size}px solid ${border_color}`;
562
+ arrow.style.top = `calc(50% - ${arrow_px}px)`;
563
+ arrow.style.left = `-${arrow_px}px`;
564
+ arrow.style.borderTop = `${arrow_px}px solid transparent`;
565
+ arrow.style.borderBottom = `${arrow_px}px solid transparent`;
566
+ arrow.style.borderRight = `${arrow_px}px solid var(--tooltip-bg, #333)`;
504
567
  }
505
568
  else { // bottom
506
- border_arrow.style.left = `calc(50% - ${border_size}px)`;
507
- border_arrow.style.top = `-${border_size}px`;
508
- border_arrow.style.borderLeft = `${border_size}px solid transparent`;
509
- border_arrow.style.borderRight = `${border_size}px solid transparent`;
510
- border_arrow.style.borderBottom = `${border_size}px solid ${border_color}`;
569
+ arrow.style.left = `calc(50% - ${arrow_px}px)`;
570
+ arrow.style.top = `-${arrow_px}px`;
571
+ arrow.style.borderLeft = `${arrow_px}px solid transparent`;
572
+ arrow.style.borderRight = `${arrow_px}px solid transparent`;
573
+ arrow.style.borderBottom = `${arrow_px}px solid var(--tooltip-bg, #333)`;
511
574
  }
512
- tooltip.appendChild(border_arrow);
513
- };
514
- // Create the fill arrow on top
515
- if (placement === `top`) {
516
- arrow.style.left = `calc(50% - ${arrow_px}px)`;
517
- arrow.style.bottom = `-${arrow_px}px`;
518
- arrow.style.borderLeft = `${arrow_px}px solid transparent`;
519
- arrow.style.borderRight = `${arrow_px}px solid transparent`;
520
- arrow.style.borderTop = `${arrow_px}px solid var(--tooltip-bg, #333)`;
575
+ append_border_arrow();
576
+ tooltip_el.appendChild(arrow);
521
577
  }
522
- else if (placement === `left`) {
523
- arrow.style.top = `calc(50% - ${arrow_px}px)`;
524
- arrow.style.right = `-${arrow_px}px`;
525
- arrow.style.borderTop = `${arrow_px}px solid transparent`;
526
- arrow.style.borderBottom = `${arrow_px}px solid transparent`;
527
- arrow.style.borderLeft = `${arrow_px}px solid var(--tooltip-bg, #333)`;
528
- }
529
- else if (placement === `right`) {
530
- arrow.style.top = `calc(50% - ${arrow_px}px)`;
531
- arrow.style.left = `-${arrow_px}px`;
532
- arrow.style.borderTop = `${arrow_px}px solid transparent`;
533
- arrow.style.borderBottom = `${arrow_px}px solid transparent`;
534
- arrow.style.borderRight = `${arrow_px}px solid var(--tooltip-bg, #333)`;
535
- }
536
- else { // bottom
537
- arrow.style.left = `calc(50% - ${arrow_px}px)`;
538
- arrow.style.top = `-${arrow_px}px`;
539
- arrow.style.borderLeft = `${arrow_px}px solid transparent`;
540
- arrow.style.borderRight = `${arrow_px}px solid transparent`;
541
- arrow.style.borderBottom = `${arrow_px}px solid var(--tooltip-bg, #333)`;
542
- }
543
- maybe_append_border_arrow();
544
- tooltip.appendChild(arrow);
545
578
  // Position tooltip
546
579
  const rect = element.getBoundingClientRect();
547
- const tooltip_rect = tooltip.getBoundingClientRect();
548
- const margin = 12;
580
+ const tooltip_rect = tooltip_el.getBoundingClientRect();
581
+ const margin = options.offset ?? 12;
549
582
  let top = 0, left = 0;
550
583
  if (placement === `top`) {
551
584
  top = rect.top - tooltip_rect.height - margin;
@@ -566,22 +599,26 @@ export const tooltip = (options = {}) => (node) => {
566
599
  // Keep in viewport
567
600
  left = Math.max(8, Math.min(left, globalThis.innerWidth - tooltip_rect.width - 8));
568
601
  top = Math.max(8, Math.min(top, globalThis.innerHeight - tooltip_rect.height - 8));
569
- tooltip.style.left = `${left + globalThis.scrollX}px`;
570
- tooltip.style.top = `${top + globalThis.scrollY}px`;
602
+ tooltip_el.style.left = `${left + globalThis.scrollX}px`;
603
+ tooltip_el.style.top = `${top + globalThis.scrollY}px`;
571
604
  const custom_opacity = trigger_styles.getPropertyValue(`--tooltip-opacity`).trim();
572
- tooltip.style.opacity = custom_opacity || `1`;
573
- current_tooltip = Object.assign(tooltip, { _owner: element });
574
- }, safe_options.delay || 100);
605
+ tooltip_el.style.opacity = custom_opacity || `1`;
606
+ current_tooltip = Object.assign(tooltip_el, { _owner: element });
607
+ }, options.delay || 100);
608
+ }
609
+ function handle_keydown(event) {
610
+ if (event.key === `Escape` && current_tooltip?._owner === element)
611
+ clear_tooltip();
575
612
  }
576
613
  function hide_tooltip() {
577
- clear_tooltip();
578
- if (current_tooltip) {
579
- current_tooltip.style.opacity = `0`;
580
- if (current_tooltip) {
581
- current_tooltip.remove();
582
- current_tooltip = null;
583
- }
614
+ if (show_timeout)
615
+ clearTimeout(show_timeout);
616
+ const delay = options.hide_delay ?? 0;
617
+ if (delay > 0) {
618
+ hide_timeout = setTimeout(() => clear_tooltip(), delay);
584
619
  }
620
+ else
621
+ clear_tooltip();
585
622
  }
586
623
  function handle_scroll(event) {
587
624
  // Hide if document or any ancestor scrolls (would move element). Skip internal element scrolls.
@@ -595,10 +632,26 @@ export const tooltip = (options = {}) => (node) => {
595
632
  events.forEach((event, idx) => element.addEventListener(event, handlers[idx]));
596
633
  // Hide tooltip when user scrolls the page (not element-level scrolls like input fields)
597
634
  globalThis.addEventListener(`scroll`, handle_scroll, true);
635
+ // Add Escape key listener to dismiss tooltip
636
+ document.addEventListener(`keydown`, handle_keydown);
637
+ // Watch for element removal from DOM to prevent orphaned tooltips
638
+ const removal_observer = new MutationObserver((mutations) => {
639
+ const was_removed = mutations.some((mut) => Array.from(mut.removedNodes).some((node) => node === element || (node instanceof Element && node.contains(element))));
640
+ if (was_removed && current_tooltip?._owner === element)
641
+ clear_tooltip();
642
+ });
643
+ if (element.parentElement) {
644
+ removal_observer.observe(element.parentElement, { childList: true, subtree: true });
645
+ }
598
646
  return () => {
599
647
  observer.disconnect();
648
+ removal_observer.disconnect();
600
649
  events.forEach((event, idx) => element.removeEventListener(event, handlers[idx]));
601
650
  globalThis.removeEventListener(`scroll`, handle_scroll, true);
651
+ document.removeEventListener(`keydown`, handle_keydown);
652
+ // Clear tooltip if this element owns it (also removes aria-describedby)
653
+ if (current_tooltip?._owner === element)
654
+ clear_tooltip();
602
655
  const original_title = element.getAttribute(`data-original-title`);
603
656
  if (original_title) {
604
657
  element.setAttribute(`title`, original_title);
@@ -0,0 +1,14 @@
1
+ /** @type {() => import('svelte/compiler').PreprocessorGroup} */
2
+ export declare function heading_ids(): {
3
+ name: string;
4
+ markup({ content }: {
5
+ content: string;
6
+ }): {
7
+ code: string;
8
+ };
9
+ };
10
+ export interface HeadingAnchorsOptions {
11
+ selector?: string;
12
+ icon_svg?: string;
13
+ }
14
+ export declare const heading_anchors: (options?: HeadingAnchorsOptions) => (node: Element) => (() => void) | undefined;
@@ -0,0 +1,120 @@
1
+ // Svelte preprocessor that adds IDs to headings at build time for SSR support
2
+ // This ensures fragment navigation (#heading-id) works on initial page load
3
+ // Match headings in two contexts:
4
+ // 1. Start of line (for .svelte files with formatted HTML)
5
+ // 2. After > (for mdsvex output where HTML is on single line, e.g., "</p> <h2>")
6
+ // Avoid matching inside src={...} attributes by requiring these specific contexts
7
+ // Note: [^>]* for attributes won't match if an attribute value contains > (e.g., data-foo="a>b")
8
+ // This edge case is rare in practice and would require significantly more complex parsing
9
+ const heading_regex_line_start = /^(\s*)<(h[2-6])([^>]*)>([\s\S]*?)<\/\2>/gim;
10
+ const heading_regex_after_tag = /(>)(\s*)<(h[2-6])([^>]*)>([\s\S]*?)<\/\3>/gi;
11
+ // Remove Svelte expressions handling nested braces (e.g., {fn({a: 1})})
12
+ // Treats unmatched } as literal text to avoid dropping content
13
+ function strip_svelte_expressions(str) {
14
+ let result = ``;
15
+ let depth = 0;
16
+ for (const char of str) {
17
+ if (char === `{`)
18
+ depth++;
19
+ else if (char === `}` && depth > 0)
20
+ depth--;
21
+ else if (depth === 0)
22
+ result += char;
23
+ }
24
+ return result;
25
+ }
26
+ // Generate URL-friendly slug from text
27
+ const slugify = (text) => text
28
+ .toLowerCase()
29
+ .replace(/\s+/g, `-`)
30
+ .replace(/[^\w-]/g, ``)
31
+ .replace(/-+/g, `-`) // collapse multiple dashes
32
+ .replace(/^-|-$/g, ``); // trim leading/trailing dashes
33
+ /** @type {() => import('svelte/compiler').PreprocessorGroup} */
34
+ export function heading_ids() {
35
+ return {
36
+ name: `heading-ids`,
37
+ markup({ content }) {
38
+ const seen_ids = new Map();
39
+ let result = content;
40
+ const process_heading = (attrs, inner) => {
41
+ // Skip if already has an id (use ^|\s to avoid matching data-id, aria-id, etc.)
42
+ if (/(^|\s)id\s*=/.test(attrs))
43
+ return null;
44
+ const text = strip_svelte_expressions(inner.replace(/<[^>]+>/g, ``)).trim();
45
+ if (!text)
46
+ return null;
47
+ const base_id = slugify(text);
48
+ if (!base_id)
49
+ return null;
50
+ // Handle duplicates within same file
51
+ const count = seen_ids.get(base_id) ?? 0;
52
+ seen_ids.set(base_id, count + 1);
53
+ return count ? `${base_id}-${count}` : base_id;
54
+ };
55
+ // Pass 1: Match headings at start of line (for .svelte files)
56
+ result = result.replace(heading_regex_line_start, (match, indent, tag, attrs, inner) => {
57
+ const id = process_heading(attrs, inner);
58
+ return id ? `${indent}<${tag} id="${id}"${attrs}>${inner}</${tag}>` : match;
59
+ });
60
+ // Pass 2: Match headings after closing tag (for mdsvex single-line output)
61
+ result = result.replace(heading_regex_after_tag, (match, gt, space, tag, attrs, inner) => {
62
+ const id = process_heading(attrs, inner);
63
+ return id ? `${gt}${space}<${tag} id="${id}"${attrs}>${inner}</${tag}>` : match;
64
+ });
65
+ return { code: result };
66
+ },
67
+ };
68
+ }
69
+ // SVG link icon for heading anchors
70
+ const link_svg = `<svg width="16" height="16" viewBox="0 0 16 16" aria-label="Link to heading" role="img"><path d="M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 0 1 0-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 0 1-2.83 0z" fill="currentColor"/></svg>`;
71
+ // Add anchor link to a single heading element
72
+ function add_anchor_to_heading(heading, icon_svg = link_svg) {
73
+ if (heading.querySelector(`a[aria-hidden="true"]`))
74
+ return;
75
+ if (!heading.id) {
76
+ // Generate ID from text content (fallback for dynamic headings)
77
+ const base_id = slugify((heading.textContent ?? ``).trim());
78
+ if (!base_id)
79
+ return;
80
+ // Ensure unique ID in document
81
+ let counter = 0;
82
+ while (document.getElementById(counter ? `${base_id}-${counter}` : base_id))
83
+ counter++;
84
+ heading.id = counter ? `${base_id}-${counter}` : base_id;
85
+ }
86
+ const anchor = document.createElement(`a`);
87
+ anchor.href = `#${heading.id}`;
88
+ anchor.setAttribute(`aria-hidden`, `true`);
89
+ anchor.innerHTML = icon_svg;
90
+ heading.appendChild(anchor);
91
+ }
92
+ // Svelte 5 attachment that adds anchor links to headings within a container
93
+ // Uses MutationObserver to handle dynamically added headings
94
+ export const heading_anchors = (options = {}) => (node) => {
95
+ if (typeof document === `undefined`)
96
+ return;
97
+ const selector = options.selector ?? `h2, h3, h4, h5, h6`;
98
+ const icon_svg = options.icon_svg ?? link_svg;
99
+ // Process existing headings
100
+ for (const heading of Array.from(node.querySelectorAll(selector))) {
101
+ add_anchor_to_heading(heading, icon_svg);
102
+ }
103
+ // Watch for new headings
104
+ const observer = new MutationObserver((mutations) => {
105
+ for (const { addedNodes } of mutations) {
106
+ for (const added of Array.from(addedNodes)) {
107
+ if (added.nodeType !== Node.ELEMENT_NODE)
108
+ continue;
109
+ const el = added;
110
+ if (el.matches?.(selector))
111
+ add_anchor_to_heading(el, icon_svg);
112
+ for (const hdn of Array.from(el.querySelectorAll(selector))) {
113
+ add_anchor_to_heading(hdn, icon_svg);
114
+ }
115
+ }
116
+ }
117
+ });
118
+ observer.observe(node, { childList: true, subtree: true });
119
+ return () => observer.disconnect();
120
+ };