@twobirds/microcomponents 0.9.0 → 0.10.0

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.
Files changed (45) hide show
  1. package/README.md +2 -16
  2. package/dist/cli/bundle.d.ts +106 -0
  3. package/dist/cli/bundle.d.ts.map +1 -0
  4. package/dist/cli/bundler.d.ts +44 -0
  5. package/dist/cli/bundler.d.ts.map +1 -0
  6. package/dist/cli/create.d.ts.map +1 -1
  7. package/dist/cli/dev.d.ts +87 -0
  8. package/dist/cli/dev.d.ts.map +1 -0
  9. package/dist/cli/dispatch.d.ts +6 -2
  10. package/dist/cli/dispatch.d.ts.map +1 -1
  11. package/dist/cli/index.js +1807 -1
  12. package/dist/cli/inliner.d.ts +42 -0
  13. package/dist/cli/inliner.d.ts.map +1 -0
  14. package/dist/cli/preview.d.ts +48 -0
  15. package/dist/cli/preview.d.ts.map +1 -0
  16. package/dist/core/elements.js +1294 -75
  17. package/dist/core/mcGuards.js +5 -8
  18. package/dist/mcp/api.json +2 -17
  19. package/dist/mcp/concepts/project-layout.md +9 -8
  20. package/dist/mcp/index.js +450 -230
  21. package/dist/microcomponents.js +2882 -9
  22. package/dist/state/formValues.js +2236 -281
  23. package/dist/state/observables.js +1794 -342
  24. package/dist/utils/events.js +171 -197
  25. package/package.json +3 -7
  26. package/dist/cli/create.js +0 -653
  27. package/dist/cli/dispatch.js +0 -316
  28. package/dist/cli/scaffold.js +0 -109
  29. package/dist/cli/writeScaffold.js +0 -114
  30. package/dist/core/MC.js +0 -837
  31. package/dist/core/index.js +0 -72
  32. package/dist/state/index.js +0 -5
  33. package/dist/state/placeholders.js +0 -187
  34. package/dist/types/index.js +0 -1
  35. package/dist/types/mcTypes.js +0 -9
  36. package/dist/utils/autoload.js +0 -160
  37. package/dist/utils/helpers.js +0 -532
  38. package/dist/utils/index.js +0 -12
  39. package/dist/utils/resolveComponentPath.js +0 -39
  40. package/dist/vite/index.d.ts +0 -7
  41. package/dist/vite/index.d.ts.map +0 -1
  42. package/dist/vite/index.js +0 -6
  43. package/dist/vite/loadAssets.d.ts +0 -36
  44. package/dist/vite/loadAssets.d.ts.map +0 -1
  45. package/dist/vite/loadAssets.js +0 -221
@@ -1,9 +1,2882 @@
1
- 'use strict';
2
- export * from './utils/helpers.js';
3
- export * from './core/mcGuards.js';
4
- export * from './core/MC.js';
5
- export * from './utils/autoload.js';
6
- export * from './utils/events.js';
7
- export * from './core/elements.js';
8
- export * from './state/observables.js';
9
- export * from './state/formValues.js';
1
+ //#region src/utils/events.ts
2
+ /**
3
+ @module Events
4
+
5
+ This module provides event utilities for interfacing **between microcomponents and external
6
+ frameworks or native DOM/JS environments**. It is not intended for communication between
7
+ frontend microcomponents themselves — for that purpose, use the inherited methods
8
+ ( `.on()`, `.one()`, `.off()`, `.trigger()`) that exist when you extend `DC`.
9
+
10
+ Import this module when you need to exchange information with microcomponents from outside
11
+ the microcomponent system (e.g., vanilla JS, React via `useRef()`, or other frameworks).
12
+
13
+ The core feature is the ability to explicitly control event flow direction when using {@link trigger}:
14
+ - upward propagation to ancestor components ('u')
15
+ - local dispatch only ('l') [default]
16
+ - downward propagation to descendant components ('d').
17
+
18
+ @example
19
+ html:
20
+ ```html
21
+ <div class="grandparent">
22
+ <div class="parent">
23
+ <div class="child">
24
+ </div>
25
+ </div>
26
+ </div>
27
+ ```
28
+
29
+ javascript:
30
+ ```javascript
31
+ import { on, one, trigger, off } from '@twobirds/microcomponents/events';
32
+
33
+ const grandParent = document.querySelector('.grandparent');
34
+ const parent = document.querySelector('.parent');
35
+ const child = document.querySelector('.child');
36
+ if (!grandParent || !parent || !child) throw new Error('example markup missing');
37
+
38
+ const grandParentHandler = on(grandParent, 'dataReady', (ev) => console.log(ev.detail.data));
39
+
40
+ const parentHandler = on(parent, 'dataReady', (ev) => ev.detail.control.stop());
41
+
42
+ trigger(child, 'dataReady', { value: 42 }, 'u');
43
+
44
+ off(parent, 'dataReady', parentHandler);
45
+ off(grandParent, 'dataReady', grandParentHandler);
46
+ ```
47
+
48
+ The `dataReady` event bubbles upward from the innermost `div`, but `parentHandler`
49
+ calls `ev.detail.control.stop()`, so the `grandParent` handler is never reached.
50
+ Handlers are detached with `off()` once they are no longer needed.
51
+ */
52
+ /**
53
+ * Converts a standard event name to its microcomponent-prefixed variant.
54
+ *
55
+ * This utility is used internally to namespace custom events (e.g., 'Click' → '_mcClick').
56
+ *
57
+ * @param eventName - The original event name (e.g., 'click', 'update').
58
+ * @returns The microcomponent-prefixed event name.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * console.log(getMcEventName('update'));
63
+ * ```
64
+ */
65
+ function getMcEventName(eventName) {
66
+ return "_mc" + eventName[0].toUpperCase() + eventName.substring(1);
67
+ }
68
+ /**
69
+ * Returns the correct event name to listen for on a DOM element.
70
+ *
71
+ * If `eventName` (case-insensitive) matches a native HTML event handler property
72
+ * (`'on' + eventName.toLowerCase()`) on the target, returns the lowercase native
73
+ * name so that standard DOM events like `'click'` are listened to correctly.
74
+ * Otherwise, prefixes with `_mc` for microcomponent custom events.
75
+ *
76
+ * @hidden
77
+ * @param target - The DOM element whose native event support is checked.
78
+ * @param eventName - The event name as provided by the caller (e.g., `'click'`, `'Update'`).
79
+ * @returns The resolved event name: lowercase native or `_mc`-prefixed custom.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * const btn = document.createElement('button');
84
+ * console.log(trueEventName(btn, 'Click'));
85
+ * console.log(trueEventName(btn, 'Update'));
86
+ * ```
87
+ *
88
+ * `'Click'` resolves to the native `'click'` because the button has an `onclick` handler
89
+ * property; `'Update'` has no native counterpart, so it becomes the `_mc`-prefixed custom
90
+ * event name.
91
+ */
92
+ function trueEventName(target, eventName) {
93
+ const lower = eventName.toLowerCase();
94
+ if ("on" + lower in target) return lower;
95
+ return "_mc" + eventName[0].toUpperCase() + eventName.substring(1);
96
+ }
97
+ /**
98
+ Removes a previously attached event handler from the DOM node. See {@link on} and {@link one}.
99
+
100
+ Uses internal microcomponent-prefixed naming for event handlers.
101
+ This is primarily used to interface with external frameworks or vanilla JS environments.
102
+ For communication between frontend microcomponents, use the `DC.off()` method instead.
103
+
104
+ @param target - The DOM element from which to remove the listener.
105
+ @param eventName - The name of the event (e.g., 'click', 'update').
106
+ @param handler - The listener function or object to detach.
107
+
108
+ @example
109
+ ```ts
110
+ const handler = on(myElement, 'click', () => console.log('clicked'));
111
+ off(myElement, 'click', handler);
112
+ ```
113
+ @group Events
114
+ */
115
+ function off(target, eventName, handler) {
116
+ /* v8 ignore next */
117
+ if (!target) return;
118
+ if (typeof target === "string") {
119
+ document.querySelectorAll(target).forEach((el) => off(el, eventName, handler));
120
+ return;
121
+ }
122
+ target.removeEventListener(trueEventName(target, eventName), handler);
123
+ }
124
+ /**
125
+ Attaches a persistent event listener that will fire every time the specified event occurs.
126
+
127
+ This is primarily used to interface with external frameworks or vanilla JS environments.
128
+ For communication between frontend microcomponents, use the `DC.on()` method instead.
129
+
130
+ The handler receives a `CustomEvent<McDetail<D>>`: the payload sits under
131
+ `ev.detail.data`, the bubble-control handle under `ev.detail.control`.
132
+
133
+ @param target - The DOM element to attach the listener to.
134
+ @param eventName - The name of the event (e.g., 'click', 'update').
135
+ @param handler - The callback function or object to invoke on each event.
136
+ @param once - If true, remove the listener after first invocation.
137
+ @returns The attached handler reference. Useful for later deletion with {@link off}.
138
+
139
+ @example
140
+ ```ts
141
+ const handler = on<{ value: number }>(myElement, 'update', (ev) => {
142
+ console.log(ev.detail.data.value);
143
+ });
144
+ off(myElement, 'update', handler);
145
+ ```
146
+ @group Events
147
+ */
148
+ function on(target, eventName, handler, once = false) {
149
+ /* v8 ignore next */
150
+ if (!target) return;
151
+ if (typeof target === "string") {
152
+ document.querySelectorAll(target).forEach((el) => on(el, eventName, handler));
153
+ return;
154
+ }
155
+ target.addEventListener(trueEventName(target, eventName), handler, {
156
+ capture: false,
157
+ once
158
+ });
159
+ return handler;
160
+ }
161
+ /**
162
+ Attaches an event listener that will fire only once.
163
+
164
+ Internally calls {@link on} with the `once` flag set to true. The handler
165
+ is automatically removed after its first invocation.
166
+ This is primarily used to interface with external frameworks or vanilla JS environments.
167
+ For communication between frontend microcomponents, use the `DC.one()` method instead.
168
+
169
+ @param target - The DOM element to attach the listener to.
170
+ @param eventName - The name of the event (e.g., 'click', 'update').
171
+ @param handler - The callback function or object to invoke once.
172
+ @returns The attached one-time handler reference. Useful for later deletion with {@link off}.
173
+
174
+ @example
175
+ ```ts
176
+ one<{ ready: boolean }>(myElement, 'dataReady', (ev) => console.log('received:', ev.detail.data));
177
+ ```
178
+
179
+ The listener fires only once and detaches automatically.
180
+ @group Events
181
+ */
182
+ function one(target, eventName, handler) {
183
+ /* v8 ignore next */
184
+ if (!target) return;
185
+ if (typeof target === "string") {
186
+ document.querySelectorAll(target).forEach((el) => one(el, eventName, handler));
187
+ return;
188
+ }
189
+ on(target, eventName, handler, true);
190
+ return handler;
191
+ }
192
+ /**
193
+ * @hidden
194
+ * A controller object used to manage and potentially abort event bubbling.
195
+ *
196
+ * The `trigger()` function internally creates instance of this class,
197
+ * allowing the an eventual handler to call `ev.detail.control.stop()` to
198
+ * prevent further propagation in either direction (up/down).
199
+ *
200
+ * This is only used internally.
201
+ * @example
202
+ * ```ts
203
+ * trigger(myElement, 'update', { value: 42 }, 'u');
204
+ *
205
+ * onUpdate(ev) {
206
+ * ev.detail.control.stop();
207
+ * }
208
+ * ```
209
+ */
210
+ var BubbleController = class {
211
+ stopped = false;
212
+ constructor() {}
213
+ /**
214
+ * Signals that event bubbling should be halted.
215
+ *
216
+ * After calling this method, subsequent `trigger()` calls will abort
217
+ * before dispatching the event to any targets.
218
+ *
219
+ * @example
220
+ * ```ts
221
+ * trigger(myElement, 'update', { value: 42 }, 'u');
222
+ *
223
+ * onUpdate(ev) {
224
+ * ev.detail.control.stop();
225
+ * }
226
+ * ```
227
+ */
228
+ stop() {
229
+ this.stopped = true;
230
+ }
231
+ };
232
+ /**
233
+ * Creates and dispatches a custom event on the specified DOM node.
234
+ *
235
+ * The event is namespaced with an `_mc` prefix by default, unless already prefixed.
236
+ * Supports bidirectional bubbling (up/down the DOM tree) based on the `bubble` parameter:
237
+ * - 'l': local dispatch only
238
+ * - 'u': bubble up towards ancestors
239
+ * - 'd': bubble down to descendants
240
+ *
241
+ * The dispatched event is a `CustomEvent<McDetail<D>>`: handlers find the payload
242
+ * under `ev.detail.data` and can call `ev.detail.control.stop()` to cancel bubbling.
243
+ *
244
+ * Prefer a direct target over bubbling: name the receiving element in `target` and keep the
245
+ * default `'l'`, so one `querySelectorAll` reaches it in a single step. Reserve `'u'`/`'d'`
246
+ * for receivers that cannot be named in advance, since they walk the tree and re-dispatch the
247
+ * event at every level. Pass a typed object as `data` rather than a bare string, so the
248
+ * handler reads named fields from `ev.detail.data` and the flow stays readable as it grows.
249
+ *
250
+ * @param target - The DOM element on which to trigger the event, or a querySelectorAll(selector) selector string.
251
+ * @param event - The name of the event (will be prefixed if needed).
252
+ * @param data - Additional data payload attached to `event.detail.data`.
253
+ * @param bubble - Bubbling direction ('l', 'u', 'd' or combinations).
254
+ * @param controller - Optional controller for managing bubbling behavior.
255
+ *
256
+ * @example
257
+ * ```ts
258
+ * trigger(myElement, 'update', { value: 42 }, 'l');
259
+ * trigger(myElement, 'dataReady', { ready: true }, 'u');
260
+ * ```
261
+ *
262
+ * The first dispatch fires locally only; the second bubbles upward to ancestor components.
263
+ */
264
+ function trigger(target, event, data = {}, bubble = "l", controller) {
265
+ /* v8 ignore next */
266
+ if (!target) return;
267
+ if (typeof target === "string") {
268
+ document.querySelectorAll(target).forEach((el) => trigger(el, event, data, bubble));
269
+ return;
270
+ }
271
+ const eventName = /^_mc/.test(event) ? event : trueEventName(target, event), bubbleController = controller ? controller : new BubbleController(), ev = new CustomEvent(eventName, {
272
+ detail: {
273
+ data,
274
+ control: bubbleController
275
+ },
276
+ bubbles: false
277
+ });
278
+ if (bubbleController.stopped) return;
279
+ if (/l/.test(bubble)) {
280
+ target.dispatchEvent(ev);
281
+ if (bubbleController.stopped) return;
282
+ }
283
+ if (/[ud]/.test(bubble)) queueMicrotask(() => {
284
+ if (bubble.indexOf("u") > -1) trigger(target.parentElement, eventName, data, "lu", bubbleController);
285
+ if (bubble.indexOf("d") > -1) for (const child of target.children) trigger(child, eventName, data, "ld");
286
+ });
287
+ }
288
+ //#endregion
289
+ //#region src/utils/helpers.ts
290
+ /**
291
+ * Determines whether the current runtime environment is a web browser.
292
+ *
293
+ * This function works by attempting to access `this` within an anonymous function
294
+ * and checking if it refers to the global `window` object. It returns `true`
295
+ * when executed in a browser context, and `false` otherwise (e.g., Node.js).
296
+ *
297
+ * Note: While modern environments typically provide more reliable detection mechanisms,
298
+ * this fallback remains compatible across older runtimes where other detection techniques
299
+ * may be unreliable.
300
+ *
301
+ * @returns `true` if the code is running in a browser environment, `false` otherwise.
302
+ *
303
+ * @example
304
+ * ```ts
305
+ * if (isBrowser()) {
306
+ * console.log('Running in a browser');
307
+ * }
308
+ * ```
309
+ */
310
+ function isBrowser() {
311
+ return typeof window !== "undefined" && typeof window.document !== "undefined";
312
+ }
313
+ /**
314
+ * Checks if a value is a native JavaScript object (created via {} or new Object()).
315
+ * @param value - The value to check.
316
+ * @returns True if the value is a plain object, false otherwise.
317
+ *
318
+ * @example
319
+ * ```ts
320
+ * console.log(isNativeObject({}));
321
+ * console.log(isNativeObject(new Object()));
322
+ * console.log(isNativeObject([]));
323
+ * console.log(isNativeObject(null));
324
+ * ```
325
+ */
326
+ function isNativeObject(value) {
327
+ return typeof value === "object" && value !== null && value.constructor === Object;
328
+ }
329
+ /**
330
+ * Recursively sorts all keys in an object alphabetically, preserving nested structure.
331
+ * @param source - The object to sort.
332
+ * @returns A new object with sorted keys at all levels.
333
+ *
334
+ * @example
335
+ * ```ts
336
+ * const obj = { z: 1, a: { y: 2, b: 3 } };
337
+ * console.log(JSON.stringify(sortObject(obj)));
338
+ * ```
339
+ */
340
+ function sortObject(source) {
341
+ const result = {}, keys = Object.keys(source).sort();
342
+ for (let i = 0; i < keys.length; i++) {
343
+ const key = keys[i], value = source[key];
344
+ if (isNativeObject(value)) result[key] = sortObject(value);
345
+ else result[key] = value;
346
+ }
347
+ return result;
348
+ }
349
+ /**
350
+ * Flattens a nested object into a single-level object with dot-separated keys.
351
+ * Optionally sorts the resulting flat keys alphabetically (default: true).
352
+ * @param source - The object to flatten.
353
+ * @param doSort - Whether to sort the resulting keys (default: true).
354
+ * @returns A flattened object with dot-notation keys.
355
+ *
356
+ * @example
357
+ * ```ts
358
+ * const obj = { user: { name: 'Alice', age: 30 } };
359
+ * console.log(JSON.stringify(flattenObject(obj)));
360
+ * ```
361
+ */
362
+ function flattenObject(source, doSort = true) {
363
+ const result = {};
364
+ const record = source;
365
+ const names = Object.getOwnPropertyNames(source);
366
+ for (let i = 0; i < names.length; i++) {
367
+ const propName = names[i], value = record[propName];
368
+ if (value instanceof Object) {
369
+ const temp = flattenObject(value, false);
370
+ for (const j in temp) result[propName + "." + j] = temp[j];
371
+ } else result[propName] = value;
372
+ }
373
+ return doSort ? sortObject(result) : result;
374
+ }
375
+ /**
376
+ * Structural deep-equality using SameValueZero for primitives.
377
+ *
378
+ * See {@link https://tc39.es/ecma262/multipage/abstract-operations.html#sec-samevaluezero}.
379
+ *
380
+ * Note: `Map`/`Set` are not structurally compared.
381
+ *
382
+ * @param a - The first value to compare.
383
+ * @param b - The second value to compare.
384
+ * @returns True if the values are deeply equal, false otherwise.
385
+ *
386
+ * @example
387
+ * ```ts
388
+ * console.log(deepEqual({ a: 1 }, { a: 1 }));
389
+ * console.log(deepEqual({ a: { b: 2 } }, { a: {} }));
390
+ * console.log(deepEqual({ a: NaN }, { a: NaN }));
391
+ * console.log(deepEqual({ a: NaN }, { a: null }));
392
+ * ```
393
+ */
394
+ function deepEqual(a, b) {
395
+ if (a === b || a !== a && b !== b) return true;
396
+ /* v8 ignore start */
397
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
398
+ if (a.constructor !== b.constructor) return false;
399
+ if (a instanceof Date) return a.getTime() === b.getTime();
400
+ if (a instanceof RegExp) return a.toString() === b.toString();
401
+ /* v8 ignore end */
402
+ if (Array.isArray(a)) {
403
+ const bArr = b;
404
+ /* v8 ignore next */
405
+ if (a.length !== bArr.length) return false;
406
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], bArr[i])) return false;
407
+ return true;
408
+ }
409
+ const aObj = a, bObj = b, keys = Object.keys(aObj);
410
+ if (keys.length !== Object.keys(bObj).length) return false;
411
+ for (let i = 0; i < keys.length; i++) {
412
+ const key = keys[i];
413
+ if (!Object.prototype.hasOwnProperty.call(bObj, key) || !deepEqual(aObj[key], bObj[key])) return false;
414
+ }
415
+ return true;
416
+ }
417
+ /**
418
+ * Retrieves a nested value from an object using a namespace path (dot-separated).
419
+ * @param ns - The namespace string or array of keys representing the path.
420
+ * @param obj - The object to traverse.
421
+ * @returns The value at the specified namespace path, or undefined if not found.
422
+ *
423
+ * @example
424
+ * ```ts
425
+ * const data = { user: { profile: { name: 'Alice' } } };
426
+ * console.log(nameSpace('user.profile.name', data));
427
+ * console.log(nameSpace(['user', 'profile'], data));
428
+ * ```
429
+ */
430
+ function nameSpace(ns, obj = {}) {
431
+ const source = obj;
432
+ const nsa = typeof ns === "string" ? ns.split(".") : ns;
433
+ if (!source || !nsa[0] || !source[nsa[0]]) return;
434
+ if (nsa.length > 1) {
435
+ const next = source[nsa[0]];
436
+ if (isNativeObject(next)) return nameSpace(nsa.slice(1), next);
437
+ return;
438
+ }
439
+ return source[nsa[0]];
440
+ }
441
+ /**
442
+ * Parses a template string or object, replacing placeholders in the form of {key.path} with
443
+ * corresponding values from the provided data object using namespace resolution.
444
+ * Also recursively processes objects and arrays within the input.
445
+ * @param template - The string or object to parse (can contain {{placeholder}} expressions).
446
+ * @param parseThis - The object containing values for placeholder substitution.
447
+ * @returns The parsed result, with placeholders replaced by actual values.
448
+ *
449
+ * @example
450
+ * ```ts
451
+ * const data = { user: { name: 'Alice' } };
452
+ * console.log(parse('Hello {user.name}!', data));
453
+ * console.log(parse(['Hi {user.name}', 'Bye {user.name}'], data));
454
+ * ```
455
+ */
456
+ function parse(template, parseThis, ...rest) {
457
+ const args = [
458
+ template,
459
+ parseThis,
460
+ ...rest
461
+ ];
462
+ if (args.length > 2) {
463
+ while (args.length > 1) {
464
+ args[0] = parse(args[0], args[1]);
465
+ args.splice(1, 1);
466
+ }
467
+ return args[0];
468
+ }
469
+ if (typeof template === "string") {
470
+ let result = template;
471
+ template.match(/\{[^\{\}]*\}/g)?.forEach((pPropname) => {
472
+ const value = nameSpace(pPropname.substring(1, pPropname.length - 1), parseThis);
473
+ if (typeof value !== "undefined") result = result.replace(pPropname, String(value));
474
+ });
475
+ return result;
476
+ } else if (isNativeObject(template)) {
477
+ const record = template;
478
+ Object.keys(record).forEach((pKey) => {
479
+ /* v8 ignore next */
480
+ if (Object.prototype.hasOwnProperty.call(record, pKey)) record[pKey] = parse(record[pKey], parseThis);
481
+ });
482
+ }
483
+ return template;
484
+ }
485
+ /**
486
+ * Converts a kebab-case string to PascalCase.
487
+ * Example: 'my-component-name' -> 'MyComponentName'
488
+ * @param str - The kebab-case string to convert.
489
+ * @returns The converted PascalCase string.
490
+ *
491
+ * @example
492
+ * ```ts
493
+ * console.log(kebabToPascal('my-element'));
494
+ * ```
495
+ */
496
+ function kebabToPascal(str) {
497
+ return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
498
+ }
499
+ /**
500
+ * Creates a debounced version of a function that delays execution until after the specified
501
+ * number of milliseconds have elapsed since the last time this function was invoked.
502
+ * Useful for rate-limiting expensive operations like event handlers or API calls.
503
+ * @param func - The function to debounce.
504
+ * @param milliseconds - The number of milliseconds to delay execution.
505
+ * @returns A debounced version of the input function.
506
+ *
507
+ * @example
508
+ * ```ts
509
+ * const log = debounce(() => console.log('called'), 300);
510
+ * log();
511
+ * log();
512
+ * ```
513
+ */
514
+ function debounce(func, milliseconds) {
515
+ let timeout;
516
+ return (...args) => {
517
+ clearTimeout(timeout);
518
+ timeout = setTimeout(() => {
519
+ func(...args);
520
+ }, milliseconds);
521
+ };
522
+ }
523
+ /**
524
+ * Parses an HTML string and returns an array of DOM elements.
525
+ * Handles self-closing tags (XHTML style) by converting them to full closing tags first.
526
+ * @hidden
527
+ * @param html - The HTML string to parse.
528
+ * @returns An array of HTMLElement objects representing the parsed HTML nodes.
529
+ *
530
+ * @example
531
+ * ```ts
532
+ * const elems = htmlToElements('<div>Hello</div><span>World</span>');
533
+ * console.log(elems.map((el) => el.tagName));
534
+ * ```
535
+ */
536
+ function htmlToElements(html) {
537
+ const template = document.createElement("template");
538
+ html = html.replace(/<([A-Za-z0-9\-]*)([^>\/]*)(\/>)/gi, "<$1$2></$1>");
539
+ template.innerHTML = html;
540
+ template.content.normalize();
541
+ return Array.from(template.content.childNodes);
542
+ }
543
+ /**
544
+ * Copies all getter and setter properties from a source object to a target object.
545
+ * Does not copy methods or regular data properties—only accessor properties (get/set).
546
+ * @hidden
547
+ * @param source - The source object with getters and/or setters to copy.
548
+ * @param target - The target object to which the accessors will be copied.
549
+ *
550
+ * @example
551
+ * ```ts
552
+ * const src = { get name() { return 'Alice'; } };
553
+ * const tgt = {};
554
+ * copyGettersSetters(src, tgt);
555
+ * console.log(tgt.name);
556
+ * ```
557
+ */
558
+ function copyGettersSetters(source, target) {
559
+ const descriptors = Object.getOwnPropertyDescriptors(source);
560
+ for (const [key, descriptor] of Object.entries(descriptors)) if ("get" in descriptor || "set" in descriptor) Object.defineProperty(target, key, descriptor);
561
+ }
562
+ /**
563
+ * Injects a CSS string into the given scope (document or shadow root) as an adopted stylesheet.
564
+ * Enables css-in-js so that every custom element is self-contained and importable.
565
+ *
566
+ * @hidden
567
+ * @param target - The scope to inject styles into: `document` for global styles, or a `ShadowRoot` for encapsulated component internals.
568
+ * @param css - The CSS string to inject.
569
+ * @returns The created `CSSStyleSheet` instance that was adopted into the target scope.
570
+ *
571
+ * When targeting **document**: selectors like `:host`, `::slotted()`, and shadow-internal classes are **not allowed** — they have no effect outside a shadow root. Use only global selectors, element tags, or layout presets.
572
+ * When targeting **shadowRoot**: use `:host` for the custom element itself, standard selectors (`.inner`, `#id`) for internal structure, and `::slotted()` for projected light-DOM content.
573
+ *
574
+ * @example Inject global styles into the document:
575
+ * ```ts
576
+ * import { injectCSS } from '@twobirds/mc/utils/helpers';
577
+ * injectCSS(document, 'body { display: block; }');
578
+ * ```
579
+ *
580
+ * @example Inject global styles for a micro component into the document:
581
+ * ```ts
582
+ * import { injectCSS } from '@twobirds/mc/utils/helpers';
583
+ * injectCSS(document, 'my-microcomponent { display: block; }');
584
+ * ```
585
+ *
586
+ * @example Inject scoped styles into a shadow root:
587
+ * ```ts
588
+ * const shadow = element.attachShadow({ mode: 'open' });
589
+ * injectCSS(shadow, ':host { color: var(--text); } .inner { padding: 8px; }');
590
+ * ```
591
+ * Styles can also be added using a compound innerHTML statement: `shadow.innerHTML = \`<style>${style}</style>${template}\``
592
+ */
593
+ var _injectedCSS = /* @__PURE__ */ new WeakMap();
594
+ function injectCSS(target, css) {
595
+ const injected = _injectedCSS.get(target) ?? /* @__PURE__ */ new Set();
596
+ if (injected.has(css)) return target.adoptedStyleSheets[0];
597
+ injected.add(css);
598
+ _injectedCSS.set(target, injected);
599
+ const sheet = new CSSStyleSheet();
600
+ sheet.replaceSync(css);
601
+ target.adoptedStyleSheets.push(sheet);
602
+ return sheet;
603
+ }
604
+ /**
605
+ * Processes a record of asset strings and applies them to the given target element.
606
+ * Supports injecting CSS into document or shadow root, and setting innerHTML for
607
+ * both target and shadow DOM. Automatically attaches a shadow root if any key starts
608
+ * with `'shadow'`.
609
+ *
610
+ * Recognized keys: `'css'`, `'targetCss'` (document-level styles), `'shadowCss'`
611
+ * (shadow-root styles), `'shadowHtml'` (shadow innerHTML), `'targetHtml'` (target innerHTML).
612
+ *
613
+ * @param instance - The micro component to apply assets onto.
614
+ * @param assets - A record mapping asset type keys to their string content.
615
+ *
616
+ * @example
617
+ * ```ts
618
+ * processAssets(this, {
619
+ * css: 'body { margin: 0; }',
620
+ * shadowCss: ':host { display: block; }',
621
+ * shadowHtml: '<slot></slot>'
622
+ * });
623
+ * ```
624
+ */
625
+ function processAssets(instance, assets) {
626
+ const target = instance.target;
627
+ let shadow = target.shadowRoot;
628
+ if (!shadow && Object.keys(assets).some((key) => key.indexOf("shadow") === 0)) shadow = target.attachShadow({ mode: "open" });
629
+ Object.keys(assets).forEach((key) => {
630
+ switch (key) {
631
+ case "css":
632
+ injectCSS(document, assets[key]);
633
+ break;
634
+ case "targetCss":
635
+ injectCSS(document, assets[key]);
636
+ break;
637
+ case "shadowCss":
638
+ injectCSS(shadow, assets[key]);
639
+ break;
640
+ case "shadowHtml":
641
+ shadow.innerHTML = assets[key];
642
+ bindTemplateEvents(shadow, instance);
643
+ break;
644
+ case "targetHtml":
645
+ target.innerHTML = assets[key];
646
+ bindTemplateEvents(target, instance);
647
+ break;
648
+ }
649
+ });
650
+ }
651
+ /**
652
+ * Loads companion asset files (CSS, HTML) for a given component basename from the server.
653
+ *
654
+ * Each entry in `assets` encodes an optional variant and extension in camelCase, e.g. `'shadowCss'` → `{name}.shadow.css`,
655
+ * `'targetHtml'` → `{name}.target.html`. Plain entries like `'css'` or `'html'` resolve to
656
+ * `{name}.css` / `{name}.html`.
657
+ *
658
+ * In bundled mode the Vite plugin replaces this call with an inline array literal, so `.then()`
659
+ * fires synchronously — no async overhead.
660
+ *
661
+ * @hidden
662
+ * @param name - The component basename (e.g. `'shop-catalog'`).
663
+ * @param assets - Array of asset descriptors such as `'shadowCss'`, `'targetHtml'`, `'shadowHtml'`, `'css'`, `'html'`.
664
+ * @returns A promise resolving to an array of file contents in the same order as `assets`. If a file is not found, the corresponding entry is `'file not found: ' + filename`.
665
+ *
666
+ * @example
667
+ * ```ts
668
+ * export const MyComponent = loadAssets('my-component', ['shadowCss', 'shadowHtml']).then(([shadowCss, shadowHtml]) => {
669
+ * class _MyComponent extends DC { ... }
670
+ * defineCE('my-component', _MyComponent);
671
+ * return _MyComponent;
672
+ * });
673
+ * ```
674
+ */
675
+ function loadAssets(name, assets) {
676
+ const parseAsset = (asset) => {
677
+ const m = asset.match(/^([a-z]+)([A-Z][a-z]*)$/);
678
+ if (m) return `./${name.replace(/-/g, "/")}.${m[1]}.${m[2].toLowerCase()}`;
679
+ return `./${name.replace(/-/g, "/")}.${asset.toLowerCase()}`;
680
+ };
681
+ return Promise.all(assets.map(async (asset) => {
682
+ const filename = parseAsset(asset);
683
+ try {
684
+ const resp = await fetch(filename);
685
+ if (!resp.ok) return `file not found: ${filename}`;
686
+ if (filename.endsWith("json")) {
687
+ const text = await resp.text();
688
+ try {
689
+ return JSON.parse(text);
690
+ } catch {
691
+ console.error(`could not JSON.parse() asset '${asset}' from ${filename}:\n`, text);
692
+ return null;
693
+ }
694
+ }
695
+ return resp.text();
696
+ } catch {
697
+ return `file not found: ${filename}`;
698
+ }
699
+ }));
700
+ }
701
+ /**
702
+ * Binds declarative event attributes in a template fragment to microcomponent triggers.
703
+ *
704
+ * Scans all elements under `root` for attributes whose names correspond to native DOM
705
+ * events (e.g. `click`, `submit`, `change`). The attribute value encodes the target and
706
+ * handler method in the form `target|methodName`:
707
+ *
708
+ * - `this|myHandler` — triggers locally on the component attached to this element.
709
+ * - `other-comp|submit` — resolves elements via `querySelectorAll('other-comp')` and
710
+ * triggers the event on each matched host's `_mc`.
711
+ *
712
+ * An optional sibling `payload` attribute supplies the data argument for `trigger()`.
713
+ * After binding, both the event attribute and the payload attribute are removed from the DOM.
714
+ *
715
+ * @param rootElement - The container element whose descendants should be scanned for bindings.
716
+ * @param instance - The instance to trigger events on.
717
+ *
718
+ * @example
719
+ * ```html
720
+ * <button click='this|onSubmit|{"action":"go"}'>Submit</button>
721
+ * <input change="|cart-form|refresh">
722
+ * ```
723
+ */
724
+ function bindTemplateEvents(rootElement, instance) {
725
+ const allElements = [...rootElement.querySelectorAll("*")];
726
+ for (const el of allElements) {
727
+ if (!(el instanceof HTMLElement)) continue;
728
+ const attrs = [...el.attributes];
729
+ for (const attr of attrs) {
730
+ const lowerName = attr.name.toLowerCase();
731
+ if (!("on" + lowerName in el)) continue;
732
+ const parts = attr.value.trim().split("|");
733
+ if (parts.length < 2) continue;
734
+ const [selector, event, payload] = parts;
735
+ el.removeAttribute(attr.name);
736
+ let payloadData;
737
+ if (payload) try {
738
+ payloadData = JSON.parse(payload);
739
+ } catch {
740
+ payloadData = payload;
741
+ }
742
+ if (selector === "this" || selector === "") on(el, lowerName, () => instance.trigger(event, payloadData));
743
+ else on(el, lowerName, () => trigger(selector, event, payloadData));
744
+ }
745
+ }
746
+ }
747
+ //#endregion
748
+ //#region src/core/mcGuards.ts
749
+ function isMcHost(element, key) {
750
+ return typeof HTMLElement !== "undefined" && element instanceof HTMLElement && "_mc" in element && (key === void 0 || key in element._mc);
751
+ }
752
+ //#endregion
753
+ //#region src/core/MC.ts
754
+ function add(targetHTMLElement, key, element = {}) {
755
+ const node = targetHTMLElement;
756
+ if (!("_mc" in targetHTMLElement)) node._mc = new DC(targetHTMLElement);
757
+ const _mc = node._mc;
758
+ if (key.length && !_mc[key]) {
759
+ _mc[key] = element;
760
+ if (targetHTMLElement instanceof HTMLElement && !targetHTMLElement.getAttribute("data-mc")) targetHTMLElement.setAttribute("data-mc", "");
761
+ }
762
+ return node;
763
+ }
764
+ /**
765
+ Removes a named microcomponent from the internal `_mc` collection of a target DOM node.
766
+
767
+ This function deletes the specified key from the element's `_mc` hash object and, if the
768
+ collection becomes empty, removes the `data-mc` attribute from the DOM node.
769
+
770
+ The bare poisoned value (`el._mc.gone` without member access) is `never`,
771
+ which is assignable to anything — only member/method use of a removed entry is caught.
772
+
773
+ @template T - A plain or already-enhanced element type.
774
+ @template K - The literal registration key; its entry is poisoned in the registry type.
775
+ @param target - The DOM element whose microcomponent should be removed.
776
+ @param key - The property name/key corresponding to the microcomponent to remove.
777
+ */
778
+ function remove(target, key) {
779
+ if (!key) return;
780
+ const _mc = target._mc;
781
+ if (!_mc) return;
782
+ if (_mc[key]) delete _mc[key];
783
+ if (!Object.keys(_mc).length && target instanceof HTMLElement && target.hasAttribute("data-mc")) target.removeAttribute("data-mc");
784
+ }
785
+ /**
786
+ Runs the "Init" lifecycle event on a DOM microcomponent instance asynchronously.
787
+
788
+ This function schedules the `trigger('Init')` call in a microtask, ensuring
789
+ that all synchronous initialization is complete before the lifecycle hook
790
+ executes. Only `DC` instances have an Init lifecycle — the `McBase`
791
+ constructor calls this exclusively behind an `instanceof DC` check, so the
792
+ signature encodes that invariant instead of re-checking it at runtime.
793
+
794
+ @param element - The DOM microcomponent instance on which to trigger the Init event.
795
+ */
796
+ function init(element) {
797
+ queueMicrotask(() => {
798
+ element.trigger("Init", null);
799
+ });
800
+ }
801
+ /**
802
+ Attaches event listeners to DOM elements based on method naming conventions.
803
+
804
+ Scans the prototype of the provided element and attaches listeners for methods
805
+ prefixed with `on` or `one`, corresponding to either native HTML events (e.g.,
806
+ 'onclick') or microcomponent-prefixed custom events (e.g., '_mcUpdate').
807
+
808
+ @param element - The instance whose prototype should be scanned for event handlers.
809
+ */
810
+ function autoAttachListeners(element) {
811
+ const sourceTarget = element.target;
812
+ const prototype = Object.getPrototypeOf(element);
813
+ const sourceElement = element;
814
+ const definedEventHandlerNames = Object.getOwnPropertyNames(prototype).filter((key) => /^(on[A-Z]|one[A-Z])/.test(key) && typeof prototype[key] === "function");
815
+ for (const eventHandlerName of definedEventHandlerNames) {
816
+ const once = /^one/.test(eventHandlerName);
817
+ const eventName = eventHandlerName.replace(/^one?/, "");
818
+ const listener = (ev) => sourceElement[eventHandlerName](ev);
819
+ on(sourceTarget, eventName, listener, once);
820
+ }
821
+ }
822
+ /**
823
+ Retrieves immediate child microcomponents of a DOM node.
824
+
825
+ Queries the DOM for descendant elements marked with `data-mc` that are direct
826
+ children of the provided element and returns their associated `_mc` instances.
827
+
828
+ @param domNode - The DOM element whose child microcomponents to find.
829
+ @returns An array of child microcomponent instances.
830
+ */
831
+ function getMcChildNodes(domNode) {
832
+ return Array.from(domNode.querySelectorAll("[data-mc]:not(:scope [data-mc] [data-mc])")).filter((e) => e instanceof HTMLElement && "_mc" in e).map((e) => e._mc);
833
+ }
834
+ /**
835
+ Walks up the DOM from `from`, returning the `_mc` controller of the nearest
836
+ `data-mc` ancestor — restricted, when `key` is given, to hosts whose registry
837
+ actually carries that key. Hosts marked `data-mc` but not (yet) carrying an
838
+ `_mc` record (e.g. autoload pending) are skipped rather than returned.
839
+
840
+ @param from - The element whose ancestors to search (exclusive).
841
+ @param key - Optional registry key the ancestor's `_mc` must contain.
842
+ @returns The nearest matching controller, or null.
843
+ */
844
+ function findMcAncestor(from, key) {
845
+ let el = from.parentElement?.closest("[data-mc]");
846
+ while (el instanceof HTMLElement) {
847
+ /* v8 ignore next */
848
+ if ("_mc" in el) {
849
+ const mc = el._mc;
850
+ if (key === void 0 || key in mc) return mc;
851
+ }
852
+ el = el.parentElement?.closest("[data-mc]");
853
+ }
854
+ return null;
855
+ }
856
+ /**
857
+ The base class for all microcomponents — shared root of both {@link MC} and {@link DC}.
858
+
859
+ Holds a `WeakRef` to the target (a DOM element for `DC`, or any plain object for `MC`)
860
+ to avoid circular references. For `DC` subclasses it also bootstraps two automatic
861
+ behaviours:
862
+
863
+ 1. **Event listener auto-attachment** — methods whose names start with `on` or `one`
864
+ (e.g. `onClick`, `oneInit`) are wired to the target element via {@link on} / {@link one}.
865
+ 2. **"Init" lifecycle event** — dispatched asynchronously in a microtask so that all
866
+ synchronous constructor work finishes first.
867
+
868
+ Both mechanisms run only when the instance is a `DC` (checked with `instanceof DC`).
869
+ Plain `MC` instances skip them entirely, keeping server-side usage lightweight.
870
+
871
+ ### Plugin extension points
872
+
873
+ The prototype of `McBase` is the attachment point for optional plugins that curry
874
+ additional instance methods onto every microcomponent. Two built-in plugins modify
875
+ this class at runtime:
876
+
877
+ - **{@link State.observePlugin}** — adds `notify()`, `subscribe()`, and `bind()` so that
878
+ observable properties can be controlled directly on the component instance with
879
+ fluent chaining. Call `observePlugin(DC)` once at module level to activate.
880
+
881
+ - **{@link State.formValuePlugin}** — adds `formValues()` for two-way binding between DOM
882
+ form controls and an observable property. Also pulls in all methods from
883
+ {@link State.observePlugin}, so you can chain `.subscribe()` and `.bind()` after
884
+ `.formValues()`. Call `formValuePlugin(DC)` once at module level to activate.
885
+
886
+ Both plugins are idempotent — calling them multiple times has no side effects. They
887
+ work by assigning methods onto `McBase.prototype` (reached via
888
+ `Object.getPrototypeOf(Class.prototype)`) and use TypeScript module augmentation
889
+ (`declare module`) so the added signatures appear in type-checking automatically.
890
+
891
+ @template TTarget - The concrete type of the wrapped target; preserved by the
892
+ {@link McBase.target} getter.
893
+
894
+ @example Extending with plugins at runtime
895
+
896
+ ```ts
897
+ import { DC, observePlugin, formValuePlugin } from '@twobirds/microcomponents';
898
+
899
+ observePlugin(DC);
900
+ formValuePlugin(DC);
901
+
902
+ class MyForm extends DC {
903
+ data = { name: '' };
904
+
905
+ oneInit() {
906
+ this
907
+ .formValues(this.target, 'data')
908
+ .subscribe('data', (v) => console.log(v))
909
+ .bind('data', this.target);
910
+ }
911
+ }
912
+ ```
913
+
914
+ `observePlugin(DC)` adds `notify()`, `subscribe()`, `bind()` to `McBase.prototype`;
915
+ `formValuePlugin(DC)` adds `formValues()` and re-runs `observePlugin`. `oneInit()`
916
+ runs once after the constructor.
917
+ @group Micro Components
918
+ */
919
+ var McBase = class {
920
+ _target;
921
+ /**
922
+ The constructor for the base `McBase` class.
923
+
924
+ Initializes a weak reference to the target and triggers auto-attachment
925
+ of listeners and initialization lifecycle events.
926
+
927
+ @param target - The object or element this microcomponent represents.
928
+ */
929
+ constructor(target) {
930
+ this._target = new WeakRef(target || {});
931
+ if (this instanceof DC && this.constructor !== DC) {
932
+ autoAttachListeners(this);
933
+ const tag = this.constructor.tag;
934
+ if (tag === void 0 || tag.toUpperCase() === this.target.tagName) init(this);
935
+ }
936
+ }
937
+ /**
938
+ Returns the current target object (DOM node or other data) referenced by this microcomponent.
939
+
940
+ Uses a `WeakRef` internally to avoid circular references, and safely dereferences it here.
941
+
942
+ The dereference is asserted non-null: the controller is stored on its target
943
+ (`element._mc`), so any code that can reach the controller through the element is
944
+ holding the target alive. Only a controller retained independently after its element
945
+ was garbage-collected could observe a stale reference — do not cache controllers
946
+ detached from their elements.
947
+
948
+ @returns The target object.
949
+ */
950
+ get target() {
951
+ return this._target.deref();
952
+ }
953
+ };
954
+ /**
955
+ A general-purpose base microcomponent that does not require a DOM target.
956
+
957
+ Inherits from `McBase`, but is primarily used as a lightweight container or
958
+ for logic not directly tied to DOM elements.
959
+
960
+ @template TTarget - The concrete type of the wrapped target data; preserved by
961
+ the `target` getter, so `new MC({ some: 'data' }).target.some` stays typed.
962
+ @group Micro Components
963
+ */
964
+ var MC = class extends McBase {
965
+ /**
966
+ Constructs an instance of the general-purpose microcomponent.
967
+
968
+ @param target - Target object or data.
969
+ */
970
+ constructor(target) {
971
+ super(target);
972
+ }
973
+ };
974
+ /**
975
+ @class DC
976
+ ---
977
+
978
+ A DOM-centric microcomponent that operates on a specific HTML element.
979
+
980
+ Extends {@link McBase} and associates itself with an actual DOM node via the
981
+ `_target` reference. Provides methods for event handling, DOM traversal,
982
+ and interaction with sibling or ancestor microcomponents.
983
+
984
+ ---
985
+
986
+ ## Static Configuration Properties
987
+
988
+ DC classes support three static properties that configure custom elements
989
+ before any instance is constructed:
990
+
991
+ ### `static tag?: string`
992
+
993
+ Replaces the default target element with a semantic HTML element, or removes it entirely.
994
+
995
+ - **Non-empty string** (e.g., `'header'`) — the original custom element is replaced
996
+ with the specified built-in element. The original tag name is added as a CSS class.
997
+ - **Empty string `''`** — the target element is removed from the DOM entirely.
998
+ Useful for components that only inject global styles or act as style containers.
999
+
1000
+ @example static tag — replace with semantic element
1001
+
1002
+ ```ts
1003
+ class ShopHeader extends DC {
1004
+ static tag = 'header';
1005
+ }
1006
+ defineCE('shop-header', ShopHeader);
1007
+ ```
1008
+
1009
+ Produces `<header class="shop-header">` instead of `<shop-header>`.
1010
+
1011
+ @example static tag — remove element entirely
1012
+
1013
+ ```ts
1014
+ class GlobalStyles extends DC {
1015
+ static tag = '';
1016
+ }
1017
+ defineCE('global-styles', GlobalStyles);
1018
+ ```
1019
+
1020
+ The element self-destructs; only side effects (e.g., injected styles) remain.
1021
+
1022
+ ### `static assets: string[]`
1023
+
1024
+ Declares companion files for auto-loading by convention. Asset names are camelCase
1025
+ and translated into file extensions: `'targetHtml'` → `.target.html`, `'json'` → `.json`.
1026
+
1027
+ The path is derived from the custom element tag segments, and the basename is the
1028
+ last segment. For `<cart-container-checkout>`, assets load from `/cart/container/checkout.*`.
1029
+
1030
+ When assets finish loading, an `'assets'` event fires so that `oneAssets(ev)` can
1031
+ process them. Everything ending in `Json` arrives parsed; all other files arrive as text.
1032
+
1033
+ **One-time global styles**: Assets named `'css'` and `'targetCss'` inject CSS into the
1034
+ global document scope via `injectCSS(document, ...)`. Because these are idempotent
1035
+ global side effects, they are processed only once — after the first instance loads them,
1036
+ the entries are removed from both `static assets` and `static _assets`. Subsequent
1037
+ instances skip loading and re-processing them entirely.
1038
+
1039
+ **CSS file roles**:
1040
+
1041
+ - `.css` — design tokens only (CSS custom-property presets). Injected into the document
1042
+ scope once, like `targetCss`.
1043
+ - `.target.css` — layout rules for the light DOM. Also document-scoped and one-time.
1044
+ - `.shadow.css` — layout rules for the shadow DOM. Injected per instance into the
1045
+ component's own shadow root, so each instance gets its own copy.
1046
+
1047
+ Every declared asset — CSS, HTML, JSON — is inlined into `static _assets` at build time by
1048
+ the load-assets plugin, so a bundled build performs no fetches at all.
1049
+
1050
+ In a bundled build the plain `.css` files are additionally concatenated into a single
1051
+ `bundle.css` by the bundling script. That file is a convenience listing of the available
1052
+ variables: copy the ones you want to change into your own stylesheet and override them
1053
+ there. It is not required at runtime, and it cannot drift from the inlined presets — both
1054
+ are read from the same source files.
1055
+
1056
+ The runtime decides whether an asset still needs fetching with `!ctor._assets[key]`, so an
1057
+ inlined entry must never be an empty string: that would trigger a re-fetch of a file that
1058
+ does not exist in a bundled build.
1059
+
1060
+ @example static assets with autoload
1061
+
1062
+ ```ts
1063
+ class CartCheckout extends DC {
1064
+ static assets = ['targetHtml', 'targetCss', 'configJson'];
1065
+
1066
+ oneAssets(ev) {
1067
+ const { configJson } = ev.detail.data;
1068
+ console.log(configJson);
1069
+ }
1070
+ }
1071
+ defineCE('cart-checkout', CartCheckout);
1072
+ ```
1073
+
1074
+ `configJson` arrives already parsed into an object.
1075
+
1076
+ ### `static attributes?: object`
1077
+
1078
+ Sets HTML attributes on every instance automatically.
1079
+
1080
+ @example static attributes
1081
+
1082
+ ```ts
1083
+ class ProductCard extends DC {
1084
+ static attributes = { role: 'listitem' };
1085
+ }
1086
+ defineCE('product-card', ProductCard);
1087
+ ```
1088
+
1089
+ Every `<product-card>` carries `role="listitem"`.
1090
+
1091
+ ### Combined usage
1092
+
1093
+ All three can be combined in one class:
1094
+
1095
+ @example All static properties together
1096
+
1097
+ ```ts
1098
+ class ShopCatalog extends DC {
1099
+ static tag = 'section';
1100
+ static attributes = { role: 'list', 'aria-label': 'products' };
1101
+ static assets = ['targetHtml', 'targetCss'];
1102
+ }
1103
+ defineCE('shop-catalog', ShopCatalog);
1104
+ ```
1105
+
1106
+ Produces `<section role="list" aria-label="products">`.
1107
+
1108
+ ---
1109
+
1110
+ ## Event Handler Conventions
1111
+
1112
+ Methods whose names start with `on` or `one` are automatically wired as event listeners
1113
+ on the target element. The prefix determines persistence:
1114
+
1115
+ - **`on<EventName>`** — persistent listener (fires every time)
1116
+ - **`one<EventName>`** — one-time listener (fires once, then detaches)
1117
+
1118
+ ### Web Component Lifecycle Events
1119
+
1120
+ When a DC is registered via {@link defineCE}, the custom element dispatches these
1121
+ internal events that can be caught with handler methods:
1122
+
1123
+ | Method | When it fires |
1124
+ |--------|---------------|
1125
+ | `onConnected()` | Element was inserted into the DOM (`connectedCallback`) |
1126
+ | `onDisconnected()` | Element was removed from the DOM (`disconnectedCallback`) |
1127
+ | `onAdopted()` | Element was moved to a new document (`adoptedCallback`) |
1128
+ | `onAttributeChanged()` | An observed attribute changed (`attributeChangedCallback`) |
1129
+ | `oneInit()` | Constructor finished; all synchronous setup complete (microtask) |
1130
+ | `oneAssets()` | Constructor finished; all assets loaded and available; ev contains assets object |
1131
+
1132
+ @example Event handlers in action
1133
+
1134
+ ```ts
1135
+ class MyElement extends DC {
1136
+ onConnected() {
1137
+ console.log('entered the DOM');
1138
+ }
1139
+
1140
+ onDisconnected() {
1141
+ console.log('left the DOM');
1142
+ }
1143
+
1144
+ onClick() {
1145
+ this.trigger('something', 'payload');
1146
+ }
1147
+
1148
+ oneSomething(ev) {
1149
+ console.log('first time only:', ev.detail.data);
1150
+ }
1151
+ }
1152
+ defineCE('my-element', MyElement);
1153
+ ```
1154
+
1155
+ ---
1156
+
1157
+ ## Construction Patterns
1158
+
1159
+ ### Template usage (declarative)
1160
+
1161
+ ```ts
1162
+ class MyElement extends DC {}
1163
+ defineCE('my-element', MyElement);
1164
+ ```
1165
+
1166
+ The registered element is then used in HTML as `<my-element></my-element>`.
1167
+
1168
+ ### Programmatic construction with data
1169
+
1170
+ When constructing programmatically via {@link getCE}, pass only the `data` payload —
1171
+ the target element is created automatically by the custom element constructor.
1172
+
1173
+ @example Programmatic construction
1174
+
1175
+ ```ts
1176
+ class MyElement extends DC {
1177
+ data = {};
1178
+
1179
+ constructor(target, data) {
1180
+ super(target);
1181
+ this.data = data;
1182
+ }
1183
+ }
1184
+ defineCE('my-element', MyElement);
1185
+
1186
+ const Ctor = getCE('my-element');
1187
+ document.body.append(new Ctor({ name: 'John' }));
1188
+ ```
1189
+
1190
+ ### Data from HTML attributes
1191
+
1192
+ @example Reading data from an attribute
1193
+
1194
+ ```ts
1195
+ class MyElement extends DC {
1196
+ data = {};
1197
+
1198
+ constructor(target) {
1199
+ super(target);
1200
+ this.data = JSON.parse(target.getAttribute('my-data'));
1201
+ }
1202
+ }
1203
+ defineCE('my-element', MyElement);
1204
+ ```
1205
+
1206
+ Used in HTML as `<my-element my-data='{"name":"John"}'></my-element>`.
1207
+
1208
+ ---
1209
+
1210
+ ## Plugin Extension Points
1211
+
1212
+ The prototype of `DC` (inherited from {@link McBase}) is extended at runtime by optional
1213
+ plugins. See the {@link McBase} documentation for details on {@link State.observePlugin} and
1214
+ {@link State.formValuePlugin}.
1215
+
1216
+ @template T - The concrete element type of the host element.
1217
+ @template EM - The component's custom event map (`event name → payload type`).
1218
+ Declaring it registers events on the type level: `trigger()` enforces the
1219
+ payload for registered names, and `on()`/`one()` handlers receive
1220
+ `CustomEvent<McDetail<Payload>>` with no annotation needed. Unregistered
1221
+ names keep the open `unknown`-payload behavior.
1222
+
1223
+ @example Typed event map
1224
+
1225
+ ```ts
1226
+ class Cart extends DC<HTMLFormElement, { CartUpdate: { count: number } }> {
1227
+ checkout() {
1228
+ this.trigger('CartUpdate', { count: 2 }, 'u');
1229
+ this.on('CartUpdate', (ev) => ev.detail.data.count);
1230
+ }
1231
+ }
1232
+ ```
1233
+
1234
+ `trigger()` checks the payload against the registered `CartUpdate` shape, and the `on()`
1235
+ handler receives a fully typed `ev`.
1236
+ @group Micro Components
1237
+ */
1238
+ var DC = class DC extends McBase {
1239
+ /**
1240
+ * Replaces the target element with a semantic HTML tag, or removes it entirely.
1241
+ * Set to `'header'` to swap `<my-element>` for `<header>`, or `''` to self-destruct.
1242
+ */
1243
+ static tag;
1244
+ /**
1245
+ * Result container for assets (e.g., `['targetHtml', 'json']`).
1246
+ * Asset names (keys) are camelCase and translated into file extensions: `'targetHtml'` → `.target.html`.
1247
+ */
1248
+ static _assets = {};
1249
+ /**
1250
+ * Declares companion files for auto-loading by convention (e.g., `['targetHtml', 'json']`).
1251
+ * Asset names are camelCase and translated into file extensions: `'targetHtml'` → `.target.html`.
1252
+ */
1253
+ static assets = [];
1254
+ /**
1255
+ * In-flight asset request, cached per class so that instances constructed while
1256
+ * the first fetch is still pending (e.g. in a loop) share a single request
1257
+ * instead of each triggering their own. Resolves to the freshly loaded assets.
1258
+ */
1259
+ static _assetsPromise;
1260
+ /**
1261
+ * Sets HTML attributes on every instance automatically.
1262
+ * Example: `{ role: 'listitem', 'aria-label': 'nav' }`.
1263
+ */
1264
+ static attributes = {};
1265
+ /**
1266
+ * Constructs a new DC instance and applies static configuration.
1267
+ *
1268
+ * The constructor performs these steps in order:
1269
+ * 1. Calls {@link McBase} to establish the target reference and auto-attach listeners.
1270
+ * 2. If `static tag` is set and differs from the current element, replaces the target
1271
+ * with a new built-in element (or removes it if `tag === ''`).
1272
+ * 3. If `static assets` has entries, initiates asynchronous loading, fires an `'assets'` event when complete handled by `oneAssets(ev)`.
1273
+ * 4. Applies any `static attributes` to the target element.
1274
+ *
1275
+ * @param target - The host DOM element this component operates on.
1276
+ */
1277
+ constructor(target) {
1278
+ super(target);
1279
+ const ctor = this.constructor;
1280
+ if (ctor === DC) return;
1281
+ const tagName = this.target.tagName.toLowerCase();
1282
+ if (ctor["tag"] !== void 0 && tagName !== ctor.tag && ctor.tag !== "") {
1283
+ const element = document.createElement(ctor.tag);
1284
+ element.classList.add(tagName);
1285
+ ctor._originalTag = tagName;
1286
+ DC.add(element, ctor.name, new ctor(element));
1287
+ delete ctor._originalTag;
1288
+ if (tagName.includes("-") && !this.target.isConnected) this.on("connected", () => this.target.replaceWith(element));
1289
+ else this.target.replaceWith(element);
1290
+ return;
1291
+ }
1292
+ const attributes = ctor.attributes;
1293
+ if (ctor.attributes) Object.keys(ctor.attributes).forEach((key) => {
1294
+ this.target.setAttribute(key, attributes[key]);
1295
+ });
1296
+ const that = this;
1297
+ const remainingAssets = ctor.assets.filter((key) => !ctor._assets[key]);
1298
+ if (Object.keys(ctor._assets).length) processAssets(this, ctor._assets);
1299
+ if (remainingAssets.length) {
1300
+ if (!ctor._assetsPromise) ctor._assetsPromise = loadAssets(ctor._originalTag || tagName, remainingAssets).then((res) => {
1301
+ const loadedAssets = {};
1302
+ remainingAssets.forEach((key, i) => {
1303
+ loadedAssets[key] = res[i];
1304
+ });
1305
+ ctor._assets = Object.assign({}, ctor._assets, loadedAssets);
1306
+ for (const onceKey of ["css", "targetCss"]) if (loadedAssets[onceKey] !== void 0) delete ctor._assets[onceKey];
1307
+ ctor.assets = ctor.assets.filter((key) => key !== "css" && key !== "targetCss");
1308
+ return loadedAssets;
1309
+ });
1310
+ ctor._assetsPromise.then((loadedAssets) => {
1311
+ processAssets(this, loadedAssets);
1312
+ that.trigger("assets", ctor._assets);
1313
+ });
1314
+ } else if ([...Object.keys(ctor._assets)].length) queueMicrotask(() => that.trigger("assets", ctor._assets));
1315
+ if (ctor.tag === "") {
1316
+ this.target.remove();
1317
+ return;
1318
+ }
1319
+ }
1320
+ /**
1321
+ Returns the loaded assets for this component instance.
1322
+
1323
+ Assets are loaded asynchronously when `static assets` is defined on the class.
1324
+ This method returns the resolved asset map keyed by filename, or `undefined`
1325
+ if assets have not yet been loaded.
1326
+
1327
+ @returns A record mapping asset keys to their content strings, or undefined.
1328
+ */
1329
+ getAssets() {
1330
+ return this.constructor._assets || {};
1331
+ }
1332
+ /**
1333
+ Static helper to attach a microcomponent instance to a DOM element.
1334
+
1335
+ Wrapper around the `add()` utility function, providing a class-level API
1336
+ for adding child microcomponents to a target node.
1337
+
1338
+ @template T - The concrete element type of the target; preserved in the return type.
1339
+ @template K - The literal registration key; becomes a known entry of the returned `_mc` registry.
1340
+ @template E - The type of the attached element; retrievable as `result._mc[key]`.
1341
+ @param target - The DOM element to which to attach the microcomponent.
1342
+ @param key - The property name/key under which to store the instance in `_mc`.
1343
+ @returns The target DOM element with the attached microcomponent.
1344
+ */
1345
+ static add(target, key, element = {}) {
1346
+ return add(target, key, element);
1347
+ }
1348
+ /**
1349
+ Static helper to remove a named microcomponent from a DOM element.
1350
+
1351
+ Wrapper around the `remove()` utility function, providing a class-level API
1352
+ for removing child microcomponents.
1353
+
1354
+ @template T - A plain or already-enhanced element type.
1355
+ @template K - The literal registration key; its entry is poisoned in the registry type.
1356
+ @param target - The DOM element from which to remove the microcomponent.
1357
+ @param key - The property name/key corresponding to the microcomponent to remove.
1358
+ */
1359
+ static remove(target, key) {
1360
+ remove(target, key);
1361
+ }
1362
+ /**
1363
+ Class-verified entry lookup: returns the component stored under `key` on the
1364
+ element's `_mc` record — but only if it is an instance of `Klass`.
1365
+
1366
+ This is the *sound* retrieval primitive: presence AND type identity are both
1367
+ proven at runtime (`instanceof`), so it needs no global `McRegistry`
1368
+ declaration and stays correct even when different elements use the same key
1369
+ name for different component types. Accepts any value (including
1370
+ `querySelector` results that may be null) — every failure mode returns
1371
+ `undefined`.
1372
+
1373
+ @template E - The component type, inferred from `Klass`.
1374
+ @param element - The value to look the entry up on; anything non-host yields undefined.
1375
+ @param key - The registry key name to look under.
1376
+ @param elementMcClass - The class the entry must be an instance of.
1377
+ @returns The verified instance, or undefined.
1378
+ @example
1379
+ ```ts
1380
+ const cart = DC.get(document.querySelector('form'), 'cart', Cart);
1381
+ cart?.addItem(item);
1382
+ ```
1383
+
1384
+ `cart` is `Cart | undefined` — presence and type identity are both proven by the
1385
+ `instanceof` check inside `get()`, with no global declaration involved.
1386
+ */
1387
+ static get(element, key, elementMcClass) {
1388
+ if (!isMcHost(element)) return void 0;
1389
+ const entry = element._mc[key];
1390
+ return entry instanceof elementMcClass ? entry : void 0;
1391
+ }
1392
+ /**
1393
+ Detaches an event listener from the DOM node.
1394
+
1395
+ This is an instance method wrapper for `off()`, using the internal `_target` DOM reference.
1396
+
1397
+ @param eventName - The name of the event (e.g., 'click', 'update').
1398
+ @param handler - The previously attached callback function or object to remove.
1399
+ */
1400
+ off(eventName, handler) {
1401
+ off(this.target, eventName, handler);
1402
+ }
1403
+ /**
1404
+ Attaches a persistent event listener to the DOM node.
1405
+
1406
+ This is an instance method wrapper for `on()`, using the internal `_target` DOM reference.
1407
+
1408
+ The handler receives a `CustomEvent<McDetail<Payload>>`. For event names
1409
+ registered in the component's event map `EM` the payload is resolved from the
1410
+ map — an unannotated arrow handler is fully typed. For unregistered names the
1411
+ payload defaults to `unknown` and can be pinned via the type argument
1412
+ (`on<Payload>('Name', handler)`) or the handler's own annotation.
1413
+
1414
+ @param eventName - The name of the event (e.g., 'click', 'update').
1415
+ @param handler - The callback function or object to invoke.
1416
+ @returns The attached handler reference, unchanged — usable directly with {@link off}.
1417
+ */
1418
+ on(eventName, handler) {
1419
+ return on(this.target, eventName, handler);
1420
+ }
1421
+ /**
1422
+ Attaches a one-time event listener to the DOM node.
1423
+
1424
+ This is an instance method wrapper for `one()`, using the internal `_target` DOM reference.
1425
+ Payload typing follows the same rules as {@link DC.on}.
1426
+
1427
+ @param eventName - The name of the event (e.g., 'click', 'update').
1428
+ @param handler - The callback function or object to invoke once.
1429
+ @returns The attached one-time handler reference, unchanged — usable directly with {@link off}.
1430
+ */
1431
+ one(eventName, handler) {
1432
+ return one(this.target, eventName, handler);
1433
+ }
1434
+ /**
1435
+ Triggers a custom event on the DOM node and optionally bubbles it.
1436
+
1437
+ This is an instance method wrapper for `trigger()`, using the internal `_target` DOM reference.
1438
+
1439
+ For event names registered in the component's event map `EM` the payload type
1440
+ is enforced; unregistered names accept any payload (typed via inference or an
1441
+ explicit `trigger<Payload>(...)` argument).
1442
+
1443
+ @param eventName - The name of the event (will be prefixed if needed).
1444
+ @param data - Additional data payload attached to `event.detail.data`.
1445
+ @param bubble - Bubbling direction ('l', 'u', 'd' or combinations).
1446
+ @param controller - Optional controller for managing bubbling behavior.
1447
+ */
1448
+ trigger(eventName, data = {}, bubble = "l", controller) {
1449
+ trigger(this.target, eventName, data, bubble, controller);
1450
+ }
1451
+ /**
1452
+ Returns an array of all ancestor microcomponents up the DOM tree.
1453
+
1454
+ Traverses upward from this element's target, collecting the `_mc` instances
1455
+ of ancestor elements marked with `data-mc`. The keyed form only collects
1456
+ hosts whose registry carries `key`, typed from the global {@link McRegistry}
1457
+ binding — non-optional, since presence is checked during the walk.
1458
+
1459
+ @template R - Known registry entries of the results; pass a record to assert
1460
+ a site-specific shape: `ancestors<{ cart: Cart }>()`. Defaults to none.
1461
+ @returns An array of ancestor microcomponent instances (empty if none found).
1462
+ */
1463
+ ancestors(key) {
1464
+ const result = [];
1465
+ let current = findMcAncestor(this.target, key);
1466
+ while (current) {
1467
+ result.push(current);
1468
+ current = findMcAncestor(current.target, key);
1469
+ }
1470
+ return result;
1471
+ }
1472
+ /**
1473
+ Returns the closest ancestor microcomponent or null if none found.
1474
+
1475
+ This method is equivalent to calling `ancestors()[0]`, but more concise.
1476
+ The keyed form walks up to the nearest host whose registry carries `key`,
1477
+ typed from the global {@link McRegistry} binding — non-optional, since
1478
+ presence is checked during the walk.
1479
+
1480
+ @template R - Known registry entries of the result; pass a record to assert
1481
+ a site-specific shape: `closest<{ cart: Cart }>()?.cart.addItem(...)`.
1482
+ Defaults to none.
1483
+ @returns The nearest matching ancestor's `_mc`, or null if not found.
1484
+ */
1485
+ closest(key) {
1486
+ return findMcAncestor(this.target, key);
1487
+ }
1488
+ /**
1489
+ Returns an array of direct child microcomponents.
1490
+
1491
+ Calls `getMcChildNodes()` internally to find immediate descendants only,
1492
+ excluding nested grandchildren and deeper levels. The keyed form filters to
1493
+ hosts whose registry carries `key`; see {@link DC.closest}.
1494
+
1495
+ @template R - Known registry entries of the results; see {@link DC.closest}.
1496
+ @returns An array of child microcomponent instances (empty if none found).
1497
+ */
1498
+ children(key) {
1499
+ const kids = getMcChildNodes(this.target);
1500
+ return key === void 0 ? kids : kids.filter((mc) => key in mc);
1501
+ }
1502
+ /**
1503
+ Returns an array of all descendant microcomponents recursively.
1504
+
1505
+ Uses a `querySelectorAll` query to collect every nested element marked with
1506
+ `data-mc`, returning their associated `_mc` instances. The keyed form
1507
+ filters to hosts whose registry carries `key`; see {@link DC.closest}.
1508
+
1509
+ @template R - Known registry entries of the results; see {@link DC.closest}.
1510
+ @returns An array of descendant microcomponent instances (empty if none found).
1511
+ */
1512
+ descendants(key) {
1513
+ return Array.from(this.target.querySelectorAll("[data-mc]")).filter((e) => e instanceof HTMLElement && "_mc" in e).map((e) => e._mc).filter((mc) => key === void 0 || key in mc);
1514
+ }
1515
+ };
1516
+ //#endregion
1517
+ //#region src/utils/resolveComponentPath.ts
1518
+ /**
1519
+ @module Autoload
1520
+
1521
+ Single source of truth for the tag-to-file convention: hyphens become path
1522
+ separators, so `my-card` resolves to `./my/card.js`. Imported by the runtime
1523
+ autoload loader and by build-time tooling so both agree on the path without
1524
+ either duplicating the rule.
1525
+ */
1526
+ /**
1527
+ The tag-to-path primitive: hyphens become path separators, with no prefix or
1528
+ extension. `my-card` → `my/card`. The single source of truth for the
1529
+ convention; {@link resolveComponentPath} and build-time tooling both derive
1530
+ their paths from it so neither duplicates the rule.
1531
+
1532
+ @example Returns `'my/card'` for `my-card`, and `'app/header'` for `app-header`.
1533
+ ```ts
1534
+ tagPath('my-card');
1535
+ tagPath('app-header');
1536
+ ```
1537
+ @hidden
1538
+ @group Autoload
1539
+ */
1540
+ function tagPath(tag) {
1541
+ return tag.split("-").join("/");
1542
+ }
1543
+ /**
1544
+ Resolves a microcomponent tag name to its autoload module path by prefixing and
1545
+ suffixing {@link tagPath}: `my-card` → `./my/card.js`.
1546
+
1547
+ @example Returns `'./my/card.js'` for `my-card`, and `'./app/header.js'` for `app-header`.
1548
+ ```ts
1549
+ resolveComponentPath('my-card');
1550
+ resolveComponentPath('app-header');
1551
+ ```
1552
+ @group Autoload
1553
+ */
1554
+ function resolveComponentPath(tag) {
1555
+ return "./" + tagPath(tag) + ".js";
1556
+ }
1557
+ //#endregion
1558
+ //#region src/utils/autoload.ts
1559
+ /**
1560
+ @module Autoload
1561
+
1562
+ The {@linkcode autoload} function enables automatic detection and instantiation of microcomponents in the DOM via a `MutationObserver`. When enabled, newly added elements with undefined custom tags or `data-mc` references will trigger dynamic script loading and component attachment.
1563
+
1564
+ Kept in its own module so the core controller classes in MC.ts stay free of
1565
+ side-effect-bearing DOM machinery (`MutationObserver`, script injection):
1566
+ importing `DC`/`MC` never pulls a DOM observer into the bundle unless
1567
+ {@link autoload} itself is imported.
1568
+ */
1569
+ var loadingDCs = /* @__PURE__ */ new Set();
1570
+ /**
1571
+ Creates a closure that attaches DOM elements with the specified tag name to
1572
+ their corresponding microcomponent instances.
1573
+
1574
+ Used internally by `makeLoadScript()` when dynamically loading external
1575
+ custom element definitions. The returned function is invoked after the
1576
+ element definition becomes available, and performs:
1577
+ - Querying of all matching elements in the DOM
1578
+ - Instantiation of new `DC` instances
1579
+ - Attachment to each DOM node via `DC.add()`
1580
+
1581
+ @param element - The tag name (custom element name) of the microcomponent to instantiate.
1582
+ @returns A closure that performs the attachment logic when invoked.
1583
+ @group Autoload
1584
+ */
1585
+ function attachDC(element) {
1586
+ return () => {
1587
+ const elements = Array.from(document.querySelectorAll(`[data-mc*="${element}"]`)).filter((e) => e instanceof HTMLElement);
1588
+ if (elements.length === 0) return;
1589
+ const customElementClass = customElements.get(element);
1590
+ for (let node of elements) {
1591
+ try {
1592
+ DC.add(node, customElementClass.name, new customElementClass(node));
1593
+ } catch (error) {
1594
+ /* v8 ignore start */
1595
+ if ("create" in customElementClass && typeof customElementClass.create === "function") {
1596
+ const customElement = customElementClass.create(node);
1597
+ if (!isMcHost(customElement)) throw `create() of Class ${customElementClass.name} did not return a McHost.`;
1598
+ customElement.setAttribute("data-mc", node.getAttribute("data-mc") ?? "");
1599
+ node.replaceWith(customElement);
1600
+ node = customElement;
1601
+ } else throw `Class ${customElementClass.name} doesn't have a callable constructor AND class doesn't have a static .create() method.`;
1602
+ }
1603
+ const newMcName = node.getAttribute("data-mc").split(" ").filter((name) => name !== element && name !== "").join(" ").trim();
1604
+ node.setAttribute("data-mc", newMcName);
1605
+ }
1606
+ };
1607
+ }
1608
+ /**
1609
+ Dynamically loads and registers a custom element definition from an external JS file.
1610
+
1611
+ Constructs the script URL based on kebab-case naming (e.g., `my-element` → `./my/element.js`).
1612
+ Registers a handler to be executed once the element is defined, then appends the
1613
+ `<script>` tag to `<head>`. Handles both successful loading and error cases.
1614
+
1615
+ @param element - The custom element tag name (e.g., 'mc-button', 'my-widget').
1616
+ */
1617
+ function makeLoadScript(element) {
1618
+ /* v8 ignore next */
1619
+ if (loadingDCs.has(element)) return;
1620
+ loadingDCs.add(element);
1621
+ const fileName = resolveComponentPath(element);
1622
+ const scriptElement = document.createElement("script");
1623
+ scriptElement.setAttribute("src", fileName);
1624
+ scriptElement.setAttribute("blocking", "render");
1625
+ scriptElement.async = true;
1626
+ scriptElement.setAttribute("type", "module");
1627
+ scriptElement.setAttribute("data-name", element);
1628
+ scriptElement.setAttribute("loading", "");
1629
+ customElements.whenDefined(element).then(attachDC(element));
1630
+ scriptElement.addEventListener("load", (ev) => {
1631
+ loadingDCs.delete(element);
1632
+ document.head.querySelector(`script[data-name="${element}"]`)?.remove();
1633
+ });
1634
+ scriptElement.addEventListener("error", (ev) => {
1635
+ console.error("could not load DC code from", fileName);
1636
+ });
1637
+ document.head.append(scriptElement);
1638
+ }
1639
+ /**
1640
+ Scans the DOM for undefined custom elements and pending microcomponents,
1641
+ then loads their definitions if necessary.
1642
+
1643
+ This is the main driver function for the autoload mechanism. It performs:
1644
+ 1. Finds all undefined custom elements in the document
1645
+ 2. Checks whether they are already loading or defined
1646
+ 3. Calls `makeLoadScript()` to fetch missing definitions
1647
+ 4. Invokes `attachDC()` where element definitions exist but haven't been attached yet
1648
+
1649
+ It is called both immediately after enabling autoload and on each DOM mutation.
1650
+ */
1651
+ function loadUndefinedMCs() {
1652
+ [...document.querySelectorAll(":not(:defined)")].filter((element) => !!element && !customElements.get(element.tagName.toLowerCase()) && !loadingDCs.has(element.tagName.toLowerCase())).forEach((el) => {
1653
+ makeLoadScript(el.tagName.toLowerCase());
1654
+ });
1655
+ [...document.querySelectorAll("[data-mc]:not([data-mc=\"\"]")].filter((element) => !!element).forEach((el) => {
1656
+ (el.getAttribute("data-mc")?.split(" ").filter((name) => name !== ""))?.forEach((element) => {
1657
+ const isDefined = customElements.get(element);
1658
+ if (!loadingDCs.has(element) && !isDefined) makeLoadScript(element);
1659
+ else if (isDefined) attachDC(element)();
1660
+ });
1661
+ });
1662
+ }
1663
+ var observer = null;
1664
+ function autoload(enable = true) {
1665
+ if (!enable) {
1666
+ if (observer) {
1667
+ observer.disconnect();
1668
+ observer = null;
1669
+ }
1670
+ return false;
1671
+ }
1672
+ if (!!observer) return true;
1673
+ observer = new MutationObserver(loadUndefinedMCs);
1674
+ setTimeout(() => {
1675
+ observer.observe(document.body, {
1676
+ childList: true,
1677
+ subtree: true
1678
+ });
1679
+ loadUndefinedMCs();
1680
+ }, 5);
1681
+ window.autoload ??= autoload;
1682
+ return true;
1683
+ }
1684
+ autoload.state = loadingDCs;
1685
+ //#endregion
1686
+ //#region src/core/elements.ts
1687
+ /**
1688
+ Custom element registration utilities — bridges DC microcomponents with the native
1689
+ `CustomElementRegistry`.
1690
+
1691
+ This module provides:
1692
+ - {@link defineCE} — wraps `customElements.define()` to register a custom element that
1693
+ hosts a DC instance in its `_mc` registry. The defining class also controls observed
1694
+ attributes; all inner MCs receive lifecycle events (`connected`, `disconnected`, etc.).
1695
+ - {@link getCE} — typed wrapper for `customElements.get()`.
1696
+ - {@link listCE} — returns every tag name registered via {@link defineCE}.
1697
+
1698
+ Familiarity with the [Custom Elements Web Standard](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements) is recommended.
1699
+
1700
+ For DC-specific documentation (static properties, event handlers, lifecycle events,
1701
+ construction patterns), see the {@link DC} class in `MC.ts`.
1702
+
1703
+ @group Elements
1704
+
1705
+ @example Define custom element in registry
1706
+
1707
+ ```ts
1708
+ class MyElement extends DC {}
1709
+ defineCE('my-element', MyElement);
1710
+ ```
1711
+
1712
+ The registered element is then used in HTML as `<my-element></my-element>`.
1713
+
1714
+ @example Retrieve custom element from registry
1715
+
1716
+ ```ts
1717
+ const MyElementClass = getCE('my-element');
1718
+ if (MyElementClass) {
1719
+ document.body.append(new MyElementClass());
1720
+ }
1721
+ ```
1722
+
1723
+ @example List all custom element tags registered by defineCE
1724
+
1725
+ ```ts
1726
+ defineCE('my-element', MyElement);
1727
+ defineCE('another-widget', AnotherWidget);
1728
+ console.log(listCE());
1729
+ ```
1730
+ */
1731
+ /**
1732
+ Creates a named class constructor with the specified name.
1733
+ @param name - The desired name for the class.
1734
+ @param targetClass - The class constructor to which the name will be assigned.
1735
+ @returns The class constructor with the name property set.
1736
+
1737
+ @example
1738
+ ```ts
1739
+ const MyNamedClass = createNamedClass('MyNamedClass', class { });
1740
+ console.log(MyNamedClass.name);
1741
+ ```
1742
+ */
1743
+ function createNamedClass(name, targetClass) {
1744
+ return Object.defineProperty(targetClass, "name", { value: name });
1745
+ }
1746
+ function Base(ElementClass) {
1747
+ return class extends ElementClass {
1748
+ constructor(...args) {
1749
+ super(...args);
1750
+ }
1751
+ };
1752
+ }
1753
+ /**
1754
+ ### Examples
1755
+ ---
1756
+
1757
+ #### Define custom element in registry
1758
+
1759
+ ```ts
1760
+ class MyElement extends DC {}
1761
+ defineCE('my-element', MyElement);
1762
+ ```
1763
+
1764
+ The registered element is then used in HTML as `<my-element></my-element>`.
1765
+
1766
+ #### Define custom element in registry with observed attributes
1767
+
1768
+ ```ts
1769
+ class MyElement extends DC {
1770
+ onAttributeChanged(ev){
1771
+ const newValue = ev.detail.data.newValue;
1772
+ }
1773
+ }
1774
+ defineCE('my-element', MyElement, ['observed']);
1775
+ ```
1776
+
1777
+ Used in HTML as `<my-element observed></my-element>`. When the `observed` attribute
1778
+ changes, `attributeChanged` is triggered, which calls `onAttributeChanged` with the
1779
+ change data in the payload.
1780
+
1781
+ Defines a custom HTML element with optional observed attributes and micro component integration.
1782
+ The class provided in DefinitionClass will be added to the ._mc hash object appended to the custom element instance.
1783
+ Automatically assigns a "PascalCase" class name derived from the <kebab-case> tagName.
1784
+ If the provided definition class extends McBase (indicating a DC or MC class extension), it wraps the constructor to register
1785
+ the element instance with DC (DomComponent).
1786
+
1787
+ @param tagName - The HTML tag name for the custom element (must contain at least one hyphen, enforced at compile time).
1788
+ @param DefinitionClass - The constructor function or class that defines the element's behavior. Its `target` parameter type follows `extend`; an optional second `data` parameter receives the payload passed on programmatic construction.
1789
+ @param observedAttributes - An optional array of attribute names to observe for changes.
1790
+ @param extend - DEPRECATED! An optional built-in HTML element tag name to extend, enabling customized built-in elements.
1791
+ @returns The registered custom element constructor, typed to accept the `data` payload
1792
+ and exposing the `_mc` entry under the PascalCase tag name — or `undefined` if the
1793
+ tag name was already defined. Feed it (or an equivalent `McElementConstructor`) to
1794
+ the global {@link McElementTagNameMap} to make `getCE()` return it typed by string.
1795
+ @group Elements
1796
+ */
1797
+ function defineCE(tagName, DefinitionClass, observedAttributes = [], extend) {
1798
+ if (customElements.get(tagName)) {
1799
+ console.warn(`IGNORED: Custom element "${tagName}" is already defined!`);
1800
+ return;
1801
+ }
1802
+ if (!tagName.includes("-")) throw new Error("createCustomElement: tagName must contain at least one hyphen (\"-\").");
1803
+ let extendClass = HTMLElement;
1804
+ if (extend) extendClass = document.createElement(extend).constructor;
1805
+ const pascalCaseName = kebabToPascal(tagName);
1806
+ if (!(DefinitionClass.prototype instanceof McBase)) {
1807
+ customElements.define(tagName, DefinitionClass);
1808
+ return DefinitionClass;
1809
+ }
1810
+ const McElementClass = createNamedClass(pascalCaseName, class extends Base(extendClass) {
1811
+ constructor(data) {
1812
+ super();
1813
+ DC.add(this, pascalCaseName, new DefinitionClass(this, data));
1814
+ }
1815
+ connectedCallback() {
1816
+ this.dispatchEvent(new CustomEvent("_mcConnected"));
1817
+ }
1818
+ connectedMoveCallback() {
1819
+ this.dispatchEvent(new CustomEvent("_mcConnectedMove"));
1820
+ }
1821
+ disconnectedCallback() {
1822
+ this.dispatchEvent(new CustomEvent("_mcDisconnected"));
1823
+ }
1824
+ adoptedCallback() {
1825
+ this.dispatchEvent(new CustomEvent("_mcAdopted"));
1826
+ }
1827
+ attributeChangedCallback(name, oldValue, newValue) {
1828
+ this.dispatchEvent(new CustomEvent("_mcAttributeChanged", { detail: {
1829
+ name,
1830
+ oldValue,
1831
+ newValue
1832
+ } }));
1833
+ }
1834
+ formResetCallback() {
1835
+ this.dispatchEvent(new CustomEvent("_mcFormReset"));
1836
+ }
1837
+ static get observedAttributes() {
1838
+ return [...observedAttributes];
1839
+ }
1840
+ });
1841
+ customElements.define(tagName, McElementClass, { extends: extend });
1842
+ if (!defineCE.list) defineCE.list = [];
1843
+ defineCE.list.push(tagName);
1844
+ return McElementClass;
1845
+ }
1846
+ function getCE(tagName) {
1847
+ return customElements.get(tagName);
1848
+ }
1849
+ /**
1850
+ Returns an array of all tag names registered via {@link defineCE}.
1851
+
1852
+ @example
1853
+ ```ts
1854
+ defineCE('my-element', MyElement);
1855
+ defineCE('another-widget', AnotherWidget);
1856
+ console.log(listCE());
1857
+ ```
1858
+ @group Elements
1859
+ */
1860
+ function listCE() {
1861
+ return defineCE.list ?? [];
1862
+ }
1863
+ //#endregion
1864
+ //#region src/state/placeholders.ts
1865
+ /**
1866
+ @hidden
1867
+ DOM placeholder binding: `{name}` templates in attributes and text nodes,
1868
+ kept up to date from observable data.
1869
+
1870
+ Split from observables.ts so the pure observable machinery carries no DOM
1871
+ scanning/decoration code. The two modules reference each other only inside
1872
+ function bodies (`Placeholders` calls `observe(this)`; `bind()` calls
1873
+ {@link makePlaceholders}), never during module evaluation, so the circular
1874
+ import is benign under ESM live bindings.
1875
+ */
1876
+ /**
1877
+ Extracts placeholder names from an HTML element's attributes and text content.
1878
+
1879
+ Placeholders are strings enclosed in curly braces `{}`. This function scans
1880
+ all attributes and direct child text nodes of the target element for placeholders
1881
+ and returns them as an array of unique names (without braces).
1882
+
1883
+ @param target - The HTMLElement to scan for placeholders.
1884
+ @returns An array of placeholder names found within the element's attributes and text content.
1885
+ */
1886
+ function getPlaceholders(target) {
1887
+ const placeholders = /* @__PURE__ */ new Set(), regEx = /\{[^\{\}]*\}/g;
1888
+ (target.getAttributeNames ? target.getAttributeNames() : []).filter((attr) => target.getAttribute(attr)?.match(regEx)).forEach((attr) => {
1889
+ new Set(target.getAttribute(attr)?.match(regEx)).forEach((attrPlaceholder) => placeholders.add(attrPlaceholder.replace(/[\{\}]/g, "")));
1890
+ });
1891
+ [...target.childNodes].filter((childNode) => {
1892
+ return childNode.nodeType === 3 && (childNode.nodeValue || "").match(regEx);
1893
+ }).forEach((childNode) => {
1894
+ (childNode?.nodeValue || "").match(regEx).forEach((textNodePlaceholder) => placeholders.add(textNodePlaceholder.replace(/[\{\}]/g, "")));
1895
+ });
1896
+ return [...placeholders];
1897
+ }
1898
+ var inputTags = [
1899
+ "INPUT",
1900
+ "SELECT",
1901
+ "TEXTAREA"
1902
+ ];
1903
+ /**
1904
+ A class that manages placeholders within an HTML element and updates them dynamically.
1905
+
1906
+ This class extends `DC` (Data Controller) and processes the target element to identify
1907
+ placeholder patterns in attributes and text nodes. It then observes changes on its internal
1908
+ data object and updates the corresponding DOM locations when notified of changes.
1909
+ */
1910
+ var Placeholders = class extends DC {
1911
+ data = {};
1912
+ oldDisplay = "";
1913
+ /**
1914
+ Creates a new `Placeholders` instance for the given HTML element.
1915
+
1916
+ Initializes the internal data with empty strings for each placeholder
1917
+ found in the target and starts observing the data object. Attaches
1918
+ callbacks to update placeholders as needed.
1919
+
1920
+ @param target - The HTMLElement to process for placeholders.
1921
+ */
1922
+ constructor(target) {
1923
+ super(target);
1924
+ getPlaceholders(target).forEach((property) => this.data[property] = "");
1925
+ observe(this);
1926
+ this.#attachCallbacks();
1927
+ }
1928
+ #attachCallbacks() {
1929
+ const target = this.target;
1930
+ let hide = false;
1931
+ (target.getAttributeNames ? target.getAttributeNames() : []).filter((attr) => target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g)).forEach((attr) => {
1932
+ const placeholders = new Set(target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g));
1933
+ /* v8 ignore next -- attr passed the filter above, so its value is a non-null string */
1934
+ const attrTemplate = target.getAttribute(attr) ?? "";
1935
+ this.data.subscribe(((template) => (data) => {
1936
+ let result = template;
1937
+ placeholders.forEach((placeholder) => {
1938
+ result = result.replace(placeholder, String(data[placeholder.replace(/[\{\}]/g, "")]));
1939
+ });
1940
+ target.setAttribute(attr, result);
1941
+ })(attrTemplate));
1942
+ });
1943
+ [...target.childNodes].filter((childNode) => {
1944
+ return childNode.nodeType === 3 && (childNode.nodeValue || "").match(/\{[^\{\}]*\}/g);
1945
+ }).forEach((childNode) => {
1946
+ hide = true;
1947
+ /* v8 ignore next -- the filter above only admits text nodes with a non-empty
1948
+ nodeValue, so the ?? fallback is unreachable */
1949
+ const textTemplate = childNode.nodeValue ?? "";
1950
+ const placeholders = new Set(textTemplate.match(/\{[^\{\}]*\}/g));
1951
+ this.data.subscribe(((template) => (data) => {
1952
+ let result = template;
1953
+ placeholders.forEach((placeholder) => {
1954
+ result = result.replace(placeholder, String(data[placeholder.replace(/[\{\}]/g, "")]));
1955
+ });
1956
+ if (this.oldDisplay) target.style = this.oldDisplay;
1957
+ else target.removeAttribute("style");
1958
+ delete this.oldDisplay;
1959
+ childNode.nodeValue = result;
1960
+ })(textTemplate));
1961
+ this.oldDisplay = target.getAttribute("style") || "";
1962
+ if (hide && !inputTags.includes(target.tagName) && target.tagName !== "FIELDSET") target.style.display = "none";
1963
+ });
1964
+ }
1965
+ /**
1966
+ Handles a `data` custom event to update the internal placeholder data.
1967
+
1968
+ Extracts matching keys from the event's detail data and updates only those
1969
+ properties in the internal `data` object that were previously defined as placeholders.
1970
+
1971
+ @param ev - The CustomEvent containing new data to merge into the placeholders.
1972
+ */
1973
+ onData(ev) {
1974
+ const data = {};
1975
+ Object.keys(ev.detail.data).forEach((key) => {
1976
+ if (this.data.hasOwnProperty(key)) data[key] = ev.detail.data[key];
1977
+ });
1978
+ if (JSON.stringify(data) !== JSON.stringify(this.data)) Object.assign(this.data, data);
1979
+ }
1980
+ };
1981
+ /**
1982
+ Finds all elements within a given scope that contain placeholders.
1983
+
1984
+ @param target - The HTMLElement, DocumentFragment, or ShadowRoot to search.
1985
+ @returns An array of HTMLElements containing placeholder patterns.
1986
+ */
1987
+ function getPlaceholderElements(target) {
1988
+ return [target].concat([...target.querySelectorAll("*")]).filter((e) => !!getPlaceholders(e).length).filter((e) => e?.tagName !== "STYLE").reverse();
1989
+ }
1990
+ /**
1991
+ Applies placeholder support to an element and its descendants.
1992
+
1993
+ This function scans the provided target for elements containing placeholders
1994
+ and attaches a `Placeholders` controller instance to each one if not already present.
1995
+
1996
+ @param element - The HTMLElement, DocumentFragment, or ShadowRoot to process.
1997
+ */
1998
+ function makePlaceholders(element) {
1999
+ getPlaceholderElements(element).forEach((e) => {
2000
+ if (e?._mc?._placeholders) return;
2001
+ DC.add(e, "_placeholders", new Placeholders(e));
2002
+ });
2003
+ }
2004
+ //#endregion
2005
+ //#region src/state/observables.ts
2006
+ /**
2007
+ Reactive data binding via observables — object properties trigger callbacks and update
2008
+ DOM placeholders automatically when their values change. Supports primitives, nested objects,
2009
+ and deep-change detection with debounced notifications.
2010
+
2011
+ @module Observables
2012
+
2013
+ Two APIs are provided for different contexts:
2014
+
2015
+ - **`observe()`** — standalone function for general usage outside the MC ecosystem.
2016
+ Call it on any plain object to make its properties observable. You manage the resulting
2017
+ data object yourself (subscribe, bind, notify).
2018
+
2019
+ - **`observePlugin()`** — used inside MC custom elements (classes extending `DC`).
2020
+ It attaches `.notify()`, `.subscribe()`, and `.bind()` as chainable instance methods
2021
+ that implicitly create the observable wrapper for you. Just declare a plain data property
2022
+ on your component and call `this.subscribe('data', …)` — the observable is generated on demand.
2023
+
2024
+ > **Note:** `.bind()` only works with objects because their properties become the placeholder
2025
+ > names in HTML (e.g., `{name}`, `{count}`). Binding primitives to the DOM is silently ignored.
2026
+
2027
+ @example Plugin — inside an MC custom element (recommended within DC classes)
2028
+ ```ts
2029
+ import { DC, defineCE, observePlugin } from '@twobirds/microcomponents';
2030
+
2031
+ observePlugin(DC);
2032
+
2033
+ class Counter extends DC {
2034
+ data = { clicks: 1 };
2035
+
2036
+ oneAssets() {
2037
+ this
2038
+ .subscribe('data', (v) => console.log('data →', v))
2039
+ .bind('data', this.target);
2040
+ }
2041
+ }
2042
+
2043
+ defineCE('mc-counter', Counter);
2044
+ ```
2045
+
2046
+ @example Standalone — outside the MC ecosystem (plain objects, vanilla JS, etc.)
2047
+ ```ts
2048
+ import { observe } from '@twobirds/microcomponents';
2049
+
2050
+ const state = observe({ count: 0, label: 'hi' });
2051
+ state.count.subscribe((v) => console.log('count is now', v));
2052
+ state.count++;
2053
+ ```
2054
+
2055
+ Incrementing `state.count` triggers the subscribed callback.
2056
+
2057
+ @example Deep change detection on a nested property
2058
+ ```ts
2059
+ import { observe } from '@twobirds/microcomponents';
2060
+
2061
+ const state = observe({ data: { name: 'John', age: 30 } });
2062
+ state.data.subscribe((d) => console.log('data changed:', d));
2063
+ state.data.name = 'Jane';
2064
+ ```
2065
+
2066
+ Writing into a nested property triggers the callback with the updated object.
2067
+
2068
+ @example DOM binding — only observable properties that hold objects can be bound
2069
+ ```ts
2070
+ import { observe } from '@twobirds/microcomponents';
2071
+
2072
+ const state = observe({ user: { name: 'Alice', count: 0 } });
2073
+ state.user.bind(document.querySelector('.model'));
2074
+ ```
2075
+
2076
+ Inside the bound model, `<span>{name}</span>` and `<span>{count}</span>` update
2077
+ automatically on every change.
2078
+
2079
+ */
2080
+ /**
2081
+ @hidden
2082
+ Determines if a value is an observable object.
2083
+
2084
+ @param value - The value to check.
2085
+ @returns `true` if the value is a non-null object, otherwise `false`.
2086
+ */
2087
+ function isObservableObject(value) {
2088
+ return value != null && typeof value === "object";
2089
+ }
2090
+ var classRepo = {};
2091
+ /**
2092
+ @hidden
2093
+ Creates a new observable wrapper class for the given value's constructor.
2094
+
2095
+ Returns a subclass of the original constructor that implements the Observable interface,
2096
+ allowing observation and notification of changes. The new class is cached in `classRepo`.
2097
+
2098
+ @param val - The value whose constructor should be wrapped with observable behavior.
2099
+ @returns A new Observable class that wraps the original constructor.
2100
+ */
2101
+ function makeObservable(val) {
2102
+ const Constructor = val.constructor, ConstructorName = Constructor.name;
2103
+ const ObservableClass = class extends Constructor {
2104
+ #notify = true;
2105
+ #callbacks = [];
2106
+ constructor(valArg) {
2107
+ super(valArg);
2108
+ const ctor = this.constructor;
2109
+ ctor.callbacks = [];
2110
+ if (isObservableObject(valArg)) Object.getOwnPropertyNames(valArg).forEach((key) => {
2111
+ this[key] = valArg[key];
2112
+ });
2113
+ }
2114
+ get callbacks() {
2115
+ return this.#callbacks;
2116
+ }
2117
+ set callbacks(callbacks) {
2118
+ this.#callbacks = callbacks;
2119
+ }
2120
+ /**
2121
+ Coerces the observable to its underlying primitive value when used in
2122
+ arithmetic, string interpolation, or other contexts requiring a primitive.
2123
+
2124
+ This allows `observable + 1` or `` `${observable}` `` to work naturally
2125
+ while still exposing `.subscribe()`, `.notify()`, etc. as methods on the object.
2126
+
2127
+ @param hint - The coercion context: 'number', 'string', or 'default'.
2128
+ @returns The underlying primitive value coerced to the requested type.
2129
+
2130
+ @example
2131
+ ```ts
2132
+ const state = observe({ count: 42 });
2133
+ console.log(state.count + 10);
2134
+ console.log(`${state.count}`);
2135
+ state.count.subscribe((v) => console.log('changed to', v));
2136
+ ```
2137
+
2138
+ Arithmetic coerces with `hint === 'number'` (logs `52`), interpolation with
2139
+ `hint === 'string'` (logs `"42"`), and `.subscribe()` stays callable on the
2140
+ wrapper.
2141
+ */
2142
+ [Symbol.toPrimitive](hint) {
2143
+ const value = this.valueOf();
2144
+ if (hint === "string") return String(value);
2145
+ if (hint === "number") return Number(value);
2146
+ return value;
2147
+ }
2148
+ /**
2149
+ Registers a callback function to be invoked when the observable object is notified.
2150
+
2151
+ @param callback - The function to invoke on notification.
2152
+ @returns This instance for chaining.
2153
+
2154
+ @example
2155
+ ```ts
2156
+ const state = observe({ count: 0 });
2157
+ state.count.subscribe((v) => console.log('count changed to', v));
2158
+ state.count++;
2159
+ ```
2160
+
2161
+ The increment fires the callback, logging `count changed to 1`.
2162
+ */
2163
+ subscribe(callback) {
2164
+ this.#callbacks.push(callback);
2165
+ return this;
2166
+ }
2167
+ /**
2168
+ Removes a previously registered callback so it no longer fires on notify.
2169
+
2170
+ Detaching by the same function reference that was passed to `subscribe()` —
2171
+ unknown callbacks are a silent no-op. Long-lived subscribers should call
2172
+ this when they are done to avoid leaking callbacks.
2173
+
2174
+ @param callback - The exact callback reference to remove.
2175
+ @returns This instance for chaining.
2176
+
2177
+ @example
2178
+ ```ts
2179
+ const state = observe({ count: 0 });
2180
+ const log = (v) => console.log(v);
2181
+ state.count.subscribe(log);
2182
+ state.count.unsubscribe(log);
2183
+ ```
2184
+
2185
+ After `unsubscribe(log)`, the callback no longer fires on changes.
2186
+ */
2187
+ unsubscribe(callback) {
2188
+ this.#callbacks = this.#callbacks.filter((registered) => registered !== callback);
2189
+ return this;
2190
+ }
2191
+ /**
2192
+ Binds the observable to a DOM target, setting up placeholder updates and
2193
+ triggering initial callbacks.
2194
+
2195
+ Scans the target and its descendants for placeholders and registers handlers
2196
+ to update them with flattened data on notification. Only works in browser environments.
2197
+
2198
+ @param target - The HTMLElement, DocumentFragment, or ShadowRoot to bind.
2199
+ @returns This instance for chaining.
2200
+
2201
+ @example
2202
+ ```ts
2203
+ const state = observe({ user: { name: 'Alice', count: 0 } });
2204
+ state.user.bind(document.querySelector('.model'));
2205
+ ```
2206
+
2207
+ Only object-valued observables can be bound: inside the model, `{name}` and
2208
+ `{count}` placeholders update on every change.
2209
+ */
2210
+ bind(target) {
2211
+ if (!isBrowser()) return;
2212
+ if (isObservableObject(this.valueOf())) {
2213
+ makePlaceholders(target);
2214
+ const placeholders = [target, ...target.querySelectorAll("[data-mc]")].filter((e) => {
2215
+ return !!e?._mc?._placeholders;
2216
+ });
2217
+ this.#callbacks.push(function callback(data) {
2218
+ const flattenedData = flattenObject(data);
2219
+ placeholders.forEach((node) => {
2220
+ /* v8 ignore next */
2221
+ if (!node) return;
2222
+ trigger(node, "data", flattenedData);
2223
+ });
2224
+ });
2225
+ this.notify();
2226
+ } else console.warn("IGNORED - cannot bind a non-Object to the dom, IS:", typeof this.valueOf(), this);
2227
+ return this;
2228
+ }
2229
+ /**
2230
+ Notifies registered callbacks with the current observable data.
2231
+
2232
+ Clones the internal value (if it's an object and not an array) before passing
2233
+ to callbacks. Respects the internal `#notify` flag that can suppress notifications.
2234
+
2235
+ @param notify - Optional boolean override for the notification state.
2236
+ @returns This instance for chaining.
2237
+
2238
+ @example
2239
+ ```ts
2240
+ const state = observe({ count: 0 });
2241
+ state.count.subscribe((v) => console.log('notified:', v));
2242
+ state.count.notify();
2243
+ ```
2244
+
2245
+ The manual `notify()` call pushes the current value to subscribers, logging
2246
+ `notified: 0`.
2247
+ */
2248
+ notify(notify) {
2249
+ const data = isObservableObject(this.valueOf()) && this.valueOf() instanceof Array === false ? structuredClone(Object.assign({}, this)) : this.valueOf();
2250
+ if (notify !== void 0) this.#notify = notify;
2251
+ if (this.#notify) this.#callbacks.forEach((f) => {
2252
+ f(data);
2253
+ });
2254
+ return this;
2255
+ }
2256
+ };
2257
+ classRepo[ConstructorName] = ObservableClass;
2258
+ return ObservableClass;
2259
+ }
2260
+ /**
2261
+ Retrieves or creates an observable wrapper class for the given value.
2262
+
2263
+ @param val - The value whose constructor should be wrapped with observable behavior.
2264
+ @returns A class that wraps the original constructor and implements observable functionality.
2265
+ */
2266
+ function getConstructor(val) {
2267
+ return classRepo[val.constructor.name] || makeObservable(val);
2268
+ }
2269
+ /**
2270
+ @hidden
2271
+ Whether a value is already an observable wrapper — `subscribe` is reachable on its prototype chain.
2272
+ Used by {@link observe} to skip re-wrapping and to detect the "not yet observed" state on reassignment.
2273
+
2274
+ @example
2275
+ ```ts
2276
+ const state = observe({ count: 0 });
2277
+ console.log(isAlreadyObserved(state.count));
2278
+ console.log(isAlreadyObserved(42));
2279
+ ```
2280
+
2281
+ The wrapped property reports `true`, a plain primitive reports `false`.
2282
+ */
2283
+ function isAlreadyObserved(value) {
2284
+ return (typeof value === "object" || typeof value === "function") && value !== null && "subscribe" in value;
2285
+ }
2286
+ /**
2287
+ @hidden
2288
+ Makes a target object's properties observable by replacing them with wrapped values.
2289
+
2290
+ Wraps each property of `target` that is not already observable (and meets criteria)
2291
+ in an observable wrapper. Also sets up deep change detection for objects and
2292
+ debounced notifications when the data changes.
2293
+
2294
+ There are two sides to using it — pick per call site:
2295
+
2296
+ **Declarative (typed class fields).** Declare the field as `Observable<T>` up
2297
+ front, then let `observe(this, 'field')` install the live wrapper in the
2298
+ constructor. The declaration needs the double cast once (`0 as unknown as
2299
+ Observable<number>`): the field starts life as a plain value and only becomes
2300
+ the wrapper when `observe()` runs. In exchange, every later use is fully typed —
2301
+ arithmetic, `subscribe`, `bind` — with no casts anywhere else.
2302
+
2303
+ **Imperative (wrapping existing data).** Hand any object to `observe()` after
2304
+ the fact — no declarations, the return type is inferred: every property comes
2305
+ back as `Observable<PropertyType>`.
2306
+
2307
+ @param target - The object whose properties should be made observable.
2308
+ @param propertyName - Optional specific property name to make one property observable instead of all properties.
2309
+ @returns The original target object, now with observable properties.
2310
+
2311
+ @example Imperative usage — wrap an existing plain object:
2312
+ ```ts
2313
+ const state = observe({ count: 0, label: 'hi' });
2314
+ state.count.subscribe((v) => console.log('count is now', v));
2315
+ state.count++;
2316
+ ```
2317
+
2318
+ Incrementing `state.count` triggers the subscribed callback.
2319
+
2320
+ @example Declarative usage — typed class field with double cast:
2321
+ ```ts
2322
+ class Counter {
2323
+ clicks: Observable<number> = 0 as unknown as Observable<number>;
2324
+
2325
+ constructor() {
2326
+ observe(this, 'clicks');
2327
+ }
2328
+
2329
+ bump() {
2330
+ this.clicks++;
2331
+ }
2332
+ }
2333
+ ```
2334
+
2335
+ Plain arithmetic compiles because the declaration carries the observable type.
2336
+
2337
+ @example Schema-first usage — divergent read/write types via hand-declared interface:
2338
+ ```ts
2339
+ interface CounterState {
2340
+ get count(): Observable<number>;
2341
+ set count(v: number);
2342
+ }
2343
+
2344
+ const state = observe<CounterState>({ count: 0 });
2345
+ state.count.subscribe((v) => console.log(v));
2346
+ state.count = 5;
2347
+ ```
2348
+
2349
+ The seed object is validated against the schema, the subscription callback is fully
2350
+ typed, and plain assignment is legal because the declared setter type permits it.
2351
+ */
2352
+ function observe(target, propertyName) {
2353
+ const source = target;
2354
+ (propertyName ? [propertyName] : [...Object.getOwnPropertyNames(target)]).forEach((key) => {
2355
+ const current = source[key];
2356
+ if (Object.prototype.hasOwnProperty.call(target, key) && typeof current !== "function" && !isAlreadyObserved(current)) {
2357
+ if (key[0] === "_" || current === void 0) return;
2358
+ const wrappedVal = current;
2359
+ let o = new (getConstructor(wrappedVal))(wrappedVal);
2360
+ const debouncedNotify = function() {
2361
+ queueMicrotask(function() {
2362
+ o.notify();
2363
+ });
2364
+ };
2365
+ Object.defineProperty(target, key, {
2366
+ enumerable: true,
2367
+ get() {
2368
+ if (isObservableObject(o.valueOf())) {
2369
+ const snapshot = structuredClone(Object.assign({}, o));
2370
+ queueMicrotask(function checkChanges() {
2371
+ if (!deepEqual(structuredClone(Object.assign({}, o)), snapshot)) debouncedNotify();
2372
+ });
2373
+ }
2374
+ return o;
2375
+ },
2376
+ set(val) {
2377
+ if (typeof val === typeof o.valueOf()) if (isObservableObject(o.valueOf()) && o.valueOf() instanceof Array === false) {
2378
+ /* v8 ignore next */
2379
+ if (!isAlreadyObserved(o)) {
2380
+ /* v8 ignore next */
2381
+ o = new (getConstructor(val))(structuredClone(Object.assign({}, val)));
2382
+ /* v8 ignore next */
2383
+ copyGettersSetters(val, o);
2384
+ }
2385
+ Object.assign(o, val);
2386
+ o.notify();
2387
+ } else {
2388
+ const old = o.valueOf(), callbacks = o.callbacks;
2389
+ if (val !== old) {
2390
+ o = new (getConstructor(val))(val);
2391
+ o.callbacks = callbacks;
2392
+ debouncedNotify();
2393
+ }
2394
+ }
2395
+ else console.warn("IGNORED - cannot change observable types, IS:", typeof o.valueOf(), o, "- CANNOT BECOME:", typeof val);
2396
+ }
2397
+ });
2398
+ source[key] = wrappedVal;
2399
+ }
2400
+ });
2401
+ return target;
2402
+ }
2403
+ /**
2404
+ Attaches convenience methods `notify()`, `subscribe()`, and `bind()` to the prototype of a
2405
+ microcomponent class so that observable properties can be controlled directly on the instance.
2406
+
2407
+ All three methods return the component instance, so you can chain them fluently.
2408
+
2409
+ Call it once at module level before defining any component classes. The plugin is idempotent —
2410
+ subsequent calls are no-ops.
2411
+
2412
+ @param Class - A constructor function whose prototype chain includes {@link Core.McBase}.
2413
+
2414
+ @example
2415
+ ```ts
2416
+ class Counter extends DC {
2417
+ data = { clicks: 1 };
2418
+
2419
+ oneAssets() {
2420
+ this
2421
+ .subscribe('data', (v) => console.log('data →', v))
2422
+ .bind('data', this.target);
2423
+ }
2424
+ }
2425
+ ```
2426
+ */
2427
+ function observePlugin(Class) {
2428
+ const McBaseProto = Object.getPrototypeOf(Class.prototype);
2429
+ function makePropObservable(obj, propName) {
2430
+ const obs = obj[propName];
2431
+ if (!isAlreadyObserved(obs)) observe(obj, propName);
2432
+ }
2433
+ if (!Object.hasOwn(McBaseProto, "notify")) Object.assign(McBaseProto, {
2434
+ /**
2435
+ * Manually trigger a notification on an observable property.
2436
+ * Ensures the property is observed first if it hasn't been yet.
2437
+ * @param propName - The name of the observable property to notify.
2438
+ * @returns This instance for chaining.
2439
+
2440
+ @example
2441
+ ```ts
2442
+ this.notify('count');
2443
+ ```
2444
+ */
2445
+ notify: function(propName) {
2446
+ makePropObservable(this, propName);
2447
+ this[propName].notify();
2448
+ return this;
2449
+ },
2450
+ /**
2451
+ * Subscribe a callback to changes on an observable property.
2452
+ * Ensures the property is observed first if it hasn't been yet.
2453
+ * @param propName - The name of the observable property.
2454
+ * @param callback - Function invoked with the new value on every change.
2455
+ * @returns This instance for chaining.
2456
+
2457
+ @example
2458
+ ```ts
2459
+ this.subscribe('count', (v) => console.log('count →', v));
2460
+ ```
2461
+ */
2462
+ subscribe: function(propName, callback) {
2463
+ makePropObservable(this, propName);
2464
+ this[propName].subscribe(callback);
2465
+ return this;
2466
+ },
2467
+ /**
2468
+ * Bind an observable property to a DOM target so that `{placeholder}` tokens are updated live.
2469
+ * Ensures the property is observed first if it hasn't been yet.
2470
+ * @param propName - The name of the observable property.
2471
+ * @param target - HTMLElement, ShadowRoot, or DocumentFragment containing placeholders.
2472
+ * @returns This instance for chaining.
2473
+
2474
+ @example
2475
+ ```ts
2476
+ this.bind('data', this.target);
2477
+ ```
2478
+ */
2479
+ bind: function(propName, target) {
2480
+ makePropObservable(this, propName);
2481
+ this[propName].bind(target);
2482
+ return this;
2483
+ }
2484
+ });
2485
+ }
2486
+ //#endregion
2487
+ //#region src/state/formValues.ts
2488
+ /**
2489
+ Two-way binding between DOM form controls (`input`, `select`, `textarea`) and an observable
2490
+ object property. Reads reflect live DOM values; writes update the controls synchronously.
2491
+
2492
+ @module Form Values
2493
+
2494
+ Two APIs are provided for different contexts:
2495
+
2496
+ - **`formValues()`** — standalone function for general usage outside the MC ecosystem.
2497
+ Call it on any plain object to bind form controls to a new observable property. You manage
2498
+ subscriptions and bindings manually on the resulting data object.
2499
+
2500
+ - **`formValuePlugin()`** — used inside MC custom elements (classes extending `DC`).
2501
+ It attaches `.formValues()` as a chainable instance method that implicitly creates both the
2502
+ form-values binding and the underlying observable wrapper. Just call
2503
+ `this.formValues(this.target, 'formData')` and chain `.subscribe()` / `.bind()` — everything
2504
+ is set up for you automatically.
2505
+
2506
+ @example Plugin — inside an MC custom element (recommended within DC classes)
2507
+ ```ts
2508
+ import { DC, defineCE, formValuePlugin } from '@twobirds/microcomponents';
2509
+
2510
+ formValuePlugin(DC);
2511
+
2512
+ class CheckoutForm extends DC {
2513
+ static assets = ['targetHtml'];
2514
+
2515
+ oneAssets() {
2516
+ this
2517
+ .formValues(this.target, 'formData')
2518
+ .subscribe('formData', (data) => console.log(data))
2519
+ .bind('formData', this.target);
2520
+ }
2521
+ }
2522
+
2523
+ defineCE('checkout-form', CheckoutForm);
2524
+ ```
2525
+
2526
+ @example Standalone — outside the MC ecosystem (plain objects, vanilla JS, etc.)
2527
+ ```ts
2528
+ import { formValues } from '@twobirds/microcomponents/formValues';
2529
+
2530
+ const state = {};
2531
+ formValues(state, 'formData', document.getElementById('myForm'));
2532
+ console.log(state.formData.username);
2533
+ state.formData.role = 'admin';
2534
+ ```
2535
+
2536
+ Reading `state.formData.username` reflects the live `<input name="username">` value;
2537
+ assigning `state.formData.role` writes straight back to the `<select>` control.
2538
+ */
2539
+ observePlugin(DC);
2540
+ /**
2541
+ Extracts the lowercase tag name of an element (e.g., 'input', 'select', 'textarea').
2542
+ @param inputElement - The HTML element to extract the tag name from.
2543
+ @returns The lowercase tag name string.
2544
+ */
2545
+ var getInputName = (inputElement) => {
2546
+ return inputElement.tagName.toLowerCase();
2547
+ };
2548
+ /**
2549
+ Abstract base class for form input value handlers.
2550
+ Defines the interface that all handler classes must implement.
2551
+ */
2552
+ var InputBase = class {
2553
+ /**
2554
+ Creates a new InputBase instance.
2555
+ @param e - The HTML element being handled (input, select, or textarea).
2556
+ */
2557
+ constructor(e) {}
2558
+ };
2559
+ /**
2560
+ Handler class specifically for radio button groups.
2561
+ Provides synchronized access to the selected value in a group of related radio buttons.
2562
+ */
2563
+ var RadioHandler = class extends InputBase {
2564
+ #e;
2565
+ /**
2566
+ Creates a new RadioHandler instance.
2567
+ @param e - The first radio button element in the group (used to determine name and form scope).
2568
+ */
2569
+ constructor(e) {
2570
+ super(e);
2571
+ this.#e = e;
2572
+ }
2573
+ /**
2574
+ Gets the currently selected value of the radio button group using FormData API.
2575
+ @returns The string value of the selected radio button, or null if none is selected.
2576
+ */
2577
+ get val() {
2578
+ if (!this.#e.form) return null;
2579
+ return new FormData(this.#e.form).get(this.#e.name) ?? null;
2580
+ }
2581
+ /**
2582
+ Sets the selected radio button by value.
2583
+ Finds and checks the first radio input matching the specified value within the same group.
2584
+ @param value - The value to match against radio inputs in this group.
2585
+ */
2586
+ set val(value) {
2587
+ /* v8 ignore next */
2588
+ const arr = [...this.#e.form?.querySelectorAll(`input[type="radio"][name="${this.#e.name}"]`) ?? []];
2589
+ arr.filter((e) => e.getAttribute("value") === value).shift().checked = true;
2590
+ }
2591
+ };
2592
+ /**
2593
+ * Handler class for checkbhox inputs.
2594
+ * Provides synchronized get/set access to the checked state of a single checkbox.
2595
+ */
2596
+ var CheckboxHandler = class extends InputBase {
2597
+ #e;
2598
+ /**
2599
+ Creates a new CheckboxHandler instance.
2600
+ @param e - The checkbox element to handle.
2601
+ */
2602
+ constructor(e) {
2603
+ super(e);
2604
+ this.#e = e;
2605
+ }
2606
+ /**
2607
+ Gets the checked state of the checkbox.
2608
+ @returns True if the checkbox is checked, false otherwise.
2609
+ */
2610
+ get val() {
2611
+ return this.#e.checked;
2612
+ }
2613
+ /**
2614
+ Sets the checked state of the checkbox.
2615
+ @param value - The boolean value to assign as the new checked state.
2616
+ */
2617
+ set val(value) {
2618
+ this.#e.checked = value;
2619
+ }
2620
+ };
2621
+ /**
2622
+ Handler class for standard single-line input fields (text, password, email, etc.).
2623
+ Provides synchronized get/set access to the value of an input field.
2624
+ */
2625
+ var InputHandler = class extends InputBase {
2626
+ #e;
2627
+ /**
2628
+ * Creates a new InputHandler instance.
2629
+ * @param e - The HTML input element to handle.
2630
+ */
2631
+ constructor(e) {
2632
+ super(e);
2633
+ this.#e = e;
2634
+ }
2635
+ /**
2636
+ * Gets the current value of the input field.
2637
+ * @returns The string content of the input element.
2638
+ */
2639
+ get val() {
2640
+ return this.#e.value;
2641
+ }
2642
+ /**
2643
+ * Sets the value of the input field.
2644
+ * @param value - The new string value for the input element.
2645
+ */
2646
+ set val(value) {
2647
+ this.#e.value = value;
2648
+ }
2649
+ };
2650
+ /**
2651
+ Handler class for select dropdown elements, supporting both single and multiple selection modes.
2652
+ Provides synchronized get/set access to selected option values.
2653
+ */
2654
+ var SelectHandler = class extends InputBase {
2655
+ #e;
2656
+ #multi = false;
2657
+ /**
2658
+ * Creates a new SelectHandler instance.
2659
+ * @param e - The select element to handle.
2660
+ */
2661
+ constructor(e) {
2662
+ super(e);
2663
+ this.#e = e;
2664
+ this.#multi = e.multiple;
2665
+ }
2666
+ /**
2667
+ * Gets the currently selected option value(s).
2668
+ * For single selection, returns a string. For multiple selection, returns an json.stringify()'ied array of strings.
2669
+ * @returns A string or JSON.stringify array of strings representing selected values.
2670
+ */
2671
+ get val() {
2672
+ const ret = [];
2673
+ [...this.#e.querySelectorAll("option")].filter((o) => o.selected).forEach((o) => {
2674
+ ret.push(o.value);
2675
+ });
2676
+ return this.#multi ? ret : ret[0];
2677
+ }
2678
+ /**
2679
+ * Sets the selected option(s) by value.
2680
+ * @param value - A string or array of strings representing values to select.
2681
+ */
2682
+ set val(value) {
2683
+ value = typeof value === "string" ? [value] : value;
2684
+ [...this.#e.querySelectorAll("option")].forEach((o) => {
2685
+ if (value.indexOf(o.value) > -1) o.selected = true;
2686
+ else o.selected = false;
2687
+ });
2688
+ }
2689
+ };
2690
+ /**
2691
+ Handler class for textarea elements.
2692
+ Provides synchronized get/set access to the multi-line text content of a textarea.
2693
+ */
2694
+ var TextAreaHandler = class extends InputBase {
2695
+ #e;
2696
+ /**
2697
+ * Creates a new TextAreaHandler instance.
2698
+ * @param e - The textarea element to handle.
2699
+ */
2700
+ constructor(e) {
2701
+ super(e);
2702
+ this.#e = e;
2703
+ }
2704
+ /**
2705
+ * Gets the current value of the textarea (defaults to empty string if null/undefined).
2706
+ * @returns The string content of the textarea element.
2707
+ */
2708
+ get val() {
2709
+ return this.#e.value || "";
2710
+ }
2711
+ /**
2712
+ * Sets the value of the textarea.
2713
+ * @param value - The new string value for the textarea element.
2714
+ */
2715
+ set val(value) {
2716
+ this.#e.value = value;
2717
+ }
2718
+ };
2719
+ /**
2720
+ Collects all form controls within a given DOM scope and creates handler objects
2721
+ that provide synchronized get/set access to their values.
2722
+
2723
+ @param e - Optional root DOM element or shadow root to search within; defaults to document if omitted.
2724
+ @returns An object where each key is the name attribute of an input, select, or textarea,
2725
+ and each value is a proxy-like accessor object with `val` getter/setter methods.
2726
+ */
2727
+ var getInputHandlers = (e) => {
2728
+ const inputList = [...(e || document).querySelectorAll("input,select,textarea")], handlers = {};
2729
+ inputList.forEach((input) => {
2730
+ const name = input.getAttribute("name");
2731
+ if (name) {
2732
+ let handler = false;
2733
+ switch (getInputName(input)) {
2734
+ case "input":
2735
+ switch (input.type) {
2736
+ case "radio":
2737
+ handler = new RadioHandler(input);
2738
+ break;
2739
+ case "checkbox":
2740
+ handler = new CheckboxHandler(input);
2741
+ break;
2742
+ default: handler = new InputHandler(input);
2743
+ }
2744
+ break;
2745
+ case "select":
2746
+ handler = new SelectHandler(input);
2747
+ break;
2748
+ case "textarea":
2749
+ handler = new TextAreaHandler(input);
2750
+ break;
2751
+ }
2752
+ /* v8 ignore next */
2753
+ if (handler) {
2754
+ const h = handler;
2755
+ DC.add(input, name, h);
2756
+ Object.defineProperty(handlers, name, {
2757
+ enumerable: true,
2758
+ configurable: true,
2759
+ get: () => h.val,
2760
+ set: (value) => {
2761
+ if (h.val !== value) h.val = value;
2762
+ }
2763
+ });
2764
+ }
2765
+ }
2766
+ });
2767
+ return handlers;
2768
+ };
2769
+ /**
2770
+ @hidden
2771
+
2772
+ Attaches a form-values observable to a target object property, enabling two-way binding
2773
+ between DOM form controls and the object's state.
2774
+
2775
+ Scans the given DOM subtree for `input`, `select`, and `textarea` elements with a `name`
2776
+ attribute and creates live getters/setters on the resulting observable. Input events trigger
2777
+ automatic notifications so subscribers react to user changes.
2778
+
2779
+ @param targetObject - The object that will receive the form-data property.
2780
+ @param propertyName - The property name under which the bound form data is stored.
2781
+ @param dom - HTMLElement or ShadowRoot containing the form controls.
2782
+
2783
+ @example Bind a plain object to an HTML form element
2784
+ ```ts
2785
+ import { formValues } from '@twobirds/microcomponents/formValues';
2786
+
2787
+ const state = {};
2788
+ formValues(state, 'formData', document.getElementById('myForm'));
2789
+ console.log(state.formData.username);
2790
+ state.formData.role = 'admin';
2791
+ ```
2792
+
2793
+ Reading `state.formData.username` reflects the live `<input name="username">` value;
2794
+ assigning `state.formData.role` writes straight back to the `<select>` control.
2795
+
2796
+ @example Subscribe to form changes inside a component
2797
+ ```ts
2798
+ import { DC, defineCE, formValues } from '@twobirds/microcomponents';
2799
+
2800
+ class MyForm extends DC {
2801
+ static assets = ['targetHtml'];
2802
+
2803
+ oneAssets() {
2804
+ formValues(this, 'formData', this.target);
2805
+ this.formData.subscribe((data) => console.log('form changed:', data));
2806
+ }
2807
+ }
2808
+
2809
+ defineCE('my-form', MyForm);
2810
+ ```
2811
+ */
2812
+ function formValues(targetObject, propertyName, dom) {
2813
+ /* v8 ignore next */
2814
+ if (!isBrowser()) return;
2815
+ const handlers = getInputHandlers(dom);
2816
+ const source = targetObject;
2817
+ source[propertyName] = Object.assign({}, handlers);
2818
+ observe(targetObject, propertyName);
2819
+ const observable = source[propertyName];
2820
+ copyGettersSetters(handlers, observable);
2821
+ [...dom.querySelectorAll("input, textarea, select")].forEach((input) => {
2822
+ input.addEventListener("input", () => {
2823
+ observable.notify();
2824
+ });
2825
+ });
2826
+ }
2827
+ /**
2828
+ Attaches a convenience method `formValues()` to the prototype of a microcomponent class so that
2829
+ form controls can be bound directly on the instance without importing the standalone function.
2830
+
2831
+ This plugin also pulls in all observable methods from {@link observePlugin} — `notify()`,
2832
+ `subscribe()`, and `bind()` — so you can chain them fluently after binding form data.
2833
+
2834
+ Call it once at module level before defining any component classes. The plugin is idempotent —
2835
+ subsequent calls are no-ops.
2836
+
2837
+ @param Class - A constructor function whose prototype chain includes {@link Core.McBase}.
2838
+
2839
+ @example
2840
+ ```ts
2841
+ import { DC, defineCE, formValuePlugin } from '@twobirds/microcomponents';
2842
+
2843
+ formValuePlugin(DC);
2844
+
2845
+ class CheckoutForm extends DC {
2846
+ static assets = ['targetHtml'];
2847
+
2848
+ oneAssets() {
2849
+ this
2850
+ .formValues(this.target, 'formData')
2851
+ .subscribe('formData', (data) => console.log(data))
2852
+ .bind('formData', this.target);
2853
+ }
2854
+ }
2855
+
2856
+ defineCE('checkout-form', CheckoutForm);
2857
+ ```
2858
+ */
2859
+ function formValuePlugin(Class) {
2860
+ const McBaseProto = Object.getPrototypeOf(Class.prototype);
2861
+ if (!Object.hasOwn(McBaseProto, "formValues")) Object.assign(McBaseProto, {
2862
+ /**
2863
+ * Bind all named form controls inside a DOM subtree to an observable property on this instance.
2864
+ * Ensures the property becomes observable and stays in sync with input changes.
2865
+ * @param target - HTMLElement or ShadowRoot containing the form controls.
2866
+ * @param propName - The name of the property that will hold the bound form data.
2867
+ * @returns This instance for chaining.
2868
+
2869
+ @example
2870
+ ```ts
2871
+ this.formValues(this.target, 'formData');
2872
+ ```
2873
+
2874
+ After this call, `this.formData.username` reflects the live `<input name="username">` value.
2875
+ */
2876
+ formValues: function(target, propName) {
2877
+ formValues(this, propName, target);
2878
+ return this;
2879
+ } });
2880
+ }
2881
+ //#endregion
2882
+ export { BubbleController, DC, MC, McBase, autoload, bindTemplateEvents, copyGettersSetters, debounce, deepEqual, defineCE, flattenObject, formValuePlugin, formValues, getCE, getMcEventName, htmlToElements, injectCSS, isAlreadyObserved, isBrowser, isMcHost, isNativeObject, kebabToPascal, listCE, loadAssets, nameSpace, observe, observePlugin, off, on, one, parse, processAssets, sortObject, trigger, trueEventName };