@agent-ui-kit/components 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base-styles.css +8 -0
- package/dist/component-styles.css +147 -0
- package/dist/controls/_base/index.d.ts +3 -0
- package/dist/controls/_base/index.js +9 -0
- package/dist/controls/_base/indicator-element.d.ts +132 -0
- package/dist/controls/_base/indicator-element.js +158 -0
- package/dist/controls/_base/listbox-element.d.ts +70 -0
- package/dist/controls/_base/listbox-element.js +74 -0
- package/dist/controls/_base/range-element.d.ts +120 -0
- package/dist/controls/_base/range-element.js +158 -0
- package/dist/controls/_surface/container-box.css +301 -0
- package/dist/controls/_surface/container.css +156 -0
- package/dist/controls/_token-surface/token-surface.d.ts +35 -0
- package/dist/controls/_token-surface/token-surface.js +97 -0
- package/dist/controls/attachment/attachment-meta.d.ts +8 -0
- package/dist/controls/attachment/attachment-meta.js +84 -0
- package/dist/controls/attachment/attachment.css +91 -0
- package/dist/controls/attachment/attachment.d.ts +40 -0
- package/dist/controls/attachment/attachment.js +70 -0
- package/dist/controls/avatar/avatar-initials.d.ts +4 -0
- package/dist/controls/avatar/avatar-initials.js +30 -0
- package/dist/controls/avatar/avatar.css +87 -0
- package/dist/controls/avatar/avatar.d.ts +40 -0
- package/dist/controls/avatar/avatar.js +77 -0
- package/dist/controls/badge/badge.css +175 -0
- package/dist/controls/badge/badge.d.ts +36 -0
- package/dist/controls/badge/badge.js +71 -0
- package/dist/controls/bar-chart/bar-chart.css +110 -0
- package/dist/controls/bar-chart/bar-chart.d.ts +26 -0
- package/dist/controls/bar-chart/bar-chart.js +80 -0
- package/dist/controls/bar-chart/bar-math.d.ts +31 -0
- package/dist/controls/bar-chart/bar-math.js +106 -0
- package/dist/controls/button/button.css +263 -0
- package/dist/controls/button/button.d.ts +61 -0
- package/dist/controls/button/button.js +115 -0
- package/dist/controls/calendar/calendar.css +495 -0
- package/dist/controls/calendar/calendar.d.ts +161 -0
- package/dist/controls/calendar/calendar.js +1004 -0
- package/dist/controls/card/card-content.d.ts +24 -0
- package/dist/controls/card/card-content.js +133 -0
- package/dist/controls/card/card-footer.d.ts +3 -0
- package/dist/controls/card/card-footer.js +16 -0
- package/dist/controls/card/card-header.d.ts +3 -0
- package/dist/controls/card/card-header.js +18 -0
- package/dist/controls/card/card.css +445 -0
- package/dist/controls/card/card.d.ts +51 -0
- package/dist/controls/card/card.js +61 -0
- package/dist/controls/checkbox/checkbox.css +204 -0
- package/dist/controls/checkbox/checkbox.d.ts +114 -0
- package/dist/controls/checkbox/checkbox.js +130 -0
- package/dist/controls/code/code.css +72 -0
- package/dist/controls/code/code.d.ts +23 -0
- package/dist/controls/code/code.js +40 -0
- package/dist/controls/color-picker/color-picker.css +209 -0
- package/dist/controls/color-picker/color-picker.d.ts +84 -0
- package/dist/controls/color-picker/color-picker.js +545 -0
- package/dist/controls/color-picker/color.d.ts +40 -0
- package/dist/controls/color-picker/color.js +159 -0
- package/dist/controls/column/column.css +151 -0
- package/dist/controls/column/column.d.ts +107 -0
- package/dist/controls/column/column.js +57 -0
- package/dist/controls/combo-box/combo-box.css +397 -0
- package/dist/controls/combo-box/combo-box.d.ts +133 -0
- package/dist/controls/combo-box/combo-box.js +680 -0
- package/dist/controls/command-modal/command-modal.css +226 -0
- package/dist/controls/command-modal/command-modal.d.ts +42 -0
- package/dist/controls/command-modal/command-modal.js +315 -0
- package/dist/controls/disclosure/disclosure.css +162 -0
- package/dist/controls/disclosure/disclosure.d.ts +37 -0
- package/dist/controls/disclosure/disclosure.js +207 -0
- package/dist/controls/field/field.css +140 -0
- package/dist/controls/field/field.d.ts +27 -0
- package/dist/controls/field/field.js +228 -0
- package/dist/controls/form-provider/form-provider.css +40 -0
- package/dist/controls/form-provider/form-provider.d.ts +42 -0
- package/dist/controls/form-provider/form-provider.js +105 -0
- package/dist/controls/grid/grid.css +72 -0
- package/dist/controls/grid/grid.d.ts +60 -0
- package/dist/controls/grid/grid.js +56 -0
- package/dist/controls/icon/icon.css +37 -0
- package/dist/controls/icon/icon.d.ts +25 -0
- package/dist/controls/icon/icon.js +55 -0
- package/dist/controls/index.d.ts +57 -0
- package/dist/controls/index.js +136 -0
- package/dist/controls/ladder/ladder.css +89 -0
- package/dist/controls/ladder/ladder.d.ts +27 -0
- package/dist/controls/ladder/ladder.js +78 -0
- package/dist/controls/list/list.css +118 -0
- package/dist/controls/list/list.d.ts +84 -0
- package/dist/controls/list/list.js +48 -0
- package/dist/controls/menu/menu.css +195 -0
- package/dist/controls/menu/menu.d.ts +44 -0
- package/dist/controls/menu/menu.js +248 -0
- package/dist/controls/modal/modal.css +99 -0
- package/dist/controls/modal/modal.d.ts +61 -0
- package/dist/controls/modal/modal.js +155 -0
- package/dist/controls/popover/popover.css +83 -0
- package/dist/controls/popover/popover.d.ts +44 -0
- package/dist/controls/popover/popover.js +164 -0
- package/dist/controls/progress/progress.css +106 -0
- package/dist/controls/progress/progress.d.ts +28 -0
- package/dist/controls/progress/progress.js +80 -0
- package/dist/controls/radio/radio-group.css +84 -0
- package/dist/controls/radio/radio-group.d.ts +152 -0
- package/dist/controls/radio/radio-group.js +350 -0
- package/dist/controls/radio/radio.css +219 -0
- package/dist/controls/radio/radio.d.ts +19 -0
- package/dist/controls/radio/radio.js +75 -0
- package/dist/controls/ramp/ramp.css +75 -0
- package/dist/controls/ramp/ramp.d.ts +29 -0
- package/dist/controls/ramp/ramp.js +72 -0
- package/dist/controls/row/row.css +152 -0
- package/dist/controls/row/row.d.ts +95 -0
- package/dist/controls/row/row.js +45 -0
- package/dist/controls/segment/segment.css +66 -0
- package/dist/controls/segment/segment.d.ts +3 -0
- package/dist/controls/segment/segment.js +26 -0
- package/dist/controls/segmented-control/segmented-control.css +189 -0
- package/dist/controls/segmented-control/segmented-control.d.ts +20 -0
- package/dist/controls/segmented-control/segmented-control.js +64 -0
- package/dist/controls/select/select.css +467 -0
- package/dist/controls/select/select.d.ts +147 -0
- package/dist/controls/select/select.js +538 -0
- package/dist/controls/slider/slider.css +231 -0
- package/dist/controls/slider/slider.d.ts +111 -0
- package/dist/controls/slider/slider.js +60 -0
- package/dist/controls/slider-multi/slider-multi.css +197 -0
- package/dist/controls/slider-multi/slider-multi.d.ts +149 -0
- package/dist/controls/slider-multi/slider-multi.js +380 -0
- package/dist/controls/sparkline/sparkline-math.d.ts +23 -0
- package/dist/controls/sparkline/sparkline-math.js +79 -0
- package/dist/controls/sparkline/sparkline.css +61 -0
- package/dist/controls/sparkline/sparkline.d.ts +27 -0
- package/dist/controls/sparkline/sparkline.js +74 -0
- package/dist/controls/split/constrain.d.ts +44 -0
- package/dist/controls/split/constrain.js +126 -0
- package/dist/controls/split/split-pane.css +63 -0
- package/dist/controls/split/split-pane.d.ts +49 -0
- package/dist/controls/split/split-pane.js +42 -0
- package/dist/controls/split/split.css +127 -0
- package/dist/controls/split/split.d.ts +42 -0
- package/dist/controls/split/split.js +364 -0
- package/dist/controls/stat/stat-model.d.ts +22 -0
- package/dist/controls/stat/stat-model.js +69 -0
- package/dist/controls/stat/stat.css +101 -0
- package/dist/controls/stat/stat.d.ts +30 -0
- package/dist/controls/stat/stat.js +73 -0
- package/dist/controls/status-stream/status-stream.css +151 -0
- package/dist/controls/status-stream/status-stream.d.ts +108 -0
- package/dist/controls/status-stream/status-stream.js +417 -0
- package/dist/controls/swatch/swatch.css +66 -0
- package/dist/controls/swatch/swatch.d.ts +28 -0
- package/dist/controls/swatch/swatch.js +74 -0
- package/dist/controls/swiper/swiper-item.css +17 -0
- package/dist/controls/swiper/swiper-item.d.ts +31 -0
- package/dist/controls/swiper/swiper-item.js +47 -0
- package/dist/controls/swiper/swiper-label.css +10 -0
- package/dist/controls/swiper/swiper-label.d.ts +4 -0
- package/dist/controls/swiper/swiper-label.js +20 -0
- package/dist/controls/swiper/swiper-paddles.css +26 -0
- package/dist/controls/swiper/swiper-paddles.d.ts +10 -0
- package/dist/controls/swiper/swiper-paddles.js +62 -0
- package/dist/controls/swiper/swiper-pagination.css +60 -0
- package/dist/controls/swiper/swiper-pagination.d.ts +31 -0
- package/dist/controls/swiper/swiper-pagination.js +78 -0
- package/dist/controls/swiper/swiper.css +153 -0
- package/dist/controls/swiper/swiper.d.ts +158 -0
- package/dist/controls/swiper/swiper.js +659 -0
- package/dist/controls/switch/switch.css +194 -0
- package/dist/controls/switch/switch.d.ts +5 -0
- package/dist/controls/switch/switch.js +18 -0
- package/dist/controls/table/table-model.d.ts +43 -0
- package/dist/controls/table/table-model.js +124 -0
- package/dist/controls/table/table.css +90 -0
- package/dist/controls/table/table.d.ts +29 -0
- package/dist/controls/table/table.js +178 -0
- package/dist/controls/tabs/tab-panel.d.ts +10 -0
- package/dist/controls/tabs/tab-panel.js +89 -0
- package/dist/controls/tabs/tab.d.ts +26 -0
- package/dist/controls/tabs/tab.js +67 -0
- package/dist/controls/tabs/tabs.css +181 -0
- package/dist/controls/tabs/tabs.d.ts +61 -0
- package/dist/controls/tabs/tabs.js +176 -0
- package/dist/controls/text/href.d.ts +26 -0
- package/dist/controls/text/href.js +48 -0
- package/dist/controls/text/text.css +328 -0
- package/dist/controls/text/text.d.ts +85 -0
- package/dist/controls/text/text.js +273 -0
- package/dist/controls/text-field/text-field.css +498 -0
- package/dist/controls/text-field/text-field.d.ts +226 -0
- package/dist/controls/text-field/text-field.js +905 -0
- package/dist/controls/textarea/textarea.css +251 -0
- package/dist/controls/textarea/textarea.d.ts +136 -0
- package/dist/controls/textarea/textarea.js +312 -0
- package/dist/controls/theme-provider/theme-provider.css +48 -0
- package/dist/controls/theme-provider/theme-provider.d.ts +59 -0
- package/dist/controls/theme-provider/theme-provider.js +40 -0
- package/dist/controls/timeline/timeline.css +33 -0
- package/dist/controls/timeline/timeline.d.ts +39 -0
- package/dist/controls/timeline/timeline.js +52 -0
- package/dist/controls/timeline-item/timeline-item.css +495 -0
- package/dist/controls/timeline-item/timeline-item.d.ts +79 -0
- package/dist/controls/timeline-item/timeline-item.js +360 -0
- package/dist/controls/toast/toast-region.css +56 -0
- package/dist/controls/toast/toast-region.d.ts +20 -0
- package/dist/controls/toast/toast-region.js +113 -0
- package/dist/controls/toast/toast.css +96 -0
- package/dist/controls/toast/toast.d.ts +28 -0
- package/dist/controls/toast/toast.js +191 -0
- package/dist/controls/toolbar/toolbar.css +125 -0
- package/dist/controls/toolbar/toolbar.d.ts +108 -0
- package/dist/controls/toolbar/toolbar.js +68 -0
- package/dist/controls/tooltip/tooltip.css +80 -0
- package/dist/controls/tooltip/tooltip.d.ts +57 -0
- package/dist/controls/tooltip/tooltip.js +205 -0
- package/dist/descriptor/component-descriptor.d.ts +112 -0
- package/dist/descriptor/component-descriptor.js +492 -0
- package/dist/descriptor/index.d.ts +1 -0
- package/dist/descriptor/index.js +5 -0
- package/dist/dom/container.d.ts +45 -0
- package/dist/dom/container.js +60 -0
- package/dist/dom/element.d.ts +98 -0
- package/dist/dom/element.js +221 -0
- package/dist/dom/form.d.ts +240 -0
- package/dist/dom/form.js +399 -0
- package/dist/dom/index.d.ts +10 -0
- package/dist/dom/index.js +31 -0
- package/dist/dom/props.d.ts +56 -0
- package/dist/dom/props.js +231 -0
- package/dist/dom/repeat.d.ts +10 -0
- package/dist/dom/repeat.js +202 -0
- package/dist/dom/template.d.ts +161 -0
- package/dist/dom/template.js +634 -0
- package/dist/dom/watch.d.ts +11 -0
- package/dist/dom/watch.js +82 -0
- package/dist/foundation-styles.css +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/reactive/graph.d.ts +65 -0
- package/dist/reactive/graph.js +310 -0
- package/dist/reactive/index.d.ts +3 -0
- package/dist/reactive/index.js +4 -0
- package/dist/reactive/scheduler.d.ts +13 -0
- package/dist/reactive/scheduler.js +49 -0
- package/dist/traits/area-drag.d.ts +12 -0
- package/dist/traits/area-drag.js +70 -0
- package/dist/traits/form-registry.d.ts +36 -0
- package/dist/traits/form-registry.js +108 -0
- package/dist/traits/index.d.ts +24 -0
- package/dist/traits/index.js +19 -0
- package/dist/traits/overlay.d.ts +50 -0
- package/dist/traits/overlay.js +277 -0
- package/dist/traits/pane-resize.d.ts +26 -0
- package/dist/traits/pane-resize.js +103 -0
- package/dist/traits/press-activation.d.ts +11 -0
- package/dist/traits/press-activation.js +55 -0
- package/dist/traits/roving-focus.d.ts +52 -0
- package/dist/traits/roving-focus.js +214 -0
- package/dist/traits/scroll-fade.d.ts +28 -0
- package/dist/traits/scroll-fade.js +120 -0
- package/dist/traits/selection-commit.d.ts +30 -0
- package/dist/traits/selection-commit.js +146 -0
- package/dist/traits/tabbable.d.ts +12 -0
- package/dist/traits/tabbable.js +59 -0
- package/dist/traits/track-user-invalid.d.ts +36 -0
- package/dist/traits/track-user-invalid.js +52 -0
- package/dist/traits/value-codec.d.ts +120 -0
- package/dist/traits/value-codec.js +497 -0
- package/dist/traits/value-drag.d.ts +18 -0
- package/dist/traits/value-drag.js +84 -0
- package/package.json +263 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// watch.ts — the `watch` directive (plan §6; G3 slice 2, per-hole scoped reactivity).
|
|
2
|
+
//
|
|
3
|
+
// `watch(source)` is a value-producing child directive: it owns ONE inner sub-`ChildPart` and ONE
|
|
4
|
+
// connection-scope-owned effect. The effect reads `source` (a `ReadonlySignal`'s `.value`, or a thunk
|
|
5
|
+
// `() => T`) UNDER ITS OWN tracking context and commits the value through the inner part — so the value can
|
|
6
|
+
// be text, a `TemplateResult`, an array, or even a nested directive (full part machinery, including the
|
|
7
|
+
// per-part `Object.is` skip). The headline invariant: a watched-signal change wakes ONLY this hole's effect;
|
|
8
|
+
// the HOST render effect gains no source on the watched signal, so it does NOT re-run (rubric D5).
|
|
9
|
+
//
|
|
10
|
+
// The effect is installed via `ctx.effect` (the scope_seam from template.ts), NOT a bare `effect()`. This
|
|
11
|
+
// makes it owned by the CONNECTION SCOPE: it survives unrelated host re-renders, dies on disconnect with
|
|
12
|
+
// zero residue (the watched signal drops to zero subscribers), and respawns on reconnect. The `installed`
|
|
13
|
+
// flag (reset by the effect's own cleanup when the scope disposes it) guarantees install-once — a host
|
|
14
|
+
// re-render or a reconnect never double-installs. The directive instance persists in the (persisted)
|
|
15
|
+
// `ChildPart` across disconnect/reconnect; only the scope-owned effect dies and is re-installed.
|
|
16
|
+
//
|
|
17
|
+
// Imports only `../reactive` (the `ReadonlySignal` type) + the directive seam from `./template.ts`; not
|
|
18
|
+
// re-exported from this file — the dom barrel (S3) re-exports `watch`.
|
|
19
|
+
import { Directive, directive, NO_COMMIT } from "./template.js";
|
|
20
|
+
/** Read a source UNDER the caller's tracking context — `.value` for a signal, a call for a thunk. */
|
|
21
|
+
function readSource(source) {
|
|
22
|
+
return typeof source === 'function' ? source() : source.value;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The `watch` directive instance. One inner sub-part + one scope-owned effect. `update` stores the current
|
|
26
|
+
* source/mapper (a host re-render may hand new ones) and installs the effect ONCE, via `ctx.effect`. The
|
|
27
|
+
* effect reads the source under its own tracking context — subscribing THIS effect, not the host render
|
|
28
|
+
* effect — and commits the (optionally mapped) value through the inner part. Returns `NO_COMMIT`: the
|
|
29
|
+
* directive owns its DOM (the inner sub-part), so the host part commits nothing.
|
|
30
|
+
*/
|
|
31
|
+
class WatchDirective extends Directive {
|
|
32
|
+
// The sub-`ChildPart` the effect commits into. Created in the field initializer (runs after the base
|
|
33
|
+
// constructor stored the host part), so it persists with the instance across disconnect/reconnect.
|
|
34
|
+
#inner = this.createPart();
|
|
35
|
+
#source;
|
|
36
|
+
#mapper;
|
|
37
|
+
#installed = false;
|
|
38
|
+
// The effect's disposer — kept so a LEAVE-directive-mode `dispose()` (while still connected) can stop the
|
|
39
|
+
// effect before it commits into the torn-down inner part. On DISCONNECT the connection scope disposes the
|
|
40
|
+
// effect directly (this disposer is not called); both paths are idempotent.
|
|
41
|
+
#disposeEffect;
|
|
42
|
+
update(args, ctx) {
|
|
43
|
+
this.#source = args[0];
|
|
44
|
+
this.#mapper = args[1];
|
|
45
|
+
// Install ONCE per connection. A host re-render re-runs `update` with `installed === true` → only the
|
|
46
|
+
// source/mapper fields above are refreshed; the effect is NOT re-installed (no per-render churn). Without
|
|
47
|
+
// a `ctx` (a bare 2-arg `render`, no host scope) there is no owner to install under — stay un-installed.
|
|
48
|
+
if (!this.#installed && ctx) {
|
|
49
|
+
this.#disposeEffect = ctx.effect(() => {
|
|
50
|
+
// Re-assert liveness at the TOP of every run. The kernel calls an effect's cleanup before EVERY
|
|
51
|
+
// re-run (not only on disposal), so resetting `installed` in the cleanup alone would wrongly mark a
|
|
52
|
+
// signal-driven re-run as un-installed and let the next host re-render double-install. The body runs
|
|
53
|
+
// only while the effect is alive, so a normal re-run nets `true` (cleanup false → body true) while a
|
|
54
|
+
// DISPOSAL nets `false` (cleanup false → body never runs) — the flag tracks true liveness.
|
|
55
|
+
this.#installed = true;
|
|
56
|
+
const raw = readSource(this.#source); // tracked by THIS effect (activeConsumer = this effect)
|
|
57
|
+
this.#inner.commit(this.#mapper ? this.#mapper(raw) : raw);
|
|
58
|
+
return () => {
|
|
59
|
+
this.#installed = false;
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return NO_COMMIT;
|
|
64
|
+
}
|
|
65
|
+
dispose() {
|
|
66
|
+
this.#disposeEffect?.(); // stop the effect first so it cannot commit into a torn-down inner part
|
|
67
|
+
this.#disposeEffect = undefined;
|
|
68
|
+
this.#inner.dispose();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const watchDirective = directive(WatchDirective);
|
|
72
|
+
/**
|
|
73
|
+
* `watch(source, mapper?)` — bind a child hole to a reactive `source` so only that hole updates when the
|
|
74
|
+
* source changes (the host render effect never re-runs for it). `source` is a `ReadonlySignal<T>` (read via
|
|
75
|
+
* `.value`) or a thunk `() => T` (the general form; a thunk tracks whatever signals it reads at run time).
|
|
76
|
+
* An optional `mapper` transforms the value before it is committed. Use inside a `UIElement.render()` so the
|
|
77
|
+
* host threads its connection scope; outside a host (a bare 2-arg `render`) there is no scope to own the
|
|
78
|
+
* effect and the hole stays empty.
|
|
79
|
+
*/
|
|
80
|
+
export function watch(source, mapper) {
|
|
81
|
+
return watchDirective(source, mapper);
|
|
82
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* foundation-styles.css — the global foundation stylesheet barrel (ADR-0003). Aggregates the cross-package
|
|
2
|
+
* foundation layer from `@agent-ui-kit/shared`: the colour `tokens.css` FIRST, then the dimensional ramp
|
|
3
|
+
* `dimensions.css` (authored to load AFTER tokens). A host page links this BEFORE component-styles.css, so
|
|
4
|
+
* the `--md-sys-color-*` roles + `--ui-{height,font,gap}-*` ramp exist when a control's `:where()` block resolves them.
|
|
5
|
+
* Exposed as the package `./foundation-styles.css` export. `@agent-ui-kit/shared` is the one allowed
|
|
6
|
+
* cross-package dependency (ADR-0003 / the import-layering law). Order is load-bearing — do not reorder.
|
|
7
|
+
*/
|
|
8
|
+
@import '@agent-ui-kit/shared/tokens.css';
|
|
9
|
+
@import '@agent-ui-kit/shared/dimensions.css';
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a computed is read during its own recomputation — a reactive cycle.
|
|
3
|
+
* Without this guard a WARM cycle serves a stale value silently: the re-entrant
|
|
4
|
+
* refresh sees the sources the outer run just cleared, verification concludes
|
|
5
|
+
* "unchanged", and the stale value is returned with no error.
|
|
6
|
+
*/
|
|
7
|
+
export declare class CycleError extends Error {
|
|
8
|
+
}
|
|
9
|
+
/** A reactive value you can read (tracked) and peek (untracked) but not write. */
|
|
10
|
+
export interface ReadonlySignal<T> {
|
|
11
|
+
readonly value: T;
|
|
12
|
+
peek(): T;
|
|
13
|
+
}
|
|
14
|
+
/** A writable reactive cell. */
|
|
15
|
+
export interface Signal<T> extends ReadonlySignal<T> {
|
|
16
|
+
value: T;
|
|
17
|
+
}
|
|
18
|
+
/** Ownership container: every computed/effect created inside dies with it. */
|
|
19
|
+
export interface Scope {
|
|
20
|
+
run<R>(fn: () => R): R;
|
|
21
|
+
dispose(): void;
|
|
22
|
+
}
|
|
23
|
+
/** A read-only, graph-inert snapshot of a node (the public face of the internals). */
|
|
24
|
+
export type NodeSnapshot = Readonly<{
|
|
25
|
+
kind: 'signal';
|
|
26
|
+
value: unknown;
|
|
27
|
+
version: number;
|
|
28
|
+
subscribers: number;
|
|
29
|
+
}> | Readonly<{
|
|
30
|
+
kind: 'computed';
|
|
31
|
+
value: unknown;
|
|
32
|
+
version: number;
|
|
33
|
+
subscribers: number;
|
|
34
|
+
sources: number;
|
|
35
|
+
dirty: boolean;
|
|
36
|
+
failed: boolean;
|
|
37
|
+
disposed: boolean;
|
|
38
|
+
}>;
|
|
39
|
+
/** Read without subscribing the current consumer. */
|
|
40
|
+
export declare function untracked<T>(fn: () => T): T;
|
|
41
|
+
/**
|
|
42
|
+
* Create reactive nodes OUTSIDE any ownership scope — the mirror of `untracked`.
|
|
43
|
+
* Module-singleton machinery created lazily on first touch must not be adopted by
|
|
44
|
+
* whichever component scope happened to trigger that first touch, or the component's
|
|
45
|
+
* disconnect freezes the singleton.
|
|
46
|
+
*/
|
|
47
|
+
export declare function unowned<T>(fn: () => T): T;
|
|
48
|
+
/** A writable reactive cell. */
|
|
49
|
+
export declare const signal: <T>(initial: T) => Signal<T>;
|
|
50
|
+
/** A derived value: recomputed lazily, cut on equality. */
|
|
51
|
+
export declare const computed: <T>(fn: () => T) => ReadonlySignal<T>;
|
|
52
|
+
/**
|
|
53
|
+
* Runs synchronously once, then re-runs batched + deduped on dependency change.
|
|
54
|
+
* Returns a dispose function.
|
|
55
|
+
*/
|
|
56
|
+
export declare function effect(fn: () => void | (() => void)): () => void;
|
|
57
|
+
/** A flat ownership scope: every computed/effect created inside dies with it. */
|
|
58
|
+
export declare function createScope(): Scope;
|
|
59
|
+
/**
|
|
60
|
+
* Read-only introspection of a graph node. PURE and graph-inert: it reads cached
|
|
61
|
+
* private state directly, so it NEVER subscribes, refreshes, or bumps a version —
|
|
62
|
+
* safe to call from anywhere, including inside a tracking context. It reports what
|
|
63
|
+
* the node CURRENTLY holds; it does not force a stale computed to evaluate.
|
|
64
|
+
*/
|
|
65
|
+
export declare function inspect(node: ReadonlySignal<unknown>): NodeSnapshot;
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// graph.ts — the producer/consumer half of the reactivity kernel.
|
|
2
|
+
//
|
|
3
|
+
// Push possible-staleness, pull values, cut on equality (version-verified at both
|
|
4
|
+
// computeds AND effects). Eager invalidation, lazy recomputation. Ownership scopes
|
|
5
|
+
// make disposal provable: after scope.dispose(), no producer holds a reference to
|
|
6
|
+
// any node created inside it. Its only import is the scheduler half, pulling exactly
|
|
7
|
+
// { schedule, dequeue }.
|
|
8
|
+
import { schedule, dequeue } from "./scheduler.js";
|
|
9
|
+
const UNSET = Symbol('unset');
|
|
10
|
+
/**
|
|
11
|
+
* Thrown when a computed is read during its own recomputation — a reactive cycle.
|
|
12
|
+
* Without this guard a WARM cycle serves a stale value silently: the re-entrant
|
|
13
|
+
* refresh sees the sources the outer run just cleared, verification concludes
|
|
14
|
+
* "unchanged", and the stale value is returned with no error.
|
|
15
|
+
*/
|
|
16
|
+
export class CycleError extends Error {
|
|
17
|
+
}
|
|
18
|
+
let activeConsumer = null;
|
|
19
|
+
let activeOwner = null;
|
|
20
|
+
/** Read without subscribing the current consumer. */
|
|
21
|
+
export function untracked(fn) {
|
|
22
|
+
const prev = activeConsumer;
|
|
23
|
+
activeConsumer = null;
|
|
24
|
+
try {
|
|
25
|
+
return fn();
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
activeConsumer = prev;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create reactive nodes OUTSIDE any ownership scope — the mirror of `untracked`.
|
|
33
|
+
* Module-singleton machinery created lazily on first touch must not be adopted by
|
|
34
|
+
* whichever component scope happened to trigger that first touch, or the component's
|
|
35
|
+
* disconnect freezes the singleton.
|
|
36
|
+
*/
|
|
37
|
+
export function unowned(fn) {
|
|
38
|
+
const prev = activeOwner;
|
|
39
|
+
activeOwner = null;
|
|
40
|
+
try {
|
|
41
|
+
return fn();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
activeOwner = prev;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// ── producers / consumers ─────────────────────────────────────────────────────
|
|
48
|
+
class SignalNode {
|
|
49
|
+
subs = new Set();
|
|
50
|
+
version = 0;
|
|
51
|
+
#value;
|
|
52
|
+
constructor(v) {
|
|
53
|
+
this.#value = v;
|
|
54
|
+
}
|
|
55
|
+
get value() {
|
|
56
|
+
activeConsumer?.track(this);
|
|
57
|
+
return this.#value;
|
|
58
|
+
}
|
|
59
|
+
set value(next) {
|
|
60
|
+
if (Object.is(next, this.#value))
|
|
61
|
+
return;
|
|
62
|
+
this.#value = next;
|
|
63
|
+
this.version++;
|
|
64
|
+
for (const c of [...this.subs])
|
|
65
|
+
c.markStale();
|
|
66
|
+
}
|
|
67
|
+
peek() {
|
|
68
|
+
return this.#value;
|
|
69
|
+
}
|
|
70
|
+
snapshot() {
|
|
71
|
+
return Object.freeze({ kind: 'signal', value: this.#value, version: this.version, subscribers: this.subs.size });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
class ComputedNode {
|
|
75
|
+
subs = new Set();
|
|
76
|
+
sources = new Map(); // producer -> version seen
|
|
77
|
+
version = 0;
|
|
78
|
+
#dirty = true;
|
|
79
|
+
#failed = false;
|
|
80
|
+
#disposed = false;
|
|
81
|
+
#running = false;
|
|
82
|
+
#value = UNSET;
|
|
83
|
+
#fn;
|
|
84
|
+
constructor(fn) {
|
|
85
|
+
this.#fn = fn;
|
|
86
|
+
activeOwner?.add(this);
|
|
87
|
+
}
|
|
88
|
+
get value() {
|
|
89
|
+
if (!this.#disposed) {
|
|
90
|
+
// Settle BEFORE the consumer records our version — track-first records a
|
|
91
|
+
// stale version and forces one spurious downstream recompute per change.
|
|
92
|
+
this.refresh();
|
|
93
|
+
activeConsumer?.track(this);
|
|
94
|
+
}
|
|
95
|
+
return this.#value;
|
|
96
|
+
}
|
|
97
|
+
peek() {
|
|
98
|
+
if (!this.#disposed)
|
|
99
|
+
this.refresh();
|
|
100
|
+
return this.#value;
|
|
101
|
+
}
|
|
102
|
+
track(src) {
|
|
103
|
+
this.sources.set(src, src.version);
|
|
104
|
+
src.subs.add(this);
|
|
105
|
+
}
|
|
106
|
+
markStale() {
|
|
107
|
+
if (this.#dirty || this.#disposed)
|
|
108
|
+
return;
|
|
109
|
+
this.#dirty = true;
|
|
110
|
+
for (const c of [...this.subs])
|
|
111
|
+
c.markStale(); // propagate possible-staleness
|
|
112
|
+
}
|
|
113
|
+
refresh() {
|
|
114
|
+
if (this.#running)
|
|
115
|
+
throw new CycleError('reactive cycle: computed re-entered during its own recomputation');
|
|
116
|
+
if (!this.#dirty || this.#disposed)
|
|
117
|
+
return;
|
|
118
|
+
this.#running = true;
|
|
119
|
+
try {
|
|
120
|
+
this.#refreshInner();
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
this.#running = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
#refreshInner() {
|
|
127
|
+
// `#dirty` clears only on a SUCCESSFUL path. A throwing computed stays stale and
|
|
128
|
+
// retries on the next read — it never silently serves a stale value after an error.
|
|
129
|
+
if (this.#value !== UNSET && !this.#failed) {
|
|
130
|
+
// Verification: recompute only if some source's VALUE changed. Skipped after a
|
|
131
|
+
// failure — the failed run partially re-tracked sources at their CURRENT versions,
|
|
132
|
+
// so verification would wrongly conclude "unchanged" and serve the stale value.
|
|
133
|
+
let changed = false;
|
|
134
|
+
for (const [src, seen] of this.sources) {
|
|
135
|
+
src.refresh?.(); // pull nested computeds fresh first (may throw → still dirty)
|
|
136
|
+
if (src.version !== seen) {
|
|
137
|
+
changed = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!changed) {
|
|
142
|
+
this.#dirty = false; // sources were noisy, values unchanged
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const src of this.sources.keys())
|
|
147
|
+
src.subs.delete(this);
|
|
148
|
+
this.sources.clear();
|
|
149
|
+
const prev = activeConsumer;
|
|
150
|
+
activeConsumer = this;
|
|
151
|
+
let next;
|
|
152
|
+
try {
|
|
153
|
+
next = this.#fn();
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
this.#failed = true; // poison verification: the retry must recompute
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
activeConsumer = prev;
|
|
161
|
+
}
|
|
162
|
+
this.#failed = false;
|
|
163
|
+
this.#dirty = false;
|
|
164
|
+
if (!Object.is(next, this.#value)) {
|
|
165
|
+
this.#value = next;
|
|
166
|
+
this.version++; // equality cutoff: no bump → downstream never wakes
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
dispose() {
|
|
170
|
+
if (this.#disposed)
|
|
171
|
+
return;
|
|
172
|
+
this.#disposed = true;
|
|
173
|
+
for (const src of this.sources.keys())
|
|
174
|
+
src.subs.delete(this);
|
|
175
|
+
this.sources.clear();
|
|
176
|
+
this.subs.clear();
|
|
177
|
+
}
|
|
178
|
+
snapshot() {
|
|
179
|
+
return Object.freeze({
|
|
180
|
+
kind: 'computed',
|
|
181
|
+
value: this.#value === UNSET ? undefined : this.#value,
|
|
182
|
+
version: this.version,
|
|
183
|
+
subscribers: this.subs.size,
|
|
184
|
+
sources: this.sources.size,
|
|
185
|
+
dirty: this.#dirty,
|
|
186
|
+
failed: this.#failed,
|
|
187
|
+
disposed: this.#disposed,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
class EffectNode {
|
|
192
|
+
sources = new Map();
|
|
193
|
+
#disposed = false;
|
|
194
|
+
#failed = false;
|
|
195
|
+
#cleanup = undefined;
|
|
196
|
+
#fn;
|
|
197
|
+
constructor(fn) {
|
|
198
|
+
this.#fn = fn;
|
|
199
|
+
activeOwner?.add(this);
|
|
200
|
+
}
|
|
201
|
+
track(src) {
|
|
202
|
+
this.sources.set(src, src.version);
|
|
203
|
+
src.subs.add(this);
|
|
204
|
+
}
|
|
205
|
+
markStale() {
|
|
206
|
+
if (!this.#disposed)
|
|
207
|
+
schedule(this);
|
|
208
|
+
}
|
|
209
|
+
run() {
|
|
210
|
+
if (this.#disposed)
|
|
211
|
+
return;
|
|
212
|
+
if (this.sources.size && !this.#failed) {
|
|
213
|
+
// Same verification as Computed: a scheduled effect whose sources verify
|
|
214
|
+
// unchanged skips its body — the equality cutoff reaches effects.
|
|
215
|
+
let changed = false;
|
|
216
|
+
for (const [src, seen] of this.sources) {
|
|
217
|
+
src.refresh?.();
|
|
218
|
+
if (src.version !== seen) {
|
|
219
|
+
changed = true;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!changed)
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this.#cleanup?.();
|
|
227
|
+
this.#cleanup = undefined; // a throwing body must not leave a consumed cleanup re-callable
|
|
228
|
+
for (const src of this.sources.keys())
|
|
229
|
+
src.subs.delete(this);
|
|
230
|
+
this.sources.clear();
|
|
231
|
+
const prev = activeConsumer;
|
|
232
|
+
activeConsumer = this;
|
|
233
|
+
try {
|
|
234
|
+
const r = this.#fn();
|
|
235
|
+
this.#cleanup = typeof r === 'function' ? r : undefined;
|
|
236
|
+
this.#failed = false;
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
this.#failed = true;
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
activeConsumer = prev;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
dispose() {
|
|
247
|
+
if (this.#disposed)
|
|
248
|
+
return;
|
|
249
|
+
this.#disposed = true;
|
|
250
|
+
dequeue(this); // the dispose-while-queued seam
|
|
251
|
+
this.#cleanup?.();
|
|
252
|
+
this.#cleanup = undefined;
|
|
253
|
+
for (const src of this.sources.keys())
|
|
254
|
+
src.subs.delete(this);
|
|
255
|
+
this.sources.clear();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// ── ownership ─────────────────────────────────────────────────────────────────
|
|
259
|
+
class ScopeImpl {
|
|
260
|
+
#nodes = new Set();
|
|
261
|
+
add(node) {
|
|
262
|
+
this.#nodes.add(node);
|
|
263
|
+
}
|
|
264
|
+
run(fn) {
|
|
265
|
+
const prev = activeOwner;
|
|
266
|
+
activeOwner = this;
|
|
267
|
+
try {
|
|
268
|
+
return fn();
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
activeOwner = prev;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
dispose() {
|
|
275
|
+
for (const n of this.#nodes)
|
|
276
|
+
n.dispose();
|
|
277
|
+
this.#nodes.clear();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// ── factories ───────────────────────────────────────────────────────────────
|
|
281
|
+
/** A writable reactive cell. */
|
|
282
|
+
export const signal = (initial) => new SignalNode(initial);
|
|
283
|
+
/** A derived value: recomputed lazily, cut on equality. */
|
|
284
|
+
export const computed = (fn) => new ComputedNode(fn);
|
|
285
|
+
/**
|
|
286
|
+
* Runs synchronously once, then re-runs batched + deduped on dependency change.
|
|
287
|
+
* Returns a dispose function.
|
|
288
|
+
*/
|
|
289
|
+
export function effect(fn) {
|
|
290
|
+
const e = new EffectNode(fn);
|
|
291
|
+
e.run();
|
|
292
|
+
return () => e.dispose();
|
|
293
|
+
}
|
|
294
|
+
/** A flat ownership scope: every computed/effect created inside dies with it. */
|
|
295
|
+
export function createScope() {
|
|
296
|
+
return new ScopeImpl();
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Read-only introspection of a graph node. PURE and graph-inert: it reads cached
|
|
300
|
+
* private state directly, so it NEVER subscribes, refreshes, or bumps a version —
|
|
301
|
+
* safe to call from anywhere, including inside a tracking context. It reports what
|
|
302
|
+
* the node CURRENTLY holds; it does not force a stale computed to evaluate.
|
|
303
|
+
*/
|
|
304
|
+
export function inspect(node) {
|
|
305
|
+
if (node instanceof SignalNode)
|
|
306
|
+
return node.snapshot();
|
|
307
|
+
if (node instanceof ComputedNode)
|
|
308
|
+
return node.snapshot();
|
|
309
|
+
throw new TypeError('inspect: not a reactive node (expected a signal or computed)');
|
|
310
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// reactive — the signals kernel. The bottom layer: imports nothing outside reactive/.
|
|
2
|
+
// Public surface only; the Producer/Consumer protocol and the node classes stay internal.
|
|
3
|
+
export { signal, computed, effect, createScope, untracked, unowned, inspect, CycleError } from "./graph.js";
|
|
4
|
+
export { whenFlushed } from "./scheduler.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The queue's structural element type — anything that can be run once. Naming it
|
|
2
|
+
* structurally (not Effect) is what keeps this module import-free. */
|
|
3
|
+
export interface Schedulable {
|
|
4
|
+
run(): void;
|
|
5
|
+
}
|
|
6
|
+
/** Enqueue an effect; arm a microtask flush on the first add of a batch. */
|
|
7
|
+
export declare function schedule(effect: Schedulable): void;
|
|
8
|
+
/** Drop an effect from the pending queue — the dispose-while-queued seam. */
|
|
9
|
+
export declare function dequeue(effect: Schedulable): void;
|
|
10
|
+
/** Drain the queue now. Production flow is microtask-driven; probes call this. */
|
|
11
|
+
export declare function flush(): void;
|
|
12
|
+
/** Resolves after the current batch settles. */
|
|
13
|
+
export declare const whenFlushed: () => Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// scheduler.ts — the microtask-queue half of the reactivity kernel.
|
|
2
|
+
//
|
|
3
|
+
// Owns the effect queue, the ~100-wave write-loop budget, and the settled-promise
|
|
4
|
+
// machinery behind whenFlushed(). Imports NOTHING: the queue's element type is the
|
|
5
|
+
// structural `Schedulable` below, so this half never names the graph half. The
|
|
6
|
+
// cross-module seam is inbound only — an Effect calls schedule(this) on staleness
|
|
7
|
+
// and dequeue(this) on dispose.
|
|
8
|
+
let queue = new Set(); // Set: structural dedupe — no `queued` flag to desync
|
|
9
|
+
let scheduled = false;
|
|
10
|
+
let settled = Promise.resolve();
|
|
11
|
+
let resolveSettled = null;
|
|
12
|
+
/** Enqueue an effect; arm a microtask flush on the first add of a batch. */
|
|
13
|
+
export function schedule(effect) {
|
|
14
|
+
queue.add(effect);
|
|
15
|
+
if (!scheduled) {
|
|
16
|
+
scheduled = true;
|
|
17
|
+
settled = new Promise((r) => (resolveSettled = r));
|
|
18
|
+
queueMicrotask(flush);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Drop an effect from the pending queue — the dispose-while-queued seam. */
|
|
22
|
+
export function dequeue(effect) {
|
|
23
|
+
queue.delete(effect);
|
|
24
|
+
}
|
|
25
|
+
/** Drain the queue now. Production flow is microtask-driven; probes call this. */
|
|
26
|
+
export function flush() {
|
|
27
|
+
let waves = 0;
|
|
28
|
+
try {
|
|
29
|
+
while (queue.size) {
|
|
30
|
+
if (++waves > 100) {
|
|
31
|
+
queue.clear();
|
|
32
|
+
throw new Error('effect write-loop: queue failed to settle in 100 waves');
|
|
33
|
+
}
|
|
34
|
+
const wave = [...queue];
|
|
35
|
+
queue.clear();
|
|
36
|
+
for (const e of wave)
|
|
37
|
+
e.run();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
// Runs on a normal drain, the budget throw, AND a throwing effect — the
|
|
42
|
+
// scheduler must never wedge in `scheduled = true`.
|
|
43
|
+
scheduled = false;
|
|
44
|
+
resolveSettled?.();
|
|
45
|
+
resolveSettled = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Resolves after the current batch settles. */
|
|
49
|
+
export const whenFlushed = () => settled;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { UIElement } from '../dom/index.js';
|
|
2
|
+
export interface AreaDragOptions {
|
|
3
|
+
/** Live accessor returning the 2-axis area element. Re-read on each pointerdown. */
|
|
4
|
+
area: () => HTMLElement | null;
|
|
5
|
+
/** Called with the pointer-mapped ratios (both ∈ [0,1], clamped) during drag — x = left→right, y = top→bottom. */
|
|
6
|
+
onValue: (x: number, y: number) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Wire pointer→(x,y) gesture tracking on a `UIElement` host. Invoke from `connected()` so listeners ride the
|
|
10
|
+
* connection AbortSignal (auto-removed on disconnect). Returns cleanup (idempotent).
|
|
11
|
+
*/
|
|
12
|
+
export declare function areaDrag(host: UIElement, opts: AreaDragOptions): () => void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// area-drag.ts — the pointer→(x,y) gesture controller (LLD-C4, color-picker.lld.md · ADR-0123 cl.4). The
|
|
2
|
+
// 2-axis sibling of value-drag.ts: maps pointer events on an area element's rect to a (ratioX, ratioY) pair
|
|
3
|
+
// ∈ [0,1]², clamped, x = left→right and y = top→bottom. `value-drag` stays UNCHANGED (reused verbatim by the
|
|
4
|
+
// composed `ui-slider` channels) — this is the honest new primitive the 2D pad needs, not an edit to it.
|
|
5
|
+
//
|
|
6
|
+
// Protocol — identical lifetime discipline to value-drag: pointerdown → setPointerCapture → pointermove (both
|
|
7
|
+
// ratios re-computed from a re-read rect → onValue(x, y)) → pointerup / lostpointercapture / pointercancel
|
|
8
|
+
// (drag end). The outer pointerdown listener rides host.listen (connection-signal-scoped, zero residue on
|
|
9
|
+
// disconnect); per-drag listeners live on the area under a per-drag AbortController aborted on any drag-end
|
|
10
|
+
// event, bounding their lifetime to the drag itself (the value-drag precedent, unchanged).
|
|
11
|
+
//
|
|
12
|
+
// `traits → dom` is the one allowed cross-layer direction; the host type only.
|
|
13
|
+
/** Map a client point to a [0,1]² ratio pair along the area rect. */
|
|
14
|
+
function ratiosFromPoint(clientX, clientY, rect) {
|
|
15
|
+
const x = rect.width <= 0 ? 0 : Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
16
|
+
const y = rect.height <= 0 ? 0 : Math.max(0, Math.min(1, (clientY - rect.top) / rect.height));
|
|
17
|
+
return { x, y };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Wire pointer→(x,y) gesture tracking on a `UIElement` host. Invoke from `connected()` so listeners ride the
|
|
21
|
+
* connection AbortSignal (auto-removed on disconnect). Returns cleanup (idempotent).
|
|
22
|
+
*/
|
|
23
|
+
export function areaDrag(host, opts) {
|
|
24
|
+
let released = false;
|
|
25
|
+
// Listen on the host so this listener auto-removes on disconnect (host.listen = connection-signal-scoped).
|
|
26
|
+
// A pointerdown on an area child bubbles up here; opts.area() is re-read on every press.
|
|
27
|
+
host.listen(host, 'pointerdown', (event) => {
|
|
28
|
+
if (released)
|
|
29
|
+
return;
|
|
30
|
+
const area = opts.area();
|
|
31
|
+
if (!area)
|
|
32
|
+
return;
|
|
33
|
+
const pe = event;
|
|
34
|
+
// Ignore presses that did not originate from within the area.
|
|
35
|
+
if (!area.contains(pe.target))
|
|
36
|
+
return;
|
|
37
|
+
area.setPointerCapture(pe.pointerId);
|
|
38
|
+
// Snapshot the rect on press; re-snapshot on each move (accounts for scroll / resize).
|
|
39
|
+
let rect = area.getBoundingClientRect();
|
|
40
|
+
const first = ratiosFromPoint(pe.clientX, pe.clientY, rect);
|
|
41
|
+
opts.onValue(first.x, first.y);
|
|
42
|
+
// Per-drag AbortController — aborted on any drag-end event, bounding all drag listeners.
|
|
43
|
+
const dragAc = new AbortController();
|
|
44
|
+
const endDrag = () => { dragAc.abort(); };
|
|
45
|
+
area.addEventListener('pointermove', (e) => {
|
|
46
|
+
const me = e;
|
|
47
|
+
if (me.pointerId !== pe.pointerId)
|
|
48
|
+
return;
|
|
49
|
+
rect = area.getBoundingClientRect();
|
|
50
|
+
const next = ratiosFromPoint(me.clientX, me.clientY, rect);
|
|
51
|
+
opts.onValue(next.x, next.y);
|
|
52
|
+
}, { signal: dragAc.signal });
|
|
53
|
+
area.addEventListener('pointerup', (e) => {
|
|
54
|
+
if (e.pointerId !== pe.pointerId)
|
|
55
|
+
return;
|
|
56
|
+
endDrag();
|
|
57
|
+
}, { signal: dragAc.signal });
|
|
58
|
+
area.addEventListener('lostpointercapture', (e) => {
|
|
59
|
+
if (e.pointerId !== pe.pointerId)
|
|
60
|
+
return;
|
|
61
|
+
endDrag();
|
|
62
|
+
}, { signal: dragAc.signal });
|
|
63
|
+
area.addEventListener('pointercancel', (e) => {
|
|
64
|
+
if (e.pointerId !== pe.pointerId)
|
|
65
|
+
return;
|
|
66
|
+
endDrag();
|
|
67
|
+
}, { signal: dragAc.signal });
|
|
68
|
+
});
|
|
69
|
+
return () => { released = true; };
|
|
70
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReadonlySignal } from '../reactive/index.js';
|
|
2
|
+
import { UIFormElement } from '../dom/index.js';
|
|
3
|
+
import type { UIElement, FormValue, ValidityResult } from '../dom/index.js';
|
|
4
|
+
/** One registered control's PUBLIC reactive surface — the `ui-form-connect` detail, minus the teardown
|
|
5
|
+
* handle the registry has already consumed (its `abort` is wired to `removeMember` at registration). */
|
|
6
|
+
export interface FormMember {
|
|
7
|
+
control: UIFormElement;
|
|
8
|
+
value: () => FormValue;
|
|
9
|
+
validity: () => ValidityResult;
|
|
10
|
+
}
|
|
11
|
+
export interface FormRegistryController {
|
|
12
|
+
/** Live members, registration order (≈ document order for parse/append; a re-slotted control re-enters
|
|
13
|
+
* at the end — documented, not corrected). */
|
|
14
|
+
members: ReadonlySignal<readonly FormMember[]>;
|
|
15
|
+
/** Submission entries — native FormData parity: name ≠ '', not effectiveDisabled(), value() ≠ null;
|
|
16
|
+
* duplicate names PRESERVED. */
|
|
17
|
+
entries: ReadonlySignal<ReadonlyArray<readonly [string, FormValue]>>;
|
|
18
|
+
/** Keyed convenience view of entries — LAST entry wins on a duplicate name (documented). */
|
|
19
|
+
values: ReadonlySignal<Readonly<Record<string, FormValue>>>;
|
|
20
|
+
/** Members whose merged verdict is invalid — unnamed controls INCLUDED (native: validation is
|
|
21
|
+
* name-independent), effectiveDisabled() members EXCLUDED (native: disabled ⇒ barred from constraint
|
|
22
|
+
* validation). */
|
|
23
|
+
invalid: ReadonlySignal<readonly UIFormElement[]>;
|
|
24
|
+
/** invalid().length === 0 */
|
|
25
|
+
valid: ReadonlySignal<boolean>;
|
|
26
|
+
/** Idempotent early teardown (the trait contract); otherwise everything dies with the connection. */
|
|
27
|
+
release: () => void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Attach the reactive form registry to a `ui-form-provider` host. Invoke from `connected()` (connection
|
|
31
|
+
* scope + AbortSignal live). Listens for `ui-form-connect` on the host, registers each control at the
|
|
32
|
+
* NEAREST provider (`stopPropagation`), and deregisters on the control's connection-scoped `abort` — a
|
|
33
|
+
* late-added control is discovered by construction (its own connect dispatch re-fires), a removed one drops
|
|
34
|
+
* out with zero residue.
|
|
35
|
+
*/
|
|
36
|
+
export declare function formRegistry(host: UIElement): FormRegistryController;
|