bitwrench 2.0.16 → 2.0.18

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 (68) hide show
  1. package/README.md +127 -38
  2. package/dist/bitwrench-bccl.cjs.js +13 -9
  3. package/dist/bitwrench-bccl.cjs.min.js +2 -2
  4. package/dist/bitwrench-bccl.esm.js +13 -9
  5. package/dist/bitwrench-bccl.esm.min.js +2 -2
  6. package/dist/bitwrench-bccl.umd.js +13 -9
  7. package/dist/bitwrench-bccl.umd.min.js +2 -2
  8. package/dist/bitwrench-code-edit.cjs.js +1 -1
  9. package/dist/bitwrench-code-edit.cjs.min.js +1 -1
  10. package/dist/bitwrench-code-edit.es5.js +1 -1
  11. package/dist/bitwrench-code-edit.es5.min.js +1 -1
  12. package/dist/bitwrench-code-edit.esm.js +1 -1
  13. package/dist/bitwrench-code-edit.esm.min.js +1 -1
  14. package/dist/bitwrench-code-edit.umd.js +1 -1
  15. package/dist/bitwrench-code-edit.umd.min.js +1 -1
  16. package/dist/bitwrench-lean.cjs.js +1438 -920
  17. package/dist/bitwrench-lean.cjs.min.js +20 -20
  18. package/dist/bitwrench-lean.es5.js +1518 -1105
  19. package/dist/bitwrench-lean.es5.min.js +18 -18
  20. package/dist/bitwrench-lean.esm.js +1437 -920
  21. package/dist/bitwrench-lean.esm.min.js +20 -20
  22. package/dist/bitwrench-lean.umd.js +1438 -920
  23. package/dist/bitwrench-lean.umd.min.js +20 -20
  24. package/dist/bitwrench-util-css.cjs.js +236 -0
  25. package/dist/bitwrench-util-css.cjs.min.js +22 -0
  26. package/dist/bitwrench-util-css.es5.js +414 -0
  27. package/dist/bitwrench-util-css.es5.min.js +21 -0
  28. package/dist/bitwrench-util-css.esm.js +230 -0
  29. package/dist/bitwrench-util-css.esm.min.js +21 -0
  30. package/dist/bitwrench-util-css.umd.js +242 -0
  31. package/dist/bitwrench-util-css.umd.min.js +21 -0
  32. package/dist/bitwrench.cjs.js +1450 -928
  33. package/dist/bitwrench.cjs.min.js +21 -21
  34. package/dist/bitwrench.css +456 -132
  35. package/dist/bitwrench.es5.js +1563 -1140
  36. package/dist/bitwrench.es5.min.js +19 -19
  37. package/dist/bitwrench.esm.js +1450 -929
  38. package/dist/bitwrench.esm.min.js +21 -21
  39. package/dist/bitwrench.min.css +1 -1
  40. package/dist/bitwrench.umd.js +1450 -928
  41. package/dist/bitwrench.umd.min.js +21 -21
  42. package/dist/builds.json +178 -90
  43. package/dist/bwserve.cjs.js +528 -68
  44. package/dist/bwserve.esm.js +527 -69
  45. package/dist/sri.json +44 -36
  46. package/package.json +5 -2
  47. package/readme.html +136 -49
  48. package/src/bitwrench-bccl.js +12 -8
  49. package/src/bitwrench-color-utils.js +31 -9
  50. package/src/bitwrench-esm-entry.js +11 -0
  51. package/src/bitwrench-styles.js +439 -232
  52. package/src/bitwrench-util-css.js +229 -0
  53. package/src/bitwrench.js +979 -630
  54. package/src/bwserve/attach.js +57 -0
  55. package/src/bwserve/bwclient.js +141 -0
  56. package/src/bwserve/bwshell.js +102 -0
  57. package/src/bwserve/client.js +151 -1
  58. package/src/bwserve/index.js +139 -29
  59. package/src/cli/attach.js +555 -0
  60. package/src/cli/convert.js +2 -5
  61. package/src/cli/index.js +7 -0
  62. package/src/cli/inject.js +1 -1
  63. package/src/cli/layout-default.js +47 -32
  64. package/src/cli/serve.js +6 -2
  65. package/src/generate-css.js +11 -4
  66. package/src/vendor/html2canvas.min.js +20 -0
  67. package/src/version.js +3 -3
  68. package/src/bwserve/shell.js +0 -103
package/src/bitwrench.js CHANGED
@@ -8,11 +8,11 @@
8
8
  */
9
9
 
10
10
  import { VERSION_INFO } from './version.js';
11
- import { getStructuralStyles,
12
- generateThemedCSS, generateAlternateCSS, derivePalette as _derivePalette,
11
+ import { getStructuralStyles, getResetStyles,
12
+ generateThemedCSS, derivePalette as _derivePalette,
13
13
  DEFAULT_PALETTE_CONFIG, SPACING_PRESETS, RADIUS_PRESETS, THEME_PRESETS,
14
14
  TYPE_RATIO_PRESETS, ELEVATION_PRESETS, MOTION_PRESETS, generateTypeScale,
15
- resolveLayout } from './bitwrench-styles.js';
15
+ resolveLayout, scopeRulesUnder } from './bitwrench-styles.js';
16
16
  import { hexToHsl, hslToHex, adjustLightness, mixColor,
17
17
  relativeLuminance, textOnColor, deriveShades,
18
18
  derivePalette, harmonize, deriveAlternateSeed, deriveAlternateConfig,
@@ -80,7 +80,7 @@ const bw = {
80
80
  __monkey_patch_is_nodejs__: {
81
81
  _value: 'ignore',
82
82
  set: function(x) {
83
- this._value = (typeof x === 'boolean') ? x : 'ignore';
83
+ this._value = _is(x, 'boolean') ? x : 'ignore';
84
84
  },
85
85
  get: function() {
86
86
  return this._value;
@@ -128,6 +128,67 @@ Object.defineProperty(bw, '_isBrowser', {
128
128
  configurable: true
129
129
  });
130
130
 
131
+ // ── Internal aliases ─────────────────────────────────────────────────────
132
+ // Short names for frequently-used builtins and internal methods.
133
+ // Same pattern as v1 (_to = bw.typeOf, etc.).
134
+ //
135
+ // Why: Terser can't shorten global property chains (console.warn,
136
+ // Object.prototype.hasOwnProperty, Array.isArray, document.createElement)
137
+ // because it can't prove they're side-effect-free. We can, so we alias
138
+ // them here. Each alias saves bytes in the minified output, and the short
139
+ // names also reduce visual noise in the hot paths (binding pipeline,
140
+ // createDOM, etc.).
141
+ //
142
+ // Alias Target Sites
143
+ // ───────── ────────────────────────────────────── ─────
144
+ // _hop Object.prototype.hasOwnProperty 15
145
+ // _isA Array.isArray 25
146
+ // _keys Object.keys 7
147
+ // _to bw.typeOf (type string) 26
148
+ // _is type check boolean: _is(x,'string') ~50
149
+ // _cw console.warn 8
150
+ // _cl console.log 11
151
+ // _ce console.error 4
152
+ // _chp ComponentHandle.prototype 28 (defined after constructor)
153
+ //
154
+ // Note: document.createElement etc. are NOT aliased because they require
155
+ // `this === document` and .bind() would add overhead on every call.
156
+ // Console aliases use thin wrappers (not direct refs) so test monkey-
157
+ // patching of console.warn/log/error continues to work.
158
+ //
159
+ // `typeof x` for UNDECLARED globals (window, document, process, require,
160
+ // EventSource, navigator, Promise, __filename, import.meta) MUST stay as
161
+ // raw `typeof` — calling _to(x) when x doesn't exist throws ReferenceError.
162
+ //
163
+ // ── v1 functional type helpers (kept for reference, not currently used) ──
164
+ // _toa(x, type, trueVal, falseVal) — bw.typeAssign:
165
+ // returns trueVal if _to(x)===type, else falseVal.
166
+ // Replaces: (typeof x === 'string') ? A : B → _toa(x,'string',A,B)
167
+ // _toc(x, type, trueVal, falseVal) — bw.typeConvert:
168
+ // same as _toa but if trueVal/falseVal are functions, calls them with x.
169
+ // Replaces: typeof x === 'string' ? fn(x) : default → _toc(x,'string',fn,default)
170
+ // Uncomment if pattern frequency justifies them:
171
+ // var _toa = function(x, t, y, n) { return _to(x) === t ? y : n; };
172
+ // 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); };
173
+ // ─────────────────────────────────────────────────────────────────────────
174
+ var _hop = Object.prototype.hasOwnProperty;
175
+ var _isA = Array.isArray;
176
+ var _keys = Object.keys;
177
+ var _to = _typeOf; // imported from bitwrench-utils.js
178
+ var _is = function(x, t) { var r = _to(x); return r === t || r.toLowerCase() === t; };
179
+ // Console aliases use thin wrappers (not direct references) so that test
180
+ // code can monkey-patch console.warn/log/error and the patches take effect.
181
+ var _cw = function() { console.warn.apply(console, arguments); };
182
+ var _cl = function() { console.log.apply(console, arguments); };
183
+ var _ce = function() { console.error.apply(console, arguments); };
184
+
185
+ /**
186
+ * Debug flag. When true, emits console.warn for silent binding failures
187
+ * (missing paths, null refs, auto-created intermediate objects).
188
+ * @type {boolean}
189
+ */
190
+ bw.debug = false;
191
+
131
192
  /**
132
193
  * Lazy-resolve Node.js `fs` module.
133
194
  * Tries require('fs') first (available in CJS/UMD Node.js builds),
@@ -275,7 +336,7 @@ bw.uuid = function(prefix) {
275
336
  */
276
337
  bw._el = function(id) {
277
338
  // Pass-through for DOM elements
278
- if (typeof id !== 'string') return id || null;
339
+ if (!_is(id, 'string')) return id || null;
279
340
  if (!id) return null;
280
341
  if (!bw._isBrowser) return null;
281
342
 
@@ -303,7 +364,12 @@ bw._el = function(id) {
303
364
  el = document.querySelector('[data-bw_id="' + id + '"]');
304
365
  }
305
366
 
306
- // 5. Cache the result for next time
367
+ // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
368
+ if (!el && id.indexOf('bw_uuid_') === 0) {
369
+ el = document.querySelector('.' + id);
370
+ }
371
+
372
+ // 6. Cache the result for next time
307
373
  if (el) {
308
374
  bw._nodeMap[id] = el;
309
375
  }
@@ -356,6 +422,84 @@ bw._deregisterNode = function(el, bwId) {
356
422
  }
357
423
  };
358
424
 
425
+ // ===================================================================================
426
+ // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
427
+ // ===================================================================================
428
+
429
+ /**
430
+ * Regex to match a bw_uuid_* token in a class string.
431
+ * @private
432
+ */
433
+ var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
434
+
435
+ /**
436
+ * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
437
+ *
438
+ * Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
439
+ * to replace an existing UUID (useful in loops where each TACO needs a unique ID).
440
+ *
441
+ * @param {Object} taco - A TACO object `{t, a, c, o}`
442
+ * @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
443
+ * @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
444
+ * @category Identifiers
445
+ * @example
446
+ * var card = bw.makeStatCard({ value: '0', label: 'Scans' });
447
+ * var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
448
+ * var same = bw.assignUUID(card); // same UUID (idempotent)
449
+ * var diff = bw.assignUUID(card, true); // new UUID (forced)
450
+ */
451
+ bw.assignUUID = function(taco, forceNew) {
452
+ if (!taco || !_is(taco, 'object')) return null;
453
+
454
+ // Ensure taco.a exists
455
+ if (!taco.a) taco.a = {};
456
+ if (!_is(taco.a.class, 'string')) taco.a.class = taco.a.class ? String(taco.a.class) : '';
457
+
458
+ var existing = taco.a.class.match(_UUID_RE);
459
+
460
+ if (existing && !forceNew) {
461
+ return existing[0];
462
+ }
463
+
464
+ // Remove old UUID if forceNew
465
+ if (existing) {
466
+ taco.a.class = taco.a.class.replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
467
+ }
468
+
469
+ var uuid = bw.uuid('uuid');
470
+ taco.a.class = (taco.a.class ? taco.a.class + ' ' : '') + uuid;
471
+ return uuid;
472
+ };
473
+
474
+ /**
475
+ * Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
476
+ *
477
+ * @param {Object|Element} tacoOrElement - A TACO object or DOM element
478
+ * @returns {string|null} The UUID string, or null if none assigned
479
+ * @category Identifiers
480
+ * @example
481
+ * bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
482
+ * bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
483
+ * bw.getUUID({t:'div'}) // null (no UUID)
484
+ */
485
+ bw.getUUID = function(tacoOrElement) {
486
+ if (!tacoOrElement) return null;
487
+
488
+ var classStr;
489
+ // DOM element: check className
490
+ if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
491
+ classStr = tacoOrElement.className;
492
+ }
493
+ // TACO object: check a.class
494
+ else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
495
+ classStr = tacoOrElement.a.class;
496
+ }
497
+
498
+ if (!classStr) return null;
499
+ var match = classStr.match(_UUID_RE);
500
+ return match ? match[0] : null;
501
+ };
502
+
359
503
  /**
360
504
  * Escape HTML special characters to prevent XSS.
361
505
  *
@@ -371,7 +515,7 @@ bw._deregisterNode = function(el, bwId) {
371
515
  * // => '<b>Hello</b> & "world"'
372
516
  */
373
517
  bw.escapeHTML = function(str) {
374
- if (typeof str !== 'string') return '';
518
+ if (!_is(str, 'string')) return '';
375
519
 
376
520
  const escapeMap = {
377
521
  '&': '&',
@@ -405,6 +549,42 @@ bw.raw = function(str) {
405
549
  return { __bw_raw: true, v: String(str) };
406
550
  };
407
551
 
552
+ /**
553
+ * Hyperscript-style TACO constructor.
554
+ *
555
+ * A convenience helper that returns a canonical TACO object from positional
556
+ * arguments. The return value is a plain object — serializable, works with
557
+ * bwserve, and accepted everywhere TACO is accepted.
558
+ *
559
+ * @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
560
+ * @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
561
+ * @param {*} [content] - Content: string, number, TACO object, or array of children.
562
+ * @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
563
+ * @returns {Object} Plain TACO object {t, a?, c?, o?}
564
+ * @category Utilities
565
+ * @see bw.html
566
+ * @see bw.createDOM
567
+ * @see bw.DOM
568
+ * @example
569
+ * bw.h('div')
570
+ * // => { t: 'div' }
571
+ *
572
+ * bw.h('p', { class: 'bw_text_muted' }, 'Hello')
573
+ * // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
574
+ *
575
+ * bw.h('ul', null, [
576
+ * bw.h('li', null, 'one'),
577
+ * bw.h('li', null, 'two')
578
+ * ])
579
+ * // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
580
+ */
581
+ bw.h = function(tag, attrs, content, options) {
582
+ var taco = { t: String(tag) };
583
+ if (attrs !== null && attrs !== undefined) taco.a = attrs;
584
+ if (content !== undefined) taco.c = content;
585
+ if (options !== undefined) taco.o = options;
586
+ return taco;
587
+ };
408
588
 
409
589
  /**
410
590
  * Convert a TACO object (or array of TACOs) to an HTML string.
@@ -444,7 +624,7 @@ bw.html = function(taco, options = {}) {
444
624
  }
445
625
 
446
626
  // Handle arrays of TACOs
447
- if (Array.isArray(taco)) {
627
+ if (_isA(taco)) {
448
628
  return taco.map(t => bw.html(t, options)).join('');
449
629
  }
450
630
 
@@ -467,15 +647,15 @@ bw.html = function(taco, options = {}) {
467
647
  if (taco && taco._bwEach && options.state) {
468
648
  var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
469
649
  var arr = bw._evaluatePath(options.state, eachExpr);
470
- if (!Array.isArray(arr)) return '';
650
+ if (!_isA(arr)) return '';
471
651
  return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
472
652
  }
473
653
 
474
654
  // Handle primitives and non-TACO objects
475
- if (typeof taco !== 'object' || !taco.t) {
655
+ if (!_is(taco, 'object') || !taco.t) {
476
656
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
477
657
  // Resolve template bindings if state provided
478
- if (options.state && typeof str === 'string' && str.indexOf('${') >= 0) {
658
+ if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
479
659
  str = bw._resolveTemplate(str, options.state, !!options.compile);
480
660
  }
481
661
  return str;
@@ -495,10 +675,18 @@ bw.html = function(taco, options = {}) {
495
675
  // Skip null, undefined, false
496
676
  if (value == null || value === false) continue;
497
677
 
498
- // Skip event handlers (they're for DOM only)
499
- if (key.startsWith('on')) continue;
678
+ // Serialize event handlers via funcRegister
679
+ if (key.startsWith('on')) {
680
+ if (_is(value, 'function')) {
681
+ var fnId = bw.funcRegister(value);
682
+ attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
683
+ } else if (_is(value, 'string')) {
684
+ attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
685
+ }
686
+ continue;
687
+ }
500
688
 
501
- if (key === 'style' && typeof value === 'object') {
689
+ if (key === 'style' && _is(value, 'object')) {
502
690
  // Convert style object to string
503
691
  const styleStr = Object.entries(value)
504
692
  .filter(([, v]) => v != null)
@@ -509,7 +697,7 @@ bw.html = function(taco, options = {}) {
509
697
  }
510
698
  } else if (key === 'class') {
511
699
  // Handle class as array or string
512
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
700
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
513
701
  if (classStr) {
514
702
  attrStr += ` class="${bw.escapeHTML(classStr)}"`;
515
703
  }
@@ -545,13 +733,184 @@ bw.html = function(taco, options = {}) {
545
733
  // Process content recursively
546
734
  let contentStr = content != null ? bw.html(content, options) : '';
547
735
  // Resolve template bindings in content if state provided
548
- if (options.state && typeof contentStr === 'string' && contentStr.indexOf('${') >= 0) {
736
+ if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
549
737
  contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
550
738
  }
551
739
 
552
740
  return `<${tag}${attrStr}>${contentStr}</${tag}>`;
553
741
  };
554
742
 
743
+ /**
744
+ * Generate a complete, self-contained HTML document from TACO content.
745
+ *
746
+ * Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
747
+ * func registry emission (so serialized event handlers work), optional theme,
748
+ * and extra head elements. Designed for static site generation, offline/airgapped
749
+ * use, and the "static site that isn't static" workflow.
750
+ *
751
+ * @param {Object} [opts={}] - Page options
752
+ * @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
753
+ * @param {string} [opts.title='bitwrench'] - Page title
754
+ * @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
755
+ * @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
756
+ * @param {string} [opts.css=''] - Additional CSS for <style> block
757
+ * @param {string|Object} [opts.theme=null] - Theme preset name or config object
758
+ * @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
759
+ * @param {string} [opts.favicon=''] - Favicon URL
760
+ * @param {string} [opts.lang='en'] - HTML lang attribute
761
+ * @returns {string} Complete HTML document string
762
+ * @category DOM Generation
763
+ * @see bw.html
764
+ * @example
765
+ * bw.htmlPage({
766
+ * title: 'My App',
767
+ * body: { t: 'h1', c: 'Hello World' },
768
+ * runtime: 'shim'
769
+ * })
770
+ */
771
+ bw.htmlPage = function(opts) {
772
+ opts = opts || {};
773
+ var title = opts.title || 'bitwrench';
774
+ var body = opts.body || '';
775
+ var state = opts.state || undefined;
776
+ var runtime = opts.runtime || 'shim';
777
+ var css = opts.css || '';
778
+ var theme = opts.theme || null;
779
+ var headExtra = opts.head || [];
780
+ var favicon = opts.favicon || '';
781
+ var lang = opts.lang || 'en';
782
+
783
+ // Snapshot funcRegistry counter before rendering
784
+ var fnCounterBefore = bw._fnIDCounter;
785
+
786
+ // Render body content
787
+ var bodyHTML = '';
788
+ if (_is(body, 'string')) {
789
+ bodyHTML = body;
790
+ } else {
791
+ var htmlOpts = {};
792
+ if (state) htmlOpts.state = state;
793
+ bodyHTML = bw.html(body, htmlOpts);
794
+ }
795
+
796
+ // Collect functions registered during this render
797
+ var fnCounterAfter = bw._fnIDCounter;
798
+ var registryEntries = '';
799
+ for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
800
+ var fnKey = 'bw_fn_' + i;
801
+ if (bw._fnRegistry[fnKey]) {
802
+ registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' +
803
+ bw._fnRegistry[fnKey].toString() + ';\n';
804
+ }
805
+ }
806
+
807
+ // Build runtime script for <head>
808
+ var runtimeHead = '';
809
+ if (runtime === 'inline') {
810
+ // Read UMD bundle synchronously if in Node.js
811
+ var umdSource = null;
812
+ if (bw._isNode) {
813
+ try {
814
+ var fs = (typeof require === 'function') ? require('fs') : null;
815
+ var pathMod = (typeof require === 'function') ? require('path') : null;
816
+ if (fs && pathMod) {
817
+ // Resolve dist/ relative to this source file
818
+ var srcDir = '';
819
+ try { srcDir = pathMod.dirname((typeof __filename !== 'undefined') ? __filename : ''); }
820
+ catch(e2) { /* ESM: __filename not available */ }
821
+ if (!srcDir && typeof import.meta !== 'undefined' && import.meta.url) {
822
+ var url = (typeof require === 'function') ? require('url') : null;
823
+ if (url && url.fileURLToPath) srcDir = pathMod.dirname(url.fileURLToPath(import.meta.url));
824
+ }
825
+ if (srcDir) {
826
+ var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
827
+ umdSource = fs.readFileSync(distPath, 'utf8');
828
+ }
829
+ }
830
+ } catch(e) { /* fall through */ }
831
+ }
832
+ if (umdSource) {
833
+ runtimeHead = '<script>' + umdSource + '</script>';
834
+ } else {
835
+ // Fallback to shim in browser or if dist not available
836
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
837
+ }
838
+ } else if (runtime === 'cdn') {
839
+ runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
840
+ } else if (runtime === 'shim') {
841
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
842
+ }
843
+ // runtime === 'none' → empty
844
+
845
+ // Theme CSS
846
+ var themeCSS = '';
847
+ if (theme) {
848
+ var themeConfig = _is(theme, 'string')
849
+ ? (THEME_PRESETS[theme.toLowerCase()] || null)
850
+ : theme;
851
+ if (themeConfig) {
852
+ var themeResult = bw.makeStyles(themeConfig);
853
+ themeCSS = themeResult.css;
854
+ }
855
+ }
856
+
857
+ // Extra <head> elements
858
+ var headHTML = '';
859
+ if (_isA(headExtra) && headExtra.length > 0) {
860
+ headHTML = headExtra.map(function(el) { return bw.html(el); }).join('\n');
861
+ }
862
+
863
+ // Favicon
864
+ var faviconTag = '';
865
+ if (favicon) {
866
+ var safeFavicon = favicon.replace(/[&<>"']/g, function(c) {
867
+ return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
868
+ });
869
+ faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
870
+ }
871
+
872
+ // Escaped title
873
+ var safeTitle = bw.escapeHTML(title);
874
+
875
+ // Combine all CSS
876
+ var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
877
+
878
+ // Body-end script: registry entries + optional loadStyles
879
+ var bodyEndScript = '';
880
+ var bodyEndParts = [];
881
+ if (registryEntries) {
882
+ bodyEndParts.push(registryEntries);
883
+ }
884
+ if (runtime === 'inline' || runtime === 'cdn') {
885
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
886
+ }
887
+ if (bodyEndParts.length > 0) {
888
+ bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
889
+ }
890
+
891
+ // Assemble document
892
+ var parts = [
893
+ '<!DOCTYPE html>',
894
+ '<html lang="' + lang + '">',
895
+ '<head>',
896
+ '<meta charset="UTF-8">',
897
+ '<meta name="viewport" content="width=device-width, initial-scale=1">'
898
+ ];
899
+ parts.push('<title>' + safeTitle + '</title>');
900
+ if (faviconTag) parts.push(faviconTag);
901
+ if (runtimeHead) parts.push(runtimeHead);
902
+ if (headHTML) parts.push(headHTML);
903
+ if (allCSS) parts.push('<style>' + allCSS + '</style>');
904
+ parts.push('</head>');
905
+ parts.push('<body>');
906
+ parts.push(bodyHTML);
907
+ if (bodyEndScript) parts.push(bodyEndScript);
908
+ parts.push('</body>');
909
+ parts.push('</html>');
910
+
911
+ return parts.join('\n');
912
+ };
913
+
555
914
  /**
556
915
  * Create a live DOM element from a TACO object (browser only).
557
916
  *
@@ -596,7 +955,7 @@ bw.createDOM = function(taco, options = {}) {
596
955
  }
597
956
 
598
957
  // Handle text nodes
599
- if (typeof taco !== 'object' || !taco.t) {
958
+ if (!_is(taco, 'object') || !taco.t) {
600
959
  return document.createTextNode(String(taco));
601
960
  }
602
961
 
@@ -609,16 +968,16 @@ bw.createDOM = function(taco, options = {}) {
609
968
  for (const [key, value] of Object.entries(attrs)) {
610
969
  if (value == null || value === false) continue;
611
970
 
612
- if (key === 'style' && typeof value === 'object') {
971
+ if (key === 'style' && _is(value, 'object')) {
613
972
  // Apply styles directly
614
973
  Object.assign(el.style, value);
615
974
  } else if (key === 'class') {
616
975
  // Handle class as array or string
617
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
976
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
618
977
  if (classStr) {
619
978
  el.className = classStr;
620
979
  }
621
- } else if (key.startsWith('on') && typeof value === 'function') {
980
+ } else if (key.startsWith('on') && _is(value, 'function')) {
622
981
  // Event handlers
623
982
  const eventName = key.slice(2).toLowerCase();
624
983
  el.addEventListener(eventName, value);
@@ -638,7 +997,7 @@ bw.createDOM = function(taco, options = {}) {
638
997
  // Children with data-bw_id or id attributes get local refs on the parent,
639
998
  // so o.render functions can access them without any DOM lookup.
640
999
  if (content != null) {
641
- if (Array.isArray(content)) {
1000
+ if (_isA(content)) {
642
1001
  content.forEach(child => {
643
1002
  if (child != null) {
644
1003
  // Handle ComponentHandle in content arrays (Level 2 children)
@@ -658,20 +1017,20 @@ bw.createDOM = function(taco, options = {}) {
658
1017
  if (childEl._bw_refs) {
659
1018
  if (!el._bw_refs) el._bw_refs = {};
660
1019
  for (var rk in childEl._bw_refs) {
661
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
1020
+ if (_hop.call(childEl._bw_refs, rk)) {
662
1021
  el._bw_refs[rk] = childEl._bw_refs[rk];
663
1022
  }
664
1023
  }
665
1024
  }
666
1025
  }
667
1026
  });
668
- } else if (typeof content === 'object' && content.__bw_raw) {
1027
+ } else if (_is(content, 'object') && content.__bw_raw) {
669
1028
  // Raw HTML content — inject via innerHTML
670
1029
  el.innerHTML = content.v;
671
1030
  } else if (content._bwComponent === true) {
672
1031
  // Single ComponentHandle as content
673
1032
  content.mount(el);
674
- } else if (typeof content === 'object' && content.t) {
1033
+ } else if (_is(content, 'object') && content.t) {
675
1034
  var childEl = bw.createDOM(content, options);
676
1035
  el.appendChild(childEl);
677
1036
  var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
@@ -682,7 +1041,7 @@ bw.createDOM = function(taco, options = {}) {
682
1041
  if (childEl._bw_refs) {
683
1042
  if (!el._bw_refs) el._bw_refs = {};
684
1043
  for (var rk in childEl._bw_refs) {
685
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
1044
+ if (_hop.call(childEl._bw_refs, rk)) {
686
1045
  el._bw_refs[rk] = childEl._bw_refs[rk];
687
1046
  }
688
1047
  }
@@ -697,6 +1056,14 @@ bw.createDOM = function(taco, options = {}) {
697
1056
  bw._registerNode(el, null);
698
1057
  }
699
1058
 
1059
+ // Register UUID class in node cache (bw_uuid_* tokens in class string)
1060
+ if (el.className) {
1061
+ var uuidMatch = el.className.match(_UUID_RE);
1062
+ if (uuidMatch) {
1063
+ bw._nodeMap[uuidMatch[0]] = el;
1064
+ }
1065
+ }
1066
+
700
1067
  // Handle lifecycle hooks and state
701
1068
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
702
1069
  const id = attrs['data-bw_id'] || bw.uuid();
@@ -715,7 +1082,7 @@ bw.createDOM = function(taco, options = {}) {
715
1082
  el._bw_render = opts.render;
716
1083
 
717
1084
  if (opts.mounted) {
718
- console.warn('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
1085
+ _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
719
1086
  }
720
1087
 
721
1088
  // Queue initial render (same timing as mounted)
@@ -788,7 +1155,7 @@ bw.DOM = function(target, taco, options = {}) {
788
1155
  const targetEl = bw._el(target);
789
1156
 
790
1157
  if (!targetEl) {
791
- console.error('bw.DOM: Target element not found:', target);
1158
+ _ce('bw.DOM: Target element not found:', target);
792
1159
  return null;
793
1160
  }
794
1161
 
@@ -828,7 +1195,7 @@ bw.DOM = function(target, taco, options = {}) {
828
1195
  targetEl.appendChild(taco.element);
829
1196
  }
830
1197
  // Handle arrays
831
- else if (Array.isArray(taco)) {
1198
+ else if (_isA(taco)) {
832
1199
  taco.forEach(t => {
833
1200
  if (t != null) {
834
1201
  if (t._bwComponent === true) {
@@ -864,7 +1231,7 @@ bw.DOM = function(target, taco, options = {}) {
864
1231
  bw.compileProps = function(handle, props = {}) {
865
1232
  const compiledProps = {};
866
1233
 
867
- Object.keys(props).forEach(key => {
1234
+ _keys(props).forEach(key => {
868
1235
  // Create getter/setter for each prop
869
1236
  Object.defineProperty(compiledProps, key, {
870
1237
  get() {
@@ -1069,6 +1436,16 @@ bw.renderComponent = function(taco, options = {}) {
1069
1436
  bw.cleanup = function(element) {
1070
1437
  if (!bw._isBrowser || !element) return;
1071
1438
 
1439
+ // Deregister UUID classes from node cache (element + descendants)
1440
+ // Covers elements that have UUID but no data-bw_id
1441
+ var selfUuidMatch = element.className && element.className.match(_UUID_RE);
1442
+ if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
1443
+ var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
1444
+ uuidEls.forEach(function(uel) {
1445
+ var m = uel.className && uel.className.match(_UUID_RE);
1446
+ if (m) delete bw._nodeMap[m[0]];
1447
+ });
1448
+
1072
1449
  // Find all elements with data-bw_id
1073
1450
  const elements = element.querySelectorAll('[data-bw_id]');
1074
1451
 
@@ -1084,6 +1461,10 @@ bw.cleanup = function(element) {
1084
1461
  // Deregister from node cache
1085
1462
  bw._deregisterNode(el, id);
1086
1463
 
1464
+ // Deregister UUID class from node cache
1465
+ var uuidMatch = el.className && el.className.match(_UUID_RE);
1466
+ if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
1467
+
1087
1468
  // Clean up pub/sub subscriptions tied to this element
1088
1469
  if (el._bw_subs) {
1089
1470
  el._bw_subs.forEach(function(unsub) { unsub(); });
@@ -1108,6 +1489,10 @@ bw.cleanup = function(element) {
1108
1489
  // Deregister from node cache
1109
1490
  bw._deregisterNode(element, id);
1110
1491
 
1492
+ // Deregister UUID class from node cache
1493
+ var elemUuidMatch = element.className && element.className.match(_UUID_RE);
1494
+ if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
1495
+
1111
1496
  // Clean up pub/sub subscriptions tied to element itself
1112
1497
  if (element._bw_subs) {
1113
1498
  element._bw_subs.forEach(function(unsub) { unsub(); });
@@ -1182,17 +1567,17 @@ bw.patch = function(id, content, attr) {
1182
1567
  if (attr) {
1183
1568
  // Patch an attribute
1184
1569
  el.setAttribute(attr, String(content));
1185
- } else if (Array.isArray(content)) {
1570
+ } else if (_isA(content)) {
1186
1571
  // Patch with array of children (strings and/or TACOs)
1187
1572
  el.innerHTML = '';
1188
1573
  content.forEach(function(item) {
1189
- if (typeof item === 'string' || typeof item === 'number') {
1574
+ if (_is(item, 'string') || _is(item, 'number')) {
1190
1575
  el.appendChild(document.createTextNode(String(item)));
1191
1576
  } else if (item && item.t) {
1192
1577
  el.appendChild(bw.createDOM(item));
1193
1578
  }
1194
1579
  });
1195
- } else if (typeof content === 'object' && content !== null && content.t) {
1580
+ } else if (_is(content, 'object') && content.t) {
1196
1581
  // Patch with a TACO — replace children
1197
1582
  el.innerHTML = '';
1198
1583
  el.appendChild(bw.createDOM(content));
@@ -1223,7 +1608,7 @@ bw.patch = function(id, content, attr) {
1223
1608
  bw.patchAll = function(patches) {
1224
1609
  var results = {};
1225
1610
  for (var id in patches) {
1226
- if (Object.prototype.hasOwnProperty.call(patches, id)) {
1611
+ if (_hop.call(patches, id)) {
1227
1612
  results[id] = bw.patch(id, patches[id]);
1228
1613
  }
1229
1614
  }
@@ -1320,7 +1705,7 @@ bw.pub = function(topic, detail) {
1320
1705
  snapshot[i].handler(detail);
1321
1706
  called++;
1322
1707
  } catch (err) {
1323
- console.warn('bw.pub: subscriber error on topic "' + topic + '":', err);
1708
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
1324
1709
  }
1325
1710
  }
1326
1711
  return called;
@@ -1416,8 +1801,8 @@ bw._fnIDCounter = 0;
1416
1801
  * @see bw.funcGetDispatchStr
1417
1802
  */
1418
1803
  bw.funcRegister = function(fn, name) {
1419
- if (typeof fn !== 'function') return '';
1420
- var fnID = (typeof name === 'string' && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
1804
+ if (!_is(fn, 'function')) return '';
1805
+ var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
1421
1806
  bw._fnRegistry[fnID] = fn;
1422
1807
  return fnID;
1423
1808
  };
@@ -1436,7 +1821,7 @@ bw.funcRegister = function(fn, name) {
1436
1821
  bw.funcGetById = function(name, errFn) {
1437
1822
  name = String(name);
1438
1823
  if (name in bw._fnRegistry) return bw._fnRegistry[name];
1439
- return (typeof errFn === 'function') ? errFn : function() { console.warn('bw.funcGetById: unregistered fn "' + name + '"'); };
1824
+ return _is(errFn, 'function') ? errFn : function() { _cw('bw.funcGetById: unregistered fn "' + name + '"'); };
1440
1825
  };
1441
1826
 
1442
1827
  /**
@@ -1477,13 +1862,30 @@ bw.funcUnregister = function(name) {
1477
1862
  bw.funcGetRegistry = function() {
1478
1863
  var copy = {};
1479
1864
  for (var k in bw._fnRegistry) {
1480
- if (Object.prototype.hasOwnProperty.call(bw._fnRegistry, k)) {
1865
+ if (_hop.call(bw._fnRegistry, k)) {
1481
1866
  copy[k] = bw._fnRegistry[k];
1482
1867
  }
1483
1868
  }
1484
1869
  return copy;
1485
1870
  };
1486
1871
 
1872
+ /**
1873
+ * Minimal runtime shim for funcRegister dispatch in static HTML.
1874
+ * When embedded in a `<script>` tag, provides just enough infrastructure
1875
+ * for `bw.funcGetById()` calls to resolve. The actual function bodies
1876
+ * are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
1877
+ * @type {string}
1878
+ * @category Function Registry
1879
+ */
1880
+ bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' +
1881
+ 'if(!bw._fnRegistry)bw._fnRegistry={};' +
1882
+ 'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' +
1883
+ 'console.warn("bw: unregistered fn "+n)};};' +
1884
+ 'bw.funcRegister=function(fn,name){' +
1885
+ 'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' +
1886
+ 'bw._fnRegistry[id]=fn;return id;};' +
1887
+ 'window.bw=bw;})();';
1888
+
1487
1889
  // ===================================================================================
1488
1890
  // Template Binding Utilities
1489
1891
  // ===================================================================================
@@ -1511,7 +1913,10 @@ bw._evaluatePath = function(state, path) {
1511
1913
  var parts = path.split('.');
1512
1914
  var val = state;
1513
1915
  for (var i = 0; i < parts.length; i++) {
1514
- if (val == null) return '';
1916
+ if (val == null) {
1917
+ if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
1918
+ return '';
1919
+ }
1515
1920
  val = val[parts[i]];
1516
1921
  }
1517
1922
  return (val == null) ? '' : val;
@@ -1531,7 +1936,7 @@ bw._evaluatePath = function(state, path) {
1531
1936
  */
1532
1937
  bw._compiledExprs = {};
1533
1938
  bw._resolveTemplate = function(str, state, compile) {
1534
- if (typeof str !== 'string' || str.indexOf('${') < 0) return str;
1939
+ if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
1535
1940
  var bindings = bw._parseBindings(str);
1536
1941
  if (bindings.length === 0) return str;
1537
1942
 
@@ -1553,6 +1958,7 @@ bw._resolveTemplate = function(str, state, compile) {
1553
1958
  try {
1554
1959
  val = bw._compiledExprs[b.expr](state);
1555
1960
  } catch (e) {
1961
+ if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
1556
1962
  val = '';
1557
1963
  }
1558
1964
  } else {
@@ -1661,7 +2067,7 @@ function ComponentHandle(taco) {
1661
2067
  this._state = {};
1662
2068
  if (o.state) {
1663
2069
  for (var k in o.state) {
1664
- if (Object.prototype.hasOwnProperty.call(o.state, k)) {
2070
+ if (_hop.call(o.state, k)) {
1665
2071
  this._state[k] = o.state[k];
1666
2072
  }
1667
2073
  }
@@ -1670,7 +2076,7 @@ function ComponentHandle(taco) {
1670
2076
  this._actions = {};
1671
2077
  if (o.actions) {
1672
2078
  for (var k2 in o.actions) {
1673
- if (Object.prototype.hasOwnProperty.call(o.actions, k2)) {
2079
+ if (_hop.call(o.actions, k2)) {
1674
2080
  this._actions[k2] = o.actions[k2];
1675
2081
  }
1676
2082
  }
@@ -1680,7 +2086,7 @@ function ComponentHandle(taco) {
1680
2086
  if (o.methods) {
1681
2087
  var self = this;
1682
2088
  for (var k3 in o.methods) {
1683
- if (Object.prototype.hasOwnProperty.call(o.methods, k3)) {
2089
+ if (_hop.call(o.methods, k3)) {
1684
2090
  this._methods[k3] = o.methods[k3];
1685
2091
  (function(methodName, methodFn) {
1686
2092
  self[methodName] = function() {
@@ -1698,7 +2104,7 @@ function ComponentHandle(taco) {
1698
2104
  willMount: o.willMount || null,
1699
2105
  mounted: o.mounted || null,
1700
2106
  willUpdate: o.willUpdate || null,
1701
- onUpdate: o.onUpdate || null,
2107
+ onUpdate: o.onUpdate || o.updated || null,
1702
2108
  unmount: o.unmount || null,
1703
2109
  willDestroy: o.willDestroy || null
1704
2110
  };
@@ -1713,14 +2119,23 @@ function ComponentHandle(taco) {
1713
2119
  this._compile = !!o.compile;
1714
2120
  this._bw_refs = {};
1715
2121
  this._refCounter = 0;
2122
+ // Child component ownership (Bug #5)
2123
+ this._children = [];
2124
+ this._parent = null;
2125
+ // Factory metadata for BCCL rebuild (Bug #6)
2126
+ this._factory = taco._bwFactory || null;
1716
2127
  }
1717
2128
 
2129
+ // Short alias for ComponentHandle.prototype (see alias block at top of file).
2130
+ // 28 method definitions × 25 chars = ~700B raw savings in minified output.
2131
+ var _chp = ComponentHandle.prototype;
2132
+
1718
2133
  // ── State Methods ──
1719
2134
 
1720
2135
  /**
1721
2136
  * Get a state value. Dot-path supported: `get('user.name')`
1722
2137
  */
1723
- ComponentHandle.prototype.get = function(key) {
2138
+ _chp.get = function(key) {
1724
2139
  return bw._evaluatePath(this._state, key);
1725
2140
  };
1726
2141
 
@@ -1730,12 +2145,13 @@ ComponentHandle.prototype.get = function(key) {
1730
2145
  * @param {*} value - New value
1731
2146
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
1732
2147
  */
1733
- ComponentHandle.prototype.set = function(key, value, opts) {
2148
+ _chp.set = function(key, value, opts) {
1734
2149
  // Dot-path set
1735
2150
  var parts = key.split('.');
1736
2151
  var obj = this._state;
1737
2152
  for (var i = 0; i < parts.length - 1; i++) {
1738
- if (obj[parts[i]] == null || typeof obj[parts[i]] !== 'object') {
2153
+ if (!_is(obj[parts[i]], 'object')) {
2154
+ if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
1739
2155
  obj[parts[i]] = {};
1740
2156
  }
1741
2157
  obj = obj[parts[i]];
@@ -1755,10 +2171,10 @@ ComponentHandle.prototype.set = function(key, value, opts) {
1755
2171
  /**
1756
2172
  * Get a shallow clone of the full state.
1757
2173
  */
1758
- ComponentHandle.prototype.getState = function() {
2174
+ _chp.getState = function() {
1759
2175
  var clone = {};
1760
2176
  for (var k in this._state) {
1761
- if (Object.prototype.hasOwnProperty.call(this._state, k)) {
2177
+ if (_hop.call(this._state, k)) {
1762
2178
  clone[k] = this._state[k];
1763
2179
  }
1764
2180
  }
@@ -1770,9 +2186,9 @@ ComponentHandle.prototype.getState = function() {
1770
2186
  * @param {Object} updates - Key-value pairs to merge
1771
2187
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
1772
2188
  */
1773
- ComponentHandle.prototype.setState = function(updates, opts) {
2189
+ _chp.setState = function(updates, opts) {
1774
2190
  for (var k in updates) {
1775
- if (Object.prototype.hasOwnProperty.call(updates, k)) {
2191
+ if (_hop.call(updates, k)) {
1776
2192
  this._state[k] = updates[k];
1777
2193
  this._dirtyKeys[k] = true;
1778
2194
  }
@@ -1789,9 +2205,9 @@ ComponentHandle.prototype.setState = function(updates, opts) {
1789
2205
  /**
1790
2206
  * Push a value onto an array in state. Clones the array.
1791
2207
  */
1792
- ComponentHandle.prototype.push = function(key, val) {
2208
+ _chp.push = function(key, val) {
1793
2209
  var arr = this.get(key);
1794
- var newArr = Array.isArray(arr) ? arr.slice() : [];
2210
+ var newArr = _isA(arr) ? arr.slice() : [];
1795
2211
  newArr.push(val);
1796
2212
  this.set(key, newArr);
1797
2213
  };
@@ -1799,9 +2215,9 @@ ComponentHandle.prototype.push = function(key, val) {
1799
2215
  /**
1800
2216
  * Splice an array in state. Clones the array.
1801
2217
  */
1802
- ComponentHandle.prototype.splice = function(key, start, deleteCount) {
2218
+ _chp.splice = function(key, start, deleteCount) {
1803
2219
  var arr = this.get(key);
1804
- var newArr = Array.isArray(arr) ? arr.slice() : [];
2220
+ var newArr = _isA(arr) ? arr.slice() : [];
1805
2221
  var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
1806
2222
  Array.prototype.splice.apply(newArr, args);
1807
2223
  this.set(key, newArr);
@@ -1809,7 +2225,7 @@ ComponentHandle.prototype.splice = function(key, start, deleteCount) {
1809
2225
 
1810
2226
  // ── Scheduling ──
1811
2227
 
1812
- ComponentHandle.prototype._scheduleDirty = function() {
2228
+ _chp._scheduleDirty = function() {
1813
2229
  if (!this._scheduled) {
1814
2230
  this._scheduled = true;
1815
2231
  bw._dirtyComponents.push(this);
@@ -1824,17 +2240,17 @@ ComponentHandle.prototype._scheduleDirty = function() {
1824
2240
  * Creates binding descriptors with refIds for targeted DOM updates.
1825
2241
  * @private
1826
2242
  */
1827
- ComponentHandle.prototype._compileBindings = function() {
2243
+ _chp._compileBindings = function() {
1828
2244
  this._bindings = [];
1829
2245
  this._refCounter = 0;
1830
- var stateKeys = Object.keys(this._state);
2246
+ var stateKeys = _keys(this._state);
1831
2247
  var self = this;
1832
2248
 
1833
2249
  function walkTaco(taco, path) {
1834
- if (taco == null || typeof taco !== 'object' || !taco.t) return taco;
2250
+ if (!_is(taco, 'object') || !taco.t) return taco;
1835
2251
 
1836
2252
  // Check content for bindings
1837
- if (typeof taco.c === 'string' && taco.c.indexOf('${') >= 0) {
2253
+ if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
1838
2254
  var refId = 'bw_ref_' + self._refCounter++;
1839
2255
  var parsed = bw._parseBindings(taco.c);
1840
2256
  var deps = [];
@@ -1856,10 +2272,10 @@ ComponentHandle.prototype._compileBindings = function() {
1856
2272
  // Check attributes for bindings
1857
2273
  if (taco.a) {
1858
2274
  for (var attrName in taco.a) {
1859
- if (!Object.prototype.hasOwnProperty.call(taco.a, attrName)) continue;
2275
+ if (!_hop.call(taco.a, attrName)) continue;
1860
2276
  if (attrName === 'data-bw_ref') continue;
1861
2277
  var attrVal = taco.a[attrName];
1862
- if (typeof attrVal === 'string' && attrVal.indexOf('${') >= 0) {
2278
+ if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
1863
2279
  var refId2 = 'bw_ref_' + self._refCounter++;
1864
2280
  var parsed2 = bw._parseBindings(attrVal);
1865
2281
  var deps2 = [];
@@ -1885,9 +2301,27 @@ ComponentHandle.prototype._compileBindings = function() {
1885
2301
  }
1886
2302
 
1887
2303
  // Recurse into children
1888
- if (Array.isArray(taco.c)) {
2304
+ if (_isA(taco.c)) {
1889
2305
  for (var i = 0; i < taco.c.length; i++) {
1890
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
2306
+ // Wrap string children with ${expr} in a span so patches target the span, not the parent
2307
+ if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
2308
+ var mixedRefId = 'bw_ref_' + self._refCounter++;
2309
+ var mixedParsed = bw._parseBindings(taco.c[i]);
2310
+ var mixedDeps = [];
2311
+ for (var mi = 0; mi < mixedParsed.length; mi++) {
2312
+ mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
2313
+ }
2314
+ self._bindings.push({
2315
+ expr: taco.c[i],
2316
+ type: 'content',
2317
+ refId: mixedRefId,
2318
+ deps: mixedDeps,
2319
+ template: taco.c[i]
2320
+ });
2321
+ // Replace string with a span wrapper so textContent targets the span only
2322
+ taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
2323
+ }
2324
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
1891
2325
  walkTaco(taco.c[i], path.concat(i));
1892
2326
  }
1893
2327
  // Handle bw.when/bw.each markers
@@ -1922,7 +2356,7 @@ ComponentHandle.prototype._compileBindings = function() {
1922
2356
  taco.c[i]._refId = eachRefId;
1923
2357
  }
1924
2358
  }
1925
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
2359
+ } else if (_is(taco.c, 'object') && taco.c.t) {
1926
2360
  walkTaco(taco.c, path.concat(0));
1927
2361
  }
1928
2362
 
@@ -1938,7 +2372,7 @@ ComponentHandle.prototype._compileBindings = function() {
1938
2372
  * Build ref map from the live DOM after createDOM.
1939
2373
  * @private
1940
2374
  */
1941
- ComponentHandle.prototype._collectRefs = function() {
2375
+ _chp._collectRefs = function() {
1942
2376
  this._bw_refs = {};
1943
2377
  if (!this.element) return;
1944
2378
  var els = this.element.querySelectorAll('[data-bw_ref]');
@@ -1959,7 +2393,7 @@ ComponentHandle.prototype._collectRefs = function() {
1959
2393
  * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
1960
2394
  * @param {Element} parentEl - DOM element to mount into
1961
2395
  */
1962
- ComponentHandle.prototype.mount = function(parentEl) {
2396
+ _chp.mount = function(parentEl) {
1963
2397
  // willMount hook
1964
2398
  if (this._hooks.willMount) this._hooks.willMount(this);
1965
2399
 
@@ -1981,7 +2415,7 @@ ComponentHandle.prototype.mount = function(parentEl) {
1981
2415
  // Register named actions in function registry
1982
2416
  var self = this;
1983
2417
  for (var actionName in this._actions) {
1984
- if (Object.prototype.hasOwnProperty.call(this._actions, actionName)) {
2418
+ if (_hop.call(this._actions, actionName)) {
1985
2419
  var registeredName = this._bwId + '_' + actionName;
1986
2420
  (function(aName) {
1987
2421
  bw.funcRegister(function(evt) {
@@ -2000,6 +2434,11 @@ ComponentHandle.prototype.mount = function(parentEl) {
2000
2434
  this.element = bw.createDOM(tacoForDOM);
2001
2435
  this.element._bwComponentHandle = this;
2002
2436
  this.element.setAttribute('data-bw_comp_id', this._bwId);
2437
+
2438
+ // Restore o.render from original TACO (stripped by _tacoForDOM)
2439
+ if (this.taco.o && this.taco.o.render) {
2440
+ this.element._bw_render = this.taco.o.render;
2441
+ }
2003
2442
  if (this._userTag) {
2004
2443
  this.element.classList.add(this._userTag);
2005
2444
  }
@@ -2015,6 +2454,16 @@ ComponentHandle.prototype.mount = function(parentEl) {
2015
2454
 
2016
2455
  this.mounted = true;
2017
2456
 
2457
+ // Scan for child ComponentHandles and link parent/child (Bug #5)
2458
+ var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
2459
+ for (var ci = 0; ci < childEls.length; ci++) {
2460
+ var ch = childEls[ci]._bwComponentHandle;
2461
+ if (ch && ch !== this && !ch._parent) {
2462
+ ch._parent = this;
2463
+ this._children.push(ch);
2464
+ }
2465
+ }
2466
+
2018
2467
  // mounted hook (backward compat: fn.length === 2 wraps (el, state))
2019
2468
  if (this._hooks.mounted) {
2020
2469
  if (this._hooks.mounted.length === 2) {
@@ -2023,16 +2472,21 @@ ComponentHandle.prototype.mount = function(parentEl) {
2023
2472
  this._hooks.mounted(this);
2024
2473
  }
2025
2474
  }
2475
+
2476
+ // Invoke o.render on initial mount (if present)
2477
+ if (this.element._bw_render) {
2478
+ this.element._bw_render(this.element, this._state);
2479
+ }
2026
2480
  };
2027
2481
 
2028
2482
  /**
2029
2483
  * Prepare TACO for initial render: resolve when/each markers.
2030
2484
  * @private
2031
2485
  */
2032
- ComponentHandle.prototype._prepareTaco = function(taco) {
2033
- if (!taco || typeof taco !== 'object') return;
2486
+ _chp._prepareTaco = function(taco) {
2487
+ if (!_is(taco, 'object')) return;
2034
2488
 
2035
- if (Array.isArray(taco.c)) {
2489
+ if (_isA(taco.c)) {
2036
2490
  for (var i = taco.c.length - 1; i >= 0; i--) {
2037
2491
  var child = taco.c[i];
2038
2492
  if (child && child._bwWhen) {
@@ -2057,18 +2511,18 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
2057
2511
  var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
2058
2512
  var arr = bw._evaluatePath(this._state, eachExprStr);
2059
2513
  var items = [];
2060
- if (Array.isArray(arr)) {
2514
+ if (_isA(arr)) {
2061
2515
  for (var j = 0; j < arr.length; j++) {
2062
2516
  items.push(child.factory(arr[j], j));
2063
2517
  }
2064
2518
  }
2065
2519
  taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
2066
2520
  }
2067
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
2521
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
2068
2522
  this._prepareTaco(taco.c[i]);
2069
2523
  }
2070
2524
  }
2071
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
2525
+ } else if (_is(taco.c, 'object') && taco.c.t) {
2072
2526
  this._prepareTaco(taco.c);
2073
2527
  }
2074
2528
  };
@@ -2077,12 +2531,12 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
2077
2531
  * Wire action name strings (in onclick etc.) to dispatch function calls.
2078
2532
  * @private
2079
2533
  */
2080
- ComponentHandle.prototype._wireActions = function(taco) {
2081
- if (!taco || typeof taco !== 'object' || !taco.t) return;
2534
+ _chp._wireActions = function(taco) {
2535
+ if (!_is(taco, 'object') || !taco.t) return;
2082
2536
  if (taco.a) {
2083
2537
  for (var key in taco.a) {
2084
- if (!Object.prototype.hasOwnProperty.call(taco.a, key)) continue;
2085
- if (key.startsWith('on') && typeof taco.a[key] === 'string') {
2538
+ if (!_hop.call(taco.a, key)) continue;
2539
+ if (key.startsWith('on') && _is(taco.a[key], 'string')) {
2086
2540
  var actionName = taco.a[key];
2087
2541
  if (actionName in this._actions) {
2088
2542
  var registeredName = this._bwId + '_' + actionName;
@@ -2096,11 +2550,11 @@ ComponentHandle.prototype._wireActions = function(taco) {
2096
2550
  }
2097
2551
  }
2098
2552
  }
2099
- if (Array.isArray(taco.c)) {
2553
+ if (_isA(taco.c)) {
2100
2554
  for (var i = 0; i < taco.c.length; i++) {
2101
2555
  this._wireActions(taco.c[i]);
2102
2556
  }
2103
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
2557
+ } else if (_is(taco.c, 'object') && taco.c.t) {
2104
2558
  this._wireActions(taco.c);
2105
2559
  }
2106
2560
  };
@@ -2109,7 +2563,7 @@ ComponentHandle.prototype._wireActions = function(taco) {
2109
2563
  * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
2110
2564
  * @private
2111
2565
  */
2112
- ComponentHandle.prototype._deepCloneTaco = function(taco) {
2566
+ _chp._deepCloneTaco = function(taco) {
2113
2567
  if (taco == null) return taco;
2114
2568
  // Preserve _bwWhen / _bwEach markers (contain functions)
2115
2569
  if (taco._bwWhen) {
@@ -2121,18 +2575,18 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
2121
2575
  if (taco._bwEach) {
2122
2576
  return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
2123
2577
  }
2124
- if (typeof taco !== 'object' || !taco.t) return taco;
2578
+ if (!_is(taco, 'object') || !taco.t) return taco;
2125
2579
  var result = { t: taco.t };
2126
2580
  if (taco.a) {
2127
2581
  result.a = {};
2128
2582
  for (var k in taco.a) {
2129
- if (Object.prototype.hasOwnProperty.call(taco.a, k)) result.a[k] = taco.a[k];
2583
+ if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
2130
2584
  }
2131
2585
  }
2132
2586
  if (taco.c != null) {
2133
- if (Array.isArray(taco.c)) {
2587
+ if (_isA(taco.c)) {
2134
2588
  result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
2135
- } else if (typeof taco.c === 'object') {
2589
+ } else if (_is(taco.c, 'object')) {
2136
2590
  result.c = this._deepCloneTaco(taco.c);
2137
2591
  } else {
2138
2592
  result.c = taco.c;
@@ -2146,27 +2600,31 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
2146
2600
  * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
2147
2601
  * @private
2148
2602
  */
2149
- ComponentHandle.prototype._tacoForDOM = function(taco) {
2150
- if (!taco || typeof taco !== 'object' || !taco.t) return taco;
2603
+ _chp._tacoForDOM = function(taco) {
2604
+ if (!_is(taco, 'object') || !taco.t) return taco;
2151
2605
  var result = { t: taco.t };
2152
2606
  if (taco.a) result.a = taco.a;
2153
2607
  if (taco.c != null) {
2154
- if (Array.isArray(taco.c)) {
2608
+ if (_isA(taco.c)) {
2155
2609
  result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
2156
- } else if (typeof taco.c === 'object' && taco.c.t) {
2610
+ } else if (_is(taco.c, 'object') && taco.c.t) {
2157
2611
  result.c = this._tacoForDOM(taco.c);
2158
2612
  } else {
2159
2613
  result.c = taco.c;
2160
2614
  }
2161
2615
  }
2162
2616
  // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
2617
+ if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
2618
+ _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
2619
+ '>. Use onclick attribute or bw.component() for child interactivity.');
2620
+ }
2163
2621
  return result;
2164
2622
  };
2165
2623
 
2166
2624
  /**
2167
2625
  * Unmount: remove from DOM, deactivate, preserve state for re-mount.
2168
2626
  */
2169
- ComponentHandle.prototype.unmount = function() {
2627
+ _chp.unmount = function() {
2170
2628
  if (!this.mounted) return;
2171
2629
 
2172
2630
  // unmount hook
@@ -2201,12 +2659,23 @@ ComponentHandle.prototype.unmount = function() {
2201
2659
  /**
2202
2660
  * Destroy: unmount + clear state + unregister actions.
2203
2661
  */
2204
- ComponentHandle.prototype.destroy = function() {
2662
+ _chp.destroy = function() {
2205
2663
  // willDestroy hook
2206
2664
  if (this._hooks.willDestroy) {
2207
2665
  this._hooks.willDestroy(this);
2208
2666
  }
2209
2667
 
2668
+ // Cascade destroy to children depth-first (Bug #5)
2669
+ for (var ci = this._children.length - 1; ci >= 0; ci--) {
2670
+ this._children[ci].destroy();
2671
+ }
2672
+ this._children = [];
2673
+ if (this._parent) {
2674
+ var idx = this._parent._children.indexOf(this);
2675
+ if (idx >= 0) this._parent._children.splice(idx, 1);
2676
+ this._parent = null;
2677
+ }
2678
+
2210
2679
  this.unmount();
2211
2680
 
2212
2681
  // Unregister actions from function registry
@@ -2233,12 +2702,36 @@ ComponentHandle.prototype.destroy = function() {
2233
2702
  * Flush dirty state: resolve changed bindings and apply to DOM.
2234
2703
  * @private
2235
2704
  */
2236
- ComponentHandle.prototype._flush = function() {
2705
+ _chp._flush = function() {
2237
2706
  this._scheduled = false;
2238
- var changedKeys = Object.keys(this._dirtyKeys);
2707
+ var changedKeys = _keys(this._dirtyKeys);
2239
2708
  this._dirtyKeys = {};
2240
2709
  if (changedKeys.length === 0 || !this.mounted) return;
2241
2710
 
2711
+ // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
2712
+ // rebuild the TACO from the factory with merged state (Bug #6)
2713
+ if (this._factory) {
2714
+ var rebuildNeeded = false;
2715
+ for (var fi = 0; fi < changedKeys.length; fi++) {
2716
+ if (_hop.call(this._factory.props, changedKeys[fi])) {
2717
+ rebuildNeeded = true; break;
2718
+ }
2719
+ }
2720
+ if (rebuildNeeded) {
2721
+ var merged = {};
2722
+ for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
2723
+ for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
2724
+ this._factory.props = merged;
2725
+ var newTaco = bw.make(this._factory.type, merged);
2726
+ newTaco._bwFactory = this._factory;
2727
+ this.taco = newTaco;
2728
+ this._originalTaco = this._deepCloneTaco(newTaco);
2729
+ this._render();
2730
+ if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
2731
+ return;
2732
+ }
2733
+ }
2734
+
2242
2735
  // willUpdate hook
2243
2736
  if (this._hooks.willUpdate) {
2244
2737
  this._hooks.willUpdate(this, changedKeys);
@@ -2277,7 +2770,7 @@ ComponentHandle.prototype._flush = function() {
2277
2770
  * Returns list of patches to apply.
2278
2771
  * @private
2279
2772
  */
2280
- ComponentHandle.prototype._resolveBindings = function(changedKeys) {
2773
+ _chp._resolveBindings = function(changedKeys) {
2281
2774
  var patches = [];
2282
2775
  for (var i = 0; i < this._bindings.length; i++) {
2283
2776
  var b = this._bindings[i];
@@ -2313,11 +2806,14 @@ ComponentHandle.prototype._resolveBindings = function(changedKeys) {
2313
2806
  * Apply patches to DOM.
2314
2807
  * @private
2315
2808
  */
2316
- ComponentHandle.prototype._applyPatches = function(patches) {
2809
+ _chp._applyPatches = function(patches) {
2317
2810
  for (var i = 0; i < patches.length; i++) {
2318
2811
  var p = patches[i];
2319
2812
  var el = this._bw_refs[p.refId];
2320
- if (!el) continue;
2813
+ if (!el) {
2814
+ if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
2815
+ continue;
2816
+ }
2321
2817
  if (p.type === 'content') {
2322
2818
  el.textContent = p.value;
2323
2819
  } else if (p.type === 'attribute') {
@@ -2334,7 +2830,7 @@ ComponentHandle.prototype._applyPatches = function(patches) {
2334
2830
  * Resolve all bindings and apply (used for initial render).
2335
2831
  * @private
2336
2832
  */
2337
- ComponentHandle.prototype._resolveAndApplyAll = function() {
2833
+ _chp._resolveAndApplyAll = function() {
2338
2834
  var patches = [];
2339
2835
  for (var i = 0; i < this._bindings.length; i++) {
2340
2836
  var b = this._bindings[i];
@@ -2357,7 +2853,7 @@ ComponentHandle.prototype._resolveAndApplyAll = function() {
2357
2853
  * Full re-render for structural changes (when/each branch switches).
2358
2854
  * @private
2359
2855
  */
2360
- ComponentHandle.prototype._render = function() {
2856
+ _chp._render = function() {
2361
2857
  if (!this.element || !this.element.parentNode) return;
2362
2858
  var parent = this.element.parentNode;
2363
2859
  var nextSibling = this.element.nextSibling;
@@ -2397,7 +2893,7 @@ ComponentHandle.prototype._render = function() {
2397
2893
  * @param {string} event - Event name (e.g., 'click')
2398
2894
  * @param {Function} handler - Event handler
2399
2895
  */
2400
- ComponentHandle.prototype.on = function(event, handler) {
2896
+ _chp.on = function(event, handler) {
2401
2897
  if (this.element) {
2402
2898
  this.element.addEventListener(event, handler);
2403
2899
  }
@@ -2409,7 +2905,7 @@ ComponentHandle.prototype.on = function(event, handler) {
2409
2905
  * @param {string} event - Event name
2410
2906
  * @param {Function} handler - Handler to remove
2411
2907
  */
2412
- ComponentHandle.prototype.off = function(event, handler) {
2908
+ _chp.off = function(event, handler) {
2413
2909
  if (this.element) {
2414
2910
  this.element.removeEventListener(event, handler);
2415
2911
  }
@@ -2424,7 +2920,7 @@ ComponentHandle.prototype.off = function(event, handler) {
2424
2920
  * @param {Function} handler - Handler function
2425
2921
  * @returns {Function} Unsubscribe function
2426
2922
  */
2427
- ComponentHandle.prototype.sub = function(topic, handler) {
2923
+ _chp.sub = function(topic, handler) {
2428
2924
  var unsub = bw.sub(topic, handler);
2429
2925
  this._subs.push(unsub);
2430
2926
  return unsub;
@@ -2435,10 +2931,10 @@ ComponentHandle.prototype.sub = function(topic, handler) {
2435
2931
  * @param {string} name - Action name
2436
2932
  * @param {...*} args - Arguments passed after comp
2437
2933
  */
2438
- ComponentHandle.prototype.action = function(name) {
2934
+ _chp.action = function(name) {
2439
2935
  var fn = this._actions[name];
2440
2936
  if (!fn) {
2441
- console.warn('ComponentHandle.action: unknown action "' + name + '"');
2937
+ _cw('ComponentHandle.action: unknown action "' + name + '"');
2442
2938
  return;
2443
2939
  }
2444
2940
  var args = [this].concat(Array.prototype.slice.call(arguments, 1));
@@ -2450,7 +2946,7 @@ ComponentHandle.prototype.action = function(name) {
2450
2946
  * @param {string} sel - CSS selector
2451
2947
  * @returns {Element|null}
2452
2948
  */
2453
- ComponentHandle.prototype.select = function(sel) {
2949
+ _chp.select = function(sel) {
2454
2950
  return this.element ? this.element.querySelector(sel) : null;
2455
2951
  };
2456
2952
 
@@ -2459,7 +2955,7 @@ ComponentHandle.prototype.select = function(sel) {
2459
2955
  * @param {string} sel - CSS selector
2460
2956
  * @returns {Element[]}
2461
2957
  */
2462
- ComponentHandle.prototype.selectAll = function(sel) {
2958
+ _chp.selectAll = function(sel) {
2463
2959
  if (!this.element) return [];
2464
2960
  return Array.prototype.slice.call(this.element.querySelectorAll(sel));
2465
2961
  };
@@ -2470,7 +2966,7 @@ ComponentHandle.prototype.selectAll = function(sel) {
2470
2966
  * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
2471
2967
  * @returns {ComponentHandle} this (for chaining)
2472
2968
  */
2473
- ComponentHandle.prototype.userTag = function(tag) {
2969
+ _chp.userTag = function(tag) {
2474
2970
  this._userTag = tag;
2475
2971
  if (this.element) {
2476
2972
  this.element.classList.add(tag);
@@ -2547,7 +3043,7 @@ bw.component = function(taco) {
2547
3043
  * and calls the named method. This is the bitwrench equivalent of
2548
3044
  * Win32 SendMessage(hwnd, msg, wParam, lParam).
2549
3045
  *
2550
- * @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
3046
+ * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
2551
3047
  * @param {string} action - Method name to call on the component
2552
3048
  * @param {*} data - Data to pass to the method
2553
3049
  * @returns {boolean} True if message was dispatched successfully
@@ -2564,15 +3060,20 @@ bw.component = function(taco) {
2564
3060
  * };
2565
3061
  */
2566
3062
  bw.message = function(target, action, data) {
2567
- // Try data-bw_comp_id attribute first, then CSS class (user tag)
2568
- var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
2569
- if (!el) {
3063
+ // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
3064
+ var el = bw._el(target);
3065
+ // Then try data-bw_comp_id attribute
3066
+ if (!el || !el._bwComponentHandle) {
3067
+ el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
3068
+ }
3069
+ // Then try CSS class (user tag)
3070
+ if (!el || !el._bwComponentHandle) {
2570
3071
  el = bw.$('.' + target)[0];
2571
3072
  }
2572
3073
  if (!el || !el._bwComponentHandle) return false;
2573
3074
  var comp = el._bwComponentHandle;
2574
- if (typeof comp[action] !== 'function') {
2575
- console.warn('bw.message: unknown action "' + action + '" on component ' + target);
3075
+ if (!_is(comp[action], 'function')) {
3076
+ _cw('bw.message: unknown action "' + action + '" on component ' + target);
2576
3077
  return false;
2577
3078
  }
2578
3079
  comp[action](data);
@@ -2580,59 +3081,24 @@ bw.message = function(target, action, data) {
2580
3081
  };
2581
3082
 
2582
3083
  // ===================================================================================
2583
- // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
3084
+ // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
2584
3085
  // ===================================================================================
2585
3086
 
2586
3087
  /**
2587
3088
  * Registry of named functions sent via register messages.
2588
- * Populated by clientApply({ type: 'register', name, body }).
2589
- * Invoked by clientApply({ type: 'call', name, args }).
3089
+ * Populated by bw.apply({ type: 'register', name, body }).
3090
+ * Invoked by bw.apply({ type: 'call', name, args }).
2590
3091
  * @private
2591
3092
  */
2592
3093
  bw._clientFunctions = {};
2593
3094
 
2594
3095
  /**
2595
- * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
3096
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
2596
3097
  * Default false — exec messages are rejected unless explicitly opted in.
2597
3098
  * @private
2598
3099
  */
2599
3100
  bw._allowExec = false;
2600
3101
 
2601
- /**
2602
- * Built-in client functions available via call() without registration.
2603
- * @private
2604
- */
2605
- bw._builtinClientFunctions = {
2606
- scrollTo: function(selector) {
2607
- var el = bw._el(selector);
2608
- if (el) el.scrollTop = el.scrollHeight;
2609
- },
2610
- focus: function(selector) {
2611
- var el = bw._el(selector);
2612
- if (el && typeof el.focus === 'function') el.focus();
2613
- },
2614
- download: function(filename, content, mimeType) {
2615
- if (typeof document === 'undefined') return;
2616
- var blob = new Blob([content], { type: mimeType || 'text/plain' });
2617
- var a = document.createElement('a');
2618
- a.href = URL.createObjectURL(blob);
2619
- a.download = filename;
2620
- a.click();
2621
- URL.revokeObjectURL(a.href);
2622
- },
2623
- clipboard: function(text) {
2624
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
2625
- navigator.clipboard.writeText(text);
2626
- }
2627
- },
2628
- redirect: function(url) {
2629
- if (typeof window !== 'undefined') window.location.href = url;
2630
- },
2631
- log: function() {
2632
- console.log.apply(console, arguments);
2633
- }
2634
- };
2635
-
2636
3102
  /**
2637
3103
  * Parse a bwserve protocol message string, supporting both strict JSON
2638
3104
  * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
@@ -2647,9 +3113,9 @@ bw._builtinClientFunctions = {
2647
3113
  * @param {string} str - JSON or r-prefixed relaxed JSON string
2648
3114
  * @returns {Object} Parsed message object
2649
3115
  * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
2650
- * @category Server
3116
+ * @category Core
2651
3117
  */
2652
- bw.clientParse = function(str) {
3118
+ bw.parseJSONFlex = function(str) {
2653
3119
  str = (str || '').trim();
2654
3120
  if (str.charAt(0) !== 'r') return JSON.parse(str);
2655
3121
  str = str.slice(1);
@@ -2734,10 +3200,10 @@ bw.clientParse = function(str) {
2734
3200
  * append — target.appendChild(bw.createDOM(node))
2735
3201
  * remove — bw.cleanup(target); target.remove()
2736
3202
  * patch — bw.patch(target, content, attr)
2737
- * batch — iterate ops, call clientApply for each
3203
+ * batch — iterate ops, call bw.apply for each
2738
3204
  * message — bw.message(target, action, data)
2739
3205
  * register — store a named function for later call()
2740
- * call — invoke a registered or built-in function
3206
+ * call — invoke a registered function
2741
3207
  * exec — execute arbitrary JS (requires allowExec)
2742
3208
  *
2743
3209
  * Target resolution:
@@ -2746,9 +3212,9 @@ bw.clientParse = function(str) {
2746
3212
  *
2747
3213
  * @param {Object} msg - Protocol message
2748
3214
  * @returns {boolean} true if the message was applied successfully
2749
- * @category Server
3215
+ * @category Core
2750
3216
  */
2751
- bw.clientApply = function(msg) {
3217
+ bw.apply = function(msg) {
2752
3218
  if (!msg || !msg.type) return false;
2753
3219
 
2754
3220
  var type = msg.type;
@@ -2774,15 +3240,15 @@ bw.clientApply = function(msg) {
2774
3240
  } else if (type === 'remove') {
2775
3241
  var toRemove = bw._el(target);
2776
3242
  if (!toRemove) return false;
2777
- if (typeof bw.cleanup === 'function') bw.cleanup(toRemove);
3243
+ if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
2778
3244
  toRemove.remove();
2779
3245
  return true;
2780
3246
 
2781
3247
  } else if (type === 'batch') {
2782
- if (!Array.isArray(msg.ops)) return false;
3248
+ if (!_isA(msg.ops)) return false;
2783
3249
  var allOk = true;
2784
3250
  msg.ops.forEach(function(op) {
2785
- if (!bw.clientApply(op)) allOk = false;
3251
+ if (!bw.apply(op)) allOk = false;
2786
3252
  });
2787
3253
  return allOk;
2788
3254
 
@@ -2795,26 +3261,26 @@ bw.clientApply = function(msg) {
2795
3261
  bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
2796
3262
  return true;
2797
3263
  } catch (e) {
2798
- console.error('[bw] register error:', msg.name, e);
3264
+ _ce('[bw] register error:', msg.name, e);
2799
3265
  return false;
2800
3266
  }
2801
3267
 
2802
3268
  } else if (type === 'call') {
2803
3269
  if (!msg.name) return false;
2804
- var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
2805
- if (typeof fn !== 'function') return false;
3270
+ var fn = bw._clientFunctions[msg.name];
3271
+ if (!_is(fn, 'function')) return false;
2806
3272
  try {
2807
- var args = Array.isArray(msg.args) ? msg.args : [];
3273
+ var args = _isA(msg.args) ? msg.args : [];
2808
3274
  fn.apply(null, args);
2809
3275
  return true;
2810
3276
  } catch (e) {
2811
- console.error('[bw] call error:', msg.name, e);
3277
+ _ce('[bw] call error:', msg.name, e);
2812
3278
  return false;
2813
3279
  }
2814
3280
 
2815
3281
  } else if (type === 'exec') {
2816
3282
  if (!bw._allowExec) {
2817
- console.warn('[bw] exec rejected: allowExec is not enabled');
3283
+ _cw('[bw] exec rejected: allowExec is not enabled');
2818
3284
  return false;
2819
3285
  }
2820
3286
  if (!msg.code) return false;
@@ -2822,7 +3288,7 @@ bw.clientApply = function(msg) {
2822
3288
  new Function(msg.code)();
2823
3289
  return true;
2824
3290
  } catch (e) {
2825
- console.error('[bw] exec error:', e);
3291
+ _ce('[bw] exec error:', e);
2826
3292
  return false;
2827
3293
  }
2828
3294
  }
@@ -2830,139 +3296,6 @@ bw.clientApply = function(msg) {
2830
3296
  return false;
2831
3297
  };
2832
3298
 
2833
- /**
2834
- * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
2835
- *
2836
- * Returns a connection object with sendAction(), on(), and close() methods.
2837
- *
2838
- * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
2839
- * @param {Object} [opts] - Connection options
2840
- * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
2841
- * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
2842
- * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
2843
- * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
2844
- * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
2845
- * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
2846
- * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
2847
- * @returns {Object} Connection object { sendAction, on, close, status }
2848
- * @category Server
2849
- */
2850
- bw.clientConnect = function(url, opts) {
2851
- opts = opts || {};
2852
- var transport = opts.transport || 'sse';
2853
- var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
2854
- var reconnect = opts.reconnect !== false;
2855
- var onStatus = opts.onStatus || function() {};
2856
- var onMessage = opts.onMessage || null;
2857
- var handlers = {};
2858
- // Set the global allowExec flag from connection options
2859
- bw._allowExec = !!opts.allowExec;
2860
- var conn = {
2861
- status: 'connecting',
2862
- _es: null,
2863
- _pollTimer: null
2864
- };
2865
-
2866
- function setStatus(s) {
2867
- conn.status = s;
2868
- onStatus(s);
2869
- }
2870
-
2871
- function handleMessage(data) {
2872
- try {
2873
- var msg = typeof data === 'string' ? bw.clientParse(data) : data;
2874
- if (onMessage) onMessage(msg);
2875
- if (handlers.message) handlers.message(msg);
2876
- bw.clientApply(msg);
2877
- } catch (e) {
2878
- if (handlers.error) handlers.error(e);
2879
- }
2880
- }
2881
-
2882
- if (transport === 'sse' && typeof EventSource !== 'undefined') {
2883
- setStatus('connecting');
2884
- var es = new EventSource(url);
2885
- conn._es = es;
2886
-
2887
- es.onopen = function() {
2888
- setStatus('connected');
2889
- if (handlers.open) handlers.open();
2890
- };
2891
-
2892
- es.onmessage = function(e) {
2893
- handleMessage(e.data);
2894
- };
2895
-
2896
- es.onerror = function() {
2897
- if (conn.status === 'connected') {
2898
- setStatus('disconnected');
2899
- }
2900
- if (handlers.error) handlers.error(new Error('SSE connection error'));
2901
- if (!reconnect) {
2902
- es.close();
2903
- }
2904
- // EventSource auto-reconnects by default when reconnect=true
2905
- };
2906
- } else if (transport === 'poll') {
2907
- var interval = opts.interval || 2000;
2908
- setStatus('connected');
2909
- conn._pollTimer = setInterval(function() {
2910
- fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
2911
- if (Array.isArray(msgs)) {
2912
- msgs.forEach(handleMessage);
2913
- } else if (msgs && msgs.type) {
2914
- handleMessage(msgs);
2915
- }
2916
- }).catch(function(e) {
2917
- if (handlers.error) handlers.error(e);
2918
- });
2919
- }, interval);
2920
- }
2921
-
2922
- /**
2923
- * Send an action to the server via POST.
2924
- * @param {string} action - Action name
2925
- * @param {Object} [data] - Action payload
2926
- */
2927
- conn.sendAction = function(action, data) {
2928
- var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
2929
- fetch(actionUrl, {
2930
- method: 'POST',
2931
- headers: { 'Content-Type': 'application/json' },
2932
- body: body
2933
- }).catch(function(e) {
2934
- if (handlers.error) handlers.error(e);
2935
- });
2936
- };
2937
-
2938
- /**
2939
- * Register an event handler.
2940
- * @param {string} event - 'open'|'message'|'error'|'close'
2941
- * @param {Function} handler
2942
- */
2943
- conn.on = function(event, handler) {
2944
- handlers[event] = handler;
2945
- return conn;
2946
- };
2947
-
2948
- /**
2949
- * Close the connection.
2950
- */
2951
- conn.close = function() {
2952
- if (conn._es) {
2953
- conn._es.close();
2954
- conn._es = null;
2955
- }
2956
- if (conn._pollTimer) {
2957
- clearInterval(conn._pollTimer);
2958
- conn._pollTimer = null;
2959
- }
2960
- setStatus('disconnected');
2961
- if (handlers.close) handlers.close();
2962
- };
2963
-
2964
- return conn;
2965
- };
2966
3299
 
2967
3300
  // ===================================================================================
2968
3301
  // bw.inspect() — Debug utility
@@ -2990,33 +3323,33 @@ bw.inspect = function(target) {
2990
3323
  el = target.element;
2991
3324
  comp = target;
2992
3325
  } else {
2993
- if (typeof target === 'string') {
3326
+ if (_is(target, 'string')) {
2994
3327
  el = bw.$(target)[0];
2995
3328
  }
2996
3329
  if (!el) {
2997
- console.warn('bw.inspect: element not found');
3330
+ _cw('bw.inspect: element not found');
2998
3331
  return null;
2999
3332
  }
3000
3333
  comp = el._bwComponentHandle;
3001
3334
  }
3002
3335
  if (!comp) {
3003
- console.log('bw.inspect: no ComponentHandle on this element');
3004
- console.log(' Tag:', el.tagName);
3005
- console.log(' Classes:', el.className);
3006
- console.log(' _bw_state:', el._bw_state || '(none)');
3336
+ _cl('bw.inspect: no ComponentHandle on this element');
3337
+ _cl(' Tag:', el.tagName);
3338
+ _cl(' Classes:', el.className);
3339
+ _cl(' _bw_state:', el._bw_state || '(none)');
3007
3340
  return null;
3008
3341
  }
3009
3342
  var deps = comp._bindings.reduce(function(s, b) {
3010
3343
  return s.concat(b.deps || []);
3011
3344
  }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
3012
3345
  console.group('Component: ' + comp._bwId);
3013
- console.log('State:', comp._state);
3014
- console.log('Bindings:', comp._bindings.length, '(deps:', deps, ')');
3015
- console.log('Methods:', Object.keys(comp._methods));
3016
- console.log('Actions:', Object.keys(comp._actions));
3017
- console.log('User tag:', comp._userTag || '(none)');
3018
- console.log('Mounted:', comp.mounted);
3019
- console.log('Element:', comp.element);
3346
+ _cl('State:', comp._state);
3347
+ _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
3348
+ _cl('Methods:', _keys(comp._methods));
3349
+ _cl('Actions:', _keys(comp._actions));
3350
+ _cl('User tag:', comp._userTag || '(none)');
3351
+ _cl('Mounted:', comp.mounted);
3352
+ _cl('Element:', comp.element);
3020
3353
  console.groupEnd();
3021
3354
  return comp;
3022
3355
  };
@@ -3039,8 +3372,8 @@ bw.compile = function(taco) {
3039
3372
  // Pre-extract all binding expressions
3040
3373
  var precompiled = [];
3041
3374
  function walkExpressions(node) {
3042
- if (!node || typeof node !== 'object') return;
3043
- if (typeof node.c === 'string' && node.c.indexOf('${') >= 0) {
3375
+ if (!_is(node, 'object')) return;
3376
+ if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
3044
3377
  var parsed = bw._parseBindings(node.c);
3045
3378
  for (var i = 0; i < parsed.length; i++) {
3046
3379
  try {
@@ -3055,9 +3388,9 @@ bw.compile = function(taco) {
3055
3388
  }
3056
3389
  if (node.a) {
3057
3390
  for (var key in node.a) {
3058
- if (Object.prototype.hasOwnProperty.call(node.a, key)) {
3391
+ if (_hop.call(node.a, key)) {
3059
3392
  var v = node.a[key];
3060
- if (typeof v === 'string' && v.indexOf('${') >= 0) {
3393
+ if (_is(v, 'string') && v.indexOf('${') >= 0) {
3061
3394
  var parsed2 = bw._parseBindings(v);
3062
3395
  for (var j = 0; j < parsed2.length; j++) {
3063
3396
  try {
@@ -3073,9 +3406,9 @@ bw.compile = function(taco) {
3073
3406
  }
3074
3407
  }
3075
3408
  }
3076
- if (Array.isArray(node.c)) {
3409
+ if (_isA(node.c)) {
3077
3410
  for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
3078
- } else if (node.c && typeof node.c === 'object' && node.c.t) {
3411
+ } else if (_is(node.c, 'object') && node.c.t) {
3079
3412
  walkExpressions(node.c);
3080
3413
  }
3081
3414
  }
@@ -3087,7 +3420,7 @@ bw.compile = function(taco) {
3087
3420
  handle._precompiledBindings = precompiled;
3088
3421
  if (initialState) {
3089
3422
  for (var k in initialState) {
3090
- if (Object.prototype.hasOwnProperty.call(initialState, k)) {
3423
+ if (_hop.call(initialState, k)) {
3091
3424
  handle._state[k] = initialState[k];
3092
3425
  }
3093
3426
  }
@@ -3118,18 +3451,18 @@ bw.compile = function(taco) {
3118
3451
  bw.css = function(rules, options = {}) {
3119
3452
  const { minify = false, pretty = !minify } = options;
3120
3453
 
3121
- if (typeof rules === 'string') return rules;
3454
+ if (_is(rules, 'string')) return rules;
3122
3455
 
3123
3456
  let css = '';
3124
3457
  const indent = pretty ? ' ' : '';
3125
3458
  const newline = pretty ? '\n' : '';
3126
3459
  const space = pretty ? ' ' : '';
3127
3460
 
3128
- if (Array.isArray(rules)) {
3461
+ if (_isA(rules)) {
3129
3462
  css = rules.map(rule => bw.css(rule, options)).join(newline);
3130
- } else if (typeof rules === 'object') {
3463
+ } else if (_is(rules, 'object')) {
3131
3464
  Object.entries(rules).forEach(([selector, styles]) => {
3132
- if (typeof styles === 'object' && !Array.isArray(styles)) {
3465
+ if (_is(styles, 'object')) {
3133
3466
  // Handle @media, @keyframes, @supports — recurse into nested block
3134
3467
  if (selector.charAt(0) === '@') {
3135
3468
  const inner = bw.css(styles, options);
@@ -3171,14 +3504,14 @@ bw.css = function(rules, options = {}) {
3171
3504
  * @returns {Element} The style element
3172
3505
  * @category CSS & Styling
3173
3506
  * @see bw.css
3174
- * @see bw.loadDefaultStyles
3507
+ * @see bw.loadStyles
3175
3508
  * @example
3176
3509
  * bw.injectCSS('.my-class { color: red; }');
3177
3510
  * bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
3178
3511
  */
3179
3512
  bw.injectCSS = function(css, options = {}) {
3180
3513
  if (!bw._isBrowser) {
3181
- console.warn('bw.injectCSS requires a DOM environment');
3514
+ _cw('bw.injectCSS requires a DOM environment');
3182
3515
  return null;
3183
3516
  }
3184
3517
 
@@ -3195,7 +3528,7 @@ bw.injectCSS = function(css, options = {}) {
3195
3528
  }
3196
3529
 
3197
3530
  // Convert CSS if needed
3198
- const cssStr = typeof css === 'string' ? css : bw.css(css, options);
3531
+ const cssStr = _is(css, 'string') ? css : bw.css(css, options);
3199
3532
 
3200
3533
  // Set or append CSS
3201
3534
  if (append && styleEl.textContent) {
@@ -3216,113 +3549,19 @@ bw.injectCSS = function(css, options = {}) {
3216
3549
  * @param {...Object} styles - Style objects to merge (left-to-right)
3217
3550
  * @returns {Object} Merged style object
3218
3551
  * @category CSS & Styling
3219
- * @see bw.u
3220
3552
  * @example
3221
- * var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
3553
+ * var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
3222
3554
  * // => { display: 'flex', gap: '1rem', color: 'red' }
3223
3555
  */
3224
3556
  bw.s = function() {
3225
3557
  var result = {};
3226
3558
  for (var i = 0; i < arguments.length; i++) {
3227
3559
  var arg = arguments[i];
3228
- if (arg && typeof arg === 'object') Object.assign(result, arg);
3560
+ if (_is(arg, 'object')) Object.assign(result, arg);
3229
3561
  }
3230
3562
  return result;
3231
3563
  };
3232
3564
 
3233
- /**
3234
- * Pre-built CSS utility objects (like Tailwind utilities, but in JS).
3235
- *
3236
- * Compose with `bw.s()` to build inline styles without writing raw CSS strings.
3237
- * Includes flex, padding, margin, typography, color, border, and transition utilities.
3238
- *
3239
- * @category CSS & Styling
3240
- * @see bw.s
3241
- * @example
3242
- * { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
3243
- * c: 'Flexbox with 1rem gap and padding' }
3244
- */
3245
- bw.u = {
3246
- // Display
3247
- flex: { display: 'flex' },
3248
- flexCol: { display: 'flex', flexDirection: 'column' },
3249
- flexRow: { display: 'flex', flexDirection: 'row' },
3250
- flexWrap: { display: 'flex', flexWrap: 'wrap' },
3251
- block: { display: 'block' },
3252
- inline: { display: 'inline' },
3253
- hidden: { display: 'none' },
3254
-
3255
- // Flex alignment
3256
- justifyCenter: { justifyContent: 'center' },
3257
- justifyBetween: { justifyContent: 'space-between' },
3258
- justifyEnd: { justifyContent: 'flex-end' },
3259
- alignCenter: { alignItems: 'center' },
3260
- alignStart: { alignItems: 'flex-start' },
3261
- alignEnd: { alignItems: 'flex-end' },
3262
-
3263
- // Gap (0.25rem increments)
3264
- gap1: { gap: '0.25rem' },
3265
- gap2: { gap: '0.5rem' },
3266
- gap3: { gap: '0.75rem' },
3267
- gap4: { gap: '1rem' },
3268
- gap6: { gap: '1.5rem' },
3269
- gap8: { gap: '2rem' },
3270
-
3271
- // Padding
3272
- p0: { padding: '0' },
3273
- p1: { padding: '0.25rem' },
3274
- p2: { padding: '0.5rem' },
3275
- p3: { padding: '0.75rem' },
3276
- p4: { padding: '1rem' },
3277
- p6: { padding: '1.5rem' },
3278
- p8: { padding: '2rem' },
3279
- px4: { paddingLeft: '1rem', paddingRight: '1rem' },
3280
- py2: { paddingTop: '0.5rem', paddingBottom: '0.5rem' },
3281
- py4: { paddingTop: '1rem', paddingBottom: '1rem' },
3282
-
3283
- // Margin (same scale)
3284
- m0: { margin: '0' },
3285
- m4: { margin: '1rem' },
3286
- mt2: { marginTop: '0.5rem' },
3287
- mt4: { marginTop: '1rem' },
3288
- mb2: { marginBottom: '0.5rem' },
3289
- mb4: { marginBottom: '1rem' },
3290
- mx_auto: { marginLeft: 'auto', marginRight: 'auto' },
3291
-
3292
- // Typography
3293
- textSm: { fontSize: '0.875rem' },
3294
- textBase: { fontSize: '1rem' },
3295
- textLg: { fontSize: '1.125rem' },
3296
- textXl: { fontSize: '1.25rem' },
3297
- text2xl: { fontSize: '1.5rem' },
3298
- text3xl: { fontSize: '1.875rem' },
3299
- bold: { fontWeight: '700' },
3300
- semibold: { fontWeight: '600' },
3301
- italic: { fontStyle: 'italic' },
3302
- textCenter: { textAlign: 'center' },
3303
- textRight: { textAlign: 'right' },
3304
-
3305
- // Colors (from design tokens)
3306
- bgWhite: { background: '#ffffff' },
3307
- bgTeal: { background: '#006666', color: '#ffffff' },
3308
- textWhite: { color: '#ffffff' },
3309
- textTeal: { color: '#006666' },
3310
- textMuted: { color: '#888' },
3311
-
3312
- // Borders
3313
- rounded: { borderRadius: '0.375rem' },
3314
- roundedLg: { borderRadius: '0.5rem' },
3315
- roundedFull: { borderRadius: '9999px' },
3316
- border: { border: '1px solid #d8d8d8' },
3317
-
3318
- // Sizing
3319
- wFull: { width: '100%' },
3320
- hFull: { height: '100%' },
3321
-
3322
- // Transitions
3323
- transition: { transition: 'all 0.2s ease' }
3324
- };
3325
-
3326
3565
  /**
3327
3566
  * Generate responsive CSS with media query breakpoints.
3328
3567
  *
@@ -3348,7 +3587,7 @@ bw.u = {
3348
3587
  bw.responsive = function(selector, breakpoints) {
3349
3588
  var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
3350
3589
  var parts = [];
3351
- Object.keys(breakpoints).forEach(function(key) {
3590
+ _keys(breakpoints).forEach(function(key) {
3352
3591
  var rules = {};
3353
3592
  if (key === 'base') {
3354
3593
  rules[selector] = breakpoints[key];
@@ -3420,18 +3659,18 @@ if (bw._isBrowser) {
3420
3659
  if (!selector) return [];
3421
3660
 
3422
3661
  // Already an array
3423
- if (Array.isArray(selector)) return selector;
3662
+ if (_isA(selector)) return selector;
3424
3663
 
3425
3664
  // Single element
3426
3665
  if (selector.nodeType) return [selector];
3427
3666
 
3428
3667
  // NodeList or HTMLCollection
3429
- if (selector.length !== undefined && typeof selector !== 'string') {
3668
+ if (selector.length !== undefined && !_is(selector, 'string')) {
3430
3669
  return Array.from(selector);
3431
3670
  }
3432
3671
 
3433
3672
  // CSS selector string
3434
- if (typeof selector === 'string') {
3673
+ if (_is(selector, 'string')) {
3435
3674
  return Array.from(document.querySelectorAll(selector));
3436
3675
  }
3437
3676
 
@@ -3444,103 +3683,49 @@ if (bw._isBrowser) {
3444
3683
  };
3445
3684
  }
3446
3685
 
3447
- /**
3448
- * Load the built-in Bootstrap-inspired default stylesheet.
3449
- *
3450
- * Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
3451
- * alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
3452
- * Returns null in Node.js (no DOM).
3453
- *
3454
- * @param {Object} [options] - Style loading options
3455
- * @param {boolean} [options.minify=true] - Minify the CSS output
3456
- * @returns {Element|null} Style element if in browser, null in Node.js
3457
- * @category CSS & Styling
3458
- * @see bw.setTheme
3459
- * @see bw.applyTheme
3460
- * @see bw.toggleTheme
3461
- * @example
3462
- * bw.loadDefaultStyles(); // inject all default CSS
3463
- */
3464
- bw.loadDefaultStyles = function(options = {}) {
3465
- const { minify = true, palette } = options;
3466
-
3467
- // 1. Inject structural CSS (layout, sizing — never changes with theme)
3468
- if (bw._isBrowser) {
3469
- var structuralCSS = bw.css(getStructuralStyles());
3470
- bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
3471
- }
3472
3686
 
3473
- // 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
3474
- var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
3475
- var result = bw.generateTheme('', Object.assign({}, paletteConfig, { inject: true }));
3476
- return result;
3477
- };
3687
+ // =========================================================================
3688
+ // v2.0.18 Clean Styles API makeStyles / applyStyles / loadStyles / etc.
3689
+ // =========================================================================
3478
3690
 
3691
+ /**
3692
+ * Convert a scope selector to a <style> element id.
3693
+ * @private
3694
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
3695
+ * @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
3696
+ */
3697
+ function _scopeToStyleId(scope) {
3698
+ if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
3699
+ if (scope === 'reset') return 'bw_style_reset';
3700
+ // Strip leading # or . and convert - to _
3701
+ var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
3702
+ return 'bw_style_' + clean;
3703
+ }
3479
3704
 
3480
3705
  /**
3481
- * Generate a complete, scoped theme from seed colors.
3706
+ * Generate a complete styles object from seed colors and layout config.
3707
+ * Pure function — no DOM, no state, no side effects.
3482
3708
  *
3483
- * Produces CSS for all themed components (buttons, alerts, badges, cards,
3484
- * forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
3485
- * scoped under `.name` class. Multiple themes can coexist in the stylesheet.
3486
- * Swap themes by changing the class on a container element.
3709
+ * All parameters are optional. Defaults to the bitwrench default palette.
3487
3710
  *
3488
- * @param {string} name - CSS scope class (e.g. 'ocean'). Empty string = unscoped global.
3489
- * @param {Object} config - Theme configuration
3490
- * @param {string} config.primary - Primary brand color hex
3491
- * @param {string} config.secondary - Secondary color hex
3492
- * @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
3493
- * @param {string} [config.success='#198754'] - Success color hex
3494
- * @param {string} [config.danger='#dc3545'] - Danger color hex
3495
- * @param {string} [config.warning='#ffc107'] - Warning color hex
3496
- * @param {string} [config.info='#0dcaf0'] - Info color hex
3497
- * @param {string} [config.light='#f8f9fa'] - Light color hex
3498
- * @param {string} [config.dark='#212529'] - Dark color hex
3499
- * @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
3500
- * @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
3711
+ * @param {Object} [config] - Style configuration
3712
+ * @param {string} [config.primary='#006666'] - Primary brand color hex
3713
+ * @param {string} [config.secondary='#6c757d'] - Secondary color hex
3714
+ * @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
3501
3715
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
3502
3716
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
3503
- * @param {number} [config.fontSize=1.0] - Base font size scale factor
3504
- * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
3505
- * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
3506
- * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
3507
- * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
3508
- * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
3509
- * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
3717
+ * @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
3510
3718
  * @category CSS & Styling
3511
- * @see bw.applyTheme
3512
- * @see bw.toggleTheme
3513
- * @see bw.loadDefaultStyles
3719
+ * @see bw.applyStyles
3720
+ * @see bw.loadStyles
3514
3721
  * @example
3515
- * // Generate and inject an ocean theme (primary + alternate)
3516
- * var theme = bw.generateTheme('ocean', {
3517
- * primary: '#0077b6',
3518
- * secondary: '#90e0ef',
3519
- * tertiary: '#00b4d8'
3520
- * });
3521
- *
3522
- * // Apply to a container
3523
- * document.getElementById('app').classList.add('ocean');
3524
- *
3525
- * // Toggle to alternate palette
3526
- * bw.toggleTheme();
3527
- *
3528
- * // Generate CSS for static export (Node.js)
3529
- * var result = bw.generateTheme('sunset', {
3530
- * primary: '#e76f51',
3531
- * secondary: '#264653',
3532
- * inject: false
3533
- * });
3534
- * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
3722
+ * var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
3723
+ * console.log(styles.palette.primary.base); // '#4f46e5'
3724
+ * // styles.css contains all themed CSS — nothing injected
3535
3725
  */
3536
- bw.generateTheme = function(name, config) {
3537
- if (!config || !config.primary || !config.secondary) {
3538
- throw new Error('bw.generateTheme requires config.primary and config.secondary');
3539
- }
3540
-
3541
- // Merge with defaults; if user didn't supply tertiary, default to their primary
3542
- var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
3543
- if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
3726
+ bw.makeStyles = function(config) {
3727
+ var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
3728
+ if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
3544
3729
 
3545
3730
  // Derive primary palette
3546
3731
  var palette = derivePalette(fullConfig);
@@ -3548,131 +3733,207 @@ bw.generateTheme = function(name, config) {
3548
3733
  // Resolve layout
3549
3734
  var layout = resolveLayout(fullConfig);
3550
3735
 
3551
- // Generate primary themed CSS rules
3552
- var themedRules = generateThemedCSS(name, palette, layout);
3736
+ // Generate primary themed CSS rules (unscoped)
3737
+ var themedRules = generateThemedCSS('', palette, layout);
3553
3738
  var cssStr = bw.css(themedRules);
3554
3739
 
3555
3740
  // Derive alternate palette (luminance-inverted)
3556
3741
  var altConfig = deriveAlternateConfig(fullConfig);
3557
3742
  var altPalette = derivePalette(altConfig);
3558
3743
 
3559
- // Generate alternate CSS scoped under .bw_theme_alt
3560
- var altRules = generateAlternateCSS(name, altPalette, layout);
3561
- var altCssStr = bw.css(altRules);
3744
+ // Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
3745
+ // applyStyles() wraps them appropriately based on scope
3746
+ var altRawRules = generateThemedCSS('', altPalette, layout);
3747
+
3748
+ // Add body-level surface overrides for the alternate palette.
3749
+ // When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
3750
+ altRawRules['body'] = {
3751
+ 'color': altPalette.dark.base,
3752
+ 'background-color': altPalette.surface || altPalette.light.base
3753
+ };
3754
+
3755
+ var altCssStr = bw.css(altRawRules);
3562
3756
 
3563
3757
  // Determine if primary is light-flavored
3564
3758
  var lightPrimary = isLightPalette(fullConfig);
3565
3759
 
3566
- // Inject both CSS sets into DOM if requested
3567
- var shouldInject = config.inject !== false;
3568
- if (shouldInject && bw._isBrowser) {
3569
- var safeName = name ? name.replace(/-/g, '_') : '';
3570
- var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
3571
- var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
3572
-
3573
- bw.injectCSS(cssStr, { id: styleId, append: false });
3574
- bw.injectCSS(altCssStr, { id: altStyleId, append: false });
3760
+ return {
3761
+ css: cssStr,
3762
+ alternateCss: altCssStr,
3763
+ rules: themedRules,
3764
+ alternateRules: altRawRules,
3765
+ palette: palette,
3766
+ alternatePalette: altPalette,
3767
+ isLightPrimary: lightPrimary
3768
+ };
3769
+ };
3575
3770
 
3576
- bw._activeThemeStyleIds = [styleId, altStyleId];
3771
+ /**
3772
+ * Inject styles into the DOM with optional scoping.
3773
+ *
3774
+ * Takes a styles object from `makeStyles()` and creates a single `<style>`
3775
+ * element in `<head>`. If a scope selector is provided, all CSS rules are
3776
+ * wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
3777
+ *
3778
+ * @param {Object} styles - Result of `bw.makeStyles()`
3779
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
3780
+ * @returns {Element|null} The `<style>` element, or null in Node.js
3781
+ * @category CSS & Styling
3782
+ * @see bw.makeStyles
3783
+ * @see bw.loadStyles
3784
+ * @see bw.clearStyles
3785
+ * @example
3786
+ * var styles = bw.makeStyles({ primary: '#4f46e5' });
3787
+ * bw.applyStyles(styles); // global
3788
+ * bw.applyStyles(styles, '#my-dashboard'); // scoped
3789
+ */
3790
+ bw.applyStyles = function(styles, scope) {
3791
+ if (!bw._isBrowser) return null;
3792
+ if (!styles || !styles.rules) {
3793
+ _cw('bw.applyStyles: invalid styles object');
3794
+ return null;
3577
3795
  }
3578
3796
 
3579
- // Update bw.u color entries to reflect the palette
3580
- if (!name) {
3581
- bw.u.bgTeal = { background: palette.primary.base, color: palette.primary.textOn };
3582
- bw.u.textTeal = { color: palette.primary.base };
3583
- bw.u.bgWhite = { background: '#ffffff' };
3584
- bw.u.textWhite = { color: '#ffffff' };
3797
+ var styleId = _scopeToStyleId(scope);
3798
+
3799
+ // Scope the primary rules if a scope is provided
3800
+ var primaryRules = styles.rules;
3801
+ if (scope) {
3802
+ primaryRules = scopeRulesUnder(primaryRules, scope);
3585
3803
  }
3586
3804
 
3587
- // Store active theme state
3588
- var result = {
3589
- css: cssStr,
3590
- palette: palette,
3591
- name: name,
3592
- isLightPrimary: lightPrimary,
3593
- alternate: {
3594
- css: altCssStr,
3595
- palette: altPalette
3805
+ // Wrap alternate rules with .bw_theme_alt
3806
+ var altRules = styles.alternateRules;
3807
+ if (altRules) {
3808
+ if (scope) {
3809
+ // Scoped compound: #scope.bw_theme_alt .bw_card
3810
+ altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
3811
+ } else {
3812
+ // Global: .bw_theme_alt .bw_card
3813
+ altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
3596
3814
  }
3597
- };
3598
- bw._activeTheme = result;
3599
- bw._activeThemeMode = 'primary';
3815
+ }
3600
3816
 
3601
- return result;
3817
+ // Combine primary + alternate into one CSS string
3818
+ var combined = bw.css(primaryRules);
3819
+ if (altRules) {
3820
+ combined += '\n' + bw.css(altRules);
3821
+ }
3822
+
3823
+ return bw.injectCSS(combined, { id: styleId, append: false });
3602
3824
  };
3603
3825
 
3604
3826
  /**
3605
- * Apply a theme mode. Switches between primary and alternate palettes
3606
- * by adding/removing the `bw_theme_alt` class on `<html>`.
3827
+ * Generate and apply styles in one call. Convenience wrapper.
3828
+ *
3829
+ * Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
3607
3830
  *
3608
- * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
3609
- * @returns {string} Active mode: 'primary' or 'alternate'
3831
+ * @param {Object} [config] - Style configuration (same as `makeStyles`)
3832
+ * @param {string} [scope] - Scope selector (same as `applyStyles`)
3833
+ * @returns {Element|null} The `<style>` element, or null in Node.js
3610
3834
  * @category CSS & Styling
3611
- * @see bw.generateTheme
3612
- * @see bw.toggleTheme
3835
+ * @see bw.makeStyles
3836
+ * @see bw.applyStyles
3613
3837
  * @example
3614
- * bw.applyTheme('alternate'); // switch to alternate palette
3615
- * bw.applyTheme('dark'); // switch to whichever palette is darker
3616
- * bw.applyTheme('primary'); // switch back to primary palette
3838
+ * bw.loadStyles(); // defaults, global
3839
+ * bw.loadStyles({ primary: '#4f46e5' }); // custom, global
3840
+ * bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
3617
3841
  */
3618
- bw.applyTheme = function(mode) {
3619
- if (!bw._isBrowser) return mode || 'primary';
3620
- var root = document.documentElement;
3621
- var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
3622
-
3623
- var wantAlt;
3624
- if (mode === 'primary') wantAlt = false;
3625
- else if (mode === 'alternate') wantAlt = true;
3626
- else if (mode === 'light') wantAlt = !isLight;
3627
- else if (mode === 'dark') wantAlt = isLight;
3628
- else wantAlt = false;
3629
-
3630
- if (wantAlt) {
3631
- root.classList.add('bw_theme_alt');
3632
- } else {
3633
- root.classList.remove('bw_theme_alt');
3842
+ bw.loadStyles = function(config, scope) {
3843
+ // Also inject structural CSS first (only once)
3844
+ if (bw._isBrowser) {
3845
+ var existing = document.getElementById('bw_structural');
3846
+ if (!existing) {
3847
+ var structuralCSS = bw.css(getStructuralStyles());
3848
+ bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
3849
+ }
3634
3850
  }
3851
+ return bw.applyStyles(bw.makeStyles(config), scope);
3852
+ };
3635
3853
 
3636
- bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
3637
- return bw._activeThemeMode;
3854
+ /**
3855
+ * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
3856
+ * Idempotent — if already injected, returns the existing `<style>` element.
3857
+ *
3858
+ * @returns {Element|null} The `<style>` element, or null in Node.js
3859
+ * @category CSS & Styling
3860
+ * @see bw.loadStyles
3861
+ * @see bw.clearStyles
3862
+ * @example
3863
+ * bw.loadReset(); // inject once, safe to call multiple times
3864
+ */
3865
+ bw.loadReset = function() {
3866
+ if (!bw._isBrowser) return null;
3867
+ var existing = document.getElementById('bw_style_reset');
3868
+ if (existing) return existing;
3869
+ return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
3638
3870
  };
3639
3871
 
3640
3872
  /**
3641
- * Toggle between primary and alternate theme palettes.
3873
+ * Toggle between primary and alternate palettes.
3642
3874
  *
3875
+ * Adds/removes the `bw_theme_alt` class on the scoping element.
3876
+ * Without a scope, toggles on `<html>` (global).
3877
+ * With a scope, toggles on the first matching element.
3878
+ *
3879
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
3643
3880
  * @returns {string} Active mode after toggle: 'primary' or 'alternate'
3644
3881
  * @category CSS & Styling
3645
- * @see bw.applyTheme
3646
- * @see bw.generateTheme
3882
+ * @see bw.applyStyles
3883
+ * @see bw.clearStyles
3647
3884
  * @example
3648
- * bw.toggleTheme(); // flip between primary and alternate
3885
+ * bw.toggleStyles(); // global toggle on <html>
3886
+ * bw.toggleStyles('#my-dashboard'); // scoped toggle
3649
3887
  */
3650
- bw.toggleTheme = function() {
3651
- var current = bw._activeThemeMode || 'primary';
3652
- return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
3888
+ bw.toggleStyles = function(scope) {
3889
+ if (!bw._isBrowser) return 'primary';
3890
+ var target;
3891
+ if (scope) {
3892
+ var els = bw.$(scope);
3893
+ target = els[0];
3894
+ } else {
3895
+ target = document.documentElement;
3896
+ }
3897
+ if (!target) return 'primary';
3898
+
3899
+ var hasAlt = target.classList.contains('bw_theme_alt');
3900
+ if (hasAlt) {
3901
+ target.classList.remove('bw_theme_alt');
3902
+ return 'primary';
3903
+ } else {
3904
+ target.classList.add('bw_theme_alt');
3905
+ return 'alternate';
3906
+ }
3653
3907
  };
3654
3908
 
3655
3909
  /**
3656
- * Remove the currently active theme's injected style elements from the DOM.
3657
- * Use this before generating a new theme with a different name to prevent
3658
- * stale CSS accumulation.
3910
+ * Remove injected styles for a given scope.
3911
+ *
3912
+ * Finds the `<style>` element by id and removes it. Also removes
3913
+ * the `bw_theme_alt` class from the relevant element.
3659
3914
  *
3915
+ * @param {string} [scope] - Scope selector. Omit to remove global styles.
3660
3916
  * @category CSS & Styling
3661
- * @see bw.generateTheme
3917
+ * @see bw.applyStyles
3918
+ * @see bw.loadStyles
3662
3919
  * @example
3663
- * bw.clearTheme(); // remove current theme styles
3664
- * bw.generateTheme('sunset', conf); // inject fresh theme
3920
+ * bw.clearStyles(); // remove global styles
3921
+ * bw.clearStyles('#my-dashboard'); // remove scoped styles
3922
+ * bw.clearStyles('reset'); // remove the CSS reset
3665
3923
  */
3666
- bw.clearTheme = function() {
3667
- if (bw._activeThemeStyleIds && bw._isBrowser) {
3668
- bw._activeThemeStyleIds.forEach(function(id) {
3669
- var el = document.getElementById(id);
3670
- if (el) el.remove();
3671
- });
3672
- bw._activeThemeStyleIds = null;
3924
+ bw.clearStyles = function(scope) {
3925
+ if (!bw._isBrowser) return;
3926
+ var styleId = _scopeToStyleId(scope);
3927
+ var el = document.getElementById(styleId);
3928
+ if (el) el.remove();
3929
+
3930
+ // Also remove bw_theme_alt from the relevant element
3931
+ if (scope && scope !== 'reset' && scope !== 'global') {
3932
+ var targets = bw.$(scope);
3933
+ if (targets[0]) targets[0].classList.remove('bw_theme_alt');
3934
+ } else if (!scope || scope === 'global') {
3935
+ document.documentElement.classList.remove('bw_theme_alt');
3673
3936
  }
3674
- bw._activeTheme = null;
3675
- bw._activeThemeMode = 'primary';
3676
3937
  };
3677
3938
 
3678
3939
  // Expose color utility functions on bw namespace
@@ -3895,10 +4156,15 @@ bw.copyToClipboard = function(text) {
3895
4156
  * @param {Object} config - Table configuration
3896
4157
  * @param {Array<Object>} config.data - Array of row objects to display
3897
4158
  * @param {Array<Object>} [config.columns] - Column definitions with key, label, render
3898
- * @param {string} [config.className='table'] - CSS class for table element
4159
+ * @param {string} [config.className=''] - Additional CSS classes for table element
3899
4160
  * @param {boolean} [config.sortable=true] - Enable click-to-sort headers
3900
4161
  * @param {Function} [config.onSort] - Sort callback (column, direction)
3901
- * @returns {Object} TACO object for table
4162
+ * @param {boolean} [config.selectable=false] - Enable row selection on click
4163
+ * @param {Function} [config.onRowClick] - Row click callback (row, index, event)
4164
+ * @param {number} [config.pageSize] - Rows per page (enables pagination when set)
4165
+ * @param {number} [config.currentPage=1] - Current page number (1-based)
4166
+ * @param {Function} [config.onPageChange] - Page change callback (newPage)
4167
+ * @returns {Object} TACO object for table (with optional pagination controls)
3902
4168
  * @category Component Builders
3903
4169
  * @see bw.makeDataTable
3904
4170
  * @example
@@ -3910,7 +4176,12 @@ bw.copyToClipboard = function(text) {
3910
4176
  * columns: [
3911
4177
  * { key: 'name', label: 'Name' },
3912
4178
  * { key: 'age', label: 'Age' }
3913
- * ]
4179
+ * ],
4180
+ * selectable: true,
4181
+ * onRowClick: function(row, i) { console.log('clicked', row.name); },
4182
+ * pageSize: 10,
4183
+ * currentPage: 1,
4184
+ * onPageChange: function(page) { console.log('page', page); }
3914
4185
  * });
3915
4186
  */
3916
4187
  bw.makeTable = function(config) {
@@ -3923,41 +4194,47 @@ bw.makeTable = function(config) {
3923
4194
  sortable = true,
3924
4195
  onSort,
3925
4196
  sortColumn,
3926
- sortDirection = 'asc'
4197
+ sortDirection = 'asc',
4198
+ selectable = false,
4199
+ onRowClick,
4200
+ pageSize,
4201
+ currentPage = 1,
4202
+ onPageChange
3927
4203
  } = config;
3928
4204
 
3929
- // Build class list: always include bw_table, add striped/hover, append user className
4205
+ // Build class list: always include bw_table, add striped/hover/selectable, append user className
3930
4206
  let cls = 'bw_table';
3931
4207
  if (striped) cls += ' bw_table_striped';
3932
- if (hover) cls += ' bw_table_hover';
4208
+ if (hover || selectable) cls += ' bw_table_hover';
4209
+ if (selectable) cls += ' bw_table_selectable';
3933
4210
  if (className) cls += ' ' + className;
3934
4211
  cls = cls.trim();
3935
-
4212
+
3936
4213
  // Auto-detect columns if not provided
3937
- const cols = columns || (data.length > 0
3938
- ? Object.keys(data[0]).map(key => ({ key, label: key }))
4214
+ const cols = columns || (data.length > 0
4215
+ ? _keys(data[0]).map(key => ({ key, label: key }))
3939
4216
  : []);
3940
-
4217
+
3941
4218
  // Current sort state
3942
4219
  let currentSortColumn = sortColumn || null;
3943
4220
  let currentSortDirection = sortDirection;
3944
-
4221
+
3945
4222
  // Sort data if column specified
3946
4223
  let sortedData = [...data];
3947
4224
  if (currentSortColumn) {
3948
4225
  sortedData.sort((a, b) => {
3949
4226
  const aVal = a[currentSortColumn];
3950
4227
  const bVal = b[currentSortColumn];
3951
-
4228
+
3952
4229
  // Handle different types
3953
- if (typeof aVal === 'number' && typeof bVal === 'number') {
4230
+ if (_is(aVal, 'number') && _is(bVal, 'number')) {
3954
4231
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
3955
4232
  }
3956
-
4233
+
3957
4234
  // String comparison
3958
4235
  const aStr = String(aVal || '').toLowerCase();
3959
4236
  const bStr = String(bVal || '').toLowerCase();
3960
-
4237
+
3961
4238
  if (currentSortDirection === 'asc') {
3962
4239
  return aStr.localeCompare(bStr);
3963
4240
  } else {
@@ -3965,23 +4242,32 @@ bw.makeTable = function(config) {
3965
4242
  }
3966
4243
  });
3967
4244
  }
3968
-
4245
+
4246
+ // Pagination
4247
+ const totalRows = sortedData.length;
4248
+ const totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
4249
+ const page = Math.max(1, Math.min(currentPage, totalPages));
4250
+ if (pageSize) {
4251
+ const start = (page - 1) * pageSize;
4252
+ sortedData = sortedData.slice(start, start + pageSize);
4253
+ }
4254
+
3969
4255
  // Create sort handler
3970
4256
  const handleSort = (column) => {
3971
4257
  if (!sortable) return;
3972
-
4258
+
3973
4259
  if (currentSortColumn === column) {
3974
4260
  currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
3975
4261
  } else {
3976
4262
  currentSortColumn = column;
3977
4263
  currentSortDirection = 'asc';
3978
4264
  }
3979
-
4265
+
3980
4266
  if (onSort) {
3981
4267
  onSort(column, currentSortDirection);
3982
4268
  }
3983
4269
  };
3984
-
4270
+
3985
4271
  // Build table header
3986
4272
  const thead = {
3987
4273
  t: 'thead',
@@ -4004,24 +4290,87 @@ bw.makeTable = function(config) {
4004
4290
  }))
4005
4291
  }
4006
4292
  };
4007
-
4008
- // Build table body
4293
+
4294
+ // Build table body with selectable/onRowClick support
4009
4295
  const tbody = {
4010
4296
  t: 'tbody',
4011
- c: sortedData.map(row => ({
4012
- t: 'tr',
4013
- c: cols.map(col => ({
4014
- t: 'td',
4015
- c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
4016
- }))
4017
- }))
4297
+ c: sortedData.map((row, idx) => {
4298
+ const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
4299
+ const rowAttrs = {};
4300
+ if (selectable || onRowClick) {
4301
+ rowAttrs.style = 'cursor:pointer;';
4302
+ rowAttrs.onclick = function(e) {
4303
+ if (selectable) {
4304
+ // Toggle selected class on this row
4305
+ var tr = e.currentTarget;
4306
+ tr.classList.toggle('bw_table_row_selected');
4307
+ }
4308
+ if (onRowClick) {
4309
+ onRowClick(row, globalIdx, e);
4310
+ }
4311
+ };
4312
+ }
4313
+ return {
4314
+ t: 'tr',
4315
+ a: rowAttrs,
4316
+ c: cols.map(col => ({
4317
+ t: 'td',
4318
+ c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
4319
+ }))
4320
+ };
4321
+ })
4018
4322
  };
4019
-
4020
- return {
4323
+
4324
+ const table = {
4021
4325
  t: 'table',
4022
4326
  a: { class: cls },
4023
4327
  c: [thead, tbody]
4024
4328
  };
4329
+
4330
+ // If no pagination, return table directly
4331
+ if (!pageSize) return table;
4332
+
4333
+ // Build pagination controls
4334
+ const pageButtons = [];
4335
+ // Previous button
4336
+ pageButtons.push({
4337
+ t: 'button',
4338
+ a: {
4339
+ class: 'bw_btn bw_btn_sm',
4340
+ disabled: page <= 1 ? 'disabled' : undefined,
4341
+ onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
4342
+ },
4343
+ c: 'Prev'
4344
+ });
4345
+ // Page info
4346
+ pageButtons.push({
4347
+ t: 'span',
4348
+ a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
4349
+ c: 'Page ' + page + ' of ' + totalPages
4350
+ });
4351
+ // Next button
4352
+ pageButtons.push({
4353
+ t: 'button',
4354
+ a: {
4355
+ class: 'bw_btn bw_btn_sm',
4356
+ disabled: page >= totalPages ? 'disabled' : undefined,
4357
+ onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
4358
+ },
4359
+ c: 'Next'
4360
+ });
4361
+
4362
+ return {
4363
+ t: 'div',
4364
+ a: { class: 'bw_table_paginated' },
4365
+ c: [
4366
+ table,
4367
+ {
4368
+ t: 'div',
4369
+ a: { class: 'bw_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
4370
+ c: pageButtons
4371
+ }
4372
+ ]
4373
+ };
4025
4374
  };
4026
4375
 
4027
4376
  /**
@@ -4060,7 +4409,7 @@ bw.makeTable = function(config) {
4060
4409
  bw.makeTableFromArray = function(config) {
4061
4410
  const { data = [], headerRow = true, columns, ...rest } = config;
4062
4411
 
4063
- if (!Array.isArray(data) || data.length === 0) {
4412
+ if (!_isA(data) || data.length === 0) {
4064
4413
  return bw.makeTable({ data: [], columns: columns || [], ...rest });
4065
4414
  }
4066
4415
 
@@ -4142,7 +4491,7 @@ bw.makeBarChart = function(config) {
4142
4491
  className = ''
4143
4492
  } = config;
4144
4493
 
4145
- if (!Array.isArray(data) || data.length === 0) {
4494
+ if (!_isA(data) || data.length === 0) {
4146
4495
  return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
4147
4496
  }
4148
4497
 
@@ -4291,7 +4640,7 @@ bw._componentRegistry = new Map();
4291
4640
  */
4292
4641
  bw.render = function(element, position, taco) {
4293
4642
  // Get target element
4294
- const targetEl = typeof element === 'string'
4643
+ const targetEl = _is(element, 'string')
4295
4644
  ? document.querySelector(element)
4296
4645
  : element;
4297
4646
 
@@ -4441,7 +4790,7 @@ bw.render = function(element, position, taco) {
4441
4790
  setContent(content) {
4442
4791
  this._taco.c = content;
4443
4792
  if (this.element) {
4444
- if (typeof content === 'string') {
4793
+ if (_is(content, 'string')) {
4445
4794
  this.element.textContent = content;
4446
4795
  } else {
4447
4796
  // Re-render for complex content