svelora 3.0.5 → 3.0.7
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/BentoGrid/BentoCard.svelte +45 -0
- package/dist/BentoGrid/BentoCard.svelte.d.ts +4 -0
- package/dist/BentoGrid/BentoGrid.svelte +9 -0
- package/dist/BentoGrid/BentoGrid.svelte.d.ts +4 -0
- package/dist/BentoGrid/bento-grid.types.d.ts +47 -0
- package/dist/BentoGrid/bento-grid.types.js +1 -0
- package/dist/BentoGrid/bento-grid.variants.d.ts +30 -0
- package/dist/BentoGrid/bento-grid.variants.js +16 -0
- package/dist/BentoGrid/index.d.ts +5 -0
- package/dist/BentoGrid/index.js +5 -0
- package/dist/Chart/Chart.svelte +47 -0
- package/dist/Chart/Chart.svelte.d.ts +4 -0
- package/dist/Chart/chart.types.d.ts +20 -0
- package/dist/Chart/chart.types.js +1 -0
- package/dist/Chart/chart.variants.d.ts +3 -0
- package/dist/Chart/chart.variants.js +4 -0
- package/dist/Chart/index.d.ts +4 -0
- package/dist/Chart/index.js +4 -0
- package/dist/Chat/ChatBubble.svelte +30 -0
- package/dist/Chat/ChatBubble.svelte.d.ts +4 -0
- package/dist/Chat/ChatInput.svelte +50 -0
- package/dist/Chat/ChatInput.svelte.d.ts +4 -0
- package/dist/Chat/ChatMessage.svelte +15 -0
- package/dist/Chat/ChatMessage.svelte.d.ts +4 -0
- package/dist/Chat/chat.types.d.ts +63 -0
- package/dist/Chat/chat.types.js +1 -0
- package/dist/Chat/chat.variants.d.ts +117 -0
- package/dist/Chat/chat.variants.js +47 -0
- package/dist/Chat/index.d.ts +6 -0
- package/dist/Chat/index.js +6 -0
- package/dist/ColorPicker/ColorPicker.svelte +109 -0
- package/dist/ColorPicker/ColorPicker.svelte.d.ts +4 -0
- package/dist/ColorPicker/color-picker.types.d.ts +26 -0
- package/dist/ColorPicker/color-picker.types.js +1 -0
- package/dist/ColorPicker/color-picker.variants.d.ts +69 -0
- package/dist/ColorPicker/color-picker.variants.js +13 -0
- package/dist/ColorPicker/index.d.ts +4 -0
- package/dist/ColorPicker/index.js +4 -0
- package/dist/DateRangePicker/DateRangePicker.svelte +59 -0
- package/dist/DateRangePicker/DateRangePicker.svelte.d.ts +4 -0
- package/dist/DateRangePicker/date-range-picker.types.d.ts +34 -0
- package/dist/DateRangePicker/date-range-picker.types.js +1 -0
- package/dist/DateRangePicker/date-range-picker.variants.d.ts +39 -0
- package/dist/DateRangePicker/date-range-picker.variants.js +20 -0
- package/dist/DateRangePicker/index.d.ts +4 -0
- package/dist/DateRangePicker/index.js +4 -0
- 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/List/List.svelte +14 -0
- package/dist/List/List.svelte.d.ts +4 -0
- package/dist/List/ListItem.svelte +64 -0
- package/dist/List/ListItem.svelte.d.ts +4 -0
- package/dist/List/index.d.ts +5 -0
- package/dist/List/index.js +5 -0
- package/dist/List/list.types.d.ts +62 -0
- package/dist/List/list.types.js +1 -0
- package/dist/List/list.variants.d.ts +99 -0
- package/dist/List/list.variants.js +42 -0
- package/dist/Marquee/Marquee.svelte +50 -0
- package/dist/Marquee/Marquee.svelte.d.ts +4 -0
- package/dist/Marquee/index.d.ts +4 -0
- package/dist/Marquee/index.js +4 -0
- package/dist/Marquee/marquee.types.d.ts +38 -0
- package/dist/Marquee/marquee.types.js +1 -0
- package/dist/Marquee/marquee.variants.d.ts +78 -0
- package/dist/Marquee/marquee.variants.js +28 -0
- package/dist/Menu/Menu.svelte +134 -0
- package/dist/Menu/Menu.svelte.d.ts +4 -0
- package/dist/Menu/index.d.ts +4 -0
- package/dist/Menu/index.js +4 -0
- package/dist/Menu/menu.types.d.ts +82 -0
- package/dist/Menu/menu.types.js +1 -0
- package/dist/Menu/menu.variants.d.ts +46 -0
- package/dist/Menu/menu.variants.js +32 -0
- package/dist/NumberTicker/NumberTicker.svelte +59 -0
- package/dist/NumberTicker/NumberTicker.svelte.d.ts +4 -0
- package/dist/NumberTicker/index.d.ts +4 -0
- package/dist/NumberTicker/index.js +4 -0
- package/dist/NumberTicker/number-ticker.types.d.ts +26 -0
- package/dist/NumberTicker/number-ticker.types.js +1 -0
- package/dist/NumberTicker/number-ticker.variants.d.ts +27 -0
- package/dist/NumberTicker/number-ticker.variants.js +6 -0
- package/dist/PasswordInput/PasswordInput.svelte +74 -0
- package/dist/PasswordInput/PasswordInput.svelte.d.ts +4 -0
- package/dist/PasswordInput/index.d.ts +4 -0
- package/dist/PasswordInput/index.js +4 -0
- package/dist/PasswordInput/password-input.types.d.ts +18 -0
- package/dist/PasswordInput/password-input.types.js +1 -0
- package/dist/PasswordInput/password-input.variants.d.ts +57 -0
- package/dist/PasswordInput/password-input.variants.js +11 -0
- package/dist/Prose/Prose.svelte +13 -0
- package/dist/Prose/Prose.svelte.d.ts +4 -0
- package/dist/Prose/index.d.ts +4 -0
- package/dist/Prose/index.js +4 -0
- package/dist/Prose/prose.types.d.ts +22 -0
- package/dist/Prose/prose.types.js +1 -0
- package/dist/Prose/prose.variants.d.ts +45 -0
- package/dist/Prose/prose.variants.js +45 -0
- package/dist/Rating/Rating.svelte +93 -0
- package/dist/Rating/Rating.svelte.d.ts +4 -0
- package/dist/Rating/index.d.ts +4 -0
- package/dist/Rating/index.js +4 -0
- package/dist/Rating/rating.types.d.ts +59 -0
- package/dist/Rating/rating.types.js +1 -0
- package/dist/Rating/rating.variants.d.ts +93 -0
- package/dist/Rating/rating.variants.js +32 -0
- package/dist/Resizable/Resizable.svelte +9 -0
- package/dist/Resizable/Resizable.svelte.d.ts +4 -0
- package/dist/Resizable/index.d.ts +4 -0
- package/dist/Resizable/index.js +4 -0
- package/dist/Resizable/resizable.types.d.ts +18 -0
- package/dist/Resizable/resizable.types.js +1 -0
- package/dist/Resizable/resizable.variants.d.ts +48 -0
- package/dist/Resizable/resizable.variants.js +17 -0
- package/dist/ScrollArea/ScrollArea.svelte +54 -0
- package/dist/ScrollArea/ScrollArea.svelte.d.ts +4 -0
- package/dist/ScrollArea/index.d.ts +4 -0
- package/dist/ScrollArea/index.js +4 -0
- package/dist/ScrollArea/scroll-area.types.d.ts +27 -0
- package/dist/ScrollArea/scroll-area.types.js +1 -0
- package/dist/ScrollArea/scroll-area.variants.d.ts +45 -0
- package/dist/ScrollArea/scroll-area.variants.js +27 -0
- package/dist/SelectMenu/SelectMenu.svelte +46 -14
- package/dist/Sidebar/Sidebar.svelte +30 -0
- package/dist/Sidebar/Sidebar.svelte.d.ts +4 -0
- package/dist/Sidebar/index.d.ts +4 -0
- package/dist/Sidebar/index.js +4 -0
- package/dist/Sidebar/sidebar.types.d.ts +31 -0
- package/dist/Sidebar/sidebar.types.js +1 -0
- package/dist/Sidebar/sidebar.variants.d.ts +69 -0
- package/dist/Sidebar/sidebar.variants.js +23 -0
- package/dist/Spotlight/Spotlight.svelte +31 -0
- package/dist/Spotlight/Spotlight.svelte.d.ts +4 -0
- package/dist/Spotlight/index.d.ts +4 -0
- package/dist/Spotlight/index.js +4 -0
- package/dist/Spotlight/spotlight.types.d.ts +22 -0
- package/dist/Spotlight/spotlight.types.js +1 -0
- package/dist/Spotlight/spotlight.variants.d.ts +39 -0
- package/dist/Spotlight/spotlight.variants.js +8 -0
- package/dist/Stepper/Stepper.svelte +12 -9
- package/dist/TagsInput/TagsInput.svelte +100 -0
- package/dist/TagsInput/TagsInput.svelte.d.ts +4 -0
- package/dist/TagsInput/index.d.ts +4 -0
- package/dist/TagsInput/index.js +4 -0
- package/dist/TagsInput/tags-input.types.d.ts +32 -0
- package/dist/TagsInput/tags-input.types.js +1 -0
- package/dist/TagsInput/tags-input.variants.d.ts +45 -0
- package/dist/TagsInput/tags-input.variants.js +22 -0
- package/dist/TreeView/TreeView.svelte +95 -0
- package/dist/TreeView/TreeView.svelte.d.ts +4 -0
- package/dist/TreeView/index.d.ts +4 -0
- package/dist/TreeView/index.js +4 -0
- package/dist/TreeView/tree-view.types.d.ts +68 -0
- package/dist/TreeView/tree-view.types.js +1 -0
- package/dist/TreeView/tree-view.variants.d.ts +69 -0
- package/dist/TreeView/tree-view.variants.js +30 -0
- package/dist/docs/navigation.js +162 -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/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/mcp/svelora-docs.data.json +59 -5
- package/package.json +8 -6
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
type ElementTarget = HTMLElement | null | undefined | (() => HTMLElement | null | undefined);
|
|
2
|
+
export interface UseFocusTrapOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Whether the trap is active. Accepts a value or a reactive getter.
|
|
5
|
+
* @default true
|
|
6
|
+
*/
|
|
7
|
+
active?: boolean | (() => boolean);
|
|
8
|
+
/**
|
|
9
|
+
* Element to focus when the trap activates. A getter is resolved on activate.
|
|
10
|
+
* Pass `false` to skip auto-focus. Defaults to the first focusable element.
|
|
11
|
+
*/
|
|
12
|
+
initialFocus?: HTMLElement | (() => HTMLElement | null) | false;
|
|
13
|
+
/**
|
|
14
|
+
* Restore focus to the previously focused element when the trap deactivates.
|
|
15
|
+
* @default true
|
|
16
|
+
*/
|
|
17
|
+
restoreFocus?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Trap keyboard focus within an element while active, and restore focus on exit.
|
|
21
|
+
*
|
|
22
|
+
* Cycles Tab / Shift+Tab among the focusable descendants of `target`, focuses an
|
|
23
|
+
* initial element on activation, and (by default) returns focus to the previously
|
|
24
|
+
* focused element when it deactivates or unmounts. The target and `active` may be
|
|
25
|
+
* values or reactive getters. SSR-safe: a nullish/inactive target is a no-op.
|
|
26
|
+
*
|
|
27
|
+
* Intended for custom focus-scoped UI. Components built on bits-ui primitives
|
|
28
|
+
* (Modal, Slideover, Drawer, Popover) already trap focus — do not double-wrap them.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```svelte
|
|
32
|
+
* <script>
|
|
33
|
+
* import { useFocusTrap } from 'svelora'
|
|
34
|
+
*
|
|
35
|
+
* let open = $state(false)
|
|
36
|
+
* let panel = $state<HTMLElement>()
|
|
37
|
+
* useFocusTrap(() => panel, { active: () => open })
|
|
38
|
+
* </script>
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function useFocusTrap(target: ElementTarget, options?: UseFocusTrapOptions): void;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { toGetter } from './utils.js';
|
|
2
|
+
import { useEventListener } from './useEventListener.svelte.js';
|
|
3
|
+
const FOCUSABLE_SELECTOR = [
|
|
4
|
+
'a[href]',
|
|
5
|
+
'button:not([disabled])',
|
|
6
|
+
'input:not([disabled])',
|
|
7
|
+
'select:not([disabled])',
|
|
8
|
+
'textarea:not([disabled])',
|
|
9
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
10
|
+
'audio[controls]',
|
|
11
|
+
'video[controls]',
|
|
12
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
13
|
+
'details > summary:first-of-type'
|
|
14
|
+
].join(',');
|
|
15
|
+
function getFocusable(container) {
|
|
16
|
+
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Trap keyboard focus within an element while active, and restore focus on exit.
|
|
20
|
+
*
|
|
21
|
+
* Cycles Tab / Shift+Tab among the focusable descendants of `target`, focuses an
|
|
22
|
+
* initial element on activation, and (by default) returns focus to the previously
|
|
23
|
+
* focused element when it deactivates or unmounts. The target and `active` may be
|
|
24
|
+
* values or reactive getters. SSR-safe: a nullish/inactive target is a no-op.
|
|
25
|
+
*
|
|
26
|
+
* Intended for custom focus-scoped UI. Components built on bits-ui primitives
|
|
27
|
+
* (Modal, Slideover, Drawer, Popover) already trap focus — do not double-wrap them.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```svelte
|
|
31
|
+
* <script>
|
|
32
|
+
* import { useFocusTrap } from 'svelora'
|
|
33
|
+
*
|
|
34
|
+
* let open = $state(false)
|
|
35
|
+
* let panel = $state<HTMLElement>()
|
|
36
|
+
* useFocusTrap(() => panel, { active: () => open })
|
|
37
|
+
* </script>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function useFocusTrap(target, options = {}) {
|
|
41
|
+
const resolveTarget = toGetter(target);
|
|
42
|
+
const resolveActive = toGetter(options.active ?? true);
|
|
43
|
+
const { initialFocus, restoreFocus = true } = options;
|
|
44
|
+
$effect(() => {
|
|
45
|
+
if (!resolveActive())
|
|
46
|
+
return;
|
|
47
|
+
const resolved = resolveTarget();
|
|
48
|
+
if (!resolved)
|
|
49
|
+
return;
|
|
50
|
+
const el = resolved;
|
|
51
|
+
const previouslyFocused = document.activeElement;
|
|
52
|
+
if (initialFocus !== false) {
|
|
53
|
+
const initial = typeof initialFocus === 'function' ? initialFocus() : initialFocus;
|
|
54
|
+
(initial ?? getFocusable(el)[0] ?? el).focus();
|
|
55
|
+
}
|
|
56
|
+
return () => {
|
|
57
|
+
if (restoreFocus && previouslyFocused && document.contains(previouslyFocused)) {
|
|
58
|
+
previouslyFocused.focus();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
useEventListener(() => (resolveActive() && resolveTarget() ? document : null), 'keydown', (event) => {
|
|
63
|
+
if (event.key !== 'Tab')
|
|
64
|
+
return;
|
|
65
|
+
const el = resolveTarget();
|
|
66
|
+
if (!el)
|
|
67
|
+
return;
|
|
68
|
+
const focusable = getFocusable(el);
|
|
69
|
+
if (focusable.length === 0) {
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const first = focusable[0];
|
|
74
|
+
const last = focusable[focusable.length - 1];
|
|
75
|
+
const active = document.activeElement;
|
|
76
|
+
if (event.shiftKey) {
|
|
77
|
+
if (active === first || !el.contains(active)) {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
last.focus();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (active === last || !el.contains(active)) {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
first.focus();
|
|
85
|
+
}
|
|
86
|
+
}, true);
|
|
87
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
type ElementTarget = Element | null | undefined | (() => Element | null | undefined);
|
|
2
|
+
export type UseIntersectionObserverOptions = IntersectionObserverInit;
|
|
3
|
+
interface UseIntersectionObserverReturn {
|
|
4
|
+
/** Whether the target currently intersects the root, per the latest entry. */
|
|
5
|
+
readonly isIntersecting: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Observe an element's intersection with the viewport (or a root) via
|
|
9
|
+
* `IntersectionObserver`, with automatic cleanup.
|
|
10
|
+
*
|
|
11
|
+
* The target may be a value or a getter; when it is a getter that reads reactive
|
|
12
|
+
* state, the observer re-attaches as the target changes. SSR-safe: a nullish
|
|
13
|
+
* target is a no-op.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```svelte
|
|
17
|
+
* <script>
|
|
18
|
+
* import { useIntersectionObserver } from 'svelora'
|
|
19
|
+
*
|
|
20
|
+
* let el = $state<HTMLElement>()
|
|
21
|
+
* const io = useIntersectionObserver(() => el, (entry) => {
|
|
22
|
+
* if (entry.isIntersecting) loadImage()
|
|
23
|
+
* }, { rootMargin: '200px' })
|
|
24
|
+
* </script>
|
|
25
|
+
*
|
|
26
|
+
* <div bind:this={el}>{io.isIntersecting ? 'visible' : 'hidden'}</div>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function useIntersectionObserver(target: ElementTarget, callback?: (entry: IntersectionObserverEntry) => void, options?: UseIntersectionObserverOptions): UseIntersectionObserverReturn;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { toGetter } from './utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Observe an element's intersection with the viewport (or a root) via
|
|
4
|
+
* `IntersectionObserver`, with automatic cleanup.
|
|
5
|
+
*
|
|
6
|
+
* The target may be a value or a getter; when it is a getter that reads reactive
|
|
7
|
+
* state, the observer re-attaches as the target changes. SSR-safe: a nullish
|
|
8
|
+
* target is a no-op.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <script>
|
|
13
|
+
* import { useIntersectionObserver } from 'svelora'
|
|
14
|
+
*
|
|
15
|
+
* let el = $state<HTMLElement>()
|
|
16
|
+
* const io = useIntersectionObserver(() => el, (entry) => {
|
|
17
|
+
* if (entry.isIntersecting) loadImage()
|
|
18
|
+
* }, { rootMargin: '200px' })
|
|
19
|
+
* </script>
|
|
20
|
+
*
|
|
21
|
+
* <div bind:this={el}>{io.isIntersecting ? 'visible' : 'hidden'}</div>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function useIntersectionObserver(target, callback, options) {
|
|
25
|
+
const resolveTarget = toGetter(target);
|
|
26
|
+
let isIntersecting = $state(false);
|
|
27
|
+
$effect(() => {
|
|
28
|
+
const el = resolveTarget();
|
|
29
|
+
if (!el)
|
|
30
|
+
return;
|
|
31
|
+
const observer = new IntersectionObserver((entries) => {
|
|
32
|
+
const entry = entries[entries.length - 1];
|
|
33
|
+
if (!entry)
|
|
34
|
+
return;
|
|
35
|
+
isIntersecting = entry.isIntersecting;
|
|
36
|
+
callback?.(entry);
|
|
37
|
+
}, options);
|
|
38
|
+
observer.observe(el);
|
|
39
|
+
return () => observer.disconnect();
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
get isIntersecting() {
|
|
43
|
+
return isIntersecting;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface UseLocalStorageSerializer<T> {
|
|
2
|
+
parse: (raw: string) => T;
|
|
3
|
+
stringify: (value: T) => string;
|
|
4
|
+
}
|
|
5
|
+
export interface UseLocalStorageOptions<T> {
|
|
6
|
+
/**
|
|
7
|
+
* Custom serializer. Defaults to JSON.
|
|
8
|
+
*/
|
|
9
|
+
serializer?: UseLocalStorageSerializer<T>;
|
|
10
|
+
/**
|
|
11
|
+
* Update the value when another tab changes the same key (via the `storage` event).
|
|
12
|
+
* @default true
|
|
13
|
+
*/
|
|
14
|
+
syncTabs?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface UseLocalStorageReturn<T> {
|
|
17
|
+
/** The persisted value. Read it, or assign to write through to `localStorage`. */
|
|
18
|
+
current: T;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Reactive `localStorage`-backed value with cross-tab sync.
|
|
22
|
+
*
|
|
23
|
+
* Reads the stored value on mount (falling back to `initial`), writes through to
|
|
24
|
+
* `localStorage` whenever `.current` changes, and — when `syncTabs` is on — updates
|
|
25
|
+
* when another tab changes the same key. SSR-safe: renders `initial` on the server
|
|
26
|
+
* and hydrates on mount; parse/quota errors are tolerated.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```svelte
|
|
30
|
+
* <script>
|
|
31
|
+
* import { useLocalStorage } from 'svelora'
|
|
32
|
+
*
|
|
33
|
+
* const theme = useLocalStorage('theme', 'system')
|
|
34
|
+
* </script>
|
|
35
|
+
*
|
|
36
|
+
* <button onclick={() => (theme.current = 'dark')}>{theme.current}</button>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function useLocalStorage<T>(key: string, initial: T, options?: UseLocalStorageOptions<T>): UseLocalStorageReturn<T>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useEventListener } from './useEventListener.svelte.js';
|
|
2
|
+
const jsonSerializer = {
|
|
3
|
+
parse: (raw) => JSON.parse(raw),
|
|
4
|
+
stringify: (value) => JSON.stringify(value)
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Reactive `localStorage`-backed value with cross-tab sync.
|
|
8
|
+
*
|
|
9
|
+
* Reads the stored value on mount (falling back to `initial`), writes through to
|
|
10
|
+
* `localStorage` whenever `.current` changes, and — when `syncTabs` is on — updates
|
|
11
|
+
* when another tab changes the same key. SSR-safe: renders `initial` on the server
|
|
12
|
+
* and hydrates on mount; parse/quota errors are tolerated.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```svelte
|
|
16
|
+
* <script>
|
|
17
|
+
* import { useLocalStorage } from 'svelora'
|
|
18
|
+
*
|
|
19
|
+
* const theme = useLocalStorage('theme', 'system')
|
|
20
|
+
* </script>
|
|
21
|
+
*
|
|
22
|
+
* <button onclick={() => (theme.current = 'dark')}>{theme.current}</button>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function useLocalStorage(key, initial, options = {}) {
|
|
26
|
+
const serializer = options.serializer ?? jsonSerializer;
|
|
27
|
+
const { syncTabs = true } = options;
|
|
28
|
+
let value = $state(initial);
|
|
29
|
+
$effect(() => {
|
|
30
|
+
const raw = localStorage.getItem(key);
|
|
31
|
+
if (raw !== null) {
|
|
32
|
+
try {
|
|
33
|
+
value = serializer.parse(raw);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
value = initial;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
$effect(() => {
|
|
41
|
+
try {
|
|
42
|
+
const serialized = serializer.stringify(value);
|
|
43
|
+
if (localStorage.getItem(key) !== serialized) {
|
|
44
|
+
localStorage.setItem(key, serialized);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// ignore serialization / quota errors
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
useEventListener(() => (syncTabs ? window : null), 'storage', (event) => {
|
|
52
|
+
if (event.key !== key || event.storageArea !== localStorage)
|
|
53
|
+
return;
|
|
54
|
+
if (event.newValue === null) {
|
|
55
|
+
value = initial;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
value = serializer.parse(event.newValue);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// ignore malformed external writes
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
get current() {
|
|
67
|
+
return value;
|
|
68
|
+
},
|
|
69
|
+
set current(next) {
|
|
70
|
+
value = next;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
type ElementTarget = HTMLElement | null | undefined | (() => HTMLElement | null | undefined);
|
|
2
|
+
/**
|
|
3
|
+
* Observe an element's size changes with a `ResizeObserver` and automatic cleanup.
|
|
4
|
+
*
|
|
5
|
+
* The target may be a value or a getter; when it is a getter that reads reactive
|
|
6
|
+
* state, the observer re-attaches as the target changes. SSR-safe: a nullish
|
|
7
|
+
* target is a no-op.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```svelte
|
|
11
|
+
* <script>
|
|
12
|
+
* import { useResizeObserver } from 'svelora'
|
|
13
|
+
*
|
|
14
|
+
* let el = $state<HTMLElement>()
|
|
15
|
+
* useResizeObserver(() => el, ([entry]) => {
|
|
16
|
+
* console.log(entry.contentRect.width)
|
|
17
|
+
* })
|
|
18
|
+
* </script>
|
|
19
|
+
*
|
|
20
|
+
* <div bind:this={el}>resize me</div>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function useResizeObserver(target: ElementTarget, callback: ResizeObserverCallback, options?: ResizeObserverOptions): void;
|
|
24
|
+
interface UseElementSizeReturn {
|
|
25
|
+
/** Current content-box width in pixels. */
|
|
26
|
+
readonly width: number;
|
|
27
|
+
/** Current content-box height in pixels. */
|
|
28
|
+
readonly height: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Track an element's content-box size reactively.
|
|
32
|
+
*
|
|
33
|
+
* Built on `useResizeObserver`. The size starts at `0` and updates on the
|
|
34
|
+
* observer's initial callback and on every subsequent resize. SSR-safe.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```svelte
|
|
38
|
+
* <script>
|
|
39
|
+
* import { useElementSize } from 'svelora'
|
|
40
|
+
*
|
|
41
|
+
* let el = $state<HTMLElement>()
|
|
42
|
+
* const size = useElementSize(() => el)
|
|
43
|
+
* </script>
|
|
44
|
+
*
|
|
45
|
+
* <div bind:this={el}>...</div>
|
|
46
|
+
* <p>{size.width} × {size.height}</p>
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function useElementSize(target: ElementTarget): UseElementSizeReturn;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { toGetter } from './utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Observe an element's size changes with a `ResizeObserver` and automatic cleanup.
|
|
4
|
+
*
|
|
5
|
+
* The target may be a value or a getter; when it is a getter that reads reactive
|
|
6
|
+
* state, the observer re-attaches as the target changes. SSR-safe: a nullish
|
|
7
|
+
* target is a no-op.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```svelte
|
|
11
|
+
* <script>
|
|
12
|
+
* import { useResizeObserver } from 'svelora'
|
|
13
|
+
*
|
|
14
|
+
* let el = $state<HTMLElement>()
|
|
15
|
+
* useResizeObserver(() => el, ([entry]) => {
|
|
16
|
+
* console.log(entry.contentRect.width)
|
|
17
|
+
* })
|
|
18
|
+
* </script>
|
|
19
|
+
*
|
|
20
|
+
* <div bind:this={el}>resize me</div>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function useResizeObserver(target, callback, options) {
|
|
24
|
+
const resolveTarget = toGetter(target);
|
|
25
|
+
$effect(() => {
|
|
26
|
+
const el = resolveTarget();
|
|
27
|
+
if (!el)
|
|
28
|
+
return;
|
|
29
|
+
const observer = new ResizeObserver(callback);
|
|
30
|
+
observer.observe(el, options);
|
|
31
|
+
return () => observer.disconnect();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Track an element's content-box size reactively.
|
|
36
|
+
*
|
|
37
|
+
* Built on `useResizeObserver`. The size starts at `0` and updates on the
|
|
38
|
+
* observer's initial callback and on every subsequent resize. SSR-safe.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```svelte
|
|
42
|
+
* <script>
|
|
43
|
+
* import { useElementSize } from 'svelora'
|
|
44
|
+
*
|
|
45
|
+
* let el = $state<HTMLElement>()
|
|
46
|
+
* const size = useElementSize(() => el)
|
|
47
|
+
* </script>
|
|
48
|
+
*
|
|
49
|
+
* <div bind:this={el}>...</div>
|
|
50
|
+
* <p>{size.width} × {size.height}</p>
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function useElementSize(target) {
|
|
54
|
+
let width = $state(0);
|
|
55
|
+
let height = $state(0);
|
|
56
|
+
useResizeObserver(target, (entries) => {
|
|
57
|
+
const entry = entries[0];
|
|
58
|
+
if (!entry)
|
|
59
|
+
return;
|
|
60
|
+
width = entry.contentRect.width;
|
|
61
|
+
height = entry.contentRect.height;
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
get width() {
|
|
65
|
+
return width;
|
|
66
|
+
},
|
|
67
|
+
get height() {
|
|
68
|
+
return height;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type ElementTarget = HTMLElement | null | undefined | (() => HTMLElement | null | undefined);
|
|
2
|
+
interface UseScrollLockReturn {
|
|
3
|
+
lock: () => void;
|
|
4
|
+
unlock: () => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Lock scroll on an element (default `document.body`) while `locked` is true.
|
|
8
|
+
*
|
|
9
|
+
* Sets `overflow: hidden` and compensates for the scrollbar width to avoid a
|
|
10
|
+
* layout jump. Reference-counted per target, so nested overlays locking the same
|
|
11
|
+
* element do not unlock it prematurely. Original styles are restored when the
|
|
12
|
+
* last lock releases or on teardown. SSR-safe: a nullish target is a no-op.
|
|
13
|
+
*
|
|
14
|
+
* Pass a reactive `() => open` getter for the common case, or use the returned
|
|
15
|
+
* `lock` / `unlock` for imperative control (do not mix both on one instance).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```svelte
|
|
19
|
+
* <script>
|
|
20
|
+
* import { useScrollLock } from 'svelora'
|
|
21
|
+
*
|
|
22
|
+
* let open = $state(false)
|
|
23
|
+
* useScrollLock(() => open)
|
|
24
|
+
* </script>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function useScrollLock(locked: boolean | (() => boolean), target?: ElementTarget): UseScrollLockReturn;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { toGetter } from './utils.js';
|
|
2
|
+
const registry = new WeakMap();
|
|
3
|
+
function scrollbarWidth(el) {
|
|
4
|
+
if (el === document.body || el === document.documentElement) {
|
|
5
|
+
return window.innerWidth - document.documentElement.clientWidth;
|
|
6
|
+
}
|
|
7
|
+
return el.offsetWidth - el.clientWidth;
|
|
8
|
+
}
|
|
9
|
+
function acquire(el) {
|
|
10
|
+
let state = registry.get(el);
|
|
11
|
+
if (!state) {
|
|
12
|
+
state = { count: 0, overflow: el.style.overflow, paddingRight: el.style.paddingRight };
|
|
13
|
+
registry.set(el, state);
|
|
14
|
+
}
|
|
15
|
+
if (state.count === 0) {
|
|
16
|
+
const gap = scrollbarWidth(el);
|
|
17
|
+
el.style.overflow = 'hidden';
|
|
18
|
+
if (gap > 0) {
|
|
19
|
+
const current = parseFloat(getComputedStyle(el).paddingRight) || 0;
|
|
20
|
+
el.style.paddingRight = `${current + gap}px`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
state.count++;
|
|
24
|
+
}
|
|
25
|
+
function release(el) {
|
|
26
|
+
const state = registry.get(el);
|
|
27
|
+
if (!state)
|
|
28
|
+
return;
|
|
29
|
+
state.count--;
|
|
30
|
+
if (state.count <= 0) {
|
|
31
|
+
el.style.overflow = state.overflow;
|
|
32
|
+
el.style.paddingRight = state.paddingRight;
|
|
33
|
+
registry.delete(el);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Lock scroll on an element (default `document.body`) while `locked` is true.
|
|
38
|
+
*
|
|
39
|
+
* Sets `overflow: hidden` and compensates for the scrollbar width to avoid a
|
|
40
|
+
* layout jump. Reference-counted per target, so nested overlays locking the same
|
|
41
|
+
* element do not unlock it prematurely. Original styles are restored when the
|
|
42
|
+
* last lock releases or on teardown. SSR-safe: a nullish target is a no-op.
|
|
43
|
+
*
|
|
44
|
+
* Pass a reactive `() => open` getter for the common case, or use the returned
|
|
45
|
+
* `lock` / `unlock` for imperative control (do not mix both on one instance).
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```svelte
|
|
49
|
+
* <script>
|
|
50
|
+
* import { useScrollLock } from 'svelora'
|
|
51
|
+
*
|
|
52
|
+
* let open = $state(false)
|
|
53
|
+
* useScrollLock(() => open)
|
|
54
|
+
* </script>
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function useScrollLock(locked, target) {
|
|
58
|
+
const resolveLocked = toGetter(locked);
|
|
59
|
+
const resolveTarget = typeof target === 'function' ? target : () => target ?? document.body;
|
|
60
|
+
let manual = $state(null);
|
|
61
|
+
const isLocked = $derived(manual ?? resolveLocked());
|
|
62
|
+
$effect(() => {
|
|
63
|
+
if (!isLocked)
|
|
64
|
+
return;
|
|
65
|
+
const el = resolveTarget();
|
|
66
|
+
if (!el)
|
|
67
|
+
return;
|
|
68
|
+
acquire(el);
|
|
69
|
+
return () => release(el);
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
lock() {
|
|
73
|
+
manual = true;
|
|
74
|
+
},
|
|
75
|
+
unlock() {
|
|
76
|
+
manual = false;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface UseThrottleOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Minimum time in milliseconds between invocations.
|
|
4
|
+
* @default 200
|
|
5
|
+
*/
|
|
6
|
+
delay?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Reactive throttle hook. Caps how often a callback runs to at most once per
|
|
10
|
+
* `delay`, with leading and trailing invocation.
|
|
11
|
+
*
|
|
12
|
+
* The companion to `useDebounce`: debounce waits for a pause, throttle
|
|
13
|
+
* guarantees a steady maximum rate — ideal for scroll, resize, mousemove, and
|
|
14
|
+
* drag handlers. The first call runs immediately; calls during the cooldown
|
|
15
|
+
* window are coalesced into a single trailing call (latest wins). Any pending
|
|
16
|
+
* timer is cleared on teardown.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```svelte
|
|
20
|
+
* <script>
|
|
21
|
+
* import { useThrottle } from 'svelora'
|
|
22
|
+
*
|
|
23
|
+
* const throttle = useThrottle({ delay: 100 })
|
|
24
|
+
*
|
|
25
|
+
* function onScroll() {
|
|
26
|
+
* throttle.run(() => updatePosition(window.scrollY))
|
|
27
|
+
* }
|
|
28
|
+
* </script>
|
|
29
|
+
*
|
|
30
|
+
* <svelte:window onscroll={onScroll} />
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function useThrottle(options?: UseThrottleOptions): {
|
|
34
|
+
readonly pending: boolean;
|
|
35
|
+
run: (callback: () => void) => void;
|
|
36
|
+
cancel: () => void;
|
|
37
|
+
};
|