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,20 +1,21 @@
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
  'use strict';
3
3
 
4
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
4
5
  /**
5
6
  * Auto-generated version file from package.json
6
7
  * DO NOT EDIT DIRECTLY - Use npm run generate-version
7
8
  */
8
9
 
9
10
  const VERSION_INFO = {
10
- version: '2.0.16',
11
+ version: '2.0.18',
11
12
  name: 'bitwrench',
12
13
  description: 'A library for javascript UI functions.',
13
14
  license: 'BSD-2-Clause',
14
15
  homepage: 'https://deftio.github.com/bitwrench/pages',
15
16
  repository: 'git+https://github.com/deftio/bitwrench.git',
16
17
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
17
- buildDate: '2026-03-12T08:05:52.043Z'
18
+ buildDate: '2026-03-17T00:50:09.505Z'
18
19
  };
19
20
 
20
21
  /**
@@ -308,13 +309,18 @@ function harmonize(sourceHex, targetHex, amount) {
308
309
  */
309
310
  function deriveShades(hex) {
310
311
  var rgb = colorParse(hex);
312
+ // For light input colors (L > 75), mixing toward white produces invisible borders.
313
+ // Darken instead so borders remain visible against light backgrounds.
314
+ var borderColor = hexToHsl(hex)[2] > 75
315
+ ? adjustLightness(hex, -18)
316
+ : mixColor(hex, '#ffffff', 0.60);
311
317
  return {
312
318
  base: hex,
313
319
  hover: adjustLightness(hex, -10),
314
320
  active: adjustLightness(hex, -15),
315
321
  light: mixColor(hex, '#ffffff', 0.85),
316
322
  darkText: adjustLightness(hex, -40),
317
- border: mixColor(hex, '#ffffff', 0.60),
323
+ border: borderColor,
318
324
  focus: 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',0.25)',
319
325
  textOn: textOnColor(hex)
320
326
  };
@@ -373,19 +379,27 @@ function deriveAlternateConfig(config) {
373
379
  alt.secondary = deriveAlternateSeed(config.secondary);
374
380
  alt.tertiary = config.tertiary ? deriveAlternateSeed(config.tertiary) : alt.primary;
375
381
 
376
- // Derive alternate surface colors from primary hue
382
+ // Derive alternate surface colors from primary hue.
383
+ // Check actual page surface brightness (not seed color brightness) to decide
384
+ // whether alternate should be dark or light. The page surface is what the
385
+ // user sees; seeds can be dark while the page is still light (default L=96).
377
386
  var priHsl = hexToHsl(config.primary);
378
387
  var h = priHsl[0];
379
- var isLight = isLightPalette(config);
388
+ var primarySurface = config.surface || hslToHex([h, 8, 96]);
389
+ var isLight = relativeLuminance(primarySurface) > 0.179;
380
390
 
381
391
  if (isLight) {
382
- // Primary is light → alternate needs dark surfaces
392
+ // Page surface is light → alternate needs dark surfaces
383
393
  alt.light = hslToHex([h, Math.min(priHsl[1], 15), 15]);
384
394
  alt.dark = hslToHex([h, 5, 88]);
395
+ alt.surface = hslToHex([h, 12, 18]);
396
+ alt.background = hslToHex([h, 10, 14]);
385
397
  } else {
386
- // Primary is dark → alternate needs light surfaces
398
+ // Page surface is dark → alternate needs light surfaces
387
399
  alt.light = hslToHex([h, Math.min(priHsl[1], 10), 96]);
388
400
  alt.dark = hslToHex([h, 10, 18]);
401
+ alt.surface = hslToHex([h, 8, 96]);
402
+ alt.background = hslToHex([h, 6, 98]);
389
403
  }
390
404
 
391
405
  // Semantic colors: harmonize toward primary, then invert for alternate
@@ -433,10 +447,18 @@ function derivePalette(config) {
433
447
  var darkBase = config.dark || hslToHex([h, 10, 13]);
434
448
 
435
449
  // Background & surface tokens — tinted with primary hue for theme personality.
436
- // Very subtle: bg at L=98/S=6, surface at L=96/S=8.
450
+ // Saturation high enough that the hue is visible (each theme feels distinct)
451
+ // but low enough to stay neutral and readable.
437
452
  // User can override with config.background / config.surface.
438
- var bgBase = config.background || hslToHex([h, 6, 98]);
439
- var surfBase = config.surface || hslToHex([h, 8, 96]);
453
+ var bgBase = config.background || hslToHex([h, 22, 96]);
454
+ var surfBase = config.surface || hslToHex([h, 25, 94]);
455
+
456
+ // surfaceAlt: subtle background variant for striped rows, hover states, headers.
457
+ // Slightly lighter than surface in dark mode, slightly darker in light mode.
458
+ var surfHsl = hexToHsl(surfBase);
459
+ var surfAlt = surfHsl[2] <= 50
460
+ ? hslToHex([surfHsl[0], surfHsl[1], Math.min(surfHsl[2] + 8, 100)])
461
+ : hslToHex([surfHsl[0], surfHsl[1], Math.max(surfHsl[2] - 3, 0)]);
440
462
 
441
463
  var palette = {
442
464
  primary: deriveShades(config.primary),
@@ -449,7 +471,8 @@ function derivePalette(config) {
449
471
  light: deriveShades(lightBase),
450
472
  dark: deriveShades(darkBase),
451
473
  background: bgBase,
452
- surface: surfBase
474
+ surface: surfBase,
475
+ surfaceAlt: surfAlt
453
476
  };
454
477
 
455
478
  return palette;
@@ -496,10 +519,12 @@ var SPACING_SCALE = {
496
519
  5: '1.5rem', // 24px
497
520
  6: '2rem'};
498
521
 
522
+ let _S=SPACING_SCALE;
523
+
499
524
  var SPACING_PRESETS = {
500
- 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] },
501
- 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] },
502
- 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] }
525
+ 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] },
526
+ 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] },
527
+ 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] }
503
528
  };
504
529
 
505
530
  var RADIUS_PRESETS = {
@@ -611,20 +636,14 @@ var DEFAULT_PALETTE_CONFIG = {
611
636
  * Built-in theme presets — named color combinations
612
637
  * Each preset provides primary, secondary, and tertiary seed colors.
613
638
  */
614
- var THEME_PRESETS = {
615
- teal: { primary: '#006666', secondary: '#6c757d', tertiary: '#006666' },
616
- ocean: { primary: '#0077b6', secondary: '#90e0ef', tertiary: '#00b4d8' },
617
- sunset: { primary: '#e76f51', secondary: '#264653', tertiary: '#e9c46a' },
618
- forest: { primary: '#2d6a4f', secondary: '#95d5b2', tertiary: '#52b788' },
619
- slate: { primary: '#343a40', secondary: '#adb5bd', tertiary: '#6c757d' },
620
- rose: { primary: '#e11d48', secondary: '#fda4af', tertiary: '#fb7185' },
621
- indigo: { primary: '#4f46e5', secondary: '#a5b4fc', tertiary: '#818cf8' },
622
- amber: { primary: '#d97706', secondary: '#fbbf24', tertiary: '#f59e0b' },
623
- emerald: { primary: '#059669', secondary: '#6ee7b7', tertiary: '#34d399' },
624
- nord: { primary: '#5e81ac', secondary: '#88c0d0', tertiary: '#81a1c1' },
625
- coral: { primary: '#ef6461', secondary: '#4a7c7e', tertiary: '#e8a87c' },
626
- midnight: { primary: '#1e3a5f', secondary: '#7c8db5', tertiary: '#3d5a80' }
627
- };
639
+ var THEME_PRESETS = Object.fromEntries([
640
+ ['teal','#006666','#6c757d','#006666'],['ocean','#0077b6','#90e0ef','#00b4d8'],
641
+ ['sunset','#e76f51','#264653','#e9c46a'],['forest','#2d6a4f','#95d5b2','#52b788'],
642
+ ['slate','#343a40','#adb5bd','#6c757d'],['rose','#e11d48','#fda4af','#fb7185'],
643
+ ['indigo','#4f46e5','#a5b4fc','#818cf8'],['amber','#d97706','#fbbf24','#f59e0b'],
644
+ ['emerald','#059669','#6ee7b7','#34d399'],['nord','#5e81ac','#88c0d0','#81a1c1'],
645
+ ['coral','#ef6461','#4a7c7e','#e8a87c'],['midnight','#1e3a5f','#7c8db5','#3d5a80']
646
+ ].map(function(e) { return [e[0], {primary:e[1],secondary:e[2],tertiary:e[3]}]; }));
628
647
 
629
648
  /**
630
649
  * Resolve layout config to spacing, radius, typeScale, elevation, and motion objects.
@@ -671,6 +690,7 @@ function scopeSelector(name, sel) {
671
690
  if (sel.includes(',')) return sel.split(',').map(function(s) { return '.' + name + ' ' + s.trim(); }).join(', ');
672
691
  return '.' + name + ' ' + sel;
673
692
  }
693
+ var _sx=scopeSelector;
674
694
 
675
695
  // =========================================================================
676
696
  // Themed CSS generators
@@ -679,12 +699,12 @@ function scopeSelector(name, sel) {
679
699
  function generateTypographyThemed(scope, palette, layout) {
680
700
  var mot = layout.motion;
681
701
  var rules = {};
682
- rules[scopeSelector(scope, 'a')] = {
702
+ rules[_sx(scope, 'a')] = {
683
703
  'color': palette.primary.base,
684
704
  'text-decoration': 'none',
685
705
  'transition': 'color ' + mot.fast + ' ' + mot.easing
686
706
  };
687
- rules[scopeSelector(scope, 'a:hover')] = {
707
+ rules[_sx(scope, 'a:hover')] = {
688
708
  'color': palette.primary.hover,
689
709
  'text-decoration': 'underline'
690
710
  };
@@ -697,11 +717,11 @@ function generateButtons(scope, palette, layout) {
697
717
  var rd = layout.radius;
698
718
 
699
719
  // Base button (only when scoped — unscoped uses defaultStyles)
700
- rules[scopeSelector(scope, '.bw_btn')] = {
720
+ rules[_sx(scope, '.bw_btn')] = {
701
721
  'padding': sp.btn,
702
722
  'border-radius': rd.btn
703
723
  };
704
- rules[scopeSelector(scope, '.bw_btn:focus-visible')] = {
724
+ rules[_sx(scope, '.bw_btn:focus-visible')] = {
705
725
  'outline': '2px solid currentColor',
706
726
  'outline-offset': '2px',
707
727
  'box-shadow': '0 0 0 3px ' + palette.primary.focus
@@ -710,12 +730,12 @@ function generateButtons(scope, palette, layout) {
710
730
  // Variant colors handled by palette class on component root
711
731
 
712
732
  // Size variants (structural, reuse layout radius)
713
- rules[scopeSelector(scope, '.bw_btn_lg')] = {
733
+ rules[_sx(scope, '.bw_btn_lg')] = {
714
734
  'padding': '0.625rem 1.5rem',
715
735
  'font-size': '1rem',
716
736
  'border-radius': rd.btn === '50rem' ? '50rem' : (parseInt(rd.btn) + 2) + 'px'
717
737
  };
718
- rules[scopeSelector(scope, '.bw_btn_sm')] = {
738
+ rules[_sx(scope, '.bw_btn_sm')] = {
719
739
  'padding': '0.25rem 0.75rem',
720
740
  'font-size': '0.8125rem',
721
741
  'border-radius': rd.btn === '50rem' ? '50rem' : (Math.max(parseInt(rd.btn) - 1, 0)) + 'px'
@@ -729,7 +749,7 @@ function generateAlerts(scope, palette, layout) {
729
749
  var sp = layout.spacing;
730
750
  var rd = layout.radius;
731
751
 
732
- rules[scopeSelector(scope, '.bw_alert')] = {
752
+ rules[_sx(scope, '.bw_alert')] = {
733
753
  'padding': sp.alert,
734
754
  'border-radius': rd.alert
735
755
  };
@@ -748,36 +768,36 @@ function generateCards(scope, palette, layout) {
748
768
 
749
769
  var elev = layout.elevation;
750
770
  var motion = layout.motion;
751
- rules[scopeSelector(scope, '.bw_card')] = {
771
+ rules[_sx(scope, '.bw_card')] = {
752
772
  'background-color': palette.surface || '#fff',
753
773
  'border': '1px solid ' + palette.light.border,
754
774
  'border-radius': rd.card,
755
775
  'box-shadow': elev.sm,
756
776
  'transition': 'box-shadow ' + motion.normal + ' ' + motion.easing + ', transform ' + motion.normal + ' ' + motion.easing
757
777
  };
758
- rules[scopeSelector(scope, '.bw_card:hover')] = {
778
+ rules[_sx(scope, '.bw_card:hover')] = {
759
779
  'box-shadow': elev.md
760
780
  };
761
- rules[scopeSelector(scope, '.bw_card_hoverable:hover')] = {
781
+ rules[_sx(scope, '.bw_card_hoverable:hover')] = {
762
782
  'box-shadow': elev.lg
763
783
  };
764
- rules[scopeSelector(scope, '.bw_card_body')] = {
784
+ rules[_sx(scope, '.bw_card_body')] = {
765
785
  'padding': sp.card
766
786
  };
767
- rules[scopeSelector(scope, '.bw_card_header')] = {
787
+ rules[_sx(scope, '.bw_card_header')] = {
768
788
  'padding': sp.card.split(' ').map(function(v) { return (parseFloat(v) * 0.7).toFixed(3).replace(/\.?0+$/, '') + 'rem'; }).join(' '),
769
- 'background-color': palette.light.light,
789
+ 'background-color': palette.surfaceAlt,
770
790
  'border-bottom': '1px solid ' + palette.light.border
771
791
  };
772
- rules[scopeSelector(scope, '.bw_card_footer')] = {
773
- 'background-color': palette.light.light,
792
+ rules[_sx(scope, '.bw_card_footer')] = {
793
+ 'background-color': palette.surfaceAlt,
774
794
  'border-top': '1px solid ' + palette.light.border,
775
795
  'color': palette.secondary.base
776
796
  };
777
- rules[scopeSelector(scope, '.bw_card_title')] = {
797
+ rules[_sx(scope, '.bw_card_title')] = {
778
798
  'color': palette.dark.base
779
799
  };
780
- rules[scopeSelector(scope, '.bw_card_subtitle')] = {
800
+ rules[_sx(scope, '.bw_card_subtitle')] = {
781
801
  'color': palette.secondary.base
782
802
  };
783
803
 
@@ -791,55 +811,55 @@ function generateForms(scope, palette, layout) {
791
811
  var sp = layout.spacing;
792
812
  var rd = layout.radius;
793
813
 
794
- rules[scopeSelector(scope, '.bw_form_control')] = {
814
+ rules[_sx(scope, '.bw_form_control')] = {
795
815
  'padding': sp.input,
796
816
  'border-radius': rd.input,
797
817
  'color': palette.dark.base,
798
818
  'background-color': palette.surface || '#fff',
799
819
  'border-color': palette.light.border
800
820
  };
801
- rules[scopeSelector(scope, '.bw_form_control:focus')] = {
821
+ rules[_sx(scope, '.bw_form_control:focus')] = {
802
822
  'border-color': palette.primary.border,
803
823
  'outline': '2px solid ' + palette.primary.base,
804
824
  'outline-offset': '-1px',
805
825
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
806
826
  };
807
- rules[scopeSelector(scope, '.bw_form_control::placeholder')] = {
827
+ rules[_sx(scope, '.bw_form_control::placeholder')] = {
808
828
  'color': palette.secondary.base
809
829
  };
810
- rules[scopeSelector(scope, '.bw_form_label')] = {
830
+ rules[_sx(scope, '.bw_form_label')] = {
811
831
  'color': palette.dark.base
812
832
  };
813
- rules[scopeSelector(scope, '.bw_form_text')] = {
833
+ rules[_sx(scope, '.bw_form_text')] = {
814
834
  'color': palette.secondary.base
815
835
  };
816
- rules[scopeSelector(scope, '.bw_form_check_input:checked')] = {
836
+ rules[_sx(scope, '.bw_form_check_input:checked')] = {
817
837
  'background-color': palette.primary.base,
818
838
  'border-color': palette.primary.base
819
839
  };
820
- rules[scopeSelector(scope, '.bw_form_check_input:focus')] = {
840
+ rules[_sx(scope, '.bw_form_check_input:focus')] = {
821
841
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
822
842
  };
823
843
  // Validation states
824
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid')] = { 'border-color': palette.success.base };
825
- rules[scopeSelector(scope, '.bw_form_control.bw_is_valid:focus')] = {
844
+ rules[_sx(scope, '.bw_form_control.bw_is_valid')] = { 'border-color': palette.success.base };
845
+ rules[_sx(scope, '.bw_form_control.bw_is_valid:focus')] = {
826
846
  'border-color': palette.success.base,
827
847
  'box-shadow': '0 0 0 0.2rem ' + palette.success.focus
828
848
  };
829
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid')] = { 'border-color': palette.danger.base };
830
- rules[scopeSelector(scope, '.bw_form_control.bw_is_invalid:focus')] = {
849
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid')] = { 'border-color': palette.danger.base };
850
+ rules[_sx(scope, '.bw_form_control.bw_is_invalid:focus')] = {
831
851
  'border-color': palette.danger.base,
832
852
  'box-shadow': '0 0 0 0.2rem ' + palette.danger.focus
833
853
  };
834
854
  // Form select
835
- rules[scopeSelector(scope, '.bw_form_select')] = {
855
+ rules[_sx(scope, '.bw_form_select')] = {
836
856
  'padding': sp.input,
837
857
  'border-radius': rd.input,
838
858
  'color': palette.dark.base,
839
859
  'background-color': palette.surface || '#fff',
840
860
  'border-color': palette.light.border
841
861
  };
842
- rules[scopeSelector(scope, '.bw_form_select:focus')] = {
862
+ rules[_sx(scope, '.bw_form_select:focus')] = {
843
863
  'border-color': palette.primary.border,
844
864
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
845
865
  };
@@ -847,43 +867,46 @@ function generateForms(scope, palette, layout) {
847
867
  return rules;
848
868
  }
849
869
 
850
- function generateNavigation(scope, palette) {
870
+ function generateNavigation(scope, palette, layout) {
851
871
  var rules = {};
852
- rules[scopeSelector(scope, '.bw_navbar')] = {
853
- 'background-color': palette.light.light,
872
+ rules[_sx(scope, '.bw_navbar')] = {
873
+ 'background-color': palette.surfaceAlt,
854
874
  'border-bottom-color': palette.light.border
855
875
  };
856
- rules[scopeSelector(scope, '.bw_navbar_brand')] = {
876
+ rules[_sx(scope, '.bw_navbar_brand')] = {
857
877
  'color': palette.dark.base
858
878
  };
859
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link')] = {
860
- 'color': palette.secondary.base
879
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link')] = {
880
+ 'color': palette.secondary.base,
881
+ 'border-radius': layout.radius.btn
861
882
  };
862
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
863
- 'color': palette.dark.base
883
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
884
+ 'color': palette.dark.base,
885
+ 'background-color': palette.surfaceAlt
864
886
  };
865
- rules[scopeSelector(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
887
+ rules[_sx(scope, '.bw_navbar_nav .bw_nav_link.active')] = {
866
888
  'color': palette.primary.base,
867
- 'background-color': palette.primary.focus
889
+ 'background-color': palette.primary.focus,
890
+ 'font-weight': '600'
868
891
  };
869
- rules[scopeSelector(scope, '.bw_navbar_dark')] = {
892
+ rules[_sx(scope, '.bw_navbar_dark')] = {
870
893
  'background-color': palette.dark.base,
871
894
  'border-bottom-color': palette.dark.hover
872
895
  };
873
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
896
+ rules[_sx(scope, '.bw_navbar_dark .bw_navbar_brand')] = {
874
897
  'color': palette.light.base
875
898
  };
876
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link')] = {
877
- 'color': 'rgba(255,255,255,.65)'
899
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link')] = {
900
+ 'color': palette.light.border
878
901
  };
879
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
880
- 'color': '#fff'
902
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link:hover')] = {
903
+ 'color': palette.light.base
881
904
  };
882
- rules[scopeSelector(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
883
- 'color': '#fff',
905
+ rules[_sx(scope, '.bw_navbar_dark .bw_nav_link.active')] = {
906
+ 'color': palette.light.base,
884
907
  'font-weight': '600'
885
908
  };
886
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link.active')] = {
909
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link.active')] = {
887
910
  'color': palette.primary.textOn,
888
911
  'background-color': palette.primary.base
889
912
  };
@@ -894,49 +917,58 @@ function generateTables(scope, palette, layout) {
894
917
  var rules = {};
895
918
  var sp = layout.spacing;
896
919
 
897
- rules[scopeSelector(scope, '.bw_table')] = {
920
+ rules[_sx(scope, '.bw_table')] = {
898
921
  'color': palette.dark.base,
899
922
  'border-color': palette.light.border
900
923
  };
901
- rules[scopeSelector(scope, '.bw_table > :not(caption) > * > *')] = {
924
+ rules[_sx(scope, '.bw_table > :not(caption) > * > *')] = {
902
925
  'padding': sp.cell,
903
926
  'border-bottom-color': palette.light.border
904
927
  };
905
- rules[scopeSelector(scope, '.bw_table > thead > tr > *')] = {
928
+ rules[_sx(scope, '.bw_table > thead > tr > *')] = {
906
929
  'color': palette.secondary.base,
907
930
  'border-bottom-color': palette.light.border,
908
- 'background-color': palette.light.light
931
+ 'background-color': palette.surfaceAlt
909
932
  };
910
- rules[scopeSelector(scope, '.bw_table_striped > tbody > tr:nth-of-type(odd) > *')] = {
911
- 'background-color': 'rgba(0, 0, 0, 0.05)'
933
+ rules[_sx(scope, '.bw_table_striped > tbody > tr:nth-of-type(odd) > *')] = {
934
+ 'background-color': palette.surfaceAlt
912
935
  };
913
- rules[scopeSelector(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
936
+ rules[_sx(scope, '.bw_table_hover > tbody > tr:hover > *')] = {
914
937
  'background-color': palette.primary.focus
915
938
  };
916
- rules[scopeSelector(scope, '.bw_table_bordered')] = {
939
+ rules[_sx(scope, '.bw_table_selectable > tbody > tr')] = {
940
+ 'cursor': 'pointer'
941
+ };
942
+ rules[_sx(scope, '.bw_table > tbody > tr.bw_table_row_selected > *')] = {
943
+ 'background-color': palette.primary.light
944
+ };
945
+ rules[_sx(scope, '.bw_table_bordered')] = {
917
946
  'border-color': palette.light.border
918
947
  };
919
- rules[scopeSelector(scope, '.bw_table caption')] = {
948
+ rules[_sx(scope, '.bw_table caption')] = {
920
949
  'color': palette.secondary.base
921
950
  };
922
951
 
923
952
  return rules;
924
953
  }
925
954
 
926
- function generateTabs(scope, palette) {
927
- var rules = {};
928
- rules[scopeSelector(scope, '.bw_nav_tabs')] = {
955
+ function generateTabs(scope, palette, layout) {
956
+ var rules = {}, mo = layout.motion;
957
+ rules[_sx(scope, '.bw_nav_tabs')] = {
929
958
  'border-bottom-color': palette.light.border
930
959
  };
931
- rules[scopeSelector(scope, '.bw_nav_link')] = {
932
- 'color': palette.secondary.base
960
+ rules[_sx(scope, '.bw_nav_link')] = {
961
+ 'color': palette.secondary.base,
962
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
933
963
  };
934
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
964
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link:hover')] = {
935
965
  'color': palette.dark.base,
966
+ 'background-color': palette.surfaceAlt,
936
967
  'border-bottom-color': palette.light.border
937
968
  };
938
- rules[scopeSelector(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
969
+ rules[_sx(scope, '.bw_nav_tabs .bw_nav_link.active')] = {
939
970
  'color': palette.primary.base,
971
+ 'background-color': palette.primary.focus,
940
972
  'border-bottom': '2px solid ' + palette.primary.base
941
973
  };
942
974
  return rules;
@@ -945,23 +977,25 @@ function generateTabs(scope, palette) {
945
977
  function generateListGroups(scope, palette, layout) {
946
978
  var rules = {};
947
979
  var sp = layout.spacing;
980
+ var mo = layout.motion;
948
981
 
949
- rules[scopeSelector(scope, '.bw_list_group_item')] = {
982
+ rules[_sx(scope, '.bw_list_group_item')] = {
950
983
  'padding': sp.cell,
951
984
  'color': palette.dark.base,
952
985
  'background-color': palette.surface || '#fff',
953
- 'border-color': palette.light.border
986
+ 'border-color': palette.light.border,
987
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
954
988
  };
955
- rules[scopeSelector(scope, 'a.bw_list_group_item:hover')] = {
956
- 'background-color': palette.light.light,
989
+ rules[_sx(scope, 'a.bw_list_group_item:hover')] = {
990
+ 'background-color': palette.surfaceAlt,
957
991
  'color': palette.dark.hover
958
992
  };
959
- rules[scopeSelector(scope, '.bw_list_group_item.active')] = {
993
+ rules[_sx(scope, '.bw_list_group_item.active')] = {
960
994
  'color': palette.primary.textOn,
961
995
  'background-color': palette.primary.base,
962
996
  'border-color': palette.primary.base
963
997
  };
964
- rules[scopeSelector(scope, '.bw_list_group_item.disabled')] = {
998
+ rules[_sx(scope, '.bw_list_group_item.disabled')] = {
965
999
  'color': palette.secondary.base,
966
1000
  'background-color': palette.surface || '#fff'
967
1001
  };
@@ -969,28 +1003,37 @@ function generateListGroups(scope, palette, layout) {
969
1003
  return rules;
970
1004
  }
971
1005
 
972
- function generatePagination(scope, palette) {
973
- var rules = {};
974
- rules[scopeSelector(scope, '.bw_page_link')] = {
1006
+ function generatePagination(scope, palette, layout) {
1007
+ var rules = {}, mo = layout.motion, rd = layout.radius;
1008
+ rules[_sx(scope, '.bw_page_item:first-child .bw_page_link')] = {
1009
+ 'border-top-left-radius': rd.btn,
1010
+ 'border-bottom-left-radius': rd.btn
1011
+ };
1012
+ rules[_sx(scope, '.bw_page_item:last-child .bw_page_link')] = {
1013
+ 'border-top-right-radius': rd.btn,
1014
+ 'border-bottom-right-radius': rd.btn
1015
+ };
1016
+ rules[_sx(scope, '.bw_page_link')] = {
975
1017
  'color': palette.primary.base,
976
1018
  'background-color': palette.surface || '#fff',
977
- 'border-color': palette.light.border
1019
+ 'border-color': palette.light.border,
1020
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
978
1021
  };
979
- rules[scopeSelector(scope, '.bw_page_link:hover')] = {
1022
+ rules[_sx(scope, '.bw_page_link:hover')] = {
980
1023
  'color': palette.primary.hover,
981
- 'background-color': palette.light.light,
1024
+ 'background-color': palette.surfaceAlt,
982
1025
  'border-color': palette.light.border
983
1026
  };
984
- rules[scopeSelector(scope, '.bw_page_link:focus')] = {
1027
+ rules[_sx(scope, '.bw_page_link:focus')] = {
985
1028
  'outline': '2px solid ' + palette.primary.base,
986
1029
  'outline-offset': '-2px'
987
1030
  };
988
- rules[scopeSelector(scope, '.bw_page_item.bw_active .bw_page_link')] = {
1031
+ rules[_sx(scope, '.bw_page_item.bw_active .bw_page_link')] = {
989
1032
  'color': palette.primary.textOn,
990
1033
  'background-color': palette.primary.base,
991
1034
  'border-color': palette.primary.base
992
1035
  };
993
- rules[scopeSelector(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
1036
+ rules[_sx(scope, '.bw_page_item.bw_disabled .bw_page_link')] = {
994
1037
  'color': palette.secondary.base,
995
1038
  'background-color': palette.surface || '#fff',
996
1039
  'border-color': palette.light.border
@@ -1000,12 +1043,12 @@ function generatePagination(scope, palette) {
1000
1043
 
1001
1044
  function generateProgress(scope, palette) {
1002
1045
  var rules = {};
1003
- rules[scopeSelector(scope, '.bw_progress')] = {
1004
- 'background-color': palette.light.light,
1046
+ rules[_sx(scope, '.bw_progress')] = {
1047
+ 'background-color': palette.surfaceAlt,
1005
1048
  'box-shadow': 'inset 0 1px 2px rgba(0,0,0,.1)'
1006
1049
  };
1007
- rules[scopeSelector(scope, '.bw_progress_bar')] = {
1008
- 'color': '#fff',
1050
+ rules[_sx(scope, '.bw_progress_bar')] = {
1051
+ 'color': palette.primary.textOn,
1009
1052
  'background-color': palette.primary.base,
1010
1053
  'box-shadow': 'inset 0 -1px 0 rgba(0,0,0,.15)'
1011
1054
  };
@@ -1024,26 +1067,31 @@ function generateResetThemed(scope, palette) {
1024
1067
  'color': palette.dark.base,
1025
1068
  'background-color': bg
1026
1069
  };
1027
- rules[scopeSelector(scope, 'body')] = baseReset;
1028
- // Also apply to the scope element itself so themes work on any container, not just body
1029
- if (scope) {
1030
- rules['.' + scope] = baseReset;
1031
- }
1070
+ rules[_sx(scope, 'body')] = baseReset;
1032
1071
  return rules;
1033
1072
  }
1034
1073
 
1035
- function generateBreadcrumbThemed(scope, palette) {
1036
- var rules = {};
1037
- rules[scopeSelector(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1038
- 'color': palette.secondary.base
1074
+ function generateBreadcrumbThemed(scope, palette, layout) {
1075
+ var rules = {}, mo = layout.motion;
1076
+ rules[_sx(scope, '.bw_breadcrumb')] = {
1077
+ 'background-color': palette.surfaceAlt,
1078
+ 'padding': '0.625rem 1rem',
1079
+ 'border-radius': layout.radius.btn
1039
1080
  };
1040
- rules[scopeSelector(scope, '.bw_breadcrumb_item.active')] = {
1081
+ rules[_sx(scope, '.bw_breadcrumb_item + .bw_breadcrumb_item::before')] = {
1041
1082
  'color': palette.secondary.base
1042
1083
  };
1043
- rules[scopeSelector(scope, '.bw_breadcrumb_item a:hover')] = {
1084
+ rules[_sx(scope, '.bw_breadcrumb_item a')] = {
1085
+ 'color': palette.primary.base,
1086
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing
1087
+ };
1088
+ rules[_sx(scope, '.bw_breadcrumb_item a:hover')] = {
1044
1089
  'color': palette.primary.hover,
1045
1090
  'text-decoration': 'underline'
1046
1091
  };
1092
+ rules[_sx(scope, '.bw_breadcrumb_item.active')] = {
1093
+ 'color': palette.dark.base
1094
+ };
1047
1095
  return rules;
1048
1096
  }
1049
1097
 
@@ -1051,11 +1099,11 @@ function generateBreadcrumbThemed(scope, palette) {
1051
1099
 
1052
1100
  function generateCloseButtonThemed(scope, palette) {
1053
1101
  var rules = {};
1054
- rules[scopeSelector(scope, '.bw_close')] = {
1102
+ rules[_sx(scope, '.bw_close')] = {
1055
1103
  'color': palette.dark.base,
1056
1104
  'opacity': '0.5'
1057
1105
  };
1058
- rules[scopeSelector(scope, '.bw_close:focus')] = {
1106
+ rules[_sx(scope, '.bw_close:focus')] = {
1059
1107
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1060
1108
  };
1061
1109
  return rules;
@@ -1063,82 +1111,94 @@ function generateCloseButtonThemed(scope, palette) {
1063
1111
 
1064
1112
  function generateSectionsThemed(scope, palette) {
1065
1113
  var rules = {};
1066
- rules[scopeSelector(scope, '.bw_section_subtitle')] = {
1114
+ rules[_sx(scope, '.bw_section_subtitle')] = {
1067
1115
  'color': palette.secondary.base
1068
1116
  };
1069
- rules[scopeSelector(scope, '.bw_feature_description')] = {
1117
+ rules[_sx(scope, '.bw_feature_description')] = {
1070
1118
  'color': palette.secondary.base
1071
1119
  };
1072
- rules[scopeSelector(scope, '.bw_cta_description')] = {
1120
+ rules[_sx(scope, '.bw_cta_description')] = {
1073
1121
  'color': palette.secondary.base
1074
1122
  };
1075
1123
  return rules;
1076
1124
  }
1077
1125
 
1078
- function generateAccordionThemed(scope, palette) {
1126
+ function generateAccordionThemed(scope, palette, layout) {
1079
1127
  var rules = {};
1080
- rules[scopeSelector(scope, '.bw_accordion_item')] = {
1128
+ var rd = layout ? layout.radius : { card: '8px' };
1129
+ rules[_sx(scope, '.bw_accordion_item')] = {
1081
1130
  'background-color': palette.surface || '#fff',
1082
1131
  'border-color': palette.light.border
1083
1132
  };
1084
- rules[scopeSelector(scope, '.bw_accordion_button')] = {
1133
+ rules[_sx(scope, '.bw_accordion_item:first-child')] = {
1134
+ 'border-top-left-radius': rd.card,
1135
+ 'border-top-right-radius': rd.card
1136
+ };
1137
+ rules[_sx(scope, '.bw_accordion_item:last-child')] = {
1138
+ 'border-bottom-left-radius': rd.card,
1139
+ 'border-bottom-right-radius': rd.card
1140
+ };
1141
+ rules[_sx(scope, '.bw_accordion_button')] = {
1085
1142
  'color': palette.dark.base
1086
1143
  };
1087
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1144
+ rules[_sx(scope, '.bw_accordion_button:not(.bw_collapsed)')] = {
1088
1145
  'color': palette.primary.darkText,
1089
- 'background-color': palette.primary.light
1146
+ 'background-color': palette.primary.light,
1147
+ 'border-left': '3px solid ' + palette.primary.base
1090
1148
  };
1091
- rules[scopeSelector(scope, '.bw_accordion_button:hover')] = {
1092
- 'background-color': palette.light.light
1149
+ rules[_sx(scope, '.bw_accordion_button:hover')] = {
1150
+ 'background-color': palette.surfaceAlt
1093
1151
  };
1094
- rules[scopeSelector(scope, '.bw_accordion_button:not(.bw_collapsed):hover')] = {
1095
- 'background-color': palette.primary.hover
1152
+ rules[_sx(scope, '.bw_accordion_button:not(.bw_collapsed):hover')] = {
1153
+ 'background-color': palette.primary.base,
1154
+ 'color': palette.primary.textOn
1096
1155
  };
1097
- rules[scopeSelector(scope, '.bw_accordion_button:focus-visible')] = {
1156
+ rules[_sx(scope, '.bw_accordion_button:focus-visible')] = {
1098
1157
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1099
1158
  };
1100
- rules[scopeSelector(scope, '.bw_accordion_body')] = {
1101
- 'border-top': '1px solid ' + palette.light.border
1159
+ rules[_sx(scope, '.bw_accordion_body')] = {
1160
+ 'border-top': '1px solid ' + palette.light.border,
1161
+ 'background-color': palette.surfaceAlt
1102
1162
  };
1103
1163
  return rules;
1104
1164
  }
1105
1165
 
1106
1166
  function generateCarouselThemed(scope, palette) {
1107
1167
  var rules = {};
1108
- rules[scopeSelector(scope, '.bw_carousel')] = {
1109
- 'background-color': palette.light.light
1168
+ rules[_sx(scope, '.bw_carousel')] = {
1169
+ 'background-color': palette.surfaceAlt
1110
1170
  };
1111
- rules[scopeSelector(scope, '.bw_carousel_indicator.active')] = {
1171
+ rules[_sx(scope, '.bw_carousel_indicator.active')] = {
1112
1172
  'background-color': palette.primary.base
1113
1173
  };
1114
- rules[scopeSelector(scope, '.bw_carousel_control')] = {
1115
- 'background-color': 'rgba(0,0,0,0.4)',
1116
- 'color': '#fff'
1174
+ rules[_sx(scope, '.bw_carousel_control')] = {
1175
+ 'background-color': palette.dark.base,
1176
+ 'color': palette.dark.textOn
1117
1177
  };
1118
- rules[scopeSelector(scope, '.bw_carousel_control:hover')] = {
1119
- 'background-color': 'rgba(0,0,0,0.6)'
1178
+ rules[_sx(scope, '.bw_carousel_control:hover')] = {
1179
+ 'background-color': palette.dark.hover
1120
1180
  };
1121
- rules[scopeSelector(scope, '.bw_carousel_caption')] = {
1122
- 'background': 'linear-gradient(transparent, rgba(0,0,0,0.6))',
1123
- 'color': '#fff'
1181
+ rules[_sx(scope, '.bw_carousel_caption')] = {
1182
+ 'background': 'linear-gradient(transparent, ' + palette.dark.base + ')',
1183
+ 'color': palette.dark.textOn
1124
1184
  };
1125
1185
  return rules;
1126
1186
  }
1127
1187
 
1128
1188
  function generateModalThemed(scope, palette, layout) {
1129
1189
  var rules = {};
1130
- rules[scopeSelector(scope, '.bw_modal_content')] = {
1190
+ rules[_sx(scope, '.bw_modal_content')] = {
1131
1191
  'background-color': palette.surface || '#fff',
1132
1192
  'border-color': palette.light.border,
1133
1193
  'box-shadow': layout.elevation.lg
1134
1194
  };
1135
- rules[scopeSelector(scope, '.bw_modal_header')] = {
1195
+ rules[_sx(scope, '.bw_modal_header')] = {
1136
1196
  'border-bottom-color': palette.light.border
1137
1197
  };
1138
- rules[scopeSelector(scope, '.bw_modal_footer')] = {
1198
+ rules[_sx(scope, '.bw_modal_footer')] = {
1139
1199
  'border-top-color': palette.light.border
1140
1200
  };
1141
- rules[scopeSelector(scope, '.bw_modal_title')] = {
1201
+ rules[_sx(scope, '.bw_modal_title')] = {
1142
1202
  'color': palette.dark.base
1143
1203
  };
1144
1204
  return rules;
@@ -1146,13 +1206,13 @@ function generateModalThemed(scope, palette, layout) {
1146
1206
 
1147
1207
  function generateToastThemed(scope, palette, layout) {
1148
1208
  var rules = {};
1149
- rules[scopeSelector(scope, '.bw_toast')] = {
1209
+ rules[_sx(scope, '.bw_toast')] = {
1150
1210
  'background-color': palette.surface || '#fff',
1151
- 'border-color': 'rgba(0,0,0,0.1)',
1211
+ 'border-color': palette.light.border,
1152
1212
  'box-shadow': layout.elevation.lg
1153
1213
  };
1154
- rules[scopeSelector(scope, '.bw_toast_header')] = {
1155
- 'border-bottom-color': 'rgba(0,0,0,0.05)'
1214
+ rules[_sx(scope, '.bw_toast_header')] = {
1215
+ 'border-bottom-color': palette.light.border
1156
1216
  };
1157
1217
  // Variant toast borders handled by palette class
1158
1218
  return rules;
@@ -1160,22 +1220,23 @@ function generateToastThemed(scope, palette, layout) {
1160
1220
 
1161
1221
  function generateDropdownThemed(scope, palette, layout) {
1162
1222
  var rules = {};
1163
- rules[scopeSelector(scope, '.bw_dropdown_menu')] = {
1223
+ rules[_sx(scope, '.bw_dropdown_menu')] = {
1164
1224
  'background-color': palette.surface || '#fff',
1165
1225
  'border-color': palette.light.border,
1166
1226
  'box-shadow': layout.elevation.md
1167
1227
  };
1168
- rules[scopeSelector(scope, '.bw_dropdown_item')] = {
1169
- 'color': palette.dark.base
1228
+ rules[_sx(scope, '.bw_dropdown_item')] = {
1229
+ 'color': palette.dark.base,
1230
+ 'transition': 'background-color ' + layout.motion.fast + ' ' + layout.motion.easing
1170
1231
  };
1171
- rules[scopeSelector(scope, '.bw_dropdown_item:hover')] = {
1232
+ rules[_sx(scope, '.bw_dropdown_item:hover')] = {
1172
1233
  'color': palette.dark.hover,
1173
- 'background-color': palette.light.light
1234
+ 'background-color': palette.surfaceAlt
1174
1235
  };
1175
- rules[scopeSelector(scope, '.bw_dropdown_item.disabled')] = {
1236
+ rules[_sx(scope, '.bw_dropdown_item.disabled')] = {
1176
1237
  'color': palette.secondary.base
1177
1238
  };
1178
- rules[scopeSelector(scope, '.bw_dropdown_divider')] = {
1239
+ rules[_sx(scope, '.bw_dropdown_divider')] = {
1179
1240
  'border-top-color': palette.light.border
1180
1241
  };
1181
1242
  return rules;
@@ -1183,15 +1244,15 @@ function generateDropdownThemed(scope, palette, layout) {
1183
1244
 
1184
1245
  function generateSwitchThemed(scope, palette) {
1185
1246
  var rules = {};
1186
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input')] = {
1247
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input')] = {
1187
1248
  'background-color': palette.secondary.base,
1188
1249
  'border-color': palette.secondary.base
1189
1250
  };
1190
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1251
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:checked')] = {
1191
1252
  'background-color': palette.primary.base,
1192
1253
  'border-color': palette.primary.base
1193
1254
  };
1194
- rules[scopeSelector(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1255
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1195
1256
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1196
1257
  };
1197
1258
  return rules;
@@ -1199,88 +1260,102 @@ function generateSwitchThemed(scope, palette) {
1199
1260
 
1200
1261
  function generateSkeletonThemed(scope, palette) {
1201
1262
  var rules = {};
1202
- rules[scopeSelector(scope, '.bw_skeleton')] = {
1203
- 'background': 'linear-gradient(90deg, ' + palette.light.border + ' 25%, ' + palette.light.light + ' 37%, ' + palette.light.border + ' 63%)'
1263
+ rules[_sx(scope, '.bw_skeleton')] = {
1264
+ 'background': 'linear-gradient(90deg, ' + palette.light.border + ' 25%, ' + palette.surfaceAlt + ' 37%, ' + palette.light.border + ' 63%)'
1204
1265
  };
1205
1266
  return rules;
1206
1267
  }
1207
1268
 
1208
1269
  // generateAvatarThemed: removed — palette class on root handles variants
1209
1270
 
1210
- function generateStatCardThemed(scope, palette) {
1211
- var rules = {};
1271
+ function generateStatCardThemed(scope, palette, layout) {
1272
+ var rules = {}, mo = layout.motion, el = layout.elevation, rd = layout.radius;
1273
+ rules[_sx(scope, '.bw_stat_card')] = {
1274
+ 'background-color': palette.surface || '#fff',
1275
+ 'color': palette.dark.base,
1276
+ 'border': '1px solid ' + palette.light.border,
1277
+ 'border-radius': rd.card,
1278
+ 'box-shadow': el.sm,
1279
+ 'transition': 'box-shadow ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1280
+ };
1281
+ rules[_sx(scope, '.bw_stat_card:hover')] = { 'box-shadow': el.md };
1212
1282
  // Variant border colors handled by palette class
1213
- rules[scopeSelector(scope, '.bw_stat_change_up')] = { 'color': palette.success.base };
1214
- rules[scopeSelector(scope, '.bw_stat_change_down')] = { 'color': palette.danger.base };
1283
+ rules[_sx(scope, '.bw_stat_change_up')] = { 'color': palette.success.base };
1284
+ rules[_sx(scope, '.bw_stat_change_down')] = { 'color': palette.danger.base };
1215
1285
  return rules;
1216
1286
  }
1217
1287
 
1218
1288
  function generateTimelineThemed(scope, palette) {
1219
1289
  var rules = {};
1220
- rules[scopeSelector(scope, '.bw_timeline::before')] = { 'background-color': palette.light.border };
1290
+ rules[_sx(scope, '.bw_timeline::before')] = { 'background-color': palette.light.border };
1221
1291
  // Variant marker colors handled by palette class
1222
- rules[scopeSelector(scope, '.bw_timeline_date')] = { 'color': palette.secondary.base };
1292
+ rules[_sx(scope, '.bw_timeline_date')] = { 'color': palette.secondary.base };
1223
1293
  return rules;
1224
1294
  }
1225
1295
 
1226
1296
  function generateStepperThemed(scope, palette) {
1227
1297
  var rules = {};
1228
- rules[scopeSelector(scope, '.bw_step_indicator')] = {
1229
- 'background-color': palette.light.light,
1298
+ rules[_sx(scope, '.bw_step_indicator')] = {
1299
+ 'background-color': palette.surfaceAlt,
1230
1300
  'border': '2px solid ' + palette.light.border,
1231
1301
  'color': palette.secondary.base
1232
1302
  };
1233
- rules[scopeSelector(scope, '.bw_step + .bw_step::before')] = { 'background-color': palette.light.border };
1234
- rules[scopeSelector(scope, '.bw_step_active .bw_step_indicator')] = {
1303
+ rules[_sx(scope, '.bw_step + .bw_step::before')] = { 'background-color': palette.light.border };
1304
+ rules[_sx(scope, '.bw_step_active .bw_step_indicator')] = {
1235
1305
  'background-color': palette.primary.base,
1236
1306
  'color': palette.primary.textOn
1237
1307
  };
1238
- rules[scopeSelector(scope, '.bw_step_active .bw_step_label')] = {
1308
+ rules[_sx(scope, '.bw_step_active .bw_step_label')] = {
1239
1309
  'color': palette.dark.base,
1240
1310
  'font-weight': '600'
1241
1311
  };
1242
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_indicator')] = {
1312
+ rules[_sx(scope, '.bw_step_completed .bw_step_indicator')] = {
1243
1313
  'background-color': palette.primary.base,
1244
1314
  'color': palette.primary.textOn
1245
1315
  };
1246
- rules[scopeSelector(scope, '.bw_step_completed .bw_step_label')] = { 'color': palette.primary.base };
1247
- rules[scopeSelector(scope, '.bw_step_completed + .bw_step::before')] = { 'background-color': palette.primary.base };
1316
+ rules[_sx(scope, '.bw_step_completed .bw_step_label')] = { 'color': palette.primary.base };
1317
+ rules[_sx(scope, '.bw_step_completed + .bw_step::before')] = { 'background-color': palette.primary.base };
1248
1318
  return rules;
1249
1319
  }
1250
1320
 
1251
1321
  function generateChipInputThemed(scope, palette) {
1252
1322
  var rules = {};
1253
- rules[scopeSelector(scope, '.bw_chip_input')] = { 'border-color': palette.light.border };
1254
- rules[scopeSelector(scope, '.bw_chip_input:focus-within')] = {
1323
+ rules[_sx(scope, '.bw_chip_input')] = {
1324
+ 'border-color': palette.light.border,
1325
+ 'background-color': palette.surface || '#fff',
1326
+ 'color': palette.dark.base
1327
+ };
1328
+ rules[_sx(scope, '.bw_chip_input:focus-within')] = {
1255
1329
  'border-color': palette.primary.base,
1256
1330
  'box-shadow': '0 0 0 0.2rem ' + palette.primary.focus
1257
1331
  };
1258
- rules[scopeSelector(scope, '.bw_chip')] = {
1259
- 'background-color': palette.light.light,
1332
+ rules[_sx(scope, '.bw_chip')] = {
1333
+ 'background-color': palette.surfaceAlt,
1260
1334
  'color': palette.dark.base
1261
1335
  };
1262
- rules[scopeSelector(scope, '.bw_chip_remove:hover')] = {
1336
+ rules[_sx(scope, '.bw_chip_remove:hover')] = {
1263
1337
  'color': palette.danger.base,
1264
1338
  'background-color': palette.danger.light
1265
1339
  };
1266
1340
  return rules;
1267
1341
  }
1268
1342
 
1269
- function generateFileUploadThemed(scope, palette) {
1270
- var rules = {};
1271
- rules[scopeSelector(scope, '.bw_file_upload')] = {
1343
+ function generateFileUploadThemed(scope, palette, layout) {
1344
+ var rules = {}, mo = layout.motion;
1345
+ rules[_sx(scope, '.bw_file_upload')] = {
1272
1346
  'border-color': palette.light.border,
1273
- 'background-color': palette.light.light
1347
+ 'background-color': palette.surfaceAlt,
1348
+ 'transition': 'border-color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1274
1349
  };
1275
- rules[scopeSelector(scope, '.bw_file_upload:hover')] = {
1350
+ rules[_sx(scope, '.bw_file_upload:hover')] = {
1276
1351
  'border-color': palette.primary.base,
1277
1352
  'background-color': palette.primary.light
1278
1353
  };
1279
- rules[scopeSelector(scope, '.bw_file_upload:focus')] = {
1354
+ rules[_sx(scope, '.bw_file_upload:focus')] = {
1280
1355
  'outline': '2px solid ' + palette.primary.base,
1281
1356
  'outline-offset': '2px'
1282
1357
  };
1283
- rules[scopeSelector(scope, '.bw_file_upload.bw_file_upload_active')] = {
1358
+ rules[_sx(scope, '.bw_file_upload.bw_file_upload_active')] = {
1284
1359
  'border-color': palette.primary.base,
1285
1360
  'background-color': palette.primary.light,
1286
1361
  'border-style': 'solid'
@@ -1290,35 +1365,73 @@ function generateFileUploadThemed(scope, palette) {
1290
1365
 
1291
1366
  function generateRangeThemed(scope, palette) {
1292
1367
  var rules = {};
1293
- rules[scopeSelector(scope, '.bw_range')] = { 'background-color': palette.light.border };
1294
- rules[scopeSelector(scope, '.bw_range::-webkit-slider-thumb')] = {
1368
+ rules[_sx(scope, '.bw_range')] = { 'background-color': palette.light.border };
1369
+ rules[_sx(scope, '.bw_range::-webkit-slider-thumb')] = {
1295
1370
  'background-color': palette.primary.base,
1296
- 'border-color': '#fff',
1371
+ 'border-color': palette.surface || '#fff',
1297
1372
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)',
1298
1373
  'transition': 'background-color 0.15s ease-out, transform 0.15s ease-out'
1299
1374
  };
1300
- rules[scopeSelector(scope, '.bw_range::-moz-range-thumb')] = {
1375
+ rules[_sx(scope, '.bw_range::-moz-range-thumb')] = {
1301
1376
  'background-color': palette.primary.base,
1302
- 'border-color': '#fff',
1377
+ 'border-color': palette.surface || '#fff',
1303
1378
  'box-shadow': '0 1px 3px rgba(0,0,0,0.2)'
1304
1379
  };
1305
1380
  return rules;
1306
1381
  }
1307
1382
 
1308
- function generateSearchThemed(scope, palette) {
1309
- var rules = {};
1310
- rules[scopeSelector(scope, '.bw_search_clear:hover')] = { 'color': palette.dark.base };
1383
+ function generateTooltipThemed(scope, palette, layout) {
1384
+ var rules = {}, sp = layout.spacing, rd = layout.radius, el = layout.elevation, mo = layout.motion;
1385
+ rules[_sx(scope, '.bw_tooltip')] = {
1386
+ 'background-color': palette.dark.base, 'color': palette.dark.textOn,
1387
+ 'padding': sp.input, 'border-radius': rd.badge, 'box-shadow': el.md,
1388
+ 'transition': 'opacity ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1389
+ };
1390
+ return rules;
1391
+ }
1392
+
1393
+ function generatePopoverThemed(scope, palette, layout) {
1394
+ var rules = {}, sp = layout.spacing, rd = layout.radius, el = layout.elevation, mo = layout.motion;
1395
+ rules[_sx(scope, '.bw_popover')] = {
1396
+ 'background-color': palette.surface || '#fff', 'color': palette.dark.base,
1397
+ 'border': '1px solid ' + palette.light.border, 'border-radius': rd.card, 'box-shadow': el.lg,
1398
+ 'transition': 'opacity ' + mo.fast + ' ' + mo.easing + ', transform ' + mo.fast + ' ' + mo.easing
1399
+ };
1400
+ rules[_sx(scope, '.bw_popover_header')] = {
1401
+ 'background-color': palette.surfaceAlt, 'border-bottom': '1px solid ' + palette.light.border,
1402
+ 'padding': sp.input
1403
+ };
1404
+ rules[_sx(scope, '.bw_popover_body')] = { 'padding': sp.card };
1311
1405
  return rules;
1312
1406
  }
1313
1407
 
1314
- function generateCodeDemoThemed(scope, palette) {
1408
+ function generateSearchThemed(scope, palette, layout) {
1409
+ var rules = {}, mo = layout.motion;
1410
+ rules[_sx(scope, '.bw_search_input')] = {
1411
+ 'background-color': palette.surface || '#fff',
1412
+ 'color': palette.dark.base
1413
+ };
1414
+ rules[_sx(scope, '.bw_search_clear')] = {
1415
+ 'transition': 'color ' + mo.fast + ' ' + mo.easing + ', background-color ' + mo.fast + ' ' + mo.easing
1416
+ };
1417
+ rules[_sx(scope, '.bw_search_clear:hover')] = { 'color': palette.dark.base };
1418
+ return rules;
1419
+ }
1420
+
1421
+ function generateCodeDemoThemed(scope, palette, layout) {
1315
1422
  var rules = {};
1316
- rules[scopeSelector(scope, '.bw_code_copy_btn_copied')] = {
1423
+ var rd = layout ? layout.radius : { card: '0.375rem' };
1424
+ rules[_sx(scope, '.bw_code_demo')] = {
1425
+ 'background-color': palette.surface || '#fff',
1426
+ 'color': palette.dark.base,
1427
+ 'border-radius': rd.card
1428
+ };
1429
+ rules[_sx(scope, '.bw_code_copy_btn_copied')] = {
1317
1430
  'background': palette.success.base,
1318
1431
  'color': palette.success.textOn,
1319
1432
  'border-color': palette.success.base
1320
1433
  };
1321
- rules[scopeSelector(scope, '.bw_copy_btn:hover')] = {
1434
+ rules[_sx(scope, '.bw_copy_btn:hover')] = {
1322
1435
  'background': 'rgba(255,255,255,0.2)',
1323
1436
  'color': '#fff'
1324
1437
  };
@@ -1328,7 +1441,7 @@ function generateCodeDemoThemed(scope, palette) {
1328
1441
  function generateNavPillsThemed(scope, palette, layout) {
1329
1442
  var rules = {};
1330
1443
  var rd = layout.radius;
1331
- rules[scopeSelector(scope, '.bw_nav_pills .bw_nav_link')] = { 'border-radius': rd.btn };
1444
+ rules[_sx(scope, '.bw_nav_pills .bw_nav_link')] = { 'border-radius': rd.btn };
1332
1445
  return rules;
1333
1446
  }
1334
1447
 
@@ -1354,21 +1467,21 @@ function generatePaletteClasses(scope, palette) {
1354
1467
  var s = palette[k];
1355
1468
 
1356
1469
  // --- Root palette class: sets default bg/color/border ---
1357
- rules[scopeSelector(scope, '.bw_' + k)] = {
1470
+ rules[_sx(scope, '.bw_' + k)] = {
1358
1471
  'background-color': s.base,
1359
1472
  'color': s.textOn,
1360
1473
  'border-color': s.base
1361
1474
  };
1362
1475
 
1363
1476
  // --- Pseudo-states (shared across all components) ---
1364
- rules[scopeSelector(scope, '.bw_' + k + ':hover')] = {
1477
+ rules[_sx(scope, '.bw_' + k + ':hover')] = {
1365
1478
  'background-color': s.hover,
1366
1479
  'border-color': s.active
1367
1480
  };
1368
- rules[scopeSelector(scope, '.bw_' + k + ':active')] = {
1481
+ rules[_sx(scope, '.bw_' + k + ':active')] = {
1369
1482
  'background-color': s.active
1370
1483
  };
1371
- rules[scopeSelector(scope, '.bw_' + k + ':focus-visible')] = {
1484
+ rules[_sx(scope, '.bw_' + k + ':focus-visible')] = {
1372
1485
  'box-shadow': '0 0 0 3px ' + s.focus,
1373
1486
  'outline': 'none'
1374
1487
  };
@@ -1376,70 +1489,99 @@ function generatePaletteClasses(scope, palette) {
1376
1489
  // --- Component-specific overrides ---
1377
1490
 
1378
1491
  // Alerts: light bg, dark text, subtle border
1379
- rules[scopeSelector(scope, '.bw_alert.bw_' + k)] = {
1492
+ rules[_sx(scope, '.bw_alert.bw_' + k)] = {
1380
1493
  'background-color': s.light,
1381
1494
  'color': s.darkText,
1382
1495
  'border-color': s.border
1383
1496
  };
1384
1497
 
1385
1498
  // Toast: inherit bg, left border accent
1386
- rules[scopeSelector(scope, '.bw_toast.bw_' + k)] = {
1499
+ rules[_sx(scope, '.bw_toast.bw_' + k)] = {
1387
1500
  'background-color': 'inherit',
1388
1501
  'color': 'inherit',
1389
1502
  'border-left': '4px solid ' + s.base
1390
1503
  };
1391
1504
 
1392
1505
  // Stat card: inherit bg, left border accent
1393
- rules[scopeSelector(scope, '.bw_stat_card.bw_' + k)] = {
1506
+ rules[_sx(scope, '.bw_stat_card.bw_' + k)] = {
1394
1507
  'background-color': 'inherit',
1395
1508
  'color': 'inherit',
1396
1509
  'border-left-color': s.base
1397
1510
  };
1398
1511
 
1399
1512
  // Card accent: left border accent, inherit bg
1400
- rules[scopeSelector(scope, '.bw_card.bw_' + k)] = {
1513
+ rules[_sx(scope, '.bw_card.bw_' + k)] = {
1401
1514
  'background-color': 'inherit',
1402
1515
  'color': 'inherit',
1403
1516
  'border-left': '4px solid ' + s.base
1404
1517
  };
1405
1518
 
1406
1519
  // Timeline marker: colored dot
1407
- rules[scopeSelector(scope, '.bw_timeline_marker.bw_' + k)] = {
1520
+ rules[_sx(scope, '.bw_timeline_marker.bw_' + k)] = {
1408
1521
  'box-shadow': '0 0 0 2px ' + s.base
1409
1522
  };
1410
1523
 
1411
- // Spinner: text color only, transparent bg
1412
- rules[scopeSelector(scope, '.bw_spinner_border.bw_' + k + ',\n' + scopeSelector(scope, '.bw_spinner_grow.bw_' + k))] = {
1524
+ // Spinner: set color, re-apply border pattern so the root palette class
1525
+ // border-color doesn't fill in the transparent gap that makes it spin.
1526
+ // Also neutralize hover/active which would override border-right-color.
1527
+ rules[_sx(scope, '.bw_spinner_border.bw_' + k)] = {
1413
1528
  'background-color': 'transparent',
1414
1529
  'color': s.base,
1415
- 'border-color': 'currentColor'
1530
+ 'border-color': s.base,
1531
+ 'border-right-color': 'transparent'
1532
+ };
1533
+ rules[_sx(scope, '.bw_spinner_border.bw_' + k + ':hover')] = {
1534
+ 'background-color': 'transparent',
1535
+ 'border-color': s.base,
1536
+ 'border-right-color': 'transparent'
1537
+ };
1538
+ rules[_sx(scope, '.bw_spinner_grow.bw_' + k)] = {
1539
+ 'background-color': s.base,
1540
+ 'color': s.base
1416
1541
  };
1417
1542
 
1418
1543
  // Outline button: transparent bg, colored border+text, solid on hover
1419
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k)] = {
1544
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k)] = {
1420
1545
  'background-color': 'transparent',
1421
1546
  'color': s.base,
1422
1547
  'border-color': s.base
1423
1548
  };
1424
- rules[scopeSelector(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1549
+ rules[_sx(scope, '.bw_btn_outline.bw_' + k + ':hover')] = {
1425
1550
  'background-color': s.base,
1426
1551
  'color': s.textOn
1427
1552
  };
1428
1553
 
1429
1554
  // Hero: gradient background
1430
- rules[scopeSelector(scope, '.bw_hero.bw_' + k)] = {
1555
+ rules[_sx(scope, '.bw_hero.bw_' + k)] = {
1431
1556
  'background': 'linear-gradient(135deg, ' + s.base + ' 0%, ' + s.hover + ' 100%)',
1432
1557
  'color': s.textOn
1433
1558
  };
1434
1559
 
1435
- // Progress bar: white text on colored bg (default is fine, just ensure text)
1436
- rules[scopeSelector(scope, '.bw_progress_bar.bw_' + k)] = {
1437
- 'color': '#fff'
1560
+ // Progress bar: contrasting text on colored bg
1561
+ rules[_sx(scope, '.bw_progress_bar.bw_' + k)] = {
1562
+ 'color': s.textOn
1563
+ };
1564
+
1565
+ // Background utility: .bw_bg_primary, .bw_bg_secondary, etc.
1566
+ rules[_sx(scope, '.bw_bg_' + k)] = {
1567
+ 'background-color': s.base,
1568
+ 'color': s.textOn
1569
+ };
1570
+
1571
+ // Text color utility: .bw_text_primary, .bw_text_secondary, etc.
1572
+ rules[_sx(scope, '.bw_text_' + k)] = {
1573
+ 'color': s.base
1438
1574
  };
1439
1575
  });
1440
1576
 
1441
- // Text muted
1442
- rules[scopeSelector(scope, '.bw_text_muted')] = { 'color': palette.secondary.base };
1577
+ // Text muted — always a neutral gray, never a brand color
1578
+ rules[_sx(scope, '.bw_text_muted')] = { 'color': '#6c757d' };
1579
+
1580
+ // Common bg/text utilities that aren't per-variant
1581
+ rules[_sx(scope, '.bw_bg_dark')] = { 'background-color': '#212529', 'color': '#f8f9fa' };
1582
+ rules[_sx(scope, '.bw_bg_light')] = { 'background-color': '#f8f9fa', 'color': '#212529' };
1583
+ rules[_sx(scope, '.bw_text_light')] = { 'color': '#f8f9fa' };
1584
+ rules[_sx(scope, '.bw_text_dark')] = { 'color': '#212529' };
1443
1585
 
1444
1586
  return rules;
1445
1587
  }
@@ -1461,30 +1603,32 @@ function generateThemedCSS(scopeName, palette, layout) {
1461
1603
  generateAlerts(scopeName, palette, layout),
1462
1604
  generateCards(scopeName, palette, layout),
1463
1605
  generateForms(scopeName, palette, layout),
1464
- generateNavigation(scopeName, palette),
1606
+ generateNavigation(scopeName, palette, layout),
1465
1607
  generateTables(scopeName, palette, layout),
1466
- generateTabs(scopeName, palette),
1608
+ generateTabs(scopeName, palette, layout),
1467
1609
  generateListGroups(scopeName, palette, layout),
1468
- generatePagination(scopeName, palette),
1610
+ generatePagination(scopeName, palette, layout),
1469
1611
  generateProgress(scopeName, palette),
1470
- generateBreadcrumbThemed(scopeName, palette),
1612
+ generateBreadcrumbThemed(scopeName, palette, layout),
1471
1613
  generateCloseButtonThemed(scopeName, palette),
1472
1614
  generateSectionsThemed(scopeName, palette),
1473
- generateAccordionThemed(scopeName, palette),
1615
+ generateAccordionThemed(scopeName, palette, layout),
1474
1616
  generateCarouselThemed(scopeName, palette),
1475
1617
  generateModalThemed(scopeName, palette, layout),
1476
1618
  generateToastThemed(scopeName, palette, layout),
1477
1619
  generateDropdownThemed(scopeName, palette, layout),
1478
1620
  generateSwitchThemed(scopeName, palette),
1479
1621
  generateSkeletonThemed(scopeName, palette),
1480
- generateStatCardThemed(scopeName, palette),
1622
+ generateStatCardThemed(scopeName, palette, layout),
1481
1623
  generateTimelineThemed(scopeName, palette),
1482
1624
  generateStepperThemed(scopeName, palette),
1483
1625
  generateChipInputThemed(scopeName, palette),
1484
- generateFileUploadThemed(scopeName, palette),
1626
+ generateFileUploadThemed(scopeName, palette, layout),
1485
1627
  generateRangeThemed(scopeName, palette),
1486
- generateSearchThemed(scopeName, palette),
1487
- generateCodeDemoThemed(scopeName, palette),
1628
+ generateSearchThemed(scopeName, palette, layout),
1629
+ generateTooltipThemed(scopeName, palette, layout),
1630
+ generatePopoverThemed(scopeName, palette, layout),
1631
+ generateCodeDemoThemed(scopeName, palette, layout),
1488
1632
  generateNavPillsThemed(scopeName, palette, layout),
1489
1633
  generatePaletteClasses(scopeName, palette)
1490
1634
  );
@@ -1709,6 +1853,8 @@ var structuralRules = {
1709
1853
  },
1710
1854
  '.bw_table caption': { 'font-size': '0.875rem', 'caption-side': 'bottom' },
1711
1855
  '.bw_table_bordered > :not(caption) > * > *': { 'border-width': '1px', 'border-style': 'solid' },
1856
+ '.bw_table_selectable > tbody > tr': { 'cursor': 'pointer' },
1857
+ '.bw_table > tbody > tr.bw_table_row_selected > *': { 'background-color': 'rgba(0, 102, 102, 0.1)' },
1712
1858
  '.bw_table_responsive': { 'overflow-x': 'auto', '-webkit-overflow-scrolling': 'touch' }
1713
1859
  },
1714
1860
 
@@ -1762,6 +1908,7 @@ var structuralRules = {
1762
1908
  '.bw_nav_tabs .bw_nav_item': { 'margin-bottom': '-2px' },
1763
1909
  '.bw_nav_link': {
1764
1910
  'display': 'block', 'font-size': '0.875rem', 'font-weight': '500',
1911
+ 'padding': '0.625rem 1rem',
1765
1912
  'text-decoration': 'none', 'cursor': 'pointer',
1766
1913
  'border': 'none', 'background': 'transparent', 'font-family': 'inherit'
1767
1914
  },
@@ -1796,10 +1943,11 @@ var structuralRules = {
1796
1943
  '.bw_page_item': { 'display': 'list-item', 'list-style': 'none' },
1797
1944
  '.bw_page_link': {
1798
1945
  'position': 'relative', 'display': 'block', 'padding': '0.375rem 0.75rem',
1799
- 'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none'
1946
+ 'margin-left': '-1px', 'line-height': '1.25', 'text-decoration': 'none',
1947
+ 'border': '1px solid transparent', 'cursor': 'pointer',
1948
+ 'font-family': 'inherit', 'font-size': 'inherit', 'background': 'none'
1800
1949
  },
1801
- '.bw_page_item:first-child .bw_page_link': { 'margin-left': '0', 'border-top-left-radius': '0.375rem', 'border-bottom-left-radius': '0.375rem' },
1802
- '.bw_page_item:last-child .bw_page_link': { 'border-top-right-radius': '0.375rem', 'border-bottom-right-radius': '0.375rem' },
1950
+ '.bw_page_item:first-child .bw_page_link': { 'margin-left': '0' },
1803
1951
  '.bw_page_link:focus-visible': { 'z-index': '3', 'outline': '2px solid currentColor', 'outline-offset': '-2px' }
1804
1952
  },
1805
1953
 
@@ -1956,6 +2104,7 @@ var structuralRules = {
1956
2104
  '.bw_accordion_header': { 'margin': '0' },
1957
2105
  '.bw_accordion_button': {
1958
2106
  'position': 'relative', 'display': 'flex', 'align-items': 'center', 'width': '100%',
2107
+ 'padding': '0.875rem 1.25rem',
1959
2108
  'font-size': '1rem', 'font-weight': '500', 'text-align': 'left',
1960
2109
  'background-color': 'transparent', 'border': '0', 'overflow-anchor': 'none', 'cursor': 'pointer',
1961
2110
  'font-family': 'inherit'
@@ -1967,10 +2116,9 @@ var structuralRules = {
1967
2116
  'background-repeat': 'no-repeat', 'background-size': '1.25rem'
1968
2117
  },
1969
2118
  '.bw_accordion_button:not(.bw_collapsed)::after': { 'transform': 'rotate(-180deg)' },
1970
- '.bw_accordion_collapse': { 'max-height': '0', 'overflow': 'hidden' },
1971
- '.bw_accordion_collapse.bw_collapse_show': { 'max-height': 'none' },
1972
- '.bw_accordion_item:first-child': { 'border-top-left-radius': '8px', 'border-top-right-radius': '8px' },
1973
- '.bw_accordion_item:last-child': { 'border-bottom-left-radius': '8px', 'border-bottom-right-radius': '8px' }
2119
+ '.bw_accordion_body': { 'padding': '1rem 1.25rem' },
2120
+ '.bw_accordion_collapse': { 'max-height': '0', 'overflow': 'hidden', 'transition': 'max-height 0.3s ease' },
2121
+ '.bw_accordion_collapse.bw_collapse_show': { 'max-height': 'none' }
1974
2122
  },
1975
2123
 
1976
2124
  // ---- Carousel ----
@@ -2116,7 +2264,13 @@ var structuralRules = {
2116
2264
 
2117
2265
  // ---- Stat card ----
2118
2266
  statCard: {
2119
- '.bw_stat_card': { 'border-left': '4px solid transparent' },
2267
+ '.bw_stat_card': {
2268
+ 'padding': '1.25rem',
2269
+ 'border-left': '4px solid transparent',
2270
+ 'border-radius': '0.375rem',
2271
+ 'background-color': 'inherit',
2272
+ 'transition': 'transform 0.15s ease'
2273
+ },
2120
2274
  '.bw_stat_card:hover': { 'transform': 'translateY(-1px)' },
2121
2275
  '.bw_stat_icon': { 'font-size': '1.5rem', 'margin-bottom': '0.5rem' },
2122
2276
  '.bw_stat_value': { 'font-size': '2rem', 'font-weight': '700', 'line-height': '1.2' },
@@ -2479,6 +2633,20 @@ function generateUtilityRules() {
2479
2633
  rules['.list-inline-item'] = { 'display': 'inline-block' };
2480
2634
  rules['.list-inline-item:not(:last-child)'] = { 'margin-right': '.5rem' };
2481
2635
 
2636
+ // Typography — bw_ prefixed utilities via loops
2637
+ var _imp = function(p, v) { var o = {}; o[p] = v + ' !important'; return o; };
2638
+ [['fs',{'xs':'0.75rem','sm':'0.875rem','base':'1rem','lg':'1.125rem','xl':'1.25rem','2xl':'1.5rem'},'font-size'],
2639
+ ['fw',{light:'300',normal:'400',medium:'500',semibold:'600',bold:'700'},'font-weight'],
2640
+ ['lh',{tight:'1.25',normal:'1.5',relaxed:'1.75'},'line-height']
2641
+ ].forEach(function(d) { for (var dk in d[1]) rules['.bw_'+d[0]+'_'+dk] = _imp(d[2], d[1][dk]); });
2642
+
2643
+ // Flex utilities
2644
+ rules['.bw_flex'] = { 'display': 'flex' };
2645
+ rules['.bw_flex_column'] = { 'flex-direction': 'column' };
2646
+ rules['.bw_flex_wrap'] = { 'flex-wrap': 'wrap' };
2647
+ rules['.bw_flex_center'] = { 'display': 'flex', 'align-items': 'center', 'justify-content': 'center' };
2648
+ for (var gk in spacingValues) rules['.bw_gap_' + gk] = { 'gap': spacingValues[gk] + ' !important' };
2649
+
2482
2650
  // Visibility
2483
2651
  rules['.bw_visible, .visible'] = { 'visibility': 'visible !important' };
2484
2652
  rules['.bw_invisible, .invisible'] = { 'visibility': 'hidden !important' };
@@ -2539,6 +2707,26 @@ function getStructuralStyles() {
2539
2707
  return getStructuralCSS();
2540
2708
  }
2541
2709
 
2710
+ /**
2711
+ * Get CSS reset rules only (box-sizing, html/body font, reduced-motion).
2712
+ * Separate from themed/structural rules for independent injection.
2713
+ * @returns {Object} CSS rules object for the reset layer
2714
+ */
2715
+ function getResetStyles() {
2716
+ var rules = {};
2717
+ Object.assign(rules, structuralRules.base);
2718
+ // Include reduced-motion preference
2719
+ rules['@media (prefers-reduced-motion: reduce)'] = {
2720
+ '*, *::before, *::after': {
2721
+ 'animation-duration': '0.01ms !important',
2722
+ 'animation-iteration-count': '1 !important',
2723
+ 'transition-duration': '0.01ms !important',
2724
+ 'scroll-behavior': 'auto !important'
2725
+ }
2726
+ };
2727
+ return rules;
2728
+ }
2729
+
2542
2730
  // =========================================================================
2543
2731
  // defaultStyles — backward-compatible categorized view
2544
2732
  // =========================================================================
@@ -2568,60 +2756,41 @@ Object.assign({}, structuralRules, {
2568
2756
  });
2569
2757
 
2570
2758
  /**
2571
- * Generate alternate-palette CSS scoped under `.bw_theme_alt`.
2572
- * Uses the same `generateThemedCSS()` pipeline as the primary palette —
2573
- * both sides go through identical code paths.
2574
- *
2575
- * @param {string} name - Theme scope name (e.g. 'ocean'). '' for global.
2576
- * @param {Object} altPalette - From derivePalette(deriveAlternateConfig(...))
2577
- * @param {Object} layout - From resolveLayout()
2578
- * @returns {Object} CSS rules object scoped under .bw_theme_alt (+ optional .name)
2759
+ * Prefix every selector in a rules object with a scope selector.
2760
+ * Handles @media/@keyframes blocks and comma-separated selectors.
2761
+ * @param {Object} rules - CSS rules object
2762
+ * @param {string} prefix - Scope prefix (e.g. '#my-dashboard', '.bw_theme_alt')
2763
+ * @param {boolean} [compound=false] - If true, use compound selector (no space)
2764
+ * for the first segment: `#scope.bw_theme_alt .sel` vs `#scope .sel`
2765
+ * @returns {Object} New rules object with scoped selectors
2579
2766
  */
2580
- function generateAlternateCSS(name, altPalette, layout) {
2581
- // Generate themed CSS using the same pipeline as primary
2582
- var rawRules = generateThemedCSS('', altPalette, layout);
2583
-
2584
- // Re-scope every selector under .bw_theme_alt (+ optional theme name)
2585
- var altPrefix = name ? '.' + name + '.bw_theme_alt' : '.bw_theme_alt';
2586
- var altRules = {};
2587
-
2588
- for (var sel in rawRules) {
2589
- if (!rawRules.hasOwnProperty(sel)) continue;
2590
-
2767
+ function scopeRulesUnder(rules, prefix, compound) {
2768
+ var scoped = {};
2769
+ for (var sel in rules) {
2770
+ if (!rules.hasOwnProperty(sel)) continue;
2591
2771
  if (sel.charAt(0) === '@') {
2592
2772
  // @media / @keyframes — recurse into the block
2593
- var innerBlock = rawRules[sel];
2594
- var altInner = {};
2773
+ var innerBlock = rules[sel];
2774
+ var scopedInner = {};
2595
2775
  for (var innerSel in innerBlock) {
2596
2776
  if (!innerBlock.hasOwnProperty(innerSel)) continue;
2597
- altInner[altPrefix + ' ' + innerSel] = innerBlock[innerSel];
2777
+ scopedInner[_prefixSelector(innerSel, prefix)] = innerBlock[innerSel];
2598
2778
  }
2599
- altRules[sel] = altInner;
2779
+ scoped[sel] = scopedInner;
2600
2780
  } else {
2601
- // Regular selector — prefix with alt scope
2602
- // Handle comma-separated selectors
2603
- var parts = sel.split(',');
2604
- var scopedParts = [];
2605
- for (var i = 0; i < parts.length; i++) {
2606
- var s = parts[i].trim();
2607
- // 'body' selector gets special treatment: .bw_theme_alt body
2608
- if (s === 'body' || s.indexOf('body') === 0) {
2609
- scopedParts.push(altPrefix + ' ' + s);
2610
- } else {
2611
- scopedParts.push(altPrefix + ' ' + s);
2612
- }
2613
- }
2614
- altRules[scopedParts.join(', ')] = rawRules[sel];
2781
+ scoped[_prefixSelector(sel, prefix)] = rules[sel];
2615
2782
  }
2616
2783
  }
2784
+ return scoped;
2785
+ }
2617
2786
 
2618
- // Add body-level overrides for the alternate surface
2619
- altRules[altPrefix + ' body, :root' + altPrefix + ' body'] = {
2620
- 'color': altPalette.dark.base,
2621
- 'background-color': altPalette.light.base
2622
- };
2623
-
2624
- return altRules;
2787
+ function _prefixSelector(sel, prefix) {
2788
+ var parts = sel.split(',');
2789
+ var result = [];
2790
+ for (var i = 0; i < parts.length; i++) {
2791
+ result.push(prefix + ' ' + parts[i].trim());
2792
+ }
2793
+ return result.join(', ');
2625
2794
  }
2626
2795
 
2627
2796
  /**
@@ -3593,7 +3762,7 @@ function makeCol(props = {}) {
3593
3762
  if (breakpoint === 'xs') {
3594
3763
  classes.push(`bw_col_${value}`);
3595
3764
  } else {
3596
- classes.push(`bw_col_${breakpoint}-${value}`);
3765
+ classes.push(`bw_col_${breakpoint}_${value}`);
3597
3766
  }
3598
3767
  });
3599
3768
  } else if (size) {
@@ -4976,8 +5145,8 @@ function makePagination(props = {}) {
4976
5145
  t: 'li',
4977
5146
  a: { class: `bw_page_item ${currentPage <= 1 ? 'bw_disabled' : ''}`.trim() },
4978
5147
  c: {
4979
- t: 'a',
4980
- a: { class: 'bw_page_link', href: '#', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous' },
5148
+ t: 'button',
5149
+ a: { class: 'bw_page_link', type: 'button', onclick: handleClick(currentPage - 1), 'aria-label': 'Previous', disabled: currentPage <= 1 ? true : undefined },
4981
5150
  c: '\u2039'
4982
5151
  }
4983
5152
  });
@@ -4989,8 +5158,8 @@ function makePagination(props = {}) {
4989
5158
  t: 'li',
4990
5159
  a: { class: `bw_page_item ${pageNum === currentPage ? 'bw_active' : ''}`.trim() },
4991
5160
  c: {
4992
- t: 'a',
4993
- a: { class: 'bw_page_link', href: '#', onclick: handleClick(pageNum) },
5161
+ t: 'button',
5162
+ a: { class: 'bw_page_link', type: 'button', onclick: handleClick(pageNum), 'aria-current': pageNum === currentPage ? 'page' : undefined },
4994
5163
  c: '' + pageNum
4995
5164
  }
4996
5165
  });
@@ -5002,8 +5171,8 @@ function makePagination(props = {}) {
5002
5171
  t: 'li',
5003
5172
  a: { class: `bw_page_item ${currentPage >= pages ? 'bw_disabled' : ''}`.trim() },
5004
5173
  c: {
5005
- t: 'a',
5006
- a: { class: 'bw_page_link', href: '#', onclick: handleClick(currentPage + 1), 'aria-label': 'Next' },
5174
+ t: 'button',
5175
+ a: { class: 'bw_page_link', type: 'button', onclick: handleClick(currentPage + 1), 'aria-label': 'Next', disabled: currentPage >= pages ? true : undefined },
5007
5176
  c: '\u203A'
5008
5177
  }
5009
5178
  });
@@ -6876,7 +7045,11 @@ var BCCL = {
6876
7045
  function make(type, props) {
6877
7046
  var def = BCCL[type];
6878
7047
  if (!def) throw new Error('bw.make: unknown component type "' + type + '". Available: ' + Object.keys(BCCL).join(', '));
6879
- return def.make(props || {});
7048
+ var taco = def.make(props || {});
7049
+ if (taco && typeof taco === 'object') {
7050
+ taco._bwFactory = { type: type, props: props || {} };
7051
+ }
7052
+ return taco;
6880
7053
  }
6881
7054
 
6882
7055
  var components = /*#__PURE__*/Object.freeze({
@@ -6997,7 +7170,7 @@ const bw = {
6997
7170
  __monkey_patch_is_nodejs__: {
6998
7171
  _value: 'ignore',
6999
7172
  set: function(x) {
7000
- this._value = (typeof x === 'boolean') ? x : 'ignore';
7173
+ this._value = _is(x, 'boolean') ? x : 'ignore';
7001
7174
  },
7002
7175
  get: function() {
7003
7176
  return this._value;
@@ -7045,6 +7218,67 @@ Object.defineProperty(bw, '_isBrowser', {
7045
7218
  configurable: true
7046
7219
  });
7047
7220
 
7221
+ // ── Internal aliases ─────────────────────────────────────────────────────
7222
+ // Short names for frequently-used builtins and internal methods.
7223
+ // Same pattern as v1 (_to = bw.typeOf, etc.).
7224
+ //
7225
+ // Why: Terser can't shorten global property chains (console.warn,
7226
+ // Object.prototype.hasOwnProperty, Array.isArray, document.createElement)
7227
+ // because it can't prove they're side-effect-free. We can, so we alias
7228
+ // them here. Each alias saves bytes in the minified output, and the short
7229
+ // names also reduce visual noise in the hot paths (binding pipeline,
7230
+ // createDOM, etc.).
7231
+ //
7232
+ // Alias Target Sites
7233
+ // ───────── ────────────────────────────────────── ─────
7234
+ // _hop Object.prototype.hasOwnProperty 15
7235
+ // _isA Array.isArray 25
7236
+ // _keys Object.keys 7
7237
+ // _to bw.typeOf (type string) 26
7238
+ // _is type check boolean: _is(x,'string') ~50
7239
+ // _cw console.warn 8
7240
+ // _cl console.log 11
7241
+ // _ce console.error 4
7242
+ // _chp ComponentHandle.prototype 28 (defined after constructor)
7243
+ //
7244
+ // Note: document.createElement etc. are NOT aliased because they require
7245
+ // `this === document` and .bind() would add overhead on every call.
7246
+ // Console aliases use thin wrappers (not direct refs) so test monkey-
7247
+ // patching of console.warn/log/error continues to work.
7248
+ //
7249
+ // `typeof x` for UNDECLARED globals (window, document, process, require,
7250
+ // EventSource, navigator, Promise, __filename, import.meta) MUST stay as
7251
+ // raw `typeof` — calling _to(x) when x doesn't exist throws ReferenceError.
7252
+ //
7253
+ // ── v1 functional type helpers (kept for reference, not currently used) ──
7254
+ // _toa(x, type, trueVal, falseVal) — bw.typeAssign:
7255
+ // returns trueVal if _to(x)===type, else falseVal.
7256
+ // Replaces: (typeof x === 'string') ? A : B → _toa(x,'string',A,B)
7257
+ // _toc(x, type, trueVal, falseVal) — bw.typeConvert:
7258
+ // same as _toa but if trueVal/falseVal are functions, calls them with x.
7259
+ // Replaces: typeof x === 'string' ? fn(x) : default → _toc(x,'string',fn,default)
7260
+ // Uncomment if pattern frequency justifies them:
7261
+ // var _toa = function(x, t, y, n) { return _to(x) === t ? y : n; };
7262
+ // 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); };
7263
+ // ─────────────────────────────────────────────────────────────────────────
7264
+ var _hop = Object.prototype.hasOwnProperty;
7265
+ var _isA = Array.isArray;
7266
+ var _keys = Object.keys;
7267
+ var _to = typeOf; // imported from bitwrench-utils.js
7268
+ var _is = function(x, t) { var r = _to(x); return r === t || r.toLowerCase() === t; };
7269
+ // Console aliases use thin wrappers (not direct references) so that test
7270
+ // code can monkey-patch console.warn/log/error and the patches take effect.
7271
+ var _cw = function() { console.warn.apply(console, arguments); };
7272
+ var _cl = function() { console.log.apply(console, arguments); };
7273
+ var _ce = function() { console.error.apply(console, arguments); };
7274
+
7275
+ /**
7276
+ * Debug flag. When true, emits console.warn for silent binding failures
7277
+ * (missing paths, null refs, auto-created intermediate objects).
7278
+ * @type {boolean}
7279
+ */
7280
+ bw.debug = false;
7281
+
7048
7282
  /**
7049
7283
  * Lazy-resolve Node.js `fs` module.
7050
7284
  * Tries require('fs') first (available in CJS/UMD Node.js builds),
@@ -7192,7 +7426,7 @@ bw.uuid = function(prefix) {
7192
7426
  */
7193
7427
  bw._el = function(id) {
7194
7428
  // Pass-through for DOM elements
7195
- if (typeof id !== 'string') return id || null;
7429
+ if (!_is(id, 'string')) return id || null;
7196
7430
  if (!id) return null;
7197
7431
  if (!bw._isBrowser) return null;
7198
7432
 
@@ -7220,7 +7454,12 @@ bw._el = function(id) {
7220
7454
  el = document.querySelector('[data-bw_id="' + id + '"]');
7221
7455
  }
7222
7456
 
7223
- // 5. Cache the result for next time
7457
+ // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
7458
+ if (!el && id.indexOf('bw_uuid_') === 0) {
7459
+ el = document.querySelector('.' + id);
7460
+ }
7461
+
7462
+ // 6. Cache the result for next time
7224
7463
  if (el) {
7225
7464
  bw._nodeMap[id] = el;
7226
7465
  }
@@ -7273,6 +7512,84 @@ bw._deregisterNode = function(el, bwId) {
7273
7512
  }
7274
7513
  };
7275
7514
 
7515
+ // ===================================================================================
7516
+ // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
7517
+ // ===================================================================================
7518
+
7519
+ /**
7520
+ * Regex to match a bw_uuid_* token in a class string.
7521
+ * @private
7522
+ */
7523
+ var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
7524
+
7525
+ /**
7526
+ * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
7527
+ *
7528
+ * Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
7529
+ * to replace an existing UUID (useful in loops where each TACO needs a unique ID).
7530
+ *
7531
+ * @param {Object} taco - A TACO object `{t, a, c, o}`
7532
+ * @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
7533
+ * @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
7534
+ * @category Identifiers
7535
+ * @example
7536
+ * var card = bw.makeStatCard({ value: '0', label: 'Scans' });
7537
+ * var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
7538
+ * var same = bw.assignUUID(card); // same UUID (idempotent)
7539
+ * var diff = bw.assignUUID(card, true); // new UUID (forced)
7540
+ */
7541
+ bw.assignUUID = function(taco, forceNew) {
7542
+ if (!taco || !_is(taco, 'object')) return null;
7543
+
7544
+ // Ensure taco.a exists
7545
+ if (!taco.a) taco.a = {};
7546
+ if (!_is(taco.a.class, 'string')) taco.a.class = taco.a.class ? String(taco.a.class) : '';
7547
+
7548
+ var existing = taco.a.class.match(_UUID_RE);
7549
+
7550
+ if (existing && !forceNew) {
7551
+ return existing[0];
7552
+ }
7553
+
7554
+ // Remove old UUID if forceNew
7555
+ if (existing) {
7556
+ taco.a.class = taco.a.class.replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
7557
+ }
7558
+
7559
+ var uuid = bw.uuid('uuid');
7560
+ taco.a.class = (taco.a.class ? taco.a.class + ' ' : '') + uuid;
7561
+ return uuid;
7562
+ };
7563
+
7564
+ /**
7565
+ * Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
7566
+ *
7567
+ * @param {Object|Element} tacoOrElement - A TACO object or DOM element
7568
+ * @returns {string|null} The UUID string, or null if none assigned
7569
+ * @category Identifiers
7570
+ * @example
7571
+ * bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
7572
+ * bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
7573
+ * bw.getUUID({t:'div'}) // null (no UUID)
7574
+ */
7575
+ bw.getUUID = function(tacoOrElement) {
7576
+ if (!tacoOrElement) return null;
7577
+
7578
+ var classStr;
7579
+ // DOM element: check className
7580
+ if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
7581
+ classStr = tacoOrElement.className;
7582
+ }
7583
+ // TACO object: check a.class
7584
+ else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
7585
+ classStr = tacoOrElement.a.class;
7586
+ }
7587
+
7588
+ if (!classStr) return null;
7589
+ var match = classStr.match(_UUID_RE);
7590
+ return match ? match[0] : null;
7591
+ };
7592
+
7276
7593
  /**
7277
7594
  * Escape HTML special characters to prevent XSS.
7278
7595
  *
@@ -7288,7 +7605,7 @@ bw._deregisterNode = function(el, bwId) {
7288
7605
  * // => '&lt;b&gt;Hello&lt;&#x2F;b&gt; &amp; &quot;world&quot;'
7289
7606
  */
7290
7607
  bw.escapeHTML = function(str) {
7291
- if (typeof str !== 'string') return '';
7608
+ if (!_is(str, 'string')) return '';
7292
7609
 
7293
7610
  const escapeMap = {
7294
7611
  '&': '&amp;',
@@ -7322,6 +7639,42 @@ bw.raw = function(str) {
7322
7639
  return { __bw_raw: true, v: String(str) };
7323
7640
  };
7324
7641
 
7642
+ /**
7643
+ * Hyperscript-style TACO constructor.
7644
+ *
7645
+ * A convenience helper that returns a canonical TACO object from positional
7646
+ * arguments. The return value is a plain object — serializable, works with
7647
+ * bwserve, and accepted everywhere TACO is accepted.
7648
+ *
7649
+ * @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
7650
+ * @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
7651
+ * @param {*} [content] - Content: string, number, TACO object, or array of children.
7652
+ * @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
7653
+ * @returns {Object} Plain TACO object {t, a?, c?, o?}
7654
+ * @category Utilities
7655
+ * @see bw.html
7656
+ * @see bw.createDOM
7657
+ * @see bw.DOM
7658
+ * @example
7659
+ * bw.h('div')
7660
+ * // => { t: 'div' }
7661
+ *
7662
+ * bw.h('p', { class: 'bw_text_muted' }, 'Hello')
7663
+ * // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
7664
+ *
7665
+ * bw.h('ul', null, [
7666
+ * bw.h('li', null, 'one'),
7667
+ * bw.h('li', null, 'two')
7668
+ * ])
7669
+ * // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
7670
+ */
7671
+ bw.h = function(tag, attrs, content, options) {
7672
+ var taco = { t: String(tag) };
7673
+ if (attrs !== null && attrs !== undefined) taco.a = attrs;
7674
+ if (content !== undefined) taco.c = content;
7675
+ if (options !== undefined) taco.o = options;
7676
+ return taco;
7677
+ };
7325
7678
 
7326
7679
  /**
7327
7680
  * Convert a TACO object (or array of TACOs) to an HTML string.
@@ -7361,7 +7714,7 @@ bw.html = function(taco, options = {}) {
7361
7714
  }
7362
7715
 
7363
7716
  // Handle arrays of TACOs
7364
- if (Array.isArray(taco)) {
7717
+ if (_isA(taco)) {
7365
7718
  return taco.map(t => bw.html(t, options)).join('');
7366
7719
  }
7367
7720
 
@@ -7384,15 +7737,15 @@ bw.html = function(taco, options = {}) {
7384
7737
  if (taco && taco._bwEach && options.state) {
7385
7738
  var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
7386
7739
  var arr = bw._evaluatePath(options.state, eachExpr);
7387
- if (!Array.isArray(arr)) return '';
7740
+ if (!_isA(arr)) return '';
7388
7741
  return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
7389
7742
  }
7390
7743
 
7391
7744
  // Handle primitives and non-TACO objects
7392
- if (typeof taco !== 'object' || !taco.t) {
7745
+ if (!_is(taco, 'object') || !taco.t) {
7393
7746
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
7394
7747
  // Resolve template bindings if state provided
7395
- if (options.state && typeof str === 'string' && str.indexOf('${') >= 0) {
7748
+ if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
7396
7749
  str = bw._resolveTemplate(str, options.state, !!options.compile);
7397
7750
  }
7398
7751
  return str;
@@ -7412,10 +7765,18 @@ bw.html = function(taco, options = {}) {
7412
7765
  // Skip null, undefined, false
7413
7766
  if (value == null || value === false) continue;
7414
7767
 
7415
- // Skip event handlers (they're for DOM only)
7416
- if (key.startsWith('on')) continue;
7768
+ // Serialize event handlers via funcRegister
7769
+ if (key.startsWith('on')) {
7770
+ if (_is(value, 'function')) {
7771
+ var fnId = bw.funcRegister(value);
7772
+ attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
7773
+ } else if (_is(value, 'string')) {
7774
+ attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
7775
+ }
7776
+ continue;
7777
+ }
7417
7778
 
7418
- if (key === 'style' && typeof value === 'object') {
7779
+ if (key === 'style' && _is(value, 'object')) {
7419
7780
  // Convert style object to string
7420
7781
  const styleStr = Object.entries(value)
7421
7782
  .filter(([, v]) => v != null)
@@ -7426,7 +7787,7 @@ bw.html = function(taco, options = {}) {
7426
7787
  }
7427
7788
  } else if (key === 'class') {
7428
7789
  // Handle class as array or string
7429
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
7790
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
7430
7791
  if (classStr) {
7431
7792
  attrStr += ` class="${bw.escapeHTML(classStr)}"`;
7432
7793
  }
@@ -7462,13 +7823,184 @@ bw.html = function(taco, options = {}) {
7462
7823
  // Process content recursively
7463
7824
  let contentStr = content != null ? bw.html(content, options) : '';
7464
7825
  // Resolve template bindings in content if state provided
7465
- if (options.state && typeof contentStr === 'string' && contentStr.indexOf('${') >= 0) {
7826
+ if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
7466
7827
  contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
7467
7828
  }
7468
7829
 
7469
7830
  return `<${tag}${attrStr}>${contentStr}</${tag}>`;
7470
7831
  };
7471
7832
 
7833
+ /**
7834
+ * Generate a complete, self-contained HTML document from TACO content.
7835
+ *
7836
+ * Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
7837
+ * func registry emission (so serialized event handlers work), optional theme,
7838
+ * and extra head elements. Designed for static site generation, offline/airgapped
7839
+ * use, and the "static site that isn't static" workflow.
7840
+ *
7841
+ * @param {Object} [opts={}] - Page options
7842
+ * @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
7843
+ * @param {string} [opts.title='bitwrench'] - Page title
7844
+ * @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
7845
+ * @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
7846
+ * @param {string} [opts.css=''] - Additional CSS for <style> block
7847
+ * @param {string|Object} [opts.theme=null] - Theme preset name or config object
7848
+ * @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
7849
+ * @param {string} [opts.favicon=''] - Favicon URL
7850
+ * @param {string} [opts.lang='en'] - HTML lang attribute
7851
+ * @returns {string} Complete HTML document string
7852
+ * @category DOM Generation
7853
+ * @see bw.html
7854
+ * @example
7855
+ * bw.htmlPage({
7856
+ * title: 'My App',
7857
+ * body: { t: 'h1', c: 'Hello World' },
7858
+ * runtime: 'shim'
7859
+ * })
7860
+ */
7861
+ bw.htmlPage = function(opts) {
7862
+ opts = opts || {};
7863
+ var title = opts.title || 'bitwrench';
7864
+ var body = opts.body || '';
7865
+ var state = opts.state || undefined;
7866
+ var runtime = opts.runtime || 'shim';
7867
+ var css = opts.css || '';
7868
+ var theme = opts.theme || null;
7869
+ var headExtra = opts.head || [];
7870
+ var favicon = opts.favicon || '';
7871
+ var lang = opts.lang || 'en';
7872
+
7873
+ // Snapshot funcRegistry counter before rendering
7874
+ var fnCounterBefore = bw._fnIDCounter;
7875
+
7876
+ // Render body content
7877
+ var bodyHTML = '';
7878
+ if (_is(body, 'string')) {
7879
+ bodyHTML = body;
7880
+ } else {
7881
+ var htmlOpts = {};
7882
+ if (state) htmlOpts.state = state;
7883
+ bodyHTML = bw.html(body, htmlOpts);
7884
+ }
7885
+
7886
+ // Collect functions registered during this render
7887
+ var fnCounterAfter = bw._fnIDCounter;
7888
+ var registryEntries = '';
7889
+ for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
7890
+ var fnKey = 'bw_fn_' + i;
7891
+ if (bw._fnRegistry[fnKey]) {
7892
+ registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' +
7893
+ bw._fnRegistry[fnKey].toString() + ';\n';
7894
+ }
7895
+ }
7896
+
7897
+ // Build runtime script for <head>
7898
+ var runtimeHead = '';
7899
+ if (runtime === 'inline') {
7900
+ // Read UMD bundle synchronously if in Node.js
7901
+ var umdSource = null;
7902
+ if (bw._isNode) {
7903
+ try {
7904
+ var fs = (typeof require === 'function') ? require('fs') : null;
7905
+ var pathMod = (typeof require === 'function') ? require('path') : null;
7906
+ if (fs && pathMod) {
7907
+ // Resolve dist/ relative to this source file
7908
+ var srcDir = '';
7909
+ try { srcDir = pathMod.dirname((typeof __filename !== 'undefined') ? __filename : ''); }
7910
+ catch(e2) { /* ESM: __filename not available */ }
7911
+ if (!srcDir && typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench.cjs.js', document.baseURI).href)) }) !== 'undefined' && (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench.cjs.js', document.baseURI).href))) {
7912
+ var url = (typeof require === 'function') ? require('url') : null;
7913
+ if (url && url.fileURLToPath) srcDir = pathMod.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench.cjs.js', document.baseURI).href))));
7914
+ }
7915
+ if (srcDir) {
7916
+ var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
7917
+ umdSource = fs.readFileSync(distPath, 'utf8');
7918
+ }
7919
+ }
7920
+ } catch(e) { /* fall through */ }
7921
+ }
7922
+ if (umdSource) {
7923
+ runtimeHead = '<script>' + umdSource + '</script>';
7924
+ } else {
7925
+ // Fallback to shim in browser or if dist not available
7926
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
7927
+ }
7928
+ } else if (runtime === 'cdn') {
7929
+ runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
7930
+ } else if (runtime === 'shim') {
7931
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
7932
+ }
7933
+ // runtime === 'none' → empty
7934
+
7935
+ // Theme CSS
7936
+ var themeCSS = '';
7937
+ if (theme) {
7938
+ var themeConfig = _is(theme, 'string')
7939
+ ? (THEME_PRESETS[theme.toLowerCase()] || null)
7940
+ : theme;
7941
+ if (themeConfig) {
7942
+ var themeResult = bw.makeStyles(themeConfig);
7943
+ themeCSS = themeResult.css;
7944
+ }
7945
+ }
7946
+
7947
+ // Extra <head> elements
7948
+ var headHTML = '';
7949
+ if (_isA(headExtra) && headExtra.length > 0) {
7950
+ headHTML = headExtra.map(function(el) { return bw.html(el); }).join('\n');
7951
+ }
7952
+
7953
+ // Favicon
7954
+ var faviconTag = '';
7955
+ if (favicon) {
7956
+ var safeFavicon = favicon.replace(/[&<>"']/g, function(c) {
7957
+ return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
7958
+ });
7959
+ faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
7960
+ }
7961
+
7962
+ // Escaped title
7963
+ var safeTitle = bw.escapeHTML(title);
7964
+
7965
+ // Combine all CSS
7966
+ var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
7967
+
7968
+ // Body-end script: registry entries + optional loadStyles
7969
+ var bodyEndScript = '';
7970
+ var bodyEndParts = [];
7971
+ if (registryEntries) {
7972
+ bodyEndParts.push(registryEntries);
7973
+ }
7974
+ if (runtime === 'inline' || runtime === 'cdn') {
7975
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
7976
+ }
7977
+ if (bodyEndParts.length > 0) {
7978
+ bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
7979
+ }
7980
+
7981
+ // Assemble document
7982
+ var parts = [
7983
+ '<!DOCTYPE html>',
7984
+ '<html lang="' + lang + '">',
7985
+ '<head>',
7986
+ '<meta charset="UTF-8">',
7987
+ '<meta name="viewport" content="width=device-width, initial-scale=1">'
7988
+ ];
7989
+ parts.push('<title>' + safeTitle + '</title>');
7990
+ if (faviconTag) parts.push(faviconTag);
7991
+ if (runtimeHead) parts.push(runtimeHead);
7992
+ if (headHTML) parts.push(headHTML);
7993
+ if (allCSS) parts.push('<style>' + allCSS + '</style>');
7994
+ parts.push('</head>');
7995
+ parts.push('<body>');
7996
+ parts.push(bodyHTML);
7997
+ if (bodyEndScript) parts.push(bodyEndScript);
7998
+ parts.push('</body>');
7999
+ parts.push('</html>');
8000
+
8001
+ return parts.join('\n');
8002
+ };
8003
+
7472
8004
  /**
7473
8005
  * Create a live DOM element from a TACO object (browser only).
7474
8006
  *
@@ -7513,7 +8045,7 @@ bw.createDOM = function(taco, options = {}) {
7513
8045
  }
7514
8046
 
7515
8047
  // Handle text nodes
7516
- if (typeof taco !== 'object' || !taco.t) {
8048
+ if (!_is(taco, 'object') || !taco.t) {
7517
8049
  return document.createTextNode(String(taco));
7518
8050
  }
7519
8051
 
@@ -7526,16 +8058,16 @@ bw.createDOM = function(taco, options = {}) {
7526
8058
  for (const [key, value] of Object.entries(attrs)) {
7527
8059
  if (value == null || value === false) continue;
7528
8060
 
7529
- if (key === 'style' && typeof value === 'object') {
8061
+ if (key === 'style' && _is(value, 'object')) {
7530
8062
  // Apply styles directly
7531
8063
  Object.assign(el.style, value);
7532
8064
  } else if (key === 'class') {
7533
8065
  // Handle class as array or string
7534
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
8066
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
7535
8067
  if (classStr) {
7536
8068
  el.className = classStr;
7537
8069
  }
7538
- } else if (key.startsWith('on') && typeof value === 'function') {
8070
+ } else if (key.startsWith('on') && _is(value, 'function')) {
7539
8071
  // Event handlers
7540
8072
  const eventName = key.slice(2).toLowerCase();
7541
8073
  el.addEventListener(eventName, value);
@@ -7555,7 +8087,7 @@ bw.createDOM = function(taco, options = {}) {
7555
8087
  // Children with data-bw_id or id attributes get local refs on the parent,
7556
8088
  // so o.render functions can access them without any DOM lookup.
7557
8089
  if (content != null) {
7558
- if (Array.isArray(content)) {
8090
+ if (_isA(content)) {
7559
8091
  content.forEach(child => {
7560
8092
  if (child != null) {
7561
8093
  // Handle ComponentHandle in content arrays (Level 2 children)
@@ -7575,20 +8107,20 @@ bw.createDOM = function(taco, options = {}) {
7575
8107
  if (childEl._bw_refs) {
7576
8108
  if (!el._bw_refs) el._bw_refs = {};
7577
8109
  for (var rk in childEl._bw_refs) {
7578
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
8110
+ if (_hop.call(childEl._bw_refs, rk)) {
7579
8111
  el._bw_refs[rk] = childEl._bw_refs[rk];
7580
8112
  }
7581
8113
  }
7582
8114
  }
7583
8115
  }
7584
8116
  });
7585
- } else if (typeof content === 'object' && content.__bw_raw) {
8117
+ } else if (_is(content, 'object') && content.__bw_raw) {
7586
8118
  // Raw HTML content — inject via innerHTML
7587
8119
  el.innerHTML = content.v;
7588
8120
  } else if (content._bwComponent === true) {
7589
8121
  // Single ComponentHandle as content
7590
8122
  content.mount(el);
7591
- } else if (typeof content === 'object' && content.t) {
8123
+ } else if (_is(content, 'object') && content.t) {
7592
8124
  var childEl = bw.createDOM(content, options);
7593
8125
  el.appendChild(childEl);
7594
8126
  var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
@@ -7599,7 +8131,7 @@ bw.createDOM = function(taco, options = {}) {
7599
8131
  if (childEl._bw_refs) {
7600
8132
  if (!el._bw_refs) el._bw_refs = {};
7601
8133
  for (var rk in childEl._bw_refs) {
7602
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
8134
+ if (_hop.call(childEl._bw_refs, rk)) {
7603
8135
  el._bw_refs[rk] = childEl._bw_refs[rk];
7604
8136
  }
7605
8137
  }
@@ -7614,6 +8146,14 @@ bw.createDOM = function(taco, options = {}) {
7614
8146
  bw._registerNode(el, null);
7615
8147
  }
7616
8148
 
8149
+ // Register UUID class in node cache (bw_uuid_* tokens in class string)
8150
+ if (el.className) {
8151
+ var uuidMatch = el.className.match(_UUID_RE);
8152
+ if (uuidMatch) {
8153
+ bw._nodeMap[uuidMatch[0]] = el;
8154
+ }
8155
+ }
8156
+
7617
8157
  // Handle lifecycle hooks and state
7618
8158
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
7619
8159
  const id = attrs['data-bw_id'] || bw.uuid();
@@ -7632,7 +8172,7 @@ bw.createDOM = function(taco, options = {}) {
7632
8172
  el._bw_render = opts.render;
7633
8173
 
7634
8174
  if (opts.mounted) {
7635
- console.warn('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
8175
+ _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
7636
8176
  }
7637
8177
 
7638
8178
  // Queue initial render (same timing as mounted)
@@ -7705,7 +8245,7 @@ bw.DOM = function(target, taco, options = {}) {
7705
8245
  const targetEl = bw._el(target);
7706
8246
 
7707
8247
  if (!targetEl) {
7708
- console.error('bw.DOM: Target element not found:', target);
8248
+ _ce('bw.DOM: Target element not found:', target);
7709
8249
  return null;
7710
8250
  }
7711
8251
 
@@ -7745,7 +8285,7 @@ bw.DOM = function(target, taco, options = {}) {
7745
8285
  targetEl.appendChild(taco.element);
7746
8286
  }
7747
8287
  // Handle arrays
7748
- else if (Array.isArray(taco)) {
8288
+ else if (_isA(taco)) {
7749
8289
  taco.forEach(t => {
7750
8290
  if (t != null) {
7751
8291
  if (t._bwComponent === true) {
@@ -7781,7 +8321,7 @@ bw.DOM = function(target, taco, options = {}) {
7781
8321
  bw.compileProps = function(handle, props = {}) {
7782
8322
  const compiledProps = {};
7783
8323
 
7784
- Object.keys(props).forEach(key => {
8324
+ _keys(props).forEach(key => {
7785
8325
  // Create getter/setter for each prop
7786
8326
  Object.defineProperty(compiledProps, key, {
7787
8327
  get() {
@@ -7986,6 +8526,16 @@ bw.renderComponent = function(taco, options = {}) {
7986
8526
  bw.cleanup = function(element) {
7987
8527
  if (!bw._isBrowser || !element) return;
7988
8528
 
8529
+ // Deregister UUID classes from node cache (element + descendants)
8530
+ // Covers elements that have UUID but no data-bw_id
8531
+ var selfUuidMatch = element.className && element.className.match(_UUID_RE);
8532
+ if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
8533
+ var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
8534
+ uuidEls.forEach(function(uel) {
8535
+ var m = uel.className && uel.className.match(_UUID_RE);
8536
+ if (m) delete bw._nodeMap[m[0]];
8537
+ });
8538
+
7989
8539
  // Find all elements with data-bw_id
7990
8540
  const elements = element.querySelectorAll('[data-bw_id]');
7991
8541
 
@@ -8001,6 +8551,10 @@ bw.cleanup = function(element) {
8001
8551
  // Deregister from node cache
8002
8552
  bw._deregisterNode(el, id);
8003
8553
 
8554
+ // Deregister UUID class from node cache
8555
+ var uuidMatch = el.className && el.className.match(_UUID_RE);
8556
+ if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
8557
+
8004
8558
  // Clean up pub/sub subscriptions tied to this element
8005
8559
  if (el._bw_subs) {
8006
8560
  el._bw_subs.forEach(function(unsub) { unsub(); });
@@ -8025,6 +8579,10 @@ bw.cleanup = function(element) {
8025
8579
  // Deregister from node cache
8026
8580
  bw._deregisterNode(element, id);
8027
8581
 
8582
+ // Deregister UUID class from node cache
8583
+ var elemUuidMatch = element.className && element.className.match(_UUID_RE);
8584
+ if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
8585
+
8028
8586
  // Clean up pub/sub subscriptions tied to element itself
8029
8587
  if (element._bw_subs) {
8030
8588
  element._bw_subs.forEach(function(unsub) { unsub(); });
@@ -8099,17 +8657,17 @@ bw.patch = function(id, content, attr) {
8099
8657
  if (attr) {
8100
8658
  // Patch an attribute
8101
8659
  el.setAttribute(attr, String(content));
8102
- } else if (Array.isArray(content)) {
8660
+ } else if (_isA(content)) {
8103
8661
  // Patch with array of children (strings and/or TACOs)
8104
8662
  el.innerHTML = '';
8105
8663
  content.forEach(function(item) {
8106
- if (typeof item === 'string' || typeof item === 'number') {
8664
+ if (_is(item, 'string') || _is(item, 'number')) {
8107
8665
  el.appendChild(document.createTextNode(String(item)));
8108
8666
  } else if (item && item.t) {
8109
8667
  el.appendChild(bw.createDOM(item));
8110
8668
  }
8111
8669
  });
8112
- } else if (typeof content === 'object' && content !== null && content.t) {
8670
+ } else if (_is(content, 'object') && content.t) {
8113
8671
  // Patch with a TACO — replace children
8114
8672
  el.innerHTML = '';
8115
8673
  el.appendChild(bw.createDOM(content));
@@ -8140,7 +8698,7 @@ bw.patch = function(id, content, attr) {
8140
8698
  bw.patchAll = function(patches) {
8141
8699
  var results = {};
8142
8700
  for (var id in patches) {
8143
- if (Object.prototype.hasOwnProperty.call(patches, id)) {
8701
+ if (_hop.call(patches, id)) {
8144
8702
  results[id] = bw.patch(id, patches[id]);
8145
8703
  }
8146
8704
  }
@@ -8237,7 +8795,7 @@ bw.pub = function(topic, detail) {
8237
8795
  snapshot[i].handler(detail);
8238
8796
  called++;
8239
8797
  } catch (err) {
8240
- console.warn('bw.pub: subscriber error on topic "' + topic + '":', err);
8798
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
8241
8799
  }
8242
8800
  }
8243
8801
  return called;
@@ -8333,8 +8891,8 @@ bw._fnIDCounter = 0;
8333
8891
  * @see bw.funcGetDispatchStr
8334
8892
  */
8335
8893
  bw.funcRegister = function(fn, name) {
8336
- if (typeof fn !== 'function') return '';
8337
- var fnID = (typeof name === 'string' && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
8894
+ if (!_is(fn, 'function')) return '';
8895
+ var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
8338
8896
  bw._fnRegistry[fnID] = fn;
8339
8897
  return fnID;
8340
8898
  };
@@ -8353,7 +8911,7 @@ bw.funcRegister = function(fn, name) {
8353
8911
  bw.funcGetById = function(name, errFn) {
8354
8912
  name = String(name);
8355
8913
  if (name in bw._fnRegistry) return bw._fnRegistry[name];
8356
- return (typeof errFn === 'function') ? errFn : function() { console.warn('bw.funcGetById: unregistered fn "' + name + '"'); };
8914
+ return _is(errFn, 'function') ? errFn : function() { _cw('bw.funcGetById: unregistered fn "' + name + '"'); };
8357
8915
  };
8358
8916
 
8359
8917
  /**
@@ -8394,13 +8952,30 @@ bw.funcUnregister = function(name) {
8394
8952
  bw.funcGetRegistry = function() {
8395
8953
  var copy = {};
8396
8954
  for (var k in bw._fnRegistry) {
8397
- if (Object.prototype.hasOwnProperty.call(bw._fnRegistry, k)) {
8955
+ if (_hop.call(bw._fnRegistry, k)) {
8398
8956
  copy[k] = bw._fnRegistry[k];
8399
8957
  }
8400
8958
  }
8401
8959
  return copy;
8402
8960
  };
8403
8961
 
8962
+ /**
8963
+ * Minimal runtime shim for funcRegister dispatch in static HTML.
8964
+ * When embedded in a `<script>` tag, provides just enough infrastructure
8965
+ * for `bw.funcGetById()` calls to resolve. The actual function bodies
8966
+ * are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
8967
+ * @type {string}
8968
+ * @category Function Registry
8969
+ */
8970
+ bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' +
8971
+ 'if(!bw._fnRegistry)bw._fnRegistry={};' +
8972
+ 'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' +
8973
+ 'console.warn("bw: unregistered fn "+n)};};' +
8974
+ 'bw.funcRegister=function(fn,name){' +
8975
+ 'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' +
8976
+ 'bw._fnRegistry[id]=fn;return id;};' +
8977
+ 'window.bw=bw;})();';
8978
+
8404
8979
  // ===================================================================================
8405
8980
  // Template Binding Utilities
8406
8981
  // ===================================================================================
@@ -8428,7 +9003,10 @@ bw._evaluatePath = function(state, path) {
8428
9003
  var parts = path.split('.');
8429
9004
  var val = state;
8430
9005
  for (var i = 0; i < parts.length; i++) {
8431
- if (val == null) return '';
9006
+ if (val == null) {
9007
+ if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
9008
+ return '';
9009
+ }
8432
9010
  val = val[parts[i]];
8433
9011
  }
8434
9012
  return (val == null) ? '' : val;
@@ -8448,7 +9026,7 @@ bw._evaluatePath = function(state, path) {
8448
9026
  */
8449
9027
  bw._compiledExprs = {};
8450
9028
  bw._resolveTemplate = function(str, state, compile) {
8451
- if (typeof str !== 'string' || str.indexOf('${') < 0) return str;
9029
+ if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
8452
9030
  var bindings = bw._parseBindings(str);
8453
9031
  if (bindings.length === 0) return str;
8454
9032
 
@@ -8470,6 +9048,7 @@ bw._resolveTemplate = function(str, state, compile) {
8470
9048
  try {
8471
9049
  val = bw._compiledExprs[b.expr](state);
8472
9050
  } catch (e) {
9051
+ if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
8473
9052
  val = '';
8474
9053
  }
8475
9054
  } else {
@@ -8578,7 +9157,7 @@ function ComponentHandle(taco) {
8578
9157
  this._state = {};
8579
9158
  if (o.state) {
8580
9159
  for (var k in o.state) {
8581
- if (Object.prototype.hasOwnProperty.call(o.state, k)) {
9160
+ if (_hop.call(o.state, k)) {
8582
9161
  this._state[k] = o.state[k];
8583
9162
  }
8584
9163
  }
@@ -8587,7 +9166,7 @@ function ComponentHandle(taco) {
8587
9166
  this._actions = {};
8588
9167
  if (o.actions) {
8589
9168
  for (var k2 in o.actions) {
8590
- if (Object.prototype.hasOwnProperty.call(o.actions, k2)) {
9169
+ if (_hop.call(o.actions, k2)) {
8591
9170
  this._actions[k2] = o.actions[k2];
8592
9171
  }
8593
9172
  }
@@ -8597,7 +9176,7 @@ function ComponentHandle(taco) {
8597
9176
  if (o.methods) {
8598
9177
  var self = this;
8599
9178
  for (var k3 in o.methods) {
8600
- if (Object.prototype.hasOwnProperty.call(o.methods, k3)) {
9179
+ if (_hop.call(o.methods, k3)) {
8601
9180
  this._methods[k3] = o.methods[k3];
8602
9181
  (function(methodName, methodFn) {
8603
9182
  self[methodName] = function() {
@@ -8615,7 +9194,7 @@ function ComponentHandle(taco) {
8615
9194
  willMount: o.willMount || null,
8616
9195
  mounted: o.mounted || null,
8617
9196
  willUpdate: o.willUpdate || null,
8618
- onUpdate: o.onUpdate || null,
9197
+ onUpdate: o.onUpdate || o.updated || null,
8619
9198
  unmount: o.unmount || null,
8620
9199
  willDestroy: o.willDestroy || null
8621
9200
  };
@@ -8630,14 +9209,23 @@ function ComponentHandle(taco) {
8630
9209
  this._compile = !!o.compile;
8631
9210
  this._bw_refs = {};
8632
9211
  this._refCounter = 0;
9212
+ // Child component ownership (Bug #5)
9213
+ this._children = [];
9214
+ this._parent = null;
9215
+ // Factory metadata for BCCL rebuild (Bug #6)
9216
+ this._factory = taco._bwFactory || null;
8633
9217
  }
8634
9218
 
9219
+ // Short alias for ComponentHandle.prototype (see alias block at top of file).
9220
+ // 28 method definitions × 25 chars = ~700B raw savings in minified output.
9221
+ var _chp = ComponentHandle.prototype;
9222
+
8635
9223
  // ── State Methods ──
8636
9224
 
8637
9225
  /**
8638
9226
  * Get a state value. Dot-path supported: `get('user.name')`
8639
9227
  */
8640
- ComponentHandle.prototype.get = function(key) {
9228
+ _chp.get = function(key) {
8641
9229
  return bw._evaluatePath(this._state, key);
8642
9230
  };
8643
9231
 
@@ -8647,12 +9235,13 @@ ComponentHandle.prototype.get = function(key) {
8647
9235
  * @param {*} value - New value
8648
9236
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
8649
9237
  */
8650
- ComponentHandle.prototype.set = function(key, value, opts) {
9238
+ _chp.set = function(key, value, opts) {
8651
9239
  // Dot-path set
8652
9240
  var parts = key.split('.');
8653
9241
  var obj = this._state;
8654
9242
  for (var i = 0; i < parts.length - 1; i++) {
8655
- if (obj[parts[i]] == null || typeof obj[parts[i]] !== 'object') {
9243
+ if (!_is(obj[parts[i]], 'object')) {
9244
+ if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
8656
9245
  obj[parts[i]] = {};
8657
9246
  }
8658
9247
  obj = obj[parts[i]];
@@ -8672,10 +9261,10 @@ ComponentHandle.prototype.set = function(key, value, opts) {
8672
9261
  /**
8673
9262
  * Get a shallow clone of the full state.
8674
9263
  */
8675
- ComponentHandle.prototype.getState = function() {
9264
+ _chp.getState = function() {
8676
9265
  var clone = {};
8677
9266
  for (var k in this._state) {
8678
- if (Object.prototype.hasOwnProperty.call(this._state, k)) {
9267
+ if (_hop.call(this._state, k)) {
8679
9268
  clone[k] = this._state[k];
8680
9269
  }
8681
9270
  }
@@ -8687,9 +9276,9 @@ ComponentHandle.prototype.getState = function() {
8687
9276
  * @param {Object} updates - Key-value pairs to merge
8688
9277
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
8689
9278
  */
8690
- ComponentHandle.prototype.setState = function(updates, opts) {
9279
+ _chp.setState = function(updates, opts) {
8691
9280
  for (var k in updates) {
8692
- if (Object.prototype.hasOwnProperty.call(updates, k)) {
9281
+ if (_hop.call(updates, k)) {
8693
9282
  this._state[k] = updates[k];
8694
9283
  this._dirtyKeys[k] = true;
8695
9284
  }
@@ -8706,9 +9295,9 @@ ComponentHandle.prototype.setState = function(updates, opts) {
8706
9295
  /**
8707
9296
  * Push a value onto an array in state. Clones the array.
8708
9297
  */
8709
- ComponentHandle.prototype.push = function(key, val) {
9298
+ _chp.push = function(key, val) {
8710
9299
  var arr = this.get(key);
8711
- var newArr = Array.isArray(arr) ? arr.slice() : [];
9300
+ var newArr = _isA(arr) ? arr.slice() : [];
8712
9301
  newArr.push(val);
8713
9302
  this.set(key, newArr);
8714
9303
  };
@@ -8716,9 +9305,9 @@ ComponentHandle.prototype.push = function(key, val) {
8716
9305
  /**
8717
9306
  * Splice an array in state. Clones the array.
8718
9307
  */
8719
- ComponentHandle.prototype.splice = function(key, start, deleteCount) {
9308
+ _chp.splice = function(key, start, deleteCount) {
8720
9309
  var arr = this.get(key);
8721
- var newArr = Array.isArray(arr) ? arr.slice() : [];
9310
+ var newArr = _isA(arr) ? arr.slice() : [];
8722
9311
  var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
8723
9312
  Array.prototype.splice.apply(newArr, args);
8724
9313
  this.set(key, newArr);
@@ -8726,7 +9315,7 @@ ComponentHandle.prototype.splice = function(key, start, deleteCount) {
8726
9315
 
8727
9316
  // ── Scheduling ──
8728
9317
 
8729
- ComponentHandle.prototype._scheduleDirty = function() {
9318
+ _chp._scheduleDirty = function() {
8730
9319
  if (!this._scheduled) {
8731
9320
  this._scheduled = true;
8732
9321
  bw._dirtyComponents.push(this);
@@ -8741,17 +9330,17 @@ ComponentHandle.prototype._scheduleDirty = function() {
8741
9330
  * Creates binding descriptors with refIds for targeted DOM updates.
8742
9331
  * @private
8743
9332
  */
8744
- ComponentHandle.prototype._compileBindings = function() {
9333
+ _chp._compileBindings = function() {
8745
9334
  this._bindings = [];
8746
9335
  this._refCounter = 0;
8747
- var stateKeys = Object.keys(this._state);
9336
+ var stateKeys = _keys(this._state);
8748
9337
  var self = this;
8749
9338
 
8750
9339
  function walkTaco(taco, path) {
8751
- if (taco == null || typeof taco !== 'object' || !taco.t) return taco;
9340
+ if (!_is(taco, 'object') || !taco.t) return taco;
8752
9341
 
8753
9342
  // Check content for bindings
8754
- if (typeof taco.c === 'string' && taco.c.indexOf('${') >= 0) {
9343
+ if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
8755
9344
  var refId = 'bw_ref_' + self._refCounter++;
8756
9345
  var parsed = bw._parseBindings(taco.c);
8757
9346
  var deps = [];
@@ -8773,10 +9362,10 @@ ComponentHandle.prototype._compileBindings = function() {
8773
9362
  // Check attributes for bindings
8774
9363
  if (taco.a) {
8775
9364
  for (var attrName in taco.a) {
8776
- if (!Object.prototype.hasOwnProperty.call(taco.a, attrName)) continue;
9365
+ if (!_hop.call(taco.a, attrName)) continue;
8777
9366
  if (attrName === 'data-bw_ref') continue;
8778
9367
  var attrVal = taco.a[attrName];
8779
- if (typeof attrVal === 'string' && attrVal.indexOf('${') >= 0) {
9368
+ if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
8780
9369
  var refId2 = 'bw_ref_' + self._refCounter++;
8781
9370
  var parsed2 = bw._parseBindings(attrVal);
8782
9371
  var deps2 = [];
@@ -8802,9 +9391,27 @@ ComponentHandle.prototype._compileBindings = function() {
8802
9391
  }
8803
9392
 
8804
9393
  // Recurse into children
8805
- if (Array.isArray(taco.c)) {
9394
+ if (_isA(taco.c)) {
8806
9395
  for (var i = 0; i < taco.c.length; i++) {
8807
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
9396
+ // Wrap string children with ${expr} in a span so patches target the span, not the parent
9397
+ if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
9398
+ var mixedRefId = 'bw_ref_' + self._refCounter++;
9399
+ var mixedParsed = bw._parseBindings(taco.c[i]);
9400
+ var mixedDeps = [];
9401
+ for (var mi = 0; mi < mixedParsed.length; mi++) {
9402
+ mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
9403
+ }
9404
+ self._bindings.push({
9405
+ expr: taco.c[i],
9406
+ type: 'content',
9407
+ refId: mixedRefId,
9408
+ deps: mixedDeps,
9409
+ template: taco.c[i]
9410
+ });
9411
+ // Replace string with a span wrapper so textContent targets the span only
9412
+ taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
9413
+ }
9414
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
8808
9415
  walkTaco(taco.c[i], path.concat(i));
8809
9416
  }
8810
9417
  // Handle bw.when/bw.each markers
@@ -8839,7 +9446,7 @@ ComponentHandle.prototype._compileBindings = function() {
8839
9446
  taco.c[i]._refId = eachRefId;
8840
9447
  }
8841
9448
  }
8842
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9449
+ } else if (_is(taco.c, 'object') && taco.c.t) {
8843
9450
  walkTaco(taco.c, path.concat(0));
8844
9451
  }
8845
9452
 
@@ -8855,7 +9462,7 @@ ComponentHandle.prototype._compileBindings = function() {
8855
9462
  * Build ref map from the live DOM after createDOM.
8856
9463
  * @private
8857
9464
  */
8858
- ComponentHandle.prototype._collectRefs = function() {
9465
+ _chp._collectRefs = function() {
8859
9466
  this._bw_refs = {};
8860
9467
  if (!this.element) return;
8861
9468
  var els = this.element.querySelectorAll('[data-bw_ref]');
@@ -8876,7 +9483,7 @@ ComponentHandle.prototype._collectRefs = function() {
8876
9483
  * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
8877
9484
  * @param {Element} parentEl - DOM element to mount into
8878
9485
  */
8879
- ComponentHandle.prototype.mount = function(parentEl) {
9486
+ _chp.mount = function(parentEl) {
8880
9487
  // willMount hook
8881
9488
  if (this._hooks.willMount) this._hooks.willMount(this);
8882
9489
 
@@ -8898,7 +9505,7 @@ ComponentHandle.prototype.mount = function(parentEl) {
8898
9505
  // Register named actions in function registry
8899
9506
  var self = this;
8900
9507
  for (var actionName in this._actions) {
8901
- if (Object.prototype.hasOwnProperty.call(this._actions, actionName)) {
9508
+ if (_hop.call(this._actions, actionName)) {
8902
9509
  var registeredName = this._bwId + '_' + actionName;
8903
9510
  (function(aName) {
8904
9511
  bw.funcRegister(function(evt) {
@@ -8917,6 +9524,11 @@ ComponentHandle.prototype.mount = function(parentEl) {
8917
9524
  this.element = bw.createDOM(tacoForDOM);
8918
9525
  this.element._bwComponentHandle = this;
8919
9526
  this.element.setAttribute('data-bw_comp_id', this._bwId);
9527
+
9528
+ // Restore o.render from original TACO (stripped by _tacoForDOM)
9529
+ if (this.taco.o && this.taco.o.render) {
9530
+ this.element._bw_render = this.taco.o.render;
9531
+ }
8920
9532
  if (this._userTag) {
8921
9533
  this.element.classList.add(this._userTag);
8922
9534
  }
@@ -8932,6 +9544,16 @@ ComponentHandle.prototype.mount = function(parentEl) {
8932
9544
 
8933
9545
  this.mounted = true;
8934
9546
 
9547
+ // Scan for child ComponentHandles and link parent/child (Bug #5)
9548
+ var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
9549
+ for (var ci = 0; ci < childEls.length; ci++) {
9550
+ var ch = childEls[ci]._bwComponentHandle;
9551
+ if (ch && ch !== this && !ch._parent) {
9552
+ ch._parent = this;
9553
+ this._children.push(ch);
9554
+ }
9555
+ }
9556
+
8935
9557
  // mounted hook (backward compat: fn.length === 2 wraps (el, state))
8936
9558
  if (this._hooks.mounted) {
8937
9559
  if (this._hooks.mounted.length === 2) {
@@ -8940,16 +9562,21 @@ ComponentHandle.prototype.mount = function(parentEl) {
8940
9562
  this._hooks.mounted(this);
8941
9563
  }
8942
9564
  }
9565
+
9566
+ // Invoke o.render on initial mount (if present)
9567
+ if (this.element._bw_render) {
9568
+ this.element._bw_render(this.element, this._state);
9569
+ }
8943
9570
  };
8944
9571
 
8945
9572
  /**
8946
9573
  * Prepare TACO for initial render: resolve when/each markers.
8947
9574
  * @private
8948
9575
  */
8949
- ComponentHandle.prototype._prepareTaco = function(taco) {
8950
- if (!taco || typeof taco !== 'object') return;
9576
+ _chp._prepareTaco = function(taco) {
9577
+ if (!_is(taco, 'object')) return;
8951
9578
 
8952
- if (Array.isArray(taco.c)) {
9579
+ if (_isA(taco.c)) {
8953
9580
  for (var i = taco.c.length - 1; i >= 0; i--) {
8954
9581
  var child = taco.c[i];
8955
9582
  if (child && child._bwWhen) {
@@ -8974,18 +9601,18 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
8974
9601
  var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
8975
9602
  var arr = bw._evaluatePath(this._state, eachExprStr);
8976
9603
  var items = [];
8977
- if (Array.isArray(arr)) {
9604
+ if (_isA(arr)) {
8978
9605
  for (var j = 0; j < arr.length; j++) {
8979
9606
  items.push(child.factory(arr[j], j));
8980
9607
  }
8981
9608
  }
8982
9609
  taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
8983
9610
  }
8984
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
9611
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
8985
9612
  this._prepareTaco(taco.c[i]);
8986
9613
  }
8987
9614
  }
8988
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9615
+ } else if (_is(taco.c, 'object') && taco.c.t) {
8989
9616
  this._prepareTaco(taco.c);
8990
9617
  }
8991
9618
  };
@@ -8994,12 +9621,12 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
8994
9621
  * Wire action name strings (in onclick etc.) to dispatch function calls.
8995
9622
  * @private
8996
9623
  */
8997
- ComponentHandle.prototype._wireActions = function(taco) {
8998
- if (!taco || typeof taco !== 'object' || !taco.t) return;
9624
+ _chp._wireActions = function(taco) {
9625
+ if (!_is(taco, 'object') || !taco.t) return;
8999
9626
  if (taco.a) {
9000
9627
  for (var key in taco.a) {
9001
- if (!Object.prototype.hasOwnProperty.call(taco.a, key)) continue;
9002
- if (key.startsWith('on') && typeof taco.a[key] === 'string') {
9628
+ if (!_hop.call(taco.a, key)) continue;
9629
+ if (key.startsWith('on') && _is(taco.a[key], 'string')) {
9003
9630
  var actionName = taco.a[key];
9004
9631
  if (actionName in this._actions) {
9005
9632
  var registeredName = this._bwId + '_' + actionName;
@@ -9013,11 +9640,11 @@ ComponentHandle.prototype._wireActions = function(taco) {
9013
9640
  }
9014
9641
  }
9015
9642
  }
9016
- if (Array.isArray(taco.c)) {
9643
+ if (_isA(taco.c)) {
9017
9644
  for (var i = 0; i < taco.c.length; i++) {
9018
9645
  this._wireActions(taco.c[i]);
9019
9646
  }
9020
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
9647
+ } else if (_is(taco.c, 'object') && taco.c.t) {
9021
9648
  this._wireActions(taco.c);
9022
9649
  }
9023
9650
  };
@@ -9026,7 +9653,7 @@ ComponentHandle.prototype._wireActions = function(taco) {
9026
9653
  * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
9027
9654
  * @private
9028
9655
  */
9029
- ComponentHandle.prototype._deepCloneTaco = function(taco) {
9656
+ _chp._deepCloneTaco = function(taco) {
9030
9657
  if (taco == null) return taco;
9031
9658
  // Preserve _bwWhen / _bwEach markers (contain functions)
9032
9659
  if (taco._bwWhen) {
@@ -9038,18 +9665,18 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
9038
9665
  if (taco._bwEach) {
9039
9666
  return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
9040
9667
  }
9041
- if (typeof taco !== 'object' || !taco.t) return taco;
9668
+ if (!_is(taco, 'object') || !taco.t) return taco;
9042
9669
  var result = { t: taco.t };
9043
9670
  if (taco.a) {
9044
9671
  result.a = {};
9045
9672
  for (var k in taco.a) {
9046
- if (Object.prototype.hasOwnProperty.call(taco.a, k)) result.a[k] = taco.a[k];
9673
+ if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
9047
9674
  }
9048
9675
  }
9049
9676
  if (taco.c != null) {
9050
- if (Array.isArray(taco.c)) {
9677
+ if (_isA(taco.c)) {
9051
9678
  result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
9052
- } else if (typeof taco.c === 'object') {
9679
+ } else if (_is(taco.c, 'object')) {
9053
9680
  result.c = this._deepCloneTaco(taco.c);
9054
9681
  } else {
9055
9682
  result.c = taco.c;
@@ -9063,27 +9690,31 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
9063
9690
  * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
9064
9691
  * @private
9065
9692
  */
9066
- ComponentHandle.prototype._tacoForDOM = function(taco) {
9067
- if (!taco || typeof taco !== 'object' || !taco.t) return taco;
9693
+ _chp._tacoForDOM = function(taco) {
9694
+ if (!_is(taco, 'object') || !taco.t) return taco;
9068
9695
  var result = { t: taco.t };
9069
9696
  if (taco.a) result.a = taco.a;
9070
9697
  if (taco.c != null) {
9071
- if (Array.isArray(taco.c)) {
9698
+ if (_isA(taco.c)) {
9072
9699
  result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
9073
- } else if (typeof taco.c === 'object' && taco.c.t) {
9700
+ } else if (_is(taco.c, 'object') && taco.c.t) {
9074
9701
  result.c = this._tacoForDOM(taco.c);
9075
9702
  } else {
9076
9703
  result.c = taco.c;
9077
9704
  }
9078
9705
  }
9079
9706
  // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
9707
+ if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
9708
+ _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
9709
+ '>. Use onclick attribute or bw.component() for child interactivity.');
9710
+ }
9080
9711
  return result;
9081
9712
  };
9082
9713
 
9083
9714
  /**
9084
9715
  * Unmount: remove from DOM, deactivate, preserve state for re-mount.
9085
9716
  */
9086
- ComponentHandle.prototype.unmount = function() {
9717
+ _chp.unmount = function() {
9087
9718
  if (!this.mounted) return;
9088
9719
 
9089
9720
  // unmount hook
@@ -9118,12 +9749,23 @@ ComponentHandle.prototype.unmount = function() {
9118
9749
  /**
9119
9750
  * Destroy: unmount + clear state + unregister actions.
9120
9751
  */
9121
- ComponentHandle.prototype.destroy = function() {
9752
+ _chp.destroy = function() {
9122
9753
  // willDestroy hook
9123
9754
  if (this._hooks.willDestroy) {
9124
9755
  this._hooks.willDestroy(this);
9125
9756
  }
9126
9757
 
9758
+ // Cascade destroy to children depth-first (Bug #5)
9759
+ for (var ci = this._children.length - 1; ci >= 0; ci--) {
9760
+ this._children[ci].destroy();
9761
+ }
9762
+ this._children = [];
9763
+ if (this._parent) {
9764
+ var idx = this._parent._children.indexOf(this);
9765
+ if (idx >= 0) this._parent._children.splice(idx, 1);
9766
+ this._parent = null;
9767
+ }
9768
+
9127
9769
  this.unmount();
9128
9770
 
9129
9771
  // Unregister actions from function registry
@@ -9150,12 +9792,36 @@ ComponentHandle.prototype.destroy = function() {
9150
9792
  * Flush dirty state: resolve changed bindings and apply to DOM.
9151
9793
  * @private
9152
9794
  */
9153
- ComponentHandle.prototype._flush = function() {
9795
+ _chp._flush = function() {
9154
9796
  this._scheduled = false;
9155
- var changedKeys = Object.keys(this._dirtyKeys);
9797
+ var changedKeys = _keys(this._dirtyKeys);
9156
9798
  this._dirtyKeys = {};
9157
9799
  if (changedKeys.length === 0 || !this.mounted) return;
9158
9800
 
9801
+ // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
9802
+ // rebuild the TACO from the factory with merged state (Bug #6)
9803
+ if (this._factory) {
9804
+ var rebuildNeeded = false;
9805
+ for (var fi = 0; fi < changedKeys.length; fi++) {
9806
+ if (_hop.call(this._factory.props, changedKeys[fi])) {
9807
+ rebuildNeeded = true; break;
9808
+ }
9809
+ }
9810
+ if (rebuildNeeded) {
9811
+ var merged = {};
9812
+ for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
9813
+ for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
9814
+ this._factory.props = merged;
9815
+ var newTaco = bw.make(this._factory.type, merged);
9816
+ newTaco._bwFactory = this._factory;
9817
+ this.taco = newTaco;
9818
+ this._originalTaco = this._deepCloneTaco(newTaco);
9819
+ this._render();
9820
+ if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
9821
+ return;
9822
+ }
9823
+ }
9824
+
9159
9825
  // willUpdate hook
9160
9826
  if (this._hooks.willUpdate) {
9161
9827
  this._hooks.willUpdate(this, changedKeys);
@@ -9194,7 +9860,7 @@ ComponentHandle.prototype._flush = function() {
9194
9860
  * Returns list of patches to apply.
9195
9861
  * @private
9196
9862
  */
9197
- ComponentHandle.prototype._resolveBindings = function(changedKeys) {
9863
+ _chp._resolveBindings = function(changedKeys) {
9198
9864
  var patches = [];
9199
9865
  for (var i = 0; i < this._bindings.length; i++) {
9200
9866
  var b = this._bindings[i];
@@ -9230,11 +9896,14 @@ ComponentHandle.prototype._resolveBindings = function(changedKeys) {
9230
9896
  * Apply patches to DOM.
9231
9897
  * @private
9232
9898
  */
9233
- ComponentHandle.prototype._applyPatches = function(patches) {
9899
+ _chp._applyPatches = function(patches) {
9234
9900
  for (var i = 0; i < patches.length; i++) {
9235
9901
  var p = patches[i];
9236
9902
  var el = this._bw_refs[p.refId];
9237
- if (!el) continue;
9903
+ if (!el) {
9904
+ if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
9905
+ continue;
9906
+ }
9238
9907
  if (p.type === 'content') {
9239
9908
  el.textContent = p.value;
9240
9909
  } else if (p.type === 'attribute') {
@@ -9251,7 +9920,7 @@ ComponentHandle.prototype._applyPatches = function(patches) {
9251
9920
  * Resolve all bindings and apply (used for initial render).
9252
9921
  * @private
9253
9922
  */
9254
- ComponentHandle.prototype._resolveAndApplyAll = function() {
9923
+ _chp._resolveAndApplyAll = function() {
9255
9924
  var patches = [];
9256
9925
  for (var i = 0; i < this._bindings.length; i++) {
9257
9926
  var b = this._bindings[i];
@@ -9274,7 +9943,7 @@ ComponentHandle.prototype._resolveAndApplyAll = function() {
9274
9943
  * Full re-render for structural changes (when/each branch switches).
9275
9944
  * @private
9276
9945
  */
9277
- ComponentHandle.prototype._render = function() {
9946
+ _chp._render = function() {
9278
9947
  if (!this.element || !this.element.parentNode) return;
9279
9948
  var parent = this.element.parentNode;
9280
9949
  var nextSibling = this.element.nextSibling;
@@ -9314,7 +9983,7 @@ ComponentHandle.prototype._render = function() {
9314
9983
  * @param {string} event - Event name (e.g., 'click')
9315
9984
  * @param {Function} handler - Event handler
9316
9985
  */
9317
- ComponentHandle.prototype.on = function(event, handler) {
9986
+ _chp.on = function(event, handler) {
9318
9987
  if (this.element) {
9319
9988
  this.element.addEventListener(event, handler);
9320
9989
  }
@@ -9326,7 +9995,7 @@ ComponentHandle.prototype.on = function(event, handler) {
9326
9995
  * @param {string} event - Event name
9327
9996
  * @param {Function} handler - Handler to remove
9328
9997
  */
9329
- ComponentHandle.prototype.off = function(event, handler) {
9998
+ _chp.off = function(event, handler) {
9330
9999
  if (this.element) {
9331
10000
  this.element.removeEventListener(event, handler);
9332
10001
  }
@@ -9341,7 +10010,7 @@ ComponentHandle.prototype.off = function(event, handler) {
9341
10010
  * @param {Function} handler - Handler function
9342
10011
  * @returns {Function} Unsubscribe function
9343
10012
  */
9344
- ComponentHandle.prototype.sub = function(topic, handler) {
10013
+ _chp.sub = function(topic, handler) {
9345
10014
  var unsub = bw.sub(topic, handler);
9346
10015
  this._subs.push(unsub);
9347
10016
  return unsub;
@@ -9352,10 +10021,10 @@ ComponentHandle.prototype.sub = function(topic, handler) {
9352
10021
  * @param {string} name - Action name
9353
10022
  * @param {...*} args - Arguments passed after comp
9354
10023
  */
9355
- ComponentHandle.prototype.action = function(name) {
10024
+ _chp.action = function(name) {
9356
10025
  var fn = this._actions[name];
9357
10026
  if (!fn) {
9358
- console.warn('ComponentHandle.action: unknown action "' + name + '"');
10027
+ _cw('ComponentHandle.action: unknown action "' + name + '"');
9359
10028
  return;
9360
10029
  }
9361
10030
  var args = [this].concat(Array.prototype.slice.call(arguments, 1));
@@ -9367,7 +10036,7 @@ ComponentHandle.prototype.action = function(name) {
9367
10036
  * @param {string} sel - CSS selector
9368
10037
  * @returns {Element|null}
9369
10038
  */
9370
- ComponentHandle.prototype.select = function(sel) {
10039
+ _chp.select = function(sel) {
9371
10040
  return this.element ? this.element.querySelector(sel) : null;
9372
10041
  };
9373
10042
 
@@ -9376,7 +10045,7 @@ ComponentHandle.prototype.select = function(sel) {
9376
10045
  * @param {string} sel - CSS selector
9377
10046
  * @returns {Element[]}
9378
10047
  */
9379
- ComponentHandle.prototype.selectAll = function(sel) {
10048
+ _chp.selectAll = function(sel) {
9380
10049
  if (!this.element) return [];
9381
10050
  return Array.prototype.slice.call(this.element.querySelectorAll(sel));
9382
10051
  };
@@ -9387,7 +10056,7 @@ ComponentHandle.prototype.selectAll = function(sel) {
9387
10056
  * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
9388
10057
  * @returns {ComponentHandle} this (for chaining)
9389
10058
  */
9390
- ComponentHandle.prototype.userTag = function(tag) {
10059
+ _chp.userTag = function(tag) {
9391
10060
  this._userTag = tag;
9392
10061
  if (this.element) {
9393
10062
  this.element.classList.add(tag);
@@ -9464,7 +10133,7 @@ bw.component = function(taco) {
9464
10133
  * and calls the named method. This is the bitwrench equivalent of
9465
10134
  * Win32 SendMessage(hwnd, msg, wParam, lParam).
9466
10135
  *
9467
- * @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
10136
+ * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
9468
10137
  * @param {string} action - Method name to call on the component
9469
10138
  * @param {*} data - Data to pass to the method
9470
10139
  * @returns {boolean} True if message was dispatched successfully
@@ -9481,15 +10150,20 @@ bw.component = function(taco) {
9481
10150
  * };
9482
10151
  */
9483
10152
  bw.message = function(target, action, data) {
9484
- // Try data-bw_comp_id attribute first, then CSS class (user tag)
9485
- var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
9486
- if (!el) {
10153
+ // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
10154
+ var el = bw._el(target);
10155
+ // Then try data-bw_comp_id attribute
10156
+ if (!el || !el._bwComponentHandle) {
10157
+ el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
10158
+ }
10159
+ // Then try CSS class (user tag)
10160
+ if (!el || !el._bwComponentHandle) {
9487
10161
  el = bw.$('.' + target)[0];
9488
10162
  }
9489
10163
  if (!el || !el._bwComponentHandle) return false;
9490
10164
  var comp = el._bwComponentHandle;
9491
- if (typeof comp[action] !== 'function') {
9492
- console.warn('bw.message: unknown action "' + action + '" on component ' + target);
10165
+ if (!_is(comp[action], 'function')) {
10166
+ _cw('bw.message: unknown action "' + action + '" on component ' + target);
9493
10167
  return false;
9494
10168
  }
9495
10169
  comp[action](data);
@@ -9497,59 +10171,24 @@ bw.message = function(target, action, data) {
9497
10171
  };
9498
10172
 
9499
10173
  // ===================================================================================
9500
- // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
10174
+ // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
9501
10175
  // ===================================================================================
9502
10176
 
9503
10177
  /**
9504
10178
  * Registry of named functions sent via register messages.
9505
- * Populated by clientApply({ type: 'register', name, body }).
9506
- * Invoked by clientApply({ type: 'call', name, args }).
10179
+ * Populated by bw.apply({ type: 'register', name, body }).
10180
+ * Invoked by bw.apply({ type: 'call', name, args }).
9507
10181
  * @private
9508
10182
  */
9509
10183
  bw._clientFunctions = {};
9510
10184
 
9511
10185
  /**
9512
- * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
10186
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
9513
10187
  * Default false — exec messages are rejected unless explicitly opted in.
9514
10188
  * @private
9515
10189
  */
9516
10190
  bw._allowExec = false;
9517
10191
 
9518
- /**
9519
- * Built-in client functions available via call() without registration.
9520
- * @private
9521
- */
9522
- bw._builtinClientFunctions = {
9523
- scrollTo: function(selector) {
9524
- var el = bw._el(selector);
9525
- if (el) el.scrollTop = el.scrollHeight;
9526
- },
9527
- focus: function(selector) {
9528
- var el = bw._el(selector);
9529
- if (el && typeof el.focus === 'function') el.focus();
9530
- },
9531
- download: function(filename, content, mimeType) {
9532
- if (typeof document === 'undefined') return;
9533
- var blob = new Blob([content], { type: mimeType || 'text/plain' });
9534
- var a = document.createElement('a');
9535
- a.href = URL.createObjectURL(blob);
9536
- a.download = filename;
9537
- a.click();
9538
- URL.revokeObjectURL(a.href);
9539
- },
9540
- clipboard: function(text) {
9541
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
9542
- navigator.clipboard.writeText(text);
9543
- }
9544
- },
9545
- redirect: function(url) {
9546
- if (typeof window !== 'undefined') window.location.href = url;
9547
- },
9548
- log: function() {
9549
- console.log.apply(console, arguments);
9550
- }
9551
- };
9552
-
9553
10192
  /**
9554
10193
  * Parse a bwserve protocol message string, supporting both strict JSON
9555
10194
  * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
@@ -9564,9 +10203,9 @@ bw._builtinClientFunctions = {
9564
10203
  * @param {string} str - JSON or r-prefixed relaxed JSON string
9565
10204
  * @returns {Object} Parsed message object
9566
10205
  * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
9567
- * @category Server
10206
+ * @category Core
9568
10207
  */
9569
- bw.clientParse = function(str) {
10208
+ bw.parseJSONFlex = function(str) {
9570
10209
  str = (str || '').trim();
9571
10210
  if (str.charAt(0) !== 'r') return JSON.parse(str);
9572
10211
  str = str.slice(1);
@@ -9651,10 +10290,10 @@ bw.clientParse = function(str) {
9651
10290
  * append — target.appendChild(bw.createDOM(node))
9652
10291
  * remove — bw.cleanup(target); target.remove()
9653
10292
  * patch — bw.patch(target, content, attr)
9654
- * batch — iterate ops, call clientApply for each
10293
+ * batch — iterate ops, call bw.apply for each
9655
10294
  * message — bw.message(target, action, data)
9656
10295
  * register — store a named function for later call()
9657
- * call — invoke a registered or built-in function
10296
+ * call — invoke a registered function
9658
10297
  * exec — execute arbitrary JS (requires allowExec)
9659
10298
  *
9660
10299
  * Target resolution:
@@ -9663,9 +10302,9 @@ bw.clientParse = function(str) {
9663
10302
  *
9664
10303
  * @param {Object} msg - Protocol message
9665
10304
  * @returns {boolean} true if the message was applied successfully
9666
- * @category Server
10305
+ * @category Core
9667
10306
  */
9668
- bw.clientApply = function(msg) {
10307
+ bw.apply = function(msg) {
9669
10308
  if (!msg || !msg.type) return false;
9670
10309
 
9671
10310
  var type = msg.type;
@@ -9691,15 +10330,15 @@ bw.clientApply = function(msg) {
9691
10330
  } else if (type === 'remove') {
9692
10331
  var toRemove = bw._el(target);
9693
10332
  if (!toRemove) return false;
9694
- if (typeof bw.cleanup === 'function') bw.cleanup(toRemove);
10333
+ if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
9695
10334
  toRemove.remove();
9696
10335
  return true;
9697
10336
 
9698
10337
  } else if (type === 'batch') {
9699
- if (!Array.isArray(msg.ops)) return false;
10338
+ if (!_isA(msg.ops)) return false;
9700
10339
  var allOk = true;
9701
10340
  msg.ops.forEach(function(op) {
9702
- if (!bw.clientApply(op)) allOk = false;
10341
+ if (!bw.apply(op)) allOk = false;
9703
10342
  });
9704
10343
  return allOk;
9705
10344
 
@@ -9712,26 +10351,26 @@ bw.clientApply = function(msg) {
9712
10351
  bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
9713
10352
  return true;
9714
10353
  } catch (e) {
9715
- console.error('[bw] register error:', msg.name, e);
10354
+ _ce('[bw] register error:', msg.name, e);
9716
10355
  return false;
9717
10356
  }
9718
10357
 
9719
10358
  } else if (type === 'call') {
9720
10359
  if (!msg.name) return false;
9721
- var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
9722
- if (typeof fn !== 'function') return false;
10360
+ var fn = bw._clientFunctions[msg.name];
10361
+ if (!_is(fn, 'function')) return false;
9723
10362
  try {
9724
- var args = Array.isArray(msg.args) ? msg.args : [];
10363
+ var args = _isA(msg.args) ? msg.args : [];
9725
10364
  fn.apply(null, args);
9726
10365
  return true;
9727
10366
  } catch (e) {
9728
- console.error('[bw] call error:', msg.name, e);
10367
+ _ce('[bw] call error:', msg.name, e);
9729
10368
  return false;
9730
10369
  }
9731
10370
 
9732
10371
  } else if (type === 'exec') {
9733
10372
  if (!bw._allowExec) {
9734
- console.warn('[bw] exec rejected: allowExec is not enabled');
10373
+ _cw('[bw] exec rejected: allowExec is not enabled');
9735
10374
  return false;
9736
10375
  }
9737
10376
  if (!msg.code) return false;
@@ -9739,7 +10378,7 @@ bw.clientApply = function(msg) {
9739
10378
  new Function(msg.code)();
9740
10379
  return true;
9741
10380
  } catch (e) {
9742
- console.error('[bw] exec error:', e);
10381
+ _ce('[bw] exec error:', e);
9743
10382
  return false;
9744
10383
  }
9745
10384
  }
@@ -9747,139 +10386,6 @@ bw.clientApply = function(msg) {
9747
10386
  return false;
9748
10387
  };
9749
10388
 
9750
- /**
9751
- * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
9752
- *
9753
- * Returns a connection object with sendAction(), on(), and close() methods.
9754
- *
9755
- * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
9756
- * @param {Object} [opts] - Connection options
9757
- * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
9758
- * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
9759
- * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
9760
- * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
9761
- * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
9762
- * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
9763
- * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
9764
- * @returns {Object} Connection object { sendAction, on, close, status }
9765
- * @category Server
9766
- */
9767
- bw.clientConnect = function(url, opts) {
9768
- opts = opts || {};
9769
- var transport = opts.transport || 'sse';
9770
- var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
9771
- var reconnect = opts.reconnect !== false;
9772
- var onStatus = opts.onStatus || function() {};
9773
- var onMessage = opts.onMessage || null;
9774
- var handlers = {};
9775
- // Set the global allowExec flag from connection options
9776
- bw._allowExec = !!opts.allowExec;
9777
- var conn = {
9778
- status: 'connecting',
9779
- _es: null,
9780
- _pollTimer: null
9781
- };
9782
-
9783
- function setStatus(s) {
9784
- conn.status = s;
9785
- onStatus(s);
9786
- }
9787
-
9788
- function handleMessage(data) {
9789
- try {
9790
- var msg = typeof data === 'string' ? bw.clientParse(data) : data;
9791
- if (onMessage) onMessage(msg);
9792
- if (handlers.message) handlers.message(msg);
9793
- bw.clientApply(msg);
9794
- } catch (e) {
9795
- if (handlers.error) handlers.error(e);
9796
- }
9797
- }
9798
-
9799
- if (transport === 'sse' && typeof EventSource !== 'undefined') {
9800
- setStatus('connecting');
9801
- var es = new EventSource(url);
9802
- conn._es = es;
9803
-
9804
- es.onopen = function() {
9805
- setStatus('connected');
9806
- if (handlers.open) handlers.open();
9807
- };
9808
-
9809
- es.onmessage = function(e) {
9810
- handleMessage(e.data);
9811
- };
9812
-
9813
- es.onerror = function() {
9814
- if (conn.status === 'connected') {
9815
- setStatus('disconnected');
9816
- }
9817
- if (handlers.error) handlers.error(new Error('SSE connection error'));
9818
- if (!reconnect) {
9819
- es.close();
9820
- }
9821
- // EventSource auto-reconnects by default when reconnect=true
9822
- };
9823
- } else if (transport === 'poll') {
9824
- var interval = opts.interval || 2000;
9825
- setStatus('connected');
9826
- conn._pollTimer = setInterval(function() {
9827
- fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
9828
- if (Array.isArray(msgs)) {
9829
- msgs.forEach(handleMessage);
9830
- } else if (msgs && msgs.type) {
9831
- handleMessage(msgs);
9832
- }
9833
- }).catch(function(e) {
9834
- if (handlers.error) handlers.error(e);
9835
- });
9836
- }, interval);
9837
- }
9838
-
9839
- /**
9840
- * Send an action to the server via POST.
9841
- * @param {string} action - Action name
9842
- * @param {Object} [data] - Action payload
9843
- */
9844
- conn.sendAction = function(action, data) {
9845
- var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
9846
- fetch(actionUrl, {
9847
- method: 'POST',
9848
- headers: { 'Content-Type': 'application/json' },
9849
- body: body
9850
- }).catch(function(e) {
9851
- if (handlers.error) handlers.error(e);
9852
- });
9853
- };
9854
-
9855
- /**
9856
- * Register an event handler.
9857
- * @param {string} event - 'open'|'message'|'error'|'close'
9858
- * @param {Function} handler
9859
- */
9860
- conn.on = function(event, handler) {
9861
- handlers[event] = handler;
9862
- return conn;
9863
- };
9864
-
9865
- /**
9866
- * Close the connection.
9867
- */
9868
- conn.close = function() {
9869
- if (conn._es) {
9870
- conn._es.close();
9871
- conn._es = null;
9872
- }
9873
- if (conn._pollTimer) {
9874
- clearInterval(conn._pollTimer);
9875
- conn._pollTimer = null;
9876
- }
9877
- setStatus('disconnected');
9878
- if (handlers.close) handlers.close();
9879
- };
9880
-
9881
- return conn;
9882
- };
9883
10389
 
9884
10390
  // ===================================================================================
9885
10391
  // bw.inspect() — Debug utility
@@ -9907,33 +10413,33 @@ bw.inspect = function(target) {
9907
10413
  el = target.element;
9908
10414
  comp = target;
9909
10415
  } else {
9910
- if (typeof target === 'string') {
10416
+ if (_is(target, 'string')) {
9911
10417
  el = bw.$(target)[0];
9912
10418
  }
9913
10419
  if (!el) {
9914
- console.warn('bw.inspect: element not found');
10420
+ _cw('bw.inspect: element not found');
9915
10421
  return null;
9916
10422
  }
9917
10423
  comp = el._bwComponentHandle;
9918
10424
  }
9919
10425
  if (!comp) {
9920
- console.log('bw.inspect: no ComponentHandle on this element');
9921
- console.log(' Tag:', el.tagName);
9922
- console.log(' Classes:', el.className);
9923
- console.log(' _bw_state:', el._bw_state || '(none)');
10426
+ _cl('bw.inspect: no ComponentHandle on this element');
10427
+ _cl(' Tag:', el.tagName);
10428
+ _cl(' Classes:', el.className);
10429
+ _cl(' _bw_state:', el._bw_state || '(none)');
9924
10430
  return null;
9925
10431
  }
9926
10432
  var deps = comp._bindings.reduce(function(s, b) {
9927
10433
  return s.concat(b.deps || []);
9928
10434
  }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
9929
10435
  console.group('Component: ' + comp._bwId);
9930
- console.log('State:', comp._state);
9931
- console.log('Bindings:', comp._bindings.length, '(deps:', deps, ')');
9932
- console.log('Methods:', Object.keys(comp._methods));
9933
- console.log('Actions:', Object.keys(comp._actions));
9934
- console.log('User tag:', comp._userTag || '(none)');
9935
- console.log('Mounted:', comp.mounted);
9936
- console.log('Element:', comp.element);
10436
+ _cl('State:', comp._state);
10437
+ _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
10438
+ _cl('Methods:', _keys(comp._methods));
10439
+ _cl('Actions:', _keys(comp._actions));
10440
+ _cl('User tag:', comp._userTag || '(none)');
10441
+ _cl('Mounted:', comp.mounted);
10442
+ _cl('Element:', comp.element);
9937
10443
  console.groupEnd();
9938
10444
  return comp;
9939
10445
  };
@@ -9956,8 +10462,8 @@ bw.compile = function(taco) {
9956
10462
  // Pre-extract all binding expressions
9957
10463
  var precompiled = [];
9958
10464
  function walkExpressions(node) {
9959
- if (!node || typeof node !== 'object') return;
9960
- if (typeof node.c === 'string' && node.c.indexOf('${') >= 0) {
10465
+ if (!_is(node, 'object')) return;
10466
+ if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
9961
10467
  var parsed = bw._parseBindings(node.c);
9962
10468
  for (var i = 0; i < parsed.length; i++) {
9963
10469
  try {
@@ -9972,9 +10478,9 @@ bw.compile = function(taco) {
9972
10478
  }
9973
10479
  if (node.a) {
9974
10480
  for (var key in node.a) {
9975
- if (Object.prototype.hasOwnProperty.call(node.a, key)) {
10481
+ if (_hop.call(node.a, key)) {
9976
10482
  var v = node.a[key];
9977
- if (typeof v === 'string' && v.indexOf('${') >= 0) {
10483
+ if (_is(v, 'string') && v.indexOf('${') >= 0) {
9978
10484
  var parsed2 = bw._parseBindings(v);
9979
10485
  for (var j = 0; j < parsed2.length; j++) {
9980
10486
  try {
@@ -9990,9 +10496,9 @@ bw.compile = function(taco) {
9990
10496
  }
9991
10497
  }
9992
10498
  }
9993
- if (Array.isArray(node.c)) {
10499
+ if (_isA(node.c)) {
9994
10500
  for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
9995
- } else if (node.c && typeof node.c === 'object' && node.c.t) {
10501
+ } else if (_is(node.c, 'object') && node.c.t) {
9996
10502
  walkExpressions(node.c);
9997
10503
  }
9998
10504
  }
@@ -10004,7 +10510,7 @@ bw.compile = function(taco) {
10004
10510
  handle._precompiledBindings = precompiled;
10005
10511
  if (initialState) {
10006
10512
  for (var k in initialState) {
10007
- if (Object.prototype.hasOwnProperty.call(initialState, k)) {
10513
+ if (_hop.call(initialState, k)) {
10008
10514
  handle._state[k] = initialState[k];
10009
10515
  }
10010
10516
  }
@@ -10035,18 +10541,18 @@ bw.compile = function(taco) {
10035
10541
  bw.css = function(rules, options = {}) {
10036
10542
  const { minify = false, pretty = !minify } = options;
10037
10543
 
10038
- if (typeof rules === 'string') return rules;
10544
+ if (_is(rules, 'string')) return rules;
10039
10545
 
10040
10546
  let css = '';
10041
10547
  const indent = pretty ? ' ' : '';
10042
10548
  const newline = pretty ? '\n' : '';
10043
10549
  const space = pretty ? ' ' : '';
10044
10550
 
10045
- if (Array.isArray(rules)) {
10551
+ if (_isA(rules)) {
10046
10552
  css = rules.map(rule => bw.css(rule, options)).join(newline);
10047
- } else if (typeof rules === 'object') {
10553
+ } else if (_is(rules, 'object')) {
10048
10554
  Object.entries(rules).forEach(([selector, styles]) => {
10049
- if (typeof styles === 'object' && !Array.isArray(styles)) {
10555
+ if (_is(styles, 'object')) {
10050
10556
  // Handle @media, @keyframes, @supports — recurse into nested block
10051
10557
  if (selector.charAt(0) === '@') {
10052
10558
  const inner = bw.css(styles, options);
@@ -10088,14 +10594,14 @@ bw.css = function(rules, options = {}) {
10088
10594
  * @returns {Element} The style element
10089
10595
  * @category CSS & Styling
10090
10596
  * @see bw.css
10091
- * @see bw.loadDefaultStyles
10597
+ * @see bw.loadStyles
10092
10598
  * @example
10093
10599
  * bw.injectCSS('.my-class { color: red; }');
10094
10600
  * bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
10095
10601
  */
10096
10602
  bw.injectCSS = function(css, options = {}) {
10097
10603
  if (!bw._isBrowser) {
10098
- console.warn('bw.injectCSS requires a DOM environment');
10604
+ _cw('bw.injectCSS requires a DOM environment');
10099
10605
  return null;
10100
10606
  }
10101
10607
 
@@ -10112,7 +10618,7 @@ bw.injectCSS = function(css, options = {}) {
10112
10618
  }
10113
10619
 
10114
10620
  // Convert CSS if needed
10115
- const cssStr = typeof css === 'string' ? css : bw.css(css, options);
10621
+ const cssStr = _is(css, 'string') ? css : bw.css(css, options);
10116
10622
 
10117
10623
  // Set or append CSS
10118
10624
  if (append && styleEl.textContent) {
@@ -10133,113 +10639,19 @@ bw.injectCSS = function(css, options = {}) {
10133
10639
  * @param {...Object} styles - Style objects to merge (left-to-right)
10134
10640
  * @returns {Object} Merged style object
10135
10641
  * @category CSS & Styling
10136
- * @see bw.u
10137
10642
  * @example
10138
- * var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
10643
+ * var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
10139
10644
  * // => { display: 'flex', gap: '1rem', color: 'red' }
10140
10645
  */
10141
10646
  bw.s = function() {
10142
10647
  var result = {};
10143
10648
  for (var i = 0; i < arguments.length; i++) {
10144
10649
  var arg = arguments[i];
10145
- if (arg && typeof arg === 'object') Object.assign(result, arg);
10650
+ if (_is(arg, 'object')) Object.assign(result, arg);
10146
10651
  }
10147
10652
  return result;
10148
10653
  };
10149
10654
 
10150
- /**
10151
- * Pre-built CSS utility objects (like Tailwind utilities, but in JS).
10152
- *
10153
- * Compose with `bw.s()` to build inline styles without writing raw CSS strings.
10154
- * Includes flex, padding, margin, typography, color, border, and transition utilities.
10155
- *
10156
- * @category CSS & Styling
10157
- * @see bw.s
10158
- * @example
10159
- * { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
10160
- * c: 'Flexbox with 1rem gap and padding' }
10161
- */
10162
- bw.u = {
10163
- // Display
10164
- flex: { display: 'flex' },
10165
- flexCol: { display: 'flex', flexDirection: 'column' },
10166
- flexRow: { display: 'flex', flexDirection: 'row' },
10167
- flexWrap: { display: 'flex', flexWrap: 'wrap' },
10168
- block: { display: 'block' },
10169
- inline: { display: 'inline' },
10170
- hidden: { display: 'none' },
10171
-
10172
- // Flex alignment
10173
- justifyCenter: { justifyContent: 'center' },
10174
- justifyBetween: { justifyContent: 'space-between' },
10175
- justifyEnd: { justifyContent: 'flex-end' },
10176
- alignCenter: { alignItems: 'center' },
10177
- alignStart: { alignItems: 'flex-start' },
10178
- alignEnd: { alignItems: 'flex-end' },
10179
-
10180
- // Gap (0.25rem increments)
10181
- gap1: { gap: '0.25rem' },
10182
- gap2: { gap: '0.5rem' },
10183
- gap3: { gap: '0.75rem' },
10184
- gap4: { gap: '1rem' },
10185
- gap6: { gap: '1.5rem' },
10186
- gap8: { gap: '2rem' },
10187
-
10188
- // Padding
10189
- p0: { padding: '0' },
10190
- p1: { padding: '0.25rem' },
10191
- p2: { padding: '0.5rem' },
10192
- p3: { padding: '0.75rem' },
10193
- p4: { padding: '1rem' },
10194
- p6: { padding: '1.5rem' },
10195
- p8: { padding: '2rem' },
10196
- px4: { paddingLeft: '1rem', paddingRight: '1rem' },
10197
- py2: { paddingTop: '0.5rem', paddingBottom: '0.5rem' },
10198
- py4: { paddingTop: '1rem', paddingBottom: '1rem' },
10199
-
10200
- // Margin (same scale)
10201
- m0: { margin: '0' },
10202
- m4: { margin: '1rem' },
10203
- mt2: { marginTop: '0.5rem' },
10204
- mt4: { marginTop: '1rem' },
10205
- mb2: { marginBottom: '0.5rem' },
10206
- mb4: { marginBottom: '1rem' },
10207
- mx_auto: { marginLeft: 'auto', marginRight: 'auto' },
10208
-
10209
- // Typography
10210
- textSm: { fontSize: '0.875rem' },
10211
- textBase: { fontSize: '1rem' },
10212
- textLg: { fontSize: '1.125rem' },
10213
- textXl: { fontSize: '1.25rem' },
10214
- text2xl: { fontSize: '1.5rem' },
10215
- text3xl: { fontSize: '1.875rem' },
10216
- bold: { fontWeight: '700' },
10217
- semibold: { fontWeight: '600' },
10218
- italic: { fontStyle: 'italic' },
10219
- textCenter: { textAlign: 'center' },
10220
- textRight: { textAlign: 'right' },
10221
-
10222
- // Colors (from design tokens)
10223
- bgWhite: { background: '#ffffff' },
10224
- bgTeal: { background: '#006666', color: '#ffffff' },
10225
- textWhite: { color: '#ffffff' },
10226
- textTeal: { color: '#006666' },
10227
- textMuted: { color: '#888' },
10228
-
10229
- // Borders
10230
- rounded: { borderRadius: '0.375rem' },
10231
- roundedLg: { borderRadius: '0.5rem' },
10232
- roundedFull: { borderRadius: '9999px' },
10233
- border: { border: '1px solid #d8d8d8' },
10234
-
10235
- // Sizing
10236
- wFull: { width: '100%' },
10237
- hFull: { height: '100%' },
10238
-
10239
- // Transitions
10240
- transition: { transition: 'all 0.2s ease' }
10241
- };
10242
-
10243
10655
  /**
10244
10656
  * Generate responsive CSS with media query breakpoints.
10245
10657
  *
@@ -10265,7 +10677,7 @@ bw.u = {
10265
10677
  bw.responsive = function(selector, breakpoints) {
10266
10678
  var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
10267
10679
  var parts = [];
10268
- Object.keys(breakpoints).forEach(function(key) {
10680
+ _keys(breakpoints).forEach(function(key) {
10269
10681
  var rules = {};
10270
10682
  if (key === 'base') {
10271
10683
  rules[selector] = breakpoints[key];
@@ -10337,18 +10749,18 @@ if (bw._isBrowser) {
10337
10749
  if (!selector) return [];
10338
10750
 
10339
10751
  // Already an array
10340
- if (Array.isArray(selector)) return selector;
10752
+ if (_isA(selector)) return selector;
10341
10753
 
10342
10754
  // Single element
10343
10755
  if (selector.nodeType) return [selector];
10344
10756
 
10345
10757
  // NodeList or HTMLCollection
10346
- if (selector.length !== undefined && typeof selector !== 'string') {
10758
+ if (selector.length !== undefined && !_is(selector, 'string')) {
10347
10759
  return Array.from(selector);
10348
10760
  }
10349
10761
 
10350
10762
  // CSS selector string
10351
- if (typeof selector === 'string') {
10763
+ if (_is(selector, 'string')) {
10352
10764
  return Array.from(document.querySelectorAll(selector));
10353
10765
  }
10354
10766
 
@@ -10361,103 +10773,49 @@ if (bw._isBrowser) {
10361
10773
  };
10362
10774
  }
10363
10775
 
10364
- /**
10365
- * Load the built-in Bootstrap-inspired default stylesheet.
10366
- *
10367
- * Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
10368
- * alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
10369
- * Returns null in Node.js (no DOM).
10370
- *
10371
- * @param {Object} [options] - Style loading options
10372
- * @param {boolean} [options.minify=true] - Minify the CSS output
10373
- * @returns {Element|null} Style element if in browser, null in Node.js
10374
- * @category CSS & Styling
10375
- * @see bw.setTheme
10376
- * @see bw.applyTheme
10377
- * @see bw.toggleTheme
10378
- * @example
10379
- * bw.loadDefaultStyles(); // inject all default CSS
10380
- */
10381
- bw.loadDefaultStyles = function(options = {}) {
10382
- const { minify = true, palette } = options;
10383
-
10384
- // 1. Inject structural CSS (layout, sizing — never changes with theme)
10385
- if (bw._isBrowser) {
10386
- var structuralCSS = bw.css(getStructuralStyles());
10387
- bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
10388
- }
10389
10776
 
10390
- // 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
10391
- var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
10392
- var result = bw.generateTheme('', Object.assign({}, paletteConfig, { inject: true }));
10393
- return result;
10394
- };
10777
+ // =========================================================================
10778
+ // v2.0.18 Clean Styles API makeStyles / applyStyles / loadStyles / etc.
10779
+ // =========================================================================
10395
10780
 
10781
+ /**
10782
+ * Convert a scope selector to a <style> element id.
10783
+ * @private
10784
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
10785
+ * @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
10786
+ */
10787
+ function _scopeToStyleId(scope) {
10788
+ if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
10789
+ if (scope === 'reset') return 'bw_style_reset';
10790
+ // Strip leading # or . and convert - to _
10791
+ var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
10792
+ return 'bw_style_' + clean;
10793
+ }
10396
10794
 
10397
10795
  /**
10398
- * Generate a complete, scoped theme from seed colors.
10796
+ * Generate a complete styles object from seed colors and layout config.
10797
+ * Pure function — no DOM, no state, no side effects.
10399
10798
  *
10400
- * Produces CSS for all themed components (buttons, alerts, badges, cards,
10401
- * forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
10402
- * scoped under `.name` class. Multiple themes can coexist in the stylesheet.
10403
- * Swap themes by changing the class on a container element.
10799
+ * All parameters are optional. Defaults to the bitwrench default palette.
10404
10800
  *
10405
- * @param {string} name - CSS scope class (e.g. 'ocean'). Empty string = unscoped global.
10406
- * @param {Object} config - Theme configuration
10407
- * @param {string} config.primary - Primary brand color hex
10408
- * @param {string} config.secondary - Secondary color hex
10409
- * @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
10410
- * @param {string} [config.success='#198754'] - Success color hex
10411
- * @param {string} [config.danger='#dc3545'] - Danger color hex
10412
- * @param {string} [config.warning='#ffc107'] - Warning color hex
10413
- * @param {string} [config.info='#0dcaf0'] - Info color hex
10414
- * @param {string} [config.light='#f8f9fa'] - Light color hex
10415
- * @param {string} [config.dark='#212529'] - Dark color hex
10416
- * @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
10417
- * @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
10801
+ * @param {Object} [config] - Style configuration
10802
+ * @param {string} [config.primary='#006666'] - Primary brand color hex
10803
+ * @param {string} [config.secondary='#6c757d'] - Secondary color hex
10804
+ * @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
10418
10805
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
10419
10806
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
10420
- * @param {number} [config.fontSize=1.0] - Base font size scale factor
10421
- * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
10422
- * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
10423
- * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
10424
- * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
10425
- * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
10426
- * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
10807
+ * @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
10427
10808
  * @category CSS & Styling
10428
- * @see bw.applyTheme
10429
- * @see bw.toggleTheme
10430
- * @see bw.loadDefaultStyles
10809
+ * @see bw.applyStyles
10810
+ * @see bw.loadStyles
10431
10811
  * @example
10432
- * // Generate and inject an ocean theme (primary + alternate)
10433
- * var theme = bw.generateTheme('ocean', {
10434
- * primary: '#0077b6',
10435
- * secondary: '#90e0ef',
10436
- * tertiary: '#00b4d8'
10437
- * });
10438
- *
10439
- * // Apply to a container
10440
- * document.getElementById('app').classList.add('ocean');
10441
- *
10442
- * // Toggle to alternate palette
10443
- * bw.toggleTheme();
10444
- *
10445
- * // Generate CSS for static export (Node.js)
10446
- * var result = bw.generateTheme('sunset', {
10447
- * primary: '#e76f51',
10448
- * secondary: '#264653',
10449
- * inject: false
10450
- * });
10451
- * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
10812
+ * var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
10813
+ * console.log(styles.palette.primary.base); // '#4f46e5'
10814
+ * // styles.css contains all themed CSS — nothing injected
10452
10815
  */
10453
- bw.generateTheme = function(name, config) {
10454
- if (!config || !config.primary || !config.secondary) {
10455
- throw new Error('bw.generateTheme requires config.primary and config.secondary');
10456
- }
10457
-
10458
- // Merge with defaults; if user didn't supply tertiary, default to their primary
10459
- var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
10460
- if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
10816
+ bw.makeStyles = function(config) {
10817
+ var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
10818
+ if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
10461
10819
 
10462
10820
  // Derive primary palette
10463
10821
  var palette = derivePalette(fullConfig);
@@ -10465,131 +10823,207 @@ bw.generateTheme = function(name, config) {
10465
10823
  // Resolve layout
10466
10824
  var layout = resolveLayout(fullConfig);
10467
10825
 
10468
- // Generate primary themed CSS rules
10469
- var themedRules = generateThemedCSS(name, palette, layout);
10826
+ // Generate primary themed CSS rules (unscoped)
10827
+ var themedRules = generateThemedCSS('', palette, layout);
10470
10828
  var cssStr = bw.css(themedRules);
10471
10829
 
10472
10830
  // Derive alternate palette (luminance-inverted)
10473
10831
  var altConfig = deriveAlternateConfig(fullConfig);
10474
10832
  var altPalette = derivePalette(altConfig);
10475
10833
 
10476
- // Generate alternate CSS scoped under .bw_theme_alt
10477
- var altRules = generateAlternateCSS(name, altPalette, layout);
10478
- var altCssStr = bw.css(altRules);
10834
+ // Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
10835
+ // applyStyles() wraps them appropriately based on scope
10836
+ var altRawRules = generateThemedCSS('', altPalette, layout);
10837
+
10838
+ // Add body-level surface overrides for the alternate palette.
10839
+ // When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
10840
+ altRawRules['body'] = {
10841
+ 'color': altPalette.dark.base,
10842
+ 'background-color': altPalette.surface || altPalette.light.base
10843
+ };
10844
+
10845
+ var altCssStr = bw.css(altRawRules);
10479
10846
 
10480
10847
  // Determine if primary is light-flavored
10481
10848
  var lightPrimary = isLightPalette(fullConfig);
10482
10849
 
10483
- // Inject both CSS sets into DOM if requested
10484
- var shouldInject = config.inject !== false;
10485
- if (shouldInject && bw._isBrowser) {
10486
- var safeName = name ? name.replace(/-/g, '_') : '';
10487
- var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
10488
- var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
10489
-
10490
- bw.injectCSS(cssStr, { id: styleId, append: false });
10491
- bw.injectCSS(altCssStr, { id: altStyleId, append: false });
10850
+ return {
10851
+ css: cssStr,
10852
+ alternateCss: altCssStr,
10853
+ rules: themedRules,
10854
+ alternateRules: altRawRules,
10855
+ palette: palette,
10856
+ alternatePalette: altPalette,
10857
+ isLightPrimary: lightPrimary
10858
+ };
10859
+ };
10492
10860
 
10493
- bw._activeThemeStyleIds = [styleId, altStyleId];
10861
+ /**
10862
+ * Inject styles into the DOM with optional scoping.
10863
+ *
10864
+ * Takes a styles object from `makeStyles()` and creates a single `<style>`
10865
+ * element in `<head>`. If a scope selector is provided, all CSS rules are
10866
+ * wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
10867
+ *
10868
+ * @param {Object} styles - Result of `bw.makeStyles()`
10869
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
10870
+ * @returns {Element|null} The `<style>` element, or null in Node.js
10871
+ * @category CSS & Styling
10872
+ * @see bw.makeStyles
10873
+ * @see bw.loadStyles
10874
+ * @see bw.clearStyles
10875
+ * @example
10876
+ * var styles = bw.makeStyles({ primary: '#4f46e5' });
10877
+ * bw.applyStyles(styles); // global
10878
+ * bw.applyStyles(styles, '#my-dashboard'); // scoped
10879
+ */
10880
+ bw.applyStyles = function(styles, scope) {
10881
+ if (!bw._isBrowser) return null;
10882
+ if (!styles || !styles.rules) {
10883
+ _cw('bw.applyStyles: invalid styles object');
10884
+ return null;
10494
10885
  }
10495
10886
 
10496
- // Update bw.u color entries to reflect the palette
10497
- if (!name) {
10498
- bw.u.bgTeal = { background: palette.primary.base, color: palette.primary.textOn };
10499
- bw.u.textTeal = { color: palette.primary.base };
10500
- bw.u.bgWhite = { background: '#ffffff' };
10501
- bw.u.textWhite = { color: '#ffffff' };
10887
+ var styleId = _scopeToStyleId(scope);
10888
+
10889
+ // Scope the primary rules if a scope is provided
10890
+ var primaryRules = styles.rules;
10891
+ if (scope) {
10892
+ primaryRules = scopeRulesUnder(primaryRules, scope);
10502
10893
  }
10503
10894
 
10504
- // Store active theme state
10505
- var result = {
10506
- css: cssStr,
10507
- palette: palette,
10508
- name: name,
10509
- isLightPrimary: lightPrimary,
10510
- alternate: {
10511
- css: altCssStr,
10512
- palette: altPalette
10895
+ // Wrap alternate rules with .bw_theme_alt
10896
+ var altRules = styles.alternateRules;
10897
+ if (altRules) {
10898
+ if (scope) {
10899
+ // Scoped compound: #scope.bw_theme_alt .bw_card
10900
+ altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
10901
+ } else {
10902
+ // Global: .bw_theme_alt .bw_card
10903
+ altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
10513
10904
  }
10514
- };
10515
- bw._activeTheme = result;
10516
- bw._activeThemeMode = 'primary';
10905
+ }
10517
10906
 
10518
- return result;
10907
+ // Combine primary + alternate into one CSS string
10908
+ var combined = bw.css(primaryRules);
10909
+ if (altRules) {
10910
+ combined += '\n' + bw.css(altRules);
10911
+ }
10912
+
10913
+ return bw.injectCSS(combined, { id: styleId, append: false });
10519
10914
  };
10520
10915
 
10521
10916
  /**
10522
- * Apply a theme mode. Switches between primary and alternate palettes
10523
- * by adding/removing the `bw_theme_alt` class on `<html>`.
10917
+ * Generate and apply styles in one call. Convenience wrapper.
10918
+ *
10919
+ * Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
10524
10920
  *
10525
- * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
10526
- * @returns {string} Active mode: 'primary' or 'alternate'
10921
+ * @param {Object} [config] - Style configuration (same as `makeStyles`)
10922
+ * @param {string} [scope] - Scope selector (same as `applyStyles`)
10923
+ * @returns {Element|null} The `<style>` element, or null in Node.js
10527
10924
  * @category CSS & Styling
10528
- * @see bw.generateTheme
10529
- * @see bw.toggleTheme
10925
+ * @see bw.makeStyles
10926
+ * @see bw.applyStyles
10530
10927
  * @example
10531
- * bw.applyTheme('alternate'); // switch to alternate palette
10532
- * bw.applyTheme('dark'); // switch to whichever palette is darker
10533
- * bw.applyTheme('primary'); // switch back to primary palette
10534
- */
10535
- bw.applyTheme = function(mode) {
10536
- if (!bw._isBrowser) return mode || 'primary';
10537
- var root = document.documentElement;
10538
- var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
10539
-
10540
- var wantAlt;
10541
- if (mode === 'primary') wantAlt = false;
10542
- else if (mode === 'alternate') wantAlt = true;
10543
- else if (mode === 'light') wantAlt = !isLight;
10544
- else if (mode === 'dark') wantAlt = isLight;
10545
- else wantAlt = false;
10546
-
10547
- if (wantAlt) {
10548
- root.classList.add('bw_theme_alt');
10549
- } else {
10550
- root.classList.remove('bw_theme_alt');
10928
+ * bw.loadStyles(); // defaults, global
10929
+ * bw.loadStyles({ primary: '#4f46e5' }); // custom, global
10930
+ * bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
10931
+ */
10932
+ bw.loadStyles = function(config, scope) {
10933
+ // Also inject structural CSS first (only once)
10934
+ if (bw._isBrowser) {
10935
+ var existing = document.getElementById('bw_structural');
10936
+ if (!existing) {
10937
+ var structuralCSS = bw.css(getStructuralStyles());
10938
+ bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
10939
+ }
10551
10940
  }
10941
+ return bw.applyStyles(bw.makeStyles(config), scope);
10942
+ };
10552
10943
 
10553
- bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
10554
- return bw._activeThemeMode;
10944
+ /**
10945
+ * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
10946
+ * Idempotent — if already injected, returns the existing `<style>` element.
10947
+ *
10948
+ * @returns {Element|null} The `<style>` element, or null in Node.js
10949
+ * @category CSS & Styling
10950
+ * @see bw.loadStyles
10951
+ * @see bw.clearStyles
10952
+ * @example
10953
+ * bw.loadReset(); // inject once, safe to call multiple times
10954
+ */
10955
+ bw.loadReset = function() {
10956
+ if (!bw._isBrowser) return null;
10957
+ var existing = document.getElementById('bw_style_reset');
10958
+ if (existing) return existing;
10959
+ return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
10555
10960
  };
10556
10961
 
10557
10962
  /**
10558
- * Toggle between primary and alternate theme palettes.
10963
+ * Toggle between primary and alternate palettes.
10964
+ *
10965
+ * Adds/removes the `bw_theme_alt` class on the scoping element.
10966
+ * Without a scope, toggles on `<html>` (global).
10967
+ * With a scope, toggles on the first matching element.
10559
10968
  *
10969
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
10560
10970
  * @returns {string} Active mode after toggle: 'primary' or 'alternate'
10561
10971
  * @category CSS & Styling
10562
- * @see bw.applyTheme
10563
- * @see bw.generateTheme
10972
+ * @see bw.applyStyles
10973
+ * @see bw.clearStyles
10564
10974
  * @example
10565
- * bw.toggleTheme(); // flip between primary and alternate
10975
+ * bw.toggleStyles(); // global toggle on <html>
10976
+ * bw.toggleStyles('#my-dashboard'); // scoped toggle
10566
10977
  */
10567
- bw.toggleTheme = function() {
10568
- var current = bw._activeThemeMode || 'primary';
10569
- return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
10978
+ bw.toggleStyles = function(scope) {
10979
+ if (!bw._isBrowser) return 'primary';
10980
+ var target;
10981
+ if (scope) {
10982
+ var els = bw.$(scope);
10983
+ target = els[0];
10984
+ } else {
10985
+ target = document.documentElement;
10986
+ }
10987
+ if (!target) return 'primary';
10988
+
10989
+ var hasAlt = target.classList.contains('bw_theme_alt');
10990
+ if (hasAlt) {
10991
+ target.classList.remove('bw_theme_alt');
10992
+ return 'primary';
10993
+ } else {
10994
+ target.classList.add('bw_theme_alt');
10995
+ return 'alternate';
10996
+ }
10570
10997
  };
10571
10998
 
10572
10999
  /**
10573
- * Remove the currently active theme's injected style elements from the DOM.
10574
- * Use this before generating a new theme with a different name to prevent
10575
- * stale CSS accumulation.
11000
+ * Remove injected styles for a given scope.
11001
+ *
11002
+ * Finds the `<style>` element by id and removes it. Also removes
11003
+ * the `bw_theme_alt` class from the relevant element.
10576
11004
  *
11005
+ * @param {string} [scope] - Scope selector. Omit to remove global styles.
10577
11006
  * @category CSS & Styling
10578
- * @see bw.generateTheme
11007
+ * @see bw.applyStyles
11008
+ * @see bw.loadStyles
10579
11009
  * @example
10580
- * bw.clearTheme(); // remove current theme styles
10581
- * bw.generateTheme('sunset', conf); // inject fresh theme
10582
- */
10583
- bw.clearTheme = function() {
10584
- if (bw._activeThemeStyleIds && bw._isBrowser) {
10585
- bw._activeThemeStyleIds.forEach(function(id) {
10586
- var el = document.getElementById(id);
10587
- if (el) el.remove();
10588
- });
10589
- bw._activeThemeStyleIds = null;
11010
+ * bw.clearStyles(); // remove global styles
11011
+ * bw.clearStyles('#my-dashboard'); // remove scoped styles
11012
+ * bw.clearStyles('reset'); // remove the CSS reset
11013
+ */
11014
+ bw.clearStyles = function(scope) {
11015
+ if (!bw._isBrowser) return;
11016
+ var styleId = _scopeToStyleId(scope);
11017
+ var el = document.getElementById(styleId);
11018
+ if (el) el.remove();
11019
+
11020
+ // Also remove bw_theme_alt from the relevant element
11021
+ if (scope && scope !== 'reset' && scope !== 'global') {
11022
+ var targets = bw.$(scope);
11023
+ if (targets[0]) targets[0].classList.remove('bw_theme_alt');
11024
+ } else if (!scope || scope === 'global') {
11025
+ document.documentElement.classList.remove('bw_theme_alt');
10590
11026
  }
10591
- bw._activeTheme = null;
10592
- bw._activeThemeMode = 'primary';
10593
11027
  };
10594
11028
 
10595
11029
  // Expose color utility functions on bw namespace
@@ -10812,10 +11246,15 @@ bw.copyToClipboard = function(text) {
10812
11246
  * @param {Object} config - Table configuration
10813
11247
  * @param {Array<Object>} config.data - Array of row objects to display
10814
11248
  * @param {Array<Object>} [config.columns] - Column definitions with key, label, render
10815
- * @param {string} [config.className='table'] - CSS class for table element
11249
+ * @param {string} [config.className=''] - Additional CSS classes for table element
10816
11250
  * @param {boolean} [config.sortable=true] - Enable click-to-sort headers
10817
11251
  * @param {Function} [config.onSort] - Sort callback (column, direction)
10818
- * @returns {Object} TACO object for table
11252
+ * @param {boolean} [config.selectable=false] - Enable row selection on click
11253
+ * @param {Function} [config.onRowClick] - Row click callback (row, index, event)
11254
+ * @param {number} [config.pageSize] - Rows per page (enables pagination when set)
11255
+ * @param {number} [config.currentPage=1] - Current page number (1-based)
11256
+ * @param {Function} [config.onPageChange] - Page change callback (newPage)
11257
+ * @returns {Object} TACO object for table (with optional pagination controls)
10819
11258
  * @category Component Builders
10820
11259
  * @see bw.makeDataTable
10821
11260
  * @example
@@ -10827,7 +11266,12 @@ bw.copyToClipboard = function(text) {
10827
11266
  * columns: [
10828
11267
  * { key: 'name', label: 'Name' },
10829
11268
  * { key: 'age', label: 'Age' }
10830
- * ]
11269
+ * ],
11270
+ * selectable: true,
11271
+ * onRowClick: function(row, i) { console.log('clicked', row.name); },
11272
+ * pageSize: 10,
11273
+ * currentPage: 1,
11274
+ * onPageChange: function(page) { console.log('page', page); }
10831
11275
  * });
10832
11276
  */
10833
11277
  bw.makeTable = function(config) {
@@ -10840,41 +11284,47 @@ bw.makeTable = function(config) {
10840
11284
  sortable = true,
10841
11285
  onSort,
10842
11286
  sortColumn,
10843
- sortDirection = 'asc'
11287
+ sortDirection = 'asc',
11288
+ selectable = false,
11289
+ onRowClick,
11290
+ pageSize,
11291
+ currentPage = 1,
11292
+ onPageChange
10844
11293
  } = config;
10845
11294
 
10846
- // Build class list: always include bw_table, add striped/hover, append user className
11295
+ // Build class list: always include bw_table, add striped/hover/selectable, append user className
10847
11296
  let cls = 'bw_table';
10848
11297
  if (striped) cls += ' bw_table_striped';
10849
- if (hover) cls += ' bw_table_hover';
11298
+ if (hover || selectable) cls += ' bw_table_hover';
11299
+ if (selectable) cls += ' bw_table_selectable';
10850
11300
  if (className) cls += ' ' + className;
10851
11301
  cls = cls.trim();
10852
-
11302
+
10853
11303
  // Auto-detect columns if not provided
10854
- const cols = columns || (data.length > 0
10855
- ? Object.keys(data[0]).map(key => ({ key, label: key }))
11304
+ const cols = columns || (data.length > 0
11305
+ ? _keys(data[0]).map(key => ({ key, label: key }))
10856
11306
  : []);
10857
-
11307
+
10858
11308
  // Current sort state
10859
11309
  let currentSortColumn = sortColumn || null;
10860
11310
  let currentSortDirection = sortDirection;
10861
-
11311
+
10862
11312
  // Sort data if column specified
10863
11313
  let sortedData = [...data];
10864
11314
  if (currentSortColumn) {
10865
11315
  sortedData.sort((a, b) => {
10866
11316
  const aVal = a[currentSortColumn];
10867
11317
  const bVal = b[currentSortColumn];
10868
-
11318
+
10869
11319
  // Handle different types
10870
- if (typeof aVal === 'number' && typeof bVal === 'number') {
11320
+ if (_is(aVal, 'number') && _is(bVal, 'number')) {
10871
11321
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
10872
11322
  }
10873
-
11323
+
10874
11324
  // String comparison
10875
11325
  const aStr = String(aVal || '').toLowerCase();
10876
11326
  const bStr = String(bVal || '').toLowerCase();
10877
-
11327
+
10878
11328
  if (currentSortDirection === 'asc') {
10879
11329
  return aStr.localeCompare(bStr);
10880
11330
  } else {
@@ -10882,23 +11332,32 @@ bw.makeTable = function(config) {
10882
11332
  }
10883
11333
  });
10884
11334
  }
10885
-
11335
+
11336
+ // Pagination
11337
+ const totalRows = sortedData.length;
11338
+ const totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
11339
+ const page = Math.max(1, Math.min(currentPage, totalPages));
11340
+ if (pageSize) {
11341
+ const start = (page - 1) * pageSize;
11342
+ sortedData = sortedData.slice(start, start + pageSize);
11343
+ }
11344
+
10886
11345
  // Create sort handler
10887
11346
  const handleSort = (column) => {
10888
11347
  if (!sortable) return;
10889
-
11348
+
10890
11349
  if (currentSortColumn === column) {
10891
11350
  currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
10892
11351
  } else {
10893
11352
  currentSortColumn = column;
10894
11353
  currentSortDirection = 'asc';
10895
11354
  }
10896
-
11355
+
10897
11356
  if (onSort) {
10898
11357
  onSort(column, currentSortDirection);
10899
11358
  }
10900
11359
  };
10901
-
11360
+
10902
11361
  // Build table header
10903
11362
  const thead = {
10904
11363
  t: 'thead',
@@ -10921,24 +11380,87 @@ bw.makeTable = function(config) {
10921
11380
  }))
10922
11381
  }
10923
11382
  };
10924
-
10925
- // Build table body
11383
+
11384
+ // Build table body with selectable/onRowClick support
10926
11385
  const tbody = {
10927
11386
  t: 'tbody',
10928
- c: sortedData.map(row => ({
10929
- t: 'tr',
10930
- c: cols.map(col => ({
10931
- t: 'td',
10932
- c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
10933
- }))
10934
- }))
11387
+ c: sortedData.map((row, idx) => {
11388
+ const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
11389
+ const rowAttrs = {};
11390
+ if (selectable || onRowClick) {
11391
+ rowAttrs.style = 'cursor:pointer;';
11392
+ rowAttrs.onclick = function(e) {
11393
+ if (selectable) {
11394
+ // Toggle selected class on this row
11395
+ var tr = e.currentTarget;
11396
+ tr.classList.toggle('bw_table_row_selected');
11397
+ }
11398
+ if (onRowClick) {
11399
+ onRowClick(row, globalIdx, e);
11400
+ }
11401
+ };
11402
+ }
11403
+ return {
11404
+ t: 'tr',
11405
+ a: rowAttrs,
11406
+ c: cols.map(col => ({
11407
+ t: 'td',
11408
+ c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
11409
+ }))
11410
+ };
11411
+ })
10935
11412
  };
10936
-
10937
- return {
11413
+
11414
+ const table = {
10938
11415
  t: 'table',
10939
11416
  a: { class: cls },
10940
11417
  c: [thead, tbody]
10941
11418
  };
11419
+
11420
+ // If no pagination, return table directly
11421
+ if (!pageSize) return table;
11422
+
11423
+ // Build pagination controls
11424
+ const pageButtons = [];
11425
+ // Previous button
11426
+ pageButtons.push({
11427
+ t: 'button',
11428
+ a: {
11429
+ class: 'bw_btn bw_btn_sm',
11430
+ disabled: page <= 1 ? 'disabled' : undefined,
11431
+ onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
11432
+ },
11433
+ c: 'Prev'
11434
+ });
11435
+ // Page info
11436
+ pageButtons.push({
11437
+ t: 'span',
11438
+ a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
11439
+ c: 'Page ' + page + ' of ' + totalPages
11440
+ });
11441
+ // Next button
11442
+ pageButtons.push({
11443
+ t: 'button',
11444
+ a: {
11445
+ class: 'bw_btn bw_btn_sm',
11446
+ disabled: page >= totalPages ? 'disabled' : undefined,
11447
+ onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
11448
+ },
11449
+ c: 'Next'
11450
+ });
11451
+
11452
+ return {
11453
+ t: 'div',
11454
+ a: { class: 'bw_table_paginated' },
11455
+ c: [
11456
+ table,
11457
+ {
11458
+ t: 'div',
11459
+ a: { class: 'bw_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
11460
+ c: pageButtons
11461
+ }
11462
+ ]
11463
+ };
10942
11464
  };
10943
11465
 
10944
11466
  /**
@@ -10977,7 +11499,7 @@ bw.makeTable = function(config) {
10977
11499
  bw.makeTableFromArray = function(config) {
10978
11500
  const { data = [], headerRow = true, columns, ...rest } = config;
10979
11501
 
10980
- if (!Array.isArray(data) || data.length === 0) {
11502
+ if (!_isA(data) || data.length === 0) {
10981
11503
  return bw.makeTable({ data: [], columns: columns || [], ...rest });
10982
11504
  }
10983
11505
 
@@ -11059,7 +11581,7 @@ bw.makeBarChart = function(config) {
11059
11581
  className = ''
11060
11582
  } = config;
11061
11583
 
11062
- if (!Array.isArray(data) || data.length === 0) {
11584
+ if (!_isA(data) || data.length === 0) {
11063
11585
  return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
11064
11586
  }
11065
11587
 
@@ -11208,7 +11730,7 @@ bw._componentRegistry = new Map();
11208
11730
  */
11209
11731
  bw.render = function(element, position, taco) {
11210
11732
  // Get target element
11211
- const targetEl = typeof element === 'string'
11733
+ const targetEl = _is(element, 'string')
11212
11734
  ? document.querySelector(element)
11213
11735
  : element;
11214
11736
 
@@ -11358,7 +11880,7 @@ bw.render = function(element, position, taco) {
11358
11880
  setContent(content) {
11359
11881
  this._taco.c = content;
11360
11882
  if (this.element) {
11361
- if (typeof content === 'string') {
11883
+ if (_is(content, 'string')) {
11362
11884
  this.element.textContent = content;
11363
11885
  } else {
11364
11886
  // Re-render for complex content