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,24 +1,25 @@
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
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
5
5
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.bw = factory());
6
6
  })(this, (function () { 'use strict';
7
7
 
8
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
8
9
  /**
9
10
  * Auto-generated version file from package.json
10
11
  * DO NOT EDIT DIRECTLY - Use npm run generate-version
11
12
  */
12
13
 
13
14
  const VERSION_INFO = {
14
- version: '2.0.15',
15
+ version: '2.0.17',
15
16
  name: 'bitwrench',
16
17
  description: 'A library for javascript UI functions.',
17
18
  license: 'BSD-2-Clause',
18
19
  homepage: 'https://deftio.github.com/bitwrench/pages',
19
20
  repository: 'git+https://github.com/deftio/bitwrench.git',
20
21
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
21
- buildDate: '2026-03-10T09:08:17.015Z'
22
+ buildDate: '2026-03-13T23:15:10.823Z'
22
23
  };
23
24
 
24
25
  /**
@@ -436,12 +437,11 @@
436
437
  var lightBase = config.light || hslToHex([h, 8, 97]);
437
438
  var darkBase = config.dark || hslToHex([h, 10, 13]);
438
439
 
439
- // Background & surface tokens — default to light (white/near-white).
440
- // Dark backgrounds require explicit config.background / config.surface.
441
- // Primary/secondary colors are accents, not page backgrounds, so
442
- // isLightPalette should NOT drive bg/surface defaults.
443
- var bgBase = config.background || '#ffffff';
444
- var surfBase = config.surface || '#f8f9fa';
440
+ // Background & surface tokens — tinted with primary hue for theme personality.
441
+ // Very subtle: bg at L=98/S=6, surface at L=96/S=8.
442
+ // User can override with config.background / config.surface.
443
+ var bgBase = config.background || hslToHex([h, 6, 98]);
444
+ var surfBase = config.surface || hslToHex([h, 8, 96]);
445
445
 
446
446
  var palette = {
447
447
  primary: deriveShades(config.primary),
@@ -1574,7 +1574,7 @@
1574
1574
  '@media (min-width: 992px)': { '.bw_container': { 'max-width': '960px' } },
1575
1575
  '@media (min-width: 1200px)': { '.bw_container': { 'max-width': '1140px' } },
1576
1576
  '.bw_container_fluid': {
1577
- 'width': '100%', 'padding-right': '15px', 'padding-left': '15px',
1577
+ 'width': '100%', 'padding-right': '0.75rem', 'padding-left': '0.75rem',
1578
1578
  'margin-right': 'auto', 'margin-left': 'auto'
1579
1579
  },
1580
1580
  '.bw_row': {
@@ -1735,7 +1735,8 @@
1735
1735
  '.bw_badge': {
1736
1736
  'display': 'inline-block', 'font-size': '0.875rem',
1737
1737
  'font-weight': '600', 'line-height': '1.3', 'text-align': 'center',
1738
- 'white-space': 'nowrap', 'vertical-align': 'baseline'
1738
+ 'white-space': 'nowrap', 'vertical-align': 'baseline',
1739
+ 'padding': '0.35rem 0.65rem', 'border-radius': '0.25rem'
1739
1740
  },
1740
1741
  '.bw_badge:empty': { 'display': 'none' },
1741
1742
  '.bw_badge_sm': { 'font-size': '0.75rem', 'padding': '0.25rem 0.5rem' },
@@ -1920,7 +1921,7 @@
1920
1921
  // ---- Code demo ----
1921
1922
  codeDemo: {
1922
1923
  '.bw_code_demo': { 'margin-bottom': '2rem' },
1923
- '.bw_code_pre': { 'margin': '0', 'border': 'none', 'overflow-x': 'auto' },
1924
+ '.bw_code_pre': { 'margin': '0', 'border': 'none', 'overflow-x': 'auto', 'max-width': '100%' },
1924
1925
  '.bw_code_block': {
1925
1926
  'display': 'block', 'padding': '1.25rem',
1926
1927
  'font-family': '"SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace',
@@ -2017,7 +2018,7 @@
2017
2018
  },
2018
2019
  '.bw_modal.bw_modal_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
2019
2020
  '.bw_modal_dialog': {
2020
- 'position': 'relative', 'width': '100%', 'max-width': '500px', 'margin': '1.75rem auto',
2021
+ 'position': 'relative', 'width': 'calc(100% - 1rem)', 'max-width': '500px', 'margin': '1.75rem auto',
2021
2022
  'pointer-events': 'none'
2022
2023
  },
2023
2024
  '.bw_modal.bw_modal_show .bw_modal_dialog': { 'transform': 'translateY(0)' },
@@ -2047,7 +2048,7 @@
2047
2048
  '.bw_toast_container.bw_toast_top_center': { 'top': '0', 'left': '50%', 'transform': 'translateX(-50%)' },
2048
2049
  '.bw_toast_container.bw_toast_bottom_center': { 'bottom': '0', 'left': '50%', 'transform': 'translateX(-50%)' },
2049
2050
  '.bw_toast': {
2050
- 'pointer-events': 'auto', 'width': '350px', 'max-width': '100%', 'background-clip': 'padding-box',
2051
+ 'pointer-events': 'auto', 'width': '350px', 'max-width': 'calc(100vw - 2rem)', 'background-clip': 'padding-box',
2051
2052
  'opacity': '0'
2052
2053
  },
2053
2054
  '.bw_toast.bw_toast_show': { 'opacity': '1', 'transform': 'translateY(0)' },
@@ -2133,7 +2134,7 @@
2133
2134
  '.bw_tooltip_wrapper': { 'position': 'relative', 'display': 'inline-block' },
2134
2135
  '.bw_tooltip': {
2135
2136
  'position': 'absolute', 'z-index': '999',
2136
- 'font-size': '0.875rem', 'white-space': 'nowrap', 'pointer-events': 'none',
2137
+ 'font-size': '0.875rem', 'white-space': 'nowrap', 'max-width': 'min(300px, calc(100vw - 1rem))', 'pointer-events': 'none',
2137
2138
  'opacity': '0', 'visibility': 'hidden'
2138
2139
  },
2139
2140
  '.bw_tooltip.bw_tooltip_show': { 'opacity': '1', 'visibility': 'visible' },
@@ -2153,7 +2154,7 @@
2153
2154
  '.bw_popover_trigger': { 'cursor': 'pointer' },
2154
2155
  '.bw_popover': {
2155
2156
  'position': 'absolute', 'z-index': '1000',
2156
- 'min-width': '200px', 'max-width': '320px',
2157
+ 'min-width': '200px', 'max-width': 'min(320px, calc(100vw - 2rem))',
2157
2158
  'pointer-events': 'none', 'opacity': '0', 'visibility': 'hidden'
2158
2159
  },
2159
2160
  '.bw_popover.bw_popover_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
@@ -2336,7 +2337,18 @@
2336
2337
  '.bw_hero, .bw_hero': { 'padding': '2rem 1rem' },
2337
2338
  '.bw_cta_actions, .bw_cta-actions': { 'flex-direction': 'column' },
2338
2339
  '.bw_hstack, .bw_hstack': { 'flex-direction': 'column' },
2339
- '.bw_feature_grid, .bw_feature-grid': { 'grid-template-columns': '1fr' }
2340
+ '.bw_feature_grid, .bw_feature-grid': { 'grid-template-columns': '1fr' },
2341
+ '.bw_modal_dialog': { 'margin': '0.5rem auto' },
2342
+ '.bw_modal_lg': { 'max-width': 'calc(100% - 1rem)' },
2343
+ '.bw_modal_xl': { 'max-width': 'calc(100% - 1rem)' },
2344
+ '.bw_navbar': { 'padding': '0.5rem 0.75rem' },
2345
+ '.bw_navbar_brand': { 'margin-right': '0.5rem', 'font-size': '1rem' },
2346
+ '.bw_navbar_nav': { 'flex-wrap': 'wrap' },
2347
+ '.bw_tooltip': { 'white-space': 'normal' },
2348
+ '.bw_table': { 'display': 'block', 'overflow-x': 'auto', '-webkit-overflow-scrolling': 'touch' },
2349
+ '.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%' },
2350
+ '.bw_container': { 'padding-right': '0.5rem', 'padding-left': '0.5rem' },
2351
+ '.bw_container_fluid': { 'padding-right': '0.5rem', 'padding-left': '0.5rem' }
2340
2352
  }
2341
2353
  }
2342
2354
  };
@@ -6869,7 +6881,11 @@
6869
6881
  function make(type, props) {
6870
6882
  var def = BCCL[type];
6871
6883
  if (!def) throw new Error('bw.make: unknown component type "' + type + '". Available: ' + Object.keys(BCCL).join(', '));
6872
- return def.make(props || {});
6884
+ var taco = def.make(props || {});
6885
+ if (taco && typeof taco === 'object') {
6886
+ taco._bwFactory = { type: type, props: props || {} };
6887
+ }
6888
+ return taco;
6873
6889
  }
6874
6890
 
6875
6891
  var components = /*#__PURE__*/Object.freeze({
@@ -6990,7 +7006,7 @@
6990
7006
  __monkey_patch_is_nodejs__: {
6991
7007
  _value: 'ignore',
6992
7008
  set: function(x) {
6993
- this._value = (typeof x === 'boolean') ? x : 'ignore';
7009
+ this._value = _is(x, 'boolean') ? x : 'ignore';
6994
7010
  },
6995
7011
  get: function() {
6996
7012
  return this._value;
@@ -7038,6 +7054,67 @@
7038
7054
  configurable: true
7039
7055
  });
7040
7056
 
7057
+ // ── Internal aliases ─────────────────────────────────────────────────────
7058
+ // Short names for frequently-used builtins and internal methods.
7059
+ // Same pattern as v1 (_to = bw.typeOf, etc.).
7060
+ //
7061
+ // Why: Terser can't shorten global property chains (console.warn,
7062
+ // Object.prototype.hasOwnProperty, Array.isArray, document.createElement)
7063
+ // because it can't prove they're side-effect-free. We can, so we alias
7064
+ // them here. Each alias saves bytes in the minified output, and the short
7065
+ // names also reduce visual noise in the hot paths (binding pipeline,
7066
+ // createDOM, etc.).
7067
+ //
7068
+ // Alias Target Sites
7069
+ // ───────── ────────────────────────────────────── ─────
7070
+ // _hop Object.prototype.hasOwnProperty 15
7071
+ // _isA Array.isArray 25
7072
+ // _keys Object.keys 7
7073
+ // _to bw.typeOf (type string) 26
7074
+ // _is type check boolean: _is(x,'string') ~50
7075
+ // _cw console.warn 8
7076
+ // _cl console.log 11
7077
+ // _ce console.error 4
7078
+ // _chp ComponentHandle.prototype 28 (defined after constructor)
7079
+ //
7080
+ // Note: document.createElement etc. are NOT aliased because they require
7081
+ // `this === document` and .bind() would add overhead on every call.
7082
+ // Console aliases use thin wrappers (not direct refs) so test monkey-
7083
+ // patching of console.warn/log/error continues to work.
7084
+ //
7085
+ // `typeof x` for UNDECLARED globals (window, document, process, require,
7086
+ // EventSource, navigator, Promise, __filename, import.meta) MUST stay as
7087
+ // raw `typeof` — calling _to(x) when x doesn't exist throws ReferenceError.
7088
+ //
7089
+ // ── v1 functional type helpers (kept for reference, not currently used) ──
7090
+ // _toa(x, type, trueVal, falseVal) — bw.typeAssign:
7091
+ // returns trueVal if _to(x)===type, else falseVal.
7092
+ // Replaces: (typeof x === 'string') ? A : B → _toa(x,'string',A,B)
7093
+ // _toc(x, type, trueVal, falseVal) — bw.typeConvert:
7094
+ // same as _toa but if trueVal/falseVal are functions, calls them with x.
7095
+ // Replaces: typeof x === 'string' ? fn(x) : default → _toc(x,'string',fn,default)
7096
+ // Uncomment if pattern frequency justifies them:
7097
+ // var _toa = function(x, t, y, n) { return _to(x) === t ? y : n; };
7098
+ // 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); };
7099
+ // ─────────────────────────────────────────────────────────────────────────
7100
+ var _hop = Object.prototype.hasOwnProperty;
7101
+ var _isA = Array.isArray;
7102
+ var _keys = Object.keys;
7103
+ var _to = typeOf; // imported from bitwrench-utils.js
7104
+ var _is = function(x, t) { var r = _to(x); return r === t || r.toLowerCase() === t; };
7105
+ // Console aliases use thin wrappers (not direct references) so that test
7106
+ // code can monkey-patch console.warn/log/error and the patches take effect.
7107
+ var _cw = function() { console.warn.apply(console, arguments); };
7108
+ var _cl = function() { console.log.apply(console, arguments); };
7109
+ var _ce = function() { console.error.apply(console, arguments); };
7110
+
7111
+ /**
7112
+ * Debug flag. When true, emits console.warn for silent binding failures
7113
+ * (missing paths, null refs, auto-created intermediate objects).
7114
+ * @type {boolean}
7115
+ */
7116
+ bw.debug = false;
7117
+
7041
7118
  /**
7042
7119
  * Lazy-resolve Node.js `fs` module.
7043
7120
  * Tries require('fs') first (available in CJS/UMD Node.js builds),
@@ -7185,7 +7262,7 @@
7185
7262
  */
7186
7263
  bw._el = function(id) {
7187
7264
  // Pass-through for DOM elements
7188
- if (typeof id !== 'string') return id || null;
7265
+ if (!_is(id, 'string')) return id || null;
7189
7266
  if (!id) return null;
7190
7267
  if (!bw._isBrowser) return null;
7191
7268
 
@@ -7281,7 +7358,7 @@
7281
7358
  * // => '&lt;b&gt;Hello&lt;&#x2F;b&gt; &amp; &quot;world&quot;'
7282
7359
  */
7283
7360
  bw.escapeHTML = function(str) {
7284
- if (typeof str !== 'string') return '';
7361
+ if (!_is(str, 'string')) return '';
7285
7362
 
7286
7363
  const escapeMap = {
7287
7364
  '&': '&amp;',
@@ -7354,7 +7431,7 @@
7354
7431
  }
7355
7432
 
7356
7433
  // Handle arrays of TACOs
7357
- if (Array.isArray(taco)) {
7434
+ if (_isA(taco)) {
7358
7435
  return taco.map(t => bw.html(t, options)).join('');
7359
7436
  }
7360
7437
 
@@ -7377,15 +7454,15 @@
7377
7454
  if (taco && taco._bwEach && options.state) {
7378
7455
  var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
7379
7456
  var arr = bw._evaluatePath(options.state, eachExpr);
7380
- if (!Array.isArray(arr)) return '';
7457
+ if (!_isA(arr)) return '';
7381
7458
  return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
7382
7459
  }
7383
7460
 
7384
7461
  // Handle primitives and non-TACO objects
7385
- if (typeof taco !== 'object' || !taco.t) {
7462
+ if (!_is(taco, 'object') || !taco.t) {
7386
7463
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
7387
7464
  // Resolve template bindings if state provided
7388
- if (options.state && typeof str === 'string' && str.indexOf('${') >= 0) {
7465
+ if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
7389
7466
  str = bw._resolveTemplate(str, options.state, !!options.compile);
7390
7467
  }
7391
7468
  return str;
@@ -7405,10 +7482,18 @@
7405
7482
  // Skip null, undefined, false
7406
7483
  if (value == null || value === false) continue;
7407
7484
 
7408
- // Skip event handlers (they're for DOM only)
7409
- if (key.startsWith('on')) continue;
7485
+ // Serialize event handlers via funcRegister
7486
+ if (key.startsWith('on')) {
7487
+ if (_is(value, 'function')) {
7488
+ var fnId = bw.funcRegister(value);
7489
+ attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
7490
+ } else if (_is(value, 'string')) {
7491
+ attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
7492
+ }
7493
+ continue;
7494
+ }
7410
7495
 
7411
- if (key === 'style' && typeof value === 'object') {
7496
+ if (key === 'style' && _is(value, 'object')) {
7412
7497
  // Convert style object to string
7413
7498
  const styleStr = Object.entries(value)
7414
7499
  .filter(([, v]) => v != null)
@@ -7419,7 +7504,7 @@
7419
7504
  }
7420
7505
  } else if (key === 'class') {
7421
7506
  // Handle class as array or string
7422
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
7507
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
7423
7508
  if (classStr) {
7424
7509
  attrStr += ` class="${bw.escapeHTML(classStr)}"`;
7425
7510
  }
@@ -7455,13 +7540,184 @@
7455
7540
  // Process content recursively
7456
7541
  let contentStr = content != null ? bw.html(content, options) : '';
7457
7542
  // Resolve template bindings in content if state provided
7458
- if (options.state && typeof contentStr === 'string' && contentStr.indexOf('${') >= 0) {
7543
+ if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
7459
7544
  contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
7460
7545
  }
7461
7546
 
7462
7547
  return `<${tag}${attrStr}>${contentStr}</${tag}>`;
7463
7548
  };
7464
7549
 
7550
+ /**
7551
+ * Generate a complete, self-contained HTML document from TACO content.
7552
+ *
7553
+ * Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
7554
+ * func registry emission (so serialized event handlers work), optional theme,
7555
+ * and extra head elements. Designed for static site generation, offline/airgapped
7556
+ * use, and the "static site that isn't static" workflow.
7557
+ *
7558
+ * @param {Object} [opts={}] - Page options
7559
+ * @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
7560
+ * @param {string} [opts.title='bitwrench'] - Page title
7561
+ * @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
7562
+ * @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
7563
+ * @param {string} [opts.css=''] - Additional CSS for <style> block
7564
+ * @param {string|Object} [opts.theme=null] - Theme preset name or config object
7565
+ * @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
7566
+ * @param {string} [opts.favicon=''] - Favicon URL
7567
+ * @param {string} [opts.lang='en'] - HTML lang attribute
7568
+ * @returns {string} Complete HTML document string
7569
+ * @category DOM Generation
7570
+ * @see bw.html
7571
+ * @example
7572
+ * bw.htmlPage({
7573
+ * title: 'My App',
7574
+ * body: { t: 'h1', c: 'Hello World' },
7575
+ * runtime: 'shim'
7576
+ * })
7577
+ */
7578
+ bw.htmlPage = function(opts) {
7579
+ opts = opts || {};
7580
+ var title = opts.title || 'bitwrench';
7581
+ var body = opts.body || '';
7582
+ var state = opts.state || undefined;
7583
+ var runtime = opts.runtime || 'shim';
7584
+ var css = opts.css || '';
7585
+ var theme = opts.theme || null;
7586
+ var headExtra = opts.head || [];
7587
+ var favicon = opts.favicon || '';
7588
+ var lang = opts.lang || 'en';
7589
+
7590
+ // Snapshot funcRegistry counter before rendering
7591
+ var fnCounterBefore = bw._fnIDCounter;
7592
+
7593
+ // Render body content
7594
+ var bodyHTML = '';
7595
+ if (_is(body, 'string')) {
7596
+ bodyHTML = body;
7597
+ } else {
7598
+ var htmlOpts = {};
7599
+ if (state) htmlOpts.state = state;
7600
+ bodyHTML = bw.html(body, htmlOpts);
7601
+ }
7602
+
7603
+ // Collect functions registered during this render
7604
+ var fnCounterAfter = bw._fnIDCounter;
7605
+ var registryEntries = '';
7606
+ for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
7607
+ var fnKey = 'bw_fn_' + i;
7608
+ if (bw._fnRegistry[fnKey]) {
7609
+ registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' +
7610
+ bw._fnRegistry[fnKey].toString() + ';\n';
7611
+ }
7612
+ }
7613
+
7614
+ // Build runtime script for <head>
7615
+ var runtimeHead = '';
7616
+ if (runtime === 'inline') {
7617
+ // Read UMD bundle synchronously if in Node.js
7618
+ var umdSource = null;
7619
+ if (bw._isNode) {
7620
+ try {
7621
+ var fs = (typeof require === 'function') ? require('fs') : null;
7622
+ var pathMod = (typeof require === 'function') ? require('path') : null;
7623
+ if (fs && pathMod) {
7624
+ // Resolve dist/ relative to this source file
7625
+ var srcDir = '';
7626
+ try { srcDir = pathMod.dirname((typeof __filename !== 'undefined') ? __filename : ''); }
7627
+ catch(e2) { /* ESM: __filename not available */ }
7628
+ if (!srcDir && typeof ({ url: (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench.umd.js', document.baseURI).href)) }) !== 'undefined' && (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench.umd.js', document.baseURI).href))) {
7629
+ var url = (typeof require === 'function') ? require('url') : null;
7630
+ if (url && url.fileURLToPath) srcDir = pathMod.dirname(url.fileURLToPath((typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench.umd.js', document.baseURI).href))));
7631
+ }
7632
+ if (srcDir) {
7633
+ var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
7634
+ umdSource = fs.readFileSync(distPath, 'utf8');
7635
+ }
7636
+ }
7637
+ } catch(e) { /* fall through */ }
7638
+ }
7639
+ if (umdSource) {
7640
+ runtimeHead = '<script>' + umdSource + '</script>';
7641
+ } else {
7642
+ // Fallback to shim in browser or if dist not available
7643
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
7644
+ }
7645
+ } else if (runtime === 'cdn') {
7646
+ runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
7647
+ } else if (runtime === 'shim') {
7648
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
7649
+ }
7650
+ // runtime === 'none' → empty
7651
+
7652
+ // Theme CSS
7653
+ var themeCSS = '';
7654
+ if (theme) {
7655
+ var themeConfig = _is(theme, 'string')
7656
+ ? (THEME_PRESETS[theme.toLowerCase()] || null)
7657
+ : theme;
7658
+ if (themeConfig) {
7659
+ var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, { inject: false }));
7660
+ themeCSS = themeResult.css;
7661
+ }
7662
+ }
7663
+
7664
+ // Extra <head> elements
7665
+ var headHTML = '';
7666
+ if (_isA(headExtra) && headExtra.length > 0) {
7667
+ headHTML = headExtra.map(function(el) { return bw.html(el); }).join('\n');
7668
+ }
7669
+
7670
+ // Favicon
7671
+ var faviconTag = '';
7672
+ if (favicon) {
7673
+ var safeFavicon = favicon.replace(/[&<>"']/g, function(c) {
7674
+ return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
7675
+ });
7676
+ faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
7677
+ }
7678
+
7679
+ // Escaped title
7680
+ var safeTitle = bw.escapeHTML(title);
7681
+
7682
+ // Combine all CSS
7683
+ var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
7684
+
7685
+ // Body-end script: registry entries + optional loadDefaultStyles
7686
+ var bodyEndScript = '';
7687
+ var bodyEndParts = [];
7688
+ if (registryEntries) {
7689
+ bodyEndParts.push(registryEntries);
7690
+ }
7691
+ if (runtime === 'inline' || runtime === 'cdn') {
7692
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
7693
+ }
7694
+ if (bodyEndParts.length > 0) {
7695
+ bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
7696
+ }
7697
+
7698
+ // Assemble document
7699
+ var parts = [
7700
+ '<!DOCTYPE html>',
7701
+ '<html lang="' + lang + '">',
7702
+ '<head>',
7703
+ '<meta charset="UTF-8">',
7704
+ '<meta name="viewport" content="width=device-width, initial-scale=1">'
7705
+ ];
7706
+ parts.push('<title>' + safeTitle + '</title>');
7707
+ if (faviconTag) parts.push(faviconTag);
7708
+ if (runtimeHead) parts.push(runtimeHead);
7709
+ if (headHTML) parts.push(headHTML);
7710
+ if (allCSS) parts.push('<style>' + allCSS + '</style>');
7711
+ parts.push('</head>');
7712
+ parts.push('<body>');
7713
+ parts.push(bodyHTML);
7714
+ if (bodyEndScript) parts.push(bodyEndScript);
7715
+ parts.push('</body>');
7716
+ parts.push('</html>');
7717
+
7718
+ return parts.join('\n');
7719
+ };
7720
+
7465
7721
  /**
7466
7722
  * Create a live DOM element from a TACO object (browser only).
7467
7723
  *
@@ -7506,7 +7762,7 @@
7506
7762
  }
7507
7763
 
7508
7764
  // Handle text nodes
7509
- if (typeof taco !== 'object' || !taco.t) {
7765
+ if (!_is(taco, 'object') || !taco.t) {
7510
7766
  return document.createTextNode(String(taco));
7511
7767
  }
7512
7768
 
@@ -7519,16 +7775,16 @@
7519
7775
  for (const [key, value] of Object.entries(attrs)) {
7520
7776
  if (value == null || value === false) continue;
7521
7777
 
7522
- if (key === 'style' && typeof value === 'object') {
7778
+ if (key === 'style' && _is(value, 'object')) {
7523
7779
  // Apply styles directly
7524
7780
  Object.assign(el.style, value);
7525
7781
  } else if (key === 'class') {
7526
7782
  // Handle class as array or string
7527
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
7783
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
7528
7784
  if (classStr) {
7529
7785
  el.className = classStr;
7530
7786
  }
7531
- } else if (key.startsWith('on') && typeof value === 'function') {
7787
+ } else if (key.startsWith('on') && _is(value, 'function')) {
7532
7788
  // Event handlers
7533
7789
  const eventName = key.slice(2).toLowerCase();
7534
7790
  el.addEventListener(eventName, value);
@@ -7548,7 +7804,7 @@
7548
7804
  // Children with data-bw_id or id attributes get local refs on the parent,
7549
7805
  // so o.render functions can access them without any DOM lookup.
7550
7806
  if (content != null) {
7551
- if (Array.isArray(content)) {
7807
+ if (_isA(content)) {
7552
7808
  content.forEach(child => {
7553
7809
  if (child != null) {
7554
7810
  // Handle ComponentHandle in content arrays (Level 2 children)
@@ -7568,20 +7824,20 @@
7568
7824
  if (childEl._bw_refs) {
7569
7825
  if (!el._bw_refs) el._bw_refs = {};
7570
7826
  for (var rk in childEl._bw_refs) {
7571
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
7827
+ if (_hop.call(childEl._bw_refs, rk)) {
7572
7828
  el._bw_refs[rk] = childEl._bw_refs[rk];
7573
7829
  }
7574
7830
  }
7575
7831
  }
7576
7832
  }
7577
7833
  });
7578
- } else if (typeof content === 'object' && content.__bw_raw) {
7834
+ } else if (_is(content, 'object') && content.__bw_raw) {
7579
7835
  // Raw HTML content — inject via innerHTML
7580
7836
  el.innerHTML = content.v;
7581
7837
  } else if (content._bwComponent === true) {
7582
7838
  // Single ComponentHandle as content
7583
7839
  content.mount(el);
7584
- } else if (typeof content === 'object' && content.t) {
7840
+ } else if (_is(content, 'object') && content.t) {
7585
7841
  var childEl = bw.createDOM(content, options);
7586
7842
  el.appendChild(childEl);
7587
7843
  var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
@@ -7592,7 +7848,7 @@
7592
7848
  if (childEl._bw_refs) {
7593
7849
  if (!el._bw_refs) el._bw_refs = {};
7594
7850
  for (var rk in childEl._bw_refs) {
7595
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
7851
+ if (_hop.call(childEl._bw_refs, rk)) {
7596
7852
  el._bw_refs[rk] = childEl._bw_refs[rk];
7597
7853
  }
7598
7854
  }
@@ -7625,7 +7881,7 @@
7625
7881
  el._bw_render = opts.render;
7626
7882
 
7627
7883
  if (opts.mounted) {
7628
- console.warn('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
7884
+ _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
7629
7885
  }
7630
7886
 
7631
7887
  // Queue initial render (same timing as mounted)
@@ -7698,7 +7954,7 @@
7698
7954
  const targetEl = bw._el(target);
7699
7955
 
7700
7956
  if (!targetEl) {
7701
- console.error('bw.DOM: Target element not found:', target);
7957
+ _ce('bw.DOM: Target element not found:', target);
7702
7958
  return null;
7703
7959
  }
7704
7960
 
@@ -7738,7 +7994,7 @@
7738
7994
  targetEl.appendChild(taco.element);
7739
7995
  }
7740
7996
  // Handle arrays
7741
- else if (Array.isArray(taco)) {
7997
+ else if (_isA(taco)) {
7742
7998
  taco.forEach(t => {
7743
7999
  if (t != null) {
7744
8000
  if (t._bwComponent === true) {
@@ -7774,7 +8030,7 @@
7774
8030
  bw.compileProps = function(handle, props = {}) {
7775
8031
  const compiledProps = {};
7776
8032
 
7777
- Object.keys(props).forEach(key => {
8033
+ _keys(props).forEach(key => {
7778
8034
  // Create getter/setter for each prop
7779
8035
  Object.defineProperty(compiledProps, key, {
7780
8036
  get() {
@@ -8092,17 +8348,17 @@
8092
8348
  if (attr) {
8093
8349
  // Patch an attribute
8094
8350
  el.setAttribute(attr, String(content));
8095
- } else if (Array.isArray(content)) {
8351
+ } else if (_isA(content)) {
8096
8352
  // Patch with array of children (strings and/or TACOs)
8097
8353
  el.innerHTML = '';
8098
8354
  content.forEach(function(item) {
8099
- if (typeof item === 'string' || typeof item === 'number') {
8355
+ if (_is(item, 'string') || _is(item, 'number')) {
8100
8356
  el.appendChild(document.createTextNode(String(item)));
8101
8357
  } else if (item && item.t) {
8102
8358
  el.appendChild(bw.createDOM(item));
8103
8359
  }
8104
8360
  });
8105
- } else if (typeof content === 'object' && content !== null && content.t) {
8361
+ } else if (_is(content, 'object') && content.t) {
8106
8362
  // Patch with a TACO — replace children
8107
8363
  el.innerHTML = '';
8108
8364
  el.appendChild(bw.createDOM(content));
@@ -8133,7 +8389,7 @@
8133
8389
  bw.patchAll = function(patches) {
8134
8390
  var results = {};
8135
8391
  for (var id in patches) {
8136
- if (Object.prototype.hasOwnProperty.call(patches, id)) {
8392
+ if (_hop.call(patches, id)) {
8137
8393
  results[id] = bw.patch(id, patches[id]);
8138
8394
  }
8139
8395
  }
@@ -8230,7 +8486,7 @@
8230
8486
  snapshot[i].handler(detail);
8231
8487
  called++;
8232
8488
  } catch (err) {
8233
- console.warn('bw.pub: subscriber error on topic "' + topic + '":', err);
8489
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
8234
8490
  }
8235
8491
  }
8236
8492
  return called;
@@ -8326,8 +8582,8 @@
8326
8582
  * @see bw.funcGetDispatchStr
8327
8583
  */
8328
8584
  bw.funcRegister = function(fn, name) {
8329
- if (typeof fn !== 'function') return '';
8330
- var fnID = (typeof name === 'string' && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
8585
+ if (!_is(fn, 'function')) return '';
8586
+ var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
8331
8587
  bw._fnRegistry[fnID] = fn;
8332
8588
  return fnID;
8333
8589
  };
@@ -8346,7 +8602,7 @@
8346
8602
  bw.funcGetById = function(name, errFn) {
8347
8603
  name = String(name);
8348
8604
  if (name in bw._fnRegistry) return bw._fnRegistry[name];
8349
- return (typeof errFn === 'function') ? errFn : function() { console.warn('bw.funcGetById: unregistered fn "' + name + '"'); };
8605
+ return _is(errFn, 'function') ? errFn : function() { _cw('bw.funcGetById: unregistered fn "' + name + '"'); };
8350
8606
  };
8351
8607
 
8352
8608
  /**
@@ -8387,13 +8643,30 @@
8387
8643
  bw.funcGetRegistry = function() {
8388
8644
  var copy = {};
8389
8645
  for (var k in bw._fnRegistry) {
8390
- if (Object.prototype.hasOwnProperty.call(bw._fnRegistry, k)) {
8646
+ if (_hop.call(bw._fnRegistry, k)) {
8391
8647
  copy[k] = bw._fnRegistry[k];
8392
8648
  }
8393
8649
  }
8394
8650
  return copy;
8395
8651
  };
8396
8652
 
8653
+ /**
8654
+ * Minimal runtime shim for funcRegister dispatch in static HTML.
8655
+ * When embedded in a `<script>` tag, provides just enough infrastructure
8656
+ * for `bw.funcGetById()` calls to resolve. The actual function bodies
8657
+ * are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
8658
+ * @type {string}
8659
+ * @category Function Registry
8660
+ */
8661
+ bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' +
8662
+ 'if(!bw._fnRegistry)bw._fnRegistry={};' +
8663
+ 'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' +
8664
+ 'console.warn("bw: unregistered fn "+n)};};' +
8665
+ 'bw.funcRegister=function(fn,name){' +
8666
+ 'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' +
8667
+ 'bw._fnRegistry[id]=fn;return id;};' +
8668
+ 'window.bw=bw;})();';
8669
+
8397
8670
  // ===================================================================================
8398
8671
  // Template Binding Utilities
8399
8672
  // ===================================================================================
@@ -8421,7 +8694,10 @@
8421
8694
  var parts = path.split('.');
8422
8695
  var val = state;
8423
8696
  for (var i = 0; i < parts.length; i++) {
8424
- if (val == null) return '';
8697
+ if (val == null) {
8698
+ if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
8699
+ return '';
8700
+ }
8425
8701
  val = val[parts[i]];
8426
8702
  }
8427
8703
  return (val == null) ? '' : val;
@@ -8441,7 +8717,7 @@
8441
8717
  */
8442
8718
  bw._compiledExprs = {};
8443
8719
  bw._resolveTemplate = function(str, state, compile) {
8444
- if (typeof str !== 'string' || str.indexOf('${') < 0) return str;
8720
+ if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
8445
8721
  var bindings = bw._parseBindings(str);
8446
8722
  if (bindings.length === 0) return str;
8447
8723
 
@@ -8463,6 +8739,7 @@
8463
8739
  try {
8464
8740
  val = bw._compiledExprs[b.expr](state);
8465
8741
  } catch (e) {
8742
+ if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
8466
8743
  val = '';
8467
8744
  }
8468
8745
  } else {
@@ -8571,7 +8848,7 @@
8571
8848
  this._state = {};
8572
8849
  if (o.state) {
8573
8850
  for (var k in o.state) {
8574
- if (Object.prototype.hasOwnProperty.call(o.state, k)) {
8851
+ if (_hop.call(o.state, k)) {
8575
8852
  this._state[k] = o.state[k];
8576
8853
  }
8577
8854
  }
@@ -8580,7 +8857,7 @@
8580
8857
  this._actions = {};
8581
8858
  if (o.actions) {
8582
8859
  for (var k2 in o.actions) {
8583
- if (Object.prototype.hasOwnProperty.call(o.actions, k2)) {
8860
+ if (_hop.call(o.actions, k2)) {
8584
8861
  this._actions[k2] = o.actions[k2];
8585
8862
  }
8586
8863
  }
@@ -8590,7 +8867,7 @@
8590
8867
  if (o.methods) {
8591
8868
  var self = this;
8592
8869
  for (var k3 in o.methods) {
8593
- if (Object.prototype.hasOwnProperty.call(o.methods, k3)) {
8870
+ if (_hop.call(o.methods, k3)) {
8594
8871
  this._methods[k3] = o.methods[k3];
8595
8872
  (function(methodName, methodFn) {
8596
8873
  self[methodName] = function() {
@@ -8623,14 +8900,23 @@
8623
8900
  this._compile = !!o.compile;
8624
8901
  this._bw_refs = {};
8625
8902
  this._refCounter = 0;
8903
+ // Child component ownership (Bug #5)
8904
+ this._children = [];
8905
+ this._parent = null;
8906
+ // Factory metadata for BCCL rebuild (Bug #6)
8907
+ this._factory = taco._bwFactory || null;
8626
8908
  }
8627
8909
 
8910
+ // Short alias for ComponentHandle.prototype (see alias block at top of file).
8911
+ // 28 method definitions × 25 chars = ~700B raw savings in minified output.
8912
+ var _chp = ComponentHandle.prototype;
8913
+
8628
8914
  // ── State Methods ──
8629
8915
 
8630
8916
  /**
8631
8917
  * Get a state value. Dot-path supported: `get('user.name')`
8632
8918
  */
8633
- ComponentHandle.prototype.get = function(key) {
8919
+ _chp.get = function(key) {
8634
8920
  return bw._evaluatePath(this._state, key);
8635
8921
  };
8636
8922
 
@@ -8640,12 +8926,13 @@
8640
8926
  * @param {*} value - New value
8641
8927
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
8642
8928
  */
8643
- ComponentHandle.prototype.set = function(key, value, opts) {
8929
+ _chp.set = function(key, value, opts) {
8644
8930
  // Dot-path set
8645
8931
  var parts = key.split('.');
8646
8932
  var obj = this._state;
8647
8933
  for (var i = 0; i < parts.length - 1; i++) {
8648
- if (obj[parts[i]] == null || typeof obj[parts[i]] !== 'object') {
8934
+ if (!_is(obj[parts[i]], 'object')) {
8935
+ if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
8649
8936
  obj[parts[i]] = {};
8650
8937
  }
8651
8938
  obj = obj[parts[i]];
@@ -8665,10 +8952,10 @@
8665
8952
  /**
8666
8953
  * Get a shallow clone of the full state.
8667
8954
  */
8668
- ComponentHandle.prototype.getState = function() {
8955
+ _chp.getState = function() {
8669
8956
  var clone = {};
8670
8957
  for (var k in this._state) {
8671
- if (Object.prototype.hasOwnProperty.call(this._state, k)) {
8958
+ if (_hop.call(this._state, k)) {
8672
8959
  clone[k] = this._state[k];
8673
8960
  }
8674
8961
  }
@@ -8680,9 +8967,9 @@
8680
8967
  * @param {Object} updates - Key-value pairs to merge
8681
8968
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
8682
8969
  */
8683
- ComponentHandle.prototype.setState = function(updates, opts) {
8970
+ _chp.setState = function(updates, opts) {
8684
8971
  for (var k in updates) {
8685
- if (Object.prototype.hasOwnProperty.call(updates, k)) {
8972
+ if (_hop.call(updates, k)) {
8686
8973
  this._state[k] = updates[k];
8687
8974
  this._dirtyKeys[k] = true;
8688
8975
  }
@@ -8699,9 +8986,9 @@
8699
8986
  /**
8700
8987
  * Push a value onto an array in state. Clones the array.
8701
8988
  */
8702
- ComponentHandle.prototype.push = function(key, val) {
8989
+ _chp.push = function(key, val) {
8703
8990
  var arr = this.get(key);
8704
- var newArr = Array.isArray(arr) ? arr.slice() : [];
8991
+ var newArr = _isA(arr) ? arr.slice() : [];
8705
8992
  newArr.push(val);
8706
8993
  this.set(key, newArr);
8707
8994
  };
@@ -8709,9 +8996,9 @@
8709
8996
  /**
8710
8997
  * Splice an array in state. Clones the array.
8711
8998
  */
8712
- ComponentHandle.prototype.splice = function(key, start, deleteCount) {
8999
+ _chp.splice = function(key, start, deleteCount) {
8713
9000
  var arr = this.get(key);
8714
- var newArr = Array.isArray(arr) ? arr.slice() : [];
9001
+ var newArr = _isA(arr) ? arr.slice() : [];
8715
9002
  var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
8716
9003
  Array.prototype.splice.apply(newArr, args);
8717
9004
  this.set(key, newArr);
@@ -8719,7 +9006,7 @@
8719
9006
 
8720
9007
  // ── Scheduling ──
8721
9008
 
8722
- ComponentHandle.prototype._scheduleDirty = function() {
9009
+ _chp._scheduleDirty = function() {
8723
9010
  if (!this._scheduled) {
8724
9011
  this._scheduled = true;
8725
9012
  bw._dirtyComponents.push(this);
@@ -8734,17 +9021,17 @@
8734
9021
  * Creates binding descriptors with refIds for targeted DOM updates.
8735
9022
  * @private
8736
9023
  */
8737
- ComponentHandle.prototype._compileBindings = function() {
9024
+ _chp._compileBindings = function() {
8738
9025
  this._bindings = [];
8739
9026
  this._refCounter = 0;
8740
- var stateKeys = Object.keys(this._state);
9027
+ var stateKeys = _keys(this._state);
8741
9028
  var self = this;
8742
9029
 
8743
9030
  function walkTaco(taco, path) {
8744
- if (taco == null || typeof taco !== 'object' || !taco.t) return taco;
9031
+ if (!_is(taco, 'object') || !taco.t) return taco;
8745
9032
 
8746
9033
  // Check content for bindings
8747
- if (typeof taco.c === 'string' && taco.c.indexOf('${') >= 0) {
9034
+ if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
8748
9035
  var refId = 'bw_ref_' + self._refCounter++;
8749
9036
  var parsed = bw._parseBindings(taco.c);
8750
9037
  var deps = [];
@@ -8766,10 +9053,10 @@
8766
9053
  // Check attributes for bindings
8767
9054
  if (taco.a) {
8768
9055
  for (var attrName in taco.a) {
8769
- if (!Object.prototype.hasOwnProperty.call(taco.a, attrName)) continue;
9056
+ if (!_hop.call(taco.a, attrName)) continue;
8770
9057
  if (attrName === 'data-bw_ref') continue;
8771
9058
  var attrVal = taco.a[attrName];
8772
- if (typeof attrVal === 'string' && attrVal.indexOf('${') >= 0) {
9059
+ if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
8773
9060
  var refId2 = 'bw_ref_' + self._refCounter++;
8774
9061
  var parsed2 = bw._parseBindings(attrVal);
8775
9062
  var deps2 = [];
@@ -8795,9 +9082,27 @@
8795
9082
  }
8796
9083
 
8797
9084
  // Recurse into children
8798
- if (Array.isArray(taco.c)) {
9085
+ if (_isA(taco.c)) {
8799
9086
  for (var i = 0; i < taco.c.length; i++) {
8800
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
9087
+ // Wrap string children with ${expr} in a span so patches target the span, not the parent
9088
+ if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
9089
+ var mixedRefId = 'bw_ref_' + self._refCounter++;
9090
+ var mixedParsed = bw._parseBindings(taco.c[i]);
9091
+ var mixedDeps = [];
9092
+ for (var mi = 0; mi < mixedParsed.length; mi++) {
9093
+ mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
9094
+ }
9095
+ self._bindings.push({
9096
+ expr: taco.c[i],
9097
+ type: 'content',
9098
+ refId: mixedRefId,
9099
+ deps: mixedDeps,
9100
+ template: taco.c[i]
9101
+ });
9102
+ // Replace string with a span wrapper so textContent targets the span only
9103
+ taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
9104
+ }
9105
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
8801
9106
  walkTaco(taco.c[i], path.concat(i));
8802
9107
  }
8803
9108
  // Handle bw.when/bw.each markers
@@ -8832,7 +9137,7 @@
8832
9137
  taco.c[i]._refId = eachRefId;
8833
9138
  }
8834
9139
  }
8835
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9140
+ } else if (_is(taco.c, 'object') && taco.c.t) {
8836
9141
  walkTaco(taco.c, path.concat(0));
8837
9142
  }
8838
9143
 
@@ -8848,7 +9153,7 @@
8848
9153
  * Build ref map from the live DOM after createDOM.
8849
9154
  * @private
8850
9155
  */
8851
- ComponentHandle.prototype._collectRefs = function() {
9156
+ _chp._collectRefs = function() {
8852
9157
  this._bw_refs = {};
8853
9158
  if (!this.element) return;
8854
9159
  var els = this.element.querySelectorAll('[data-bw_ref]');
@@ -8869,7 +9174,7 @@
8869
9174
  * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
8870
9175
  * @param {Element} parentEl - DOM element to mount into
8871
9176
  */
8872
- ComponentHandle.prototype.mount = function(parentEl) {
9177
+ _chp.mount = function(parentEl) {
8873
9178
  // willMount hook
8874
9179
  if (this._hooks.willMount) this._hooks.willMount(this);
8875
9180
 
@@ -8891,7 +9196,7 @@
8891
9196
  // Register named actions in function registry
8892
9197
  var self = this;
8893
9198
  for (var actionName in this._actions) {
8894
- if (Object.prototype.hasOwnProperty.call(this._actions, actionName)) {
9199
+ if (_hop.call(this._actions, actionName)) {
8895
9200
  var registeredName = this._bwId + '_' + actionName;
8896
9201
  (function(aName) {
8897
9202
  bw.funcRegister(function(evt) {
@@ -8910,6 +9215,11 @@
8910
9215
  this.element = bw.createDOM(tacoForDOM);
8911
9216
  this.element._bwComponentHandle = this;
8912
9217
  this.element.setAttribute('data-bw_comp_id', this._bwId);
9218
+
9219
+ // Restore o.render from original TACO (stripped by _tacoForDOM)
9220
+ if (this.taco.o && this.taco.o.render) {
9221
+ this.element._bw_render = this.taco.o.render;
9222
+ }
8913
9223
  if (this._userTag) {
8914
9224
  this.element.classList.add(this._userTag);
8915
9225
  }
@@ -8925,6 +9235,16 @@
8925
9235
 
8926
9236
  this.mounted = true;
8927
9237
 
9238
+ // Scan for child ComponentHandles and link parent/child (Bug #5)
9239
+ var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
9240
+ for (var ci = 0; ci < childEls.length; ci++) {
9241
+ var ch = childEls[ci]._bwComponentHandle;
9242
+ if (ch && ch !== this && !ch._parent) {
9243
+ ch._parent = this;
9244
+ this._children.push(ch);
9245
+ }
9246
+ }
9247
+
8928
9248
  // mounted hook (backward compat: fn.length === 2 wraps (el, state))
8929
9249
  if (this._hooks.mounted) {
8930
9250
  if (this._hooks.mounted.length === 2) {
@@ -8933,16 +9253,21 @@
8933
9253
  this._hooks.mounted(this);
8934
9254
  }
8935
9255
  }
9256
+
9257
+ // Invoke o.render on initial mount (if present)
9258
+ if (this.element._bw_render) {
9259
+ this.element._bw_render(this.element, this._state);
9260
+ }
8936
9261
  };
8937
9262
 
8938
9263
  /**
8939
9264
  * Prepare TACO for initial render: resolve when/each markers.
8940
9265
  * @private
8941
9266
  */
8942
- ComponentHandle.prototype._prepareTaco = function(taco) {
8943
- if (!taco || typeof taco !== 'object') return;
9267
+ _chp._prepareTaco = function(taco) {
9268
+ if (!_is(taco, 'object')) return;
8944
9269
 
8945
- if (Array.isArray(taco.c)) {
9270
+ if (_isA(taco.c)) {
8946
9271
  for (var i = taco.c.length - 1; i >= 0; i--) {
8947
9272
  var child = taco.c[i];
8948
9273
  if (child && child._bwWhen) {
@@ -8967,18 +9292,18 @@
8967
9292
  var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
8968
9293
  var arr = bw._evaluatePath(this._state, eachExprStr);
8969
9294
  var items = [];
8970
- if (Array.isArray(arr)) {
9295
+ if (_isA(arr)) {
8971
9296
  for (var j = 0; j < arr.length; j++) {
8972
9297
  items.push(child.factory(arr[j], j));
8973
9298
  }
8974
9299
  }
8975
9300
  taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
8976
9301
  }
8977
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
9302
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
8978
9303
  this._prepareTaco(taco.c[i]);
8979
9304
  }
8980
9305
  }
8981
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9306
+ } else if (_is(taco.c, 'object') && taco.c.t) {
8982
9307
  this._prepareTaco(taco.c);
8983
9308
  }
8984
9309
  };
@@ -8987,12 +9312,12 @@
8987
9312
  * Wire action name strings (in onclick etc.) to dispatch function calls.
8988
9313
  * @private
8989
9314
  */
8990
- ComponentHandle.prototype._wireActions = function(taco) {
8991
- if (!taco || typeof taco !== 'object' || !taco.t) return;
9315
+ _chp._wireActions = function(taco) {
9316
+ if (!_is(taco, 'object') || !taco.t) return;
8992
9317
  if (taco.a) {
8993
9318
  for (var key in taco.a) {
8994
- if (!Object.prototype.hasOwnProperty.call(taco.a, key)) continue;
8995
- if (key.startsWith('on') && typeof taco.a[key] === 'string') {
9319
+ if (!_hop.call(taco.a, key)) continue;
9320
+ if (key.startsWith('on') && _is(taco.a[key], 'string')) {
8996
9321
  var actionName = taco.a[key];
8997
9322
  if (actionName in this._actions) {
8998
9323
  var registeredName = this._bwId + '_' + actionName;
@@ -9006,11 +9331,11 @@
9006
9331
  }
9007
9332
  }
9008
9333
  }
9009
- if (Array.isArray(taco.c)) {
9334
+ if (_isA(taco.c)) {
9010
9335
  for (var i = 0; i < taco.c.length; i++) {
9011
9336
  this._wireActions(taco.c[i]);
9012
9337
  }
9013
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9338
+ } else if (_is(taco.c, 'object') && taco.c.t) {
9014
9339
  this._wireActions(taco.c);
9015
9340
  }
9016
9341
  };
@@ -9019,7 +9344,7 @@
9019
9344
  * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
9020
9345
  * @private
9021
9346
  */
9022
- ComponentHandle.prototype._deepCloneTaco = function(taco) {
9347
+ _chp._deepCloneTaco = function(taco) {
9023
9348
  if (taco == null) return taco;
9024
9349
  // Preserve _bwWhen / _bwEach markers (contain functions)
9025
9350
  if (taco._bwWhen) {
@@ -9031,18 +9356,18 @@
9031
9356
  if (taco._bwEach) {
9032
9357
  return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
9033
9358
  }
9034
- if (typeof taco !== 'object' || !taco.t) return taco;
9359
+ if (!_is(taco, 'object') || !taco.t) return taco;
9035
9360
  var result = { t: taco.t };
9036
9361
  if (taco.a) {
9037
9362
  result.a = {};
9038
9363
  for (var k in taco.a) {
9039
- if (Object.prototype.hasOwnProperty.call(taco.a, k)) result.a[k] = taco.a[k];
9364
+ if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
9040
9365
  }
9041
9366
  }
9042
9367
  if (taco.c != null) {
9043
- if (Array.isArray(taco.c)) {
9368
+ if (_isA(taco.c)) {
9044
9369
  result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
9045
- } else if (typeof taco.c === 'object') {
9370
+ } else if (_is(taco.c, 'object')) {
9046
9371
  result.c = this._deepCloneTaco(taco.c);
9047
9372
  } else {
9048
9373
  result.c = taco.c;
@@ -9056,27 +9381,31 @@
9056
9381
  * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
9057
9382
  * @private
9058
9383
  */
9059
- ComponentHandle.prototype._tacoForDOM = function(taco) {
9060
- if (!taco || typeof taco !== 'object' || !taco.t) return taco;
9384
+ _chp._tacoForDOM = function(taco) {
9385
+ if (!_is(taco, 'object') || !taco.t) return taco;
9061
9386
  var result = { t: taco.t };
9062
9387
  if (taco.a) result.a = taco.a;
9063
9388
  if (taco.c != null) {
9064
- if (Array.isArray(taco.c)) {
9389
+ if (_isA(taco.c)) {
9065
9390
  result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
9066
- } else if (typeof taco.c === 'object' && taco.c.t) {
9391
+ } else if (_is(taco.c, 'object') && taco.c.t) {
9067
9392
  result.c = this._tacoForDOM(taco.c);
9068
9393
  } else {
9069
9394
  result.c = taco.c;
9070
9395
  }
9071
9396
  }
9072
9397
  // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
9398
+ if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
9399
+ _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
9400
+ '>. Use onclick attribute or bw.component() for child interactivity.');
9401
+ }
9073
9402
  return result;
9074
9403
  };
9075
9404
 
9076
9405
  /**
9077
9406
  * Unmount: remove from DOM, deactivate, preserve state for re-mount.
9078
9407
  */
9079
- ComponentHandle.prototype.unmount = function() {
9408
+ _chp.unmount = function() {
9080
9409
  if (!this.mounted) return;
9081
9410
 
9082
9411
  // unmount hook
@@ -9111,12 +9440,23 @@
9111
9440
  /**
9112
9441
  * Destroy: unmount + clear state + unregister actions.
9113
9442
  */
9114
- ComponentHandle.prototype.destroy = function() {
9443
+ _chp.destroy = function() {
9115
9444
  // willDestroy hook
9116
9445
  if (this._hooks.willDestroy) {
9117
9446
  this._hooks.willDestroy(this);
9118
9447
  }
9119
9448
 
9449
+ // Cascade destroy to children depth-first (Bug #5)
9450
+ for (var ci = this._children.length - 1; ci >= 0; ci--) {
9451
+ this._children[ci].destroy();
9452
+ }
9453
+ this._children = [];
9454
+ if (this._parent) {
9455
+ var idx = this._parent._children.indexOf(this);
9456
+ if (idx >= 0) this._parent._children.splice(idx, 1);
9457
+ this._parent = null;
9458
+ }
9459
+
9120
9460
  this.unmount();
9121
9461
 
9122
9462
  // Unregister actions from function registry
@@ -9143,12 +9483,36 @@
9143
9483
  * Flush dirty state: resolve changed bindings and apply to DOM.
9144
9484
  * @private
9145
9485
  */
9146
- ComponentHandle.prototype._flush = function() {
9486
+ _chp._flush = function() {
9147
9487
  this._scheduled = false;
9148
- var changedKeys = Object.keys(this._dirtyKeys);
9488
+ var changedKeys = _keys(this._dirtyKeys);
9149
9489
  this._dirtyKeys = {};
9150
9490
  if (changedKeys.length === 0 || !this.mounted) return;
9151
9491
 
9492
+ // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
9493
+ // rebuild the TACO from the factory with merged state (Bug #6)
9494
+ if (this._factory) {
9495
+ var rebuildNeeded = false;
9496
+ for (var fi = 0; fi < changedKeys.length; fi++) {
9497
+ if (_hop.call(this._factory.props, changedKeys[fi])) {
9498
+ rebuildNeeded = true; break;
9499
+ }
9500
+ }
9501
+ if (rebuildNeeded) {
9502
+ var merged = {};
9503
+ for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
9504
+ for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
9505
+ this._factory.props = merged;
9506
+ var newTaco = bw.make(this._factory.type, merged);
9507
+ newTaco._bwFactory = this._factory;
9508
+ this.taco = newTaco;
9509
+ this._originalTaco = this._deepCloneTaco(newTaco);
9510
+ this._render();
9511
+ if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
9512
+ return;
9513
+ }
9514
+ }
9515
+
9152
9516
  // willUpdate hook
9153
9517
  if (this._hooks.willUpdate) {
9154
9518
  this._hooks.willUpdate(this, changedKeys);
@@ -9187,7 +9551,7 @@
9187
9551
  * Returns list of patches to apply.
9188
9552
  * @private
9189
9553
  */
9190
- ComponentHandle.prototype._resolveBindings = function(changedKeys) {
9554
+ _chp._resolveBindings = function(changedKeys) {
9191
9555
  var patches = [];
9192
9556
  for (var i = 0; i < this._bindings.length; i++) {
9193
9557
  var b = this._bindings[i];
@@ -9223,11 +9587,14 @@
9223
9587
  * Apply patches to DOM.
9224
9588
  * @private
9225
9589
  */
9226
- ComponentHandle.prototype._applyPatches = function(patches) {
9590
+ _chp._applyPatches = function(patches) {
9227
9591
  for (var i = 0; i < patches.length; i++) {
9228
9592
  var p = patches[i];
9229
9593
  var el = this._bw_refs[p.refId];
9230
- if (!el) continue;
9594
+ if (!el) {
9595
+ if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
9596
+ continue;
9597
+ }
9231
9598
  if (p.type === 'content') {
9232
9599
  el.textContent = p.value;
9233
9600
  } else if (p.type === 'attribute') {
@@ -9244,7 +9611,7 @@
9244
9611
  * Resolve all bindings and apply (used for initial render).
9245
9612
  * @private
9246
9613
  */
9247
- ComponentHandle.prototype._resolveAndApplyAll = function() {
9614
+ _chp._resolveAndApplyAll = function() {
9248
9615
  var patches = [];
9249
9616
  for (var i = 0; i < this._bindings.length; i++) {
9250
9617
  var b = this._bindings[i];
@@ -9267,7 +9634,7 @@
9267
9634
  * Full re-render for structural changes (when/each branch switches).
9268
9635
  * @private
9269
9636
  */
9270
- ComponentHandle.prototype._render = function() {
9637
+ _chp._render = function() {
9271
9638
  if (!this.element || !this.element.parentNode) return;
9272
9639
  var parent = this.element.parentNode;
9273
9640
  var nextSibling = this.element.nextSibling;
@@ -9307,7 +9674,7 @@
9307
9674
  * @param {string} event - Event name (e.g., 'click')
9308
9675
  * @param {Function} handler - Event handler
9309
9676
  */
9310
- ComponentHandle.prototype.on = function(event, handler) {
9677
+ _chp.on = function(event, handler) {
9311
9678
  if (this.element) {
9312
9679
  this.element.addEventListener(event, handler);
9313
9680
  }
@@ -9319,7 +9686,7 @@
9319
9686
  * @param {string} event - Event name
9320
9687
  * @param {Function} handler - Handler to remove
9321
9688
  */
9322
- ComponentHandle.prototype.off = function(event, handler) {
9689
+ _chp.off = function(event, handler) {
9323
9690
  if (this.element) {
9324
9691
  this.element.removeEventListener(event, handler);
9325
9692
  }
@@ -9334,7 +9701,7 @@
9334
9701
  * @param {Function} handler - Handler function
9335
9702
  * @returns {Function} Unsubscribe function
9336
9703
  */
9337
- ComponentHandle.prototype.sub = function(topic, handler) {
9704
+ _chp.sub = function(topic, handler) {
9338
9705
  var unsub = bw.sub(topic, handler);
9339
9706
  this._subs.push(unsub);
9340
9707
  return unsub;
@@ -9345,10 +9712,10 @@
9345
9712
  * @param {string} name - Action name
9346
9713
  * @param {...*} args - Arguments passed after comp
9347
9714
  */
9348
- ComponentHandle.prototype.action = function(name) {
9715
+ _chp.action = function(name) {
9349
9716
  var fn = this._actions[name];
9350
9717
  if (!fn) {
9351
- console.warn('ComponentHandle.action: unknown action "' + name + '"');
9718
+ _cw('ComponentHandle.action: unknown action "' + name + '"');
9352
9719
  return;
9353
9720
  }
9354
9721
  var args = [this].concat(Array.prototype.slice.call(arguments, 1));
@@ -9360,7 +9727,7 @@
9360
9727
  * @param {string} sel - CSS selector
9361
9728
  * @returns {Element|null}
9362
9729
  */
9363
- ComponentHandle.prototype.select = function(sel) {
9730
+ _chp.select = function(sel) {
9364
9731
  return this.element ? this.element.querySelector(sel) : null;
9365
9732
  };
9366
9733
 
@@ -9369,7 +9736,7 @@
9369
9736
  * @param {string} sel - CSS selector
9370
9737
  * @returns {Element[]}
9371
9738
  */
9372
- ComponentHandle.prototype.selectAll = function(sel) {
9739
+ _chp.selectAll = function(sel) {
9373
9740
  if (!this.element) return [];
9374
9741
  return Array.prototype.slice.call(this.element.querySelectorAll(sel));
9375
9742
  };
@@ -9380,7 +9747,7 @@
9380
9747
  * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
9381
9748
  * @returns {ComponentHandle} this (for chaining)
9382
9749
  */
9383
- ComponentHandle.prototype.userTag = function(tag) {
9750
+ _chp.userTag = function(tag) {
9384
9751
  this._userTag = tag;
9385
9752
  if (this.element) {
9386
9753
  this.element.classList.add(tag);
@@ -9481,14 +9848,399 @@
9481
9848
  }
9482
9849
  if (!el || !el._bwComponentHandle) return false;
9483
9850
  var comp = el._bwComponentHandle;
9484
- if (typeof comp[action] !== 'function') {
9485
- console.warn('bw.message: unknown action "' + action + '" on component ' + target);
9851
+ if (!_is(comp[action], 'function')) {
9852
+ _cw('bw.message: unknown action "' + action + '" on component ' + target);
9486
9853
  return false;
9487
9854
  }
9488
9855
  comp[action](data);
9489
9856
  return true;
9490
9857
  };
9491
9858
 
9859
+ // ===================================================================================
9860
+ // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
9861
+ // ===================================================================================
9862
+
9863
+ /**
9864
+ * Registry of named functions sent via register messages.
9865
+ * Populated by clientApply({ type: 'register', name, body }).
9866
+ * Invoked by clientApply({ type: 'call', name, args }).
9867
+ * @private
9868
+ */
9869
+ bw._clientFunctions = {};
9870
+
9871
+ /**
9872
+ * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
9873
+ * Default false — exec messages are rejected unless explicitly opted in.
9874
+ * @private
9875
+ */
9876
+ bw._allowExec = false;
9877
+
9878
+ /**
9879
+ * Built-in client functions available via call() without registration.
9880
+ * @private
9881
+ */
9882
+ bw._builtinClientFunctions = {
9883
+ scrollTo: function(selector) {
9884
+ var el = bw._el(selector);
9885
+ if (el) el.scrollTop = el.scrollHeight;
9886
+ },
9887
+ focus: function(selector) {
9888
+ var el = bw._el(selector);
9889
+ if (el && _is(el.focus, 'function')) el.focus();
9890
+ },
9891
+ download: function(filename, content, mimeType) {
9892
+ if (typeof document === 'undefined') return;
9893
+ var blob = new Blob([content], { type: mimeType || 'text/plain' });
9894
+ var a = document.createElement('a');
9895
+ a.href = URL.createObjectURL(blob);
9896
+ a.download = filename;
9897
+ a.click();
9898
+ URL.revokeObjectURL(a.href);
9899
+ },
9900
+ clipboard: function(text) {
9901
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
9902
+ navigator.clipboard.writeText(text);
9903
+ }
9904
+ },
9905
+ redirect: function(url) {
9906
+ if (typeof window !== 'undefined') window.location.href = url;
9907
+ },
9908
+ log: function() {
9909
+ console.log.apply(console, arguments);
9910
+ }
9911
+ };
9912
+
9913
+ /**
9914
+ * Parse a bwserve protocol message string, supporting both strict JSON
9915
+ * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
9916
+ *
9917
+ * The r-prefix format is designed for C/C++ string literals where
9918
+ * double-quote escaping is painful. The parser is a state machine
9919
+ * that walks character by character — not a regex replace.
9920
+ *
9921
+ * Escaping: apostrophes inside single-quoted values must be escaped
9922
+ * with backslash: r{'name':'Barry\'s room'}
9923
+ *
9924
+ * @param {string} str - JSON or r-prefixed relaxed JSON string
9925
+ * @returns {Object} Parsed message object
9926
+ * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
9927
+ * @category Server
9928
+ */
9929
+ bw.clientParse = function(str) {
9930
+ str = (str || '').trim();
9931
+ if (str.charAt(0) !== 'r') return JSON.parse(str);
9932
+ str = str.slice(1);
9933
+
9934
+ var out = [];
9935
+ var i = 0;
9936
+ var len = str.length;
9937
+
9938
+ while (i < len) {
9939
+ var ch = str[i];
9940
+
9941
+ if (ch === "'") {
9942
+ // Single-quoted string → emit as double-quoted
9943
+ out.push('"');
9944
+ i++;
9945
+ while (i < len) {
9946
+ var c = str[i];
9947
+ if (c === '\\' && i + 1 < len) {
9948
+ var next = str[i + 1];
9949
+ if (next === "'") {
9950
+ out.push("'"); // \' in input → ' in output
9951
+ } else {
9952
+ out.push('\\');
9953
+ out.push(next);
9954
+ }
9955
+ i += 2;
9956
+ } else if (c === '"') {
9957
+ out.push('\\"');
9958
+ i++;
9959
+ } else if (c === "'") {
9960
+ break;
9961
+ } else {
9962
+ out.push(c);
9963
+ i++;
9964
+ }
9965
+ }
9966
+ out.push('"');
9967
+ i++; // skip closing '
9968
+
9969
+ } else if (ch === '"') {
9970
+ // Double-quoted string — pass through verbatim
9971
+ out.push(ch);
9972
+ i++;
9973
+ while (i < len) {
9974
+ var c2 = str[i];
9975
+ if (c2 === '\\' && i + 1 < len) {
9976
+ out.push(c2);
9977
+ out.push(str[i + 1]);
9978
+ i += 2;
9979
+ } else {
9980
+ out.push(c2);
9981
+ i++;
9982
+ if (c2 === '"') break;
9983
+ }
9984
+ }
9985
+
9986
+ } else if (ch === ',') {
9987
+ // Trailing comma check: skip comma if next non-whitespace is } or ]
9988
+ var j = i + 1;
9989
+ while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) j++;
9990
+ if (j < len && (str[j] === '}' || str[j] === ']')) {
9991
+ i++; // skip trailing comma
9992
+ } else {
9993
+ out.push(ch);
9994
+ i++;
9995
+ }
9996
+
9997
+ } else {
9998
+ out.push(ch);
9999
+ i++;
10000
+ }
10001
+ }
10002
+
10003
+ return JSON.parse(out.join(''));
10004
+ };
10005
+
10006
+ /**
10007
+ * Apply a bwserve protocol message to the DOM.
10008
+ *
10009
+ * Dispatches one of 9 message types:
10010
+ * replace — bw.DOM(target, node)
10011
+ * append — target.appendChild(bw.createDOM(node))
10012
+ * remove — bw.cleanup(target); target.remove()
10013
+ * patch — bw.patch(target, content, attr)
10014
+ * batch — iterate ops, call clientApply for each
10015
+ * message — bw.message(target, action, data)
10016
+ * register — store a named function for later call()
10017
+ * call — invoke a registered or built-in function
10018
+ * exec — execute arbitrary JS (requires allowExec)
10019
+ *
10020
+ * Target resolution:
10021
+ * Starts with '#' or '.' → CSS selector (querySelector)
10022
+ * Otherwise → getElementById, then bw._el fallback
10023
+ *
10024
+ * @param {Object} msg - Protocol message
10025
+ * @returns {boolean} true if the message was applied successfully
10026
+ * @category Server
10027
+ */
10028
+ bw.clientApply = function(msg) {
10029
+ if (!msg || !msg.type) return false;
10030
+
10031
+ var type = msg.type;
10032
+ var target = msg.target;
10033
+
10034
+ if (type === 'replace') {
10035
+ var el = bw._el(target);
10036
+ if (!el) return false;
10037
+ bw.DOM(el, msg.node);
10038
+ return true;
10039
+
10040
+ } else if (type === 'patch') {
10041
+ var patched = bw.patch(target, msg.content, msg.attr);
10042
+ return patched !== null;
10043
+
10044
+ } else if (type === 'append') {
10045
+ var parent = bw._el(target);
10046
+ if (!parent) return false;
10047
+ var child = bw.createDOM(msg.node);
10048
+ parent.appendChild(child);
10049
+ return true;
10050
+
10051
+ } else if (type === 'remove') {
10052
+ var toRemove = bw._el(target);
10053
+ if (!toRemove) return false;
10054
+ if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
10055
+ toRemove.remove();
10056
+ return true;
10057
+
10058
+ } else if (type === 'batch') {
10059
+ if (!_isA(msg.ops)) return false;
10060
+ var allOk = true;
10061
+ msg.ops.forEach(function(op) {
10062
+ if (!bw.clientApply(op)) allOk = false;
10063
+ });
10064
+ return allOk;
10065
+
10066
+ } else if (type === 'message') {
10067
+ return bw.message(msg.target, msg.action, msg.data);
10068
+
10069
+ } else if (type === 'register') {
10070
+ if (!msg.name || !msg.body) return false;
10071
+ try {
10072
+ bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
10073
+ return true;
10074
+ } catch (e) {
10075
+ _ce('[bw] register error:', msg.name, e);
10076
+ return false;
10077
+ }
10078
+
10079
+ } else if (type === 'call') {
10080
+ if (!msg.name) return false;
10081
+ var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
10082
+ if (!_is(fn, 'function')) return false;
10083
+ try {
10084
+ var args = _isA(msg.args) ? msg.args : [];
10085
+ fn.apply(null, args);
10086
+ return true;
10087
+ } catch (e) {
10088
+ _ce('[bw] call error:', msg.name, e);
10089
+ return false;
10090
+ }
10091
+
10092
+ } else if (type === 'exec') {
10093
+ if (!bw._allowExec) {
10094
+ _cw('[bw] exec rejected: allowExec is not enabled');
10095
+ return false;
10096
+ }
10097
+ if (!msg.code) return false;
10098
+ try {
10099
+ new Function(msg.code)();
10100
+ return true;
10101
+ } catch (e) {
10102
+ _ce('[bw] exec error:', e);
10103
+ return false;
10104
+ }
10105
+ }
10106
+
10107
+ return false;
10108
+ };
10109
+
10110
+ /**
10111
+ * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
10112
+ *
10113
+ * Returns a connection object with sendAction(), on(), and close() methods.
10114
+ *
10115
+ * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
10116
+ * @param {Object} [opts] - Connection options
10117
+ * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
10118
+ * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
10119
+ * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
10120
+ * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
10121
+ * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
10122
+ * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
10123
+ * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
10124
+ * @returns {Object} Connection object { sendAction, on, close, status }
10125
+ * @category Server
10126
+ */
10127
+ bw.clientConnect = function(url, opts) {
10128
+ opts = opts || {};
10129
+ var transport = opts.transport || 'sse';
10130
+ var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
10131
+ var reconnect = opts.reconnect !== false;
10132
+ var onStatus = opts.onStatus || function() {};
10133
+ var onMessage = opts.onMessage || null;
10134
+ var handlers = {};
10135
+ // Set the global allowExec flag from connection options
10136
+ bw._allowExec = !!opts.allowExec;
10137
+ var conn = {
10138
+ status: 'connecting',
10139
+ _es: null,
10140
+ _pollTimer: null
10141
+ };
10142
+
10143
+ function setStatus(s) {
10144
+ conn.status = s;
10145
+ onStatus(s);
10146
+ }
10147
+
10148
+ function handleMessage(data) {
10149
+ try {
10150
+ var msg = _is(data, 'string') ? bw.clientParse(data) : data;
10151
+ if (onMessage) onMessage(msg);
10152
+ if (handlers.message) handlers.message(msg);
10153
+ bw.clientApply(msg);
10154
+ } catch (e) {
10155
+ if (handlers.error) handlers.error(e);
10156
+ }
10157
+ }
10158
+
10159
+ if (transport === 'sse' && typeof EventSource !== 'undefined') {
10160
+ setStatus('connecting');
10161
+ var es = new EventSource(url);
10162
+ conn._es = es;
10163
+
10164
+ es.onopen = function() {
10165
+ setStatus('connected');
10166
+ if (handlers.open) handlers.open();
10167
+ };
10168
+
10169
+ es.onmessage = function(e) {
10170
+ handleMessage(e.data);
10171
+ };
10172
+
10173
+ es.onerror = function() {
10174
+ if (conn.status === 'connected') {
10175
+ setStatus('disconnected');
10176
+ }
10177
+ if (handlers.error) handlers.error(new Error('SSE connection error'));
10178
+ if (!reconnect) {
10179
+ es.close();
10180
+ }
10181
+ // EventSource auto-reconnects by default when reconnect=true
10182
+ };
10183
+ } else if (transport === 'poll') {
10184
+ var interval = opts.interval || 2000;
10185
+ setStatus('connected');
10186
+ conn._pollTimer = setInterval(function() {
10187
+ fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
10188
+ if (_isA(msgs)) {
10189
+ msgs.forEach(handleMessage);
10190
+ } else if (msgs && msgs.type) {
10191
+ handleMessage(msgs);
10192
+ }
10193
+ }).catch(function(e) {
10194
+ if (handlers.error) handlers.error(e);
10195
+ });
10196
+ }, interval);
10197
+ }
10198
+
10199
+ /**
10200
+ * Send an action to the server via POST.
10201
+ * @param {string} action - Action name
10202
+ * @param {Object} [data] - Action payload
10203
+ */
10204
+ conn.sendAction = function(action, data) {
10205
+ var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
10206
+ fetch(actionUrl, {
10207
+ method: 'POST',
10208
+ headers: { 'Content-Type': 'application/json' },
10209
+ body: body
10210
+ }).catch(function(e) {
10211
+ if (handlers.error) handlers.error(e);
10212
+ });
10213
+ };
10214
+
10215
+ /**
10216
+ * Register an event handler.
10217
+ * @param {string} event - 'open'|'message'|'error'|'close'
10218
+ * @param {Function} handler
10219
+ */
10220
+ conn.on = function(event, handler) {
10221
+ handlers[event] = handler;
10222
+ return conn;
10223
+ };
10224
+
10225
+ /**
10226
+ * Close the connection.
10227
+ */
10228
+ conn.close = function() {
10229
+ if (conn._es) {
10230
+ conn._es.close();
10231
+ conn._es = null;
10232
+ }
10233
+ if (conn._pollTimer) {
10234
+ clearInterval(conn._pollTimer);
10235
+ conn._pollTimer = null;
10236
+ }
10237
+ setStatus('disconnected');
10238
+ if (handlers.close) handlers.close();
10239
+ };
10240
+
10241
+ return conn;
10242
+ };
10243
+
9492
10244
  // ===================================================================================
9493
10245
  // bw.inspect() — Debug utility
9494
10246
  // ===================================================================================
@@ -9515,33 +10267,33 @@
9515
10267
  el = target.element;
9516
10268
  comp = target;
9517
10269
  } else {
9518
- if (typeof target === 'string') {
10270
+ if (_is(target, 'string')) {
9519
10271
  el = bw.$(target)[0];
9520
10272
  }
9521
10273
  if (!el) {
9522
- console.warn('bw.inspect: element not found');
10274
+ _cw('bw.inspect: element not found');
9523
10275
  return null;
9524
10276
  }
9525
10277
  comp = el._bwComponentHandle;
9526
10278
  }
9527
10279
  if (!comp) {
9528
- console.log('bw.inspect: no ComponentHandle on this element');
9529
- console.log(' Tag:', el.tagName);
9530
- console.log(' Classes:', el.className);
9531
- console.log(' _bw_state:', el._bw_state || '(none)');
10280
+ _cl('bw.inspect: no ComponentHandle on this element');
10281
+ _cl(' Tag:', el.tagName);
10282
+ _cl(' Classes:', el.className);
10283
+ _cl(' _bw_state:', el._bw_state || '(none)');
9532
10284
  return null;
9533
10285
  }
9534
10286
  var deps = comp._bindings.reduce(function(s, b) {
9535
10287
  return s.concat(b.deps || []);
9536
10288
  }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
9537
10289
  console.group('Component: ' + comp._bwId);
9538
- console.log('State:', comp._state);
9539
- console.log('Bindings:', comp._bindings.length, '(deps:', deps, ')');
9540
- console.log('Methods:', Object.keys(comp._methods));
9541
- console.log('Actions:', Object.keys(comp._actions));
9542
- console.log('User tag:', comp._userTag || '(none)');
9543
- console.log('Mounted:', comp.mounted);
9544
- console.log('Element:', comp.element);
10290
+ _cl('State:', comp._state);
10291
+ _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
10292
+ _cl('Methods:', _keys(comp._methods));
10293
+ _cl('Actions:', _keys(comp._actions));
10294
+ _cl('User tag:', comp._userTag || '(none)');
10295
+ _cl('Mounted:', comp.mounted);
10296
+ _cl('Element:', comp.element);
9545
10297
  console.groupEnd();
9546
10298
  return comp;
9547
10299
  };
@@ -9564,8 +10316,8 @@
9564
10316
  // Pre-extract all binding expressions
9565
10317
  var precompiled = [];
9566
10318
  function walkExpressions(node) {
9567
- if (!node || typeof node !== 'object') return;
9568
- if (typeof node.c === 'string' && node.c.indexOf('${') >= 0) {
10319
+ if (!_is(node, 'object')) return;
10320
+ if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
9569
10321
  var parsed = bw._parseBindings(node.c);
9570
10322
  for (var i = 0; i < parsed.length; i++) {
9571
10323
  try {
@@ -9580,9 +10332,9 @@
9580
10332
  }
9581
10333
  if (node.a) {
9582
10334
  for (var key in node.a) {
9583
- if (Object.prototype.hasOwnProperty.call(node.a, key)) {
10335
+ if (_hop.call(node.a, key)) {
9584
10336
  var v = node.a[key];
9585
- if (typeof v === 'string' && v.indexOf('${') >= 0) {
10337
+ if (_is(v, 'string') && v.indexOf('${') >= 0) {
9586
10338
  var parsed2 = bw._parseBindings(v);
9587
10339
  for (var j = 0; j < parsed2.length; j++) {
9588
10340
  try {
@@ -9598,9 +10350,9 @@
9598
10350
  }
9599
10351
  }
9600
10352
  }
9601
- if (Array.isArray(node.c)) {
10353
+ if (_isA(node.c)) {
9602
10354
  for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
9603
- } else if (node.c && typeof node.c === 'object' && node.c.t) {
10355
+ } else if (_is(node.c, 'object') && node.c.t) {
9604
10356
  walkExpressions(node.c);
9605
10357
  }
9606
10358
  }
@@ -9612,7 +10364,7 @@
9612
10364
  handle._precompiledBindings = precompiled;
9613
10365
  if (initialState) {
9614
10366
  for (var k in initialState) {
9615
- if (Object.prototype.hasOwnProperty.call(initialState, k)) {
10367
+ if (_hop.call(initialState, k)) {
9616
10368
  handle._state[k] = initialState[k];
9617
10369
  }
9618
10370
  }
@@ -9643,18 +10395,18 @@
9643
10395
  bw.css = function(rules, options = {}) {
9644
10396
  const { minify = false, pretty = !minify } = options;
9645
10397
 
9646
- if (typeof rules === 'string') return rules;
10398
+ if (_is(rules, 'string')) return rules;
9647
10399
 
9648
10400
  let css = '';
9649
10401
  const indent = pretty ? ' ' : '';
9650
10402
  const newline = pretty ? '\n' : '';
9651
10403
  const space = pretty ? ' ' : '';
9652
10404
 
9653
- if (Array.isArray(rules)) {
10405
+ if (_isA(rules)) {
9654
10406
  css = rules.map(rule => bw.css(rule, options)).join(newline);
9655
- } else if (typeof rules === 'object') {
10407
+ } else if (_is(rules, 'object')) {
9656
10408
  Object.entries(rules).forEach(([selector, styles]) => {
9657
- if (typeof styles === 'object' && !Array.isArray(styles)) {
10409
+ if (_is(styles, 'object')) {
9658
10410
  // Handle @media, @keyframes, @supports — recurse into nested block
9659
10411
  if (selector.charAt(0) === '@') {
9660
10412
  const inner = bw.css(styles, options);
@@ -9703,7 +10455,7 @@
9703
10455
  */
9704
10456
  bw.injectCSS = function(css, options = {}) {
9705
10457
  if (!bw._isBrowser) {
9706
- console.warn('bw.injectCSS requires a DOM environment');
10458
+ _cw('bw.injectCSS requires a DOM environment');
9707
10459
  return null;
9708
10460
  }
9709
10461
 
@@ -9720,7 +10472,7 @@
9720
10472
  }
9721
10473
 
9722
10474
  // Convert CSS if needed
9723
- const cssStr = typeof css === 'string' ? css : bw.css(css, options);
10475
+ const cssStr = _is(css, 'string') ? css : bw.css(css, options);
9724
10476
 
9725
10477
  // Set or append CSS
9726
10478
  if (append && styleEl.textContent) {
@@ -9750,7 +10502,7 @@
9750
10502
  var result = {};
9751
10503
  for (var i = 0; i < arguments.length; i++) {
9752
10504
  var arg = arguments[i];
9753
- if (arg && typeof arg === 'object') Object.assign(result, arg);
10505
+ if (_is(arg, 'object')) Object.assign(result, arg);
9754
10506
  }
9755
10507
  return result;
9756
10508
  };
@@ -9873,7 +10625,7 @@
9873
10625
  bw.responsive = function(selector, breakpoints) {
9874
10626
  var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
9875
10627
  var parts = [];
9876
- Object.keys(breakpoints).forEach(function(key) {
10628
+ _keys(breakpoints).forEach(function(key) {
9877
10629
  var rules = {};
9878
10630
  if (key === 'base') {
9879
10631
  rules[selector] = breakpoints[key];
@@ -9945,18 +10697,18 @@
9945
10697
  if (!selector) return [];
9946
10698
 
9947
10699
  // Already an array
9948
- if (Array.isArray(selector)) return selector;
10700
+ if (_isA(selector)) return selector;
9949
10701
 
9950
10702
  // Single element
9951
10703
  if (selector.nodeType) return [selector];
9952
10704
 
9953
10705
  // NodeList or HTMLCollection
9954
- if (selector.length !== undefined && typeof selector !== 'string') {
10706
+ if (selector.length !== undefined && !_is(selector, 'string')) {
9955
10707
  return Array.from(selector);
9956
10708
  }
9957
10709
 
9958
10710
  // CSS selector string
9959
- if (typeof selector === 'string') {
10711
+ if (_is(selector, 'string')) {
9960
10712
  return Array.from(document.querySelectorAll(selector));
9961
10713
  }
9962
10714
 
@@ -10460,7 +11212,7 @@
10460
11212
 
10461
11213
  // Auto-detect columns if not provided
10462
11214
  const cols = columns || (data.length > 0
10463
- ? Object.keys(data[0]).map(key => ({ key, label: key }))
11215
+ ? _keys(data[0]).map(key => ({ key, label: key }))
10464
11216
  : []);
10465
11217
 
10466
11218
  // Current sort state
@@ -10475,7 +11227,7 @@
10475
11227
  const bVal = b[currentSortColumn];
10476
11228
 
10477
11229
  // Handle different types
10478
- if (typeof aVal === 'number' && typeof bVal === 'number') {
11230
+ if (_is(aVal, 'number') && _is(bVal, 'number')) {
10479
11231
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
10480
11232
  }
10481
11233
 
@@ -10585,7 +11337,7 @@
10585
11337
  bw.makeTableFromArray = function(config) {
10586
11338
  const { data = [], headerRow = true, columns, ...rest } = config;
10587
11339
 
10588
- if (!Array.isArray(data) || data.length === 0) {
11340
+ if (!_isA(data) || data.length === 0) {
10589
11341
  return bw.makeTable({ data: [], columns: columns || [], ...rest });
10590
11342
  }
10591
11343
 
@@ -10667,7 +11419,7 @@
10667
11419
  className = ''
10668
11420
  } = config;
10669
11421
 
10670
- if (!Array.isArray(data) || data.length === 0) {
11422
+ if (!_isA(data) || data.length === 0) {
10671
11423
  return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
10672
11424
  }
10673
11425
 
@@ -10816,7 +11568,7 @@
10816
11568
  */
10817
11569
  bw.render = function(element, position, taco) {
10818
11570
  // Get target element
10819
- const targetEl = typeof element === 'string'
11571
+ const targetEl = _is(element, 'string')
10820
11572
  ? document.querySelector(element)
10821
11573
  : element;
10822
11574
 
@@ -10966,7 +11718,7 @@
10966
11718
  setContent(content) {
10967
11719
  this._taco.c = content;
10968
11720
  if (this.element) {
10969
- if (typeof content === 'string') {
11721
+ if (_is(content, 'string')) {
10970
11722
  this.element.textContent = content;
10971
11723
  } else {
10972
11724
  // Re-render for complex content