bitwrench 2.0.17 → 2.0.19

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 (72) hide show
  1. package/README.md +169 -75
  2. package/dist/bitwrench-bccl.cjs.js +228 -55
  3. package/dist/bitwrench-bccl.cjs.min.js +3 -3
  4. package/dist/bitwrench-bccl.esm.js +228 -55
  5. package/dist/bitwrench-bccl.esm.min.js +3 -3
  6. package/dist/bitwrench-bccl.umd.js +228 -55
  7. package/dist/bitwrench-bccl.umd.min.js +3 -3
  8. package/dist/bitwrench-code-edit.cjs.js +7 -9
  9. package/dist/bitwrench-code-edit.cjs.min.js +5 -7
  10. package/dist/bitwrench-code-edit.es5.js +6 -8
  11. package/dist/bitwrench-code-edit.es5.min.js +5 -7
  12. package/dist/bitwrench-code-edit.esm.js +7 -9
  13. package/dist/bitwrench-code-edit.esm.min.js +5 -7
  14. package/dist/bitwrench-code-edit.umd.js +7 -9
  15. package/dist/bitwrench-code-edit.umd.min.js +5 -7
  16. package/dist/bitwrench-debug.js +268 -0
  17. package/dist/bitwrench-debug.min.js +3 -0
  18. package/dist/bitwrench-lean.cjs.js +1190 -2348
  19. package/dist/bitwrench-lean.cjs.min.js +20 -20
  20. package/dist/bitwrench-lean.es5.js +1285 -2551
  21. package/dist/bitwrench-lean.es5.min.js +18 -18
  22. package/dist/bitwrench-lean.esm.js +1190 -2348
  23. package/dist/bitwrench-lean.esm.min.js +20 -20
  24. package/dist/bitwrench-lean.umd.js +1190 -2348
  25. package/dist/bitwrench-lean.umd.min.js +20 -20
  26. package/dist/bitwrench-util-css.cjs.js +236 -0
  27. package/dist/bitwrench-util-css.cjs.min.js +22 -0
  28. package/dist/bitwrench-util-css.es5.js +414 -0
  29. package/dist/bitwrench-util-css.es5.min.js +21 -0
  30. package/dist/bitwrench-util-css.esm.js +230 -0
  31. package/dist/bitwrench-util-css.esm.min.js +21 -0
  32. package/dist/bitwrench-util-css.umd.js +242 -0
  33. package/dist/bitwrench-util-css.umd.min.js +21 -0
  34. package/dist/bitwrench.cjs.js +1404 -2388
  35. package/dist/bitwrench.cjs.min.js +21 -21
  36. package/dist/bitwrench.css +503 -132
  37. package/dist/bitwrench.es5.js +1588 -2659
  38. package/dist/bitwrench.es5.min.js +19 -19
  39. package/dist/bitwrench.esm.js +1405 -2389
  40. package/dist/bitwrench.esm.min.js +21 -21
  41. package/dist/bitwrench.min.css +1 -1
  42. package/dist/bitwrench.umd.js +1404 -2388
  43. package/dist/bitwrench.umd.min.js +21 -21
  44. package/dist/builds.json +214 -104
  45. package/dist/bwserve.cjs.js +514 -68
  46. package/dist/bwserve.esm.js +513 -69
  47. package/dist/sri.json +46 -36
  48. package/package.json +6 -3
  49. package/readme.html +183 -85
  50. package/src/bitwrench-bccl-entry.js +3 -4
  51. package/src/bitwrench-bccl.js +224 -50
  52. package/src/bitwrench-code-edit.js +6 -8
  53. package/src/bitwrench-color-utils.js +31 -9
  54. package/src/bitwrench-debug.js +245 -0
  55. package/src/bitwrench-esm-entry.js +11 -0
  56. package/src/bitwrench-styles.js +474 -240
  57. package/src/bitwrench-util-css.js +229 -0
  58. package/src/bitwrench.js +689 -2042
  59. package/src/bwserve/attach.js +57 -0
  60. package/src/bwserve/bwclient.js +141 -0
  61. package/src/bwserve/bwshell.js +102 -0
  62. package/src/bwserve/client.js +151 -1
  63. package/src/bwserve/index.js +127 -28
  64. package/src/cli/attach.js +587 -0
  65. package/src/cli/convert.js +2 -5
  66. package/src/cli/index.js +7 -0
  67. package/src/cli/inject.js +1 -1
  68. package/src/cli/serve.js +185 -5
  69. package/src/generate-css.js +11 -4
  70. package/src/vendor/html2canvas.min.js +20 -0
  71. package/src/version.js +3 -3
  72. package/src/bwserve/shell.js +0 -106
package/src/bitwrench.js CHANGED
@@ -8,11 +8,11 @@
8
8
  */
9
9
 
10
10
  import { VERSION_INFO } from './version.js';
11
- import { getStructuralStyles,
12
- generateThemedCSS, generateAlternateCSS, derivePalette as _derivePalette,
11
+ import { getStructuralStyles, getResetStyles,
12
+ generateThemedCSS, derivePalette as _derivePalette,
13
13
  DEFAULT_PALETTE_CONFIG, SPACING_PRESETS, RADIUS_PRESETS, THEME_PRESETS,
14
14
  TYPE_RATIO_PRESETS, ELEVATION_PRESETS, MOTION_PRESETS, generateTypeScale,
15
- resolveLayout } from './bitwrench-styles.js';
15
+ resolveLayout, scopeRulesUnder } from './bitwrench-styles.js';
16
16
  import { hexToHsl, hslToHex, adjustLightness, mixColor,
17
17
  relativeLuminance, textOnColor, deriveShades,
18
18
  derivePalette, harmonize, deriveAlternateSeed, deriveAlternateConfig,
@@ -56,12 +56,11 @@ const bw = {
56
56
  _subIdCounter: 0, // monotonic ID for subscriptions
57
57
 
58
58
  // ── Node reference cache ──────────────────────────────────────────────
59
- // Fast O(1) lookup for elements by bw_id, id attribute, or bw_uuid.
59
+ // Fast O(1) lookup for elements by id attribute or bw_uuid_* class.
60
60
  //
61
61
  // Populated by bw.createDOM() when elements have:
62
- // - data-bw_id attribute (user-declared addressable elements)
63
62
  // - id attribute (standard HTML id)
64
- // - bw_uuid (internal, for lifecycle-managed elements)
63
+ // - bw_uuid_* class (lifecycle-managed or explicitly addressed elements)
65
64
  //
66
65
  // Cleaned up by bw.cleanup() when elements are destroyed via bitwrench APIs.
67
66
  // On cache miss, falls back to querySelector/getElementById — never fails,
@@ -69,7 +68,7 @@ const bw = {
69
68
  // via parentNode === null check (IE11-safe, unlike el.isConnected).
70
69
  //
71
70
  // Elements created via bw.createDOM() also get el._bw_refs — a local map of
72
- // child bw_id DOM node ref for fast parentchild access in o.render.
71
+ // child id/UUID -> DOM node ref for fast parent->child access in o.render.
73
72
  // This is the bitwrench equivalent of React's compiled template "holes".
74
73
  //
75
74
  // Contract: if you remove elements outside of bitwrench APIs (raw el.remove()),
@@ -149,7 +148,6 @@ Object.defineProperty(bw, '_isBrowser', {
149
148
  // _cw console.warn 8
150
149
  // _cl console.log 11
151
150
  // _ce console.error 4
152
- // _chp ComponentHandle.prototype 28 (defined after constructor)
153
151
  //
154
152
  // Note: document.createElement etc. are NOT aliased because they require
155
153
  // `this === document` and .bind() would add overhead on every call.
@@ -322,15 +320,15 @@ bw.uuid = function(prefix) {
322
320
  * 1. Check `bw._nodeMap[id]` — if found and still attached (parentNode !== null), return it
323
321
  * 2. If cached ref is detached (parentNode === null), remove stale entry
324
322
  * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
325
- * 4. If fallback finds the element, cache it for next time
326
- * 5. If not found anywhere, return null
323
+ * 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
324
+ * 5. Cache the result for next time
327
325
  *
328
326
  * Accepts a DOM element directly (pass-through) or a string identifier.
329
327
  * String identifiers are tried as: direct map key, getElementById,
330
328
  * querySelector (for CSS selectors starting with . or #), and
331
- * data-bw_id attribute selector.
329
+ * bw_uuid_* class selector.
332
330
  *
333
- * @param {string|Element} id - Element ID, CSS selector, data-bw_id value, or DOM element
331
+ * @param {string|Element} id - Element ID, CSS selector, bw_uuid_* class, or DOM element
334
332
  * @returns {Element|null} The DOM element, or null if not found
335
333
  * @category Internal
336
334
  */
@@ -359,9 +357,9 @@ bw._el = function(id) {
359
357
  el = document.querySelector(id);
360
358
  }
361
359
 
362
- // 4. Try data-bw_id attribute (for bw.uuid-generated IDs)
363
- if (!el) {
364
- el = document.querySelector('[data-bw_id="' + id + '"]');
360
+ // 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
361
+ if (!el && id.indexOf('bw_uuid_') === 0) {
362
+ el = document.querySelector('.' + id);
365
363
  }
366
364
 
367
365
  // 5. Cache the result for next time
@@ -376,17 +374,17 @@ bw._el = function(id) {
376
374
  * Register a DOM element in the node cache under one or more keys.
377
375
  *
378
376
  * Called internally by `bw.createDOM()`. Registers elements that have
379
- * id attributes, data-bw_id attributes, or both.
377
+ * id attributes, UUID classes, or both.
380
378
  *
381
379
  * @param {Element} el - DOM element to register
382
- * @param {string} [bwId] - data-bw_id value to register under
380
+ * @param {string} [uuid] - bw_uuid_* class token to register under
383
381
  * @category Internal
384
382
  */
385
- bw._registerNode = function(el, bwId) {
383
+ bw._registerNode = function(el, uuid) {
386
384
  if (!el) return;
387
- // Register under data-bw_id
388
- if (bwId) {
389
- bw._nodeMap[bwId] = el;
385
+ // Register under UUID class token
386
+ if (uuid) {
387
+ bw._nodeMap[uuid] = el;
390
388
  }
391
389
  // Register under id attribute
392
390
  var htmlId = el.getAttribute ? el.getAttribute('id') : null;
@@ -402,13 +400,13 @@ bw._registerNode = function(el, bwId) {
402
400
  * through bitwrench APIs.
403
401
  *
404
402
  * @param {Element} el - DOM element to deregister
405
- * @param {string} [bwId] - data-bw_id value to remove
403
+ * @param {string} [uuid] - bw_uuid_* class token to remove
406
404
  * @category Internal
407
405
  */
408
- bw._deregisterNode = function(el, bwId) {
409
- // Remove data-bw_id entry
410
- if (bwId) {
411
- delete bw._nodeMap[bwId];
406
+ bw._deregisterNode = function(el, uuid) {
407
+ // Remove UUID class entry
408
+ if (uuid) {
409
+ delete bw._nodeMap[uuid];
412
410
  }
413
411
  // Remove id attribute entry
414
412
  var htmlId = el && el.getAttribute ? el.getAttribute('id') : null;
@@ -417,6 +415,91 @@ bw._deregisterNode = function(el, bwId) {
417
415
  }
418
416
  };
419
417
 
418
+ // ===================================================================================
419
+ // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
420
+ // ===================================================================================
421
+
422
+ /**
423
+ * Marker class for elements with lifecycle hooks (mounted/unmount/render/state).
424
+ * Used by cleanup() to find lifecycle-managed elements via querySelectorAll('.bw_lc').
425
+ * @private
426
+ */
427
+ var _BW_LC = 'bw_lc';
428
+
429
+ /**
430
+ * Regex to match a bw_uuid_* token in a class string.
431
+ * @private
432
+ */
433
+ var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
434
+
435
+ /**
436
+ * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
437
+ *
438
+ * Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
439
+ * to replace an existing UUID (useful in loops where each TACO needs a unique ID).
440
+ *
441
+ * @param {Object} taco - A TACO object `{t, a, c, o}`
442
+ * @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
443
+ * @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
444
+ * @category Identifiers
445
+ * @example
446
+ * var card = bw.makeStatCard({ value: '0', label: 'Scans' });
447
+ * var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
448
+ * var same = bw.assignUUID(card); // same UUID (idempotent)
449
+ * var diff = bw.assignUUID(card, true); // new UUID (forced)
450
+ */
451
+ bw.assignUUID = function(taco, forceNew) {
452
+ if (!taco || !_is(taco, 'object')) return null;
453
+
454
+ // Ensure taco.a exists
455
+ if (!taco.a) taco.a = {};
456
+ if (!_is(taco.a.class, 'string')) taco.a.class = taco.a.class ? String(taco.a.class) : '';
457
+
458
+ var existing = taco.a.class.match(_UUID_RE);
459
+
460
+ if (existing && !forceNew) {
461
+ return existing[0];
462
+ }
463
+
464
+ // Remove old UUID if forceNew
465
+ if (existing) {
466
+ taco.a.class = taco.a.class.replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
467
+ }
468
+
469
+ var uuid = bw.uuid('uuid');
470
+ taco.a.class = (taco.a.class ? taco.a.class + ' ' : '') + uuid;
471
+ return uuid;
472
+ };
473
+
474
+ /**
475
+ * Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
476
+ *
477
+ * @param {Object|Element} tacoOrElement - A TACO object or DOM element
478
+ * @returns {string|null} The UUID string, or null if none assigned
479
+ * @category Identifiers
480
+ * @example
481
+ * bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
482
+ * bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
483
+ * bw.getUUID({t:'div'}) // null (no UUID)
484
+ */
485
+ bw.getUUID = function(tacoOrElement) {
486
+ if (!tacoOrElement) return null;
487
+
488
+ var classStr;
489
+ // DOM element: check className
490
+ if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
491
+ classStr = tacoOrElement.className;
492
+ }
493
+ // TACO object: check a.class
494
+ else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
495
+ classStr = tacoOrElement.a.class;
496
+ }
497
+
498
+ if (!classStr) return null;
499
+ var match = classStr.match(_UUID_RE);
500
+ return match ? match[0] : null;
501
+ };
502
+
420
503
  /**
421
504
  * Escape HTML special characters to prevent XSS.
422
505
  *
@@ -466,6 +549,42 @@ bw.raw = function(str) {
466
549
  return { __bw_raw: true, v: String(str) };
467
550
  };
468
551
 
552
+ /**
553
+ * Hyperscript-style TACO constructor.
554
+ *
555
+ * A convenience helper that returns a canonical TACO object from positional
556
+ * arguments. The return value is a plain object — serializable, works with
557
+ * bwserve, and accepted everywhere TACO is accepted.
558
+ *
559
+ * @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
560
+ * @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
561
+ * @param {*} [content] - Content: string, number, TACO object, or array of children.
562
+ * @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
563
+ * @returns {Object} Plain TACO object {t, a?, c?, o?}
564
+ * @category Utilities
565
+ * @see bw.html
566
+ * @see bw.createDOM
567
+ * @see bw.DOM
568
+ * @example
569
+ * bw.h('div')
570
+ * // => { t: 'div' }
571
+ *
572
+ * bw.h('p', { class: 'bw_text_muted' }, 'Hello')
573
+ * // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
574
+ *
575
+ * bw.h('ul', null, [
576
+ * bw.h('li', null, 'one'),
577
+ * bw.h('li', null, 'two')
578
+ * ])
579
+ * // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
580
+ */
581
+ bw.h = function(tag, attrs, content, options) {
582
+ var taco = { t: String(tag) };
583
+ if (attrs !== null && attrs !== undefined) taco.a = attrs;
584
+ if (content !== undefined) taco.c = content;
585
+ if (options !== undefined) taco.o = options;
586
+ return taco;
587
+ };
469
588
 
470
589
  /**
471
590
  * Convert a TACO object (or array of TACOs) to an HTML string.
@@ -495,15 +614,6 @@ bw.html = function(taco, options = {}) {
495
614
  // Handle null/undefined
496
615
  if (taco == null) return '';
497
616
 
498
- // Handle ComponentHandle — use its .taco
499
- if (taco && taco._bwComponent === true) {
500
- var compOptions = Object.assign({}, options);
501
- if (!compOptions.state && taco._state) {
502
- compOptions.state = taco._state;
503
- }
504
- return bw.html(taco.taco, compOptions);
505
- }
506
-
507
617
  // Handle arrays of TACOs
508
618
  if (_isA(taco)) {
509
619
  return taco.map(t => bw.html(t, options)).join('');
@@ -514,24 +624,6 @@ bw.html = function(taco, options = {}) {
514
624
  return taco.v;
515
625
  }
516
626
 
517
- // Handle bw.when() markers
518
- if (taco && taco._bwWhen && options.state) {
519
- var whenExpr = taco.expr.replace(/^\$\{|\}$/g, '');
520
- var whenVal = options.compile
521
- ? bw._resolveTemplate('${' + whenExpr + '}', options.state, true)
522
- : bw._evaluatePath(options.state, whenExpr);
523
- var branch = whenVal ? taco.branches[0] : (taco.branches[1] || null);
524
- return branch ? bw.html(branch, options) : '';
525
- }
526
-
527
- // Handle bw.each() markers
528
- if (taco && taco._bwEach && options.state) {
529
- var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
530
- var arr = bw._evaluatePath(options.state, eachExpr);
531
- if (!_isA(arr)) return '';
532
- return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
533
- }
534
-
535
627
  // Handle primitives and non-TACO objects
536
628
  if (!_is(taco, 'object') || !taco.t) {
537
629
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
@@ -595,14 +687,14 @@ bw.html = function(taco, options = {}) {
595
687
  }
596
688
  }
597
689
 
598
- // Add bw_id as a class if lifecycle hooks present
599
- if ((opts.mounted || opts.unmount) && !attrs.class?.includes('bw_id_')) {
600
- const id = opts.bw_id || bw.uuid();
690
+ // Add bw_uuid + bw_lc classes if lifecycle hooks present
691
+ if ((opts.mounted || opts.unmount) && !_UUID_RE.test(attrs.class || '')) {
692
+ const uuid = bw.uuid('uuid');
601
693
  attrStr = attrStr.replace(/class="([^"]*)"/, (_match, classes) => {
602
- return `class="${classes} bw_id_${id}"`.trim();
694
+ return `class="${classes} ${uuid} ${_BW_LC}"`.trim();
603
695
  });
604
696
  if (!attrStr.includes('class=')) {
605
- attrStr += ` class="bw_id_${id}"`;
697
+ attrStr += ` class="${uuid} ${_BW_LC}"`;
606
698
  }
607
699
  }
608
700
 
@@ -730,7 +822,7 @@ bw.htmlPage = function(opts) {
730
822
  ? (THEME_PRESETS[theme.toLowerCase()] || null)
731
823
  : theme;
732
824
  if (themeConfig) {
733
- var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, { inject: false }));
825
+ var themeResult = bw.makeStyles(themeConfig);
734
826
  themeCSS = themeResult.css;
735
827
  }
736
828
  }
@@ -756,14 +848,14 @@ bw.htmlPage = function(opts) {
756
848
  // Combine all CSS
757
849
  var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
758
850
 
759
- // Body-end script: registry entries + optional loadDefaultStyles
851
+ // Body-end script: registry entries + optional loadStyles
760
852
  var bodyEndScript = '';
761
853
  var bodyEndParts = [];
762
854
  if (registryEntries) {
763
855
  bodyEndParts.push(registryEntries);
764
856
  }
765
857
  if (runtime === 'inline' || runtime === 'cdn') {
766
- bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
858
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
767
859
  }
768
860
  if (bodyEndParts.length > 0) {
769
861
  bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
@@ -830,11 +922,6 @@ bw.createDOM = function(taco, options = {}) {
830
922
  return frag;
831
923
  }
832
924
 
833
- // Handle ComponentHandle — extract .taco for DOM creation
834
- if (taco && taco._bwComponent === true) {
835
- return bw.createDOM(taco.taco, options);
836
- }
837
-
838
925
  // Handle text nodes
839
926
  if (!_is(taco, 'object') || !taco.t) {
840
927
  return document.createTextNode(String(taco));
@@ -875,24 +962,19 @@ bw.createDOM = function(taco, options = {}) {
875
962
  }
876
963
 
877
964
  // Add children, building _bw_refs for fast parent→child access.
878
- // Children with data-bw_id or id attributes get local refs on the parent,
965
+ // Children with id attributes or bw_uuid_* classes get local refs on the parent,
879
966
  // so o.render functions can access them without any DOM lookup.
880
967
  if (content != null) {
881
968
  if (_isA(content)) {
882
969
  content.forEach(child => {
883
970
  if (child != null) {
884
- // Handle ComponentHandle in content arrays (Level 2 children)
885
- if (child._bwComponent === true) {
886
- child.mount(el);
887
- return;
888
- }
889
971
  var childEl = bw.createDOM(child, options);
890
972
  el.appendChild(childEl);
891
973
  // Build local refs for addressable children
892
- var childBwId = (child && child.a) ? (child.a['data-bw_id'] || child.a.id) : null;
893
- if (childBwId) {
974
+ var childRefId = (child && child.a) ? (child.a.id || bw.getUUID(child)) : null;
975
+ if (childRefId) {
894
976
  if (!el._bw_refs) el._bw_refs = {};
895
- el._bw_refs[childBwId] = childEl;
977
+ el._bw_refs[childRefId] = childEl;
896
978
  }
897
979
  // Bubble up grandchild refs (flatten one level)
898
980
  if (childEl._bw_refs) {
@@ -908,16 +990,13 @@ bw.createDOM = function(taco, options = {}) {
908
990
  } else if (_is(content, 'object') && content.__bw_raw) {
909
991
  // Raw HTML content — inject via innerHTML
910
992
  el.innerHTML = content.v;
911
- } else if (content._bwComponent === true) {
912
- // Single ComponentHandle as content
913
- content.mount(el);
914
993
  } else if (_is(content, 'object') && content.t) {
915
994
  var childEl = bw.createDOM(content, options);
916
995
  el.appendChild(childEl);
917
- var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
918
- if (childBwId) {
996
+ var childRefId = content.a ? (content.a.id || bw.getUUID(content)) : null;
997
+ if (childRefId) {
919
998
  if (!el._bw_refs) el._bw_refs = {};
920
- el._bw_refs[childBwId] = childEl;
999
+ el._bw_refs[childRefId] = childEl;
921
1000
  }
922
1001
  if (childEl._bw_refs) {
923
1002
  if (!el._bw_refs) el._bw_refs = {};
@@ -937,59 +1016,98 @@ bw.createDOM = function(taco, options = {}) {
937
1016
  bw._registerNode(el, null);
938
1017
  }
939
1018
 
1019
+ // Register UUID class in node cache (bw_uuid_* tokens in class string)
1020
+ if (el.className) {
1021
+ var uuidMatch = el.className.match(_UUID_RE);
1022
+ if (uuidMatch) {
1023
+ bw._nodeMap[uuidMatch[0]] = el;
1024
+ }
1025
+ }
1026
+
940
1027
  // Handle lifecycle hooks and state
941
1028
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
942
- const id = attrs['data-bw_id'] || bw.uuid();
943
- el.setAttribute('data-bw_id', id);
1029
+ // Ensure element has a UUID class for identity
1030
+ var uuid = bw.getUUID(el) || bw.uuid('uuid');
1031
+ el.classList.add(uuid);
1032
+ el.classList.add(_BW_LC);
944
1033
 
945
- // Register in node cache under data-bw_id
946
- bw._registerNode(el, id);
1034
+ // Register in node cache under UUID class
1035
+ bw._registerNode(el, uuid);
947
1036
 
948
1037
  // Store state
949
1038
  if (opts.state) {
950
1039
  el._bw_state = opts.state;
951
1040
  }
952
1041
 
953
- // o.render — first-class render function (replaces mounted boilerplate)
1042
+ // o.render — store the render function for bw.update()
954
1043
  if (opts.render) {
955
1044
  el._bw_render = opts.render;
1045
+ }
956
1046
 
957
- if (opts.mounted) {
958
- _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
959
- }
1047
+ // Determine what to call on mount:
1048
+ // - If o.mounted exists, call it (it can call el._bw_render() for initial render)
1049
+ // - Otherwise if o.render exists, auto-call it as a convenience shorthand
1050
+ var mountFn = opts.mounted || (opts.render ? function(mountEl) {
1051
+ opts.render(mountEl, mountEl._bw_state || {});
1052
+ } : null);
960
1053
 
961
- // Queue initial render (same timing as mounted)
962
- if (document.body.contains(el)) {
963
- opts.render(el, el._bw_state || {});
964
- } else {
965
- requestAnimationFrame(() => {
966
- if (document.body.contains(el)) {
967
- opts.render(el, el._bw_state || {});
968
- }
969
- });
970
- }
971
- } else if (opts.mounted) {
972
- // Queue mounted callback (legacy pattern)
1054
+ if (mountFn) {
973
1055
  if (document.body.contains(el)) {
974
- opts.mounted(el, el._bw_state || {});
1056
+ mountFn(el, el._bw_state || {});
975
1057
  } else {
976
1058
  requestAnimationFrame(() => {
977
1059
  if (document.body.contains(el)) {
978
- opts.mounted(el, el._bw_state || {});
1060
+ mountFn(el, el._bw_state || {});
979
1061
  }
980
1062
  });
981
1063
  }
982
1064
  }
983
1065
 
984
- // Store unmount callback
1066
+ // Store unmount callback keyed by UUID class
985
1067
  if (opts.unmount) {
986
- bw._unmountCallbacks.set(id, () => {
1068
+ bw._unmountCallbacks.set(uuid, () => {
987
1069
  opts.unmount(el, el._bw_state || {});
988
1070
  });
989
1071
  }
990
- } else if (attrs['data-bw_id']) {
991
- // Element has explicit data-bw_id but no lifecycle hooks — still register it
992
- bw._registerNode(el, attrs['data-bw_id']);
1072
+ }
1073
+
1074
+ // Component handle: attach methods to el.bw namespace
1075
+ if (opts.handle || opts.slots) {
1076
+ if (!el.bw) el.bw = {};
1077
+
1078
+ // Explicit handle methods: fn(el, ...args) -> el.bw.method(...args)
1079
+ if (opts.handle) {
1080
+ for (var hk in opts.handle) {
1081
+ if (_hop.call(opts.handle, hk)) {
1082
+ el.bw[hk] = opts.handle[hk].bind(null, el);
1083
+ }
1084
+ }
1085
+ }
1086
+
1087
+ // Slot declarations: auto-generate setX/getX pairs
1088
+ if (opts.slots) {
1089
+ for (var sk in opts.slots) {
1090
+ if (_hop.call(opts.slots, sk)) {
1091
+ (function(name, selector) {
1092
+ var cap = name.charAt(0).toUpperCase() + name.slice(1);
1093
+ el.bw['set' + cap] = function(value) {
1094
+ var t = el.querySelector(selector);
1095
+ if (!t) return;
1096
+ if (value != null && typeof value === 'object' && value.t) {
1097
+ t.innerHTML = '';
1098
+ t.appendChild(bw.createDOM(value));
1099
+ } else {
1100
+ t.textContent = (value != null) ? String(value) : '';
1101
+ }
1102
+ };
1103
+ el.bw['get' + cap] = function() {
1104
+ var t = el.querySelector(selector);
1105
+ return t ? t.textContent : '';
1106
+ };
1107
+ })(sk, opts.slots[sk]);
1108
+ }
1109
+ }
1110
+ }
993
1111
  }
994
1112
 
995
1113
  return el;
@@ -1036,7 +1154,7 @@ bw.DOM = function(target, taco, options = {}) {
1036
1154
  // the target is the mount point, not the content being replaced)
1037
1155
  const savedState = targetEl._bw_state;
1038
1156
  const savedRender = targetEl._bw_render;
1039
- const savedBwId = targetEl.getAttribute('data-bw_id');
1157
+ const savedUuid = bw.getUUID(targetEl);
1040
1158
  const savedSubs = targetEl._bw_subs;
1041
1159
 
1042
1160
  // Temporarily remove _bw_subs so cleanup doesn't call them
@@ -1048,10 +1166,9 @@ bw.DOM = function(target, taco, options = {}) {
1048
1166
  // Restore the target's own state/render/subs after cleanup
1049
1167
  if (savedState !== undefined) targetEl._bw_state = savedState;
1050
1168
  if (savedRender) targetEl._bw_render = savedRender;
1051
- if (savedBwId) {
1052
- targetEl.setAttribute('data-bw_id', savedBwId);
1053
- // Re-register mount point in node cache (cleanup deregistered it)
1054
- bw._registerNode(targetEl, savedBwId);
1169
+ if (savedUuid) {
1170
+ // UUID class stays on element through cleanup; re-register in cache
1171
+ bw._registerNode(targetEl, savedUuid);
1055
1172
  }
1056
1173
  if (savedSubs) targetEl._bw_subs = savedSubs;
1057
1174
 
@@ -1059,25 +1176,11 @@ bw.DOM = function(target, taco, options = {}) {
1059
1176
  targetEl.innerHTML = '';
1060
1177
 
1061
1178
  if (taco != null) {
1062
- // Handle ComponentHandle (reactive components from bw.component())
1063
- if (taco._bwComponent === true) {
1064
- taco.mount(targetEl);
1065
- }
1066
- // Handle component handles (objects with element property)
1067
- else if (taco.element instanceof Element) {
1068
- targetEl.appendChild(taco.element);
1069
- }
1070
1179
  // Handle arrays
1071
- else if (_isA(taco)) {
1180
+ if (_isA(taco)) {
1072
1181
  taco.forEach(t => {
1073
1182
  if (t != null) {
1074
- if (t._bwComponent === true) {
1075
- t.mount(targetEl);
1076
- } else if (t.element instanceof Element) {
1077
- targetEl.appendChild(t.element);
1078
- } else {
1079
- targetEl.appendChild(bw.createDOM(t, options));
1080
- }
1183
+ targetEl.appendChild(bw.createDOM(t, options));
1081
1184
  }
1082
1185
  });
1083
1186
  }
@@ -1090,205 +1193,36 @@ bw.DOM = function(target, taco, options = {}) {
1090
1193
  return targetEl;
1091
1194
  };
1092
1195
 
1093
- /**
1094
- * Compile props into getter/setter functions for reactive updates.
1095
- *
1096
- * Used internally by `bw.renderComponent()`. Creates a proxy-like object
1097
- * where setting a property triggers `handle.onPropChange()`.
1098
- *
1099
- * @param {Object} handle - Component handle
1100
- * @param {Object} props - Initial props
1101
- * @returns {Object} Compiled props object with getters/setters
1102
- * @category DOM Generation
1103
- */
1104
- bw.compileProps = function(handle, props = {}) {
1105
- const compiledProps = {};
1106
-
1107
- _keys(props).forEach(key => {
1108
- // Create getter/setter for each prop
1109
- Object.defineProperty(compiledProps, key, {
1110
- get() {
1111
- return handle._props[key];
1112
- },
1113
- set(value) {
1114
- const oldValue = handle._props[key];
1115
- if (oldValue !== value) {
1116
- handle._props[key] = value;
1117
- // Trigger update if prop changed
1118
- if (handle.onPropChange) {
1119
- handle.onPropChange(key, value, oldValue);
1120
- }
1121
- }
1122
- },
1123
- enumerable: true,
1124
- configurable: true
1125
- });
1126
- });
1127
-
1128
- return compiledProps;
1129
- };
1196
+ // Deprecation stubs for removed ComponentHandle APIs
1197
+ bw.compileProps = function() { throw new Error('bw.compileProps() removed in v2.0.19. Use o.handle/o.slots instead.'); };
1198
+ bw.renderComponent = function() { throw new Error('bw.renderComponent() removed in v2.0.19. Use bw.mount() with o.handle/o.slots instead.'); };
1130
1199
 
1131
1200
  /**
1132
- * Render a TACO component and return an enhanced handle object.
1133
- *
1134
- * The handle provides compiled props, state management, child registration,
1135
- * and a destroy method. Used internally by `bw.createCard()`, `bw.createTable()`, etc.
1201
+ * Mount a TACO into a target element and return the created root element.
1202
+ * Like bw.DOM() but returns the root element of the TACO (not the container),
1203
+ * giving direct access to el.bw handle methods.
1136
1204
  *
1137
- * @param {Object} taco - TACO object to render
1138
- * @param {Object} [options] - Render options
1139
- * @returns {Object} Component handle with element, props, state, update(), destroy()
1205
+ * @param {string|Element} target - CSS selector or DOM element
1206
+ * @param {Object} taco - TACO to render
1207
+ * @param {Object} [options] - Mount options
1208
+ * @returns {Element} The created root element
1140
1209
  * @category DOM Generation
1141
- */
1142
- bw.renderComponent = function(taco, options = {}) {
1143
- const element = bw.createDOM(taco, options);
1144
-
1145
- // Enhanced handle with prop compilation
1146
- const handle = {
1147
- element,
1148
- taco,
1149
- _props: { ...taco.a }, // Store props internally
1150
- _state: taco.o?.state || {},
1151
- _children: {}, // Store child component references
1152
-
1153
- // Get compiled props with getters/setters
1154
- get props() {
1155
- if (!this._compiledProps) {
1156
- this._compiledProps = bw.compileProps(this, this._props);
1157
- }
1158
- return this._compiledProps;
1159
- },
1160
-
1161
- /**
1162
- * Query all matching elements within this component
1163
- * @param {string} selector - CSS selector
1164
- * @returns {NodeList} Matching elements
1165
- */
1166
- $(selector) {
1167
- return this.element.querySelectorAll(selector);
1168
- },
1169
-
1170
- /**
1171
- * Query the first matching element within this component
1172
- * @param {string} selector - CSS selector
1173
- * @returns {Element|null} First matching element or null
1174
- */
1175
- $first(selector) {
1176
- return this.element.querySelector(selector);
1177
- },
1178
-
1179
- /**
1180
- * Update component with new props and re-render in place
1181
- * @param {Object} newProps - Properties to merge into current props
1182
- * @returns {Object} this handle (for chaining)
1183
- */
1184
- update(newProps) {
1185
- // Update internal props
1186
- Object.assign(this._props, newProps);
1187
-
1188
- // Rebuild TACO with new props
1189
- const newTaco = { ...this.taco, a: { ...this.taco.a, ...newProps } };
1190
- const newElement = bw.createDOM(newTaco, options);
1191
-
1192
- // Replace in DOM
1193
- this.element.replaceWith(newElement);
1194
- this.element = newElement;
1195
- this.taco = newTaco;
1196
-
1197
- return this;
1198
- },
1199
-
1200
- /**
1201
- * Re-render the component from its current TACO, replacing the DOM element
1202
- * @returns {Object} this handle (for chaining)
1203
- */
1204
- render() {
1205
- const newElement = bw.createDOM(this.taco, options);
1206
- this.element.replaceWith(newElement);
1207
- this.element = newElement;
1208
- return this;
1209
- },
1210
-
1211
- /**
1212
- * Called when a compiled prop value changes. Override to customize behavior.
1213
- * Default implementation triggers a full re-render.
1214
- * @param {string} key - Property name that changed
1215
- * @param {*} newValue - New property value
1216
- * @param {*} oldValue - Previous property value
1217
- */
1218
- onPropChange(_key, _newValue, _oldValue) {
1219
- // Auto re-render on prop change by default
1220
- this.render();
1221
- },
1222
-
1223
- // State management
1224
- get state() {
1225
- return this._state;
1226
- },
1227
-
1228
- set state(newState) {
1229
- this._state = newState;
1230
- this.render();
1231
- },
1232
-
1233
- /**
1234
- * Merge state updates and re-render the component
1235
- * @param {Object} updates - State properties to merge
1236
- * @returns {Object} this handle (for chaining)
1237
- */
1238
- setState(updates) {
1239
- Object.assign(this._state, updates);
1240
- this.render();
1241
- return this;
1242
- },
1243
-
1244
- /**
1245
- * Register a child component under a name for later retrieval
1246
- * @param {string} name - Child name key
1247
- * @param {Object} component - Child component handle
1248
- * @returns {Object} this handle (for chaining)
1249
- */
1250
- addChild(name, component) {
1251
- this._children[name] = component;
1252
- return this;
1253
- },
1254
-
1255
- /**
1256
- * Retrieve a registered child component by name
1257
- * @param {string} name - Child name key
1258
- * @returns {Object|undefined} Child component handle
1259
- */
1260
- getChild(name) {
1261
- return this._children[name];
1262
- },
1263
-
1264
- /**
1265
- * Destroy this component and all registered children
1266
- *
1267
- * Calls destroy() recursively on children, runs bw.cleanup(),
1268
- * removes the element from DOM, and clears all internal references.
1269
- */
1270
- destroy() {
1271
- // Destroy children first
1272
- Object.values(this._children).forEach(child => {
1273
- if (child && child.destroy) child.destroy();
1274
- });
1275
-
1276
- // Clean up this component
1277
- bw.cleanup(this.element);
1278
- this.element.remove();
1279
-
1280
- // Clear references
1281
- this._children = {};
1282
- this._props = {};
1283
- this._state = {};
1284
- this._compiledProps = null;
1285
- }
1286
- };
1287
-
1288
- // Store handle reference on element
1289
- element._bwHandle = handle;
1290
-
1291
- return handle;
1210
+ * @example
1211
+ * var el = bw.mount('#app', bw.makeCarousel({ items: slides }));
1212
+ * el.bw.goToSlide(2);
1213
+ * el.bw.next();
1214
+ */
1215
+ bw.mount = function(target, taco, options) {
1216
+ var container = _is(target, 'string') ? bw.$(target)[0] : target;
1217
+ if (!container) {
1218
+ _cw('bw.mount: target not found');
1219
+ return null;
1220
+ }
1221
+ bw.cleanup(container);
1222
+ container.innerHTML = '';
1223
+ var el = bw.createDOM(taco, options || {});
1224
+ container.appendChild(el);
1225
+ return el;
1292
1226
  };
1293
1227
 
1294
1228
  /**
@@ -1309,20 +1243,29 @@ bw.renderComponent = function(taco, options = {}) {
1309
1243
  bw.cleanup = function(element) {
1310
1244
  if (!bw._isBrowser || !element) return;
1311
1245
 
1312
- // Find all elements with data-bw_id
1313
- const elements = element.querySelectorAll('[data-bw_id]');
1246
+ // Deregister UUID classes from node cache for non-lifecycle UUID elements
1247
+ var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
1248
+ uuidEls.forEach(function(uel) {
1249
+ var m = uel.className && uel.className.match(_UUID_RE);
1250
+ if (m) delete bw._nodeMap[m[0]];
1251
+ });
1252
+
1253
+ // Find all lifecycle-managed elements (have bw_lc marker class)
1254
+ const elements = element.querySelectorAll('.' + _BW_LC);
1314
1255
 
1315
1256
  elements.forEach(el => {
1316
- const id = el.getAttribute('data-bw_id');
1317
- const callback = bw._unmountCallbacks.get(id);
1257
+ var uuid = bw.getUUID(el);
1318
1258
 
1319
- if (callback) {
1320
- callback();
1321
- bw._unmountCallbacks.delete(id);
1322
- }
1259
+ if (uuid) {
1260
+ const callback = bw._unmountCallbacks.get(uuid);
1261
+ if (callback) {
1262
+ callback();
1263
+ bw._unmountCallbacks.delete(uuid);
1264
+ }
1323
1265
 
1324
- // Deregister from node cache
1325
- bw._deregisterNode(el, id);
1266
+ // Deregister from node cache
1267
+ bw._deregisterNode(el, uuid);
1268
+ }
1326
1269
 
1327
1270
  // Clean up pub/sub subscriptions tied to this element
1328
1271
  if (el._bw_subs) {
@@ -1337,16 +1280,18 @@ bw.cleanup = function(element) {
1337
1280
  });
1338
1281
 
1339
1282
  // Check element itself
1340
- const id = element.getAttribute('data-bw_id');
1341
- if (id) {
1342
- const callback = bw._unmountCallbacks.get(id);
1283
+ var selfUuid = bw.getUUID(element);
1284
+ if (selfUuid) {
1285
+ delete bw._nodeMap[selfUuid];
1286
+
1287
+ const callback = bw._unmountCallbacks.get(selfUuid);
1343
1288
  if (callback) {
1344
1289
  callback();
1345
- bw._unmountCallbacks.delete(id);
1290
+ bw._unmountCallbacks.delete(selfUuid);
1346
1291
  }
1347
1292
 
1348
1293
  // Deregister from node cache
1349
- bw._deregisterNode(element, id);
1294
+ bw._deregisterNode(element, selfUuid);
1350
1295
 
1351
1296
  // Clean up pub/sub subscriptions tied to element itself
1352
1297
  if (element._bw_subs) {
@@ -1357,11 +1302,11 @@ bw.cleanup = function(element) {
1357
1302
  delete element._bw_render;
1358
1303
  delete element._bw_refs;
1359
1304
 
1360
- // Clean up ComponentHandle back-reference
1361
- if (element._bwComponentHandle) {
1362
- element._bwComponentHandle.mounted = false;
1363
- element._bwComponentHandle.element = null;
1364
- delete element._bwComponentHandle;
1305
+ } else {
1306
+ // No UUID on element itself, but still check for _bw_subs (from bw.sub())
1307
+ if (element._bw_subs) {
1308
+ element._bw_subs.forEach(function(unsub) { unsub(); });
1309
+ delete element._bw_subs;
1365
1310
  }
1366
1311
  }
1367
1312
  };
@@ -1377,7 +1322,7 @@ bw.cleanup = function(element) {
1377
1322
  * Calls `el._bw_render(el, state)` and emits `bw:statechange` so other
1378
1323
  * components can react without tight coupling.
1379
1324
  *
1380
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element
1325
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element
1381
1326
  * @returns {Element|null} The element, or null if not found / no render function
1382
1327
  * @category State Management
1383
1328
  * @see bw.patch
@@ -1402,7 +1347,7 @@ bw.update = function(target) {
1402
1347
  * Use `bw.patch()` for lightweight value updates (scores, labels, counters)
1403
1348
  * and `bw.update()` for full structural re-renders.
1404
1349
  *
1405
- * @param {string|Element} id - Element ID, data-bw_id, CSS selector, or DOM element.
1350
+ * @param {string|Element} id - Element ID, bw_uuid_* class, CSS selector, or DOM element.
1406
1351
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
1407
1352
  * @param {string|Object} content - New text content, or TACO object to replace children
1408
1353
  * @param {string} [attr] - If provided, sets this attribute instead of content
@@ -1477,7 +1422,7 @@ bw.patchAll = function(patches) {
1477
1422
  * bubble by default so ancestor elements can listen. Use with `bw.on()` for
1478
1423
  * DOM-scoped communication between components.
1479
1424
  *
1480
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
1425
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element.
1481
1426
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
1482
1427
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
1483
1428
  * @param {*} [detail] - Data to pass with the event
@@ -1504,7 +1449,7 @@ bw.emit = function(target, eventName, detail) {
1504
1449
  * is the first argument so you don't need to destructure `e.detail`.
1505
1450
  * Events bubble, so you can listen on an ancestor element.
1506
1451
  *
1507
- * @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
1452
+ * @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element.
1508
1453
  * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
1509
1454
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
1510
1455
  * @param {Function} handler - Called with (detail, event)
@@ -1602,10 +1547,12 @@ bw.sub = function(topic, handler, el) {
1602
1547
  if (el) {
1603
1548
  if (!el._bw_subs) el._bw_subs = [];
1604
1549
  el._bw_subs.push(unsub);
1605
- // Ensure element has data-bw_id so bw.cleanup() finds it
1606
- if (!el.getAttribute('data-bw_id')) {
1607
- var bwId = 'bw_sub_' + id;
1608
- el.setAttribute('data-bw_id', bwId);
1550
+ // Ensure element has UUID + bw_lc so bw.cleanup() finds it
1551
+ if (!bw.getUUID(el)) {
1552
+ el.classList.add(bw.uuid('uuid'));
1553
+ }
1554
+ if (!el.classList.contains(_BW_LC)) {
1555
+ el.classList.add(_BW_LC);
1609
1556
  }
1610
1557
  }
1611
1558
 
@@ -1806,1108 +1753,67 @@ bw._resolveTemplate = function(str, state, compile) {
1806
1753
  if (!bw._compiledExprs[b.expr]) {
1807
1754
  try {
1808
1755
  bw._compiledExprs[b.expr] = new Function('state', 'with(state){return (' + b.expr + ');}');
1809
- } catch (e) {
1810
- bw._compiledExprs[b.expr] = function() { return ''; };
1811
- }
1812
- }
1813
- try {
1814
- val = bw._compiledExprs[b.expr](state);
1815
- } catch (e) {
1816
- if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
1817
- val = '';
1818
- }
1819
- } else {
1820
- // Tier 1: dot-path only
1821
- val = bw._evaluatePath(state, b.expr);
1822
- }
1823
- result += (val == null) ? '' : String(val);
1824
- lastEnd = b.end;
1825
- }
1826
- result += str.slice(lastEnd);
1827
- return result;
1828
- };
1829
-
1830
- /**
1831
- * Extract top-level state keys that an expression depends on.
1832
- * @param {string} expr - Expression string
1833
- * @param {string[]} stateKeys - Declared state keys
1834
- * @returns {string[]} Matching dependency keys
1835
- * @private
1836
- */
1837
- bw._extractDeps = function(expr, stateKeys) {
1838
- var deps = [];
1839
- for (var i = 0; i < stateKeys.length; i++) {
1840
- var key = stateKeys[i];
1841
- // Match word boundary: key must be preceded by start/non-word and followed by non-word/end
1842
- var re = new RegExp('(?:^|[^\\w$.])' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:[^\\w$]|$)');
1843
- if (re.test(expr) || expr === key || expr.indexOf(key + '.') === 0) {
1844
- deps.push(key);
1845
- }
1846
- }
1847
- return deps;
1848
- };
1849
-
1850
- // ===================================================================================
1851
- // Microtask Batching
1852
- // ===================================================================================
1853
-
1854
- bw._dirtyComponents = [];
1855
- bw._flushScheduled = false;
1856
-
1857
- /**
1858
- * Schedule a microtask flush for dirty components.
1859
- * @private
1860
- */
1861
- bw._scheduleFlush = function() {
1862
- if (bw._flushScheduled) return;
1863
- bw._flushScheduled = true;
1864
- if (typeof Promise !== 'undefined') {
1865
- Promise.resolve().then(bw._doFlush);
1866
- } else {
1867
- setTimeout(bw._doFlush, 0);
1868
- }
1869
- };
1870
-
1871
- /**
1872
- * Flush all dirty components. Deduplicates by _bwId.
1873
- * @private
1874
- */
1875
- bw._doFlush = function() {
1876
- bw._flushScheduled = false;
1877
- var queue = bw._dirtyComponents.slice();
1878
- bw._dirtyComponents = [];
1879
- // Deduplicate by _bwId
1880
- var seen = {};
1881
- for (var i = 0; i < queue.length; i++) {
1882
- var comp = queue[i];
1883
- if (!seen[comp._bwId]) {
1884
- seen[comp._bwId] = true;
1885
- comp._flush();
1886
- }
1887
- }
1888
- };
1889
-
1890
- /**
1891
- * Synchronous flush for testing and imperative code.
1892
- * Forces immediate re-render of all dirty components.
1893
- *
1894
- * @category Component
1895
- */
1896
- bw.flush = function() {
1897
- bw._doFlush();
1898
- };
1899
-
1900
- // ===================================================================================
1901
- // ComponentHandle — unified reactive component (Phase 1)
1902
- // ===================================================================================
1903
-
1904
- /**
1905
- * ComponentHandle constructor.
1906
- * Wraps a TACO definition with reactive state, lifecycle hooks,
1907
- * template bindings, and named actions.
1908
- *
1909
- * @param {Object} taco - TACO definition {t, a, c, o}
1910
- * @constructor
1911
- * @private
1912
- */
1913
- function ComponentHandle(taco) {
1914
- this._bwComponent = true; // duck-type marker
1915
- this._bwId = bw.uuid('comp');
1916
- this.taco = taco;
1917
- this.element = null;
1918
- this.mounted = false;
1919
-
1920
- var o = taco.o || {};
1921
- // Copy initial state
1922
- this._state = {};
1923
- if (o.state) {
1924
- for (var k in o.state) {
1925
- if (_hop.call(o.state, k)) {
1926
- this._state[k] = o.state[k];
1927
- }
1928
- }
1929
- }
1930
- // Copy actions
1931
- this._actions = {};
1932
- if (o.actions) {
1933
- for (var k2 in o.actions) {
1934
- if (_hop.call(o.actions, k2)) {
1935
- this._actions[k2] = o.actions[k2];
1936
- }
1937
- }
1938
- }
1939
- // Promote o.methods to handle API (MFC/Qt pattern: component owns its methods)
1940
- this._methods = {};
1941
- if (o.methods) {
1942
- var self = this;
1943
- for (var k3 in o.methods) {
1944
- if (_hop.call(o.methods, k3)) {
1945
- this._methods[k3] = o.methods[k3];
1946
- (function(methodName, methodFn) {
1947
- self[methodName] = function() {
1948
- var args = [self].concat(Array.prototype.slice.call(arguments));
1949
- return methodFn.apply(null, args);
1950
- };
1951
- })(k3, o.methods[k3]);
1952
- }
1953
- }
1954
- }
1955
- // User tag for addressing via bw.message()
1956
- this._userTag = null;
1957
- // Lifecycle hooks
1958
- this._hooks = {
1959
- willMount: o.willMount || null,
1960
- mounted: o.mounted || null,
1961
- willUpdate: o.willUpdate || null,
1962
- onUpdate: o.onUpdate || null,
1963
- unmount: o.unmount || null,
1964
- willDestroy: o.willDestroy || null
1965
- };
1966
- // Binding tracking
1967
- this._bindings = [];
1968
- this._dirtyKeys = {};
1969
- this._scheduled = false;
1970
- this._subs = [];
1971
- this._eventListeners = [];
1972
- this._registeredActions = [];
1973
- this._prevValues = {};
1974
- this._compile = !!o.compile;
1975
- this._bw_refs = {};
1976
- this._refCounter = 0;
1977
- // Child component ownership (Bug #5)
1978
- this._children = [];
1979
- this._parent = null;
1980
- // Factory metadata for BCCL rebuild (Bug #6)
1981
- this._factory = taco._bwFactory || null;
1982
- }
1983
-
1984
- // Short alias for ComponentHandle.prototype (see alias block at top of file).
1985
- // 28 method definitions × 25 chars = ~700B raw savings in minified output.
1986
- var _chp = ComponentHandle.prototype;
1987
-
1988
- // ── State Methods ──
1989
-
1990
- /**
1991
- * Get a state value. Dot-path supported: `get('user.name')`
1992
- */
1993
- _chp.get = function(key) {
1994
- return bw._evaluatePath(this._state, key);
1995
- };
1996
-
1997
- /**
1998
- * Set a state value. Dot-path supported. Schedules re-render.
1999
- * @param {string} key - State key (dot-path)
2000
- * @param {*} value - New value
2001
- * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
2002
- */
2003
- _chp.set = function(key, value, opts) {
2004
- // Dot-path set
2005
- var parts = key.split('.');
2006
- var obj = this._state;
2007
- for (var i = 0; i < parts.length - 1; i++) {
2008
- if (!_is(obj[parts[i]], 'object')) {
2009
- if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
2010
- obj[parts[i]] = {};
2011
- }
2012
- obj = obj[parts[i]];
2013
- }
2014
- obj[parts[parts.length - 1]] = value;
2015
- // Mark top-level key dirty
2016
- this._dirtyKeys[parts[0]] = true;
2017
- if (this.mounted) {
2018
- if (opts && opts.sync) {
2019
- this._flush();
2020
- } else {
2021
- this._scheduleDirty();
2022
- }
2023
- }
2024
- };
2025
-
2026
- /**
2027
- * Get a shallow clone of the full state.
2028
- */
2029
- _chp.getState = function() {
2030
- var clone = {};
2031
- for (var k in this._state) {
2032
- if (_hop.call(this._state, k)) {
2033
- clone[k] = this._state[k];
2034
- }
2035
- }
2036
- return clone;
2037
- };
2038
-
2039
- /**
2040
- * Merge multiple state keys. Schedules re-render.
2041
- * @param {Object} updates - Key-value pairs to merge
2042
- * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
2043
- */
2044
- _chp.setState = function(updates, opts) {
2045
- for (var k in updates) {
2046
- if (_hop.call(updates, k)) {
2047
- this._state[k] = updates[k];
2048
- this._dirtyKeys[k] = true;
2049
- }
2050
- }
2051
- if (this.mounted) {
2052
- if (opts && opts.sync) {
2053
- this._flush();
2054
- } else {
2055
- this._scheduleDirty();
2056
- }
2057
- }
2058
- };
2059
-
2060
- /**
2061
- * Push a value onto an array in state. Clones the array.
2062
- */
2063
- _chp.push = function(key, val) {
2064
- var arr = this.get(key);
2065
- var newArr = _isA(arr) ? arr.slice() : [];
2066
- newArr.push(val);
2067
- this.set(key, newArr);
2068
- };
2069
-
2070
- /**
2071
- * Splice an array in state. Clones the array.
2072
- */
2073
- _chp.splice = function(key, start, deleteCount) {
2074
- var arr = this.get(key);
2075
- var newArr = _isA(arr) ? arr.slice() : [];
2076
- var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
2077
- Array.prototype.splice.apply(newArr, args);
2078
- this.set(key, newArr);
2079
- };
2080
-
2081
- // ── Scheduling ──
2082
-
2083
- _chp._scheduleDirty = function() {
2084
- if (!this._scheduled) {
2085
- this._scheduled = true;
2086
- bw._dirtyComponents.push(this);
2087
- bw._scheduleFlush();
2088
- }
2089
- };
2090
-
2091
- // ── Binding Compilation ──
2092
-
2093
- /**
2094
- * Walk the TACO tree and extract ${expr} bindings.
2095
- * Creates binding descriptors with refIds for targeted DOM updates.
2096
- * @private
2097
- */
2098
- _chp._compileBindings = function() {
2099
- this._bindings = [];
2100
- this._refCounter = 0;
2101
- var stateKeys = _keys(this._state);
2102
- var self = this;
2103
-
2104
- function walkTaco(taco, path) {
2105
- if (!_is(taco, 'object') || !taco.t) return taco;
2106
-
2107
- // Check content for bindings
2108
- if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
2109
- var refId = 'bw_ref_' + self._refCounter++;
2110
- var parsed = bw._parseBindings(taco.c);
2111
- var deps = [];
2112
- for (var j = 0; j < parsed.length; j++) {
2113
- deps = deps.concat(bw._extractDeps(parsed[j].expr, stateKeys));
2114
- }
2115
- self._bindings.push({
2116
- expr: taco.c,
2117
- type: 'content',
2118
- refId: refId,
2119
- deps: deps,
2120
- template: taco.c
2121
- });
2122
- // Inject data-bw_ref on the TACO for createDOM to pick up
2123
- if (!taco.a) taco.a = {};
2124
- taco.a['data-bw_ref'] = refId;
2125
- }
2126
-
2127
- // Check attributes for bindings
2128
- if (taco.a) {
2129
- for (var attrName in taco.a) {
2130
- if (!_hop.call(taco.a, attrName)) continue;
2131
- if (attrName === 'data-bw_ref') continue;
2132
- var attrVal = taco.a[attrName];
2133
- if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
2134
- var refId2 = 'bw_ref_' + self._refCounter++;
2135
- var parsed2 = bw._parseBindings(attrVal);
2136
- var deps2 = [];
2137
- for (var j2 = 0; j2 < parsed2.length; j2++) {
2138
- deps2 = deps2.concat(bw._extractDeps(parsed2[j2].expr, stateKeys));
2139
- }
2140
- self._bindings.push({
2141
- expr: attrVal,
2142
- type: 'attribute',
2143
- attrName: attrName,
2144
- refId: refId2,
2145
- deps: deps2,
2146
- template: attrVal
2147
- });
2148
- if (!taco.a) taco.a = {};
2149
- taco.a['data-bw_ref'] = taco.a['data-bw_ref'] || refId2;
2150
- // If multiple attribute bindings on same element, store additional marker
2151
- if (taco.a['data-bw_ref'] !== refId2) {
2152
- taco.a['data-bw_ref_' + attrName] = refId2;
2153
- }
2154
- }
2155
- }
2156
- }
2157
-
2158
- // Recurse into children
2159
- if (_isA(taco.c)) {
2160
- for (var i = 0; i < taco.c.length; i++) {
2161
- // Wrap string children with ${expr} in a span so patches target the span, not the parent
2162
- if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
2163
- var mixedRefId = 'bw_ref_' + self._refCounter++;
2164
- var mixedParsed = bw._parseBindings(taco.c[i]);
2165
- var mixedDeps = [];
2166
- for (var mi = 0; mi < mixedParsed.length; mi++) {
2167
- mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
2168
- }
2169
- self._bindings.push({
2170
- expr: taco.c[i],
2171
- type: 'content',
2172
- refId: mixedRefId,
2173
- deps: mixedDeps,
2174
- template: taco.c[i]
2175
- });
2176
- // Replace string with a span wrapper so textContent targets the span only
2177
- taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
2178
- }
2179
- if (_is(taco.c[i], 'object') && taco.c[i].t) {
2180
- walkTaco(taco.c[i], path.concat(i));
2181
- }
2182
- // Handle bw.when/bw.each markers
2183
- if (taco.c[i] && taco.c[i]._bwWhen) {
2184
- var whenRefId = 'bw_ref_' + self._refCounter++;
2185
- var whenDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
2186
- self._bindings.push({
2187
- expr: taco.c[i].expr,
2188
- type: 'structural',
2189
- subtype: 'when',
2190
- refId: whenRefId,
2191
- deps: whenDeps,
2192
- branches: taco.c[i].branches,
2193
- index: i,
2194
- parentPath: path
2195
- });
2196
- taco.c[i]._refId = whenRefId;
2197
- }
2198
- if (taco.c[i] && taco.c[i]._bwEach) {
2199
- var eachRefId = 'bw_ref_' + self._refCounter++;
2200
- var eachDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
2201
- self._bindings.push({
2202
- expr: taco.c[i].expr,
2203
- type: 'structural',
2204
- subtype: 'each',
2205
- refId: eachRefId,
2206
- deps: eachDeps,
2207
- factory: taco.c[i].factory,
2208
- index: i,
2209
- parentPath: path
2210
- });
2211
- taco.c[i]._refId = eachRefId;
2212
- }
2213
- }
2214
- } else if (_is(taco.c, 'object') && taco.c.t) {
2215
- walkTaco(taco.c, path.concat(0));
2216
- }
2217
-
2218
- return taco;
2219
- }
2220
-
2221
- walkTaco(this.taco, []);
2222
- };
2223
-
2224
- // ── DOM Reference Collection ──
2225
-
2226
- /**
2227
- * Build ref map from the live DOM after createDOM.
2228
- * @private
2229
- */
2230
- _chp._collectRefs = function() {
2231
- this._bw_refs = {};
2232
- if (!this.element) return;
2233
- var els = this.element.querySelectorAll('[data-bw_ref]');
2234
- for (var i = 0; i < els.length; i++) {
2235
- this._bw_refs[els[i].getAttribute('data-bw_ref')] = els[i];
2236
- }
2237
- // Also check root element
2238
- var rootRef = this.element.getAttribute && this.element.getAttribute('data-bw_ref');
2239
- if (rootRef) {
2240
- this._bw_refs[rootRef] = this.element;
2241
- }
2242
- };
2243
-
2244
- // ── Lifecycle ──
2245
-
2246
- /**
2247
- * Mount the component into a parent DOM element.
2248
- * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
2249
- * @param {Element} parentEl - DOM element to mount into
2250
- */
2251
- _chp.mount = function(parentEl) {
2252
- // willMount hook
2253
- if (this._hooks.willMount) this._hooks.willMount(this);
2254
-
2255
- // Save original TACO for re-renders (structural changes clone from this)
2256
- if (!this._originalTaco) {
2257
- this._originalTaco = this.taco;
2258
- }
2259
-
2260
- // Deep-clone TACO so binding annotations don't mutate original.
2261
- // Custom clone to preserve _bwWhen/_bwEach markers and their factory functions.
2262
- this.taco = this._deepCloneTaco(this._originalTaco);
2263
-
2264
- // Compile bindings (annotates TACO with data-bw_ref attributes)
2265
- this._compileBindings();
2266
-
2267
- // Prepare TACO: resolve initial binding values, evaluate when/each
2268
- this._prepareTaco(this.taco);
2269
-
2270
- // Register named actions in function registry
2271
- var self = this;
2272
- for (var actionName in this._actions) {
2273
- if (_hop.call(this._actions, actionName)) {
2274
- var registeredName = this._bwId + '_' + actionName;
2275
- (function(aName) {
2276
- bw.funcRegister(function(evt) {
2277
- self._actions[aName](self, evt);
2278
- }, registeredName);
2279
- })(actionName);
2280
- this._registeredActions.push(registeredName);
2281
- }
2282
- }
2283
-
2284
- // Wire action names in onclick etc. to dispatch strings
2285
- this._wireActions(this.taco);
2286
-
2287
- // Create DOM (strip o before createDOM to prevent double lifecycle)
2288
- var tacoForDOM = this._tacoForDOM(this.taco);
2289
- this.element = bw.createDOM(tacoForDOM);
2290
- this.element._bwComponentHandle = this;
2291
- this.element.setAttribute('data-bw_comp_id', this._bwId);
2292
-
2293
- // Restore o.render from original TACO (stripped by _tacoForDOM)
2294
- if (this.taco.o && this.taco.o.render) {
2295
- this.element._bw_render = this.taco.o.render;
2296
- }
2297
- if (this._userTag) {
2298
- this.element.classList.add(this._userTag);
2299
- }
2300
-
2301
- // Append to parent
2302
- parentEl.appendChild(this.element);
2303
-
2304
- // Collect refs from live DOM
2305
- this._collectRefs();
2306
-
2307
- // Resolve initial bindings and apply to DOM
2308
- this._resolveAndApplyAll();
2309
-
2310
- this.mounted = true;
2311
-
2312
- // Scan for child ComponentHandles and link parent/child (Bug #5)
2313
- var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
2314
- for (var ci = 0; ci < childEls.length; ci++) {
2315
- var ch = childEls[ci]._bwComponentHandle;
2316
- if (ch && ch !== this && !ch._parent) {
2317
- ch._parent = this;
2318
- this._children.push(ch);
2319
- }
2320
- }
2321
-
2322
- // mounted hook (backward compat: fn.length === 2 wraps (el, state))
2323
- if (this._hooks.mounted) {
2324
- if (this._hooks.mounted.length === 2) {
2325
- this._hooks.mounted(this.element, this.getState());
2326
- } else {
2327
- this._hooks.mounted(this);
2328
- }
2329
- }
2330
-
2331
- // Invoke o.render on initial mount (if present)
2332
- if (this.element._bw_render) {
2333
- this.element._bw_render(this.element, this._state);
2334
- }
2335
- };
2336
-
2337
- /**
2338
- * Prepare TACO for initial render: resolve when/each markers.
2339
- * @private
2340
- */
2341
- _chp._prepareTaco = function(taco) {
2342
- if (!_is(taco, 'object')) return;
2343
-
2344
- if (_isA(taco.c)) {
2345
- for (var i = taco.c.length - 1; i >= 0; i--) {
2346
- var child = taco.c[i];
2347
- if (child && child._bwWhen) {
2348
- var exprStr = child.expr.replace(/^\$\{|\}$/g, '');
2349
- var val;
2350
- if (this._compile) {
2351
- try {
2352
- val = (new Function('state', 'with(state){return (' + exprStr + ');}'))(this._state);
2353
- } catch(e) { val = false; }
2354
- } else {
2355
- val = bw._evaluatePath(this._state, exprStr);
2356
- }
2357
- var branch = val ? child.branches[0] : (child.branches[1] || null);
2358
- if (branch) {
2359
- // Wrap in a container so we can track it
2360
- taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: branch };
2361
- } else {
2362
- taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: '' };
2363
- }
2364
- }
2365
- if (child && child._bwEach) {
2366
- var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
2367
- var arr = bw._evaluatePath(this._state, eachExprStr);
2368
- var items = [];
2369
- if (_isA(arr)) {
2370
- for (var j = 0; j < arr.length; j++) {
2371
- items.push(child.factory(arr[j], j));
2372
- }
2373
- }
2374
- taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
2375
- }
2376
- if (_is(taco.c[i], 'object') && taco.c[i].t) {
2377
- this._prepareTaco(taco.c[i]);
2378
- }
2379
- }
2380
- } else if (_is(taco.c, 'object') && taco.c.t) {
2381
- this._prepareTaco(taco.c);
2382
- }
2383
- };
2384
-
2385
- /**
2386
- * Wire action name strings (in onclick etc.) to dispatch function calls.
2387
- * @private
2388
- */
2389
- _chp._wireActions = function(taco) {
2390
- if (!_is(taco, 'object') || !taco.t) return;
2391
- if (taco.a) {
2392
- for (var key in taco.a) {
2393
- if (!_hop.call(taco.a, key)) continue;
2394
- if (key.startsWith('on') && _is(taco.a[key], 'string')) {
2395
- var actionName = taco.a[key];
2396
- if (actionName in this._actions) {
2397
- var registeredName = this._bwId + '_' + actionName;
2398
- // Replace string with actual function for createDOM event binding
2399
- (function(rName) {
2400
- taco.a[key] = function(evt) {
2401
- bw.funcGetById(rName)(evt);
2402
- };
2403
- })(registeredName);
2404
- }
2405
- }
2406
- }
2407
- }
2408
- if (_isA(taco.c)) {
2409
- for (var i = 0; i < taco.c.length; i++) {
2410
- this._wireActions(taco.c[i]);
2411
- }
2412
- } else if (_is(taco.c, 'object') && taco.c.t) {
2413
- this._wireActions(taco.c);
2414
- }
2415
- };
2416
-
2417
- /**
2418
- * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
2419
- * @private
2420
- */
2421
- _chp._deepCloneTaco = function(taco) {
2422
- if (taco == null) return taco;
2423
- // Preserve _bwWhen / _bwEach markers (contain functions)
2424
- if (taco._bwWhen) {
2425
- return { _bwWhen: true, expr: taco.expr, branches: [
2426
- this._deepCloneTaco(taco.branches[0]),
2427
- taco.branches[1] ? this._deepCloneTaco(taco.branches[1]) : null
2428
- ], _refId: taco._refId };
2429
- }
2430
- if (taco._bwEach) {
2431
- return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
2432
- }
2433
- if (!_is(taco, 'object') || !taco.t) return taco;
2434
- var result = { t: taco.t };
2435
- if (taco.a) {
2436
- result.a = {};
2437
- for (var k in taco.a) {
2438
- if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
2439
- }
2440
- }
2441
- if (taco.c != null) {
2442
- if (_isA(taco.c)) {
2443
- result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
2444
- } else if (_is(taco.c, 'object')) {
2445
- result.c = this._deepCloneTaco(taco.c);
2446
- } else {
2447
- result.c = taco.c;
2448
- }
2449
- }
2450
- if (taco.o) result.o = taco.o; // Keep o reference (not deep-cloned; hooks are functions)
2451
- return result;
2452
- };
2453
-
2454
- /**
2455
- * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
2456
- * @private
2457
- */
2458
- _chp._tacoForDOM = function(taco) {
2459
- if (!_is(taco, 'object') || !taco.t) return taco;
2460
- var result = { t: taco.t };
2461
- if (taco.a) result.a = taco.a;
2462
- if (taco.c != null) {
2463
- if (_isA(taco.c)) {
2464
- result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
2465
- } else if (_is(taco.c, 'object') && taco.c.t) {
2466
- result.c = this._tacoForDOM(taco.c);
2467
- } else {
2468
- result.c = taco.c;
2469
- }
2470
- }
2471
- // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
2472
- if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
2473
- _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
2474
- '>. Use onclick attribute or bw.component() for child interactivity.');
2475
- }
2476
- return result;
2477
- };
2478
-
2479
- /**
2480
- * Unmount: remove from DOM, deactivate, preserve state for re-mount.
2481
- */
2482
- _chp.unmount = function() {
2483
- if (!this.mounted) return;
2484
-
2485
- // unmount hook
2486
- if (this._hooks.unmount) {
2487
- this._hooks.unmount(this);
2488
- }
2489
-
2490
- // Remove DOM event listeners
2491
- for (var i = 0; i < this._eventListeners.length; i++) {
2492
- var l = this._eventListeners[i];
2493
- if (this.element) {
2494
- this.element.removeEventListener(l.event, l.handler);
2495
- }
2496
- }
2497
- this._eventListeners = [];
2498
-
2499
- // Unsubscribe pub/sub
2500
- for (var j = 0; j < this._subs.length; j++) {
2501
- this._subs[j]();
2502
- }
2503
- this._subs = [];
2504
-
2505
- // Remove from DOM
2506
- if (this.element && this.element.parentNode) {
2507
- this.element.parentNode.removeChild(this.element);
2508
- }
2509
-
2510
- this.mounted = false;
2511
- // State preserved — can re-mount
2512
- };
2513
-
2514
- /**
2515
- * Destroy: unmount + clear state + unregister actions.
2516
- */
2517
- _chp.destroy = function() {
2518
- // willDestroy hook
2519
- if (this._hooks.willDestroy) {
2520
- this._hooks.willDestroy(this);
2521
- }
2522
-
2523
- // Cascade destroy to children depth-first (Bug #5)
2524
- for (var ci = this._children.length - 1; ci >= 0; ci--) {
2525
- this._children[ci].destroy();
2526
- }
2527
- this._children = [];
2528
- if (this._parent) {
2529
- var idx = this._parent._children.indexOf(this);
2530
- if (idx >= 0) this._parent._children.splice(idx, 1);
2531
- this._parent = null;
2532
- }
2533
-
2534
- this.unmount();
2535
-
2536
- // Unregister actions from function registry
2537
- for (var i = 0; i < this._registeredActions.length; i++) {
2538
- bw.funcUnregister(this._registeredActions[i]);
2539
- }
2540
- this._registeredActions = [];
2541
-
2542
- // Clear state
2543
- this._state = {};
2544
- this._bindings = [];
2545
- this._bw_refs = {};
2546
- this._prevValues = {};
2547
- this._dirtyKeys = {};
2548
- if (this.element) {
2549
- delete this.element._bwComponentHandle;
2550
- this.element = null;
2551
- }
2552
- };
2553
-
2554
- // ── Flush & Binding Resolution ──
2555
-
2556
- /**
2557
- * Flush dirty state: resolve changed bindings and apply to DOM.
2558
- * @private
2559
- */
2560
- _chp._flush = function() {
2561
- this._scheduled = false;
2562
- var changedKeys = _keys(this._dirtyKeys);
2563
- this._dirtyKeys = {};
2564
- if (changedKeys.length === 0 || !this.mounted) return;
2565
-
2566
- // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
2567
- // rebuild the TACO from the factory with merged state (Bug #6)
2568
- if (this._factory) {
2569
- var rebuildNeeded = false;
2570
- for (var fi = 0; fi < changedKeys.length; fi++) {
2571
- if (_hop.call(this._factory.props, changedKeys[fi])) {
2572
- rebuildNeeded = true; break;
2573
- }
2574
- }
2575
- if (rebuildNeeded) {
2576
- var merged = {};
2577
- for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
2578
- for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
2579
- this._factory.props = merged;
2580
- var newTaco = bw.make(this._factory.type, merged);
2581
- newTaco._bwFactory = this._factory;
2582
- this.taco = newTaco;
2583
- this._originalTaco = this._deepCloneTaco(newTaco);
2584
- this._render();
2585
- if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
2586
- return;
2587
- }
2588
- }
2589
-
2590
- // willUpdate hook
2591
- if (this._hooks.willUpdate) {
2592
- this._hooks.willUpdate(this, changedKeys);
2593
- }
2594
-
2595
- // Check if any structural bindings are affected
2596
- var needsFullRender = false;
2597
- for (var i = 0; i < this._bindings.length; i++) {
2598
- var b = this._bindings[i];
2599
- if (b.type === 'structural') {
2600
- for (var j = 0; j < b.deps.length; j++) {
2601
- if (changedKeys.indexOf(b.deps[j]) >= 0) {
2602
- needsFullRender = true;
2603
- break;
2604
- }
2605
- }
2606
- if (needsFullRender) break;
2607
- }
2608
- }
2609
-
2610
- if (needsFullRender) {
2611
- this._render();
2612
- } else {
2613
- var patches = this._resolveBindings(changedKeys);
2614
- this._applyPatches(patches);
2615
- }
2616
-
2617
- // onUpdate hook
2618
- if (this._hooks.onUpdate) {
2619
- this._hooks.onUpdate(this, changedKeys);
2620
- }
2621
- };
2622
-
2623
- /**
2624
- * Resolve bindings whose deps intersect with changedKeys.
2625
- * Returns list of patches to apply.
2626
- * @private
2627
- */
2628
- _chp._resolveBindings = function(changedKeys) {
2629
- var patches = [];
2630
- for (var i = 0; i < this._bindings.length; i++) {
2631
- var b = this._bindings[i];
2632
- if (b.type === 'structural') continue;
2633
-
2634
- // Check if any dep matches
2635
- var affected = false;
2636
- for (var j = 0; j < b.deps.length; j++) {
2637
- if (changedKeys.indexOf(b.deps[j]) >= 0) {
2638
- affected = true;
2639
- break;
2640
- }
2641
- }
2642
- if (!affected) continue;
2643
-
2644
- // Evaluate
2645
- var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
2646
- var prevKey = b.refId + '_' + (b.attrName || 'content');
2647
- if (this._prevValues[prevKey] !== newVal) {
2648
- this._prevValues[prevKey] = newVal;
2649
- patches.push({
2650
- refId: b.refId,
2651
- type: b.type,
2652
- attrName: b.attrName,
2653
- value: newVal
2654
- });
2655
- }
2656
- }
2657
- return patches;
2658
- };
2659
-
2660
- /**
2661
- * Apply patches to DOM.
2662
- * @private
2663
- */
2664
- _chp._applyPatches = function(patches) {
2665
- for (var i = 0; i < patches.length; i++) {
2666
- var p = patches[i];
2667
- var el = this._bw_refs[p.refId];
2668
- if (!el) {
2669
- if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
2670
- continue;
2671
- }
2672
- if (p.type === 'content') {
2673
- el.textContent = p.value;
2674
- } else if (p.type === 'attribute') {
2675
- if (p.attrName === 'class') {
2676
- el.className = p.value;
2677
- } else {
2678
- el.setAttribute(p.attrName, p.value);
2679
- }
2680
- }
2681
- }
2682
- };
2683
-
2684
- /**
2685
- * Resolve all bindings and apply (used for initial render).
2686
- * @private
2687
- */
2688
- _chp._resolveAndApplyAll = function() {
2689
- var patches = [];
2690
- for (var i = 0; i < this._bindings.length; i++) {
2691
- var b = this._bindings[i];
2692
- if (b.type === 'structural') continue;
2693
-
2694
- var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
2695
- var prevKey = b.refId + '_' + (b.attrName || 'content');
2696
- this._prevValues[prevKey] = newVal;
2697
- patches.push({
2698
- refId: b.refId,
2699
- type: b.type,
2700
- attrName: b.attrName,
2701
- value: newVal
2702
- });
2703
- }
2704
- this._applyPatches(patches);
2705
- };
2706
-
2707
- /**
2708
- * Full re-render for structural changes (when/each branch switches).
2709
- * @private
2710
- */
2711
- _chp._render = function() {
2712
- if (!this.element || !this.element.parentNode) return;
2713
- var parent = this.element.parentNode;
2714
- var nextSibling = this.element.nextSibling;
2715
-
2716
- // Remove old DOM
2717
- parent.removeChild(this.element);
2718
-
2719
- // Re-prepare TACO with current state (deep clone preserving functions)
2720
- this.taco = this._deepCloneTaco(this._originalTaco || this.taco);
2721
-
2722
- // Re-compile bindings and prepare
2723
- this._compileBindings();
2724
- this._prepareTaco(this.taco);
2725
- this._wireActions(this.taco);
2726
-
2727
- var tacoForDOM = this._tacoForDOM(this.taco);
2728
- this.element = bw.createDOM(tacoForDOM);
2729
- this.element._bwComponentHandle = this;
2730
- this.element.setAttribute('data-bw_comp_id', this._bwId);
2731
-
2732
- // Re-insert at same position
2733
- if (nextSibling) {
2734
- parent.insertBefore(this.element, nextSibling);
2735
- } else {
2736
- parent.appendChild(this.element);
2737
- }
2738
-
2739
- // Re-collect refs and apply all bindings
2740
- this._collectRefs();
2741
- this._resolveAndApplyAll();
2742
- };
2743
-
2744
- // ── Event & Pub/Sub Methods ──
2745
-
2746
- /**
2747
- * Add a DOM event listener on the component's root element.
2748
- * @param {string} event - Event name (e.g., 'click')
2749
- * @param {Function} handler - Event handler
2750
- */
2751
- _chp.on = function(event, handler) {
2752
- if (this.element) {
2753
- this.element.addEventListener(event, handler);
2754
- }
2755
- this._eventListeners.push({ event: event, handler: handler });
2756
- };
2757
-
2758
- /**
2759
- * Remove a DOM event listener.
2760
- * @param {string} event - Event name
2761
- * @param {Function} handler - Handler to remove
2762
- */
2763
- _chp.off = function(event, handler) {
2764
- if (this.element) {
2765
- this.element.removeEventListener(event, handler);
2766
- }
2767
- this._eventListeners = this._eventListeners.filter(function(l) {
2768
- return !(l.event === event && l.handler === handler);
2769
- });
2770
- };
2771
-
2772
- /**
2773
- * Subscribe to a pub/sub topic. Lifecycle-tied: auto-unsubs on destroy.
2774
- * @param {string} topic - Topic name
2775
- * @param {Function} handler - Handler function
2776
- * @returns {Function} Unsubscribe function
2777
- */
2778
- _chp.sub = function(topic, handler) {
2779
- var unsub = bw.sub(topic, handler);
2780
- this._subs.push(unsub);
2781
- return unsub;
2782
- };
2783
-
2784
- /**
2785
- * Call a named action.
2786
- * @param {string} name - Action name
2787
- * @param {...*} args - Arguments passed after comp
2788
- */
2789
- _chp.action = function(name) {
2790
- var fn = this._actions[name];
2791
- if (!fn) {
2792
- _cw('ComponentHandle.action: unknown action "' + name + '"');
2793
- return;
2794
- }
2795
- var args = [this].concat(Array.prototype.slice.call(arguments, 1));
2796
- return fn.apply(null, args);
2797
- };
2798
-
2799
- /**
2800
- * querySelector within the component's DOM.
2801
- * @param {string} sel - CSS selector
2802
- * @returns {Element|null}
2803
- */
2804
- _chp.select = function(sel) {
2805
- return this.element ? this.element.querySelector(sel) : null;
2806
- };
2807
-
2808
- /**
2809
- * querySelectorAll within the component's DOM.
2810
- * @param {string} sel - CSS selector
2811
- * @returns {Element[]}
2812
- */
2813
- _chp.selectAll = function(sel) {
2814
- if (!this.element) return [];
2815
- return Array.prototype.slice.call(this.element.querySelectorAll(sel));
2816
- };
2817
-
2818
- /**
2819
- * Tag this component with a user-defined ID for addressing via bw.message().
2820
- * The tag is added as a CSS class on the root element (DOM IS the registry).
2821
- * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
2822
- * @returns {ComponentHandle} this (for chaining)
2823
- */
2824
- _chp.userTag = function(tag) {
2825
- this._userTag = tag;
2826
- if (this.element) {
2827
- this.element.classList.add(tag);
1756
+ } catch (e) {
1757
+ bw._compiledExprs[b.expr] = function() { return ''; };
1758
+ }
1759
+ }
1760
+ try {
1761
+ val = bw._compiledExprs[b.expr](state);
1762
+ } catch (e) {
1763
+ if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
1764
+ val = '';
1765
+ }
1766
+ } else {
1767
+ // Tier 1: dot-path only
1768
+ val = bw._evaluatePath(state, b.expr);
1769
+ }
1770
+ result += (val == null) ? '' : String(val);
1771
+ lastEnd = b.end;
2828
1772
  }
2829
- return this;
1773
+ result += str.slice(lastEnd);
1774
+ return result;
2830
1775
  };
2831
1776
 
2832
- // Expose ComponentHandle on bw (for testing and advanced use)
2833
- bw._ComponentHandle = ComponentHandle;
2834
-
2835
1777
  // ===================================================================================
2836
- // Control Flow Helpers
1778
+ // Deprecation stubs for removed ComponentHandle APIs (v2.0.19)
2837
1779
  // ===================================================================================
2838
1780
 
2839
- /**
2840
- * Conditional rendering helper.
2841
- * Returns a marker object that ComponentHandle detects during binding compilation.
2842
- * In static contexts (bw.html with state), evaluates immediately.
2843
- *
2844
- * @param {string} expr - Expression string like '${loggedIn}'
2845
- * @param {Object} tacoTrue - TACO to render when truthy
2846
- * @param {Object} [tacoFalse] - TACO to render when falsy
2847
- * @returns {Object} Marker object with _bwWhen flag
2848
- * @category Component
2849
- */
2850
- bw.when = function(expr, tacoTrue, tacoFalse) {
2851
- return { _bwWhen: true, expr: expr, branches: [tacoTrue, tacoFalse || null] };
2852
- };
1781
+ bw._extractDeps = undefined;
1782
+ bw._dirtyComponents = undefined;
1783
+ bw._flushScheduled = undefined;
1784
+ bw._scheduleFlush = undefined;
1785
+ bw._doFlush = undefined;
1786
+ bw._ComponentHandle = undefined;
2853
1787
 
2854
1788
  /**
2855
- * List rendering helper.
2856
- * Returns a marker object that ComponentHandle detects during binding compilation.
2857
- *
2858
- * @param {string} expr - Expression string like '${items}'
2859
- * @param {Function} fn - Factory function(item, index) returning TACO
2860
- * @returns {Object} Marker object with _bwEach flag
1789
+ * No-op flush (ComponentHandle removed in v2.0.19).
1790
+ * Kept as no-op for backward compatibility.
2861
1791
  * @category Component
2862
1792
  */
2863
- bw.each = function(expr, fn) {
2864
- return { _bwEach: true, expr: expr, factory: fn };
2865
- };
1793
+ bw.flush = function() {};
2866
1794
 
2867
- // ===================================================================================
2868
- // bw.component() — Factory for ComponentHandle
2869
- // ===================================================================================
2870
1795
 
2871
- /**
2872
- * Create a ComponentHandle from a TACO definition.
2873
- * The returned handle has .get(), .set(), .mount(), .destroy(), etc.
2874
- *
2875
- * @param {Object} taco - TACO definition with {t, a, c, o}
2876
- * @returns {ComponentHandle} Reactive component handle
2877
- * @category Component
2878
- * @see bw.DOM
2879
- * @example
2880
- * var counter = bw.component({
2881
- * t: 'div', c: [{ t: 'h3', c: 'Count: ${count}' }],
2882
- * o: { state: { count: 0 } }
2883
- * });
2884
- * bw.DOM('#app', counter);
2885
- * counter.set('count', 42); // DOM auto-updates
2886
- */
2887
- bw.component = function(taco) {
2888
- return new ComponentHandle(taco);
2889
- };
1796
+ bw.when = function() { throw new Error('bw.when() removed in v2.0.19. Use conditional logic in o.render instead.'); };
1797
+ bw.each = function() { throw new Error('bw.each() removed in v2.0.19. Use array mapping in o.render instead.'); };
1798
+ bw.component = function() { throw new Error('bw.component() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
1799
+
2890
1800
 
2891
1801
  // ===================================================================================
2892
1802
  // bw.message() — SendMessage() for the web
2893
1803
  // ===================================================================================
2894
1804
 
2895
1805
  /**
2896
- * Dispatch a message to a component by UUID or user tag.
2897
- * Finds the component's DOM element, looks up its ComponentHandle,
2898
- * and calls the named method. This is the bitwrench equivalent of
2899
- * Win32 SendMessage(hwnd, msg, wParam, lParam).
1806
+ * Dispatch a message to a component by UUID, CSS class, or selector.
1807
+ * Finds the element, looks up el.bw, and calls the named method.
1808
+ * This is the bitwrench equivalent of Win32 SendMessage(hwnd, msg, wParam, lParam).
2900
1809
  *
2901
- * @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
2902
- * @param {string} action - Method name to call on the component
1810
+ * @param {string} target - Component UUID (bw_uuid_*), CSS class, or selector
1811
+ * @param {string} action - Method name to call on el.bw
2903
1812
  * @param {*} data - Data to pass to the method
2904
1813
  * @returns {boolean} True if message was dispatched successfully
2905
1814
  * @category Component
2906
1815
  * @example
2907
- * // Tag a component
2908
- * myDash.userTag('dashboard_prod');
2909
- * // Dispatch locally
2910
- * bw.message('dashboard_prod', 'addAlert', { severity: 'warning', text: 'CPU spike' });
1816
+ * bw.message('my_carousel', 'goToSlide', 2);
2911
1817
  * // Or from SSE handler:
2912
1818
  * es.onmessage = function(e) {
2913
1819
  * var msg = JSON.parse(e.data);
@@ -2915,75 +1821,35 @@ bw.component = function(taco) {
2915
1821
  * };
2916
1822
  */
2917
1823
  bw.message = function(target, action, data) {
2918
- // Try data-bw_comp_id attribute first, then CSS class (user tag)
2919
- var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
2920
- if (!el) {
2921
- el = bw.$('.' + target)[0];
2922
- }
2923
- if (!el || !el._bwComponentHandle) return false;
2924
- var comp = el._bwComponentHandle;
2925
- if (!_is(comp[action], 'function')) {
2926
- _cw('bw.message: unknown action "' + action + '" on component ' + target);
1824
+ var el = bw._el(target);
1825
+ if (!el) el = bw.$('.' + target)[0];
1826
+ if (!el || !el.bw || typeof el.bw[action] !== 'function') {
1827
+ _cw('bw.message: no handle method "' + action + '" on ' + target);
2927
1828
  return false;
2928
1829
  }
2929
- comp[action](data);
1830
+ el.bw[action](data);
2930
1831
  return true;
2931
1832
  };
2932
1833
 
2933
1834
  // ===================================================================================
2934
- // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
1835
+ // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
2935
1836
  // ===================================================================================
2936
1837
 
2937
1838
  /**
2938
1839
  * Registry of named functions sent via register messages.
2939
- * Populated by clientApply({ type: 'register', name, body }).
2940
- * Invoked by clientApply({ type: 'call', name, args }).
1840
+ * Populated by bw.apply({ type: 'register', name, body }).
1841
+ * Invoked by bw.apply({ type: 'call', name, args }).
2941
1842
  * @private
2942
1843
  */
2943
1844
  bw._clientFunctions = {};
2944
1845
 
2945
1846
  /**
2946
- * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
1847
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
2947
1848
  * Default false — exec messages are rejected unless explicitly opted in.
2948
1849
  * @private
2949
1850
  */
2950
1851
  bw._allowExec = false;
2951
1852
 
2952
- /**
2953
- * Built-in client functions available via call() without registration.
2954
- * @private
2955
- */
2956
- bw._builtinClientFunctions = {
2957
- scrollTo: function(selector) {
2958
- var el = bw._el(selector);
2959
- if (el) el.scrollTop = el.scrollHeight;
2960
- },
2961
- focus: function(selector) {
2962
- var el = bw._el(selector);
2963
- if (el && _is(el.focus, 'function')) el.focus();
2964
- },
2965
- download: function(filename, content, mimeType) {
2966
- if (typeof document === 'undefined') return;
2967
- var blob = new Blob([content], { type: mimeType || 'text/plain' });
2968
- var a = document.createElement('a');
2969
- a.href = URL.createObjectURL(blob);
2970
- a.download = filename;
2971
- a.click();
2972
- URL.revokeObjectURL(a.href);
2973
- },
2974
- clipboard: function(text) {
2975
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
2976
- navigator.clipboard.writeText(text);
2977
- }
2978
- },
2979
- redirect: function(url) {
2980
- if (typeof window !== 'undefined') window.location.href = url;
2981
- },
2982
- log: function() {
2983
- console.log.apply(console, arguments);
2984
- }
2985
- };
2986
-
2987
1853
  /**
2988
1854
  * Parse a bwserve protocol message string, supporting both strict JSON
2989
1855
  * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
@@ -2998,9 +1864,9 @@ bw._builtinClientFunctions = {
2998
1864
  * @param {string} str - JSON or r-prefixed relaxed JSON string
2999
1865
  * @returns {Object} Parsed message object
3000
1866
  * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
3001
- * @category Server
1867
+ * @category Core
3002
1868
  */
3003
- bw.clientParse = function(str) {
1869
+ bw.parseJSONFlex = function(str) {
3004
1870
  str = (str || '').trim();
3005
1871
  if (str.charAt(0) !== 'r') return JSON.parse(str);
3006
1872
  str = str.slice(1);
@@ -3085,10 +1951,10 @@ bw.clientParse = function(str) {
3085
1951
  * append — target.appendChild(bw.createDOM(node))
3086
1952
  * remove — bw.cleanup(target); target.remove()
3087
1953
  * patch — bw.patch(target, content, attr)
3088
- * batch — iterate ops, call clientApply for each
1954
+ * batch — iterate ops, call bw.apply for each
3089
1955
  * message — bw.message(target, action, data)
3090
1956
  * register — store a named function for later call()
3091
- * call — invoke a registered or built-in function
1957
+ * call — invoke a registered function
3092
1958
  * exec — execute arbitrary JS (requires allowExec)
3093
1959
  *
3094
1960
  * Target resolution:
@@ -3097,9 +1963,9 @@ bw.clientParse = function(str) {
3097
1963
  *
3098
1964
  * @param {Object} msg - Protocol message
3099
1965
  * @returns {boolean} true if the message was applied successfully
3100
- * @category Server
1966
+ * @category Core
3101
1967
  */
3102
- bw.clientApply = function(msg) {
1968
+ bw.apply = function(msg) {
3103
1969
  if (!msg || !msg.type) return false;
3104
1970
 
3105
1971
  var type = msg.type;
@@ -3133,7 +1999,7 @@ bw.clientApply = function(msg) {
3133
1999
  if (!_isA(msg.ops)) return false;
3134
2000
  var allOk = true;
3135
2001
  msg.ops.forEach(function(op) {
3136
- if (!bw.clientApply(op)) allOk = false;
2002
+ if (!bw.apply(op)) allOk = false;
3137
2003
  });
3138
2004
  return allOk;
3139
2005
 
@@ -3152,7 +2018,7 @@ bw.clientApply = function(msg) {
3152
2018
 
3153
2019
  } else if (type === 'call') {
3154
2020
  if (!msg.name) return false;
3155
- var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
2021
+ var fn = bw._clientFunctions[msg.name];
3156
2022
  if (!_is(fn, 'function')) return false;
3157
2023
  try {
3158
2024
  var args = _isA(msg.args) ? msg.args : [];
@@ -3181,271 +2047,35 @@ bw.clientApply = function(msg) {
3181
2047
  return false;
3182
2048
  };
3183
2049
 
3184
- /**
3185
- * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
3186
- *
3187
- * Returns a connection object with sendAction(), on(), and close() methods.
3188
- *
3189
- * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
3190
- * @param {Object} [opts] - Connection options
3191
- * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
3192
- * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
3193
- * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
3194
- * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
3195
- * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
3196
- * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
3197
- * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
3198
- * @returns {Object} Connection object { sendAction, on, close, status }
3199
- * @category Server
3200
- */
3201
- bw.clientConnect = function(url, opts) {
3202
- opts = opts || {};
3203
- var transport = opts.transport || 'sse';
3204
- var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
3205
- var reconnect = opts.reconnect !== false;
3206
- var onStatus = opts.onStatus || function() {};
3207
- var onMessage = opts.onMessage || null;
3208
- var handlers = {};
3209
- // Set the global allowExec flag from connection options
3210
- bw._allowExec = !!opts.allowExec;
3211
- var conn = {
3212
- status: 'connecting',
3213
- _es: null,
3214
- _pollTimer: null
3215
- };
3216
-
3217
- function setStatus(s) {
3218
- conn.status = s;
3219
- onStatus(s);
3220
- }
3221
-
3222
- function handleMessage(data) {
3223
- try {
3224
- var msg = _is(data, 'string') ? bw.clientParse(data) : data;
3225
- if (onMessage) onMessage(msg);
3226
- if (handlers.message) handlers.message(msg);
3227
- bw.clientApply(msg);
3228
- } catch (e) {
3229
- if (handlers.error) handlers.error(e);
3230
- }
3231
- }
3232
-
3233
- if (transport === 'sse' && typeof EventSource !== 'undefined') {
3234
- setStatus('connecting');
3235
- var es = new EventSource(url);
3236
- conn._es = es;
3237
-
3238
- es.onopen = function() {
3239
- setStatus('connected');
3240
- if (handlers.open) handlers.open();
3241
- };
3242
-
3243
- es.onmessage = function(e) {
3244
- handleMessage(e.data);
3245
- };
3246
-
3247
- es.onerror = function() {
3248
- if (conn.status === 'connected') {
3249
- setStatus('disconnected');
3250
- }
3251
- if (handlers.error) handlers.error(new Error('SSE connection error'));
3252
- if (!reconnect) {
3253
- es.close();
3254
- }
3255
- // EventSource auto-reconnects by default when reconnect=true
3256
- };
3257
- } else if (transport === 'poll') {
3258
- var interval = opts.interval || 2000;
3259
- setStatus('connected');
3260
- conn._pollTimer = setInterval(function() {
3261
- fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
3262
- if (_isA(msgs)) {
3263
- msgs.forEach(handleMessage);
3264
- } else if (msgs && msgs.type) {
3265
- handleMessage(msgs);
3266
- }
3267
- }).catch(function(e) {
3268
- if (handlers.error) handlers.error(e);
3269
- });
3270
- }, interval);
3271
- }
3272
-
3273
- /**
3274
- * Send an action to the server via POST.
3275
- * @param {string} action - Action name
3276
- * @param {Object} [data] - Action payload
3277
- */
3278
- conn.sendAction = function(action, data) {
3279
- var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
3280
- fetch(actionUrl, {
3281
- method: 'POST',
3282
- headers: { 'Content-Type': 'application/json' },
3283
- body: body
3284
- }).catch(function(e) {
3285
- if (handlers.error) handlers.error(e);
3286
- });
3287
- };
3288
-
3289
- /**
3290
- * Register an event handler.
3291
- * @param {string} event - 'open'|'message'|'error'|'close'
3292
- * @param {Function} handler
3293
- */
3294
- conn.on = function(event, handler) {
3295
- handlers[event] = handler;
3296
- return conn;
3297
- };
3298
-
3299
- /**
3300
- * Close the connection.
3301
- */
3302
- conn.close = function() {
3303
- if (conn._es) {
3304
- conn._es.close();
3305
- conn._es = null;
3306
- }
3307
- if (conn._pollTimer) {
3308
- clearInterval(conn._pollTimer);
3309
- conn._pollTimer = null;
3310
- }
3311
- setStatus('disconnected');
3312
- if (handlers.close) handlers.close();
3313
- };
3314
-
3315
- return conn;
3316
- };
3317
2050
 
3318
2051
  // ===================================================================================
3319
2052
  // bw.inspect() — Debug utility
3320
2053
  // ===================================================================================
3321
2054
 
3322
2055
  /**
3323
- * Inspect a component's state, bindings, methods, and metadata.
3324
- * Works with DOM elements, CSS selectors, or ComponentHandle objects.
3325
- * Returns the ComponentHandle for console chaining.
2056
+ * Inspect a DOM element's bitwrench state, handle methods, and metadata.
2057
+ * Works with DOM elements or CSS selectors.
3326
2058
  *
3327
- * @param {string|Element|ComponentHandle} target - Selector, element, or handle
3328
- * @returns {ComponentHandle|null} The component handle, or null if not found
2059
+ * @param {string|Element} target - Selector or DOM element
2060
+ * @returns {Element|null} The element, or null if not found
3329
2061
  * @category Component
3330
2062
  * @example
3331
- * // In browser console, click element in Elements panel then:
2063
+ * bw.inspect('#my-carousel');
3332
2064
  * bw.inspect($0);
3333
- * // Or by selector:
3334
- * var h = bw.inspect('#my-dashboard');
3335
- * h.set('count', 99); // chain from returned handle
3336
2065
  */
3337
2066
  bw.inspect = function(target) {
3338
- var el = target;
3339
- var comp;
3340
- if (target && target._bwComponent === true) {
3341
- el = target.element;
3342
- comp = target;
3343
- } else {
3344
- if (_is(target, 'string')) {
3345
- el = bw.$(target)[0];
3346
- }
3347
- if (!el) {
3348
- _cw('bw.inspect: element not found');
3349
- return null;
3350
- }
3351
- comp = el._bwComponentHandle;
3352
- }
3353
- if (!comp) {
3354
- _cl('bw.inspect: no ComponentHandle on this element');
3355
- _cl(' Tag:', el.tagName);
3356
- _cl(' Classes:', el.className);
3357
- _cl(' _bw_state:', el._bw_state || '(none)');
3358
- return null;
3359
- }
3360
- var deps = comp._bindings.reduce(function(s, b) {
3361
- return s.concat(b.deps || []);
3362
- }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
3363
- console.group('Component: ' + comp._bwId);
3364
- _cl('State:', comp._state);
3365
- _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
3366
- _cl('Methods:', _keys(comp._methods));
3367
- _cl('Actions:', _keys(comp._actions));
3368
- _cl('User tag:', comp._userTag || '(none)');
3369
- _cl('Mounted:', comp.mounted);
3370
- _cl('Element:', comp.element);
2067
+ var el = _is(target, 'string') ? bw.$(target)[0] : target;
2068
+ if (!el) { _cw('bw.inspect: element not found'); return null; }
2069
+ console.group('Element: ' + (bw.getUUID(el) || el.id || el.tagName));
2070
+ _cl('State:', el._bw_state || '(none)');
2071
+ _cl('Handle:', el.bw ? _keys(el.bw) : '(none)');
2072
+ _cl('Classes:', el.className);
2073
+ _cl('Refs:', el._bw_refs || '(none)');
3371
2074
  console.groupEnd();
3372
- return comp;
2075
+ return el;
3373
2076
  };
3374
2077
 
3375
- // ===================================================================================
3376
- // bw.compile() — Pre-compile TACO into optimized factory
3377
- // ===================================================================================
3378
-
3379
- /**
3380
- * Pre-compile a TACO definition into a factory function.
3381
- * The factory produces ComponentHandles with pre-compiled binding evaluators.
3382
- *
3383
- * Phase 1: validates API surface. Template cloning optimization deferred.
3384
- *
3385
- * @param {Object} taco - TACO definition
3386
- * @returns {Function} Factory function(initialState?) → ComponentHandle
3387
- * @category Component
3388
- */
3389
- bw.compile = function(taco) {
3390
- // Pre-extract all binding expressions
3391
- var precompiled = [];
3392
- function walkExpressions(node) {
3393
- if (!_is(node, 'object')) return;
3394
- if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
3395
- var parsed = bw._parseBindings(node.c);
3396
- for (var i = 0; i < parsed.length; i++) {
3397
- try {
3398
- precompiled.push({
3399
- expr: parsed[i].expr,
3400
- fn: new Function('state', 'with(state){return (' + parsed[i].expr + ');}')
3401
- });
3402
- } catch(e) {
3403
- precompiled.push({ expr: parsed[i].expr, fn: function() { return ''; } });
3404
- }
3405
- }
3406
- }
3407
- if (node.a) {
3408
- for (var key in node.a) {
3409
- if (_hop.call(node.a, key)) {
3410
- var v = node.a[key];
3411
- if (_is(v, 'string') && v.indexOf('${') >= 0) {
3412
- var parsed2 = bw._parseBindings(v);
3413
- for (var j = 0; j < parsed2.length; j++) {
3414
- try {
3415
- precompiled.push({
3416
- expr: parsed2[j].expr,
3417
- fn: new Function('state', 'with(state){return (' + parsed2[j].expr + ');}')
3418
- });
3419
- } catch(e2) {
3420
- precompiled.push({ expr: parsed2[j].expr, fn: function() { return ''; } });
3421
- }
3422
- }
3423
- }
3424
- }
3425
- }
3426
- }
3427
- if (_isA(node.c)) {
3428
- for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
3429
- } else if (_is(node.c, 'object') && node.c.t) {
3430
- walkExpressions(node.c);
3431
- }
3432
- }
3433
- walkExpressions(taco);
3434
-
3435
- return function(initialState) {
3436
- var handle = new ComponentHandle(taco);
3437
- handle._compile = true;
3438
- handle._precompiledBindings = precompiled;
3439
- if (initialState) {
3440
- for (var k in initialState) {
3441
- if (_hop.call(initialState, k)) {
3442
- handle._state[k] = initialState[k];
3443
- }
3444
- }
3445
- }
3446
- return handle;
3447
- };
3448
- };
2078
+ bw.compile = function() { throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
3449
2079
 
3450
2080
  /**
3451
2081
  * Generate CSS from JavaScript objects.
@@ -3522,7 +2152,7 @@ bw.css = function(rules, options = {}) {
3522
2152
  * @returns {Element} The style element
3523
2153
  * @category CSS & Styling
3524
2154
  * @see bw.css
3525
- * @see bw.loadDefaultStyles
2155
+ * @see bw.loadStyles
3526
2156
  * @example
3527
2157
  * bw.injectCSS('.my-class { color: red; }');
3528
2158
  * bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
@@ -3567,9 +2197,8 @@ bw.injectCSS = function(css, options = {}) {
3567
2197
  * @param {...Object} styles - Style objects to merge (left-to-right)
3568
2198
  * @returns {Object} Merged style object
3569
2199
  * @category CSS & Styling
3570
- * @see bw.u
3571
2200
  * @example
3572
- * var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
2201
+ * var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
3573
2202
  * // => { display: 'flex', gap: '1rem', color: 'red' }
3574
2203
  */
3575
2204
  bw.s = function() {
@@ -3581,99 +2210,6 @@ bw.s = function() {
3581
2210
  return result;
3582
2211
  };
3583
2212
 
3584
- /**
3585
- * Pre-built CSS utility objects (like Tailwind utilities, but in JS).
3586
- *
3587
- * Compose with `bw.s()` to build inline styles without writing raw CSS strings.
3588
- * Includes flex, padding, margin, typography, color, border, and transition utilities.
3589
- *
3590
- * @category CSS & Styling
3591
- * @see bw.s
3592
- * @example
3593
- * { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
3594
- * c: 'Flexbox with 1rem gap and padding' }
3595
- */
3596
- bw.u = {
3597
- // Display
3598
- flex: { display: 'flex' },
3599
- flexCol: { display: 'flex', flexDirection: 'column' },
3600
- flexRow: { display: 'flex', flexDirection: 'row' },
3601
- flexWrap: { display: 'flex', flexWrap: 'wrap' },
3602
- block: { display: 'block' },
3603
- inline: { display: 'inline' },
3604
- hidden: { display: 'none' },
3605
-
3606
- // Flex alignment
3607
- justifyCenter: { justifyContent: 'center' },
3608
- justifyBetween: { justifyContent: 'space-between' },
3609
- justifyEnd: { justifyContent: 'flex-end' },
3610
- alignCenter: { alignItems: 'center' },
3611
- alignStart: { alignItems: 'flex-start' },
3612
- alignEnd: { alignItems: 'flex-end' },
3613
-
3614
- // Gap (0.25rem increments)
3615
- gap1: { gap: '0.25rem' },
3616
- gap2: { gap: '0.5rem' },
3617
- gap3: { gap: '0.75rem' },
3618
- gap4: { gap: '1rem' },
3619
- gap6: { gap: '1.5rem' },
3620
- gap8: { gap: '2rem' },
3621
-
3622
- // Padding
3623
- p0: { padding: '0' },
3624
- p1: { padding: '0.25rem' },
3625
- p2: { padding: '0.5rem' },
3626
- p3: { padding: '0.75rem' },
3627
- p4: { padding: '1rem' },
3628
- p6: { padding: '1.5rem' },
3629
- p8: { padding: '2rem' },
3630
- px4: { paddingLeft: '1rem', paddingRight: '1rem' },
3631
- py2: { paddingTop: '0.5rem', paddingBottom: '0.5rem' },
3632
- py4: { paddingTop: '1rem', paddingBottom: '1rem' },
3633
-
3634
- // Margin (same scale)
3635
- m0: { margin: '0' },
3636
- m4: { margin: '1rem' },
3637
- mt2: { marginTop: '0.5rem' },
3638
- mt4: { marginTop: '1rem' },
3639
- mb2: { marginBottom: '0.5rem' },
3640
- mb4: { marginBottom: '1rem' },
3641
- mx_auto: { marginLeft: 'auto', marginRight: 'auto' },
3642
-
3643
- // Typography
3644
- textSm: { fontSize: '0.875rem' },
3645
- textBase: { fontSize: '1rem' },
3646
- textLg: { fontSize: '1.125rem' },
3647
- textXl: { fontSize: '1.25rem' },
3648
- text2xl: { fontSize: '1.5rem' },
3649
- text3xl: { fontSize: '1.875rem' },
3650
- bold: { fontWeight: '700' },
3651
- semibold: { fontWeight: '600' },
3652
- italic: { fontStyle: 'italic' },
3653
- textCenter: { textAlign: 'center' },
3654
- textRight: { textAlign: 'right' },
3655
-
3656
- // Colors (from design tokens)
3657
- bgWhite: { background: '#ffffff' },
3658
- bgTeal: { background: '#006666', color: '#ffffff' },
3659
- textWhite: { color: '#ffffff' },
3660
- textTeal: { color: '#006666' },
3661
- textMuted: { color: '#888' },
3662
-
3663
- // Borders
3664
- rounded: { borderRadius: '0.375rem' },
3665
- roundedLg: { borderRadius: '0.5rem' },
3666
- roundedFull: { borderRadius: '9999px' },
3667
- border: { border: '1px solid #d8d8d8' },
3668
-
3669
- // Sizing
3670
- wFull: { width: '100%' },
3671
- hFull: { height: '100%' },
3672
-
3673
- // Transitions
3674
- transition: { transition: 'all 0.2s ease' }
3675
- };
3676
-
3677
2213
  /**
3678
2214
  * Generate responsive CSS with media query breakpoints.
3679
2215
  *
@@ -3795,103 +2331,49 @@ if (bw._isBrowser) {
3795
2331
  };
3796
2332
  }
3797
2333
 
3798
- /**
3799
- * Load the built-in Bootstrap-inspired default stylesheet.
3800
- *
3801
- * Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
3802
- * alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
3803
- * Returns null in Node.js (no DOM).
3804
- *
3805
- * @param {Object} [options] - Style loading options
3806
- * @param {boolean} [options.minify=true] - Minify the CSS output
3807
- * @returns {Element|null} Style element if in browser, null in Node.js
3808
- * @category CSS & Styling
3809
- * @see bw.setTheme
3810
- * @see bw.applyTheme
3811
- * @see bw.toggleTheme
3812
- * @example
3813
- * bw.loadDefaultStyles(); // inject all default CSS
3814
- */
3815
- bw.loadDefaultStyles = function(options = {}) {
3816
- const { minify = true, palette } = options;
3817
-
3818
- // 1. Inject structural CSS (layout, sizing — never changes with theme)
3819
- if (bw._isBrowser) {
3820
- var structuralCSS = bw.css(getStructuralStyles());
3821
- bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
3822
- }
3823
2334
 
3824
- // 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
3825
- var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
3826
- var result = bw.generateTheme('', Object.assign({}, paletteConfig, { inject: true }));
3827
- return result;
3828
- };
2335
+ // =========================================================================
2336
+ // v2.0.18 Clean Styles API makeStyles / applyStyles / loadStyles / etc.
2337
+ // =========================================================================
3829
2338
 
2339
+ /**
2340
+ * Convert a scope selector to a <style> element id.
2341
+ * @private
2342
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
2343
+ * @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
2344
+ */
2345
+ function _scopeToStyleId(scope) {
2346
+ if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
2347
+ if (scope === 'reset') return 'bw_style_reset';
2348
+ // Strip leading # or . and convert - to _
2349
+ var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
2350
+ return 'bw_style_' + clean;
2351
+ }
3830
2352
 
3831
2353
  /**
3832
- * Generate a complete, scoped theme from seed colors.
2354
+ * Generate a complete styles object from seed colors and layout config.
2355
+ * Pure function — no DOM, no state, no side effects.
3833
2356
  *
3834
- * Produces CSS for all themed components (buttons, alerts, badges, cards,
3835
- * forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
3836
- * scoped under `.name` class. Multiple themes can coexist in the stylesheet.
3837
- * Swap themes by changing the class on a container element.
2357
+ * All parameters are optional. Defaults to the bitwrench default palette.
3838
2358
  *
3839
- * @param {string} name - CSS scope class (e.g. 'ocean'). Empty string = unscoped global.
3840
- * @param {Object} config - Theme configuration
3841
- * @param {string} config.primary - Primary brand color hex
3842
- * @param {string} config.secondary - Secondary color hex
3843
- * @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
3844
- * @param {string} [config.success='#198754'] - Success color hex
3845
- * @param {string} [config.danger='#dc3545'] - Danger color hex
3846
- * @param {string} [config.warning='#ffc107'] - Warning color hex
3847
- * @param {string} [config.info='#0dcaf0'] - Info color hex
3848
- * @param {string} [config.light='#f8f9fa'] - Light color hex
3849
- * @param {string} [config.dark='#212529'] - Dark color hex
3850
- * @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
3851
- * @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
2359
+ * @param {Object} [config] - Style configuration
2360
+ * @param {string} [config.primary='#006666'] - Primary brand color hex
2361
+ * @param {string} [config.secondary='#6c757d'] - Secondary color hex
2362
+ * @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
3852
2363
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
3853
2364
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
3854
- * @param {number} [config.fontSize=1.0] - Base font size scale factor
3855
- * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
3856
- * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
3857
- * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
3858
- * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
3859
- * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
3860
- * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
2365
+ * @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
3861
2366
  * @category CSS & Styling
3862
- * @see bw.applyTheme
3863
- * @see bw.toggleTheme
3864
- * @see bw.loadDefaultStyles
2367
+ * @see bw.applyStyles
2368
+ * @see bw.loadStyles
3865
2369
  * @example
3866
- * // Generate and inject an ocean theme (primary + alternate)
3867
- * var theme = bw.generateTheme('ocean', {
3868
- * primary: '#0077b6',
3869
- * secondary: '#90e0ef',
3870
- * tertiary: '#00b4d8'
3871
- * });
3872
- *
3873
- * // Apply to a container
3874
- * document.getElementById('app').classList.add('ocean');
3875
- *
3876
- * // Toggle to alternate palette
3877
- * bw.toggleTheme();
3878
- *
3879
- * // Generate CSS for static export (Node.js)
3880
- * var result = bw.generateTheme('sunset', {
3881
- * primary: '#e76f51',
3882
- * secondary: '#264653',
3883
- * inject: false
3884
- * });
3885
- * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
2370
+ * var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
2371
+ * console.log(styles.palette.primary.base); // '#4f46e5'
2372
+ * // styles.css contains all themed CSS — nothing injected
3886
2373
  */
3887
- bw.generateTheme = function(name, config) {
3888
- if (!config || !config.primary || !config.secondary) {
3889
- throw new Error('bw.generateTheme requires config.primary and config.secondary');
3890
- }
3891
-
3892
- // Merge with defaults; if user didn't supply tertiary, default to their primary
3893
- var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
3894
- if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
2374
+ bw.makeStyles = function(config) {
2375
+ var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
2376
+ if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
3895
2377
 
3896
2378
  // Derive primary palette
3897
2379
  var palette = derivePalette(fullConfig);
@@ -3899,131 +2381,207 @@ bw.generateTheme = function(name, config) {
3899
2381
  // Resolve layout
3900
2382
  var layout = resolveLayout(fullConfig);
3901
2383
 
3902
- // Generate primary themed CSS rules
3903
- var themedRules = generateThemedCSS(name, palette, layout);
2384
+ // Generate primary themed CSS rules (unscoped)
2385
+ var themedRules = generateThemedCSS('', palette, layout);
3904
2386
  var cssStr = bw.css(themedRules);
3905
2387
 
3906
2388
  // Derive alternate palette (luminance-inverted)
3907
2389
  var altConfig = deriveAlternateConfig(fullConfig);
3908
2390
  var altPalette = derivePalette(altConfig);
3909
2391
 
3910
- // Generate alternate CSS scoped under .bw_theme_alt
3911
- var altRules = generateAlternateCSS(name, altPalette, layout);
3912
- var altCssStr = bw.css(altRules);
2392
+ // Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
2393
+ // applyStyles() wraps them appropriately based on scope
2394
+ var altRawRules = generateThemedCSS('', altPalette, layout);
2395
+
2396
+ // Add body-level surface overrides for the alternate palette.
2397
+ // When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
2398
+ altRawRules['body'] = {
2399
+ 'color': altPalette.dark.base,
2400
+ 'background-color': altPalette.surface || altPalette.light.base
2401
+ };
2402
+
2403
+ var altCssStr = bw.css(altRawRules);
3913
2404
 
3914
2405
  // Determine if primary is light-flavored
3915
2406
  var lightPrimary = isLightPalette(fullConfig);
3916
2407
 
3917
- // Inject both CSS sets into DOM if requested
3918
- var shouldInject = config.inject !== false;
3919
- if (shouldInject && bw._isBrowser) {
3920
- var safeName = name ? name.replace(/-/g, '_') : '';
3921
- var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
3922
- var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
3923
-
3924
- bw.injectCSS(cssStr, { id: styleId, append: false });
3925
- bw.injectCSS(altCssStr, { id: altStyleId, append: false });
2408
+ return {
2409
+ css: cssStr,
2410
+ alternateCss: altCssStr,
2411
+ rules: themedRules,
2412
+ alternateRules: altRawRules,
2413
+ palette: palette,
2414
+ alternatePalette: altPalette,
2415
+ isLightPrimary: lightPrimary
2416
+ };
2417
+ };
3926
2418
 
3927
- bw._activeThemeStyleIds = [styleId, altStyleId];
2419
+ /**
2420
+ * Inject styles into the DOM with optional scoping.
2421
+ *
2422
+ * Takes a styles object from `makeStyles()` and creates a single `<style>`
2423
+ * element in `<head>`. If a scope selector is provided, all CSS rules are
2424
+ * wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
2425
+ *
2426
+ * @param {Object} styles - Result of `bw.makeStyles()`
2427
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
2428
+ * @returns {Element|null} The `<style>` element, or null in Node.js
2429
+ * @category CSS & Styling
2430
+ * @see bw.makeStyles
2431
+ * @see bw.loadStyles
2432
+ * @see bw.clearStyles
2433
+ * @example
2434
+ * var styles = bw.makeStyles({ primary: '#4f46e5' });
2435
+ * bw.applyStyles(styles); // global
2436
+ * bw.applyStyles(styles, '#my-dashboard'); // scoped
2437
+ */
2438
+ bw.applyStyles = function(styles, scope) {
2439
+ if (!bw._isBrowser) return null;
2440
+ if (!styles || !styles.rules) {
2441
+ _cw('bw.applyStyles: invalid styles object');
2442
+ return null;
3928
2443
  }
3929
2444
 
3930
- // Update bw.u color entries to reflect the palette
3931
- if (!name) {
3932
- bw.u.bgTeal = { background: palette.primary.base, color: palette.primary.textOn };
3933
- bw.u.textTeal = { color: palette.primary.base };
3934
- bw.u.bgWhite = { background: '#ffffff' };
3935
- bw.u.textWhite = { color: '#ffffff' };
2445
+ var styleId = _scopeToStyleId(scope);
2446
+
2447
+ // Scope the primary rules if a scope is provided
2448
+ var primaryRules = styles.rules;
2449
+ if (scope) {
2450
+ primaryRules = scopeRulesUnder(primaryRules, scope);
3936
2451
  }
3937
2452
 
3938
- // Store active theme state
3939
- var result = {
3940
- css: cssStr,
3941
- palette: palette,
3942
- name: name,
3943
- isLightPrimary: lightPrimary,
3944
- alternate: {
3945
- css: altCssStr,
3946
- palette: altPalette
2453
+ // Wrap alternate rules with .bw_theme_alt
2454
+ var altRules = styles.alternateRules;
2455
+ if (altRules) {
2456
+ if (scope) {
2457
+ // Scoped compound: #scope.bw_theme_alt .bw_card
2458
+ altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
2459
+ } else {
2460
+ // Global: .bw_theme_alt .bw_card
2461
+ altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
3947
2462
  }
3948
- };
3949
- bw._activeTheme = result;
3950
- bw._activeThemeMode = 'primary';
2463
+ }
3951
2464
 
3952
- return result;
2465
+ // Combine primary + alternate into one CSS string
2466
+ var combined = bw.css(primaryRules);
2467
+ if (altRules) {
2468
+ combined += '\n' + bw.css(altRules);
2469
+ }
2470
+
2471
+ return bw.injectCSS(combined, { id: styleId, append: false });
3953
2472
  };
3954
2473
 
3955
2474
  /**
3956
- * Apply a theme mode. Switches between primary and alternate palettes
3957
- * by adding/removing the `bw_theme_alt` class on `<html>`.
2475
+ * Generate and apply styles in one call. Convenience wrapper.
2476
+ *
2477
+ * Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
3958
2478
  *
3959
- * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
3960
- * @returns {string} Active mode: 'primary' or 'alternate'
2479
+ * @param {Object} [config] - Style configuration (same as `makeStyles`)
2480
+ * @param {string} [scope] - Scope selector (same as `applyStyles`)
2481
+ * @returns {Element|null} The `<style>` element, or null in Node.js
3961
2482
  * @category CSS & Styling
3962
- * @see bw.generateTheme
3963
- * @see bw.toggleTheme
2483
+ * @see bw.makeStyles
2484
+ * @see bw.applyStyles
3964
2485
  * @example
3965
- * bw.applyTheme('alternate'); // switch to alternate palette
3966
- * bw.applyTheme('dark'); // switch to whichever palette is darker
3967
- * bw.applyTheme('primary'); // switch back to primary palette
2486
+ * bw.loadStyles(); // defaults, global
2487
+ * bw.loadStyles({ primary: '#4f46e5' }); // custom, global
2488
+ * bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
3968
2489
  */
3969
- bw.applyTheme = function(mode) {
3970
- if (!bw._isBrowser) return mode || 'primary';
3971
- var root = document.documentElement;
3972
- var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
3973
-
3974
- var wantAlt;
3975
- if (mode === 'primary') wantAlt = false;
3976
- else if (mode === 'alternate') wantAlt = true;
3977
- else if (mode === 'light') wantAlt = !isLight;
3978
- else if (mode === 'dark') wantAlt = isLight;
3979
- else wantAlt = false;
3980
-
3981
- if (wantAlt) {
3982
- root.classList.add('bw_theme_alt');
3983
- } else {
3984
- root.classList.remove('bw_theme_alt');
2490
+ bw.loadStyles = function(config, scope) {
2491
+ // Also inject structural CSS first (only once)
2492
+ if (bw._isBrowser) {
2493
+ var existing = document.getElementById('bw_structural');
2494
+ if (!existing) {
2495
+ var structuralCSS = bw.css(getStructuralStyles());
2496
+ bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
2497
+ }
3985
2498
  }
2499
+ return bw.applyStyles(bw.makeStyles(config), scope);
2500
+ };
3986
2501
 
3987
- bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
3988
- return bw._activeThemeMode;
2502
+ /**
2503
+ * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
2504
+ * Idempotent — if already injected, returns the existing `<style>` element.
2505
+ *
2506
+ * @returns {Element|null} The `<style>` element, or null in Node.js
2507
+ * @category CSS & Styling
2508
+ * @see bw.loadStyles
2509
+ * @see bw.clearStyles
2510
+ * @example
2511
+ * bw.loadReset(); // inject once, safe to call multiple times
2512
+ */
2513
+ bw.loadReset = function() {
2514
+ if (!bw._isBrowser) return null;
2515
+ var existing = document.getElementById('bw_style_reset');
2516
+ if (existing) return existing;
2517
+ return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
3989
2518
  };
3990
2519
 
3991
2520
  /**
3992
- * Toggle between primary and alternate theme palettes.
2521
+ * Toggle between primary and alternate palettes.
3993
2522
  *
2523
+ * Adds/removes the `bw_theme_alt` class on the scoping element.
2524
+ * Without a scope, toggles on `<html>` (global).
2525
+ * With a scope, toggles on the first matching element.
2526
+ *
2527
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
3994
2528
  * @returns {string} Active mode after toggle: 'primary' or 'alternate'
3995
2529
  * @category CSS & Styling
3996
- * @see bw.applyTheme
3997
- * @see bw.generateTheme
2530
+ * @see bw.applyStyles
2531
+ * @see bw.clearStyles
3998
2532
  * @example
3999
- * bw.toggleTheme(); // flip between primary and alternate
4000
- */
4001
- bw.toggleTheme = function() {
4002
- var current = bw._activeThemeMode || 'primary';
4003
- return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
2533
+ * bw.toggleStyles(); // global toggle on <html>
2534
+ * bw.toggleStyles('#my-dashboard'); // scoped toggle
2535
+ */
2536
+ bw.toggleStyles = function(scope) {
2537
+ if (!bw._isBrowser) return 'primary';
2538
+ var target;
2539
+ if (scope) {
2540
+ var els = bw.$(scope);
2541
+ target = els[0];
2542
+ } else {
2543
+ target = document.documentElement;
2544
+ }
2545
+ if (!target) return 'primary';
2546
+
2547
+ var hasAlt = target.classList.contains('bw_theme_alt');
2548
+ if (hasAlt) {
2549
+ target.classList.remove('bw_theme_alt');
2550
+ return 'primary';
2551
+ } else {
2552
+ target.classList.add('bw_theme_alt');
2553
+ return 'alternate';
2554
+ }
4004
2555
  };
4005
2556
 
4006
2557
  /**
4007
- * Remove the currently active theme's injected style elements from the DOM.
4008
- * Use this before generating a new theme with a different name to prevent
4009
- * stale CSS accumulation.
2558
+ * Remove injected styles for a given scope.
2559
+ *
2560
+ * Finds the `<style>` element by id and removes it. Also removes
2561
+ * the `bw_theme_alt` class from the relevant element.
4010
2562
  *
2563
+ * @param {string} [scope] - Scope selector. Omit to remove global styles.
4011
2564
  * @category CSS & Styling
4012
- * @see bw.generateTheme
2565
+ * @see bw.applyStyles
2566
+ * @see bw.loadStyles
4013
2567
  * @example
4014
- * bw.clearTheme(); // remove current theme styles
4015
- * bw.generateTheme('sunset', conf); // inject fresh theme
2568
+ * bw.clearStyles(); // remove global styles
2569
+ * bw.clearStyles('#my-dashboard'); // remove scoped styles
2570
+ * bw.clearStyles('reset'); // remove the CSS reset
4016
2571
  */
4017
- bw.clearTheme = function() {
4018
- if (bw._activeThemeStyleIds && bw._isBrowser) {
4019
- bw._activeThemeStyleIds.forEach(function(id) {
4020
- var el = document.getElementById(id);
4021
- if (el) el.remove();
4022
- });
4023
- bw._activeThemeStyleIds = null;
2572
+ bw.clearStyles = function(scope) {
2573
+ if (!bw._isBrowser) return;
2574
+ var styleId = _scopeToStyleId(scope);
2575
+ var el = document.getElementById(styleId);
2576
+ if (el) el.remove();
2577
+
2578
+ // Also remove bw_theme_alt from the relevant element
2579
+ if (scope && scope !== 'reset' && scope !== 'global') {
2580
+ var targets = bw.$(scope);
2581
+ if (targets[0]) targets[0].classList.remove('bw_theme_alt');
2582
+ } else if (!scope || scope === 'global') {
2583
+ document.documentElement.classList.remove('bw_theme_alt');
4024
2584
  }
4025
- bw._activeTheme = null;
4026
- bw._activeThemeMode = 'primary';
4027
2585
  };
4028
2586
 
4029
2587
  // Expose color utility functions on bw namespace
@@ -4246,10 +2804,15 @@ bw.copyToClipboard = function(text) {
4246
2804
  * @param {Object} config - Table configuration
4247
2805
  * @param {Array<Object>} config.data - Array of row objects to display
4248
2806
  * @param {Array<Object>} [config.columns] - Column definitions with key, label, render
4249
- * @param {string} [config.className='table'] - CSS class for table element
2807
+ * @param {string} [config.className=''] - Additional CSS classes for table element
4250
2808
  * @param {boolean} [config.sortable=true] - Enable click-to-sort headers
4251
2809
  * @param {Function} [config.onSort] - Sort callback (column, direction)
4252
- * @returns {Object} TACO object for table
2810
+ * @param {boolean} [config.selectable=false] - Enable row selection on click
2811
+ * @param {Function} [config.onRowClick] - Row click callback (row, index, event)
2812
+ * @param {number} [config.pageSize] - Rows per page (enables pagination when set)
2813
+ * @param {number} [config.currentPage=1] - Current page number (1-based)
2814
+ * @param {Function} [config.onPageChange] - Page change callback (newPage)
2815
+ * @returns {Object} TACO object for table (with optional pagination controls)
4253
2816
  * @category Component Builders
4254
2817
  * @see bw.makeDataTable
4255
2818
  * @example
@@ -4261,7 +2824,12 @@ bw.copyToClipboard = function(text) {
4261
2824
  * columns: [
4262
2825
  * { key: 'name', label: 'Name' },
4263
2826
  * { key: 'age', label: 'Age' }
4264
- * ]
2827
+ * ],
2828
+ * selectable: true,
2829
+ * onRowClick: function(row, i) { console.log('clicked', row.name); },
2830
+ * pageSize: 10,
2831
+ * currentPage: 1,
2832
+ * onPageChange: function(page) { console.log('page', page); }
4265
2833
  * });
4266
2834
  */
4267
2835
  bw.makeTable = function(config) {
@@ -4274,41 +2842,47 @@ bw.makeTable = function(config) {
4274
2842
  sortable = true,
4275
2843
  onSort,
4276
2844
  sortColumn,
4277
- sortDirection = 'asc'
2845
+ sortDirection = 'asc',
2846
+ selectable = false,
2847
+ onRowClick,
2848
+ pageSize,
2849
+ currentPage = 1,
2850
+ onPageChange
4278
2851
  } = config;
4279
2852
 
4280
- // Build class list: always include bw_table, add striped/hover, append user className
2853
+ // Build class list: always include bw_table, add striped/hover/selectable, append user className
4281
2854
  let cls = 'bw_table';
4282
2855
  if (striped) cls += ' bw_table_striped';
4283
- if (hover) cls += ' bw_table_hover';
2856
+ if (hover || selectable) cls += ' bw_table_hover';
2857
+ if (selectable) cls += ' bw_table_selectable';
4284
2858
  if (className) cls += ' ' + className;
4285
2859
  cls = cls.trim();
4286
-
2860
+
4287
2861
  // Auto-detect columns if not provided
4288
- const cols = columns || (data.length > 0
2862
+ const cols = columns || (data.length > 0
4289
2863
  ? _keys(data[0]).map(key => ({ key, label: key }))
4290
2864
  : []);
4291
-
2865
+
4292
2866
  // Current sort state
4293
2867
  let currentSortColumn = sortColumn || null;
4294
2868
  let currentSortDirection = sortDirection;
4295
-
2869
+
4296
2870
  // Sort data if column specified
4297
2871
  let sortedData = [...data];
4298
2872
  if (currentSortColumn) {
4299
2873
  sortedData.sort((a, b) => {
4300
2874
  const aVal = a[currentSortColumn];
4301
2875
  const bVal = b[currentSortColumn];
4302
-
2876
+
4303
2877
  // Handle different types
4304
2878
  if (_is(aVal, 'number') && _is(bVal, 'number')) {
4305
2879
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
4306
2880
  }
4307
-
2881
+
4308
2882
  // String comparison
4309
2883
  const aStr = String(aVal || '').toLowerCase();
4310
2884
  const bStr = String(bVal || '').toLowerCase();
4311
-
2885
+
4312
2886
  if (currentSortDirection === 'asc') {
4313
2887
  return aStr.localeCompare(bStr);
4314
2888
  } else {
@@ -4316,23 +2890,32 @@ bw.makeTable = function(config) {
4316
2890
  }
4317
2891
  });
4318
2892
  }
4319
-
2893
+
2894
+ // Pagination
2895
+ const totalRows = sortedData.length;
2896
+ const totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
2897
+ const page = Math.max(1, Math.min(currentPage, totalPages));
2898
+ if (pageSize) {
2899
+ const start = (page - 1) * pageSize;
2900
+ sortedData = sortedData.slice(start, start + pageSize);
2901
+ }
2902
+
4320
2903
  // Create sort handler
4321
2904
  const handleSort = (column) => {
4322
2905
  if (!sortable) return;
4323
-
2906
+
4324
2907
  if (currentSortColumn === column) {
4325
2908
  currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
4326
2909
  } else {
4327
2910
  currentSortColumn = column;
4328
2911
  currentSortDirection = 'asc';
4329
2912
  }
4330
-
2913
+
4331
2914
  if (onSort) {
4332
2915
  onSort(column, currentSortDirection);
4333
2916
  }
4334
2917
  };
4335
-
2918
+
4336
2919
  // Build table header
4337
2920
  const thead = {
4338
2921
  t: 'thead',
@@ -4355,24 +2938,87 @@ bw.makeTable = function(config) {
4355
2938
  }))
4356
2939
  }
4357
2940
  };
4358
-
4359
- // Build table body
2941
+
2942
+ // Build table body with selectable/onRowClick support
4360
2943
  const tbody = {
4361
2944
  t: 'tbody',
4362
- c: sortedData.map(row => ({
4363
- t: 'tr',
4364
- c: cols.map(col => ({
4365
- t: 'td',
4366
- c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
4367
- }))
4368
- }))
2945
+ c: sortedData.map((row, idx) => {
2946
+ const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
2947
+ const rowAttrs = {};
2948
+ if (selectable || onRowClick) {
2949
+ rowAttrs.style = 'cursor:pointer;';
2950
+ rowAttrs.onclick = function(e) {
2951
+ if (selectable) {
2952
+ // Toggle selected class on this row
2953
+ var tr = e.currentTarget;
2954
+ tr.classList.toggle('bw_table_row_selected');
2955
+ }
2956
+ if (onRowClick) {
2957
+ onRowClick(row, globalIdx, e);
2958
+ }
2959
+ };
2960
+ }
2961
+ return {
2962
+ t: 'tr',
2963
+ a: rowAttrs,
2964
+ c: cols.map(col => ({
2965
+ t: 'td',
2966
+ c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
2967
+ }))
2968
+ };
2969
+ })
4369
2970
  };
4370
-
4371
- return {
2971
+
2972
+ const table = {
4372
2973
  t: 'table',
4373
2974
  a: { class: cls },
4374
2975
  c: [thead, tbody]
4375
2976
  };
2977
+
2978
+ // If no pagination, return table directly
2979
+ if (!pageSize) return table;
2980
+
2981
+ // Build pagination controls
2982
+ const pageButtons = [];
2983
+ // Previous button
2984
+ pageButtons.push({
2985
+ t: 'button',
2986
+ a: {
2987
+ class: 'bw_btn bw_btn_sm',
2988
+ disabled: page <= 1 ? 'disabled' : undefined,
2989
+ onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
2990
+ },
2991
+ c: 'Prev'
2992
+ });
2993
+ // Page info
2994
+ pageButtons.push({
2995
+ t: 'span',
2996
+ a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
2997
+ c: 'Page ' + page + ' of ' + totalPages
2998
+ });
2999
+ // Next button
3000
+ pageButtons.push({
3001
+ t: 'button',
3002
+ a: {
3003
+ class: 'bw_btn bw_btn_sm',
3004
+ disabled: page >= totalPages ? 'disabled' : undefined,
3005
+ onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
3006
+ },
3007
+ c: 'Next'
3008
+ });
3009
+
3010
+ return {
3011
+ t: 'div',
3012
+ a: { class: 'bw_table_paginated' },
3013
+ c: [
3014
+ table,
3015
+ {
3016
+ t: 'div',
3017
+ a: { class: 'bw_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
3018
+ c: pageButtons
3019
+ }
3020
+ ]
3021
+ };
4376
3022
  };
4377
3023
 
4378
3024
  /**
@@ -4655,8 +3301,8 @@ bw.render = function(element, position, taco) {
4655
3301
  };
4656
3302
  }
4657
3303
 
4658
- // Generate unique ID if not provided
4659
- const componentId = taco.o?.id || bw.uuid();
3304
+ // Generate unique UUID class if not provided
3305
+ const componentId = taco.o?.id || bw.uuid('uuid');
4660
3306
 
4661
3307
  // Create DOM element
4662
3308
  let domElement;
@@ -4671,9 +3317,10 @@ bw.render = function(element, position, taco) {
4671
3317
  };
4672
3318
  }
4673
3319
 
4674
- // Add component ID to element
4675
- domElement.setAttribute('data-bw_id', componentId);
4676
-
3320
+ // Add component ID as class + lifecycle marker
3321
+ domElement.classList.add(componentId);
3322
+ domElement.classList.add(_BW_LC);
3323
+
4677
3324
  // Insert into DOM based on position
4678
3325
  try {
4679
3326
  switch(position) {
@@ -4747,7 +3394,8 @@ bw.render = function(element, position, taco) {
4747
3394
 
4748
3395
  // Re-render
4749
3396
  const newElement = bw.createDOM(this._taco);
4750
- newElement.setAttribute('data-bw_id', componentId);
3397
+ newElement.classList.add(componentId);
3398
+ newElement.classList.add(_BW_LC);
4751
3399
 
4752
3400
  // Replace in DOM
4753
3401
  parent.replaceChild(newElement, this.element);
@@ -4939,13 +3587,12 @@ bw.BCCL = components.BCCL;
4939
3587
  // Variant class helper: bw.variantClass('primary') → 'bw_primary'
4940
3588
  bw.variantClass = components.variantClass;
4941
3589
 
4942
- // Create functions that return handles (plain renderComponent, no Handle overlay)
3590
+ // Create functions that return DOM elements (createCard, createTable, etc.)
4943
3591
  Object.entries(components).forEach(([name, fn]) => {
4944
3592
  if (name.startsWith('make')) {
4945
- const createName = 'create' + name.substring(4); // createCard, createTable, etc.
3593
+ const createName = 'create' + name.substring(4);
4946
3594
  bw[createName] = function(props) {
4947
- const taco = fn(props);
4948
- return bw.renderComponent(taco);
3595
+ return bw.createDOM(fn(props));
4949
3596
  };
4950
3597
  }
4951
3598
  });