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
@@ -1,24 +1,25 @@
1
- /*! bitwrench-lean v2.0.16 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench-lean v2.0.18 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
5
5
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.bw = factory());
6
6
  })(this, (function () { 'use strict';
7
7
 
8
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
8
9
  /**
9
10
  * Auto-generated version file from package.json
10
11
  * DO NOT EDIT DIRECTLY - Use npm run generate-version
11
12
  */
12
13
 
13
14
  const VERSION_INFO = {
14
- version: '2.0.16',
15
+ version: '2.0.18',
15
16
  name: 'bitwrench',
16
17
  description: 'A library for javascript UI functions.',
17
18
  license: 'BSD-2-Clause',
18
19
  homepage: 'https://deftio.github.com/bitwrench/pages',
19
20
  repository: 'git+https://github.com/deftio/bitwrench.git',
20
21
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
21
- buildDate: '2026-03-12T08:05:52.043Z'
22
+ buildDate: '2026-03-17T00:50:09.505Z'
22
23
  };
23
24
 
24
25
  /**
@@ -312,13 +313,18 @@
312
313
  */
313
314
  function deriveShades(hex) {
314
315
  var rgb = colorParse(hex);
316
+ // For light input colors (L > 75), mixing toward white produces invisible borders.
317
+ // Darken instead so borders remain visible against light backgrounds.
318
+ var borderColor = hexToHsl(hex)[2] > 75
319
+ ? adjustLightness(hex, -18)
320
+ : mixColor(hex, '#ffffff', 0.60);
315
321
  return {
316
322
  base: hex,
317
323
  hover: adjustLightness(hex, -10),
318
324
  active: adjustLightness(hex, -15),
319
325
  light: mixColor(hex, '#ffffff', 0.85),
320
326
  darkText: adjustLightness(hex, -40),
321
- border: mixColor(hex, '#ffffff', 0.60),
327
+ border: borderColor,
322
328
  focus: 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',0.25)',
323
329
  textOn: textOnColor(hex)
324
330
  };
@@ -377,19 +383,27 @@
377
383
  alt.secondary = deriveAlternateSeed(config.secondary);
378
384
  alt.tertiary = config.tertiary ? deriveAlternateSeed(config.tertiary) : alt.primary;
379
385
 
380
- // Derive alternate surface colors from primary hue
386
+ // Derive alternate surface colors from primary hue.
387
+ // Check actual page surface brightness (not seed color brightness) to decide
388
+ // whether alternate should be dark or light. The page surface is what the
389
+ // user sees; seeds can be dark while the page is still light (default L=96).
381
390
  var priHsl = hexToHsl(config.primary);
382
391
  var h = priHsl[0];
383
- var isLight = isLightPalette(config);
392
+ var primarySurface = config.surface || hslToHex([h, 8, 96]);
393
+ var isLight = relativeLuminance(primarySurface) > 0.179;
384
394
 
385
395
  if (isLight) {
386
- // Primary is light → alternate needs dark surfaces
396
+ // Page surface is light → alternate needs dark surfaces
387
397
  alt.light = hslToHex([h, Math.min(priHsl[1], 15), 15]);
388
398
  alt.dark = hslToHex([h, 5, 88]);
399
+ alt.surface = hslToHex([h, 12, 18]);
400
+ alt.background = hslToHex([h, 10, 14]);
389
401
  } else {
390
- // Primary is dark → alternate needs light surfaces
402
+ // Page surface is dark → alternate needs light surfaces
391
403
  alt.light = hslToHex([h, Math.min(priHsl[1], 10), 96]);
392
404
  alt.dark = hslToHex([h, 10, 18]);
405
+ alt.surface = hslToHex([h, 8, 96]);
406
+ alt.background = hslToHex([h, 6, 98]);
393
407
  }
394
408
 
395
409
  // Semantic colors: harmonize toward primary, then invert for alternate
@@ -437,10 +451,18 @@
437
451
  var darkBase = config.dark || hslToHex([h, 10, 13]);
438
452
 
439
453
  // Background & surface tokens — tinted with primary hue for theme personality.
440
- // Very subtle: bg at L=98/S=6, surface at L=96/S=8.
454
+ // Saturation high enough that the hue is visible (each theme feels distinct)
455
+ // but low enough to stay neutral and readable.
441
456
  // User can override with config.background / config.surface.
442
- var bgBase = config.background || hslToHex([h, 6, 98]);
443
- var surfBase = config.surface || hslToHex([h, 8, 96]);
457
+ var bgBase = config.background || hslToHex([h, 22, 96]);
458
+ var surfBase = config.surface || hslToHex([h, 25, 94]);
459
+
460
+ // surfaceAlt: subtle background variant for striped rows, hover states, headers.
461
+ // Slightly lighter than surface in dark mode, slightly darker in light mode.
462
+ var surfHsl = hexToHsl(surfBase);
463
+ var surfAlt = surfHsl[2] <= 50
464
+ ? hslToHex([surfHsl[0], surfHsl[1], Math.min(surfHsl[2] + 8, 100)])
465
+ : hslToHex([surfHsl[0], surfHsl[1], Math.max(surfHsl[2] - 3, 0)]);
444
466
 
445
467
  var palette = {
446
468
  primary: deriveShades(config.primary),
@@ -453,7 +475,8 @@
453
475
  light: deriveShades(lightBase),
454
476
  dark: deriveShades(darkBase),
455
477
  background: bgBase,
456
- surface: surfBase
478
+ surface: surfBase,
479
+ surfaceAlt: surfAlt
457
480
  };
458
481
 
459
482
  return palette;
@@ -500,10 +523,12 @@
500
523
  5: '1.5rem', // 24px
501
524
  6: '2rem'};
502
525
 
526
+ let _S=SPACING_SCALE;
527
+
503
528
  var SPACING_PRESETS = {
504
- compact: { btn: SPACING_SCALE[1] + ' ' + SPACING_SCALE[3], card: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4], alert: SPACING_SCALE[2] + ' ' + SPACING_SCALE[4], cell: SPACING_SCALE[2] + ' ' + SPACING_SCALE[3], input: SPACING_SCALE[1] + ' ' + SPACING_SCALE[3] },
505
- normal: { btn: SPACING_SCALE[2] + ' ' + SPACING_SCALE[4], card: SPACING_SCALE[5] + ' ' + SPACING_SCALE[5], alert: SPACING_SCALE[3] + ' ' + SPACING_SCALE[5], cell: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4], input: SPACING_SCALE[2] + ' ' + SPACING_SCALE[3] },
506
- spacious: { btn: SPACING_SCALE[3] + ' ' + SPACING_SCALE[5], card: SPACING_SCALE[6] + ' ' + SPACING_SCALE[6], alert: SPACING_SCALE[4] + ' ' + SPACING_SCALE[5], cell: SPACING_SCALE[4] + ' ' + SPACING_SCALE[5], input: SPACING_SCALE[3] + ' ' + SPACING_SCALE[4] }
529
+ compact: { btn: _S[1] + ' ' + _S[3], card: _S[3] + ' ' + _S[4], alert: _S[2] + ' ' + _S[4], cell: _S[2] + ' ' + _S[3], input: _S[1] + ' ' + _S[3] },
530
+ normal: { btn: _S[2] + ' ' + _S[4], card: _S[5] + ' ' + _S[5], alert: _S[3] + ' ' + _S[5], cell: _S[3] + ' ' + _S[4], input: _S[2] + ' ' + _S[3] },
531
+ spacious: { btn: _S[3] + ' ' + _S[5], card: _S[6] + ' ' + _S[6], alert: _S[4] + ' ' + _S[5], cell: _S[4] + ' ' + _S[5], input: _S[3] + ' ' + _S[4] }
507
532
  };
508
533
 
509
534
  var RADIUS_PRESETS = {
@@ -615,20 +640,14 @@
615
640
  * Built-in theme presets — named color combinations
616
641
  * Each preset provides primary, secondary, and tertiary seed colors.
617
642
  */
618
- var THEME_PRESETS = {
619
- teal: { primary: '#006666', secondary: '#6c757d', tertiary: '#006666' },
620
- ocean: { primary: '#0077b6', secondary: '#90e0ef', tertiary: '#00b4d8' },
621
- sunset: { primary: '#e76f51', secondary: '#264653', tertiary: '#e9c46a' },
622
- forest: { primary: '#2d6a4f', secondary: '#95d5b2', tertiary: '#52b788' },
623
- slate: { primary: '#343a40', secondary: '#adb5bd', tertiary: '#6c757d' },
624
- rose: { primary: '#e11d48', secondary: '#fda4af', tertiary: '#fb7185' },
625
- indigo: { primary: '#4f46e5', secondary: '#a5b4fc', tertiary: '#818cf8' },
626
- amber: { primary: '#d97706', secondary: '#fbbf24', tertiary: '#f59e0b' },
627
- emerald: { primary: '#059669', secondary: '#6ee7b7', tertiary: '#34d399' },
628
- nord: { primary: '#5e81ac', secondary: '#88c0d0', tertiary: '#81a1c1' },
629
- coral: { primary: '#ef6461', secondary: '#4a7c7e', tertiary: '#e8a87c' },
630
- midnight: { primary: '#1e3a5f', secondary: '#7c8db5', tertiary: '#3d5a80' }
631
- };
643
+ var THEME_PRESETS = Object.fromEntries([
644
+ ['teal','#006666','#6c757d','#006666'],['ocean','#0077b6','#90e0ef','#00b4d8'],
645
+ ['sunset','#e76f51','#264653','#e9c46a'],['forest','#2d6a4f','#95d5b2','#52b788'],
646
+ ['slate','#343a40','#adb5bd','#6c757d'],['rose','#e11d48','#fda4af','#fb7185'],
647
+ ['indigo','#4f46e5','#a5b4fc','#818cf8'],['amber','#d97706','#fbbf24','#f59e0b'],
648
+ ['emerald','#059669','#6ee7b7','#34d399'],['nord','#5e81ac','#88c0d0','#81a1c1'],
649
+ ['coral','#ef6461','#4a7c7e','#e8a87c'],['midnight','#1e3a5f','#7c8db5','#3d5a80']
650
+ ].map(function(e) { return [e[0], {primary:e[1],secondary:e[2],tertiary:e[3]}]; }));
632
651
 
633
652
  /**
634
653
  * Resolve layout config to spacing, radius, typeScale, elevation, and motion objects.
@@ -675,6 +694,7 @@
675
694
  if (sel.includes(',')) return sel.split(',').map(function(s) { return '.' + name + ' ' + s.trim(); }).join(', ');
676
695
  return '.' + name + ' ' + sel;
677
696
  }
697
+ var _sx=scopeSelector;
678
698
 
679
699
  // =========================================================================
680
700
  // Themed CSS generators
@@ -683,12 +703,12 @@
683
703
  function generateTypographyThemed(scope, palette, layout) {
684
704
  var mot = layout.motion;
685
705
  var rules = {};
686
- rules[scopeSelector(scope, 'a')] = {
706
+ rules[_sx(scope, 'a')] = {
687
707
  'color': palette.primary.base,
688
708
  'text-decoration': 'none',
689
709
  'transition': 'color ' + mot.fast + ' ' + mot.easing
690
710
  };
691
- rules[scopeSelector(scope, 'a:hover')] = {
711
+ rules[_sx(scope, 'a:hover')] = {
692
712
  'color': palette.primary.hover,
693
713
  'text-decoration': 'underline'
694
714
  };
@@ -701,11 +721,11 @@
701
721
  var rd = layout.radius;
702
722
 
703
723
  // Base button (only when scoped — unscoped uses defaultStyles)
704
- rules[scopeSelector(scope, '.bw_btn')] = {
724
+ rules[_sx(scope, '.bw_btn')] = {
705
725
  'padding': sp.btn,
706
726
  'border-radius': rd.btn
707
727
  };
708
- rules[scopeSelector(scope, '.bw_btn:focus-visible')] = {
728
+ rules[_sx(scope, '.bw_btn:focus-visible')] = {
709
729
  'outline': '2px solid currentColor',
710
730
  'outline-offset': '2px',
711
731
  'box-shadow': '0 0 0 3px ' + palette.primary.focus
@@ -714,12 +734,12 @@
714
734
  // Variant colors handled by palette class on component root
715
735
 
716
736
  // Size variants (structural, reuse layout radius)
717
- rules[scopeSelector(scope, '.bw_btn_lg')] = {
737
+ rules[_sx(scope, '.bw_btn_lg')] = {
718
738
  'padding': '0.625rem 1.5rem',
719
739
  'font-size': '1rem',
720
740
  'border-radius': rd.btn === '50rem' ? '50rem' : (parseInt(rd.btn) + 2) + 'px'
721
741
  };
722
- rules[scopeSelector(scope, '.bw_btn_sm')] = {
742
+ rules[_sx(scope, '.bw_btn_sm')] = {
723
743
  'padding': '0.25rem 0.75rem',
724
744
  'font-size': '0.8125rem',
725
745
  'border-radius': rd.btn === '50rem' ? '50rem' : (Math.max(parseInt(rd.btn) - 1, 0)) + 'px'
@@ -733,7 +753,7 @@
733
753
  var sp = layout.spacing;
734
754
  var rd = layout.radius;
735
755
 
736
- rules[scopeSelector(scope, '.bw_alert')] = {
756
+ rules[_sx(scope, '.bw_alert')] = {
737
757
  'padding': sp.alert,
738
758
  'border-radius': rd.alert
739
759
  };
@@ -752,36 +772,36 @@
752
772
 
753
773
  var elev = layout.elevation;
754
774
  var motion = layout.motion;
755
- rules[scopeSelector(scope, '.bw_card')] = {
775
+ rules[_sx(scope, '.bw_card')] = {
756
776
  'background-color': palette.surface || '#fff',
757
777
  'border': '1px solid ' + palette.light.border,
758
778
  'border-radius': rd.card,
759
779
  'box-shadow': elev.sm,
760
780
  'transition': 'box-shadow ' + motion.normal + ' ' + motion.easing + ', transform ' + motion.normal + ' ' + motion.easing
761
781
  };
762
- rules[scopeSelector(scope, '.bw_card:hover')] = {
782
+ rules[_sx(scope, '.bw_card:hover')] = {
763
783
  'box-shadow': elev.md
764
784
  };
765
- rules[scopeSelector(scope, '.bw_card_hoverable:hover')] = {
785
+ rules[_sx(scope, '.bw_card_hoverable:hover')] = {
766
786
  'box-shadow': elev.lg
767
787
  };
768
- rules[scopeSelector(scope, '.bw_card_body')] = {
788
+ rules[_sx(scope, '.bw_card_body')] = {
769
789
  'padding': sp.card
770
790
  };
771
- rules[scopeSelector(scope, '.bw_card_header')] = {
791
+ rules[_sx(scope, '.bw_card_header')] = {
772
792
  'padding': sp.card.split(' ').map(function(v) { return (parseFloat(v) * 0.7).toFixed(3).replace(/\.?0+$/, '') + 'rem'; }).join(' '),
773
- 'background-color': palette.light.light,
793
+ 'background-color': palette.surfaceAlt,
774
794
  'border-bottom': '1px solid ' + palette.light.border
775
795
  };
776
- rules[scopeSelector(scope, '.bw_card_footer')] = {
777
- 'background-color': palette.light.light,
796
+ rules[_sx(scope, '.bw_card_footer')] = {
797
+ 'background-color': palette.surfaceAlt,
778
798
  'border-top': '1px solid ' + palette.light.border,
779
799
  'color': palette.secondary.base
780
800
  };
781
- rules[scopeSelector(scope, '.bw_card_title')] = {
801
+ rules[_sx(scope, '.bw_card_title')] = {
782
802
  'color': palette.dark.base
783
803
  };
784
- rules[scopeSelector(scope, '.bw_card_subtitle')] = {
804
+ rules[_sx(scope, '.bw_card_subtitle')] = {
785
805
  'color': palette.secondary.base
786
806
  };
787
807
 
@@ -795,55 +815,55 @@
795
815
  var sp = layout.spacing;
796
816
  var rd = layout.radius;
797
817
 
798
- rules[scopeSelector(scope, '.bw_form_control')] = {
818
+ rules[_sx(scope, '.bw_form_control')] = {
799
819
  'padding': sp.input,
800
820
  'border-radius': rd.input,
801
821
  'color': palette.dark.base,
802
822
  'background-color': palette.surface || '#fff',
803
823
  'border-color': palette.light.border
804
824
  };
805
- rules[scopeSelector(scope, '.bw_form_control:focus')] = {
825
+ rules[_sx(scope, '.bw_form_control:focus')] = {
806
826
  'border-color': palette.primary.border,
807
827
  'outline': '2px solid ' + palette.primary.base,
808
828
  'outline-offset': '-1px',
809
829
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
810
830
  };
811
- rules[scopeSelector(scope, '.bw_form_control::placeholder')] = {
831
+ rules[_sx(scope, '.bw_form_control::placeholder')] = {
812
832
  'color': palette.secondary.base
813
833
  };
814
- rules[scopeSelector(scope, '.bw_form_label')] = {
834
+ rules[_sx(scope, '.bw_form_label')] = {
815
835
  'color': palette.dark.base
816
836
  };
817
- rules[scopeSelector(scope, '.bw_form_text')] = {
837
+ rules[_sx(scope, '.bw_form_text')] = {
818
838
  'color': palette.secondary.base
819
839
  };
820
- rules[scopeSelector(scope, '.bw_form_check_input:checked')] = {
840
+ rules[_sx(scope, '.bw_form_check_input:checked')] = {
821
841
  'background-color': palette.primary.base,
822
842
  'border-color': palette.primary.base
823
843
  };
824
- rules[scopeSelector(scope, '.bw_form_check_input:focus')] = {
844
+ rules[_sx(scope, '.bw_form_check_input:focus')] = {
825
845
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
826
846
  };
827
847
  // Validation states
828
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid')] = { 'border-color': palette.success.base };
829
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid:focus')] = {
848
+ rules[_sx(scope, '.bw_form_control.bw_is_valid')] = { 'border-color': palette.success.base };
849
+ rules[_sx(scope, '.bw_form_control.bw_is_valid:focus')] = {
830
850
  'border-color': palette.success.base,
831
851
  'box-shadow': '0 0 0 0.2rem ' + palette.success.focus
832
852
  };
833
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid')] = { 'border-color': palette.danger.base };
834
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid:focus')] = {
853
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid')] = { 'border-color': palette.danger.base };
854
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid:focus')] = {
835
855
  'border-color': palette.danger.base,
836
856
  'box-shadow': '0 0 0 0.2rem ' + palette.danger.focus
837
857
  };
838
858
  // Form select
839
- rules[scopeSelector(scope, '.bw_form_select')] = {
859
+ rules[_sx(scope, '.bw_form_select')] = {
840
860
  'padding': sp.input,
841
861
  'border-radius': rd.input,
842
862
  'color': palette.dark.base,
843
863
  'background-color': palette.surface || '#fff',
844
864
  'border-color': palette.light.border
845
865
  };
846
- rules[scopeSelector(scope, '.bw_form_select:focus')] = {
866
+ rules[_sx(scope, '.bw_form_select:focus')] = {
847
867
  'border-color': palette.primary.border,
848
868
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
849
869
  };
@@ -851,43 +871,46 @@
851
871
  return rules;
852
872
  }
853
873
 
854
- function generateNavigation(scope, palette) {
874
+ function generateNavigation(scope, palette, layout) {
855
875
  var rules = {};
856
- rules[scopeSelector(scope, '.bw_navbar')] = {
857
- 'background-color': palette.light.light,
876
+ rules[_sx(scope, '.bw_navbar')] = {
877
+ 'background-color': palette.surfaceAlt,
858
878
  'border-bottom-color': palette.light.border
859
879
  };
860
- rules[scopeSelector(scope, '.bw_navbar_brand')] = {
880
+ rules[_sx(scope, '.bw_navbar_brand')] = {
861
881
  'color': palette.dark.base
862
882
  };
863
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link')] = {
864
- 'color': palette.secondary.base
883
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link')] = {
884
+ 'color': palette.secondary.base,
885
+ 'border-radius': layout.radius.btn
865
886
  };
866
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
867
- 'color': palette.dark.base
887
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
888
+ 'color': palette.dark.base,
889
+ 'background-color': palette.surfaceAlt
868
890
  };
869
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
891
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
870
892
  'color': palette.primary.base,
871
- 'background-color': palette.primary.focus
893
+ 'background-color': palette.primary.focus,
894
+ 'font-weight': '600'
872
895
  };
873
- rules[scopeSelector(scope, '.bw_navbar_dark')] = {
896
+ rules[_sx(scope, '.bw_navbar_dark')] = {
874
897
  'background-color': palette.dark.base,
875
898
  'border-bottom-color': palette.dark.hover
876
899
  };
877
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
900
+ rules[_sx(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
878
901
  'color': palette.light.base
879
902
  };
880
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link')] = {
881
- 'color': 'rgba(255,255,255,.65)'
903
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link')] = {
904
+ 'color': palette.light.border
882
905
  };
883
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
884
- 'color': '#fff'
906
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
907
+ 'color': palette.light.base
885
908
  };
886
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
887
- 'color': '#fff',
909
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
910
+ 'color': palette.light.base,
888
911
  'font-weight': '600'
889
912
  };
890
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link.active')] = {
913
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link.active')] = {
891
914
  'color': palette.primary.textOn,
892
915
  'background-color': palette.primary.base
893
916
  };
@@ -898,49 +921,58 @@
898
921
  var rules = {};
899
922
  var sp = layout.spacing;
900
923
 
901
- rules[scopeSelector(scope, '.bw_table')] = {
924
+ rules[_sx(scope, '.bw_table')] = {
902
925
  'color': palette.dark.base,
903
926
  'border-color': palette.light.border
904
927
  };
905
- rules[scopeSelector(scope, '.bw_table > :not(caption) > * > *')] = {
928
+ rules[_sx(scope, '.bw_table > :not(caption) > * > *')] = {
906
929
  'padding': sp.cell,
907
930
  'border-bottom-color': palette.light.border
908
931
  };
909
- rules[scopeSelector(scope, '.bw_table > thead > tr > *')] = {
932
+ rules[_sx(scope, '.bw_table > thead > tr > *')] = {
910
933
  'color': palette.secondary.base,
911
934
  'border-bottom-color': palette.light.border,
912
- 'background-color': palette.light.light
935
+ 'background-color': palette.surfaceAlt
913
936
  };
914
- rules[scopeSelector(scope, '.bw_table_striped > tbody > tr:nth-of-type(odd) > *')] = {
915
- 'background-color': 'rgba(0, 0, 0, 0.05)'
937
+ rules[_sx(scope, '.bw_table_striped > tbody > tr:nth-of-type(odd) > *')] = {
938
+ 'background-color': palette.surfaceAlt
916
939
  };
917
- rules[scopeSelector(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
940
+ rules[_sx(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
918
941
  'background-color': palette.primary.focus
919
942
  };
920
- rules[scopeSelector(scope, '.bw_table_bordered')] = {
943
+ rules[_sx(scope, '.bw_table_selectable > tbody > tr')] = {
944
+ 'cursor': 'pointer'
945
+ };
946
+ rules[_sx(scope, '.bw_table > tbody > tr.bw_table_row_selected > *')] = {
947
+ 'background-color': palette.primary.light
948
+ };
949
+ rules[_sx(scope, '.bw_table_bordered')] = {
921
950
  'border-color': palette.light.border
922
951
  };
923
- rules[scopeSelector(scope, '.bw_table caption')] = {
952
+ rules[_sx(scope, '.bw_table caption')] = {
924
953
  'color': palette.secondary.base
925
954
  };
926
955
 
927
956
  return rules;
928
957
  }
929
958
 
930
- function generateTabs(scope, palette) {
931
- var rules = {};
932
- rules[scopeSelector(scope, '.bw_nav_tabs')] = {
959
+ function generateTabs(scope, palette, layout) {
960
+ var rules = {}, mo = layout.motion;
961
+ rules[_sx(scope, '.bw_nav_tabs')] = {
933
962
  'border-bottom-color': palette.light.border
934
963
  };
935
- rules[scopeSelector(scope, '.bw_nav_link')] = {
936
- 'color': palette.secondary.base
964
+ rules[_sx(scope, '.bw_nav_link')] = {
965
+ 'color': palette.secondary.base,
966
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
937
967
  };
938
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
968
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
939
969
  'color': palette.dark.base,
970
+ 'background-color': palette.surfaceAlt,
940
971
  'border-bottom-color': palette.light.border
941
972
  };
942
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
973
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
943
974
  'color': palette.primary.base,
975
+ 'background-color': palette.primary.focus,
944
976
  'border-bottom': '2px solid ' + palette.primary.base
945
977
  };
946
978
  return rules;
@@ -949,23 +981,25 @@
949
981
  function generateListGroups(scope, palette, layout) {
950
982
  var rules = {};
951
983
  var sp = layout.spacing;
984
+ var mo = layout.motion;
952
985
 
953
- rules[scopeSelector(scope, '.bw_list_group_item')] = {
986
+ rules[_sx(scope, '.bw_list_group_item')] = {
954
987
  'padding': sp.cell,
955
988
  'color': palette.dark.base,
956
989
  'background-color': palette.surface || '#fff',
957
- 'border-color': palette.light.border
990
+ 'border-color': palette.light.border,
991
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
958
992
  };
959
- rules[scopeSelector(scope, 'a.bw_list_group_item:hover')] = {
960
- 'background-color': palette.light.light,
993
+ rules[_sx(scope, 'a.bw_list_group_item:hover')] = {
994
+ 'background-color': palette.surfaceAlt,
961
995
  'color': palette.dark.hover
962
996
  };
963
- rules[scopeSelector(scope, '.bw_list_group_item.active')] = {
997
+ rules[_sx(scope, '.bw_list_group_item.active')] = {
964
998
  'color': palette.primary.textOn,
965
999
  'background-color': palette.primary.base,
966
1000
  'border-color': palette.primary.base
967
1001
  };
968
- rules[scopeSelector(scope, '.bw_list_group_item.disabled')] = {
1002
+ rules[_sx(scope, '.bw_list_group_item.disabled')] = {
969
1003
  'color': palette.secondary.base,
970
1004
  'background-color': palette.surface || '#fff'
971
1005
  };
@@ -973,28 +1007,37 @@
973
1007
  return rules;
974
1008
  }
975
1009
 
976
- function generatePagination(scope, palette) {
977
- var rules = {};
978
- rules[scopeSelector(scope, '.bw_page_link')] = {
1010
+ function generatePagination(scope, palette, layout) {
1011
+ var rules = {}, mo = layout.motion, rd = layout.radius;
1012
+ rules[_sx(scope, '.bw_page_item:first-child .bw_page_link')] = {
1013
+ 'border-top-left-radius': rd.btn,
1014
+ 'border-bottom-left-radius': rd.btn
1015
+ };
1016
+ rules[_sx(scope, '.bw_page_item:last-child .bw_page_link')] = {
1017
+ 'border-top-right-radius': rd.btn,
1018
+ 'border-bottom-right-radius': rd.btn
1019
+ };
1020
+ rules[_sx(scope, '.bw_page_link')] = {
979
1021
  'color': palette.primary.base,
980
1022
  'background-color': palette.surface || '#fff',
981
- 'border-color': palette.light.border
1023
+ 'border-color': palette.light.border,
1024
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
982
1025
  };
983
- rules[scopeSelector(scope, '.bw_page_link:hover')] = {
1026
+ rules[_sx(scope, '.bw_page_link:hover')] = {
984
1027
  'color': palette.primary.hover,
985
- 'background-color': palette.light.light,
1028
+ 'background-color': palette.surfaceAlt,
986
1029
  'border-color': palette.light.border
987
1030
  };
988
- rules[scopeSelector(scope, '.bw_page_link:focus')] = {
1031
+ rules[_sx(scope, '.bw_page_link:focus')] = {
989
1032
  'outline': '2px solid ' + palette.primary.base,
990
1033
  'outline-offset': '-2px'
991
1034
  };
992
- rules[scopeSelector(scope, '.bw_page_item.bw_active .bw_page_link')] = {
1035
+ rules[_sx(scope, '.bw_page_item.bw_active .bw_page_link')] = {
993
1036
  'color': palette.primary.textOn,
994
1037
  'background-color': palette.primary.base,
995
1038
  'border-color': palette.primary.base
996
1039
  };
997
- rules[scopeSelector(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
1040
+ rules[_sx(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
998
1041
  'color': palette.secondary.base,
999
1042
  'background-color': palette.surface || '#fff',
1000
1043
  'border-color': palette.light.border
@@ -1004,12 +1047,12 @@
1004
1047
 
1005
1048
  function generateProgress(scope, palette) {
1006
1049
  var rules = {};
1007
- rules[scopeSelector(scope, '.bw_progress')] = {
1008
- 'background-color': palette.light.light,
1050
+ rules[_sx(scope, '.bw_progress')] = {
1051
+ 'background-color': palette.surfaceAlt,
1009
1052
  'box-shadow': 'inset 0 1px 2px rgba(0,0,0,.1)'
1010
1053
  };
1011
- rules[scopeSelector(scope, '.bw_progress_bar')] = {
1012
- 'color': '#fff',
1054
+ rules[_sx(scope, '.bw_progress_bar')] = {
1055
+ 'color': palette.primary.textOn,
1013
1056
  'background-color': palette.primary.base,
1014
1057
  'box-shadow': 'inset 0 -1px 0 rgba(0,0,0,.15)'
1015
1058
  };
@@ -1028,26 +1071,31 @@
1028
1071
  'color': palette.dark.base,
1029
1072
  'background-color': bg
1030
1073
  };
1031
- rules[scopeSelector(scope, 'body')] = baseReset;
1032
- // Also apply to the scope element itself so themes work on any container, not just body
1033
- if (scope) {
1034
- rules['.' + scope] = baseReset;
1035
- }
1074
+ rules[_sx(scope, 'body')] = baseReset;
1036
1075
  return rules;
1037
1076
  }
1038
1077
 
1039
- function generateBreadcrumbThemed(scope, palette) {
1040
- var rules = {};
1041
- rules[scopeSelector(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1042
- 'color': palette.secondary.base
1078
+ function generateBreadcrumbThemed(scope, palette, layout) {
1079
+ var rules = {}, mo = layout.motion;
1080
+ rules[_sx(scope, '.bw_breadcrumb')] = {
1081
+ 'background-color': palette.surfaceAlt,
1082
+ 'padding': '0.625rem 1rem',
1083
+ 'border-radius': layout.radius.btn
1043
1084
  };
1044
- rules[scopeSelector(scope, '.bw_breadcrumb_item.active')] = {
1085
+ rules[_sx(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1045
1086
  'color': palette.secondary.base
1046
1087
  };
1047
- rules[scopeSelector(scope, '.bw_breadcrumb_item a:hover')] = {
1088
+ rules[_sx(scope, '.bw_breadcrumb_item a')] = {
1089
+ 'color': palette.primary.base,
1090
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing
1091
+ };
1092
+ rules[_sx(scope, '.bw_breadcrumb_item a:hover')] = {
1048
1093
  'color': palette.primary.hover,
1049
1094
  'text-decoration': 'underline'
1050
1095
  };
1096
+ rules[_sx(scope, '.bw_breadcrumb_item.active')] = {
1097
+ 'color': palette.dark.base
1098
+ };
1051
1099
  return rules;
1052
1100
  }
1053
1101
 
@@ -1055,11 +1103,11 @@
1055
1103
 
1056
1104
  function generateCloseButtonThemed(scope, palette) {
1057
1105
  var rules = {};
1058
- rules[scopeSelector(scope, '.bw_close')] = {
1106
+ rules[_sx(scope, '.bw_close')] = {
1059
1107
  'color': palette.dark.base,
1060
1108
  'opacity': '0.5'
1061
1109
  };
1062
- rules[scopeSelector(scope, '.bw_close:focus')] = {
1110
+ rules[_sx(scope, '.bw_close:focus')] = {
1063
1111
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1064
1112
  };
1065
1113
  return rules;
@@ -1067,82 +1115,94 @@
1067
1115
 
1068
1116
  function generateSectionsThemed(scope, palette) {
1069
1117
  var rules = {};
1070
- rules[scopeSelector(scope, '.bw_section_subtitle')] = {
1118
+ rules[_sx(scope, '.bw_section_subtitle')] = {
1071
1119
  'color': palette.secondary.base
1072
1120
  };
1073
- rules[scopeSelector(scope, '.bw_feature_description')] = {
1121
+ rules[_sx(scope, '.bw_feature_description')] = {
1074
1122
  'color': palette.secondary.base
1075
1123
  };
1076
- rules[scopeSelector(scope, '.bw_cta_description')] = {
1124
+ rules[_sx(scope, '.bw_cta_description')] = {
1077
1125
  'color': palette.secondary.base
1078
1126
  };
1079
1127
  return rules;
1080
1128
  }
1081
1129
 
1082
- function generateAccordionThemed(scope, palette) {
1130
+ function generateAccordionThemed(scope, palette, layout) {
1083
1131
  var rules = {};
1084
- rules[scopeSelector(scope, '.bw_accordion_item')] = {
1132
+ var rd = layout ? layout.radius : { card: '8px' };
1133
+ rules[_sx(scope, '.bw_accordion_item')] = {
1085
1134
  'background-color': palette.surface || '#fff',
1086
1135
  'border-color': palette.light.border
1087
1136
  };
1088
- rules[scopeSelector(scope, '.bw_accordion_button')] = {
1137
+ rules[_sx(scope, '.bw_accordion_item:first-child')] = {
1138
+ 'border-top-left-radius': rd.card,
1139
+ 'border-top-right-radius': rd.card
1140
+ };
1141
+ rules[_sx(scope, '.bw_accordion_item:last-child')] = {
1142
+ 'border-bottom-left-radius': rd.card,
1143
+ 'border-bottom-right-radius': rd.card
1144
+ };
1145
+ rules[_sx(scope, '.bw_accordion_button')] = {
1089
1146
  'color': palette.dark.base
1090
1147
  };
1091
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1148
+ rules[_sx(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1092
1149
  'color': palette.primary.darkText,
1093
- 'background-color': palette.primary.light
1150
+ 'background-color': palette.primary.light,
1151
+ 'border-left': '3px solid ' + palette.primary.base
1094
1152
  };
1095
- rules[scopeSelector(scope, '.bw_accordion_button:hover')] = {
1096
- 'background-color': palette.light.light
1153
+ rules[_sx(scope, '.bw_accordion_button:hover')] = {
1154
+ 'background-color': palette.surfaceAlt
1097
1155
  };
1098
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed):hover')] = {
1099
- 'background-color': palette.primary.hover
1156
+ rules[_sx(scope, '.bw_accordion_button:not(.bw_collapsed):hover')] = {
1157
+ 'background-color': palette.primary.base,
1158
+ 'color': palette.primary.textOn
1100
1159
  };
1101
- rules[scopeSelector(scope, '.bw_accordion_button:focus-visible')] = {
1160
+ rules[_sx(scope, '.bw_accordion_button:focus-visible')] = {
1102
1161
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1103
1162
  };
1104
- rules[scopeSelector(scope, '.bw_accordion_body')] = {
1105
- 'border-top': '1px solid ' + palette.light.border
1163
+ rules[_sx(scope, '.bw_accordion_body')] = {
1164
+ 'border-top': '1px solid ' + palette.light.border,
1165
+ 'background-color': palette.surfaceAlt
1106
1166
  };
1107
1167
  return rules;
1108
1168
  }
1109
1169
 
1110
1170
  function generateCarouselThemed(scope, palette) {
1111
1171
  var rules = {};
1112
- rules[scopeSelector(scope, '.bw_carousel')] = {
1113
- 'background-color': palette.light.light
1172
+ rules[_sx(scope, '.bw_carousel')] = {
1173
+ 'background-color': palette.surfaceAlt
1114
1174
  };
1115
- rules[scopeSelector(scope, '.bw_carousel_indicator.active')] = {
1175
+ rules[_sx(scope, '.bw_carousel_indicator.active')] = {
1116
1176
  'background-color': palette.primary.base
1117
1177
  };
1118
- rules[scopeSelector(scope, '.bw_carousel_control')] = {
1119
- 'background-color': 'rgba(0,0,0,0.4)',
1120
- 'color': '#fff'
1178
+ rules[_sx(scope, '.bw_carousel_control')] = {
1179
+ 'background-color': palette.dark.base,
1180
+ 'color': palette.dark.textOn
1121
1181
  };
1122
- rules[scopeSelector(scope, '.bw_carousel_control:hover')] = {
1123
- 'background-color': 'rgba(0,0,0,0.6)'
1182
+ rules[_sx(scope, '.bw_carousel_control:hover')] = {
1183
+ 'background-color': palette.dark.hover
1124
1184
  };
1125
- rules[scopeSelector(scope, '.bw_carousel_caption')] = {
1126
- 'background': 'linear-gradient(transparent, rgba(0,0,0,0.6))',
1127
- 'color': '#fff'
1185
+ rules[_sx(scope, '.bw_carousel_caption')] = {
1186
+ 'background': 'linear-gradient(transparent, ' + palette.dark.base + ')',
1187
+ 'color': palette.dark.textOn
1128
1188
  };
1129
1189
  return rules;
1130
1190
  }
1131
1191
 
1132
1192
  function generateModalThemed(scope, palette, layout) {
1133
1193
  var rules = {};
1134
- rules[scopeSelector(scope, '.bw_modal_content')] = {
1194
+ rules[_sx(scope, '.bw_modal_content')] = {
1135
1195
  'background-color': palette.surface || '#fff',
1136
1196
  'border-color': palette.light.border,
1137
1197
  'box-shadow': layout.elevation.lg
1138
1198
  };
1139
- rules[scopeSelector(scope, '.bw_modal_header')] = {
1199
+ rules[_sx(scope, '.bw_modal_header')] = {
1140
1200
  'border-bottom-color': palette.light.border
1141
1201
  };
1142
- rules[scopeSelector(scope, '.bw_modal_footer')] = {
1202
+ rules[_sx(scope, '.bw_modal_footer')] = {
1143
1203
  'border-top-color': palette.light.border
1144
1204
  };
1145
- rules[scopeSelector(scope, '.bw_modal_title')] = {
1205
+ rules[_sx(scope, '.bw_modal_title')] = {
1146
1206
  'color': palette.dark.base
1147
1207
  };
1148
1208
  return rules;
@@ -1150,13 +1210,13 @@
1150
1210
 
1151
1211
  function generateToastThemed(scope, palette, layout) {
1152
1212
  var rules = {};
1153
- rules[scopeSelector(scope, '.bw_toast')] = {
1213
+ rules[_sx(scope, '.bw_toast')] = {
1154
1214
  'background-color': palette.surface || '#fff',
1155
- 'border-color': 'rgba(0,0,0,0.1)',
1215
+ 'border-color': palette.light.border,
1156
1216
  'box-shadow': layout.elevation.lg
1157
1217
  };
1158
- rules[scopeSelector(scope, '.bw_toast_header')] = {
1159
- 'border-bottom-color': 'rgba(0,0,0,0.05)'
1218
+ rules[_sx(scope, '.bw_toast_header')] = {
1219
+ 'border-bottom-color': palette.light.border
1160
1220
  };
1161
1221
  // Variant toast borders handled by palette class
1162
1222
  return rules;
@@ -1164,22 +1224,23 @@
1164
1224
 
1165
1225
  function generateDropdownThemed(scope, palette, layout) {
1166
1226
  var rules = {};
1167
- rules[scopeSelector(scope, '.bw_dropdown_menu')] = {
1227
+ rules[_sx(scope, '.bw_dropdown_menu')] = {
1168
1228
  'background-color': palette.surface || '#fff',
1169
1229
  'border-color': palette.light.border,
1170
1230
  'box-shadow': layout.elevation.md
1171
1231
  };
1172
- rules[scopeSelector(scope, '.bw_dropdown_item')] = {
1173
- 'color': palette.dark.base
1232
+ rules[_sx(scope, '.bw_dropdown_item')] = {
1233
+ 'color': palette.dark.base,
1234
+ 'transition': 'background-color ' + layout.motion.fast + ' ' + layout.motion.easing
1174
1235
  };
1175
- rules[scopeSelector(scope, '.bw_dropdown_item:hover')] = {
1236
+ rules[_sx(scope, '.bw_dropdown_item:hover')] = {
1176
1237
  'color': palette.dark.hover,
1177
- 'background-color': palette.light.light
1238
+ 'background-color': palette.surfaceAlt
1178
1239
  };
1179
- rules[scopeSelector(scope, '.bw_dropdown_item.disabled')] = {
1240
+ rules[_sx(scope, '.bw_dropdown_item.disabled')] = {
1180
1241
  'color': palette.secondary.base
1181
1242
  };
1182
- rules[scopeSelector(scope, '.bw_dropdown_divider')] = {
1243
+ rules[_sx(scope, '.bw_dropdown_divider')] = {
1183
1244
  'border-top-color': palette.light.border
1184
1245
  };
1185
1246
  return rules;
@@ -1187,15 +1248,15 @@
1187
1248
 
1188
1249
  function generateSwitchThemed(scope, palette) {
1189
1250
  var rules = {};
1190
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input')] = {
1251
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input')] = {
1191
1252
  'background-color': palette.secondary.base,
1192
1253
  'border-color': palette.secondary.base
1193
1254
  };
1194
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1255
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1195
1256
  'background-color': palette.primary.base,
1196
1257
  'border-color': palette.primary.base
1197
1258
  };
1198
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1259
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1199
1260
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1200
1261
  };
1201
1262
  return rules;
@@ -1203,88 +1264,102 @@
1203
1264
 
1204
1265
  function generateSkeletonThemed(scope, palette) {
1205
1266
  var rules = {};
1206
- rules[scopeSelector(scope, '.bw_skeleton')] = {
1207
- 'background': 'linear-gradient(90deg, ' + palette.light.border + ' 25%, ' + palette.light.light + ' 37%, ' + palette.light.border + ' 63%)'
1267
+ rules[_sx(scope, '.bw_skeleton')] = {
1268
+ 'background': 'linear-gradient(90deg, ' + palette.light.border + ' 25%, ' + palette.surfaceAlt + ' 37%, ' + palette.light.border + ' 63%)'
1208
1269
  };
1209
1270
  return rules;
1210
1271
  }
1211
1272
 
1212
1273
  // generateAvatarThemed: removed — palette class on root handles variants
1213
1274
 
1214
- function generateStatCardThemed(scope, palette) {
1215
- var rules = {};
1275
+ function generateStatCardThemed(scope, palette, layout) {
1276
+ var rules = {}, mo = layout.motion, el = layout.elevation, rd = layout.radius;
1277
+ rules[_sx(scope, '.bw_stat_card')] = {
1278
+ 'background-color': palette.surface || '#fff',
1279
+ 'color': palette.dark.base,
1280
+ 'border': '1px solid ' + palette.light.border,
1281
+ 'border-radius': rd.card,
1282
+ 'box-shadow': el.sm,
1283
+ 'transition': 'box-shadow ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1284
+ };
1285
+ rules[_sx(scope, '.bw_stat_card:hover')] = { 'box-shadow': el.md };
1216
1286
  // Variant border colors handled by palette class
1217
- rules[scopeSelector(scope, '.bw_stat_change_up')] = { 'color': palette.success.base };
1218
- rules[scopeSelector(scope, '.bw_stat_change_down')] = { 'color': palette.danger.base };
1287
+ rules[_sx(scope, '.bw_stat_change_up')] = { 'color': palette.success.base };
1288
+ rules[_sx(scope, '.bw_stat_change_down')] = { 'color': palette.danger.base };
1219
1289
  return rules;
1220
1290
  }
1221
1291
 
1222
1292
  function generateTimelineThemed(scope, palette) {
1223
1293
  var rules = {};
1224
- rules[scopeSelector(scope, '.bw_timeline::before')] = { 'background-color': palette.light.border };
1294
+ rules[_sx(scope, '.bw_timeline::before')] = { 'background-color': palette.light.border };
1225
1295
  // Variant marker colors handled by palette class
1226
- rules[scopeSelector(scope, '.bw_timeline_date')] = { 'color': palette.secondary.base };
1296
+ rules[_sx(scope, '.bw_timeline_date')] = { 'color': palette.secondary.base };
1227
1297
  return rules;
1228
1298
  }
1229
1299
 
1230
1300
  function generateStepperThemed(scope, palette) {
1231
1301
  var rules = {};
1232
- rules[scopeSelector(scope, '.bw_step_indicator')] = {
1233
- 'background-color': palette.light.light,
1302
+ rules[_sx(scope, '.bw_step_indicator')] = {
1303
+ 'background-color': palette.surfaceAlt,
1234
1304
  'border': '2px solid ' + palette.light.border,
1235
1305
  'color': palette.secondary.base
1236
1306
  };
1237
- rules[scopeSelector(scope, '.bw_step + .bw_step::before')] = { 'background-color': palette.light.border };
1238
- rules[scopeSelector(scope, '.bw_step_active .bw_step_indicator')] = {
1307
+ rules[_sx(scope, '.bw_step + .bw_step::before')] = { 'background-color': palette.light.border };
1308
+ rules[_sx(scope, '.bw_step_active .bw_step_indicator')] = {
1239
1309
  'background-color': palette.primary.base,
1240
1310
  'color': palette.primary.textOn
1241
1311
  };
1242
- rules[scopeSelector(scope, '.bw_step_active .bw_step_label')] = {
1312
+ rules[_sx(scope, '.bw_step_active .bw_step_label')] = {
1243
1313
  'color': palette.dark.base,
1244
1314
  'font-weight': '600'
1245
1315
  };
1246
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_indicator')] = {
1316
+ rules[_sx(scope, '.bw_step_completed .bw_step_indicator')] = {
1247
1317
  'background-color': palette.primary.base,
1248
1318
  'color': palette.primary.textOn
1249
1319
  };
1250
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_label')] = { 'color': palette.primary.base };
1251
- rules[scopeSelector(scope, '.bw_step_completed + .bw_step::before')] = { 'background-color': palette.primary.base };
1320
+ rules[_sx(scope, '.bw_step_completed .bw_step_label')] = { 'color': palette.primary.base };
1321
+ rules[_sx(scope, '.bw_step_completed + .bw_step::before')] = { 'background-color': palette.primary.base };
1252
1322
  return rules;
1253
1323
  }
1254
1324
 
1255
1325
  function generateChipInputThemed(scope, palette) {
1256
1326
  var rules = {};
1257
- rules[scopeSelector(scope, '.bw_chip_input')] = { 'border-color': palette.light.border };
1258
- rules[scopeSelector(scope, '.bw_chip_input:focus-within')] = {
1327
+ rules[_sx(scope, '.bw_chip_input')] = {
1328
+ 'border-color': palette.light.border,
1329
+ 'background-color': palette.surface || '#fff',
1330
+ 'color': palette.dark.base
1331
+ };
1332
+ rules[_sx(scope, '.bw_chip_input:focus-within')] = {
1259
1333
  'border-color': palette.primary.base,
1260
1334
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1261
1335
  };
1262
- rules[scopeSelector(scope, '.bw_chip')] = {
1263
- 'background-color': palette.light.light,
1336
+ rules[_sx(scope, '.bw_chip')] = {
1337
+ 'background-color': palette.surfaceAlt,
1264
1338
  'color': palette.dark.base
1265
1339
  };
1266
- rules[scopeSelector(scope, '.bw_chip_remove:hover')] = {
1340
+ rules[_sx(scope, '.bw_chip_remove:hover')] = {
1267
1341
  'color': palette.danger.base,
1268
1342
  'background-color': palette.danger.light
1269
1343
  };
1270
1344
  return rules;
1271
1345
  }
1272
1346
 
1273
- function generateFileUploadThemed(scope, palette) {
1274
- var rules = {};
1275
- rules[scopeSelector(scope, '.bw_file_upload')] = {
1347
+ function generateFileUploadThemed(scope, palette, layout) {
1348
+ var rules = {}, mo = layout.motion;
1349
+ rules[_sx(scope, '.bw_file_upload')] = {
1276
1350
  'border-color': palette.light.border,
1277
- 'background-color': palette.light.light
1351
+ 'background-color': palette.surfaceAlt,
1352
+ 'transition': 'border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1278
1353
  };
1279
- rules[scopeSelector(scope, '.bw_file_upload:hover')] = {
1354
+ rules[_sx(scope, '.bw_file_upload:hover')] = {
1280
1355
  'border-color': palette.primary.base,
1281
1356
  'background-color': palette.primary.light
1282
1357
  };
1283
- rules[scopeSelector(scope, '.bw_file_upload:focus')] = {
1358
+ rules[_sx(scope, '.bw_file_upload:focus')] = {
1284
1359
  'outline': '2px solid ' + palette.primary.base,
1285
1360
  'outline-offset': '2px'
1286
1361
  };
1287
- rules[scopeSelector(scope, '.bw_file_upload.bw_file_upload_active')] = {
1362
+ rules[_sx(scope, '.bw_file_upload.bw_file_upload_active')] = {
1288
1363
  'border-color': palette.primary.base,
1289
1364
  'background-color': palette.primary.light,
1290
1365
  'border-style': 'solid'
@@ -1294,35 +1369,73 @@
1294
1369
 
1295
1370
  function generateRangeThemed(scope, palette) {
1296
1371
  var rules = {};
1297
- rules[scopeSelector(scope, '.bw_range')] = { 'background-color': palette.light.border };
1298
- rules[scopeSelector(scope, '.bw_range::-webkit-slider-thumb')] = {
1372
+ rules[_sx(scope, '.bw_range')] = { 'background-color': palette.light.border };
1373
+ rules[_sx(scope, '.bw_range::-webkit-slider-thumb')] = {
1299
1374
  'background-color': palette.primary.base,
1300
- 'border-color': '#fff',
1375
+ 'border-color': palette.surface || '#fff',
1301
1376
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)',
1302
1377
  'transition': 'background-color 0.15s ease-out, transform 0.15s ease-out'
1303
1378
  };
1304
- rules[scopeSelector(scope, '.bw_range::-moz-range-thumb')] = {
1379
+ rules[_sx(scope, '.bw_range::-moz-range-thumb')] = {
1305
1380
  'background-color': palette.primary.base,
1306
- 'border-color': '#fff',
1381
+ 'border-color': palette.surface || '#fff',
1307
1382
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)'
1308
1383
  };
1309
1384
  return rules;
1310
1385
  }
1311
1386
 
1312
- function generateSearchThemed(scope, palette) {
1313
- var rules = {};
1314
- rules[scopeSelector(scope, '.bw_search_clear:hover')] = { 'color': palette.dark.base };
1387
+ function generateTooltipThemed(scope, palette, layout) {
1388
+ var rules = {}, sp = layout.spacing, rd = layout.radius, el = layout.elevation, mo = layout.motion;
1389
+ rules[_sx(scope, '.bw_tooltip')] = {
1390
+ 'background-color': palette.dark.base, 'color': palette.dark.textOn,
1391
+ 'padding': sp.input, 'border-radius': rd.badge, 'box-shadow': el.md,
1392
+ 'transition': 'opacity ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1393
+ };
1394
+ return rules;
1395
+ }
1396
+
1397
+ function generatePopoverThemed(scope, palette, layout) {
1398
+ var rules = {}, sp = layout.spacing, rd = layout.radius, el = layout.elevation, mo = layout.motion;
1399
+ rules[_sx(scope, '.bw_popover')] = {
1400
+ 'background-color': palette.surface || '#fff', 'color': palette.dark.base,
1401
+ 'border': '1px solid ' + palette.light.border, 'border-radius': rd.card, 'box-shadow': el.lg,
1402
+ 'transition': 'opacity ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1403
+ };
1404
+ rules[_sx(scope, '.bw_popover_header')] = {
1405
+ 'background-color': palette.surfaceAlt, 'border-bottom': '1px solid ' + palette.light.border,
1406
+ 'padding': sp.input
1407
+ };
1408
+ rules[_sx(scope, '.bw_popover_body')] = { 'padding': sp.card };
1315
1409
  return rules;
1316
1410
  }
1317
1411
 
1318
- function generateCodeDemoThemed(scope, palette) {
1412
+ function generateSearchThemed(scope, palette, layout) {
1413
+ var rules = {}, mo = layout.motion;
1414
+ rules[_sx(scope, '.bw_search_input')] = {
1415
+ 'background-color': palette.surface || '#fff',
1416
+ 'color': palette.dark.base
1417
+ };
1418
+ rules[_sx(scope, '.bw_search_clear')] = {
1419
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1420
+ };
1421
+ rules[_sx(scope, '.bw_search_clear:hover')] = { 'color': palette.dark.base };
1422
+ return rules;
1423
+ }
1424
+
1425
+ function generateCodeDemoThemed(scope, palette, layout) {
1319
1426
  var rules = {};
1320
- rules[scopeSelector(scope, '.bw_code_copy_btn_copied')] = {
1427
+ var rd = layout ? layout.radius : { card: '0.375rem' };
1428
+ rules[_sx(scope, '.bw_code_demo')] = {
1429
+ 'background-color': palette.surface || '#fff',
1430
+ 'color': palette.dark.base,
1431
+ 'border-radius': rd.card
1432
+ };
1433
+ rules[_sx(scope, '.bw_code_copy_btn_copied')] = {
1321
1434
  'background': palette.success.base,
1322
1435
  'color': palette.success.textOn,
1323
1436
  'border-color': palette.success.base
1324
1437
  };
1325
- rules[scopeSelector(scope, '.bw_copy_btn:hover')] = {
1438
+ rules[_sx(scope, '.bw_copy_btn:hover')] = {
1326
1439
  'background': 'rgba(255,255,255,0.2)',
1327
1440
  'color': '#fff'
1328
1441
  };
@@ -1332,7 +1445,7 @@
1332
1445
  function generateNavPillsThemed(scope, palette, layout) {
1333
1446
  var rules = {};
1334
1447
  var rd = layout.radius;
1335
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link')] = { 'border-radius': rd.btn };
1448
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link')] = { 'border-radius': rd.btn };
1336
1449
  return rules;
1337
1450
  }
1338
1451
 
@@ -1358,21 +1471,21 @@
1358
1471
  var s = palette[k];
1359
1472
 
1360
1473
  // --- Root palette class: sets default bg/color/border ---
1361
- rules[scopeSelector(scope, '.bw_' + k)] = {
1474
+ rules[_sx(scope, '.bw_' + k)] = {
1362
1475
  'background-color': s.base,
1363
1476
  'color': s.textOn,
1364
1477
  'border-color': s.base
1365
1478
  };
1366
1479
 
1367
1480
  // --- Pseudo-states (shared across all components) ---
1368
- rules[scopeSelector(scope, '.bw_' + k + ':hover')] = {
1481
+ rules[_sx(scope, '.bw_' + k + ':hover')] = {
1369
1482
  'background-color': s.hover,
1370
1483
  'border-color': s.active
1371
1484
  };
1372
- rules[scopeSelector(scope, '.bw_' + k + ':active')] = {
1485
+ rules[_sx(scope, '.bw_' + k + ':active')] = {
1373
1486
  'background-color': s.active
1374
1487
  };
1375
- rules[scopeSelector(scope, '.bw_' + k + ':focus-visible')] = {
1488
+ rules[_sx(scope, '.bw_' + k + ':focus-visible')] = {
1376
1489
  'box-shadow': '0 0 0 3px ' + s.focus,
1377
1490
  'outline': 'none'
1378
1491
  };
@@ -1380,70 +1493,99 @@
1380
1493
  // --- Component-specific overrides ---
1381
1494
 
1382
1495
  // Alerts: light bg, dark text, subtle border
1383
- rules[scopeSelector(scope, '.bw_alert.bw_' + k)] = {
1496
+ rules[_sx(scope, '.bw_alert.bw_' + k)] = {
1384
1497
  'background-color': s.light,
1385
1498
  'color': s.darkText,
1386
1499
  'border-color': s.border
1387
1500
  };
1388
1501
 
1389
1502
  // Toast: inherit bg, left border accent
1390
- rules[scopeSelector(scope, '.bw_toast.bw_' + k)] = {
1503
+ rules[_sx(scope, '.bw_toast.bw_' + k)] = {
1391
1504
  'background-color': 'inherit',
1392
1505
  'color': 'inherit',
1393
1506
  'border-left': '4px solid ' + s.base
1394
1507
  };
1395
1508
 
1396
1509
  // Stat card: inherit bg, left border accent
1397
- rules[scopeSelector(scope, '.bw_stat_card.bw_' + k)] = {
1510
+ rules[_sx(scope, '.bw_stat_card.bw_' + k)] = {
1398
1511
  'background-color': 'inherit',
1399
1512
  'color': 'inherit',
1400
1513
  'border-left-color': s.base
1401
1514
  };
1402
1515
 
1403
1516
  // Card accent: left border accent, inherit bg
1404
- rules[scopeSelector(scope, '.bw_card.bw_' + k)] = {
1517
+ rules[_sx(scope, '.bw_card.bw_' + k)] = {
1405
1518
  'background-color': 'inherit',
1406
1519
  'color': 'inherit',
1407
1520
  'border-left': '4px solid ' + s.base
1408
1521
  };
1409
1522
 
1410
1523
  // Timeline marker: colored dot
1411
- rules[scopeSelector(scope, '.bw_timeline_marker.bw_' + k)] = {
1524
+ rules[_sx(scope, '.bw_timeline_marker.bw_' + k)] = {
1412
1525
  'box-shadow': '0 0 0 2px ' + s.base
1413
1526
  };
1414
1527
 
1415
- // Spinner: text color only, transparent bg
1416
- rules[scopeSelector(scope, '.bw_spinner_border.bw_' + k + ',\n' + scopeSelector(scope, '.bw_spinner_grow.bw_' + k))] = {
1528
+ // Spinner: set color, re-apply border pattern so the root palette class
1529
+ // border-color doesn't fill in the transparent gap that makes it spin.
1530
+ // Also neutralize hover/active which would override border-right-color.
1531
+ rules[_sx(scope, '.bw_spinner_border.bw_' + k)] = {
1417
1532
  'background-color': 'transparent',
1418
1533
  'color': s.base,
1419
- 'border-color': 'currentColor'
1534
+ 'border-color': s.base,
1535
+ 'border-right-color': 'transparent'
1536
+ };
1537
+ rules[_sx(scope, '.bw_spinner_border.bw_' + k + ':hover')] = {
1538
+ 'background-color': 'transparent',
1539
+ 'border-color': s.base,
1540
+ 'border-right-color': 'transparent'
1541
+ };
1542
+ rules[_sx(scope, '.bw_spinner_grow.bw_' + k)] = {
1543
+ 'background-color': s.base,
1544
+ 'color': s.base
1420
1545
  };
1421
1546
 
1422
1547
  // Outline button: transparent bg, colored border+text, solid on hover
1423
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k)] = {
1548
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k)] = {
1424
1549
  'background-color': 'transparent',
1425
1550
  'color': s.base,
1426
1551
  'border-color': s.base
1427
1552
  };
1428
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1553
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1429
1554
  'background-color': s.base,
1430
1555
  'color': s.textOn
1431
1556
  };
1432
1557
 
1433
1558
  // Hero: gradient background
1434
- rules[scopeSelector(scope, '.bw_hero.bw_' + k)] = {
1559
+ rules[_sx(scope, '.bw_hero.bw_' + k)] = {
1435
1560
  'background': 'linear-gradient(135deg, ' + s.base + ' 0%, ' + s.hover + ' 100%)',
1436
1561
  'color': s.textOn
1437
1562
  };
1438
1563
 
1439
- // Progress bar: white text on colored bg (default is fine, just ensure text)
1440
- rules[scopeSelector(scope, '.bw_progress_bar.bw_' + k)] = {
1441
- 'color': '#fff'
1564
+ // Progress bar: contrasting text on colored bg
1565
+ rules[_sx(scope, '.bw_progress_bar.bw_' + k)] = {
1566
+ 'color': s.textOn
1567
+ };
1568
+
1569
+ // Background utility: .bw_bg_primary, .bw_bg_secondary, etc.
1570
+ rules[_sx(scope, '.bw_bg_' + k)] = {
1571
+ 'background-color': s.base,
1572
+ 'color': s.textOn
1573
+ };
1574
+
1575
+ // Text color utility: .bw_text_primary, .bw_text_secondary, etc.
1576
+ rules[_sx(scope, '.bw_text_' + k)] = {
1577
+ 'color': s.base
1442
1578
  };
1443
1579
  });
1444
1580
 
1445
- // Text muted
1446
- rules[scopeSelector(scope, '.bw_text_muted')] = { 'color': palette.secondary.base };
1581
+ // Text muted — always a neutral gray, never a brand color
1582
+ rules[_sx(scope, '.bw_text_muted')] = { 'color': '#6c757d' };
1583
+
1584
+ // Common bg/text utilities that aren't per-variant
1585
+ rules[_sx(scope, '.bw_bg_dark')] = { 'background-color': '#212529', 'color': '#f8f9fa' };
1586
+ rules[_sx(scope, '.bw_bg_light')] = { 'background-color': '#f8f9fa', 'color': '#212529' };
1587
+ rules[_sx(scope, '.bw_text_light')] = { 'color': '#f8f9fa' };
1588
+ rules[_sx(scope, '.bw_text_dark')] = { 'color': '#212529' };
1447
1589
 
1448
1590
  return rules;
1449
1591
  }
@@ -1465,30 +1607,32 @@
1465
1607
  generateAlerts(scopeName, palette, layout),
1466
1608
  generateCards(scopeName, palette, layout),
1467
1609
  generateForms(scopeName, palette, layout),
1468
- generateNavigation(scopeName, palette),
1610
+ generateNavigation(scopeName, palette, layout),
1469
1611
  generateTables(scopeName, palette, layout),
1470
- generateTabs(scopeName, palette),
1612
+ generateTabs(scopeName, palette, layout),
1471
1613
  generateListGroups(scopeName, palette, layout),
1472
- generatePagination(scopeName, palette),
1614
+ generatePagination(scopeName, palette, layout),
1473
1615
  generateProgress(scopeName, palette),
1474
- generateBreadcrumbThemed(scopeName, palette),
1616
+ generateBreadcrumbThemed(scopeName, palette, layout),
1475
1617
  generateCloseButtonThemed(scopeName, palette),
1476
1618
  generateSectionsThemed(scopeName, palette),
1477
- generateAccordionThemed(scopeName, palette),
1619
+ generateAccordionThemed(scopeName, palette, layout),
1478
1620
  generateCarouselThemed(scopeName, palette),
1479
1621
  generateModalThemed(scopeName, palette, layout),
1480
1622
  generateToastThemed(scopeName, palette, layout),
1481
1623
  generateDropdownThemed(scopeName, palette, layout),
1482
1624
  generateSwitchThemed(scopeName, palette),
1483
1625
  generateSkeletonThemed(scopeName, palette),
1484
- generateStatCardThemed(scopeName, palette),
1626
+ generateStatCardThemed(scopeName, palette, layout),
1485
1627
  generateTimelineThemed(scopeName, palette),
1486
1628
  generateStepperThemed(scopeName, palette),
1487
1629
  generateChipInputThemed(scopeName, palette),
1488
- generateFileUploadThemed(scopeName, palette),
1630
+ generateFileUploadThemed(scopeName, palette, layout),
1489
1631
  generateRangeThemed(scopeName, palette),
1490
- generateSearchThemed(scopeName, palette),
1491
- generateCodeDemoThemed(scopeName, palette),
1632
+ generateSearchThemed(scopeName, palette, layout),
1633
+ generateTooltipThemed(scopeName, palette, layout),
1634
+ generatePopoverThemed(scopeName, palette, layout),
1635
+ generateCodeDemoThemed(scopeName, palette, layout),
1492
1636
  generateNavPillsThemed(scopeName, palette, layout),
1493
1637
  generatePaletteClasses(scopeName, palette)
1494
1638
  );
@@ -1713,6 +1857,8 @@
1713
1857
  },
1714
1858
  '.bw_table caption': { 'font-size': '0.875rem', 'caption-side': 'bottom' },
1715
1859
  '.bw_table_bordered > :not(caption) > * > *': { 'border-width': '1px', 'border-style': 'solid' },
1860
+ '.bw_table_selectable > tbody > tr': { 'cursor': 'pointer' },
1861
+ '.bw_table > tbody > tr.bw_table_row_selected > *': { 'background-color': 'rgba(0, 102, 102, 0.1)' },
1716
1862
  '.bw_table_responsive': { 'overflow-x': 'auto', '-webkit-overflow-scrolling': 'touch' }
1717
1863
  },
1718
1864
 
@@ -1766,6 +1912,7 @@
1766
1912
  '.bw_nav_tabs .bw_nav_item': { 'margin-bottom': '-2px' },
1767
1913
  '.bw_nav_link': {
1768
1914
  'display': 'block', 'font-size': '0.875rem', 'font-weight': '500',
1915
+ 'padding': '0.625rem 1rem',
1769
1916
  'text-decoration': 'none', 'cursor': 'pointer',
1770
1917
  'border': 'none', 'background': 'transparent', 'font-family': 'inherit'
1771
1918
  },
@@ -1800,10 +1947,11 @@
1800
1947
  '.bw_page_item': { 'display': 'list-item', 'list-style': 'none' },
1801
1948
  '.bw_page_link': {
1802
1949
  'position': 'relative', 'display': 'block', 'padding': '0.375rem 0.75rem',
1803
- 'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none'
1950
+ 'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none',
1951
+ 'border': '1px solid transparent', 'cursor': 'pointer',
1952
+ 'font-family': 'inherit', 'font-size': 'inherit', 'background': 'none'
1804
1953
  },
1805
- '.bw_page_item:first-child .bw_page_link': { 'margin-left': '0', 'border-top-left-radius': '0.375rem', 'border-bottom-left-radius': '0.375rem' },
1806
- '.bw_page_item:last-child .bw_page_link': { 'border-top-right-radius': '0.375rem', 'border-bottom-right-radius': '0.375rem' },
1954
+ '.bw_page_item:first-child .bw_page_link': { 'margin-left': '0' },
1807
1955
  '.bw_page_link:focus-visible': { 'z-index': '3', 'outline': '2px solid currentColor', 'outline-offset': '-2px' }
1808
1956
  },
1809
1957
 
@@ -1960,6 +2108,7 @@
1960
2108
  '.bw_accordion_header': { 'margin': '0' },
1961
2109
  '.bw_accordion_button': {
1962
2110
  'position': 'relative', 'display': 'flex', 'align-items': 'center', 'width': '100%',
2111
+ 'padding': '0.875rem 1.25rem',
1963
2112
  'font-size': '1rem', 'font-weight': '500', 'text-align': 'left',
1964
2113
  'background-color': 'transparent', 'border': '0', 'overflow-anchor': 'none', 'cursor': 'pointer',
1965
2114
  'font-family': 'inherit'
@@ -1971,10 +2120,9 @@
1971
2120
  'background-repeat': 'no-repeat', 'background-size': '1.25rem'
1972
2121
  },
1973
2122
  '.bw_accordion_button:not(.bw_collapsed)::after': { 'transform': 'rotate(-180deg)' },
1974
- '.bw_accordion_collapse': { 'max-height': '0', 'overflow': 'hidden' },
1975
- '.bw_accordion_collapse.bw_collapse_show': { 'max-height': 'none' },
1976
- '.bw_accordion_item:first-child': { 'border-top-left-radius': '8px', 'border-top-right-radius': '8px' },
1977
- '.bw_accordion_item:last-child': { 'border-bottom-left-radius': '8px', 'border-bottom-right-radius': '8px' }
2123
+ '.bw_accordion_body': { 'padding': '1rem 1.25rem' },
2124
+ '.bw_accordion_collapse': { 'max-height': '0', 'overflow': 'hidden', 'transition': 'max-height 0.3s ease' },
2125
+ '.bw_accordion_collapse.bw_collapse_show': { 'max-height': 'none' }
1978
2126
  },
1979
2127
 
1980
2128
  // ---- Carousel ----
@@ -2120,7 +2268,13 @@
2120
2268
 
2121
2269
  // ---- Stat card ----
2122
2270
  statCard: {
2123
- '.bw_stat_card': { 'border-left': '4px solid transparent' },
2271
+ '.bw_stat_card': {
2272
+ 'padding': '1.25rem',
2273
+ 'border-left': '4px solid transparent',
2274
+ 'border-radius': '0.375rem',
2275
+ 'background-color': 'inherit',
2276
+ 'transition': 'transform 0.15s ease'
2277
+ },
2124
2278
  '.bw_stat_card:hover': { 'transform': 'translateY(-1px)' },
2125
2279
  '.bw_stat_icon': { 'font-size': '1.5rem', 'margin-bottom': '0.5rem' },
2126
2280
  '.bw_stat_value': { 'font-size': '2rem', 'font-weight': '700', 'line-height': '1.2' },
@@ -2483,6 +2637,20 @@
2483
2637
  rules['.list-inline-item'] = { 'display': 'inline-block' };
2484
2638
  rules['.list-inline-item:not(:last-child)'] = { 'margin-right': '.5rem' };
2485
2639
 
2640
+ // Typography — bw_ prefixed utilities via loops
2641
+ var _imp = function(p, v) { var o = {}; o[p] = v + ' !important'; return o; };
2642
+ [['fs',{'xs':'0.75rem','sm':'0.875rem','base':'1rem','lg':'1.125rem','xl':'1.25rem','2xl':'1.5rem'},'font-size'],
2643
+ ['fw',{light:'300',normal:'400',medium:'500',semibold:'600',bold:'700'},'font-weight'],
2644
+ ['lh',{tight:'1.25',normal:'1.5',relaxed:'1.75'},'line-height']
2645
+ ].forEach(function(d) { for (var dk in d[1]) rules['.bw_'+d[0]+'_'+dk] = _imp(d[2], d[1][dk]); });
2646
+
2647
+ // Flex utilities
2648
+ rules['.bw_flex'] = { 'display': 'flex' };
2649
+ rules['.bw_flex_column'] = { 'flex-direction': 'column' };
2650
+ rules['.bw_flex_wrap'] = { 'flex-wrap': 'wrap' };
2651
+ rules['.bw_flex_center'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'center' };
2652
+ for (var gk in spacingValues) rules['.bw_gap_' + gk] = { 'gap': spacingValues[gk] + ' !important' };
2653
+
2486
2654
  // Visibility
2487
2655
  rules['.bw_visible, .visible'] = { 'visibility': 'visible !important' };
2488
2656
  rules['.bw_invisible, .invisible'] = { 'visibility': 'hidden !important' };
@@ -2543,6 +2711,26 @@
2543
2711
  return getStructuralCSS();
2544
2712
  }
2545
2713
 
2714
+ /**
2715
+ * Get CSS reset rules only (box-sizing, html/body font, reduced-motion).
2716
+ * Separate from themed/structural rules for independent injection.
2717
+ * @returns {Object} CSS rules object for the reset layer
2718
+ */
2719
+ function getResetStyles() {
2720
+ var rules = {};
2721
+ Object.assign(rules, structuralRules.base);
2722
+ // Include reduced-motion preference
2723
+ rules['@media (prefers-reduced-motion: reduce)'] = {
2724
+ '*, *::before, *::after': {
2725
+ 'animation-duration': '0.01ms !important',
2726
+ 'animation-iteration-count': '1 !important',
2727
+ 'transition-duration': '0.01ms !important',
2728
+ 'scroll-behavior': 'auto !important'
2729
+ }
2730
+ };
2731
+ return rules;
2732
+ }
2733
+
2546
2734
  // =========================================================================
2547
2735
  // defaultStyles — backward-compatible categorized view
2548
2736
  // =========================================================================
@@ -2572,60 +2760,41 @@
2572
2760
  });
2573
2761
 
2574
2762
  /**
2575
- * Generate alternate-palette CSS scoped under `.bw_theme_alt`.
2576
- * Uses the same `generateThemedCSS()` pipeline as the primary palette —
2577
- * both sides go through identical code paths.
2578
- *
2579
- * @param {string} name - Theme scope name (e.g. 'ocean'). '' for global.
2580
- * @param {Object} altPalette - From derivePalette(deriveAlternateConfig(...))
2581
- * @param {Object} layout - From resolveLayout()
2582
- * @returns {Object} CSS rules object scoped under .bw_theme_alt (+ optional .name)
2763
+ * Prefix every selector in a rules object with a scope selector.
2764
+ * Handles @media/@keyframes blocks and comma-separated selectors.
2765
+ * @param {Object} rules - CSS rules object
2766
+ * @param {string} prefix - Scope prefix (e.g. '#my-dashboard', '.bw_theme_alt')
2767
+ * @param {boolean} [compound=false] - If true, use compound selector (no space)
2768
+ * for the first segment: `#scope.bw_theme_alt .sel` vs `#scope .sel`
2769
+ * @returns {Object} New rules object with scoped selectors
2583
2770
  */
2584
- function generateAlternateCSS(name, altPalette, layout) {
2585
- // Generate themed CSS using the same pipeline as primary
2586
- var rawRules = generateThemedCSS('', altPalette, layout);
2587
-
2588
- // Re-scope every selector under .bw_theme_alt (+ optional theme name)
2589
- var altPrefix = name ? '.' + name + '.bw_theme_alt' : '.bw_theme_alt';
2590
- var altRules = {};
2591
-
2592
- for (var sel in rawRules) {
2593
- if (!rawRules.hasOwnProperty(sel)) continue;
2594
-
2771
+ function scopeRulesUnder(rules, prefix, compound) {
2772
+ var scoped = {};
2773
+ for (var sel in rules) {
2774
+ if (!rules.hasOwnProperty(sel)) continue;
2595
2775
  if (sel.charAt(0) === '@') {
2596
2776
  // @media / @keyframes — recurse into the block
2597
- var innerBlock = rawRules[sel];
2598
- var altInner = {};
2777
+ var innerBlock = rules[sel];
2778
+ var scopedInner = {};
2599
2779
  for (var innerSel in innerBlock) {
2600
2780
  if (!innerBlock.hasOwnProperty(innerSel)) continue;
2601
- altInner[altPrefix + ' ' + innerSel] = innerBlock[innerSel];
2781
+ scopedInner[_prefixSelector(innerSel, prefix)] = innerBlock[innerSel];
2602
2782
  }
2603
- altRules[sel] = altInner;
2783
+ scoped[sel] = scopedInner;
2604
2784
  } else {
2605
- // Regular selector — prefix with alt scope
2606
- // Handle comma-separated selectors
2607
- var parts = sel.split(',');
2608
- var scopedParts = [];
2609
- for (var i = 0; i < parts.length; i++) {
2610
- var s = parts[i].trim();
2611
- // 'body' selector gets special treatment: .bw_theme_alt body
2612
- if (s === 'body' || s.indexOf('body') === 0) {
2613
- scopedParts.push(altPrefix + ' ' + s);
2614
- } else {
2615
- scopedParts.push(altPrefix + ' ' + s);
2616
- }
2617
- }
2618
- altRules[scopedParts.join(', ')] = rawRules[sel];
2785
+ scoped[_prefixSelector(sel, prefix)] = rules[sel];
2619
2786
  }
2620
2787
  }
2788
+ return scoped;
2789
+ }
2621
2790
 
2622
- // Add body-level overrides for the alternate surface
2623
- altRules[altPrefix + ' body, :root' + altPrefix + ' body'] = {
2624
- 'color': altPalette.dark.base,
2625
- 'background-color': altPalette.light.base
2626
- };
2627
-
2628
- return altRules;
2791
+ function _prefixSelector(sel, prefix) {
2792
+ var parts = sel.split(',');
2793
+ var result = [];
2794
+ for (var i = 0; i < parts.length; i++) {
2795
+ result.push(prefix + ' ' + parts[i].trim());
2796
+ }
2797
+ return result.join(', ');
2629
2798
  }
2630
2799
 
2631
2800
  /**
@@ -3349,7 +3518,7 @@
3349
3518
  __monkey_patch_is_nodejs__: {
3350
3519
  _value: 'ignore',
3351
3520
  set: function(x) {
3352
- this._value = (typeof x === 'boolean') ? x : 'ignore';
3521
+ this._value = _is(x, 'boolean') ? x : 'ignore';
3353
3522
  },
3354
3523
  get: function() {
3355
3524
  return this._value;
@@ -3397,6 +3566,67 @@
3397
3566
  configurable: true
3398
3567
  });
3399
3568
 
3569
+ // ── Internal aliases ─────────────────────────────────────────────────────
3570
+ // Short names for frequently-used builtins and internal methods.
3571
+ // Same pattern as v1 (_to = bw.typeOf, etc.).
3572
+ //
3573
+ // Why: Terser can't shorten global property chains (console.warn,
3574
+ // Object.prototype.hasOwnProperty, Array.isArray, document.createElement)
3575
+ // because it can't prove they're side-effect-free. We can, so we alias
3576
+ // them here. Each alias saves bytes in the minified output, and the short
3577
+ // names also reduce visual noise in the hot paths (binding pipeline,
3578
+ // createDOM, etc.).
3579
+ //
3580
+ // Alias Target Sites
3581
+ // ───────── ────────────────────────────────────── ─────
3582
+ // _hop Object.prototype.hasOwnProperty 15
3583
+ // _isA Array.isArray 25
3584
+ // _keys Object.keys 7
3585
+ // _to bw.typeOf (type string) 26
3586
+ // _is type check boolean: _is(x,'string') ~50
3587
+ // _cw console.warn 8
3588
+ // _cl console.log 11
3589
+ // _ce console.error 4
3590
+ // _chp ComponentHandle.prototype 28 (defined after constructor)
3591
+ //
3592
+ // Note: document.createElement etc. are NOT aliased because they require
3593
+ // `this === document` and .bind() would add overhead on every call.
3594
+ // Console aliases use thin wrappers (not direct refs) so test monkey-
3595
+ // patching of console.warn/log/error continues to work.
3596
+ //
3597
+ // `typeof x` for UNDECLARED globals (window, document, process, require,
3598
+ // EventSource, navigator, Promise, __filename, import.meta) MUST stay as
3599
+ // raw `typeof` — calling _to(x) when x doesn't exist throws ReferenceError.
3600
+ //
3601
+ // ── v1 functional type helpers (kept for reference, not currently used) ──
3602
+ // _toa(x, type, trueVal, falseVal) — bw.typeAssign:
3603
+ // returns trueVal if _to(x)===type, else falseVal.
3604
+ // Replaces: (typeof x === 'string') ? A : B → _toa(x,'string',A,B)
3605
+ // _toc(x, type, trueVal, falseVal) — bw.typeConvert:
3606
+ // same as _toa but if trueVal/falseVal are functions, calls them with x.
3607
+ // Replaces: typeof x === 'string' ? fn(x) : default → _toc(x,'string',fn,default)
3608
+ // Uncomment if pattern frequency justifies them:
3609
+ // var _toa = function(x, t, y, n) { return _to(x) === t ? y : n; };
3610
+ // 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); };
3611
+ // ─────────────────────────────────────────────────────────────────────────
3612
+ var _hop = Object.prototype.hasOwnProperty;
3613
+ var _isA = Array.isArray;
3614
+ var _keys = Object.keys;
3615
+ var _to = typeOf; // imported from bitwrench-utils.js
3616
+ var _is = function(x, t) { var r = _to(x); return r === t || r.toLowerCase() === t; };
3617
+ // Console aliases use thin wrappers (not direct references) so that test
3618
+ // code can monkey-patch console.warn/log/error and the patches take effect.
3619
+ var _cw = function() { console.warn.apply(console, arguments); };
3620
+ var _cl = function() { console.log.apply(console, arguments); };
3621
+ var _ce = function() { console.error.apply(console, arguments); };
3622
+
3623
+ /**
3624
+ * Debug flag. When true, emits console.warn for silent binding failures
3625
+ * (missing paths, null refs, auto-created intermediate objects).
3626
+ * @type {boolean}
3627
+ */
3628
+ bw.debug = false;
3629
+
3400
3630
  /**
3401
3631
  * Lazy-resolve Node.js `fs` module.
3402
3632
  * Tries require('fs') first (available in CJS/UMD Node.js builds),
@@ -3544,7 +3774,7 @@
3544
3774
  */
3545
3775
  bw._el = function(id) {
3546
3776
  // Pass-through for DOM elements
3547
- if (typeof id !== 'string') return id || null;
3777
+ if (!_is(id, 'string')) return id || null;
3548
3778
  if (!id) return null;
3549
3779
  if (!bw._isBrowser) return null;
3550
3780
 
@@ -3572,7 +3802,12 @@
3572
3802
  el = document.querySelector('[data-bw_id="' + id + '"]');
3573
3803
  }
3574
3804
 
3575
- // 5. Cache the result for next time
3805
+ // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
3806
+ if (!el && id.indexOf('bw_uuid_') === 0) {
3807
+ el = document.querySelector('.' + id);
3808
+ }
3809
+
3810
+ // 6. Cache the result for next time
3576
3811
  if (el) {
3577
3812
  bw._nodeMap[id] = el;
3578
3813
  }
@@ -3625,6 +3860,84 @@
3625
3860
  }
3626
3861
  };
3627
3862
 
3863
+ // ===================================================================================
3864
+ // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
3865
+ // ===================================================================================
3866
+
3867
+ /**
3868
+ * Regex to match a bw_uuid_* token in a class string.
3869
+ * @private
3870
+ */
3871
+ var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
3872
+
3873
+ /**
3874
+ * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
3875
+ *
3876
+ * Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
3877
+ * to replace an existing UUID (useful in loops where each TACO needs a unique ID).
3878
+ *
3879
+ * @param {Object} taco - A TACO object `{t, a, c, o}`
3880
+ * @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
3881
+ * @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
3882
+ * @category Identifiers
3883
+ * @example
3884
+ * var card = bw.makeStatCard({ value: '0', label: 'Scans' });
3885
+ * var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
3886
+ * var same = bw.assignUUID(card); // same UUID (idempotent)
3887
+ * var diff = bw.assignUUID(card, true); // new UUID (forced)
3888
+ */
3889
+ bw.assignUUID = function(taco, forceNew) {
3890
+ if (!taco || !_is(taco, 'object')) return null;
3891
+
3892
+ // Ensure taco.a exists
3893
+ if (!taco.a) taco.a = {};
3894
+ if (!_is(taco.a.class, 'string')) taco.a.class = taco.a.class ? String(taco.a.class) : '';
3895
+
3896
+ var existing = taco.a.class.match(_UUID_RE);
3897
+
3898
+ if (existing && !forceNew) {
3899
+ return existing[0];
3900
+ }
3901
+
3902
+ // Remove old UUID if forceNew
3903
+ if (existing) {
3904
+ taco.a.class = taco.a.class.replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
3905
+ }
3906
+
3907
+ var uuid = bw.uuid('uuid');
3908
+ taco.a.class = (taco.a.class ? taco.a.class + ' ' : '') + uuid;
3909
+ return uuid;
3910
+ };
3911
+
3912
+ /**
3913
+ * Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
3914
+ *
3915
+ * @param {Object|Element} tacoOrElement - A TACO object or DOM element
3916
+ * @returns {string|null} The UUID string, or null if none assigned
3917
+ * @category Identifiers
3918
+ * @example
3919
+ * bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
3920
+ * bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
3921
+ * bw.getUUID({t:'div'}) // null (no UUID)
3922
+ */
3923
+ bw.getUUID = function(tacoOrElement) {
3924
+ if (!tacoOrElement) return null;
3925
+
3926
+ var classStr;
3927
+ // DOM element: check className
3928
+ if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
3929
+ classStr = tacoOrElement.className;
3930
+ }
3931
+ // TACO object: check a.class
3932
+ else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
3933
+ classStr = tacoOrElement.a.class;
3934
+ }
3935
+
3936
+ if (!classStr) return null;
3937
+ var match = classStr.match(_UUID_RE);
3938
+ return match ? match[0] : null;
3939
+ };
3940
+
3628
3941
  /**
3629
3942
  * Escape HTML special characters to prevent XSS.
3630
3943
  *
@@ -3640,7 +3953,7 @@
3640
3953
  * // => '&lt;b&gt;Hello&lt;&#x2F;b&gt; &amp; &quot;world&quot;'
3641
3954
  */
3642
3955
  bw.escapeHTML = function(str) {
3643
- if (typeof str !== 'string') return '';
3956
+ if (!_is(str, 'string')) return '';
3644
3957
 
3645
3958
  const escapeMap = {
3646
3959
  '&': '&amp;',
@@ -3674,6 +3987,42 @@
3674
3987
  return { __bw_raw: true, v: String(str) };
3675
3988
  };
3676
3989
 
3990
+ /**
3991
+ * Hyperscript-style TACO constructor.
3992
+ *
3993
+ * A convenience helper that returns a canonical TACO object from positional
3994
+ * arguments. The return value is a plain object — serializable, works with
3995
+ * bwserve, and accepted everywhere TACO is accepted.
3996
+ *
3997
+ * @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
3998
+ * @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
3999
+ * @param {*} [content] - Content: string, number, TACO object, or array of children.
4000
+ * @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
4001
+ * @returns {Object} Plain TACO object {t, a?, c?, o?}
4002
+ * @category Utilities
4003
+ * @see bw.html
4004
+ * @see bw.createDOM
4005
+ * @see bw.DOM
4006
+ * @example
4007
+ * bw.h('div')
4008
+ * // => { t: 'div' }
4009
+ *
4010
+ * bw.h('p', { class: 'bw_text_muted' }, 'Hello')
4011
+ * // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
4012
+ *
4013
+ * bw.h('ul', null, [
4014
+ * bw.h('li', null, 'one'),
4015
+ * bw.h('li', null, 'two')
4016
+ * ])
4017
+ * // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
4018
+ */
4019
+ bw.h = function(tag, attrs, content, options) {
4020
+ var taco = { t: String(tag) };
4021
+ if (attrs !== null && attrs !== undefined) taco.a = attrs;
4022
+ if (content !== undefined) taco.c = content;
4023
+ if (options !== undefined) taco.o = options;
4024
+ return taco;
4025
+ };
3677
4026
 
3678
4027
  /**
3679
4028
  * Convert a TACO object (or array of TACOs) to an HTML string.
@@ -3713,7 +4062,7 @@
3713
4062
  }
3714
4063
 
3715
4064
  // Handle arrays of TACOs
3716
- if (Array.isArray(taco)) {
4065
+ if (_isA(taco)) {
3717
4066
  return taco.map(t => bw.html(t, options)).join('');
3718
4067
  }
3719
4068
 
@@ -3736,15 +4085,15 @@
3736
4085
  if (taco && taco._bwEach && options.state) {
3737
4086
  var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
3738
4087
  var arr = bw._evaluatePath(options.state, eachExpr);
3739
- if (!Array.isArray(arr)) return '';
4088
+ if (!_isA(arr)) return '';
3740
4089
  return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
3741
4090
  }
3742
4091
 
3743
4092
  // Handle primitives and non-TACO objects
3744
- if (typeof taco !== 'object' || !taco.t) {
4093
+ if (!_is(taco, 'object') || !taco.t) {
3745
4094
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
3746
4095
  // Resolve template bindings if state provided
3747
- if (options.state && typeof str === 'string' && str.indexOf('${') >= 0) {
4096
+ if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
3748
4097
  str = bw._resolveTemplate(str, options.state, !!options.compile);
3749
4098
  }
3750
4099
  return str;
@@ -3764,10 +4113,18 @@
3764
4113
  // Skip null, undefined, false
3765
4114
  if (value == null || value === false) continue;
3766
4115
 
3767
- // Skip event handlers (they're for DOM only)
3768
- if (key.startsWith('on')) continue;
4116
+ // Serialize event handlers via funcRegister
4117
+ if (key.startsWith('on')) {
4118
+ if (_is(value, 'function')) {
4119
+ var fnId = bw.funcRegister(value);
4120
+ attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
4121
+ } else if (_is(value, 'string')) {
4122
+ attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
4123
+ }
4124
+ continue;
4125
+ }
3769
4126
 
3770
- if (key === 'style' && typeof value === 'object') {
4127
+ if (key === 'style' && _is(value, 'object')) {
3771
4128
  // Convert style object to string
3772
4129
  const styleStr = Object.entries(value)
3773
4130
  .filter(([, v]) => v != null)
@@ -3778,7 +4135,7 @@
3778
4135
  }
3779
4136
  } else if (key === 'class') {
3780
4137
  // Handle class as array or string
3781
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
4138
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
3782
4139
  if (classStr) {
3783
4140
  attrStr += ` class="${bw.escapeHTML(classStr)}"`;
3784
4141
  }
@@ -3814,13 +4171,184 @@
3814
4171
  // Process content recursively
3815
4172
  let contentStr = content != null ? bw.html(content, options) : '';
3816
4173
  // Resolve template bindings in content if state provided
3817
- if (options.state && typeof contentStr === 'string' && contentStr.indexOf('${') >= 0) {
4174
+ if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
3818
4175
  contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
3819
4176
  }
3820
4177
 
3821
4178
  return `<${tag}${attrStr}>${contentStr}</${tag}>`;
3822
4179
  };
3823
4180
 
4181
+ /**
4182
+ * Generate a complete, self-contained HTML document from TACO content.
4183
+ *
4184
+ * Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
4185
+ * func registry emission (so serialized event handlers work), optional theme,
4186
+ * and extra head elements. Designed for static site generation, offline/airgapped
4187
+ * use, and the "static site that isn't static" workflow.
4188
+ *
4189
+ * @param {Object} [opts={}] - Page options
4190
+ * @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
4191
+ * @param {string} [opts.title='bitwrench'] - Page title
4192
+ * @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
4193
+ * @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
4194
+ * @param {string} [opts.css=''] - Additional CSS for <style> block
4195
+ * @param {string|Object} [opts.theme=null] - Theme preset name or config object
4196
+ * @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
4197
+ * @param {string} [opts.favicon=''] - Favicon URL
4198
+ * @param {string} [opts.lang='en'] - HTML lang attribute
4199
+ * @returns {string} Complete HTML document string
4200
+ * @category DOM Generation
4201
+ * @see bw.html
4202
+ * @example
4203
+ * bw.htmlPage({
4204
+ * title: 'My App',
4205
+ * body: { t: 'h1', c: 'Hello World' },
4206
+ * runtime: 'shim'
4207
+ * })
4208
+ */
4209
+ bw.htmlPage = function(opts) {
4210
+ opts = opts || {};
4211
+ var title = opts.title || 'bitwrench';
4212
+ var body = opts.body || '';
4213
+ var state = opts.state || undefined;
4214
+ var runtime = opts.runtime || 'shim';
4215
+ var css = opts.css || '';
4216
+ var theme = opts.theme || null;
4217
+ var headExtra = opts.head || [];
4218
+ var favicon = opts.favicon || '';
4219
+ var lang = opts.lang || 'en';
4220
+
4221
+ // Snapshot funcRegistry counter before rendering
4222
+ var fnCounterBefore = bw._fnIDCounter;
4223
+
4224
+ // Render body content
4225
+ var bodyHTML = '';
4226
+ if (_is(body, 'string')) {
4227
+ bodyHTML = body;
4228
+ } else {
4229
+ var htmlOpts = {};
4230
+ if (state) htmlOpts.state = state;
4231
+ bodyHTML = bw.html(body, htmlOpts);
4232
+ }
4233
+
4234
+ // Collect functions registered during this render
4235
+ var fnCounterAfter = bw._fnIDCounter;
4236
+ var registryEntries = '';
4237
+ for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
4238
+ var fnKey = 'bw_fn_' + i;
4239
+ if (bw._fnRegistry[fnKey]) {
4240
+ registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' +
4241
+ bw._fnRegistry[fnKey].toString() + ';\n';
4242
+ }
4243
+ }
4244
+
4245
+ // Build runtime script for <head>
4246
+ var runtimeHead = '';
4247
+ if (runtime === 'inline') {
4248
+ // Read UMD bundle synchronously if in Node.js
4249
+ var umdSource = null;
4250
+ if (bw._isNode) {
4251
+ try {
4252
+ var fs = (typeof require === 'function') ? require('fs') : null;
4253
+ var pathMod = (typeof require === 'function') ? require('path') : null;
4254
+ if (fs && pathMod) {
4255
+ // Resolve dist/ relative to this source file
4256
+ var srcDir = '';
4257
+ try { srcDir = pathMod.dirname((typeof __filename !== 'undefined') ? __filename : ''); }
4258
+ catch(e2) { /* ESM: __filename not available */ }
4259
+ if (!srcDir && typeof ({ url: (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench-lean.umd.js', document.baseURI).href)) }) !== 'undefined' && (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench-lean.umd.js', document.baseURI).href))) {
4260
+ var url = (typeof require === 'function') ? require('url') : null;
4261
+ if (url && url.fileURLToPath) srcDir = pathMod.dirname(url.fileURLToPath((typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench-lean.umd.js', document.baseURI).href))));
4262
+ }
4263
+ if (srcDir) {
4264
+ var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
4265
+ umdSource = fs.readFileSync(distPath, 'utf8');
4266
+ }
4267
+ }
4268
+ } catch(e) { /* fall through */ }
4269
+ }
4270
+ if (umdSource) {
4271
+ runtimeHead = '<script>' + umdSource + '</script>';
4272
+ } else {
4273
+ // Fallback to shim in browser or if dist not available
4274
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
4275
+ }
4276
+ } else if (runtime === 'cdn') {
4277
+ runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
4278
+ } else if (runtime === 'shim') {
4279
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
4280
+ }
4281
+ // runtime === 'none' → empty
4282
+
4283
+ // Theme CSS
4284
+ var themeCSS = '';
4285
+ if (theme) {
4286
+ var themeConfig = _is(theme, 'string')
4287
+ ? (THEME_PRESETS[theme.toLowerCase()] || null)
4288
+ : theme;
4289
+ if (themeConfig) {
4290
+ var themeResult = bw.makeStyles(themeConfig);
4291
+ themeCSS = themeResult.css;
4292
+ }
4293
+ }
4294
+
4295
+ // Extra <head> elements
4296
+ var headHTML = '';
4297
+ if (_isA(headExtra) && headExtra.length > 0) {
4298
+ headHTML = headExtra.map(function(el) { return bw.html(el); }).join('\n');
4299
+ }
4300
+
4301
+ // Favicon
4302
+ var faviconTag = '';
4303
+ if (favicon) {
4304
+ var safeFavicon = favicon.replace(/[&<>"']/g, function(c) {
4305
+ return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
4306
+ });
4307
+ faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
4308
+ }
4309
+
4310
+ // Escaped title
4311
+ var safeTitle = bw.escapeHTML(title);
4312
+
4313
+ // Combine all CSS
4314
+ var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
4315
+
4316
+ // Body-end script: registry entries + optional loadStyles
4317
+ var bodyEndScript = '';
4318
+ var bodyEndParts = [];
4319
+ if (registryEntries) {
4320
+ bodyEndParts.push(registryEntries);
4321
+ }
4322
+ if (runtime === 'inline' || runtime === 'cdn') {
4323
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
4324
+ }
4325
+ if (bodyEndParts.length > 0) {
4326
+ bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
4327
+ }
4328
+
4329
+ // Assemble document
4330
+ var parts = [
4331
+ '<!DOCTYPE html>',
4332
+ '<html lang="' + lang + '">',
4333
+ '<head>',
4334
+ '<meta charset="UTF-8">',
4335
+ '<meta name="viewport" content="width=device-width, initial-scale=1">'
4336
+ ];
4337
+ parts.push('<title>' + safeTitle + '</title>');
4338
+ if (faviconTag) parts.push(faviconTag);
4339
+ if (runtimeHead) parts.push(runtimeHead);
4340
+ if (headHTML) parts.push(headHTML);
4341
+ if (allCSS) parts.push('<style>' + allCSS + '</style>');
4342
+ parts.push('</head>');
4343
+ parts.push('<body>');
4344
+ parts.push(bodyHTML);
4345
+ if (bodyEndScript) parts.push(bodyEndScript);
4346
+ parts.push('</body>');
4347
+ parts.push('</html>');
4348
+
4349
+ return parts.join('\n');
4350
+ };
4351
+
3824
4352
  /**
3825
4353
  * Create a live DOM element from a TACO object (browser only).
3826
4354
  *
@@ -3865,7 +4393,7 @@
3865
4393
  }
3866
4394
 
3867
4395
  // Handle text nodes
3868
- if (typeof taco !== 'object' || !taco.t) {
4396
+ if (!_is(taco, 'object') || !taco.t) {
3869
4397
  return document.createTextNode(String(taco));
3870
4398
  }
3871
4399
 
@@ -3878,16 +4406,16 @@
3878
4406
  for (const [key, value] of Object.entries(attrs)) {
3879
4407
  if (value == null || value === false) continue;
3880
4408
 
3881
- if (key === 'style' && typeof value === 'object') {
4409
+ if (key === 'style' && _is(value, 'object')) {
3882
4410
  // Apply styles directly
3883
4411
  Object.assign(el.style, value);
3884
4412
  } else if (key === 'class') {
3885
4413
  // Handle class as array or string
3886
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
4414
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
3887
4415
  if (classStr) {
3888
4416
  el.className = classStr;
3889
4417
  }
3890
- } else if (key.startsWith('on') && typeof value === 'function') {
4418
+ } else if (key.startsWith('on') && _is(value, 'function')) {
3891
4419
  // Event handlers
3892
4420
  const eventName = key.slice(2).toLowerCase();
3893
4421
  el.addEventListener(eventName, value);
@@ -3907,7 +4435,7 @@
3907
4435
  // Children with data-bw_id or id attributes get local refs on the parent,
3908
4436
  // so o.render functions can access them without any DOM lookup.
3909
4437
  if (content != null) {
3910
- if (Array.isArray(content)) {
4438
+ if (_isA(content)) {
3911
4439
  content.forEach(child => {
3912
4440
  if (child != null) {
3913
4441
  // Handle ComponentHandle in content arrays (Level 2 children)
@@ -3927,20 +4455,20 @@
3927
4455
  if (childEl._bw_refs) {
3928
4456
  if (!el._bw_refs) el._bw_refs = {};
3929
4457
  for (var rk in childEl._bw_refs) {
3930
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
4458
+ if (_hop.call(childEl._bw_refs, rk)) {
3931
4459
  el._bw_refs[rk] = childEl._bw_refs[rk];
3932
4460
  }
3933
4461
  }
3934
4462
  }
3935
4463
  }
3936
4464
  });
3937
- } else if (typeof content === 'object' && content.__bw_raw) {
4465
+ } else if (_is(content, 'object') && content.__bw_raw) {
3938
4466
  // Raw HTML content — inject via innerHTML
3939
4467
  el.innerHTML = content.v;
3940
4468
  } else if (content._bwComponent === true) {
3941
4469
  // Single ComponentHandle as content
3942
4470
  content.mount(el);
3943
- } else if (typeof content === 'object' && content.t) {
4471
+ } else if (_is(content, 'object') && content.t) {
3944
4472
  var childEl = bw.createDOM(content, options);
3945
4473
  el.appendChild(childEl);
3946
4474
  var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
@@ -3951,7 +4479,7 @@
3951
4479
  if (childEl._bw_refs) {
3952
4480
  if (!el._bw_refs) el._bw_refs = {};
3953
4481
  for (var rk in childEl._bw_refs) {
3954
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
4482
+ if (_hop.call(childEl._bw_refs, rk)) {
3955
4483
  el._bw_refs[rk] = childEl._bw_refs[rk];
3956
4484
  }
3957
4485
  }
@@ -3966,6 +4494,14 @@
3966
4494
  bw._registerNode(el, null);
3967
4495
  }
3968
4496
 
4497
+ // Register UUID class in node cache (bw_uuid_* tokens in class string)
4498
+ if (el.className) {
4499
+ var uuidMatch = el.className.match(_UUID_RE);
4500
+ if (uuidMatch) {
4501
+ bw._nodeMap[uuidMatch[0]] = el;
4502
+ }
4503
+ }
4504
+
3969
4505
  // Handle lifecycle hooks and state
3970
4506
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
3971
4507
  const id = attrs['data-bw_id'] || bw.uuid();
@@ -3984,7 +4520,7 @@
3984
4520
  el._bw_render = opts.render;
3985
4521
 
3986
4522
  if (opts.mounted) {
3987
- console.warn('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
4523
+ _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
3988
4524
  }
3989
4525
 
3990
4526
  // Queue initial render (same timing as mounted)
@@ -4057,7 +4593,7 @@
4057
4593
  const targetEl = bw._el(target);
4058
4594
 
4059
4595
  if (!targetEl) {
4060
- console.error('bw.DOM: Target element not found:', target);
4596
+ _ce('bw.DOM: Target element not found:', target);
4061
4597
  return null;
4062
4598
  }
4063
4599
 
@@ -4097,7 +4633,7 @@
4097
4633
  targetEl.appendChild(taco.element);
4098
4634
  }
4099
4635
  // Handle arrays
4100
- else if (Array.isArray(taco)) {
4636
+ else if (_isA(taco)) {
4101
4637
  taco.forEach(t => {
4102
4638
  if (t != null) {
4103
4639
  if (t._bwComponent === true) {
@@ -4133,7 +4669,7 @@
4133
4669
  bw.compileProps = function(handle, props = {}) {
4134
4670
  const compiledProps = {};
4135
4671
 
4136
- Object.keys(props).forEach(key => {
4672
+ _keys(props).forEach(key => {
4137
4673
  // Create getter/setter for each prop
4138
4674
  Object.defineProperty(compiledProps, key, {
4139
4675
  get() {
@@ -4338,6 +4874,16 @@
4338
4874
  bw.cleanup = function(element) {
4339
4875
  if (!bw._isBrowser || !element) return;
4340
4876
 
4877
+ // Deregister UUID classes from node cache (element + descendants)
4878
+ // Covers elements that have UUID but no data-bw_id
4879
+ var selfUuidMatch = element.className && element.className.match(_UUID_RE);
4880
+ if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
4881
+ var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
4882
+ uuidEls.forEach(function(uel) {
4883
+ var m = uel.className && uel.className.match(_UUID_RE);
4884
+ if (m) delete bw._nodeMap[m[0]];
4885
+ });
4886
+
4341
4887
  // Find all elements with data-bw_id
4342
4888
  const elements = element.querySelectorAll('[data-bw_id]');
4343
4889
 
@@ -4353,6 +4899,10 @@
4353
4899
  // Deregister from node cache
4354
4900
  bw._deregisterNode(el, id);
4355
4901
 
4902
+ // Deregister UUID class from node cache
4903
+ var uuidMatch = el.className && el.className.match(_UUID_RE);
4904
+ if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
4905
+
4356
4906
  // Clean up pub/sub subscriptions tied to this element
4357
4907
  if (el._bw_subs) {
4358
4908
  el._bw_subs.forEach(function(unsub) { unsub(); });
@@ -4377,6 +4927,10 @@
4377
4927
  // Deregister from node cache
4378
4928
  bw._deregisterNode(element, id);
4379
4929
 
4930
+ // Deregister UUID class from node cache
4931
+ var elemUuidMatch = element.className && element.className.match(_UUID_RE);
4932
+ if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
4933
+
4380
4934
  // Clean up pub/sub subscriptions tied to element itself
4381
4935
  if (element._bw_subs) {
4382
4936
  element._bw_subs.forEach(function(unsub) { unsub(); });
@@ -4451,17 +5005,17 @@
4451
5005
  if (attr) {
4452
5006
  // Patch an attribute
4453
5007
  el.setAttribute(attr, String(content));
4454
- } else if (Array.isArray(content)) {
5008
+ } else if (_isA(content)) {
4455
5009
  // Patch with array of children (strings and/or TACOs)
4456
5010
  el.innerHTML = '';
4457
5011
  content.forEach(function(item) {
4458
- if (typeof item === 'string' || typeof item === 'number') {
5012
+ if (_is(item, 'string') || _is(item, 'number')) {
4459
5013
  el.appendChild(document.createTextNode(String(item)));
4460
5014
  } else if (item && item.t) {
4461
5015
  el.appendChild(bw.createDOM(item));
4462
5016
  }
4463
5017
  });
4464
- } else if (typeof content === 'object' && content !== null && content.t) {
5018
+ } else if (_is(content, 'object') && content.t) {
4465
5019
  // Patch with a TACO — replace children
4466
5020
  el.innerHTML = '';
4467
5021
  el.appendChild(bw.createDOM(content));
@@ -4492,7 +5046,7 @@
4492
5046
  bw.patchAll = function(patches) {
4493
5047
  var results = {};
4494
5048
  for (var id in patches) {
4495
- if (Object.prototype.hasOwnProperty.call(patches, id)) {
5049
+ if (_hop.call(patches, id)) {
4496
5050
  results[id] = bw.patch(id, patches[id]);
4497
5051
  }
4498
5052
  }
@@ -4589,7 +5143,7 @@
4589
5143
  snapshot[i].handler(detail);
4590
5144
  called++;
4591
5145
  } catch (err) {
4592
- console.warn('bw.pub: subscriber error on topic "' + topic + '":', err);
5146
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
4593
5147
  }
4594
5148
  }
4595
5149
  return called;
@@ -4685,8 +5239,8 @@
4685
5239
  * @see bw.funcGetDispatchStr
4686
5240
  */
4687
5241
  bw.funcRegister = function(fn, name) {
4688
- if (typeof fn !== 'function') return '';
4689
- var fnID = (typeof name === 'string' && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
5242
+ if (!_is(fn, 'function')) return '';
5243
+ var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
4690
5244
  bw._fnRegistry[fnID] = fn;
4691
5245
  return fnID;
4692
5246
  };
@@ -4705,7 +5259,7 @@
4705
5259
  bw.funcGetById = function(name, errFn) {
4706
5260
  name = String(name);
4707
5261
  if (name in bw._fnRegistry) return bw._fnRegistry[name];
4708
- return (typeof errFn === 'function') ? errFn : function() { console.warn('bw.funcGetById: unregistered fn "' + name + '"'); };
5262
+ return _is(errFn, 'function') ? errFn : function() { _cw('bw.funcGetById: unregistered fn "' + name + '"'); };
4709
5263
  };
4710
5264
 
4711
5265
  /**
@@ -4746,13 +5300,30 @@
4746
5300
  bw.funcGetRegistry = function() {
4747
5301
  var copy = {};
4748
5302
  for (var k in bw._fnRegistry) {
4749
- if (Object.prototype.hasOwnProperty.call(bw._fnRegistry, k)) {
5303
+ if (_hop.call(bw._fnRegistry, k)) {
4750
5304
  copy[k] = bw._fnRegistry[k];
4751
5305
  }
4752
5306
  }
4753
5307
  return copy;
4754
5308
  };
4755
5309
 
5310
+ /**
5311
+ * Minimal runtime shim for funcRegister dispatch in static HTML.
5312
+ * When embedded in a `<script>` tag, provides just enough infrastructure
5313
+ * for `bw.funcGetById()` calls to resolve. The actual function bodies
5314
+ * are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
5315
+ * @type {string}
5316
+ * @category Function Registry
5317
+ */
5318
+ bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' +
5319
+ 'if(!bw._fnRegistry)bw._fnRegistry={};' +
5320
+ 'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' +
5321
+ 'console.warn("bw: unregistered fn "+n)};};' +
5322
+ 'bw.funcRegister=function(fn,name){' +
5323
+ 'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' +
5324
+ 'bw._fnRegistry[id]=fn;return id;};' +
5325
+ 'window.bw=bw;})();';
5326
+
4756
5327
  // ===================================================================================
4757
5328
  // Template Binding Utilities
4758
5329
  // ===================================================================================
@@ -4780,7 +5351,10 @@
4780
5351
  var parts = path.split('.');
4781
5352
  var val = state;
4782
5353
  for (var i = 0; i < parts.length; i++) {
4783
- if (val == null) return '';
5354
+ if (val == null) {
5355
+ if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
5356
+ return '';
5357
+ }
4784
5358
  val = val[parts[i]];
4785
5359
  }
4786
5360
  return (val == null) ? '' : val;
@@ -4800,7 +5374,7 @@
4800
5374
  */
4801
5375
  bw._compiledExprs = {};
4802
5376
  bw._resolveTemplate = function(str, state, compile) {
4803
- if (typeof str !== 'string' || str.indexOf('${') < 0) return str;
5377
+ if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
4804
5378
  var bindings = bw._parseBindings(str);
4805
5379
  if (bindings.length === 0) return str;
4806
5380
 
@@ -4822,6 +5396,7 @@
4822
5396
  try {
4823
5397
  val = bw._compiledExprs[b.expr](state);
4824
5398
  } catch (e) {
5399
+ if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
4825
5400
  val = '';
4826
5401
  }
4827
5402
  } else {
@@ -4930,7 +5505,7 @@
4930
5505
  this._state = {};
4931
5506
  if (o.state) {
4932
5507
  for (var k in o.state) {
4933
- if (Object.prototype.hasOwnProperty.call(o.state, k)) {
5508
+ if (_hop.call(o.state, k)) {
4934
5509
  this._state[k] = o.state[k];
4935
5510
  }
4936
5511
  }
@@ -4939,7 +5514,7 @@
4939
5514
  this._actions = {};
4940
5515
  if (o.actions) {
4941
5516
  for (var k2 in o.actions) {
4942
- if (Object.prototype.hasOwnProperty.call(o.actions, k2)) {
5517
+ if (_hop.call(o.actions, k2)) {
4943
5518
  this._actions[k2] = o.actions[k2];
4944
5519
  }
4945
5520
  }
@@ -4949,7 +5524,7 @@
4949
5524
  if (o.methods) {
4950
5525
  var self = this;
4951
5526
  for (var k3 in o.methods) {
4952
- if (Object.prototype.hasOwnProperty.call(o.methods, k3)) {
5527
+ if (_hop.call(o.methods, k3)) {
4953
5528
  this._methods[k3] = o.methods[k3];
4954
5529
  (function(methodName, methodFn) {
4955
5530
  self[methodName] = function() {
@@ -4967,7 +5542,7 @@
4967
5542
  willMount: o.willMount || null,
4968
5543
  mounted: o.mounted || null,
4969
5544
  willUpdate: o.willUpdate || null,
4970
- onUpdate: o.onUpdate || null,
5545
+ onUpdate: o.onUpdate || o.updated || null,
4971
5546
  unmount: o.unmount || null,
4972
5547
  willDestroy: o.willDestroy || null
4973
5548
  };
@@ -4982,14 +5557,23 @@
4982
5557
  this._compile = !!o.compile;
4983
5558
  this._bw_refs = {};
4984
5559
  this._refCounter = 0;
5560
+ // Child component ownership (Bug #5)
5561
+ this._children = [];
5562
+ this._parent = null;
5563
+ // Factory metadata for BCCL rebuild (Bug #6)
5564
+ this._factory = taco._bwFactory || null;
4985
5565
  }
4986
5566
 
5567
+ // Short alias for ComponentHandle.prototype (see alias block at top of file).
5568
+ // 28 method definitions × 25 chars = ~700B raw savings in minified output.
5569
+ var _chp = ComponentHandle.prototype;
5570
+
4987
5571
  // ── State Methods ──
4988
5572
 
4989
5573
  /**
4990
5574
  * Get a state value. Dot-path supported: `get('user.name')`
4991
5575
  */
4992
- ComponentHandle.prototype.get = function(key) {
5576
+ _chp.get = function(key) {
4993
5577
  return bw._evaluatePath(this._state, key);
4994
5578
  };
4995
5579
 
@@ -4999,12 +5583,13 @@
4999
5583
  * @param {*} value - New value
5000
5584
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
5001
5585
  */
5002
- ComponentHandle.prototype.set = function(key, value, opts) {
5586
+ _chp.set = function(key, value, opts) {
5003
5587
  // Dot-path set
5004
5588
  var parts = key.split('.');
5005
5589
  var obj = this._state;
5006
5590
  for (var i = 0; i < parts.length - 1; i++) {
5007
- if (obj[parts[i]] == null || typeof obj[parts[i]] !== 'object') {
5591
+ if (!_is(obj[parts[i]], 'object')) {
5592
+ if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
5008
5593
  obj[parts[i]] = {};
5009
5594
  }
5010
5595
  obj = obj[parts[i]];
@@ -5024,10 +5609,10 @@
5024
5609
  /**
5025
5610
  * Get a shallow clone of the full state.
5026
5611
  */
5027
- ComponentHandle.prototype.getState = function() {
5612
+ _chp.getState = function() {
5028
5613
  var clone = {};
5029
5614
  for (var k in this._state) {
5030
- if (Object.prototype.hasOwnProperty.call(this._state, k)) {
5615
+ if (_hop.call(this._state, k)) {
5031
5616
  clone[k] = this._state[k];
5032
5617
  }
5033
5618
  }
@@ -5039,9 +5624,9 @@
5039
5624
  * @param {Object} updates - Key-value pairs to merge
5040
5625
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
5041
5626
  */
5042
- ComponentHandle.prototype.setState = function(updates, opts) {
5627
+ _chp.setState = function(updates, opts) {
5043
5628
  for (var k in updates) {
5044
- if (Object.prototype.hasOwnProperty.call(updates, k)) {
5629
+ if (_hop.call(updates, k)) {
5045
5630
  this._state[k] = updates[k];
5046
5631
  this._dirtyKeys[k] = true;
5047
5632
  }
@@ -5058,9 +5643,9 @@
5058
5643
  /**
5059
5644
  * Push a value onto an array in state. Clones the array.
5060
5645
  */
5061
- ComponentHandle.prototype.push = function(key, val) {
5646
+ _chp.push = function(key, val) {
5062
5647
  var arr = this.get(key);
5063
- var newArr = Array.isArray(arr) ? arr.slice() : [];
5648
+ var newArr = _isA(arr) ? arr.slice() : [];
5064
5649
  newArr.push(val);
5065
5650
  this.set(key, newArr);
5066
5651
  };
@@ -5068,9 +5653,9 @@
5068
5653
  /**
5069
5654
  * Splice an array in state. Clones the array.
5070
5655
  */
5071
- ComponentHandle.prototype.splice = function(key, start, deleteCount) {
5656
+ _chp.splice = function(key, start, deleteCount) {
5072
5657
  var arr = this.get(key);
5073
- var newArr = Array.isArray(arr) ? arr.slice() : [];
5658
+ var newArr = _isA(arr) ? arr.slice() : [];
5074
5659
  var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
5075
5660
  Array.prototype.splice.apply(newArr, args);
5076
5661
  this.set(key, newArr);
@@ -5078,7 +5663,7 @@
5078
5663
 
5079
5664
  // ── Scheduling ──
5080
5665
 
5081
- ComponentHandle.prototype._scheduleDirty = function() {
5666
+ _chp._scheduleDirty = function() {
5082
5667
  if (!this._scheduled) {
5083
5668
  this._scheduled = true;
5084
5669
  bw._dirtyComponents.push(this);
@@ -5093,17 +5678,17 @@
5093
5678
  * Creates binding descriptors with refIds for targeted DOM updates.
5094
5679
  * @private
5095
5680
  */
5096
- ComponentHandle.prototype._compileBindings = function() {
5681
+ _chp._compileBindings = function() {
5097
5682
  this._bindings = [];
5098
5683
  this._refCounter = 0;
5099
- var stateKeys = Object.keys(this._state);
5684
+ var stateKeys = _keys(this._state);
5100
5685
  var self = this;
5101
5686
 
5102
5687
  function walkTaco(taco, path) {
5103
- if (taco == null || typeof taco !== 'object' || !taco.t) return taco;
5688
+ if (!_is(taco, 'object') || !taco.t) return taco;
5104
5689
 
5105
5690
  // Check content for bindings
5106
- if (typeof taco.c === 'string' && taco.c.indexOf('${') >= 0) {
5691
+ if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
5107
5692
  var refId = 'bw_ref_' + self._refCounter++;
5108
5693
  var parsed = bw._parseBindings(taco.c);
5109
5694
  var deps = [];
@@ -5125,10 +5710,10 @@
5125
5710
  // Check attributes for bindings
5126
5711
  if (taco.a) {
5127
5712
  for (var attrName in taco.a) {
5128
- if (!Object.prototype.hasOwnProperty.call(taco.a, attrName)) continue;
5713
+ if (!_hop.call(taco.a, attrName)) continue;
5129
5714
  if (attrName === 'data-bw_ref') continue;
5130
5715
  var attrVal = taco.a[attrName];
5131
- if (typeof attrVal === 'string' && attrVal.indexOf('${') >= 0) {
5716
+ if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
5132
5717
  var refId2 = 'bw_ref_' + self._refCounter++;
5133
5718
  var parsed2 = bw._parseBindings(attrVal);
5134
5719
  var deps2 = [];
@@ -5154,9 +5739,27 @@
5154
5739
  }
5155
5740
 
5156
5741
  // Recurse into children
5157
- if (Array.isArray(taco.c)) {
5742
+ if (_isA(taco.c)) {
5158
5743
  for (var i = 0; i < taco.c.length; i++) {
5159
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
5744
+ // Wrap string children with ${expr} in a span so patches target the span, not the parent
5745
+ if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
5746
+ var mixedRefId = 'bw_ref_' + self._refCounter++;
5747
+ var mixedParsed = bw._parseBindings(taco.c[i]);
5748
+ var mixedDeps = [];
5749
+ for (var mi = 0; mi < mixedParsed.length; mi++) {
5750
+ mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
5751
+ }
5752
+ self._bindings.push({
5753
+ expr: taco.c[i],
5754
+ type: 'content',
5755
+ refId: mixedRefId,
5756
+ deps: mixedDeps,
5757
+ template: taco.c[i]
5758
+ });
5759
+ // Replace string with a span wrapper so textContent targets the span only
5760
+ taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
5761
+ }
5762
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
5160
5763
  walkTaco(taco.c[i], path.concat(i));
5161
5764
  }
5162
5765
  // Handle bw.when/bw.each markers
@@ -5191,7 +5794,7 @@
5191
5794
  taco.c[i]._refId = eachRefId;
5192
5795
  }
5193
5796
  }
5194
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
5797
+ } else if (_is(taco.c, 'object') && taco.c.t) {
5195
5798
  walkTaco(taco.c, path.concat(0));
5196
5799
  }
5197
5800
 
@@ -5207,7 +5810,7 @@
5207
5810
  * Build ref map from the live DOM after createDOM.
5208
5811
  * @private
5209
5812
  */
5210
- ComponentHandle.prototype._collectRefs = function() {
5813
+ _chp._collectRefs = function() {
5211
5814
  this._bw_refs = {};
5212
5815
  if (!this.element) return;
5213
5816
  var els = this.element.querySelectorAll('[data-bw_ref]');
@@ -5228,7 +5831,7 @@
5228
5831
  * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
5229
5832
  * @param {Element} parentEl - DOM element to mount into
5230
5833
  */
5231
- ComponentHandle.prototype.mount = function(parentEl) {
5834
+ _chp.mount = function(parentEl) {
5232
5835
  // willMount hook
5233
5836
  if (this._hooks.willMount) this._hooks.willMount(this);
5234
5837
 
@@ -5250,7 +5853,7 @@
5250
5853
  // Register named actions in function registry
5251
5854
  var self = this;
5252
5855
  for (var actionName in this._actions) {
5253
- if (Object.prototype.hasOwnProperty.call(this._actions, actionName)) {
5856
+ if (_hop.call(this._actions, actionName)) {
5254
5857
  var registeredName = this._bwId + '_' + actionName;
5255
5858
  (function(aName) {
5256
5859
  bw.funcRegister(function(evt) {
@@ -5269,6 +5872,11 @@
5269
5872
  this.element = bw.createDOM(tacoForDOM);
5270
5873
  this.element._bwComponentHandle = this;
5271
5874
  this.element.setAttribute('data-bw_comp_id', this._bwId);
5875
+
5876
+ // Restore o.render from original TACO (stripped by _tacoForDOM)
5877
+ if (this.taco.o && this.taco.o.render) {
5878
+ this.element._bw_render = this.taco.o.render;
5879
+ }
5272
5880
  if (this._userTag) {
5273
5881
  this.element.classList.add(this._userTag);
5274
5882
  }
@@ -5284,6 +5892,16 @@
5284
5892
 
5285
5893
  this.mounted = true;
5286
5894
 
5895
+ // Scan for child ComponentHandles and link parent/child (Bug #5)
5896
+ var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
5897
+ for (var ci = 0; ci < childEls.length; ci++) {
5898
+ var ch = childEls[ci]._bwComponentHandle;
5899
+ if (ch && ch !== this && !ch._parent) {
5900
+ ch._parent = this;
5901
+ this._children.push(ch);
5902
+ }
5903
+ }
5904
+
5287
5905
  // mounted hook (backward compat: fn.length === 2 wraps (el, state))
5288
5906
  if (this._hooks.mounted) {
5289
5907
  if (this._hooks.mounted.length === 2) {
@@ -5292,16 +5910,21 @@
5292
5910
  this._hooks.mounted(this);
5293
5911
  }
5294
5912
  }
5913
+
5914
+ // Invoke o.render on initial mount (if present)
5915
+ if (this.element._bw_render) {
5916
+ this.element._bw_render(this.element, this._state);
5917
+ }
5295
5918
  };
5296
5919
 
5297
5920
  /**
5298
5921
  * Prepare TACO for initial render: resolve when/each markers.
5299
5922
  * @private
5300
5923
  */
5301
- ComponentHandle.prototype._prepareTaco = function(taco) {
5302
- if (!taco || typeof taco !== 'object') return;
5924
+ _chp._prepareTaco = function(taco) {
5925
+ if (!_is(taco, 'object')) return;
5303
5926
 
5304
- if (Array.isArray(taco.c)) {
5927
+ if (_isA(taco.c)) {
5305
5928
  for (var i = taco.c.length - 1; i >= 0; i--) {
5306
5929
  var child = taco.c[i];
5307
5930
  if (child && child._bwWhen) {
@@ -5326,18 +5949,18 @@
5326
5949
  var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
5327
5950
  var arr = bw._evaluatePath(this._state, eachExprStr);
5328
5951
  var items = [];
5329
- if (Array.isArray(arr)) {
5952
+ if (_isA(arr)) {
5330
5953
  for (var j = 0; j < arr.length; j++) {
5331
5954
  items.push(child.factory(arr[j], j));
5332
5955
  }
5333
5956
  }
5334
5957
  taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
5335
5958
  }
5336
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
5959
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
5337
5960
  this._prepareTaco(taco.c[i]);
5338
5961
  }
5339
5962
  }
5340
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
5963
+ } else if (_is(taco.c, 'object') && taco.c.t) {
5341
5964
  this._prepareTaco(taco.c);
5342
5965
  }
5343
5966
  };
@@ -5346,12 +5969,12 @@
5346
5969
  * Wire action name strings (in onclick etc.) to dispatch function calls.
5347
5970
  * @private
5348
5971
  */
5349
- ComponentHandle.prototype._wireActions = function(taco) {
5350
- if (!taco || typeof taco !== 'object' || !taco.t) return;
5972
+ _chp._wireActions = function(taco) {
5973
+ if (!_is(taco, 'object') || !taco.t) return;
5351
5974
  if (taco.a) {
5352
5975
  for (var key in taco.a) {
5353
- if (!Object.prototype.hasOwnProperty.call(taco.a, key)) continue;
5354
- if (key.startsWith('on') && typeof taco.a[key] === 'string') {
5976
+ if (!_hop.call(taco.a, key)) continue;
5977
+ if (key.startsWith('on') && _is(taco.a[key], 'string')) {
5355
5978
  var actionName = taco.a[key];
5356
5979
  if (actionName in this._actions) {
5357
5980
  var registeredName = this._bwId + '_' + actionName;
@@ -5365,11 +5988,11 @@
5365
5988
  }
5366
5989
  }
5367
5990
  }
5368
- if (Array.isArray(taco.c)) {
5991
+ if (_isA(taco.c)) {
5369
5992
  for (var i = 0; i < taco.c.length; i++) {
5370
5993
  this._wireActions(taco.c[i]);
5371
5994
  }
5372
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
5995
+ } else if (_is(taco.c, 'object') && taco.c.t) {
5373
5996
  this._wireActions(taco.c);
5374
5997
  }
5375
5998
  };
@@ -5378,7 +6001,7 @@
5378
6001
  * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
5379
6002
  * @private
5380
6003
  */
5381
- ComponentHandle.prototype._deepCloneTaco = function(taco) {
6004
+ _chp._deepCloneTaco = function(taco) {
5382
6005
  if (taco == null) return taco;
5383
6006
  // Preserve _bwWhen / _bwEach markers (contain functions)
5384
6007
  if (taco._bwWhen) {
@@ -5390,18 +6013,18 @@
5390
6013
  if (taco._bwEach) {
5391
6014
  return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
5392
6015
  }
5393
- if (typeof taco !== 'object' || !taco.t) return taco;
6016
+ if (!_is(taco, 'object') || !taco.t) return taco;
5394
6017
  var result = { t: taco.t };
5395
6018
  if (taco.a) {
5396
6019
  result.a = {};
5397
6020
  for (var k in taco.a) {
5398
- if (Object.prototype.hasOwnProperty.call(taco.a, k)) result.a[k] = taco.a[k];
6021
+ if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
5399
6022
  }
5400
6023
  }
5401
6024
  if (taco.c != null) {
5402
- if (Array.isArray(taco.c)) {
6025
+ if (_isA(taco.c)) {
5403
6026
  result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
5404
- } else if (typeof taco.c === 'object') {
6027
+ } else if (_is(taco.c, 'object')) {
5405
6028
  result.c = this._deepCloneTaco(taco.c);
5406
6029
  } else {
5407
6030
  result.c = taco.c;
@@ -5415,27 +6038,31 @@
5415
6038
  * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
5416
6039
  * @private
5417
6040
  */
5418
- ComponentHandle.prototype._tacoForDOM = function(taco) {
5419
- if (!taco || typeof taco !== 'object' || !taco.t) return taco;
6041
+ _chp._tacoForDOM = function(taco) {
6042
+ if (!_is(taco, 'object') || !taco.t) return taco;
5420
6043
  var result = { t: taco.t };
5421
6044
  if (taco.a) result.a = taco.a;
5422
6045
  if (taco.c != null) {
5423
- if (Array.isArray(taco.c)) {
6046
+ if (_isA(taco.c)) {
5424
6047
  result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
5425
- } else if (typeof taco.c === 'object' && taco.c.t) {
6048
+ } else if (_is(taco.c, 'object') && taco.c.t) {
5426
6049
  result.c = this._tacoForDOM(taco.c);
5427
6050
  } else {
5428
6051
  result.c = taco.c;
5429
6052
  }
5430
6053
  }
5431
6054
  // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
6055
+ if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
6056
+ _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
6057
+ '>. Use onclick attribute or bw.component() for child interactivity.');
6058
+ }
5432
6059
  return result;
5433
6060
  };
5434
6061
 
5435
6062
  /**
5436
6063
  * Unmount: remove from DOM, deactivate, preserve state for re-mount.
5437
6064
  */
5438
- ComponentHandle.prototype.unmount = function() {
6065
+ _chp.unmount = function() {
5439
6066
  if (!this.mounted) return;
5440
6067
 
5441
6068
  // unmount hook
@@ -5470,12 +6097,23 @@
5470
6097
  /**
5471
6098
  * Destroy: unmount + clear state + unregister actions.
5472
6099
  */
5473
- ComponentHandle.prototype.destroy = function() {
6100
+ _chp.destroy = function() {
5474
6101
  // willDestroy hook
5475
6102
  if (this._hooks.willDestroy) {
5476
6103
  this._hooks.willDestroy(this);
5477
6104
  }
5478
6105
 
6106
+ // Cascade destroy to children depth-first (Bug #5)
6107
+ for (var ci = this._children.length - 1; ci >= 0; ci--) {
6108
+ this._children[ci].destroy();
6109
+ }
6110
+ this._children = [];
6111
+ if (this._parent) {
6112
+ var idx = this._parent._children.indexOf(this);
6113
+ if (idx >= 0) this._parent._children.splice(idx, 1);
6114
+ this._parent = null;
6115
+ }
6116
+
5479
6117
  this.unmount();
5480
6118
 
5481
6119
  // Unregister actions from function registry
@@ -5502,12 +6140,36 @@
5502
6140
  * Flush dirty state: resolve changed bindings and apply to DOM.
5503
6141
  * @private
5504
6142
  */
5505
- ComponentHandle.prototype._flush = function() {
6143
+ _chp._flush = function() {
5506
6144
  this._scheduled = false;
5507
- var changedKeys = Object.keys(this._dirtyKeys);
6145
+ var changedKeys = _keys(this._dirtyKeys);
5508
6146
  this._dirtyKeys = {};
5509
6147
  if (changedKeys.length === 0 || !this.mounted) return;
5510
6148
 
6149
+ // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
6150
+ // rebuild the TACO from the factory with merged state (Bug #6)
6151
+ if (this._factory) {
6152
+ var rebuildNeeded = false;
6153
+ for (var fi = 0; fi < changedKeys.length; fi++) {
6154
+ if (_hop.call(this._factory.props, changedKeys[fi])) {
6155
+ rebuildNeeded = true; break;
6156
+ }
6157
+ }
6158
+ if (rebuildNeeded) {
6159
+ var merged = {};
6160
+ for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
6161
+ for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
6162
+ this._factory.props = merged;
6163
+ var newTaco = bw.make(this._factory.type, merged);
6164
+ newTaco._bwFactory = this._factory;
6165
+ this.taco = newTaco;
6166
+ this._originalTaco = this._deepCloneTaco(newTaco);
6167
+ this._render();
6168
+ if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
6169
+ return;
6170
+ }
6171
+ }
6172
+
5511
6173
  // willUpdate hook
5512
6174
  if (this._hooks.willUpdate) {
5513
6175
  this._hooks.willUpdate(this, changedKeys);
@@ -5546,7 +6208,7 @@
5546
6208
  * Returns list of patches to apply.
5547
6209
  * @private
5548
6210
  */
5549
- ComponentHandle.prototype._resolveBindings = function(changedKeys) {
6211
+ _chp._resolveBindings = function(changedKeys) {
5550
6212
  var patches = [];
5551
6213
  for (var i = 0; i < this._bindings.length; i++) {
5552
6214
  var b = this._bindings[i];
@@ -5582,11 +6244,14 @@
5582
6244
  * Apply patches to DOM.
5583
6245
  * @private
5584
6246
  */
5585
- ComponentHandle.prototype._applyPatches = function(patches) {
6247
+ _chp._applyPatches = function(patches) {
5586
6248
  for (var i = 0; i < patches.length; i++) {
5587
6249
  var p = patches[i];
5588
6250
  var el = this._bw_refs[p.refId];
5589
- if (!el) continue;
6251
+ if (!el) {
6252
+ if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
6253
+ continue;
6254
+ }
5590
6255
  if (p.type === 'content') {
5591
6256
  el.textContent = p.value;
5592
6257
  } else if (p.type === 'attribute') {
@@ -5603,7 +6268,7 @@
5603
6268
  * Resolve all bindings and apply (used for initial render).
5604
6269
  * @private
5605
6270
  */
5606
- ComponentHandle.prototype._resolveAndApplyAll = function() {
6271
+ _chp._resolveAndApplyAll = function() {
5607
6272
  var patches = [];
5608
6273
  for (var i = 0; i < this._bindings.length; i++) {
5609
6274
  var b = this._bindings[i];
@@ -5626,7 +6291,7 @@
5626
6291
  * Full re-render for structural changes (when/each branch switches).
5627
6292
  * @private
5628
6293
  */
5629
- ComponentHandle.prototype._render = function() {
6294
+ _chp._render = function() {
5630
6295
  if (!this.element || !this.element.parentNode) return;
5631
6296
  var parent = this.element.parentNode;
5632
6297
  var nextSibling = this.element.nextSibling;
@@ -5666,7 +6331,7 @@
5666
6331
  * @param {string} event - Event name (e.g., 'click')
5667
6332
  * @param {Function} handler - Event handler
5668
6333
  */
5669
- ComponentHandle.prototype.on = function(event, handler) {
6334
+ _chp.on = function(event, handler) {
5670
6335
  if (this.element) {
5671
6336
  this.element.addEventListener(event, handler);
5672
6337
  }
@@ -5678,7 +6343,7 @@
5678
6343
  * @param {string} event - Event name
5679
6344
  * @param {Function} handler - Handler to remove
5680
6345
  */
5681
- ComponentHandle.prototype.off = function(event, handler) {
6346
+ _chp.off = function(event, handler) {
5682
6347
  if (this.element) {
5683
6348
  this.element.removeEventListener(event, handler);
5684
6349
  }
@@ -5693,7 +6358,7 @@
5693
6358
  * @param {Function} handler - Handler function
5694
6359
  * @returns {Function} Unsubscribe function
5695
6360
  */
5696
- ComponentHandle.prototype.sub = function(topic, handler) {
6361
+ _chp.sub = function(topic, handler) {
5697
6362
  var unsub = bw.sub(topic, handler);
5698
6363
  this._subs.push(unsub);
5699
6364
  return unsub;
@@ -5704,10 +6369,10 @@
5704
6369
  * @param {string} name - Action name
5705
6370
  * @param {...*} args - Arguments passed after comp
5706
6371
  */
5707
- ComponentHandle.prototype.action = function(name) {
6372
+ _chp.action = function(name) {
5708
6373
  var fn = this._actions[name];
5709
6374
  if (!fn) {
5710
- console.warn('ComponentHandle.action: unknown action "' + name + '"');
6375
+ _cw('ComponentHandle.action: unknown action "' + name + '"');
5711
6376
  return;
5712
6377
  }
5713
6378
  var args = [this].concat(Array.prototype.slice.call(arguments, 1));
@@ -5719,7 +6384,7 @@
5719
6384
  * @param {string} sel - CSS selector
5720
6385
  * @returns {Element|null}
5721
6386
  */
5722
- ComponentHandle.prototype.select = function(sel) {
6387
+ _chp.select = function(sel) {
5723
6388
  return this.element ? this.element.querySelector(sel) : null;
5724
6389
  };
5725
6390
 
@@ -5728,7 +6393,7 @@
5728
6393
  * @param {string} sel - CSS selector
5729
6394
  * @returns {Element[]}
5730
6395
  */
5731
- ComponentHandle.prototype.selectAll = function(sel) {
6396
+ _chp.selectAll = function(sel) {
5732
6397
  if (!this.element) return [];
5733
6398
  return Array.prototype.slice.call(this.element.querySelectorAll(sel));
5734
6399
  };
@@ -5739,7 +6404,7 @@
5739
6404
  * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
5740
6405
  * @returns {ComponentHandle} this (for chaining)
5741
6406
  */
5742
- ComponentHandle.prototype.userTag = function(tag) {
6407
+ _chp.userTag = function(tag) {
5743
6408
  this._userTag = tag;
5744
6409
  if (this.element) {
5745
6410
  this.element.classList.add(tag);
@@ -5816,7 +6481,7 @@
5816
6481
  * and calls the named method. This is the bitwrench equivalent of
5817
6482
  * Win32 SendMessage(hwnd, msg, wParam, lParam).
5818
6483
  *
5819
- * @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
6484
+ * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
5820
6485
  * @param {string} action - Method name to call on the component
5821
6486
  * @param {*} data - Data to pass to the method
5822
6487
  * @returns {boolean} True if message was dispatched successfully
@@ -5833,15 +6498,20 @@
5833
6498
  * };
5834
6499
  */
5835
6500
  bw.message = function(target, action, data) {
5836
- // Try data-bw_comp_id attribute first, then CSS class (user tag)
5837
- var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
5838
- if (!el) {
6501
+ // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
6502
+ var el = bw._el(target);
6503
+ // Then try data-bw_comp_id attribute
6504
+ if (!el || !el._bwComponentHandle) {
6505
+ el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
6506
+ }
6507
+ // Then try CSS class (user tag)
6508
+ if (!el || !el._bwComponentHandle) {
5839
6509
  el = bw.$('.' + target)[0];
5840
6510
  }
5841
6511
  if (!el || !el._bwComponentHandle) return false;
5842
6512
  var comp = el._bwComponentHandle;
5843
- if (typeof comp[action] !== 'function') {
5844
- console.warn('bw.message: unknown action "' + action + '" on component ' + target);
6513
+ if (!_is(comp[action], 'function')) {
6514
+ _cw('bw.message: unknown action "' + action + '" on component ' + target);
5845
6515
  return false;
5846
6516
  }
5847
6517
  comp[action](data);
@@ -5849,59 +6519,24 @@
5849
6519
  };
5850
6520
 
5851
6521
  // ===================================================================================
5852
- // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
6522
+ // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
5853
6523
  // ===================================================================================
5854
6524
 
5855
6525
  /**
5856
6526
  * Registry of named functions sent via register messages.
5857
- * Populated by clientApply({ type: 'register', name, body }).
5858
- * Invoked by clientApply({ type: 'call', name, args }).
6527
+ * Populated by bw.apply({ type: 'register', name, body }).
6528
+ * Invoked by bw.apply({ type: 'call', name, args }).
5859
6529
  * @private
5860
6530
  */
5861
6531
  bw._clientFunctions = {};
5862
6532
 
5863
6533
  /**
5864
- * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
6534
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
5865
6535
  * Default false — exec messages are rejected unless explicitly opted in.
5866
6536
  * @private
5867
6537
  */
5868
6538
  bw._allowExec = false;
5869
6539
 
5870
- /**
5871
- * Built-in client functions available via call() without registration.
5872
- * @private
5873
- */
5874
- bw._builtinClientFunctions = {
5875
- scrollTo: function(selector) {
5876
- var el = bw._el(selector);
5877
- if (el) el.scrollTop = el.scrollHeight;
5878
- },
5879
- focus: function(selector) {
5880
- var el = bw._el(selector);
5881
- if (el && typeof el.focus === 'function') el.focus();
5882
- },
5883
- download: function(filename, content, mimeType) {
5884
- if (typeof document === 'undefined') return;
5885
- var blob = new Blob([content], { type: mimeType || 'text/plain' });
5886
- var a = document.createElement('a');
5887
- a.href = URL.createObjectURL(blob);
5888
- a.download = filename;
5889
- a.click();
5890
- URL.revokeObjectURL(a.href);
5891
- },
5892
- clipboard: function(text) {
5893
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
5894
- navigator.clipboard.writeText(text);
5895
- }
5896
- },
5897
- redirect: function(url) {
5898
- if (typeof window !== 'undefined') window.location.href = url;
5899
- },
5900
- log: function() {
5901
- console.log.apply(console, arguments);
5902
- }
5903
- };
5904
-
5905
6540
  /**
5906
6541
  * Parse a bwserve protocol message string, supporting both strict JSON
5907
6542
  * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
@@ -5916,9 +6551,9 @@
5916
6551
  * @param {string} str - JSON or r-prefixed relaxed JSON string
5917
6552
  * @returns {Object} Parsed message object
5918
6553
  * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
5919
- * @category Server
6554
+ * @category Core
5920
6555
  */
5921
- bw.clientParse = function(str) {
6556
+ bw.parseJSONFlex = function(str) {
5922
6557
  str = (str || '').trim();
5923
6558
  if (str.charAt(0) !== 'r') return JSON.parse(str);
5924
6559
  str = str.slice(1);
@@ -6003,10 +6638,10 @@
6003
6638
  * append — target.appendChild(bw.createDOM(node))
6004
6639
  * remove — bw.cleanup(target); target.remove()
6005
6640
  * patch — bw.patch(target, content, attr)
6006
- * batch — iterate ops, call clientApply for each
6641
+ * batch — iterate ops, call bw.apply for each
6007
6642
  * message — bw.message(target, action, data)
6008
6643
  * register — store a named function for later call()
6009
- * call — invoke a registered or built-in function
6644
+ * call — invoke a registered function
6010
6645
  * exec — execute arbitrary JS (requires allowExec)
6011
6646
  *
6012
6647
  * Target resolution:
@@ -6015,9 +6650,9 @@
6015
6650
  *
6016
6651
  * @param {Object} msg - Protocol message
6017
6652
  * @returns {boolean} true if the message was applied successfully
6018
- * @category Server
6653
+ * @category Core
6019
6654
  */
6020
- bw.clientApply = function(msg) {
6655
+ bw.apply = function(msg) {
6021
6656
  if (!msg || !msg.type) return false;
6022
6657
 
6023
6658
  var type = msg.type;
@@ -6043,15 +6678,15 @@
6043
6678
  } else if (type === 'remove') {
6044
6679
  var toRemove = bw._el(target);
6045
6680
  if (!toRemove) return false;
6046
- if (typeof bw.cleanup === 'function') bw.cleanup(toRemove);
6681
+ if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
6047
6682
  toRemove.remove();
6048
6683
  return true;
6049
6684
 
6050
6685
  } else if (type === 'batch') {
6051
- if (!Array.isArray(msg.ops)) return false;
6686
+ if (!_isA(msg.ops)) return false;
6052
6687
  var allOk = true;
6053
6688
  msg.ops.forEach(function(op) {
6054
- if (!bw.clientApply(op)) allOk = false;
6689
+ if (!bw.apply(op)) allOk = false;
6055
6690
  });
6056
6691
  return allOk;
6057
6692
 
@@ -6064,26 +6699,26 @@
6064
6699
  bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
6065
6700
  return true;
6066
6701
  } catch (e) {
6067
- console.error('[bw] register error:', msg.name, e);
6702
+ _ce('[bw] register error:', msg.name, e);
6068
6703
  return false;
6069
6704
  }
6070
6705
 
6071
6706
  } else if (type === 'call') {
6072
6707
  if (!msg.name) return false;
6073
- var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
6074
- if (typeof fn !== 'function') return false;
6708
+ var fn = bw._clientFunctions[msg.name];
6709
+ if (!_is(fn, 'function')) return false;
6075
6710
  try {
6076
- var args = Array.isArray(msg.args) ? msg.args : [];
6711
+ var args = _isA(msg.args) ? msg.args : [];
6077
6712
  fn.apply(null, args);
6078
6713
  return true;
6079
6714
  } catch (e) {
6080
- console.error('[bw] call error:', msg.name, e);
6715
+ _ce('[bw] call error:', msg.name, e);
6081
6716
  return false;
6082
6717
  }
6083
6718
 
6084
6719
  } else if (type === 'exec') {
6085
6720
  if (!bw._allowExec) {
6086
- console.warn('[bw] exec rejected: allowExec is not enabled');
6721
+ _cw('[bw] exec rejected: allowExec is not enabled');
6087
6722
  return false;
6088
6723
  }
6089
6724
  if (!msg.code) return false;
@@ -6091,7 +6726,7 @@
6091
6726
  new Function(msg.code)();
6092
6727
  return true;
6093
6728
  } catch (e) {
6094
- console.error('[bw] exec error:', e);
6729
+ _ce('[bw] exec error:', e);
6095
6730
  return false;
6096
6731
  }
6097
6732
  }
@@ -6099,139 +6734,6 @@
6099
6734
  return false;
6100
6735
  };
6101
6736
 
6102
- /**
6103
- * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
6104
- *
6105
- * Returns a connection object with sendAction(), on(), and close() methods.
6106
- *
6107
- * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
6108
- * @param {Object} [opts] - Connection options
6109
- * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
6110
- * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
6111
- * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
6112
- * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
6113
- * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
6114
- * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
6115
- * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
6116
- * @returns {Object} Connection object { sendAction, on, close, status }
6117
- * @category Server
6118
- */
6119
- bw.clientConnect = function(url, opts) {
6120
- opts = opts || {};
6121
- var transport = opts.transport || 'sse';
6122
- var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
6123
- var reconnect = opts.reconnect !== false;
6124
- var onStatus = opts.onStatus || function() {};
6125
- var onMessage = opts.onMessage || null;
6126
- var handlers = {};
6127
- // Set the global allowExec flag from connection options
6128
- bw._allowExec = !!opts.allowExec;
6129
- var conn = {
6130
- status: 'connecting',
6131
- _es: null,
6132
- _pollTimer: null
6133
- };
6134
-
6135
- function setStatus(s) {
6136
- conn.status = s;
6137
- onStatus(s);
6138
- }
6139
-
6140
- function handleMessage(data) {
6141
- try {
6142
- var msg = typeof data === 'string' ? bw.clientParse(data) : data;
6143
- if (onMessage) onMessage(msg);
6144
- if (handlers.message) handlers.message(msg);
6145
- bw.clientApply(msg);
6146
- } catch (e) {
6147
- if (handlers.error) handlers.error(e);
6148
- }
6149
- }
6150
-
6151
- if (transport === 'sse' && typeof EventSource !== 'undefined') {
6152
- setStatus('connecting');
6153
- var es = new EventSource(url);
6154
- conn._es = es;
6155
-
6156
- es.onopen = function() {
6157
- setStatus('connected');
6158
- if (handlers.open) handlers.open();
6159
- };
6160
-
6161
- es.onmessage = function(e) {
6162
- handleMessage(e.data);
6163
- };
6164
-
6165
- es.onerror = function() {
6166
- if (conn.status === 'connected') {
6167
- setStatus('disconnected');
6168
- }
6169
- if (handlers.error) handlers.error(new Error('SSE connection error'));
6170
- if (!reconnect) {
6171
- es.close();
6172
- }
6173
- // EventSource auto-reconnects by default when reconnect=true
6174
- };
6175
- } else if (transport === 'poll') {
6176
- var interval = opts.interval || 2000;
6177
- setStatus('connected');
6178
- conn._pollTimer = setInterval(function() {
6179
- fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
6180
- if (Array.isArray(msgs)) {
6181
- msgs.forEach(handleMessage);
6182
- } else if (msgs && msgs.type) {
6183
- handleMessage(msgs);
6184
- }
6185
- }).catch(function(e) {
6186
- if (handlers.error) handlers.error(e);
6187
- });
6188
- }, interval);
6189
- }
6190
-
6191
- /**
6192
- * Send an action to the server via POST.
6193
- * @param {string} action - Action name
6194
- * @param {Object} [data] - Action payload
6195
- */
6196
- conn.sendAction = function(action, data) {
6197
- var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
6198
- fetch(actionUrl, {
6199
- method: 'POST',
6200
- headers: { 'Content-Type': 'application/json' },
6201
- body: body
6202
- }).catch(function(e) {
6203
- if (handlers.error) handlers.error(e);
6204
- });
6205
- };
6206
-
6207
- /**
6208
- * Register an event handler.
6209
- * @param {string} event - 'open'|'message'|'error'|'close'
6210
- * @param {Function} handler
6211
- */
6212
- conn.on = function(event, handler) {
6213
- handlers[event] = handler;
6214
- return conn;
6215
- };
6216
-
6217
- /**
6218
- * Close the connection.
6219
- */
6220
- conn.close = function() {
6221
- if (conn._es) {
6222
- conn._es.close();
6223
- conn._es = null;
6224
- }
6225
- if (conn._pollTimer) {
6226
- clearInterval(conn._pollTimer);
6227
- conn._pollTimer = null;
6228
- }
6229
- setStatus('disconnected');
6230
- if (handlers.close) handlers.close();
6231
- };
6232
-
6233
- return conn;
6234
- };
6235
6737
 
6236
6738
  // ===================================================================================
6237
6739
  // bw.inspect() — Debug utility
@@ -6259,33 +6761,33 @@
6259
6761
  el = target.element;
6260
6762
  comp = target;
6261
6763
  } else {
6262
- if (typeof target === 'string') {
6764
+ if (_is(target, 'string')) {
6263
6765
  el = bw.$(target)[0];
6264
6766
  }
6265
6767
  if (!el) {
6266
- console.warn('bw.inspect: element not found');
6768
+ _cw('bw.inspect: element not found');
6267
6769
  return null;
6268
6770
  }
6269
6771
  comp = el._bwComponentHandle;
6270
6772
  }
6271
6773
  if (!comp) {
6272
- console.log('bw.inspect: no ComponentHandle on this element');
6273
- console.log(' Tag:', el.tagName);
6274
- console.log(' Classes:', el.className);
6275
- console.log(' _bw_state:', el._bw_state || '(none)');
6774
+ _cl('bw.inspect: no ComponentHandle on this element');
6775
+ _cl(' Tag:', el.tagName);
6776
+ _cl(' Classes:', el.className);
6777
+ _cl(' _bw_state:', el._bw_state || '(none)');
6276
6778
  return null;
6277
6779
  }
6278
6780
  var deps = comp._bindings.reduce(function(s, b) {
6279
6781
  return s.concat(b.deps || []);
6280
6782
  }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
6281
6783
  console.group('Component: ' + comp._bwId);
6282
- console.log('State:', comp._state);
6283
- console.log('Bindings:', comp._bindings.length, '(deps:', deps, ')');
6284
- console.log('Methods:', Object.keys(comp._methods));
6285
- console.log('Actions:', Object.keys(comp._actions));
6286
- console.log('User tag:', comp._userTag || '(none)');
6287
- console.log('Mounted:', comp.mounted);
6288
- console.log('Element:', comp.element);
6784
+ _cl('State:', comp._state);
6785
+ _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
6786
+ _cl('Methods:', _keys(comp._methods));
6787
+ _cl('Actions:', _keys(comp._actions));
6788
+ _cl('User tag:', comp._userTag || '(none)');
6789
+ _cl('Mounted:', comp.mounted);
6790
+ _cl('Element:', comp.element);
6289
6791
  console.groupEnd();
6290
6792
  return comp;
6291
6793
  };
@@ -6308,8 +6810,8 @@
6308
6810
  // Pre-extract all binding expressions
6309
6811
  var precompiled = [];
6310
6812
  function walkExpressions(node) {
6311
- if (!node || typeof node !== 'object') return;
6312
- if (typeof node.c === 'string' && node.c.indexOf('${') >= 0) {
6813
+ if (!_is(node, 'object')) return;
6814
+ if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
6313
6815
  var parsed = bw._parseBindings(node.c);
6314
6816
  for (var i = 0; i < parsed.length; i++) {
6315
6817
  try {
@@ -6324,9 +6826,9 @@
6324
6826
  }
6325
6827
  if (node.a) {
6326
6828
  for (var key in node.a) {
6327
- if (Object.prototype.hasOwnProperty.call(node.a, key)) {
6829
+ if (_hop.call(node.a, key)) {
6328
6830
  var v = node.a[key];
6329
- if (typeof v === 'string' && v.indexOf('${') >= 0) {
6831
+ if (_is(v, 'string') && v.indexOf('${') >= 0) {
6330
6832
  var parsed2 = bw._parseBindings(v);
6331
6833
  for (var j = 0; j < parsed2.length; j++) {
6332
6834
  try {
@@ -6342,9 +6844,9 @@
6342
6844
  }
6343
6845
  }
6344
6846
  }
6345
- if (Array.isArray(node.c)) {
6847
+ if (_isA(node.c)) {
6346
6848
  for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
6347
- } else if (node.c && typeof node.c === 'object' && node.c.t) {
6849
+ } else if (_is(node.c, 'object') && node.c.t) {
6348
6850
  walkExpressions(node.c);
6349
6851
  }
6350
6852
  }
@@ -6356,7 +6858,7 @@
6356
6858
  handle._precompiledBindings = precompiled;
6357
6859
  if (initialState) {
6358
6860
  for (var k in initialState) {
6359
- if (Object.prototype.hasOwnProperty.call(initialState, k)) {
6861
+ if (_hop.call(initialState, k)) {
6360
6862
  handle._state[k] = initialState[k];
6361
6863
  }
6362
6864
  }
@@ -6387,18 +6889,18 @@
6387
6889
  bw.css = function(rules, options = {}) {
6388
6890
  const { minify = false, pretty = !minify } = options;
6389
6891
 
6390
- if (typeof rules === 'string') return rules;
6892
+ if (_is(rules, 'string')) return rules;
6391
6893
 
6392
6894
  let css = '';
6393
6895
  const indent = pretty ? ' ' : '';
6394
6896
  const newline = pretty ? '\n' : '';
6395
6897
  const space = pretty ? ' ' : '';
6396
6898
 
6397
- if (Array.isArray(rules)) {
6899
+ if (_isA(rules)) {
6398
6900
  css = rules.map(rule => bw.css(rule, options)).join(newline);
6399
- } else if (typeof rules === 'object') {
6901
+ } else if (_is(rules, 'object')) {
6400
6902
  Object.entries(rules).forEach(([selector, styles]) => {
6401
- if (typeof styles === 'object' && !Array.isArray(styles)) {
6903
+ if (_is(styles, 'object')) {
6402
6904
  // Handle @media, @keyframes, @supports — recurse into nested block
6403
6905
  if (selector.charAt(0) === '@') {
6404
6906
  const inner = bw.css(styles, options);
@@ -6440,14 +6942,14 @@
6440
6942
  * @returns {Element} The style element
6441
6943
  * @category CSS & Styling
6442
6944
  * @see bw.css
6443
- * @see bw.loadDefaultStyles
6945
+ * @see bw.loadStyles
6444
6946
  * @example
6445
6947
  * bw.injectCSS('.my-class { color: red; }');
6446
6948
  * bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
6447
6949
  */
6448
6950
  bw.injectCSS = function(css, options = {}) {
6449
6951
  if (!bw._isBrowser) {
6450
- console.warn('bw.injectCSS requires a DOM environment');
6952
+ _cw('bw.injectCSS requires a DOM environment');
6451
6953
  return null;
6452
6954
  }
6453
6955
 
@@ -6464,7 +6966,7 @@
6464
6966
  }
6465
6967
 
6466
6968
  // Convert CSS if needed
6467
- const cssStr = typeof css === 'string' ? css : bw.css(css, options);
6969
+ const cssStr = _is(css, 'string') ? css : bw.css(css, options);
6468
6970
 
6469
6971
  // Set or append CSS
6470
6972
  if (append && styleEl.textContent) {
@@ -6485,113 +6987,19 @@
6485
6987
  * @param {...Object} styles - Style objects to merge (left-to-right)
6486
6988
  * @returns {Object} Merged style object
6487
6989
  * @category CSS & Styling
6488
- * @see bw.u
6489
6990
  * @example
6490
- * var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
6991
+ * var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
6491
6992
  * // => { display: 'flex', gap: '1rem', color: 'red' }
6492
6993
  */
6493
6994
  bw.s = function() {
6494
6995
  var result = {};
6495
6996
  for (var i = 0; i < arguments.length; i++) {
6496
6997
  var arg = arguments[i];
6497
- if (arg && typeof arg === 'object') Object.assign(result, arg);
6998
+ if (_is(arg, 'object')) Object.assign(result, arg);
6498
6999
  }
6499
7000
  return result;
6500
7001
  };
6501
7002
 
6502
- /**
6503
- * Pre-built CSS utility objects (like Tailwind utilities, but in JS).
6504
- *
6505
- * Compose with `bw.s()` to build inline styles without writing raw CSS strings.
6506
- * Includes flex, padding, margin, typography, color, border, and transition utilities.
6507
- *
6508
- * @category CSS & Styling
6509
- * @see bw.s
6510
- * @example
6511
- * { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
6512
- * c: 'Flexbox with 1rem gap and padding' }
6513
- */
6514
- bw.u = {
6515
- // Display
6516
- flex: { display: 'flex' },
6517
- flexCol: { display: 'flex', flexDirection: 'column' },
6518
- flexRow: { display: 'flex', flexDirection: 'row' },
6519
- flexWrap: { display: 'flex', flexWrap: 'wrap' },
6520
- block: { display: 'block' },
6521
- inline: { display: 'inline' },
6522
- hidden: { display: 'none' },
6523
-
6524
- // Flex alignment
6525
- justifyCenter: { justifyContent: 'center' },
6526
- justifyBetween: { justifyContent: 'space-between' },
6527
- justifyEnd: { justifyContent: 'flex-end' },
6528
- alignCenter: { alignItems: 'center' },
6529
- alignStart: { alignItems: 'flex-start' },
6530
- alignEnd: { alignItems: 'flex-end' },
6531
-
6532
- // Gap (0.25rem increments)
6533
- gap1: { gap: '0.25rem' },
6534
- gap2: { gap: '0.5rem' },
6535
- gap3: { gap: '0.75rem' },
6536
- gap4: { gap: '1rem' },
6537
- gap6: { gap: '1.5rem' },
6538
- gap8: { gap: '2rem' },
6539
-
6540
- // Padding
6541
- p0: { padding: '0' },
6542
- p1: { padding: '0.25rem' },
6543
- p2: { padding: '0.5rem' },
6544
- p3: { padding: '0.75rem' },
6545
- p4: { padding: '1rem' },
6546
- p6: { padding: '1.5rem' },
6547
- p8: { padding: '2rem' },
6548
- px4: { paddingLeft: '1rem', paddingRight: '1rem' },
6549
- py2: { paddingTop: '0.5rem', paddingBottom: '0.5rem' },
6550
- py4: { paddingTop: '1rem', paddingBottom: '1rem' },
6551
-
6552
- // Margin (same scale)
6553
- m0: { margin: '0' },
6554
- m4: { margin: '1rem' },
6555
- mt2: { marginTop: '0.5rem' },
6556
- mt4: { marginTop: '1rem' },
6557
- mb2: { marginBottom: '0.5rem' },
6558
- mb4: { marginBottom: '1rem' },
6559
- mx_auto: { marginLeft: 'auto', marginRight: 'auto' },
6560
-
6561
- // Typography
6562
- textSm: { fontSize: '0.875rem' },
6563
- textBase: { fontSize: '1rem' },
6564
- textLg: { fontSize: '1.125rem' },
6565
- textXl: { fontSize: '1.25rem' },
6566
- text2xl: { fontSize: '1.5rem' },
6567
- text3xl: { fontSize: '1.875rem' },
6568
- bold: { fontWeight: '700' },
6569
- semibold: { fontWeight: '600' },
6570
- italic: { fontStyle: 'italic' },
6571
- textCenter: { textAlign: 'center' },
6572
- textRight: { textAlign: 'right' },
6573
-
6574
- // Colors (from design tokens)
6575
- bgWhite: { background: '#ffffff' },
6576
- bgTeal: { background: '#006666', color: '#ffffff' },
6577
- textWhite: { color: '#ffffff' },
6578
- textTeal: { color: '#006666' },
6579
- textMuted: { color: '#888' },
6580
-
6581
- // Borders
6582
- rounded: { borderRadius: '0.375rem' },
6583
- roundedLg: { borderRadius: '0.5rem' },
6584
- roundedFull: { borderRadius: '9999px' },
6585
- border: { border: '1px solid #d8d8d8' },
6586
-
6587
- // Sizing
6588
- wFull: { width: '100%' },
6589
- hFull: { height: '100%' },
6590
-
6591
- // Transitions
6592
- transition: { transition: 'all 0.2s ease' }
6593
- };
6594
-
6595
7003
  /**
6596
7004
  * Generate responsive CSS with media query breakpoints.
6597
7005
  *
@@ -6617,7 +7025,7 @@
6617
7025
  bw.responsive = function(selector, breakpoints) {
6618
7026
  var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
6619
7027
  var parts = [];
6620
- Object.keys(breakpoints).forEach(function(key) {
7028
+ _keys(breakpoints).forEach(function(key) {
6621
7029
  var rules = {};
6622
7030
  if (key === 'base') {
6623
7031
  rules[selector] = breakpoints[key];
@@ -6689,18 +7097,18 @@
6689
7097
  if (!selector) return [];
6690
7098
 
6691
7099
  // Already an array
6692
- if (Array.isArray(selector)) return selector;
7100
+ if (_isA(selector)) return selector;
6693
7101
 
6694
7102
  // Single element
6695
7103
  if (selector.nodeType) return [selector];
6696
7104
 
6697
7105
  // NodeList or HTMLCollection
6698
- if (selector.length !== undefined && typeof selector !== 'string') {
7106
+ if (selector.length !== undefined && !_is(selector, 'string')) {
6699
7107
  return Array.from(selector);
6700
7108
  }
6701
7109
 
6702
7110
  // CSS selector string
6703
- if (typeof selector === 'string') {
7111
+ if (_is(selector, 'string')) {
6704
7112
  return Array.from(document.querySelectorAll(selector));
6705
7113
  }
6706
7114
 
@@ -6713,103 +7121,49 @@
6713
7121
  };
6714
7122
  }
6715
7123
 
6716
- /**
6717
- * Load the built-in Bootstrap-inspired default stylesheet.
6718
- *
6719
- * Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
6720
- * alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
6721
- * Returns null in Node.js (no DOM).
6722
- *
6723
- * @param {Object} [options] - Style loading options
6724
- * @param {boolean} [options.minify=true] - Minify the CSS output
6725
- * @returns {Element|null} Style element if in browser, null in Node.js
6726
- * @category CSS & Styling
6727
- * @see bw.setTheme
6728
- * @see bw.applyTheme
6729
- * @see bw.toggleTheme
6730
- * @example
6731
- * bw.loadDefaultStyles(); // inject all default CSS
6732
- */
6733
- bw.loadDefaultStyles = function(options = {}) {
6734
- const { minify = true, palette } = options;
6735
-
6736
- // 1. Inject structural CSS (layout, sizing — never changes with theme)
6737
- if (bw._isBrowser) {
6738
- var structuralCSS = bw.css(getStructuralStyles());
6739
- bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
6740
- }
6741
7124
 
6742
- // 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
6743
- var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
6744
- var result = bw.generateTheme('', Object.assign({}, paletteConfig, { inject: true }));
6745
- return result;
6746
- };
7125
+ // =========================================================================
7126
+ // v2.0.18 Clean Styles API makeStyles / applyStyles / loadStyles / etc.
7127
+ // =========================================================================
6747
7128
 
7129
+ /**
7130
+ * Convert a scope selector to a <style> element id.
7131
+ * @private
7132
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
7133
+ * @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
7134
+ */
7135
+ function _scopeToStyleId(scope) {
7136
+ if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
7137
+ if (scope === 'reset') return 'bw_style_reset';
7138
+ // Strip leading # or . and convert - to _
7139
+ var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
7140
+ return 'bw_style_' + clean;
7141
+ }
6748
7142
 
6749
7143
  /**
6750
- * Generate a complete, scoped theme from seed colors.
7144
+ * Generate a complete styles object from seed colors and layout config.
7145
+ * Pure function — no DOM, no state, no side effects.
6751
7146
  *
6752
- * Produces CSS for all themed components (buttons, alerts, badges, cards,
6753
- * forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
6754
- * scoped under `.name` class. Multiple themes can coexist in the stylesheet.
6755
- * Swap themes by changing the class on a container element.
7147
+ * All parameters are optional. Defaults to the bitwrench default palette.
6756
7148
  *
6757
- * @param {string} name - CSS scope class (e.g. 'ocean'). Empty string = unscoped global.
6758
- * @param {Object} config - Theme configuration
6759
- * @param {string} config.primary - Primary brand color hex
6760
- * @param {string} config.secondary - Secondary color hex
6761
- * @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
6762
- * @param {string} [config.success='#198754'] - Success color hex
6763
- * @param {string} [config.danger='#dc3545'] - Danger color hex
6764
- * @param {string} [config.warning='#ffc107'] - Warning color hex
6765
- * @param {string} [config.info='#0dcaf0'] - Info color hex
6766
- * @param {string} [config.light='#f8f9fa'] - Light color hex
6767
- * @param {string} [config.dark='#212529'] - Dark color hex
6768
- * @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
6769
- * @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
7149
+ * @param {Object} [config] - Style configuration
7150
+ * @param {string} [config.primary='#006666'] - Primary brand color hex
7151
+ * @param {string} [config.secondary='#6c757d'] - Secondary color hex
7152
+ * @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
6770
7153
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
6771
7154
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
6772
- * @param {number} [config.fontSize=1.0] - Base font size scale factor
6773
- * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
6774
- * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
6775
- * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
6776
- * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
6777
- * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
6778
- * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
7155
+ * @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
6779
7156
  * @category CSS & Styling
6780
- * @see bw.applyTheme
6781
- * @see bw.toggleTheme
6782
- * @see bw.loadDefaultStyles
7157
+ * @see bw.applyStyles
7158
+ * @see bw.loadStyles
6783
7159
  * @example
6784
- * // Generate and inject an ocean theme (primary + alternate)
6785
- * var theme = bw.generateTheme('ocean', {
6786
- * primary: '#0077b6',
6787
- * secondary: '#90e0ef',
6788
- * tertiary: '#00b4d8'
6789
- * });
6790
- *
6791
- * // Apply to a container
6792
- * document.getElementById('app').classList.add('ocean');
6793
- *
6794
- * // Toggle to alternate palette
6795
- * bw.toggleTheme();
6796
- *
6797
- * // Generate CSS for static export (Node.js)
6798
- * var result = bw.generateTheme('sunset', {
6799
- * primary: '#e76f51',
6800
- * secondary: '#264653',
6801
- * inject: false
6802
- * });
6803
- * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
7160
+ * var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
7161
+ * console.log(styles.palette.primary.base); // '#4f46e5'
7162
+ * // styles.css contains all themed CSS — nothing injected
6804
7163
  */
6805
- bw.generateTheme = function(name, config) {
6806
- if (!config || !config.primary || !config.secondary) {
6807
- throw new Error('bw.generateTheme requires config.primary and config.secondary');
6808
- }
6809
-
6810
- // Merge with defaults; if user didn't supply tertiary, default to their primary
6811
- var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
6812
- if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
7164
+ bw.makeStyles = function(config) {
7165
+ var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
7166
+ if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
6813
7167
 
6814
7168
  // Derive primary palette
6815
7169
  var palette = derivePalette(fullConfig);
@@ -6817,131 +7171,207 @@
6817
7171
  // Resolve layout
6818
7172
  var layout = resolveLayout(fullConfig);
6819
7173
 
6820
- // Generate primary themed CSS rules
6821
- var themedRules = generateThemedCSS(name, palette, layout);
7174
+ // Generate primary themed CSS rules (unscoped)
7175
+ var themedRules = generateThemedCSS('', palette, layout);
6822
7176
  var cssStr = bw.css(themedRules);
6823
7177
 
6824
7178
  // Derive alternate palette (luminance-inverted)
6825
7179
  var altConfig = deriveAlternateConfig(fullConfig);
6826
7180
  var altPalette = derivePalette(altConfig);
6827
7181
 
6828
- // Generate alternate CSS scoped under .bw_theme_alt
6829
- var altRules = generateAlternateCSS(name, altPalette, layout);
6830
- var altCssStr = bw.css(altRules);
7182
+ // Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
7183
+ // applyStyles() wraps them appropriately based on scope
7184
+ var altRawRules = generateThemedCSS('', altPalette, layout);
7185
+
7186
+ // Add body-level surface overrides for the alternate palette.
7187
+ // When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
7188
+ altRawRules['body'] = {
7189
+ 'color': altPalette.dark.base,
7190
+ 'background-color': altPalette.surface || altPalette.light.base
7191
+ };
7192
+
7193
+ var altCssStr = bw.css(altRawRules);
6831
7194
 
6832
7195
  // Determine if primary is light-flavored
6833
7196
  var lightPrimary = isLightPalette(fullConfig);
6834
7197
 
6835
- // Inject both CSS sets into DOM if requested
6836
- var shouldInject = config.inject !== false;
6837
- if (shouldInject && bw._isBrowser) {
6838
- var safeName = name ? name.replace(/-/g, '_') : '';
6839
- var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
6840
- var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
6841
-
6842
- bw.injectCSS(cssStr, { id: styleId, append: false });
6843
- bw.injectCSS(altCssStr, { id: altStyleId, append: false });
7198
+ return {
7199
+ css: cssStr,
7200
+ alternateCss: altCssStr,
7201
+ rules: themedRules,
7202
+ alternateRules: altRawRules,
7203
+ palette: palette,
7204
+ alternatePalette: altPalette,
7205
+ isLightPrimary: lightPrimary
7206
+ };
7207
+ };
6844
7208
 
6845
- bw._activeThemeStyleIds = [styleId, altStyleId];
7209
+ /**
7210
+ * Inject styles into the DOM with optional scoping.
7211
+ *
7212
+ * Takes a styles object from `makeStyles()` and creates a single `<style>`
7213
+ * element in `<head>`. If a scope selector is provided, all CSS rules are
7214
+ * wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
7215
+ *
7216
+ * @param {Object} styles - Result of `bw.makeStyles()`
7217
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
7218
+ * @returns {Element|null} The `<style>` element, or null in Node.js
7219
+ * @category CSS & Styling
7220
+ * @see bw.makeStyles
7221
+ * @see bw.loadStyles
7222
+ * @see bw.clearStyles
7223
+ * @example
7224
+ * var styles = bw.makeStyles({ primary: '#4f46e5' });
7225
+ * bw.applyStyles(styles); // global
7226
+ * bw.applyStyles(styles, '#my-dashboard'); // scoped
7227
+ */
7228
+ bw.applyStyles = function(styles, scope) {
7229
+ if (!bw._isBrowser) return null;
7230
+ if (!styles || !styles.rules) {
7231
+ _cw('bw.applyStyles: invalid styles object');
7232
+ return null;
6846
7233
  }
6847
7234
 
6848
- // Update bw.u color entries to reflect the palette
6849
- if (!name) {
6850
- bw.u.bgTeal = { background: palette.primary.base, color: palette.primary.textOn };
6851
- bw.u.textTeal = { color: palette.primary.base };
6852
- bw.u.bgWhite = { background: '#ffffff' };
6853
- bw.u.textWhite = { color: '#ffffff' };
7235
+ var styleId = _scopeToStyleId(scope);
7236
+
7237
+ // Scope the primary rules if a scope is provided
7238
+ var primaryRules = styles.rules;
7239
+ if (scope) {
7240
+ primaryRules = scopeRulesUnder(primaryRules, scope);
6854
7241
  }
6855
7242
 
6856
- // Store active theme state
6857
- var result = {
6858
- css: cssStr,
6859
- palette: palette,
6860
- name: name,
6861
- isLightPrimary: lightPrimary,
6862
- alternate: {
6863
- css: altCssStr,
6864
- palette: altPalette
7243
+ // Wrap alternate rules with .bw_theme_alt
7244
+ var altRules = styles.alternateRules;
7245
+ if (altRules) {
7246
+ if (scope) {
7247
+ // Scoped compound: #scope.bw_theme_alt .bw_card
7248
+ altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
7249
+ } else {
7250
+ // Global: .bw_theme_alt .bw_card
7251
+ altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
6865
7252
  }
6866
- };
6867
- bw._activeTheme = result;
6868
- bw._activeThemeMode = 'primary';
7253
+ }
6869
7254
 
6870
- return result;
7255
+ // Combine primary + alternate into one CSS string
7256
+ var combined = bw.css(primaryRules);
7257
+ if (altRules) {
7258
+ combined += '\n' + bw.css(altRules);
7259
+ }
7260
+
7261
+ return bw.injectCSS(combined, { id: styleId, append: false });
6871
7262
  };
6872
7263
 
6873
7264
  /**
6874
- * Apply a theme mode. Switches between primary and alternate palettes
6875
- * by adding/removing the `bw_theme_alt` class on `<html>`.
7265
+ * Generate and apply styles in one call. Convenience wrapper.
7266
+ *
7267
+ * Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
6876
7268
  *
6877
- * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
6878
- * @returns {string} Active mode: 'primary' or 'alternate'
7269
+ * @param {Object} [config] - Style configuration (same as `makeStyles`)
7270
+ * @param {string} [scope] - Scope selector (same as `applyStyles`)
7271
+ * @returns {Element|null} The `<style>` element, or null in Node.js
6879
7272
  * @category CSS & Styling
6880
- * @see bw.generateTheme
6881
- * @see bw.toggleTheme
7273
+ * @see bw.makeStyles
7274
+ * @see bw.applyStyles
6882
7275
  * @example
6883
- * bw.applyTheme('alternate'); // switch to alternate palette
6884
- * bw.applyTheme('dark'); // switch to whichever palette is darker
6885
- * bw.applyTheme('primary'); // switch back to primary palette
6886
- */
6887
- bw.applyTheme = function(mode) {
6888
- if (!bw._isBrowser) return mode || 'primary';
6889
- var root = document.documentElement;
6890
- var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
6891
-
6892
- var wantAlt;
6893
- if (mode === 'primary') wantAlt = false;
6894
- else if (mode === 'alternate') wantAlt = true;
6895
- else if (mode === 'light') wantAlt = !isLight;
6896
- else if (mode === 'dark') wantAlt = isLight;
6897
- else wantAlt = false;
6898
-
6899
- if (wantAlt) {
6900
- root.classList.add('bw_theme_alt');
6901
- } else {
6902
- root.classList.remove('bw_theme_alt');
7276
+ * bw.loadStyles(); // defaults, global
7277
+ * bw.loadStyles({ primary: '#4f46e5' }); // custom, global
7278
+ * bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
7279
+ */
7280
+ bw.loadStyles = function(config, scope) {
7281
+ // Also inject structural CSS first (only once)
7282
+ if (bw._isBrowser) {
7283
+ var existing = document.getElementById('bw_structural');
7284
+ if (!existing) {
7285
+ var structuralCSS = bw.css(getStructuralStyles());
7286
+ bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
7287
+ }
6903
7288
  }
7289
+ return bw.applyStyles(bw.makeStyles(config), scope);
7290
+ };
6904
7291
 
6905
- bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
6906
- return bw._activeThemeMode;
7292
+ /**
7293
+ * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
7294
+ * Idempotent — if already injected, returns the existing `<style>` element.
7295
+ *
7296
+ * @returns {Element|null} The `<style>` element, or null in Node.js
7297
+ * @category CSS & Styling
7298
+ * @see bw.loadStyles
7299
+ * @see bw.clearStyles
7300
+ * @example
7301
+ * bw.loadReset(); // inject once, safe to call multiple times
7302
+ */
7303
+ bw.loadReset = function() {
7304
+ if (!bw._isBrowser) return null;
7305
+ var existing = document.getElementById('bw_style_reset');
7306
+ if (existing) return existing;
7307
+ return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
6907
7308
  };
6908
7309
 
6909
7310
  /**
6910
- * Toggle between primary and alternate theme palettes.
7311
+ * Toggle between primary and alternate palettes.
7312
+ *
7313
+ * Adds/removes the `bw_theme_alt` class on the scoping element.
7314
+ * Without a scope, toggles on `<html>` (global).
7315
+ * With a scope, toggles on the first matching element.
6911
7316
  *
7317
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
6912
7318
  * @returns {string} Active mode after toggle: 'primary' or 'alternate'
6913
7319
  * @category CSS & Styling
6914
- * @see bw.applyTheme
6915
- * @see bw.generateTheme
7320
+ * @see bw.applyStyles
7321
+ * @see bw.clearStyles
6916
7322
  * @example
6917
- * bw.toggleTheme(); // flip between primary and alternate
7323
+ * bw.toggleStyles(); // global toggle on <html>
7324
+ * bw.toggleStyles('#my-dashboard'); // scoped toggle
6918
7325
  */
6919
- bw.toggleTheme = function() {
6920
- var current = bw._activeThemeMode || 'primary';
6921
- return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
7326
+ bw.toggleStyles = function(scope) {
7327
+ if (!bw._isBrowser) return 'primary';
7328
+ var target;
7329
+ if (scope) {
7330
+ var els = bw.$(scope);
7331
+ target = els[0];
7332
+ } else {
7333
+ target = document.documentElement;
7334
+ }
7335
+ if (!target) return 'primary';
7336
+
7337
+ var hasAlt = target.classList.contains('bw_theme_alt');
7338
+ if (hasAlt) {
7339
+ target.classList.remove('bw_theme_alt');
7340
+ return 'primary';
7341
+ } else {
7342
+ target.classList.add('bw_theme_alt');
7343
+ return 'alternate';
7344
+ }
6922
7345
  };
6923
7346
 
6924
7347
  /**
6925
- * Remove the currently active theme's injected style elements from the DOM.
6926
- * Use this before generating a new theme with a different name to prevent
6927
- * stale CSS accumulation.
7348
+ * Remove injected styles for a given scope.
7349
+ *
7350
+ * Finds the `<style>` element by id and removes it. Also removes
7351
+ * the `bw_theme_alt` class from the relevant element.
6928
7352
  *
7353
+ * @param {string} [scope] - Scope selector. Omit to remove global styles.
6929
7354
  * @category CSS & Styling
6930
- * @see bw.generateTheme
7355
+ * @see bw.applyStyles
7356
+ * @see bw.loadStyles
6931
7357
  * @example
6932
- * bw.clearTheme(); // remove current theme styles
6933
- * bw.generateTheme('sunset', conf); // inject fresh theme
6934
- */
6935
- bw.clearTheme = function() {
6936
- if (bw._activeThemeStyleIds && bw._isBrowser) {
6937
- bw._activeThemeStyleIds.forEach(function(id) {
6938
- var el = document.getElementById(id);
6939
- if (el) el.remove();
6940
- });
6941
- bw._activeThemeStyleIds = null;
7358
+ * bw.clearStyles(); // remove global styles
7359
+ * bw.clearStyles('#my-dashboard'); // remove scoped styles
7360
+ * bw.clearStyles('reset'); // remove the CSS reset
7361
+ */
7362
+ bw.clearStyles = function(scope) {
7363
+ if (!bw._isBrowser) return;
7364
+ var styleId = _scopeToStyleId(scope);
7365
+ var el = document.getElementById(styleId);
7366
+ if (el) el.remove();
7367
+
7368
+ // Also remove bw_theme_alt from the relevant element
7369
+ if (scope && scope !== 'reset' && scope !== 'global') {
7370
+ var targets = bw.$(scope);
7371
+ if (targets[0]) targets[0].classList.remove('bw_theme_alt');
7372
+ } else if (!scope || scope === 'global') {
7373
+ document.documentElement.classList.remove('bw_theme_alt');
6942
7374
  }
6943
- bw._activeTheme = null;
6944
- bw._activeThemeMode = 'primary';
6945
7375
  };
6946
7376
 
6947
7377
  // Expose color utility functions on bw namespace
@@ -7164,10 +7594,15 @@
7164
7594
  * @param {Object} config - Table configuration
7165
7595
  * @param {Array<Object>} config.data - Array of row objects to display
7166
7596
  * @param {Array<Object>} [config.columns] - Column definitions with key, label, render
7167
- * @param {string} [config.className='table'] - CSS class for table element
7597
+ * @param {string} [config.className=''] - Additional CSS classes for table element
7168
7598
  * @param {boolean} [config.sortable=true] - Enable click-to-sort headers
7169
7599
  * @param {Function} [config.onSort] - Sort callback (column, direction)
7170
- * @returns {Object} TACO object for table
7600
+ * @param {boolean} [config.selectable=false] - Enable row selection on click
7601
+ * @param {Function} [config.onRowClick] - Row click callback (row, index, event)
7602
+ * @param {number} [config.pageSize] - Rows per page (enables pagination when set)
7603
+ * @param {number} [config.currentPage=1] - Current page number (1-based)
7604
+ * @param {Function} [config.onPageChange] - Page change callback (newPage)
7605
+ * @returns {Object} TACO object for table (with optional pagination controls)
7171
7606
  * @category Component Builders
7172
7607
  * @see bw.makeDataTable
7173
7608
  * @example
@@ -7179,7 +7614,12 @@
7179
7614
  * columns: [
7180
7615
  * { key: 'name', label: 'Name' },
7181
7616
  * { key: 'age', label: 'Age' }
7182
- * ]
7617
+ * ],
7618
+ * selectable: true,
7619
+ * onRowClick: function(row, i) { console.log('clicked', row.name); },
7620
+ * pageSize: 10,
7621
+ * currentPage: 1,
7622
+ * onPageChange: function(page) { console.log('page', page); }
7183
7623
  * });
7184
7624
  */
7185
7625
  bw.makeTable = function(config) {
@@ -7192,41 +7632,47 @@
7192
7632
  sortable = true,
7193
7633
  onSort,
7194
7634
  sortColumn,
7195
- sortDirection = 'asc'
7635
+ sortDirection = 'asc',
7636
+ selectable = false,
7637
+ onRowClick,
7638
+ pageSize,
7639
+ currentPage = 1,
7640
+ onPageChange
7196
7641
  } = config;
7197
7642
 
7198
- // Build class list: always include bw_table, add striped/hover, append user className
7643
+ // Build class list: always include bw_table, add striped/hover/selectable, append user className
7199
7644
  let cls = 'bw_table';
7200
7645
  if (striped) cls += ' bw_table_striped';
7201
- if (hover) cls += ' bw_table_hover';
7646
+ if (hover || selectable) cls += ' bw_table_hover';
7647
+ if (selectable) cls += ' bw_table_selectable';
7202
7648
  if (className) cls += ' ' + className;
7203
7649
  cls = cls.trim();
7204
-
7650
+
7205
7651
  // Auto-detect columns if not provided
7206
- const cols = columns || (data.length > 0
7207
- ? Object.keys(data[0]).map(key => ({ key, label: key }))
7652
+ const cols = columns || (data.length > 0
7653
+ ? _keys(data[0]).map(key => ({ key, label: key }))
7208
7654
  : []);
7209
-
7655
+
7210
7656
  // Current sort state
7211
7657
  let currentSortColumn = sortColumn || null;
7212
7658
  let currentSortDirection = sortDirection;
7213
-
7659
+
7214
7660
  // Sort data if column specified
7215
7661
  let sortedData = [...data];
7216
7662
  if (currentSortColumn) {
7217
7663
  sortedData.sort((a, b) => {
7218
7664
  const aVal = a[currentSortColumn];
7219
7665
  const bVal = b[currentSortColumn];
7220
-
7666
+
7221
7667
  // Handle different types
7222
- if (typeof aVal === 'number' && typeof bVal === 'number') {
7668
+ if (_is(aVal, 'number') && _is(bVal, 'number')) {
7223
7669
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
7224
7670
  }
7225
-
7671
+
7226
7672
  // String comparison
7227
7673
  const aStr = String(aVal || '').toLowerCase();
7228
7674
  const bStr = String(bVal || '').toLowerCase();
7229
-
7675
+
7230
7676
  if (currentSortDirection === 'asc') {
7231
7677
  return aStr.localeCompare(bStr);
7232
7678
  } else {
@@ -7234,23 +7680,32 @@
7234
7680
  }
7235
7681
  });
7236
7682
  }
7237
-
7683
+
7684
+ // Pagination
7685
+ const totalRows = sortedData.length;
7686
+ const totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
7687
+ const page = Math.max(1, Math.min(currentPage, totalPages));
7688
+ if (pageSize) {
7689
+ const start = (page - 1) * pageSize;
7690
+ sortedData = sortedData.slice(start, start + pageSize);
7691
+ }
7692
+
7238
7693
  // Create sort handler
7239
7694
  const handleSort = (column) => {
7240
7695
  if (!sortable) return;
7241
-
7696
+
7242
7697
  if (currentSortColumn === column) {
7243
7698
  currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
7244
7699
  } else {
7245
7700
  currentSortColumn = column;
7246
7701
  currentSortDirection = 'asc';
7247
7702
  }
7248
-
7703
+
7249
7704
  if (onSort) {
7250
7705
  onSort(column, currentSortDirection);
7251
7706
  }
7252
7707
  };
7253
-
7708
+
7254
7709
  // Build table header
7255
7710
  const thead = {
7256
7711
  t: 'thead',
@@ -7273,24 +7728,87 @@
7273
7728
  }))
7274
7729
  }
7275
7730
  };
7276
-
7277
- // Build table body
7731
+
7732
+ // Build table body with selectable/onRowClick support
7278
7733
  const tbody = {
7279
7734
  t: 'tbody',
7280
- c: sortedData.map(row => ({
7281
- t: 'tr',
7282
- c: cols.map(col => ({
7283
- t: 'td',
7284
- c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
7285
- }))
7286
- }))
7735
+ c: sortedData.map((row, idx) => {
7736
+ const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
7737
+ const rowAttrs = {};
7738
+ if (selectable || onRowClick) {
7739
+ rowAttrs.style = 'cursor:pointer;';
7740
+ rowAttrs.onclick = function(e) {
7741
+ if (selectable) {
7742
+ // Toggle selected class on this row
7743
+ var tr = e.currentTarget;
7744
+ tr.classList.toggle('bw_table_row_selected');
7745
+ }
7746
+ if (onRowClick) {
7747
+ onRowClick(row, globalIdx, e);
7748
+ }
7749
+ };
7750
+ }
7751
+ return {
7752
+ t: 'tr',
7753
+ a: rowAttrs,
7754
+ c: cols.map(col => ({
7755
+ t: 'td',
7756
+ c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
7757
+ }))
7758
+ };
7759
+ })
7287
7760
  };
7288
-
7289
- return {
7761
+
7762
+ const table = {
7290
7763
  t: 'table',
7291
7764
  a: { class: cls },
7292
7765
  c: [thead, tbody]
7293
7766
  };
7767
+
7768
+ // If no pagination, return table directly
7769
+ if (!pageSize) return table;
7770
+
7771
+ // Build pagination controls
7772
+ const pageButtons = [];
7773
+ // Previous button
7774
+ pageButtons.push({
7775
+ t: 'button',
7776
+ a: {
7777
+ class: 'bw_btn bw_btn_sm',
7778
+ disabled: page <= 1 ? 'disabled' : undefined,
7779
+ onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
7780
+ },
7781
+ c: 'Prev'
7782
+ });
7783
+ // Page info
7784
+ pageButtons.push({
7785
+ t: 'span',
7786
+ a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
7787
+ c: 'Page ' + page + ' of ' + totalPages
7788
+ });
7789
+ // Next button
7790
+ pageButtons.push({
7791
+ t: 'button',
7792
+ a: {
7793
+ class: 'bw_btn bw_btn_sm',
7794
+ disabled: page >= totalPages ? 'disabled' : undefined,
7795
+ onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
7796
+ },
7797
+ c: 'Next'
7798
+ });
7799
+
7800
+ return {
7801
+ t: 'div',
7802
+ a: { class: 'bw_table_paginated' },
7803
+ c: [
7804
+ table,
7805
+ {
7806
+ t: 'div',
7807
+ a: { class: 'bw_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
7808
+ c: pageButtons
7809
+ }
7810
+ ]
7811
+ };
7294
7812
  };
7295
7813
 
7296
7814
  /**
@@ -7329,7 +7847,7 @@
7329
7847
  bw.makeTableFromArray = function(config) {
7330
7848
  const { data = [], headerRow = true, columns, ...rest } = config;
7331
7849
 
7332
- if (!Array.isArray(data) || data.length === 0) {
7850
+ if (!_isA(data) || data.length === 0) {
7333
7851
  return bw.makeTable({ data: [], columns: columns || [], ...rest });
7334
7852
  }
7335
7853
 
@@ -7411,7 +7929,7 @@
7411
7929
  className = ''
7412
7930
  } = config;
7413
7931
 
7414
- if (!Array.isArray(data) || data.length === 0) {
7932
+ if (!_isA(data) || data.length === 0) {
7415
7933
  return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
7416
7934
  }
7417
7935
 
@@ -7560,7 +8078,7 @@
7560
8078
  */
7561
8079
  bw.render = function(element, position, taco) {
7562
8080
  // Get target element
7563
- const targetEl = typeof element === 'string'
8081
+ const targetEl = _is(element, 'string')
7564
8082
  ? document.querySelector(element)
7565
8083
  : element;
7566
8084
 
@@ -7710,7 +8228,7 @@
7710
8228
  setContent(content) {
7711
8229
  this._taco.c = content;
7712
8230
  if (this.element) {
7713
- if (typeof content === 'string') {
8231
+ if (_is(content, 'string')) {
7714
8232
  this.element.textContent = content;
7715
8233
  } else {
7716
8234
  // Re-render for complex content