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 v2.0.15 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench 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
  };
@@ -6865,7 +6877,11 @@ var BCCL = {
6865
6877
  function make(type, props) {
6866
6878
  var def = BCCL[type];
6867
6879
  if (!def) throw new Error('bw.make: unknown component type "' + type + '". Available: ' + Object.keys(BCCL).join(', '));
6868
- return def.make(props || {});
6880
+ var taco = def.make(props || {});
6881
+ if (taco && typeof taco === 'object') {
6882
+ taco._bwFactory = { type: type, props: props || {} };
6883
+ }
6884
+ return taco;
6869
6885
  }
6870
6886
 
6871
6887
  var components = /*#__PURE__*/Object.freeze({
@@ -6986,7 +7002,7 @@ const bw = {
6986
7002
  __monkey_patch_is_nodejs__: {
6987
7003
  _value: 'ignore',
6988
7004
  set: function(x) {
6989
- this._value = (typeof x === 'boolean') ? x : 'ignore';
7005
+ this._value = _is(x, 'boolean') ? x : 'ignore';
6990
7006
  },
6991
7007
  get: function() {
6992
7008
  return this._value;
@@ -7034,6 +7050,67 @@ Object.defineProperty(bw, '_isBrowser', {
7034
7050
  configurable: true
7035
7051
  });
7036
7052
 
7053
+ // ── Internal aliases ─────────────────────────────────────────────────────
7054
+ // Short names for frequently-used builtins and internal methods.
7055
+ // Same pattern as v1 (_to = bw.typeOf, etc.).
7056
+ //
7057
+ // Why: Terser can't shorten global property chains (console.warn,
7058
+ // Object.prototype.hasOwnProperty, Array.isArray, document.createElement)
7059
+ // because it can't prove they're side-effect-free. We can, so we alias
7060
+ // them here. Each alias saves bytes in the minified output, and the short
7061
+ // names also reduce visual noise in the hot paths (binding pipeline,
7062
+ // createDOM, etc.).
7063
+ //
7064
+ // Alias Target Sites
7065
+ // ───────── ────────────────────────────────────── ─────
7066
+ // _hop Object.prototype.hasOwnProperty 15
7067
+ // _isA Array.isArray 25
7068
+ // _keys Object.keys 7
7069
+ // _to bw.typeOf (type string) 26
7070
+ // _is type check boolean: _is(x,'string') ~50
7071
+ // _cw console.warn 8
7072
+ // _cl console.log 11
7073
+ // _ce console.error 4
7074
+ // _chp ComponentHandle.prototype 28 (defined after constructor)
7075
+ //
7076
+ // Note: document.createElement etc. are NOT aliased because they require
7077
+ // `this === document` and .bind() would add overhead on every call.
7078
+ // Console aliases use thin wrappers (not direct refs) so test monkey-
7079
+ // patching of console.warn/log/error continues to work.
7080
+ //
7081
+ // `typeof x` for UNDECLARED globals (window, document, process, require,
7082
+ // EventSource, navigator, Promise, __filename, import.meta) MUST stay as
7083
+ // raw `typeof` — calling _to(x) when x doesn't exist throws ReferenceError.
7084
+ //
7085
+ // ── v1 functional type helpers (kept for reference, not currently used) ──
7086
+ // _toa(x, type, trueVal, falseVal) — bw.typeAssign:
7087
+ // returns trueVal if _to(x)===type, else falseVal.
7088
+ // Replaces: (typeof x === 'string') ? A : B → _toa(x,'string',A,B)
7089
+ // _toc(x, type, trueVal, falseVal) — bw.typeConvert:
7090
+ // same as _toa but if trueVal/falseVal are functions, calls them with x.
7091
+ // Replaces: typeof x === 'string' ? fn(x) : default → _toc(x,'string',fn,default)
7092
+ // Uncomment if pattern frequency justifies them:
7093
+ // var _toa = function(x, t, y, n) { return _to(x) === t ? y : n; };
7094
+ // 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); };
7095
+ // ─────────────────────────────────────────────────────────────────────────
7096
+ var _hop = Object.prototype.hasOwnProperty;
7097
+ var _isA = Array.isArray;
7098
+ var _keys = Object.keys;
7099
+ var _to = typeOf; // imported from bitwrench-utils.js
7100
+ var _is = function(x, t) { var r = _to(x); return r === t || r.toLowerCase() === t; };
7101
+ // Console aliases use thin wrappers (not direct references) so that test
7102
+ // code can monkey-patch console.warn/log/error and the patches take effect.
7103
+ var _cw = function() { console.warn.apply(console, arguments); };
7104
+ var _cl = function() { console.log.apply(console, arguments); };
7105
+ var _ce = function() { console.error.apply(console, arguments); };
7106
+
7107
+ /**
7108
+ * Debug flag. When true, emits console.warn for silent binding failures
7109
+ * (missing paths, null refs, auto-created intermediate objects).
7110
+ * @type {boolean}
7111
+ */
7112
+ bw.debug = false;
7113
+
7037
7114
  /**
7038
7115
  * Lazy-resolve Node.js `fs` module.
7039
7116
  * Tries require('fs') first (available in CJS/UMD Node.js builds),
@@ -7181,7 +7258,7 @@ bw.uuid = function(prefix) {
7181
7258
  */
7182
7259
  bw._el = function(id) {
7183
7260
  // Pass-through for DOM elements
7184
- if (typeof id !== 'string') return id || null;
7261
+ if (!_is(id, 'string')) return id || null;
7185
7262
  if (!id) return null;
7186
7263
  if (!bw._isBrowser) return null;
7187
7264
 
@@ -7277,7 +7354,7 @@ bw._deregisterNode = function(el, bwId) {
7277
7354
  * // => '&lt;b&gt;Hello&lt;&#x2F;b&gt; &amp; &quot;world&quot;'
7278
7355
  */
7279
7356
  bw.escapeHTML = function(str) {
7280
- if (typeof str !== 'string') return '';
7357
+ if (!_is(str, 'string')) return '';
7281
7358
 
7282
7359
  const escapeMap = {
7283
7360
  '&': '&amp;',
@@ -7350,7 +7427,7 @@ bw.html = function(taco, options = {}) {
7350
7427
  }
7351
7428
 
7352
7429
  // Handle arrays of TACOs
7353
- if (Array.isArray(taco)) {
7430
+ if (_isA(taco)) {
7354
7431
  return taco.map(t => bw.html(t, options)).join('');
7355
7432
  }
7356
7433
 
@@ -7373,15 +7450,15 @@ bw.html = function(taco, options = {}) {
7373
7450
  if (taco && taco._bwEach && options.state) {
7374
7451
  var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
7375
7452
  var arr = bw._evaluatePath(options.state, eachExpr);
7376
- if (!Array.isArray(arr)) return '';
7453
+ if (!_isA(arr)) return '';
7377
7454
  return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
7378
7455
  }
7379
7456
 
7380
7457
  // Handle primitives and non-TACO objects
7381
- if (typeof taco !== 'object' || !taco.t) {
7458
+ if (!_is(taco, 'object') || !taco.t) {
7382
7459
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
7383
7460
  // Resolve template bindings if state provided
7384
- if (options.state && typeof str === 'string' && str.indexOf('${') >= 0) {
7461
+ if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
7385
7462
  str = bw._resolveTemplate(str, options.state, !!options.compile);
7386
7463
  }
7387
7464
  return str;
@@ -7401,10 +7478,18 @@ bw.html = function(taco, options = {}) {
7401
7478
  // Skip null, undefined, false
7402
7479
  if (value == null || value === false) continue;
7403
7480
 
7404
- // Skip event handlers (they're for DOM only)
7405
- if (key.startsWith('on')) continue;
7481
+ // Serialize event handlers via funcRegister
7482
+ if (key.startsWith('on')) {
7483
+ if (_is(value, 'function')) {
7484
+ var fnId = bw.funcRegister(value);
7485
+ attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
7486
+ } else if (_is(value, 'string')) {
7487
+ attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
7488
+ }
7489
+ continue;
7490
+ }
7406
7491
 
7407
- if (key === 'style' && typeof value === 'object') {
7492
+ if (key === 'style' && _is(value, 'object')) {
7408
7493
  // Convert style object to string
7409
7494
  const styleStr = Object.entries(value)
7410
7495
  .filter(([, v]) => v != null)
@@ -7415,7 +7500,7 @@ bw.html = function(taco, options = {}) {
7415
7500
  }
7416
7501
  } else if (key === 'class') {
7417
7502
  // Handle class as array or string
7418
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
7503
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
7419
7504
  if (classStr) {
7420
7505
  attrStr += ` class="${bw.escapeHTML(classStr)}"`;
7421
7506
  }
@@ -7451,13 +7536,184 @@ bw.html = function(taco, options = {}) {
7451
7536
  // Process content recursively
7452
7537
  let contentStr = content != null ? bw.html(content, options) : '';
7453
7538
  // Resolve template bindings in content if state provided
7454
- if (options.state && typeof contentStr === 'string' && contentStr.indexOf('${') >= 0) {
7539
+ if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
7455
7540
  contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
7456
7541
  }
7457
7542
 
7458
7543
  return `<${tag}${attrStr}>${contentStr}</${tag}>`;
7459
7544
  };
7460
7545
 
7546
+ /**
7547
+ * Generate a complete, self-contained HTML document from TACO content.
7548
+ *
7549
+ * Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
7550
+ * func registry emission (so serialized event handlers work), optional theme,
7551
+ * and extra head elements. Designed for static site generation, offline/airgapped
7552
+ * use, and the "static site that isn't static" workflow.
7553
+ *
7554
+ * @param {Object} [opts={}] - Page options
7555
+ * @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
7556
+ * @param {string} [opts.title='bitwrench'] - Page title
7557
+ * @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
7558
+ * @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
7559
+ * @param {string} [opts.css=''] - Additional CSS for <style> block
7560
+ * @param {string|Object} [opts.theme=null] - Theme preset name or config object
7561
+ * @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
7562
+ * @param {string} [opts.favicon=''] - Favicon URL
7563
+ * @param {string} [opts.lang='en'] - HTML lang attribute
7564
+ * @returns {string} Complete HTML document string
7565
+ * @category DOM Generation
7566
+ * @see bw.html
7567
+ * @example
7568
+ * bw.htmlPage({
7569
+ * title: 'My App',
7570
+ * body: { t: 'h1', c: 'Hello World' },
7571
+ * runtime: 'shim'
7572
+ * })
7573
+ */
7574
+ bw.htmlPage = function(opts) {
7575
+ opts = opts || {};
7576
+ var title = opts.title || 'bitwrench';
7577
+ var body = opts.body || '';
7578
+ var state = opts.state || undefined;
7579
+ var runtime = opts.runtime || 'shim';
7580
+ var css = opts.css || '';
7581
+ var theme = opts.theme || null;
7582
+ var headExtra = opts.head || [];
7583
+ var favicon = opts.favicon || '';
7584
+ var lang = opts.lang || 'en';
7585
+
7586
+ // Snapshot funcRegistry counter before rendering
7587
+ var fnCounterBefore = bw._fnIDCounter;
7588
+
7589
+ // Render body content
7590
+ var bodyHTML = '';
7591
+ if (_is(body, 'string')) {
7592
+ bodyHTML = body;
7593
+ } else {
7594
+ var htmlOpts = {};
7595
+ if (state) htmlOpts.state = state;
7596
+ bodyHTML = bw.html(body, htmlOpts);
7597
+ }
7598
+
7599
+ // Collect functions registered during this render
7600
+ var fnCounterAfter = bw._fnIDCounter;
7601
+ var registryEntries = '';
7602
+ for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
7603
+ var fnKey = 'bw_fn_' + i;
7604
+ if (bw._fnRegistry[fnKey]) {
7605
+ registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' +
7606
+ bw._fnRegistry[fnKey].toString() + ';\n';
7607
+ }
7608
+ }
7609
+
7610
+ // Build runtime script for <head>
7611
+ var runtimeHead = '';
7612
+ if (runtime === 'inline') {
7613
+ // Read UMD bundle synchronously if in Node.js
7614
+ var umdSource = null;
7615
+ if (bw._isNode) {
7616
+ try {
7617
+ var fs = (typeof require === 'function') ? require('fs') : null;
7618
+ var pathMod = (typeof require === 'function') ? require('path') : null;
7619
+ if (fs && pathMod) {
7620
+ // Resolve dist/ relative to this source file
7621
+ var srcDir = '';
7622
+ try { srcDir = pathMod.dirname((typeof __filename !== 'undefined') ? __filename : ''); }
7623
+ catch(e2) { /* ESM: __filename not available */ }
7624
+ if (!srcDir && typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench.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.cjs.js', document.baseURI).href))) {
7625
+ var url = (typeof require === 'function') ? require('url') : null;
7626
+ 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.cjs.js', document.baseURI).href))));
7627
+ }
7628
+ if (srcDir) {
7629
+ var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
7630
+ umdSource = fs.readFileSync(distPath, 'utf8');
7631
+ }
7632
+ }
7633
+ } catch(e) { /* fall through */ }
7634
+ }
7635
+ if (umdSource) {
7636
+ runtimeHead = '<script>' + umdSource + '</script>';
7637
+ } else {
7638
+ // Fallback to shim in browser or if dist not available
7639
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
7640
+ }
7641
+ } else if (runtime === 'cdn') {
7642
+ runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
7643
+ } else if (runtime === 'shim') {
7644
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
7645
+ }
7646
+ // runtime === 'none' → empty
7647
+
7648
+ // Theme CSS
7649
+ var themeCSS = '';
7650
+ if (theme) {
7651
+ var themeConfig = _is(theme, 'string')
7652
+ ? (THEME_PRESETS[theme.toLowerCase()] || null)
7653
+ : theme;
7654
+ if (themeConfig) {
7655
+ var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, { inject: false }));
7656
+ themeCSS = themeResult.css;
7657
+ }
7658
+ }
7659
+
7660
+ // Extra <head> elements
7661
+ var headHTML = '';
7662
+ if (_isA(headExtra) && headExtra.length > 0) {
7663
+ headHTML = headExtra.map(function(el) { return bw.html(el); }).join('\n');
7664
+ }
7665
+
7666
+ // Favicon
7667
+ var faviconTag = '';
7668
+ if (favicon) {
7669
+ var safeFavicon = favicon.replace(/[&<>"']/g, function(c) {
7670
+ return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
7671
+ });
7672
+ faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
7673
+ }
7674
+
7675
+ // Escaped title
7676
+ var safeTitle = bw.escapeHTML(title);
7677
+
7678
+ // Combine all CSS
7679
+ var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
7680
+
7681
+ // Body-end script: registry entries + optional loadDefaultStyles
7682
+ var bodyEndScript = '';
7683
+ var bodyEndParts = [];
7684
+ if (registryEntries) {
7685
+ bodyEndParts.push(registryEntries);
7686
+ }
7687
+ if (runtime === 'inline' || runtime === 'cdn') {
7688
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
7689
+ }
7690
+ if (bodyEndParts.length > 0) {
7691
+ bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
7692
+ }
7693
+
7694
+ // Assemble document
7695
+ var parts = [
7696
+ '<!DOCTYPE html>',
7697
+ '<html lang="' + lang + '">',
7698
+ '<head>',
7699
+ '<meta charset="UTF-8">',
7700
+ '<meta name="viewport" content="width=device-width, initial-scale=1">'
7701
+ ];
7702
+ parts.push('<title>' + safeTitle + '</title>');
7703
+ if (faviconTag) parts.push(faviconTag);
7704
+ if (runtimeHead) parts.push(runtimeHead);
7705
+ if (headHTML) parts.push(headHTML);
7706
+ if (allCSS) parts.push('<style>' + allCSS + '</style>');
7707
+ parts.push('</head>');
7708
+ parts.push('<body>');
7709
+ parts.push(bodyHTML);
7710
+ if (bodyEndScript) parts.push(bodyEndScript);
7711
+ parts.push('</body>');
7712
+ parts.push('</html>');
7713
+
7714
+ return parts.join('\n');
7715
+ };
7716
+
7461
7717
  /**
7462
7718
  * Create a live DOM element from a TACO object (browser only).
7463
7719
  *
@@ -7502,7 +7758,7 @@ bw.createDOM = function(taco, options = {}) {
7502
7758
  }
7503
7759
 
7504
7760
  // Handle text nodes
7505
- if (typeof taco !== 'object' || !taco.t) {
7761
+ if (!_is(taco, 'object') || !taco.t) {
7506
7762
  return document.createTextNode(String(taco));
7507
7763
  }
7508
7764
 
@@ -7515,16 +7771,16 @@ bw.createDOM = function(taco, options = {}) {
7515
7771
  for (const [key, value] of Object.entries(attrs)) {
7516
7772
  if (value == null || value === false) continue;
7517
7773
 
7518
- if (key === 'style' && typeof value === 'object') {
7774
+ if (key === 'style' && _is(value, 'object')) {
7519
7775
  // Apply styles directly
7520
7776
  Object.assign(el.style, value);
7521
7777
  } else if (key === 'class') {
7522
7778
  // Handle class as array or string
7523
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
7779
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
7524
7780
  if (classStr) {
7525
7781
  el.className = classStr;
7526
7782
  }
7527
- } else if (key.startsWith('on') && typeof value === 'function') {
7783
+ } else if (key.startsWith('on') && _is(value, 'function')) {
7528
7784
  // Event handlers
7529
7785
  const eventName = key.slice(2).toLowerCase();
7530
7786
  el.addEventListener(eventName, value);
@@ -7544,7 +7800,7 @@ bw.createDOM = function(taco, options = {}) {
7544
7800
  // Children with data-bw_id or id attributes get local refs on the parent,
7545
7801
  // so o.render functions can access them without any DOM lookup.
7546
7802
  if (content != null) {
7547
- if (Array.isArray(content)) {
7803
+ if (_isA(content)) {
7548
7804
  content.forEach(child => {
7549
7805
  if (child != null) {
7550
7806
  // Handle ComponentHandle in content arrays (Level 2 children)
@@ -7564,20 +7820,20 @@ bw.createDOM = function(taco, options = {}) {
7564
7820
  if (childEl._bw_refs) {
7565
7821
  if (!el._bw_refs) el._bw_refs = {};
7566
7822
  for (var rk in childEl._bw_refs) {
7567
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
7823
+ if (_hop.call(childEl._bw_refs, rk)) {
7568
7824
  el._bw_refs[rk] = childEl._bw_refs[rk];
7569
7825
  }
7570
7826
  }
7571
7827
  }
7572
7828
  }
7573
7829
  });
7574
- } else if (typeof content === 'object' && content.__bw_raw) {
7830
+ } else if (_is(content, 'object') && content.__bw_raw) {
7575
7831
  // Raw HTML content — inject via innerHTML
7576
7832
  el.innerHTML = content.v;
7577
7833
  } else if (content._bwComponent === true) {
7578
7834
  // Single ComponentHandle as content
7579
7835
  content.mount(el);
7580
- } else if (typeof content === 'object' && content.t) {
7836
+ } else if (_is(content, 'object') && content.t) {
7581
7837
  var childEl = bw.createDOM(content, options);
7582
7838
  el.appendChild(childEl);
7583
7839
  var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
@@ -7588,7 +7844,7 @@ bw.createDOM = function(taco, options = {}) {
7588
7844
  if (childEl._bw_refs) {
7589
7845
  if (!el._bw_refs) el._bw_refs = {};
7590
7846
  for (var rk in childEl._bw_refs) {
7591
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
7847
+ if (_hop.call(childEl._bw_refs, rk)) {
7592
7848
  el._bw_refs[rk] = childEl._bw_refs[rk];
7593
7849
  }
7594
7850
  }
@@ -7621,7 +7877,7 @@ bw.createDOM = function(taco, options = {}) {
7621
7877
  el._bw_render = opts.render;
7622
7878
 
7623
7879
  if (opts.mounted) {
7624
- console.warn('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
7880
+ _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
7625
7881
  }
7626
7882
 
7627
7883
  // Queue initial render (same timing as mounted)
@@ -7694,7 +7950,7 @@ bw.DOM = function(target, taco, options = {}) {
7694
7950
  const targetEl = bw._el(target);
7695
7951
 
7696
7952
  if (!targetEl) {
7697
- console.error('bw.DOM: Target element not found:', target);
7953
+ _ce('bw.DOM: Target element not found:', target);
7698
7954
  return null;
7699
7955
  }
7700
7956
 
@@ -7734,7 +7990,7 @@ bw.DOM = function(target, taco, options = {}) {
7734
7990
  targetEl.appendChild(taco.element);
7735
7991
  }
7736
7992
  // Handle arrays
7737
- else if (Array.isArray(taco)) {
7993
+ else if (_isA(taco)) {
7738
7994
  taco.forEach(t => {
7739
7995
  if (t != null) {
7740
7996
  if (t._bwComponent === true) {
@@ -7770,7 +8026,7 @@ bw.DOM = function(target, taco, options = {}) {
7770
8026
  bw.compileProps = function(handle, props = {}) {
7771
8027
  const compiledProps = {};
7772
8028
 
7773
- Object.keys(props).forEach(key => {
8029
+ _keys(props).forEach(key => {
7774
8030
  // Create getter/setter for each prop
7775
8031
  Object.defineProperty(compiledProps, key, {
7776
8032
  get() {
@@ -8088,17 +8344,17 @@ bw.patch = function(id, content, attr) {
8088
8344
  if (attr) {
8089
8345
  // Patch an attribute
8090
8346
  el.setAttribute(attr, String(content));
8091
- } else if (Array.isArray(content)) {
8347
+ } else if (_isA(content)) {
8092
8348
  // Patch with array of children (strings and/or TACOs)
8093
8349
  el.innerHTML = '';
8094
8350
  content.forEach(function(item) {
8095
- if (typeof item === 'string' || typeof item === 'number') {
8351
+ if (_is(item, 'string') || _is(item, 'number')) {
8096
8352
  el.appendChild(document.createTextNode(String(item)));
8097
8353
  } else if (item && item.t) {
8098
8354
  el.appendChild(bw.createDOM(item));
8099
8355
  }
8100
8356
  });
8101
- } else if (typeof content === 'object' && content !== null && content.t) {
8357
+ } else if (_is(content, 'object') && content.t) {
8102
8358
  // Patch with a TACO — replace children
8103
8359
  el.innerHTML = '';
8104
8360
  el.appendChild(bw.createDOM(content));
@@ -8129,7 +8385,7 @@ bw.patch = function(id, content, attr) {
8129
8385
  bw.patchAll = function(patches) {
8130
8386
  var results = {};
8131
8387
  for (var id in patches) {
8132
- if (Object.prototype.hasOwnProperty.call(patches, id)) {
8388
+ if (_hop.call(patches, id)) {
8133
8389
  results[id] = bw.patch(id, patches[id]);
8134
8390
  }
8135
8391
  }
@@ -8226,7 +8482,7 @@ bw.pub = function(topic, detail) {
8226
8482
  snapshot[i].handler(detail);
8227
8483
  called++;
8228
8484
  } catch (err) {
8229
- console.warn('bw.pub: subscriber error on topic "' + topic + '":', err);
8485
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
8230
8486
  }
8231
8487
  }
8232
8488
  return called;
@@ -8322,8 +8578,8 @@ bw._fnIDCounter = 0;
8322
8578
  * @see bw.funcGetDispatchStr
8323
8579
  */
8324
8580
  bw.funcRegister = function(fn, name) {
8325
- if (typeof fn !== 'function') return '';
8326
- var fnID = (typeof name === 'string' && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
8581
+ if (!_is(fn, 'function')) return '';
8582
+ var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
8327
8583
  bw._fnRegistry[fnID] = fn;
8328
8584
  return fnID;
8329
8585
  };
@@ -8342,7 +8598,7 @@ bw.funcRegister = function(fn, name) {
8342
8598
  bw.funcGetById = function(name, errFn) {
8343
8599
  name = String(name);
8344
8600
  if (name in bw._fnRegistry) return bw._fnRegistry[name];
8345
- return (typeof errFn === 'function') ? errFn : function() { console.warn('bw.funcGetById: unregistered fn "' + name + '"'); };
8601
+ return _is(errFn, 'function') ? errFn : function() { _cw('bw.funcGetById: unregistered fn "' + name + '"'); };
8346
8602
  };
8347
8603
 
8348
8604
  /**
@@ -8383,13 +8639,30 @@ bw.funcUnregister = function(name) {
8383
8639
  bw.funcGetRegistry = function() {
8384
8640
  var copy = {};
8385
8641
  for (var k in bw._fnRegistry) {
8386
- if (Object.prototype.hasOwnProperty.call(bw._fnRegistry, k)) {
8642
+ if (_hop.call(bw._fnRegistry, k)) {
8387
8643
  copy[k] = bw._fnRegistry[k];
8388
8644
  }
8389
8645
  }
8390
8646
  return copy;
8391
8647
  };
8392
8648
 
8649
+ /**
8650
+ * Minimal runtime shim for funcRegister dispatch in static HTML.
8651
+ * When embedded in a `<script>` tag, provides just enough infrastructure
8652
+ * for `bw.funcGetById()` calls to resolve. The actual function bodies
8653
+ * are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
8654
+ * @type {string}
8655
+ * @category Function Registry
8656
+ */
8657
+ bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' +
8658
+ 'if(!bw._fnRegistry)bw._fnRegistry={};' +
8659
+ 'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' +
8660
+ 'console.warn("bw: unregistered fn "+n)};};' +
8661
+ 'bw.funcRegister=function(fn,name){' +
8662
+ 'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' +
8663
+ 'bw._fnRegistry[id]=fn;return id;};' +
8664
+ 'window.bw=bw;})();';
8665
+
8393
8666
  // ===================================================================================
8394
8667
  // Template Binding Utilities
8395
8668
  // ===================================================================================
@@ -8417,7 +8690,10 @@ bw._evaluatePath = function(state, path) {
8417
8690
  var parts = path.split('.');
8418
8691
  var val = state;
8419
8692
  for (var i = 0; i < parts.length; i++) {
8420
- if (val == null) return '';
8693
+ if (val == null) {
8694
+ if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
8695
+ return '';
8696
+ }
8421
8697
  val = val[parts[i]];
8422
8698
  }
8423
8699
  return (val == null) ? '' : val;
@@ -8437,7 +8713,7 @@ bw._evaluatePath = function(state, path) {
8437
8713
  */
8438
8714
  bw._compiledExprs = {};
8439
8715
  bw._resolveTemplate = function(str, state, compile) {
8440
- if (typeof str !== 'string' || str.indexOf('${') < 0) return str;
8716
+ if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
8441
8717
  var bindings = bw._parseBindings(str);
8442
8718
  if (bindings.length === 0) return str;
8443
8719
 
@@ -8459,6 +8735,7 @@ bw._resolveTemplate = function(str, state, compile) {
8459
8735
  try {
8460
8736
  val = bw._compiledExprs[b.expr](state);
8461
8737
  } catch (e) {
8738
+ if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
8462
8739
  val = '';
8463
8740
  }
8464
8741
  } else {
@@ -8567,7 +8844,7 @@ function ComponentHandle(taco) {
8567
8844
  this._state = {};
8568
8845
  if (o.state) {
8569
8846
  for (var k in o.state) {
8570
- if (Object.prototype.hasOwnProperty.call(o.state, k)) {
8847
+ if (_hop.call(o.state, k)) {
8571
8848
  this._state[k] = o.state[k];
8572
8849
  }
8573
8850
  }
@@ -8576,7 +8853,7 @@ function ComponentHandle(taco) {
8576
8853
  this._actions = {};
8577
8854
  if (o.actions) {
8578
8855
  for (var k2 in o.actions) {
8579
- if (Object.prototype.hasOwnProperty.call(o.actions, k2)) {
8856
+ if (_hop.call(o.actions, k2)) {
8580
8857
  this._actions[k2] = o.actions[k2];
8581
8858
  }
8582
8859
  }
@@ -8586,7 +8863,7 @@ function ComponentHandle(taco) {
8586
8863
  if (o.methods) {
8587
8864
  var self = this;
8588
8865
  for (var k3 in o.methods) {
8589
- if (Object.prototype.hasOwnProperty.call(o.methods, k3)) {
8866
+ if (_hop.call(o.methods, k3)) {
8590
8867
  this._methods[k3] = o.methods[k3];
8591
8868
  (function(methodName, methodFn) {
8592
8869
  self[methodName] = function() {
@@ -8619,14 +8896,23 @@ function ComponentHandle(taco) {
8619
8896
  this._compile = !!o.compile;
8620
8897
  this._bw_refs = {};
8621
8898
  this._refCounter = 0;
8899
+ // Child component ownership (Bug #5)
8900
+ this._children = [];
8901
+ this._parent = null;
8902
+ // Factory metadata for BCCL rebuild (Bug #6)
8903
+ this._factory = taco._bwFactory || null;
8622
8904
  }
8623
8905
 
8906
+ // Short alias for ComponentHandle.prototype (see alias block at top of file).
8907
+ // 28 method definitions × 25 chars = ~700B raw savings in minified output.
8908
+ var _chp = ComponentHandle.prototype;
8909
+
8624
8910
  // ── State Methods ──
8625
8911
 
8626
8912
  /**
8627
8913
  * Get a state value. Dot-path supported: `get('user.name')`
8628
8914
  */
8629
- ComponentHandle.prototype.get = function(key) {
8915
+ _chp.get = function(key) {
8630
8916
  return bw._evaluatePath(this._state, key);
8631
8917
  };
8632
8918
 
@@ -8636,12 +8922,13 @@ ComponentHandle.prototype.get = function(key) {
8636
8922
  * @param {*} value - New value
8637
8923
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
8638
8924
  */
8639
- ComponentHandle.prototype.set = function(key, value, opts) {
8925
+ _chp.set = function(key, value, opts) {
8640
8926
  // Dot-path set
8641
8927
  var parts = key.split('.');
8642
8928
  var obj = this._state;
8643
8929
  for (var i = 0; i < parts.length - 1; i++) {
8644
- if (obj[parts[i]] == null || typeof obj[parts[i]] !== 'object') {
8930
+ if (!_is(obj[parts[i]], 'object')) {
8931
+ if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
8645
8932
  obj[parts[i]] = {};
8646
8933
  }
8647
8934
  obj = obj[parts[i]];
@@ -8661,10 +8948,10 @@ ComponentHandle.prototype.set = function(key, value, opts) {
8661
8948
  /**
8662
8949
  * Get a shallow clone of the full state.
8663
8950
  */
8664
- ComponentHandle.prototype.getState = function() {
8951
+ _chp.getState = function() {
8665
8952
  var clone = {};
8666
8953
  for (var k in this._state) {
8667
- if (Object.prototype.hasOwnProperty.call(this._state, k)) {
8954
+ if (_hop.call(this._state, k)) {
8668
8955
  clone[k] = this._state[k];
8669
8956
  }
8670
8957
  }
@@ -8676,9 +8963,9 @@ ComponentHandle.prototype.getState = function() {
8676
8963
  * @param {Object} updates - Key-value pairs to merge
8677
8964
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
8678
8965
  */
8679
- ComponentHandle.prototype.setState = function(updates, opts) {
8966
+ _chp.setState = function(updates, opts) {
8680
8967
  for (var k in updates) {
8681
- if (Object.prototype.hasOwnProperty.call(updates, k)) {
8968
+ if (_hop.call(updates, k)) {
8682
8969
  this._state[k] = updates[k];
8683
8970
  this._dirtyKeys[k] = true;
8684
8971
  }
@@ -8695,9 +8982,9 @@ ComponentHandle.prototype.setState = function(updates, opts) {
8695
8982
  /**
8696
8983
  * Push a value onto an array in state. Clones the array.
8697
8984
  */
8698
- ComponentHandle.prototype.push = function(key, val) {
8985
+ _chp.push = function(key, val) {
8699
8986
  var arr = this.get(key);
8700
- var newArr = Array.isArray(arr) ? arr.slice() : [];
8987
+ var newArr = _isA(arr) ? arr.slice() : [];
8701
8988
  newArr.push(val);
8702
8989
  this.set(key, newArr);
8703
8990
  };
@@ -8705,9 +8992,9 @@ ComponentHandle.prototype.push = function(key, val) {
8705
8992
  /**
8706
8993
  * Splice an array in state. Clones the array.
8707
8994
  */
8708
- ComponentHandle.prototype.splice = function(key, start, deleteCount) {
8995
+ _chp.splice = function(key, start, deleteCount) {
8709
8996
  var arr = this.get(key);
8710
- var newArr = Array.isArray(arr) ? arr.slice() : [];
8997
+ var newArr = _isA(arr) ? arr.slice() : [];
8711
8998
  var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
8712
8999
  Array.prototype.splice.apply(newArr, args);
8713
9000
  this.set(key, newArr);
@@ -8715,7 +9002,7 @@ ComponentHandle.prototype.splice = function(key, start, deleteCount) {
8715
9002
 
8716
9003
  // ── Scheduling ──
8717
9004
 
8718
- ComponentHandle.prototype._scheduleDirty = function() {
9005
+ _chp._scheduleDirty = function() {
8719
9006
  if (!this._scheduled) {
8720
9007
  this._scheduled = true;
8721
9008
  bw._dirtyComponents.push(this);
@@ -8730,17 +9017,17 @@ ComponentHandle.prototype._scheduleDirty = function() {
8730
9017
  * Creates binding descriptors with refIds for targeted DOM updates.
8731
9018
  * @private
8732
9019
  */
8733
- ComponentHandle.prototype._compileBindings = function() {
9020
+ _chp._compileBindings = function() {
8734
9021
  this._bindings = [];
8735
9022
  this._refCounter = 0;
8736
- var stateKeys = Object.keys(this._state);
9023
+ var stateKeys = _keys(this._state);
8737
9024
  var self = this;
8738
9025
 
8739
9026
  function walkTaco(taco, path) {
8740
- if (taco == null || typeof taco !== 'object' || !taco.t) return taco;
9027
+ if (!_is(taco, 'object') || !taco.t) return taco;
8741
9028
 
8742
9029
  // Check content for bindings
8743
- if (typeof taco.c === 'string' && taco.c.indexOf('${') >= 0) {
9030
+ if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
8744
9031
  var refId = 'bw_ref_' + self._refCounter++;
8745
9032
  var parsed = bw._parseBindings(taco.c);
8746
9033
  var deps = [];
@@ -8762,10 +9049,10 @@ ComponentHandle.prototype._compileBindings = function() {
8762
9049
  // Check attributes for bindings
8763
9050
  if (taco.a) {
8764
9051
  for (var attrName in taco.a) {
8765
- if (!Object.prototype.hasOwnProperty.call(taco.a, attrName)) continue;
9052
+ if (!_hop.call(taco.a, attrName)) continue;
8766
9053
  if (attrName === 'data-bw_ref') continue;
8767
9054
  var attrVal = taco.a[attrName];
8768
- if (typeof attrVal === 'string' && attrVal.indexOf('${') >= 0) {
9055
+ if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
8769
9056
  var refId2 = 'bw_ref_' + self._refCounter++;
8770
9057
  var parsed2 = bw._parseBindings(attrVal);
8771
9058
  var deps2 = [];
@@ -8791,9 +9078,27 @@ ComponentHandle.prototype._compileBindings = function() {
8791
9078
  }
8792
9079
 
8793
9080
  // Recurse into children
8794
- if (Array.isArray(taco.c)) {
9081
+ if (_isA(taco.c)) {
8795
9082
  for (var i = 0; i < taco.c.length; i++) {
8796
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
9083
+ // Wrap string children with ${expr} in a span so patches target the span, not the parent
9084
+ if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
9085
+ var mixedRefId = 'bw_ref_' + self._refCounter++;
9086
+ var mixedParsed = bw._parseBindings(taco.c[i]);
9087
+ var mixedDeps = [];
9088
+ for (var mi = 0; mi < mixedParsed.length; mi++) {
9089
+ mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
9090
+ }
9091
+ self._bindings.push({
9092
+ expr: taco.c[i],
9093
+ type: 'content',
9094
+ refId: mixedRefId,
9095
+ deps: mixedDeps,
9096
+ template: taco.c[i]
9097
+ });
9098
+ // Replace string with a span wrapper so textContent targets the span only
9099
+ taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
9100
+ }
9101
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
8797
9102
  walkTaco(taco.c[i], path.concat(i));
8798
9103
  }
8799
9104
  // Handle bw.when/bw.each markers
@@ -8828,7 +9133,7 @@ ComponentHandle.prototype._compileBindings = function() {
8828
9133
  taco.c[i]._refId = eachRefId;
8829
9134
  }
8830
9135
  }
8831
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9136
+ } else if (_is(taco.c, 'object') && taco.c.t) {
8832
9137
  walkTaco(taco.c, path.concat(0));
8833
9138
  }
8834
9139
 
@@ -8844,7 +9149,7 @@ ComponentHandle.prototype._compileBindings = function() {
8844
9149
  * Build ref map from the live DOM after createDOM.
8845
9150
  * @private
8846
9151
  */
8847
- ComponentHandle.prototype._collectRefs = function() {
9152
+ _chp._collectRefs = function() {
8848
9153
  this._bw_refs = {};
8849
9154
  if (!this.element) return;
8850
9155
  var els = this.element.querySelectorAll('[data-bw_ref]');
@@ -8865,7 +9170,7 @@ ComponentHandle.prototype._collectRefs = function() {
8865
9170
  * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
8866
9171
  * @param {Element} parentEl - DOM element to mount into
8867
9172
  */
8868
- ComponentHandle.prototype.mount = function(parentEl) {
9173
+ _chp.mount = function(parentEl) {
8869
9174
  // willMount hook
8870
9175
  if (this._hooks.willMount) this._hooks.willMount(this);
8871
9176
 
@@ -8887,7 +9192,7 @@ ComponentHandle.prototype.mount = function(parentEl) {
8887
9192
  // Register named actions in function registry
8888
9193
  var self = this;
8889
9194
  for (var actionName in this._actions) {
8890
- if (Object.prototype.hasOwnProperty.call(this._actions, actionName)) {
9195
+ if (_hop.call(this._actions, actionName)) {
8891
9196
  var registeredName = this._bwId + '_' + actionName;
8892
9197
  (function(aName) {
8893
9198
  bw.funcRegister(function(evt) {
@@ -8906,6 +9211,11 @@ ComponentHandle.prototype.mount = function(parentEl) {
8906
9211
  this.element = bw.createDOM(tacoForDOM);
8907
9212
  this.element._bwComponentHandle = this;
8908
9213
  this.element.setAttribute('data-bw_comp_id', this._bwId);
9214
+
9215
+ // Restore o.render from original TACO (stripped by _tacoForDOM)
9216
+ if (this.taco.o && this.taco.o.render) {
9217
+ this.element._bw_render = this.taco.o.render;
9218
+ }
8909
9219
  if (this._userTag) {
8910
9220
  this.element.classList.add(this._userTag);
8911
9221
  }
@@ -8921,6 +9231,16 @@ ComponentHandle.prototype.mount = function(parentEl) {
8921
9231
 
8922
9232
  this.mounted = true;
8923
9233
 
9234
+ // Scan for child ComponentHandles and link parent/child (Bug #5)
9235
+ var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
9236
+ for (var ci = 0; ci < childEls.length; ci++) {
9237
+ var ch = childEls[ci]._bwComponentHandle;
9238
+ if (ch && ch !== this && !ch._parent) {
9239
+ ch._parent = this;
9240
+ this._children.push(ch);
9241
+ }
9242
+ }
9243
+
8924
9244
  // mounted hook (backward compat: fn.length === 2 wraps (el, state))
8925
9245
  if (this._hooks.mounted) {
8926
9246
  if (this._hooks.mounted.length === 2) {
@@ -8929,16 +9249,21 @@ ComponentHandle.prototype.mount = function(parentEl) {
8929
9249
  this._hooks.mounted(this);
8930
9250
  }
8931
9251
  }
9252
+
9253
+ // Invoke o.render on initial mount (if present)
9254
+ if (this.element._bw_render) {
9255
+ this.element._bw_render(this.element, this._state);
9256
+ }
8932
9257
  };
8933
9258
 
8934
9259
  /**
8935
9260
  * Prepare TACO for initial render: resolve when/each markers.
8936
9261
  * @private
8937
9262
  */
8938
- ComponentHandle.prototype._prepareTaco = function(taco) {
8939
- if (!taco || typeof taco !== 'object') return;
9263
+ _chp._prepareTaco = function(taco) {
9264
+ if (!_is(taco, 'object')) return;
8940
9265
 
8941
- if (Array.isArray(taco.c)) {
9266
+ if (_isA(taco.c)) {
8942
9267
  for (var i = taco.c.length - 1; i >= 0; i--) {
8943
9268
  var child = taco.c[i];
8944
9269
  if (child && child._bwWhen) {
@@ -8963,18 +9288,18 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
8963
9288
  var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
8964
9289
  var arr = bw._evaluatePath(this._state, eachExprStr);
8965
9290
  var items = [];
8966
- if (Array.isArray(arr)) {
9291
+ if (_isA(arr)) {
8967
9292
  for (var j = 0; j < arr.length; j++) {
8968
9293
  items.push(child.factory(arr[j], j));
8969
9294
  }
8970
9295
  }
8971
9296
  taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
8972
9297
  }
8973
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
9298
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
8974
9299
  this._prepareTaco(taco.c[i]);
8975
9300
  }
8976
9301
  }
8977
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9302
+ } else if (_is(taco.c, 'object') && taco.c.t) {
8978
9303
  this._prepareTaco(taco.c);
8979
9304
  }
8980
9305
  };
@@ -8983,12 +9308,12 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
8983
9308
  * Wire action name strings (in onclick etc.) to dispatch function calls.
8984
9309
  * @private
8985
9310
  */
8986
- ComponentHandle.prototype._wireActions = function(taco) {
8987
- if (!taco || typeof taco !== 'object' || !taco.t) return;
9311
+ _chp._wireActions = function(taco) {
9312
+ if (!_is(taco, 'object') || !taco.t) return;
8988
9313
  if (taco.a) {
8989
9314
  for (var key in taco.a) {
8990
- if (!Object.prototype.hasOwnProperty.call(taco.a, key)) continue;
8991
- if (key.startsWith('on') && typeof taco.a[key] === 'string') {
9315
+ if (!_hop.call(taco.a, key)) continue;
9316
+ if (key.startsWith('on') && _is(taco.a[key], 'string')) {
8992
9317
  var actionName = taco.a[key];
8993
9318
  if (actionName in this._actions) {
8994
9319
  var registeredName = this._bwId + '_' + actionName;
@@ -9002,11 +9327,11 @@ ComponentHandle.prototype._wireActions = function(taco) {
9002
9327
  }
9003
9328
  }
9004
9329
  }
9005
- if (Array.isArray(taco.c)) {
9330
+ if (_isA(taco.c)) {
9006
9331
  for (var i = 0; i < taco.c.length; i++) {
9007
9332
  this._wireActions(taco.c[i]);
9008
9333
  }
9009
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9334
+ } else if (_is(taco.c, 'object') && taco.c.t) {
9010
9335
  this._wireActions(taco.c);
9011
9336
  }
9012
9337
  };
@@ -9015,7 +9340,7 @@ ComponentHandle.prototype._wireActions = function(taco) {
9015
9340
  * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
9016
9341
  * @private
9017
9342
  */
9018
- ComponentHandle.prototype._deepCloneTaco = function(taco) {
9343
+ _chp._deepCloneTaco = function(taco) {
9019
9344
  if (taco == null) return taco;
9020
9345
  // Preserve _bwWhen / _bwEach markers (contain functions)
9021
9346
  if (taco._bwWhen) {
@@ -9027,18 +9352,18 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
9027
9352
  if (taco._bwEach) {
9028
9353
  return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
9029
9354
  }
9030
- if (typeof taco !== 'object' || !taco.t) return taco;
9355
+ if (!_is(taco, 'object') || !taco.t) return taco;
9031
9356
  var result = { t: taco.t };
9032
9357
  if (taco.a) {
9033
9358
  result.a = {};
9034
9359
  for (var k in taco.a) {
9035
- if (Object.prototype.hasOwnProperty.call(taco.a, k)) result.a[k] = taco.a[k];
9360
+ if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
9036
9361
  }
9037
9362
  }
9038
9363
  if (taco.c != null) {
9039
- if (Array.isArray(taco.c)) {
9364
+ if (_isA(taco.c)) {
9040
9365
  result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
9041
- } else if (typeof taco.c === 'object') {
9366
+ } else if (_is(taco.c, 'object')) {
9042
9367
  result.c = this._deepCloneTaco(taco.c);
9043
9368
  } else {
9044
9369
  result.c = taco.c;
@@ -9052,27 +9377,31 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
9052
9377
  * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
9053
9378
  * @private
9054
9379
  */
9055
- ComponentHandle.prototype._tacoForDOM = function(taco) {
9056
- if (!taco || typeof taco !== 'object' || !taco.t) return taco;
9380
+ _chp._tacoForDOM = function(taco) {
9381
+ if (!_is(taco, 'object') || !taco.t) return taco;
9057
9382
  var result = { t: taco.t };
9058
9383
  if (taco.a) result.a = taco.a;
9059
9384
  if (taco.c != null) {
9060
- if (Array.isArray(taco.c)) {
9385
+ if (_isA(taco.c)) {
9061
9386
  result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
9062
- } else if (typeof taco.c === 'object' && taco.c.t) {
9387
+ } else if (_is(taco.c, 'object') && taco.c.t) {
9063
9388
  result.c = this._tacoForDOM(taco.c);
9064
9389
  } else {
9065
9390
  result.c = taco.c;
9066
9391
  }
9067
9392
  }
9068
9393
  // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
9394
+ if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
9395
+ _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
9396
+ '>. Use onclick attribute or bw.component() for child interactivity.');
9397
+ }
9069
9398
  return result;
9070
9399
  };
9071
9400
 
9072
9401
  /**
9073
9402
  * Unmount: remove from DOM, deactivate, preserve state for re-mount.
9074
9403
  */
9075
- ComponentHandle.prototype.unmount = function() {
9404
+ _chp.unmount = function() {
9076
9405
  if (!this.mounted) return;
9077
9406
 
9078
9407
  // unmount hook
@@ -9107,12 +9436,23 @@ ComponentHandle.prototype.unmount = function() {
9107
9436
  /**
9108
9437
  * Destroy: unmount + clear state + unregister actions.
9109
9438
  */
9110
- ComponentHandle.prototype.destroy = function() {
9439
+ _chp.destroy = function() {
9111
9440
  // willDestroy hook
9112
9441
  if (this._hooks.willDestroy) {
9113
9442
  this._hooks.willDestroy(this);
9114
9443
  }
9115
9444
 
9445
+ // Cascade destroy to children depth-first (Bug #5)
9446
+ for (var ci = this._children.length - 1; ci >= 0; ci--) {
9447
+ this._children[ci].destroy();
9448
+ }
9449
+ this._children = [];
9450
+ if (this._parent) {
9451
+ var idx = this._parent._children.indexOf(this);
9452
+ if (idx >= 0) this._parent._children.splice(idx, 1);
9453
+ this._parent = null;
9454
+ }
9455
+
9116
9456
  this.unmount();
9117
9457
 
9118
9458
  // Unregister actions from function registry
@@ -9139,12 +9479,36 @@ ComponentHandle.prototype.destroy = function() {
9139
9479
  * Flush dirty state: resolve changed bindings and apply to DOM.
9140
9480
  * @private
9141
9481
  */
9142
- ComponentHandle.prototype._flush = function() {
9482
+ _chp._flush = function() {
9143
9483
  this._scheduled = false;
9144
- var changedKeys = Object.keys(this._dirtyKeys);
9484
+ var changedKeys = _keys(this._dirtyKeys);
9145
9485
  this._dirtyKeys = {};
9146
9486
  if (changedKeys.length === 0 || !this.mounted) return;
9147
9487
 
9488
+ // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
9489
+ // rebuild the TACO from the factory with merged state (Bug #6)
9490
+ if (this._factory) {
9491
+ var rebuildNeeded = false;
9492
+ for (var fi = 0; fi < changedKeys.length; fi++) {
9493
+ if (_hop.call(this._factory.props, changedKeys[fi])) {
9494
+ rebuildNeeded = true; break;
9495
+ }
9496
+ }
9497
+ if (rebuildNeeded) {
9498
+ var merged = {};
9499
+ for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
9500
+ for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
9501
+ this._factory.props = merged;
9502
+ var newTaco = bw.make(this._factory.type, merged);
9503
+ newTaco._bwFactory = this._factory;
9504
+ this.taco = newTaco;
9505
+ this._originalTaco = this._deepCloneTaco(newTaco);
9506
+ this._render();
9507
+ if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
9508
+ return;
9509
+ }
9510
+ }
9511
+
9148
9512
  // willUpdate hook
9149
9513
  if (this._hooks.willUpdate) {
9150
9514
  this._hooks.willUpdate(this, changedKeys);
@@ -9183,7 +9547,7 @@ ComponentHandle.prototype._flush = function() {
9183
9547
  * Returns list of patches to apply.
9184
9548
  * @private
9185
9549
  */
9186
- ComponentHandle.prototype._resolveBindings = function(changedKeys) {
9550
+ _chp._resolveBindings = function(changedKeys) {
9187
9551
  var patches = [];
9188
9552
  for (var i = 0; i < this._bindings.length; i++) {
9189
9553
  var b = this._bindings[i];
@@ -9219,11 +9583,14 @@ ComponentHandle.prototype._resolveBindings = function(changedKeys) {
9219
9583
  * Apply patches to DOM.
9220
9584
  * @private
9221
9585
  */
9222
- ComponentHandle.prototype._applyPatches = function(patches) {
9586
+ _chp._applyPatches = function(patches) {
9223
9587
  for (var i = 0; i < patches.length; i++) {
9224
9588
  var p = patches[i];
9225
9589
  var el = this._bw_refs[p.refId];
9226
- if (!el) continue;
9590
+ if (!el) {
9591
+ if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
9592
+ continue;
9593
+ }
9227
9594
  if (p.type === 'content') {
9228
9595
  el.textContent = p.value;
9229
9596
  } else if (p.type === 'attribute') {
@@ -9240,7 +9607,7 @@ ComponentHandle.prototype._applyPatches = function(patches) {
9240
9607
  * Resolve all bindings and apply (used for initial render).
9241
9608
  * @private
9242
9609
  */
9243
- ComponentHandle.prototype._resolveAndApplyAll = function() {
9610
+ _chp._resolveAndApplyAll = function() {
9244
9611
  var patches = [];
9245
9612
  for (var i = 0; i < this._bindings.length; i++) {
9246
9613
  var b = this._bindings[i];
@@ -9263,7 +9630,7 @@ ComponentHandle.prototype._resolveAndApplyAll = function() {
9263
9630
  * Full re-render for structural changes (when/each branch switches).
9264
9631
  * @private
9265
9632
  */
9266
- ComponentHandle.prototype._render = function() {
9633
+ _chp._render = function() {
9267
9634
  if (!this.element || !this.element.parentNode) return;
9268
9635
  var parent = this.element.parentNode;
9269
9636
  var nextSibling = this.element.nextSibling;
@@ -9303,7 +9670,7 @@ ComponentHandle.prototype._render = function() {
9303
9670
  * @param {string} event - Event name (e.g., 'click')
9304
9671
  * @param {Function} handler - Event handler
9305
9672
  */
9306
- ComponentHandle.prototype.on = function(event, handler) {
9673
+ _chp.on = function(event, handler) {
9307
9674
  if (this.element) {
9308
9675
  this.element.addEventListener(event, handler);
9309
9676
  }
@@ -9315,7 +9682,7 @@ ComponentHandle.prototype.on = function(event, handler) {
9315
9682
  * @param {string} event - Event name
9316
9683
  * @param {Function} handler - Handler to remove
9317
9684
  */
9318
- ComponentHandle.prototype.off = function(event, handler) {
9685
+ _chp.off = function(event, handler) {
9319
9686
  if (this.element) {
9320
9687
  this.element.removeEventListener(event, handler);
9321
9688
  }
@@ -9330,7 +9697,7 @@ ComponentHandle.prototype.off = function(event, handler) {
9330
9697
  * @param {Function} handler - Handler function
9331
9698
  * @returns {Function} Unsubscribe function
9332
9699
  */
9333
- ComponentHandle.prototype.sub = function(topic, handler) {
9700
+ _chp.sub = function(topic, handler) {
9334
9701
  var unsub = bw.sub(topic, handler);
9335
9702
  this._subs.push(unsub);
9336
9703
  return unsub;
@@ -9341,10 +9708,10 @@ ComponentHandle.prototype.sub = function(topic, handler) {
9341
9708
  * @param {string} name - Action name
9342
9709
  * @param {...*} args - Arguments passed after comp
9343
9710
  */
9344
- ComponentHandle.prototype.action = function(name) {
9711
+ _chp.action = function(name) {
9345
9712
  var fn = this._actions[name];
9346
9713
  if (!fn) {
9347
- console.warn('ComponentHandle.action: unknown action "' + name + '"');
9714
+ _cw('ComponentHandle.action: unknown action "' + name + '"');
9348
9715
  return;
9349
9716
  }
9350
9717
  var args = [this].concat(Array.prototype.slice.call(arguments, 1));
@@ -9356,7 +9723,7 @@ ComponentHandle.prototype.action = function(name) {
9356
9723
  * @param {string} sel - CSS selector
9357
9724
  * @returns {Element|null}
9358
9725
  */
9359
- ComponentHandle.prototype.select = function(sel) {
9726
+ _chp.select = function(sel) {
9360
9727
  return this.element ? this.element.querySelector(sel) : null;
9361
9728
  };
9362
9729
 
@@ -9365,7 +9732,7 @@ ComponentHandle.prototype.select = function(sel) {
9365
9732
  * @param {string} sel - CSS selector
9366
9733
  * @returns {Element[]}
9367
9734
  */
9368
- ComponentHandle.prototype.selectAll = function(sel) {
9735
+ _chp.selectAll = function(sel) {
9369
9736
  if (!this.element) return [];
9370
9737
  return Array.prototype.slice.call(this.element.querySelectorAll(sel));
9371
9738
  };
@@ -9376,7 +9743,7 @@ ComponentHandle.prototype.selectAll = function(sel) {
9376
9743
  * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
9377
9744
  * @returns {ComponentHandle} this (for chaining)
9378
9745
  */
9379
- ComponentHandle.prototype.userTag = function(tag) {
9746
+ _chp.userTag = function(tag) {
9380
9747
  this._userTag = tag;
9381
9748
  if (this.element) {
9382
9749
  this.element.classList.add(tag);
@@ -9477,14 +9844,399 @@ bw.message = function(target, action, data) {
9477
9844
  }
9478
9845
  if (!el || !el._bwComponentHandle) return false;
9479
9846
  var comp = el._bwComponentHandle;
9480
- if (typeof comp[action] !== 'function') {
9481
- console.warn('bw.message: unknown action "' + action + '" on component ' + target);
9847
+ if (!_is(comp[action], 'function')) {
9848
+ _cw('bw.message: unknown action "' + action + '" on component ' + target);
9482
9849
  return false;
9483
9850
  }
9484
9851
  comp[action](data);
9485
9852
  return true;
9486
9853
  };
9487
9854
 
9855
+ // ===================================================================================
9856
+ // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
9857
+ // ===================================================================================
9858
+
9859
+ /**
9860
+ * Registry of named functions sent via register messages.
9861
+ * Populated by clientApply({ type: 'register', name, body }).
9862
+ * Invoked by clientApply({ type: 'call', name, args }).
9863
+ * @private
9864
+ */
9865
+ bw._clientFunctions = {};
9866
+
9867
+ /**
9868
+ * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
9869
+ * Default false — exec messages are rejected unless explicitly opted in.
9870
+ * @private
9871
+ */
9872
+ bw._allowExec = false;
9873
+
9874
+ /**
9875
+ * Built-in client functions available via call() without registration.
9876
+ * @private
9877
+ */
9878
+ bw._builtinClientFunctions = {
9879
+ scrollTo: function(selector) {
9880
+ var el = bw._el(selector);
9881
+ if (el) el.scrollTop = el.scrollHeight;
9882
+ },
9883
+ focus: function(selector) {
9884
+ var el = bw._el(selector);
9885
+ if (el && _is(el.focus, 'function')) el.focus();
9886
+ },
9887
+ download: function(filename, content, mimeType) {
9888
+ if (typeof document === 'undefined') return;
9889
+ var blob = new Blob([content], { type: mimeType || 'text/plain' });
9890
+ var a = document.createElement('a');
9891
+ a.href = URL.createObjectURL(blob);
9892
+ a.download = filename;
9893
+ a.click();
9894
+ URL.revokeObjectURL(a.href);
9895
+ },
9896
+ clipboard: function(text) {
9897
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
9898
+ navigator.clipboard.writeText(text);
9899
+ }
9900
+ },
9901
+ redirect: function(url) {
9902
+ if (typeof window !== 'undefined') window.location.href = url;
9903
+ },
9904
+ log: function() {
9905
+ console.log.apply(console, arguments);
9906
+ }
9907
+ };
9908
+
9909
+ /**
9910
+ * Parse a bwserve protocol message string, supporting both strict JSON
9911
+ * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
9912
+ *
9913
+ * The r-prefix format is designed for C/C++ string literals where
9914
+ * double-quote escaping is painful. The parser is a state machine
9915
+ * that walks character by character — not a regex replace.
9916
+ *
9917
+ * Escaping: apostrophes inside single-quoted values must be escaped
9918
+ * with backslash: r{'name':'Barry\'s room'}
9919
+ *
9920
+ * @param {string} str - JSON or r-prefixed relaxed JSON string
9921
+ * @returns {Object} Parsed message object
9922
+ * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
9923
+ * @category Server
9924
+ */
9925
+ bw.clientParse = function(str) {
9926
+ str = (str || '').trim();
9927
+ if (str.charAt(0) !== 'r') return JSON.parse(str);
9928
+ str = str.slice(1);
9929
+
9930
+ var out = [];
9931
+ var i = 0;
9932
+ var len = str.length;
9933
+
9934
+ while (i < len) {
9935
+ var ch = str[i];
9936
+
9937
+ if (ch === "'") {
9938
+ // Single-quoted string → emit as double-quoted
9939
+ out.push('"');
9940
+ i++;
9941
+ while (i < len) {
9942
+ var c = str[i];
9943
+ if (c === '\\' && i + 1 < len) {
9944
+ var next = str[i + 1];
9945
+ if (next === "'") {
9946
+ out.push("'"); // \' in input → ' in output
9947
+ } else {
9948
+ out.push('\\');
9949
+ out.push(next);
9950
+ }
9951
+ i += 2;
9952
+ } else if (c === '"') {
9953
+ out.push('\\"');
9954
+ i++;
9955
+ } else if (c === "'") {
9956
+ break;
9957
+ } else {
9958
+ out.push(c);
9959
+ i++;
9960
+ }
9961
+ }
9962
+ out.push('"');
9963
+ i++; // skip closing '
9964
+
9965
+ } else if (ch === '"') {
9966
+ // Double-quoted string — pass through verbatim
9967
+ out.push(ch);
9968
+ i++;
9969
+ while (i < len) {
9970
+ var c2 = str[i];
9971
+ if (c2 === '\\' && i + 1 < len) {
9972
+ out.push(c2);
9973
+ out.push(str[i + 1]);
9974
+ i += 2;
9975
+ } else {
9976
+ out.push(c2);
9977
+ i++;
9978
+ if (c2 === '"') break;
9979
+ }
9980
+ }
9981
+
9982
+ } else if (ch === ',') {
9983
+ // Trailing comma check: skip comma if next non-whitespace is } or ]
9984
+ var j = i + 1;
9985
+ while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) j++;
9986
+ if (j < len && (str[j] === '}' || str[j] === ']')) {
9987
+ i++; // skip trailing comma
9988
+ } else {
9989
+ out.push(ch);
9990
+ i++;
9991
+ }
9992
+
9993
+ } else {
9994
+ out.push(ch);
9995
+ i++;
9996
+ }
9997
+ }
9998
+
9999
+ return JSON.parse(out.join(''));
10000
+ };
10001
+
10002
+ /**
10003
+ * Apply a bwserve protocol message to the DOM.
10004
+ *
10005
+ * Dispatches one of 9 message types:
10006
+ * replace — bw.DOM(target, node)
10007
+ * append — target.appendChild(bw.createDOM(node))
10008
+ * remove — bw.cleanup(target); target.remove()
10009
+ * patch — bw.patch(target, content, attr)
10010
+ * batch — iterate ops, call clientApply for each
10011
+ * message — bw.message(target, action, data)
10012
+ * register — store a named function for later call()
10013
+ * call — invoke a registered or built-in function
10014
+ * exec — execute arbitrary JS (requires allowExec)
10015
+ *
10016
+ * Target resolution:
10017
+ * Starts with '#' or '.' → CSS selector (querySelector)
10018
+ * Otherwise → getElementById, then bw._el fallback
10019
+ *
10020
+ * @param {Object} msg - Protocol message
10021
+ * @returns {boolean} true if the message was applied successfully
10022
+ * @category Server
10023
+ */
10024
+ bw.clientApply = function(msg) {
10025
+ if (!msg || !msg.type) return false;
10026
+
10027
+ var type = msg.type;
10028
+ var target = msg.target;
10029
+
10030
+ if (type === 'replace') {
10031
+ var el = bw._el(target);
10032
+ if (!el) return false;
10033
+ bw.DOM(el, msg.node);
10034
+ return true;
10035
+
10036
+ } else if (type === 'patch') {
10037
+ var patched = bw.patch(target, msg.content, msg.attr);
10038
+ return patched !== null;
10039
+
10040
+ } else if (type === 'append') {
10041
+ var parent = bw._el(target);
10042
+ if (!parent) return false;
10043
+ var child = bw.createDOM(msg.node);
10044
+ parent.appendChild(child);
10045
+ return true;
10046
+
10047
+ } else if (type === 'remove') {
10048
+ var toRemove = bw._el(target);
10049
+ if (!toRemove) return false;
10050
+ if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
10051
+ toRemove.remove();
10052
+ return true;
10053
+
10054
+ } else if (type === 'batch') {
10055
+ if (!_isA(msg.ops)) return false;
10056
+ var allOk = true;
10057
+ msg.ops.forEach(function(op) {
10058
+ if (!bw.clientApply(op)) allOk = false;
10059
+ });
10060
+ return allOk;
10061
+
10062
+ } else if (type === 'message') {
10063
+ return bw.message(msg.target, msg.action, msg.data);
10064
+
10065
+ } else if (type === 'register') {
10066
+ if (!msg.name || !msg.body) return false;
10067
+ try {
10068
+ bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
10069
+ return true;
10070
+ } catch (e) {
10071
+ _ce('[bw] register error:', msg.name, e);
10072
+ return false;
10073
+ }
10074
+
10075
+ } else if (type === 'call') {
10076
+ if (!msg.name) return false;
10077
+ var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
10078
+ if (!_is(fn, 'function')) return false;
10079
+ try {
10080
+ var args = _isA(msg.args) ? msg.args : [];
10081
+ fn.apply(null, args);
10082
+ return true;
10083
+ } catch (e) {
10084
+ _ce('[bw] call error:', msg.name, e);
10085
+ return false;
10086
+ }
10087
+
10088
+ } else if (type === 'exec') {
10089
+ if (!bw._allowExec) {
10090
+ _cw('[bw] exec rejected: allowExec is not enabled');
10091
+ return false;
10092
+ }
10093
+ if (!msg.code) return false;
10094
+ try {
10095
+ new Function(msg.code)();
10096
+ return true;
10097
+ } catch (e) {
10098
+ _ce('[bw] exec error:', e);
10099
+ return false;
10100
+ }
10101
+ }
10102
+
10103
+ return false;
10104
+ };
10105
+
10106
+ /**
10107
+ * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
10108
+ *
10109
+ * Returns a connection object with sendAction(), on(), and close() methods.
10110
+ *
10111
+ * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
10112
+ * @param {Object} [opts] - Connection options
10113
+ * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
10114
+ * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
10115
+ * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
10116
+ * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
10117
+ * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
10118
+ * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
10119
+ * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
10120
+ * @returns {Object} Connection object { sendAction, on, close, status }
10121
+ * @category Server
10122
+ */
10123
+ bw.clientConnect = function(url, opts) {
10124
+ opts = opts || {};
10125
+ var transport = opts.transport || 'sse';
10126
+ var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
10127
+ var reconnect = opts.reconnect !== false;
10128
+ var onStatus = opts.onStatus || function() {};
10129
+ var onMessage = opts.onMessage || null;
10130
+ var handlers = {};
10131
+ // Set the global allowExec flag from connection options
10132
+ bw._allowExec = !!opts.allowExec;
10133
+ var conn = {
10134
+ status: 'connecting',
10135
+ _es: null,
10136
+ _pollTimer: null
10137
+ };
10138
+
10139
+ function setStatus(s) {
10140
+ conn.status = s;
10141
+ onStatus(s);
10142
+ }
10143
+
10144
+ function handleMessage(data) {
10145
+ try {
10146
+ var msg = _is(data, 'string') ? bw.clientParse(data) : data;
10147
+ if (onMessage) onMessage(msg);
10148
+ if (handlers.message) handlers.message(msg);
10149
+ bw.clientApply(msg);
10150
+ } catch (e) {
10151
+ if (handlers.error) handlers.error(e);
10152
+ }
10153
+ }
10154
+
10155
+ if (transport === 'sse' && typeof EventSource !== 'undefined') {
10156
+ setStatus('connecting');
10157
+ var es = new EventSource(url);
10158
+ conn._es = es;
10159
+
10160
+ es.onopen = function() {
10161
+ setStatus('connected');
10162
+ if (handlers.open) handlers.open();
10163
+ };
10164
+
10165
+ es.onmessage = function(e) {
10166
+ handleMessage(e.data);
10167
+ };
10168
+
10169
+ es.onerror = function() {
10170
+ if (conn.status === 'connected') {
10171
+ setStatus('disconnected');
10172
+ }
10173
+ if (handlers.error) handlers.error(new Error('SSE connection error'));
10174
+ if (!reconnect) {
10175
+ es.close();
10176
+ }
10177
+ // EventSource auto-reconnects by default when reconnect=true
10178
+ };
10179
+ } else if (transport === 'poll') {
10180
+ var interval = opts.interval || 2000;
10181
+ setStatus('connected');
10182
+ conn._pollTimer = setInterval(function() {
10183
+ fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
10184
+ if (_isA(msgs)) {
10185
+ msgs.forEach(handleMessage);
10186
+ } else if (msgs && msgs.type) {
10187
+ handleMessage(msgs);
10188
+ }
10189
+ }).catch(function(e) {
10190
+ if (handlers.error) handlers.error(e);
10191
+ });
10192
+ }, interval);
10193
+ }
10194
+
10195
+ /**
10196
+ * Send an action to the server via POST.
10197
+ * @param {string} action - Action name
10198
+ * @param {Object} [data] - Action payload
10199
+ */
10200
+ conn.sendAction = function(action, data) {
10201
+ var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
10202
+ fetch(actionUrl, {
10203
+ method: 'POST',
10204
+ headers: { 'Content-Type': 'application/json' },
10205
+ body: body
10206
+ }).catch(function(e) {
10207
+ if (handlers.error) handlers.error(e);
10208
+ });
10209
+ };
10210
+
10211
+ /**
10212
+ * Register an event handler.
10213
+ * @param {string} event - 'open'|'message'|'error'|'close'
10214
+ * @param {Function} handler
10215
+ */
10216
+ conn.on = function(event, handler) {
10217
+ handlers[event] = handler;
10218
+ return conn;
10219
+ };
10220
+
10221
+ /**
10222
+ * Close the connection.
10223
+ */
10224
+ conn.close = function() {
10225
+ if (conn._es) {
10226
+ conn._es.close();
10227
+ conn._es = null;
10228
+ }
10229
+ if (conn._pollTimer) {
10230
+ clearInterval(conn._pollTimer);
10231
+ conn._pollTimer = null;
10232
+ }
10233
+ setStatus('disconnected');
10234
+ if (handlers.close) handlers.close();
10235
+ };
10236
+
10237
+ return conn;
10238
+ };
10239
+
9488
10240
  // ===================================================================================
9489
10241
  // bw.inspect() — Debug utility
9490
10242
  // ===================================================================================
@@ -9511,33 +10263,33 @@ bw.inspect = function(target) {
9511
10263
  el = target.element;
9512
10264
  comp = target;
9513
10265
  } else {
9514
- if (typeof target === 'string') {
10266
+ if (_is(target, 'string')) {
9515
10267
  el = bw.$(target)[0];
9516
10268
  }
9517
10269
  if (!el) {
9518
- console.warn('bw.inspect: element not found');
10270
+ _cw('bw.inspect: element not found');
9519
10271
  return null;
9520
10272
  }
9521
10273
  comp = el._bwComponentHandle;
9522
10274
  }
9523
10275
  if (!comp) {
9524
- console.log('bw.inspect: no ComponentHandle on this element');
9525
- console.log(' Tag:', el.tagName);
9526
- console.log(' Classes:', el.className);
9527
- console.log(' _bw_state:', el._bw_state || '(none)');
10276
+ _cl('bw.inspect: no ComponentHandle on this element');
10277
+ _cl(' Tag:', el.tagName);
10278
+ _cl(' Classes:', el.className);
10279
+ _cl(' _bw_state:', el._bw_state || '(none)');
9528
10280
  return null;
9529
10281
  }
9530
10282
  var deps = comp._bindings.reduce(function(s, b) {
9531
10283
  return s.concat(b.deps || []);
9532
10284
  }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
9533
10285
  console.group('Component: ' + comp._bwId);
9534
- console.log('State:', comp._state);
9535
- console.log('Bindings:', comp._bindings.length, '(deps:', deps, ')');
9536
- console.log('Methods:', Object.keys(comp._methods));
9537
- console.log('Actions:', Object.keys(comp._actions));
9538
- console.log('User tag:', comp._userTag || '(none)');
9539
- console.log('Mounted:', comp.mounted);
9540
- console.log('Element:', comp.element);
10286
+ _cl('State:', comp._state);
10287
+ _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
10288
+ _cl('Methods:', _keys(comp._methods));
10289
+ _cl('Actions:', _keys(comp._actions));
10290
+ _cl('User tag:', comp._userTag || '(none)');
10291
+ _cl('Mounted:', comp.mounted);
10292
+ _cl('Element:', comp.element);
9541
10293
  console.groupEnd();
9542
10294
  return comp;
9543
10295
  };
@@ -9560,8 +10312,8 @@ bw.compile = function(taco) {
9560
10312
  // Pre-extract all binding expressions
9561
10313
  var precompiled = [];
9562
10314
  function walkExpressions(node) {
9563
- if (!node || typeof node !== 'object') return;
9564
- if (typeof node.c === 'string' && node.c.indexOf('${') >= 0) {
10315
+ if (!_is(node, 'object')) return;
10316
+ if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
9565
10317
  var parsed = bw._parseBindings(node.c);
9566
10318
  for (var i = 0; i < parsed.length; i++) {
9567
10319
  try {
@@ -9576,9 +10328,9 @@ bw.compile = function(taco) {
9576
10328
  }
9577
10329
  if (node.a) {
9578
10330
  for (var key in node.a) {
9579
- if (Object.prototype.hasOwnProperty.call(node.a, key)) {
10331
+ if (_hop.call(node.a, key)) {
9580
10332
  var v = node.a[key];
9581
- if (typeof v === 'string' && v.indexOf('${') >= 0) {
10333
+ if (_is(v, 'string') && v.indexOf('${') >= 0) {
9582
10334
  var parsed2 = bw._parseBindings(v);
9583
10335
  for (var j = 0; j < parsed2.length; j++) {
9584
10336
  try {
@@ -9594,9 +10346,9 @@ bw.compile = function(taco) {
9594
10346
  }
9595
10347
  }
9596
10348
  }
9597
- if (Array.isArray(node.c)) {
10349
+ if (_isA(node.c)) {
9598
10350
  for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
9599
- } else if (node.c && typeof node.c === 'object' && node.c.t) {
10351
+ } else if (_is(node.c, 'object') && node.c.t) {
9600
10352
  walkExpressions(node.c);
9601
10353
  }
9602
10354
  }
@@ -9608,7 +10360,7 @@ bw.compile = function(taco) {
9608
10360
  handle._precompiledBindings = precompiled;
9609
10361
  if (initialState) {
9610
10362
  for (var k in initialState) {
9611
- if (Object.prototype.hasOwnProperty.call(initialState, k)) {
10363
+ if (_hop.call(initialState, k)) {
9612
10364
  handle._state[k] = initialState[k];
9613
10365
  }
9614
10366
  }
@@ -9639,18 +10391,18 @@ bw.compile = function(taco) {
9639
10391
  bw.css = function(rules, options = {}) {
9640
10392
  const { minify = false, pretty = !minify } = options;
9641
10393
 
9642
- if (typeof rules === 'string') return rules;
10394
+ if (_is(rules, 'string')) return rules;
9643
10395
 
9644
10396
  let css = '';
9645
10397
  const indent = pretty ? ' ' : '';
9646
10398
  const newline = pretty ? '\n' : '';
9647
10399
  const space = pretty ? ' ' : '';
9648
10400
 
9649
- if (Array.isArray(rules)) {
10401
+ if (_isA(rules)) {
9650
10402
  css = rules.map(rule => bw.css(rule, options)).join(newline);
9651
- } else if (typeof rules === 'object') {
10403
+ } else if (_is(rules, 'object')) {
9652
10404
  Object.entries(rules).forEach(([selector, styles]) => {
9653
- if (typeof styles === 'object' && !Array.isArray(styles)) {
10405
+ if (_is(styles, 'object')) {
9654
10406
  // Handle @media, @keyframes, @supports — recurse into nested block
9655
10407
  if (selector.charAt(0) === '@') {
9656
10408
  const inner = bw.css(styles, options);
@@ -9699,7 +10451,7 @@ bw.css = function(rules, options = {}) {
9699
10451
  */
9700
10452
  bw.injectCSS = function(css, options = {}) {
9701
10453
  if (!bw._isBrowser) {
9702
- console.warn('bw.injectCSS requires a DOM environment');
10454
+ _cw('bw.injectCSS requires a DOM environment');
9703
10455
  return null;
9704
10456
  }
9705
10457
 
@@ -9716,7 +10468,7 @@ bw.injectCSS = function(css, options = {}) {
9716
10468
  }
9717
10469
 
9718
10470
  // Convert CSS if needed
9719
- const cssStr = typeof css === 'string' ? css : bw.css(css, options);
10471
+ const cssStr = _is(css, 'string') ? css : bw.css(css, options);
9720
10472
 
9721
10473
  // Set or append CSS
9722
10474
  if (append && styleEl.textContent) {
@@ -9746,7 +10498,7 @@ bw.s = function() {
9746
10498
  var result = {};
9747
10499
  for (var i = 0; i < arguments.length; i++) {
9748
10500
  var arg = arguments[i];
9749
- if (arg && typeof arg === 'object') Object.assign(result, arg);
10501
+ if (_is(arg, 'object')) Object.assign(result, arg);
9750
10502
  }
9751
10503
  return result;
9752
10504
  };
@@ -9869,7 +10621,7 @@ bw.u = {
9869
10621
  bw.responsive = function(selector, breakpoints) {
9870
10622
  var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
9871
10623
  var parts = [];
9872
- Object.keys(breakpoints).forEach(function(key) {
10624
+ _keys(breakpoints).forEach(function(key) {
9873
10625
  var rules = {};
9874
10626
  if (key === 'base') {
9875
10627
  rules[selector] = breakpoints[key];
@@ -9941,18 +10693,18 @@ if (bw._isBrowser) {
9941
10693
  if (!selector) return [];
9942
10694
 
9943
10695
  // Already an array
9944
- if (Array.isArray(selector)) return selector;
10696
+ if (_isA(selector)) return selector;
9945
10697
 
9946
10698
  // Single element
9947
10699
  if (selector.nodeType) return [selector];
9948
10700
 
9949
10701
  // NodeList or HTMLCollection
9950
- if (selector.length !== undefined && typeof selector !== 'string') {
10702
+ if (selector.length !== undefined && !_is(selector, 'string')) {
9951
10703
  return Array.from(selector);
9952
10704
  }
9953
10705
 
9954
10706
  // CSS selector string
9955
- if (typeof selector === 'string') {
10707
+ if (_is(selector, 'string')) {
9956
10708
  return Array.from(document.querySelectorAll(selector));
9957
10709
  }
9958
10710
 
@@ -10456,7 +11208,7 @@ bw.makeTable = function(config) {
10456
11208
 
10457
11209
  // Auto-detect columns if not provided
10458
11210
  const cols = columns || (data.length > 0
10459
- ? Object.keys(data[0]).map(key => ({ key, label: key }))
11211
+ ? _keys(data[0]).map(key => ({ key, label: key }))
10460
11212
  : []);
10461
11213
 
10462
11214
  // Current sort state
@@ -10471,7 +11223,7 @@ bw.makeTable = function(config) {
10471
11223
  const bVal = b[currentSortColumn];
10472
11224
 
10473
11225
  // Handle different types
10474
- if (typeof aVal === 'number' && typeof bVal === 'number') {
11226
+ if (_is(aVal, 'number') && _is(bVal, 'number')) {
10475
11227
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
10476
11228
  }
10477
11229
 
@@ -10581,7 +11333,7 @@ bw.makeTable = function(config) {
10581
11333
  bw.makeTableFromArray = function(config) {
10582
11334
  const { data = [], headerRow = true, columns, ...rest } = config;
10583
11335
 
10584
- if (!Array.isArray(data) || data.length === 0) {
11336
+ if (!_isA(data) || data.length === 0) {
10585
11337
  return bw.makeTable({ data: [], columns: columns || [], ...rest });
10586
11338
  }
10587
11339
 
@@ -10663,7 +11415,7 @@ bw.makeBarChart = function(config) {
10663
11415
  className = ''
10664
11416
  } = config;
10665
11417
 
10666
- if (!Array.isArray(data) || data.length === 0) {
11418
+ if (!_isA(data) || data.length === 0) {
10667
11419
  return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
10668
11420
  }
10669
11421
 
@@ -10812,7 +11564,7 @@ bw._componentRegistry = new Map();
10812
11564
  */
10813
11565
  bw.render = function(element, position, taco) {
10814
11566
  // Get target element
10815
- const targetEl = typeof element === 'string'
11567
+ const targetEl = _is(element, 'string')
10816
11568
  ? document.querySelector(element)
10817
11569
  : element;
10818
11570
 
@@ -10962,7 +11714,7 @@ bw.render = function(element, position, taco) {
10962
11714
  setContent(content) {
10963
11715
  this._taco.c = content;
10964
11716
  if (this.element) {
10965
- if (typeof content === 'string') {
11717
+ if (_is(content, 'string')) {
10966
11718
  this.element.textContent = content;
10967
11719
  } else {
10968
11720
  // Re-render for complex content