bitwrench 2.0.15 → 2.0.17

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 (53) hide show
  1. package/README.md +57 -21
  2. package/dist/bitwrench-bccl.cjs.js +3750 -0
  3. package/dist/bitwrench-bccl.cjs.min.js +40 -0
  4. package/dist/bitwrench-bccl.esm.js +3745 -0
  5. package/dist/bitwrench-bccl.esm.min.js +40 -0
  6. package/dist/bitwrench-bccl.umd.js +3756 -0
  7. package/dist/bitwrench-bccl.umd.min.js +40 -0
  8. package/dist/bitwrench-code-edit.cjs.js +57 -7
  9. package/dist/bitwrench-code-edit.cjs.min.js +9 -2
  10. package/dist/bitwrench-code-edit.es5.js +74 -11
  11. package/dist/bitwrench-code-edit.es5.min.js +9 -2
  12. package/dist/bitwrench-code-edit.esm.js +57 -7
  13. package/dist/bitwrench-code-edit.esm.min.js +9 -2
  14. package/dist/bitwrench-code-edit.umd.js +57 -7
  15. package/dist/bitwrench-code-edit.umd.min.js +9 -2
  16. package/dist/bitwrench-lean.cjs.js +905 -157
  17. package/dist/bitwrench-lean.cjs.min.js +7 -7
  18. package/dist/bitwrench-lean.es5.js +931 -157
  19. package/dist/bitwrench-lean.es5.min.js +5 -5
  20. package/dist/bitwrench-lean.esm.js +904 -157
  21. package/dist/bitwrench-lean.esm.min.js +7 -7
  22. package/dist/bitwrench-lean.umd.js +905 -157
  23. package/dist/bitwrench-lean.umd.min.js +7 -7
  24. package/dist/bitwrench.cjs.js +910 -158
  25. package/dist/bitwrench.cjs.min.js +8 -8
  26. package/dist/bitwrench.css +60 -17
  27. package/dist/bitwrench.es5.js +939 -158
  28. package/dist/bitwrench.es5.min.js +6 -6
  29. package/dist/bitwrench.esm.js +909 -158
  30. package/dist/bitwrench.esm.min.js +8 -8
  31. package/dist/bitwrench.min.css +1 -1
  32. package/dist/bitwrench.umd.js +910 -158
  33. package/dist/bitwrench.umd.min.js +8 -8
  34. package/dist/builds.json +168 -80
  35. package/dist/bwserve.cjs.js +660 -0
  36. package/dist/bwserve.esm.js +652 -0
  37. package/dist/sri.json +36 -28
  38. package/package.json +20 -3
  39. package/readme.html +62 -23
  40. package/src/bitwrench-bccl-entry.js +72 -0
  41. package/src/bitwrench-bccl.js +5 -1
  42. package/src/bitwrench-code-edit.js +56 -6
  43. package/src/bitwrench-color-utils.js +5 -6
  44. package/src/bitwrench-styles.js +20 -8
  45. package/src/bitwrench.js +876 -140
  46. package/src/bwserve/client.js +182 -0
  47. package/src/bwserve/index.js +363 -0
  48. package/src/bwserve/shell.js +106 -0
  49. package/src/cli/index.js +36 -15
  50. package/src/cli/layout-default.js +47 -32
  51. package/src/cli/serve.js +325 -0
  52. package/src/version.js +3 -3
  53. /package/bin/{bitwrench.js → bwcli.js} +0 -0
package/src/bitwrench.js CHANGED
@@ -80,7 +80,7 @@ const bw = {
80
80
  __monkey_patch_is_nodejs__: {
81
81
  _value: 'ignore',
82
82
  set: function(x) {
83
- this._value = (typeof x === 'boolean') ? x : 'ignore';
83
+ this._value = _is(x, 'boolean') ? x : 'ignore';
84
84
  },
85
85
  get: function() {
86
86
  return this._value;
@@ -128,6 +128,67 @@ Object.defineProperty(bw, '_isBrowser', {
128
128
  configurable: true
129
129
  });
130
130
 
131
+ // ── Internal aliases ─────────────────────────────────────────────────────
132
+ // Short names for frequently-used builtins and internal methods.
133
+ // Same pattern as v1 (_to = bw.typeOf, etc.).
134
+ //
135
+ // Why: Terser can't shorten global property chains (console.warn,
136
+ // Object.prototype.hasOwnProperty, Array.isArray, document.createElement)
137
+ // because it can't prove they're side-effect-free. We can, so we alias
138
+ // them here. Each alias saves bytes in the minified output, and the short
139
+ // names also reduce visual noise in the hot paths (binding pipeline,
140
+ // createDOM, etc.).
141
+ //
142
+ // Alias Target Sites
143
+ // ───────── ────────────────────────────────────── ─────
144
+ // _hop Object.prototype.hasOwnProperty 15
145
+ // _isA Array.isArray 25
146
+ // _keys Object.keys 7
147
+ // _to bw.typeOf (type string) 26
148
+ // _is type check boolean: _is(x,'string') ~50
149
+ // _cw console.warn 8
150
+ // _cl console.log 11
151
+ // _ce console.error 4
152
+ // _chp ComponentHandle.prototype 28 (defined after constructor)
153
+ //
154
+ // Note: document.createElement etc. are NOT aliased because they require
155
+ // `this === document` and .bind() would add overhead on every call.
156
+ // Console aliases use thin wrappers (not direct refs) so test monkey-
157
+ // patching of console.warn/log/error continues to work.
158
+ //
159
+ // `typeof x` for UNDECLARED globals (window, document, process, require,
160
+ // EventSource, navigator, Promise, __filename, import.meta) MUST stay as
161
+ // raw `typeof` — calling _to(x) when x doesn't exist throws ReferenceError.
162
+ //
163
+ // ── v1 functional type helpers (kept for reference, not currently used) ──
164
+ // _toa(x, type, trueVal, falseVal) — bw.typeAssign:
165
+ // returns trueVal if _to(x)===type, else falseVal.
166
+ // Replaces: (typeof x === 'string') ? A : B → _toa(x,'string',A,B)
167
+ // _toc(x, type, trueVal, falseVal) — bw.typeConvert:
168
+ // same as _toa but if trueVal/falseVal are functions, calls them with x.
169
+ // Replaces: typeof x === 'string' ? fn(x) : default → _toc(x,'string',fn,default)
170
+ // Uncomment if pattern frequency justifies them:
171
+ // var _toa = function(x, t, y, n) { return _to(x) === t ? y : n; };
172
+ // var _toc = function(x, t, y, n) { var r = _to(x)===t; return r ? (_to(y)==='function'?y(x):y) : (_to(n)==='function'?n(x):n); };
173
+ // ─────────────────────────────────────────────────────────────────────────
174
+ var _hop = Object.prototype.hasOwnProperty;
175
+ var _isA = Array.isArray;
176
+ var _keys = Object.keys;
177
+ var _to = _typeOf; // imported from bitwrench-utils.js
178
+ var _is = function(x, t) { var r = _to(x); return r === t || r.toLowerCase() === t; };
179
+ // Console aliases use thin wrappers (not direct references) so that test
180
+ // code can monkey-patch console.warn/log/error and the patches take effect.
181
+ var _cw = function() { console.warn.apply(console, arguments); };
182
+ var _cl = function() { console.log.apply(console, arguments); };
183
+ var _ce = function() { console.error.apply(console, arguments); };
184
+
185
+ /**
186
+ * Debug flag. When true, emits console.warn for silent binding failures
187
+ * (missing paths, null refs, auto-created intermediate objects).
188
+ * @type {boolean}
189
+ */
190
+ bw.debug = false;
191
+
131
192
  /**
132
193
  * Lazy-resolve Node.js `fs` module.
133
194
  * Tries require('fs') first (available in CJS/UMD Node.js builds),
@@ -275,7 +336,7 @@ bw.uuid = function(prefix) {
275
336
  */
276
337
  bw._el = function(id) {
277
338
  // Pass-through for DOM elements
278
- if (typeof id !== 'string') return id || null;
339
+ if (!_is(id, 'string')) return id || null;
279
340
  if (!id) return null;
280
341
  if (!bw._isBrowser) return null;
281
342
 
@@ -371,7 +432,7 @@ bw._deregisterNode = function(el, bwId) {
371
432
  * // => '<b>Hello</b> & "world"'
372
433
  */
373
434
  bw.escapeHTML = function(str) {
374
- if (typeof str !== 'string') return '';
435
+ if (!_is(str, 'string')) return '';
375
436
 
376
437
  const escapeMap = {
377
438
  '&': '&',
@@ -444,7 +505,7 @@ bw.html = function(taco, options = {}) {
444
505
  }
445
506
 
446
507
  // Handle arrays of TACOs
447
- if (Array.isArray(taco)) {
508
+ if (_isA(taco)) {
448
509
  return taco.map(t => bw.html(t, options)).join('');
449
510
  }
450
511
 
@@ -467,15 +528,15 @@ bw.html = function(taco, options = {}) {
467
528
  if (taco && taco._bwEach && options.state) {
468
529
  var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
469
530
  var arr = bw._evaluatePath(options.state, eachExpr);
470
- if (!Array.isArray(arr)) return '';
531
+ if (!_isA(arr)) return '';
471
532
  return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
472
533
  }
473
534
 
474
535
  // Handle primitives and non-TACO objects
475
- if (typeof taco !== 'object' || !taco.t) {
536
+ if (!_is(taco, 'object') || !taco.t) {
476
537
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
477
538
  // Resolve template bindings if state provided
478
- if (options.state && typeof str === 'string' && str.indexOf('${') >= 0) {
539
+ if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
479
540
  str = bw._resolveTemplate(str, options.state, !!options.compile);
480
541
  }
481
542
  return str;
@@ -495,10 +556,18 @@ bw.html = function(taco, options = {}) {
495
556
  // Skip null, undefined, false
496
557
  if (value == null || value === false) continue;
497
558
 
498
- // Skip event handlers (they're for DOM only)
499
- if (key.startsWith('on')) continue;
559
+ // Serialize event handlers via funcRegister
560
+ if (key.startsWith('on')) {
561
+ if (_is(value, 'function')) {
562
+ var fnId = bw.funcRegister(value);
563
+ attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
564
+ } else if (_is(value, 'string')) {
565
+ attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
566
+ }
567
+ continue;
568
+ }
500
569
 
501
- if (key === 'style' && typeof value === 'object') {
570
+ if (key === 'style' && _is(value, 'object')) {
502
571
  // Convert style object to string
503
572
  const styleStr = Object.entries(value)
504
573
  .filter(([, v]) => v != null)
@@ -509,7 +578,7 @@ bw.html = function(taco, options = {}) {
509
578
  }
510
579
  } else if (key === 'class') {
511
580
  // Handle class as array or string
512
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
581
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
513
582
  if (classStr) {
514
583
  attrStr += ` class="${bw.escapeHTML(classStr)}"`;
515
584
  }
@@ -545,13 +614,184 @@ bw.html = function(taco, options = {}) {
545
614
  // Process content recursively
546
615
  let contentStr = content != null ? bw.html(content, options) : '';
547
616
  // Resolve template bindings in content if state provided
548
- if (options.state && typeof contentStr === 'string' && contentStr.indexOf('${') >= 0) {
617
+ if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
549
618
  contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
550
619
  }
551
620
 
552
621
  return `<${tag}${attrStr}>${contentStr}</${tag}>`;
553
622
  };
554
623
 
624
+ /**
625
+ * Generate a complete, self-contained HTML document from TACO content.
626
+ *
627
+ * Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
628
+ * func registry emission (so serialized event handlers work), optional theme,
629
+ * and extra head elements. Designed for static site generation, offline/airgapped
630
+ * use, and the "static site that isn't static" workflow.
631
+ *
632
+ * @param {Object} [opts={}] - Page options
633
+ * @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
634
+ * @param {string} [opts.title='bitwrench'] - Page title
635
+ * @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
636
+ * @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
637
+ * @param {string} [opts.css=''] - Additional CSS for <style> block
638
+ * @param {string|Object} [opts.theme=null] - Theme preset name or config object
639
+ * @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
640
+ * @param {string} [opts.favicon=''] - Favicon URL
641
+ * @param {string} [opts.lang='en'] - HTML lang attribute
642
+ * @returns {string} Complete HTML document string
643
+ * @category DOM Generation
644
+ * @see bw.html
645
+ * @example
646
+ * bw.htmlPage({
647
+ * title: 'My App',
648
+ * body: { t: 'h1', c: 'Hello World' },
649
+ * runtime: 'shim'
650
+ * })
651
+ */
652
+ bw.htmlPage = function(opts) {
653
+ opts = opts || {};
654
+ var title = opts.title || 'bitwrench';
655
+ var body = opts.body || '';
656
+ var state = opts.state || undefined;
657
+ var runtime = opts.runtime || 'shim';
658
+ var css = opts.css || '';
659
+ var theme = opts.theme || null;
660
+ var headExtra = opts.head || [];
661
+ var favicon = opts.favicon || '';
662
+ var lang = opts.lang || 'en';
663
+
664
+ // Snapshot funcRegistry counter before rendering
665
+ var fnCounterBefore = bw._fnIDCounter;
666
+
667
+ // Render body content
668
+ var bodyHTML = '';
669
+ if (_is(body, 'string')) {
670
+ bodyHTML = body;
671
+ } else {
672
+ var htmlOpts = {};
673
+ if (state) htmlOpts.state = state;
674
+ bodyHTML = bw.html(body, htmlOpts);
675
+ }
676
+
677
+ // Collect functions registered during this render
678
+ var fnCounterAfter = bw._fnIDCounter;
679
+ var registryEntries = '';
680
+ for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
681
+ var fnKey = 'bw_fn_' + i;
682
+ if (bw._fnRegistry[fnKey]) {
683
+ registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' +
684
+ bw._fnRegistry[fnKey].toString() + ';\n';
685
+ }
686
+ }
687
+
688
+ // Build runtime script for <head>
689
+ var runtimeHead = '';
690
+ if (runtime === 'inline') {
691
+ // Read UMD bundle synchronously if in Node.js
692
+ var umdSource = null;
693
+ if (bw._isNode) {
694
+ try {
695
+ var fs = (typeof require === 'function') ? require('fs') : null;
696
+ var pathMod = (typeof require === 'function') ? require('path') : null;
697
+ if (fs && pathMod) {
698
+ // Resolve dist/ relative to this source file
699
+ var srcDir = '';
700
+ try { srcDir = pathMod.dirname((typeof __filename !== 'undefined') ? __filename : ''); }
701
+ catch(e2) { /* ESM: __filename not available */ }
702
+ if (!srcDir && typeof import.meta !== 'undefined' && import.meta.url) {
703
+ var url = (typeof require === 'function') ? require('url') : null;
704
+ if (url && url.fileURLToPath) srcDir = pathMod.dirname(url.fileURLToPath(import.meta.url));
705
+ }
706
+ if (srcDir) {
707
+ var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
708
+ umdSource = fs.readFileSync(distPath, 'utf8');
709
+ }
710
+ }
711
+ } catch(e) { /* fall through */ }
712
+ }
713
+ if (umdSource) {
714
+ runtimeHead = '<script>' + umdSource + '</script>';
715
+ } else {
716
+ // Fallback to shim in browser or if dist not available
717
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
718
+ }
719
+ } else if (runtime === 'cdn') {
720
+ runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
721
+ } else if (runtime === 'shim') {
722
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
723
+ }
724
+ // runtime === 'none' → empty
725
+
726
+ // Theme CSS
727
+ var themeCSS = '';
728
+ if (theme) {
729
+ var themeConfig = _is(theme, 'string')
730
+ ? (THEME_PRESETS[theme.toLowerCase()] || null)
731
+ : theme;
732
+ if (themeConfig) {
733
+ var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, { inject: false }));
734
+ themeCSS = themeResult.css;
735
+ }
736
+ }
737
+
738
+ // Extra <head> elements
739
+ var headHTML = '';
740
+ if (_isA(headExtra) && headExtra.length > 0) {
741
+ headHTML = headExtra.map(function(el) { return bw.html(el); }).join('\n');
742
+ }
743
+
744
+ // Favicon
745
+ var faviconTag = '';
746
+ if (favicon) {
747
+ var safeFavicon = favicon.replace(/[&<>"']/g, function(c) {
748
+ return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
749
+ });
750
+ faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
751
+ }
752
+
753
+ // Escaped title
754
+ var safeTitle = bw.escapeHTML(title);
755
+
756
+ // Combine all CSS
757
+ var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
758
+
759
+ // Body-end script: registry entries + optional loadDefaultStyles
760
+ var bodyEndScript = '';
761
+ var bodyEndParts = [];
762
+ if (registryEntries) {
763
+ bodyEndParts.push(registryEntries);
764
+ }
765
+ if (runtime === 'inline' || runtime === 'cdn') {
766
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
767
+ }
768
+ if (bodyEndParts.length > 0) {
769
+ bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
770
+ }
771
+
772
+ // Assemble document
773
+ var parts = [
774
+ '<!DOCTYPE html>',
775
+ '<html lang="' + lang + '">',
776
+ '<head>',
777
+ '<meta charset="UTF-8">',
778
+ '<meta name="viewport" content="width=device-width, initial-scale=1">'
779
+ ];
780
+ parts.push('<title>' + safeTitle + '</title>');
781
+ if (faviconTag) parts.push(faviconTag);
782
+ if (runtimeHead) parts.push(runtimeHead);
783
+ if (headHTML) parts.push(headHTML);
784
+ if (allCSS) parts.push('<style>' + allCSS + '</style>');
785
+ parts.push('</head>');
786
+ parts.push('<body>');
787
+ parts.push(bodyHTML);
788
+ if (bodyEndScript) parts.push(bodyEndScript);
789
+ parts.push('</body>');
790
+ parts.push('</html>');
791
+
792
+ return parts.join('\n');
793
+ };
794
+
555
795
  /**
556
796
  * Create a live DOM element from a TACO object (browser only).
557
797
  *
@@ -596,7 +836,7 @@ bw.createDOM = function(taco, options = {}) {
596
836
  }
597
837
 
598
838
  // Handle text nodes
599
- if (typeof taco !== 'object' || !taco.t) {
839
+ if (!_is(taco, 'object') || !taco.t) {
600
840
  return document.createTextNode(String(taco));
601
841
  }
602
842
 
@@ -609,16 +849,16 @@ bw.createDOM = function(taco, options = {}) {
609
849
  for (const [key, value] of Object.entries(attrs)) {
610
850
  if (value == null || value === false) continue;
611
851
 
612
- if (key === 'style' && typeof value === 'object') {
852
+ if (key === 'style' && _is(value, 'object')) {
613
853
  // Apply styles directly
614
854
  Object.assign(el.style, value);
615
855
  } else if (key === 'class') {
616
856
  // Handle class as array or string
617
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
857
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
618
858
  if (classStr) {
619
859
  el.className = classStr;
620
860
  }
621
- } else if (key.startsWith('on') && typeof value === 'function') {
861
+ } else if (key.startsWith('on') && _is(value, 'function')) {
622
862
  // Event handlers
623
863
  const eventName = key.slice(2).toLowerCase();
624
864
  el.addEventListener(eventName, value);
@@ -638,7 +878,7 @@ bw.createDOM = function(taco, options = {}) {
638
878
  // Children with data-bw_id or id attributes get local refs on the parent,
639
879
  // so o.render functions can access them without any DOM lookup.
640
880
  if (content != null) {
641
- if (Array.isArray(content)) {
881
+ if (_isA(content)) {
642
882
  content.forEach(child => {
643
883
  if (child != null) {
644
884
  // Handle ComponentHandle in content arrays (Level 2 children)
@@ -658,20 +898,20 @@ bw.createDOM = function(taco, options = {}) {
658
898
  if (childEl._bw_refs) {
659
899
  if (!el._bw_refs) el._bw_refs = {};
660
900
  for (var rk in childEl._bw_refs) {
661
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
901
+ if (_hop.call(childEl._bw_refs, rk)) {
662
902
  el._bw_refs[rk] = childEl._bw_refs[rk];
663
903
  }
664
904
  }
665
905
  }
666
906
  }
667
907
  });
668
- } else if (typeof content === 'object' && content.__bw_raw) {
908
+ } else if (_is(content, 'object') && content.__bw_raw) {
669
909
  // Raw HTML content — inject via innerHTML
670
910
  el.innerHTML = content.v;
671
911
  } else if (content._bwComponent === true) {
672
912
  // Single ComponentHandle as content
673
913
  content.mount(el);
674
- } else if (typeof content === 'object' && content.t) {
914
+ } else if (_is(content, 'object') && content.t) {
675
915
  var childEl = bw.createDOM(content, options);
676
916
  el.appendChild(childEl);
677
917
  var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
@@ -682,7 +922,7 @@ bw.createDOM = function(taco, options = {}) {
682
922
  if (childEl._bw_refs) {
683
923
  if (!el._bw_refs) el._bw_refs = {};
684
924
  for (var rk in childEl._bw_refs) {
685
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
925
+ if (_hop.call(childEl._bw_refs, rk)) {
686
926
  el._bw_refs[rk] = childEl._bw_refs[rk];
687
927
  }
688
928
  }
@@ -715,7 +955,7 @@ bw.createDOM = function(taco, options = {}) {
715
955
  el._bw_render = opts.render;
716
956
 
717
957
  if (opts.mounted) {
718
- console.warn('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
958
+ _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
719
959
  }
720
960
 
721
961
  // Queue initial render (same timing as mounted)
@@ -788,7 +1028,7 @@ bw.DOM = function(target, taco, options = {}) {
788
1028
  const targetEl = bw._el(target);
789
1029
 
790
1030
  if (!targetEl) {
791
- console.error('bw.DOM: Target element not found:', target);
1031
+ _ce('bw.DOM: Target element not found:', target);
792
1032
  return null;
793
1033
  }
794
1034
 
@@ -828,7 +1068,7 @@ bw.DOM = function(target, taco, options = {}) {
828
1068
  targetEl.appendChild(taco.element);
829
1069
  }
830
1070
  // Handle arrays
831
- else if (Array.isArray(taco)) {
1071
+ else if (_isA(taco)) {
832
1072
  taco.forEach(t => {
833
1073
  if (t != null) {
834
1074
  if (t._bwComponent === true) {
@@ -864,7 +1104,7 @@ bw.DOM = function(target, taco, options = {}) {
864
1104
  bw.compileProps = function(handle, props = {}) {
865
1105
  const compiledProps = {};
866
1106
 
867
- Object.keys(props).forEach(key => {
1107
+ _keys(props).forEach(key => {
868
1108
  // Create getter/setter for each prop
869
1109
  Object.defineProperty(compiledProps, key, {
870
1110
  get() {
@@ -1182,17 +1422,17 @@ bw.patch = function(id, content, attr) {
1182
1422
  if (attr) {
1183
1423
  // Patch an attribute
1184
1424
  el.setAttribute(attr, String(content));
1185
- } else if (Array.isArray(content)) {
1425
+ } else if (_isA(content)) {
1186
1426
  // Patch with array of children (strings and/or TACOs)
1187
1427
  el.innerHTML = '';
1188
1428
  content.forEach(function(item) {
1189
- if (typeof item === 'string' || typeof item === 'number') {
1429
+ if (_is(item, 'string') || _is(item, 'number')) {
1190
1430
  el.appendChild(document.createTextNode(String(item)));
1191
1431
  } else if (item && item.t) {
1192
1432
  el.appendChild(bw.createDOM(item));
1193
1433
  }
1194
1434
  });
1195
- } else if (typeof content === 'object' && content !== null && content.t) {
1435
+ } else if (_is(content, 'object') && content.t) {
1196
1436
  // Patch with a TACO — replace children
1197
1437
  el.innerHTML = '';
1198
1438
  el.appendChild(bw.createDOM(content));
@@ -1223,7 +1463,7 @@ bw.patch = function(id, content, attr) {
1223
1463
  bw.patchAll = function(patches) {
1224
1464
  var results = {};
1225
1465
  for (var id in patches) {
1226
- if (Object.prototype.hasOwnProperty.call(patches, id)) {
1466
+ if (_hop.call(patches, id)) {
1227
1467
  results[id] = bw.patch(id, patches[id]);
1228
1468
  }
1229
1469
  }
@@ -1320,7 +1560,7 @@ bw.pub = function(topic, detail) {
1320
1560
  snapshot[i].handler(detail);
1321
1561
  called++;
1322
1562
  } catch (err) {
1323
- console.warn('bw.pub: subscriber error on topic "' + topic + '":', err);
1563
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
1324
1564
  }
1325
1565
  }
1326
1566
  return called;
@@ -1416,8 +1656,8 @@ bw._fnIDCounter = 0;
1416
1656
  * @see bw.funcGetDispatchStr
1417
1657
  */
1418
1658
  bw.funcRegister = function(fn, name) {
1419
- if (typeof fn !== 'function') return '';
1420
- var fnID = (typeof name === 'string' && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
1659
+ if (!_is(fn, 'function')) return '';
1660
+ var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
1421
1661
  bw._fnRegistry[fnID] = fn;
1422
1662
  return fnID;
1423
1663
  };
@@ -1436,7 +1676,7 @@ bw.funcRegister = function(fn, name) {
1436
1676
  bw.funcGetById = function(name, errFn) {
1437
1677
  name = String(name);
1438
1678
  if (name in bw._fnRegistry) return bw._fnRegistry[name];
1439
- return (typeof errFn === 'function') ? errFn : function() { console.warn('bw.funcGetById: unregistered fn "' + name + '"'); };
1679
+ return _is(errFn, 'function') ? errFn : function() { _cw('bw.funcGetById: unregistered fn "' + name + '"'); };
1440
1680
  };
1441
1681
 
1442
1682
  /**
@@ -1477,13 +1717,30 @@ bw.funcUnregister = function(name) {
1477
1717
  bw.funcGetRegistry = function() {
1478
1718
  var copy = {};
1479
1719
  for (var k in bw._fnRegistry) {
1480
- if (Object.prototype.hasOwnProperty.call(bw._fnRegistry, k)) {
1720
+ if (_hop.call(bw._fnRegistry, k)) {
1481
1721
  copy[k] = bw._fnRegistry[k];
1482
1722
  }
1483
1723
  }
1484
1724
  return copy;
1485
1725
  };
1486
1726
 
1727
+ /**
1728
+ * Minimal runtime shim for funcRegister dispatch in static HTML.
1729
+ * When embedded in a `<script>` tag, provides just enough infrastructure
1730
+ * for `bw.funcGetById()` calls to resolve. The actual function bodies
1731
+ * are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
1732
+ * @type {string}
1733
+ * @category Function Registry
1734
+ */
1735
+ bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' +
1736
+ 'if(!bw._fnRegistry)bw._fnRegistry={};' +
1737
+ 'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' +
1738
+ 'console.warn("bw: unregistered fn "+n)};};' +
1739
+ 'bw.funcRegister=function(fn,name){' +
1740
+ 'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' +
1741
+ 'bw._fnRegistry[id]=fn;return id;};' +
1742
+ 'window.bw=bw;})();';
1743
+
1487
1744
  // ===================================================================================
1488
1745
  // Template Binding Utilities
1489
1746
  // ===================================================================================
@@ -1511,7 +1768,10 @@ bw._evaluatePath = function(state, path) {
1511
1768
  var parts = path.split('.');
1512
1769
  var val = state;
1513
1770
  for (var i = 0; i < parts.length; i++) {
1514
- if (val == null) return '';
1771
+ if (val == null) {
1772
+ if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
1773
+ return '';
1774
+ }
1515
1775
  val = val[parts[i]];
1516
1776
  }
1517
1777
  return (val == null) ? '' : val;
@@ -1531,7 +1791,7 @@ bw._evaluatePath = function(state, path) {
1531
1791
  */
1532
1792
  bw._compiledExprs = {};
1533
1793
  bw._resolveTemplate = function(str, state, compile) {
1534
- if (typeof str !== 'string' || str.indexOf('${') < 0) return str;
1794
+ if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
1535
1795
  var bindings = bw._parseBindings(str);
1536
1796
  if (bindings.length === 0) return str;
1537
1797
 
@@ -1553,6 +1813,7 @@ bw._resolveTemplate = function(str, state, compile) {
1553
1813
  try {
1554
1814
  val = bw._compiledExprs[b.expr](state);
1555
1815
  } catch (e) {
1816
+ if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
1556
1817
  val = '';
1557
1818
  }
1558
1819
  } else {
@@ -1661,7 +1922,7 @@ function ComponentHandle(taco) {
1661
1922
  this._state = {};
1662
1923
  if (o.state) {
1663
1924
  for (var k in o.state) {
1664
- if (Object.prototype.hasOwnProperty.call(o.state, k)) {
1925
+ if (_hop.call(o.state, k)) {
1665
1926
  this._state[k] = o.state[k];
1666
1927
  }
1667
1928
  }
@@ -1670,7 +1931,7 @@ function ComponentHandle(taco) {
1670
1931
  this._actions = {};
1671
1932
  if (o.actions) {
1672
1933
  for (var k2 in o.actions) {
1673
- if (Object.prototype.hasOwnProperty.call(o.actions, k2)) {
1934
+ if (_hop.call(o.actions, k2)) {
1674
1935
  this._actions[k2] = o.actions[k2];
1675
1936
  }
1676
1937
  }
@@ -1680,7 +1941,7 @@ function ComponentHandle(taco) {
1680
1941
  if (o.methods) {
1681
1942
  var self = this;
1682
1943
  for (var k3 in o.methods) {
1683
- if (Object.prototype.hasOwnProperty.call(o.methods, k3)) {
1944
+ if (_hop.call(o.methods, k3)) {
1684
1945
  this._methods[k3] = o.methods[k3];
1685
1946
  (function(methodName, methodFn) {
1686
1947
  self[methodName] = function() {
@@ -1713,14 +1974,23 @@ function ComponentHandle(taco) {
1713
1974
  this._compile = !!o.compile;
1714
1975
  this._bw_refs = {};
1715
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;
1716
1982
  }
1717
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
+
1718
1988
  // ── State Methods ──
1719
1989
 
1720
1990
  /**
1721
1991
  * Get a state value. Dot-path supported: `get('user.name')`
1722
1992
  */
1723
- ComponentHandle.prototype.get = function(key) {
1993
+ _chp.get = function(key) {
1724
1994
  return bw._evaluatePath(this._state, key);
1725
1995
  };
1726
1996
 
@@ -1730,12 +2000,13 @@ ComponentHandle.prototype.get = function(key) {
1730
2000
  * @param {*} value - New value
1731
2001
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
1732
2002
  */
1733
- ComponentHandle.prototype.set = function(key, value, opts) {
2003
+ _chp.set = function(key, value, opts) {
1734
2004
  // Dot-path set
1735
2005
  var parts = key.split('.');
1736
2006
  var obj = this._state;
1737
2007
  for (var i = 0; i < parts.length - 1; i++) {
1738
- if (obj[parts[i]] == null || typeof obj[parts[i]] !== 'object') {
2008
+ if (!_is(obj[parts[i]], 'object')) {
2009
+ if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
1739
2010
  obj[parts[i]] = {};
1740
2011
  }
1741
2012
  obj = obj[parts[i]];
@@ -1755,10 +2026,10 @@ ComponentHandle.prototype.set = function(key, value, opts) {
1755
2026
  /**
1756
2027
  * Get a shallow clone of the full state.
1757
2028
  */
1758
- ComponentHandle.prototype.getState = function() {
2029
+ _chp.getState = function() {
1759
2030
  var clone = {};
1760
2031
  for (var k in this._state) {
1761
- if (Object.prototype.hasOwnProperty.call(this._state, k)) {
2032
+ if (_hop.call(this._state, k)) {
1762
2033
  clone[k] = this._state[k];
1763
2034
  }
1764
2035
  }
@@ -1770,9 +2041,9 @@ ComponentHandle.prototype.getState = function() {
1770
2041
  * @param {Object} updates - Key-value pairs to merge
1771
2042
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
1772
2043
  */
1773
- ComponentHandle.prototype.setState = function(updates, opts) {
2044
+ _chp.setState = function(updates, opts) {
1774
2045
  for (var k in updates) {
1775
- if (Object.prototype.hasOwnProperty.call(updates, k)) {
2046
+ if (_hop.call(updates, k)) {
1776
2047
  this._state[k] = updates[k];
1777
2048
  this._dirtyKeys[k] = true;
1778
2049
  }
@@ -1789,9 +2060,9 @@ ComponentHandle.prototype.setState = function(updates, opts) {
1789
2060
  /**
1790
2061
  * Push a value onto an array in state. Clones the array.
1791
2062
  */
1792
- ComponentHandle.prototype.push = function(key, val) {
2063
+ _chp.push = function(key, val) {
1793
2064
  var arr = this.get(key);
1794
- var newArr = Array.isArray(arr) ? arr.slice() : [];
2065
+ var newArr = _isA(arr) ? arr.slice() : [];
1795
2066
  newArr.push(val);
1796
2067
  this.set(key, newArr);
1797
2068
  };
@@ -1799,9 +2070,9 @@ ComponentHandle.prototype.push = function(key, val) {
1799
2070
  /**
1800
2071
  * Splice an array in state. Clones the array.
1801
2072
  */
1802
- ComponentHandle.prototype.splice = function(key, start, deleteCount) {
2073
+ _chp.splice = function(key, start, deleteCount) {
1803
2074
  var arr = this.get(key);
1804
- var newArr = Array.isArray(arr) ? arr.slice() : [];
2075
+ var newArr = _isA(arr) ? arr.slice() : [];
1805
2076
  var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
1806
2077
  Array.prototype.splice.apply(newArr, args);
1807
2078
  this.set(key, newArr);
@@ -1809,7 +2080,7 @@ ComponentHandle.prototype.splice = function(key, start, deleteCount) {
1809
2080
 
1810
2081
  // ── Scheduling ──
1811
2082
 
1812
- ComponentHandle.prototype._scheduleDirty = function() {
2083
+ _chp._scheduleDirty = function() {
1813
2084
  if (!this._scheduled) {
1814
2085
  this._scheduled = true;
1815
2086
  bw._dirtyComponents.push(this);
@@ -1824,17 +2095,17 @@ ComponentHandle.prototype._scheduleDirty = function() {
1824
2095
  * Creates binding descriptors with refIds for targeted DOM updates.
1825
2096
  * @private
1826
2097
  */
1827
- ComponentHandle.prototype._compileBindings = function() {
2098
+ _chp._compileBindings = function() {
1828
2099
  this._bindings = [];
1829
2100
  this._refCounter = 0;
1830
- var stateKeys = Object.keys(this._state);
2101
+ var stateKeys = _keys(this._state);
1831
2102
  var self = this;
1832
2103
 
1833
2104
  function walkTaco(taco, path) {
1834
- if (taco == null || typeof taco !== 'object' || !taco.t) return taco;
2105
+ if (!_is(taco, 'object') || !taco.t) return taco;
1835
2106
 
1836
2107
  // Check content for bindings
1837
- if (typeof taco.c === 'string' && taco.c.indexOf('${') >= 0) {
2108
+ if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
1838
2109
  var refId = 'bw_ref_' + self._refCounter++;
1839
2110
  var parsed = bw._parseBindings(taco.c);
1840
2111
  var deps = [];
@@ -1856,10 +2127,10 @@ ComponentHandle.prototype._compileBindings = function() {
1856
2127
  // Check attributes for bindings
1857
2128
  if (taco.a) {
1858
2129
  for (var attrName in taco.a) {
1859
- if (!Object.prototype.hasOwnProperty.call(taco.a, attrName)) continue;
2130
+ if (!_hop.call(taco.a, attrName)) continue;
1860
2131
  if (attrName === 'data-bw_ref') continue;
1861
2132
  var attrVal = taco.a[attrName];
1862
- if (typeof attrVal === 'string' && attrVal.indexOf('${') >= 0) {
2133
+ if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
1863
2134
  var refId2 = 'bw_ref_' + self._refCounter++;
1864
2135
  var parsed2 = bw._parseBindings(attrVal);
1865
2136
  var deps2 = [];
@@ -1885,9 +2156,27 @@ ComponentHandle.prototype._compileBindings = function() {
1885
2156
  }
1886
2157
 
1887
2158
  // Recurse into children
1888
- if (Array.isArray(taco.c)) {
2159
+ if (_isA(taco.c)) {
1889
2160
  for (var i = 0; i < taco.c.length; i++) {
1890
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
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) {
1891
2180
  walkTaco(taco.c[i], path.concat(i));
1892
2181
  }
1893
2182
  // Handle bw.when/bw.each markers
@@ -1922,7 +2211,7 @@ ComponentHandle.prototype._compileBindings = function() {
1922
2211
  taco.c[i]._refId = eachRefId;
1923
2212
  }
1924
2213
  }
1925
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
2214
+ } else if (_is(taco.c, 'object') && taco.c.t) {
1926
2215
  walkTaco(taco.c, path.concat(0));
1927
2216
  }
1928
2217
 
@@ -1938,7 +2227,7 @@ ComponentHandle.prototype._compileBindings = function() {
1938
2227
  * Build ref map from the live DOM after createDOM.
1939
2228
  * @private
1940
2229
  */
1941
- ComponentHandle.prototype._collectRefs = function() {
2230
+ _chp._collectRefs = function() {
1942
2231
  this._bw_refs = {};
1943
2232
  if (!this.element) return;
1944
2233
  var els = this.element.querySelectorAll('[data-bw_ref]');
@@ -1959,7 +2248,7 @@ ComponentHandle.prototype._collectRefs = function() {
1959
2248
  * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
1960
2249
  * @param {Element} parentEl - DOM element to mount into
1961
2250
  */
1962
- ComponentHandle.prototype.mount = function(parentEl) {
2251
+ _chp.mount = function(parentEl) {
1963
2252
  // willMount hook
1964
2253
  if (this._hooks.willMount) this._hooks.willMount(this);
1965
2254
 
@@ -1981,7 +2270,7 @@ ComponentHandle.prototype.mount = function(parentEl) {
1981
2270
  // Register named actions in function registry
1982
2271
  var self = this;
1983
2272
  for (var actionName in this._actions) {
1984
- if (Object.prototype.hasOwnProperty.call(this._actions, actionName)) {
2273
+ if (_hop.call(this._actions, actionName)) {
1985
2274
  var registeredName = this._bwId + '_' + actionName;
1986
2275
  (function(aName) {
1987
2276
  bw.funcRegister(function(evt) {
@@ -2000,6 +2289,11 @@ ComponentHandle.prototype.mount = function(parentEl) {
2000
2289
  this.element = bw.createDOM(tacoForDOM);
2001
2290
  this.element._bwComponentHandle = this;
2002
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
+ }
2003
2297
  if (this._userTag) {
2004
2298
  this.element.classList.add(this._userTag);
2005
2299
  }
@@ -2015,6 +2309,16 @@ ComponentHandle.prototype.mount = function(parentEl) {
2015
2309
 
2016
2310
  this.mounted = true;
2017
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
+
2018
2322
  // mounted hook (backward compat: fn.length === 2 wraps (el, state))
2019
2323
  if (this._hooks.mounted) {
2020
2324
  if (this._hooks.mounted.length === 2) {
@@ -2023,16 +2327,21 @@ ComponentHandle.prototype.mount = function(parentEl) {
2023
2327
  this._hooks.mounted(this);
2024
2328
  }
2025
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
+ }
2026
2335
  };
2027
2336
 
2028
2337
  /**
2029
2338
  * Prepare TACO for initial render: resolve when/each markers.
2030
2339
  * @private
2031
2340
  */
2032
- ComponentHandle.prototype._prepareTaco = function(taco) {
2033
- if (!taco || typeof taco !== 'object') return;
2341
+ _chp._prepareTaco = function(taco) {
2342
+ if (!_is(taco, 'object')) return;
2034
2343
 
2035
- if (Array.isArray(taco.c)) {
2344
+ if (_isA(taco.c)) {
2036
2345
  for (var i = taco.c.length - 1; i >= 0; i--) {
2037
2346
  var child = taco.c[i];
2038
2347
  if (child && child._bwWhen) {
@@ -2057,18 +2366,18 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
2057
2366
  var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
2058
2367
  var arr = bw._evaluatePath(this._state, eachExprStr);
2059
2368
  var items = [];
2060
- if (Array.isArray(arr)) {
2369
+ if (_isA(arr)) {
2061
2370
  for (var j = 0; j < arr.length; j++) {
2062
2371
  items.push(child.factory(arr[j], j));
2063
2372
  }
2064
2373
  }
2065
2374
  taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
2066
2375
  }
2067
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
2376
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
2068
2377
  this._prepareTaco(taco.c[i]);
2069
2378
  }
2070
2379
  }
2071
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
2380
+ } else if (_is(taco.c, 'object') && taco.c.t) {
2072
2381
  this._prepareTaco(taco.c);
2073
2382
  }
2074
2383
  };
@@ -2077,12 +2386,12 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
2077
2386
  * Wire action name strings (in onclick etc.) to dispatch function calls.
2078
2387
  * @private
2079
2388
  */
2080
- ComponentHandle.prototype._wireActions = function(taco) {
2081
- if (!taco || typeof taco !== 'object' || !taco.t) return;
2389
+ _chp._wireActions = function(taco) {
2390
+ if (!_is(taco, 'object') || !taco.t) return;
2082
2391
  if (taco.a) {
2083
2392
  for (var key in taco.a) {
2084
- if (!Object.prototype.hasOwnProperty.call(taco.a, key)) continue;
2085
- if (key.startsWith('on') && typeof taco.a[key] === 'string') {
2393
+ if (!_hop.call(taco.a, key)) continue;
2394
+ if (key.startsWith('on') && _is(taco.a[key], 'string')) {
2086
2395
  var actionName = taco.a[key];
2087
2396
  if (actionName in this._actions) {
2088
2397
  var registeredName = this._bwId + '_' + actionName;
@@ -2096,11 +2405,11 @@ ComponentHandle.prototype._wireActions = function(taco) {
2096
2405
  }
2097
2406
  }
2098
2407
  }
2099
- if (Array.isArray(taco.c)) {
2408
+ if (_isA(taco.c)) {
2100
2409
  for (var i = 0; i < taco.c.length; i++) {
2101
2410
  this._wireActions(taco.c[i]);
2102
2411
  }
2103
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
2412
+ } else if (_is(taco.c, 'object') && taco.c.t) {
2104
2413
  this._wireActions(taco.c);
2105
2414
  }
2106
2415
  };
@@ -2109,7 +2418,7 @@ ComponentHandle.prototype._wireActions = function(taco) {
2109
2418
  * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
2110
2419
  * @private
2111
2420
  */
2112
- ComponentHandle.prototype._deepCloneTaco = function(taco) {
2421
+ _chp._deepCloneTaco = function(taco) {
2113
2422
  if (taco == null) return taco;
2114
2423
  // Preserve _bwWhen / _bwEach markers (contain functions)
2115
2424
  if (taco._bwWhen) {
@@ -2121,18 +2430,18 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
2121
2430
  if (taco._bwEach) {
2122
2431
  return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
2123
2432
  }
2124
- if (typeof taco !== 'object' || !taco.t) return taco;
2433
+ if (!_is(taco, 'object') || !taco.t) return taco;
2125
2434
  var result = { t: taco.t };
2126
2435
  if (taco.a) {
2127
2436
  result.a = {};
2128
2437
  for (var k in taco.a) {
2129
- if (Object.prototype.hasOwnProperty.call(taco.a, k)) result.a[k] = taco.a[k];
2438
+ if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
2130
2439
  }
2131
2440
  }
2132
2441
  if (taco.c != null) {
2133
- if (Array.isArray(taco.c)) {
2442
+ if (_isA(taco.c)) {
2134
2443
  result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
2135
- } else if (typeof taco.c === 'object') {
2444
+ } else if (_is(taco.c, 'object')) {
2136
2445
  result.c = this._deepCloneTaco(taco.c);
2137
2446
  } else {
2138
2447
  result.c = taco.c;
@@ -2146,27 +2455,31 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
2146
2455
  * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
2147
2456
  * @private
2148
2457
  */
2149
- ComponentHandle.prototype._tacoForDOM = function(taco) {
2150
- if (!taco || typeof taco !== 'object' || !taco.t) return taco;
2458
+ _chp._tacoForDOM = function(taco) {
2459
+ if (!_is(taco, 'object') || !taco.t) return taco;
2151
2460
  var result = { t: taco.t };
2152
2461
  if (taco.a) result.a = taco.a;
2153
2462
  if (taco.c != null) {
2154
- if (Array.isArray(taco.c)) {
2463
+ if (_isA(taco.c)) {
2155
2464
  result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
2156
- } else if (typeof taco.c === 'object' && taco.c.t) {
2465
+ } else if (_is(taco.c, 'object') && taco.c.t) {
2157
2466
  result.c = this._tacoForDOM(taco.c);
2158
2467
  } else {
2159
2468
  result.c = taco.c;
2160
2469
  }
2161
2470
  }
2162
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
+ }
2163
2476
  return result;
2164
2477
  };
2165
2478
 
2166
2479
  /**
2167
2480
  * Unmount: remove from DOM, deactivate, preserve state for re-mount.
2168
2481
  */
2169
- ComponentHandle.prototype.unmount = function() {
2482
+ _chp.unmount = function() {
2170
2483
  if (!this.mounted) return;
2171
2484
 
2172
2485
  // unmount hook
@@ -2201,12 +2514,23 @@ ComponentHandle.prototype.unmount = function() {
2201
2514
  /**
2202
2515
  * Destroy: unmount + clear state + unregister actions.
2203
2516
  */
2204
- ComponentHandle.prototype.destroy = function() {
2517
+ _chp.destroy = function() {
2205
2518
  // willDestroy hook
2206
2519
  if (this._hooks.willDestroy) {
2207
2520
  this._hooks.willDestroy(this);
2208
2521
  }
2209
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
+
2210
2534
  this.unmount();
2211
2535
 
2212
2536
  // Unregister actions from function registry
@@ -2233,12 +2557,36 @@ ComponentHandle.prototype.destroy = function() {
2233
2557
  * Flush dirty state: resolve changed bindings and apply to DOM.
2234
2558
  * @private
2235
2559
  */
2236
- ComponentHandle.prototype._flush = function() {
2560
+ _chp._flush = function() {
2237
2561
  this._scheduled = false;
2238
- var changedKeys = Object.keys(this._dirtyKeys);
2562
+ var changedKeys = _keys(this._dirtyKeys);
2239
2563
  this._dirtyKeys = {};
2240
2564
  if (changedKeys.length === 0 || !this.mounted) return;
2241
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
+
2242
2590
  // willUpdate hook
2243
2591
  if (this._hooks.willUpdate) {
2244
2592
  this._hooks.willUpdate(this, changedKeys);
@@ -2277,7 +2625,7 @@ ComponentHandle.prototype._flush = function() {
2277
2625
  * Returns list of patches to apply.
2278
2626
  * @private
2279
2627
  */
2280
- ComponentHandle.prototype._resolveBindings = function(changedKeys) {
2628
+ _chp._resolveBindings = function(changedKeys) {
2281
2629
  var patches = [];
2282
2630
  for (var i = 0; i < this._bindings.length; i++) {
2283
2631
  var b = this._bindings[i];
@@ -2313,11 +2661,14 @@ ComponentHandle.prototype._resolveBindings = function(changedKeys) {
2313
2661
  * Apply patches to DOM.
2314
2662
  * @private
2315
2663
  */
2316
- ComponentHandle.prototype._applyPatches = function(patches) {
2664
+ _chp._applyPatches = function(patches) {
2317
2665
  for (var i = 0; i < patches.length; i++) {
2318
2666
  var p = patches[i];
2319
2667
  var el = this._bw_refs[p.refId];
2320
- if (!el) continue;
2668
+ if (!el) {
2669
+ if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
2670
+ continue;
2671
+ }
2321
2672
  if (p.type === 'content') {
2322
2673
  el.textContent = p.value;
2323
2674
  } else if (p.type === 'attribute') {
@@ -2334,7 +2685,7 @@ ComponentHandle.prototype._applyPatches = function(patches) {
2334
2685
  * Resolve all bindings and apply (used for initial render).
2335
2686
  * @private
2336
2687
  */
2337
- ComponentHandle.prototype._resolveAndApplyAll = function() {
2688
+ _chp._resolveAndApplyAll = function() {
2338
2689
  var patches = [];
2339
2690
  for (var i = 0; i < this._bindings.length; i++) {
2340
2691
  var b = this._bindings[i];
@@ -2357,7 +2708,7 @@ ComponentHandle.prototype._resolveAndApplyAll = function() {
2357
2708
  * Full re-render for structural changes (when/each branch switches).
2358
2709
  * @private
2359
2710
  */
2360
- ComponentHandle.prototype._render = function() {
2711
+ _chp._render = function() {
2361
2712
  if (!this.element || !this.element.parentNode) return;
2362
2713
  var parent = this.element.parentNode;
2363
2714
  var nextSibling = this.element.nextSibling;
@@ -2397,7 +2748,7 @@ ComponentHandle.prototype._render = function() {
2397
2748
  * @param {string} event - Event name (e.g., 'click')
2398
2749
  * @param {Function} handler - Event handler
2399
2750
  */
2400
- ComponentHandle.prototype.on = function(event, handler) {
2751
+ _chp.on = function(event, handler) {
2401
2752
  if (this.element) {
2402
2753
  this.element.addEventListener(event, handler);
2403
2754
  }
@@ -2409,7 +2760,7 @@ ComponentHandle.prototype.on = function(event, handler) {
2409
2760
  * @param {string} event - Event name
2410
2761
  * @param {Function} handler - Handler to remove
2411
2762
  */
2412
- ComponentHandle.prototype.off = function(event, handler) {
2763
+ _chp.off = function(event, handler) {
2413
2764
  if (this.element) {
2414
2765
  this.element.removeEventListener(event, handler);
2415
2766
  }
@@ -2424,7 +2775,7 @@ ComponentHandle.prototype.off = function(event, handler) {
2424
2775
  * @param {Function} handler - Handler function
2425
2776
  * @returns {Function} Unsubscribe function
2426
2777
  */
2427
- ComponentHandle.prototype.sub = function(topic, handler) {
2778
+ _chp.sub = function(topic, handler) {
2428
2779
  var unsub = bw.sub(topic, handler);
2429
2780
  this._subs.push(unsub);
2430
2781
  return unsub;
@@ -2435,10 +2786,10 @@ ComponentHandle.prototype.sub = function(topic, handler) {
2435
2786
  * @param {string} name - Action name
2436
2787
  * @param {...*} args - Arguments passed after comp
2437
2788
  */
2438
- ComponentHandle.prototype.action = function(name) {
2789
+ _chp.action = function(name) {
2439
2790
  var fn = this._actions[name];
2440
2791
  if (!fn) {
2441
- console.warn('ComponentHandle.action: unknown action "' + name + '"');
2792
+ _cw('ComponentHandle.action: unknown action "' + name + '"');
2442
2793
  return;
2443
2794
  }
2444
2795
  var args = [this].concat(Array.prototype.slice.call(arguments, 1));
@@ -2450,7 +2801,7 @@ ComponentHandle.prototype.action = function(name) {
2450
2801
  * @param {string} sel - CSS selector
2451
2802
  * @returns {Element|null}
2452
2803
  */
2453
- ComponentHandle.prototype.select = function(sel) {
2804
+ _chp.select = function(sel) {
2454
2805
  return this.element ? this.element.querySelector(sel) : null;
2455
2806
  };
2456
2807
 
@@ -2459,7 +2810,7 @@ ComponentHandle.prototype.select = function(sel) {
2459
2810
  * @param {string} sel - CSS selector
2460
2811
  * @returns {Element[]}
2461
2812
  */
2462
- ComponentHandle.prototype.selectAll = function(sel) {
2813
+ _chp.selectAll = function(sel) {
2463
2814
  if (!this.element) return [];
2464
2815
  return Array.prototype.slice.call(this.element.querySelectorAll(sel));
2465
2816
  };
@@ -2470,7 +2821,7 @@ ComponentHandle.prototype.selectAll = function(sel) {
2470
2821
  * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
2471
2822
  * @returns {ComponentHandle} this (for chaining)
2472
2823
  */
2473
- ComponentHandle.prototype.userTag = function(tag) {
2824
+ _chp.userTag = function(tag) {
2474
2825
  this._userTag = tag;
2475
2826
  if (this.element) {
2476
2827
  this.element.classList.add(tag);
@@ -2571,14 +2922,399 @@ bw.message = function(target, action, data) {
2571
2922
  }
2572
2923
  if (!el || !el._bwComponentHandle) return false;
2573
2924
  var comp = el._bwComponentHandle;
2574
- if (typeof comp[action] !== 'function') {
2575
- console.warn('bw.message: unknown action "' + action + '" on component ' + target);
2925
+ if (!_is(comp[action], 'function')) {
2926
+ _cw('bw.message: unknown action "' + action + '" on component ' + target);
2576
2927
  return false;
2577
2928
  }
2578
2929
  comp[action](data);
2579
2930
  return true;
2580
2931
  };
2581
2932
 
2933
+ // ===================================================================================
2934
+ // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
2935
+ // ===================================================================================
2936
+
2937
+ /**
2938
+ * Registry of named functions sent via register messages.
2939
+ * Populated by clientApply({ type: 'register', name, body }).
2940
+ * Invoked by clientApply({ type: 'call', name, args }).
2941
+ * @private
2942
+ */
2943
+ bw._clientFunctions = {};
2944
+
2945
+ /**
2946
+ * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
2947
+ * Default false — exec messages are rejected unless explicitly opted in.
2948
+ * @private
2949
+ */
2950
+ bw._allowExec = false;
2951
+
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
+ /**
2988
+ * Parse a bwserve protocol message string, supporting both strict JSON
2989
+ * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
2990
+ *
2991
+ * The r-prefix format is designed for C/C++ string literals where
2992
+ * double-quote escaping is painful. The parser is a state machine
2993
+ * that walks character by character — not a regex replace.
2994
+ *
2995
+ * Escaping: apostrophes inside single-quoted values must be escaped
2996
+ * with backslash: r{'name':'Barry\'s room'}
2997
+ *
2998
+ * @param {string} str - JSON or r-prefixed relaxed JSON string
2999
+ * @returns {Object} Parsed message object
3000
+ * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
3001
+ * @category Server
3002
+ */
3003
+ bw.clientParse = function(str) {
3004
+ str = (str || '').trim();
3005
+ if (str.charAt(0) !== 'r') return JSON.parse(str);
3006
+ str = str.slice(1);
3007
+
3008
+ var out = [];
3009
+ var i = 0;
3010
+ var len = str.length;
3011
+
3012
+ while (i < len) {
3013
+ var ch = str[i];
3014
+
3015
+ if (ch === "'") {
3016
+ // Single-quoted string → emit as double-quoted
3017
+ out.push('"');
3018
+ i++;
3019
+ while (i < len) {
3020
+ var c = str[i];
3021
+ if (c === '\\' && i + 1 < len) {
3022
+ var next = str[i + 1];
3023
+ if (next === "'") {
3024
+ out.push("'"); // \' in input → ' in output
3025
+ } else {
3026
+ out.push('\\');
3027
+ out.push(next);
3028
+ }
3029
+ i += 2;
3030
+ } else if (c === '"') {
3031
+ out.push('\\"');
3032
+ i++;
3033
+ } else if (c === "'") {
3034
+ break;
3035
+ } else {
3036
+ out.push(c);
3037
+ i++;
3038
+ }
3039
+ }
3040
+ out.push('"');
3041
+ i++; // skip closing '
3042
+
3043
+ } else if (ch === '"') {
3044
+ // Double-quoted string — pass through verbatim
3045
+ out.push(ch);
3046
+ i++;
3047
+ while (i < len) {
3048
+ var c2 = str[i];
3049
+ if (c2 === '\\' && i + 1 < len) {
3050
+ out.push(c2);
3051
+ out.push(str[i + 1]);
3052
+ i += 2;
3053
+ } else {
3054
+ out.push(c2);
3055
+ i++;
3056
+ if (c2 === '"') break;
3057
+ }
3058
+ }
3059
+
3060
+ } else if (ch === ',') {
3061
+ // Trailing comma check: skip comma if next non-whitespace is } or ]
3062
+ var j = i + 1;
3063
+ while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) j++;
3064
+ if (j < len && (str[j] === '}' || str[j] === ']')) {
3065
+ i++; // skip trailing comma
3066
+ } else {
3067
+ out.push(ch);
3068
+ i++;
3069
+ }
3070
+
3071
+ } else {
3072
+ out.push(ch);
3073
+ i++;
3074
+ }
3075
+ }
3076
+
3077
+ return JSON.parse(out.join(''));
3078
+ };
3079
+
3080
+ /**
3081
+ * Apply a bwserve protocol message to the DOM.
3082
+ *
3083
+ * Dispatches one of 9 message types:
3084
+ * replace — bw.DOM(target, node)
3085
+ * append — target.appendChild(bw.createDOM(node))
3086
+ * remove — bw.cleanup(target); target.remove()
3087
+ * patch — bw.patch(target, content, attr)
3088
+ * batch — iterate ops, call clientApply for each
3089
+ * message — bw.message(target, action, data)
3090
+ * register — store a named function for later call()
3091
+ * call — invoke a registered or built-in function
3092
+ * exec — execute arbitrary JS (requires allowExec)
3093
+ *
3094
+ * Target resolution:
3095
+ * Starts with '#' or '.' → CSS selector (querySelector)
3096
+ * Otherwise → getElementById, then bw._el fallback
3097
+ *
3098
+ * @param {Object} msg - Protocol message
3099
+ * @returns {boolean} true if the message was applied successfully
3100
+ * @category Server
3101
+ */
3102
+ bw.clientApply = function(msg) {
3103
+ if (!msg || !msg.type) return false;
3104
+
3105
+ var type = msg.type;
3106
+ var target = msg.target;
3107
+
3108
+ if (type === 'replace') {
3109
+ var el = bw._el(target);
3110
+ if (!el) return false;
3111
+ bw.DOM(el, msg.node);
3112
+ return true;
3113
+
3114
+ } else if (type === 'patch') {
3115
+ var patched = bw.patch(target, msg.content, msg.attr);
3116
+ return patched !== null;
3117
+
3118
+ } else if (type === 'append') {
3119
+ var parent = bw._el(target);
3120
+ if (!parent) return false;
3121
+ var child = bw.createDOM(msg.node);
3122
+ parent.appendChild(child);
3123
+ return true;
3124
+
3125
+ } else if (type === 'remove') {
3126
+ var toRemove = bw._el(target);
3127
+ if (!toRemove) return false;
3128
+ if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
3129
+ toRemove.remove();
3130
+ return true;
3131
+
3132
+ } else if (type === 'batch') {
3133
+ if (!_isA(msg.ops)) return false;
3134
+ var allOk = true;
3135
+ msg.ops.forEach(function(op) {
3136
+ if (!bw.clientApply(op)) allOk = false;
3137
+ });
3138
+ return allOk;
3139
+
3140
+ } else if (type === 'message') {
3141
+ return bw.message(msg.target, msg.action, msg.data);
3142
+
3143
+ } else if (type === 'register') {
3144
+ if (!msg.name || !msg.body) return false;
3145
+ try {
3146
+ bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
3147
+ return true;
3148
+ } catch (e) {
3149
+ _ce('[bw] register error:', msg.name, e);
3150
+ return false;
3151
+ }
3152
+
3153
+ } else if (type === 'call') {
3154
+ if (!msg.name) return false;
3155
+ var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
3156
+ if (!_is(fn, 'function')) return false;
3157
+ try {
3158
+ var args = _isA(msg.args) ? msg.args : [];
3159
+ fn.apply(null, args);
3160
+ return true;
3161
+ } catch (e) {
3162
+ _ce('[bw] call error:', msg.name, e);
3163
+ return false;
3164
+ }
3165
+
3166
+ } else if (type === 'exec') {
3167
+ if (!bw._allowExec) {
3168
+ _cw('[bw] exec rejected: allowExec is not enabled');
3169
+ return false;
3170
+ }
3171
+ if (!msg.code) return false;
3172
+ try {
3173
+ new Function(msg.code)();
3174
+ return true;
3175
+ } catch (e) {
3176
+ _ce('[bw] exec error:', e);
3177
+ return false;
3178
+ }
3179
+ }
3180
+
3181
+ return false;
3182
+ };
3183
+
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
+
2582
3318
  // ===================================================================================
2583
3319
  // bw.inspect() — Debug utility
2584
3320
  // ===================================================================================
@@ -2605,33 +3341,33 @@ bw.inspect = function(target) {
2605
3341
  el = target.element;
2606
3342
  comp = target;
2607
3343
  } else {
2608
- if (typeof target === 'string') {
3344
+ if (_is(target, 'string')) {
2609
3345
  el = bw.$(target)[0];
2610
3346
  }
2611
3347
  if (!el) {
2612
- console.warn('bw.inspect: element not found');
3348
+ _cw('bw.inspect: element not found');
2613
3349
  return null;
2614
3350
  }
2615
3351
  comp = el._bwComponentHandle;
2616
3352
  }
2617
3353
  if (!comp) {
2618
- console.log('bw.inspect: no ComponentHandle on this element');
2619
- console.log(' Tag:', el.tagName);
2620
- console.log(' Classes:', el.className);
2621
- console.log(' _bw_state:', el._bw_state || '(none)');
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)');
2622
3358
  return null;
2623
3359
  }
2624
3360
  var deps = comp._bindings.reduce(function(s, b) {
2625
3361
  return s.concat(b.deps || []);
2626
3362
  }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
2627
3363
  console.group('Component: ' + comp._bwId);
2628
- console.log('State:', comp._state);
2629
- console.log('Bindings:', comp._bindings.length, '(deps:', deps, ')');
2630
- console.log('Methods:', Object.keys(comp._methods));
2631
- console.log('Actions:', Object.keys(comp._actions));
2632
- console.log('User tag:', comp._userTag || '(none)');
2633
- console.log('Mounted:', comp.mounted);
2634
- console.log('Element:', comp.element);
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);
2635
3371
  console.groupEnd();
2636
3372
  return comp;
2637
3373
  };
@@ -2654,8 +3390,8 @@ bw.compile = function(taco) {
2654
3390
  // Pre-extract all binding expressions
2655
3391
  var precompiled = [];
2656
3392
  function walkExpressions(node) {
2657
- if (!node || typeof node !== 'object') return;
2658
- if (typeof node.c === 'string' && node.c.indexOf('${') >= 0) {
3393
+ if (!_is(node, 'object')) return;
3394
+ if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
2659
3395
  var parsed = bw._parseBindings(node.c);
2660
3396
  for (var i = 0; i < parsed.length; i++) {
2661
3397
  try {
@@ -2670,9 +3406,9 @@ bw.compile = function(taco) {
2670
3406
  }
2671
3407
  if (node.a) {
2672
3408
  for (var key in node.a) {
2673
- if (Object.prototype.hasOwnProperty.call(node.a, key)) {
3409
+ if (_hop.call(node.a, key)) {
2674
3410
  var v = node.a[key];
2675
- if (typeof v === 'string' && v.indexOf('${') >= 0) {
3411
+ if (_is(v, 'string') && v.indexOf('${') >= 0) {
2676
3412
  var parsed2 = bw._parseBindings(v);
2677
3413
  for (var j = 0; j < parsed2.length; j++) {
2678
3414
  try {
@@ -2688,9 +3424,9 @@ bw.compile = function(taco) {
2688
3424
  }
2689
3425
  }
2690
3426
  }
2691
- if (Array.isArray(node.c)) {
3427
+ if (_isA(node.c)) {
2692
3428
  for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
2693
- } else if (node.c && typeof node.c === 'object' && node.c.t) {
3429
+ } else if (_is(node.c, 'object') && node.c.t) {
2694
3430
  walkExpressions(node.c);
2695
3431
  }
2696
3432
  }
@@ -2702,7 +3438,7 @@ bw.compile = function(taco) {
2702
3438
  handle._precompiledBindings = precompiled;
2703
3439
  if (initialState) {
2704
3440
  for (var k in initialState) {
2705
- if (Object.prototype.hasOwnProperty.call(initialState, k)) {
3441
+ if (_hop.call(initialState, k)) {
2706
3442
  handle._state[k] = initialState[k];
2707
3443
  }
2708
3444
  }
@@ -2733,18 +3469,18 @@ bw.compile = function(taco) {
2733
3469
  bw.css = function(rules, options = {}) {
2734
3470
  const { minify = false, pretty = !minify } = options;
2735
3471
 
2736
- if (typeof rules === 'string') return rules;
3472
+ if (_is(rules, 'string')) return rules;
2737
3473
 
2738
3474
  let css = '';
2739
3475
  const indent = pretty ? ' ' : '';
2740
3476
  const newline = pretty ? '\n' : '';
2741
3477
  const space = pretty ? ' ' : '';
2742
3478
 
2743
- if (Array.isArray(rules)) {
3479
+ if (_isA(rules)) {
2744
3480
  css = rules.map(rule => bw.css(rule, options)).join(newline);
2745
- } else if (typeof rules === 'object') {
3481
+ } else if (_is(rules, 'object')) {
2746
3482
  Object.entries(rules).forEach(([selector, styles]) => {
2747
- if (typeof styles === 'object' && !Array.isArray(styles)) {
3483
+ if (_is(styles, 'object')) {
2748
3484
  // Handle @media, @keyframes, @supports — recurse into nested block
2749
3485
  if (selector.charAt(0) === '@') {
2750
3486
  const inner = bw.css(styles, options);
@@ -2793,7 +3529,7 @@ bw.css = function(rules, options = {}) {
2793
3529
  */
2794
3530
  bw.injectCSS = function(css, options = {}) {
2795
3531
  if (!bw._isBrowser) {
2796
- console.warn('bw.injectCSS requires a DOM environment');
3532
+ _cw('bw.injectCSS requires a DOM environment');
2797
3533
  return null;
2798
3534
  }
2799
3535
 
@@ -2810,7 +3546,7 @@ bw.injectCSS = function(css, options = {}) {
2810
3546
  }
2811
3547
 
2812
3548
  // Convert CSS if needed
2813
- const cssStr = typeof css === 'string' ? css : bw.css(css, options);
3549
+ const cssStr = _is(css, 'string') ? css : bw.css(css, options);
2814
3550
 
2815
3551
  // Set or append CSS
2816
3552
  if (append && styleEl.textContent) {
@@ -2840,7 +3576,7 @@ bw.s = function() {
2840
3576
  var result = {};
2841
3577
  for (var i = 0; i < arguments.length; i++) {
2842
3578
  var arg = arguments[i];
2843
- if (arg && typeof arg === 'object') Object.assign(result, arg);
3579
+ if (_is(arg, 'object')) Object.assign(result, arg);
2844
3580
  }
2845
3581
  return result;
2846
3582
  };
@@ -2963,7 +3699,7 @@ bw.u = {
2963
3699
  bw.responsive = function(selector, breakpoints) {
2964
3700
  var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
2965
3701
  var parts = [];
2966
- Object.keys(breakpoints).forEach(function(key) {
3702
+ _keys(breakpoints).forEach(function(key) {
2967
3703
  var rules = {};
2968
3704
  if (key === 'base') {
2969
3705
  rules[selector] = breakpoints[key];
@@ -3035,18 +3771,18 @@ if (bw._isBrowser) {
3035
3771
  if (!selector) return [];
3036
3772
 
3037
3773
  // Already an array
3038
- if (Array.isArray(selector)) return selector;
3774
+ if (_isA(selector)) return selector;
3039
3775
 
3040
3776
  // Single element
3041
3777
  if (selector.nodeType) return [selector];
3042
3778
 
3043
3779
  // NodeList or HTMLCollection
3044
- if (selector.length !== undefined && typeof selector !== 'string') {
3780
+ if (selector.length !== undefined && !_is(selector, 'string')) {
3045
3781
  return Array.from(selector);
3046
3782
  }
3047
3783
 
3048
3784
  // CSS selector string
3049
- if (typeof selector === 'string') {
3785
+ if (_is(selector, 'string')) {
3050
3786
  return Array.from(document.querySelectorAll(selector));
3051
3787
  }
3052
3788
 
@@ -3550,7 +4286,7 @@ bw.makeTable = function(config) {
3550
4286
 
3551
4287
  // Auto-detect columns if not provided
3552
4288
  const cols = columns || (data.length > 0
3553
- ? Object.keys(data[0]).map(key => ({ key, label: key }))
4289
+ ? _keys(data[0]).map(key => ({ key, label: key }))
3554
4290
  : []);
3555
4291
 
3556
4292
  // Current sort state
@@ -3565,7 +4301,7 @@ bw.makeTable = function(config) {
3565
4301
  const bVal = b[currentSortColumn];
3566
4302
 
3567
4303
  // Handle different types
3568
- if (typeof aVal === 'number' && typeof bVal === 'number') {
4304
+ if (_is(aVal, 'number') && _is(bVal, 'number')) {
3569
4305
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
3570
4306
  }
3571
4307
 
@@ -3675,7 +4411,7 @@ bw.makeTable = function(config) {
3675
4411
  bw.makeTableFromArray = function(config) {
3676
4412
  const { data = [], headerRow = true, columns, ...rest } = config;
3677
4413
 
3678
- if (!Array.isArray(data) || data.length === 0) {
4414
+ if (!_isA(data) || data.length === 0) {
3679
4415
  return bw.makeTable({ data: [], columns: columns || [], ...rest });
3680
4416
  }
3681
4417
 
@@ -3757,7 +4493,7 @@ bw.makeBarChart = function(config) {
3757
4493
  className = ''
3758
4494
  } = config;
3759
4495
 
3760
- if (!Array.isArray(data) || data.length === 0) {
4496
+ if (!_isA(data) || data.length === 0) {
3761
4497
  return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
3762
4498
  }
3763
4499
 
@@ -3906,7 +4642,7 @@ bw._componentRegistry = new Map();
3906
4642
  */
3907
4643
  bw.render = function(element, position, taco) {
3908
4644
  // Get target element
3909
- const targetEl = typeof element === 'string'
4645
+ const targetEl = _is(element, 'string')
3910
4646
  ? document.querySelector(element)
3911
4647
  : element;
3912
4648
 
@@ -4056,7 +4792,7 @@ bw.render = function(element, position, taco) {
4056
4792
  setContent(content) {
4057
4793
  this._taco.c = content;
4058
4794
  if (this.element) {
4059
- if (typeof content === 'string') {
4795
+ if (_is(content, 'string')) {
4060
4796
  this.element.textContent = content;
4061
4797
  } else {
4062
4798
  // Re-render for complex content