@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,634 @@
|
|
|
1
|
+
// template.ts — the tagged-template engine (plan §6; G3 slice 1, the template core).
|
|
2
|
+
//
|
|
3
|
+
// `html\`…\`` returns an inert `TemplateResult` (the frozen `strings` + the dynamic `values`). The FIRST
|
|
4
|
+
// render of a call site `prepare`s its `strings` ONCE — markers → one `<template>` parse → a parts
|
|
5
|
+
// manifest — memoized in a `WeakMap` keyed by `strings` IDENTITY (stable per call site). Re-renders
|
|
6
|
+
// re-parse nothing: they reuse the prepared template + the live parts and write only the holes whose
|
|
7
|
+
// values changed (per-part `Object.is` skip). This fills `UIElement.render()` (the no-op G2 left): the
|
|
8
|
+
// host's one scope-owned render effect calls `render()`, which calls this engine into `renderRoot`.
|
|
9
|
+
//
|
|
10
|
+
// Parts: ChildPart (text · nested template · array · directive), AttrPart (string attribute), BooleanPart
|
|
11
|
+
// (`?bool`), PropertyPart (`.prop`), EventPart (`@event`, stable listener identity). `svg\`\`` fragments
|
|
12
|
+
// parse in the SVG namespace, and unsupported binding positions (tag-name, comment, partial attribute)
|
|
13
|
+
// throw at prepare naming the position. A child hole also routes a branded `DirectiveResult` to a stateful
|
|
14
|
+
// `Directive` (the opt-in seam below; `repeat`/`watch` are riders that import it from here). Imports nothing
|
|
15
|
+
// — the engine is pure DOM; reactivity comes from the host render effect re-running `render()`.
|
|
16
|
+
// `render(result, container, ctx?)` threads an optional `RenderContext` (the host's scope-owned `effect`)
|
|
17
|
+
// down to directives, so a per-hole directive effect (`watch`) is owned by the connection scope; 2-arg
|
|
18
|
+
// calls keep working. The seam types are module-internal — NOT re-exported from the dom barrel.
|
|
19
|
+
var _a;
|
|
20
|
+
// ── TemplateResult — the inert result (t-result) ─────────────────────────────
|
|
21
|
+
/**
|
|
22
|
+
* The inert product of `html\`…\`` / `svg\`…\``: the call site's frozen `strings`, this render's dynamic
|
|
23
|
+
* `values`, and the parse MODE (`svg` fragments parse in the SVG namespace). No DOM work happens at the
|
|
24
|
+
* call site — preparation is lazy, at render.
|
|
25
|
+
*/
|
|
26
|
+
export class TemplateResult {
|
|
27
|
+
strings;
|
|
28
|
+
values;
|
|
29
|
+
svg;
|
|
30
|
+
constructor(strings, values, svg = false) {
|
|
31
|
+
this.strings = strings;
|
|
32
|
+
this.values = values;
|
|
33
|
+
this.svg = svg;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Tagged-template tag → an inert HTML `TemplateResult`. */
|
|
37
|
+
export function html(strings, ...values) {
|
|
38
|
+
return new TemplateResult(strings, values);
|
|
39
|
+
}
|
|
40
|
+
/** Like `html`, but the fragment parses in the SVG NAMESPACE — `<circle>`/`<path>`/… become real SVGElements. */
|
|
41
|
+
export function svg(strings, ...values) {
|
|
42
|
+
return new TemplateResult(strings, values, true);
|
|
43
|
+
}
|
|
44
|
+
/** The attribute name (with any binding sigil) immediately before a hole, captured from the source chunk. */
|
|
45
|
+
const ATTR_NAME = /([.?@]?[A-Za-z][\w.:-]*)\s*=\s*["']?$/;
|
|
46
|
+
function attrNameBefore(chunk) {
|
|
47
|
+
const m = ATTR_NAME.exec(chunk);
|
|
48
|
+
return m ? m[1] : null;
|
|
49
|
+
}
|
|
50
|
+
/** Route a source attribute name (with sigil) to its part kind, stripping the sigil from the bound name. */
|
|
51
|
+
function attrPartInfo(nodeIndex, sourceName) {
|
|
52
|
+
switch (sourceName[0]) {
|
|
53
|
+
case '?':
|
|
54
|
+
return { kind: 'bool', nodeIndex, name: sourceName.slice(1) };
|
|
55
|
+
case '.':
|
|
56
|
+
return { kind: 'prop', nodeIndex, name: sourceName.slice(1) };
|
|
57
|
+
case '@':
|
|
58
|
+
return { kind: 'event', nodeIndex, name: sourceName.slice(1) };
|
|
59
|
+
default:
|
|
60
|
+
return { kind: 'attr', nodeIndex, name: sourceName };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// A per-process unique marker so it cannot collide with template text. Child holes become a comment node
|
|
64
|
+
// (an anchor); attribute holes become this marker as the attribute value (cleaned out at prepare time).
|
|
65
|
+
const MARKER = `ui$${String(Math.random()).slice(2)}$`;
|
|
66
|
+
const CHILD_MARKER = `<!--${MARKER}-->`;
|
|
67
|
+
const PREPARED = new WeakMap();
|
|
68
|
+
const ST_TEXT = 0;
|
|
69
|
+
const ST_TAG_NAME = 1;
|
|
70
|
+
const ST_TAG_ATTRS = 2;
|
|
71
|
+
const ST_COMMENT = 3;
|
|
72
|
+
const isNameStart = (c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
|
73
|
+
const isSpace = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f';
|
|
74
|
+
/**
|
|
75
|
+
* Classify each hole's position by a minimal state scan of the static source. Deliberately NOT a full
|
|
76
|
+
* tokenizer: quotes inside attribute values are not tracked, so a literal `>` inside a quoted value can
|
|
77
|
+
* misclassify (same crude-scan caveat as slices 1–2). Enough to route child/attr and to NAME the three
|
|
78
|
+
* unsupported positions (tag-name, comment, partial attribute).
|
|
79
|
+
*/
|
|
80
|
+
function classifyHoles(strings) {
|
|
81
|
+
const out = [];
|
|
82
|
+
let state = ST_TEXT;
|
|
83
|
+
for (let i = 0; i < strings.length; i++) {
|
|
84
|
+
const s = strings[i];
|
|
85
|
+
for (let j = 0; j < s.length; j++) {
|
|
86
|
+
const c = s[j];
|
|
87
|
+
if (state === ST_COMMENT) {
|
|
88
|
+
if (s.startsWith('-->', j)) {
|
|
89
|
+
state = ST_TEXT;
|
|
90
|
+
j += 2; // + the loop's ++ skips `-->`
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (state === ST_TEXT) {
|
|
94
|
+
if (s.startsWith('<!--', j)) {
|
|
95
|
+
state = ST_COMMENT;
|
|
96
|
+
j += 3;
|
|
97
|
+
}
|
|
98
|
+
else if (c === '<') {
|
|
99
|
+
const n = s[j + 1];
|
|
100
|
+
if (n === undefined || n === '/' || isNameStart(n))
|
|
101
|
+
state = ST_TAG_NAME; // a real tag (or a tag-name hole)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (state === ST_TAG_NAME) {
|
|
105
|
+
if (c === '>')
|
|
106
|
+
state = ST_TEXT;
|
|
107
|
+
else if (isSpace(c))
|
|
108
|
+
state = ST_TAG_ATTRS;
|
|
109
|
+
}
|
|
110
|
+
else if (c === '>') {
|
|
111
|
+
state = ST_TEXT; // ST_TAG_ATTRS
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (i < strings.length - 1)
|
|
115
|
+
out.push(classifyAt(state, s));
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
function classifyAt(state, chunk) {
|
|
120
|
+
if (state === ST_COMMENT)
|
|
121
|
+
return { kind: 'comment' };
|
|
122
|
+
if (state === ST_TAG_NAME)
|
|
123
|
+
return { kind: 'tag' };
|
|
124
|
+
if (state === ST_TAG_ATTRS) {
|
|
125
|
+
const name = attrNameBefore(chunk);
|
|
126
|
+
return name != null ? { kind: 'attr', name } : { kind: 'partial-attr' };
|
|
127
|
+
}
|
|
128
|
+
return { kind: 'child' };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Prepare a call site's `strings`: build the marker HTML, parse it into one `<template>`, and walk it
|
|
132
|
+
* once to build the parts manifest (cleaning attribute markers). Memoized by `strings` IDENTITY — a
|
|
133
|
+
* re-render of the same call site hits the cache and re-parses nothing. Exported for the cache probes
|
|
134
|
+
* (an internal seam; NOT re-exported from the dom barrel).
|
|
135
|
+
*/
|
|
136
|
+
export function prepare(strings, svg = false) {
|
|
137
|
+
const cached = PREPARED.get(strings);
|
|
138
|
+
if (cached)
|
|
139
|
+
return cached;
|
|
140
|
+
// Pass 0 — classify each hole's position and throw (NAMING the position) on an unsupported one.
|
|
141
|
+
const positions = classifyHoles(strings);
|
|
142
|
+
positions.forEach((pos, i) => {
|
|
143
|
+
if (pos.kind === 'tag') {
|
|
144
|
+
throw new Error(`template: hole #${i} is in TAG-NAME position (\`<\${…}>\` / \`</\${…}>\`) — the tag name must be static; a dynamic tag is not supported.`);
|
|
145
|
+
}
|
|
146
|
+
if (pos.kind === 'comment') {
|
|
147
|
+
throw new Error(`template: hole #${i} is inside an HTML COMMENT (\`<!-- \${…} -->\`) — not a supported binding position.`);
|
|
148
|
+
}
|
|
149
|
+
if (pos.kind === 'partial-attr') {
|
|
150
|
+
throw new Error(`template: hole #${i} is in a PARTIAL attribute value (\`name="… \${…} …"\`) — only a whole-value attribute hole (\`name=\${…}\`) is supported.`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
// Pass 1 — build the marker HTML from the validated positions, capturing each hole's SOURCE attribute
|
|
154
|
+
// name (the HTML parser lowercases attribute names, which would corrupt a `.prop`'s camelCase). `null` = child.
|
|
155
|
+
let markup = '';
|
|
156
|
+
const holeNames = [];
|
|
157
|
+
for (let i = 0; i < strings.length; i++) {
|
|
158
|
+
markup += strings[i];
|
|
159
|
+
if (i < strings.length - 1) {
|
|
160
|
+
const pos = positions[i];
|
|
161
|
+
if (pos.kind === 'attr') {
|
|
162
|
+
markup += MARKER;
|
|
163
|
+
holeNames.push(pos.name);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
markup += CHILD_MARKER; // 'child' (tag/comment/partial already threw)
|
|
167
|
+
holeNames.push(null);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Parse. For `svg`, wrap the markup in an `<svg>` so its children parse in the SVG namespace, then LIFT
|
|
172
|
+
// those children up to the template content (their namespace is fixed at creation, so lifting/cloning
|
|
173
|
+
// keeps it) and drop the wrapper.
|
|
174
|
+
const template = document.createElement('template');
|
|
175
|
+
if (svg) {
|
|
176
|
+
template.innerHTML = `<svg>${markup}</svg>`;
|
|
177
|
+
const wrap = template.content.firstElementChild;
|
|
178
|
+
if (wrap) {
|
|
179
|
+
while (wrap.firstChild)
|
|
180
|
+
template.content.appendChild(wrap.firstChild);
|
|
181
|
+
wrap.remove();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
template.innerHTML = markup;
|
|
186
|
+
}
|
|
187
|
+
const found = [];
|
|
188
|
+
const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT);
|
|
189
|
+
let nodeIndex = -1;
|
|
190
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
191
|
+
nodeIndex++;
|
|
192
|
+
if (node.nodeType === Node.COMMENT_NODE) {
|
|
193
|
+
if (node.data === MARKER)
|
|
194
|
+
found.push({ kind: 'child', nodeIndex });
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const el = node;
|
|
198
|
+
const markerAttrs = [];
|
|
199
|
+
for (const attr of Array.from(el.attributes)) {
|
|
200
|
+
if (attr.value === MARKER) {
|
|
201
|
+
found.push({ kind: 'attr', nodeIndex });
|
|
202
|
+
markerAttrs.push(attr.name);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const name of markerAttrs)
|
|
206
|
+
el.removeAttribute(name); // the live part sets the real value
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Zip the found sites (document order) with the source names (source order) — they align hole-for-hole.
|
|
210
|
+
// Invariant: one site per hole, and the site's kind matches the source's classification; otherwise a hole
|
|
211
|
+
// landed in a position slice 1–2 doesn't support and would drop its value SILENTLY. Fail loud instead;
|
|
212
|
+
// slice 3 (t-positions) enriches this into a position-naming message.
|
|
213
|
+
const holes = strings.length - 1;
|
|
214
|
+
const mismatch = () => {
|
|
215
|
+
throw new Error(`template: ${holes} hole(s) but ${found.length} binding site(s), or a mis-positioned hole — an ` +
|
|
216
|
+
`unsupported binding position. Supported: a child hole, or a whole-value attribute hole ` +
|
|
217
|
+
`(plain, or ?bool / .prop / @event). Richer diagnostics land in slice 3.`);
|
|
218
|
+
};
|
|
219
|
+
if (found.length !== holes)
|
|
220
|
+
mismatch();
|
|
221
|
+
const parts = found.map((site, k) => {
|
|
222
|
+
if (site.kind === 'child') {
|
|
223
|
+
if (holeNames[k] != null)
|
|
224
|
+
return mismatch();
|
|
225
|
+
return { kind: 'child', nodeIndex: site.nodeIndex };
|
|
226
|
+
}
|
|
227
|
+
const name = holeNames[k];
|
|
228
|
+
if (name == null)
|
|
229
|
+
return mismatch();
|
|
230
|
+
return attrPartInfo(site.nodeIndex, name);
|
|
231
|
+
});
|
|
232
|
+
const prepared = { template, parts };
|
|
233
|
+
PREPARED.set(strings, prepared);
|
|
234
|
+
return prepared;
|
|
235
|
+
}
|
|
236
|
+
// ── directives — the opt-in extension seam (d-seam) ──────────────────────────
|
|
237
|
+
//
|
|
238
|
+
// A directive is a BRANDED factory → a stateful instance whose `update` threads state across commits of
|
|
239
|
+
// the SAME child hole, with a `dispose()`. `ChildPart` recognizes a branded `DirectiveResult` and routes
|
|
240
|
+
// to the instance — the SOLE extension point; `repeat`/`watch` (later slices) are riders, never special-
|
|
241
|
+
// cased in the part engine. The seam base lives HERE (beside `ChildPart`) so the rider files import it
|
|
242
|
+
// from this module with no import cycle; none of it is re-exported from the dom barrel (an internal seam).
|
|
243
|
+
const DIRECTIVE_BRAND = Symbol('ui.directive');
|
|
244
|
+
/** A directive's `update` returns this when it manages its OWN DOM (via sub-parts) — the host part commits nothing. */
|
|
245
|
+
export const NO_COMMIT = Symbol('ui.directive.no-commit');
|
|
246
|
+
/**
|
|
247
|
+
* A directive instance: `update` runs on every commit of its hole (threading state across commits of the
|
|
248
|
+
* SAME hole), returning a value the host part commits through its normal child path OR `NO_COMMIT` when the
|
|
249
|
+
* directive drives its own DOM; `dispose` tears it down. The host `ChildPart` is handed to the constructor
|
|
250
|
+
* so a DOM-owning directive can drive sub-parts via the `protected` primitives below. Subclassed by the
|
|
251
|
+
* rider files (repeat.ts/watch.ts) — never instantiated by consumers; reached only through `directive()`.
|
|
252
|
+
*/
|
|
253
|
+
export class Directive {
|
|
254
|
+
#part;
|
|
255
|
+
constructor(part) {
|
|
256
|
+
this.#part = part;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Create a sub-`ChildPart` rendering into THIS directive's hole (its anchor sits just before the host
|
|
260
|
+
* hole's anchor). `watch` drives one inner part; `repeat` drives one per item. The returned part's
|
|
261
|
+
* `commit` / `dispose` / `moveBefore` / `startNode` are the directive's DOM primitives.
|
|
262
|
+
*/
|
|
263
|
+
createPart() {
|
|
264
|
+
return this.#part.createChild();
|
|
265
|
+
}
|
|
266
|
+
/** The host hole's end boundary — sub-part content orders BEFORE this node (a `repeat` reorder reference). */
|
|
267
|
+
get endNode() {
|
|
268
|
+
return this.#part.endNode;
|
|
269
|
+
}
|
|
270
|
+
/** Tear down owned DOM/effects. Default no-op; the part isolates a throw so sibling teardown still runs. */
|
|
271
|
+
dispose() { }
|
|
272
|
+
}
|
|
273
|
+
function isDirectiveResult(value) {
|
|
274
|
+
return typeof value === 'object' && value !== null && value[DIRECTIVE_BRAND] === true;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Wrap a `Directive` subclass into a factory: `const repeat = directive(RepeatDirective)`. Calling the
|
|
278
|
+
* factory returns an inert branded `DirectiveResult` that a child hole routes to its (per-hole, state-
|
|
279
|
+
* threading) instance. Rider files import this from template.ts — the one extension point.
|
|
280
|
+
*/
|
|
281
|
+
export function directive(ctor) {
|
|
282
|
+
return (...args) => ({ [DIRECTIVE_BRAND]: true, ctor, args });
|
|
283
|
+
}
|
|
284
|
+
/** Teardown isolation: a throwing directive disposer is reported, never rethrown, so siblings still tear down. */
|
|
285
|
+
function reportDirectiveError(err) {
|
|
286
|
+
console.error('directive dispose threw (isolated):', err);
|
|
287
|
+
}
|
|
288
|
+
// ── parts — one site, one Object.is dirty check (t-child-part, t-attr-parts) ──
|
|
289
|
+
const UNCOMMITTED = Symbol('uncommitted'); // never Object.is-equal to any committed value → first commit always runs
|
|
290
|
+
/** A child hole: commits a primitive (text, updated in place), a nested `TemplateResult`, an array, or a directive. */
|
|
291
|
+
class ChildPart {
|
|
292
|
+
#anchor; // the marker comment; content is inserted as its previous siblings
|
|
293
|
+
#committed = UNCOMMITTED;
|
|
294
|
+
#nodes = []; // the nodes this part directly owns (text / nested-template top nodes)
|
|
295
|
+
#nested = null; // the live instance when the value is a nested template
|
|
296
|
+
#items = null; // one sub-part per array item, each with its own anchor
|
|
297
|
+
#directive = null; // the live directive when the value is a branded DirectiveResult
|
|
298
|
+
#directiveCtor = null; // its ctor, so a same-ctor re-commit threads state
|
|
299
|
+
#ctx = undefined; // the render context for THIS commit pass (handed to directives)
|
|
300
|
+
constructor(anchor) {
|
|
301
|
+
this.#anchor = anchor;
|
|
302
|
+
}
|
|
303
|
+
commit(value, ctx) {
|
|
304
|
+
this.#ctx = ctx;
|
|
305
|
+
if (isDirectiveResult(value)) {
|
|
306
|
+
this.#commitDirective(value);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (this.#directive) {
|
|
310
|
+
// Leaving directive mode (the hole is no longer a directive) → dispose it (it owned the DOM) and reset.
|
|
311
|
+
this.#disposeDirective(); // isolated; #clear() only touches content, never the directive
|
|
312
|
+
this.#clear();
|
|
313
|
+
this.#committed = UNCOMMITTED;
|
|
314
|
+
}
|
|
315
|
+
this.#commitChild(value);
|
|
316
|
+
}
|
|
317
|
+
/** Remove this part's content + its directive AND its own anchor — for an array sub-part being dropped. */
|
|
318
|
+
dispose() {
|
|
319
|
+
this.#disposeDirective();
|
|
320
|
+
this.#clear();
|
|
321
|
+
this.#anchor.remove();
|
|
322
|
+
}
|
|
323
|
+
/** Create a sub-part whose anchor sits just before THIS part's anchor — a directive's DOM primitive. */
|
|
324
|
+
createChild() {
|
|
325
|
+
const anchor = document.createComment('');
|
|
326
|
+
this.#anchor.before(anchor);
|
|
327
|
+
return new _a(anchor);
|
|
328
|
+
}
|
|
329
|
+
/** This part's end boundary — a directive orders sub-part content before it. */
|
|
330
|
+
get endNode() {
|
|
331
|
+
return this.#anchor;
|
|
332
|
+
}
|
|
333
|
+
/** The first DOM node of this part's range (its first owned node, else its anchor) — a reorder reference. */
|
|
334
|
+
get startNode() {
|
|
335
|
+
return this.#nodes.length > 0 ? this.#nodes[0] : this.#anchor;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Relocate this part's owned nodes + anchor to just before `ref`. TWO-TIER guarantee (ADR-0022): NODE
|
|
339
|
+
* IDENTITY is preserved always, on every engine (the same node objects move, never re-created). ELEMENT
|
|
340
|
+
* STATE — focus, selection, transitions, `<iframe>`/media — survives ONLY on the native path, where the
|
|
341
|
+
* parent supports `Node.prototype.moveBefore` (an atomic move that never detaches the node from the
|
|
342
|
+
* document); on the `before()` fallback (native absent) the nodes are detached + re-inserted, so identity
|
|
343
|
+
* is kept but focus/state are NOT (graceful degradation — the reconcile stays correct, only the focus
|
|
344
|
+
* nicety is absent). Covers a part holding text or a single nested template (the `repeat` item norm); a
|
|
345
|
+
* part holding an array/nested directive keeps deeper content in sub-anchors `#nodes` does not track.
|
|
346
|
+
*
|
|
347
|
+
* Caller invariant (repeat): `ref` and the moving `#nodes`/`#anchor` always share the same connected
|
|
348
|
+
* parent (the directive's container) — native `moveBefore`'s happy path, so no `try/catch` is needed; a
|
|
349
|
+
* future caller that violated same-parent/connected would get a loud `HierarchyRequestError`, not a
|
|
350
|
+
* silent mis-move.
|
|
351
|
+
*/
|
|
352
|
+
moveBefore(ref) {
|
|
353
|
+
const parent = ref.parentNode;
|
|
354
|
+
if (parent && 'moveBefore' in parent) {
|
|
355
|
+
// native atomic move: relocate WITHOUT leaving the document → focus/selection/state survive
|
|
356
|
+
const p = parent;
|
|
357
|
+
for (const node of this.#nodes)
|
|
358
|
+
p.moveBefore(node, ref);
|
|
359
|
+
p.moveBefore(this.#anchor, ref);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// fallback: detach+reinsert — identity preserved, focus/selection NOT (graceful degradation)
|
|
363
|
+
for (const node of this.#nodes)
|
|
364
|
+
ref.before(node);
|
|
365
|
+
ref.before(this.#anchor);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/** Commit a plain (non-directive) value through the text/template/array dispatch, honoring the Object.is skip. */
|
|
369
|
+
#commitChild(value) {
|
|
370
|
+
if (Object.is(value, this.#committed))
|
|
371
|
+
return; // Object.is skip — an equal (same-ref) hole writes no DOM
|
|
372
|
+
if (Array.isArray(value))
|
|
373
|
+
this.#commitArray(value);
|
|
374
|
+
else if (value instanceof TemplateResult)
|
|
375
|
+
this.#commitTemplate(value);
|
|
376
|
+
else
|
|
377
|
+
this.#commitText(value);
|
|
378
|
+
this.#committed = value;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Route a branded `DirectiveResult` to its instance: a same-ctor re-commit reuses the instance (state
|
|
382
|
+
* threads across commits of the same hole); a different ctor (or first entry) disposes the prior content
|
|
383
|
+
* + directive and constructs a fresh one. The instance's `update` either RETURNS a value committed through
|
|
384
|
+
* the normal child path, or `NO_COMMIT` when it drives its own DOM. The ctx (the host's connection scope)
|
|
385
|
+
* is handed through so a per-hole effect (`watch`) is scope-owned.
|
|
386
|
+
*/
|
|
387
|
+
#commitDirective(result) {
|
|
388
|
+
if (this.#directiveCtor !== result.ctor) {
|
|
389
|
+
this.#disposeDirective(); // dispose any prior directive (isolated) before swapping in a different one
|
|
390
|
+
this.#clear(); // clear leftover content (a value-producing directive's host-owned DOM)
|
|
391
|
+
this.#committed = UNCOMMITTED;
|
|
392
|
+
this.#directive = new result.ctor(this);
|
|
393
|
+
this.#directiveCtor = result.ctor;
|
|
394
|
+
}
|
|
395
|
+
const out = this.#directive.update(result.args, this.#ctx);
|
|
396
|
+
if (out === NO_COMMIT) {
|
|
397
|
+
this.#committed = result; // in directive mode; a fresh DirectiveResult each render never Object.is-skips
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
this.#commitChild(out); // a value-producing directive commits through the normal child path
|
|
401
|
+
}
|
|
402
|
+
/** Dispose the live directive, ISOLATING a throwing disposer (reported, not rethrown) so siblings tear down. */
|
|
403
|
+
#disposeDirective() {
|
|
404
|
+
const dir = this.#directive;
|
|
405
|
+
if (!dir)
|
|
406
|
+
return;
|
|
407
|
+
this.#directive = null;
|
|
408
|
+
this.#directiveCtor = null;
|
|
409
|
+
try {
|
|
410
|
+
dir.dispose();
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
reportDirectiveError(err);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
#commitText(value) {
|
|
417
|
+
const text = value == null ? '' : String(value);
|
|
418
|
+
const only = this.#nodes.length === 1 ? this.#nodes[0] : null;
|
|
419
|
+
if (only && only.nodeType === Node.TEXT_NODE) {
|
|
420
|
+
;
|
|
421
|
+
only.data = text; // reuse the text node — stable identity, in-place update
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
this.#clear();
|
|
425
|
+
const node = document.createTextNode(text);
|
|
426
|
+
this.#anchor.before(node);
|
|
427
|
+
this.#nodes = [node];
|
|
428
|
+
}
|
|
429
|
+
#commitTemplate(result) {
|
|
430
|
+
if (this.#nested && this.#nested.strings === result.strings) {
|
|
431
|
+
commitInstance(this.#nested, result.values, this.#ctx); // same call site → update parts in place (recursive skip)
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
this.#clear(); // a DIFFERENT template (or leaving text/array mode) → replace
|
|
435
|
+
const created = createInstance(result);
|
|
436
|
+
commitInstance(created.instance, result.values, this.#ctx);
|
|
437
|
+
this.#nodes = Array.from(created.fragment.childNodes); // capture BEFORE insertion empties the fragment
|
|
438
|
+
this.#anchor.before(created.fragment);
|
|
439
|
+
this.#nested = created.instance;
|
|
440
|
+
}
|
|
441
|
+
/** Position reconcile (NOT keyed — that is `repeat`, slice 5): append/remove/update each item by index. */
|
|
442
|
+
#commitArray(items) {
|
|
443
|
+
if (!this.#items) {
|
|
444
|
+
this.#clear(); // leaving text/template mode
|
|
445
|
+
this.#items = [];
|
|
446
|
+
}
|
|
447
|
+
const parts = this.#items;
|
|
448
|
+
while (parts.length < items.length) {
|
|
449
|
+
const anchor = document.createComment('');
|
|
450
|
+
this.#anchor.before(anchor); // each sub-part's anchor sits before this part's anchor, in order
|
|
451
|
+
parts.push(new _a(anchor));
|
|
452
|
+
}
|
|
453
|
+
while (parts.length > items.length)
|
|
454
|
+
parts.pop()?.dispose();
|
|
455
|
+
for (let i = 0; i < items.length; i++)
|
|
456
|
+
parts[i].commit(items[i], this.#ctx);
|
|
457
|
+
}
|
|
458
|
+
/** Clear CONTENT only (items / owned nodes / nested instance). The directive is torn down separately, at
|
|
459
|
+
* the real teardown points (leaving directive mode, swapping directives, disposing the part) — never on a
|
|
460
|
+
* value-producing directive's own #commitChild, which routes through here. */
|
|
461
|
+
#clear() {
|
|
462
|
+
if (this.#items) {
|
|
463
|
+
for (const item of this.#items)
|
|
464
|
+
item.dispose();
|
|
465
|
+
this.#items = null;
|
|
466
|
+
}
|
|
467
|
+
for (const node of this.#nodes)
|
|
468
|
+
node.remove();
|
|
469
|
+
this.#nodes = [];
|
|
470
|
+
this.#nested = null;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
_a = ChildPart;
|
|
474
|
+
/** An attribute hole: commits a string attribute; `null`/`undefined`/`false` remove it. */
|
|
475
|
+
class AttrPart {
|
|
476
|
+
#el;
|
|
477
|
+
#name;
|
|
478
|
+
#committed = UNCOMMITTED;
|
|
479
|
+
constructor(el, name) {
|
|
480
|
+
this.#el = el;
|
|
481
|
+
this.#name = name;
|
|
482
|
+
}
|
|
483
|
+
commit(value) {
|
|
484
|
+
if (Object.is(value, this.#committed))
|
|
485
|
+
return; // Object.is skip
|
|
486
|
+
this.#committed = value;
|
|
487
|
+
if (value == null || value === false)
|
|
488
|
+
this.#el.removeAttribute(this.#name);
|
|
489
|
+
else
|
|
490
|
+
this.#el.setAttribute(this.#name, String(value));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/** A `?bool` hole: toggles the boolean attribute's presence (truthy adds, falsy removes). */
|
|
494
|
+
class BooleanPart {
|
|
495
|
+
#el;
|
|
496
|
+
#name;
|
|
497
|
+
#committed = UNCOMMITTED;
|
|
498
|
+
constructor(el, name) {
|
|
499
|
+
this.#el = el;
|
|
500
|
+
this.#name = name;
|
|
501
|
+
}
|
|
502
|
+
commit(value) {
|
|
503
|
+
if (Object.is(value, this.#committed))
|
|
504
|
+
return; // Object.is skip
|
|
505
|
+
this.#committed = value;
|
|
506
|
+
this.#el.toggleAttribute(this.#name, Boolean(value));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/** A `.prop` hole: assigns the DOM PROPERTY (not an attribute). The name keeps its source camelCase. */
|
|
510
|
+
class PropertyPart {
|
|
511
|
+
#el;
|
|
512
|
+
#name;
|
|
513
|
+
#committed = UNCOMMITTED;
|
|
514
|
+
constructor(el, name) {
|
|
515
|
+
this.#el = el;
|
|
516
|
+
this.#name = name;
|
|
517
|
+
}
|
|
518
|
+
commit(value) {
|
|
519
|
+
if (Object.is(value, this.#committed))
|
|
520
|
+
return; // Object.is skip
|
|
521
|
+
this.#committed = value;
|
|
522
|
+
this.#el[this.#name] = value;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* An `@event` hole with STABLE listener identity: `addEventListener` is called once for a stable wrapper
|
|
527
|
+
* that forwards to the CURRENT handler, so re-renders never churn the platform listener (no remove/add,
|
|
528
|
+
* no event dropped mid-swap). A changed handler just swaps the internal ref; an `Object.is`-equal handler
|
|
529
|
+
* is a no-op; clearing to a non-function removes the listener.
|
|
530
|
+
*/
|
|
531
|
+
class EventPart {
|
|
532
|
+
#el;
|
|
533
|
+
#type;
|
|
534
|
+
#handler = null;
|
|
535
|
+
#attached = false;
|
|
536
|
+
#committed = UNCOMMITTED;
|
|
537
|
+
#listener = (event) => {
|
|
538
|
+
this.#handler?.(event);
|
|
539
|
+
};
|
|
540
|
+
constructor(el, type) {
|
|
541
|
+
this.#el = el;
|
|
542
|
+
this.#type = type;
|
|
543
|
+
}
|
|
544
|
+
commit(value) {
|
|
545
|
+
if (Object.is(value, this.#committed))
|
|
546
|
+
return; // Object.is skip — same handler, no churn
|
|
547
|
+
this.#committed = value;
|
|
548
|
+
this.#handler = typeof value === 'function' ? value : null;
|
|
549
|
+
if (this.#handler && !this.#attached) {
|
|
550
|
+
this.#el.addEventListener(this.#type, this.#listener); // the ONE, stable listener
|
|
551
|
+
this.#attached = true;
|
|
552
|
+
}
|
|
553
|
+
else if (!this.#handler && this.#attached) {
|
|
554
|
+
this.#el.removeEventListener(this.#type, this.#listener);
|
|
555
|
+
this.#attached = false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function assertNever(x) {
|
|
560
|
+
throw new Error(`template: unhandled part kind ${JSON.stringify(x)}`);
|
|
561
|
+
}
|
|
562
|
+
/** Pre-order (element + comment) node list — the same order `prepare` indexed, so manifest indices map. */
|
|
563
|
+
function collectNodes(root) {
|
|
564
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT);
|
|
565
|
+
const nodes = [];
|
|
566
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode())
|
|
567
|
+
nodes.push(n);
|
|
568
|
+
return nodes;
|
|
569
|
+
}
|
|
570
|
+
/** Clone a prepared template and bind a live part to each manifest entry. The fragment is the new content. */
|
|
571
|
+
function createInstance(result) {
|
|
572
|
+
const prepared = prepare(result.strings, result.svg);
|
|
573
|
+
const fragment = prepared.template.content.cloneNode(true);
|
|
574
|
+
const nodes = collectNodes(fragment);
|
|
575
|
+
const parts = prepared.parts.map((info) => {
|
|
576
|
+
const node = nodes[info.nodeIndex];
|
|
577
|
+
switch (info.kind) {
|
|
578
|
+
case 'child':
|
|
579
|
+
return new ChildPart(node);
|
|
580
|
+
case 'attr':
|
|
581
|
+
return new AttrPart(node, info.name);
|
|
582
|
+
case 'bool':
|
|
583
|
+
return new BooleanPart(node, info.name);
|
|
584
|
+
case 'prop':
|
|
585
|
+
return new PropertyPart(node, info.name);
|
|
586
|
+
case 'event':
|
|
587
|
+
return new EventPart(node, info.name);
|
|
588
|
+
default:
|
|
589
|
+
return assertNever(info);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
return { instance: { strings: result.strings, parts }, fragment };
|
|
593
|
+
}
|
|
594
|
+
/** Commit each value to its part (each part owns its own `Object.is` skip); thread the render context to directives. */
|
|
595
|
+
function commitInstance(instance, values, ctx) {
|
|
596
|
+
for (let i = 0; i < instance.parts.length; i++)
|
|
597
|
+
instance.parts[i].commit(values[i], ctx);
|
|
598
|
+
}
|
|
599
|
+
const RENDERED = new WeakMap();
|
|
600
|
+
/**
|
|
601
|
+
* Commit `result` into `container`. The first render of a given `strings` at a container instantiates the
|
|
602
|
+
* prepared template; a re-render with the SAME `strings` reuses that instance and writes only the changed
|
|
603
|
+
* holes (per-part `Object.is`). A different `strings` replaces the container's content. The optional `ctx`
|
|
604
|
+
* (the host's scope-owned `effect`) threads to directives so a per-hole effect (`watch`) is scope-owned;
|
|
605
|
+
* 2-arg calls keep working (`ctx` undefined → directives that need it simply have none).
|
|
606
|
+
*/
|
|
607
|
+
export function render(result, container, ctx) {
|
|
608
|
+
let instance = RENDERED.get(container);
|
|
609
|
+
if (!instance || instance.strings !== result.strings) {
|
|
610
|
+
container.textContent = ''; // this container is render-owned; clear before a fresh instantiation
|
|
611
|
+
const created = createInstance(result);
|
|
612
|
+
instance = created.instance;
|
|
613
|
+
container.appendChild(created.fragment);
|
|
614
|
+
RENDERED.set(container, instance);
|
|
615
|
+
}
|
|
616
|
+
commitInstance(instance, result.values, ctx);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Mount a kernel directive IMPERATIVELY (ADR-0023): commit `result` into `container` and return a teardown
|
|
620
|
+
* that disposes the directive (+ its sub-parts), clears its content, and removes the mount anchor. Reuses
|
|
621
|
+
* the SAME `ChildPart` engine `render()` rides — no parallel commit path: an anchor comment is appended to
|
|
622
|
+
* `container` (the directive's content commits as the anchor's previous siblings → inside `container`,
|
|
623
|
+
* leaving any pre-existing content untouched), a `ChildPart` is bound to it, and `result` is committed
|
|
624
|
+
* through the normal directive route. `ctx` threads the connection scope, so a per-hole effect (the `watch`
|
|
625
|
+
* pattern) stays scope-owned exactly as under `render()`. The public host for imperative consumers (the a2ui
|
|
626
|
+
* renderer) that need a kernel directive — e.g. `repeat` — WITHOUT the private `html\`\`` template entry.
|
|
627
|
+
*/
|
|
628
|
+
export function mount(result, container, ctx) {
|
|
629
|
+
const anchor = document.createComment('');
|
|
630
|
+
container.appendChild(anchor);
|
|
631
|
+
const part = new ChildPart(anchor);
|
|
632
|
+
part.commit(result, ctx);
|
|
633
|
+
return () => part.dispose();
|
|
634
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type DirectiveResult } from './template.js';
|
|
2
|
+
import type { ReadonlySignal } from '../reactive/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* `watch(source, mapper?)` — bind a child hole to a reactive `source` so only that hole updates when the
|
|
5
|
+
* source changes (the host render effect never re-runs for it). `source` is a `ReadonlySignal<T>` (read via
|
|
6
|
+
* `.value`) or a thunk `() => T` (the general form; a thunk tracks whatever signals it reads at run time).
|
|
7
|
+
* An optional `mapper` transforms the value before it is committed. Use inside a `UIElement.render()` so the
|
|
8
|
+
* host threads its connection scope; outside a host (a bare 2-arg `render`) there is no scope to own the
|
|
9
|
+
* effect and the hole stays empty.
|
|
10
|
+
*/
|
|
11
|
+
export declare function watch<T>(source: ReadonlySignal<T> | (() => T), mapper?: (value: T) => unknown): DirectiveResult;
|