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 v2.0.16 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench 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
  /**
@@ -3597,7 +3766,7 @@
3597
3766
  if (breakpoint === 'xs') {
3598
3767
  classes.push(`bw_col_${value}`);
3599
3768
  } else {
3600
- classes.push(`bw_col_${breakpoint}-${value}`);
3769
+ classes.push(`bw_col_${breakpoint}_${value}`);
3601
3770
  }
3602
3771
  });
3603
3772
  } else if (size) {
@@ -4980,8 +5149,8 @@
4980
5149
  t: 'li',
4981
5150
  a: { class: `bw_page_item ${currentPage <= 1 ? 'bw_disabled' : ''}`.trim() },
4982
5151
  c: {
4983
- t: 'a',
4984
- a: { class: 'bw_page_link', href: '#', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous' },
5152
+ t: 'button',
5153
+ a: { class: 'bw_page_link', type: 'button', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous', disabled: currentPage <= 1 ? true : undefined },
4985
5154
  c: '\u2039'
4986
5155
  }
4987
5156
  });
@@ -4993,8 +5162,8 @@
4993
5162
  t: 'li',
4994
5163
  a: { class: `bw_page_item ${pageNum === currentPage ? 'bw_active' : ''}`.trim() },
4995
5164
  c: {
4996
- t: 'a',
4997
- a: { class: 'bw_page_link', href: '#', onclick: handleClick(pageNum) },
5165
+ t: 'button',
5166
+ a: { class: 'bw_page_link', type: 'button', onclick: handleClick(pageNum), 'aria-current': pageNum === currentPage ? 'page' : undefined },
4998
5167
  c: '' + pageNum
4999
5168
  }
5000
5169
  });
@@ -5006,8 +5175,8 @@
5006
5175
  t: 'li',
5007
5176
  a: { class: `bw_page_item ${currentPage >= pages ? 'bw_disabled' : ''}`.trim() },
5008
5177
  c: {
5009
- t: 'a',
5010
- a: { class: 'bw_page_link', href: '#', onclick: handleClick(currentPage + 1), 'aria-label': 'Next' },
5178
+ t: 'button',
5179
+ a: { class: 'bw_page_link', type: 'button', onclick: handleClick(currentPage + 1), 'aria-label': 'Next', disabled: currentPage >= pages ? true : undefined },
5011
5180
  c: '\u203A'
5012
5181
  }
5013
5182
  });
@@ -6880,7 +7049,11 @@
6880
7049
  function make(type, props) {
6881
7050
  var def = BCCL[type];
6882
7051
  if (!def) throw new Error('bw.make: unknown component type "' + type + '". Available: ' + Object.keys(BCCL).join(', '));
6883
- return def.make(props || {});
7052
+ var taco = def.make(props || {});
7053
+ if (taco && typeof taco === 'object') {
7054
+ taco._bwFactory = { type: type, props: props || {} };
7055
+ }
7056
+ return taco;
6884
7057
  }
6885
7058
 
6886
7059
  var components = /*#__PURE__*/Object.freeze({
@@ -7001,7 +7174,7 @@
7001
7174
  __monkey_patch_is_nodejs__: {
7002
7175
  _value: 'ignore',
7003
7176
  set: function(x) {
7004
- this._value = (typeof x === 'boolean') ? x : 'ignore';
7177
+ this._value = _is(x, 'boolean') ? x : 'ignore';
7005
7178
  },
7006
7179
  get: function() {
7007
7180
  return this._value;
@@ -7049,6 +7222,67 @@
7049
7222
  configurable: true
7050
7223
  });
7051
7224
 
7225
+ // ── Internal aliases ─────────────────────────────────────────────────────
7226
+ // Short names for frequently-used builtins and internal methods.
7227
+ // Same pattern as v1 (_to = bw.typeOf, etc.).
7228
+ //
7229
+ // Why: Terser can't shorten global property chains (console.warn,
7230
+ // Object.prototype.hasOwnProperty, Array.isArray, document.createElement)
7231
+ // because it can't prove they're side-effect-free. We can, so we alias
7232
+ // them here. Each alias saves bytes in the minified output, and the short
7233
+ // names also reduce visual noise in the hot paths (binding pipeline,
7234
+ // createDOM, etc.).
7235
+ //
7236
+ // Alias Target Sites
7237
+ // ───────── ────────────────────────────────────── ─────
7238
+ // _hop Object.prototype.hasOwnProperty 15
7239
+ // _isA Array.isArray 25
7240
+ // _keys Object.keys 7
7241
+ // _to bw.typeOf (type string) 26
7242
+ // _is type check boolean: _is(x,'string') ~50
7243
+ // _cw console.warn 8
7244
+ // _cl console.log 11
7245
+ // _ce console.error 4
7246
+ // _chp ComponentHandle.prototype 28 (defined after constructor)
7247
+ //
7248
+ // Note: document.createElement etc. are NOT aliased because they require
7249
+ // `this === document` and .bind() would add overhead on every call.
7250
+ // Console aliases use thin wrappers (not direct refs) so test monkey-
7251
+ // patching of console.warn/log/error continues to work.
7252
+ //
7253
+ // `typeof x` for UNDECLARED globals (window, document, process, require,
7254
+ // EventSource, navigator, Promise, __filename, import.meta) MUST stay as
7255
+ // raw `typeof` — calling _to(x) when x doesn't exist throws ReferenceError.
7256
+ //
7257
+ // ── v1 functional type helpers (kept for reference, not currently used) ──
7258
+ // _toa(x, type, trueVal, falseVal) — bw.typeAssign:
7259
+ // returns trueVal if _to(x)===type, else falseVal.
7260
+ // Replaces: (typeof x === 'string') ? A : B → _toa(x,'string',A,B)
7261
+ // _toc(x, type, trueVal, falseVal) — bw.typeConvert:
7262
+ // same as _toa but if trueVal/falseVal are functions, calls them with x.
7263
+ // Replaces: typeof x === 'string' ? fn(x) : default → _toc(x,'string',fn,default)
7264
+ // Uncomment if pattern frequency justifies them:
7265
+ // var _toa = function(x, t, y, n) { return _to(x) === t ? y : n; };
7266
+ // 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); };
7267
+ // ─────────────────────────────────────────────────────────────────────────
7268
+ var _hop = Object.prototype.hasOwnProperty;
7269
+ var _isA = Array.isArray;
7270
+ var _keys = Object.keys;
7271
+ var _to = typeOf; // imported from bitwrench-utils.js
7272
+ var _is = function(x, t) { var r = _to(x); return r === t || r.toLowerCase() === t; };
7273
+ // Console aliases use thin wrappers (not direct references) so that test
7274
+ // code can monkey-patch console.warn/log/error and the patches take effect.
7275
+ var _cw = function() { console.warn.apply(console, arguments); };
7276
+ var _cl = function() { console.log.apply(console, arguments); };
7277
+ var _ce = function() { console.error.apply(console, arguments); };
7278
+
7279
+ /**
7280
+ * Debug flag. When true, emits console.warn for silent binding failures
7281
+ * (missing paths, null refs, auto-created intermediate objects).
7282
+ * @type {boolean}
7283
+ */
7284
+ bw.debug = false;
7285
+
7052
7286
  /**
7053
7287
  * Lazy-resolve Node.js `fs` module.
7054
7288
  * Tries require('fs') first (available in CJS/UMD Node.js builds),
@@ -7196,7 +7430,7 @@
7196
7430
  */
7197
7431
  bw._el = function(id) {
7198
7432
  // Pass-through for DOM elements
7199
- if (typeof id !== 'string') return id || null;
7433
+ if (!_is(id, 'string')) return id || null;
7200
7434
  if (!id) return null;
7201
7435
  if (!bw._isBrowser) return null;
7202
7436
 
@@ -7224,7 +7458,12 @@
7224
7458
  el = document.querySelector('[data-bw_id="' + id + '"]');
7225
7459
  }
7226
7460
 
7227
- // 5. Cache the result for next time
7461
+ // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
7462
+ if (!el && id.indexOf('bw_uuid_') === 0) {
7463
+ el = document.querySelector('.' + id);
7464
+ }
7465
+
7466
+ // 6. Cache the result for next time
7228
7467
  if (el) {
7229
7468
  bw._nodeMap[id] = el;
7230
7469
  }
@@ -7277,6 +7516,84 @@
7277
7516
  }
7278
7517
  };
7279
7518
 
7519
+ // ===================================================================================
7520
+ // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
7521
+ // ===================================================================================
7522
+
7523
+ /**
7524
+ * Regex to match a bw_uuid_* token in a class string.
7525
+ * @private
7526
+ */
7527
+ var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
7528
+
7529
+ /**
7530
+ * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
7531
+ *
7532
+ * Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
7533
+ * to replace an existing UUID (useful in loops where each TACO needs a unique ID).
7534
+ *
7535
+ * @param {Object} taco - A TACO object `{t, a, c, o}`
7536
+ * @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
7537
+ * @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
7538
+ * @category Identifiers
7539
+ * @example
7540
+ * var card = bw.makeStatCard({ value: '0', label: 'Scans' });
7541
+ * var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
7542
+ * var same = bw.assignUUID(card); // same UUID (idempotent)
7543
+ * var diff = bw.assignUUID(card, true); // new UUID (forced)
7544
+ */
7545
+ bw.assignUUID = function(taco, forceNew) {
7546
+ if (!taco || !_is(taco, 'object')) return null;
7547
+
7548
+ // Ensure taco.a exists
7549
+ if (!taco.a) taco.a = {};
7550
+ if (!_is(taco.a.class, 'string')) taco.a.class = taco.a.class ? String(taco.a.class) : '';
7551
+
7552
+ var existing = taco.a.class.match(_UUID_RE);
7553
+
7554
+ if (existing && !forceNew) {
7555
+ return existing[0];
7556
+ }
7557
+
7558
+ // Remove old UUID if forceNew
7559
+ if (existing) {
7560
+ taco.a.class = taco.a.class.replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
7561
+ }
7562
+
7563
+ var uuid = bw.uuid('uuid');
7564
+ taco.a.class = (taco.a.class ? taco.a.class + ' ' : '') + uuid;
7565
+ return uuid;
7566
+ };
7567
+
7568
+ /**
7569
+ * Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
7570
+ *
7571
+ * @param {Object|Element} tacoOrElement - A TACO object or DOM element
7572
+ * @returns {string|null} The UUID string, or null if none assigned
7573
+ * @category Identifiers
7574
+ * @example
7575
+ * bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
7576
+ * bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
7577
+ * bw.getUUID({t:'div'}) // null (no UUID)
7578
+ */
7579
+ bw.getUUID = function(tacoOrElement) {
7580
+ if (!tacoOrElement) return null;
7581
+
7582
+ var classStr;
7583
+ // DOM element: check className
7584
+ if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
7585
+ classStr = tacoOrElement.className;
7586
+ }
7587
+ // TACO object: check a.class
7588
+ else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
7589
+ classStr = tacoOrElement.a.class;
7590
+ }
7591
+
7592
+ if (!classStr) return null;
7593
+ var match = classStr.match(_UUID_RE);
7594
+ return match ? match[0] : null;
7595
+ };
7596
+
7280
7597
  /**
7281
7598
  * Escape HTML special characters to prevent XSS.
7282
7599
  *
@@ -7292,7 +7609,7 @@
7292
7609
  * // => '&lt;b&gt;Hello&lt;&#x2F;b&gt; &amp; &quot;world&quot;'
7293
7610
  */
7294
7611
  bw.escapeHTML = function(str) {
7295
- if (typeof str !== 'string') return '';
7612
+ if (!_is(str, 'string')) return '';
7296
7613
 
7297
7614
  const escapeMap = {
7298
7615
  '&': '&amp;',
@@ -7326,6 +7643,42 @@
7326
7643
  return { __bw_raw: true, v: String(str) };
7327
7644
  };
7328
7645
 
7646
+ /**
7647
+ * Hyperscript-style TACO constructor.
7648
+ *
7649
+ * A convenience helper that returns a canonical TACO object from positional
7650
+ * arguments. The return value is a plain object — serializable, works with
7651
+ * bwserve, and accepted everywhere TACO is accepted.
7652
+ *
7653
+ * @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
7654
+ * @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
7655
+ * @param {*} [content] - Content: string, number, TACO object, or array of children.
7656
+ * @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
7657
+ * @returns {Object} Plain TACO object {t, a?, c?, o?}
7658
+ * @category Utilities
7659
+ * @see bw.html
7660
+ * @see bw.createDOM
7661
+ * @see bw.DOM
7662
+ * @example
7663
+ * bw.h('div')
7664
+ * // => { t: 'div' }
7665
+ *
7666
+ * bw.h('p', { class: 'bw_text_muted' }, 'Hello')
7667
+ * // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
7668
+ *
7669
+ * bw.h('ul', null, [
7670
+ * bw.h('li', null, 'one'),
7671
+ * bw.h('li', null, 'two')
7672
+ * ])
7673
+ * // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
7674
+ */
7675
+ bw.h = function(tag, attrs, content, options) {
7676
+ var taco = { t: String(tag) };
7677
+ if (attrs !== null && attrs !== undefined) taco.a = attrs;
7678
+ if (content !== undefined) taco.c = content;
7679
+ if (options !== undefined) taco.o = options;
7680
+ return taco;
7681
+ };
7329
7682
 
7330
7683
  /**
7331
7684
  * Convert a TACO object (or array of TACOs) to an HTML string.
@@ -7365,7 +7718,7 @@
7365
7718
  }
7366
7719
 
7367
7720
  // Handle arrays of TACOs
7368
- if (Array.isArray(taco)) {
7721
+ if (_isA(taco)) {
7369
7722
  return taco.map(t => bw.html(t, options)).join('');
7370
7723
  }
7371
7724
 
@@ -7388,15 +7741,15 @@
7388
7741
  if (taco && taco._bwEach && options.state) {
7389
7742
  var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
7390
7743
  var arr = bw._evaluatePath(options.state, eachExpr);
7391
- if (!Array.isArray(arr)) return '';
7744
+ if (!_isA(arr)) return '';
7392
7745
  return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
7393
7746
  }
7394
7747
 
7395
7748
  // Handle primitives and non-TACO objects
7396
- if (typeof taco !== 'object' || !taco.t) {
7749
+ if (!_is(taco, 'object') || !taco.t) {
7397
7750
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
7398
7751
  // Resolve template bindings if state provided
7399
- if (options.state && typeof str === 'string' && str.indexOf('${') >= 0) {
7752
+ if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
7400
7753
  str = bw._resolveTemplate(str, options.state, !!options.compile);
7401
7754
  }
7402
7755
  return str;
@@ -7416,10 +7769,18 @@
7416
7769
  // Skip null, undefined, false
7417
7770
  if (value == null || value === false) continue;
7418
7771
 
7419
- // Skip event handlers (they're for DOM only)
7420
- if (key.startsWith('on')) continue;
7772
+ // Serialize event handlers via funcRegister
7773
+ if (key.startsWith('on')) {
7774
+ if (_is(value, 'function')) {
7775
+ var fnId = bw.funcRegister(value);
7776
+ attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
7777
+ } else if (_is(value, 'string')) {
7778
+ attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
7779
+ }
7780
+ continue;
7781
+ }
7421
7782
 
7422
- if (key === 'style' && typeof value === 'object') {
7783
+ if (key === 'style' && _is(value, 'object')) {
7423
7784
  // Convert style object to string
7424
7785
  const styleStr = Object.entries(value)
7425
7786
  .filter(([, v]) => v != null)
@@ -7430,7 +7791,7 @@
7430
7791
  }
7431
7792
  } else if (key === 'class') {
7432
7793
  // Handle class as array or string
7433
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
7794
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
7434
7795
  if (classStr) {
7435
7796
  attrStr += ` class="${bw.escapeHTML(classStr)}"`;
7436
7797
  }
@@ -7466,13 +7827,184 @@
7466
7827
  // Process content recursively
7467
7828
  let contentStr = content != null ? bw.html(content, options) : '';
7468
7829
  // Resolve template bindings in content if state provided
7469
- if (options.state && typeof contentStr === 'string' && contentStr.indexOf('${') >= 0) {
7830
+ if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
7470
7831
  contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
7471
7832
  }
7472
7833
 
7473
7834
  return `<${tag}${attrStr}>${contentStr}</${tag}>`;
7474
7835
  };
7475
7836
 
7837
+ /**
7838
+ * Generate a complete, self-contained HTML document from TACO content.
7839
+ *
7840
+ * Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
7841
+ * func registry emission (so serialized event handlers work), optional theme,
7842
+ * and extra head elements. Designed for static site generation, offline/airgapped
7843
+ * use, and the "static site that isn't static" workflow.
7844
+ *
7845
+ * @param {Object} [opts={}] - Page options
7846
+ * @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
7847
+ * @param {string} [opts.title='bitwrench'] - Page title
7848
+ * @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
7849
+ * @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
7850
+ * @param {string} [opts.css=''] - Additional CSS for <style> block
7851
+ * @param {string|Object} [opts.theme=null] - Theme preset name or config object
7852
+ * @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
7853
+ * @param {string} [opts.favicon=''] - Favicon URL
7854
+ * @param {string} [opts.lang='en'] - HTML lang attribute
7855
+ * @returns {string} Complete HTML document string
7856
+ * @category DOM Generation
7857
+ * @see bw.html
7858
+ * @example
7859
+ * bw.htmlPage({
7860
+ * title: 'My App',
7861
+ * body: { t: 'h1', c: 'Hello World' },
7862
+ * runtime: 'shim'
7863
+ * })
7864
+ */
7865
+ bw.htmlPage = function(opts) {
7866
+ opts = opts || {};
7867
+ var title = opts.title || 'bitwrench';
7868
+ var body = opts.body || '';
7869
+ var state = opts.state || undefined;
7870
+ var runtime = opts.runtime || 'shim';
7871
+ var css = opts.css || '';
7872
+ var theme = opts.theme || null;
7873
+ var headExtra = opts.head || [];
7874
+ var favicon = opts.favicon || '';
7875
+ var lang = opts.lang || 'en';
7876
+
7877
+ // Snapshot funcRegistry counter before rendering
7878
+ var fnCounterBefore = bw._fnIDCounter;
7879
+
7880
+ // Render body content
7881
+ var bodyHTML = '';
7882
+ if (_is(body, 'string')) {
7883
+ bodyHTML = body;
7884
+ } else {
7885
+ var htmlOpts = {};
7886
+ if (state) htmlOpts.state = state;
7887
+ bodyHTML = bw.html(body, htmlOpts);
7888
+ }
7889
+
7890
+ // Collect functions registered during this render
7891
+ var fnCounterAfter = bw._fnIDCounter;
7892
+ var registryEntries = '';
7893
+ for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
7894
+ var fnKey = 'bw_fn_' + i;
7895
+ if (bw._fnRegistry[fnKey]) {
7896
+ registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' +
7897
+ bw._fnRegistry[fnKey].toString() + ';\n';
7898
+ }
7899
+ }
7900
+
7901
+ // Build runtime script for <head>
7902
+ var runtimeHead = '';
7903
+ if (runtime === 'inline') {
7904
+ // Read UMD bundle synchronously if in Node.js
7905
+ var umdSource = null;
7906
+ if (bw._isNode) {
7907
+ try {
7908
+ var fs = (typeof require === 'function') ? require('fs') : null;
7909
+ var pathMod = (typeof require === 'function') ? require('path') : null;
7910
+ if (fs && pathMod) {
7911
+ // Resolve dist/ relative to this source file
7912
+ var srcDir = '';
7913
+ try { srcDir = pathMod.dirname((typeof __filename !== 'undefined') ? __filename : ''); }
7914
+ catch(e2) { /* ESM: __filename not available */ }
7915
+ if (!srcDir && typeof ({ url: (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench.umd.js', document.baseURI).href)) }) !== 'undefined' && (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench.umd.js', document.baseURI).href))) {
7916
+ var url = (typeof require === 'function') ? require('url') : null;
7917
+ if (url && url.fileURLToPath) srcDir = pathMod.dirname(url.fileURLToPath((typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench.umd.js', document.baseURI).href))));
7918
+ }
7919
+ if (srcDir) {
7920
+ var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
7921
+ umdSource = fs.readFileSync(distPath, 'utf8');
7922
+ }
7923
+ }
7924
+ } catch(e) { /* fall through */ }
7925
+ }
7926
+ if (umdSource) {
7927
+ runtimeHead = '<script>' + umdSource + '</script>';
7928
+ } else {
7929
+ // Fallback to shim in browser or if dist not available
7930
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
7931
+ }
7932
+ } else if (runtime === 'cdn') {
7933
+ runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
7934
+ } else if (runtime === 'shim') {
7935
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
7936
+ }
7937
+ // runtime === 'none' → empty
7938
+
7939
+ // Theme CSS
7940
+ var themeCSS = '';
7941
+ if (theme) {
7942
+ var themeConfig = _is(theme, 'string')
7943
+ ? (THEME_PRESETS[theme.toLowerCase()] || null)
7944
+ : theme;
7945
+ if (themeConfig) {
7946
+ var themeResult = bw.makeStyles(themeConfig);
7947
+ themeCSS = themeResult.css;
7948
+ }
7949
+ }
7950
+
7951
+ // Extra <head> elements
7952
+ var headHTML = '';
7953
+ if (_isA(headExtra) && headExtra.length > 0) {
7954
+ headHTML = headExtra.map(function(el) { return bw.html(el); }).join('\n');
7955
+ }
7956
+
7957
+ // Favicon
7958
+ var faviconTag = '';
7959
+ if (favicon) {
7960
+ var safeFavicon = favicon.replace(/[&<>"']/g, function(c) {
7961
+ return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
7962
+ });
7963
+ faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
7964
+ }
7965
+
7966
+ // Escaped title
7967
+ var safeTitle = bw.escapeHTML(title);
7968
+
7969
+ // Combine all CSS
7970
+ var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
7971
+
7972
+ // Body-end script: registry entries + optional loadStyles
7973
+ var bodyEndScript = '';
7974
+ var bodyEndParts = [];
7975
+ if (registryEntries) {
7976
+ bodyEndParts.push(registryEntries);
7977
+ }
7978
+ if (runtime === 'inline' || runtime === 'cdn') {
7979
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
7980
+ }
7981
+ if (bodyEndParts.length > 0) {
7982
+ bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
7983
+ }
7984
+
7985
+ // Assemble document
7986
+ var parts = [
7987
+ '<!DOCTYPE html>',
7988
+ '<html lang="' + lang + '">',
7989
+ '<head>',
7990
+ '<meta charset="UTF-8">',
7991
+ '<meta name="viewport" content="width=device-width, initial-scale=1">'
7992
+ ];
7993
+ parts.push('<title>' + safeTitle + '</title>');
7994
+ if (faviconTag) parts.push(faviconTag);
7995
+ if (runtimeHead) parts.push(runtimeHead);
7996
+ if (headHTML) parts.push(headHTML);
7997
+ if (allCSS) parts.push('<style>' + allCSS + '</style>');
7998
+ parts.push('</head>');
7999
+ parts.push('<body>');
8000
+ parts.push(bodyHTML);
8001
+ if (bodyEndScript) parts.push(bodyEndScript);
8002
+ parts.push('</body>');
8003
+ parts.push('</html>');
8004
+
8005
+ return parts.join('\n');
8006
+ };
8007
+
7476
8008
  /**
7477
8009
  * Create a live DOM element from a TACO object (browser only).
7478
8010
  *
@@ -7517,7 +8049,7 @@
7517
8049
  }
7518
8050
 
7519
8051
  // Handle text nodes
7520
- if (typeof taco !== 'object' || !taco.t) {
8052
+ if (!_is(taco, 'object') || !taco.t) {
7521
8053
  return document.createTextNode(String(taco));
7522
8054
  }
7523
8055
 
@@ -7530,16 +8062,16 @@
7530
8062
  for (const [key, value] of Object.entries(attrs)) {
7531
8063
  if (value == null || value === false) continue;
7532
8064
 
7533
- if (key === 'style' && typeof value === 'object') {
8065
+ if (key === 'style' && _is(value, 'object')) {
7534
8066
  // Apply styles directly
7535
8067
  Object.assign(el.style, value);
7536
8068
  } else if (key === 'class') {
7537
8069
  // Handle class as array or string
7538
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
8070
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
7539
8071
  if (classStr) {
7540
8072
  el.className = classStr;
7541
8073
  }
7542
- } else if (key.startsWith('on') && typeof value === 'function') {
8074
+ } else if (key.startsWith('on') && _is(value, 'function')) {
7543
8075
  // Event handlers
7544
8076
  const eventName = key.slice(2).toLowerCase();
7545
8077
  el.addEventListener(eventName, value);
@@ -7559,7 +8091,7 @@
7559
8091
  // Children with data-bw_id or id attributes get local refs on the parent,
7560
8092
  // so o.render functions can access them without any DOM lookup.
7561
8093
  if (content != null) {
7562
- if (Array.isArray(content)) {
8094
+ if (_isA(content)) {
7563
8095
  content.forEach(child => {
7564
8096
  if (child != null) {
7565
8097
  // Handle ComponentHandle in content arrays (Level 2 children)
@@ -7579,20 +8111,20 @@
7579
8111
  if (childEl._bw_refs) {
7580
8112
  if (!el._bw_refs) el._bw_refs = {};
7581
8113
  for (var rk in childEl._bw_refs) {
7582
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
8114
+ if (_hop.call(childEl._bw_refs, rk)) {
7583
8115
  el._bw_refs[rk] = childEl._bw_refs[rk];
7584
8116
  }
7585
8117
  }
7586
8118
  }
7587
8119
  }
7588
8120
  });
7589
- } else if (typeof content === 'object' && content.__bw_raw) {
8121
+ } else if (_is(content, 'object') && content.__bw_raw) {
7590
8122
  // Raw HTML content — inject via innerHTML
7591
8123
  el.innerHTML = content.v;
7592
8124
  } else if (content._bwComponent === true) {
7593
8125
  // Single ComponentHandle as content
7594
8126
  content.mount(el);
7595
- } else if (typeof content === 'object' && content.t) {
8127
+ } else if (_is(content, 'object') && content.t) {
7596
8128
  var childEl = bw.createDOM(content, options);
7597
8129
  el.appendChild(childEl);
7598
8130
  var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
@@ -7603,7 +8135,7 @@
7603
8135
  if (childEl._bw_refs) {
7604
8136
  if (!el._bw_refs) el._bw_refs = {};
7605
8137
  for (var rk in childEl._bw_refs) {
7606
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
8138
+ if (_hop.call(childEl._bw_refs, rk)) {
7607
8139
  el._bw_refs[rk] = childEl._bw_refs[rk];
7608
8140
  }
7609
8141
  }
@@ -7618,6 +8150,14 @@
7618
8150
  bw._registerNode(el, null);
7619
8151
  }
7620
8152
 
8153
+ // Register UUID class in node cache (bw_uuid_* tokens in class string)
8154
+ if (el.className) {
8155
+ var uuidMatch = el.className.match(_UUID_RE);
8156
+ if (uuidMatch) {
8157
+ bw._nodeMap[uuidMatch[0]] = el;
8158
+ }
8159
+ }
8160
+
7621
8161
  // Handle lifecycle hooks and state
7622
8162
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
7623
8163
  const id = attrs['data-bw_id'] || bw.uuid();
@@ -7636,7 +8176,7 @@
7636
8176
  el._bw_render = opts.render;
7637
8177
 
7638
8178
  if (opts.mounted) {
7639
- console.warn('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
8179
+ _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
7640
8180
  }
7641
8181
 
7642
8182
  // Queue initial render (same timing as mounted)
@@ -7709,7 +8249,7 @@
7709
8249
  const targetEl = bw._el(target);
7710
8250
 
7711
8251
  if (!targetEl) {
7712
- console.error('bw.DOM: Target element not found:', target);
8252
+ _ce('bw.DOM: Target element not found:', target);
7713
8253
  return null;
7714
8254
  }
7715
8255
 
@@ -7749,7 +8289,7 @@
7749
8289
  targetEl.appendChild(taco.element);
7750
8290
  }
7751
8291
  // Handle arrays
7752
- else if (Array.isArray(taco)) {
8292
+ else if (_isA(taco)) {
7753
8293
  taco.forEach(t => {
7754
8294
  if (t != null) {
7755
8295
  if (t._bwComponent === true) {
@@ -7785,7 +8325,7 @@
7785
8325
  bw.compileProps = function(handle, props = {}) {
7786
8326
  const compiledProps = {};
7787
8327
 
7788
- Object.keys(props).forEach(key => {
8328
+ _keys(props).forEach(key => {
7789
8329
  // Create getter/setter for each prop
7790
8330
  Object.defineProperty(compiledProps, key, {
7791
8331
  get() {
@@ -7990,6 +8530,16 @@
7990
8530
  bw.cleanup = function(element) {
7991
8531
  if (!bw._isBrowser || !element) return;
7992
8532
 
8533
+ // Deregister UUID classes from node cache (element + descendants)
8534
+ // Covers elements that have UUID but no data-bw_id
8535
+ var selfUuidMatch = element.className && element.className.match(_UUID_RE);
8536
+ if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
8537
+ var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
8538
+ uuidEls.forEach(function(uel) {
8539
+ var m = uel.className && uel.className.match(_UUID_RE);
8540
+ if (m) delete bw._nodeMap[m[0]];
8541
+ });
8542
+
7993
8543
  // Find all elements with data-bw_id
7994
8544
  const elements = element.querySelectorAll('[data-bw_id]');
7995
8545
 
@@ -8005,6 +8555,10 @@
8005
8555
  // Deregister from node cache
8006
8556
  bw._deregisterNode(el, id);
8007
8557
 
8558
+ // Deregister UUID class from node cache
8559
+ var uuidMatch = el.className && el.className.match(_UUID_RE);
8560
+ if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
8561
+
8008
8562
  // Clean up pub/sub subscriptions tied to this element
8009
8563
  if (el._bw_subs) {
8010
8564
  el._bw_subs.forEach(function(unsub) { unsub(); });
@@ -8029,6 +8583,10 @@
8029
8583
  // Deregister from node cache
8030
8584
  bw._deregisterNode(element, id);
8031
8585
 
8586
+ // Deregister UUID class from node cache
8587
+ var elemUuidMatch = element.className && element.className.match(_UUID_RE);
8588
+ if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
8589
+
8032
8590
  // Clean up pub/sub subscriptions tied to element itself
8033
8591
  if (element._bw_subs) {
8034
8592
  element._bw_subs.forEach(function(unsub) { unsub(); });
@@ -8103,17 +8661,17 @@
8103
8661
  if (attr) {
8104
8662
  // Patch an attribute
8105
8663
  el.setAttribute(attr, String(content));
8106
- } else if (Array.isArray(content)) {
8664
+ } else if (_isA(content)) {
8107
8665
  // Patch with array of children (strings and/or TACOs)
8108
8666
  el.innerHTML = '';
8109
8667
  content.forEach(function(item) {
8110
- if (typeof item === 'string' || typeof item === 'number') {
8668
+ if (_is(item, 'string') || _is(item, 'number')) {
8111
8669
  el.appendChild(document.createTextNode(String(item)));
8112
8670
  } else if (item && item.t) {
8113
8671
  el.appendChild(bw.createDOM(item));
8114
8672
  }
8115
8673
  });
8116
- } else if (typeof content === 'object' && content !== null && content.t) {
8674
+ } else if (_is(content, 'object') && content.t) {
8117
8675
  // Patch with a TACO — replace children
8118
8676
  el.innerHTML = '';
8119
8677
  el.appendChild(bw.createDOM(content));
@@ -8144,7 +8702,7 @@
8144
8702
  bw.patchAll = function(patches) {
8145
8703
  var results = {};
8146
8704
  for (var id in patches) {
8147
- if (Object.prototype.hasOwnProperty.call(patches, id)) {
8705
+ if (_hop.call(patches, id)) {
8148
8706
  results[id] = bw.patch(id, patches[id]);
8149
8707
  }
8150
8708
  }
@@ -8241,7 +8799,7 @@
8241
8799
  snapshot[i].handler(detail);
8242
8800
  called++;
8243
8801
  } catch (err) {
8244
- console.warn('bw.pub: subscriber error on topic "' + topic + '":', err);
8802
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
8245
8803
  }
8246
8804
  }
8247
8805
  return called;
@@ -8337,8 +8895,8 @@
8337
8895
  * @see bw.funcGetDispatchStr
8338
8896
  */
8339
8897
  bw.funcRegister = function(fn, name) {
8340
- if (typeof fn !== 'function') return '';
8341
- var fnID = (typeof name === 'string' && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
8898
+ if (!_is(fn, 'function')) return '';
8899
+ var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
8342
8900
  bw._fnRegistry[fnID] = fn;
8343
8901
  return fnID;
8344
8902
  };
@@ -8357,7 +8915,7 @@
8357
8915
  bw.funcGetById = function(name, errFn) {
8358
8916
  name = String(name);
8359
8917
  if (name in bw._fnRegistry) return bw._fnRegistry[name];
8360
- return (typeof errFn === 'function') ? errFn : function() { console.warn('bw.funcGetById: unregistered fn "' + name + '"'); };
8918
+ return _is(errFn, 'function') ? errFn : function() { _cw('bw.funcGetById: unregistered fn "' + name + '"'); };
8361
8919
  };
8362
8920
 
8363
8921
  /**
@@ -8398,13 +8956,30 @@
8398
8956
  bw.funcGetRegistry = function() {
8399
8957
  var copy = {};
8400
8958
  for (var k in bw._fnRegistry) {
8401
- if (Object.prototype.hasOwnProperty.call(bw._fnRegistry, k)) {
8959
+ if (_hop.call(bw._fnRegistry, k)) {
8402
8960
  copy[k] = bw._fnRegistry[k];
8403
8961
  }
8404
8962
  }
8405
8963
  return copy;
8406
8964
  };
8407
8965
 
8966
+ /**
8967
+ * Minimal runtime shim for funcRegister dispatch in static HTML.
8968
+ * When embedded in a `<script>` tag, provides just enough infrastructure
8969
+ * for `bw.funcGetById()` calls to resolve. The actual function bodies
8970
+ * are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
8971
+ * @type {string}
8972
+ * @category Function Registry
8973
+ */
8974
+ bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' +
8975
+ 'if(!bw._fnRegistry)bw._fnRegistry={};' +
8976
+ 'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' +
8977
+ 'console.warn("bw: unregistered fn "+n)};};' +
8978
+ 'bw.funcRegister=function(fn,name){' +
8979
+ 'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' +
8980
+ 'bw._fnRegistry[id]=fn;return id;};' +
8981
+ 'window.bw=bw;})();';
8982
+
8408
8983
  // ===================================================================================
8409
8984
  // Template Binding Utilities
8410
8985
  // ===================================================================================
@@ -8432,7 +9007,10 @@
8432
9007
  var parts = path.split('.');
8433
9008
  var val = state;
8434
9009
  for (var i = 0; i < parts.length; i++) {
8435
- if (val == null) return '';
9010
+ if (val == null) {
9011
+ if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
9012
+ return '';
9013
+ }
8436
9014
  val = val[parts[i]];
8437
9015
  }
8438
9016
  return (val == null) ? '' : val;
@@ -8452,7 +9030,7 @@
8452
9030
  */
8453
9031
  bw._compiledExprs = {};
8454
9032
  bw._resolveTemplate = function(str, state, compile) {
8455
- if (typeof str !== 'string' || str.indexOf('${') < 0) return str;
9033
+ if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
8456
9034
  var bindings = bw._parseBindings(str);
8457
9035
  if (bindings.length === 0) return str;
8458
9036
 
@@ -8474,6 +9052,7 @@
8474
9052
  try {
8475
9053
  val = bw._compiledExprs[b.expr](state);
8476
9054
  } catch (e) {
9055
+ if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
8477
9056
  val = '';
8478
9057
  }
8479
9058
  } else {
@@ -8582,7 +9161,7 @@
8582
9161
  this._state = {};
8583
9162
  if (o.state) {
8584
9163
  for (var k in o.state) {
8585
- if (Object.prototype.hasOwnProperty.call(o.state, k)) {
9164
+ if (_hop.call(o.state, k)) {
8586
9165
  this._state[k] = o.state[k];
8587
9166
  }
8588
9167
  }
@@ -8591,7 +9170,7 @@
8591
9170
  this._actions = {};
8592
9171
  if (o.actions) {
8593
9172
  for (var k2 in o.actions) {
8594
- if (Object.prototype.hasOwnProperty.call(o.actions, k2)) {
9173
+ if (_hop.call(o.actions, k2)) {
8595
9174
  this._actions[k2] = o.actions[k2];
8596
9175
  }
8597
9176
  }
@@ -8601,7 +9180,7 @@
8601
9180
  if (o.methods) {
8602
9181
  var self = this;
8603
9182
  for (var k3 in o.methods) {
8604
- if (Object.prototype.hasOwnProperty.call(o.methods, k3)) {
9183
+ if (_hop.call(o.methods, k3)) {
8605
9184
  this._methods[k3] = o.methods[k3];
8606
9185
  (function(methodName, methodFn) {
8607
9186
  self[methodName] = function() {
@@ -8619,7 +9198,7 @@
8619
9198
  willMount: o.willMount || null,
8620
9199
  mounted: o.mounted || null,
8621
9200
  willUpdate: o.willUpdate || null,
8622
- onUpdate: o.onUpdate || null,
9201
+ onUpdate: o.onUpdate || o.updated || null,
8623
9202
  unmount: o.unmount || null,
8624
9203
  willDestroy: o.willDestroy || null
8625
9204
  };
@@ -8634,14 +9213,23 @@
8634
9213
  this._compile = !!o.compile;
8635
9214
  this._bw_refs = {};
8636
9215
  this._refCounter = 0;
9216
+ // Child component ownership (Bug #5)
9217
+ this._children = [];
9218
+ this._parent = null;
9219
+ // Factory metadata for BCCL rebuild (Bug #6)
9220
+ this._factory = taco._bwFactory || null;
8637
9221
  }
8638
9222
 
9223
+ // Short alias for ComponentHandle.prototype (see alias block at top of file).
9224
+ // 28 method definitions × 25 chars = ~700B raw savings in minified output.
9225
+ var _chp = ComponentHandle.prototype;
9226
+
8639
9227
  // ── State Methods ──
8640
9228
 
8641
9229
  /**
8642
9230
  * Get a state value. Dot-path supported: `get('user.name')`
8643
9231
  */
8644
- ComponentHandle.prototype.get = function(key) {
9232
+ _chp.get = function(key) {
8645
9233
  return bw._evaluatePath(this._state, key);
8646
9234
  };
8647
9235
 
@@ -8651,12 +9239,13 @@
8651
9239
  * @param {*} value - New value
8652
9240
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
8653
9241
  */
8654
- ComponentHandle.prototype.set = function(key, value, opts) {
9242
+ _chp.set = function(key, value, opts) {
8655
9243
  // Dot-path set
8656
9244
  var parts = key.split('.');
8657
9245
  var obj = this._state;
8658
9246
  for (var i = 0; i < parts.length - 1; i++) {
8659
- if (obj[parts[i]] == null || typeof obj[parts[i]] !== 'object') {
9247
+ if (!_is(obj[parts[i]], 'object')) {
9248
+ if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
8660
9249
  obj[parts[i]] = {};
8661
9250
  }
8662
9251
  obj = obj[parts[i]];
@@ -8676,10 +9265,10 @@
8676
9265
  /**
8677
9266
  * Get a shallow clone of the full state.
8678
9267
  */
8679
- ComponentHandle.prototype.getState = function() {
9268
+ _chp.getState = function() {
8680
9269
  var clone = {};
8681
9270
  for (var k in this._state) {
8682
- if (Object.prototype.hasOwnProperty.call(this._state, k)) {
9271
+ if (_hop.call(this._state, k)) {
8683
9272
  clone[k] = this._state[k];
8684
9273
  }
8685
9274
  }
@@ -8691,9 +9280,9 @@
8691
9280
  * @param {Object} updates - Key-value pairs to merge
8692
9281
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
8693
9282
  */
8694
- ComponentHandle.prototype.setState = function(updates, opts) {
9283
+ _chp.setState = function(updates, opts) {
8695
9284
  for (var k in updates) {
8696
- if (Object.prototype.hasOwnProperty.call(updates, k)) {
9285
+ if (_hop.call(updates, k)) {
8697
9286
  this._state[k] = updates[k];
8698
9287
  this._dirtyKeys[k] = true;
8699
9288
  }
@@ -8710,9 +9299,9 @@
8710
9299
  /**
8711
9300
  * Push a value onto an array in state. Clones the array.
8712
9301
  */
8713
- ComponentHandle.prototype.push = function(key, val) {
9302
+ _chp.push = function(key, val) {
8714
9303
  var arr = this.get(key);
8715
- var newArr = Array.isArray(arr) ? arr.slice() : [];
9304
+ var newArr = _isA(arr) ? arr.slice() : [];
8716
9305
  newArr.push(val);
8717
9306
  this.set(key, newArr);
8718
9307
  };
@@ -8720,9 +9309,9 @@
8720
9309
  /**
8721
9310
  * Splice an array in state. Clones the array.
8722
9311
  */
8723
- ComponentHandle.prototype.splice = function(key, start, deleteCount) {
9312
+ _chp.splice = function(key, start, deleteCount) {
8724
9313
  var arr = this.get(key);
8725
- var newArr = Array.isArray(arr) ? arr.slice() : [];
9314
+ var newArr = _isA(arr) ? arr.slice() : [];
8726
9315
  var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
8727
9316
  Array.prototype.splice.apply(newArr, args);
8728
9317
  this.set(key, newArr);
@@ -8730,7 +9319,7 @@
8730
9319
 
8731
9320
  // ── Scheduling ──
8732
9321
 
8733
- ComponentHandle.prototype._scheduleDirty = function() {
9322
+ _chp._scheduleDirty = function() {
8734
9323
  if (!this._scheduled) {
8735
9324
  this._scheduled = true;
8736
9325
  bw._dirtyComponents.push(this);
@@ -8745,17 +9334,17 @@
8745
9334
  * Creates binding descriptors with refIds for targeted DOM updates.
8746
9335
  * @private
8747
9336
  */
8748
- ComponentHandle.prototype._compileBindings = function() {
9337
+ _chp._compileBindings = function() {
8749
9338
  this._bindings = [];
8750
9339
  this._refCounter = 0;
8751
- var stateKeys = Object.keys(this._state);
9340
+ var stateKeys = _keys(this._state);
8752
9341
  var self = this;
8753
9342
 
8754
9343
  function walkTaco(taco, path) {
8755
- if (taco == null || typeof taco !== 'object' || !taco.t) return taco;
9344
+ if (!_is(taco, 'object') || !taco.t) return taco;
8756
9345
 
8757
9346
  // Check content for bindings
8758
- if (typeof taco.c === 'string' && taco.c.indexOf('${') >= 0) {
9347
+ if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
8759
9348
  var refId = 'bw_ref_' + self._refCounter++;
8760
9349
  var parsed = bw._parseBindings(taco.c);
8761
9350
  var deps = [];
@@ -8777,10 +9366,10 @@
8777
9366
  // Check attributes for bindings
8778
9367
  if (taco.a) {
8779
9368
  for (var attrName in taco.a) {
8780
- if (!Object.prototype.hasOwnProperty.call(taco.a, attrName)) continue;
9369
+ if (!_hop.call(taco.a, attrName)) continue;
8781
9370
  if (attrName === 'data-bw_ref') continue;
8782
9371
  var attrVal = taco.a[attrName];
8783
- if (typeof attrVal === 'string' && attrVal.indexOf('${') >= 0) {
9372
+ if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
8784
9373
  var refId2 = 'bw_ref_' + self._refCounter++;
8785
9374
  var parsed2 = bw._parseBindings(attrVal);
8786
9375
  var deps2 = [];
@@ -8806,9 +9395,27 @@
8806
9395
  }
8807
9396
 
8808
9397
  // Recurse into children
8809
- if (Array.isArray(taco.c)) {
9398
+ if (_isA(taco.c)) {
8810
9399
  for (var i = 0; i < taco.c.length; i++) {
8811
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
9400
+ // Wrap string children with ${expr} in a span so patches target the span, not the parent
9401
+ if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
9402
+ var mixedRefId = 'bw_ref_' + self._refCounter++;
9403
+ var mixedParsed = bw._parseBindings(taco.c[i]);
9404
+ var mixedDeps = [];
9405
+ for (var mi = 0; mi < mixedParsed.length; mi++) {
9406
+ mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
9407
+ }
9408
+ self._bindings.push({
9409
+ expr: taco.c[i],
9410
+ type: 'content',
9411
+ refId: mixedRefId,
9412
+ deps: mixedDeps,
9413
+ template: taco.c[i]
9414
+ });
9415
+ // Replace string with a span wrapper so textContent targets the span only
9416
+ taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
9417
+ }
9418
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
8812
9419
  walkTaco(taco.c[i], path.concat(i));
8813
9420
  }
8814
9421
  // Handle bw.when/bw.each markers
@@ -8843,7 +9450,7 @@
8843
9450
  taco.c[i]._refId = eachRefId;
8844
9451
  }
8845
9452
  }
8846
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9453
+ } else if (_is(taco.c, 'object') && taco.c.t) {
8847
9454
  walkTaco(taco.c, path.concat(0));
8848
9455
  }
8849
9456
 
@@ -8859,7 +9466,7 @@
8859
9466
  * Build ref map from the live DOM after createDOM.
8860
9467
  * @private
8861
9468
  */
8862
- ComponentHandle.prototype._collectRefs = function() {
9469
+ _chp._collectRefs = function() {
8863
9470
  this._bw_refs = {};
8864
9471
  if (!this.element) return;
8865
9472
  var els = this.element.querySelectorAll('[data-bw_ref]');
@@ -8880,7 +9487,7 @@
8880
9487
  * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
8881
9488
  * @param {Element} parentEl - DOM element to mount into
8882
9489
  */
8883
- ComponentHandle.prototype.mount = function(parentEl) {
9490
+ _chp.mount = function(parentEl) {
8884
9491
  // willMount hook
8885
9492
  if (this._hooks.willMount) this._hooks.willMount(this);
8886
9493
 
@@ -8902,7 +9509,7 @@
8902
9509
  // Register named actions in function registry
8903
9510
  var self = this;
8904
9511
  for (var actionName in this._actions) {
8905
- if (Object.prototype.hasOwnProperty.call(this._actions, actionName)) {
9512
+ if (_hop.call(this._actions, actionName)) {
8906
9513
  var registeredName = this._bwId + '_' + actionName;
8907
9514
  (function(aName) {
8908
9515
  bw.funcRegister(function(evt) {
@@ -8921,6 +9528,11 @@
8921
9528
  this.element = bw.createDOM(tacoForDOM);
8922
9529
  this.element._bwComponentHandle = this;
8923
9530
  this.element.setAttribute('data-bw_comp_id', this._bwId);
9531
+
9532
+ // Restore o.render from original TACO (stripped by _tacoForDOM)
9533
+ if (this.taco.o && this.taco.o.render) {
9534
+ this.element._bw_render = this.taco.o.render;
9535
+ }
8924
9536
  if (this._userTag) {
8925
9537
  this.element.classList.add(this._userTag);
8926
9538
  }
@@ -8936,6 +9548,16 @@
8936
9548
 
8937
9549
  this.mounted = true;
8938
9550
 
9551
+ // Scan for child ComponentHandles and link parent/child (Bug #5)
9552
+ var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
9553
+ for (var ci = 0; ci < childEls.length; ci++) {
9554
+ var ch = childEls[ci]._bwComponentHandle;
9555
+ if (ch && ch !== this && !ch._parent) {
9556
+ ch._parent = this;
9557
+ this._children.push(ch);
9558
+ }
9559
+ }
9560
+
8939
9561
  // mounted hook (backward compat: fn.length === 2 wraps (el, state))
8940
9562
  if (this._hooks.mounted) {
8941
9563
  if (this._hooks.mounted.length === 2) {
@@ -8944,16 +9566,21 @@
8944
9566
  this._hooks.mounted(this);
8945
9567
  }
8946
9568
  }
9569
+
9570
+ // Invoke o.render on initial mount (if present)
9571
+ if (this.element._bw_render) {
9572
+ this.element._bw_render(this.element, this._state);
9573
+ }
8947
9574
  };
8948
9575
 
8949
9576
  /**
8950
9577
  * Prepare TACO for initial render: resolve when/each markers.
8951
9578
  * @private
8952
9579
  */
8953
- ComponentHandle.prototype._prepareTaco = function(taco) {
8954
- if (!taco || typeof taco !== 'object') return;
9580
+ _chp._prepareTaco = function(taco) {
9581
+ if (!_is(taco, 'object')) return;
8955
9582
 
8956
- if (Array.isArray(taco.c)) {
9583
+ if (_isA(taco.c)) {
8957
9584
  for (var i = taco.c.length - 1; i >= 0; i--) {
8958
9585
  var child = taco.c[i];
8959
9586
  if (child && child._bwWhen) {
@@ -8978,18 +9605,18 @@
8978
9605
  var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
8979
9606
  var arr = bw._evaluatePath(this._state, eachExprStr);
8980
9607
  var items = [];
8981
- if (Array.isArray(arr)) {
9608
+ if (_isA(arr)) {
8982
9609
  for (var j = 0; j < arr.length; j++) {
8983
9610
  items.push(child.factory(arr[j], j));
8984
9611
  }
8985
9612
  }
8986
9613
  taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
8987
9614
  }
8988
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
9615
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
8989
9616
  this._prepareTaco(taco.c[i]);
8990
9617
  }
8991
9618
  }
8992
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9619
+ } else if (_is(taco.c, 'object') && taco.c.t) {
8993
9620
  this._prepareTaco(taco.c);
8994
9621
  }
8995
9622
  };
@@ -8998,12 +9625,12 @@
8998
9625
  * Wire action name strings (in onclick etc.) to dispatch function calls.
8999
9626
  * @private
9000
9627
  */
9001
- ComponentHandle.prototype._wireActions = function(taco) {
9002
- if (!taco || typeof taco !== 'object' || !taco.t) return;
9628
+ _chp._wireActions = function(taco) {
9629
+ if (!_is(taco, 'object') || !taco.t) return;
9003
9630
  if (taco.a) {
9004
9631
  for (var key in taco.a) {
9005
- if (!Object.prototype.hasOwnProperty.call(taco.a, key)) continue;
9006
- if (key.startsWith('on') && typeof taco.a[key] === 'string') {
9632
+ if (!_hop.call(taco.a, key)) continue;
9633
+ if (key.startsWith('on') && _is(taco.a[key], 'string')) {
9007
9634
  var actionName = taco.a[key];
9008
9635
  if (actionName in this._actions) {
9009
9636
  var registeredName = this._bwId + '_' + actionName;
@@ -9017,11 +9644,11 @@
9017
9644
  }
9018
9645
  }
9019
9646
  }
9020
- if (Array.isArray(taco.c)) {
9647
+ if (_isA(taco.c)) {
9021
9648
  for (var i = 0; i < taco.c.length; i++) {
9022
9649
  this._wireActions(taco.c[i]);
9023
9650
  }
9024
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9651
+ } else if (_is(taco.c, 'object') && taco.c.t) {
9025
9652
  this._wireActions(taco.c);
9026
9653
  }
9027
9654
  };
@@ -9030,7 +9657,7 @@
9030
9657
  * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
9031
9658
  * @private
9032
9659
  */
9033
- ComponentHandle.prototype._deepCloneTaco = function(taco) {
9660
+ _chp._deepCloneTaco = function(taco) {
9034
9661
  if (taco == null) return taco;
9035
9662
  // Preserve _bwWhen / _bwEach markers (contain functions)
9036
9663
  if (taco._bwWhen) {
@@ -9042,18 +9669,18 @@
9042
9669
  if (taco._bwEach) {
9043
9670
  return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
9044
9671
  }
9045
- if (typeof taco !== 'object' || !taco.t) return taco;
9672
+ if (!_is(taco, 'object') || !taco.t) return taco;
9046
9673
  var result = { t: taco.t };
9047
9674
  if (taco.a) {
9048
9675
  result.a = {};
9049
9676
  for (var k in taco.a) {
9050
- if (Object.prototype.hasOwnProperty.call(taco.a, k)) result.a[k] = taco.a[k];
9677
+ if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
9051
9678
  }
9052
9679
  }
9053
9680
  if (taco.c != null) {
9054
- if (Array.isArray(taco.c)) {
9681
+ if (_isA(taco.c)) {
9055
9682
  result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
9056
- } else if (typeof taco.c === 'object') {
9683
+ } else if (_is(taco.c, 'object')) {
9057
9684
  result.c = this._deepCloneTaco(taco.c);
9058
9685
  } else {
9059
9686
  result.c = taco.c;
@@ -9067,27 +9694,31 @@
9067
9694
  * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
9068
9695
  * @private
9069
9696
  */
9070
- ComponentHandle.prototype._tacoForDOM = function(taco) {
9071
- if (!taco || typeof taco !== 'object' || !taco.t) return taco;
9697
+ _chp._tacoForDOM = function(taco) {
9698
+ if (!_is(taco, 'object') || !taco.t) return taco;
9072
9699
  var result = { t: taco.t };
9073
9700
  if (taco.a) result.a = taco.a;
9074
9701
  if (taco.c != null) {
9075
- if (Array.isArray(taco.c)) {
9702
+ if (_isA(taco.c)) {
9076
9703
  result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
9077
- } else if (typeof taco.c === 'object' && taco.c.t) {
9704
+ } else if (_is(taco.c, 'object') && taco.c.t) {
9078
9705
  result.c = this._tacoForDOM(taco.c);
9079
9706
  } else {
9080
9707
  result.c = taco.c;
9081
9708
  }
9082
9709
  }
9083
9710
  // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
9711
+ if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
9712
+ _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
9713
+ '>. Use onclick attribute or bw.component() for child interactivity.');
9714
+ }
9084
9715
  return result;
9085
9716
  };
9086
9717
 
9087
9718
  /**
9088
9719
  * Unmount: remove from DOM, deactivate, preserve state for re-mount.
9089
9720
  */
9090
- ComponentHandle.prototype.unmount = function() {
9721
+ _chp.unmount = function() {
9091
9722
  if (!this.mounted) return;
9092
9723
 
9093
9724
  // unmount hook
@@ -9122,12 +9753,23 @@
9122
9753
  /**
9123
9754
  * Destroy: unmount + clear state + unregister actions.
9124
9755
  */
9125
- ComponentHandle.prototype.destroy = function() {
9756
+ _chp.destroy = function() {
9126
9757
  // willDestroy hook
9127
9758
  if (this._hooks.willDestroy) {
9128
9759
  this._hooks.willDestroy(this);
9129
9760
  }
9130
9761
 
9762
+ // Cascade destroy to children depth-first (Bug #5)
9763
+ for (var ci = this._children.length - 1; ci >= 0; ci--) {
9764
+ this._children[ci].destroy();
9765
+ }
9766
+ this._children = [];
9767
+ if (this._parent) {
9768
+ var idx = this._parent._children.indexOf(this);
9769
+ if (idx >= 0) this._parent._children.splice(idx, 1);
9770
+ this._parent = null;
9771
+ }
9772
+
9131
9773
  this.unmount();
9132
9774
 
9133
9775
  // Unregister actions from function registry
@@ -9154,12 +9796,36 @@
9154
9796
  * Flush dirty state: resolve changed bindings and apply to DOM.
9155
9797
  * @private
9156
9798
  */
9157
- ComponentHandle.prototype._flush = function() {
9799
+ _chp._flush = function() {
9158
9800
  this._scheduled = false;
9159
- var changedKeys = Object.keys(this._dirtyKeys);
9801
+ var changedKeys = _keys(this._dirtyKeys);
9160
9802
  this._dirtyKeys = {};
9161
9803
  if (changedKeys.length === 0 || !this.mounted) return;
9162
9804
 
9805
+ // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
9806
+ // rebuild the TACO from the factory with merged state (Bug #6)
9807
+ if (this._factory) {
9808
+ var rebuildNeeded = false;
9809
+ for (var fi = 0; fi < changedKeys.length; fi++) {
9810
+ if (_hop.call(this._factory.props, changedKeys[fi])) {
9811
+ rebuildNeeded = true; break;
9812
+ }
9813
+ }
9814
+ if (rebuildNeeded) {
9815
+ var merged = {};
9816
+ for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
9817
+ for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
9818
+ this._factory.props = merged;
9819
+ var newTaco = bw.make(this._factory.type, merged);
9820
+ newTaco._bwFactory = this._factory;
9821
+ this.taco = newTaco;
9822
+ this._originalTaco = this._deepCloneTaco(newTaco);
9823
+ this._render();
9824
+ if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
9825
+ return;
9826
+ }
9827
+ }
9828
+
9163
9829
  // willUpdate hook
9164
9830
  if (this._hooks.willUpdate) {
9165
9831
  this._hooks.willUpdate(this, changedKeys);
@@ -9198,7 +9864,7 @@
9198
9864
  * Returns list of patches to apply.
9199
9865
  * @private
9200
9866
  */
9201
- ComponentHandle.prototype._resolveBindings = function(changedKeys) {
9867
+ _chp._resolveBindings = function(changedKeys) {
9202
9868
  var patches = [];
9203
9869
  for (var i = 0; i < this._bindings.length; i++) {
9204
9870
  var b = this._bindings[i];
@@ -9234,11 +9900,14 @@
9234
9900
  * Apply patches to DOM.
9235
9901
  * @private
9236
9902
  */
9237
- ComponentHandle.prototype._applyPatches = function(patches) {
9903
+ _chp._applyPatches = function(patches) {
9238
9904
  for (var i = 0; i < patches.length; i++) {
9239
9905
  var p = patches[i];
9240
9906
  var el = this._bw_refs[p.refId];
9241
- if (!el) continue;
9907
+ if (!el) {
9908
+ if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
9909
+ continue;
9910
+ }
9242
9911
  if (p.type === 'content') {
9243
9912
  el.textContent = p.value;
9244
9913
  } else if (p.type === 'attribute') {
@@ -9255,7 +9924,7 @@
9255
9924
  * Resolve all bindings and apply (used for initial render).
9256
9925
  * @private
9257
9926
  */
9258
- ComponentHandle.prototype._resolveAndApplyAll = function() {
9927
+ _chp._resolveAndApplyAll = function() {
9259
9928
  var patches = [];
9260
9929
  for (var i = 0; i < this._bindings.length; i++) {
9261
9930
  var b = this._bindings[i];
@@ -9278,7 +9947,7 @@
9278
9947
  * Full re-render for structural changes (when/each branch switches).
9279
9948
  * @private
9280
9949
  */
9281
- ComponentHandle.prototype._render = function() {
9950
+ _chp._render = function() {
9282
9951
  if (!this.element || !this.element.parentNode) return;
9283
9952
  var parent = this.element.parentNode;
9284
9953
  var nextSibling = this.element.nextSibling;
@@ -9318,7 +9987,7 @@
9318
9987
  * @param {string} event - Event name (e.g., 'click')
9319
9988
  * @param {Function} handler - Event handler
9320
9989
  */
9321
- ComponentHandle.prototype.on = function(event, handler) {
9990
+ _chp.on = function(event, handler) {
9322
9991
  if (this.element) {
9323
9992
  this.element.addEventListener(event, handler);
9324
9993
  }
@@ -9330,7 +9999,7 @@
9330
9999
  * @param {string} event - Event name
9331
10000
  * @param {Function} handler - Handler to remove
9332
10001
  */
9333
- ComponentHandle.prototype.off = function(event, handler) {
10002
+ _chp.off = function(event, handler) {
9334
10003
  if (this.element) {
9335
10004
  this.element.removeEventListener(event, handler);
9336
10005
  }
@@ -9345,7 +10014,7 @@
9345
10014
  * @param {Function} handler - Handler function
9346
10015
  * @returns {Function} Unsubscribe function
9347
10016
  */
9348
- ComponentHandle.prototype.sub = function(topic, handler) {
10017
+ _chp.sub = function(topic, handler) {
9349
10018
  var unsub = bw.sub(topic, handler);
9350
10019
  this._subs.push(unsub);
9351
10020
  return unsub;
@@ -9356,10 +10025,10 @@
9356
10025
  * @param {string} name - Action name
9357
10026
  * @param {...*} args - Arguments passed after comp
9358
10027
  */
9359
- ComponentHandle.prototype.action = function(name) {
10028
+ _chp.action = function(name) {
9360
10029
  var fn = this._actions[name];
9361
10030
  if (!fn) {
9362
- console.warn('ComponentHandle.action: unknown action "' + name + '"');
10031
+ _cw('ComponentHandle.action: unknown action "' + name + '"');
9363
10032
  return;
9364
10033
  }
9365
10034
  var args = [this].concat(Array.prototype.slice.call(arguments, 1));
@@ -9371,7 +10040,7 @@
9371
10040
  * @param {string} sel - CSS selector
9372
10041
  * @returns {Element|null}
9373
10042
  */
9374
- ComponentHandle.prototype.select = function(sel) {
10043
+ _chp.select = function(sel) {
9375
10044
  return this.element ? this.element.querySelector(sel) : null;
9376
10045
  };
9377
10046
 
@@ -9380,7 +10049,7 @@
9380
10049
  * @param {string} sel - CSS selector
9381
10050
  * @returns {Element[]}
9382
10051
  */
9383
- ComponentHandle.prototype.selectAll = function(sel) {
10052
+ _chp.selectAll = function(sel) {
9384
10053
  if (!this.element) return [];
9385
10054
  return Array.prototype.slice.call(this.element.querySelectorAll(sel));
9386
10055
  };
@@ -9391,7 +10060,7 @@
9391
10060
  * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
9392
10061
  * @returns {ComponentHandle} this (for chaining)
9393
10062
  */
9394
- ComponentHandle.prototype.userTag = function(tag) {
10063
+ _chp.userTag = function(tag) {
9395
10064
  this._userTag = tag;
9396
10065
  if (this.element) {
9397
10066
  this.element.classList.add(tag);
@@ -9468,7 +10137,7 @@
9468
10137
  * and calls the named method. This is the bitwrench equivalent of
9469
10138
  * Win32 SendMessage(hwnd, msg, wParam, lParam).
9470
10139
  *
9471
- * @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
10140
+ * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
9472
10141
  * @param {string} action - Method name to call on the component
9473
10142
  * @param {*} data - Data to pass to the method
9474
10143
  * @returns {boolean} True if message was dispatched successfully
@@ -9485,15 +10154,20 @@
9485
10154
  * };
9486
10155
  */
9487
10156
  bw.message = function(target, action, data) {
9488
- // Try data-bw_comp_id attribute first, then CSS class (user tag)
9489
- var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
9490
- if (!el) {
10157
+ // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
10158
+ var el = bw._el(target);
10159
+ // Then try data-bw_comp_id attribute
10160
+ if (!el || !el._bwComponentHandle) {
10161
+ el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
10162
+ }
10163
+ // Then try CSS class (user tag)
10164
+ if (!el || !el._bwComponentHandle) {
9491
10165
  el = bw.$('.' + target)[0];
9492
10166
  }
9493
10167
  if (!el || !el._bwComponentHandle) return false;
9494
10168
  var comp = el._bwComponentHandle;
9495
- if (typeof comp[action] !== 'function') {
9496
- console.warn('bw.message: unknown action "' + action + '" on component ' + target);
10169
+ if (!_is(comp[action], 'function')) {
10170
+ _cw('bw.message: unknown action "' + action + '" on component ' + target);
9497
10171
  return false;
9498
10172
  }
9499
10173
  comp[action](data);
@@ -9501,59 +10175,24 @@
9501
10175
  };
9502
10176
 
9503
10177
  // ===================================================================================
9504
- // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
10178
+ // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
9505
10179
  // ===================================================================================
9506
10180
 
9507
10181
  /**
9508
10182
  * Registry of named functions sent via register messages.
9509
- * Populated by clientApply({ type: 'register', name, body }).
9510
- * Invoked by clientApply({ type: 'call', name, args }).
10183
+ * Populated by bw.apply({ type: 'register', name, body }).
10184
+ * Invoked by bw.apply({ type: 'call', name, args }).
9511
10185
  * @private
9512
10186
  */
9513
10187
  bw._clientFunctions = {};
9514
10188
 
9515
10189
  /**
9516
- * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
10190
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
9517
10191
  * Default false — exec messages are rejected unless explicitly opted in.
9518
10192
  * @private
9519
10193
  */
9520
10194
  bw._allowExec = false;
9521
10195
 
9522
- /**
9523
- * Built-in client functions available via call() without registration.
9524
- * @private
9525
- */
9526
- bw._builtinClientFunctions = {
9527
- scrollTo: function(selector) {
9528
- var el = bw._el(selector);
9529
- if (el) el.scrollTop = el.scrollHeight;
9530
- },
9531
- focus: function(selector) {
9532
- var el = bw._el(selector);
9533
- if (el && typeof el.focus === 'function') el.focus();
9534
- },
9535
- download: function(filename, content, mimeType) {
9536
- if (typeof document === 'undefined') return;
9537
- var blob = new Blob([content], { type: mimeType || 'text/plain' });
9538
- var a = document.createElement('a');
9539
- a.href = URL.createObjectURL(blob);
9540
- a.download = filename;
9541
- a.click();
9542
- URL.revokeObjectURL(a.href);
9543
- },
9544
- clipboard: function(text) {
9545
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
9546
- navigator.clipboard.writeText(text);
9547
- }
9548
- },
9549
- redirect: function(url) {
9550
- if (typeof window !== 'undefined') window.location.href = url;
9551
- },
9552
- log: function() {
9553
- console.log.apply(console, arguments);
9554
- }
9555
- };
9556
-
9557
10196
  /**
9558
10197
  * Parse a bwserve protocol message string, supporting both strict JSON
9559
10198
  * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
@@ -9568,9 +10207,9 @@
9568
10207
  * @param {string} str - JSON or r-prefixed relaxed JSON string
9569
10208
  * @returns {Object} Parsed message object
9570
10209
  * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
9571
- * @category Server
10210
+ * @category Core
9572
10211
  */
9573
- bw.clientParse = function(str) {
10212
+ bw.parseJSONFlex = function(str) {
9574
10213
  str = (str || '').trim();
9575
10214
  if (str.charAt(0) !== 'r') return JSON.parse(str);
9576
10215
  str = str.slice(1);
@@ -9655,10 +10294,10 @@
9655
10294
  * append — target.appendChild(bw.createDOM(node))
9656
10295
  * remove — bw.cleanup(target); target.remove()
9657
10296
  * patch — bw.patch(target, content, attr)
9658
- * batch — iterate ops, call clientApply for each
10297
+ * batch — iterate ops, call bw.apply for each
9659
10298
  * message — bw.message(target, action, data)
9660
10299
  * register — store a named function for later call()
9661
- * call — invoke a registered or built-in function
10300
+ * call — invoke a registered function
9662
10301
  * exec — execute arbitrary JS (requires allowExec)
9663
10302
  *
9664
10303
  * Target resolution:
@@ -9667,9 +10306,9 @@
9667
10306
  *
9668
10307
  * @param {Object} msg - Protocol message
9669
10308
  * @returns {boolean} true if the message was applied successfully
9670
- * @category Server
10309
+ * @category Core
9671
10310
  */
9672
- bw.clientApply = function(msg) {
10311
+ bw.apply = function(msg) {
9673
10312
  if (!msg || !msg.type) return false;
9674
10313
 
9675
10314
  var type = msg.type;
@@ -9695,15 +10334,15 @@
9695
10334
  } else if (type === 'remove') {
9696
10335
  var toRemove = bw._el(target);
9697
10336
  if (!toRemove) return false;
9698
- if (typeof bw.cleanup === 'function') bw.cleanup(toRemove);
10337
+ if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
9699
10338
  toRemove.remove();
9700
10339
  return true;
9701
10340
 
9702
10341
  } else if (type === 'batch') {
9703
- if (!Array.isArray(msg.ops)) return false;
10342
+ if (!_isA(msg.ops)) return false;
9704
10343
  var allOk = true;
9705
10344
  msg.ops.forEach(function(op) {
9706
- if (!bw.clientApply(op)) allOk = false;
10345
+ if (!bw.apply(op)) allOk = false;
9707
10346
  });
9708
10347
  return allOk;
9709
10348
 
@@ -9716,26 +10355,26 @@
9716
10355
  bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
9717
10356
  return true;
9718
10357
  } catch (e) {
9719
- console.error('[bw] register error:', msg.name, e);
10358
+ _ce('[bw] register error:', msg.name, e);
9720
10359
  return false;
9721
10360
  }
9722
10361
 
9723
10362
  } else if (type === 'call') {
9724
10363
  if (!msg.name) return false;
9725
- var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
9726
- if (typeof fn !== 'function') return false;
10364
+ var fn = bw._clientFunctions[msg.name];
10365
+ if (!_is(fn, 'function')) return false;
9727
10366
  try {
9728
- var args = Array.isArray(msg.args) ? msg.args : [];
10367
+ var args = _isA(msg.args) ? msg.args : [];
9729
10368
  fn.apply(null, args);
9730
10369
  return true;
9731
10370
  } catch (e) {
9732
- console.error('[bw] call error:', msg.name, e);
10371
+ _ce('[bw] call error:', msg.name, e);
9733
10372
  return false;
9734
10373
  }
9735
10374
 
9736
10375
  } else if (type === 'exec') {
9737
10376
  if (!bw._allowExec) {
9738
- console.warn('[bw] exec rejected: allowExec is not enabled');
10377
+ _cw('[bw] exec rejected: allowExec is not enabled');
9739
10378
  return false;
9740
10379
  }
9741
10380
  if (!msg.code) return false;
@@ -9743,7 +10382,7 @@
9743
10382
  new Function(msg.code)();
9744
10383
  return true;
9745
10384
  } catch (e) {
9746
- console.error('[bw] exec error:', e);
10385
+ _ce('[bw] exec error:', e);
9747
10386
  return false;
9748
10387
  }
9749
10388
  }
@@ -9751,139 +10390,6 @@
9751
10390
  return false;
9752
10391
  };
9753
10392
 
9754
- /**
9755
- * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
9756
- *
9757
- * Returns a connection object with sendAction(), on(), and close() methods.
9758
- *
9759
- * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
9760
- * @param {Object} [opts] - Connection options
9761
- * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
9762
- * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
9763
- * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
9764
- * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
9765
- * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
9766
- * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
9767
- * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
9768
- * @returns {Object} Connection object { sendAction, on, close, status }
9769
- * @category Server
9770
- */
9771
- bw.clientConnect = function(url, opts) {
9772
- opts = opts || {};
9773
- var transport = opts.transport || 'sse';
9774
- var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
9775
- var reconnect = opts.reconnect !== false;
9776
- var onStatus = opts.onStatus || function() {};
9777
- var onMessage = opts.onMessage || null;
9778
- var handlers = {};
9779
- // Set the global allowExec flag from connection options
9780
- bw._allowExec = !!opts.allowExec;
9781
- var conn = {
9782
- status: 'connecting',
9783
- _es: null,
9784
- _pollTimer: null
9785
- };
9786
-
9787
- function setStatus(s) {
9788
- conn.status = s;
9789
- onStatus(s);
9790
- }
9791
-
9792
- function handleMessage(data) {
9793
- try {
9794
- var msg = typeof data === 'string' ? bw.clientParse(data) : data;
9795
- if (onMessage) onMessage(msg);
9796
- if (handlers.message) handlers.message(msg);
9797
- bw.clientApply(msg);
9798
- } catch (e) {
9799
- if (handlers.error) handlers.error(e);
9800
- }
9801
- }
9802
-
9803
- if (transport === 'sse' && typeof EventSource !== 'undefined') {
9804
- setStatus('connecting');
9805
- var es = new EventSource(url);
9806
- conn._es = es;
9807
-
9808
- es.onopen = function() {
9809
- setStatus('connected');
9810
- if (handlers.open) handlers.open();
9811
- };
9812
-
9813
- es.onmessage = function(e) {
9814
- handleMessage(e.data);
9815
- };
9816
-
9817
- es.onerror = function() {
9818
- if (conn.status === 'connected') {
9819
- setStatus('disconnected');
9820
- }
9821
- if (handlers.error) handlers.error(new Error('SSE connection error'));
9822
- if (!reconnect) {
9823
- es.close();
9824
- }
9825
- // EventSource auto-reconnects by default when reconnect=true
9826
- };
9827
- } else if (transport === 'poll') {
9828
- var interval = opts.interval || 2000;
9829
- setStatus('connected');
9830
- conn._pollTimer = setInterval(function() {
9831
- fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
9832
- if (Array.isArray(msgs)) {
9833
- msgs.forEach(handleMessage);
9834
- } else if (msgs && msgs.type) {
9835
- handleMessage(msgs);
9836
- }
9837
- }).catch(function(e) {
9838
- if (handlers.error) handlers.error(e);
9839
- });
9840
- }, interval);
9841
- }
9842
-
9843
- /**
9844
- * Send an action to the server via POST.
9845
- * @param {string} action - Action name
9846
- * @param {Object} [data] - Action payload
9847
- */
9848
- conn.sendAction = function(action, data) {
9849
- var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
9850
- fetch(actionUrl, {
9851
- method: 'POST',
9852
- headers: { 'Content-Type': 'application/json' },
9853
- body: body
9854
- }).catch(function(e) {
9855
- if (handlers.error) handlers.error(e);
9856
- });
9857
- };
9858
-
9859
- /**
9860
- * Register an event handler.
9861
- * @param {string} event - 'open'|'message'|'error'|'close'
9862
- * @param {Function} handler
9863
- */
9864
- conn.on = function(event, handler) {
9865
- handlers[event] = handler;
9866
- return conn;
9867
- };
9868
-
9869
- /**
9870
- * Close the connection.
9871
- */
9872
- conn.close = function() {
9873
- if (conn._es) {
9874
- conn._es.close();
9875
- conn._es = null;
9876
- }
9877
- if (conn._pollTimer) {
9878
- clearInterval(conn._pollTimer);
9879
- conn._pollTimer = null;
9880
- }
9881
- setStatus('disconnected');
9882
- if (handlers.close) handlers.close();
9883
- };
9884
-
9885
- return conn;
9886
- };
9887
10393
 
9888
10394
  // ===================================================================================
9889
10395
  // bw.inspect() — Debug utility
@@ -9911,33 +10417,33 @@
9911
10417
  el = target.element;
9912
10418
  comp = target;
9913
10419
  } else {
9914
- if (typeof target === 'string') {
10420
+ if (_is(target, 'string')) {
9915
10421
  el = bw.$(target)[0];
9916
10422
  }
9917
10423
  if (!el) {
9918
- console.warn('bw.inspect: element not found');
10424
+ _cw('bw.inspect: element not found');
9919
10425
  return null;
9920
10426
  }
9921
10427
  comp = el._bwComponentHandle;
9922
10428
  }
9923
10429
  if (!comp) {
9924
- console.log('bw.inspect: no ComponentHandle on this element');
9925
- console.log(' Tag:', el.tagName);
9926
- console.log(' Classes:', el.className);
9927
- console.log(' _bw_state:', el._bw_state || '(none)');
10430
+ _cl('bw.inspect: no ComponentHandle on this element');
10431
+ _cl(' Tag:', el.tagName);
10432
+ _cl(' Classes:', el.className);
10433
+ _cl(' _bw_state:', el._bw_state || '(none)');
9928
10434
  return null;
9929
10435
  }
9930
10436
  var deps = comp._bindings.reduce(function(s, b) {
9931
10437
  return s.concat(b.deps || []);
9932
10438
  }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
9933
10439
  console.group('Component: ' + comp._bwId);
9934
- console.log('State:', comp._state);
9935
- console.log('Bindings:', comp._bindings.length, '(deps:', deps, ')');
9936
- console.log('Methods:', Object.keys(comp._methods));
9937
- console.log('Actions:', Object.keys(comp._actions));
9938
- console.log('User tag:', comp._userTag || '(none)');
9939
- console.log('Mounted:', comp.mounted);
9940
- console.log('Element:', comp.element);
10440
+ _cl('State:', comp._state);
10441
+ _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
10442
+ _cl('Methods:', _keys(comp._methods));
10443
+ _cl('Actions:', _keys(comp._actions));
10444
+ _cl('User tag:', comp._userTag || '(none)');
10445
+ _cl('Mounted:', comp.mounted);
10446
+ _cl('Element:', comp.element);
9941
10447
  console.groupEnd();
9942
10448
  return comp;
9943
10449
  };
@@ -9960,8 +10466,8 @@
9960
10466
  // Pre-extract all binding expressions
9961
10467
  var precompiled = [];
9962
10468
  function walkExpressions(node) {
9963
- if (!node || typeof node !== 'object') return;
9964
- if (typeof node.c === 'string' && node.c.indexOf('${') >= 0) {
10469
+ if (!_is(node, 'object')) return;
10470
+ if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
9965
10471
  var parsed = bw._parseBindings(node.c);
9966
10472
  for (var i = 0; i < parsed.length; i++) {
9967
10473
  try {
@@ -9976,9 +10482,9 @@
9976
10482
  }
9977
10483
  if (node.a) {
9978
10484
  for (var key in node.a) {
9979
- if (Object.prototype.hasOwnProperty.call(node.a, key)) {
10485
+ if (_hop.call(node.a, key)) {
9980
10486
  var v = node.a[key];
9981
- if (typeof v === 'string' && v.indexOf('${') >= 0) {
10487
+ if (_is(v, 'string') && v.indexOf('${') >= 0) {
9982
10488
  var parsed2 = bw._parseBindings(v);
9983
10489
  for (var j = 0; j < parsed2.length; j++) {
9984
10490
  try {
@@ -9994,9 +10500,9 @@
9994
10500
  }
9995
10501
  }
9996
10502
  }
9997
- if (Array.isArray(node.c)) {
10503
+ if (_isA(node.c)) {
9998
10504
  for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
9999
- } else if (node.c && typeof node.c === 'object' && node.c.t) {
10505
+ } else if (_is(node.c, 'object') && node.c.t) {
10000
10506
  walkExpressions(node.c);
10001
10507
  }
10002
10508
  }
@@ -10008,7 +10514,7 @@
10008
10514
  handle._precompiledBindings = precompiled;
10009
10515
  if (initialState) {
10010
10516
  for (var k in initialState) {
10011
- if (Object.prototype.hasOwnProperty.call(initialState, k)) {
10517
+ if (_hop.call(initialState, k)) {
10012
10518
  handle._state[k] = initialState[k];
10013
10519
  }
10014
10520
  }
@@ -10039,18 +10545,18 @@
10039
10545
  bw.css = function(rules, options = {}) {
10040
10546
  const { minify = false, pretty = !minify } = options;
10041
10547
 
10042
- if (typeof rules === 'string') return rules;
10548
+ if (_is(rules, 'string')) return rules;
10043
10549
 
10044
10550
  let css = '';
10045
10551
  const indent = pretty ? ' ' : '';
10046
10552
  const newline = pretty ? '\n' : '';
10047
10553
  const space = pretty ? ' ' : '';
10048
10554
 
10049
- if (Array.isArray(rules)) {
10555
+ if (_isA(rules)) {
10050
10556
  css = rules.map(rule => bw.css(rule, options)).join(newline);
10051
- } else if (typeof rules === 'object') {
10557
+ } else if (_is(rules, 'object')) {
10052
10558
  Object.entries(rules).forEach(([selector, styles]) => {
10053
- if (typeof styles === 'object' && !Array.isArray(styles)) {
10559
+ if (_is(styles, 'object')) {
10054
10560
  // Handle @media, @keyframes, @supports — recurse into nested block
10055
10561
  if (selector.charAt(0) === '@') {
10056
10562
  const inner = bw.css(styles, options);
@@ -10092,14 +10598,14 @@
10092
10598
  * @returns {Element} The style element
10093
10599
  * @category CSS & Styling
10094
10600
  * @see bw.css
10095
- * @see bw.loadDefaultStyles
10601
+ * @see bw.loadStyles
10096
10602
  * @example
10097
10603
  * bw.injectCSS('.my-class { color: red; }');
10098
10604
  * bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
10099
10605
  */
10100
10606
  bw.injectCSS = function(css, options = {}) {
10101
10607
  if (!bw._isBrowser) {
10102
- console.warn('bw.injectCSS requires a DOM environment');
10608
+ _cw('bw.injectCSS requires a DOM environment');
10103
10609
  return null;
10104
10610
  }
10105
10611
 
@@ -10116,7 +10622,7 @@
10116
10622
  }
10117
10623
 
10118
10624
  // Convert CSS if needed
10119
- const cssStr = typeof css === 'string' ? css : bw.css(css, options);
10625
+ const cssStr = _is(css, 'string') ? css : bw.css(css, options);
10120
10626
 
10121
10627
  // Set or append CSS
10122
10628
  if (append && styleEl.textContent) {
@@ -10137,113 +10643,19 @@
10137
10643
  * @param {...Object} styles - Style objects to merge (left-to-right)
10138
10644
  * @returns {Object} Merged style object
10139
10645
  * @category CSS & Styling
10140
- * @see bw.u
10141
10646
  * @example
10142
- * var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
10647
+ * var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
10143
10648
  * // => { display: 'flex', gap: '1rem', color: 'red' }
10144
10649
  */
10145
10650
  bw.s = function() {
10146
10651
  var result = {};
10147
10652
  for (var i = 0; i < arguments.length; i++) {
10148
10653
  var arg = arguments[i];
10149
- if (arg && typeof arg === 'object') Object.assign(result, arg);
10654
+ if (_is(arg, 'object')) Object.assign(result, arg);
10150
10655
  }
10151
10656
  return result;
10152
10657
  };
10153
10658
 
10154
- /**
10155
- * Pre-built CSS utility objects (like Tailwind utilities, but in JS).
10156
- *
10157
- * Compose with `bw.s()` to build inline styles without writing raw CSS strings.
10158
- * Includes flex, padding, margin, typography, color, border, and transition utilities.
10159
- *
10160
- * @category CSS & Styling
10161
- * @see bw.s
10162
- * @example
10163
- * { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
10164
- * c: 'Flexbox with 1rem gap and padding' }
10165
- */
10166
- bw.u = {
10167
- // Display
10168
- flex: { display: 'flex' },
10169
- flexCol: { display: 'flex', flexDirection: 'column' },
10170
- flexRow: { display: 'flex', flexDirection: 'row' },
10171
- flexWrap: { display: 'flex', flexWrap: 'wrap' },
10172
- block: { display: 'block' },
10173
- inline: { display: 'inline' },
10174
- hidden: { display: 'none' },
10175
-
10176
- // Flex alignment
10177
- justifyCenter: { justifyContent: 'center' },
10178
- justifyBetween: { justifyContent: 'space-between' },
10179
- justifyEnd: { justifyContent: 'flex-end' },
10180
- alignCenter: { alignItems: 'center' },
10181
- alignStart: { alignItems: 'flex-start' },
10182
- alignEnd: { alignItems: 'flex-end' },
10183
-
10184
- // Gap (0.25rem increments)
10185
- gap1: { gap: '0.25rem' },
10186
- gap2: { gap: '0.5rem' },
10187
- gap3: { gap: '0.75rem' },
10188
- gap4: { gap: '1rem' },
10189
- gap6: { gap: '1.5rem' },
10190
- gap8: { gap: '2rem' },
10191
-
10192
- // Padding
10193
- p0: { padding: '0' },
10194
- p1: { padding: '0.25rem' },
10195
- p2: { padding: '0.5rem' },
10196
- p3: { padding: '0.75rem' },
10197
- p4: { padding: '1rem' },
10198
- p6: { padding: '1.5rem' },
10199
- p8: { padding: '2rem' },
10200
- px4: { paddingLeft: '1rem', paddingRight: '1rem' },
10201
- py2: { paddingTop: '0.5rem', paddingBottom: '0.5rem' },
10202
- py4: { paddingTop: '1rem', paddingBottom: '1rem' },
10203
-
10204
- // Margin (same scale)
10205
- m0: { margin: '0' },
10206
- m4: { margin: '1rem' },
10207
- mt2: { marginTop: '0.5rem' },
10208
- mt4: { marginTop: '1rem' },
10209
- mb2: { marginBottom: '0.5rem' },
10210
- mb4: { marginBottom: '1rem' },
10211
- mx_auto: { marginLeft: 'auto', marginRight: 'auto' },
10212
-
10213
- // Typography
10214
- textSm: { fontSize: '0.875rem' },
10215
- textBase: { fontSize: '1rem' },
10216
- textLg: { fontSize: '1.125rem' },
10217
- textXl: { fontSize: '1.25rem' },
10218
- text2xl: { fontSize: '1.5rem' },
10219
- text3xl: { fontSize: '1.875rem' },
10220
- bold: { fontWeight: '700' },
10221
- semibold: { fontWeight: '600' },
10222
- italic: { fontStyle: 'italic' },
10223
- textCenter: { textAlign: 'center' },
10224
- textRight: { textAlign: 'right' },
10225
-
10226
- // Colors (from design tokens)
10227
- bgWhite: { background: '#ffffff' },
10228
- bgTeal: { background: '#006666', color: '#ffffff' },
10229
- textWhite: { color: '#ffffff' },
10230
- textTeal: { color: '#006666' },
10231
- textMuted: { color: '#888' },
10232
-
10233
- // Borders
10234
- rounded: { borderRadius: '0.375rem' },
10235
- roundedLg: { borderRadius: '0.5rem' },
10236
- roundedFull: { borderRadius: '9999px' },
10237
- border: { border: '1px solid #d8d8d8' },
10238
-
10239
- // Sizing
10240
- wFull: { width: '100%' },
10241
- hFull: { height: '100%' },
10242
-
10243
- // Transitions
10244
- transition: { transition: 'all 0.2s ease' }
10245
- };
10246
-
10247
10659
  /**
10248
10660
  * Generate responsive CSS with media query breakpoints.
10249
10661
  *
@@ -10269,7 +10681,7 @@
10269
10681
  bw.responsive = function(selector, breakpoints) {
10270
10682
  var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
10271
10683
  var parts = [];
10272
- Object.keys(breakpoints).forEach(function(key) {
10684
+ _keys(breakpoints).forEach(function(key) {
10273
10685
  var rules = {};
10274
10686
  if (key === 'base') {
10275
10687
  rules[selector] = breakpoints[key];
@@ -10341,18 +10753,18 @@
10341
10753
  if (!selector) return [];
10342
10754
 
10343
10755
  // Already an array
10344
- if (Array.isArray(selector)) return selector;
10756
+ if (_isA(selector)) return selector;
10345
10757
 
10346
10758
  // Single element
10347
10759
  if (selector.nodeType) return [selector];
10348
10760
 
10349
10761
  // NodeList or HTMLCollection
10350
- if (selector.length !== undefined && typeof selector !== 'string') {
10762
+ if (selector.length !== undefined && !_is(selector, 'string')) {
10351
10763
  return Array.from(selector);
10352
10764
  }
10353
10765
 
10354
10766
  // CSS selector string
10355
- if (typeof selector === 'string') {
10767
+ if (_is(selector, 'string')) {
10356
10768
  return Array.from(document.querySelectorAll(selector));
10357
10769
  }
10358
10770
 
@@ -10365,103 +10777,49 @@
10365
10777
  };
10366
10778
  }
10367
10779
 
10368
- /**
10369
- * Load the built-in Bootstrap-inspired default stylesheet.
10370
- *
10371
- * Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
10372
- * alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
10373
- * Returns null in Node.js (no DOM).
10374
- *
10375
- * @param {Object} [options] - Style loading options
10376
- * @param {boolean} [options.minify=true] - Minify the CSS output
10377
- * @returns {Element|null} Style element if in browser, null in Node.js
10378
- * @category CSS & Styling
10379
- * @see bw.setTheme
10380
- * @see bw.applyTheme
10381
- * @see bw.toggleTheme
10382
- * @example
10383
- * bw.loadDefaultStyles(); // inject all default CSS
10384
- */
10385
- bw.loadDefaultStyles = function(options = {}) {
10386
- const { minify = true, palette } = options;
10387
-
10388
- // 1. Inject structural CSS (layout, sizing — never changes with theme)
10389
- if (bw._isBrowser) {
10390
- var structuralCSS = bw.css(getStructuralStyles());
10391
- bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
10392
- }
10393
10780
 
10394
- // 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
10395
- var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
10396
- var result = bw.generateTheme('', Object.assign({}, paletteConfig, { inject: true }));
10397
- return result;
10398
- };
10781
+ // =========================================================================
10782
+ // v2.0.18 Clean Styles API makeStyles / applyStyles / loadStyles / etc.
10783
+ // =========================================================================
10399
10784
 
10785
+ /**
10786
+ * Convert a scope selector to a <style> element id.
10787
+ * @private
10788
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
10789
+ * @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
10790
+ */
10791
+ function _scopeToStyleId(scope) {
10792
+ if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
10793
+ if (scope === 'reset') return 'bw_style_reset';
10794
+ // Strip leading # or . and convert - to _
10795
+ var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
10796
+ return 'bw_style_' + clean;
10797
+ }
10400
10798
 
10401
10799
  /**
10402
- * Generate a complete, scoped theme from seed colors.
10800
+ * Generate a complete styles object from seed colors and layout config.
10801
+ * Pure function — no DOM, no state, no side effects.
10403
10802
  *
10404
- * Produces CSS for all themed components (buttons, alerts, badges, cards,
10405
- * forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
10406
- * scoped under `.name` class. Multiple themes can coexist in the stylesheet.
10407
- * Swap themes by changing the class on a container element.
10803
+ * All parameters are optional. Defaults to the bitwrench default palette.
10408
10804
  *
10409
- * @param {string} name - CSS scope class (e.g. 'ocean'). Empty string = unscoped global.
10410
- * @param {Object} config - Theme configuration
10411
- * @param {string} config.primary - Primary brand color hex
10412
- * @param {string} config.secondary - Secondary color hex
10413
- * @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
10414
- * @param {string} [config.success='#198754'] - Success color hex
10415
- * @param {string} [config.danger='#dc3545'] - Danger color hex
10416
- * @param {string} [config.warning='#ffc107'] - Warning color hex
10417
- * @param {string} [config.info='#0dcaf0'] - Info color hex
10418
- * @param {string} [config.light='#f8f9fa'] - Light color hex
10419
- * @param {string} [config.dark='#212529'] - Dark color hex
10420
- * @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
10421
- * @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
10805
+ * @param {Object} [config] - Style configuration
10806
+ * @param {string} [config.primary='#006666'] - Primary brand color hex
10807
+ * @param {string} [config.secondary='#6c757d'] - Secondary color hex
10808
+ * @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
10422
10809
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
10423
10810
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
10424
- * @param {number} [config.fontSize=1.0] - Base font size scale factor
10425
- * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
10426
- * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
10427
- * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
10428
- * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
10429
- * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
10430
- * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
10811
+ * @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
10431
10812
  * @category CSS & Styling
10432
- * @see bw.applyTheme
10433
- * @see bw.toggleTheme
10434
- * @see bw.loadDefaultStyles
10813
+ * @see bw.applyStyles
10814
+ * @see bw.loadStyles
10435
10815
  * @example
10436
- * // Generate and inject an ocean theme (primary + alternate)
10437
- * var theme = bw.generateTheme('ocean', {
10438
- * primary: '#0077b6',
10439
- * secondary: '#90e0ef',
10440
- * tertiary: '#00b4d8'
10441
- * });
10442
- *
10443
- * // Apply to a container
10444
- * document.getElementById('app').classList.add('ocean');
10445
- *
10446
- * // Toggle to alternate palette
10447
- * bw.toggleTheme();
10448
- *
10449
- * // Generate CSS for static export (Node.js)
10450
- * var result = bw.generateTheme('sunset', {
10451
- * primary: '#e76f51',
10452
- * secondary: '#264653',
10453
- * inject: false
10454
- * });
10455
- * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
10816
+ * var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
10817
+ * console.log(styles.palette.primary.base); // '#4f46e5'
10818
+ * // styles.css contains all themed CSS — nothing injected
10456
10819
  */
10457
- bw.generateTheme = function(name, config) {
10458
- if (!config || !config.primary || !config.secondary) {
10459
- throw new Error('bw.generateTheme requires config.primary and config.secondary');
10460
- }
10461
-
10462
- // Merge with defaults; if user didn't supply tertiary, default to their primary
10463
- var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
10464
- if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
10820
+ bw.makeStyles = function(config) {
10821
+ var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
10822
+ if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
10465
10823
 
10466
10824
  // Derive primary palette
10467
10825
  var palette = derivePalette(fullConfig);
@@ -10469,131 +10827,207 @@
10469
10827
  // Resolve layout
10470
10828
  var layout = resolveLayout(fullConfig);
10471
10829
 
10472
- // Generate primary themed CSS rules
10473
- var themedRules = generateThemedCSS(name, palette, layout);
10830
+ // Generate primary themed CSS rules (unscoped)
10831
+ var themedRules = generateThemedCSS('', palette, layout);
10474
10832
  var cssStr = bw.css(themedRules);
10475
10833
 
10476
10834
  // Derive alternate palette (luminance-inverted)
10477
10835
  var altConfig = deriveAlternateConfig(fullConfig);
10478
10836
  var altPalette = derivePalette(altConfig);
10479
10837
 
10480
- // Generate alternate CSS scoped under .bw_theme_alt
10481
- var altRules = generateAlternateCSS(name, altPalette, layout);
10482
- var altCssStr = bw.css(altRules);
10838
+ // Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
10839
+ // applyStyles() wraps them appropriately based on scope
10840
+ var altRawRules = generateThemedCSS('', altPalette, layout);
10841
+
10842
+ // Add body-level surface overrides for the alternate palette.
10843
+ // When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
10844
+ altRawRules['body'] = {
10845
+ 'color': altPalette.dark.base,
10846
+ 'background-color': altPalette.surface || altPalette.light.base
10847
+ };
10848
+
10849
+ var altCssStr = bw.css(altRawRules);
10483
10850
 
10484
10851
  // Determine if primary is light-flavored
10485
10852
  var lightPrimary = isLightPalette(fullConfig);
10486
10853
 
10487
- // Inject both CSS sets into DOM if requested
10488
- var shouldInject = config.inject !== false;
10489
- if (shouldInject && bw._isBrowser) {
10490
- var safeName = name ? name.replace(/-/g, '_') : '';
10491
- var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
10492
- var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
10493
-
10494
- bw.injectCSS(cssStr, { id: styleId, append: false });
10495
- bw.injectCSS(altCssStr, { id: altStyleId, append: false });
10854
+ return {
10855
+ css: cssStr,
10856
+ alternateCss: altCssStr,
10857
+ rules: themedRules,
10858
+ alternateRules: altRawRules,
10859
+ palette: palette,
10860
+ alternatePalette: altPalette,
10861
+ isLightPrimary: lightPrimary
10862
+ };
10863
+ };
10496
10864
 
10497
- bw._activeThemeStyleIds = [styleId, altStyleId];
10865
+ /**
10866
+ * Inject styles into the DOM with optional scoping.
10867
+ *
10868
+ * Takes a styles object from `makeStyles()` and creates a single `<style>`
10869
+ * element in `<head>`. If a scope selector is provided, all CSS rules are
10870
+ * wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
10871
+ *
10872
+ * @param {Object} styles - Result of `bw.makeStyles()`
10873
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
10874
+ * @returns {Element|null} The `<style>` element, or null in Node.js
10875
+ * @category CSS & Styling
10876
+ * @see bw.makeStyles
10877
+ * @see bw.loadStyles
10878
+ * @see bw.clearStyles
10879
+ * @example
10880
+ * var styles = bw.makeStyles({ primary: '#4f46e5' });
10881
+ * bw.applyStyles(styles); // global
10882
+ * bw.applyStyles(styles, '#my-dashboard'); // scoped
10883
+ */
10884
+ bw.applyStyles = function(styles, scope) {
10885
+ if (!bw._isBrowser) return null;
10886
+ if (!styles || !styles.rules) {
10887
+ _cw('bw.applyStyles: invalid styles object');
10888
+ return null;
10498
10889
  }
10499
10890
 
10500
- // Update bw.u color entries to reflect the palette
10501
- if (!name) {
10502
- bw.u.bgTeal = { background: palette.primary.base, color: palette.primary.textOn };
10503
- bw.u.textTeal = { color: palette.primary.base };
10504
- bw.u.bgWhite = { background: '#ffffff' };
10505
- bw.u.textWhite = { color: '#ffffff' };
10891
+ var styleId = _scopeToStyleId(scope);
10892
+
10893
+ // Scope the primary rules if a scope is provided
10894
+ var primaryRules = styles.rules;
10895
+ if (scope) {
10896
+ primaryRules = scopeRulesUnder(primaryRules, scope);
10506
10897
  }
10507
10898
 
10508
- // Store active theme state
10509
- var result = {
10510
- css: cssStr,
10511
- palette: palette,
10512
- name: name,
10513
- isLightPrimary: lightPrimary,
10514
- alternate: {
10515
- css: altCssStr,
10516
- palette: altPalette
10899
+ // Wrap alternate rules with .bw_theme_alt
10900
+ var altRules = styles.alternateRules;
10901
+ if (altRules) {
10902
+ if (scope) {
10903
+ // Scoped compound: #scope.bw_theme_alt .bw_card
10904
+ altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
10905
+ } else {
10906
+ // Global: .bw_theme_alt .bw_card
10907
+ altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
10517
10908
  }
10518
- };
10519
- bw._activeTheme = result;
10520
- bw._activeThemeMode = 'primary';
10909
+ }
10521
10910
 
10522
- return result;
10911
+ // Combine primary + alternate into one CSS string
10912
+ var combined = bw.css(primaryRules);
10913
+ if (altRules) {
10914
+ combined += '\n' + bw.css(altRules);
10915
+ }
10916
+
10917
+ return bw.injectCSS(combined, { id: styleId, append: false });
10523
10918
  };
10524
10919
 
10525
10920
  /**
10526
- * Apply a theme mode. Switches between primary and alternate palettes
10527
- * by adding/removing the `bw_theme_alt` class on `<html>`.
10921
+ * Generate and apply styles in one call. Convenience wrapper.
10922
+ *
10923
+ * Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
10528
10924
  *
10529
- * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
10530
- * @returns {string} Active mode: 'primary' or 'alternate'
10925
+ * @param {Object} [config] - Style configuration (same as `makeStyles`)
10926
+ * @param {string} [scope] - Scope selector (same as `applyStyles`)
10927
+ * @returns {Element|null} The `<style>` element, or null in Node.js
10531
10928
  * @category CSS & Styling
10532
- * @see bw.generateTheme
10533
- * @see bw.toggleTheme
10929
+ * @see bw.makeStyles
10930
+ * @see bw.applyStyles
10534
10931
  * @example
10535
- * bw.applyTheme('alternate'); // switch to alternate palette
10536
- * bw.applyTheme('dark'); // switch to whichever palette is darker
10537
- * bw.applyTheme('primary'); // switch back to primary palette
10538
- */
10539
- bw.applyTheme = function(mode) {
10540
- if (!bw._isBrowser) return mode || 'primary';
10541
- var root = document.documentElement;
10542
- var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
10543
-
10544
- var wantAlt;
10545
- if (mode === 'primary') wantAlt = false;
10546
- else if (mode === 'alternate') wantAlt = true;
10547
- else if (mode === 'light') wantAlt = !isLight;
10548
- else if (mode === 'dark') wantAlt = isLight;
10549
- else wantAlt = false;
10550
-
10551
- if (wantAlt) {
10552
- root.classList.add('bw_theme_alt');
10553
- } else {
10554
- root.classList.remove('bw_theme_alt');
10932
+ * bw.loadStyles(); // defaults, global
10933
+ * bw.loadStyles({ primary: '#4f46e5' }); // custom, global
10934
+ * bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
10935
+ */
10936
+ bw.loadStyles = function(config, scope) {
10937
+ // Also inject structural CSS first (only once)
10938
+ if (bw._isBrowser) {
10939
+ var existing = document.getElementById('bw_structural');
10940
+ if (!existing) {
10941
+ var structuralCSS = bw.css(getStructuralStyles());
10942
+ bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
10943
+ }
10555
10944
  }
10945
+ return bw.applyStyles(bw.makeStyles(config), scope);
10946
+ };
10556
10947
 
10557
- bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
10558
- return bw._activeThemeMode;
10948
+ /**
10949
+ * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
10950
+ * Idempotent — if already injected, returns the existing `<style>` element.
10951
+ *
10952
+ * @returns {Element|null} The `<style>` element, or null in Node.js
10953
+ * @category CSS & Styling
10954
+ * @see bw.loadStyles
10955
+ * @see bw.clearStyles
10956
+ * @example
10957
+ * bw.loadReset(); // inject once, safe to call multiple times
10958
+ */
10959
+ bw.loadReset = function() {
10960
+ if (!bw._isBrowser) return null;
10961
+ var existing = document.getElementById('bw_style_reset');
10962
+ if (existing) return existing;
10963
+ return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
10559
10964
  };
10560
10965
 
10561
10966
  /**
10562
- * Toggle between primary and alternate theme palettes.
10967
+ * Toggle between primary and alternate palettes.
10968
+ *
10969
+ * Adds/removes the `bw_theme_alt` class on the scoping element.
10970
+ * Without a scope, toggles on `<html>` (global).
10971
+ * With a scope, toggles on the first matching element.
10563
10972
  *
10973
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
10564
10974
  * @returns {string} Active mode after toggle: 'primary' or 'alternate'
10565
10975
  * @category CSS & Styling
10566
- * @see bw.applyTheme
10567
- * @see bw.generateTheme
10976
+ * @see bw.applyStyles
10977
+ * @see bw.clearStyles
10568
10978
  * @example
10569
- * bw.toggleTheme(); // flip between primary and alternate
10979
+ * bw.toggleStyles(); // global toggle on <html>
10980
+ * bw.toggleStyles('#my-dashboard'); // scoped toggle
10570
10981
  */
10571
- bw.toggleTheme = function() {
10572
- var current = bw._activeThemeMode || 'primary';
10573
- return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
10982
+ bw.toggleStyles = function(scope) {
10983
+ if (!bw._isBrowser) return 'primary';
10984
+ var target;
10985
+ if (scope) {
10986
+ var els = bw.$(scope);
10987
+ target = els[0];
10988
+ } else {
10989
+ target = document.documentElement;
10990
+ }
10991
+ if (!target) return 'primary';
10992
+
10993
+ var hasAlt = target.classList.contains('bw_theme_alt');
10994
+ if (hasAlt) {
10995
+ target.classList.remove('bw_theme_alt');
10996
+ return 'primary';
10997
+ } else {
10998
+ target.classList.add('bw_theme_alt');
10999
+ return 'alternate';
11000
+ }
10574
11001
  };
10575
11002
 
10576
11003
  /**
10577
- * Remove the currently active theme's injected style elements from the DOM.
10578
- * Use this before generating a new theme with a different name to prevent
10579
- * stale CSS accumulation.
11004
+ * Remove injected styles for a given scope.
11005
+ *
11006
+ * Finds the `<style>` element by id and removes it. Also removes
11007
+ * the `bw_theme_alt` class from the relevant element.
10580
11008
  *
11009
+ * @param {string} [scope] - Scope selector. Omit to remove global styles.
10581
11010
  * @category CSS & Styling
10582
- * @see bw.generateTheme
11011
+ * @see bw.applyStyles
11012
+ * @see bw.loadStyles
10583
11013
  * @example
10584
- * bw.clearTheme(); // remove current theme styles
10585
- * bw.generateTheme('sunset', conf); // inject fresh theme
10586
- */
10587
- bw.clearTheme = function() {
10588
- if (bw._activeThemeStyleIds && bw._isBrowser) {
10589
- bw._activeThemeStyleIds.forEach(function(id) {
10590
- var el = document.getElementById(id);
10591
- if (el) el.remove();
10592
- });
10593
- bw._activeThemeStyleIds = null;
11014
+ * bw.clearStyles(); // remove global styles
11015
+ * bw.clearStyles('#my-dashboard'); // remove scoped styles
11016
+ * bw.clearStyles('reset'); // remove the CSS reset
11017
+ */
11018
+ bw.clearStyles = function(scope) {
11019
+ if (!bw._isBrowser) return;
11020
+ var styleId = _scopeToStyleId(scope);
11021
+ var el = document.getElementById(styleId);
11022
+ if (el) el.remove();
11023
+
11024
+ // Also remove bw_theme_alt from the relevant element
11025
+ if (scope && scope !== 'reset' && scope !== 'global') {
11026
+ var targets = bw.$(scope);
11027
+ if (targets[0]) targets[0].classList.remove('bw_theme_alt');
11028
+ } else if (!scope || scope === 'global') {
11029
+ document.documentElement.classList.remove('bw_theme_alt');
10594
11030
  }
10595
- bw._activeTheme = null;
10596
- bw._activeThemeMode = 'primary';
10597
11031
  };
10598
11032
 
10599
11033
  // Expose color utility functions on bw namespace
@@ -10816,10 +11250,15 @@
10816
11250
  * @param {Object} config - Table configuration
10817
11251
  * @param {Array<Object>} config.data - Array of row objects to display
10818
11252
  * @param {Array<Object>} [config.columns] - Column definitions with key, label, render
10819
- * @param {string} [config.className='table'] - CSS class for table element
11253
+ * @param {string} [config.className=''] - Additional CSS classes for table element
10820
11254
  * @param {boolean} [config.sortable=true] - Enable click-to-sort headers
10821
11255
  * @param {Function} [config.onSort] - Sort callback (column, direction)
10822
- * @returns {Object} TACO object for table
11256
+ * @param {boolean} [config.selectable=false] - Enable row selection on click
11257
+ * @param {Function} [config.onRowClick] - Row click callback (row, index, event)
11258
+ * @param {number} [config.pageSize] - Rows per page (enables pagination when set)
11259
+ * @param {number} [config.currentPage=1] - Current page number (1-based)
11260
+ * @param {Function} [config.onPageChange] - Page change callback (newPage)
11261
+ * @returns {Object} TACO object for table (with optional pagination controls)
10823
11262
  * @category Component Builders
10824
11263
  * @see bw.makeDataTable
10825
11264
  * @example
@@ -10831,7 +11270,12 @@
10831
11270
  * columns: [
10832
11271
  * { key: 'name', label: 'Name' },
10833
11272
  * { key: 'age', label: 'Age' }
10834
- * ]
11273
+ * ],
11274
+ * selectable: true,
11275
+ * onRowClick: function(row, i) { console.log('clicked', row.name); },
11276
+ * pageSize: 10,
11277
+ * currentPage: 1,
11278
+ * onPageChange: function(page) { console.log('page', page); }
10835
11279
  * });
10836
11280
  */
10837
11281
  bw.makeTable = function(config) {
@@ -10844,41 +11288,47 @@
10844
11288
  sortable = true,
10845
11289
  onSort,
10846
11290
  sortColumn,
10847
- sortDirection = 'asc'
11291
+ sortDirection = 'asc',
11292
+ selectable = false,
11293
+ onRowClick,
11294
+ pageSize,
11295
+ currentPage = 1,
11296
+ onPageChange
10848
11297
  } = config;
10849
11298
 
10850
- // Build class list: always include bw_table, add striped/hover, append user className
11299
+ // Build class list: always include bw_table, add striped/hover/selectable, append user className
10851
11300
  let cls = 'bw_table';
10852
11301
  if (striped) cls += ' bw_table_striped';
10853
- if (hover) cls += ' bw_table_hover';
11302
+ if (hover || selectable) cls += ' bw_table_hover';
11303
+ if (selectable) cls += ' bw_table_selectable';
10854
11304
  if (className) cls += ' ' + className;
10855
11305
  cls = cls.trim();
10856
-
11306
+
10857
11307
  // Auto-detect columns if not provided
10858
- const cols = columns || (data.length > 0
10859
- ? Object.keys(data[0]).map(key => ({ key, label: key }))
11308
+ const cols = columns || (data.length > 0
11309
+ ? _keys(data[0]).map(key => ({ key, label: key }))
10860
11310
  : []);
10861
-
11311
+
10862
11312
  // Current sort state
10863
11313
  let currentSortColumn = sortColumn || null;
10864
11314
  let currentSortDirection = sortDirection;
10865
-
11315
+
10866
11316
  // Sort data if column specified
10867
11317
  let sortedData = [...data];
10868
11318
  if (currentSortColumn) {
10869
11319
  sortedData.sort((a, b) => {
10870
11320
  const aVal = a[currentSortColumn];
10871
11321
  const bVal = b[currentSortColumn];
10872
-
11322
+
10873
11323
  // Handle different types
10874
- if (typeof aVal === 'number' && typeof bVal === 'number') {
11324
+ if (_is(aVal, 'number') && _is(bVal, 'number')) {
10875
11325
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
10876
11326
  }
10877
-
11327
+
10878
11328
  // String comparison
10879
11329
  const aStr = String(aVal || '').toLowerCase();
10880
11330
  const bStr = String(bVal || '').toLowerCase();
10881
-
11331
+
10882
11332
  if (currentSortDirection === 'asc') {
10883
11333
  return aStr.localeCompare(bStr);
10884
11334
  } else {
@@ -10886,23 +11336,32 @@
10886
11336
  }
10887
11337
  });
10888
11338
  }
10889
-
11339
+
11340
+ // Pagination
11341
+ const totalRows = sortedData.length;
11342
+ const totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
11343
+ const page = Math.max(1, Math.min(currentPage, totalPages));
11344
+ if (pageSize) {
11345
+ const start = (page - 1) * pageSize;
11346
+ sortedData = sortedData.slice(start, start + pageSize);
11347
+ }
11348
+
10890
11349
  // Create sort handler
10891
11350
  const handleSort = (column) => {
10892
11351
  if (!sortable) return;
10893
-
11352
+
10894
11353
  if (currentSortColumn === column) {
10895
11354
  currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
10896
11355
  } else {
10897
11356
  currentSortColumn = column;
10898
11357
  currentSortDirection = 'asc';
10899
11358
  }
10900
-
11359
+
10901
11360
  if (onSort) {
10902
11361
  onSort(column, currentSortDirection);
10903
11362
  }
10904
11363
  };
10905
-
11364
+
10906
11365
  // Build table header
10907
11366
  const thead = {
10908
11367
  t: 'thead',
@@ -10925,24 +11384,87 @@
10925
11384
  }))
10926
11385
  }
10927
11386
  };
10928
-
10929
- // Build table body
11387
+
11388
+ // Build table body with selectable/onRowClick support
10930
11389
  const tbody = {
10931
11390
  t: 'tbody',
10932
- c: sortedData.map(row => ({
10933
- t: 'tr',
10934
- c: cols.map(col => ({
10935
- t: 'td',
10936
- c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
10937
- }))
10938
- }))
11391
+ c: sortedData.map((row, idx) => {
11392
+ const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
11393
+ const rowAttrs = {};
11394
+ if (selectable || onRowClick) {
11395
+ rowAttrs.style = 'cursor:pointer;';
11396
+ rowAttrs.onclick = function(e) {
11397
+ if (selectable) {
11398
+ // Toggle selected class on this row
11399
+ var tr = e.currentTarget;
11400
+ tr.classList.toggle('bw_table_row_selected');
11401
+ }
11402
+ if (onRowClick) {
11403
+ onRowClick(row, globalIdx, e);
11404
+ }
11405
+ };
11406
+ }
11407
+ return {
11408
+ t: 'tr',
11409
+ a: rowAttrs,
11410
+ c: cols.map(col => ({
11411
+ t: 'td',
11412
+ c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
11413
+ }))
11414
+ };
11415
+ })
10939
11416
  };
10940
-
10941
- return {
11417
+
11418
+ const table = {
10942
11419
  t: 'table',
10943
11420
  a: { class: cls },
10944
11421
  c: [thead, tbody]
10945
11422
  };
11423
+
11424
+ // If no pagination, return table directly
11425
+ if (!pageSize) return table;
11426
+
11427
+ // Build pagination controls
11428
+ const pageButtons = [];
11429
+ // Previous button
11430
+ pageButtons.push({
11431
+ t: 'button',
11432
+ a: {
11433
+ class: 'bw_btn bw_btn_sm',
11434
+ disabled: page <= 1 ? 'disabled' : undefined,
11435
+ onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
11436
+ },
11437
+ c: 'Prev'
11438
+ });
11439
+ // Page info
11440
+ pageButtons.push({
11441
+ t: 'span',
11442
+ a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
11443
+ c: 'Page ' + page + ' of ' + totalPages
11444
+ });
11445
+ // Next button
11446
+ pageButtons.push({
11447
+ t: 'button',
11448
+ a: {
11449
+ class: 'bw_btn bw_btn_sm',
11450
+ disabled: page >= totalPages ? 'disabled' : undefined,
11451
+ onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
11452
+ },
11453
+ c: 'Next'
11454
+ });
11455
+
11456
+ return {
11457
+ t: 'div',
11458
+ a: { class: 'bw_table_paginated' },
11459
+ c: [
11460
+ table,
11461
+ {
11462
+ t: 'div',
11463
+ a: { class: 'bw_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
11464
+ c: pageButtons
11465
+ }
11466
+ ]
11467
+ };
10946
11468
  };
10947
11469
 
10948
11470
  /**
@@ -10981,7 +11503,7 @@
10981
11503
  bw.makeTableFromArray = function(config) {
10982
11504
  const { data = [], headerRow = true, columns, ...rest } = config;
10983
11505
 
10984
- if (!Array.isArray(data) || data.length === 0) {
11506
+ if (!_isA(data) || data.length === 0) {
10985
11507
  return bw.makeTable({ data: [], columns: columns || [], ...rest });
10986
11508
  }
10987
11509
 
@@ -11063,7 +11585,7 @@
11063
11585
  className = ''
11064
11586
  } = config;
11065
11587
 
11066
- if (!Array.isArray(data) || data.length === 0) {
11588
+ if (!_isA(data) || data.length === 0) {
11067
11589
  return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
11068
11590
  }
11069
11591
 
@@ -11212,7 +11734,7 @@
11212
11734
  */
11213
11735
  bw.render = function(element, position, taco) {
11214
11736
  // Get target element
11215
- const targetEl = typeof element === 'string'
11737
+ const targetEl = _is(element, 'string')
11216
11738
  ? document.querySelector(element)
11217
11739
  : element;
11218
11740
 
@@ -11362,7 +11884,7 @@
11362
11884
  setContent(content) {
11363
11885
  this._taco.c = content;
11364
11886
  if (this.element) {
11365
- if (typeof content === 'string') {
11887
+ if (_is(content, 'string')) {
11366
11888
  this.element.textContent = content;
11367
11889
  } else {
11368
11890
  // Re-render for complex content