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