@nativele/ts-dom 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,728 @@
1
+ import { a as attrNameOf, i as RESERVED_NAMES, r as holeName, t as HOLE_ANY } from "./holes-DTBtTZF_.mjs";
2
+ // src/lib/slots.ts
3
+ const slotOf = (n) => n instanceof Element ? n.getAttribute("slot") ?? "" : n instanceof Text && !n.data.trim() ? null : "";
4
+ /**
5
+ * <slot> for Light DOM: at creation time the user's child nodes are placed into the matching positions;
6
+ * afterwards a MutationObserver handles new children added directly to the element and removal of assigned nodes.
7
+ */
8
+ var LightSlots = class {
9
+ host;
10
+ onChange;
11
+ #slots = [];
12
+ #own = /* @__PURE__ */ new Set();
13
+ #observer;
14
+ constructor(host, onChange) {
15
+ this.host = host;
16
+ this.onChange = onChange;
17
+ this.#observer = new MutationObserver((records) => this.#flush(records));
18
+ }
19
+ project(frag, given) {
20
+ for (const n of frag.childNodes) this.#own.add(n);
21
+ for (const el of frag.querySelectorAll("slot")) {
22
+ const name = el.getAttribute("name") ?? "";
23
+ const nodes = given.filter((n) => slotOf(n) === name);
24
+ const start = document.createComment(`slot:${name}`);
25
+ const end = document.createComment(`/slot:${name}`);
26
+ this.#own.add(start).add(end);
27
+ const fallback = [...el.childNodes];
28
+ el.replaceWith(start, ...nodes.length ? nodes : fallback, end);
29
+ this.#slots.push({
30
+ name,
31
+ end,
32
+ fallback,
33
+ assigned: new Set(nodes)
34
+ });
35
+ }
36
+ }
37
+ /** Like the native slot.assignedNodes(): the nodes currently assigned to the slot (fallback content excluded). */
38
+ assigned(name) {
39
+ const slot = this.#slots.find((x) => x.name === name);
40
+ return slot ? [...slot.assigned] : [];
41
+ }
42
+ observe() {
43
+ this.#observer.observe(this.host, {
44
+ childList: true,
45
+ subtree: true,
46
+ attributes: true,
47
+ attributeFilter: ["slot"]
48
+ });
49
+ }
50
+ disconnect() {
51
+ this.#flush(this.#observer.takeRecords());
52
+ this.#observer.disconnect();
53
+ }
54
+ /** Put a node into its slot; with no matching slot it is not rendered, like the native behavior. */
55
+ #assign(n, changed) {
56
+ const target = this.#slots.find((s) => s.name === slotOf(n));
57
+ if (!target) {
58
+ if (slotOf(n) !== null) n.parentNode?.removeChild(n);
59
+ return;
60
+ }
61
+ if (target.assigned.size === 0) for (const f of target.fallback) f.parentNode?.removeChild(f);
62
+ target.assigned.add(n);
63
+ target.end.before(n);
64
+ changed.add(target);
65
+ }
66
+ #unassign(n, slot, changed) {
67
+ slot.assigned.delete(n);
68
+ changed.add(slot);
69
+ if (slot.assigned.size === 0) for (const f of slot.fallback) slot.end.before(f);
70
+ }
71
+ #flush(records) {
72
+ const changed = /* @__PURE__ */ new Set();
73
+ for (const r of records) {
74
+ if (r.type === "attributes") {
75
+ const n = r.target;
76
+ const current = this.#slots.find((s) => s.assigned.has(n));
77
+ if (!current || current.name === slotOf(n)) continue;
78
+ this.#unassign(n, current, changed);
79
+ this.#assign(n, changed);
80
+ continue;
81
+ }
82
+ for (const n of r.removedNodes) {
83
+ if (n.parentNode) continue;
84
+ for (const s of this.#slots) if (s.assigned.has(n)) this.#unassign(n, s, changed);
85
+ }
86
+ if (r.target !== this.host) continue;
87
+ for (const n of r.addedNodes) {
88
+ if (this.#own.has(n) || this.#slots.some((s) => s.assigned.has(n))) continue;
89
+ this.#assign(n, changed);
90
+ }
91
+ }
92
+ for (const s of changed) this.onChange(s.name);
93
+ }
94
+ };
95
+ // src/lib/style.ts
96
+ /** Limit scoped styles to a tag: use the native @scope when available, otherwise fall back to CSS nesting. */
97
+ function scopeCss(css, tag) {
98
+ if (typeof CSSScopeRule !== "undefined") return `@scope (${tag}) {\n${css}\n}`;
99
+ return `${tag} {\n${css.replaceAll(":scope", "&")}\n}`;
100
+ }
101
+ /** Take the template's top-level <style> elements out of the template (they are removed from it). */
102
+ function extractSheets(content) {
103
+ const sheets = [];
104
+ for (const child of [...content.children]) {
105
+ if (child.localName !== "style") continue;
106
+ sheets.push({
107
+ css: child.textContent ?? "",
108
+ scoped: child.hasAttribute("scoped")
109
+ });
110
+ child.remove();
111
+ }
112
+ return sheets;
113
+ }
114
+ /** Inject every stylesheet only once per document. */
115
+ function createInjector(sheets) {
116
+ const injected = /* @__PURE__ */ new WeakMap();
117
+ return (host) => {
118
+ if (!sheets.length) return;
119
+ const doc = host.ownerDocument;
120
+ const done = injected.get(doc) ?? /* @__PURE__ */ new Set();
121
+ injected.set(doc, done);
122
+ const tag = host.localName;
123
+ for (const [i, { css, scoped }] of sheets.entries()) {
124
+ const key = scoped ? `${tag}#${i}` : `#${i}`;
125
+ if (done.has(key)) continue;
126
+ done.add(key);
127
+ if (!scoped && css.includes(":host")) console.warn(`<${tag}>: [host-in-light-dom] :host has no effect in Light DOM. Fix: select the element by its tag name (${tag} { ... }) or use <style scoped> with :scope, or opt into Shadow DOM with { shadow: true }.`);
128
+ const el = doc.createElement("style");
129
+ el.dataset.tsDom = scoped ? tag : "";
130
+ el.textContent = scoped ? scopeCss(css, tag) : css;
131
+ doc.head.append(el);
132
+ }
133
+ };
134
+ }
135
+ // src/lib/cond.ts
136
+ /** Collect the data-if elements of a subtree, leaving a comment as placeholder. */
137
+ function mountConds(root) {
138
+ const map = /* @__PURE__ */ new Map();
139
+ for (const el of root.querySelectorAll("[data-if]")) {
140
+ const name = el.getAttribute("data-if");
141
+ const anchor = document.createComment(`if:${name}`);
142
+ el.before(anchor);
143
+ map.set(name, [...map.get(name) ?? [], {
144
+ el,
145
+ anchor
146
+ }]);
147
+ }
148
+ const show = (name, on) => {
149
+ for (const { el, anchor } of map.get(name) ?? []) if (on) {
150
+ if (!el.isConnected && el.parentNode === null) anchor.after(el);
151
+ } else el.remove();
152
+ };
153
+ return {
154
+ names: new Set(map.keys()),
155
+ show,
156
+ hideAll() {
157
+ for (const name of map.keys()) show(name, false);
158
+ }
159
+ };
160
+ }
161
+ // src/lib/text.ts
162
+ /** Split every {{ name }} in a subtree into its own Text node and return a writable `text` object. */
163
+ function mountHoles(root) {
164
+ const nodes = /* @__PURE__ */ new Map();
165
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
166
+ const targets = [];
167
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
168
+ const parent = n.parentElement;
169
+ if (parent && /^(style|script)$/i.test(parent.localName)) continue;
170
+ if (n.data.includes("{{")) targets.push(n);
171
+ }
172
+ for (const t of targets) {
173
+ const src = t.data;
174
+ const parts = [];
175
+ let last = 0;
176
+ for (const m of src.matchAll(HOLE_ANY)) {
177
+ const name = holeName(m[1] ?? "");
178
+ if (!name) continue;
179
+ if (m.index > last) parts.push(document.createTextNode(src.slice(last, m.index)));
180
+ const hole = document.createTextNode("");
181
+ parts.push(hole);
182
+ nodes.set(name, [...nodes.get(name) ?? [], hole]);
183
+ last = m.index + m[0].length;
184
+ }
185
+ if (last === 0 && parts.length === 0) continue;
186
+ if (last < src.length) parts.push(document.createTextNode(src.slice(last)));
187
+ t.replaceWith(...parts);
188
+ }
189
+ const values = /* @__PURE__ */ new Map();
190
+ const text = {};
191
+ for (const [name, list] of nodes) Object.defineProperty(text, name, {
192
+ enumerable: true,
193
+ get: () => values.get(name),
194
+ set(v) {
195
+ values.set(name, v);
196
+ const s = v == null || v === false ? "" : String(v);
197
+ for (const node of list) if (node.data !== s) node.data = s;
198
+ }
199
+ });
200
+ return text;
201
+ }
202
+ // src/lib/list.ts
203
+ /**
204
+ * A one-dimensional keyed list. Reordering uses `moveBefore` where the browser has it, so a moved item (a component, a playing video, a focused input) is not disconnected. It only compares keys, never subtrees;
205
+ * the content of an item is written explicitly by the user's update callback.
206
+ */
207
+ var List = class {
208
+ anchor;
209
+ proto;
210
+ onCreate;
211
+ #items = /* @__PURE__ */ new Map();
212
+ #byEl = /* @__PURE__ */ new WeakMap();
213
+ constructor(anchor, proto, onCreate) {
214
+ this.anchor = anchor;
215
+ this.proto = proto;
216
+ this.onCreate = onCreate;
217
+ }
218
+ /** From any node inside an item, find the key and data of the item it belongs to (used by event handlers). */
219
+ itemOf(node) {
220
+ for (let n = node; n; n = n.parentNode) {
221
+ const hit = n instanceof Element ? this.#byEl.get(n) : void 0;
222
+ if (hit) return hit;
223
+ }
224
+ }
225
+ get size() {
226
+ return this.#items.size;
227
+ }
228
+ has(key) {
229
+ return this.#items.has(key);
230
+ }
231
+ get(key) {
232
+ return this.#items.get(key);
233
+ }
234
+ #create(key) {
235
+ const el = this.proto.cloneNode(true);
236
+ const holder = document.createDocumentFragment();
237
+ holder.append(el);
238
+ const conds = mountConds(holder);
239
+ const text = mountHoles(holder);
240
+ conds.hideAll();
241
+ const show = conds.show;
242
+ el.remove();
243
+ const refs = {};
244
+ for (const n of [el, ...el.querySelectorAll("[data-ref]")]) {
245
+ const name = n.getAttribute("data-ref");
246
+ if (name) refs[name] = n;
247
+ }
248
+ this.onCreate?.(el);
249
+ const entry = {
250
+ el,
251
+ refs,
252
+ text,
253
+ show,
254
+ item: void 0,
255
+ fresh: true
256
+ };
257
+ this.#byEl.set(el, {
258
+ key,
259
+ item: void 0
260
+ });
261
+ return entry;
262
+ }
263
+ /** Add or update a single item; a new item is appended at the end. */
264
+ upsert(key, item, update) {
265
+ let entry = this.#items.get(key);
266
+ if (!entry) {
267
+ entry = this.#create(key);
268
+ this.#items.set(key, entry);
269
+ this.anchor.parentNode?.insertBefore(entry.el, this.anchor);
270
+ }
271
+ this.#run(entry, key, item, update);
272
+ }
273
+ #run(entry, key, item, update, skip = false) {
274
+ if (skip && !entry.fresh && Object.is(entry.item, item)) return;
275
+ entry.item = item;
276
+ entry.fresh = false;
277
+ this.#byEl.set(entry.el, {
278
+ key,
279
+ item
280
+ });
281
+ update(item, entry);
282
+ }
283
+ remove(key) {
284
+ const entry = this.#items.get(key);
285
+ if (!entry) return false;
286
+ entry.el.remove();
287
+ this.#items.delete(key);
288
+ return true;
289
+ }
290
+ clear() {
291
+ for (const { el } of this.#items.values()) el.remove();
292
+ this.#items.clear();
293
+ }
294
+ /** Keyed reconciliation: remove extra items, create missing ones, reorder, and call update() for every item. */
295
+ replace(items, key, update, opts = {}) {
296
+ const next = /* @__PURE__ */ new Set();
297
+ for (const it of items) {
298
+ const k = key(it);
299
+ if (next.has(k)) throw new Error(`[duplicate-key] replace() received two items with the key ${String(k)}, so one element cannot show both. Fix: make key() return a unique value for each item.`);
300
+ next.add(k);
301
+ }
302
+ for (const k of this.#items.keys()) if (!next.has(k)) this.remove(k);
303
+ const parent = this.anchor.parentNode;
304
+ let after = this.anchor;
305
+ for (let i = items.length - 1; i >= 0; i--) {
306
+ const it = items[i];
307
+ const k = key(it);
308
+ let entry = this.#items.get(k);
309
+ if (!entry) {
310
+ entry = this.#create(k);
311
+ this.#items.set(k, entry);
312
+ }
313
+ if (entry.el.nextSibling !== after || entry.el.parentNode !== parent) {
314
+ if (entry.el.parentNode === parent && parent?.moveBefore) parent.moveBefore(entry.el, after);
315
+ else parent?.insertBefore(entry.el, after);
316
+ }
317
+ after = entry.el;
318
+ this.#run(entry, k, it, update, opts.skipUnchanged);
319
+ }
320
+ }
321
+ };
322
+ // src/lib/mount.ts
323
+ /**
324
+ * Wire up a built template: `data-each` lists, `data-ref` elements, `data-if` conditions and `{{ }}` holes.
325
+ * Conditions stay where they are until the caller runs `conds.hideAll()`, so a caller that still moves nodes around (slots) can do that first.
326
+ */
327
+ function mountTemplate(frag, onItem) {
328
+ const lists = {};
329
+ for (const t of frag.querySelectorAll("template[data-each]")) {
330
+ const name = t.getAttribute("data-each");
331
+ const proto = t.content.firstElementChild;
332
+ if (!proto) throw new Error(`[each-root-count] data-each="${name}" needs exactly one root element. Fix: wrap the item content in a single element such as <li>.`);
333
+ const anchor = document.createComment(`each:${name}`);
334
+ t.replaceWith(anchor);
335
+ lists[name] = new List(anchor, proto, onItem);
336
+ }
337
+ const refs = {};
338
+ for (const el of frag.querySelectorAll("[data-ref]")) refs[el.getAttribute("data-ref")] = el;
339
+ return {
340
+ refs,
341
+ lists,
342
+ text: mountHoles(frag),
343
+ conds: mountConds(frag)
344
+ };
345
+ }
346
+ // src/lib/component.ts
347
+ function event(opts = {}) {
348
+ return { bubbles: opts.bubbles };
349
+ }
350
+ const NON_BUBBLING = /* @__PURE__ */ new Set([
351
+ "focus",
352
+ "blur",
353
+ "mouseenter",
354
+ "mouseleave",
355
+ "pointerenter",
356
+ "pointerleave",
357
+ "load",
358
+ "error",
359
+ "scroll",
360
+ "toggle"
361
+ ]);
362
+ function Component(tpl, opts = {}) {
363
+ const shadow = opts.shadow === true;
364
+ const hasState = opts.state !== void 0 || opts.props !== void 0 || opts.derive !== void 0;
365
+ const propDefaults = opts.props ?? {};
366
+ const propKeys = Object.keys(propDefaults);
367
+ const stateKeys = Object.keys(opts.state ?? {});
368
+ for (const k of propKeys) {
369
+ if (k in HTMLElement.prototype || RESERVED_NAMES.has(k)) throw new Error(`[reserved-prop] props.${k} has the same name as a member of HTMLElement or ts-dom, which it would shadow. Fix: rename the prop (e.g. ${k}Text).`);
370
+ if (stateKeys.includes(k)) throw new Error(`[conflicting-keys] "${k}" is declared in both state and props. Data must have exactly one owner. Fix: keep it in props (the parent owns it) or in state (the component owns it), not both.`);
371
+ }
372
+ for (const k of Object.keys(opts.derive ?? {})) if (stateKeys.includes(k) || propKeys.includes(k)) throw new Error(`[conflicting-keys] derive.${k} has the same name as a state or props field. Fix: rename the derived value.`);
373
+ for (const k of Object.keys(opts.emits ?? {})) if (`on${k}` in HTMLElement.prototype) throw new Error(`[native-event-name] emits.${k} has the same name as a native DOM event. Fix: rename the event (for example with a prefix such as "field-${k}").`);
374
+ const attrProps = /* @__PURE__ */ new Map();
375
+ for (const k of propKeys) {
376
+ const t = typeof propDefaults[k];
377
+ if (t === "string" || t === "number" || t === "boolean") attrProps.set(attrNameOf(k), {
378
+ key: k,
379
+ kind: t
380
+ });
381
+ }
382
+ const content = tpl.build();
383
+ const injectStyles = createInjector(shadow ? [] : extractSheets(content));
384
+ const eventTypes = /* @__PURE__ */ new Set();
385
+ const methodNames = /* @__PURE__ */ new Set();
386
+ (function scan(root) {
387
+ for (const el of root.querySelectorAll("*")) {
388
+ for (const a of el.attributes) {
389
+ if (!a.name.startsWith("data-on-")) continue;
390
+ eventTypes.add(a.name.slice(8));
391
+ methodNames.add(a.value.trim());
392
+ }
393
+ if (el instanceof HTMLTemplateElement) scan(el.content);
394
+ }
395
+ })(content);
396
+ const eventSelector = [...eventTypes].map((t) => `[data-on-${t}]`).join(",");
397
+ class Base extends HTMLElement {
398
+ refs = {};
399
+ lists = {};
400
+ text = {};
401
+ root;
402
+ #show = () => {};
403
+ #condNames = /* @__PURE__ */ new Set();
404
+ #slots = null;
405
+ #cleanups = [];
406
+ #owned = /* @__PURE__ */ new WeakMap();
407
+ #built = false;
408
+ #started = false;
409
+ #state = { ...opts.state };
410
+ #props = { ...propDefaults };
411
+ #derived = {};
412
+ #ready = false;
413
+ #dirtyFrom = null;
414
+ #batchDepth = 0;
415
+ #rendering = false;
416
+ static {
417
+ for (const k of propKeys) Object.defineProperty(this.prototype, k, {
418
+ configurable: true,
419
+ enumerable: true,
420
+ get() {
421
+ return this.#props[k];
422
+ },
423
+ set(v) {
424
+ this.#setProp(k, v);
425
+ }
426
+ });
427
+ }
428
+ static get observedAttributes() {
429
+ return [...attrProps.keys()];
430
+ }
431
+ constructor() {
432
+ super();
433
+ for (const k of propKeys) if (Object.hasOwn(this, k)) {
434
+ const v = this[k];
435
+ delete this[k];
436
+ this.#props[k] = v;
437
+ }
438
+ this.root = shadow ? this.attachShadow({ mode: "open" }) : this;
439
+ if (shadow) this.#build();
440
+ }
441
+ #build() {
442
+ if (this.#built) return;
443
+ this.#built = true;
444
+ if (!shadow) injectStyles(this);
445
+ const given = shadow ? [] : [...this.childNodes];
446
+ if (!shadow) this.replaceChildren();
447
+ const frag = content.cloneNode(true);
448
+ const mounted = mountTemplate(frag, (el) => this.#register(el));
449
+ this.lists = mounted.lists;
450
+ this.refs = mounted.refs;
451
+ this.#register(frag);
452
+ const conds = mounted.conds;
453
+ this.#show = conds.show;
454
+ this.#condNames = conds.names;
455
+ this.text = mounted.text;
456
+ if (!shadow) {
457
+ this.#slots = new LightSlots(this, (name) => this.slotchange(name));
458
+ this.#slots.project(frag, given);
459
+ }
460
+ conds.hideAll();
461
+ this.root.append(frag);
462
+ for (const type of eventTypes) {
463
+ const nativeBubbling = `on${type}` in HTMLElement.prototype && !NON_BUBBLING.has(type);
464
+ this.root.addEventListener(type, (e) => this.#dispatch(e), !nativeBubbling);
465
+ }
466
+ }
467
+ /** Register the data-on-* attributes in a subtree (only elements from our own template count, not user slot content). */
468
+ #register(root) {
469
+ if (!eventSelector) return;
470
+ const els = root.querySelectorAll(eventSelector);
471
+ for (const el of root instanceof Element && root.matches(eventSelector) ? [root, ...els] : els) {
472
+ const map = /* @__PURE__ */ new Map();
473
+ for (const a of el.attributes) if (a.name.startsWith("data-on-")) map.set(a.name.slice(8), a.value.trim());
474
+ this.#owned.set(el, map);
475
+ }
476
+ }
477
+ #dispatch(e) {
478
+ const bubbling = e.bubbles;
479
+ for (let n = e.target; n && n !== this.root; n = n.parentNode) {
480
+ if (n instanceof Element) {
481
+ const method = this.#owned.get(n)?.get(e.type);
482
+ if (method) {
483
+ const fn = this[method];
484
+ if (typeof fn !== "function") throw new Error(`<${this.localName}>: [missing-handler] data-on-${e.type}="${method}" refers to a method this component does not have. Fix: add a public method ${method}(e, hit) to the class.`);
485
+ let hit;
486
+ for (const l of Object.values(this.lists)) if (hit = l.itemOf(n)) break;
487
+ fn.call(this, e, {
488
+ el: n,
489
+ key: hit?.key,
490
+ item: hit?.item
491
+ });
492
+ if (e.cancelBubble) return;
493
+ }
494
+ }
495
+ if (!bubbling) return;
496
+ }
497
+ }
498
+ get state() {
499
+ return this.#state;
500
+ }
501
+ get props() {
502
+ return this.#props;
503
+ }
504
+ emit(name, detail) {
505
+ const decl = opts.emits?.[name];
506
+ if (!decl) throw new Error(`<${this.localName}>: [undeclared-event] emit("${name}") was called but the event is not declared. Fix: add it to the emits option: emits: { "${name}": event<Detail>() }.`);
507
+ this.dispatchEvent(new CustomEvent(name, {
508
+ detail,
509
+ bubbles: decl.bubbles === true,
510
+ composed: false
511
+ }));
512
+ }
513
+ set(patch) {
514
+ const p = typeof patch === "function" ? patch(this.#state) : patch;
515
+ let next = this.#state;
516
+ for (const k of Object.keys(p)) {
517
+ if (Object.hasOwn(this.#props, k)) throw new Error(`<${this.localName}>: [props-mutation] "${k}" is a prop controlled by the parent, so set() cannot change it. Fix: emit an event so the parent changes it, or declare it in state instead.`);
518
+ if (Object.is(next[k], p[k])) continue;
519
+ if (next === this.#state) next = { ...next };
520
+ next[k] = p[k];
521
+ }
522
+ if (next === this.#state) return;
523
+ this.#dirtyFrom ??= this.#merged();
524
+ this.#state = next;
525
+ this.#flushIfIdle();
526
+ }
527
+ #merged() {
528
+ return {
529
+ ...this.#state,
530
+ ...this.#props
531
+ };
532
+ }
533
+ #setProp(key, value) {
534
+ if (Object.is(this.#props[key], value)) return;
535
+ this.#dirtyFrom ??= this.#merged();
536
+ this.#props = {
537
+ ...this.#props,
538
+ [key]: value
539
+ };
540
+ this.#flushIfIdle();
541
+ }
542
+ attributeChangedCallback(attr, _old, value) {
543
+ const p = attrProps.get(attr);
544
+ if (!p) return;
545
+ const def = propDefaults[p.key];
546
+ let v;
547
+ if (p.kind === "boolean") v = value !== null && value !== "false";
548
+ else if (p.kind === "number") {
549
+ v = value === null ? def : Number(value);
550
+ if (Number.isNaN(v)) {
551
+ console.warn(`<${this.localName}>: [invalid-prop-value] the attribute ${attr}="${value}" is not a number; using the default value instead. Fix: write a numeric value such as ${attr}="5".`);
552
+ v = def;
553
+ }
554
+ } else v = value ?? def;
555
+ this.#setProp(p.key, v);
556
+ }
557
+ batch(fn) {
558
+ this.#batchDepth++;
559
+ try {
560
+ fn();
561
+ } finally {
562
+ this.#batchDepth--;
563
+ this.#flushIfIdle();
564
+ }
565
+ }
566
+ #flushIfIdle() {
567
+ if (this.#ready && this.#batchDepth === 0 && !this.#rendering) this.#flush();
568
+ }
569
+ #flush() {
570
+ for (let guard = 0; this.#dirtyFrom; guard++) {
571
+ if (guard >= 50) {
572
+ this.#dirtyFrom = null;
573
+ throw new Error(`<${this.localName}>: [render-loop] render() keeps calling set() and the state never settles (50 passes). Fix: only call set() inside render() when the value actually changes, or move the logic into derive.`);
574
+ }
575
+ const from = this.#dirtyFrom;
576
+ this.#dirtyFrom = null;
577
+ this.#rendering = true;
578
+ try {
579
+ this.#apply(from, this.#merged());
580
+ } finally {
581
+ this.#rendering = false;
582
+ }
583
+ }
584
+ }
585
+ /** Bind same-named {{ holes }} and data-if automatically, then call render(). `from` is null for the first render (all fields). */
586
+ #apply(from, to) {
587
+ const keys = from ? [.../* @__PURE__ */ new Set([...Object.keys(from), ...Object.keys(to)])].filter((k) => !Object.is(from[k], to[k])) : Object.keys(to);
588
+ if (!keys.length) return;
589
+ const bindName = (name, value) => {
590
+ if (Object.hasOwn(this.text, name)) this.text[name] = value;
591
+ if (this.#condNames.has(name)) this.#show(name, Boolean(value));
592
+ };
593
+ for (const k of keys) bindName(k, to[k]);
594
+ const derived = {};
595
+ for (const [name, fn] of Object.entries(opts.derive ?? {})) {
596
+ const v = fn(to);
597
+ derived[name] = v;
598
+ if (!from || !Object.is(this.#derived[name], v)) bindName(name, v);
599
+ }
600
+ this.#derived = derived;
601
+ this.render({
602
+ ...to,
603
+ ...derived
604
+ }, new Set(keys));
605
+ }
606
+ show(name, on) {
607
+ this.#show(name, on);
608
+ }
609
+ bind(store, fn) {
610
+ fn(store.get());
611
+ this.#cleanups.push(store.subscribe(fn));
612
+ }
613
+ on(target, type, fn, opts) {
614
+ const l = fn;
615
+ target.addEventListener(type, l, opts);
616
+ const off = () => target.removeEventListener(type, l, opts);
617
+ this.#cleanups.push(off);
618
+ return off;
619
+ }
620
+ onCleanup(fn) {
621
+ this.#cleanups.push(fn);
622
+ }
623
+ connectedCallback() {
624
+ if (!shadow && !this.#built && document.readyState === "loading") {
625
+ document.addEventListener("DOMContentLoaded", () => this.isConnected && this.#start(), { once: true });
626
+ return;
627
+ }
628
+ this.#start();
629
+ }
630
+ #start() {
631
+ this.#build();
632
+ if (!this.#started) {
633
+ this.#started = true;
634
+ for (const m of methodNames) if (typeof this[m] !== "function") throw new Error(`<${this.localName}>: [missing-handler] the HTML refers to the method ${m} (data-on-*), but the component does not have it. Fix: add a public method ${m}(e, hit) to the class.`);
635
+ if (hasState) {
636
+ this.#ready = true;
637
+ this.#dirtyFrom = null;
638
+ this.#rendering = true;
639
+ try {
640
+ this.#apply(null, this.#merged());
641
+ } finally {
642
+ this.#rendering = false;
643
+ }
644
+ this.#flush();
645
+ }
646
+ }
647
+ this.#slots?.observe();
648
+ this.connected();
649
+ }
650
+ /** Opts in to atomic moves (`moveBefore`): a moved component stays connected, keeping its state, focus and subscriptions. */
651
+ connectedMoveCallback() {}
652
+ disconnectedCallback() {
653
+ this.#slots?.disconnect();
654
+ for (const fn of this.#cleanups.splice(0)) fn();
655
+ this.disconnected();
656
+ }
657
+ connected() {}
658
+ disconnected() {}
659
+ slotchange(_name) {}
660
+ render(_view, _changed) {}
661
+ assignedNodes(name) {
662
+ if (this.#slots) return this.#slots.assigned(name);
663
+ const sel = name ? `slot[name="${name.replaceAll("\"", "\\\"")}"]` : "slot:not([name])";
664
+ return this.shadowRoot?.querySelector(sel)?.assignedNodes() ?? [];
665
+ }
666
+ }
667
+ return Base;
668
+ }
669
+ /**
670
+ * Register a custom element. Also checks types: every method name referenced by data-on-* in the HTML must exist on the
671
+ * component as a public method, otherwise tsc reports an error that lists the missing names (`missingHandlers`).
672
+ */
673
+ function define(name, ctor) {
674
+ if (!customElements.get(name)) customElements.define(name, ctor);
675
+ }
676
+ // src/lib/store.ts
677
+ /** A tiny store: no dependency tracking, only get / set / subscribe. */
678
+ function createStore(initial) {
679
+ let value = initial;
680
+ const subs = /* @__PURE__ */ new Set();
681
+ return {
682
+ get: () => value,
683
+ set(next) {
684
+ const v = typeof next === "function" ? next(value) : next;
685
+ if (Object.is(v, value)) return;
686
+ value = v;
687
+ for (const fn of [...subs]) fn(value);
688
+ },
689
+ subscribe(fn) {
690
+ subs.add(fn);
691
+ return () => void subs.delete(fn);
692
+ }
693
+ };
694
+ }
695
+ // src/lib/template.ts
696
+ function template(nodes) {
697
+ return {
698
+ build: () => buildNodes(nodes),
699
+ instantiate() {
700
+ const root = buildNodes(nodes);
701
+ const { refs, lists, text, conds } = mountTemplate(root);
702
+ conds.hideAll();
703
+ return {
704
+ root,
705
+ refs,
706
+ lists,
707
+ text,
708
+ show: conds.show
709
+ };
710
+ }
711
+ };
712
+ }
713
+ function buildNodes(nodes) {
714
+ const frag = document.createDocumentFragment();
715
+ for (const node of nodes) frag.append(buildNode(node));
716
+ return frag;
717
+ }
718
+ function buildNode(node) {
719
+ if (typeof node === "string") return document.createTextNode(node);
720
+ if (node[0] === "#comment") return document.createComment(node[1]);
721
+ const [tag, attrs, children, ns] = node;
722
+ const el = ns ? document.createElementNS(ns, tag) : document.createElement(tag);
723
+ for (const name in attrs) el.setAttribute(name, attrs[name]);
724
+ const into = el instanceof HTMLTemplateElement ? el.content : el;
725
+ for (const child of children) into.append(buildNode(child));
726
+ return el;
727
+ }
728
+ export { Component, List, createStore, define, event, template };