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
@@ -1,18 +1,18 @@
1
- /*! bitwrench-lean v2.0.15 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench-lean v2.0.17 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  /**
3
3
  * Auto-generated version file from package.json
4
4
  * DO NOT EDIT DIRECTLY - Use npm run generate-version
5
5
  */
6
6
 
7
7
  const VERSION_INFO = {
8
- version: '2.0.15',
8
+ version: '2.0.17',
9
9
  name: 'bitwrench',
10
10
  description: 'A library for javascript UI functions.',
11
11
  license: 'BSD-2-Clause',
12
12
  homepage: 'https://deftio.github.com/bitwrench/pages',
13
13
  repository: 'git+https://github.com/deftio/bitwrench.git',
14
14
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
15
- buildDate: '2026-03-10T09:08:17.015Z'
15
+ buildDate: '2026-03-13T23:15:10.823Z'
16
16
  };
17
17
 
18
18
  /**
@@ -430,12 +430,11 @@ function derivePalette(config) {
430
430
  var lightBase = config.light || hslToHex([h, 8, 97]);
431
431
  var darkBase = config.dark || hslToHex([h, 10, 13]);
432
432
 
433
- // Background & surface tokens — default to light (white/near-white).
434
- // Dark backgrounds require explicit config.background / config.surface.
435
- // Primary/secondary colors are accents, not page backgrounds, so
436
- // isLightPalette should NOT drive bg/surface defaults.
437
- var bgBase = config.background || '#ffffff';
438
- var surfBase = config.surface || '#f8f9fa';
433
+ // Background & surface tokens — tinted with primary hue for theme personality.
434
+ // Very subtle: bg at L=98/S=6, surface at L=96/S=8.
435
+ // User can override with config.background / config.surface.
436
+ var bgBase = config.background || hslToHex([h, 6, 98]);
437
+ var surfBase = config.surface || hslToHex([h, 8, 96]);
439
438
 
440
439
  var palette = {
441
440
  primary: deriveShades(config.primary),
@@ -1568,7 +1567,7 @@ var structuralRules = {
1568
1567
  '@media (min-width: 992px)': { '.bw_container': { 'max-width': '960px' } },
1569
1568
  '@media (min-width: 1200px)': { '.bw_container': { 'max-width': '1140px' } },
1570
1569
  '.bw_container_fluid': {
1571
- 'width': '100%', 'padding-right': '15px', 'padding-left': '15px',
1570
+ 'width': '100%', 'padding-right': '0.75rem', 'padding-left': '0.75rem',
1572
1571
  'margin-right': 'auto', 'margin-left': 'auto'
1573
1572
  },
1574
1573
  '.bw_row': {
@@ -1729,7 +1728,8 @@ var structuralRules = {
1729
1728
  '.bw_badge': {
1730
1729
  'display': 'inline-block', 'font-size': '0.875rem',
1731
1730
  'font-weight': '600', 'line-height': '1.3', 'text-align': 'center',
1732
- 'white-space': 'nowrap', 'vertical-align': 'baseline'
1731
+ 'white-space': 'nowrap', 'vertical-align': 'baseline',
1732
+ 'padding': '0.35rem 0.65rem', 'border-radius': '0.25rem'
1733
1733
  },
1734
1734
  '.bw_badge:empty': { 'display': 'none' },
1735
1735
  '.bw_badge_sm': { 'font-size': '0.75rem', 'padding': '0.25rem 0.5rem' },
@@ -1914,7 +1914,7 @@ var structuralRules = {
1914
1914
  // ---- Code demo ----
1915
1915
  codeDemo: {
1916
1916
  '.bw_code_demo': { 'margin-bottom': '2rem' },
1917
- '.bw_code_pre': { 'margin': '0', 'border': 'none', 'overflow-x': 'auto' },
1917
+ '.bw_code_pre': { 'margin': '0', 'border': 'none', 'overflow-x': 'auto', 'max-width': '100%' },
1918
1918
  '.bw_code_block': {
1919
1919
  'display': 'block', 'padding': '1.25rem',
1920
1920
  'font-family': '"SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace',
@@ -2011,7 +2011,7 @@ var structuralRules = {
2011
2011
  },
2012
2012
  '.bw_modal.bw_modal_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
2013
2013
  '.bw_modal_dialog': {
2014
- 'position': 'relative', 'width': '100%', 'max-width': '500px', 'margin': '1.75rem auto',
2014
+ 'position': 'relative', 'width': 'calc(100% - 1rem)', 'max-width': '500px', 'margin': '1.75rem auto',
2015
2015
  'pointer-events': 'none'
2016
2016
  },
2017
2017
  '.bw_modal.bw_modal_show .bw_modal_dialog': { 'transform': 'translateY(0)' },
@@ -2041,7 +2041,7 @@ var structuralRules = {
2041
2041
  '.bw_toast_container.bw_toast_top_center': { 'top': '0', 'left': '50%', 'transform': 'translateX(-50%)' },
2042
2042
  '.bw_toast_container.bw_toast_bottom_center': { 'bottom': '0', 'left': '50%', 'transform': 'translateX(-50%)' },
2043
2043
  '.bw_toast': {
2044
- 'pointer-events': 'auto', 'width': '350px', 'max-width': '100%', 'background-clip': 'padding-box',
2044
+ 'pointer-events': 'auto', 'width': '350px', 'max-width': 'calc(100vw - 2rem)', 'background-clip': 'padding-box',
2045
2045
  'opacity': '0'
2046
2046
  },
2047
2047
  '.bw_toast.bw_toast_show': { 'opacity': '1', 'transform': 'translateY(0)' },
@@ -2127,7 +2127,7 @@ var structuralRules = {
2127
2127
  '.bw_tooltip_wrapper': { 'position': 'relative', 'display': 'inline-block' },
2128
2128
  '.bw_tooltip': {
2129
2129
  'position': 'absolute', 'z-index': '999',
2130
- 'font-size': '0.875rem', 'white-space': 'nowrap', 'pointer-events': 'none',
2130
+ 'font-size': '0.875rem', 'white-space': 'nowrap', 'max-width': 'min(300px, calc(100vw - 1rem))', 'pointer-events': 'none',
2131
2131
  'opacity': '0', 'visibility': 'hidden'
2132
2132
  },
2133
2133
  '.bw_tooltip.bw_tooltip_show': { 'opacity': '1', 'visibility': 'visible' },
@@ -2147,7 +2147,7 @@ var structuralRules = {
2147
2147
  '.bw_popover_trigger': { 'cursor': 'pointer' },
2148
2148
  '.bw_popover': {
2149
2149
  'position': 'absolute', 'z-index': '1000',
2150
- 'min-width': '200px', 'max-width': '320px',
2150
+ 'min-width': '200px', 'max-width': 'min(320px, calc(100vw - 2rem))',
2151
2151
  'pointer-events': 'none', 'opacity': '0', 'visibility': 'hidden'
2152
2152
  },
2153
2153
  '.bw_popover.bw_popover_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
@@ -2330,7 +2330,18 @@ var structuralRules = {
2330
2330
  '.bw_hero, .bw_hero': { 'padding': '2rem 1rem' },
2331
2331
  '.bw_cta_actions, .bw_cta-actions': { 'flex-direction': 'column' },
2332
2332
  '.bw_hstack, .bw_hstack': { 'flex-direction': 'column' },
2333
- '.bw_feature_grid, .bw_feature-grid': { 'grid-template-columns': '1fr' }
2333
+ '.bw_feature_grid, .bw_feature-grid': { 'grid-template-columns': '1fr' },
2334
+ '.bw_modal_dialog': { 'margin': '0.5rem auto' },
2335
+ '.bw_modal_lg': { 'max-width': 'calc(100% - 1rem)' },
2336
+ '.bw_modal_xl': { 'max-width': 'calc(100% - 1rem)' },
2337
+ '.bw_navbar': { 'padding': '0.5rem 0.75rem' },
2338
+ '.bw_navbar_brand': { 'margin-right': '0.5rem', 'font-size': '1rem' },
2339
+ '.bw_navbar_nav': { 'flex-wrap': 'wrap' },
2340
+ '.bw_tooltip': { 'white-space': 'normal' },
2341
+ '.bw_table': { 'display': 'block', 'overflow-x': 'auto', '-webkit-overflow-scrolling': 'touch' },
2342
+ '.bw_col, .bw_col_1, .bw_col_2, .bw_col_3, .bw_col_4, .bw_col_5, .bw_col_6, .bw_col_7, .bw_col_8, .bw_col_9, .bw_col_10, .bw_col_11, .bw_col_12': { 'flex': '0 0 100%', 'max-width': '100%' },
2343
+ '.bw_container': { 'padding-right': '0.5rem', 'padding-left': '0.5rem' },
2344
+ '.bw_container_fluid': { 'padding-right': '0.5rem', 'padding-left': '0.5rem' }
2334
2345
  }
2335
2346
  }
2336
2347
  };
@@ -3332,7 +3343,7 @@ const bw = {
3332
3343
  __monkey_patch_is_nodejs__: {
3333
3344
  _value: 'ignore',
3334
3345
  set: function(x) {
3335
- this._value = (typeof x === 'boolean') ? x : 'ignore';
3346
+ this._value = _is(x, 'boolean') ? x : 'ignore';
3336
3347
  },
3337
3348
  get: function() {
3338
3349
  return this._value;
@@ -3380,6 +3391,67 @@ Object.defineProperty(bw, '_isBrowser', {
3380
3391
  configurable: true
3381
3392
  });
3382
3393
 
3394
+ // ── Internal aliases ─────────────────────────────────────────────────────
3395
+ // Short names for frequently-used builtins and internal methods.
3396
+ // Same pattern as v1 (_to = bw.typeOf, etc.).
3397
+ //
3398
+ // Why: Terser can't shorten global property chains (console.warn,
3399
+ // Object.prototype.hasOwnProperty, Array.isArray, document.createElement)
3400
+ // because it can't prove they're side-effect-free. We can, so we alias
3401
+ // them here. Each alias saves bytes in the minified output, and the short
3402
+ // names also reduce visual noise in the hot paths (binding pipeline,
3403
+ // createDOM, etc.).
3404
+ //
3405
+ // Alias Target Sites
3406
+ // ───────── ────────────────────────────────────── ─────
3407
+ // _hop Object.prototype.hasOwnProperty 15
3408
+ // _isA Array.isArray 25
3409
+ // _keys Object.keys 7
3410
+ // _to bw.typeOf (type string) 26
3411
+ // _is type check boolean: _is(x,'string') ~50
3412
+ // _cw console.warn 8
3413
+ // _cl console.log 11
3414
+ // _ce console.error 4
3415
+ // _chp ComponentHandle.prototype 28 (defined after constructor)
3416
+ //
3417
+ // Note: document.createElement etc. are NOT aliased because they require
3418
+ // `this === document` and .bind() would add overhead on every call.
3419
+ // Console aliases use thin wrappers (not direct refs) so test monkey-
3420
+ // patching of console.warn/log/error continues to work.
3421
+ //
3422
+ // `typeof x` for UNDECLARED globals (window, document, process, require,
3423
+ // EventSource, navigator, Promise, __filename, import.meta) MUST stay as
3424
+ // raw `typeof` — calling _to(x) when x doesn't exist throws ReferenceError.
3425
+ //
3426
+ // ── v1 functional type helpers (kept for reference, not currently used) ──
3427
+ // _toa(x, type, trueVal, falseVal) — bw.typeAssign:
3428
+ // returns trueVal if _to(x)===type, else falseVal.
3429
+ // Replaces: (typeof x === 'string') ? A : B → _toa(x,'string',A,B)
3430
+ // _toc(x, type, trueVal, falseVal) — bw.typeConvert:
3431
+ // same as _toa but if trueVal/falseVal are functions, calls them with x.
3432
+ // Replaces: typeof x === 'string' ? fn(x) : default → _toc(x,'string',fn,default)
3433
+ // Uncomment if pattern frequency justifies them:
3434
+ // var _toa = function(x, t, y, n) { return _to(x) === t ? y : n; };
3435
+ // 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); };
3436
+ // ─────────────────────────────────────────────────────────────────────────
3437
+ var _hop = Object.prototype.hasOwnProperty;
3438
+ var _isA = Array.isArray;
3439
+ var _keys = Object.keys;
3440
+ var _to = typeOf; // imported from bitwrench-utils.js
3441
+ var _is = function(x, t) { var r = _to(x); return r === t || r.toLowerCase() === t; };
3442
+ // Console aliases use thin wrappers (not direct references) so that test
3443
+ // code can monkey-patch console.warn/log/error and the patches take effect.
3444
+ var _cw = function() { console.warn.apply(console, arguments); };
3445
+ var _cl = function() { console.log.apply(console, arguments); };
3446
+ var _ce = function() { console.error.apply(console, arguments); };
3447
+
3448
+ /**
3449
+ * Debug flag. When true, emits console.warn for silent binding failures
3450
+ * (missing paths, null refs, auto-created intermediate objects).
3451
+ * @type {boolean}
3452
+ */
3453
+ bw.debug = false;
3454
+
3383
3455
  /**
3384
3456
  * Lazy-resolve Node.js `fs` module.
3385
3457
  * Tries require('fs') first (available in CJS/UMD Node.js builds),
@@ -3527,7 +3599,7 @@ bw.uuid = function(prefix) {
3527
3599
  */
3528
3600
  bw._el = function(id) {
3529
3601
  // Pass-through for DOM elements
3530
- if (typeof id !== 'string') return id || null;
3602
+ if (!_is(id, 'string')) return id || null;
3531
3603
  if (!id) return null;
3532
3604
  if (!bw._isBrowser) return null;
3533
3605
 
@@ -3623,7 +3695,7 @@ bw._deregisterNode = function(el, bwId) {
3623
3695
  * // => '&lt;b&gt;Hello&lt;&#x2F;b&gt; &amp; &quot;world&quot;'
3624
3696
  */
3625
3697
  bw.escapeHTML = function(str) {
3626
- if (typeof str !== 'string') return '';
3698
+ if (!_is(str, 'string')) return '';
3627
3699
 
3628
3700
  const escapeMap = {
3629
3701
  '&': '&amp;',
@@ -3696,7 +3768,7 @@ bw.html = function(taco, options = {}) {
3696
3768
  }
3697
3769
 
3698
3770
  // Handle arrays of TACOs
3699
- if (Array.isArray(taco)) {
3771
+ if (_isA(taco)) {
3700
3772
  return taco.map(t => bw.html(t, options)).join('');
3701
3773
  }
3702
3774
 
@@ -3719,15 +3791,15 @@ bw.html = function(taco, options = {}) {
3719
3791
  if (taco && taco._bwEach && options.state) {
3720
3792
  var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
3721
3793
  var arr = bw._evaluatePath(options.state, eachExpr);
3722
- if (!Array.isArray(arr)) return '';
3794
+ if (!_isA(arr)) return '';
3723
3795
  return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
3724
3796
  }
3725
3797
 
3726
3798
  // Handle primitives and non-TACO objects
3727
- if (typeof taco !== 'object' || !taco.t) {
3799
+ if (!_is(taco, 'object') || !taco.t) {
3728
3800
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
3729
3801
  // Resolve template bindings if state provided
3730
- if (options.state && typeof str === 'string' && str.indexOf('${') >= 0) {
3802
+ if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
3731
3803
  str = bw._resolveTemplate(str, options.state, !!options.compile);
3732
3804
  }
3733
3805
  return str;
@@ -3747,10 +3819,18 @@ bw.html = function(taco, options = {}) {
3747
3819
  // Skip null, undefined, false
3748
3820
  if (value == null || value === false) continue;
3749
3821
 
3750
- // Skip event handlers (they're for DOM only)
3751
- if (key.startsWith('on')) continue;
3822
+ // Serialize event handlers via funcRegister
3823
+ if (key.startsWith('on')) {
3824
+ if (_is(value, 'function')) {
3825
+ var fnId = bw.funcRegister(value);
3826
+ attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
3827
+ } else if (_is(value, 'string')) {
3828
+ attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
3829
+ }
3830
+ continue;
3831
+ }
3752
3832
 
3753
- if (key === 'style' && typeof value === 'object') {
3833
+ if (key === 'style' && _is(value, 'object')) {
3754
3834
  // Convert style object to string
3755
3835
  const styleStr = Object.entries(value)
3756
3836
  .filter(([, v]) => v != null)
@@ -3761,7 +3841,7 @@ bw.html = function(taco, options = {}) {
3761
3841
  }
3762
3842
  } else if (key === 'class') {
3763
3843
  // Handle class as array or string
3764
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
3844
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
3765
3845
  if (classStr) {
3766
3846
  attrStr += ` class="${bw.escapeHTML(classStr)}"`;
3767
3847
  }
@@ -3797,13 +3877,184 @@ bw.html = function(taco, options = {}) {
3797
3877
  // Process content recursively
3798
3878
  let contentStr = content != null ? bw.html(content, options) : '';
3799
3879
  // Resolve template bindings in content if state provided
3800
- if (options.state && typeof contentStr === 'string' && contentStr.indexOf('${') >= 0) {
3880
+ if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
3801
3881
  contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
3802
3882
  }
3803
3883
 
3804
3884
  return `<${tag}${attrStr}>${contentStr}</${tag}>`;
3805
3885
  };
3806
3886
 
3887
+ /**
3888
+ * Generate a complete, self-contained HTML document from TACO content.
3889
+ *
3890
+ * Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
3891
+ * func registry emission (so serialized event handlers work), optional theme,
3892
+ * and extra head elements. Designed for static site generation, offline/airgapped
3893
+ * use, and the "static site that isn't static" workflow.
3894
+ *
3895
+ * @param {Object} [opts={}] - Page options
3896
+ * @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
3897
+ * @param {string} [opts.title='bitwrench'] - Page title
3898
+ * @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
3899
+ * @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
3900
+ * @param {string} [opts.css=''] - Additional CSS for <style> block
3901
+ * @param {string|Object} [opts.theme=null] - Theme preset name or config object
3902
+ * @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
3903
+ * @param {string} [opts.favicon=''] - Favicon URL
3904
+ * @param {string} [opts.lang='en'] - HTML lang attribute
3905
+ * @returns {string} Complete HTML document string
3906
+ * @category DOM Generation
3907
+ * @see bw.html
3908
+ * @example
3909
+ * bw.htmlPage({
3910
+ * title: 'My App',
3911
+ * body: { t: 'h1', c: 'Hello World' },
3912
+ * runtime: 'shim'
3913
+ * })
3914
+ */
3915
+ bw.htmlPage = function(opts) {
3916
+ opts = opts || {};
3917
+ var title = opts.title || 'bitwrench';
3918
+ var body = opts.body || '';
3919
+ var state = opts.state || undefined;
3920
+ var runtime = opts.runtime || 'shim';
3921
+ var css = opts.css || '';
3922
+ var theme = opts.theme || null;
3923
+ var headExtra = opts.head || [];
3924
+ var favicon = opts.favicon || '';
3925
+ var lang = opts.lang || 'en';
3926
+
3927
+ // Snapshot funcRegistry counter before rendering
3928
+ var fnCounterBefore = bw._fnIDCounter;
3929
+
3930
+ // Render body content
3931
+ var bodyHTML = '';
3932
+ if (_is(body, 'string')) {
3933
+ bodyHTML = body;
3934
+ } else {
3935
+ var htmlOpts = {};
3936
+ if (state) htmlOpts.state = state;
3937
+ bodyHTML = bw.html(body, htmlOpts);
3938
+ }
3939
+
3940
+ // Collect functions registered during this render
3941
+ var fnCounterAfter = bw._fnIDCounter;
3942
+ var registryEntries = '';
3943
+ for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
3944
+ var fnKey = 'bw_fn_' + i;
3945
+ if (bw._fnRegistry[fnKey]) {
3946
+ registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' +
3947
+ bw._fnRegistry[fnKey].toString() + ';\n';
3948
+ }
3949
+ }
3950
+
3951
+ // Build runtime script for <head>
3952
+ var runtimeHead = '';
3953
+ if (runtime === 'inline') {
3954
+ // Read UMD bundle synchronously if in Node.js
3955
+ var umdSource = null;
3956
+ if (bw._isNode) {
3957
+ try {
3958
+ var fs = (typeof require === 'function') ? require('fs') : null;
3959
+ var pathMod = (typeof require === 'function') ? require('path') : null;
3960
+ if (fs && pathMod) {
3961
+ // Resolve dist/ relative to this source file
3962
+ var srcDir = '';
3963
+ try { srcDir = pathMod.dirname((typeof __filename !== 'undefined') ? __filename : ''); }
3964
+ catch(e2) { /* ESM: __filename not available */ }
3965
+ if (!srcDir && typeof import.meta !== 'undefined' && import.meta.url) {
3966
+ var url = (typeof require === 'function') ? require('url') : null;
3967
+ if (url && url.fileURLToPath) srcDir = pathMod.dirname(url.fileURLToPath(import.meta.url));
3968
+ }
3969
+ if (srcDir) {
3970
+ var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
3971
+ umdSource = fs.readFileSync(distPath, 'utf8');
3972
+ }
3973
+ }
3974
+ } catch(e) { /* fall through */ }
3975
+ }
3976
+ if (umdSource) {
3977
+ runtimeHead = '<script>' + umdSource + '</script>';
3978
+ } else {
3979
+ // Fallback to shim in browser or if dist not available
3980
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
3981
+ }
3982
+ } else if (runtime === 'cdn') {
3983
+ runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
3984
+ } else if (runtime === 'shim') {
3985
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
3986
+ }
3987
+ // runtime === 'none' → empty
3988
+
3989
+ // Theme CSS
3990
+ var themeCSS = '';
3991
+ if (theme) {
3992
+ var themeConfig = _is(theme, 'string')
3993
+ ? (THEME_PRESETS[theme.toLowerCase()] || null)
3994
+ : theme;
3995
+ if (themeConfig) {
3996
+ var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, { inject: false }));
3997
+ themeCSS = themeResult.css;
3998
+ }
3999
+ }
4000
+
4001
+ // Extra <head> elements
4002
+ var headHTML = '';
4003
+ if (_isA(headExtra) && headExtra.length > 0) {
4004
+ headHTML = headExtra.map(function(el) { return bw.html(el); }).join('\n');
4005
+ }
4006
+
4007
+ // Favicon
4008
+ var faviconTag = '';
4009
+ if (favicon) {
4010
+ var safeFavicon = favicon.replace(/[&<>"']/g, function(c) {
4011
+ return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
4012
+ });
4013
+ faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
4014
+ }
4015
+
4016
+ // Escaped title
4017
+ var safeTitle = bw.escapeHTML(title);
4018
+
4019
+ // Combine all CSS
4020
+ var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
4021
+
4022
+ // Body-end script: registry entries + optional loadDefaultStyles
4023
+ var bodyEndScript = '';
4024
+ var bodyEndParts = [];
4025
+ if (registryEntries) {
4026
+ bodyEndParts.push(registryEntries);
4027
+ }
4028
+ if (runtime === 'inline' || runtime === 'cdn') {
4029
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
4030
+ }
4031
+ if (bodyEndParts.length > 0) {
4032
+ bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
4033
+ }
4034
+
4035
+ // Assemble document
4036
+ var parts = [
4037
+ '<!DOCTYPE html>',
4038
+ '<html lang="' + lang + '">',
4039
+ '<head>',
4040
+ '<meta charset="UTF-8">',
4041
+ '<meta name="viewport" content="width=device-width, initial-scale=1">'
4042
+ ];
4043
+ parts.push('<title>' + safeTitle + '</title>');
4044
+ if (faviconTag) parts.push(faviconTag);
4045
+ if (runtimeHead) parts.push(runtimeHead);
4046
+ if (headHTML) parts.push(headHTML);
4047
+ if (allCSS) parts.push('<style>' + allCSS + '</style>');
4048
+ parts.push('</head>');
4049
+ parts.push('<body>');
4050
+ parts.push(bodyHTML);
4051
+ if (bodyEndScript) parts.push(bodyEndScript);
4052
+ parts.push('</body>');
4053
+ parts.push('</html>');
4054
+
4055
+ return parts.join('\n');
4056
+ };
4057
+
3807
4058
  /**
3808
4059
  * Create a live DOM element from a TACO object (browser only).
3809
4060
  *
@@ -3848,7 +4099,7 @@ bw.createDOM = function(taco, options = {}) {
3848
4099
  }
3849
4100
 
3850
4101
  // Handle text nodes
3851
- if (typeof taco !== 'object' || !taco.t) {
4102
+ if (!_is(taco, 'object') || !taco.t) {
3852
4103
  return document.createTextNode(String(taco));
3853
4104
  }
3854
4105
 
@@ -3861,16 +4112,16 @@ bw.createDOM = function(taco, options = {}) {
3861
4112
  for (const [key, value] of Object.entries(attrs)) {
3862
4113
  if (value == null || value === false) continue;
3863
4114
 
3864
- if (key === 'style' && typeof value === 'object') {
4115
+ if (key === 'style' && _is(value, 'object')) {
3865
4116
  // Apply styles directly
3866
4117
  Object.assign(el.style, value);
3867
4118
  } else if (key === 'class') {
3868
4119
  // Handle class as array or string
3869
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
4120
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
3870
4121
  if (classStr) {
3871
4122
  el.className = classStr;
3872
4123
  }
3873
- } else if (key.startsWith('on') && typeof value === 'function') {
4124
+ } else if (key.startsWith('on') && _is(value, 'function')) {
3874
4125
  // Event handlers
3875
4126
  const eventName = key.slice(2).toLowerCase();
3876
4127
  el.addEventListener(eventName, value);
@@ -3890,7 +4141,7 @@ bw.createDOM = function(taco, options = {}) {
3890
4141
  // Children with data-bw_id or id attributes get local refs on the parent,
3891
4142
  // so o.render functions can access them without any DOM lookup.
3892
4143
  if (content != null) {
3893
- if (Array.isArray(content)) {
4144
+ if (_isA(content)) {
3894
4145
  content.forEach(child => {
3895
4146
  if (child != null) {
3896
4147
  // Handle ComponentHandle in content arrays (Level 2 children)
@@ -3910,20 +4161,20 @@ bw.createDOM = function(taco, options = {}) {
3910
4161
  if (childEl._bw_refs) {
3911
4162
  if (!el._bw_refs) el._bw_refs = {};
3912
4163
  for (var rk in childEl._bw_refs) {
3913
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
4164
+ if (_hop.call(childEl._bw_refs, rk)) {
3914
4165
  el._bw_refs[rk] = childEl._bw_refs[rk];
3915
4166
  }
3916
4167
  }
3917
4168
  }
3918
4169
  }
3919
4170
  });
3920
- } else if (typeof content === 'object' && content.__bw_raw) {
4171
+ } else if (_is(content, 'object') && content.__bw_raw) {
3921
4172
  // Raw HTML content — inject via innerHTML
3922
4173
  el.innerHTML = content.v;
3923
4174
  } else if (content._bwComponent === true) {
3924
4175
  // Single ComponentHandle as content
3925
4176
  content.mount(el);
3926
- } else if (typeof content === 'object' && content.t) {
4177
+ } else if (_is(content, 'object') && content.t) {
3927
4178
  var childEl = bw.createDOM(content, options);
3928
4179
  el.appendChild(childEl);
3929
4180
  var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
@@ -3934,7 +4185,7 @@ bw.createDOM = function(taco, options = {}) {
3934
4185
  if (childEl._bw_refs) {
3935
4186
  if (!el._bw_refs) el._bw_refs = {};
3936
4187
  for (var rk in childEl._bw_refs) {
3937
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
4188
+ if (_hop.call(childEl._bw_refs, rk)) {
3938
4189
  el._bw_refs[rk] = childEl._bw_refs[rk];
3939
4190
  }
3940
4191
  }
@@ -3967,7 +4218,7 @@ bw.createDOM = function(taco, options = {}) {
3967
4218
  el._bw_render = opts.render;
3968
4219
 
3969
4220
  if (opts.mounted) {
3970
- console.warn('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
4221
+ _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
3971
4222
  }
3972
4223
 
3973
4224
  // Queue initial render (same timing as mounted)
@@ -4040,7 +4291,7 @@ bw.DOM = function(target, taco, options = {}) {
4040
4291
  const targetEl = bw._el(target);
4041
4292
 
4042
4293
  if (!targetEl) {
4043
- console.error('bw.DOM: Target element not found:', target);
4294
+ _ce('bw.DOM: Target element not found:', target);
4044
4295
  return null;
4045
4296
  }
4046
4297
 
@@ -4080,7 +4331,7 @@ bw.DOM = function(target, taco, options = {}) {
4080
4331
  targetEl.appendChild(taco.element);
4081
4332
  }
4082
4333
  // Handle arrays
4083
- else if (Array.isArray(taco)) {
4334
+ else if (_isA(taco)) {
4084
4335
  taco.forEach(t => {
4085
4336
  if (t != null) {
4086
4337
  if (t._bwComponent === true) {
@@ -4116,7 +4367,7 @@ bw.DOM = function(target, taco, options = {}) {
4116
4367
  bw.compileProps = function(handle, props = {}) {
4117
4368
  const compiledProps = {};
4118
4369
 
4119
- Object.keys(props).forEach(key => {
4370
+ _keys(props).forEach(key => {
4120
4371
  // Create getter/setter for each prop
4121
4372
  Object.defineProperty(compiledProps, key, {
4122
4373
  get() {
@@ -4434,17 +4685,17 @@ bw.patch = function(id, content, attr) {
4434
4685
  if (attr) {
4435
4686
  // Patch an attribute
4436
4687
  el.setAttribute(attr, String(content));
4437
- } else if (Array.isArray(content)) {
4688
+ } else if (_isA(content)) {
4438
4689
  // Patch with array of children (strings and/or TACOs)
4439
4690
  el.innerHTML = '';
4440
4691
  content.forEach(function(item) {
4441
- if (typeof item === 'string' || typeof item === 'number') {
4692
+ if (_is(item, 'string') || _is(item, 'number')) {
4442
4693
  el.appendChild(document.createTextNode(String(item)));
4443
4694
  } else if (item && item.t) {
4444
4695
  el.appendChild(bw.createDOM(item));
4445
4696
  }
4446
4697
  });
4447
- } else if (typeof content === 'object' && content !== null && content.t) {
4698
+ } else if (_is(content, 'object') && content.t) {
4448
4699
  // Patch with a TACO — replace children
4449
4700
  el.innerHTML = '';
4450
4701
  el.appendChild(bw.createDOM(content));
@@ -4475,7 +4726,7 @@ bw.patch = function(id, content, attr) {
4475
4726
  bw.patchAll = function(patches) {
4476
4727
  var results = {};
4477
4728
  for (var id in patches) {
4478
- if (Object.prototype.hasOwnProperty.call(patches, id)) {
4729
+ if (_hop.call(patches, id)) {
4479
4730
  results[id] = bw.patch(id, patches[id]);
4480
4731
  }
4481
4732
  }
@@ -4572,7 +4823,7 @@ bw.pub = function(topic, detail) {
4572
4823
  snapshot[i].handler(detail);
4573
4824
  called++;
4574
4825
  } catch (err) {
4575
- console.warn('bw.pub: subscriber error on topic "' + topic + '":', err);
4826
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
4576
4827
  }
4577
4828
  }
4578
4829
  return called;
@@ -4668,8 +4919,8 @@ bw._fnIDCounter = 0;
4668
4919
  * @see bw.funcGetDispatchStr
4669
4920
  */
4670
4921
  bw.funcRegister = function(fn, name) {
4671
- if (typeof fn !== 'function') return '';
4672
- var fnID = (typeof name === 'string' && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
4922
+ if (!_is(fn, 'function')) return '';
4923
+ var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
4673
4924
  bw._fnRegistry[fnID] = fn;
4674
4925
  return fnID;
4675
4926
  };
@@ -4688,7 +4939,7 @@ bw.funcRegister = function(fn, name) {
4688
4939
  bw.funcGetById = function(name, errFn) {
4689
4940
  name = String(name);
4690
4941
  if (name in bw._fnRegistry) return bw._fnRegistry[name];
4691
- return (typeof errFn === 'function') ? errFn : function() { console.warn('bw.funcGetById: unregistered fn "' + name + '"'); };
4942
+ return _is(errFn, 'function') ? errFn : function() { _cw('bw.funcGetById: unregistered fn "' + name + '"'); };
4692
4943
  };
4693
4944
 
4694
4945
  /**
@@ -4729,13 +4980,30 @@ bw.funcUnregister = function(name) {
4729
4980
  bw.funcGetRegistry = function() {
4730
4981
  var copy = {};
4731
4982
  for (var k in bw._fnRegistry) {
4732
- if (Object.prototype.hasOwnProperty.call(bw._fnRegistry, k)) {
4983
+ if (_hop.call(bw._fnRegistry, k)) {
4733
4984
  copy[k] = bw._fnRegistry[k];
4734
4985
  }
4735
4986
  }
4736
4987
  return copy;
4737
4988
  };
4738
4989
 
4990
+ /**
4991
+ * Minimal runtime shim for funcRegister dispatch in static HTML.
4992
+ * When embedded in a `<script>` tag, provides just enough infrastructure
4993
+ * for `bw.funcGetById()` calls to resolve. The actual function bodies
4994
+ * are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
4995
+ * @type {string}
4996
+ * @category Function Registry
4997
+ */
4998
+ bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' +
4999
+ 'if(!bw._fnRegistry)bw._fnRegistry={};' +
5000
+ 'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' +
5001
+ 'console.warn("bw: unregistered fn "+n)};};' +
5002
+ 'bw.funcRegister=function(fn,name){' +
5003
+ 'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' +
5004
+ 'bw._fnRegistry[id]=fn;return id;};' +
5005
+ 'window.bw=bw;})();';
5006
+
4739
5007
  // ===================================================================================
4740
5008
  // Template Binding Utilities
4741
5009
  // ===================================================================================
@@ -4763,7 +5031,10 @@ bw._evaluatePath = function(state, path) {
4763
5031
  var parts = path.split('.');
4764
5032
  var val = state;
4765
5033
  for (var i = 0; i < parts.length; i++) {
4766
- if (val == null) return '';
5034
+ if (val == null) {
5035
+ if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
5036
+ return '';
5037
+ }
4767
5038
  val = val[parts[i]];
4768
5039
  }
4769
5040
  return (val == null) ? '' : val;
@@ -4783,7 +5054,7 @@ bw._evaluatePath = function(state, path) {
4783
5054
  */
4784
5055
  bw._compiledExprs = {};
4785
5056
  bw._resolveTemplate = function(str, state, compile) {
4786
- if (typeof str !== 'string' || str.indexOf('${') < 0) return str;
5057
+ if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
4787
5058
  var bindings = bw._parseBindings(str);
4788
5059
  if (bindings.length === 0) return str;
4789
5060
 
@@ -4805,6 +5076,7 @@ bw._resolveTemplate = function(str, state, compile) {
4805
5076
  try {
4806
5077
  val = bw._compiledExprs[b.expr](state);
4807
5078
  } catch (e) {
5079
+ if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
4808
5080
  val = '';
4809
5081
  }
4810
5082
  } else {
@@ -4913,7 +5185,7 @@ function ComponentHandle(taco) {
4913
5185
  this._state = {};
4914
5186
  if (o.state) {
4915
5187
  for (var k in o.state) {
4916
- if (Object.prototype.hasOwnProperty.call(o.state, k)) {
5188
+ if (_hop.call(o.state, k)) {
4917
5189
  this._state[k] = o.state[k];
4918
5190
  }
4919
5191
  }
@@ -4922,7 +5194,7 @@ function ComponentHandle(taco) {
4922
5194
  this._actions = {};
4923
5195
  if (o.actions) {
4924
5196
  for (var k2 in o.actions) {
4925
- if (Object.prototype.hasOwnProperty.call(o.actions, k2)) {
5197
+ if (_hop.call(o.actions, k2)) {
4926
5198
  this._actions[k2] = o.actions[k2];
4927
5199
  }
4928
5200
  }
@@ -4932,7 +5204,7 @@ function ComponentHandle(taco) {
4932
5204
  if (o.methods) {
4933
5205
  var self = this;
4934
5206
  for (var k3 in o.methods) {
4935
- if (Object.prototype.hasOwnProperty.call(o.methods, k3)) {
5207
+ if (_hop.call(o.methods, k3)) {
4936
5208
  this._methods[k3] = o.methods[k3];
4937
5209
  (function(methodName, methodFn) {
4938
5210
  self[methodName] = function() {
@@ -4965,14 +5237,23 @@ function ComponentHandle(taco) {
4965
5237
  this._compile = !!o.compile;
4966
5238
  this._bw_refs = {};
4967
5239
  this._refCounter = 0;
5240
+ // Child component ownership (Bug #5)
5241
+ this._children = [];
5242
+ this._parent = null;
5243
+ // Factory metadata for BCCL rebuild (Bug #6)
5244
+ this._factory = taco._bwFactory || null;
4968
5245
  }
4969
5246
 
5247
+ // Short alias for ComponentHandle.prototype (see alias block at top of file).
5248
+ // 28 method definitions × 25 chars = ~700B raw savings in minified output.
5249
+ var _chp = ComponentHandle.prototype;
5250
+
4970
5251
  // ── State Methods ──
4971
5252
 
4972
5253
  /**
4973
5254
  * Get a state value. Dot-path supported: `get('user.name')`
4974
5255
  */
4975
- ComponentHandle.prototype.get = function(key) {
5256
+ _chp.get = function(key) {
4976
5257
  return bw._evaluatePath(this._state, key);
4977
5258
  };
4978
5259
 
@@ -4982,12 +5263,13 @@ ComponentHandle.prototype.get = function(key) {
4982
5263
  * @param {*} value - New value
4983
5264
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
4984
5265
  */
4985
- ComponentHandle.prototype.set = function(key, value, opts) {
5266
+ _chp.set = function(key, value, opts) {
4986
5267
  // Dot-path set
4987
5268
  var parts = key.split('.');
4988
5269
  var obj = this._state;
4989
5270
  for (var i = 0; i < parts.length - 1; i++) {
4990
- if (obj[parts[i]] == null || typeof obj[parts[i]] !== 'object') {
5271
+ if (!_is(obj[parts[i]], 'object')) {
5272
+ if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
4991
5273
  obj[parts[i]] = {};
4992
5274
  }
4993
5275
  obj = obj[parts[i]];
@@ -5007,10 +5289,10 @@ ComponentHandle.prototype.set = function(key, value, opts) {
5007
5289
  /**
5008
5290
  * Get a shallow clone of the full state.
5009
5291
  */
5010
- ComponentHandle.prototype.getState = function() {
5292
+ _chp.getState = function() {
5011
5293
  var clone = {};
5012
5294
  for (var k in this._state) {
5013
- if (Object.prototype.hasOwnProperty.call(this._state, k)) {
5295
+ if (_hop.call(this._state, k)) {
5014
5296
  clone[k] = this._state[k];
5015
5297
  }
5016
5298
  }
@@ -5022,9 +5304,9 @@ ComponentHandle.prototype.getState = function() {
5022
5304
  * @param {Object} updates - Key-value pairs to merge
5023
5305
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
5024
5306
  */
5025
- ComponentHandle.prototype.setState = function(updates, opts) {
5307
+ _chp.setState = function(updates, opts) {
5026
5308
  for (var k in updates) {
5027
- if (Object.prototype.hasOwnProperty.call(updates, k)) {
5309
+ if (_hop.call(updates, k)) {
5028
5310
  this._state[k] = updates[k];
5029
5311
  this._dirtyKeys[k] = true;
5030
5312
  }
@@ -5041,9 +5323,9 @@ ComponentHandle.prototype.setState = function(updates, opts) {
5041
5323
  /**
5042
5324
  * Push a value onto an array in state. Clones the array.
5043
5325
  */
5044
- ComponentHandle.prototype.push = function(key, val) {
5326
+ _chp.push = function(key, val) {
5045
5327
  var arr = this.get(key);
5046
- var newArr = Array.isArray(arr) ? arr.slice() : [];
5328
+ var newArr = _isA(arr) ? arr.slice() : [];
5047
5329
  newArr.push(val);
5048
5330
  this.set(key, newArr);
5049
5331
  };
@@ -5051,9 +5333,9 @@ ComponentHandle.prototype.push = function(key, val) {
5051
5333
  /**
5052
5334
  * Splice an array in state. Clones the array.
5053
5335
  */
5054
- ComponentHandle.prototype.splice = function(key, start, deleteCount) {
5336
+ _chp.splice = function(key, start, deleteCount) {
5055
5337
  var arr = this.get(key);
5056
- var newArr = Array.isArray(arr) ? arr.slice() : [];
5338
+ var newArr = _isA(arr) ? arr.slice() : [];
5057
5339
  var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
5058
5340
  Array.prototype.splice.apply(newArr, args);
5059
5341
  this.set(key, newArr);
@@ -5061,7 +5343,7 @@ ComponentHandle.prototype.splice = function(key, start, deleteCount) {
5061
5343
 
5062
5344
  // ── Scheduling ──
5063
5345
 
5064
- ComponentHandle.prototype._scheduleDirty = function() {
5346
+ _chp._scheduleDirty = function() {
5065
5347
  if (!this._scheduled) {
5066
5348
  this._scheduled = true;
5067
5349
  bw._dirtyComponents.push(this);
@@ -5076,17 +5358,17 @@ ComponentHandle.prototype._scheduleDirty = function() {
5076
5358
  * Creates binding descriptors with refIds for targeted DOM updates.
5077
5359
  * @private
5078
5360
  */
5079
- ComponentHandle.prototype._compileBindings = function() {
5361
+ _chp._compileBindings = function() {
5080
5362
  this._bindings = [];
5081
5363
  this._refCounter = 0;
5082
- var stateKeys = Object.keys(this._state);
5364
+ var stateKeys = _keys(this._state);
5083
5365
  var self = this;
5084
5366
 
5085
5367
  function walkTaco(taco, path) {
5086
- if (taco == null || typeof taco !== 'object' || !taco.t) return taco;
5368
+ if (!_is(taco, 'object') || !taco.t) return taco;
5087
5369
 
5088
5370
  // Check content for bindings
5089
- if (typeof taco.c === 'string' && taco.c.indexOf('${') >= 0) {
5371
+ if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
5090
5372
  var refId = 'bw_ref_' + self._refCounter++;
5091
5373
  var parsed = bw._parseBindings(taco.c);
5092
5374
  var deps = [];
@@ -5108,10 +5390,10 @@ ComponentHandle.prototype._compileBindings = function() {
5108
5390
  // Check attributes for bindings
5109
5391
  if (taco.a) {
5110
5392
  for (var attrName in taco.a) {
5111
- if (!Object.prototype.hasOwnProperty.call(taco.a, attrName)) continue;
5393
+ if (!_hop.call(taco.a, attrName)) continue;
5112
5394
  if (attrName === 'data-bw_ref') continue;
5113
5395
  var attrVal = taco.a[attrName];
5114
- if (typeof attrVal === 'string' && attrVal.indexOf('${') >= 0) {
5396
+ if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
5115
5397
  var refId2 = 'bw_ref_' + self._refCounter++;
5116
5398
  var parsed2 = bw._parseBindings(attrVal);
5117
5399
  var deps2 = [];
@@ -5137,9 +5419,27 @@ ComponentHandle.prototype._compileBindings = function() {
5137
5419
  }
5138
5420
 
5139
5421
  // Recurse into children
5140
- if (Array.isArray(taco.c)) {
5422
+ if (_isA(taco.c)) {
5141
5423
  for (var i = 0; i < taco.c.length; i++) {
5142
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
5424
+ // Wrap string children with ${expr} in a span so patches target the span, not the parent
5425
+ if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
5426
+ var mixedRefId = 'bw_ref_' + self._refCounter++;
5427
+ var mixedParsed = bw._parseBindings(taco.c[i]);
5428
+ var mixedDeps = [];
5429
+ for (var mi = 0; mi < mixedParsed.length; mi++) {
5430
+ mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
5431
+ }
5432
+ self._bindings.push({
5433
+ expr: taco.c[i],
5434
+ type: 'content',
5435
+ refId: mixedRefId,
5436
+ deps: mixedDeps,
5437
+ template: taco.c[i]
5438
+ });
5439
+ // Replace string with a span wrapper so textContent targets the span only
5440
+ taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
5441
+ }
5442
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
5143
5443
  walkTaco(taco.c[i], path.concat(i));
5144
5444
  }
5145
5445
  // Handle bw.when/bw.each markers
@@ -5174,7 +5474,7 @@ ComponentHandle.prototype._compileBindings = function() {
5174
5474
  taco.c[i]._refId = eachRefId;
5175
5475
  }
5176
5476
  }
5177
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
5477
+ } else if (_is(taco.c, 'object') && taco.c.t) {
5178
5478
  walkTaco(taco.c, path.concat(0));
5179
5479
  }
5180
5480
 
@@ -5190,7 +5490,7 @@ ComponentHandle.prototype._compileBindings = function() {
5190
5490
  * Build ref map from the live DOM after createDOM.
5191
5491
  * @private
5192
5492
  */
5193
- ComponentHandle.prototype._collectRefs = function() {
5493
+ _chp._collectRefs = function() {
5194
5494
  this._bw_refs = {};
5195
5495
  if (!this.element) return;
5196
5496
  var els = this.element.querySelectorAll('[data-bw_ref]');
@@ -5211,7 +5511,7 @@ ComponentHandle.prototype._collectRefs = function() {
5211
5511
  * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
5212
5512
  * @param {Element} parentEl - DOM element to mount into
5213
5513
  */
5214
- ComponentHandle.prototype.mount = function(parentEl) {
5514
+ _chp.mount = function(parentEl) {
5215
5515
  // willMount hook
5216
5516
  if (this._hooks.willMount) this._hooks.willMount(this);
5217
5517
 
@@ -5233,7 +5533,7 @@ ComponentHandle.prototype.mount = function(parentEl) {
5233
5533
  // Register named actions in function registry
5234
5534
  var self = this;
5235
5535
  for (var actionName in this._actions) {
5236
- if (Object.prototype.hasOwnProperty.call(this._actions, actionName)) {
5536
+ if (_hop.call(this._actions, actionName)) {
5237
5537
  var registeredName = this._bwId + '_' + actionName;
5238
5538
  (function(aName) {
5239
5539
  bw.funcRegister(function(evt) {
@@ -5252,6 +5552,11 @@ ComponentHandle.prototype.mount = function(parentEl) {
5252
5552
  this.element = bw.createDOM(tacoForDOM);
5253
5553
  this.element._bwComponentHandle = this;
5254
5554
  this.element.setAttribute('data-bw_comp_id', this._bwId);
5555
+
5556
+ // Restore o.render from original TACO (stripped by _tacoForDOM)
5557
+ if (this.taco.o && this.taco.o.render) {
5558
+ this.element._bw_render = this.taco.o.render;
5559
+ }
5255
5560
  if (this._userTag) {
5256
5561
  this.element.classList.add(this._userTag);
5257
5562
  }
@@ -5267,6 +5572,16 @@ ComponentHandle.prototype.mount = function(parentEl) {
5267
5572
 
5268
5573
  this.mounted = true;
5269
5574
 
5575
+ // Scan for child ComponentHandles and link parent/child (Bug #5)
5576
+ var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
5577
+ for (var ci = 0; ci < childEls.length; ci++) {
5578
+ var ch = childEls[ci]._bwComponentHandle;
5579
+ if (ch && ch !== this && !ch._parent) {
5580
+ ch._parent = this;
5581
+ this._children.push(ch);
5582
+ }
5583
+ }
5584
+
5270
5585
  // mounted hook (backward compat: fn.length === 2 wraps (el, state))
5271
5586
  if (this._hooks.mounted) {
5272
5587
  if (this._hooks.mounted.length === 2) {
@@ -5275,16 +5590,21 @@ ComponentHandle.prototype.mount = function(parentEl) {
5275
5590
  this._hooks.mounted(this);
5276
5591
  }
5277
5592
  }
5593
+
5594
+ // Invoke o.render on initial mount (if present)
5595
+ if (this.element._bw_render) {
5596
+ this.element._bw_render(this.element, this._state);
5597
+ }
5278
5598
  };
5279
5599
 
5280
5600
  /**
5281
5601
  * Prepare TACO for initial render: resolve when/each markers.
5282
5602
  * @private
5283
5603
  */
5284
- ComponentHandle.prototype._prepareTaco = function(taco) {
5285
- if (!taco || typeof taco !== 'object') return;
5604
+ _chp._prepareTaco = function(taco) {
5605
+ if (!_is(taco, 'object')) return;
5286
5606
 
5287
- if (Array.isArray(taco.c)) {
5607
+ if (_isA(taco.c)) {
5288
5608
  for (var i = taco.c.length - 1; i >= 0; i--) {
5289
5609
  var child = taco.c[i];
5290
5610
  if (child && child._bwWhen) {
@@ -5309,18 +5629,18 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
5309
5629
  var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
5310
5630
  var arr = bw._evaluatePath(this._state, eachExprStr);
5311
5631
  var items = [];
5312
- if (Array.isArray(arr)) {
5632
+ if (_isA(arr)) {
5313
5633
  for (var j = 0; j < arr.length; j++) {
5314
5634
  items.push(child.factory(arr[j], j));
5315
5635
  }
5316
5636
  }
5317
5637
  taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
5318
5638
  }
5319
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
5639
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
5320
5640
  this._prepareTaco(taco.c[i]);
5321
5641
  }
5322
5642
  }
5323
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
5643
+ } else if (_is(taco.c, 'object') && taco.c.t) {
5324
5644
  this._prepareTaco(taco.c);
5325
5645
  }
5326
5646
  };
@@ -5329,12 +5649,12 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
5329
5649
  * Wire action name strings (in onclick etc.) to dispatch function calls.
5330
5650
  * @private
5331
5651
  */
5332
- ComponentHandle.prototype._wireActions = function(taco) {
5333
- if (!taco || typeof taco !== 'object' || !taco.t) return;
5652
+ _chp._wireActions = function(taco) {
5653
+ if (!_is(taco, 'object') || !taco.t) return;
5334
5654
  if (taco.a) {
5335
5655
  for (var key in taco.a) {
5336
- if (!Object.prototype.hasOwnProperty.call(taco.a, key)) continue;
5337
- if (key.startsWith('on') && typeof taco.a[key] === 'string') {
5656
+ if (!_hop.call(taco.a, key)) continue;
5657
+ if (key.startsWith('on') && _is(taco.a[key], 'string')) {
5338
5658
  var actionName = taco.a[key];
5339
5659
  if (actionName in this._actions) {
5340
5660
  var registeredName = this._bwId + '_' + actionName;
@@ -5348,11 +5668,11 @@ ComponentHandle.prototype._wireActions = function(taco) {
5348
5668
  }
5349
5669
  }
5350
5670
  }
5351
- if (Array.isArray(taco.c)) {
5671
+ if (_isA(taco.c)) {
5352
5672
  for (var i = 0; i < taco.c.length; i++) {
5353
5673
  this._wireActions(taco.c[i]);
5354
5674
  }
5355
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
5675
+ } else if (_is(taco.c, 'object') && taco.c.t) {
5356
5676
  this._wireActions(taco.c);
5357
5677
  }
5358
5678
  };
@@ -5361,7 +5681,7 @@ ComponentHandle.prototype._wireActions = function(taco) {
5361
5681
  * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
5362
5682
  * @private
5363
5683
  */
5364
- ComponentHandle.prototype._deepCloneTaco = function(taco) {
5684
+ _chp._deepCloneTaco = function(taco) {
5365
5685
  if (taco == null) return taco;
5366
5686
  // Preserve _bwWhen / _bwEach markers (contain functions)
5367
5687
  if (taco._bwWhen) {
@@ -5373,18 +5693,18 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
5373
5693
  if (taco._bwEach) {
5374
5694
  return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
5375
5695
  }
5376
- if (typeof taco !== 'object' || !taco.t) return taco;
5696
+ if (!_is(taco, 'object') || !taco.t) return taco;
5377
5697
  var result = { t: taco.t };
5378
5698
  if (taco.a) {
5379
5699
  result.a = {};
5380
5700
  for (var k in taco.a) {
5381
- if (Object.prototype.hasOwnProperty.call(taco.a, k)) result.a[k] = taco.a[k];
5701
+ if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
5382
5702
  }
5383
5703
  }
5384
5704
  if (taco.c != null) {
5385
- if (Array.isArray(taco.c)) {
5705
+ if (_isA(taco.c)) {
5386
5706
  result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
5387
- } else if (typeof taco.c === 'object') {
5707
+ } else if (_is(taco.c, 'object')) {
5388
5708
  result.c = this._deepCloneTaco(taco.c);
5389
5709
  } else {
5390
5710
  result.c = taco.c;
@@ -5398,27 +5718,31 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
5398
5718
  * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
5399
5719
  * @private
5400
5720
  */
5401
- ComponentHandle.prototype._tacoForDOM = function(taco) {
5402
- if (!taco || typeof taco !== 'object' || !taco.t) return taco;
5721
+ _chp._tacoForDOM = function(taco) {
5722
+ if (!_is(taco, 'object') || !taco.t) return taco;
5403
5723
  var result = { t: taco.t };
5404
5724
  if (taco.a) result.a = taco.a;
5405
5725
  if (taco.c != null) {
5406
- if (Array.isArray(taco.c)) {
5726
+ if (_isA(taco.c)) {
5407
5727
  result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
5408
- } else if (typeof taco.c === 'object' && taco.c.t) {
5728
+ } else if (_is(taco.c, 'object') && taco.c.t) {
5409
5729
  result.c = this._tacoForDOM(taco.c);
5410
5730
  } else {
5411
5731
  result.c = taco.c;
5412
5732
  }
5413
5733
  }
5414
5734
  // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
5735
+ if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
5736
+ _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
5737
+ '>. Use onclick attribute or bw.component() for child interactivity.');
5738
+ }
5415
5739
  return result;
5416
5740
  };
5417
5741
 
5418
5742
  /**
5419
5743
  * Unmount: remove from DOM, deactivate, preserve state for re-mount.
5420
5744
  */
5421
- ComponentHandle.prototype.unmount = function() {
5745
+ _chp.unmount = function() {
5422
5746
  if (!this.mounted) return;
5423
5747
 
5424
5748
  // unmount hook
@@ -5453,12 +5777,23 @@ ComponentHandle.prototype.unmount = function() {
5453
5777
  /**
5454
5778
  * Destroy: unmount + clear state + unregister actions.
5455
5779
  */
5456
- ComponentHandle.prototype.destroy = function() {
5780
+ _chp.destroy = function() {
5457
5781
  // willDestroy hook
5458
5782
  if (this._hooks.willDestroy) {
5459
5783
  this._hooks.willDestroy(this);
5460
5784
  }
5461
5785
 
5786
+ // Cascade destroy to children depth-first (Bug #5)
5787
+ for (var ci = this._children.length - 1; ci >= 0; ci--) {
5788
+ this._children[ci].destroy();
5789
+ }
5790
+ this._children = [];
5791
+ if (this._parent) {
5792
+ var idx = this._parent._children.indexOf(this);
5793
+ if (idx >= 0) this._parent._children.splice(idx, 1);
5794
+ this._parent = null;
5795
+ }
5796
+
5462
5797
  this.unmount();
5463
5798
 
5464
5799
  // Unregister actions from function registry
@@ -5485,12 +5820,36 @@ ComponentHandle.prototype.destroy = function() {
5485
5820
  * Flush dirty state: resolve changed bindings and apply to DOM.
5486
5821
  * @private
5487
5822
  */
5488
- ComponentHandle.prototype._flush = function() {
5823
+ _chp._flush = function() {
5489
5824
  this._scheduled = false;
5490
- var changedKeys = Object.keys(this._dirtyKeys);
5825
+ var changedKeys = _keys(this._dirtyKeys);
5491
5826
  this._dirtyKeys = {};
5492
5827
  if (changedKeys.length === 0 || !this.mounted) return;
5493
5828
 
5829
+ // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
5830
+ // rebuild the TACO from the factory with merged state (Bug #6)
5831
+ if (this._factory) {
5832
+ var rebuildNeeded = false;
5833
+ for (var fi = 0; fi < changedKeys.length; fi++) {
5834
+ if (_hop.call(this._factory.props, changedKeys[fi])) {
5835
+ rebuildNeeded = true; break;
5836
+ }
5837
+ }
5838
+ if (rebuildNeeded) {
5839
+ var merged = {};
5840
+ for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
5841
+ for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
5842
+ this._factory.props = merged;
5843
+ var newTaco = bw.make(this._factory.type, merged);
5844
+ newTaco._bwFactory = this._factory;
5845
+ this.taco = newTaco;
5846
+ this._originalTaco = this._deepCloneTaco(newTaco);
5847
+ this._render();
5848
+ if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
5849
+ return;
5850
+ }
5851
+ }
5852
+
5494
5853
  // willUpdate hook
5495
5854
  if (this._hooks.willUpdate) {
5496
5855
  this._hooks.willUpdate(this, changedKeys);
@@ -5529,7 +5888,7 @@ ComponentHandle.prototype._flush = function() {
5529
5888
  * Returns list of patches to apply.
5530
5889
  * @private
5531
5890
  */
5532
- ComponentHandle.prototype._resolveBindings = function(changedKeys) {
5891
+ _chp._resolveBindings = function(changedKeys) {
5533
5892
  var patches = [];
5534
5893
  for (var i = 0; i < this._bindings.length; i++) {
5535
5894
  var b = this._bindings[i];
@@ -5565,11 +5924,14 @@ ComponentHandle.prototype._resolveBindings = function(changedKeys) {
5565
5924
  * Apply patches to DOM.
5566
5925
  * @private
5567
5926
  */
5568
- ComponentHandle.prototype._applyPatches = function(patches) {
5927
+ _chp._applyPatches = function(patches) {
5569
5928
  for (var i = 0; i < patches.length; i++) {
5570
5929
  var p = patches[i];
5571
5930
  var el = this._bw_refs[p.refId];
5572
- if (!el) continue;
5931
+ if (!el) {
5932
+ if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
5933
+ continue;
5934
+ }
5573
5935
  if (p.type === 'content') {
5574
5936
  el.textContent = p.value;
5575
5937
  } else if (p.type === 'attribute') {
@@ -5586,7 +5948,7 @@ ComponentHandle.prototype._applyPatches = function(patches) {
5586
5948
  * Resolve all bindings and apply (used for initial render).
5587
5949
  * @private
5588
5950
  */
5589
- ComponentHandle.prototype._resolveAndApplyAll = function() {
5951
+ _chp._resolveAndApplyAll = function() {
5590
5952
  var patches = [];
5591
5953
  for (var i = 0; i < this._bindings.length; i++) {
5592
5954
  var b = this._bindings[i];
@@ -5609,7 +5971,7 @@ ComponentHandle.prototype._resolveAndApplyAll = function() {
5609
5971
  * Full re-render for structural changes (when/each branch switches).
5610
5972
  * @private
5611
5973
  */
5612
- ComponentHandle.prototype._render = function() {
5974
+ _chp._render = function() {
5613
5975
  if (!this.element || !this.element.parentNode) return;
5614
5976
  var parent = this.element.parentNode;
5615
5977
  var nextSibling = this.element.nextSibling;
@@ -5649,7 +6011,7 @@ ComponentHandle.prototype._render = function() {
5649
6011
  * @param {string} event - Event name (e.g., 'click')
5650
6012
  * @param {Function} handler - Event handler
5651
6013
  */
5652
- ComponentHandle.prototype.on = function(event, handler) {
6014
+ _chp.on = function(event, handler) {
5653
6015
  if (this.element) {
5654
6016
  this.element.addEventListener(event, handler);
5655
6017
  }
@@ -5661,7 +6023,7 @@ ComponentHandle.prototype.on = function(event, handler) {
5661
6023
  * @param {string} event - Event name
5662
6024
  * @param {Function} handler - Handler to remove
5663
6025
  */
5664
- ComponentHandle.prototype.off = function(event, handler) {
6026
+ _chp.off = function(event, handler) {
5665
6027
  if (this.element) {
5666
6028
  this.element.removeEventListener(event, handler);
5667
6029
  }
@@ -5676,7 +6038,7 @@ ComponentHandle.prototype.off = function(event, handler) {
5676
6038
  * @param {Function} handler - Handler function
5677
6039
  * @returns {Function} Unsubscribe function
5678
6040
  */
5679
- ComponentHandle.prototype.sub = function(topic, handler) {
6041
+ _chp.sub = function(topic, handler) {
5680
6042
  var unsub = bw.sub(topic, handler);
5681
6043
  this._subs.push(unsub);
5682
6044
  return unsub;
@@ -5687,10 +6049,10 @@ ComponentHandle.prototype.sub = function(topic, handler) {
5687
6049
  * @param {string} name - Action name
5688
6050
  * @param {...*} args - Arguments passed after comp
5689
6051
  */
5690
- ComponentHandle.prototype.action = function(name) {
6052
+ _chp.action = function(name) {
5691
6053
  var fn = this._actions[name];
5692
6054
  if (!fn) {
5693
- console.warn('ComponentHandle.action: unknown action "' + name + '"');
6055
+ _cw('ComponentHandle.action: unknown action "' + name + '"');
5694
6056
  return;
5695
6057
  }
5696
6058
  var args = [this].concat(Array.prototype.slice.call(arguments, 1));
@@ -5702,7 +6064,7 @@ ComponentHandle.prototype.action = function(name) {
5702
6064
  * @param {string} sel - CSS selector
5703
6065
  * @returns {Element|null}
5704
6066
  */
5705
- ComponentHandle.prototype.select = function(sel) {
6067
+ _chp.select = function(sel) {
5706
6068
  return this.element ? this.element.querySelector(sel) : null;
5707
6069
  };
5708
6070
 
@@ -5711,7 +6073,7 @@ ComponentHandle.prototype.select = function(sel) {
5711
6073
  * @param {string} sel - CSS selector
5712
6074
  * @returns {Element[]}
5713
6075
  */
5714
- ComponentHandle.prototype.selectAll = function(sel) {
6076
+ _chp.selectAll = function(sel) {
5715
6077
  if (!this.element) return [];
5716
6078
  return Array.prototype.slice.call(this.element.querySelectorAll(sel));
5717
6079
  };
@@ -5722,7 +6084,7 @@ ComponentHandle.prototype.selectAll = function(sel) {
5722
6084
  * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
5723
6085
  * @returns {ComponentHandle} this (for chaining)
5724
6086
  */
5725
- ComponentHandle.prototype.userTag = function(tag) {
6087
+ _chp.userTag = function(tag) {
5726
6088
  this._userTag = tag;
5727
6089
  if (this.element) {
5728
6090
  this.element.classList.add(tag);
@@ -5823,14 +6185,399 @@ bw.message = function(target, action, data) {
5823
6185
  }
5824
6186
  if (!el || !el._bwComponentHandle) return false;
5825
6187
  var comp = el._bwComponentHandle;
5826
- if (typeof comp[action] !== 'function') {
5827
- console.warn('bw.message: unknown action "' + action + '" on component ' + target);
6188
+ if (!_is(comp[action], 'function')) {
6189
+ _cw('bw.message: unknown action "' + action + '" on component ' + target);
5828
6190
  return false;
5829
6191
  }
5830
6192
  comp[action](data);
5831
6193
  return true;
5832
6194
  };
5833
6195
 
6196
+ // ===================================================================================
6197
+ // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
6198
+ // ===================================================================================
6199
+
6200
+ /**
6201
+ * Registry of named functions sent via register messages.
6202
+ * Populated by clientApply({ type: 'register', name, body }).
6203
+ * Invoked by clientApply({ type: 'call', name, args }).
6204
+ * @private
6205
+ */
6206
+ bw._clientFunctions = {};
6207
+
6208
+ /**
6209
+ * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
6210
+ * Default false — exec messages are rejected unless explicitly opted in.
6211
+ * @private
6212
+ */
6213
+ bw._allowExec = false;
6214
+
6215
+ /**
6216
+ * Built-in client functions available via call() without registration.
6217
+ * @private
6218
+ */
6219
+ bw._builtinClientFunctions = {
6220
+ scrollTo: function(selector) {
6221
+ var el = bw._el(selector);
6222
+ if (el) el.scrollTop = el.scrollHeight;
6223
+ },
6224
+ focus: function(selector) {
6225
+ var el = bw._el(selector);
6226
+ if (el && _is(el.focus, 'function')) el.focus();
6227
+ },
6228
+ download: function(filename, content, mimeType) {
6229
+ if (typeof document === 'undefined') return;
6230
+ var blob = new Blob([content], { type: mimeType || 'text/plain' });
6231
+ var a = document.createElement('a');
6232
+ a.href = URL.createObjectURL(blob);
6233
+ a.download = filename;
6234
+ a.click();
6235
+ URL.revokeObjectURL(a.href);
6236
+ },
6237
+ clipboard: function(text) {
6238
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
6239
+ navigator.clipboard.writeText(text);
6240
+ }
6241
+ },
6242
+ redirect: function(url) {
6243
+ if (typeof window !== 'undefined') window.location.href = url;
6244
+ },
6245
+ log: function() {
6246
+ console.log.apply(console, arguments);
6247
+ }
6248
+ };
6249
+
6250
+ /**
6251
+ * Parse a bwserve protocol message string, supporting both strict JSON
6252
+ * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
6253
+ *
6254
+ * The r-prefix format is designed for C/C++ string literals where
6255
+ * double-quote escaping is painful. The parser is a state machine
6256
+ * that walks character by character — not a regex replace.
6257
+ *
6258
+ * Escaping: apostrophes inside single-quoted values must be escaped
6259
+ * with backslash: r{'name':'Barry\'s room'}
6260
+ *
6261
+ * @param {string} str - JSON or r-prefixed relaxed JSON string
6262
+ * @returns {Object} Parsed message object
6263
+ * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
6264
+ * @category Server
6265
+ */
6266
+ bw.clientParse = function(str) {
6267
+ str = (str || '').trim();
6268
+ if (str.charAt(0) !== 'r') return JSON.parse(str);
6269
+ str = str.slice(1);
6270
+
6271
+ var out = [];
6272
+ var i = 0;
6273
+ var len = str.length;
6274
+
6275
+ while (i < len) {
6276
+ var ch = str[i];
6277
+
6278
+ if (ch === "'") {
6279
+ // Single-quoted string → emit as double-quoted
6280
+ out.push('"');
6281
+ i++;
6282
+ while (i < len) {
6283
+ var c = str[i];
6284
+ if (c === '\\' && i + 1 < len) {
6285
+ var next = str[i + 1];
6286
+ if (next === "'") {
6287
+ out.push("'"); // \' in input → ' in output
6288
+ } else {
6289
+ out.push('\\');
6290
+ out.push(next);
6291
+ }
6292
+ i += 2;
6293
+ } else if (c === '"') {
6294
+ out.push('\\"');
6295
+ i++;
6296
+ } else if (c === "'") {
6297
+ break;
6298
+ } else {
6299
+ out.push(c);
6300
+ i++;
6301
+ }
6302
+ }
6303
+ out.push('"');
6304
+ i++; // skip closing '
6305
+
6306
+ } else if (ch === '"') {
6307
+ // Double-quoted string — pass through verbatim
6308
+ out.push(ch);
6309
+ i++;
6310
+ while (i < len) {
6311
+ var c2 = str[i];
6312
+ if (c2 === '\\' && i + 1 < len) {
6313
+ out.push(c2);
6314
+ out.push(str[i + 1]);
6315
+ i += 2;
6316
+ } else {
6317
+ out.push(c2);
6318
+ i++;
6319
+ if (c2 === '"') break;
6320
+ }
6321
+ }
6322
+
6323
+ } else if (ch === ',') {
6324
+ // Trailing comma check: skip comma if next non-whitespace is } or ]
6325
+ var j = i + 1;
6326
+ while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) j++;
6327
+ if (j < len && (str[j] === '}' || str[j] === ']')) {
6328
+ i++; // skip trailing comma
6329
+ } else {
6330
+ out.push(ch);
6331
+ i++;
6332
+ }
6333
+
6334
+ } else {
6335
+ out.push(ch);
6336
+ i++;
6337
+ }
6338
+ }
6339
+
6340
+ return JSON.parse(out.join(''));
6341
+ };
6342
+
6343
+ /**
6344
+ * Apply a bwserve protocol message to the DOM.
6345
+ *
6346
+ * Dispatches one of 9 message types:
6347
+ * replace — bw.DOM(target, node)
6348
+ * append — target.appendChild(bw.createDOM(node))
6349
+ * remove — bw.cleanup(target); target.remove()
6350
+ * patch — bw.patch(target, content, attr)
6351
+ * batch — iterate ops, call clientApply for each
6352
+ * message — bw.message(target, action, data)
6353
+ * register — store a named function for later call()
6354
+ * call — invoke a registered or built-in function
6355
+ * exec — execute arbitrary JS (requires allowExec)
6356
+ *
6357
+ * Target resolution:
6358
+ * Starts with '#' or '.' → CSS selector (querySelector)
6359
+ * Otherwise → getElementById, then bw._el fallback
6360
+ *
6361
+ * @param {Object} msg - Protocol message
6362
+ * @returns {boolean} true if the message was applied successfully
6363
+ * @category Server
6364
+ */
6365
+ bw.clientApply = function(msg) {
6366
+ if (!msg || !msg.type) return false;
6367
+
6368
+ var type = msg.type;
6369
+ var target = msg.target;
6370
+
6371
+ if (type === 'replace') {
6372
+ var el = bw._el(target);
6373
+ if (!el) return false;
6374
+ bw.DOM(el, msg.node);
6375
+ return true;
6376
+
6377
+ } else if (type === 'patch') {
6378
+ var patched = bw.patch(target, msg.content, msg.attr);
6379
+ return patched !== null;
6380
+
6381
+ } else if (type === 'append') {
6382
+ var parent = bw._el(target);
6383
+ if (!parent) return false;
6384
+ var child = bw.createDOM(msg.node);
6385
+ parent.appendChild(child);
6386
+ return true;
6387
+
6388
+ } else if (type === 'remove') {
6389
+ var toRemove = bw._el(target);
6390
+ if (!toRemove) return false;
6391
+ if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
6392
+ toRemove.remove();
6393
+ return true;
6394
+
6395
+ } else if (type === 'batch') {
6396
+ if (!_isA(msg.ops)) return false;
6397
+ var allOk = true;
6398
+ msg.ops.forEach(function(op) {
6399
+ if (!bw.clientApply(op)) allOk = false;
6400
+ });
6401
+ return allOk;
6402
+
6403
+ } else if (type === 'message') {
6404
+ return bw.message(msg.target, msg.action, msg.data);
6405
+
6406
+ } else if (type === 'register') {
6407
+ if (!msg.name || !msg.body) return false;
6408
+ try {
6409
+ bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
6410
+ return true;
6411
+ } catch (e) {
6412
+ _ce('[bw] register error:', msg.name, e);
6413
+ return false;
6414
+ }
6415
+
6416
+ } else if (type === 'call') {
6417
+ if (!msg.name) return false;
6418
+ var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
6419
+ if (!_is(fn, 'function')) return false;
6420
+ try {
6421
+ var args = _isA(msg.args) ? msg.args : [];
6422
+ fn.apply(null, args);
6423
+ return true;
6424
+ } catch (e) {
6425
+ _ce('[bw] call error:', msg.name, e);
6426
+ return false;
6427
+ }
6428
+
6429
+ } else if (type === 'exec') {
6430
+ if (!bw._allowExec) {
6431
+ _cw('[bw] exec rejected: allowExec is not enabled');
6432
+ return false;
6433
+ }
6434
+ if (!msg.code) return false;
6435
+ try {
6436
+ new Function(msg.code)();
6437
+ return true;
6438
+ } catch (e) {
6439
+ _ce('[bw] exec error:', e);
6440
+ return false;
6441
+ }
6442
+ }
6443
+
6444
+ return false;
6445
+ };
6446
+
6447
+ /**
6448
+ * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
6449
+ *
6450
+ * Returns a connection object with sendAction(), on(), and close() methods.
6451
+ *
6452
+ * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
6453
+ * @param {Object} [opts] - Connection options
6454
+ * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
6455
+ * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
6456
+ * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
6457
+ * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
6458
+ * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
6459
+ * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
6460
+ * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
6461
+ * @returns {Object} Connection object { sendAction, on, close, status }
6462
+ * @category Server
6463
+ */
6464
+ bw.clientConnect = function(url, opts) {
6465
+ opts = opts || {};
6466
+ var transport = opts.transport || 'sse';
6467
+ var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
6468
+ var reconnect = opts.reconnect !== false;
6469
+ var onStatus = opts.onStatus || function() {};
6470
+ var onMessage = opts.onMessage || null;
6471
+ var handlers = {};
6472
+ // Set the global allowExec flag from connection options
6473
+ bw._allowExec = !!opts.allowExec;
6474
+ var conn = {
6475
+ status: 'connecting',
6476
+ _es: null,
6477
+ _pollTimer: null
6478
+ };
6479
+
6480
+ function setStatus(s) {
6481
+ conn.status = s;
6482
+ onStatus(s);
6483
+ }
6484
+
6485
+ function handleMessage(data) {
6486
+ try {
6487
+ var msg = _is(data, 'string') ? bw.clientParse(data) : data;
6488
+ if (onMessage) onMessage(msg);
6489
+ if (handlers.message) handlers.message(msg);
6490
+ bw.clientApply(msg);
6491
+ } catch (e) {
6492
+ if (handlers.error) handlers.error(e);
6493
+ }
6494
+ }
6495
+
6496
+ if (transport === 'sse' && typeof EventSource !== 'undefined') {
6497
+ setStatus('connecting');
6498
+ var es = new EventSource(url);
6499
+ conn._es = es;
6500
+
6501
+ es.onopen = function() {
6502
+ setStatus('connected');
6503
+ if (handlers.open) handlers.open();
6504
+ };
6505
+
6506
+ es.onmessage = function(e) {
6507
+ handleMessage(e.data);
6508
+ };
6509
+
6510
+ es.onerror = function() {
6511
+ if (conn.status === 'connected') {
6512
+ setStatus('disconnected');
6513
+ }
6514
+ if (handlers.error) handlers.error(new Error('SSE connection error'));
6515
+ if (!reconnect) {
6516
+ es.close();
6517
+ }
6518
+ // EventSource auto-reconnects by default when reconnect=true
6519
+ };
6520
+ } else if (transport === 'poll') {
6521
+ var interval = opts.interval || 2000;
6522
+ setStatus('connected');
6523
+ conn._pollTimer = setInterval(function() {
6524
+ fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
6525
+ if (_isA(msgs)) {
6526
+ msgs.forEach(handleMessage);
6527
+ } else if (msgs && msgs.type) {
6528
+ handleMessage(msgs);
6529
+ }
6530
+ }).catch(function(e) {
6531
+ if (handlers.error) handlers.error(e);
6532
+ });
6533
+ }, interval);
6534
+ }
6535
+
6536
+ /**
6537
+ * Send an action to the server via POST.
6538
+ * @param {string} action - Action name
6539
+ * @param {Object} [data] - Action payload
6540
+ */
6541
+ conn.sendAction = function(action, data) {
6542
+ var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
6543
+ fetch(actionUrl, {
6544
+ method: 'POST',
6545
+ headers: { 'Content-Type': 'application/json' },
6546
+ body: body
6547
+ }).catch(function(e) {
6548
+ if (handlers.error) handlers.error(e);
6549
+ });
6550
+ };
6551
+
6552
+ /**
6553
+ * Register an event handler.
6554
+ * @param {string} event - 'open'|'message'|'error'|'close'
6555
+ * @param {Function} handler
6556
+ */
6557
+ conn.on = function(event, handler) {
6558
+ handlers[event] = handler;
6559
+ return conn;
6560
+ };
6561
+
6562
+ /**
6563
+ * Close the connection.
6564
+ */
6565
+ conn.close = function() {
6566
+ if (conn._es) {
6567
+ conn._es.close();
6568
+ conn._es = null;
6569
+ }
6570
+ if (conn._pollTimer) {
6571
+ clearInterval(conn._pollTimer);
6572
+ conn._pollTimer = null;
6573
+ }
6574
+ setStatus('disconnected');
6575
+ if (handlers.close) handlers.close();
6576
+ };
6577
+
6578
+ return conn;
6579
+ };
6580
+
5834
6581
  // ===================================================================================
5835
6582
  // bw.inspect() — Debug utility
5836
6583
  // ===================================================================================
@@ -5857,33 +6604,33 @@ bw.inspect = function(target) {
5857
6604
  el = target.element;
5858
6605
  comp = target;
5859
6606
  } else {
5860
- if (typeof target === 'string') {
6607
+ if (_is(target, 'string')) {
5861
6608
  el = bw.$(target)[0];
5862
6609
  }
5863
6610
  if (!el) {
5864
- console.warn('bw.inspect: element not found');
6611
+ _cw('bw.inspect: element not found');
5865
6612
  return null;
5866
6613
  }
5867
6614
  comp = el._bwComponentHandle;
5868
6615
  }
5869
6616
  if (!comp) {
5870
- console.log('bw.inspect: no ComponentHandle on this element');
5871
- console.log(' Tag:', el.tagName);
5872
- console.log(' Classes:', el.className);
5873
- console.log(' _bw_state:', el._bw_state || '(none)');
6617
+ _cl('bw.inspect: no ComponentHandle on this element');
6618
+ _cl(' Tag:', el.tagName);
6619
+ _cl(' Classes:', el.className);
6620
+ _cl(' _bw_state:', el._bw_state || '(none)');
5874
6621
  return null;
5875
6622
  }
5876
6623
  var deps = comp._bindings.reduce(function(s, b) {
5877
6624
  return s.concat(b.deps || []);
5878
6625
  }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
5879
6626
  console.group('Component: ' + comp._bwId);
5880
- console.log('State:', comp._state);
5881
- console.log('Bindings:', comp._bindings.length, '(deps:', deps, ')');
5882
- console.log('Methods:', Object.keys(comp._methods));
5883
- console.log('Actions:', Object.keys(comp._actions));
5884
- console.log('User tag:', comp._userTag || '(none)');
5885
- console.log('Mounted:', comp.mounted);
5886
- console.log('Element:', comp.element);
6627
+ _cl('State:', comp._state);
6628
+ _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
6629
+ _cl('Methods:', _keys(comp._methods));
6630
+ _cl('Actions:', _keys(comp._actions));
6631
+ _cl('User tag:', comp._userTag || '(none)');
6632
+ _cl('Mounted:', comp.mounted);
6633
+ _cl('Element:', comp.element);
5887
6634
  console.groupEnd();
5888
6635
  return comp;
5889
6636
  };
@@ -5906,8 +6653,8 @@ bw.compile = function(taco) {
5906
6653
  // Pre-extract all binding expressions
5907
6654
  var precompiled = [];
5908
6655
  function walkExpressions(node) {
5909
- if (!node || typeof node !== 'object') return;
5910
- if (typeof node.c === 'string' && node.c.indexOf('${') >= 0) {
6656
+ if (!_is(node, 'object')) return;
6657
+ if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
5911
6658
  var parsed = bw._parseBindings(node.c);
5912
6659
  for (var i = 0; i < parsed.length; i++) {
5913
6660
  try {
@@ -5922,9 +6669,9 @@ bw.compile = function(taco) {
5922
6669
  }
5923
6670
  if (node.a) {
5924
6671
  for (var key in node.a) {
5925
- if (Object.prototype.hasOwnProperty.call(node.a, key)) {
6672
+ if (_hop.call(node.a, key)) {
5926
6673
  var v = node.a[key];
5927
- if (typeof v === 'string' && v.indexOf('${') >= 0) {
6674
+ if (_is(v, 'string') && v.indexOf('${') >= 0) {
5928
6675
  var parsed2 = bw._parseBindings(v);
5929
6676
  for (var j = 0; j < parsed2.length; j++) {
5930
6677
  try {
@@ -5940,9 +6687,9 @@ bw.compile = function(taco) {
5940
6687
  }
5941
6688
  }
5942
6689
  }
5943
- if (Array.isArray(node.c)) {
6690
+ if (_isA(node.c)) {
5944
6691
  for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
5945
- } else if (node.c && typeof node.c === 'object' && node.c.t) {
6692
+ } else if (_is(node.c, 'object') && node.c.t) {
5946
6693
  walkExpressions(node.c);
5947
6694
  }
5948
6695
  }
@@ -5954,7 +6701,7 @@ bw.compile = function(taco) {
5954
6701
  handle._precompiledBindings = precompiled;
5955
6702
  if (initialState) {
5956
6703
  for (var k in initialState) {
5957
- if (Object.prototype.hasOwnProperty.call(initialState, k)) {
6704
+ if (_hop.call(initialState, k)) {
5958
6705
  handle._state[k] = initialState[k];
5959
6706
  }
5960
6707
  }
@@ -5985,18 +6732,18 @@ bw.compile = function(taco) {
5985
6732
  bw.css = function(rules, options = {}) {
5986
6733
  const { minify = false, pretty = !minify } = options;
5987
6734
 
5988
- if (typeof rules === 'string') return rules;
6735
+ if (_is(rules, 'string')) return rules;
5989
6736
 
5990
6737
  let css = '';
5991
6738
  const indent = pretty ? ' ' : '';
5992
6739
  const newline = pretty ? '\n' : '';
5993
6740
  const space = pretty ? ' ' : '';
5994
6741
 
5995
- if (Array.isArray(rules)) {
6742
+ if (_isA(rules)) {
5996
6743
  css = rules.map(rule => bw.css(rule, options)).join(newline);
5997
- } else if (typeof rules === 'object') {
6744
+ } else if (_is(rules, 'object')) {
5998
6745
  Object.entries(rules).forEach(([selector, styles]) => {
5999
- if (typeof styles === 'object' && !Array.isArray(styles)) {
6746
+ if (_is(styles, 'object')) {
6000
6747
  // Handle @media, @keyframes, @supports — recurse into nested block
6001
6748
  if (selector.charAt(0) === '@') {
6002
6749
  const inner = bw.css(styles, options);
@@ -6045,7 +6792,7 @@ bw.css = function(rules, options = {}) {
6045
6792
  */
6046
6793
  bw.injectCSS = function(css, options = {}) {
6047
6794
  if (!bw._isBrowser) {
6048
- console.warn('bw.injectCSS requires a DOM environment');
6795
+ _cw('bw.injectCSS requires a DOM environment');
6049
6796
  return null;
6050
6797
  }
6051
6798
 
@@ -6062,7 +6809,7 @@ bw.injectCSS = function(css, options = {}) {
6062
6809
  }
6063
6810
 
6064
6811
  // Convert CSS if needed
6065
- const cssStr = typeof css === 'string' ? css : bw.css(css, options);
6812
+ const cssStr = _is(css, 'string') ? css : bw.css(css, options);
6066
6813
 
6067
6814
  // Set or append CSS
6068
6815
  if (append && styleEl.textContent) {
@@ -6092,7 +6839,7 @@ bw.s = function() {
6092
6839
  var result = {};
6093
6840
  for (var i = 0; i < arguments.length; i++) {
6094
6841
  var arg = arguments[i];
6095
- if (arg && typeof arg === 'object') Object.assign(result, arg);
6842
+ if (_is(arg, 'object')) Object.assign(result, arg);
6096
6843
  }
6097
6844
  return result;
6098
6845
  };
@@ -6215,7 +6962,7 @@ bw.u = {
6215
6962
  bw.responsive = function(selector, breakpoints) {
6216
6963
  var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
6217
6964
  var parts = [];
6218
- Object.keys(breakpoints).forEach(function(key) {
6965
+ _keys(breakpoints).forEach(function(key) {
6219
6966
  var rules = {};
6220
6967
  if (key === 'base') {
6221
6968
  rules[selector] = breakpoints[key];
@@ -6287,18 +7034,18 @@ if (bw._isBrowser) {
6287
7034
  if (!selector) return [];
6288
7035
 
6289
7036
  // Already an array
6290
- if (Array.isArray(selector)) return selector;
7037
+ if (_isA(selector)) return selector;
6291
7038
 
6292
7039
  // Single element
6293
7040
  if (selector.nodeType) return [selector];
6294
7041
 
6295
7042
  // NodeList or HTMLCollection
6296
- if (selector.length !== undefined && typeof selector !== 'string') {
7043
+ if (selector.length !== undefined && !_is(selector, 'string')) {
6297
7044
  return Array.from(selector);
6298
7045
  }
6299
7046
 
6300
7047
  // CSS selector string
6301
- if (typeof selector === 'string') {
7048
+ if (_is(selector, 'string')) {
6302
7049
  return Array.from(document.querySelectorAll(selector));
6303
7050
  }
6304
7051
 
@@ -6802,7 +7549,7 @@ bw.makeTable = function(config) {
6802
7549
 
6803
7550
  // Auto-detect columns if not provided
6804
7551
  const cols = columns || (data.length > 0
6805
- ? Object.keys(data[0]).map(key => ({ key, label: key }))
7552
+ ? _keys(data[0]).map(key => ({ key, label: key }))
6806
7553
  : []);
6807
7554
 
6808
7555
  // Current sort state
@@ -6817,7 +7564,7 @@ bw.makeTable = function(config) {
6817
7564
  const bVal = b[currentSortColumn];
6818
7565
 
6819
7566
  // Handle different types
6820
- if (typeof aVal === 'number' && typeof bVal === 'number') {
7567
+ if (_is(aVal, 'number') && _is(bVal, 'number')) {
6821
7568
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
6822
7569
  }
6823
7570
 
@@ -6927,7 +7674,7 @@ bw.makeTable = function(config) {
6927
7674
  bw.makeTableFromArray = function(config) {
6928
7675
  const { data = [], headerRow = true, columns, ...rest } = config;
6929
7676
 
6930
- if (!Array.isArray(data) || data.length === 0) {
7677
+ if (!_isA(data) || data.length === 0) {
6931
7678
  return bw.makeTable({ data: [], columns: columns || [], ...rest });
6932
7679
  }
6933
7680
 
@@ -7009,7 +7756,7 @@ bw.makeBarChart = function(config) {
7009
7756
  className = ''
7010
7757
  } = config;
7011
7758
 
7012
- if (!Array.isArray(data) || data.length === 0) {
7759
+ if (!_isA(data) || data.length === 0) {
7013
7760
  return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
7014
7761
  }
7015
7762
 
@@ -7158,7 +7905,7 @@ bw._componentRegistry = new Map();
7158
7905
  */
7159
7906
  bw.render = function(element, position, taco) {
7160
7907
  // Get target element
7161
- const targetEl = typeof element === 'string'
7908
+ const targetEl = _is(element, 'string')
7162
7909
  ? document.querySelector(element)
7163
7910
  : element;
7164
7911
 
@@ -7308,7 +8055,7 @@ bw.render = function(element, position, taco) {
7308
8055
  setContent(content) {
7309
8056
  this._taco.c = content;
7310
8057
  if (this.element) {
7311
- if (typeof content === 'string') {
8058
+ if (_is(content, 'string')) {
7312
8059
  this.element.textContent = content;
7313
8060
  } else {
7314
8061
  // Re-render for complex content