svelora 3.0.4 → 3.0.6

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 (36) hide show
  1. package/dist/Button/Button.svelte +65 -33
  2. package/dist/Fonts/fonts.js +3 -1
  3. package/dist/Link/Link.context-harness.svelte +8 -0
  4. package/dist/Link/Link.context-harness.svelte.d.ts +7 -0
  5. package/dist/Link/Link.svelte +57 -30
  6. package/dist/Link/index.d.ts +2 -0
  7. package/dist/Link/index.js +1 -0
  8. package/dist/Link/location-context.d.ts +4 -0
  9. package/dist/Link/location-context.js +1 -0
  10. package/dist/SelectMenu/SelectMenu.svelte +46 -14
  11. package/dist/Stepper/Stepper.svelte +12 -9
  12. package/dist/docs/navigation.js +54 -0
  13. package/dist/hooks/index.d.ts +14 -0
  14. package/dist/hooks/index.js +9 -0
  15. package/dist/hooks/useDebouncedState.svelte.d.ts +30 -0
  16. package/dist/hooks/useDebouncedState.svelte.js +45 -0
  17. package/dist/hooks/useEventListener.svelte.d.ts +30 -0
  18. package/dist/hooks/useEventListener.svelte.js +16 -0
  19. package/dist/hooks/useFocusTrap.svelte.d.ts +42 -0
  20. package/dist/hooks/useFocusTrap.svelte.js +87 -0
  21. package/dist/hooks/useIntersectionObserver.svelte.d.ts +30 -0
  22. package/dist/hooks/useIntersectionObserver.svelte.js +46 -0
  23. package/dist/hooks/useLocalStorage.svelte.d.ts +39 -0
  24. package/dist/hooks/useLocalStorage.svelte.js +73 -0
  25. package/dist/hooks/useResizeObserver.svelte.d.ts +50 -0
  26. package/dist/hooks/useResizeObserver.svelte.js +71 -0
  27. package/dist/hooks/useScrollLock.svelte.d.ts +28 -0
  28. package/dist/hooks/useScrollLock.svelte.js +79 -0
  29. package/dist/hooks/useThrottle.svelte.d.ts +37 -0
  30. package/dist/hooks/useThrottle.svelte.js +72 -0
  31. package/dist/hooks/useTimers.svelte.d.ts +62 -0
  32. package/dist/hooks/useTimers.svelte.js +90 -0
  33. package/dist/hooks/utils.d.ts +1 -0
  34. package/dist/hooks/utils.js +3 -0
  35. package/dist/mcp/svelora-docs.data.json +23 -5
  36. package/package.json +3 -3
@@ -26,6 +26,7 @@ const leadingIconName = $derived(spinLeading ? loadingIcon : leadingIcon || (!tr
26
26
  const trailingIconName = $derived(spinTrailing ? loadingIcon : trailingIcon || (trailing ? icon : undefined));
27
27
  const resolvedColor = $derived(active && activeColor ? activeColor : color);
28
28
  const resolvedVariant = $derived(active && activeVariant ? activeVariant : variant);
29
+ const isLink = $derived(!!href);
29
30
  const classes = $derived.by(() => {
30
31
  const slots = buttonVariants({
31
32
  variant: resolvedVariant,
@@ -72,38 +73,69 @@ function handleClick(e) {
72
73
  }
73
74
  </script>
74
75
 
75
- <Link
76
- {...restProps}
77
- bind:ref
78
- {href}
79
- {type}
80
- {external}
81
- {active}
82
- {exact}
83
- {activeClass}
84
- {inactiveClass}
85
- raw
86
- disabled={disabled || isLoading}
87
- class={classes.base}
88
- onclick={handleClick}
89
- >
90
- {#if leadingSlot}
91
- {@render leadingSlot()}
92
- {:else if isLeading && leadingIconName}
93
- <Icon name={leadingIconName} class={classes.leadingIcon} />
94
- {:else if avatar}
95
- <Avatar {...avatar} size={classes.leadingAvatarSize} class={classes.leadingAvatar} />
96
- {/if}
76
+ {#if isLink}
77
+ <Link
78
+ {...restProps}
79
+ bind:ref
80
+ {href}
81
+ {type}
82
+ {external}
83
+ {active}
84
+ {exact}
85
+ {activeClass}
86
+ {inactiveClass}
87
+ raw
88
+ disabled={disabled || isLoading}
89
+ class={classes.base}
90
+ onclick={handleClick}
91
+ >
92
+ {#if leadingSlot}
93
+ {@render leadingSlot()}
94
+ {:else if isLeading && leadingIconName}
95
+ <Icon name={leadingIconName} class={classes.leadingIcon} />
96
+ {:else if avatar}
97
+ <Avatar {...avatar} size={classes.leadingAvatarSize} class={classes.leadingAvatar} />
98
+ {/if}
97
99
 
98
- {#if label}
99
- <span class={classes.label}>{label}</span>
100
- {:else if children}
101
- <span class={classes.label}>{@render children()}</span>
102
- {/if}
100
+ {#if label}
101
+ <span class={classes.label}>{label}</span>
102
+ {:else if children}
103
+ <span class={classes.label}>{@render children()}</span>
104
+ {/if}
103
105
 
104
- {#if trailingSlot}
105
- {@render trailingSlot()}
106
- {:else if isTrailing && trailingIconName}
107
- <Icon name={trailingIconName} class={classes.trailingIcon} />
108
- {/if}
109
- </Link>
106
+ {#if trailingSlot}
107
+ {@render trailingSlot()}
108
+ {:else if isTrailing && trailingIconName}
109
+ <Icon name={trailingIconName} class={classes.trailingIcon} />
110
+ {/if}
111
+ </Link>
112
+ {:else}
113
+ <button
114
+ {...restProps}
115
+ bind:this={ref}
116
+ type={type ?? 'button'}
117
+ disabled={disabled || isLoading}
118
+ class={classes.base}
119
+ onclick={handleClick}
120
+ >
121
+ {#if leadingSlot}
122
+ {@render leadingSlot()}
123
+ {:else if isLeading && leadingIconName}
124
+ <Icon name={leadingIconName} class={classes.leadingIcon} />
125
+ {:else if avatar}
126
+ <Avatar {...avatar} size={classes.leadingAvatarSize} class={classes.leadingAvatar} />
127
+ {/if}
128
+
129
+ {#if label}
130
+ <span class={classes.label}>{label}</span>
131
+ {:else if children}
132
+ <span class={classes.label}>{@render children()}</span>
133
+ {/if}
134
+
135
+ {#if trailingSlot}
136
+ {@render trailingSlot()}
137
+ {:else if isTrailing && trailingIconName}
138
+ <Icon name={trailingIconName} class={classes.trailingIcon} />
139
+ {/if}
140
+ </button>
141
+ {/if}
@@ -70,7 +70,9 @@ export function buildGoogleFontsUrl(fonts, display = 'swap') {
70
70
  .filter((family, index, source) => family.length > 0 && source.indexOf(family) === index);
71
71
  if (families.length === 0)
72
72
  return '';
73
- const familyParams = families.map((f) => `family=${f}`).join('&');
73
+ const familyParams = families
74
+ .map((f) => `family=${encodeURIComponent(f).replaceAll('%2B', '+')}`)
75
+ .join('&');
74
76
  return `https://fonts.googleapis.com/css2?${familyParams}&display=${display}`;
75
77
  }
76
78
  export function buildFontFamily(font) {
@@ -0,0 +1,8 @@
1
+ <script lang="ts">import { setContext } from "svelte";
2
+ import Link from "./Link.svelte";
3
+ import { LINK_LOCATION_CONTEXT_KEY } from "./location-context.js";
4
+ let { url } = $props();
5
+ setContext(LINK_LOCATION_CONTEXT_KEY, { currentUrl: () => url });
6
+ </script>
7
+
8
+ <Link href="/from-context" />
@@ -0,0 +1,7 @@
1
+ import Link from './Link.svelte';
2
+ type $$ComponentProps = {
3
+ url: URL;
4
+ };
5
+ declare const Link: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type Link = ReturnType<typeof Link>;
7
+ export default Link;
@@ -1,5 +1,6 @@
1
- <script lang="ts" module>const navigationEvent = "svelora:navigation";
2
- let isHistoryPatched = false;
1
+ <script lang="ts" module>const locationSubscribers = new Set();
2
+ let stopLocationTracking;
3
+ let lastKnownHref = "";
3
4
  function parseUrl(url, baseUrl) {
4
5
  try {
5
6
  const parsed = new URL(url, baseUrl.origin);
@@ -35,60 +36,86 @@ function isPathnameMatch(linkPath, currentPath, exactMatch) {
35
36
  const current = currentPath.replace(/\/$/, "") || "/";
36
37
  return link === "/" ? current === "/" : current === link || current.startsWith(`${link}/`);
37
38
  }
38
- function dispatchNavigationEvent() {
39
+ function dispatchLocationChange() {
39
40
  if (typeof window === "undefined") {
40
41
  return;
41
42
  }
42
- window.dispatchEvent(new Event(navigationEvent));
43
+ for (const callback of locationSubscribers) {
44
+ callback();
45
+ }
43
46
  }
44
- function patchHistoryMethod(method) {
45
- const historyMethod = window.history[method].bind(window.history);
46
- Object.defineProperty(window.history, method, {
47
- configurable: true,
48
- value: (...args) => {
49
- historyMethod(...args);
50
- dispatchNavigationEvent();
51
- }
52
- });
47
+ function syncLocation(force = false) {
48
+ if (typeof window === "undefined") {
49
+ return;
50
+ }
51
+ const href = window.location.href;
52
+ if (!force && href === lastKnownHref) {
53
+ return;
54
+ }
55
+ lastKnownHref = href;
56
+ dispatchLocationChange();
53
57
  }
54
- function ensureNavigationEvents() {
55
- if (typeof window === "undefined" || isHistoryPatched) {
58
+ function scheduleLocationSync() {
59
+ if (typeof window === "undefined") {
60
+ return;
61
+ }
62
+ queueMicrotask(() => syncLocation());
63
+ window.setTimeout(() => syncLocation(), 0);
64
+ window.requestAnimationFrame(() => syncLocation());
65
+ }
66
+ function ensureLocationTracking() {
67
+ if (typeof window === "undefined" || stopLocationTracking) {
56
68
  return;
57
69
  }
58
- isHistoryPatched = true;
59
- patchHistoryMethod("pushState");
60
- patchHistoryMethod("replaceState");
70
+ lastKnownHref = window.location.href;
71
+ const handleImmediateLocationChange = () => syncLocation();
72
+ const handleDeferredLocationChange = () => scheduleLocationSync();
73
+ const intervalId = window.setInterval(() => syncLocation(), 125);
74
+ window.addEventListener("popstate", handleImmediateLocationChange);
75
+ window.addEventListener("hashchange", handleImmediateLocationChange);
76
+ document.addEventListener("click", handleDeferredLocationChange, true);
77
+ stopLocationTracking = () => {
78
+ window.clearInterval(intervalId);
79
+ window.removeEventListener("popstate", handleImmediateLocationChange);
80
+ window.removeEventListener("hashchange", handleImmediateLocationChange);
81
+ document.removeEventListener("click", handleDeferredLocationChange, true);
82
+ stopLocationTracking = undefined;
83
+ };
61
84
  }
62
85
  function subscribeToLocation(callback) {
63
86
  if (typeof window === "undefined") {
64
87
  return () => undefined;
65
88
  }
66
- ensureNavigationEvents();
67
- const handleLocationChange = () => callback();
68
- handleLocationChange();
69
- window.addEventListener("popstate", handleLocationChange);
70
- window.addEventListener("hashchange", handleLocationChange);
71
- window.addEventListener(navigationEvent, handleLocationChange);
89
+ ensureLocationTracking();
90
+ locationSubscribers.add(callback);
91
+ callback();
72
92
  return () => {
73
- window.removeEventListener("popstate", handleLocationChange);
74
- window.removeEventListener("hashchange", handleLocationChange);
75
- window.removeEventListener(navigationEvent, handleLocationChange);
93
+ locationSubscribers.delete(callback);
94
+ if (locationSubscribers.size === 0) {
95
+ stopLocationTracking?.();
96
+ }
76
97
  };
77
98
  }
78
99
  export {};
79
100
  </script>
80
101
 
81
- <script lang="ts">import { onMount } from "svelte";
102
+ <script lang="ts">import { getContext, onMount } from "svelte";
82
103
  import { twMerge } from "tailwind-merge";
83
104
  import { getComponentConfig } from "../config.js";
105
+ import { LINK_LOCATION_CONTEXT_KEY } from "./location-context.js";
84
106
  import { linkDefaults, linkVariants } from "./link.variants.js";
85
107
  const config = getComponentConfig("link", linkDefaults);
86
108
  let { ref = $bindable(null), href, type, active, exact = false, exactQuery = false, exactHash = false, activeClass, inactiveClass, disabled = false, raw = false, external, children, class: className, ui, target, rel, onclick, ...restProps } = $props();
87
109
  const isLink = $derived(!!href);
88
- let currentUrl = $state(undefined);
110
+ const locationContext = getContext(LINK_LOCATION_CONTEXT_KEY);
111
+ let observedUrl = $state(undefined);
112
+ const currentUrl = $derived.by(() => locationContext?.currentUrl() ?? observedUrl);
89
113
  onMount(() => {
114
+ if (locationContext) {
115
+ return;
116
+ }
90
117
  return subscribeToLocation(() => {
91
- currentUrl = new URL(window.location.href);
118
+ observedUrl = new URL(window.location.href);
92
119
  });
93
120
  });
94
121
  const isExternal = $derived(isLink && (external ?? (href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//"))));
@@ -1,2 +1,4 @@
1
1
  export { default as Link } from './Link.svelte';
2
+ export { LINK_LOCATION_CONTEXT_KEY } from './location-context.js';
3
+ export type { LinkLocationContext } from './location-context.js';
2
4
  export type { LinkProps } from './link.types.js';
@@ -1 +1,2 @@
1
1
  export { default as Link } from './Link.svelte';
2
+ export { LINK_LOCATION_CONTEXT_KEY } from './location-context.js';
@@ -0,0 +1,4 @@
1
+ export declare const LINK_LOCATION_CONTEXT_KEY: unique symbol;
2
+ export interface LinkLocationContext {
3
+ currentUrl: () => URL;
4
+ }
@@ -0,0 +1 @@
1
+ export const LINK_LOCATION_CONTEXT_KEY = Symbol('svelora:link-location');
@@ -42,13 +42,33 @@ const selectedItems = $derived(selectedValues.map((v) => itemsMap.get(v)).filter
42
42
  const hasSelection = $derived(selectedValues.length > 0);
43
43
  const singleSelectedItem = $derived(multiple ? undefined : selectedItems[0]);
44
44
  const displayLabel = $derived(multiple ? selectedItems.map((i) => i.label ?? i.value).join(separator) : singleSelectedItem?.label ?? singleSelectedItem?.value ?? "");
45
+ let isAlive = true;
46
+ $effect(() => {
47
+ return () => {
48
+ isAlive = false;
49
+ };
50
+ });
51
+ function getSelectedValues() {
52
+ if (!multiple) {
53
+ return typeof value === "string" && value !== "" ? [value] : [];
54
+ }
55
+ return Array.isArray(value) ? value : [];
56
+ }
57
+ function getCombinedItems() {
58
+ const itemList = items;
59
+ const propValues = new Set(itemList.filter((i) => !("type" in i)).map((i) => i.value));
60
+ const extras = createdItems.filter((c) => !propValues.has(c.value));
61
+ return [...itemList, ...extras];
62
+ }
45
63
  function removeValue(val) {
46
64
  if (!multiple) return;
47
- value = selectedValues.filter((v) => v !== val);
65
+ if (!isAlive) return;
66
+ value = getSelectedValues().filter((v) => v !== val);
48
67
  emit.onChange();
49
68
  }
50
69
  function clearSelection() {
51
70
  if (!multiple) return;
71
+ if (!isAlive) return;
52
72
  value = [];
53
73
  emit.onChange();
54
74
  }
@@ -97,29 +117,41 @@ const showCreateItem = $derived.by(() => {
97
117
  return !exactMatchExists;
98
118
  });
99
119
  const resolvedCreateLabel = $derived(typeof createItemLabel === "function" ? createItemLabel(trimmedSearch) : createItemLabel);
100
- function findItemByCaseInsensitive(query) {
120
+ function hasExactMatch(query, combined) {
101
121
  const q = query.toLowerCase();
102
- for (const it of itemsMap.values()) {
103
- if (it.value.toLowerCase() === q || (it.label ?? it.value).toLowerCase() === q) {
104
- return it;
105
- }
122
+ for (const it of combined) {
123
+ if ("type" in it) continue;
124
+ if (it.value.toLowerCase() === q || (it.label ?? it.value).toLowerCase() === q) return true;
125
+ }
126
+ return false;
127
+ }
128
+ function findItemByCaseInsensitive(query, combined) {
129
+ const q = query.toLowerCase();
130
+ for (const it of combined) {
131
+ if ("type" in it) continue;
132
+ if (it.value.toLowerCase() === q || (it.label ?? it.value).toLowerCase() === q) return it;
106
133
  }
107
134
  return undefined;
108
135
  }
109
136
  function selectValue(val) {
137
+ if (!isAlive) return;
138
+ const current = getSelectedValues();
110
139
  if (multiple) {
111
- if (!selectedValues.includes(val)) {
112
- value = [...selectedValues, val];
113
- }
114
- } else {
115
- value = val;
140
+ if (!current.includes(val)) value = [...current, val];
141
+ return;
116
142
  }
143
+ value = val;
117
144
  }
118
145
  function handleCreate() {
119
- if (!showCreateItem) return;
120
- const newValue = trimmedSearch;
146
+ if (!isAlive) return;
147
+ if (!createItem) return;
148
+ const newValue = searchTerm.trim();
121
149
  if (!newValue) return;
122
- const existing = findItemByCaseInsensitive(newValue);
150
+ const combined = getCombinedItems();
151
+ const mode = createItem === true ? "lazy" : createItem;
152
+ const shouldShow = mode === "always" ? true : !hasExactMatch(newValue, combined);
153
+ if (!shouldShow) return;
154
+ const existing = findItemByCaseInsensitive(newValue, combined);
123
155
  if (existing) {
124
156
  selectValue(existing.value);
125
157
  } else {
@@ -55,28 +55,31 @@ function handleKeydown(e, index) {
55
55
  }
56
56
  const apiInstance = {
57
57
  next() {
58
- if (activeIndex >= items.length - 1) return;
59
- const target = items[activeIndex + 1];
58
+ const current = untrack(() => activeIndex);
59
+ if (current >= items.length - 1) return;
60
+ const target = items[current + 1];
60
61
  if (!target) return;
61
- setValue(getItemValue(target, activeIndex + 1));
62
+ setValue(getItemValue(target, current + 1));
62
63
  },
63
64
  prev() {
64
- if (activeIndex <= 0) return;
65
- const target = items[activeIndex - 1];
65
+ const current = untrack(() => activeIndex);
66
+ if (current <= 0) return;
67
+ const target = items[current - 1];
66
68
  if (!target) return;
67
- setValue(getItemValue(target, activeIndex - 1));
69
+ setValue(getItemValue(target, current - 1));
68
70
  },
69
71
  goTo(next) {
70
72
  setValue(next);
71
73
  },
72
74
  get hasNext() {
73
- return activeIndex >= 0 && activeIndex < items.length - 1;
75
+ const current = untrack(() => activeIndex);
76
+ return current >= 0 && current < items.length - 1;
74
77
  },
75
78
  get hasPrev() {
76
- return activeIndex > 0;
79
+ return untrack(() => activeIndex) > 0;
77
80
  },
78
81
  get activeIndex() {
79
- return activeIndex;
82
+ return untrack(() => activeIndex);
80
83
  }
81
84
  };
82
85
  api = apiInstance;
@@ -478,6 +478,60 @@ export const docsHookItems = [
478
478
  href: '/docs/hooks/use-debounce',
479
479
  legacyHref: '/use-debounce',
480
480
  icon: 'lucide:timer'
481
+ },
482
+ {
483
+ title: 'useDebouncedState',
484
+ href: '/docs/hooks/use-debounced-state',
485
+ legacyHref: '/use-debounced-state',
486
+ icon: 'lucide:clock-3'
487
+ },
488
+ {
489
+ title: 'useEventListener',
490
+ href: '/docs/hooks/use-event-listener',
491
+ legacyHref: '/use-event-listener',
492
+ icon: 'lucide:radio'
493
+ },
494
+ {
495
+ title: 'useResizeObserver / useElementSize',
496
+ href: '/docs/hooks/use-resize-observer',
497
+ legacyHref: '/use-resize-observer',
498
+ icon: 'lucide:scaling'
499
+ },
500
+ {
501
+ title: 'useIntersectionObserver',
502
+ href: '/docs/hooks/use-intersection-observer',
503
+ legacyHref: '/use-intersection-observer',
504
+ icon: 'lucide:scan-search'
505
+ },
506
+ {
507
+ title: 'useScrollLock',
508
+ href: '/docs/hooks/use-scroll-lock',
509
+ legacyHref: '/use-scroll-lock',
510
+ icon: 'lucide:lock'
511
+ },
512
+ {
513
+ title: 'useFocusTrap',
514
+ href: '/docs/hooks/use-focus-trap',
515
+ legacyHref: '/use-focus-trap',
516
+ icon: 'lucide:focus'
517
+ },
518
+ {
519
+ title: 'useLocalStorage',
520
+ href: '/docs/hooks/use-local-storage',
521
+ legacyHref: '/use-local-storage',
522
+ icon: 'lucide:hard-drive'
523
+ },
524
+ {
525
+ title: 'useThrottle',
526
+ href: '/docs/hooks/use-throttle',
527
+ legacyHref: '/use-throttle',
528
+ icon: 'lucide:gauge'
529
+ },
530
+ {
531
+ title: 'useTimeout / useInterval',
532
+ href: '/docs/hooks/use-timers',
533
+ legacyHref: '/use-timers',
534
+ icon: 'lucide:timer-reset'
481
535
  }
482
536
  ];
483
537
  export const docsTopNav = [
@@ -4,11 +4,25 @@ export type { UseClipboardOptions } from './useClipboard.svelte.js';
4
4
  export { useClipboard } from './useClipboard.svelte.js';
5
5
  export type { UseDebounceOptions } from './useDebounce.svelte.js';
6
6
  export { useDebounce } from './useDebounce.svelte.js';
7
+ export { useDebouncedState } from './useDebouncedState.svelte.js';
7
8
  export type { UseEscapeKeydownOptions } from './useEscapeKeydown.svelte.js';
8
9
  export { useEscapeKeydown } from './useEscapeKeydown.svelte.js';
10
+ export { useEventListener } from './useEventListener.svelte.js';
11
+ export type { UseFocusTrapOptions } from './useFocusTrap.svelte.js';
12
+ export { useFocusTrap } from './useFocusTrap.svelte.js';
9
13
  export type { FormFieldContext } from './useFormField.svelte.js';
10
14
  export { FORM_FIELD_CONTEXT_KEY, useFormField, useFormFieldEmit } from './useFormField.svelte.js';
11
15
  export type { UseInfiniteScrollOptions } from './useInfiniteScroll.svelte.js';
12
16
  export { useInfiniteScroll } from './useInfiniteScroll.svelte.js';
17
+ export type { UseIntersectionObserverOptions } from './useIntersectionObserver.svelte.js';
18
+ export { useIntersectionObserver } from './useIntersectionObserver.svelte.js';
19
+ export type { UseLocalStorageOptions, UseLocalStorageReturn, UseLocalStorageSerializer } from './useLocalStorage.svelte.js';
20
+ export { useLocalStorage } from './useLocalStorage.svelte.js';
13
21
  export type { UseMediaQueryOptions } from './useMediaQuery.svelte.js';
14
22
  export { useMediaQuery } from './useMediaQuery.svelte.js';
23
+ export { useElementSize, useResizeObserver } from './useResizeObserver.svelte.js';
24
+ export { useScrollLock } from './useScrollLock.svelte.js';
25
+ export type { UseThrottleOptions } from './useThrottle.svelte.js';
26
+ export { useThrottle } from './useThrottle.svelte.js';
27
+ export type { UseIntervalOptions } from './useTimers.svelte.js';
28
+ export { useInterval, useTimeout } from './useTimers.svelte.js';
@@ -1,7 +1,16 @@
1
1
  export { useClickOutside } from './useClickOutside.svelte.js';
2
2
  export { useClipboard } from './useClipboard.svelte.js';
3
3
  export { useDebounce } from './useDebounce.svelte.js';
4
+ export { useDebouncedState } from './useDebouncedState.svelte.js';
4
5
  export { useEscapeKeydown } from './useEscapeKeydown.svelte.js';
6
+ export { useEventListener } from './useEventListener.svelte.js';
7
+ export { useFocusTrap } from './useFocusTrap.svelte.js';
5
8
  export { FORM_FIELD_CONTEXT_KEY, useFormField, useFormFieldEmit } from './useFormField.svelte.js';
6
9
  export { useInfiniteScroll } from './useInfiniteScroll.svelte.js';
10
+ export { useIntersectionObserver } from './useIntersectionObserver.svelte.js';
11
+ export { useLocalStorage } from './useLocalStorage.svelte.js';
7
12
  export { useMediaQuery } from './useMediaQuery.svelte.js';
13
+ export { useElementSize, useResizeObserver } from './useResizeObserver.svelte.js';
14
+ export { useScrollLock } from './useScrollLock.svelte.js';
15
+ export { useThrottle } from './useThrottle.svelte.js';
16
+ export { useInterval, useTimeout } from './useTimers.svelte.js';
@@ -0,0 +1,30 @@
1
+ interface UseDebouncedStateReturn<T> {
2
+ /** The immediate value — read it or write it (bindable). */
3
+ current: T;
4
+ /** The debounced value — settles `delay` ms after the last write. */
5
+ readonly debounced: T;
6
+ /** Set both `current` and `debounced` now, cancelling any pending update. */
7
+ setImmediate(value: T): void;
8
+ }
9
+ /**
10
+ * Reactive state whose `debounced` mirror lags behind `current` by `delay` ms.
11
+ *
12
+ * Combines a value with debouncing: write `current` (e.g. bound to an input)
13
+ * and derive from `debounced` — no manual two-state + run wiring. Built on
14
+ * `useDebounce`, so the pending timer is cleared on teardown. Use `setImmediate`
15
+ * to skip the delay (e.g. on reset).
16
+ *
17
+ * @example
18
+ * ```svelte
19
+ * <script>
20
+ * import { useDebouncedState } from 'svelora'
21
+ *
22
+ * const search = useDebouncedState('', 200)
23
+ * const results = $derived(filter(items, search.debounced))
24
+ * </script>
25
+ *
26
+ * <input bind:value={search.current} />
27
+ * ```
28
+ */
29
+ export declare function useDebouncedState<T>(initial: T, delay?: number): UseDebouncedStateReturn<T>;
30
+ export {};
@@ -0,0 +1,45 @@
1
+ import { useDebounce } from './useDebounce.svelte.js';
2
+ /**
3
+ * Reactive state whose `debounced` mirror lags behind `current` by `delay` ms.
4
+ *
5
+ * Combines a value with debouncing: write `current` (e.g. bound to an input)
6
+ * and derive from `debounced` — no manual two-state + run wiring. Built on
7
+ * `useDebounce`, so the pending timer is cleared on teardown. Use `setImmediate`
8
+ * to skip the delay (e.g. on reset).
9
+ *
10
+ * @example
11
+ * ```svelte
12
+ * <script>
13
+ * import { useDebouncedState } from 'svelora'
14
+ *
15
+ * const search = useDebouncedState('', 200)
16
+ * const results = $derived(filter(items, search.debounced))
17
+ * </script>
18
+ *
19
+ * <input bind:value={search.current} />
20
+ * ```
21
+ */
22
+ export function useDebouncedState(initial, delay = 300) {
23
+ let current = $state(initial);
24
+ let debounced = $state(initial);
25
+ const debounce = useDebounce({ delay });
26
+ return {
27
+ get current() {
28
+ return current;
29
+ },
30
+ set current(value) {
31
+ current = value;
32
+ debounce.run(() => {
33
+ debounced = value;
34
+ });
35
+ },
36
+ get debounced() {
37
+ return debounced;
38
+ },
39
+ setImmediate(value) {
40
+ debounce.cancel();
41
+ current = value;
42
+ debounced = value;
43
+ }
44
+ };
45
+ }
@@ -0,0 +1,30 @@
1
+ type MaybeGetter<T> = T | (() => T);
2
+ /**
3
+ * Attach event listener(s) to a target with automatic cleanup.
4
+ *
5
+ * Binds inside an `$effect` and removes the listener(s) on teardown. The target
6
+ * may be a value or a getter; when it is a getter that reads reactive state, the
7
+ * listener re-binds as the target changes. SSR-safe: a nullish target is a no-op.
8
+ *
9
+ * @example
10
+ * ```svelte
11
+ * <script>
12
+ * import { useEventListener } from 'svelora'
13
+ *
14
+ * let el = $state<HTMLElement>()
15
+ *
16
+ * // Window target (static)
17
+ * useEventListener(window, 'resize', () => console.log('resized'))
18
+ *
19
+ * // Element target (reactive getter) + multiple event types
20
+ * useEventListener(() => el, ['pointerenter', 'focus'], () => console.log('active'))
21
+ * </script>
22
+ *
23
+ * <div bind:this={el}>hover or focus me</div>
24
+ * ```
25
+ */
26
+ export declare function useEventListener<K extends keyof WindowEventMap>(target: MaybeGetter<Window | null | undefined>, type: K | K[], handler: (this: Window, event: WindowEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void;
27
+ export declare function useEventListener<K extends keyof DocumentEventMap>(target: MaybeGetter<Document | null | undefined>, type: K | K[], handler: (this: Document, event: DocumentEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void;
28
+ export declare function useEventListener<K extends keyof HTMLElementEventMap>(target: MaybeGetter<HTMLElement | null | undefined>, type: K | K[], handler: (this: HTMLElement, event: HTMLElementEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void;
29
+ export declare function useEventListener(target: MaybeGetter<EventTarget | null | undefined>, type: string | string[], handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
30
+ export {};
@@ -0,0 +1,16 @@
1
+ import { toGetter } from './utils.js';
2
+ export function useEventListener(target, type, handler, options) {
3
+ const resolveTarget = toGetter(target);
4
+ const types = Array.isArray(type) ? type : [type];
5
+ $effect(() => {
6
+ const el = resolveTarget();
7
+ if (!el)
8
+ return;
9
+ for (const t of types)
10
+ el.addEventListener(t, handler, options);
11
+ return () => {
12
+ for (const t of types)
13
+ el.removeEventListener(t, handler, options);
14
+ };
15
+ });
16
+ }