bitwrench 2.0.15 → 2.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +57 -21
  2. package/dist/bitwrench-bccl.cjs.js +3750 -0
  3. package/dist/bitwrench-bccl.cjs.min.js +40 -0
  4. package/dist/bitwrench-bccl.esm.js +3745 -0
  5. package/dist/bitwrench-bccl.esm.min.js +40 -0
  6. package/dist/bitwrench-bccl.umd.js +3756 -0
  7. package/dist/bitwrench-bccl.umd.min.js +40 -0
  8. package/dist/bitwrench-code-edit.cjs.js +57 -7
  9. package/dist/bitwrench-code-edit.cjs.min.js +9 -2
  10. package/dist/bitwrench-code-edit.es5.js +74 -11
  11. package/dist/bitwrench-code-edit.es5.min.js +9 -2
  12. package/dist/bitwrench-code-edit.esm.js +57 -7
  13. package/dist/bitwrench-code-edit.esm.min.js +9 -2
  14. package/dist/bitwrench-code-edit.umd.js +57 -7
  15. package/dist/bitwrench-code-edit.umd.min.js +9 -2
  16. package/dist/bitwrench-lean.cjs.js +905 -157
  17. package/dist/bitwrench-lean.cjs.min.js +7 -7
  18. package/dist/bitwrench-lean.es5.js +931 -157
  19. package/dist/bitwrench-lean.es5.min.js +5 -5
  20. package/dist/bitwrench-lean.esm.js +904 -157
  21. package/dist/bitwrench-lean.esm.min.js +7 -7
  22. package/dist/bitwrench-lean.umd.js +905 -157
  23. package/dist/bitwrench-lean.umd.min.js +7 -7
  24. package/dist/bitwrench.cjs.js +910 -158
  25. package/dist/bitwrench.cjs.min.js +8 -8
  26. package/dist/bitwrench.css +60 -17
  27. package/dist/bitwrench.es5.js +939 -158
  28. package/dist/bitwrench.es5.min.js +6 -6
  29. package/dist/bitwrench.esm.js +909 -158
  30. package/dist/bitwrench.esm.min.js +8 -8
  31. package/dist/bitwrench.min.css +1 -1
  32. package/dist/bitwrench.umd.js +910 -158
  33. package/dist/bitwrench.umd.min.js +8 -8
  34. package/dist/builds.json +168 -80
  35. package/dist/bwserve.cjs.js +660 -0
  36. package/dist/bwserve.esm.js +652 -0
  37. package/dist/sri.json +36 -28
  38. package/package.json +20 -3
  39. package/readme.html +62 -23
  40. package/src/bitwrench-bccl-entry.js +72 -0
  41. package/src/bitwrench-bccl.js +5 -1
  42. package/src/bitwrench-code-edit.js +56 -6
  43. package/src/bitwrench-color-utils.js +5 -6
  44. package/src/bitwrench-styles.js +20 -8
  45. package/src/bitwrench.js +876 -140
  46. package/src/bwserve/client.js +182 -0
  47. package/src/bwserve/index.js +363 -0
  48. package/src/bwserve/shell.js +106 -0
  49. package/src/cli/index.js +36 -15
  50. package/src/cli/layout-default.js +47 -32
  51. package/src/cli/serve.js +325 -0
  52. package/src/version.js +3 -3
  53. /package/bin/{bitwrench.js → bwcli.js} +0 -0
@@ -1,18 +1,18 @@
1
- /*! bitwrench 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
  /**
3
3
  * Auto-generated version file from package.json
4
4
  * DO NOT EDIT DIRECTLY - Use npm run generate-version
5
5
  */
6
6
 
7
7
  const VERSION_INFO = {
8
- version: '2.0.15',
8
+ version: '2.0.17',
9
9
  name: 'bitwrench',
10
10
  description: 'A library for javascript UI functions.',
11
11
  license: 'BSD-2-Clause',
12
12
  homepage: 'https://deftio.github.com/bitwrench/pages',
13
13
  repository: 'git+https://github.com/deftio/bitwrench.git',
14
14
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
15
- buildDate: '2026-03-10T09:08:17.015Z'
15
+ buildDate: '2026-03-13T23:15:10.823Z'
16
16
  };
17
17
 
18
18
  /**
@@ -430,12 +430,11 @@ function derivePalette(config) {
430
430
  var lightBase = config.light || hslToHex([h, 8, 97]);
431
431
  var darkBase = config.dark || hslToHex([h, 10, 13]);
432
432
 
433
- // Background & surface tokens — default to light (white/near-white).
434
- // Dark backgrounds require explicit config.background / config.surface.
435
- // Primary/secondary colors are accents, not page backgrounds, so
436
- // isLightPalette should NOT drive bg/surface defaults.
437
- var bgBase = config.background || '#ffffff';
438
- var surfBase = config.surface || '#f8f9fa';
433
+ // Background & surface tokens — tinted with primary hue for theme personality.
434
+ // Very subtle: bg at L=98/S=6, surface at L=96/S=8.
435
+ // User can override with config.background / config.surface.
436
+ var bgBase = config.background || hslToHex([h, 6, 98]);
437
+ var surfBase = config.surface || hslToHex([h, 8, 96]);
439
438
 
440
439
  var palette = {
441
440
  primary: deriveShades(config.primary),
@@ -1568,7 +1567,7 @@ var structuralRules = {
1568
1567
  '@media (min-width: 992px)': { '.bw_container': { 'max-width': '960px' } },
1569
1568
  '@media (min-width: 1200px)': { '.bw_container': { 'max-width': '1140px' } },
1570
1569
  '.bw_container_fluid': {
1571
- 'width': '100%', 'padding-right': '15px', 'padding-left': '15px',
1570
+ 'width': '100%', 'padding-right': '0.75rem', 'padding-left': '0.75rem',
1572
1571
  'margin-right': 'auto', 'margin-left': 'auto'
1573
1572
  },
1574
1573
  '.bw_row': {
@@ -1729,7 +1728,8 @@ var structuralRules = {
1729
1728
  '.bw_badge': {
1730
1729
  'display': 'inline-block', 'font-size': '0.875rem',
1731
1730
  'font-weight': '600', 'line-height': '1.3', 'text-align': 'center',
1732
- 'white-space': 'nowrap', 'vertical-align': 'baseline'
1731
+ 'white-space': 'nowrap', 'vertical-align': 'baseline',
1732
+ 'padding': '0.35rem 0.65rem', 'border-radius': '0.25rem'
1733
1733
  },
1734
1734
  '.bw_badge:empty': { 'display': 'none' },
1735
1735
  '.bw_badge_sm': { 'font-size': '0.75rem', 'padding': '0.25rem 0.5rem' },
@@ -1914,7 +1914,7 @@ var structuralRules = {
1914
1914
  // ---- Code demo ----
1915
1915
  codeDemo: {
1916
1916
  '.bw_code_demo': { 'margin-bottom': '2rem' },
1917
- '.bw_code_pre': { 'margin': '0', 'border': 'none', 'overflow-x': 'auto' },
1917
+ '.bw_code_pre': { 'margin': '0', 'border': 'none', 'overflow-x': 'auto', 'max-width': '100%' },
1918
1918
  '.bw_code_block': {
1919
1919
  'display': 'block', 'padding': '1.25rem',
1920
1920
  'font-family': '"SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace',
@@ -2011,7 +2011,7 @@ var structuralRules = {
2011
2011
  },
2012
2012
  '.bw_modal.bw_modal_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
2013
2013
  '.bw_modal_dialog': {
2014
- 'position': 'relative', 'width': '100%', 'max-width': '500px', 'margin': '1.75rem auto',
2014
+ 'position': 'relative', 'width': 'calc(100% - 1rem)', 'max-width': '500px', 'margin': '1.75rem auto',
2015
2015
  'pointer-events': 'none'
2016
2016
  },
2017
2017
  '.bw_modal.bw_modal_show .bw_modal_dialog': { 'transform': 'translateY(0)' },
@@ -2041,7 +2041,7 @@ var structuralRules = {
2041
2041
  '.bw_toast_container.bw_toast_top_center': { 'top': '0', 'left': '50%', 'transform': 'translateX(-50%)' },
2042
2042
  '.bw_toast_container.bw_toast_bottom_center': { 'bottom': '0', 'left': '50%', 'transform': 'translateX(-50%)' },
2043
2043
  '.bw_toast': {
2044
- 'pointer-events': 'auto', 'width': '350px', 'max-width': '100%', 'background-clip': 'padding-box',
2044
+ 'pointer-events': 'auto', 'width': '350px', 'max-width': 'calc(100vw - 2rem)', 'background-clip': 'padding-box',
2045
2045
  'opacity': '0'
2046
2046
  },
2047
2047
  '.bw_toast.bw_toast_show': { 'opacity': '1', 'transform': 'translateY(0)' },
@@ -2127,7 +2127,7 @@ var structuralRules = {
2127
2127
  '.bw_tooltip_wrapper': { 'position': 'relative', 'display': 'inline-block' },
2128
2128
  '.bw_tooltip': {
2129
2129
  'position': 'absolute', 'z-index': '999',
2130
- 'font-size': '0.875rem', 'white-space': 'nowrap', 'pointer-events': 'none',
2130
+ 'font-size': '0.875rem', 'white-space': 'nowrap', 'max-width': 'min(300px, calc(100vw - 1rem))', 'pointer-events': 'none',
2131
2131
  'opacity': '0', 'visibility': 'hidden'
2132
2132
  },
2133
2133
  '.bw_tooltip.bw_tooltip_show': { 'opacity': '1', 'visibility': 'visible' },
@@ -2147,7 +2147,7 @@ var structuralRules = {
2147
2147
  '.bw_popover_trigger': { 'cursor': 'pointer' },
2148
2148
  '.bw_popover': {
2149
2149
  'position': 'absolute', 'z-index': '1000',
2150
- 'min-width': '200px', 'max-width': '320px',
2150
+ 'min-width': '200px', 'max-width': 'min(320px, calc(100vw - 2rem))',
2151
2151
  'pointer-events': 'none', 'opacity': '0', 'visibility': 'hidden'
2152
2152
  },
2153
2153
  '.bw_popover.bw_popover_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
@@ -2330,7 +2330,18 @@ var structuralRules = {
2330
2330
  '.bw_hero, .bw_hero': { 'padding': '2rem 1rem' },
2331
2331
  '.bw_cta_actions, .bw_cta-actions': { 'flex-direction': 'column' },
2332
2332
  '.bw_hstack, .bw_hstack': { 'flex-direction': 'column' },
2333
- '.bw_feature_grid, .bw_feature-grid': { 'grid-template-columns': '1fr' }
2333
+ '.bw_feature_grid, .bw_feature-grid': { 'grid-template-columns': '1fr' },
2334
+ '.bw_modal_dialog': { 'margin': '0.5rem auto' },
2335
+ '.bw_modal_lg': { 'max-width': 'calc(100% - 1rem)' },
2336
+ '.bw_modal_xl': { 'max-width': 'calc(100% - 1rem)' },
2337
+ '.bw_navbar': { 'padding': '0.5rem 0.75rem' },
2338
+ '.bw_navbar_brand': { 'margin-right': '0.5rem', 'font-size': '1rem' },
2339
+ '.bw_navbar_nav': { 'flex-wrap': 'wrap' },
2340
+ '.bw_tooltip': { 'white-space': 'normal' },
2341
+ '.bw_table': { 'display': 'block', 'overflow-x': 'auto', '-webkit-overflow-scrolling': 'touch' },
2342
+ '.bw_col, .bw_col_1, .bw_col_2, .bw_col_3, .bw_col_4, .bw_col_5, .bw_col_6, .bw_col_7, .bw_col_8, .bw_col_9, .bw_col_10, .bw_col_11, .bw_col_12': { 'flex': '0 0 100%', 'max-width': '100%' },
2343
+ '.bw_container': { 'padding-right': '0.5rem', 'padding-left': '0.5rem' },
2344
+ '.bw_container_fluid': { 'padding-right': '0.5rem', 'padding-left': '0.5rem' }
2334
2345
  }
2335
2346
  }
2336
2347
  };
@@ -6863,7 +6874,11 @@ var BCCL = {
6863
6874
  function make(type, props) {
6864
6875
  var def = BCCL[type];
6865
6876
  if (!def) throw new Error('bw.make: unknown component type "' + type + '". Available: ' + Object.keys(BCCL).join(', '));
6866
- return def.make(props || {});
6877
+ var taco = def.make(props || {});
6878
+ if (taco && typeof taco === 'object') {
6879
+ taco._bwFactory = { type: type, props: props || {} };
6880
+ }
6881
+ return taco;
6867
6882
  }
6868
6883
 
6869
6884
  var components = /*#__PURE__*/Object.freeze({
@@ -6984,7 +6999,7 @@ const bw = {
6984
6999
  __monkey_patch_is_nodejs__: {
6985
7000
  _value: 'ignore',
6986
7001
  set: function(x) {
6987
- this._value = (typeof x === 'boolean') ? x : 'ignore';
7002
+ this._value = _is(x, 'boolean') ? x : 'ignore';
6988
7003
  },
6989
7004
  get: function() {
6990
7005
  return this._value;
@@ -7032,6 +7047,67 @@ Object.defineProperty(bw, '_isBrowser', {
7032
7047
  configurable: true
7033
7048
  });
7034
7049
 
7050
+ // ── Internal aliases ─────────────────────────────────────────────────────
7051
+ // Short names for frequently-used builtins and internal methods.
7052
+ // Same pattern as v1 (_to = bw.typeOf, etc.).
7053
+ //
7054
+ // Why: Terser can't shorten global property chains (console.warn,
7055
+ // Object.prototype.hasOwnProperty, Array.isArray, document.createElement)
7056
+ // because it can't prove they're side-effect-free. We can, so we alias
7057
+ // them here. Each alias saves bytes in the minified output, and the short
7058
+ // names also reduce visual noise in the hot paths (binding pipeline,
7059
+ // createDOM, etc.).
7060
+ //
7061
+ // Alias Target Sites
7062
+ // ───────── ────────────────────────────────────── ─────
7063
+ // _hop Object.prototype.hasOwnProperty 15
7064
+ // _isA Array.isArray 25
7065
+ // _keys Object.keys 7
7066
+ // _to bw.typeOf (type string) 26
7067
+ // _is type check boolean: _is(x,'string') ~50
7068
+ // _cw console.warn 8
7069
+ // _cl console.log 11
7070
+ // _ce console.error 4
7071
+ // _chp ComponentHandle.prototype 28 (defined after constructor)
7072
+ //
7073
+ // Note: document.createElement etc. are NOT aliased because they require
7074
+ // `this === document` and .bind() would add overhead on every call.
7075
+ // Console aliases use thin wrappers (not direct refs) so test monkey-
7076
+ // patching of console.warn/log/error continues to work.
7077
+ //
7078
+ // `typeof x` for UNDECLARED globals (window, document, process, require,
7079
+ // EventSource, navigator, Promise, __filename, import.meta) MUST stay as
7080
+ // raw `typeof` — calling _to(x) when x doesn't exist throws ReferenceError.
7081
+ //
7082
+ // ── v1 functional type helpers (kept for reference, not currently used) ──
7083
+ // _toa(x, type, trueVal, falseVal) — bw.typeAssign:
7084
+ // returns trueVal if _to(x)===type, else falseVal.
7085
+ // Replaces: (typeof x === 'string') ? A : B → _toa(x,'string',A,B)
7086
+ // _toc(x, type, trueVal, falseVal) — bw.typeConvert:
7087
+ // same as _toa but if trueVal/falseVal are functions, calls them with x.
7088
+ // Replaces: typeof x === 'string' ? fn(x) : default → _toc(x,'string',fn,default)
7089
+ // Uncomment if pattern frequency justifies them:
7090
+ // var _toa = function(x, t, y, n) { return _to(x) === t ? y : n; };
7091
+ // 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); };
7092
+ // ─────────────────────────────────────────────────────────────────────────
7093
+ var _hop = Object.prototype.hasOwnProperty;
7094
+ var _isA = Array.isArray;
7095
+ var _keys = Object.keys;
7096
+ var _to = typeOf; // imported from bitwrench-utils.js
7097
+ var _is = function(x, t) { var r = _to(x); return r === t || r.toLowerCase() === t; };
7098
+ // Console aliases use thin wrappers (not direct references) so that test
7099
+ // code can monkey-patch console.warn/log/error and the patches take effect.
7100
+ var _cw = function() { console.warn.apply(console, arguments); };
7101
+ var _cl = function() { console.log.apply(console, arguments); };
7102
+ var _ce = function() { console.error.apply(console, arguments); };
7103
+
7104
+ /**
7105
+ * Debug flag. When true, emits console.warn for silent binding failures
7106
+ * (missing paths, null refs, auto-created intermediate objects).
7107
+ * @type {boolean}
7108
+ */
7109
+ bw.debug = false;
7110
+
7035
7111
  /**
7036
7112
  * Lazy-resolve Node.js `fs` module.
7037
7113
  * Tries require('fs') first (available in CJS/UMD Node.js builds),
@@ -7179,7 +7255,7 @@ bw.uuid = function(prefix) {
7179
7255
  */
7180
7256
  bw._el = function(id) {
7181
7257
  // Pass-through for DOM elements
7182
- if (typeof id !== 'string') return id || null;
7258
+ if (!_is(id, 'string')) return id || null;
7183
7259
  if (!id) return null;
7184
7260
  if (!bw._isBrowser) return null;
7185
7261
 
@@ -7275,7 +7351,7 @@ bw._deregisterNode = function(el, bwId) {
7275
7351
  * // => '&lt;b&gt;Hello&lt;&#x2F;b&gt; &amp; &quot;world&quot;'
7276
7352
  */
7277
7353
  bw.escapeHTML = function(str) {
7278
- if (typeof str !== 'string') return '';
7354
+ if (!_is(str, 'string')) return '';
7279
7355
 
7280
7356
  const escapeMap = {
7281
7357
  '&': '&amp;',
@@ -7348,7 +7424,7 @@ bw.html = function(taco, options = {}) {
7348
7424
  }
7349
7425
 
7350
7426
  // Handle arrays of TACOs
7351
- if (Array.isArray(taco)) {
7427
+ if (_isA(taco)) {
7352
7428
  return taco.map(t => bw.html(t, options)).join('');
7353
7429
  }
7354
7430
 
@@ -7371,15 +7447,15 @@ bw.html = function(taco, options = {}) {
7371
7447
  if (taco && taco._bwEach && options.state) {
7372
7448
  var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
7373
7449
  var arr = bw._evaluatePath(options.state, eachExpr);
7374
- if (!Array.isArray(arr)) return '';
7450
+ if (!_isA(arr)) return '';
7375
7451
  return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
7376
7452
  }
7377
7453
 
7378
7454
  // Handle primitives and non-TACO objects
7379
- if (typeof taco !== 'object' || !taco.t) {
7455
+ if (!_is(taco, 'object') || !taco.t) {
7380
7456
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
7381
7457
  // Resolve template bindings if state provided
7382
- if (options.state && typeof str === 'string' && str.indexOf('${') >= 0) {
7458
+ if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
7383
7459
  str = bw._resolveTemplate(str, options.state, !!options.compile);
7384
7460
  }
7385
7461
  return str;
@@ -7399,10 +7475,18 @@ bw.html = function(taco, options = {}) {
7399
7475
  // Skip null, undefined, false
7400
7476
  if (value == null || value === false) continue;
7401
7477
 
7402
- // Skip event handlers (they're for DOM only)
7403
- if (key.startsWith('on')) continue;
7478
+ // Serialize event handlers via funcRegister
7479
+ if (key.startsWith('on')) {
7480
+ if (_is(value, 'function')) {
7481
+ var fnId = bw.funcRegister(value);
7482
+ attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
7483
+ } else if (_is(value, 'string')) {
7484
+ attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
7485
+ }
7486
+ continue;
7487
+ }
7404
7488
 
7405
- if (key === 'style' && typeof value === 'object') {
7489
+ if (key === 'style' && _is(value, 'object')) {
7406
7490
  // Convert style object to string
7407
7491
  const styleStr = Object.entries(value)
7408
7492
  .filter(([, v]) => v != null)
@@ -7413,7 +7497,7 @@ bw.html = function(taco, options = {}) {
7413
7497
  }
7414
7498
  } else if (key === 'class') {
7415
7499
  // Handle class as array or string
7416
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
7500
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
7417
7501
  if (classStr) {
7418
7502
  attrStr += ` class="${bw.escapeHTML(classStr)}"`;
7419
7503
  }
@@ -7449,13 +7533,184 @@ bw.html = function(taco, options = {}) {
7449
7533
  // Process content recursively
7450
7534
  let contentStr = content != null ? bw.html(content, options) : '';
7451
7535
  // Resolve template bindings in content if state provided
7452
- if (options.state && typeof contentStr === 'string' && contentStr.indexOf('${') >= 0) {
7536
+ if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
7453
7537
  contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
7454
7538
  }
7455
7539
 
7456
7540
  return `<${tag}${attrStr}>${contentStr}</${tag}>`;
7457
7541
  };
7458
7542
 
7543
+ /**
7544
+ * Generate a complete, self-contained HTML document from TACO content.
7545
+ *
7546
+ * Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
7547
+ * func registry emission (so serialized event handlers work), optional theme,
7548
+ * and extra head elements. Designed for static site generation, offline/airgapped
7549
+ * use, and the "static site that isn't static" workflow.
7550
+ *
7551
+ * @param {Object} [opts={}] - Page options
7552
+ * @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
7553
+ * @param {string} [opts.title='bitwrench'] - Page title
7554
+ * @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
7555
+ * @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
7556
+ * @param {string} [opts.css=''] - Additional CSS for <style> block
7557
+ * @param {string|Object} [opts.theme=null] - Theme preset name or config object
7558
+ * @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
7559
+ * @param {string} [opts.favicon=''] - Favicon URL
7560
+ * @param {string} [opts.lang='en'] - HTML lang attribute
7561
+ * @returns {string} Complete HTML document string
7562
+ * @category DOM Generation
7563
+ * @see bw.html
7564
+ * @example
7565
+ * bw.htmlPage({
7566
+ * title: 'My App',
7567
+ * body: { t: 'h1', c: 'Hello World' },
7568
+ * runtime: 'shim'
7569
+ * })
7570
+ */
7571
+ bw.htmlPage = function(opts) {
7572
+ opts = opts || {};
7573
+ var title = opts.title || 'bitwrench';
7574
+ var body = opts.body || '';
7575
+ var state = opts.state || undefined;
7576
+ var runtime = opts.runtime || 'shim';
7577
+ var css = opts.css || '';
7578
+ var theme = opts.theme || null;
7579
+ var headExtra = opts.head || [];
7580
+ var favicon = opts.favicon || '';
7581
+ var lang = opts.lang || 'en';
7582
+
7583
+ // Snapshot funcRegistry counter before rendering
7584
+ var fnCounterBefore = bw._fnIDCounter;
7585
+
7586
+ // Render body content
7587
+ var bodyHTML = '';
7588
+ if (_is(body, 'string')) {
7589
+ bodyHTML = body;
7590
+ } else {
7591
+ var htmlOpts = {};
7592
+ if (state) htmlOpts.state = state;
7593
+ bodyHTML = bw.html(body, htmlOpts);
7594
+ }
7595
+
7596
+ // Collect functions registered during this render
7597
+ var fnCounterAfter = bw._fnIDCounter;
7598
+ var registryEntries = '';
7599
+ for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
7600
+ var fnKey = 'bw_fn_' + i;
7601
+ if (bw._fnRegistry[fnKey]) {
7602
+ registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' +
7603
+ bw._fnRegistry[fnKey].toString() + ';\n';
7604
+ }
7605
+ }
7606
+
7607
+ // Build runtime script for <head>
7608
+ var runtimeHead = '';
7609
+ if (runtime === 'inline') {
7610
+ // Read UMD bundle synchronously if in Node.js
7611
+ var umdSource = null;
7612
+ if (bw._isNode) {
7613
+ try {
7614
+ var fs = (typeof require === 'function') ? require('fs') : null;
7615
+ var pathMod = (typeof require === 'function') ? require('path') : null;
7616
+ if (fs && pathMod) {
7617
+ // Resolve dist/ relative to this source file
7618
+ var srcDir = '';
7619
+ try { srcDir = pathMod.dirname((typeof __filename !== 'undefined') ? __filename : ''); }
7620
+ catch(e2) { /* ESM: __filename not available */ }
7621
+ if (!srcDir && typeof import.meta !== 'undefined' && import.meta.url) {
7622
+ var url = (typeof require === 'function') ? require('url') : null;
7623
+ if (url && url.fileURLToPath) srcDir = pathMod.dirname(url.fileURLToPath(import.meta.url));
7624
+ }
7625
+ if (srcDir) {
7626
+ var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
7627
+ umdSource = fs.readFileSync(distPath, 'utf8');
7628
+ }
7629
+ }
7630
+ } catch(e) { /* fall through */ }
7631
+ }
7632
+ if (umdSource) {
7633
+ runtimeHead = '<script>' + umdSource + '</script>';
7634
+ } else {
7635
+ // Fallback to shim in browser or if dist not available
7636
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
7637
+ }
7638
+ } else if (runtime === 'cdn') {
7639
+ runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
7640
+ } else if (runtime === 'shim') {
7641
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
7642
+ }
7643
+ // runtime === 'none' → empty
7644
+
7645
+ // Theme CSS
7646
+ var themeCSS = '';
7647
+ if (theme) {
7648
+ var themeConfig = _is(theme, 'string')
7649
+ ? (THEME_PRESETS[theme.toLowerCase()] || null)
7650
+ : theme;
7651
+ if (themeConfig) {
7652
+ var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, { inject: false }));
7653
+ themeCSS = themeResult.css;
7654
+ }
7655
+ }
7656
+
7657
+ // Extra <head> elements
7658
+ var headHTML = '';
7659
+ if (_isA(headExtra) && headExtra.length > 0) {
7660
+ headHTML = headExtra.map(function(el) { return bw.html(el); }).join('\n');
7661
+ }
7662
+
7663
+ // Favicon
7664
+ var faviconTag = '';
7665
+ if (favicon) {
7666
+ var safeFavicon = favicon.replace(/[&<>"']/g, function(c) {
7667
+ return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
7668
+ });
7669
+ faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
7670
+ }
7671
+
7672
+ // Escaped title
7673
+ var safeTitle = bw.escapeHTML(title);
7674
+
7675
+ // Combine all CSS
7676
+ var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
7677
+
7678
+ // Body-end script: registry entries + optional loadDefaultStyles
7679
+ var bodyEndScript = '';
7680
+ var bodyEndParts = [];
7681
+ if (registryEntries) {
7682
+ bodyEndParts.push(registryEntries);
7683
+ }
7684
+ if (runtime === 'inline' || runtime === 'cdn') {
7685
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
7686
+ }
7687
+ if (bodyEndParts.length > 0) {
7688
+ bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
7689
+ }
7690
+
7691
+ // Assemble document
7692
+ var parts = [
7693
+ '<!DOCTYPE html>',
7694
+ '<html lang="' + lang + '">',
7695
+ '<head>',
7696
+ '<meta charset="UTF-8">',
7697
+ '<meta name="viewport" content="width=device-width, initial-scale=1">'
7698
+ ];
7699
+ parts.push('<title>' + safeTitle + '</title>');
7700
+ if (faviconTag) parts.push(faviconTag);
7701
+ if (runtimeHead) parts.push(runtimeHead);
7702
+ if (headHTML) parts.push(headHTML);
7703
+ if (allCSS) parts.push('<style>' + allCSS + '</style>');
7704
+ parts.push('</head>');
7705
+ parts.push('<body>');
7706
+ parts.push(bodyHTML);
7707
+ if (bodyEndScript) parts.push(bodyEndScript);
7708
+ parts.push('</body>');
7709
+ parts.push('</html>');
7710
+
7711
+ return parts.join('\n');
7712
+ };
7713
+
7459
7714
  /**
7460
7715
  * Create a live DOM element from a TACO object (browser only).
7461
7716
  *
@@ -7500,7 +7755,7 @@ bw.createDOM = function(taco, options = {}) {
7500
7755
  }
7501
7756
 
7502
7757
  // Handle text nodes
7503
- if (typeof taco !== 'object' || !taco.t) {
7758
+ if (!_is(taco, 'object') || !taco.t) {
7504
7759
  return document.createTextNode(String(taco));
7505
7760
  }
7506
7761
 
@@ -7513,16 +7768,16 @@ bw.createDOM = function(taco, options = {}) {
7513
7768
  for (const [key, value] of Object.entries(attrs)) {
7514
7769
  if (value == null || value === false) continue;
7515
7770
 
7516
- if (key === 'style' && typeof value === 'object') {
7771
+ if (key === 'style' && _is(value, 'object')) {
7517
7772
  // Apply styles directly
7518
7773
  Object.assign(el.style, value);
7519
7774
  } else if (key === 'class') {
7520
7775
  // Handle class as array or string
7521
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
7776
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
7522
7777
  if (classStr) {
7523
7778
  el.className = classStr;
7524
7779
  }
7525
- } else if (key.startsWith('on') && typeof value === 'function') {
7780
+ } else if (key.startsWith('on') && _is(value, 'function')) {
7526
7781
  // Event handlers
7527
7782
  const eventName = key.slice(2).toLowerCase();
7528
7783
  el.addEventListener(eventName, value);
@@ -7542,7 +7797,7 @@ bw.createDOM = function(taco, options = {}) {
7542
7797
  // Children with data-bw_id or id attributes get local refs on the parent,
7543
7798
  // so o.render functions can access them without any DOM lookup.
7544
7799
  if (content != null) {
7545
- if (Array.isArray(content)) {
7800
+ if (_isA(content)) {
7546
7801
  content.forEach(child => {
7547
7802
  if (child != null) {
7548
7803
  // Handle ComponentHandle in content arrays (Level 2 children)
@@ -7562,20 +7817,20 @@ bw.createDOM = function(taco, options = {}) {
7562
7817
  if (childEl._bw_refs) {
7563
7818
  if (!el._bw_refs) el._bw_refs = {};
7564
7819
  for (var rk in childEl._bw_refs) {
7565
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
7820
+ if (_hop.call(childEl._bw_refs, rk)) {
7566
7821
  el._bw_refs[rk] = childEl._bw_refs[rk];
7567
7822
  }
7568
7823
  }
7569
7824
  }
7570
7825
  }
7571
7826
  });
7572
- } else if (typeof content === 'object' && content.__bw_raw) {
7827
+ } else if (_is(content, 'object') && content.__bw_raw) {
7573
7828
  // Raw HTML content — inject via innerHTML
7574
7829
  el.innerHTML = content.v;
7575
7830
  } else if (content._bwComponent === true) {
7576
7831
  // Single ComponentHandle as content
7577
7832
  content.mount(el);
7578
- } else if (typeof content === 'object' && content.t) {
7833
+ } else if (_is(content, 'object') && content.t) {
7579
7834
  var childEl = bw.createDOM(content, options);
7580
7835
  el.appendChild(childEl);
7581
7836
  var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
@@ -7586,7 +7841,7 @@ bw.createDOM = function(taco, options = {}) {
7586
7841
  if (childEl._bw_refs) {
7587
7842
  if (!el._bw_refs) el._bw_refs = {};
7588
7843
  for (var rk in childEl._bw_refs) {
7589
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
7844
+ if (_hop.call(childEl._bw_refs, rk)) {
7590
7845
  el._bw_refs[rk] = childEl._bw_refs[rk];
7591
7846
  }
7592
7847
  }
@@ -7619,7 +7874,7 @@ bw.createDOM = function(taco, options = {}) {
7619
7874
  el._bw_render = opts.render;
7620
7875
 
7621
7876
  if (opts.mounted) {
7622
- console.warn('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
7877
+ _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
7623
7878
  }
7624
7879
 
7625
7880
  // Queue initial render (same timing as mounted)
@@ -7692,7 +7947,7 @@ bw.DOM = function(target, taco, options = {}) {
7692
7947
  const targetEl = bw._el(target);
7693
7948
 
7694
7949
  if (!targetEl) {
7695
- console.error('bw.DOM: Target element not found:', target);
7950
+ _ce('bw.DOM: Target element not found:', target);
7696
7951
  return null;
7697
7952
  }
7698
7953
 
@@ -7732,7 +7987,7 @@ bw.DOM = function(target, taco, options = {}) {
7732
7987
  targetEl.appendChild(taco.element);
7733
7988
  }
7734
7989
  // Handle arrays
7735
- else if (Array.isArray(taco)) {
7990
+ else if (_isA(taco)) {
7736
7991
  taco.forEach(t => {
7737
7992
  if (t != null) {
7738
7993
  if (t._bwComponent === true) {
@@ -7768,7 +8023,7 @@ bw.DOM = function(target, taco, options = {}) {
7768
8023
  bw.compileProps = function(handle, props = {}) {
7769
8024
  const compiledProps = {};
7770
8025
 
7771
- Object.keys(props).forEach(key => {
8026
+ _keys(props).forEach(key => {
7772
8027
  // Create getter/setter for each prop
7773
8028
  Object.defineProperty(compiledProps, key, {
7774
8029
  get() {
@@ -8086,17 +8341,17 @@ bw.patch = function(id, content, attr) {
8086
8341
  if (attr) {
8087
8342
  // Patch an attribute
8088
8343
  el.setAttribute(attr, String(content));
8089
- } else if (Array.isArray(content)) {
8344
+ } else if (_isA(content)) {
8090
8345
  // Patch with array of children (strings and/or TACOs)
8091
8346
  el.innerHTML = '';
8092
8347
  content.forEach(function(item) {
8093
- if (typeof item === 'string' || typeof item === 'number') {
8348
+ if (_is(item, 'string') || _is(item, 'number')) {
8094
8349
  el.appendChild(document.createTextNode(String(item)));
8095
8350
  } else if (item && item.t) {
8096
8351
  el.appendChild(bw.createDOM(item));
8097
8352
  }
8098
8353
  });
8099
- } else if (typeof content === 'object' && content !== null && content.t) {
8354
+ } else if (_is(content, 'object') && content.t) {
8100
8355
  // Patch with a TACO — replace children
8101
8356
  el.innerHTML = '';
8102
8357
  el.appendChild(bw.createDOM(content));
@@ -8127,7 +8382,7 @@ bw.patch = function(id, content, attr) {
8127
8382
  bw.patchAll = function(patches) {
8128
8383
  var results = {};
8129
8384
  for (var id in patches) {
8130
- if (Object.prototype.hasOwnProperty.call(patches, id)) {
8385
+ if (_hop.call(patches, id)) {
8131
8386
  results[id] = bw.patch(id, patches[id]);
8132
8387
  }
8133
8388
  }
@@ -8224,7 +8479,7 @@ bw.pub = function(topic, detail) {
8224
8479
  snapshot[i].handler(detail);
8225
8480
  called++;
8226
8481
  } catch (err) {
8227
- console.warn('bw.pub: subscriber error on topic "' + topic + '":', err);
8482
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
8228
8483
  }
8229
8484
  }
8230
8485
  return called;
@@ -8320,8 +8575,8 @@ bw._fnIDCounter = 0;
8320
8575
  * @see bw.funcGetDispatchStr
8321
8576
  */
8322
8577
  bw.funcRegister = function(fn, name) {
8323
- if (typeof fn !== 'function') return '';
8324
- var fnID = (typeof name === 'string' && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
8578
+ if (!_is(fn, 'function')) return '';
8579
+ var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
8325
8580
  bw._fnRegistry[fnID] = fn;
8326
8581
  return fnID;
8327
8582
  };
@@ -8340,7 +8595,7 @@ bw.funcRegister = function(fn, name) {
8340
8595
  bw.funcGetById = function(name, errFn) {
8341
8596
  name = String(name);
8342
8597
  if (name in bw._fnRegistry) return bw._fnRegistry[name];
8343
- return (typeof errFn === 'function') ? errFn : function() { console.warn('bw.funcGetById: unregistered fn "' + name + '"'); };
8598
+ return _is(errFn, 'function') ? errFn : function() { _cw('bw.funcGetById: unregistered fn "' + name + '"'); };
8344
8599
  };
8345
8600
 
8346
8601
  /**
@@ -8381,13 +8636,30 @@ bw.funcUnregister = function(name) {
8381
8636
  bw.funcGetRegistry = function() {
8382
8637
  var copy = {};
8383
8638
  for (var k in bw._fnRegistry) {
8384
- if (Object.prototype.hasOwnProperty.call(bw._fnRegistry, k)) {
8639
+ if (_hop.call(bw._fnRegistry, k)) {
8385
8640
  copy[k] = bw._fnRegistry[k];
8386
8641
  }
8387
8642
  }
8388
8643
  return copy;
8389
8644
  };
8390
8645
 
8646
+ /**
8647
+ * Minimal runtime shim for funcRegister dispatch in static HTML.
8648
+ * When embedded in a `<script>` tag, provides just enough infrastructure
8649
+ * for `bw.funcGetById()` calls to resolve. The actual function bodies
8650
+ * are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
8651
+ * @type {string}
8652
+ * @category Function Registry
8653
+ */
8654
+ bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' +
8655
+ 'if(!bw._fnRegistry)bw._fnRegistry={};' +
8656
+ 'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' +
8657
+ 'console.warn("bw: unregistered fn "+n)};};' +
8658
+ 'bw.funcRegister=function(fn,name){' +
8659
+ 'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' +
8660
+ 'bw._fnRegistry[id]=fn;return id;};' +
8661
+ 'window.bw=bw;})();';
8662
+
8391
8663
  // ===================================================================================
8392
8664
  // Template Binding Utilities
8393
8665
  // ===================================================================================
@@ -8415,7 +8687,10 @@ bw._evaluatePath = function(state, path) {
8415
8687
  var parts = path.split('.');
8416
8688
  var val = state;
8417
8689
  for (var i = 0; i < parts.length; i++) {
8418
- if (val == null) return '';
8690
+ if (val == null) {
8691
+ if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
8692
+ return '';
8693
+ }
8419
8694
  val = val[parts[i]];
8420
8695
  }
8421
8696
  return (val == null) ? '' : val;
@@ -8435,7 +8710,7 @@ bw._evaluatePath = function(state, path) {
8435
8710
  */
8436
8711
  bw._compiledExprs = {};
8437
8712
  bw._resolveTemplate = function(str, state, compile) {
8438
- if (typeof str !== 'string' || str.indexOf('${') < 0) return str;
8713
+ if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
8439
8714
  var bindings = bw._parseBindings(str);
8440
8715
  if (bindings.length === 0) return str;
8441
8716
 
@@ -8457,6 +8732,7 @@ bw._resolveTemplate = function(str, state, compile) {
8457
8732
  try {
8458
8733
  val = bw._compiledExprs[b.expr](state);
8459
8734
  } catch (e) {
8735
+ if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
8460
8736
  val = '';
8461
8737
  }
8462
8738
  } else {
@@ -8565,7 +8841,7 @@ function ComponentHandle(taco) {
8565
8841
  this._state = {};
8566
8842
  if (o.state) {
8567
8843
  for (var k in o.state) {
8568
- if (Object.prototype.hasOwnProperty.call(o.state, k)) {
8844
+ if (_hop.call(o.state, k)) {
8569
8845
  this._state[k] = o.state[k];
8570
8846
  }
8571
8847
  }
@@ -8574,7 +8850,7 @@ function ComponentHandle(taco) {
8574
8850
  this._actions = {};
8575
8851
  if (o.actions) {
8576
8852
  for (var k2 in o.actions) {
8577
- if (Object.prototype.hasOwnProperty.call(o.actions, k2)) {
8853
+ if (_hop.call(o.actions, k2)) {
8578
8854
  this._actions[k2] = o.actions[k2];
8579
8855
  }
8580
8856
  }
@@ -8584,7 +8860,7 @@ function ComponentHandle(taco) {
8584
8860
  if (o.methods) {
8585
8861
  var self = this;
8586
8862
  for (var k3 in o.methods) {
8587
- if (Object.prototype.hasOwnProperty.call(o.methods, k3)) {
8863
+ if (_hop.call(o.methods, k3)) {
8588
8864
  this._methods[k3] = o.methods[k3];
8589
8865
  (function(methodName, methodFn) {
8590
8866
  self[methodName] = function() {
@@ -8617,14 +8893,23 @@ function ComponentHandle(taco) {
8617
8893
  this._compile = !!o.compile;
8618
8894
  this._bw_refs = {};
8619
8895
  this._refCounter = 0;
8896
+ // Child component ownership (Bug #5)
8897
+ this._children = [];
8898
+ this._parent = null;
8899
+ // Factory metadata for BCCL rebuild (Bug #6)
8900
+ this._factory = taco._bwFactory || null;
8620
8901
  }
8621
8902
 
8903
+ // Short alias for ComponentHandle.prototype (see alias block at top of file).
8904
+ // 28 method definitions × 25 chars = ~700B raw savings in minified output.
8905
+ var _chp = ComponentHandle.prototype;
8906
+
8622
8907
  // ── State Methods ──
8623
8908
 
8624
8909
  /**
8625
8910
  * Get a state value. Dot-path supported: `get('user.name')`
8626
8911
  */
8627
- ComponentHandle.prototype.get = function(key) {
8912
+ _chp.get = function(key) {
8628
8913
  return bw._evaluatePath(this._state, key);
8629
8914
  };
8630
8915
 
@@ -8634,12 +8919,13 @@ ComponentHandle.prototype.get = function(key) {
8634
8919
  * @param {*} value - New value
8635
8920
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
8636
8921
  */
8637
- ComponentHandle.prototype.set = function(key, value, opts) {
8922
+ _chp.set = function(key, value, opts) {
8638
8923
  // Dot-path set
8639
8924
  var parts = key.split('.');
8640
8925
  var obj = this._state;
8641
8926
  for (var i = 0; i < parts.length - 1; i++) {
8642
- if (obj[parts[i]] == null || typeof obj[parts[i]] !== 'object') {
8927
+ if (!_is(obj[parts[i]], 'object')) {
8928
+ if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
8643
8929
  obj[parts[i]] = {};
8644
8930
  }
8645
8931
  obj = obj[parts[i]];
@@ -8659,10 +8945,10 @@ ComponentHandle.prototype.set = function(key, value, opts) {
8659
8945
  /**
8660
8946
  * Get a shallow clone of the full state.
8661
8947
  */
8662
- ComponentHandle.prototype.getState = function() {
8948
+ _chp.getState = function() {
8663
8949
  var clone = {};
8664
8950
  for (var k in this._state) {
8665
- if (Object.prototype.hasOwnProperty.call(this._state, k)) {
8951
+ if (_hop.call(this._state, k)) {
8666
8952
  clone[k] = this._state[k];
8667
8953
  }
8668
8954
  }
@@ -8674,9 +8960,9 @@ ComponentHandle.prototype.getState = function() {
8674
8960
  * @param {Object} updates - Key-value pairs to merge
8675
8961
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
8676
8962
  */
8677
- ComponentHandle.prototype.setState = function(updates, opts) {
8963
+ _chp.setState = function(updates, opts) {
8678
8964
  for (var k in updates) {
8679
- if (Object.prototype.hasOwnProperty.call(updates, k)) {
8965
+ if (_hop.call(updates, k)) {
8680
8966
  this._state[k] = updates[k];
8681
8967
  this._dirtyKeys[k] = true;
8682
8968
  }
@@ -8693,9 +8979,9 @@ ComponentHandle.prototype.setState = function(updates, opts) {
8693
8979
  /**
8694
8980
  * Push a value onto an array in state. Clones the array.
8695
8981
  */
8696
- ComponentHandle.prototype.push = function(key, val) {
8982
+ _chp.push = function(key, val) {
8697
8983
  var arr = this.get(key);
8698
- var newArr = Array.isArray(arr) ? arr.slice() : [];
8984
+ var newArr = _isA(arr) ? arr.slice() : [];
8699
8985
  newArr.push(val);
8700
8986
  this.set(key, newArr);
8701
8987
  };
@@ -8703,9 +8989,9 @@ ComponentHandle.prototype.push = function(key, val) {
8703
8989
  /**
8704
8990
  * Splice an array in state. Clones the array.
8705
8991
  */
8706
- ComponentHandle.prototype.splice = function(key, start, deleteCount) {
8992
+ _chp.splice = function(key, start, deleteCount) {
8707
8993
  var arr = this.get(key);
8708
- var newArr = Array.isArray(arr) ? arr.slice() : [];
8994
+ var newArr = _isA(arr) ? arr.slice() : [];
8709
8995
  var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
8710
8996
  Array.prototype.splice.apply(newArr, args);
8711
8997
  this.set(key, newArr);
@@ -8713,7 +8999,7 @@ ComponentHandle.prototype.splice = function(key, start, deleteCount) {
8713
8999
 
8714
9000
  // ── Scheduling ──
8715
9001
 
8716
- ComponentHandle.prototype._scheduleDirty = function() {
9002
+ _chp._scheduleDirty = function() {
8717
9003
  if (!this._scheduled) {
8718
9004
  this._scheduled = true;
8719
9005
  bw._dirtyComponents.push(this);
@@ -8728,17 +9014,17 @@ ComponentHandle.prototype._scheduleDirty = function() {
8728
9014
  * Creates binding descriptors with refIds for targeted DOM updates.
8729
9015
  * @private
8730
9016
  */
8731
- ComponentHandle.prototype._compileBindings = function() {
9017
+ _chp._compileBindings = function() {
8732
9018
  this._bindings = [];
8733
9019
  this._refCounter = 0;
8734
- var stateKeys = Object.keys(this._state);
9020
+ var stateKeys = _keys(this._state);
8735
9021
  var self = this;
8736
9022
 
8737
9023
  function walkTaco(taco, path) {
8738
- if (taco == null || typeof taco !== 'object' || !taco.t) return taco;
9024
+ if (!_is(taco, 'object') || !taco.t) return taco;
8739
9025
 
8740
9026
  // Check content for bindings
8741
- if (typeof taco.c === 'string' && taco.c.indexOf('${') >= 0) {
9027
+ if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
8742
9028
  var refId = 'bw_ref_' + self._refCounter++;
8743
9029
  var parsed = bw._parseBindings(taco.c);
8744
9030
  var deps = [];
@@ -8760,10 +9046,10 @@ ComponentHandle.prototype._compileBindings = function() {
8760
9046
  // Check attributes for bindings
8761
9047
  if (taco.a) {
8762
9048
  for (var attrName in taco.a) {
8763
- if (!Object.prototype.hasOwnProperty.call(taco.a, attrName)) continue;
9049
+ if (!_hop.call(taco.a, attrName)) continue;
8764
9050
  if (attrName === 'data-bw_ref') continue;
8765
9051
  var attrVal = taco.a[attrName];
8766
- if (typeof attrVal === 'string' && attrVal.indexOf('${') >= 0) {
9052
+ if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
8767
9053
  var refId2 = 'bw_ref_' + self._refCounter++;
8768
9054
  var parsed2 = bw._parseBindings(attrVal);
8769
9055
  var deps2 = [];
@@ -8789,9 +9075,27 @@ ComponentHandle.prototype._compileBindings = function() {
8789
9075
  }
8790
9076
 
8791
9077
  // Recurse into children
8792
- if (Array.isArray(taco.c)) {
9078
+ if (_isA(taco.c)) {
8793
9079
  for (var i = 0; i < taco.c.length; i++) {
8794
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
9080
+ // Wrap string children with ${expr} in a span so patches target the span, not the parent
9081
+ if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
9082
+ var mixedRefId = 'bw_ref_' + self._refCounter++;
9083
+ var mixedParsed = bw._parseBindings(taco.c[i]);
9084
+ var mixedDeps = [];
9085
+ for (var mi = 0; mi < mixedParsed.length; mi++) {
9086
+ mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
9087
+ }
9088
+ self._bindings.push({
9089
+ expr: taco.c[i],
9090
+ type: 'content',
9091
+ refId: mixedRefId,
9092
+ deps: mixedDeps,
9093
+ template: taco.c[i]
9094
+ });
9095
+ // Replace string with a span wrapper so textContent targets the span only
9096
+ taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
9097
+ }
9098
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
8795
9099
  walkTaco(taco.c[i], path.concat(i));
8796
9100
  }
8797
9101
  // Handle bw.when/bw.each markers
@@ -8826,7 +9130,7 @@ ComponentHandle.prototype._compileBindings = function() {
8826
9130
  taco.c[i]._refId = eachRefId;
8827
9131
  }
8828
9132
  }
8829
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9133
+ } else if (_is(taco.c, 'object') && taco.c.t) {
8830
9134
  walkTaco(taco.c, path.concat(0));
8831
9135
  }
8832
9136
 
@@ -8842,7 +9146,7 @@ ComponentHandle.prototype._compileBindings = function() {
8842
9146
  * Build ref map from the live DOM after createDOM.
8843
9147
  * @private
8844
9148
  */
8845
- ComponentHandle.prototype._collectRefs = function() {
9149
+ _chp._collectRefs = function() {
8846
9150
  this._bw_refs = {};
8847
9151
  if (!this.element) return;
8848
9152
  var els = this.element.querySelectorAll('[data-bw_ref]');
@@ -8863,7 +9167,7 @@ ComponentHandle.prototype._collectRefs = function() {
8863
9167
  * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
8864
9168
  * @param {Element} parentEl - DOM element to mount into
8865
9169
  */
8866
- ComponentHandle.prototype.mount = function(parentEl) {
9170
+ _chp.mount = function(parentEl) {
8867
9171
  // willMount hook
8868
9172
  if (this._hooks.willMount) this._hooks.willMount(this);
8869
9173
 
@@ -8885,7 +9189,7 @@ ComponentHandle.prototype.mount = function(parentEl) {
8885
9189
  // Register named actions in function registry
8886
9190
  var self = this;
8887
9191
  for (var actionName in this._actions) {
8888
- if (Object.prototype.hasOwnProperty.call(this._actions, actionName)) {
9192
+ if (_hop.call(this._actions, actionName)) {
8889
9193
  var registeredName = this._bwId + '_' + actionName;
8890
9194
  (function(aName) {
8891
9195
  bw.funcRegister(function(evt) {
@@ -8904,6 +9208,11 @@ ComponentHandle.prototype.mount = function(parentEl) {
8904
9208
  this.element = bw.createDOM(tacoForDOM);
8905
9209
  this.element._bwComponentHandle = this;
8906
9210
  this.element.setAttribute('data-bw_comp_id', this._bwId);
9211
+
9212
+ // Restore o.render from original TACO (stripped by _tacoForDOM)
9213
+ if (this.taco.o && this.taco.o.render) {
9214
+ this.element._bw_render = this.taco.o.render;
9215
+ }
8907
9216
  if (this._userTag) {
8908
9217
  this.element.classList.add(this._userTag);
8909
9218
  }
@@ -8919,6 +9228,16 @@ ComponentHandle.prototype.mount = function(parentEl) {
8919
9228
 
8920
9229
  this.mounted = true;
8921
9230
 
9231
+ // Scan for child ComponentHandles and link parent/child (Bug #5)
9232
+ var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
9233
+ for (var ci = 0; ci < childEls.length; ci++) {
9234
+ var ch = childEls[ci]._bwComponentHandle;
9235
+ if (ch && ch !== this && !ch._parent) {
9236
+ ch._parent = this;
9237
+ this._children.push(ch);
9238
+ }
9239
+ }
9240
+
8922
9241
  // mounted hook (backward compat: fn.length === 2 wraps (el, state))
8923
9242
  if (this._hooks.mounted) {
8924
9243
  if (this._hooks.mounted.length === 2) {
@@ -8927,16 +9246,21 @@ ComponentHandle.prototype.mount = function(parentEl) {
8927
9246
  this._hooks.mounted(this);
8928
9247
  }
8929
9248
  }
9249
+
9250
+ // Invoke o.render on initial mount (if present)
9251
+ if (this.element._bw_render) {
9252
+ this.element._bw_render(this.element, this._state);
9253
+ }
8930
9254
  };
8931
9255
 
8932
9256
  /**
8933
9257
  * Prepare TACO for initial render: resolve when/each markers.
8934
9258
  * @private
8935
9259
  */
8936
- ComponentHandle.prototype._prepareTaco = function(taco) {
8937
- if (!taco || typeof taco !== 'object') return;
9260
+ _chp._prepareTaco = function(taco) {
9261
+ if (!_is(taco, 'object')) return;
8938
9262
 
8939
- if (Array.isArray(taco.c)) {
9263
+ if (_isA(taco.c)) {
8940
9264
  for (var i = taco.c.length - 1; i >= 0; i--) {
8941
9265
  var child = taco.c[i];
8942
9266
  if (child && child._bwWhen) {
@@ -8961,18 +9285,18 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
8961
9285
  var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
8962
9286
  var arr = bw._evaluatePath(this._state, eachExprStr);
8963
9287
  var items = [];
8964
- if (Array.isArray(arr)) {
9288
+ if (_isA(arr)) {
8965
9289
  for (var j = 0; j < arr.length; j++) {
8966
9290
  items.push(child.factory(arr[j], j));
8967
9291
  }
8968
9292
  }
8969
9293
  taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
8970
9294
  }
8971
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
9295
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
8972
9296
  this._prepareTaco(taco.c[i]);
8973
9297
  }
8974
9298
  }
8975
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9299
+ } else if (_is(taco.c, 'object') && taco.c.t) {
8976
9300
  this._prepareTaco(taco.c);
8977
9301
  }
8978
9302
  };
@@ -8981,12 +9305,12 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
8981
9305
  * Wire action name strings (in onclick etc.) to dispatch function calls.
8982
9306
  * @private
8983
9307
  */
8984
- ComponentHandle.prototype._wireActions = function(taco) {
8985
- if (!taco || typeof taco !== 'object' || !taco.t) return;
9308
+ _chp._wireActions = function(taco) {
9309
+ if (!_is(taco, 'object') || !taco.t) return;
8986
9310
  if (taco.a) {
8987
9311
  for (var key in taco.a) {
8988
- if (!Object.prototype.hasOwnProperty.call(taco.a, key)) continue;
8989
- if (key.startsWith('on') && typeof taco.a[key] === 'string') {
9312
+ if (!_hop.call(taco.a, key)) continue;
9313
+ if (key.startsWith('on') && _is(taco.a[key], 'string')) {
8990
9314
  var actionName = taco.a[key];
8991
9315
  if (actionName in this._actions) {
8992
9316
  var registeredName = this._bwId + '_' + actionName;
@@ -9000,11 +9324,11 @@ ComponentHandle.prototype._wireActions = function(taco) {
9000
9324
  }
9001
9325
  }
9002
9326
  }
9003
- if (Array.isArray(taco.c)) {
9327
+ if (_isA(taco.c)) {
9004
9328
  for (var i = 0; i < taco.c.length; i++) {
9005
9329
  this._wireActions(taco.c[i]);
9006
9330
  }
9007
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9331
+ } else if (_is(taco.c, 'object') && taco.c.t) {
9008
9332
  this._wireActions(taco.c);
9009
9333
  }
9010
9334
  };
@@ -9013,7 +9337,7 @@ ComponentHandle.prototype._wireActions = function(taco) {
9013
9337
  * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
9014
9338
  * @private
9015
9339
  */
9016
- ComponentHandle.prototype._deepCloneTaco = function(taco) {
9340
+ _chp._deepCloneTaco = function(taco) {
9017
9341
  if (taco == null) return taco;
9018
9342
  // Preserve _bwWhen / _bwEach markers (contain functions)
9019
9343
  if (taco._bwWhen) {
@@ -9025,18 +9349,18 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
9025
9349
  if (taco._bwEach) {
9026
9350
  return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
9027
9351
  }
9028
- if (typeof taco !== 'object' || !taco.t) return taco;
9352
+ if (!_is(taco, 'object') || !taco.t) return taco;
9029
9353
  var result = { t: taco.t };
9030
9354
  if (taco.a) {
9031
9355
  result.a = {};
9032
9356
  for (var k in taco.a) {
9033
- if (Object.prototype.hasOwnProperty.call(taco.a, k)) result.a[k] = taco.a[k];
9357
+ if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
9034
9358
  }
9035
9359
  }
9036
9360
  if (taco.c != null) {
9037
- if (Array.isArray(taco.c)) {
9361
+ if (_isA(taco.c)) {
9038
9362
  result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
9039
- } else if (typeof taco.c === 'object') {
9363
+ } else if (_is(taco.c, 'object')) {
9040
9364
  result.c = this._deepCloneTaco(taco.c);
9041
9365
  } else {
9042
9366
  result.c = taco.c;
@@ -9050,27 +9374,31 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
9050
9374
  * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
9051
9375
  * @private
9052
9376
  */
9053
- ComponentHandle.prototype._tacoForDOM = function(taco) {
9054
- if (!taco || typeof taco !== 'object' || !taco.t) return taco;
9377
+ _chp._tacoForDOM = function(taco) {
9378
+ if (!_is(taco, 'object') || !taco.t) return taco;
9055
9379
  var result = { t: taco.t };
9056
9380
  if (taco.a) result.a = taco.a;
9057
9381
  if (taco.c != null) {
9058
- if (Array.isArray(taco.c)) {
9382
+ if (_isA(taco.c)) {
9059
9383
  result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
9060
- } else if (typeof taco.c === 'object' && taco.c.t) {
9384
+ } else if (_is(taco.c, 'object') && taco.c.t) {
9061
9385
  result.c = this._tacoForDOM(taco.c);
9062
9386
  } else {
9063
9387
  result.c = taco.c;
9064
9388
  }
9065
9389
  }
9066
9390
  // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
9391
+ if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
9392
+ _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
9393
+ '>. Use onclick attribute or bw.component() for child interactivity.');
9394
+ }
9067
9395
  return result;
9068
9396
  };
9069
9397
 
9070
9398
  /**
9071
9399
  * Unmount: remove from DOM, deactivate, preserve state for re-mount.
9072
9400
  */
9073
- ComponentHandle.prototype.unmount = function() {
9401
+ _chp.unmount = function() {
9074
9402
  if (!this.mounted) return;
9075
9403
 
9076
9404
  // unmount hook
@@ -9105,12 +9433,23 @@ ComponentHandle.prototype.unmount = function() {
9105
9433
  /**
9106
9434
  * Destroy: unmount + clear state + unregister actions.
9107
9435
  */
9108
- ComponentHandle.prototype.destroy = function() {
9436
+ _chp.destroy = function() {
9109
9437
  // willDestroy hook
9110
9438
  if (this._hooks.willDestroy) {
9111
9439
  this._hooks.willDestroy(this);
9112
9440
  }
9113
9441
 
9442
+ // Cascade destroy to children depth-first (Bug #5)
9443
+ for (var ci = this._children.length - 1; ci >= 0; ci--) {
9444
+ this._children[ci].destroy();
9445
+ }
9446
+ this._children = [];
9447
+ if (this._parent) {
9448
+ var idx = this._parent._children.indexOf(this);
9449
+ if (idx >= 0) this._parent._children.splice(idx, 1);
9450
+ this._parent = null;
9451
+ }
9452
+
9114
9453
  this.unmount();
9115
9454
 
9116
9455
  // Unregister actions from function registry
@@ -9137,12 +9476,36 @@ ComponentHandle.prototype.destroy = function() {
9137
9476
  * Flush dirty state: resolve changed bindings and apply to DOM.
9138
9477
  * @private
9139
9478
  */
9140
- ComponentHandle.prototype._flush = function() {
9479
+ _chp._flush = function() {
9141
9480
  this._scheduled = false;
9142
- var changedKeys = Object.keys(this._dirtyKeys);
9481
+ var changedKeys = _keys(this._dirtyKeys);
9143
9482
  this._dirtyKeys = {};
9144
9483
  if (changedKeys.length === 0 || !this.mounted) return;
9145
9484
 
9485
+ // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
9486
+ // rebuild the TACO from the factory with merged state (Bug #6)
9487
+ if (this._factory) {
9488
+ var rebuildNeeded = false;
9489
+ for (var fi = 0; fi < changedKeys.length; fi++) {
9490
+ if (_hop.call(this._factory.props, changedKeys[fi])) {
9491
+ rebuildNeeded = true; break;
9492
+ }
9493
+ }
9494
+ if (rebuildNeeded) {
9495
+ var merged = {};
9496
+ for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
9497
+ for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
9498
+ this._factory.props = merged;
9499
+ var newTaco = bw.make(this._factory.type, merged);
9500
+ newTaco._bwFactory = this._factory;
9501
+ this.taco = newTaco;
9502
+ this._originalTaco = this._deepCloneTaco(newTaco);
9503
+ this._render();
9504
+ if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
9505
+ return;
9506
+ }
9507
+ }
9508
+
9146
9509
  // willUpdate hook
9147
9510
  if (this._hooks.willUpdate) {
9148
9511
  this._hooks.willUpdate(this, changedKeys);
@@ -9181,7 +9544,7 @@ ComponentHandle.prototype._flush = function() {
9181
9544
  * Returns list of patches to apply.
9182
9545
  * @private
9183
9546
  */
9184
- ComponentHandle.prototype._resolveBindings = function(changedKeys) {
9547
+ _chp._resolveBindings = function(changedKeys) {
9185
9548
  var patches = [];
9186
9549
  for (var i = 0; i < this._bindings.length; i++) {
9187
9550
  var b = this._bindings[i];
@@ -9217,11 +9580,14 @@ ComponentHandle.prototype._resolveBindings = function(changedKeys) {
9217
9580
  * Apply patches to DOM.
9218
9581
  * @private
9219
9582
  */
9220
- ComponentHandle.prototype._applyPatches = function(patches) {
9583
+ _chp._applyPatches = function(patches) {
9221
9584
  for (var i = 0; i < patches.length; i++) {
9222
9585
  var p = patches[i];
9223
9586
  var el = this._bw_refs[p.refId];
9224
- if (!el) continue;
9587
+ if (!el) {
9588
+ if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
9589
+ continue;
9590
+ }
9225
9591
  if (p.type === 'content') {
9226
9592
  el.textContent = p.value;
9227
9593
  } else if (p.type === 'attribute') {
@@ -9238,7 +9604,7 @@ ComponentHandle.prototype._applyPatches = function(patches) {
9238
9604
  * Resolve all bindings and apply (used for initial render).
9239
9605
  * @private
9240
9606
  */
9241
- ComponentHandle.prototype._resolveAndApplyAll = function() {
9607
+ _chp._resolveAndApplyAll = function() {
9242
9608
  var patches = [];
9243
9609
  for (var i = 0; i < this._bindings.length; i++) {
9244
9610
  var b = this._bindings[i];
@@ -9261,7 +9627,7 @@ ComponentHandle.prototype._resolveAndApplyAll = function() {
9261
9627
  * Full re-render for structural changes (when/each branch switches).
9262
9628
  * @private
9263
9629
  */
9264
- ComponentHandle.prototype._render = function() {
9630
+ _chp._render = function() {
9265
9631
  if (!this.element || !this.element.parentNode) return;
9266
9632
  var parent = this.element.parentNode;
9267
9633
  var nextSibling = this.element.nextSibling;
@@ -9301,7 +9667,7 @@ ComponentHandle.prototype._render = function() {
9301
9667
  * @param {string} event - Event name (e.g., 'click')
9302
9668
  * @param {Function} handler - Event handler
9303
9669
  */
9304
- ComponentHandle.prototype.on = function(event, handler) {
9670
+ _chp.on = function(event, handler) {
9305
9671
  if (this.element) {
9306
9672
  this.element.addEventListener(event, handler);
9307
9673
  }
@@ -9313,7 +9679,7 @@ ComponentHandle.prototype.on = function(event, handler) {
9313
9679
  * @param {string} event - Event name
9314
9680
  * @param {Function} handler - Handler to remove
9315
9681
  */
9316
- ComponentHandle.prototype.off = function(event, handler) {
9682
+ _chp.off = function(event, handler) {
9317
9683
  if (this.element) {
9318
9684
  this.element.removeEventListener(event, handler);
9319
9685
  }
@@ -9328,7 +9694,7 @@ ComponentHandle.prototype.off = function(event, handler) {
9328
9694
  * @param {Function} handler - Handler function
9329
9695
  * @returns {Function} Unsubscribe function
9330
9696
  */
9331
- ComponentHandle.prototype.sub = function(topic, handler) {
9697
+ _chp.sub = function(topic, handler) {
9332
9698
  var unsub = bw.sub(topic, handler);
9333
9699
  this._subs.push(unsub);
9334
9700
  return unsub;
@@ -9339,10 +9705,10 @@ ComponentHandle.prototype.sub = function(topic, handler) {
9339
9705
  * @param {string} name - Action name
9340
9706
  * @param {...*} args - Arguments passed after comp
9341
9707
  */
9342
- ComponentHandle.prototype.action = function(name) {
9708
+ _chp.action = function(name) {
9343
9709
  var fn = this._actions[name];
9344
9710
  if (!fn) {
9345
- console.warn('ComponentHandle.action: unknown action "' + name + '"');
9711
+ _cw('ComponentHandle.action: unknown action "' + name + '"');
9346
9712
  return;
9347
9713
  }
9348
9714
  var args = [this].concat(Array.prototype.slice.call(arguments, 1));
@@ -9354,7 +9720,7 @@ ComponentHandle.prototype.action = function(name) {
9354
9720
  * @param {string} sel - CSS selector
9355
9721
  * @returns {Element|null}
9356
9722
  */
9357
- ComponentHandle.prototype.select = function(sel) {
9723
+ _chp.select = function(sel) {
9358
9724
  return this.element ? this.element.querySelector(sel) : null;
9359
9725
  };
9360
9726
 
@@ -9363,7 +9729,7 @@ ComponentHandle.prototype.select = function(sel) {
9363
9729
  * @param {string} sel - CSS selector
9364
9730
  * @returns {Element[]}
9365
9731
  */
9366
- ComponentHandle.prototype.selectAll = function(sel) {
9732
+ _chp.selectAll = function(sel) {
9367
9733
  if (!this.element) return [];
9368
9734
  return Array.prototype.slice.call(this.element.querySelectorAll(sel));
9369
9735
  };
@@ -9374,7 +9740,7 @@ ComponentHandle.prototype.selectAll = function(sel) {
9374
9740
  * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
9375
9741
  * @returns {ComponentHandle} this (for chaining)
9376
9742
  */
9377
- ComponentHandle.prototype.userTag = function(tag) {
9743
+ _chp.userTag = function(tag) {
9378
9744
  this._userTag = tag;
9379
9745
  if (this.element) {
9380
9746
  this.element.classList.add(tag);
@@ -9475,14 +9841,399 @@ bw.message = function(target, action, data) {
9475
9841
  }
9476
9842
  if (!el || !el._bwComponentHandle) return false;
9477
9843
  var comp = el._bwComponentHandle;
9478
- if (typeof comp[action] !== 'function') {
9479
- console.warn('bw.message: unknown action "' + action + '" on component ' + target);
9844
+ if (!_is(comp[action], 'function')) {
9845
+ _cw('bw.message: unknown action "' + action + '" on component ' + target);
9480
9846
  return false;
9481
9847
  }
9482
9848
  comp[action](data);
9483
9849
  return true;
9484
9850
  };
9485
9851
 
9852
+ // ===================================================================================
9853
+ // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
9854
+ // ===================================================================================
9855
+
9856
+ /**
9857
+ * Registry of named functions sent via register messages.
9858
+ * Populated by clientApply({ type: 'register', name, body }).
9859
+ * Invoked by clientApply({ type: 'call', name, args }).
9860
+ * @private
9861
+ */
9862
+ bw._clientFunctions = {};
9863
+
9864
+ /**
9865
+ * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
9866
+ * Default false — exec messages are rejected unless explicitly opted in.
9867
+ * @private
9868
+ */
9869
+ bw._allowExec = false;
9870
+
9871
+ /**
9872
+ * Built-in client functions available via call() without registration.
9873
+ * @private
9874
+ */
9875
+ bw._builtinClientFunctions = {
9876
+ scrollTo: function(selector) {
9877
+ var el = bw._el(selector);
9878
+ if (el) el.scrollTop = el.scrollHeight;
9879
+ },
9880
+ focus: function(selector) {
9881
+ var el = bw._el(selector);
9882
+ if (el && _is(el.focus, 'function')) el.focus();
9883
+ },
9884
+ download: function(filename, content, mimeType) {
9885
+ if (typeof document === 'undefined') return;
9886
+ var blob = new Blob([content], { type: mimeType || 'text/plain' });
9887
+ var a = document.createElement('a');
9888
+ a.href = URL.createObjectURL(blob);
9889
+ a.download = filename;
9890
+ a.click();
9891
+ URL.revokeObjectURL(a.href);
9892
+ },
9893
+ clipboard: function(text) {
9894
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
9895
+ navigator.clipboard.writeText(text);
9896
+ }
9897
+ },
9898
+ redirect: function(url) {
9899
+ if (typeof window !== 'undefined') window.location.href = url;
9900
+ },
9901
+ log: function() {
9902
+ console.log.apply(console, arguments);
9903
+ }
9904
+ };
9905
+
9906
+ /**
9907
+ * Parse a bwserve protocol message string, supporting both strict JSON
9908
+ * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
9909
+ *
9910
+ * The r-prefix format is designed for C/C++ string literals where
9911
+ * double-quote escaping is painful. The parser is a state machine
9912
+ * that walks character by character — not a regex replace.
9913
+ *
9914
+ * Escaping: apostrophes inside single-quoted values must be escaped
9915
+ * with backslash: r{'name':'Barry\'s room'}
9916
+ *
9917
+ * @param {string} str - JSON or r-prefixed relaxed JSON string
9918
+ * @returns {Object} Parsed message object
9919
+ * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
9920
+ * @category Server
9921
+ */
9922
+ bw.clientParse = function(str) {
9923
+ str = (str || '').trim();
9924
+ if (str.charAt(0) !== 'r') return JSON.parse(str);
9925
+ str = str.slice(1);
9926
+
9927
+ var out = [];
9928
+ var i = 0;
9929
+ var len = str.length;
9930
+
9931
+ while (i < len) {
9932
+ var ch = str[i];
9933
+
9934
+ if (ch === "'") {
9935
+ // Single-quoted string → emit as double-quoted
9936
+ out.push('"');
9937
+ i++;
9938
+ while (i < len) {
9939
+ var c = str[i];
9940
+ if (c === '\\' && i + 1 < len) {
9941
+ var next = str[i + 1];
9942
+ if (next === "'") {
9943
+ out.push("'"); // \' in input → ' in output
9944
+ } else {
9945
+ out.push('\\');
9946
+ out.push(next);
9947
+ }
9948
+ i += 2;
9949
+ } else if (c === '"') {
9950
+ out.push('\\"');
9951
+ i++;
9952
+ } else if (c === "'") {
9953
+ break;
9954
+ } else {
9955
+ out.push(c);
9956
+ i++;
9957
+ }
9958
+ }
9959
+ out.push('"');
9960
+ i++; // skip closing '
9961
+
9962
+ } else if (ch === '"') {
9963
+ // Double-quoted string — pass through verbatim
9964
+ out.push(ch);
9965
+ i++;
9966
+ while (i < len) {
9967
+ var c2 = str[i];
9968
+ if (c2 === '\\' && i + 1 < len) {
9969
+ out.push(c2);
9970
+ out.push(str[i + 1]);
9971
+ i += 2;
9972
+ } else {
9973
+ out.push(c2);
9974
+ i++;
9975
+ if (c2 === '"') break;
9976
+ }
9977
+ }
9978
+
9979
+ } else if (ch === ',') {
9980
+ // Trailing comma check: skip comma if next non-whitespace is } or ]
9981
+ var j = i + 1;
9982
+ while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) j++;
9983
+ if (j < len && (str[j] === '}' || str[j] === ']')) {
9984
+ i++; // skip trailing comma
9985
+ } else {
9986
+ out.push(ch);
9987
+ i++;
9988
+ }
9989
+
9990
+ } else {
9991
+ out.push(ch);
9992
+ i++;
9993
+ }
9994
+ }
9995
+
9996
+ return JSON.parse(out.join(''));
9997
+ };
9998
+
9999
+ /**
10000
+ * Apply a bwserve protocol message to the DOM.
10001
+ *
10002
+ * Dispatches one of 9 message types:
10003
+ * replace — bw.DOM(target, node)
10004
+ * append — target.appendChild(bw.createDOM(node))
10005
+ * remove — bw.cleanup(target); target.remove()
10006
+ * patch — bw.patch(target, content, attr)
10007
+ * batch — iterate ops, call clientApply for each
10008
+ * message — bw.message(target, action, data)
10009
+ * register — store a named function for later call()
10010
+ * call — invoke a registered or built-in function
10011
+ * exec — execute arbitrary JS (requires allowExec)
10012
+ *
10013
+ * Target resolution:
10014
+ * Starts with '#' or '.' → CSS selector (querySelector)
10015
+ * Otherwise → getElementById, then bw._el fallback
10016
+ *
10017
+ * @param {Object} msg - Protocol message
10018
+ * @returns {boolean} true if the message was applied successfully
10019
+ * @category Server
10020
+ */
10021
+ bw.clientApply = function(msg) {
10022
+ if (!msg || !msg.type) return false;
10023
+
10024
+ var type = msg.type;
10025
+ var target = msg.target;
10026
+
10027
+ if (type === 'replace') {
10028
+ var el = bw._el(target);
10029
+ if (!el) return false;
10030
+ bw.DOM(el, msg.node);
10031
+ return true;
10032
+
10033
+ } else if (type === 'patch') {
10034
+ var patched = bw.patch(target, msg.content, msg.attr);
10035
+ return patched !== null;
10036
+
10037
+ } else if (type === 'append') {
10038
+ var parent = bw._el(target);
10039
+ if (!parent) return false;
10040
+ var child = bw.createDOM(msg.node);
10041
+ parent.appendChild(child);
10042
+ return true;
10043
+
10044
+ } else if (type === 'remove') {
10045
+ var toRemove = bw._el(target);
10046
+ if (!toRemove) return false;
10047
+ if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
10048
+ toRemove.remove();
10049
+ return true;
10050
+
10051
+ } else if (type === 'batch') {
10052
+ if (!_isA(msg.ops)) return false;
10053
+ var allOk = true;
10054
+ msg.ops.forEach(function(op) {
10055
+ if (!bw.clientApply(op)) allOk = false;
10056
+ });
10057
+ return allOk;
10058
+
10059
+ } else if (type === 'message') {
10060
+ return bw.message(msg.target, msg.action, msg.data);
10061
+
10062
+ } else if (type === 'register') {
10063
+ if (!msg.name || !msg.body) return false;
10064
+ try {
10065
+ bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
10066
+ return true;
10067
+ } catch (e) {
10068
+ _ce('[bw] register error:', msg.name, e);
10069
+ return false;
10070
+ }
10071
+
10072
+ } else if (type === 'call') {
10073
+ if (!msg.name) return false;
10074
+ var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
10075
+ if (!_is(fn, 'function')) return false;
10076
+ try {
10077
+ var args = _isA(msg.args) ? msg.args : [];
10078
+ fn.apply(null, args);
10079
+ return true;
10080
+ } catch (e) {
10081
+ _ce('[bw] call error:', msg.name, e);
10082
+ return false;
10083
+ }
10084
+
10085
+ } else if (type === 'exec') {
10086
+ if (!bw._allowExec) {
10087
+ _cw('[bw] exec rejected: allowExec is not enabled');
10088
+ return false;
10089
+ }
10090
+ if (!msg.code) return false;
10091
+ try {
10092
+ new Function(msg.code)();
10093
+ return true;
10094
+ } catch (e) {
10095
+ _ce('[bw] exec error:', e);
10096
+ return false;
10097
+ }
10098
+ }
10099
+
10100
+ return false;
10101
+ };
10102
+
10103
+ /**
10104
+ * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
10105
+ *
10106
+ * Returns a connection object with sendAction(), on(), and close() methods.
10107
+ *
10108
+ * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
10109
+ * @param {Object} [opts] - Connection options
10110
+ * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
10111
+ * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
10112
+ * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
10113
+ * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
10114
+ * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
10115
+ * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
10116
+ * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
10117
+ * @returns {Object} Connection object { sendAction, on, close, status }
10118
+ * @category Server
10119
+ */
10120
+ bw.clientConnect = function(url, opts) {
10121
+ opts = opts || {};
10122
+ var transport = opts.transport || 'sse';
10123
+ var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
10124
+ var reconnect = opts.reconnect !== false;
10125
+ var onStatus = opts.onStatus || function() {};
10126
+ var onMessage = opts.onMessage || null;
10127
+ var handlers = {};
10128
+ // Set the global allowExec flag from connection options
10129
+ bw._allowExec = !!opts.allowExec;
10130
+ var conn = {
10131
+ status: 'connecting',
10132
+ _es: null,
10133
+ _pollTimer: null
10134
+ };
10135
+
10136
+ function setStatus(s) {
10137
+ conn.status = s;
10138
+ onStatus(s);
10139
+ }
10140
+
10141
+ function handleMessage(data) {
10142
+ try {
10143
+ var msg = _is(data, 'string') ? bw.clientParse(data) : data;
10144
+ if (onMessage) onMessage(msg);
10145
+ if (handlers.message) handlers.message(msg);
10146
+ bw.clientApply(msg);
10147
+ } catch (e) {
10148
+ if (handlers.error) handlers.error(e);
10149
+ }
10150
+ }
10151
+
10152
+ if (transport === 'sse' && typeof EventSource !== 'undefined') {
10153
+ setStatus('connecting');
10154
+ var es = new EventSource(url);
10155
+ conn._es = es;
10156
+
10157
+ es.onopen = function() {
10158
+ setStatus('connected');
10159
+ if (handlers.open) handlers.open();
10160
+ };
10161
+
10162
+ es.onmessage = function(e) {
10163
+ handleMessage(e.data);
10164
+ };
10165
+
10166
+ es.onerror = function() {
10167
+ if (conn.status === 'connected') {
10168
+ setStatus('disconnected');
10169
+ }
10170
+ if (handlers.error) handlers.error(new Error('SSE connection error'));
10171
+ if (!reconnect) {
10172
+ es.close();
10173
+ }
10174
+ // EventSource auto-reconnects by default when reconnect=true
10175
+ };
10176
+ } else if (transport === 'poll') {
10177
+ var interval = opts.interval || 2000;
10178
+ setStatus('connected');
10179
+ conn._pollTimer = setInterval(function() {
10180
+ fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
10181
+ if (_isA(msgs)) {
10182
+ msgs.forEach(handleMessage);
10183
+ } else if (msgs && msgs.type) {
10184
+ handleMessage(msgs);
10185
+ }
10186
+ }).catch(function(e) {
10187
+ if (handlers.error) handlers.error(e);
10188
+ });
10189
+ }, interval);
10190
+ }
10191
+
10192
+ /**
10193
+ * Send an action to the server via POST.
10194
+ * @param {string} action - Action name
10195
+ * @param {Object} [data] - Action payload
10196
+ */
10197
+ conn.sendAction = function(action, data) {
10198
+ var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
10199
+ fetch(actionUrl, {
10200
+ method: 'POST',
10201
+ headers: { 'Content-Type': 'application/json' },
10202
+ body: body
10203
+ }).catch(function(e) {
10204
+ if (handlers.error) handlers.error(e);
10205
+ });
10206
+ };
10207
+
10208
+ /**
10209
+ * Register an event handler.
10210
+ * @param {string} event - 'open'|'message'|'error'|'close'
10211
+ * @param {Function} handler
10212
+ */
10213
+ conn.on = function(event, handler) {
10214
+ handlers[event] = handler;
10215
+ return conn;
10216
+ };
10217
+
10218
+ /**
10219
+ * Close the connection.
10220
+ */
10221
+ conn.close = function() {
10222
+ if (conn._es) {
10223
+ conn._es.close();
10224
+ conn._es = null;
10225
+ }
10226
+ if (conn._pollTimer) {
10227
+ clearInterval(conn._pollTimer);
10228
+ conn._pollTimer = null;
10229
+ }
10230
+ setStatus('disconnected');
10231
+ if (handlers.close) handlers.close();
10232
+ };
10233
+
10234
+ return conn;
10235
+ };
10236
+
9486
10237
  // ===================================================================================
9487
10238
  // bw.inspect() — Debug utility
9488
10239
  // ===================================================================================
@@ -9509,33 +10260,33 @@ bw.inspect = function(target) {
9509
10260
  el = target.element;
9510
10261
  comp = target;
9511
10262
  } else {
9512
- if (typeof target === 'string') {
10263
+ if (_is(target, 'string')) {
9513
10264
  el = bw.$(target)[0];
9514
10265
  }
9515
10266
  if (!el) {
9516
- console.warn('bw.inspect: element not found');
10267
+ _cw('bw.inspect: element not found');
9517
10268
  return null;
9518
10269
  }
9519
10270
  comp = el._bwComponentHandle;
9520
10271
  }
9521
10272
  if (!comp) {
9522
- console.log('bw.inspect: no ComponentHandle on this element');
9523
- console.log(' Tag:', el.tagName);
9524
- console.log(' Classes:', el.className);
9525
- console.log(' _bw_state:', el._bw_state || '(none)');
10273
+ _cl('bw.inspect: no ComponentHandle on this element');
10274
+ _cl(' Tag:', el.tagName);
10275
+ _cl(' Classes:', el.className);
10276
+ _cl(' _bw_state:', el._bw_state || '(none)');
9526
10277
  return null;
9527
10278
  }
9528
10279
  var deps = comp._bindings.reduce(function(s, b) {
9529
10280
  return s.concat(b.deps || []);
9530
10281
  }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
9531
10282
  console.group('Component: ' + comp._bwId);
9532
- console.log('State:', comp._state);
9533
- console.log('Bindings:', comp._bindings.length, '(deps:', deps, ')');
9534
- console.log('Methods:', Object.keys(comp._methods));
9535
- console.log('Actions:', Object.keys(comp._actions));
9536
- console.log('User tag:', comp._userTag || '(none)');
9537
- console.log('Mounted:', comp.mounted);
9538
- console.log('Element:', comp.element);
10283
+ _cl('State:', comp._state);
10284
+ _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
10285
+ _cl('Methods:', _keys(comp._methods));
10286
+ _cl('Actions:', _keys(comp._actions));
10287
+ _cl('User tag:', comp._userTag || '(none)');
10288
+ _cl('Mounted:', comp.mounted);
10289
+ _cl('Element:', comp.element);
9539
10290
  console.groupEnd();
9540
10291
  return comp;
9541
10292
  };
@@ -9558,8 +10309,8 @@ bw.compile = function(taco) {
9558
10309
  // Pre-extract all binding expressions
9559
10310
  var precompiled = [];
9560
10311
  function walkExpressions(node) {
9561
- if (!node || typeof node !== 'object') return;
9562
- if (typeof node.c === 'string' && node.c.indexOf('${') >= 0) {
10312
+ if (!_is(node, 'object')) return;
10313
+ if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
9563
10314
  var parsed = bw._parseBindings(node.c);
9564
10315
  for (var i = 0; i < parsed.length; i++) {
9565
10316
  try {
@@ -9574,9 +10325,9 @@ bw.compile = function(taco) {
9574
10325
  }
9575
10326
  if (node.a) {
9576
10327
  for (var key in node.a) {
9577
- if (Object.prototype.hasOwnProperty.call(node.a, key)) {
10328
+ if (_hop.call(node.a, key)) {
9578
10329
  var v = node.a[key];
9579
- if (typeof v === 'string' && v.indexOf('${') >= 0) {
10330
+ if (_is(v, 'string') && v.indexOf('${') >= 0) {
9580
10331
  var parsed2 = bw._parseBindings(v);
9581
10332
  for (var j = 0; j < parsed2.length; j++) {
9582
10333
  try {
@@ -9592,9 +10343,9 @@ bw.compile = function(taco) {
9592
10343
  }
9593
10344
  }
9594
10345
  }
9595
- if (Array.isArray(node.c)) {
10346
+ if (_isA(node.c)) {
9596
10347
  for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
9597
- } else if (node.c && typeof node.c === 'object' && node.c.t) {
10348
+ } else if (_is(node.c, 'object') && node.c.t) {
9598
10349
  walkExpressions(node.c);
9599
10350
  }
9600
10351
  }
@@ -9606,7 +10357,7 @@ bw.compile = function(taco) {
9606
10357
  handle._precompiledBindings = precompiled;
9607
10358
  if (initialState) {
9608
10359
  for (var k in initialState) {
9609
- if (Object.prototype.hasOwnProperty.call(initialState, k)) {
10360
+ if (_hop.call(initialState, k)) {
9610
10361
  handle._state[k] = initialState[k];
9611
10362
  }
9612
10363
  }
@@ -9637,18 +10388,18 @@ bw.compile = function(taco) {
9637
10388
  bw.css = function(rules, options = {}) {
9638
10389
  const { minify = false, pretty = !minify } = options;
9639
10390
 
9640
- if (typeof rules === 'string') return rules;
10391
+ if (_is(rules, 'string')) return rules;
9641
10392
 
9642
10393
  let css = '';
9643
10394
  const indent = pretty ? ' ' : '';
9644
10395
  const newline = pretty ? '\n' : '';
9645
10396
  const space = pretty ? ' ' : '';
9646
10397
 
9647
- if (Array.isArray(rules)) {
10398
+ if (_isA(rules)) {
9648
10399
  css = rules.map(rule => bw.css(rule, options)).join(newline);
9649
- } else if (typeof rules === 'object') {
10400
+ } else if (_is(rules, 'object')) {
9650
10401
  Object.entries(rules).forEach(([selector, styles]) => {
9651
- if (typeof styles === 'object' && !Array.isArray(styles)) {
10402
+ if (_is(styles, 'object')) {
9652
10403
  // Handle @media, @keyframes, @supports — recurse into nested block
9653
10404
  if (selector.charAt(0) === '@') {
9654
10405
  const inner = bw.css(styles, options);
@@ -9697,7 +10448,7 @@ bw.css = function(rules, options = {}) {
9697
10448
  */
9698
10449
  bw.injectCSS = function(css, options = {}) {
9699
10450
  if (!bw._isBrowser) {
9700
- console.warn('bw.injectCSS requires a DOM environment');
10451
+ _cw('bw.injectCSS requires a DOM environment');
9701
10452
  return null;
9702
10453
  }
9703
10454
 
@@ -9714,7 +10465,7 @@ bw.injectCSS = function(css, options = {}) {
9714
10465
  }
9715
10466
 
9716
10467
  // Convert CSS if needed
9717
- const cssStr = typeof css === 'string' ? css : bw.css(css, options);
10468
+ const cssStr = _is(css, 'string') ? css : bw.css(css, options);
9718
10469
 
9719
10470
  // Set or append CSS
9720
10471
  if (append && styleEl.textContent) {
@@ -9744,7 +10495,7 @@ bw.s = function() {
9744
10495
  var result = {};
9745
10496
  for (var i = 0; i < arguments.length; i++) {
9746
10497
  var arg = arguments[i];
9747
- if (arg && typeof arg === 'object') Object.assign(result, arg);
10498
+ if (_is(arg, 'object')) Object.assign(result, arg);
9748
10499
  }
9749
10500
  return result;
9750
10501
  };
@@ -9867,7 +10618,7 @@ bw.u = {
9867
10618
  bw.responsive = function(selector, breakpoints) {
9868
10619
  var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
9869
10620
  var parts = [];
9870
- Object.keys(breakpoints).forEach(function(key) {
10621
+ _keys(breakpoints).forEach(function(key) {
9871
10622
  var rules = {};
9872
10623
  if (key === 'base') {
9873
10624
  rules[selector] = breakpoints[key];
@@ -9939,18 +10690,18 @@ if (bw._isBrowser) {
9939
10690
  if (!selector) return [];
9940
10691
 
9941
10692
  // Already an array
9942
- if (Array.isArray(selector)) return selector;
10693
+ if (_isA(selector)) return selector;
9943
10694
 
9944
10695
  // Single element
9945
10696
  if (selector.nodeType) return [selector];
9946
10697
 
9947
10698
  // NodeList or HTMLCollection
9948
- if (selector.length !== undefined && typeof selector !== 'string') {
10699
+ if (selector.length !== undefined && !_is(selector, 'string')) {
9949
10700
  return Array.from(selector);
9950
10701
  }
9951
10702
 
9952
10703
  // CSS selector string
9953
- if (typeof selector === 'string') {
10704
+ if (_is(selector, 'string')) {
9954
10705
  return Array.from(document.querySelectorAll(selector));
9955
10706
  }
9956
10707
 
@@ -10454,7 +11205,7 @@ bw.makeTable = function(config) {
10454
11205
 
10455
11206
  // Auto-detect columns if not provided
10456
11207
  const cols = columns || (data.length > 0
10457
- ? Object.keys(data[0]).map(key => ({ key, label: key }))
11208
+ ? _keys(data[0]).map(key => ({ key, label: key }))
10458
11209
  : []);
10459
11210
 
10460
11211
  // Current sort state
@@ -10469,7 +11220,7 @@ bw.makeTable = function(config) {
10469
11220
  const bVal = b[currentSortColumn];
10470
11221
 
10471
11222
  // Handle different types
10472
- if (typeof aVal === 'number' && typeof bVal === 'number') {
11223
+ if (_is(aVal, 'number') && _is(bVal, 'number')) {
10473
11224
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
10474
11225
  }
10475
11226
 
@@ -10579,7 +11330,7 @@ bw.makeTable = function(config) {
10579
11330
  bw.makeTableFromArray = function(config) {
10580
11331
  const { data = [], headerRow = true, columns, ...rest } = config;
10581
11332
 
10582
- if (!Array.isArray(data) || data.length === 0) {
11333
+ if (!_isA(data) || data.length === 0) {
10583
11334
  return bw.makeTable({ data: [], columns: columns || [], ...rest });
10584
11335
  }
10585
11336
 
@@ -10661,7 +11412,7 @@ bw.makeBarChart = function(config) {
10661
11412
  className = ''
10662
11413
  } = config;
10663
11414
 
10664
- if (!Array.isArray(data) || data.length === 0) {
11415
+ if (!_isA(data) || data.length === 0) {
10665
11416
  return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
10666
11417
  }
10667
11418
 
@@ -10810,7 +11561,7 @@ bw._componentRegistry = new Map();
10810
11561
  */
10811
11562
  bw.render = function(element, position, taco) {
10812
11563
  // Get target element
10813
- const targetEl = typeof element === 'string'
11564
+ const targetEl = _is(element, 'string')
10814
11565
  ? document.querySelector(element)
10815
11566
  : element;
10816
11567
 
@@ -10960,7 +11711,7 @@ bw.render = function(element, position, taco) {
10960
11711
  setContent(content) {
10961
11712
  this._taco.c = content;
10962
11713
  if (this.element) {
10963
- if (typeof content === 'string') {
11714
+ if (_is(content, 'string')) {
10964
11715
  this.element.textContent = content;
10965
11716
  } else {
10966
11717
  // Re-render for complex content