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.
- package/dist/Button/Button.svelte +65 -33
- package/dist/Fonts/fonts.js +3 -1
- package/dist/Link/Link.context-harness.svelte +8 -0
- package/dist/Link/Link.context-harness.svelte.d.ts +7 -0
- package/dist/Link/Link.svelte +57 -30
- package/dist/Link/index.d.ts +2 -0
- package/dist/Link/index.js +1 -0
- package/dist/Link/location-context.d.ts +4 -0
- package/dist/Link/location-context.js +1 -0
- package/dist/SelectMenu/SelectMenu.svelte +46 -14
- package/dist/Stepper/Stepper.svelte +12 -9
- package/dist/docs/navigation.js +54 -0
- package/dist/hooks/index.d.ts +14 -0
- package/dist/hooks/index.js +9 -0
- package/dist/hooks/useDebouncedState.svelte.d.ts +30 -0
- package/dist/hooks/useDebouncedState.svelte.js +45 -0
- package/dist/hooks/useEventListener.svelte.d.ts +30 -0
- package/dist/hooks/useEventListener.svelte.js +16 -0
- package/dist/hooks/useFocusTrap.svelte.d.ts +42 -0
- package/dist/hooks/useFocusTrap.svelte.js +87 -0
- package/dist/hooks/useIntersectionObserver.svelte.d.ts +30 -0
- package/dist/hooks/useIntersectionObserver.svelte.js +46 -0
- package/dist/hooks/useLocalStorage.svelte.d.ts +39 -0
- package/dist/hooks/useLocalStorage.svelte.js +73 -0
- package/dist/hooks/useResizeObserver.svelte.d.ts +50 -0
- package/dist/hooks/useResizeObserver.svelte.js +71 -0
- package/dist/hooks/useScrollLock.svelte.d.ts +28 -0
- package/dist/hooks/useScrollLock.svelte.js +79 -0
- package/dist/hooks/useThrottle.svelte.d.ts +37 -0
- package/dist/hooks/useThrottle.svelte.js +72 -0
- package/dist/hooks/useTimers.svelte.d.ts +62 -0
- package/dist/hooks/useTimers.svelte.js +90 -0
- package/dist/hooks/utils.d.ts +1 -0
- package/dist/hooks/utils.js +3 -0
- package/dist/mcp/svelora-docs.data.json +23 -5
- 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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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}
|
package/dist/Fonts/fonts.js
CHANGED
|
@@ -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
|
|
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" />
|
package/dist/Link/Link.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
<script lang="ts" module>const
|
|
2
|
-
let
|
|
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
|
|
39
|
+
function dispatchLocationChange() {
|
|
39
40
|
if (typeof window === "undefined") {
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
+
for (const callback of locationSubscribers) {
|
|
44
|
+
callback();
|
|
45
|
+
}
|
|
43
46
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
55
|
-
if (typeof window === "undefined"
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("//"))));
|
package/dist/Link/index.d.ts
CHANGED
package/dist/Link/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
120
|
+
function hasExactMatch(query, combined) {
|
|
101
121
|
const q = query.toLowerCase();
|
|
102
|
-
for (const it of
|
|
103
|
-
if (
|
|
104
|
-
|
|
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 (!
|
|
112
|
-
|
|
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 (!
|
|
120
|
-
|
|
146
|
+
if (!isAlive) return;
|
|
147
|
+
if (!createItem) return;
|
|
148
|
+
const newValue = searchTerm.trim();
|
|
121
149
|
if (!newValue) return;
|
|
122
|
-
const
|
|
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
|
-
|
|
59
|
-
|
|
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,
|
|
62
|
+
setValue(getItemValue(target, current + 1));
|
|
62
63
|
},
|
|
63
64
|
prev() {
|
|
64
|
-
|
|
65
|
-
|
|
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,
|
|
69
|
+
setValue(getItemValue(target, current - 1));
|
|
68
70
|
},
|
|
69
71
|
goTo(next) {
|
|
70
72
|
setValue(next);
|
|
71
73
|
},
|
|
72
74
|
get hasNext() {
|
|
73
|
-
|
|
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;
|
package/dist/docs/navigation.js
CHANGED
|
@@ -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 = [
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/hooks/index.js
CHANGED
|
@@ -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
|
+
}
|