svelora 3.0.5 → 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 (35) hide show
  1. package/dist/Fonts/fonts.js +3 -1
  2. package/dist/Link/Link.context-harness.svelte +8 -0
  3. package/dist/Link/Link.context-harness.svelte.d.ts +7 -0
  4. package/dist/Link/Link.svelte +57 -30
  5. package/dist/Link/index.d.ts +2 -0
  6. package/dist/Link/index.js +1 -0
  7. package/dist/Link/location-context.d.ts +4 -0
  8. package/dist/Link/location-context.js +1 -0
  9. package/dist/SelectMenu/SelectMenu.svelte +46 -14
  10. package/dist/Stepper/Stepper.svelte +12 -9
  11. package/dist/docs/navigation.js +54 -0
  12. package/dist/hooks/index.d.ts +14 -0
  13. package/dist/hooks/index.js +9 -0
  14. package/dist/hooks/useDebouncedState.svelte.d.ts +30 -0
  15. package/dist/hooks/useDebouncedState.svelte.js +45 -0
  16. package/dist/hooks/useEventListener.svelte.d.ts +30 -0
  17. package/dist/hooks/useEventListener.svelte.js +16 -0
  18. package/dist/hooks/useFocusTrap.svelte.d.ts +42 -0
  19. package/dist/hooks/useFocusTrap.svelte.js +87 -0
  20. package/dist/hooks/useIntersectionObserver.svelte.d.ts +30 -0
  21. package/dist/hooks/useIntersectionObserver.svelte.js +46 -0
  22. package/dist/hooks/useLocalStorage.svelte.d.ts +39 -0
  23. package/dist/hooks/useLocalStorage.svelte.js +73 -0
  24. package/dist/hooks/useResizeObserver.svelte.d.ts +50 -0
  25. package/dist/hooks/useResizeObserver.svelte.js +71 -0
  26. package/dist/hooks/useScrollLock.svelte.d.ts +28 -0
  27. package/dist/hooks/useScrollLock.svelte.js +79 -0
  28. package/dist/hooks/useThrottle.svelte.d.ts +37 -0
  29. package/dist/hooks/useThrottle.svelte.js +72 -0
  30. package/dist/hooks/useTimers.svelte.d.ts +62 -0
  31. package/dist/hooks/useTimers.svelte.js +90 -0
  32. package/dist/hooks/utils.d.ts +1 -0
  33. package/dist/hooks/utils.js +3 -0
  34. package/dist/mcp/svelora-docs.data.json +22 -4
  35. package/package.json +1 -1
@@ -0,0 +1,90 @@
1
+ import { toGetter } from './utils.js';
2
+ /**
3
+ * Run a callback on an interval with proper runes teardown.
4
+ *
5
+ * The delay may be a value or a getter; changing it restarts the interval with
6
+ * the new period. A `null`/`undefined` or non-positive delay disables it, as
7
+ * does `pause()` or the `paused` option. SSR-safe: no timer runs on the server.
8
+ *
9
+ * @example
10
+ * ```svelte
11
+ * <script>
12
+ * import { useInterval } from 'svelora'
13
+ *
14
+ * let count = $state(0)
15
+ * const timer = useInterval(() => count++, 1000)
16
+ * </script>
17
+ *
18
+ * <button onclick={timer.pause}>Pause</button>
19
+ * ```
20
+ */
21
+ export function useInterval(callback, delay, options = {}) {
22
+ const resolveDelay = toGetter(delay);
23
+ const resolvePaused = toGetter(options.paused ?? false);
24
+ let manuallyPaused = $state(false);
25
+ const active = $derived.by(() => {
26
+ if (manuallyPaused || resolvePaused())
27
+ return false;
28
+ const ms = resolveDelay();
29
+ return typeof ms === 'number' && ms > 0;
30
+ });
31
+ $effect(() => {
32
+ if (!active)
33
+ return;
34
+ const ms = resolveDelay();
35
+ if (typeof ms !== 'number' || ms <= 0)
36
+ return;
37
+ const id = setInterval(callback, ms);
38
+ return () => clearInterval(id);
39
+ });
40
+ return {
41
+ pause() {
42
+ manuallyPaused = true;
43
+ },
44
+ resume() {
45
+ manuallyPaused = false;
46
+ },
47
+ get active() {
48
+ return active;
49
+ }
50
+ };
51
+ }
52
+ /**
53
+ * Schedule a callback to run once after a delay, with proper runes teardown.
54
+ *
55
+ * Starts on mount. The delay may be a value or a getter; changing it restarts
56
+ * the timer. A `null`/`undefined` or negative delay schedules nothing. SSR-safe:
57
+ * no timer runs on the server.
58
+ *
59
+ * @example
60
+ * ```svelte
61
+ * <script>
62
+ * import { useTimeout } from 'svelora'
63
+ *
64
+ * const timer = useTimeout(() => (visible = false), 3000)
65
+ * </script>
66
+ *
67
+ * <button onclick={timer.restart}>Reset</button>
68
+ * ```
69
+ */
70
+ export function useTimeout(callback, delay) {
71
+ const resolveDelay = toGetter(delay);
72
+ let timeoutId;
73
+ let restartToken = $state(0);
74
+ $effect(() => {
75
+ void restartToken;
76
+ const ms = resolveDelay();
77
+ if (typeof ms !== 'number' || ms < 0)
78
+ return;
79
+ timeoutId = setTimeout(callback, ms);
80
+ return () => clearTimeout(timeoutId);
81
+ });
82
+ return {
83
+ restart() {
84
+ restartToken++;
85
+ },
86
+ cancel() {
87
+ clearTimeout(timeoutId);
88
+ }
89
+ };
90
+ }
@@ -0,0 +1 @@
1
+ export declare function toGetter<T>(value: T | (() => T)): () => T;
@@ -0,0 +1,3 @@
1
+ export function toGetter(value) {
2
+ return typeof value === 'function' ? value : () => value;
3
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "version": 1,
3
3
  "packageName": "svelora",
4
- "packageVersion": "3.0.5",
5
- "generatedAt": "2026-06-24T07:18:01.854Z",
4
+ "packageVersion": "3.0.6",
5
+ "generatedAt": "2026-06-25T08:54:35.884Z",
6
6
  "slugs": {
7
7
  "components": [
8
8
  "button",
@@ -69,7 +69,16 @@
69
69
  "use-click-outside",
70
70
  "use-infinite-scroll",
71
71
  "use-escape-keydown",
72
- "use-debounce"
72
+ "use-debounce",
73
+ "use-debounced-state",
74
+ "use-event-listener",
75
+ "use-resize-observer",
76
+ "use-intersection-observer",
77
+ "use-scroll-lock",
78
+ "use-focus-trap",
79
+ "use-local-storage",
80
+ "use-throttle",
81
+ "use-timers"
73
82
  ]
74
83
  },
75
84
  "pages": {
@@ -135,6 +144,15 @@
135
144
  "use-click-outside": "<script lang=\"ts\">\n import { useClickOutside } from '$lib/index.js'\n import { Button, Badge, Card } from '$lib/index.js'\n\n let dropdownOpen = $state(false)\n let clickCount = $state(0)\n\n let editMode = $state(false)\n let editValue = $state('Click to edit this text')\n\n let popupOpen = $state(false)\n let enabled = $state(true)\n</script>\n\n<div class=\"space-y-8\">\n <div class=\"space-y-2\">\n <h1 class=\"text-2xl font-bold\">useClickOutside</h1>\n <p class=\"text-on-surface-variant\">\n Svelte action that detects clicks outside an element. Use it with\n <code class=\"rounded bg-surface-container px-1\">use:useClickOutside</code>.\n </p>\n </div>\n\n <!-- Basic -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Basic</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Click inside the blue box — nothing happens. Click outside — counter increments.\n </p>\n <div class=\"rounded-lg bg-surface-container-high p-4\">\n <div class=\"flex items-center gap-4\">\n <div\n use:useClickOutside={{ handler: () => clickCount++ }}\n class=\"flex items-center justify-center rounded-lg border-2 border-dashed border-primary bg-primary/10 p-8\"\n >\n <span class=\"text-sm font-medium\">Click outside me</span>\n </div>\n <Badge\n label=\"Outside clicks: {clickCount}\"\n color={clickCount > 0 ? 'primary' : 'surface'}\n variant=\"subtle\"\n />\n </div>\n </div>\n </section>\n\n <!-- Dropdown -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Dropdown</h2>\n <div class=\"rounded-lg bg-surface-container-high p-4\">\n <div class=\"relative inline-block\">\n <Button onclick={() => (dropdownOpen = !dropdownOpen)}>\n {dropdownOpen ? 'Close Menu' : 'Open Menu'}\n </Button>\n\n {#if dropdownOpen}\n <div\n use:useClickOutside={{ handler: () => (dropdownOpen = false) }}\n class=\"absolute top-full left-0 z-10 mt-2 w-48 rounded-lg border border-outline-variant bg-surface-container p-1 shadow-lg\"\n >\n {#each ['Profile', 'Settings', 'Logout'] as item (item)}\n <button\n class=\"w-full rounded-md px-3 py-2 text-left text-sm hover:bg-surface-container-high\"\n onclick={() => (dropdownOpen = false)}\n >\n {item}\n </button>\n {/each}\n </div>\n {/if}\n </div>\n </div>\n </section>\n\n <!-- Inline Edit -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Inline Edit</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Click the text to edit. Click outside to save.\n </p>\n <div class=\"rounded-lg bg-surface-container-high p-4\">\n {#if editMode}\n <div use:useClickOutside={{ handler: () => (editMode = false) }}>\n <input\n bind:value={editValue}\n class=\"w-full max-w-sm rounded-md border border-primary bg-surface px-3 py-2 text-sm ring-2 ring-primary/30 outline-none\"\n />\n </div>\n {:else}\n <button\n class=\"rounded-md px-3 py-2 text-sm hover:bg-surface-container\"\n onclick={() => (editMode = true)}\n >\n {editValue}\n <span class=\"ml-2 text-xs text-on-surface-variant\">(click to edit)</span>\n </button>\n {/if}\n </div>\n </section>\n\n <!-- Enabled/Disabled -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Enable / Disable</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Toggle the listener on and off with the <code class=\"rounded bg-surface-container px-1\"\n >enabled</code\n > option.\n </p>\n <div class=\"rounded-lg bg-surface-container-high p-4\">\n <div class=\"flex items-center gap-4\">\n <Button variant=\"outline\" size=\"sm\" onclick={() => (enabled = !enabled)}>\n Listener: {enabled ? 'ON' : 'OFF'}\n </Button>\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n onclick={() => (popupOpen = true)}\n disabled={popupOpen}\n >\n Show Popup\n </Button>\n </div>\n\n {#if popupOpen}\n <Card class=\"mt-3 inline-block p-4\">\n <div\n use:useClickOutside={{\n handler: () => (popupOpen = false),\n enabled\n }}\n >\n <p class=\"text-sm font-medium\">Popup Content</p>\n <p class=\"text-xs text-on-surface-variant\">\n {enabled\n ? 'Click outside to close'\n : 'Outside click disabled — use button'}\n </p>\n <Button\n size=\"xs\"\n variant=\"ghost\"\n class=\"mt-2\"\n onclick={() => (popupOpen = false)}\n >\n Close manually\n </Button>\n </div>\n </Card>\n {/if}\n </div>\n </section>\n</div>\n",
136
145
  "use-infinite-scroll": "<script lang=\"ts\">\n import { useInfiniteScroll } from '$lib/index.js'\n import { Badge, Button, Skeleton, Table, type TableColumn } from '$lib/index.js'\n\n // ==================== Basic List ====================\n\n let items = $state<{ id: number; title: string }[]>(\n Array.from({ length: 20 }, (_, i) => ({ id: i + 1, title: `Item ${i + 1}` }))\n )\n let hasMore = $state(true)\n let loadCount = $state(0)\n\n async function fetchMore() {\n await new Promise((r) => setTimeout(r, 800))\n const start = items.length\n const next = Array.from({ length: 20 }, (_, i) => ({\n id: start + i + 1,\n title: `Item ${start + i + 1}`\n }))\n items.push(...next)\n loadCount++\n if (items.length >= 100) hasMore = false\n }\n\n const scroll = useInfiniteScroll({\n onLoad: fetchMore,\n threshold: 150,\n enabled: () => hasMore\n })\n\n function reset() {\n items = Array.from({ length: 20 }, (_, i) => ({ id: i + 1, title: `Item ${i + 1}` }))\n hasMore = true\n loadCount = 0\n }\n\n // ==================== Table ====================\n\n interface User {\n id: number\n name: string\n email: string\n role: string\n status: 'active' | 'inactive' | 'pending'\n }\n\n const roles = ['Admin', 'Editor', 'Viewer', 'Moderator']\n const statuses = ['active', 'inactive', 'pending'] as const\n const firstNames = [\n 'Alice',\n 'Bob',\n 'Charlie',\n 'Diana',\n 'Eve',\n 'Frank',\n 'Grace',\n 'Henry',\n 'Iris',\n 'Jack'\n ]\n const lastNames = [\n 'Johnson',\n 'Smith',\n 'Brown',\n 'Prince',\n 'Davis',\n 'Wilson',\n 'Taylor',\n 'Clark',\n 'Lee',\n 'Hall'\n ]\n\n function generateUsers(start: number, count: number): User[] {\n return Array.from({ length: count }, (_, i) => {\n const id = start + i + 1\n const first = firstNames[id % firstNames.length]\n const last = lastNames[Math.floor(id / firstNames.length) % lastNames.length]\n return {\n id,\n name: `${first} ${last}`,\n email: `${first.toLowerCase()}.${last.toLowerCase()}${id}@example.com`,\n role: roles[id % roles.length],\n status: statuses[id % statuses.length]\n }\n })\n }\n\n let users = $state<User[]>(generateUsers(0, 30))\n let tableHasMore = $state(true)\n let tableLoadCount = $state(0)\n\n async function fetchMoreUsers() {\n await new Promise((r) => setTimeout(r, 1000))\n const next = generateUsers(users.length, 30)\n users.push(...next)\n tableLoadCount++\n if (users.length >= 150) tableHasMore = false\n }\n\n const tableScroll = useInfiniteScroll({\n onLoad: fetchMoreUsers,\n threshold: 200,\n enabled: () => tableHasMore\n })\n\n const columns: TableColumn<User>[] = [\n { key: 'id', label: '#', width: 60, align: 'center' },\n { key: 'name', label: 'Name', sortable: true },\n { key: 'email', label: 'Email' },\n { key: 'role', label: 'Role' },\n { key: 'status', label: 'Status' }\n ]\n\n function resetTable() {\n users = generateUsers(0, 30)\n tableHasMore = true\n tableLoadCount = 0\n }\n</script>\n\n<div class=\"space-y-8\">\n <div class=\"space-y-2\">\n <h1 class=\"text-2xl font-bold\">useInfiniteScroll</h1>\n <p class=\"text-on-surface-variant\">\n Reactive infinite scroll hook with Svelte action. Triggers a callback when the user\n scrolls near the bottom of a container.\n </p>\n </div>\n\n <!-- Basic List -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Basic</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Scroll down inside the container to load more items. Stops at 100 items.\n </p>\n <div class=\"flex flex-wrap items-center gap-3\">\n <Badge label=\"Items: {items.length}\" color=\"primary\" variant=\"subtle\" />\n <Badge label=\"Loads: {loadCount}\" color=\"info\" variant=\"subtle\" />\n <Badge\n label={hasMore ? 'Has more' : 'All loaded'}\n color={hasMore ? 'success' : 'surface'}\n variant=\"subtle\"\n />\n <Button size=\"xs\" variant=\"outline\" onclick={reset}>Reset</Button>\n </div>\n <div\n use:scroll.action\n class=\"h-80 space-y-2 overflow-y-auto rounded-lg bg-surface-container-high p-4\"\n >\n {#each items as item (item.id)}\n <div\n class=\"flex items-center justify-between rounded-md bg-surface-container px-4 py-3\"\n >\n <span class=\"text-sm\">{item.title}</span>\n <Badge label=\"#{item.id}\" color=\"surface\" variant=\"outline\" size=\"sm\" />\n </div>\n {/each}\n\n {#if scroll.loading}\n <div class=\"space-y-2 pt-2\">\n <Skeleton class=\"h-11 w-full rounded-md\" />\n <Skeleton class=\"h-11 w-full rounded-md\" />\n <Skeleton class=\"h-11 w-full rounded-md\" />\n </div>\n {/if}\n\n {#if !hasMore}\n <p class=\"py-4 text-center text-sm text-on-surface-variant\">All items loaded</p>\n {/if}\n </div>\n </section>\n\n <!-- Table + Infinite Scroll -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Table + Infinite Scroll</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Combine with the Table component for paginated data loading. Scroll the table to\n automatically load more rows. Stops at 150 users.\n </p>\n <div class=\"flex flex-wrap items-center gap-3\">\n <Badge label=\"Users: {users.length}\" color=\"primary\" variant=\"subtle\" />\n <Badge label=\"Loads: {tableLoadCount}\" color=\"info\" variant=\"subtle\" />\n <Badge\n label={tableHasMore ? 'Has more' : 'All loaded'}\n color={tableHasMore ? 'success' : 'surface'}\n variant=\"subtle\"\n />\n <Button size=\"xs\" variant=\"outline\" onclick={resetTable}>Reset</Button>\n </div>\n <Table\n data={users}\n {columns}\n rowKey=\"id\"\n manualPagination\n total={users.length}\n pageSize={users.length}\n sticky=\"header\"\n hoverable\n loading={tableScroll.loading}\n action={tableScroll.action}\n class=\"h-112 overflow-y-auto\"\n >\n {#snippet cellSlot({ column, value })}\n {@const cellValue = String(value ?? '')}\n {#if column.key === 'status'}\n <Badge\n label={cellValue}\n color={cellValue === 'active'\n ? 'success'\n : cellValue === 'pending'\n ? 'warning'\n : 'surface'}\n variant=\"soft\"\n size=\"sm\"\n />\n {:else if column.key === 'role'}\n <Badge label={cellValue} color=\"info\" variant=\"subtle\" size=\"sm\" />\n {:else}\n {cellValue}\n {/if}\n {/snippet}\n\n {#snippet bodyBottomSlot()}\n {#if !tableHasMore}\n <tr>\n <td\n colspan={columns.length}\n class=\"py-4 text-center text-sm text-on-surface-variant\"\n >\n All users loaded\n </td>\n </tr>\n {/if}\n {/snippet}\n </Table>\n </section>\n</div>\n",
137
146
  "use-escape-keydown": "<script lang=\"ts\">\n import { useEscapeKeydown } from '$lib/index.js'\n import { Button, Badge, Card } from '$lib/index.js'\n\n let escCount = $state(0)\n\n let panelOpen = $state(false)\n let confirmOpen = $state(false)\n\n let enabled = $state(true)\n let controlledOpen = $state(false)\n</script>\n\n<div class=\"space-y-8\">\n <div class=\"space-y-2\">\n <h1 class=\"text-2xl font-bold\">useEscapeKeydown</h1>\n <p class=\"text-on-surface-variant\">\n Svelte action that listens for the Escape key. Lightweight alternative to\n <code class=\"rounded bg-surface-container px-1\">useKbd</code> when you only need Escape.\n </p>\n </div>\n\n <!-- Basic -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Basic</h2>\n <p class=\"text-sm text-on-surface-variant\">Press Escape anywhere on this page.</p>\n <div\n use:useEscapeKeydown={{ handler: () => escCount++ }}\n class=\"flex items-center gap-4 rounded-lg bg-surface-container-high p-4\"\n >\n <Badge\n label=\"Escape pressed: {escCount} time{escCount === 1 ? '' : 's'}\"\n color={escCount > 0 ? 'primary' : 'surface'}\n variant=\"subtle\"\n size=\"md\"\n />\n <Button size=\"xs\" variant=\"ghost\" onclick={() => (escCount = 0)}>Reset</Button>\n </div>\n </section>\n\n <!-- Dismiss Panel -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Dismiss Panel</h2>\n <div class=\"rounded-lg bg-surface-container-high p-4\">\n <Button onclick={() => (panelOpen = true)} disabled={panelOpen}>Show Panel</Button>\n\n {#if panelOpen}\n <Card class=\"mt-3 p-4\">\n <div use:useEscapeKeydown={{ handler: () => (panelOpen = false) }}>\n <p class=\"text-sm font-medium\">Info Panel</p>\n <p class=\"text-xs text-on-surface-variant\">\n Press <kbd\n class=\"rounded border border-outline-variant bg-surface-container px-1.5 py-0.5 font-mono text-xs\"\n >Esc</kbd\n > to close this panel.\n </p>\n </div>\n </Card>\n {/if}\n </div>\n </section>\n\n <!-- Confirmation Dialog -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Confirmation Dialog</h2>\n <p class=\"text-sm text-on-surface-variant\">Press Escape to cancel the confirmation.</p>\n <div class=\"rounded-lg bg-surface-container-high p-4\">\n {#if !confirmOpen}\n <Button color=\"error\" variant=\"soft\" onclick={() => (confirmOpen = true)}>\n Delete Item\n </Button>\n {:else}\n <Card class=\"inline-block p-4\">\n <div use:useEscapeKeydown={{ handler: () => (confirmOpen = false) }}>\n <p class=\"text-sm font-medium\">Are you sure?</p>\n <p class=\"mb-3 text-xs text-on-surface-variant\">\n Press Escape to cancel, or click Confirm.\n </p>\n <div class=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onclick={() => (confirmOpen = false)}\n >\n Cancel\n </Button>\n <Button size=\"sm\" color=\"error\" onclick={() => (confirmOpen = false)}>\n Confirm\n </Button>\n </div>\n </div>\n </Card>\n {/if}\n </div>\n </section>\n\n <!-- Enable/Disable -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Enable / Disable</h2>\n <div class=\"rounded-lg bg-surface-container-high p-4\">\n <div class=\"flex items-center gap-4\">\n <Button variant=\"outline\" size=\"sm\" onclick={() => (enabled = !enabled)}>\n Listener: {enabled ? 'ON' : 'OFF'}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onclick={() => (controlledOpen = true)}\n disabled={controlledOpen}\n >\n Show Box\n </Button>\n </div>\n\n {#if controlledOpen}\n <Card class=\"mt-3 inline-block p-4\">\n <div\n use:useEscapeKeydown={{ handler: () => (controlledOpen = false), enabled }}\n >\n <p class=\"text-sm font-medium\">Controlled Box</p>\n <p class=\"text-xs text-on-surface-variant\">\n {enabled ? 'Press Escape to close' : 'Escape disabled — close manually'}\n </p>\n <Button\n size=\"xs\"\n variant=\"ghost\"\n class=\"mt-2\"\n onclick={() => (controlledOpen = false)}\n >\n Close manually\n </Button>\n </div>\n </Card>\n {/if}\n </div>\n </section>\n</div>\n",
138
- "use-debounce": "<script lang=\"ts\">\n import { useDebounce } from '$lib/index.js'\n import { Button, Input, Badge, Card, Icon } from '$lib/index.js'\n\n // ==================== Basic ====================\n let searchQuery = $state('')\n let searchResult = $state('')\n let searchCount = $state(0)\n const searchDebounce = useDebounce({ delay: 500 })\n\n function handleSearch(e: Event) {\n searchQuery = (e.currentTarget as HTMLInputElement).value\n searchDebounce.run(() => {\n searchResult = searchQuery\n searchCount++\n })\n }\n\n // ==================== Auto-save ====================\n let noteText = $state('Start typing to auto-save...')\n let savedText = $state('')\n let saveCount = $state(0)\n const saveDebounce = useDebounce({ delay: 1000 })\n\n function handleNoteInput(e: Event) {\n noteText = (e.currentTarget as HTMLTextAreaElement).value\n saveDebounce.run(() => {\n savedText = noteText\n saveCount++\n })\n }\n\n // ==================== API Simulation ====================\n interface UserResult {\n id: number\n name: string\n }\n\n let apiQuery = $state('')\n let apiResults = $state<UserResult[]>([])\n let apiLoading = $state(false)\n const apiDebounce = useDebounce({ delay: 400 })\n\n const allUsers: UserResult[] = [\n { id: 1, name: 'Alice Johnson' },\n { id: 2, name: 'Bob Smith' },\n { id: 3, name: 'Charlie Brown' },\n { id: 4, name: 'Diana Prince' },\n { id: 5, name: 'Eve Davis' },\n { id: 6, name: 'Frank Wilson' },\n { id: 7, name: 'Grace Taylor' },\n { id: 8, name: 'Henry Clark' }\n ]\n\n function handleApiSearch(e: Event) {\n apiQuery = (e.currentTarget as HTMLInputElement).value\n if (!apiQuery.trim()) {\n apiDebounce.cancel()\n apiResults = []\n apiLoading = false\n return\n }\n apiDebounce.run(async () => {\n apiLoading = true\n await new Promise((r) => setTimeout(r, 300))\n const q = apiQuery.toLowerCase()\n apiResults = allUsers.filter((u) => u.name.toLowerCase().includes(q))\n apiLoading = false\n })\n }\n\n // ==================== Cancel & Flush ====================\n let controlValue = $state('')\n let controlResult = $state('')\n const controlDebounce = useDebounce({ delay: 2000 })\n\n function handleControlInput(e: Event) {\n controlValue = (e.currentTarget as HTMLInputElement).value\n controlDebounce.run(() => {\n controlResult = controlValue\n })\n }\n</script>\n\n<div class=\"space-y-8\">\n <div class=\"space-y-2\">\n <h1 class=\"text-2xl font-bold\">useDebounce</h1>\n <p class=\"text-on-surface-variant\">\n Reactive debounce hook. Delays execution until a pause in calls. Tracks pending state\n and supports cancel/flush.\n </p>\n </div>\n\n <!-- Basic Search -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Basic: Search</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Type in the input — the search only fires after 500ms of inactivity.\n </p>\n <div class=\"space-y-3 rounded-lg bg-surface-container-high p-4\">\n <Input\n value={searchQuery}\n oninput={handleSearch}\n placeholder=\"Search something...\"\n leadingIcon=\"lucide:search\"\n />\n <div class=\"flex flex-wrap items-center gap-3\">\n <Badge\n label={searchDebounce.pending ? 'Typing...' : 'Idle'}\n color={searchDebounce.pending ? 'warning' : 'surface'}\n variant=\"soft\"\n />\n <Badge label=\"Executed: {searchCount}x\" color=\"info\" variant=\"subtle\" />\n {#if searchResult}\n <span class=\"text-sm text-on-surface-variant\">\n Last search: <strong>{searchResult}</strong>\n </span>\n {/if}\n </div>\n </div>\n </section>\n\n <!-- Auto-save -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Auto-save</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Content auto-saves 1 second after you stop typing.\n </p>\n <div class=\"space-y-3 rounded-lg bg-surface-container-high p-4\">\n <textarea\n value={noteText}\n oninput={handleNoteInput}\n rows=\"3\"\n class=\"w-full rounded-md border border-outline-variant bg-surface px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/30\"\n ></textarea>\n <div class=\"flex items-center gap-3\">\n {#if saveDebounce.pending}\n <Badge label=\"Unsaved changes...\" color=\"warning\" variant=\"soft\" />\n {:else}\n <Badge\n label={saveCount > 0 ? 'Saved' : 'No changes'}\n color={saveCount > 0 ? 'success' : 'surface'}\n variant=\"soft\"\n />\n {/if}\n {#if savedText}\n <span class=\"text-xs text-on-surface-variant\">\n Last saved: {savedText.length} chars\n </span>\n {/if}\n </div>\n </div>\n </section>\n\n <!-- API Search -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Real World: API Search</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Debounced API call with loading state. Try typing \"alice\", \"bob\", or \"grace\".\n </p>\n <div class=\"space-y-3 rounded-lg bg-surface-container-high p-4\">\n <Input\n value={apiQuery}\n oninput={handleApiSearch}\n placeholder=\"Search users...\"\n leadingIcon=\"lucide:users\"\n loading={apiLoading}\n />\n\n {#if apiResults.length > 0}\n <div class=\"space-y-1\">\n {#each apiResults as user (user.id)}\n <div\n class=\"flex items-center gap-3 rounded-md bg-surface-container px-3 py-2\"\n >\n <Icon name=\"lucide:user\" size=\"16\" class=\"text-on-surface-variant\" />\n <span class=\"text-sm\">{user.name}</span>\n </div>\n {/each}\n </div>\n {:else if apiQuery && !apiLoading && !apiDebounce.pending}\n <p class=\"text-sm text-on-surface-variant\">No users found.</p>\n {/if}\n </div>\n </section>\n\n <!-- Cancel & Flush -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Cancel & Flush</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Delay is 2 seconds. Use <strong>Cancel</strong> to discard, or <strong>Flush</strong> to execute\n immediately.\n </p>\n <div class=\"space-y-3 rounded-lg bg-surface-container-high p-4\">\n <Input\n value={controlValue}\n oninput={handleControlInput}\n placeholder=\"Type and use buttons below...\"\n />\n <div class=\"flex flex-wrap items-center gap-3\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onclick={() => controlDebounce.cancel()}\n disabled={!controlDebounce.pending}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n variant=\"soft\"\n onclick={() => controlDebounce.flush(() => (controlResult = controlValue))}\n disabled={!controlDebounce.pending}\n >\n Flush Now\n </Button>\n <Badge\n label={controlDebounce.pending ? 'Pending (2s)...' : 'Idle'}\n color={controlDebounce.pending ? 'warning' : 'surface'}\n variant=\"soft\"\n />\n </div>\n {#if controlResult}\n <Card class=\"p-3\">\n <p class=\"text-sm\">\n Result: <strong>{controlResult}</strong>\n </p>\n </Card>\n {/if}\n </div>\n </section>\n</div>\n"
147
+ "use-debounce": "<script lang=\"ts\">\n import { useDebounce } from '$lib/index.js'\n import { Button, Input, Badge, Card, Icon } from '$lib/index.js'\n\n // ==================== Basic ====================\n let searchQuery = $state('')\n let searchResult = $state('')\n let searchCount = $state(0)\n const searchDebounce = useDebounce({ delay: 500 })\n\n function handleSearch(e: Event) {\n searchQuery = (e.currentTarget as HTMLInputElement).value\n searchDebounce.run(() => {\n searchResult = searchQuery\n searchCount++\n })\n }\n\n // ==================== Auto-save ====================\n let noteText = $state('Start typing to auto-save...')\n let savedText = $state('')\n let saveCount = $state(0)\n const saveDebounce = useDebounce({ delay: 1000 })\n\n function handleNoteInput(e: Event) {\n noteText = (e.currentTarget as HTMLTextAreaElement).value\n saveDebounce.run(() => {\n savedText = noteText\n saveCount++\n })\n }\n\n // ==================== API Simulation ====================\n interface UserResult {\n id: number\n name: string\n }\n\n let apiQuery = $state('')\n let apiResults = $state<UserResult[]>([])\n let apiLoading = $state(false)\n const apiDebounce = useDebounce({ delay: 400 })\n\n const allUsers: UserResult[] = [\n { id: 1, name: 'Alice Johnson' },\n { id: 2, name: 'Bob Smith' },\n { id: 3, name: 'Charlie Brown' },\n { id: 4, name: 'Diana Prince' },\n { id: 5, name: 'Eve Davis' },\n { id: 6, name: 'Frank Wilson' },\n { id: 7, name: 'Grace Taylor' },\n { id: 8, name: 'Henry Clark' }\n ]\n\n function handleApiSearch(e: Event) {\n apiQuery = (e.currentTarget as HTMLInputElement).value\n if (!apiQuery.trim()) {\n apiDebounce.cancel()\n apiResults = []\n apiLoading = false\n return\n }\n apiDebounce.run(async () => {\n apiLoading = true\n await new Promise((r) => setTimeout(r, 300))\n const q = apiQuery.toLowerCase()\n apiResults = allUsers.filter((u) => u.name.toLowerCase().includes(q))\n apiLoading = false\n })\n }\n\n // ==================== Cancel & Flush ====================\n let controlValue = $state('')\n let controlResult = $state('')\n const controlDebounce = useDebounce({ delay: 2000 })\n\n function handleControlInput(e: Event) {\n controlValue = (e.currentTarget as HTMLInputElement).value\n controlDebounce.run(() => {\n controlResult = controlValue\n })\n }\n</script>\n\n<div class=\"space-y-8\">\n <div class=\"space-y-2\">\n <h1 class=\"text-2xl font-bold\">useDebounce</h1>\n <p class=\"text-on-surface-variant\">\n Reactive debounce hook. Delays execution until a pause in calls. Tracks pending state\n and supports cancel/flush.\n </p>\n </div>\n\n <!-- Basic Search -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Basic: Search</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Type in the input — the search only fires after 500ms of inactivity.\n </p>\n <div class=\"space-y-3 rounded-lg bg-surface-container-high p-4\">\n <Input\n value={searchQuery}\n oninput={handleSearch}\n placeholder=\"Search something...\"\n leadingIcon=\"lucide:search\"\n />\n <div class=\"flex flex-wrap items-center gap-3\">\n <Badge\n label={searchDebounce.pending ? 'Typing...' : 'Idle'}\n color={searchDebounce.pending ? 'warning' : 'surface'}\n variant=\"soft\"\n />\n <Badge label=\"Executed: {searchCount}x\" color=\"info\" variant=\"subtle\" />\n {#if searchResult}\n <span class=\"text-sm text-on-surface-variant\">\n Last search: <strong>{searchResult}</strong>\n </span>\n {/if}\n </div>\n </div>\n </section>\n\n <!-- Auto-save -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Auto-save</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Content auto-saves 1 second after you stop typing.\n </p>\n <div class=\"space-y-3 rounded-lg bg-surface-container-high p-4\">\n <textarea\n value={noteText}\n oninput={handleNoteInput}\n rows=\"3\"\n class=\"w-full rounded-md border border-outline-variant bg-surface px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/30\"\n ></textarea>\n <div class=\"flex items-center gap-3\">\n {#if saveDebounce.pending}\n <Badge label=\"Unsaved changes...\" color=\"warning\" variant=\"soft\" />\n {:else}\n <Badge\n label={saveCount > 0 ? 'Saved' : 'No changes'}\n color={saveCount > 0 ? 'success' : 'surface'}\n variant=\"soft\"\n />\n {/if}\n {#if savedText}\n <span class=\"text-xs text-on-surface-variant\">\n Last saved: {savedText.length} chars\n </span>\n {/if}\n </div>\n </div>\n </section>\n\n <!-- API Search -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Real World: API Search</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Debounced API call with loading state. Try typing \"alice\", \"bob\", or \"grace\".\n </p>\n <div class=\"space-y-3 rounded-lg bg-surface-container-high p-4\">\n <Input\n value={apiQuery}\n oninput={handleApiSearch}\n placeholder=\"Search users...\"\n leadingIcon=\"lucide:users\"\n loading={apiLoading}\n />\n\n {#if apiResults.length > 0}\n <div class=\"space-y-1\">\n {#each apiResults as user (user.id)}\n <div\n class=\"flex items-center gap-3 rounded-md bg-surface-container px-3 py-2\"\n >\n <Icon name=\"lucide:user\" size=\"16\" class=\"text-on-surface-variant\" />\n <span class=\"text-sm\">{user.name}</span>\n </div>\n {/each}\n </div>\n {:else if apiQuery && !apiLoading && !apiDebounce.pending}\n <p class=\"text-sm text-on-surface-variant\">No users found.</p>\n {/if}\n </div>\n </section>\n\n <!-- Cancel & Flush -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Cancel & Flush</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Delay is 2 seconds. Use <strong>Cancel</strong> to discard, or <strong>Flush</strong> to execute\n immediately.\n </p>\n <div class=\"space-y-3 rounded-lg bg-surface-container-high p-4\">\n <Input\n value={controlValue}\n oninput={handleControlInput}\n placeholder=\"Type and use buttons below...\"\n />\n <div class=\"flex flex-wrap items-center gap-3\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onclick={() => controlDebounce.cancel()}\n disabled={!controlDebounce.pending}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n variant=\"soft\"\n onclick={() => controlDebounce.flush(() => (controlResult = controlValue))}\n disabled={!controlDebounce.pending}\n >\n Flush Now\n </Button>\n <Badge\n label={controlDebounce.pending ? 'Pending (2s)...' : 'Idle'}\n color={controlDebounce.pending ? 'warning' : 'surface'}\n variant=\"soft\"\n />\n </div>\n {#if controlResult}\n <Card class=\"p-3\">\n <p class=\"text-sm\">\n Result: <strong>{controlResult}</strong>\n </p>\n </Card>\n {/if}\n </div>\n </section>\n</div>\n",
148
+ "use-debounced-state": "<script lang=\"ts\">\n import { useDebouncedState } from '$lib/index.js'\n import { Input, Badge, Card, Button } from '$lib/index.js'\n\n const fruits = [\n 'Apple',\n 'Banana',\n 'Blueberry',\n 'Cherry',\n 'Grape',\n 'Mango',\n 'Orange',\n 'Peach',\n 'Pear',\n 'Pineapple',\n 'Strawberry',\n 'Watermelon'\n ]\n\n const search = useDebouncedState('', 300)\n\n const filtered = $derived(\n search.debounced.trim() === ''\n ? fruits\n : fruits.filter((f) => f.toLowerCase().includes(search.debounced.trim().toLowerCase()))\n )\n</script>\n\n<div class=\"space-y-8\">\n <div>\n <h1 class=\"text-2xl font-bold text-on-surface\">useDebouncedState</h1>\n <p class=\"mt-1 text-on-surface-variant\">\n Reactive state whose <code>debounced</code> mirror lags behind <code>current</code> by a\n delay — write <code>current</code> from an input and derive from <code>debounced</code>,\n with no manual two-state wiring.\n </p>\n </div>\n\n <Card>\n <h2 class=\"mb-1 font-semibold text-on-surface\">Debounced filter (delay: 300ms)</h2>\n <p class=\"mb-4 text-sm text-on-surface-variant\">\n <code>current</code> updates on every keystroke; the list re-filters only once typing settles.\n </p>\n\n <Input placeholder=\"Filter fruits…\" bind:value={search.current} />\n\n <div class=\"mt-4 flex flex-wrap items-center gap-3\">\n <Badge color=\"secondary\">current: \"{search.current}\"</Badge>\n <Badge color=\"primary\">debounced: \"{search.debounced}\"</Badge>\n <Button size=\"sm\" variant=\"ghost\" onclick={() => search.setImmediate('')}>Clear</Button>\n </div>\n\n <ul class=\"mt-4 flex flex-wrap gap-2\">\n {#each filtered as fruit (fruit)}\n <li class=\"rounded-md bg-surface-container px-3 py-1 text-sm text-on-surface\">\n {fruit}\n </li>\n {:else}\n <li class=\"text-sm text-on-surface-variant\">No matches.</li>\n {/each}\n </ul>\n </Card>\n</div>\n",
149
+ "use-event-listener": "<script lang=\"ts\">\n import { useEventListener } from '$lib/index.js'\n import { Badge, Card, Icon } from '$lib/index.js'\n\n // ==================== Window resize (value/getter target) ====================\n let width = $state(0)\n let height = $state(0)\n\n $effect(() => {\n width = window.innerWidth\n height = window.innerHeight\n })\n\n useEventListener(\n () => window,\n 'resize',\n () => {\n width = window.innerWidth\n height = window.innerHeight\n }\n )\n\n // ==================== Window keydown (multiple-target demo) ====================\n let lastKey = $state('—')\n let keyCount = $state(0)\n\n useEventListener(\n () => window,\n 'keydown',\n (e) => {\n lastKey = e.key === ' ' ? 'Space' : e.key\n keyCount++\n }\n )\n\n // ==================== Element pointermove (reactive getter target) ====================\n let box = $state<HTMLElement>()\n let pos = $state({ x: 0, y: 0 })\n let inside = $state(false)\n\n useEventListener(\n () => box,\n 'pointermove',\n (e) => {\n const rect = box!.getBoundingClientRect()\n pos = { x: Math.round(e.clientX - rect.left), y: Math.round(e.clientY - rect.top) }\n }\n )\n useEventListener(\n () => box,\n ['pointerenter', 'pointerleave'],\n (e) => {\n inside = e.type === 'pointerenter'\n }\n )\n</script>\n\n<div class=\"space-y-8\">\n <div class=\"space-y-2\">\n <h1 class=\"text-2xl font-bold\">useEventListener</h1>\n <p class=\"text-on-surface-variant\">\n Attach event listener(s) to a target with automatic cleanup. The target may be a value\n or a reactive getter, accepts one or many event types, and forwards listener options.\n SSR-safe — a nullish target is a no-op.\n </p>\n </div>\n\n <!-- Window resize -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Window resize</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Listens to <code>resize</code> on <code>window</code>. Resize the browser window to see\n it update.\n </p>\n <div class=\"flex flex-wrap items-center gap-3 rounded-lg bg-surface-container-high p-4\">\n <Badge label=\"{width} × {height}\" color=\"primary\" variant=\"soft\" size=\"lg\" />\n <span class=\"text-sm text-on-surface-variant\">live viewport size</span>\n </div>\n </section>\n\n <!-- Keydown -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Keyboard</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Listens to <code>keydown</code> on <code>window</code>. Press any key.\n </p>\n <div class=\"flex flex-wrap items-center gap-3 rounded-lg bg-surface-container-high p-4\">\n <Badge label=\"Last key: {lastKey}\" color=\"info\" variant=\"soft\" />\n <Badge label=\"Pressed: {keyCount}x\" color=\"surface\" variant=\"subtle\" />\n </div>\n </section>\n\n <!-- Pointer position -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Pointer position (reactive element target)</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Listens to <code>pointermove</code> / <code>pointerenter</code> /\n <code>pointerleave</code> on an element resolved via a <code>() => box</code> getter, so the\n listener binds once the element mounts. Move your cursor inside the box.\n </p>\n <div\n bind:this={box}\n class=\"relative flex h-48 items-center justify-center rounded-lg border-2 border-dashed border-outline-variant bg-surface-container-high transition-colors\"\n class:border-primary={inside}\n >\n {#if inside}\n <div\n class=\"pointer-events-none absolute size-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary\"\n style=\"left: {pos.x}px; top: {pos.y}px;\"\n ></div>\n {/if}\n <Card class=\"p-3\">\n <div class=\"flex items-center gap-2 text-sm\">\n <Icon name=\"lucide:move\" size=\"16\" class=\"text-on-surface-variant\" />\n <span>x: <strong>{pos.x}</strong>, y: <strong>{pos.y}</strong></span>\n </div>\n </Card>\n </div>\n </section>\n</div>\n",
150
+ "use-resize-observer": "<script lang=\"ts\">\n import { useElementSize, useResizeObserver } from '$lib/index.js'\n import { Badge, Icon } from '$lib/index.js'\n\n // ==================== useElementSize ====================\n let box = $state<HTMLElement>()\n const size = useElementSize(() => box)\n\n // ==================== useResizeObserver (lower-level) ====================\n let box2 = $state<HTMLElement>()\n let breakpoint = $state('—')\n\n useResizeObserver(\n () => box2,\n ([entry]) => {\n const w = entry.contentRect.width\n breakpoint = w < 260 ? 'sm' : w < 420 ? 'md' : 'lg'\n }\n )\n\n const breakpointColor = $derived(\n breakpoint === 'sm' ? 'warning' : breakpoint === 'md' ? 'info' : 'success'\n )\n</script>\n\n<div class=\"space-y-8\">\n <div class=\"space-y-2\">\n <h1 class=\"text-2xl font-bold\">useResizeObserver / useElementSize</h1>\n <p class=\"text-on-surface-variant\">\n Observe an element's size with a <code>ResizeObserver</code> and automatic cleanup.\n <code>useElementSize</code> returns the reactive content-box size; the target may be a value\n or a reactive getter. SSR-safe.\n </p>\n </div>\n\n <!-- useElementSize -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">useElementSize</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Drag the bottom-right corner of the box — the size updates live.\n </p>\n <div class=\"rounded-lg bg-surface-container-high p-4\">\n <div\n bind:this={box}\n class=\"flex min-h-32 max-w-full min-w-48 resize items-center justify-center overflow-auto rounded-lg border-2 border-dashed border-outline-variant bg-surface p-4\"\n >\n <Badge\n label=\"{Math.round(size.width)} × {Math.round(size.height)}\"\n color=\"primary\"\n variant=\"soft\"\n size=\"lg\"\n />\n </div>\n </div>\n </section>\n\n <!-- useResizeObserver -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">useResizeObserver (lower-level callback)</h2>\n <p class=\"text-sm text-on-surface-variant\">\n The raw callback reads each entry's <code>contentRect</code>. Here it maps the width to\n a responsive breakpoint — resize horizontally to see it change.\n </p>\n <div class=\"rounded-lg bg-surface-container-high p-4\">\n <div\n bind:this={box2}\n class=\"flex min-h-24 max-w-full min-w-44 resize-x items-center justify-center gap-2 overflow-auto rounded-lg border-2 border-dashed border-outline-variant bg-surface p-4\"\n >\n <Icon name=\"lucide:scaling\" size=\"16\" class=\"text-on-surface-variant\" />\n <Badge label=\"breakpoint: {breakpoint}\" color={breakpointColor} variant=\"soft\" />\n </div>\n </div>\n </section>\n</div>\n",
151
+ "use-intersection-observer": "<script lang=\"ts\">\n import { useIntersectionObserver } from '$lib/index.js'\n import { Badge, Card, Icon } from '$lib/index.js'\n\n let target = $state<HTMLElement>()\n let enterCount = $state(0)\n let loaded = $state(false)\n\n const io = useIntersectionObserver(\n () => target,\n (entry) => {\n if (entry.isIntersecting) {\n enterCount++\n loaded = true\n }\n },\n { threshold: 0.25 }\n )\n</script>\n\n<div class=\"space-y-8\">\n <div>\n <h1 class=\"text-2xl font-bold text-on-surface\">useIntersectionObserver</h1>\n <p class=\"mt-1 text-on-surface-variant\">\n Observe an element's intersection with the viewport (or a root), with automatic cleanup.\n Ideal for lazy-loading, reveal-on-scroll, and visibility tracking.\n </p>\n </div>\n\n <Card>\n <h2 class=\"mb-1 font-semibold text-on-surface\">Live visibility</h2>\n <p class=\"mb-4 text-sm text-on-surface-variant\">\n This badge tracks the target card far below. Scroll down and watch it flip as the target\n enters and leaves the viewport.\n </p>\n <div class=\"flex flex-wrap items-center gap-3\">\n <Badge color={io.isIntersecting ? 'success' : 'secondary'}>\n {io.isIntersecting ? 'In view' : 'Out of view'}\n </Badge>\n <Badge color=\"primary\">Entered: {enterCount}×</Badge>\n </div>\n </Card>\n\n <div\n class=\"flex h-[70vh] items-center justify-center gap-2 text-on-surface-variant\"\n aria-hidden=\"true\"\n >\n <Icon name=\"lucide:arrow-down\" size=\"18\" />\n Scroll down to the target\n </div>\n\n <Card>\n <h2 class=\"mb-3 font-semibold text-on-surface\">Lazy target (threshold: 0.25)</h2>\n <div\n bind:this={target}\n class=\"flex h-32 items-center justify-center rounded-lg border border-dashed transition-colors duration-500\"\n class:border-success={loaded}\n class:bg-success-container={loaded}\n class:border-outline={!loaded}\n class:bg-surface-container-low={!loaded}\n >\n {#if loaded}\n <span class=\"flex items-center gap-2 text-on-success-container\">\n <Icon name=\"lucide:check\" size=\"20\" /> Loaded — became visible!\n </span>\n {:else}\n <span class=\"flex items-center gap-2\">\n <Icon name=\"lucide:loader\" size=\"20\" /> Waiting to enter view…\n </span>\n {/if}\n </div>\n </Card>\n</div>\n",
152
+ "use-scroll-lock": "<script lang=\"ts\">\n import { useScrollLock } from '$lib/index.js'\n import { Button, Badge, Icon } from '$lib/index.js'\n\n // ==================== Element target ====================\n let box = $state<HTMLElement>()\n let boxLocked = $state(false)\n useScrollLock(\n () => boxLocked,\n () => box\n )\n\n // ==================== Page (body) target ====================\n let pageLocked = $state(false)\n useScrollLock(() => pageLocked)\n\n const rows = Array.from({ length: 24 }, (_, i) => i + 1)\n</script>\n\n<div class=\"space-y-8\">\n <div class=\"space-y-2\">\n <h1 class=\"text-2xl font-bold\">useScrollLock</h1>\n <p class=\"text-on-surface-variant\">\n Lock scroll on an element (default <code>document.body</code>) while a reactive\n <code>locked</code> is true. Compensates for the scrollbar width to avoid layout jump, reference-counts\n nested locks, and restores the original styles on release. SSR-safe.\n </p>\n </div>\n\n <!-- Element target -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Lock an element</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Toggle to lock scrolling inside the box below. Try scrolling it while locked.\n </p>\n <div class=\"space-y-3 rounded-lg bg-surface-container-high p-4\">\n <div class=\"flex items-center gap-3\">\n <Button\n size=\"sm\"\n variant={boxLocked ? 'solid' : 'outline'}\n color={boxLocked ? 'error' : 'primary'}\n onclick={() => (boxLocked = !boxLocked)}\n >\n {boxLocked ? 'Unlock' : 'Lock'} box\n </Button>\n <Badge\n label={boxLocked ? 'Locked' : 'Scrollable'}\n color={boxLocked ? 'error' : 'success'}\n variant=\"soft\"\n />\n </div>\n <div\n bind:this={box}\n class=\"h-48 space-y-1 overflow-auto rounded-lg border border-outline-variant bg-surface p-3\"\n >\n {#each rows as n (n)}\n <div class=\"rounded-md bg-surface-container px-3 py-2 text-sm\">Row {n}</div>\n {/each}\n </div>\n </div>\n </section>\n\n <!-- Page target -->\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Lock the page (body)</h2>\n <p class=\"text-sm text-on-surface-variant\">\n The default target is <code>document.body</code> — this is what Modal / Slideover / Drawer\n use to freeze the page behind an overlay. Toggle and try scrolling the page.\n </p>\n <div class=\"flex flex-wrap items-center gap-3 rounded-lg bg-surface-container-high p-4\">\n <Button\n size=\"sm\"\n variant={pageLocked ? 'solid' : 'outline'}\n color={pageLocked ? 'error' : 'primary'}\n onclick={() => (pageLocked = !pageLocked)}\n >\n <Icon name={pageLocked ? 'lucide:lock' : 'lucide:lock-open'} size=\"16\" />\n {pageLocked ? 'Unlock' : 'Lock'} page\n </Button>\n <Badge\n label={pageLocked ? 'Page locked' : 'Page scrollable'}\n color={pageLocked ? 'error' : 'success'}\n variant=\"soft\"\n />\n </div>\n </section>\n</div>\n",
153
+ "use-focus-trap": "<script lang=\"ts\">\n import { useFocusTrap } from '$lib/index.js'\n import { Button, Badge, Input } from '$lib/index.js'\n\n let active = $state(false)\n let panel = $state<HTMLElement>()\n\n useFocusTrap(() => panel, { active: () => active })\n</script>\n\n<div class=\"space-y-8\">\n <div class=\"space-y-2\">\n <h1 class=\"text-2xl font-bold\">useFocusTrap</h1>\n <p class=\"text-on-surface-variant\">\n Trap keyboard focus within an element while active, then restore focus on exit. Cycles\n Tab / Shift+Tab among the focusable descendants. Target and <code>active</code> accept a value\n or reactive getter; SSR-safe.\n </p>\n <p class=\"text-sm text-on-surface-variant\">\n Note: components built on bits-ui (Modal, Slideover, Drawer, Popover) already trap focus\n — use this for your own custom focus-scoped UI.\n </p>\n </div>\n\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Toggle a focus trap</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Activate, then press <kbd class=\"rounded bg-surface-container-high px-1\">Tab</kbd> — focus\n stays inside the panel and wraps around. Closing returns focus to the Activate button.\n </p>\n <div class=\"space-y-3 rounded-lg bg-surface-container-high p-4\">\n <div class=\"flex items-center gap-3\">\n <Button\n variant={active ? 'soft' : 'solid'}\n disabled={active}\n onclick={() => (active = true)}\n >\n Activate trap\n </Button>\n <Badge\n label={active ? 'Trapped' : 'Free'}\n color={active ? 'success' : 'surface'}\n variant=\"soft\"\n />\n </div>\n\n <div\n bind:this={panel}\n class=\"space-y-3 rounded-lg border-2 border-dashed p-4 transition-colors {active\n ? 'border-primary'\n : 'border-outline-variant'}\"\n >\n <p class=\"text-sm font-medium\">Trapped panel</p>\n <Input placeholder=\"First field\" />\n <Input placeholder=\"Second field\" />\n <div class=\"flex gap-2\">\n <Button size=\"sm\" variant=\"outline\">Action</Button>\n <Button size=\"sm\" color=\"error\" variant=\"soft\" onclick={() => (active = false)}>\n Close\n </Button>\n </div>\n </div>\n\n <p class=\"text-xs text-on-surface-variant\">\n Try tabbing past the last button — it loops back to the first field.\n </p>\n </div>\n </section>\n</div>\n",
154
+ "use-local-storage": "<script lang=\"ts\">\n import { useLocalStorage } from '$lib/index.js'\n import { Button, Input, Badge } from '$lib/index.js'\n\n const note = useLocalStorage('svelora-demo-note', '')\n const count = useLocalStorage('svelora-demo-count', 0)\n</script>\n\n<div class=\"space-y-8\">\n <div class=\"space-y-2\">\n <h1 class=\"text-2xl font-bold\">useLocalStorage</h1>\n <p class=\"text-on-surface-variant\">\n Reactive <code>localStorage</code>-backed value. Reads on mount, writes through on\n change, and syncs across tabs via the <code>storage</code> event. SSR-safe; parse/quota errors\n are tolerated.\n </p>\n </div>\n\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Persisted text</h2>\n <p class=\"text-sm text-on-surface-variant\">\n Type below, then reload the page — the value persists. Open this page in a second tab to\n see live cross-tab sync.\n </p>\n <div class=\"space-y-2 rounded-lg bg-surface-container-high p-4\">\n <Input bind:value={note.current} placeholder=\"Persists across reloads & tabs...\" />\n <p class=\"text-xs text-on-surface-variant\">\n Stored under key <code>svelora-demo-note</code>.\n </p>\n </div>\n </section>\n\n <section class=\"space-y-3\">\n <h2 class=\"text-lg font-semibold\">Persisted counter</h2>\n <div class=\"flex flex-wrap items-center gap-3 rounded-lg bg-surface-container-high p-4\">\n <Button size=\"sm\" variant=\"outline\" onclick={() => (count.current -= 1)}>−</Button>\n <Badge label={count.current} color=\"primary\" variant=\"soft\" size=\"lg\" />\n <Button size=\"sm\" variant=\"outline\" onclick={() => (count.current += 1)}>+</Button>\n <Button size=\"sm\" variant=\"soft\" color=\"error\" onclick={() => (count.current = 0)}>\n Reset\n </Button>\n </div>\n </section>\n</div>\n",
155
+ "use-throttle": "<script lang=\"ts\">\n import { useThrottle } from '$lib/index.js'\n import { Button, Badge, Card, Icon, Input } from '$lib/index.js'\n\n // ==================== Throttled input ====================\n let keystrokes = $state(0)\n let queryUpdates = $state(0)\n let throttledQuery = $state('')\n const searchThrottle = useThrottle({ delay: 300 })\n\n function handleSearch(e: Event) {\n keystrokes++\n const value = (e.currentTarget as HTMLInputElement).value\n searchThrottle.run(() => {\n throttledQuery = value\n queryUpdates++\n })\n }\n\n // ==================== Mousemove rate ====================\n let rawMoves = $state(0)\n let throttledRuns = $state(0)\n const moveThrottle = useThrottle({ delay: 100 })\n\n function handleMove() {\n rawMoves++\n moveThrottle.run(() => throttledRuns++)\n }\n\n function resetMove() {\n rawMoves = 0\n throttledRuns = 0\n }\n\n // ==================== Rapid clicks (leading + trailing) ====================\n let log = $state<{ id: number; text: string }[]>([])\n let logId = 0\n const clickThrottle = useThrottle({ delay: 600 })\n\n function handleClick() {\n clickThrottle.run(() => {\n log = [\n { id: ++logId, text: `Fired at ${new Date().toLocaleTimeString()}` },\n ...log\n ].slice(0, 6)\n })\n }\n</script>\n\n<div class=\"space-y-8\">\n <div>\n <h1 class=\"text-2xl font-bold text-on-surface\">useThrottle</h1>\n <p class=\"mt-1 text-on-surface-variant\">\n Cap a callback to at most once per <code>delay</code>, with leading and trailing\n invocation. The companion to <code>useDebounce</code> — ideal for scroll, resize, mousemove,\n and drag handlers.\n </p>\n </div>\n\n <Card>\n <h2 class=\"mb-1 font-semibold text-on-surface\">Throttled input (delay: 300ms)</h2>\n <p class=\"mb-4 text-sm text-on-surface-variant\">\n Type quickly. Unlike <code>useDebounce</code> (which waits for a pause), throttle\n updates the query at a steady rate <em>while</em> you type — good for live filtering.\n </p>\n\n <Input placeholder=\"Type to search…\" oninput={handleSearch} />\n\n <div class=\"mt-4 flex flex-wrap items-center gap-4\">\n <Badge color=\"secondary\">Keystrokes: {keystrokes}</Badge>\n <Badge color=\"primary\">Query updates: {queryUpdates}</Badge>\n <span class=\"text-sm text-on-surface-variant\">\n Throttled value: <span class=\"font-medium text-on-surface\"\n >{throttledQuery || '—'}</span\n >\n </span>\n </div>\n </Card>\n\n <Card>\n <h2 class=\"mb-1 font-semibold text-on-surface\">Mousemove rate (delay: 100ms)</h2>\n <p class=\"mb-4 text-sm text-on-surface-variant\">\n Move your cursor across the box. Raw events fire on every pixel; the throttled callback\n runs at most ~10×/second.\n </p>\n\n <!-- svelte-ignore a11y_no_static_element_interactions -->\n <div\n onmousemove={handleMove}\n class=\"flex h-40 items-center justify-center rounded-lg border border-dashed border-outline bg-surface-container-low text-on-surface-variant\"\n >\n <Icon name=\"lucide:move\" size=\"20\" />\n <span class=\"ml-2\">Move here</span>\n </div>\n\n <div class=\"mt-4 flex items-center gap-4\">\n <Badge color=\"secondary\">Raw events: {rawMoves}</Badge>\n <Badge color=\"primary\">Throttled runs: {throttledRuns}</Badge>\n <Button size=\"sm\" variant=\"ghost\" onclick={resetMove}>Reset</Button>\n </div>\n </Card>\n\n <Card>\n <h2 class=\"mb-1 font-semibold text-on-surface\">Rapid clicks (delay: 600ms)</h2>\n <p class=\"mb-4 text-sm text-on-surface-variant\">\n Click fast: the first click fires immediately (leading), then bursts collapse into one\n trailing call per window.\n </p>\n\n <div class=\"flex items-center gap-3\">\n <Button color=\"primary\" onclick={handleClick}>Click me fast</Button>\n {#if clickThrottle.pending}\n <Badge color=\"warning\">trailing pending…</Badge>\n {/if}\n </div>\n\n {#if log.length}\n <ul class=\"mt-4 space-y-1 text-sm text-on-surface-variant\">\n {#each log as entry (entry.id)}\n <li class=\"flex items-center gap-2\">\n <Icon name=\"lucide:check\" size=\"14\" class=\"text-success\" />\n {entry.text}\n </li>\n {/each}\n </ul>\n {/if}\n </Card>\n</div>\n",
156
+ "use-timers": "<script lang=\"ts\">\n import { useInterval, useTimeout } from '$lib/index.js'\n import { Button, Badge, Card } from '$lib/index.js'\n\n // ==================== useInterval ====================\n let count = $state(0)\n const ticker = useInterval(() => count++, 1000)\n\n // ==================== useTimeout ====================\n let visible = $state(true)\n const dismiss = useTimeout(() => (visible = false), 3000)\n\n function show() {\n visible = true\n dismiss.restart()\n }\n</script>\n\n<div class=\"space-y-8\">\n <div>\n <h1 class=\"text-2xl font-bold text-on-surface\">useTimeout / useInterval</h1>\n <p class=\"mt-1 text-on-surface-variant\">\n Timers with proper runes teardown — cleared automatically on unmount, with a reactive\n delay, pause/resume, and restart/cancel.\n </p>\n </div>\n\n <Card>\n <h2 class=\"mb-1 font-semibold text-on-surface\">useInterval (1000ms)</h2>\n <p class=\"mb-4 text-sm text-on-surface-variant\">\n Ticks every second. Pause and resume without leaking the timer.\n </p>\n\n <div class=\"flex flex-wrap items-center gap-4\">\n <span class=\"text-3xl font-bold text-primary tabular-nums\">{count}</span>\n <Badge color={ticker.active ? 'success' : 'secondary'}>\n {ticker.active ? 'ticking' : 'paused'}\n </Badge>\n {#if ticker.active}\n <Button size=\"sm\" variant=\"ghost\" onclick={ticker.pause}>Pause</Button>\n {:else}\n <Button size=\"sm\" variant=\"ghost\" onclick={ticker.resume}>Resume</Button>\n {/if}\n <Button size=\"sm\" variant=\"ghost\" onclick={() => (count = 0)}>Reset</Button>\n </div>\n </Card>\n\n <Card>\n <h2 class=\"mb-1 font-semibold text-on-surface\">useTimeout (3000ms)</h2>\n <p class=\"mb-4 text-sm text-on-surface-variant\">\n The message auto-dismisses after 3 seconds. <code>restart()</code> brings it back;\n <code>cancel()</code> keeps it.\n </p>\n\n <div class=\"flex items-center gap-3\">\n {#if visible}\n <Badge color=\"info\">Visible — dismissing in 3s…</Badge>\n <Button size=\"sm\" variant=\"ghost\" onclick={dismiss.cancel}>Keep it</Button>\n {:else}\n <span class=\"text-sm text-on-surface-variant\">Dismissed.</span>\n <Button size=\"sm\" color=\"primary\" onclick={show}>Show again</Button>\n {/if}\n </div>\n </Card>\n</div>\n"
139
157
  }
140
158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelora",
3
- "version": "3.0.5",
3
+ "version": "3.0.6",
4
4
  "description": "Modern primitive-based UI component library for Svelte 5",
5
5
  "packageManager": "bun@1.3.14",
6
6
  "author": "asphum",