elements-kit 0.0.15 → 0.0.17
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/README.md +138 -31
- package/dist/attributes-Dtn68R1u.d.mts +94 -0
- package/dist/attributes.d.mts +1 -93
- package/dist/custom-elements.d.mts +74 -0
- package/dist/custom-elements.mjs +100 -0
- package/dist/define-CjbTZ3VG.d.mts +23 -0
- package/dist/{element-ChF24-2z.mjs → element-CGVy_8TW.mjs} +11 -12
- package/dist/for.d.mts +45 -0
- package/dist/for.mjs +136 -0
- package/dist/{index-DUshSQ_6.d.mts → index-DydGTqZU.d.mts} +60 -10
- package/dist/index.d.mts +1 -36
- package/dist/index.mjs +1 -144
- package/dist/infer-BfzRJoCn.d.mts +203 -0
- package/dist/integrations/react.d.mts +1 -1
- package/dist/integrations/react.mjs +1 -1
- package/dist/jsx-runtime/index.d.mts +11 -20
- package/dist/jsx-runtime/index.mjs +2 -2
- package/dist/signals/index.d.mts +2 -2
- package/dist/signals/index.mjs +2 -2
- package/dist/{signals-BHmWX6ox.mjs → signals-J8dK_rA4.mjs} +57 -17
- package/dist/slot-C7GQZe-r.d.mts +104 -0
- package/dist/{slot-Cydy7-0L.mjs → slot-Kb61AcgW.mjs} +21 -2
- package/dist/slot.d.mts +2 -84
- package/dist/slot.mjs +2 -2
- package/dist/{test.BmQO5GaM-ANkhHvbr.mjs → test.BmQO5GaM-DfGStnii.mjs} +1 -1
- package/dist/utilities/_observe.d.mts +11 -0
- package/dist/utilities/_observe.mjs +15 -0
- package/dist/utilities/active-element.d.mts +1 -1
- package/dist/utilities/active-element.mjs +6 -2
- package/dist/utilities/active-element.test.mjs +1 -1
- package/dist/utilities/async.d.mts +1 -1
- package/dist/utilities/async.mjs +1 -1
- package/dist/utilities/async.test.mjs +2 -2
- package/dist/utilities/debounced.d.mts +1 -1
- package/dist/utilities/debounced.mjs +1 -1
- package/dist/utilities/debounced.test.mjs +2 -2
- package/dist/utilities/element-rect.d.mts +1 -1
- package/dist/utilities/element-rect.mjs +1 -1
- package/dist/utilities/element-rect.test.mjs +2 -2
- package/dist/utilities/element-scroll.d.mts +1 -1
- package/dist/utilities/element-scroll.mjs +7 -34
- package/dist/utilities/element-scroll.test.mjs +2 -2
- package/dist/utilities/environment.d.mts +5 -0
- package/dist/utilities/environment.mjs +5 -0
- package/dist/utilities/event-driven.d.mts +1 -1
- package/dist/utilities/event-driven.mjs +2 -2
- package/dist/utilities/event-listener.d.mts +1 -1
- package/dist/utilities/event-listener.mjs +1 -1
- package/dist/utilities/event-listener.test.mjs +2 -2
- package/dist/utilities/focus-within.d.mts +1 -1
- package/dist/utilities/focus-within.mjs +1 -1
- package/dist/utilities/focus-within.test.mjs +2 -2
- package/dist/utilities/hover.d.mts +1 -1
- package/dist/utilities/hover.mjs +1 -1
- package/dist/utilities/hover.test.mjs +2 -2
- package/dist/utilities/intersection-observer.mjs +4 -6
- package/dist/utilities/intersection-observer.test.mjs +2 -2
- package/dist/utilities/interval.d.mts +1 -1
- package/dist/utilities/interval.mjs +1 -1
- package/dist/utilities/interval.test.mjs +2 -2
- package/dist/utilities/location.d.mts +2 -14
- package/dist/utilities/location.mjs +12 -3
- package/dist/utilities/location.test.mjs +5 -5
- package/dist/utilities/long-press.mjs +1 -1
- package/dist/utilities/long-press.test.mjs +2 -2
- package/dist/utilities/media-devices.d.mts +1 -1
- package/dist/utilities/media-devices.mjs +1 -1
- package/dist/utilities/media-devices.test.mjs +2 -2
- package/dist/utilities/media-player.d.mts +1 -1
- package/dist/utilities/media-player.test.mjs +2 -2
- package/dist/utilities/media-query.d.mts +2 -3
- package/dist/utilities/media-query.mjs +3 -3
- package/dist/utilities/mutation-observer.mjs +4 -7
- package/dist/utilities/mutation-observer.test.mjs +2 -2
- package/dist/utilities/network.d.mts +2 -2
- package/dist/utilities/network.mjs +5 -3
- package/dist/utilities/network.test.mjs +1 -1
- package/dist/utilities/on-click-outside.test.mjs +2 -2
- package/dist/utilities/orientation.d.mts +1 -1
- package/dist/utilities/orientation.mjs +9 -1
- package/dist/utilities/previous.d.mts +1 -1
- package/dist/utilities/previous.mjs +1 -1
- package/dist/utilities/previous.test.mjs +2 -2
- package/dist/utilities/promise.d.mts +1 -1
- package/dist/utilities/promise.mjs +1 -1
- package/dist/utilities/promise.test.mjs +2 -2
- package/dist/utilities/resize-observer.mjs +4 -6
- package/dist/utilities/retry.mjs +1 -1
- package/dist/utilities/retry.test.mjs +2 -2
- package/dist/utilities/routing.d.mts +1 -1
- package/dist/utilities/routing.mjs +1 -1
- package/dist/utilities/routing.test.mjs +1 -1
- package/dist/utilities/search-params.d.mts +1 -1
- package/dist/utilities/search-params.test.mjs +2 -2
- package/dist/utilities/ssr.test.d.mts +1 -0
- package/dist/utilities/ssr.test.mjs +64 -0
- package/dist/utilities/storage.d.mts +1 -1
- package/dist/utilities/storage.test.mjs +2 -2
- package/dist/utilities/throttled.d.mts +1 -1
- package/dist/utilities/throttled.mjs +1 -1
- package/dist/utilities/throttled.test.mjs +2 -2
- package/dist/utilities/timeout.d.mts +1 -1
- package/dist/utilities/timeout.mjs +1 -1
- package/dist/utilities/timeout.test.mjs +2 -2
- package/dist/utilities/window-focus.d.mts +2 -2
- package/dist/utilities/window-focus.mjs +5 -3
- package/dist/utilities/window-size.d.mts +1 -1
- package/dist/utilities/window-size.mjs +10 -5
- package/dist/utilities/window-size.test.mjs +1 -1
- package/package.json +20 -10
- package/dist/lib-JA05lzCN.d.mts +0 -4
- /package/dist/{polyfill-DR5XVnh_.d.mts → polyfill-BVNd6ogU.d.mts} +0 -0
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/public/og.svg" alt="ElementsKit" width="600" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# ElementsKit
|
|
2
6
|
|
|
3
7
|
**Universal reactive primitives for the web.** Signals, JSX, and custom elements that work anywhere — standalone, inside React, Vue, or any framework, or as the foundation of your own component model.
|
|
@@ -33,6 +37,33 @@ customElements.define("x-counter", CounterElement);
|
|
|
33
37
|
|
|
34
38
|
---
|
|
35
39
|
|
|
40
|
+
## Packages
|
|
41
|
+
|
|
42
|
+
Every feature is a separate subpath export — import only what you use.
|
|
43
|
+
|
|
44
|
+
| Entry | Purpose |
|
|
45
|
+
|-------|---------|
|
|
46
|
+
| `elements-kit` | `For` and core re-exports |
|
|
47
|
+
| `elements-kit/signals` | `signal`, `computed`, `effect`, `effectScope`, `batch`, `untracked`, `trigger`, `onCleanup`, `@reactive` |
|
|
48
|
+
| `elements-kit/attributes` | `@attributes` decorator + `ATTRIBUTES` symbol for custom elements |
|
|
49
|
+
| `elements-kit/slot` | `Slot` class — comment-marker DOM regions |
|
|
50
|
+
| `elements-kit/jsx-runtime` | JSX factory (configure via `jsxImportSource`) |
|
|
51
|
+
| `elements-kit/integrations/react` | `useSignal`, `useScope` React bridge hooks |
|
|
52
|
+
| `elements-kit/utilities/*` | Reactive browser-API utilities — see [src/utilities/README.md](src/utilities/README.md) |
|
|
53
|
+
|
|
54
|
+
## Repository
|
|
55
|
+
|
|
56
|
+
- [src/](src/) — library source ([signals](src/signals/), [jsx-runtime](src/jsx-runtime/), [utilities](src/utilities/), [integrations](src/integrations/))
|
|
57
|
+
- [docs/](docs/) — Astro + Starlight documentation site
|
|
58
|
+
- [example/](example/) — Vite sandbox
|
|
59
|
+
- [ARCHITECTURE.md](ARCHITECTURE.md) — how the library works (reactive model, JSX, custom elements, cleanup)
|
|
60
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md) — quick start, quality bars, versioning, PR checklist
|
|
61
|
+
- [DOCS.md](DOCS.md) — doc-authoring rules
|
|
62
|
+
- [AGENTS.md](AGENTS.md) — agent navigation map
|
|
63
|
+
- [src/utilities/README.md](src/utilities/README.md) — utilities catalog and dependency graph
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
36
67
|
## Why ElementsKit
|
|
37
68
|
|
|
38
69
|
Modern UI frameworks solve reactivity and rendering together — you adopt the whole system or none of it. ElementsKit separates the two:
|
|
@@ -43,6 +74,16 @@ Modern UI frameworks solve reactivity and rendering together — you adopt the w
|
|
|
43
74
|
|
|
44
75
|
Use one piece, or all three. Integrate with React for complex UIs. Build web components that work anywhere HTML does.
|
|
45
76
|
|
|
77
|
+
### Primitives first, composable abstractions
|
|
78
|
+
|
|
79
|
+
Small primitives, one job each, typed interfaces at every boundary. Abstraction is assembled, not inherited — compose upward from `signal` → `sync(fromEvent(...), ...)` → `<For>` and stop at the level your feature needs. Designed for AI agents as much as for humans: each step is verifiable on its own, and there's no framework-specific mental model to learn before writing the first line.
|
|
80
|
+
|
|
81
|
+
- **No prop matrices.** No single-system components with forty flags where legal combinations aren't obvious until you read the source.
|
|
82
|
+
- **No hidden coupling.** Blocks meet at their types; invariants don't leak across layers.
|
|
83
|
+
- **Close to the platform.** `promise` wraps a native `Promise`. JSX compiles to `document.createElement`. Custom elements are `HTMLElement` subclasses. You already know the underlying shape.
|
|
84
|
+
- **Minimal surface.** `async` has no cache, no query client, no query/mutation split. If you need those, compose them on top — don't inherit them by default.
|
|
85
|
+
- **Swap, don't rewrite.** Replace one block (`throttled` → `debounced`) without touching the ones around it.
|
|
86
|
+
|
|
46
87
|
---
|
|
47
88
|
|
|
48
89
|
## Installation
|
|
@@ -247,53 +288,120 @@ The same `cart` store drives custom elements, React trees, and plain scripts —
|
|
|
247
288
|
|
|
248
289
|
## Utilities
|
|
249
290
|
|
|
250
|
-
Pre-built
|
|
291
|
+
Pre-built reactive wrappers around common browser APIs. Each utility lives at its own subpath (`elements-kit/utilities/<name>`) and ships as its own entry — you pay only for what you import. Full catalog in [src/utilities/README.md](src/utilities/README.md).
|
|
292
|
+
|
|
293
|
+
`createMediaQuery` wraps `window.matchMedia` into a reactive signal — reads inside effects or computeds re-run automatically when the media query result changes.
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
import { effect } from "elements-kit/signals";
|
|
297
|
+
import { createMediaQuery } from "elements-kit/utilities/media-query";
|
|
298
|
+
|
|
299
|
+
const isDark = createMediaQuery("(prefers-color-scheme: dark)");
|
|
300
|
+
const isMobile = createMediaQuery("(max-width: 640px)");
|
|
301
|
+
|
|
302
|
+
effect(() => document.documentElement.classList.toggle("dark", isDark()));
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Singletons like `online`, `windowFocused`, `activeElement`, and `currentLocation` are pre-instantiated — import and read them directly inside any reactive context.
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import { effect } from "elements-kit/signals";
|
|
309
|
+
import { online } from "elements-kit/utilities/network";
|
|
310
|
+
import { windowFocused } from "elements-kit/utilities/window-focus";
|
|
311
|
+
|
|
312
|
+
effect(() => console.log("online:", online(), "focused:", windowFocused()));
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Async & Promise
|
|
318
|
+
|
|
319
|
+
Two primitives convert imperative async work into reactive state: `promise` (minimal, any `Promise` → reactive state) and `async` (full controller with start/stop/run and optional reactive input).
|
|
320
|
+
|
|
321
|
+
### `promise`
|
|
322
|
+
|
|
323
|
+
Wraps an async function (or raw `Promise`) into a `ComputedPromise<T>` — awaitable **and** callable as a reactive value. Exposes `.state`, `.value`, `.reason`, `.result` as reactive reads.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import { promise } from "elements-kit/utilities/promise";
|
|
327
|
+
import { effect } from "elements-kit/signals";
|
|
328
|
+
|
|
329
|
+
const user = promise(() => fetch("/api/user").then((r) => r.json()));
|
|
330
|
+
|
|
331
|
+
effect(() => {
|
|
332
|
+
if (user.state === "pending") console.log("loading…");
|
|
333
|
+
if (user.state === "fulfilled") console.log("user:", user.value);
|
|
334
|
+
if (user.state === "rejected") console.log("error:", user.reason);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await user; // awaitable
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`ReactivePromise` is the underlying class — use it when you want the reactive state getters without the `Computed` callable interface.
|
|
341
|
+
|
|
342
|
+
### `async`
|
|
343
|
+
|
|
344
|
+
A controller around `promise`. The async function may be a plain function or a `MaybeReactive<Fn>` (so the body itself can re-read signals and rerun on change).
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
import { async } from "elements-kit/utilities/async";
|
|
348
|
+
|
|
349
|
+
const op = async(() => fetch("/api/items").then((r) => r.json()));
|
|
350
|
+
|
|
351
|
+
op.start(); // run with reactive tracking — reruns when tracked signals change
|
|
352
|
+
await op; // awaitable (delegates to .then/.catch/.finally via .raw)
|
|
353
|
+
op.stop(); // halt reruns + fire registered cleanup
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Reactive state getters: `.state`, `.value`, `.reason`, `.result`, `.pending`, `.raw` (the underlying `ComputedPromise`).
|
|
357
|
+
|
|
358
|
+
One-shot mutation (no tracking):
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
const del = async((id: number) =>
|
|
362
|
+
fetch(`/api/items/${id}`, { method: "DELETE" }).then((r) => r.json()),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
await del.run(42);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
`Async` implements `Symbol.dispose`, so `using` auto-stops on scope exit:
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
{
|
|
372
|
+
using poll = async(() => fetch("/api/poll").then((r) => r.json())).start();
|
|
373
|
+
await poll;
|
|
374
|
+
} // poll.stop() here
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Composing with retry, online, storage
|
|
378
|
+
|
|
379
|
+
`async`'s reactive body composes with other utilities. Below: fetch a todo by `id()`, retry on failure with exponential backoff, pause while offline (returning the stale cached value), and refetch when the tab regains focus.
|
|
251
380
|
|
|
252
381
|
```ts
|
|
253
382
|
import { signal, effect, untracked, onCleanup } from "elements-kit/signals";
|
|
254
|
-
import { createMediaQuery } from "elements-kit/utilities/media-query";
|
|
255
383
|
import { async } from "elements-kit/utilities/async";
|
|
256
384
|
import { retry } from "elements-kit/utilities/retry";
|
|
257
385
|
import { online } from "elements-kit/utilities/network";
|
|
258
386
|
import { windowFocused } from "elements-kit/utilities/window-focus";
|
|
259
|
-
import {
|
|
387
|
+
import { createLocalStorage } from "elements-kit/utilities/storage";
|
|
260
388
|
|
|
261
389
|
const id = signal(1);
|
|
262
|
-
const cache =
|
|
390
|
+
const cache = createLocalStorage<unknown>("todo-cache", null);
|
|
263
391
|
|
|
264
|
-
// Query — retries on failure, refetches when back online or tab regains focus
|
|
265
392
|
const fetchTodo = async(() => {
|
|
266
|
-
if (!online()) return untracked(cache);
|
|
267
|
-
windowFocused();
|
|
393
|
+
if (!online()) return untracked(cache); // pause while offline
|
|
394
|
+
windowFocused(); // refetch on tab focus
|
|
268
395
|
return retry(() => {
|
|
269
396
|
const controller = new AbortController();
|
|
270
|
-
onCleanup(() => controller.abort());
|
|
397
|
+
onCleanup(() => controller.abort()); // abort before each retry
|
|
271
398
|
return fetch(`/api/todos/${id()}`, { signal: controller.signal })
|
|
272
399
|
.then((r) => r.json())
|
|
273
|
-
.then(
|
|
274
|
-
}, 3, (n) => n * 500)();
|
|
400
|
+
.then((value) => (cache(value), value));
|
|
401
|
+
}, 3, (n) => n * 500)(); // 0 ms, 500 ms, 1000 ms backoff
|
|
275
402
|
}).start();
|
|
276
403
|
|
|
277
404
|
effect(() => console.log(fetchTodo.state, fetchTodo.value));
|
|
278
|
-
|
|
279
|
-
// Mutation — run once, no reactive tracking
|
|
280
|
-
const deleteTodo = async((todoId: number) =>
|
|
281
|
-
fetch(`/api/todos/${todoId}`, { method: "DELETE" }).then((r) => r.json()),
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
const result = await deleteTodo.run(42);
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
`createMediaQuery` wraps `window.matchMedia` into a reactive signal — reads inside effects or computeds re-run automatically when the media query result changes.
|
|
288
|
-
|
|
289
|
-
```tsx
|
|
290
|
-
import { effect } from "elements-kit/signals";
|
|
291
|
-
import { createMediaQuery } from "elements-kit/utilities/media-query";
|
|
292
|
-
|
|
293
|
-
const isDark = createMediaQuery("(prefers-color-scheme: dark)");
|
|
294
|
-
const isMobile = createMediaQuery("(max-width: 640px)");
|
|
295
|
-
|
|
296
|
-
effect(() => document.documentElement.classList.toggle("dark", isDark()));
|
|
297
405
|
```
|
|
298
406
|
|
|
299
407
|
---
|
|
@@ -303,7 +411,7 @@ effect(() => document.documentElement.classList.toggle("dark", isDark()));
|
|
|
303
411
|
Reconciles a reactive array into the DOM. Each item renders once per key — no full re-renders on reorder, add, or remove.
|
|
304
412
|
|
|
305
413
|
```tsx
|
|
306
|
-
import { For } from "elements-kit";
|
|
414
|
+
import { For } from "elements-kit/for";
|
|
307
415
|
|
|
308
416
|
<ul>
|
|
309
417
|
<For each={todos} by={(todo) => todo.id}>
|
|
@@ -362,7 +470,6 @@ class MyElement extends HTMLElement {
|
|
|
362
470
|
## Roadmap
|
|
363
471
|
|
|
364
472
|
- [ ] Context — share state across a subtree without prop drilling
|
|
365
|
-
- [ ] Async signal — `signal.from(promise)`, `signal.from(observable)`
|
|
366
473
|
- [ ] UI library — pre-built reactive components built on ElementsKit primitives
|
|
367
474
|
- [ ] More framework integrations (Vue, Solid, Angular, …)
|
|
368
475
|
- [ ] Tutorial — building a full app from scratch
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//#region src/attributes.d.ts
|
|
2
|
+
interface AttrChangeHandler<T> {
|
|
3
|
+
(this: T, value: string | null, oldValue?: string | null): void;
|
|
4
|
+
}
|
|
5
|
+
declare const ATTRIBUTES: unique symbol;
|
|
6
|
+
/**
|
|
7
|
+
* Dispatches an attribute change to the matching handler in the static `attributes` map,
|
|
8
|
+
* walking the prototype chain for inherited handlers.
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* class MyElement extends HTMLElement {
|
|
12
|
+
* static [ATTRIBUTES]: Attributes<MyElement> = {
|
|
13
|
+
* count(value) {
|
|
14
|
+
* this.#count = Number(value);
|
|
15
|
+
* },
|
|
16
|
+
* };
|
|
17
|
+
* static observedAttributes: string[] = observedAttributes(MyElement);
|
|
18
|
+
*
|
|
19
|
+
* attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
|
20
|
+
* dispatchAttrChange.call(this, name, oldValue, newValue);
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
declare function dispatchAttrChange<T extends {
|
|
26
|
+
constructor: {
|
|
27
|
+
[ATTRIBUTES]: Record<string, AttrChangeHandler<T>>;
|
|
28
|
+
};
|
|
29
|
+
}>(this: T, name: string, oldValue: string | null, newValue: string | null): void;
|
|
30
|
+
type Attributes<T> = Record<string, AttrChangeHandler<T>>;
|
|
31
|
+
/**
|
|
32
|
+
* Returns a deduplicated array of all observed attribute names for a custom element class and its ancestors.
|
|
33
|
+
*
|
|
34
|
+
* Call after defining static `[ATTRIBUTES]`, and assign to static `observedAttributes`.
|
|
35
|
+
*
|
|
36
|
+
* Example:
|
|
37
|
+
* ```ts
|
|
38
|
+
* class MyElement extends HTMLElement {
|
|
39
|
+
* static [ATTRIBUTES]: Attributes<MyElement> = {
|
|
40
|
+
* count(value) {
|
|
41
|
+
* this.#count = Number(value);
|
|
42
|
+
* },
|
|
43
|
+
* };
|
|
44
|
+
* static observedAttributes: string[] = observedAttributes(MyElement);
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* class ChildElement extends MyElement {
|
|
48
|
+
* static [ATTRIBUTES]: Attributes<ChildElement> = {
|
|
49
|
+
* bar(value) {
|
|
50
|
+
* // ...
|
|
51
|
+
* },
|
|
52
|
+
* };
|
|
53
|
+
* static observedAttributes: string[] = observedAttributes(ChildElement);
|
|
54
|
+
* }
|
|
55
|
+
* // ChildElement.observedAttributes will include both 'count' and 'bar', deduplicated.
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @param cls The custom element class constructor
|
|
59
|
+
* @returns Array of unique attribute names to observe
|
|
60
|
+
*/
|
|
61
|
+
declare function observedAttributes(cls: {
|
|
62
|
+
[ATTRIBUTES]?: Record<string, unknown>;
|
|
63
|
+
observedAttributes?: string[];
|
|
64
|
+
prototype: unknown;
|
|
65
|
+
}): string[];
|
|
66
|
+
/**
|
|
67
|
+
* A class decorator that automatically wires up `observedAttributes` and `attributeChangedCallback`
|
|
68
|
+
* from a static `[ATTRIBUTES]` map.
|
|
69
|
+
*
|
|
70
|
+
* The `this` type inside attribute handlers is automatically inferred from the decorated class.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* @attributes
|
|
75
|
+
* class MyElement extends HTMLElement {
|
|
76
|
+
* static [ATTRIBUTES] = {
|
|
77
|
+
* count(this: MyElement, value: string | null) {
|
|
78
|
+
* this.count = Number(value);
|
|
79
|
+
* },
|
|
80
|
+
* };
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
type AttributeTarget<T extends abstract new (...args: any[]) => HTMLElement> = T & {
|
|
85
|
+
[ATTRIBUTES]: Record<string, AttrChangeHandler<InstanceType<T>>>;
|
|
86
|
+
};
|
|
87
|
+
type AttributeDecorated<T extends abstract new (...args: any[]) => HTMLElement> = T & (new (...args: any[]) => InstanceType<T> & {
|
|
88
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
89
|
+
}) & {
|
|
90
|
+
observedAttributes: string[];
|
|
91
|
+
};
|
|
92
|
+
declare function attributes<T extends abstract new (...args: any[]) => HTMLElement>(target: AttributeTarget<T>, context: ClassDecoratorContext<T>): AttributeDecorated<T>;
|
|
93
|
+
//#endregion
|
|
94
|
+
export { Attributes as a, observedAttributes as c, AttributeTarget as i, AttrChangeHandler as n, attributes as o, AttributeDecorated as r, dispatchAttrChange as s, ATTRIBUTES as t };
|
package/dist/attributes.d.mts
CHANGED
|
@@ -1,94 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
interface AttrChangeHandler<T> {
|
|
3
|
-
(this: T, value: string | null, oldValue?: string | null): void;
|
|
4
|
-
}
|
|
5
|
-
declare const ATTRIBUTES: unique symbol;
|
|
6
|
-
/**
|
|
7
|
-
* Dispatches an attribute change to the matching handler in the static `attributes` map,
|
|
8
|
-
* walking the prototype chain for inherited handlers.
|
|
9
|
-
* @example
|
|
10
|
-
* ```ts
|
|
11
|
-
* class MyElement extends HTMLElement {
|
|
12
|
-
* static [ATTRIBUTES]: Attributes<MyElement> = {
|
|
13
|
-
* count(value) {
|
|
14
|
-
* this.#count = Number(value);
|
|
15
|
-
* },
|
|
16
|
-
* };
|
|
17
|
-
* static observedAttributes: string[] = observedAttributes(MyElement);
|
|
18
|
-
*
|
|
19
|
-
* attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
|
20
|
-
* dispatchAttrChange.call(this, name, oldValue, newValue);
|
|
21
|
-
* }
|
|
22
|
-
* }
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
declare function dispatchAttrChange<T extends {
|
|
26
|
-
constructor: {
|
|
27
|
-
[ATTRIBUTES]: Record<string, AttrChangeHandler<T>>;
|
|
28
|
-
};
|
|
29
|
-
}>(this: T, name: string, oldValue: string | null, newValue: string | null): void;
|
|
30
|
-
type Attributes<T> = Record<string, AttrChangeHandler<T>>;
|
|
31
|
-
/**
|
|
32
|
-
* Returns a deduplicated array of all observed attribute names for a custom element class and its ancestors.
|
|
33
|
-
*
|
|
34
|
-
* Call after defining static `[ATTRIBUTES]`, and assign to static `observedAttributes`.
|
|
35
|
-
*
|
|
36
|
-
* Example:
|
|
37
|
-
* ```ts
|
|
38
|
-
* class MyElement extends HTMLElement {
|
|
39
|
-
* static [ATTRIBUTES]: Attributes<MyElement> = {
|
|
40
|
-
* count(value) {
|
|
41
|
-
* this.#count = Number(value);
|
|
42
|
-
* },
|
|
43
|
-
* };
|
|
44
|
-
* static observedAttributes: string[] = observedAttributes(MyElement);
|
|
45
|
-
* }
|
|
46
|
-
*
|
|
47
|
-
* class ChildElement extends MyElement {
|
|
48
|
-
* static [ATTRIBUTES]: Attributes<ChildElement> = {
|
|
49
|
-
* bar(value) {
|
|
50
|
-
* // ...
|
|
51
|
-
* },
|
|
52
|
-
* };
|
|
53
|
-
* static observedAttributes: string[] = observedAttributes(ChildElement);
|
|
54
|
-
* }
|
|
55
|
-
* // ChildElement.observedAttributes will include both 'count' and 'bar', deduplicated.
|
|
56
|
-
* ```
|
|
57
|
-
*
|
|
58
|
-
* @param cls The custom element class constructor
|
|
59
|
-
* @returns Array of unique attribute names to observe
|
|
60
|
-
*/
|
|
61
|
-
declare function observedAttributes(cls: {
|
|
62
|
-
[ATTRIBUTES]?: Record<string, unknown>;
|
|
63
|
-
observedAttributes?: string[];
|
|
64
|
-
prototype: unknown;
|
|
65
|
-
}): string[];
|
|
66
|
-
/**
|
|
67
|
-
* A class decorator that automatically wires up `observedAttributes` and `attributeChangedCallback`
|
|
68
|
-
* from a static `[ATTRIBUTES]` map.
|
|
69
|
-
*
|
|
70
|
-
* The `this` type inside attribute handlers is automatically inferred from the decorated class.
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```ts
|
|
74
|
-
* @attributes
|
|
75
|
-
* class MyElement extends HTMLElement {
|
|
76
|
-
* static [ATTRIBUTES] = {
|
|
77
|
-
* count(this: MyElement, value: string | null) {
|
|
78
|
-
* this.count = Number(value);
|
|
79
|
-
* },
|
|
80
|
-
* };
|
|
81
|
-
* }
|
|
82
|
-
* ```
|
|
83
|
-
*/
|
|
84
|
-
type AttributeTarget<T extends abstract new (...args: any[]) => HTMLElement> = T & {
|
|
85
|
-
[ATTRIBUTES]: Record<string, AttrChangeHandler<InstanceType<T>>>;
|
|
86
|
-
};
|
|
87
|
-
type AttributeDecorated<T extends abstract new (...args: any[]) => HTMLElement> = T & (new (...args: any[]) => InstanceType<T> & {
|
|
88
|
-
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
89
|
-
}) & {
|
|
90
|
-
observedAttributes: string[];
|
|
91
|
-
};
|
|
92
|
-
declare function attributes<T extends abstract new (...args: any[]) => HTMLElement>(target: AttributeTarget<T>, context: ClassDecoratorContext<T>): AttributeDecorated<T>;
|
|
93
|
-
//#endregion
|
|
1
|
+
import { a as Attributes, c as observedAttributes, i as AttributeTarget, n as AttrChangeHandler, o as attributes, r as AttributeDecorated, s as dispatchAttrChange, t as ATTRIBUTES } from "./attributes-Dtn68R1u.mjs";
|
|
94
2
|
export { ATTRIBUTES, AttrChangeHandler, AttributeDecorated, AttributeTarget, Attributes, attributes, dispatchAttrChange, observedAttributes };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { n as defineElement, t as CustomElementRegistry } from "./define-CjbTZ3VG.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/custom-elements.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Runs `setup` inside a fresh `effectScope` and returns both its result and
|
|
6
|
+
* the scope's dispose handle.
|
|
7
|
+
*
|
|
8
|
+
* Use this wherever you need to run reactive code with an explicit lifetime
|
|
9
|
+
* outside the JSX element flow — most commonly inside a custom element's
|
|
10
|
+
* `connectedCallback`. `onCleanup`, nested `effect`s and any other
|
|
11
|
+
* scope-bound registrations made in `setup` are owned by the returned
|
|
12
|
+
* `dispose`.
|
|
13
|
+
*
|
|
14
|
+
* `untracked` detaches the new scope from any enclosing effect so it isn't
|
|
15
|
+
* torn down when that effect re-runs — its lifetime is solely the caller's
|
|
16
|
+
* responsibility.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* class Clock extends HTMLElement {
|
|
21
|
+
* #dispose?: () => void;
|
|
22
|
+
*
|
|
23
|
+
* connectedCallback() {
|
|
24
|
+
* const { dispose } = renderScope(() => {
|
|
25
|
+
* const id = setInterval(() => (this.textContent = String(Date.now())), 1000);
|
|
26
|
+
* onCleanup(() => clearInterval(id));
|
|
27
|
+
* });
|
|
28
|
+
* this.#dispose = dispose;
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* disconnectedCallback() {
|
|
32
|
+
* this.#dispose?.();
|
|
33
|
+
* this.#dispose = undefined;
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare function renderScope<T>(setup: () => T): {
|
|
39
|
+
result: T;
|
|
40
|
+
dispose: () => void;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Runs `setup` inside an `effectScope` tied to `el`'s connected lifetime.
|
|
44
|
+
*
|
|
45
|
+
* Call from `connectedCallback`. Effects, `onCleanup` registrations, and
|
|
46
|
+
* reactive reads inside `setup` belong to this scope. Pair with
|
|
47
|
+
* {@link disconnectedScope} from `disconnectedCallback` to dispose.
|
|
48
|
+
*
|
|
49
|
+
* Safe to call more than once (e.g. if the element is reconnected after
|
|
50
|
+
* disconnection): the previous scope is disposed first.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* class Clock extends HTMLElement {
|
|
55
|
+
* connectedCallback() {
|
|
56
|
+
* connectedScope(this, () => {
|
|
57
|
+
* const id = setInterval(() => this.textContent = String(Date.now()), 1000);
|
|
58
|
+
* onCleanup(() => clearInterval(id));
|
|
59
|
+
* });
|
|
60
|
+
* }
|
|
61
|
+
* disconnectedCallback() {
|
|
62
|
+
* disconnectedScope(this);
|
|
63
|
+
* }
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare function connectedScope(el: HTMLElement, setup: () => void): void;
|
|
68
|
+
/**
|
|
69
|
+
* Disposes the scope previously created by {@link connectedScope} for `el`.
|
|
70
|
+
* No-op if there is no active scope.
|
|
71
|
+
*/
|
|
72
|
+
declare function disconnectedScope(el: HTMLElement): void;
|
|
73
|
+
//#endregion
|
|
74
|
+
export { type CustomElementRegistry, connectedScope, defineElement, disconnectedScope, renderScope };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { b as untracked, f as effectScope } from "./signals-J8dK_rA4.mjs";
|
|
2
|
+
//#region src/define.ts
|
|
3
|
+
/**
|
|
4
|
+
* Register a custom element with the browser and return its class.
|
|
5
|
+
* Pair with a module augmentation of `CustomElementRegistry` to get typed JSX.
|
|
6
|
+
*/
|
|
7
|
+
function defineElement(tag, cls, options) {
|
|
8
|
+
customElements.define(tag, cls, options);
|
|
9
|
+
return cls;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/custom-elements.ts
|
|
13
|
+
/**
|
|
14
|
+
* Runs `setup` inside a fresh `effectScope` and returns both its result and
|
|
15
|
+
* the scope's dispose handle.
|
|
16
|
+
*
|
|
17
|
+
* Use this wherever you need to run reactive code with an explicit lifetime
|
|
18
|
+
* outside the JSX element flow — most commonly inside a custom element's
|
|
19
|
+
* `connectedCallback`. `onCleanup`, nested `effect`s and any other
|
|
20
|
+
* scope-bound registrations made in `setup` are owned by the returned
|
|
21
|
+
* `dispose`.
|
|
22
|
+
*
|
|
23
|
+
* `untracked` detaches the new scope from any enclosing effect so it isn't
|
|
24
|
+
* torn down when that effect re-runs — its lifetime is solely the caller's
|
|
25
|
+
* responsibility.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* class Clock extends HTMLElement {
|
|
30
|
+
* #dispose?: () => void;
|
|
31
|
+
*
|
|
32
|
+
* connectedCallback() {
|
|
33
|
+
* const { dispose } = renderScope(() => {
|
|
34
|
+
* const id = setInterval(() => (this.textContent = String(Date.now())), 1000);
|
|
35
|
+
* onCleanup(() => clearInterval(id));
|
|
36
|
+
* });
|
|
37
|
+
* this.#dispose = dispose;
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* disconnectedCallback() {
|
|
41
|
+
* this.#dispose?.();
|
|
42
|
+
* this.#dispose = undefined;
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
function renderScope(setup) {
|
|
48
|
+
let result;
|
|
49
|
+
let dispose;
|
|
50
|
+
untracked(() => {
|
|
51
|
+
dispose = effectScope(() => {
|
|
52
|
+
result = setup();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
result,
|
|
57
|
+
dispose
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const scopes = /* @__PURE__ */ new WeakMap();
|
|
61
|
+
/**
|
|
62
|
+
* Runs `setup` inside an `effectScope` tied to `el`'s connected lifetime.
|
|
63
|
+
*
|
|
64
|
+
* Call from `connectedCallback`. Effects, `onCleanup` registrations, and
|
|
65
|
+
* reactive reads inside `setup` belong to this scope. Pair with
|
|
66
|
+
* {@link disconnectedScope} from `disconnectedCallback` to dispose.
|
|
67
|
+
*
|
|
68
|
+
* Safe to call more than once (e.g. if the element is reconnected after
|
|
69
|
+
* disconnection): the previous scope is disposed first.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* class Clock extends HTMLElement {
|
|
74
|
+
* connectedCallback() {
|
|
75
|
+
* connectedScope(this, () => {
|
|
76
|
+
* const id = setInterval(() => this.textContent = String(Date.now()), 1000);
|
|
77
|
+
* onCleanup(() => clearInterval(id));
|
|
78
|
+
* });
|
|
79
|
+
* }
|
|
80
|
+
* disconnectedCallback() {
|
|
81
|
+
* disconnectedScope(this);
|
|
82
|
+
* }
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
function connectedScope(el, setup) {
|
|
87
|
+
scopes.get(el)?.();
|
|
88
|
+
const { dispose } = renderScope(setup);
|
|
89
|
+
scopes.set(el, dispose);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Disposes the scope previously created by {@link connectedScope} for `el`.
|
|
93
|
+
* No-op if there is no active scope.
|
|
94
|
+
*/
|
|
95
|
+
function disconnectedScope(el) {
|
|
96
|
+
scopes.get(el)?.();
|
|
97
|
+
scopes.delete(el);
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
export { connectedScope, defineElement, disconnectedScope, renderScope };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/define.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Registry of custom-element tags to their constructors.
|
|
4
|
+
* Users augment this interface to add typed JSX support for their elements:
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* declare module "elements-kit" {
|
|
9
|
+
* interface CustomElementRegistry {
|
|
10
|
+
* "x-range": typeof XRange;
|
|
11
|
+
* }
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
interface CustomElementRegistry {}
|
|
16
|
+
type AnyCtor = CustomElementConstructor;
|
|
17
|
+
/**
|
|
18
|
+
* Register a custom element with the browser and return its class.
|
|
19
|
+
* Pair with a module augmentation of `CustomElementRegistry` to get typed JSX.
|
|
20
|
+
*/
|
|
21
|
+
declare function defineElement<Tag extends `${string}-${string}`, C extends AnyCtor>(tag: Tag, cls: C, options?: ElementDefinitionOptions): C;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { defineElement as n, CustomElementRegistry as t };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as onCleanup, b as untracked, d as effect, f as effectScope, t as isReactive } from "./signals-J8dK_rA4.mjs";
|
|
2
2
|
import "./polyfill-B1lNNcum.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { on } from "./utilities/event-listener.mjs";
|
|
4
|
+
import { i as resolveNode$1, n as Slot, r as Slots, t as SLOTS } from "./slot-Kb61AcgW.mjs";
|
|
4
5
|
//#region \0rolldown/runtime.js
|
|
5
6
|
var __defProp = Object.defineProperty;
|
|
6
7
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -40,21 +41,21 @@ const ReservedNameSpaces = new Set([
|
|
|
40
41
|
//#endregion
|
|
41
42
|
//#region src/jsx-runtime/children.ts
|
|
42
43
|
function hasSlots(node) {
|
|
43
|
-
return
|
|
44
|
+
return SLOTS in node;
|
|
44
45
|
}
|
|
45
46
|
function isChildrenProperty(node, key) {
|
|
46
47
|
if (key === "children" && (node instanceof Element || node instanceof DocumentFragment)) return true;
|
|
47
48
|
if (hasSlots(node)) {
|
|
48
49
|
const slotName = key.replace(/^slot:/, "");
|
|
49
|
-
if (Slots.has(node[
|
|
50
|
+
if (Slots.has(node[SLOTS], slotName)) return true;
|
|
50
51
|
}
|
|
51
52
|
return key in node && node[key] instanceof Slot;
|
|
52
53
|
}
|
|
53
54
|
function applyChildren(node, key, value) {
|
|
54
55
|
if (hasSlots(node)) {
|
|
55
56
|
const slotName = key.replace(/^slot:/, "");
|
|
56
|
-
if (Slots.has(node[
|
|
57
|
-
applySlot(node[
|
|
57
|
+
if (Slots.has(node[SLOTS], slotName)) {
|
|
58
|
+
applySlot(node[SLOTS][slotName], value);
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
@@ -126,7 +127,7 @@ function applyProps(node, props) {
|
|
|
126
127
|
if (isReactive(value)) {
|
|
127
128
|
if (isEventKey(key)) {
|
|
128
129
|
effect(() => {
|
|
129
|
-
|
|
130
|
+
on(node, eventName(key), value());
|
|
130
131
|
});
|
|
131
132
|
continue;
|
|
132
133
|
}
|
|
@@ -134,7 +135,7 @@ function applyProps(node, props) {
|
|
|
134
135
|
continue;
|
|
135
136
|
}
|
|
136
137
|
if (isEventKey(key)) {
|
|
137
|
-
|
|
138
|
+
on(node, eventName(key), value);
|
|
138
139
|
continue;
|
|
139
140
|
}
|
|
140
141
|
setProp(node, key, value);
|
|
@@ -210,10 +211,8 @@ function setAttribute(el, key, value) {
|
|
|
210
211
|
function isEventKey(key) {
|
|
211
212
|
return key.startsWith("on:") || key.length > 2 && key.startsWith("on") && key[2] >= "A" && key[2] <= "Z";
|
|
212
213
|
}
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
el.addEventListener(event, handler);
|
|
216
|
-
return () => el.removeEventListener(event, handler);
|
|
214
|
+
function eventName(key) {
|
|
215
|
+
return key.startsWith("on:") ? key.slice(3) : key[2].toLowerCase() + key.slice(3);
|
|
217
216
|
}
|
|
218
217
|
//#endregion
|
|
219
218
|
//#region src/jsx-runtime/element.ts
|