kenspeckle 0.1.4 → 0.2.1
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/LICENSE +13 -0
- package/README.md +84 -83
- package/dist/copy.d.ts +1 -3
- package/dist/copy.js +16 -21
- package/dist/debounce.svelte.d.ts +29 -0
- package/dist/debounce.svelte.js +105 -0
- package/dist/document-visible.d.ts +4 -0
- package/dist/document-visible.js +12 -0
- package/dist/finite-state-machine.svelte.js +8 -17
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/dist/kit/view-transition-name.d.ts +1 -3
- package/dist/kit/view-transition-name.js +11 -16
- package/dist/persisted-state.svelte.d.ts +9 -0
- package/dist/persisted-state.svelte.js +129 -0
- package/dist/scroll-edges.d.ts +10 -0
- package/dist/scroll-edges.js +88 -0
- package/dist/throttle.svelte.d.ts +6 -0
- package/dist/throttle.svelte.js +8 -0
- package/package.json +3 -2
package/LICENSE
CHANGED
|
@@ -19,3 +19,16 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Third-party notices
|
|
26
|
+
|
|
27
|
+
Portions of this software are adapted from the following projects, each
|
|
28
|
+
distributed under the MIT License above with its own copyright notice.
|
|
29
|
+
|
|
30
|
+
runed
|
|
31
|
+
Copyright (c) 2024 Hunter Johnston <https://github.com/huntabyte>
|
|
32
|
+
|
|
33
|
+
svelte-put
|
|
34
|
+
Copyright (c) 2023-2024 Quang Phan
|
package/README.md
CHANGED
|
@@ -3,116 +3,119 @@
|
|
|
3
3
|
<img src="docs/assets/banner-light.svg" alt="kenspeckle, adjective, Scots, from Old Norse kennispeki: 1. easily recognised; conspicuous; familiar at sight. 2. (of Svelte utilities) knowable from the name alone.">
|
|
4
4
|
</picture>
|
|
5
5
|
|
|
6
|
-
Svelte utilities you recognize at sight
|
|
6
|
+
Svelte utilities you recognize at sight: each one is shaped the way Svelte shapes its own APIs, so it reads like something Svelte might have shipped.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Shaped like Svelte
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
A utility makes the cut only if Svelte doesn't already handle it. Handled means a Svelte or Kit API covers it (a binding, `on()`, `onDestroy`, `<svelte:document>`, an `$effect` with a returned cleanup), the platform does (`field-sizing`, `:focus-within`), or a few lines of Svelte do, in which case it becomes a [recipe](https://kenspeckle.brandonpittman.workers.dev/docs/recipes) instead of an export.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
- "a new **element size**" — not a thing you ask for → `elementSize()`
|
|
14
|
-
- "a new **is idle**" — nonsense → `idle()`
|
|
12
|
+
What makes the cut takes the shape Svelte already uses for the same kind of thing. Ask these in order; the first yes decides:
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
1. **Is it global browser state?** A singleton with `current`, like `innerWidth` in `svelte/reactivity/window`: `documentVisible.current`. It reads the live value, and on the server it is `undefined`, as Svelte's are. Where a `<svelte:document>` or `<svelte:window>` binding already exists, the singleton earns its place only if modules need to read it outside a component: a polling store that pauses in a hidden tab does, and focus logic, which lives with its element, doesn't.
|
|
15
|
+
2. **Is it a value configured at construction?** A class with `current`, like `new MediaQuery(query)`: `new Idle(60_000)`, `new PersistedState('theme', 'light')`.
|
|
16
|
+
3. **Is it a value that follows another over time?** A class with `.of(fn)`, like `Spring.of(() => number)`, called in an effect root such as component init. It earns the class only if `current` holds something you couldn't produce where the value is written, the way a tween's in-between frames are. A debounced value and a previous value both fail this: each is a value already written, so `debounce()` or a saved copy at the write site covers it.
|
|
17
|
+
4. **Do you drive it with methods?** A class: `new FiniteStateMachine()`, `new StateHistory()`.
|
|
18
|
+
5. **Is it behavior on an element?** An attachment, named for what it does, as `copy` and `fade` are (`dragScroll`, `lockScroll`), or for the event it answers (`clickOutside`). An element value flows into your own `$state` through a callback, the way a function binding does (`bind:clientWidth={null, setWidth}`), never through a box the library owns, and it is named for that value, as a binding is (`scrollEdges`).
|
|
19
|
+
6. **Otherwise**, a plain function, with no `use` prefix, which is a React Hooks rule Svelte doesn't have.
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
2. **Everything else is a camelCase function.** No `use` prefix — that's a React Hooks rule, and Svelte has no such rule. No `is` prefix either — predicates are bare words read as implicit questions: `mounted()`, `idle()`, `documentVisible()`. The question lives in the library, the answer in your variable: `const isMounted = mounted()`.
|
|
21
|
+
Each shape names the Svelte API it mirrors. An export that can't name one isn't shaped yet.
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
## Attachments, one form
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Element-bound utilities are one name in two forms. Options only → returns an [attachment](https://svelte.dev/docs/svelte/@attach). Element first → the imperative helper, cleanup returned.
|
|
25
|
+
An element-bound utility returns an [attachment](https://svelte.dev/docs/svelte/@attach) and has no second form. An attachment is a function of the element, so outside a template you call it on one and get its cleanup back.
|
|
26
26
|
|
|
27
27
|
```svelte
|
|
28
28
|
<script>
|
|
29
|
-
//
|
|
30
|
-
const cleanup = clickOutside(
|
|
29
|
+
// outside a template
|
|
30
|
+
const cleanup = clickOutside(() => close())(node);
|
|
31
31
|
</script>
|
|
32
32
|
|
|
33
33
|
<div {@attach clickOutside(() => close())}>…</div>
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
An attachment that observes the element hands what it sees to a callback, so the value lives in your state:
|
|
37
37
|
|
|
38
38
|
```svelte
|
|
39
39
|
<script>
|
|
40
|
-
import {
|
|
40
|
+
import { scrollEdges } from 'kenspeckle';
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
let atBottom = $state(false);
|
|
43
43
|
</script>
|
|
44
44
|
|
|
45
|
-
<div {@attach
|
|
45
|
+
<div {@attach scrollEdges((edges) => (atBottom = edges.blockEnd))}>…</div>
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
The same export works imperatively with a getter: `elementSize(() => node)`. Argument presence picks the mode.
|
|
49
|
-
|
|
50
48
|
## Install
|
|
51
49
|
|
|
52
50
|
```sh
|
|
53
51
|
npm install kenspeckle
|
|
54
52
|
```
|
|
55
53
|
|
|
56
|
-
Peer dependencies: `svelte >= 5.40`, plus `@sveltejs/kit >= 2` for the `kenspeckle/kit` subpath (optional). SSR-safe —
|
|
54
|
+
Peer dependencies: `svelte >= 5.40`, plus `@sveltejs/kit >= 2` for the `kenspeckle/kit` subpath (optional). SSR-safe — singletons read `undefined` on the server, as Svelte's window values do.
|
|
57
55
|
|
|
58
56
|
## What's shipped
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
Ten exports, two entry points, each documented under [the docs site](https://kenspeckle.brandonpittman.workers.dev/docs). `kenspeckle` is Svelte-only; `kenspeckle/kit` adds the SvelteKit
|
|
61
59
|
pieces, so `$app/navigation` never enters the main entry.
|
|
62
60
|
|
|
63
|
-
| export
|
|
64
|
-
|
|
|
65
|
-
| `FiniteStateMachine`
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
|
|
71
|
-
`
|
|
61
|
+
| export | form |
|
|
62
|
+
| ------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
|
63
|
+
| [`FiniteStateMachine`](https://kenspeckle.brandonpittman.workers.dev/docs/finite-state-machine) | class, with typed reactive `context` |
|
|
64
|
+
| [`PersistedState`](https://kenspeckle.brandonpittman.workers.dev/docs/persisted-state) | class, `.current` |
|
|
65
|
+
| [`documentVisible`](https://kenspeckle.brandonpittman.workers.dev/docs/document-visible) | singleton, `.current` |
|
|
66
|
+
| [`copy()` / `copyText()`](https://kenspeckle.brandonpittman.workers.dev/docs/copy) | attachment |
|
|
67
|
+
| [`scrollEdges()`](https://kenspeckle.brandonpittman.workers.dev/docs/scroll-edges) | attachment, edges to a callback |
|
|
68
|
+
| [`viewTransition()`](https://kenspeckle.brandonpittman.workers.dev/docs/view-transition) | function; navigation form under `/kit` |
|
|
69
|
+
| [`viewTransitionName()`](https://kenspeckle.brandonpittman.workers.dev/docs/view-transition-name) | attachment, `/kit` |
|
|
70
|
+
| [`retreat()`](https://kenspeckle.brandonpittman.workers.dev/docs/view-transition) | function, returns a disposer, `/kit` |
|
|
71
|
+
| [`debounce()`](https://kenspeckle.brandonpittman.workers.dev/docs/debounce) | function wrapper |
|
|
72
|
+
| [`throttle()`](https://kenspeckle.brandonpittman.workers.dev/docs/throttle) | function wrapper |
|
|
73
|
+
|
|
74
|
+
`FiniteStateMachine` has a typed, `$state`-backed `context` object visible to lifecycle hooks and
|
|
72
75
|
guards — the sidecar-data mechanism every real FSM grows, built in.
|
|
73
76
|
|
|
74
77
|
View transitions are what the package is for so far; they get their own section below.
|
|
75
78
|
|
|
76
79
|
## Planned
|
|
77
80
|
|
|
78
|
-
The rest of the
|
|
81
|
+
The rest of the plan. These names are the design, not an API you can call — nothing below has
|
|
79
82
|
shipped.
|
|
80
83
|
|
|
81
|
-
| export
|
|
82
|
-
|
|
|
83
|
-
| `StateHistory`
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
cover the server-data case
|
|
84
|
+
| export | form |
|
|
85
|
+
| --------------------- | ------------------------------------------------------------------------------------------------- |
|
|
86
|
+
| `StateHistory` | class: `new StateHistory(get, set)`, `.undo()`, `.redo()` |
|
|
87
|
+
| `Idle` | class: `new Idle(ms)`, `.current` |
|
|
88
|
+
| `AsyncPersistedState` | class over IndexedDB: `await` it, or read `.current`, `.loading`, `.error`, like a remote `query` |
|
|
89
|
+
| `CurrentPosition` | class: `new CurrentPosition(options)`, `.current` |
|
|
90
|
+
| `pressedKeys` | singleton, read like a set: `.has(key)` |
|
|
91
|
+
| `clickOutside()` | attachment |
|
|
92
|
+
| `shortcut()` | attachment |
|
|
93
|
+
| `lockScroll` | attachment ≡ helper, stacked lock counting |
|
|
94
|
+
| `dragScroll()` | attachment |
|
|
95
|
+
|
|
96
|
+
Dropped, deliberately, because Svelte, Kit or the platform already handles it:
|
|
97
|
+
|
|
98
|
+
- `Context`: Svelte ≥ 5.40's `createContext` returns a typed `[get, set]` pair.
|
|
99
|
+
- `Debounced` / `Throttled`: `debounce()` / `throttle()` writing into a `$state`, with any effect that follows a source kept in the component that owns it.
|
|
100
|
+
- `elementSize`, `elementRect`, `resized`: the dimension bindings (`bind:clientWidth`, `bind:contentRect`, `bind:borderBoxSize`) already measure with a `ResizeObserver`.
|
|
101
|
+
- `autosize`: CSS `field-sizing: content`, Baseline since June 2026.
|
|
102
|
+
- `intersected`, `inViewport`: an inline attachment that creates an `IntersectionObserver` and returns its `disconnect`.
|
|
103
|
+
- `mutated`: the same inline attachment around a `MutationObserver`.
|
|
104
|
+
- `focusWithin`: CSS `:focus-within` for styling, and `onfocusin` / `onfocusout` with `relatedTarget` for a value.
|
|
105
|
+
- `scrollState`, except its edges: `onscroll` gives an element's position, a comparison with the last value gives direction, and `<svelte:window bind:scrollY>` covers the window.
|
|
106
|
+
- `activeElement`: `<svelte:document bind:activeElement />`, since focus logic lives in the component that holds the element.
|
|
107
|
+
- `Previous`: save the old value where you write the new one (`previous = count; count = next`); for a prop you don't write, an `$effect.pre` does it.
|
|
108
|
+
- `mounted`: `onMount` setting a `$state`.
|
|
109
|
+
- `listen`: `on()` from `svelte/events`.
|
|
110
|
+
- `interval`, `animationFrames`: an `$effect` that starts the timer or loop and returns its cleanup.
|
|
111
|
+
- `onCleanup`: `onDestroy`, or an effect's returned cleanup.
|
|
112
|
+
- `watch` / `watchOnce`: an effect responds to state being a certain way, not to it changing; handle the change where it happens.
|
|
113
|
+
- `extract`: nothing here takes a value-or-getter.
|
|
114
|
+
- `boolAttr`: Svelte omits nullish attributes, so `data-x={flag || undefined}`.
|
|
115
|
+
|
|
116
|
+
The [recipes](https://kenspeckle.brandonpittman.workers.dev/docs/recipes) page spells out the replacements for `autosize`, `intersected`, `mutated`, `focusWithin` and `Previous`.
|
|
117
|
+
|
|
118
|
+
Waiting in the backlog: `resource`, since SvelteKit remote functions cover the server-data case, and `searchParams`, since Kit's `page.url.searchParams` and `goto` cover reads and writes until a typed layer earns its place.
|
|
116
119
|
|
|
117
120
|
## View transitions
|
|
118
121
|
|
|
@@ -195,11 +198,11 @@ scoped rather than left in static CSS.
|
|
|
195
198
|
```
|
|
196
199
|
|
|
197
200
|
`when` claims only for navigations it accepts; `onArrival` claims inside the update callback instead,
|
|
198
|
-
for an element that mounts during the transition.
|
|
199
|
-
|
|
201
|
+
for an element that mounts during the transition. Outside a template, call the attachment on the
|
|
202
|
+
element to get its disposer:
|
|
200
203
|
|
|
201
204
|
```js
|
|
202
|
-
const dispose = viewTransitionName(
|
|
205
|
+
const dispose = viewTransitionName('back-button')(element);
|
|
203
206
|
```
|
|
204
207
|
|
|
205
208
|
**Claims are navigation-only.** The registration form drives the capture and arrival phases;
|
|
@@ -208,18 +211,16 @@ const dispose = viewTransitionName(element, 'back-button');
|
|
|
208
211
|
|
|
209
212
|
## Claude Code plugin
|
|
210
213
|
|
|
211
|
-
This repo doubles as a Claude Code plugin
|
|
212
|
-
|
|
213
|
-
contract, `viewTransitionName` claims and their phases,
|
|
214
|
-
abort, and the list of things the library already handles so
|
|
215
|
-
reimplementing them.
|
|
214
|
+
This repo doubles as a Claude Code plugin with two agent-facing guides.
|
|
215
|
+
`skills/view-transitions/` covers the view-transition tier — the
|
|
216
|
+
`data-view-transition` contract, `viewTransitionName` claims and their phases,
|
|
217
|
+
the duplicate-name abort, and the list of things the library already handles so
|
|
218
|
+
an agent stops reimplementing them. `skills/finite-state-machine/` covers
|
|
219
|
+
`FiniteStateMachine` — the always-transitions rule, the `'*'` re-entry hazard,
|
|
220
|
+
lifecycle ordering and context. Point a marketplace entry at this repo
|
|
216
221
|
(`.claude-plugin/plugin.json` is at the root) and it installs as `kenspeckle`,
|
|
217
|
-
invoked as `/kenspeckle:view-transitions`.
|
|
222
|
+
invoked as `/kenspeckle:view-transitions` and `/kenspeckle:finite-state-machine`.
|
|
223
|
+
It bundles no MCP server; for the Svelte MCP, install the official `svelte`
|
|
224
|
+
plugin from [`sveltejs/ai-tools`](https://github.com/sveltejs/ai-tools).
|
|
218
225
|
|
|
219
226
|
The skill and the code ship in the same change, same rule as the docs pages.
|
|
220
|
-
|
|
221
|
-
## Provenance
|
|
222
|
-
|
|
223
|
-
kenspeckle is a curation, not a fork. [runed](https://runed.dev) has genuinely useful utilities under three naming regimes at once — PascalCase classes, bare functions, and React-style `use` hooks, with pairs like `Debounced` / `useDebounce` coexisting. [svelte-put](https://svelte-put.vnphanquang.com) has good actions from the Svelte 4 era, before attachments existed. kenspeckle reshapes both pools under one convention and adds what neither ships: attachments as a first-class layer.
|
|
224
|
-
|
|
225
|
-
Portions adapted from runed (MIT) and svelte-put (MIT). kenspeckle is MIT.
|
package/dist/copy.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Attachment } from 'svelte/attachments';
|
|
2
1
|
export type CopyText = string | ((node: HTMLElement) => string | Promise<string>);
|
|
3
2
|
export interface CopyOptions<K extends keyof HTMLElementEventMap = 'click'> {
|
|
4
3
|
text?: CopyText;
|
|
@@ -7,5 +6,4 @@ export interface CopyOptions<K extends keyof HTMLElementEventMap = 'click'> {
|
|
|
7
6
|
onCopy?: (text: string) => void;
|
|
8
7
|
}
|
|
9
8
|
export declare function copyText(text: string): Promise<void>;
|
|
10
|
-
export declare function copy<K extends keyof HTMLElementEventMap = 'click'>(options?: CopyOptions<K>):
|
|
11
|
-
export declare function copy<K extends keyof HTMLElementEventMap = 'click'>(node: HTMLElement, options?: CopyOptions<K>): () => void;
|
|
9
|
+
export declare function copy<K extends keyof HTMLElementEventMap = 'click'>(options?: CopyOptions<K>): (node: HTMLElement) => () => void;
|
package/dist/copy.js
CHANGED
|
@@ -15,26 +15,21 @@ export async function copyText(text) {
|
|
|
15
15
|
document.body.removeChild(el);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
export function copy(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
const offs = events.map((e) => on(node, e, handler));
|
|
36
|
-
return () => {
|
|
37
|
-
for (const off of offs)
|
|
38
|
-
off();
|
|
18
|
+
export function copy(options = {}) {
|
|
19
|
+
return (node) => {
|
|
20
|
+
const { text, event, enabled = true, onCopy } = options;
|
|
21
|
+
if (!enabled)
|
|
22
|
+
return () => { };
|
|
23
|
+
const events = (event === undefined ? ['click'] : Array.isArray(event) ? event : [event]);
|
|
24
|
+
async function handler() {
|
|
25
|
+
const resolved = typeof text === 'function' ? await text(node) : (text ?? node.textContent ?? '');
|
|
26
|
+
await copyText(resolved);
|
|
27
|
+
onCopy?.(resolved);
|
|
28
|
+
}
|
|
29
|
+
const offs = events.map((e) => on(node, e, handler));
|
|
30
|
+
return () => {
|
|
31
|
+
for (const off of offs)
|
|
32
|
+
off();
|
|
33
|
+
};
|
|
39
34
|
};
|
|
40
35
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type DebouncedFunction<Args extends unknown[], Return> = {
|
|
2
|
+
(...args: Args): Promise<Awaited<Return>>;
|
|
3
|
+
readonly pending: boolean;
|
|
4
|
+
flush(): void;
|
|
5
|
+
cancel(): void;
|
|
6
|
+
};
|
|
7
|
+
export declare const DEFAULT_WAIT = 250;
|
|
8
|
+
/**
|
|
9
|
+
* `wait` is a getter so `FiniteStateMachine.debounce` can take a different wait on each call.
|
|
10
|
+
* `leading` runs a call at once when no window is open, and the window it opens is fixed: later
|
|
11
|
+
* calls wait for it to end instead of restarting it.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Scheduler<Args extends unknown[], Return> {
|
|
14
|
+
#private;
|
|
15
|
+
/**
|
|
16
|
+
* `reactive: false` keeps `pending` out of Svelte's graph, so a `$derived` or template expression
|
|
17
|
+
* can call the scheduler: writing `$state` there throws `state_unsafe_mutation`.
|
|
18
|
+
*/
|
|
19
|
+
constructor(callback: (...args: Args) => Return, wait: () => number, { leading, reactive }?: {
|
|
20
|
+
leading?: boolean;
|
|
21
|
+
reactive?: boolean;
|
|
22
|
+
});
|
|
23
|
+
get pending(): boolean;
|
|
24
|
+
call(...args: Args): Promise<Awaited<Return>>;
|
|
25
|
+
flush(): void;
|
|
26
|
+
/** The promise is left unsettled: rejecting would be unhandled in every fire-and-forget caller. */
|
|
27
|
+
cancel(): void;
|
|
28
|
+
}
|
|
29
|
+
export declare function debounce<Args extends unknown[], Return>(callback: (...args: Args) => Return, wait?: number): DebouncedFunction<Args, Return>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export const DEFAULT_WAIT = 250;
|
|
2
|
+
/** `Promise.withResolvers` would raise the runtime floor to Safari 17.4 and Node 22. */
|
|
3
|
+
function withResolvers() {
|
|
4
|
+
let resolve;
|
|
5
|
+
let reject;
|
|
6
|
+
const promise = new Promise((res, rej) => {
|
|
7
|
+
resolve = res;
|
|
8
|
+
reject = rej;
|
|
9
|
+
});
|
|
10
|
+
return { promise, resolve, reject };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `wait` is a getter so `FiniteStateMachine.debounce` can take a different wait on each call.
|
|
14
|
+
* `leading` runs a call at once when no window is open, and the window it opens is fixed: later
|
|
15
|
+
* calls wait for it to end instead of restarting it.
|
|
16
|
+
*/
|
|
17
|
+
export class Scheduler {
|
|
18
|
+
#callback;
|
|
19
|
+
#wait;
|
|
20
|
+
#leading;
|
|
21
|
+
#timer;
|
|
22
|
+
/** Plain, so a call made inside an `$effect` doesn't make the effect depend on it. */
|
|
23
|
+
#scheduled;
|
|
24
|
+
#reactive;
|
|
25
|
+
#tracked = $state(false);
|
|
26
|
+
#untracked = false;
|
|
27
|
+
/**
|
|
28
|
+
* `reactive: false` keeps `pending` out of Svelte's graph, so a `$derived` or template expression
|
|
29
|
+
* can call the scheduler: writing `$state` there throws `state_unsafe_mutation`.
|
|
30
|
+
*/
|
|
31
|
+
constructor(callback, wait, { leading = false, reactive = true } = {}) {
|
|
32
|
+
this.#callback = callback;
|
|
33
|
+
this.#wait = wait;
|
|
34
|
+
this.#leading = leading;
|
|
35
|
+
this.#reactive = reactive;
|
|
36
|
+
}
|
|
37
|
+
get pending() {
|
|
38
|
+
return this.#pending;
|
|
39
|
+
}
|
|
40
|
+
get #pending() {
|
|
41
|
+
return this.#reactive ? this.#tracked : this.#untracked;
|
|
42
|
+
}
|
|
43
|
+
set #pending(value) {
|
|
44
|
+
if (this.#reactive)
|
|
45
|
+
this.#tracked = value;
|
|
46
|
+
else
|
|
47
|
+
this.#untracked = value;
|
|
48
|
+
}
|
|
49
|
+
call(...args) {
|
|
50
|
+
if (this.#leading && this.#timer === undefined) {
|
|
51
|
+
const deferred = withResolvers();
|
|
52
|
+
this.#start();
|
|
53
|
+
this.#run({ args, deferred });
|
|
54
|
+
return deferred.promise;
|
|
55
|
+
}
|
|
56
|
+
const deferred = this.#scheduled?.deferred ?? withResolvers();
|
|
57
|
+
this.#scheduled = { args, deferred };
|
|
58
|
+
this.#pending = true;
|
|
59
|
+
if (!this.#leading)
|
|
60
|
+
this.#start();
|
|
61
|
+
return deferred.promise;
|
|
62
|
+
}
|
|
63
|
+
flush() {
|
|
64
|
+
clearTimeout(this.#timer);
|
|
65
|
+
this.#elapse();
|
|
66
|
+
}
|
|
67
|
+
/** The promise is left unsettled: rejecting would be unhandled in every fire-and-forget caller. */
|
|
68
|
+
cancel() {
|
|
69
|
+
clearTimeout(this.#timer);
|
|
70
|
+
this.#timer = undefined;
|
|
71
|
+
this.#scheduled = undefined;
|
|
72
|
+
this.#pending = false;
|
|
73
|
+
}
|
|
74
|
+
#start() {
|
|
75
|
+
clearTimeout(this.#timer);
|
|
76
|
+
this.#timer = setTimeout(() => this.#elapse(), this.#wait());
|
|
77
|
+
}
|
|
78
|
+
#elapse() {
|
|
79
|
+
this.#timer = undefined;
|
|
80
|
+
const scheduled = this.#scheduled;
|
|
81
|
+
if (scheduled === undefined)
|
|
82
|
+
return;
|
|
83
|
+
this.#scheduled = undefined;
|
|
84
|
+
this.#pending = false;
|
|
85
|
+
if (this.#leading)
|
|
86
|
+
this.#start();
|
|
87
|
+
this.#run(scheduled);
|
|
88
|
+
}
|
|
89
|
+
#run({ args, deferred }) {
|
|
90
|
+
try {
|
|
91
|
+
deferred.resolve(this.#callback(...args));
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
deferred.reject(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function debounce(callback, wait = DEFAULT_WAIT) {
|
|
99
|
+
const scheduler = new Scheduler(callback, () => wait);
|
|
100
|
+
return Object.defineProperties((...args) => scheduler.call(...args), {
|
|
101
|
+
pending: { get: () => scheduler.pending },
|
|
102
|
+
flush: { value: () => scheduler.flush() },
|
|
103
|
+
cancel: { value: () => scheduler.cancel() }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BROWSER } from 'esm-env';
|
|
2
|
+
import { on } from 'svelte/events';
|
|
3
|
+
import { createSubscriber } from 'svelte/reactivity';
|
|
4
|
+
const subscribe = createSubscriber((update) => on(document, 'visibilitychange', update));
|
|
5
|
+
export const documentVisible = {
|
|
6
|
+
get current() {
|
|
7
|
+
if (!BROWSER)
|
|
8
|
+
return undefined;
|
|
9
|
+
subscribe();
|
|
10
|
+
return document.visibilityState === 'visible';
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEV } from 'esm-env';
|
|
2
|
+
import { DEFAULT_WAIT, Scheduler } from './debounce.svelte.js';
|
|
2
3
|
export class FiniteStateMachine {
|
|
3
4
|
#current = $state();
|
|
4
5
|
#context = $state();
|
|
@@ -35,24 +36,14 @@ export class FiniteStateMachine {
|
|
|
35
36
|
this.#transition(target, event, args);
|
|
36
37
|
return this.#current;
|
|
37
38
|
};
|
|
38
|
-
#
|
|
39
|
-
debounce = (event, wait =
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const resolvers = pending?.resolvers ?? [];
|
|
44
|
-
return new Promise((resolve) => {
|
|
45
|
-
resolvers.push(resolve);
|
|
46
|
-
this.#timeouts[event] = {
|
|
47
|
-
id: setTimeout(() => {
|
|
48
|
-
delete this.#timeouts[event];
|
|
49
|
-
const state = this.send(event, ...args);
|
|
50
|
-
for (const r of resolvers)
|
|
51
|
-
r(state);
|
|
52
|
-
}, wait),
|
|
53
|
-
resolvers
|
|
54
|
-
};
|
|
39
|
+
#debounced = {};
|
|
40
|
+
debounce = (event, wait = DEFAULT_WAIT, ...args) => {
|
|
41
|
+
const debounced = (this.#debounced[event] ??= {
|
|
42
|
+
wait,
|
|
43
|
+
scheduler: new Scheduler((...args) => this.send(event, ...args), () => debounced.wait, { reactive: false })
|
|
55
44
|
});
|
|
45
|
+
debounced.wait = wait;
|
|
46
|
+
return debounced.scheduler.call(...args);
|
|
56
47
|
};
|
|
57
48
|
#transition(to, event, args) {
|
|
58
49
|
const from = this.#current;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export { copy, copyText } from './copy.js';
|
|
2
2
|
export type { CopyOptions, CopyText } from './copy.js';
|
|
3
|
+
export { debounce } from './debounce.svelte.js';
|
|
4
|
+
export type { DebouncedFunction } from './debounce.svelte.js';
|
|
5
|
+
export { documentVisible } from './document-visible.js';
|
|
6
|
+
export type { DocumentVisible } from './document-visible.js';
|
|
3
7
|
export { FiniteStateMachine } from './finite-state-machine.svelte.js';
|
|
4
8
|
export type { Action, ActionMeta, EnterMeta, EventMap, ExitMeta, StateHandler, Transition } from './finite-state-machine.svelte.js';
|
|
9
|
+
export { PersistedState } from './persisted-state.svelte.js';
|
|
10
|
+
export type { PersistedStateOptions } from './persisted-state.svelte.js';
|
|
11
|
+
export { scrollEdges } from './scroll-edges.js';
|
|
12
|
+
export type { ScrollEdges, ScrollEdgesOptions } from './scroll-edges.js';
|
|
13
|
+
export { throttle } from './throttle.svelte.js';
|
|
14
|
+
export type { ThrottledFunction } from './throttle.svelte.js';
|
|
5
15
|
export { viewTransition } from './view-transition.js';
|
|
6
16
|
export type { StartViewTransition, ViewTransitionOptions, ViewTransitionUpdate } from './view-transition.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export { copy, copyText } from './copy.js';
|
|
2
|
+
export { debounce } from './debounce.svelte.js';
|
|
3
|
+
export { documentVisible } from './document-visible.js';
|
|
2
4
|
export { FiniteStateMachine } from './finite-state-machine.svelte.js';
|
|
5
|
+
export { PersistedState } from './persisted-state.svelte.js';
|
|
6
|
+
export { scrollEdges } from './scroll-edges.js';
|
|
7
|
+
export { throttle } from './throttle.svelte.js';
|
|
3
8
|
export { viewTransition } from './view-transition.js';
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Attachment } from 'svelte/attachments';
|
|
2
1
|
import type { Navigation } from './types.js';
|
|
3
2
|
export interface ViewTransitionNameOptions {
|
|
4
3
|
/** Only claim the name for navigations this returns true for. */
|
|
@@ -11,6 +10,5 @@ type Phase = 'capture' | 'arrival';
|
|
|
11
10
|
export declare function beginPhase(navigation: Navigation, phase: Phase): void;
|
|
12
11
|
/** @internal */
|
|
13
12
|
export declare function endTransition(): void;
|
|
14
|
-
export declare function viewTransitionName(name: string, options?: ViewTransitionNameOptions):
|
|
15
|
-
export declare function viewTransitionName(element: HTMLElement, name: string, options?: ViewTransitionNameOptions): () => void;
|
|
13
|
+
export declare function viewTransitionName(name: string, options?: ViewTransitionNameOptions): (element: HTMLElement) => () => void;
|
|
16
14
|
export {};
|
|
@@ -28,21 +28,16 @@ export function endTransition() {
|
|
|
28
28
|
for (const claim of claims)
|
|
29
29
|
release(claim);
|
|
30
30
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
export function viewTransitionName(name, options = {}) {
|
|
32
|
+
return (element) => {
|
|
33
|
+
const claim = { element, name, options, named: false };
|
|
34
|
+
claims.add(claim);
|
|
35
|
+
/** An element mounting mid-transition registers after its phase already swept. */
|
|
36
|
+
if (current && wanted(claim, current.phase, current.navigation))
|
|
37
|
+
apply(claim);
|
|
38
|
+
return () => {
|
|
39
|
+
release(claim);
|
|
40
|
+
claims.delete(claim);
|
|
41
|
+
};
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
|
-
export function viewTransitionName(first, second, third) {
|
|
43
|
-
if (typeof Element !== 'undefined' && first instanceof Element) {
|
|
44
|
-
return wire(first, second, third);
|
|
45
|
-
}
|
|
46
|
-
const options = second;
|
|
47
|
-
return (element) => wire(element, first, options);
|
|
48
|
-
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { parse, stringify } from 'devalue';
|
|
2
|
+
import { BROWSER, DEV } from 'esm-env';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
4
|
+
import { on } from 'svelte/events';
|
|
5
|
+
import { createSubscriber } from 'svelte/reactivity';
|
|
6
|
+
const probe = Symbol();
|
|
7
|
+
/**
|
|
8
|
+
* Freezing a `$state` proxy throws only after its target is already non-extensible, which breaks the
|
|
9
|
+
* caller's state. The proxy refuses a non-enumerable property before touching its target, so this
|
|
10
|
+
* probe tells one apart without changing anything.
|
|
11
|
+
*/
|
|
12
|
+
function isStateProxy(value) {
|
|
13
|
+
try {
|
|
14
|
+
const descriptor = { value: undefined, enumerable: false, configurable: true, writable: true };
|
|
15
|
+
if (Reflect.defineProperty(value, probe, descriptor))
|
|
16
|
+
Reflect.deleteProperty(value, probe);
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function freeze(value) {
|
|
24
|
+
if (!DEV || value === null || typeof value !== 'object')
|
|
25
|
+
return value;
|
|
26
|
+
const prototype = Object.getPrototypeOf(value);
|
|
27
|
+
if ((Array.isArray(value) || prototype === Object.prototype || prototype === null) &&
|
|
28
|
+
!isStateProxy(value)) {
|
|
29
|
+
Object.freeze(value);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
export class PersistedState {
|
|
34
|
+
#key;
|
|
35
|
+
#initial;
|
|
36
|
+
#area;
|
|
37
|
+
#value;
|
|
38
|
+
/** `undefined` until the first read, so a stored `null` (an empty key) still counts as seen. */
|
|
39
|
+
#seen;
|
|
40
|
+
/** Wakes readers after an own write, including one that never reached storage. */
|
|
41
|
+
#version = $state.raw(0);
|
|
42
|
+
/** Set while `#value` holds a write storage refused, so any later write to the key replaces it. */
|
|
43
|
+
#unsaved = false;
|
|
44
|
+
#subscribe = createSubscriber((update) => on(window, 'storage', (event) => {
|
|
45
|
+
if (event.storageArea !== this.#storage())
|
|
46
|
+
return;
|
|
47
|
+
if (event.key !== null && event.key !== this.#key)
|
|
48
|
+
return;
|
|
49
|
+
/**
|
|
50
|
+
* The write may have stored the very string `#seen` captured at the failure, which the getter
|
|
51
|
+
* cannot tell apart from no write at all.
|
|
52
|
+
*/
|
|
53
|
+
if (this.#unsaved) {
|
|
54
|
+
this.#unsaved = false;
|
|
55
|
+
this.#seen = undefined;
|
|
56
|
+
}
|
|
57
|
+
update();
|
|
58
|
+
}));
|
|
59
|
+
constructor(key, initial, options = {}) {
|
|
60
|
+
this.#key = key;
|
|
61
|
+
this.#initial = freeze(initial);
|
|
62
|
+
this.#area = `${options.storage ?? 'local'}Storage`;
|
|
63
|
+
this.#value = initial;
|
|
64
|
+
}
|
|
65
|
+
#storage() {
|
|
66
|
+
try {
|
|
67
|
+
return window[this.#area];
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** `undefined` when storage refuses the read. */
|
|
74
|
+
#read() {
|
|
75
|
+
try {
|
|
76
|
+
return this.#storage()?.getItem(this.#key) ?? null;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
#fail(error) {
|
|
83
|
+
if (DEV)
|
|
84
|
+
console.warn(`PersistedState could not write "${this.#key}"`, error);
|
|
85
|
+
/** Marks what storage still holds as seen, so the next read keeps the in-memory value. */
|
|
86
|
+
this.#seen = this.#read();
|
|
87
|
+
this.#unsaved = true;
|
|
88
|
+
}
|
|
89
|
+
get current() {
|
|
90
|
+
void this.#version;
|
|
91
|
+
if (!BROWSER)
|
|
92
|
+
return this.#value;
|
|
93
|
+
this.#subscribe();
|
|
94
|
+
const raw = this.#read();
|
|
95
|
+
if (raw === undefined || raw === this.#seen)
|
|
96
|
+
return this.#value;
|
|
97
|
+
this.#seen = raw;
|
|
98
|
+
try {
|
|
99
|
+
this.#value = raw === null ? this.#initial : freeze(parse(raw));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
this.#value = this.#initial;
|
|
103
|
+
}
|
|
104
|
+
return this.#value;
|
|
105
|
+
}
|
|
106
|
+
set current(value) {
|
|
107
|
+
const raw = stringify(value);
|
|
108
|
+
this.#value = freeze(value);
|
|
109
|
+
/** Untracked, so an effect that assigns `current` does not come to depend on its own write and rerun forever. */
|
|
110
|
+
this.#version = untrack(() => this.#version) + 1;
|
|
111
|
+
if (!BROWSER)
|
|
112
|
+
return;
|
|
113
|
+
const storage = this.#storage();
|
|
114
|
+
if (!storage) {
|
|
115
|
+
this.#fail(new Error(`${this.#area} is unavailable`));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
storage.setItem(this.#key, raw);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
this.#fail(error);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.#seen = raw;
|
|
126
|
+
this.#unsaved = false;
|
|
127
|
+
window.dispatchEvent(new StorageEvent('storage', { key: this.#key, newValue: raw, storageArea: storage }));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ScrollEdges {
|
|
2
|
+
blockStart: boolean;
|
|
3
|
+
blockEnd: boolean;
|
|
4
|
+
inlineStart: boolean;
|
|
5
|
+
inlineEnd: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ScrollEdgesOptions {
|
|
8
|
+
offset?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function scrollEdges(callback: (edges: ScrollEdges) => void, options?: ScrollEdgesOptions): (node: HTMLElement) => () => void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { untrack } from 'svelte';
|
|
2
|
+
import { on } from 'svelte/events';
|
|
3
|
+
export function scrollEdges(callback, options = {}) {
|
|
4
|
+
return (node) => {
|
|
5
|
+
/** The extra pixel absorbs the sub-pixel gap a fractional box height leaves at maximum scrollTop. */
|
|
6
|
+
const reach = (options.offset ?? 0) + 1;
|
|
7
|
+
let last;
|
|
8
|
+
function compute() {
|
|
9
|
+
/** A hidden or detached scroller measures 0 by 0, which would read as every edge reached; its own ResizeObserver recomputes once it renders. */
|
|
10
|
+
if (node.clientWidth === 0 && node.clientHeight === 0)
|
|
11
|
+
return;
|
|
12
|
+
const style = getComputedStyle(node);
|
|
13
|
+
const flex = style.display.includes('flex');
|
|
14
|
+
const column = style.flexDirection.startsWith('column');
|
|
15
|
+
const wrapReverse = style.flexWrap === 'wrap-reverse';
|
|
16
|
+
/** A reversed flex axis moves the scroll origin to that axis's end edge. */
|
|
17
|
+
const blockFromEnd = flex && (column ? style.flexDirection === 'column-reverse' : wrapReverse);
|
|
18
|
+
const inlineFromEnd = flex && (column ? wrapReverse : style.flexDirection === 'row-reverse');
|
|
19
|
+
/** scrollTop and scrollLeft run negative from 0 when the origin sits on the bottom or right edge. */
|
|
20
|
+
const inlineOriginRight = (style.direction === 'rtl') !== inlineFromEnd;
|
|
21
|
+
const blockMax = node.scrollHeight - node.clientHeight;
|
|
22
|
+
const inlineMax = node.scrollWidth - node.clientWidth;
|
|
23
|
+
/** Elastic overscroll pushes the position past either end, which must not count as distance. */
|
|
24
|
+
const blockScrolled = clamp(blockFromEnd ? -node.scrollTop : node.scrollTop, blockMax);
|
|
25
|
+
const inlineScrolled = clamp(inlineOriginRight ? -node.scrollLeft : node.scrollLeft, inlineMax);
|
|
26
|
+
const fromBlockStart = blockFromEnd ? blockMax - blockScrolled : blockScrolled;
|
|
27
|
+
const fromInlineStart = inlineFromEnd ? inlineMax - inlineScrolled : inlineScrolled;
|
|
28
|
+
const edges = {
|
|
29
|
+
blockStart: fromBlockStart < reach,
|
|
30
|
+
blockEnd: blockMax - fromBlockStart < reach,
|
|
31
|
+
inlineStart: fromInlineStart < reach,
|
|
32
|
+
inlineEnd: inlineMax - fromInlineStart < reach
|
|
33
|
+
};
|
|
34
|
+
if (last?.blockStart === edges.blockStart &&
|
|
35
|
+
last.blockEnd === edges.blockEnd &&
|
|
36
|
+
last.inlineStart === edges.inlineStart &&
|
|
37
|
+
last.inlineEnd === edges.inlineEnd) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
last = edges;
|
|
41
|
+
callback(edges);
|
|
42
|
+
}
|
|
43
|
+
const resize = new ResizeObserver(compute);
|
|
44
|
+
resize.observe(node);
|
|
45
|
+
for (const child of node.children)
|
|
46
|
+
resize.observe(child);
|
|
47
|
+
const mutation = new MutationObserver((records) => {
|
|
48
|
+
for (const record of records) {
|
|
49
|
+
if (record.target !== node)
|
|
50
|
+
continue;
|
|
51
|
+
/** replaceChildren can list a kept child as both removed and added, so it must be observed last. */
|
|
52
|
+
for (const removed of record.removedNodes) {
|
|
53
|
+
if (removed instanceof Element)
|
|
54
|
+
resize.unobserve(removed);
|
|
55
|
+
}
|
|
56
|
+
for (const added of record.addedNodes) {
|
|
57
|
+
if (added instanceof Element)
|
|
58
|
+
resize.observe(added);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
compute();
|
|
62
|
+
});
|
|
63
|
+
/** Rewritten text, a margin, a position or a grandchild overflowing a fixed-size child changes the scroll extent without resizing any observed box. */
|
|
64
|
+
mutation.observe(node, {
|
|
65
|
+
childList: true,
|
|
66
|
+
characterData: true,
|
|
67
|
+
attributes: true,
|
|
68
|
+
subtree: true
|
|
69
|
+
});
|
|
70
|
+
const page = node === node.ownerDocument.scrollingElement;
|
|
71
|
+
/** The browser fires a viewport scroll at the document, never at the root element that scrolls. */
|
|
72
|
+
const off = on(page ? node.ownerDocument : node, 'scroll', compute);
|
|
73
|
+
const view = node.ownerDocument.defaultView;
|
|
74
|
+
/** The root element's box follows its content, not the viewport, so a window that grows or shrinks without moving the scroll position resizes nothing observed. */
|
|
75
|
+
const offResize = page && view ? on(view, 'resize', compute) : undefined;
|
|
76
|
+
/** The first compute runs inside the attachment's effect, so state the callback reads would otherwise re-create the attachment on every write. */
|
|
77
|
+
untrack(compute);
|
|
78
|
+
return () => {
|
|
79
|
+
off();
|
|
80
|
+
offResize?.();
|
|
81
|
+
resize.disconnect();
|
|
82
|
+
mutation.disconnect();
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function clamp(value, max) {
|
|
87
|
+
return Math.min(Math.max(value, 0), max);
|
|
88
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type ThrottledFunction<Args extends unknown[], Return> = {
|
|
2
|
+
(...args: Args): Promise<Awaited<Return>>;
|
|
3
|
+
readonly pending: boolean;
|
|
4
|
+
cancel(): void;
|
|
5
|
+
};
|
|
6
|
+
export declare function throttle<Args extends unknown[], Return>(callback: (...args: Args) => Return, wait?: number): ThrottledFunction<Args, Return>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DEFAULT_WAIT, Scheduler } from './debounce.svelte.js';
|
|
2
|
+
export function throttle(callback, wait = DEFAULT_WAIT) {
|
|
3
|
+
const scheduler = new Scheduler(callback, () => wait, { leading: true });
|
|
4
|
+
return Object.defineProperties((...args) => scheduler.call(...args), {
|
|
5
|
+
pending: { get: () => scheduler.pending },
|
|
6
|
+
cancel: { value: () => scheduler.cancel() }
|
|
7
|
+
});
|
|
8
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kenspeckle",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Svelte 5 utilities under one naming convention: attachments first-class, view transitions, and a state machine with context.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Brandon Pittman <brandon@brandonpittman.com>",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/brandonpittman/kenspeckle.git"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://
|
|
11
|
+
"homepage": "https://kenspeckle.brandonpittman.workers.dev/docs",
|
|
12
12
|
"bugs": "https://github.com/brandonpittman/kenspeckle/issues",
|
|
13
13
|
"scripts": {
|
|
14
14
|
"dev": "vite dev",
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
"utilities"
|
|
97
97
|
],
|
|
98
98
|
"dependencies": {
|
|
99
|
+
"devalue": "^5.8.1 || ^6",
|
|
99
100
|
"esm-env": "^1.2.2"
|
|
100
101
|
}
|
|
101
102
|
}
|