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-lean v2.0.16 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench-lean v2.0.18 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  '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
  /**
@@ -3345,7 +3514,7 @@ const bw = {
3345
3514
  __monkey_patch_is_nodejs__: {
3346
3515
  _value: 'ignore',
3347
3516
  set: function(x) {
3348
- this._value = (typeof x === 'boolean') ? x : 'ignore';
3517
+ this._value = _is(x, 'boolean') ? x : 'ignore';
3349
3518
  },
3350
3519
  get: function() {
3351
3520
  return this._value;
@@ -3393,6 +3562,67 @@ Object.defineProperty(bw, '_isBrowser', {
3393
3562
  configurable: true
3394
3563
  });
3395
3564
 
3565
+ // ── Internal aliases ─────────────────────────────────────────────────────
3566
+ // Short names for frequently-used builtins and internal methods.
3567
+ // Same pattern as v1 (_to = bw.typeOf, etc.).
3568
+ //
3569
+ // Why: Terser can't shorten global property chains (console.warn,
3570
+ // Object.prototype.hasOwnProperty, Array.isArray, document.createElement)
3571
+ // because it can't prove they're side-effect-free. We can, so we alias
3572
+ // them here. Each alias saves bytes in the minified output, and the short
3573
+ // names also reduce visual noise in the hot paths (binding pipeline,
3574
+ // createDOM, etc.).
3575
+ //
3576
+ // Alias Target Sites
3577
+ // ───────── ────────────────────────────────────── ─────
3578
+ // _hop Object.prototype.hasOwnProperty 15
3579
+ // _isA Array.isArray 25
3580
+ // _keys Object.keys 7
3581
+ // _to bw.typeOf (type string) 26
3582
+ // _is type check boolean: _is(x,'string') ~50
3583
+ // _cw console.warn 8
3584
+ // _cl console.log 11
3585
+ // _ce console.error 4
3586
+ // _chp ComponentHandle.prototype 28 (defined after constructor)
3587
+ //
3588
+ // Note: document.createElement etc. are NOT aliased because they require
3589
+ // `this === document` and .bind() would add overhead on every call.
3590
+ // Console aliases use thin wrappers (not direct refs) so test monkey-
3591
+ // patching of console.warn/log/error continues to work.
3592
+ //
3593
+ // `typeof x` for UNDECLARED globals (window, document, process, require,
3594
+ // EventSource, navigator, Promise, __filename, import.meta) MUST stay as
3595
+ // raw `typeof` — calling _to(x) when x doesn't exist throws ReferenceError.
3596
+ //
3597
+ // ── v1 functional type helpers (kept for reference, not currently used) ──
3598
+ // _toa(x, type, trueVal, falseVal) — bw.typeAssign:
3599
+ // returns trueVal if _to(x)===type, else falseVal.
3600
+ // Replaces: (typeof x === 'string') ? A : B → _toa(x,'string',A,B)
3601
+ // _toc(x, type, trueVal, falseVal) — bw.typeConvert:
3602
+ // same as _toa but if trueVal/falseVal are functions, calls them with x.
3603
+ // Replaces: typeof x === 'string' ? fn(x) : default → _toc(x,'string',fn,default)
3604
+ // Uncomment if pattern frequency justifies them:
3605
+ // var _toa = function(x, t, y, n) { return _to(x) === t ? y : n; };
3606
+ // 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); };
3607
+ // ─────────────────────────────────────────────────────────────────────────
3608
+ var _hop = Object.prototype.hasOwnProperty;
3609
+ var _isA = Array.isArray;
3610
+ var _keys = Object.keys;
3611
+ var _to = typeOf; // imported from bitwrench-utils.js
3612
+ var _is = function(x, t) { var r = _to(x); return r === t || r.toLowerCase() === t; };
3613
+ // Console aliases use thin wrappers (not direct references) so that test
3614
+ // code can monkey-patch console.warn/log/error and the patches take effect.
3615
+ var _cw = function() { console.warn.apply(console, arguments); };
3616
+ var _cl = function() { console.log.apply(console, arguments); };
3617
+ var _ce = function() { console.error.apply(console, arguments); };
3618
+
3619
+ /**
3620
+ * Debug flag. When true, emits console.warn for silent binding failures
3621
+ * (missing paths, null refs, auto-created intermediate objects).
3622
+ * @type {boolean}
3623
+ */
3624
+ bw.debug = false;
3625
+
3396
3626
  /**
3397
3627
  * Lazy-resolve Node.js `fs` module.
3398
3628
  * Tries require('fs') first (available in CJS/UMD Node.js builds),
@@ -3540,7 +3770,7 @@ bw.uuid = function(prefix) {
3540
3770
  */
3541
3771
  bw._el = function(id) {
3542
3772
  // Pass-through for DOM elements
3543
- if (typeof id !== 'string') return id || null;
3773
+ if (!_is(id, 'string')) return id || null;
3544
3774
  if (!id) return null;
3545
3775
  if (!bw._isBrowser) return null;
3546
3776
 
@@ -3568,7 +3798,12 @@ bw._el = function(id) {
3568
3798
  el = document.querySelector('[data-bw_id="' + id + '"]');
3569
3799
  }
3570
3800
 
3571
- // 5. Cache the result for next time
3801
+ // 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
3802
+ if (!el && id.indexOf('bw_uuid_') === 0) {
3803
+ el = document.querySelector('.' + id);
3804
+ }
3805
+
3806
+ // 6. Cache the result for next time
3572
3807
  if (el) {
3573
3808
  bw._nodeMap[id] = el;
3574
3809
  }
@@ -3621,6 +3856,84 @@ bw._deregisterNode = function(el, bwId) {
3621
3856
  }
3622
3857
  };
3623
3858
 
3859
+ // ===================================================================================
3860
+ // bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
3861
+ // ===================================================================================
3862
+
3863
+ /**
3864
+ * Regex to match a bw_uuid_* token in a class string.
3865
+ * @private
3866
+ */
3867
+ var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
3868
+
3869
+ /**
3870
+ * Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
3871
+ *
3872
+ * Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
3873
+ * to replace an existing UUID (useful in loops where each TACO needs a unique ID).
3874
+ *
3875
+ * @param {Object} taco - A TACO object `{t, a, c, o}`
3876
+ * @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
3877
+ * @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
3878
+ * @category Identifiers
3879
+ * @example
3880
+ * var card = bw.makeStatCard({ value: '0', label: 'Scans' });
3881
+ * var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
3882
+ * var same = bw.assignUUID(card); // same UUID (idempotent)
3883
+ * var diff = bw.assignUUID(card, true); // new UUID (forced)
3884
+ */
3885
+ bw.assignUUID = function(taco, forceNew) {
3886
+ if (!taco || !_is(taco, 'object')) return null;
3887
+
3888
+ // Ensure taco.a exists
3889
+ if (!taco.a) taco.a = {};
3890
+ if (!_is(taco.a.class, 'string')) taco.a.class = taco.a.class ? String(taco.a.class) : '';
3891
+
3892
+ var existing = taco.a.class.match(_UUID_RE);
3893
+
3894
+ if (existing && !forceNew) {
3895
+ return existing[0];
3896
+ }
3897
+
3898
+ // Remove old UUID if forceNew
3899
+ if (existing) {
3900
+ taco.a.class = taco.a.class.replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
3901
+ }
3902
+
3903
+ var uuid = bw.uuid('uuid');
3904
+ taco.a.class = (taco.a.class ? taco.a.class + ' ' : '') + uuid;
3905
+ return uuid;
3906
+ };
3907
+
3908
+ /**
3909
+ * Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
3910
+ *
3911
+ * @param {Object|Element} tacoOrElement - A TACO object or DOM element
3912
+ * @returns {string|null} The UUID string, or null if none assigned
3913
+ * @category Identifiers
3914
+ * @example
3915
+ * bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
3916
+ * bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
3917
+ * bw.getUUID({t:'div'}) // null (no UUID)
3918
+ */
3919
+ bw.getUUID = function(tacoOrElement) {
3920
+ if (!tacoOrElement) return null;
3921
+
3922
+ var classStr;
3923
+ // DOM element: check className
3924
+ if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
3925
+ classStr = tacoOrElement.className;
3926
+ }
3927
+ // TACO object: check a.class
3928
+ else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
3929
+ classStr = tacoOrElement.a.class;
3930
+ }
3931
+
3932
+ if (!classStr) return null;
3933
+ var match = classStr.match(_UUID_RE);
3934
+ return match ? match[0] : null;
3935
+ };
3936
+
3624
3937
  /**
3625
3938
  * Escape HTML special characters to prevent XSS.
3626
3939
  *
@@ -3636,7 +3949,7 @@ bw._deregisterNode = function(el, bwId) {
3636
3949
  * // => '&lt;b&gt;Hello&lt;&#x2F;b&gt; &amp; &quot;world&quot;'
3637
3950
  */
3638
3951
  bw.escapeHTML = function(str) {
3639
- if (typeof str !== 'string') return '';
3952
+ if (!_is(str, 'string')) return '';
3640
3953
 
3641
3954
  const escapeMap = {
3642
3955
  '&': '&amp;',
@@ -3670,6 +3983,42 @@ bw.raw = function(str) {
3670
3983
  return { __bw_raw: true, v: String(str) };
3671
3984
  };
3672
3985
 
3986
+ /**
3987
+ * Hyperscript-style TACO constructor.
3988
+ *
3989
+ * A convenience helper that returns a canonical TACO object from positional
3990
+ * arguments. The return value is a plain object — serializable, works with
3991
+ * bwserve, and accepted everywhere TACO is accepted.
3992
+ *
3993
+ * @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
3994
+ * @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
3995
+ * @param {*} [content] - Content: string, number, TACO object, or array of children.
3996
+ * @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
3997
+ * @returns {Object} Plain TACO object {t, a?, c?, o?}
3998
+ * @category Utilities
3999
+ * @see bw.html
4000
+ * @see bw.createDOM
4001
+ * @see bw.DOM
4002
+ * @example
4003
+ * bw.h('div')
4004
+ * // => { t: 'div' }
4005
+ *
4006
+ * bw.h('p', { class: 'bw_text_muted' }, 'Hello')
4007
+ * // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
4008
+ *
4009
+ * bw.h('ul', null, [
4010
+ * bw.h('li', null, 'one'),
4011
+ * bw.h('li', null, 'two')
4012
+ * ])
4013
+ * // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
4014
+ */
4015
+ bw.h = function(tag, attrs, content, options) {
4016
+ var taco = { t: String(tag) };
4017
+ if (attrs !== null && attrs !== undefined) taco.a = attrs;
4018
+ if (content !== undefined) taco.c = content;
4019
+ if (options !== undefined) taco.o = options;
4020
+ return taco;
4021
+ };
3673
4022
 
3674
4023
  /**
3675
4024
  * Convert a TACO object (or array of TACOs) to an HTML string.
@@ -3709,7 +4058,7 @@ bw.html = function(taco, options = {}) {
3709
4058
  }
3710
4059
 
3711
4060
  // Handle arrays of TACOs
3712
- if (Array.isArray(taco)) {
4061
+ if (_isA(taco)) {
3713
4062
  return taco.map(t => bw.html(t, options)).join('');
3714
4063
  }
3715
4064
 
@@ -3732,15 +4081,15 @@ bw.html = function(taco, options = {}) {
3732
4081
  if (taco && taco._bwEach && options.state) {
3733
4082
  var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
3734
4083
  var arr = bw._evaluatePath(options.state, eachExpr);
3735
- if (!Array.isArray(arr)) return '';
4084
+ if (!_isA(arr)) return '';
3736
4085
  return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
3737
4086
  }
3738
4087
 
3739
4088
  // Handle primitives and non-TACO objects
3740
- if (typeof taco !== 'object' || !taco.t) {
4089
+ if (!_is(taco, 'object') || !taco.t) {
3741
4090
  var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
3742
4091
  // Resolve template bindings if state provided
3743
- if (options.state && typeof str === 'string' && str.indexOf('${') >= 0) {
4092
+ if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
3744
4093
  str = bw._resolveTemplate(str, options.state, !!options.compile);
3745
4094
  }
3746
4095
  return str;
@@ -3760,10 +4109,18 @@ bw.html = function(taco, options = {}) {
3760
4109
  // Skip null, undefined, false
3761
4110
  if (value == null || value === false) continue;
3762
4111
 
3763
- // Skip event handlers (they're for DOM only)
3764
- if (key.startsWith('on')) continue;
4112
+ // Serialize event handlers via funcRegister
4113
+ if (key.startsWith('on')) {
4114
+ if (_is(value, 'function')) {
4115
+ var fnId = bw.funcRegister(value);
4116
+ attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
4117
+ } else if (_is(value, 'string')) {
4118
+ attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
4119
+ }
4120
+ continue;
4121
+ }
3765
4122
 
3766
- if (key === 'style' && typeof value === 'object') {
4123
+ if (key === 'style' && _is(value, 'object')) {
3767
4124
  // Convert style object to string
3768
4125
  const styleStr = Object.entries(value)
3769
4126
  .filter(([, v]) => v != null)
@@ -3774,7 +4131,7 @@ bw.html = function(taco, options = {}) {
3774
4131
  }
3775
4132
  } else if (key === 'class') {
3776
4133
  // Handle class as array or string
3777
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
4134
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
3778
4135
  if (classStr) {
3779
4136
  attrStr += ` class="${bw.escapeHTML(classStr)}"`;
3780
4137
  }
@@ -3810,13 +4167,184 @@ bw.html = function(taco, options = {}) {
3810
4167
  // Process content recursively
3811
4168
  let contentStr = content != null ? bw.html(content, options) : '';
3812
4169
  // Resolve template bindings in content if state provided
3813
- if (options.state && typeof contentStr === 'string' && contentStr.indexOf('${') >= 0) {
4170
+ if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
3814
4171
  contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
3815
4172
  }
3816
4173
 
3817
4174
  return `<${tag}${attrStr}>${contentStr}</${tag}>`;
3818
4175
  };
3819
4176
 
4177
+ /**
4178
+ * Generate a complete, self-contained HTML document from TACO content.
4179
+ *
4180
+ * Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
4181
+ * func registry emission (so serialized event handlers work), optional theme,
4182
+ * and extra head elements. Designed for static site generation, offline/airgapped
4183
+ * use, and the "static site that isn't static" workflow.
4184
+ *
4185
+ * @param {Object} [opts={}] - Page options
4186
+ * @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
4187
+ * @param {string} [opts.title='bitwrench'] - Page title
4188
+ * @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
4189
+ * @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
4190
+ * @param {string} [opts.css=''] - Additional CSS for <style> block
4191
+ * @param {string|Object} [opts.theme=null] - Theme preset name or config object
4192
+ * @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
4193
+ * @param {string} [opts.favicon=''] - Favicon URL
4194
+ * @param {string} [opts.lang='en'] - HTML lang attribute
4195
+ * @returns {string} Complete HTML document string
4196
+ * @category DOM Generation
4197
+ * @see bw.html
4198
+ * @example
4199
+ * bw.htmlPage({
4200
+ * title: 'My App',
4201
+ * body: { t: 'h1', c: 'Hello World' },
4202
+ * runtime: 'shim'
4203
+ * })
4204
+ */
4205
+ bw.htmlPage = function(opts) {
4206
+ opts = opts || {};
4207
+ var title = opts.title || 'bitwrench';
4208
+ var body = opts.body || '';
4209
+ var state = opts.state || undefined;
4210
+ var runtime = opts.runtime || 'shim';
4211
+ var css = opts.css || '';
4212
+ var theme = opts.theme || null;
4213
+ var headExtra = opts.head || [];
4214
+ var favicon = opts.favicon || '';
4215
+ var lang = opts.lang || 'en';
4216
+
4217
+ // Snapshot funcRegistry counter before rendering
4218
+ var fnCounterBefore = bw._fnIDCounter;
4219
+
4220
+ // Render body content
4221
+ var bodyHTML = '';
4222
+ if (_is(body, 'string')) {
4223
+ bodyHTML = body;
4224
+ } else {
4225
+ var htmlOpts = {};
4226
+ if (state) htmlOpts.state = state;
4227
+ bodyHTML = bw.html(body, htmlOpts);
4228
+ }
4229
+
4230
+ // Collect functions registered during this render
4231
+ var fnCounterAfter = bw._fnIDCounter;
4232
+ var registryEntries = '';
4233
+ for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
4234
+ var fnKey = 'bw_fn_' + i;
4235
+ if (bw._fnRegistry[fnKey]) {
4236
+ registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' +
4237
+ bw._fnRegistry[fnKey].toString() + ';\n';
4238
+ }
4239
+ }
4240
+
4241
+ // Build runtime script for <head>
4242
+ var runtimeHead = '';
4243
+ if (runtime === 'inline') {
4244
+ // Read UMD bundle synchronously if in Node.js
4245
+ var umdSource = null;
4246
+ if (bw._isNode) {
4247
+ try {
4248
+ var fs = (typeof require === 'function') ? require('fs') : null;
4249
+ var pathMod = (typeof require === 'function') ? require('path') : null;
4250
+ if (fs && pathMod) {
4251
+ // Resolve dist/ relative to this source file
4252
+ var srcDir = '';
4253
+ try { srcDir = pathMod.dirname((typeof __filename !== 'undefined') ? __filename : ''); }
4254
+ catch(e2) { /* ESM: __filename not available */ }
4255
+ if (!srcDir && typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bitwrench-lean.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-lean.cjs.js', document.baseURI).href))) {
4256
+ var url = (typeof require === 'function') ? require('url') : null;
4257
+ 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-lean.cjs.js', document.baseURI).href))));
4258
+ }
4259
+ if (srcDir) {
4260
+ var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
4261
+ umdSource = fs.readFileSync(distPath, 'utf8');
4262
+ }
4263
+ }
4264
+ } catch(e) { /* fall through */ }
4265
+ }
4266
+ if (umdSource) {
4267
+ runtimeHead = '<script>' + umdSource + '</script>';
4268
+ } else {
4269
+ // Fallback to shim in browser or if dist not available
4270
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
4271
+ }
4272
+ } else if (runtime === 'cdn') {
4273
+ runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
4274
+ } else if (runtime === 'shim') {
4275
+ runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
4276
+ }
4277
+ // runtime === 'none' → empty
4278
+
4279
+ // Theme CSS
4280
+ var themeCSS = '';
4281
+ if (theme) {
4282
+ var themeConfig = _is(theme, 'string')
4283
+ ? (THEME_PRESETS[theme.toLowerCase()] || null)
4284
+ : theme;
4285
+ if (themeConfig) {
4286
+ var themeResult = bw.makeStyles(themeConfig);
4287
+ themeCSS = themeResult.css;
4288
+ }
4289
+ }
4290
+
4291
+ // Extra <head> elements
4292
+ var headHTML = '';
4293
+ if (_isA(headExtra) && headExtra.length > 0) {
4294
+ headHTML = headExtra.map(function(el) { return bw.html(el); }).join('\n');
4295
+ }
4296
+
4297
+ // Favicon
4298
+ var faviconTag = '';
4299
+ if (favicon) {
4300
+ var safeFavicon = favicon.replace(/[&<>"']/g, function(c) {
4301
+ return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
4302
+ });
4303
+ faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
4304
+ }
4305
+
4306
+ // Escaped title
4307
+ var safeTitle = bw.escapeHTML(title);
4308
+
4309
+ // Combine all CSS
4310
+ var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
4311
+
4312
+ // Body-end script: registry entries + optional loadStyles
4313
+ var bodyEndScript = '';
4314
+ var bodyEndParts = [];
4315
+ if (registryEntries) {
4316
+ bodyEndParts.push(registryEntries);
4317
+ }
4318
+ if (runtime === 'inline' || runtime === 'cdn') {
4319
+ bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
4320
+ }
4321
+ if (bodyEndParts.length > 0) {
4322
+ bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
4323
+ }
4324
+
4325
+ // Assemble document
4326
+ var parts = [
4327
+ '<!DOCTYPE html>',
4328
+ '<html lang="' + lang + '">',
4329
+ '<head>',
4330
+ '<meta charset="UTF-8">',
4331
+ '<meta name="viewport" content="width=device-width, initial-scale=1">'
4332
+ ];
4333
+ parts.push('<title>' + safeTitle + '</title>');
4334
+ if (faviconTag) parts.push(faviconTag);
4335
+ if (runtimeHead) parts.push(runtimeHead);
4336
+ if (headHTML) parts.push(headHTML);
4337
+ if (allCSS) parts.push('<style>' + allCSS + '</style>');
4338
+ parts.push('</head>');
4339
+ parts.push('<body>');
4340
+ parts.push(bodyHTML);
4341
+ if (bodyEndScript) parts.push(bodyEndScript);
4342
+ parts.push('</body>');
4343
+ parts.push('</html>');
4344
+
4345
+ return parts.join('\n');
4346
+ };
4347
+
3820
4348
  /**
3821
4349
  * Create a live DOM element from a TACO object (browser only).
3822
4350
  *
@@ -3861,7 +4389,7 @@ bw.createDOM = function(taco, options = {}) {
3861
4389
  }
3862
4390
 
3863
4391
  // Handle text nodes
3864
- if (typeof taco !== 'object' || !taco.t) {
4392
+ if (!_is(taco, 'object') || !taco.t) {
3865
4393
  return document.createTextNode(String(taco));
3866
4394
  }
3867
4395
 
@@ -3874,16 +4402,16 @@ bw.createDOM = function(taco, options = {}) {
3874
4402
  for (const [key, value] of Object.entries(attrs)) {
3875
4403
  if (value == null || value === false) continue;
3876
4404
 
3877
- if (key === 'style' && typeof value === 'object') {
4405
+ if (key === 'style' && _is(value, 'object')) {
3878
4406
  // Apply styles directly
3879
4407
  Object.assign(el.style, value);
3880
4408
  } else if (key === 'class') {
3881
4409
  // Handle class as array or string
3882
- const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
4410
+ const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
3883
4411
  if (classStr) {
3884
4412
  el.className = classStr;
3885
4413
  }
3886
- } else if (key.startsWith('on') && typeof value === 'function') {
4414
+ } else if (key.startsWith('on') && _is(value, 'function')) {
3887
4415
  // Event handlers
3888
4416
  const eventName = key.slice(2).toLowerCase();
3889
4417
  el.addEventListener(eventName, value);
@@ -3903,7 +4431,7 @@ bw.createDOM = function(taco, options = {}) {
3903
4431
  // Children with data-bw_id or id attributes get local refs on the parent,
3904
4432
  // so o.render functions can access them without any DOM lookup.
3905
4433
  if (content != null) {
3906
- if (Array.isArray(content)) {
4434
+ if (_isA(content)) {
3907
4435
  content.forEach(child => {
3908
4436
  if (child != null) {
3909
4437
  // Handle ComponentHandle in content arrays (Level 2 children)
@@ -3923,20 +4451,20 @@ bw.createDOM = function(taco, options = {}) {
3923
4451
  if (childEl._bw_refs) {
3924
4452
  if (!el._bw_refs) el._bw_refs = {};
3925
4453
  for (var rk in childEl._bw_refs) {
3926
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
4454
+ if (_hop.call(childEl._bw_refs, rk)) {
3927
4455
  el._bw_refs[rk] = childEl._bw_refs[rk];
3928
4456
  }
3929
4457
  }
3930
4458
  }
3931
4459
  }
3932
4460
  });
3933
- } else if (typeof content === 'object' && content.__bw_raw) {
4461
+ } else if (_is(content, 'object') && content.__bw_raw) {
3934
4462
  // Raw HTML content — inject via innerHTML
3935
4463
  el.innerHTML = content.v;
3936
4464
  } else if (content._bwComponent === true) {
3937
4465
  // Single ComponentHandle as content
3938
4466
  content.mount(el);
3939
- } else if (typeof content === 'object' && content.t) {
4467
+ } else if (_is(content, 'object') && content.t) {
3940
4468
  var childEl = bw.createDOM(content, options);
3941
4469
  el.appendChild(childEl);
3942
4470
  var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
@@ -3947,7 +4475,7 @@ bw.createDOM = function(taco, options = {}) {
3947
4475
  if (childEl._bw_refs) {
3948
4476
  if (!el._bw_refs) el._bw_refs = {};
3949
4477
  for (var rk in childEl._bw_refs) {
3950
- if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
4478
+ if (_hop.call(childEl._bw_refs, rk)) {
3951
4479
  el._bw_refs[rk] = childEl._bw_refs[rk];
3952
4480
  }
3953
4481
  }
@@ -3962,6 +4490,14 @@ bw.createDOM = function(taco, options = {}) {
3962
4490
  bw._registerNode(el, null);
3963
4491
  }
3964
4492
 
4493
+ // Register UUID class in node cache (bw_uuid_* tokens in class string)
4494
+ if (el.className) {
4495
+ var uuidMatch = el.className.match(_UUID_RE);
4496
+ if (uuidMatch) {
4497
+ bw._nodeMap[uuidMatch[0]] = el;
4498
+ }
4499
+ }
4500
+
3965
4501
  // Handle lifecycle hooks and state
3966
4502
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
3967
4503
  const id = attrs['data-bw_id'] || bw.uuid();
@@ -3980,7 +4516,7 @@ bw.createDOM = function(taco, options = {}) {
3980
4516
  el._bw_render = opts.render;
3981
4517
 
3982
4518
  if (opts.mounted) {
3983
- console.warn('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
4519
+ _cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
3984
4520
  }
3985
4521
 
3986
4522
  // Queue initial render (same timing as mounted)
@@ -4053,7 +4589,7 @@ bw.DOM = function(target, taco, options = {}) {
4053
4589
  const targetEl = bw._el(target);
4054
4590
 
4055
4591
  if (!targetEl) {
4056
- console.error('bw.DOM: Target element not found:', target);
4592
+ _ce('bw.DOM: Target element not found:', target);
4057
4593
  return null;
4058
4594
  }
4059
4595
 
@@ -4093,7 +4629,7 @@ bw.DOM = function(target, taco, options = {}) {
4093
4629
  targetEl.appendChild(taco.element);
4094
4630
  }
4095
4631
  // Handle arrays
4096
- else if (Array.isArray(taco)) {
4632
+ else if (_isA(taco)) {
4097
4633
  taco.forEach(t => {
4098
4634
  if (t != null) {
4099
4635
  if (t._bwComponent === true) {
@@ -4129,7 +4665,7 @@ bw.DOM = function(target, taco, options = {}) {
4129
4665
  bw.compileProps = function(handle, props = {}) {
4130
4666
  const compiledProps = {};
4131
4667
 
4132
- Object.keys(props).forEach(key => {
4668
+ _keys(props).forEach(key => {
4133
4669
  // Create getter/setter for each prop
4134
4670
  Object.defineProperty(compiledProps, key, {
4135
4671
  get() {
@@ -4334,6 +4870,16 @@ bw.renderComponent = function(taco, options = {}) {
4334
4870
  bw.cleanup = function(element) {
4335
4871
  if (!bw._isBrowser || !element) return;
4336
4872
 
4873
+ // Deregister UUID classes from node cache (element + descendants)
4874
+ // Covers elements that have UUID but no data-bw_id
4875
+ var selfUuidMatch = element.className && element.className.match(_UUID_RE);
4876
+ if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
4877
+ var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
4878
+ uuidEls.forEach(function(uel) {
4879
+ var m = uel.className && uel.className.match(_UUID_RE);
4880
+ if (m) delete bw._nodeMap[m[0]];
4881
+ });
4882
+
4337
4883
  // Find all elements with data-bw_id
4338
4884
  const elements = element.querySelectorAll('[data-bw_id]');
4339
4885
 
@@ -4349,6 +4895,10 @@ bw.cleanup = function(element) {
4349
4895
  // Deregister from node cache
4350
4896
  bw._deregisterNode(el, id);
4351
4897
 
4898
+ // Deregister UUID class from node cache
4899
+ var uuidMatch = el.className && el.className.match(_UUID_RE);
4900
+ if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
4901
+
4352
4902
  // Clean up pub/sub subscriptions tied to this element
4353
4903
  if (el._bw_subs) {
4354
4904
  el._bw_subs.forEach(function(unsub) { unsub(); });
@@ -4373,6 +4923,10 @@ bw.cleanup = function(element) {
4373
4923
  // Deregister from node cache
4374
4924
  bw._deregisterNode(element, id);
4375
4925
 
4926
+ // Deregister UUID class from node cache
4927
+ var elemUuidMatch = element.className && element.className.match(_UUID_RE);
4928
+ if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
4929
+
4376
4930
  // Clean up pub/sub subscriptions tied to element itself
4377
4931
  if (element._bw_subs) {
4378
4932
  element._bw_subs.forEach(function(unsub) { unsub(); });
@@ -4447,17 +5001,17 @@ bw.patch = function(id, content, attr) {
4447
5001
  if (attr) {
4448
5002
  // Patch an attribute
4449
5003
  el.setAttribute(attr, String(content));
4450
- } else if (Array.isArray(content)) {
5004
+ } else if (_isA(content)) {
4451
5005
  // Patch with array of children (strings and/or TACOs)
4452
5006
  el.innerHTML = '';
4453
5007
  content.forEach(function(item) {
4454
- if (typeof item === 'string' || typeof item === 'number') {
5008
+ if (_is(item, 'string') || _is(item, 'number')) {
4455
5009
  el.appendChild(document.createTextNode(String(item)));
4456
5010
  } else if (item && item.t) {
4457
5011
  el.appendChild(bw.createDOM(item));
4458
5012
  }
4459
5013
  });
4460
- } else if (typeof content === 'object' && content !== null && content.t) {
5014
+ } else if (_is(content, 'object') && content.t) {
4461
5015
  // Patch with a TACO — replace children
4462
5016
  el.innerHTML = '';
4463
5017
  el.appendChild(bw.createDOM(content));
@@ -4488,7 +5042,7 @@ bw.patch = function(id, content, attr) {
4488
5042
  bw.patchAll = function(patches) {
4489
5043
  var results = {};
4490
5044
  for (var id in patches) {
4491
- if (Object.prototype.hasOwnProperty.call(patches, id)) {
5045
+ if (_hop.call(patches, id)) {
4492
5046
  results[id] = bw.patch(id, patches[id]);
4493
5047
  }
4494
5048
  }
@@ -4585,7 +5139,7 @@ bw.pub = function(topic, detail) {
4585
5139
  snapshot[i].handler(detail);
4586
5140
  called++;
4587
5141
  } catch (err) {
4588
- console.warn('bw.pub: subscriber error on topic "' + topic + '":', err);
5142
+ _cw('bw.pub: subscriber error on topic "' + topic + '":', err);
4589
5143
  }
4590
5144
  }
4591
5145
  return called;
@@ -4681,8 +5235,8 @@ bw._fnIDCounter = 0;
4681
5235
  * @see bw.funcGetDispatchStr
4682
5236
  */
4683
5237
  bw.funcRegister = function(fn, name) {
4684
- if (typeof fn !== 'function') return '';
4685
- var fnID = (typeof name === 'string' && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
5238
+ if (!_is(fn, 'function')) return '';
5239
+ var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
4686
5240
  bw._fnRegistry[fnID] = fn;
4687
5241
  return fnID;
4688
5242
  };
@@ -4701,7 +5255,7 @@ bw.funcRegister = function(fn, name) {
4701
5255
  bw.funcGetById = function(name, errFn) {
4702
5256
  name = String(name);
4703
5257
  if (name in bw._fnRegistry) return bw._fnRegistry[name];
4704
- return (typeof errFn === 'function') ? errFn : function() { console.warn('bw.funcGetById: unregistered fn "' + name + '"'); };
5258
+ return _is(errFn, 'function') ? errFn : function() { _cw('bw.funcGetById: unregistered fn "' + name + '"'); };
4705
5259
  };
4706
5260
 
4707
5261
  /**
@@ -4742,13 +5296,30 @@ bw.funcUnregister = function(name) {
4742
5296
  bw.funcGetRegistry = function() {
4743
5297
  var copy = {};
4744
5298
  for (var k in bw._fnRegistry) {
4745
- if (Object.prototype.hasOwnProperty.call(bw._fnRegistry, k)) {
5299
+ if (_hop.call(bw._fnRegistry, k)) {
4746
5300
  copy[k] = bw._fnRegistry[k];
4747
5301
  }
4748
5302
  }
4749
5303
  return copy;
4750
5304
  };
4751
5305
 
5306
+ /**
5307
+ * Minimal runtime shim for funcRegister dispatch in static HTML.
5308
+ * When embedded in a `<script>` tag, provides just enough infrastructure
5309
+ * for `bw.funcGetById()` calls to resolve. The actual function bodies
5310
+ * are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
5311
+ * @type {string}
5312
+ * @category Function Registry
5313
+ */
5314
+ bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' +
5315
+ 'if(!bw._fnRegistry)bw._fnRegistry={};' +
5316
+ 'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' +
5317
+ 'console.warn("bw: unregistered fn "+n)};};' +
5318
+ 'bw.funcRegister=function(fn,name){' +
5319
+ 'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' +
5320
+ 'bw._fnRegistry[id]=fn;return id;};' +
5321
+ 'window.bw=bw;})();';
5322
+
4752
5323
  // ===================================================================================
4753
5324
  // Template Binding Utilities
4754
5325
  // ===================================================================================
@@ -4776,7 +5347,10 @@ bw._evaluatePath = function(state, path) {
4776
5347
  var parts = path.split('.');
4777
5348
  var val = state;
4778
5349
  for (var i = 0; i < parts.length; i++) {
4779
- if (val == null) return '';
5350
+ if (val == null) {
5351
+ if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
5352
+ return '';
5353
+ }
4780
5354
  val = val[parts[i]];
4781
5355
  }
4782
5356
  return (val == null) ? '' : val;
@@ -4796,7 +5370,7 @@ bw._evaluatePath = function(state, path) {
4796
5370
  */
4797
5371
  bw._compiledExprs = {};
4798
5372
  bw._resolveTemplate = function(str, state, compile) {
4799
- if (typeof str !== 'string' || str.indexOf('${') < 0) return str;
5373
+ if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
4800
5374
  var bindings = bw._parseBindings(str);
4801
5375
  if (bindings.length === 0) return str;
4802
5376
 
@@ -4818,6 +5392,7 @@ bw._resolveTemplate = function(str, state, compile) {
4818
5392
  try {
4819
5393
  val = bw._compiledExprs[b.expr](state);
4820
5394
  } catch (e) {
5395
+ if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
4821
5396
  val = '';
4822
5397
  }
4823
5398
  } else {
@@ -4926,7 +5501,7 @@ function ComponentHandle(taco) {
4926
5501
  this._state = {};
4927
5502
  if (o.state) {
4928
5503
  for (var k in o.state) {
4929
- if (Object.prototype.hasOwnProperty.call(o.state, k)) {
5504
+ if (_hop.call(o.state, k)) {
4930
5505
  this._state[k] = o.state[k];
4931
5506
  }
4932
5507
  }
@@ -4935,7 +5510,7 @@ function ComponentHandle(taco) {
4935
5510
  this._actions = {};
4936
5511
  if (o.actions) {
4937
5512
  for (var k2 in o.actions) {
4938
- if (Object.prototype.hasOwnProperty.call(o.actions, k2)) {
5513
+ if (_hop.call(o.actions, k2)) {
4939
5514
  this._actions[k2] = o.actions[k2];
4940
5515
  }
4941
5516
  }
@@ -4945,7 +5520,7 @@ function ComponentHandle(taco) {
4945
5520
  if (o.methods) {
4946
5521
  var self = this;
4947
5522
  for (var k3 in o.methods) {
4948
- if (Object.prototype.hasOwnProperty.call(o.methods, k3)) {
5523
+ if (_hop.call(o.methods, k3)) {
4949
5524
  this._methods[k3] = o.methods[k3];
4950
5525
  (function(methodName, methodFn) {
4951
5526
  self[methodName] = function() {
@@ -4963,7 +5538,7 @@ function ComponentHandle(taco) {
4963
5538
  willMount: o.willMount || null,
4964
5539
  mounted: o.mounted || null,
4965
5540
  willUpdate: o.willUpdate || null,
4966
- onUpdate: o.onUpdate || null,
5541
+ onUpdate: o.onUpdate || o.updated || null,
4967
5542
  unmount: o.unmount || null,
4968
5543
  willDestroy: o.willDestroy || null
4969
5544
  };
@@ -4978,14 +5553,23 @@ function ComponentHandle(taco) {
4978
5553
  this._compile = !!o.compile;
4979
5554
  this._bw_refs = {};
4980
5555
  this._refCounter = 0;
5556
+ // Child component ownership (Bug #5)
5557
+ this._children = [];
5558
+ this._parent = null;
5559
+ // Factory metadata for BCCL rebuild (Bug #6)
5560
+ this._factory = taco._bwFactory || null;
4981
5561
  }
4982
5562
 
5563
+ // Short alias for ComponentHandle.prototype (see alias block at top of file).
5564
+ // 28 method definitions × 25 chars = ~700B raw savings in minified output.
5565
+ var _chp = ComponentHandle.prototype;
5566
+
4983
5567
  // ── State Methods ──
4984
5568
 
4985
5569
  /**
4986
5570
  * Get a state value. Dot-path supported: `get('user.name')`
4987
5571
  */
4988
- ComponentHandle.prototype.get = function(key) {
5572
+ _chp.get = function(key) {
4989
5573
  return bw._evaluatePath(this._state, key);
4990
5574
  };
4991
5575
 
@@ -4995,12 +5579,13 @@ ComponentHandle.prototype.get = function(key) {
4995
5579
  * @param {*} value - New value
4996
5580
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
4997
5581
  */
4998
- ComponentHandle.prototype.set = function(key, value, opts) {
5582
+ _chp.set = function(key, value, opts) {
4999
5583
  // Dot-path set
5000
5584
  var parts = key.split('.');
5001
5585
  var obj = this._state;
5002
5586
  for (var i = 0; i < parts.length - 1; i++) {
5003
- if (obj[parts[i]] == null || typeof obj[parts[i]] !== 'object') {
5587
+ if (!_is(obj[parts[i]], 'object')) {
5588
+ if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
5004
5589
  obj[parts[i]] = {};
5005
5590
  }
5006
5591
  obj = obj[parts[i]];
@@ -5020,10 +5605,10 @@ ComponentHandle.prototype.set = function(key, value, opts) {
5020
5605
  /**
5021
5606
  * Get a shallow clone of the full state.
5022
5607
  */
5023
- ComponentHandle.prototype.getState = function() {
5608
+ _chp.getState = function() {
5024
5609
  var clone = {};
5025
5610
  for (var k in this._state) {
5026
- if (Object.prototype.hasOwnProperty.call(this._state, k)) {
5611
+ if (_hop.call(this._state, k)) {
5027
5612
  clone[k] = this._state[k];
5028
5613
  }
5029
5614
  }
@@ -5035,9 +5620,9 @@ ComponentHandle.prototype.getState = function() {
5035
5620
  * @param {Object} updates - Key-value pairs to merge
5036
5621
  * @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
5037
5622
  */
5038
- ComponentHandle.prototype.setState = function(updates, opts) {
5623
+ _chp.setState = function(updates, opts) {
5039
5624
  for (var k in updates) {
5040
- if (Object.prototype.hasOwnProperty.call(updates, k)) {
5625
+ if (_hop.call(updates, k)) {
5041
5626
  this._state[k] = updates[k];
5042
5627
  this._dirtyKeys[k] = true;
5043
5628
  }
@@ -5054,9 +5639,9 @@ ComponentHandle.prototype.setState = function(updates, opts) {
5054
5639
  /**
5055
5640
  * Push a value onto an array in state. Clones the array.
5056
5641
  */
5057
- ComponentHandle.prototype.push = function(key, val) {
5642
+ _chp.push = function(key, val) {
5058
5643
  var arr = this.get(key);
5059
- var newArr = Array.isArray(arr) ? arr.slice() : [];
5644
+ var newArr = _isA(arr) ? arr.slice() : [];
5060
5645
  newArr.push(val);
5061
5646
  this.set(key, newArr);
5062
5647
  };
@@ -5064,9 +5649,9 @@ ComponentHandle.prototype.push = function(key, val) {
5064
5649
  /**
5065
5650
  * Splice an array in state. Clones the array.
5066
5651
  */
5067
- ComponentHandle.prototype.splice = function(key, start, deleteCount) {
5652
+ _chp.splice = function(key, start, deleteCount) {
5068
5653
  var arr = this.get(key);
5069
- var newArr = Array.isArray(arr) ? arr.slice() : [];
5654
+ var newArr = _isA(arr) ? arr.slice() : [];
5070
5655
  var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
5071
5656
  Array.prototype.splice.apply(newArr, args);
5072
5657
  this.set(key, newArr);
@@ -5074,7 +5659,7 @@ ComponentHandle.prototype.splice = function(key, start, deleteCount) {
5074
5659
 
5075
5660
  // ── Scheduling ──
5076
5661
 
5077
- ComponentHandle.prototype._scheduleDirty = function() {
5662
+ _chp._scheduleDirty = function() {
5078
5663
  if (!this._scheduled) {
5079
5664
  this._scheduled = true;
5080
5665
  bw._dirtyComponents.push(this);
@@ -5089,17 +5674,17 @@ ComponentHandle.prototype._scheduleDirty = function() {
5089
5674
  * Creates binding descriptors with refIds for targeted DOM updates.
5090
5675
  * @private
5091
5676
  */
5092
- ComponentHandle.prototype._compileBindings = function() {
5677
+ _chp._compileBindings = function() {
5093
5678
  this._bindings = [];
5094
5679
  this._refCounter = 0;
5095
- var stateKeys = Object.keys(this._state);
5680
+ var stateKeys = _keys(this._state);
5096
5681
  var self = this;
5097
5682
 
5098
5683
  function walkTaco(taco, path) {
5099
- if (taco == null || typeof taco !== 'object' || !taco.t) return taco;
5684
+ if (!_is(taco, 'object') || !taco.t) return taco;
5100
5685
 
5101
5686
  // Check content for bindings
5102
- if (typeof taco.c === 'string' && taco.c.indexOf('${') >= 0) {
5687
+ if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
5103
5688
  var refId = 'bw_ref_' + self._refCounter++;
5104
5689
  var parsed = bw._parseBindings(taco.c);
5105
5690
  var deps = [];
@@ -5121,10 +5706,10 @@ ComponentHandle.prototype._compileBindings = function() {
5121
5706
  // Check attributes for bindings
5122
5707
  if (taco.a) {
5123
5708
  for (var attrName in taco.a) {
5124
- if (!Object.prototype.hasOwnProperty.call(taco.a, attrName)) continue;
5709
+ if (!_hop.call(taco.a, attrName)) continue;
5125
5710
  if (attrName === 'data-bw_ref') continue;
5126
5711
  var attrVal = taco.a[attrName];
5127
- if (typeof attrVal === 'string' && attrVal.indexOf('${') >= 0) {
5712
+ if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
5128
5713
  var refId2 = 'bw_ref_' + self._refCounter++;
5129
5714
  var parsed2 = bw._parseBindings(attrVal);
5130
5715
  var deps2 = [];
@@ -5150,9 +5735,27 @@ ComponentHandle.prototype._compileBindings = function() {
5150
5735
  }
5151
5736
 
5152
5737
  // Recurse into children
5153
- if (Array.isArray(taco.c)) {
5738
+ if (_isA(taco.c)) {
5154
5739
  for (var i = 0; i < taco.c.length; i++) {
5155
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
5740
+ // Wrap string children with ${expr} in a span so patches target the span, not the parent
5741
+ if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
5742
+ var mixedRefId = 'bw_ref_' + self._refCounter++;
5743
+ var mixedParsed = bw._parseBindings(taco.c[i]);
5744
+ var mixedDeps = [];
5745
+ for (var mi = 0; mi < mixedParsed.length; mi++) {
5746
+ mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
5747
+ }
5748
+ self._bindings.push({
5749
+ expr: taco.c[i],
5750
+ type: 'content',
5751
+ refId: mixedRefId,
5752
+ deps: mixedDeps,
5753
+ template: taco.c[i]
5754
+ });
5755
+ // Replace string with a span wrapper so textContent targets the span only
5756
+ taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
5757
+ }
5758
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
5156
5759
  walkTaco(taco.c[i], path.concat(i));
5157
5760
  }
5158
5761
  // Handle bw.when/bw.each markers
@@ -5187,7 +5790,7 @@ ComponentHandle.prototype._compileBindings = function() {
5187
5790
  taco.c[i]._refId = eachRefId;
5188
5791
  }
5189
5792
  }
5190
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
5793
+ } else if (_is(taco.c, 'object') && taco.c.t) {
5191
5794
  walkTaco(taco.c, path.concat(0));
5192
5795
  }
5193
5796
 
@@ -5203,7 +5806,7 @@ ComponentHandle.prototype._compileBindings = function() {
5203
5806
  * Build ref map from the live DOM after createDOM.
5204
5807
  * @private
5205
5808
  */
5206
- ComponentHandle.prototype._collectRefs = function() {
5809
+ _chp._collectRefs = function() {
5207
5810
  this._bw_refs = {};
5208
5811
  if (!this.element) return;
5209
5812
  var els = this.element.querySelectorAll('[data-bw_ref]');
@@ -5224,7 +5827,7 @@ ComponentHandle.prototype._collectRefs = function() {
5224
5827
  * Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
5225
5828
  * @param {Element} parentEl - DOM element to mount into
5226
5829
  */
5227
- ComponentHandle.prototype.mount = function(parentEl) {
5830
+ _chp.mount = function(parentEl) {
5228
5831
  // willMount hook
5229
5832
  if (this._hooks.willMount) this._hooks.willMount(this);
5230
5833
 
@@ -5246,7 +5849,7 @@ ComponentHandle.prototype.mount = function(parentEl) {
5246
5849
  // Register named actions in function registry
5247
5850
  var self = this;
5248
5851
  for (var actionName in this._actions) {
5249
- if (Object.prototype.hasOwnProperty.call(this._actions, actionName)) {
5852
+ if (_hop.call(this._actions, actionName)) {
5250
5853
  var registeredName = this._bwId + '_' + actionName;
5251
5854
  (function(aName) {
5252
5855
  bw.funcRegister(function(evt) {
@@ -5265,6 +5868,11 @@ ComponentHandle.prototype.mount = function(parentEl) {
5265
5868
  this.element = bw.createDOM(tacoForDOM);
5266
5869
  this.element._bwComponentHandle = this;
5267
5870
  this.element.setAttribute('data-bw_comp_id', this._bwId);
5871
+
5872
+ // Restore o.render from original TACO (stripped by _tacoForDOM)
5873
+ if (this.taco.o && this.taco.o.render) {
5874
+ this.element._bw_render = this.taco.o.render;
5875
+ }
5268
5876
  if (this._userTag) {
5269
5877
  this.element.classList.add(this._userTag);
5270
5878
  }
@@ -5280,6 +5888,16 @@ ComponentHandle.prototype.mount = function(parentEl) {
5280
5888
 
5281
5889
  this.mounted = true;
5282
5890
 
5891
+ // Scan for child ComponentHandles and link parent/child (Bug #5)
5892
+ var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
5893
+ for (var ci = 0; ci < childEls.length; ci++) {
5894
+ var ch = childEls[ci]._bwComponentHandle;
5895
+ if (ch && ch !== this && !ch._parent) {
5896
+ ch._parent = this;
5897
+ this._children.push(ch);
5898
+ }
5899
+ }
5900
+
5283
5901
  // mounted hook (backward compat: fn.length === 2 wraps (el, state))
5284
5902
  if (this._hooks.mounted) {
5285
5903
  if (this._hooks.mounted.length === 2) {
@@ -5288,16 +5906,21 @@ ComponentHandle.prototype.mount = function(parentEl) {
5288
5906
  this._hooks.mounted(this);
5289
5907
  }
5290
5908
  }
5909
+
5910
+ // Invoke o.render on initial mount (if present)
5911
+ if (this.element._bw_render) {
5912
+ this.element._bw_render(this.element, this._state);
5913
+ }
5291
5914
  };
5292
5915
 
5293
5916
  /**
5294
5917
  * Prepare TACO for initial render: resolve when/each markers.
5295
5918
  * @private
5296
5919
  */
5297
- ComponentHandle.prototype._prepareTaco = function(taco) {
5298
- if (!taco || typeof taco !== 'object') return;
5920
+ _chp._prepareTaco = function(taco) {
5921
+ if (!_is(taco, 'object')) return;
5299
5922
 
5300
- if (Array.isArray(taco.c)) {
5923
+ if (_isA(taco.c)) {
5301
5924
  for (var i = taco.c.length - 1; i >= 0; i--) {
5302
5925
  var child = taco.c[i];
5303
5926
  if (child && child._bwWhen) {
@@ -5322,18 +5945,18 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
5322
5945
  var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
5323
5946
  var arr = bw._evaluatePath(this._state, eachExprStr);
5324
5947
  var items = [];
5325
- if (Array.isArray(arr)) {
5948
+ if (_isA(arr)) {
5326
5949
  for (var j = 0; j < arr.length; j++) {
5327
5950
  items.push(child.factory(arr[j], j));
5328
5951
  }
5329
5952
  }
5330
5953
  taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
5331
5954
  }
5332
- if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
5955
+ if (_is(taco.c[i], 'object') && taco.c[i].t) {
5333
5956
  this._prepareTaco(taco.c[i]);
5334
5957
  }
5335
5958
  }
5336
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
5959
+ } else if (_is(taco.c, 'object') && taco.c.t) {
5337
5960
  this._prepareTaco(taco.c);
5338
5961
  }
5339
5962
  };
@@ -5342,12 +5965,12 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
5342
5965
  * Wire action name strings (in onclick etc.) to dispatch function calls.
5343
5966
  * @private
5344
5967
  */
5345
- ComponentHandle.prototype._wireActions = function(taco) {
5346
- if (!taco || typeof taco !== 'object' || !taco.t) return;
5968
+ _chp._wireActions = function(taco) {
5969
+ if (!_is(taco, 'object') || !taco.t) return;
5347
5970
  if (taco.a) {
5348
5971
  for (var key in taco.a) {
5349
- if (!Object.prototype.hasOwnProperty.call(taco.a, key)) continue;
5350
- if (key.startsWith('on') && typeof taco.a[key] === 'string') {
5972
+ if (!_hop.call(taco.a, key)) continue;
5973
+ if (key.startsWith('on') && _is(taco.a[key], 'string')) {
5351
5974
  var actionName = taco.a[key];
5352
5975
  if (actionName in this._actions) {
5353
5976
  var registeredName = this._bwId + '_' + actionName;
@@ -5361,11 +5984,11 @@ ComponentHandle.prototype._wireActions = function(taco) {
5361
5984
  }
5362
5985
  }
5363
5986
  }
5364
- if (Array.isArray(taco.c)) {
5987
+ if (_isA(taco.c)) {
5365
5988
  for (var i = 0; i < taco.c.length; i++) {
5366
5989
  this._wireActions(taco.c[i]);
5367
5990
  }
5368
- } else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
5991
+ } else if (_is(taco.c, 'object') && taco.c.t) {
5369
5992
  this._wireActions(taco.c);
5370
5993
  }
5371
5994
  };
@@ -5374,7 +5997,7 @@ ComponentHandle.prototype._wireActions = function(taco) {
5374
5997
  * Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
5375
5998
  * @private
5376
5999
  */
5377
- ComponentHandle.prototype._deepCloneTaco = function(taco) {
6000
+ _chp._deepCloneTaco = function(taco) {
5378
6001
  if (taco == null) return taco;
5379
6002
  // Preserve _bwWhen / _bwEach markers (contain functions)
5380
6003
  if (taco._bwWhen) {
@@ -5386,18 +6009,18 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
5386
6009
  if (taco._bwEach) {
5387
6010
  return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
5388
6011
  }
5389
- if (typeof taco !== 'object' || !taco.t) return taco;
6012
+ if (!_is(taco, 'object') || !taco.t) return taco;
5390
6013
  var result = { t: taco.t };
5391
6014
  if (taco.a) {
5392
6015
  result.a = {};
5393
6016
  for (var k in taco.a) {
5394
- if (Object.prototype.hasOwnProperty.call(taco.a, k)) result.a[k] = taco.a[k];
6017
+ if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
5395
6018
  }
5396
6019
  }
5397
6020
  if (taco.c != null) {
5398
- if (Array.isArray(taco.c)) {
6021
+ if (_isA(taco.c)) {
5399
6022
  result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
5400
- } else if (typeof taco.c === 'object') {
6023
+ } else if (_is(taco.c, 'object')) {
5401
6024
  result.c = this._deepCloneTaco(taco.c);
5402
6025
  } else {
5403
6026
  result.c = taco.c;
@@ -5411,27 +6034,31 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
5411
6034
  * Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
5412
6035
  * @private
5413
6036
  */
5414
- ComponentHandle.prototype._tacoForDOM = function(taco) {
5415
- if (!taco || typeof taco !== 'object' || !taco.t) return taco;
6037
+ _chp._tacoForDOM = function(taco) {
6038
+ if (!_is(taco, 'object') || !taco.t) return taco;
5416
6039
  var result = { t: taco.t };
5417
6040
  if (taco.a) result.a = taco.a;
5418
6041
  if (taco.c != null) {
5419
- if (Array.isArray(taco.c)) {
6042
+ if (_isA(taco.c)) {
5420
6043
  result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
5421
- } else if (typeof taco.c === 'object' && taco.c.t) {
6044
+ } else if (_is(taco.c, 'object') && taco.c.t) {
5422
6045
  result.c = this._tacoForDOM(taco.c);
5423
6046
  } else {
5424
6047
  result.c = taco.c;
5425
6048
  }
5426
6049
  }
5427
6050
  // Intentionally strip o (no mounted/unmount/state/render on sub-elements)
6051
+ if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
6052
+ _cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
6053
+ '>. Use onclick attribute or bw.component() for child interactivity.');
6054
+ }
5428
6055
  return result;
5429
6056
  };
5430
6057
 
5431
6058
  /**
5432
6059
  * Unmount: remove from DOM, deactivate, preserve state for re-mount.
5433
6060
  */
5434
- ComponentHandle.prototype.unmount = function() {
6061
+ _chp.unmount = function() {
5435
6062
  if (!this.mounted) return;
5436
6063
 
5437
6064
  // unmount hook
@@ -5466,12 +6093,23 @@ ComponentHandle.prototype.unmount = function() {
5466
6093
  /**
5467
6094
  * Destroy: unmount + clear state + unregister actions.
5468
6095
  */
5469
- ComponentHandle.prototype.destroy = function() {
6096
+ _chp.destroy = function() {
5470
6097
  // willDestroy hook
5471
6098
  if (this._hooks.willDestroy) {
5472
6099
  this._hooks.willDestroy(this);
5473
6100
  }
5474
6101
 
6102
+ // Cascade destroy to children depth-first (Bug #5)
6103
+ for (var ci = this._children.length - 1; ci >= 0; ci--) {
6104
+ this._children[ci].destroy();
6105
+ }
6106
+ this._children = [];
6107
+ if (this._parent) {
6108
+ var idx = this._parent._children.indexOf(this);
6109
+ if (idx >= 0) this._parent._children.splice(idx, 1);
6110
+ this._parent = null;
6111
+ }
6112
+
5475
6113
  this.unmount();
5476
6114
 
5477
6115
  // Unregister actions from function registry
@@ -5498,12 +6136,36 @@ ComponentHandle.prototype.destroy = function() {
5498
6136
  * Flush dirty state: resolve changed bindings and apply to DOM.
5499
6137
  * @private
5500
6138
  */
5501
- ComponentHandle.prototype._flush = function() {
6139
+ _chp._flush = function() {
5502
6140
  this._scheduled = false;
5503
- var changedKeys = Object.keys(this._dirtyKeys);
6141
+ var changedKeys = _keys(this._dirtyKeys);
5504
6142
  this._dirtyKeys = {};
5505
6143
  if (changedKeys.length === 0 || !this.mounted) return;
5506
6144
 
6145
+ // Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
6146
+ // rebuild the TACO from the factory with merged state (Bug #6)
6147
+ if (this._factory) {
6148
+ var rebuildNeeded = false;
6149
+ for (var fi = 0; fi < changedKeys.length; fi++) {
6150
+ if (_hop.call(this._factory.props, changedKeys[fi])) {
6151
+ rebuildNeeded = true; break;
6152
+ }
6153
+ }
6154
+ if (rebuildNeeded) {
6155
+ var merged = {};
6156
+ for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
6157
+ for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
6158
+ this._factory.props = merged;
6159
+ var newTaco = bw.make(this._factory.type, merged);
6160
+ newTaco._bwFactory = this._factory;
6161
+ this.taco = newTaco;
6162
+ this._originalTaco = this._deepCloneTaco(newTaco);
6163
+ this._render();
6164
+ if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
6165
+ return;
6166
+ }
6167
+ }
6168
+
5507
6169
  // willUpdate hook
5508
6170
  if (this._hooks.willUpdate) {
5509
6171
  this._hooks.willUpdate(this, changedKeys);
@@ -5542,7 +6204,7 @@ ComponentHandle.prototype._flush = function() {
5542
6204
  * Returns list of patches to apply.
5543
6205
  * @private
5544
6206
  */
5545
- ComponentHandle.prototype._resolveBindings = function(changedKeys) {
6207
+ _chp._resolveBindings = function(changedKeys) {
5546
6208
  var patches = [];
5547
6209
  for (var i = 0; i < this._bindings.length; i++) {
5548
6210
  var b = this._bindings[i];
@@ -5578,11 +6240,14 @@ ComponentHandle.prototype._resolveBindings = function(changedKeys) {
5578
6240
  * Apply patches to DOM.
5579
6241
  * @private
5580
6242
  */
5581
- ComponentHandle.prototype._applyPatches = function(patches) {
6243
+ _chp._applyPatches = function(patches) {
5582
6244
  for (var i = 0; i < patches.length; i++) {
5583
6245
  var p = patches[i];
5584
6246
  var el = this._bw_refs[p.refId];
5585
- if (!el) continue;
6247
+ if (!el) {
6248
+ if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
6249
+ continue;
6250
+ }
5586
6251
  if (p.type === 'content') {
5587
6252
  el.textContent = p.value;
5588
6253
  } else if (p.type === 'attribute') {
@@ -5599,7 +6264,7 @@ ComponentHandle.prototype._applyPatches = function(patches) {
5599
6264
  * Resolve all bindings and apply (used for initial render).
5600
6265
  * @private
5601
6266
  */
5602
- ComponentHandle.prototype._resolveAndApplyAll = function() {
6267
+ _chp._resolveAndApplyAll = function() {
5603
6268
  var patches = [];
5604
6269
  for (var i = 0; i < this._bindings.length; i++) {
5605
6270
  var b = this._bindings[i];
@@ -5622,7 +6287,7 @@ ComponentHandle.prototype._resolveAndApplyAll = function() {
5622
6287
  * Full re-render for structural changes (when/each branch switches).
5623
6288
  * @private
5624
6289
  */
5625
- ComponentHandle.prototype._render = function() {
6290
+ _chp._render = function() {
5626
6291
  if (!this.element || !this.element.parentNode) return;
5627
6292
  var parent = this.element.parentNode;
5628
6293
  var nextSibling = this.element.nextSibling;
@@ -5662,7 +6327,7 @@ ComponentHandle.prototype._render = function() {
5662
6327
  * @param {string} event - Event name (e.g., 'click')
5663
6328
  * @param {Function} handler - Event handler
5664
6329
  */
5665
- ComponentHandle.prototype.on = function(event, handler) {
6330
+ _chp.on = function(event, handler) {
5666
6331
  if (this.element) {
5667
6332
  this.element.addEventListener(event, handler);
5668
6333
  }
@@ -5674,7 +6339,7 @@ ComponentHandle.prototype.on = function(event, handler) {
5674
6339
  * @param {string} event - Event name
5675
6340
  * @param {Function} handler - Handler to remove
5676
6341
  */
5677
- ComponentHandle.prototype.off = function(event, handler) {
6342
+ _chp.off = function(event, handler) {
5678
6343
  if (this.element) {
5679
6344
  this.element.removeEventListener(event, handler);
5680
6345
  }
@@ -5689,7 +6354,7 @@ ComponentHandle.prototype.off = function(event, handler) {
5689
6354
  * @param {Function} handler - Handler function
5690
6355
  * @returns {Function} Unsubscribe function
5691
6356
  */
5692
- ComponentHandle.prototype.sub = function(topic, handler) {
6357
+ _chp.sub = function(topic, handler) {
5693
6358
  var unsub = bw.sub(topic, handler);
5694
6359
  this._subs.push(unsub);
5695
6360
  return unsub;
@@ -5700,10 +6365,10 @@ ComponentHandle.prototype.sub = function(topic, handler) {
5700
6365
  * @param {string} name - Action name
5701
6366
  * @param {...*} args - Arguments passed after comp
5702
6367
  */
5703
- ComponentHandle.prototype.action = function(name) {
6368
+ _chp.action = function(name) {
5704
6369
  var fn = this._actions[name];
5705
6370
  if (!fn) {
5706
- console.warn('ComponentHandle.action: unknown action "' + name + '"');
6371
+ _cw('ComponentHandle.action: unknown action "' + name + '"');
5707
6372
  return;
5708
6373
  }
5709
6374
  var args = [this].concat(Array.prototype.slice.call(arguments, 1));
@@ -5715,7 +6380,7 @@ ComponentHandle.prototype.action = function(name) {
5715
6380
  * @param {string} sel - CSS selector
5716
6381
  * @returns {Element|null}
5717
6382
  */
5718
- ComponentHandle.prototype.select = function(sel) {
6383
+ _chp.select = function(sel) {
5719
6384
  return this.element ? this.element.querySelector(sel) : null;
5720
6385
  };
5721
6386
 
@@ -5724,7 +6389,7 @@ ComponentHandle.prototype.select = function(sel) {
5724
6389
  * @param {string} sel - CSS selector
5725
6390
  * @returns {Element[]}
5726
6391
  */
5727
- ComponentHandle.prototype.selectAll = function(sel) {
6392
+ _chp.selectAll = function(sel) {
5728
6393
  if (!this.element) return [];
5729
6394
  return Array.prototype.slice.call(this.element.querySelectorAll(sel));
5730
6395
  };
@@ -5735,7 +6400,7 @@ ComponentHandle.prototype.selectAll = function(sel) {
5735
6400
  * @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
5736
6401
  * @returns {ComponentHandle} this (for chaining)
5737
6402
  */
5738
- ComponentHandle.prototype.userTag = function(tag) {
6403
+ _chp.userTag = function(tag) {
5739
6404
  this._userTag = tag;
5740
6405
  if (this.element) {
5741
6406
  this.element.classList.add(tag);
@@ -5812,7 +6477,7 @@ bw.component = function(taco) {
5812
6477
  * and calls the named method. This is the bitwrench equivalent of
5813
6478
  * Win32 SendMessage(hwnd, msg, wParam, lParam).
5814
6479
  *
5815
- * @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
6480
+ * @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
5816
6481
  * @param {string} action - Method name to call on the component
5817
6482
  * @param {*} data - Data to pass to the method
5818
6483
  * @returns {boolean} True if message was dispatched successfully
@@ -5829,15 +6494,20 @@ bw.component = function(taco) {
5829
6494
  * };
5830
6495
  */
5831
6496
  bw.message = function(target, action, data) {
5832
- // Try data-bw_comp_id attribute first, then CSS class (user tag)
5833
- var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
5834
- if (!el) {
6497
+ // Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
6498
+ var el = bw._el(target);
6499
+ // Then try data-bw_comp_id attribute
6500
+ if (!el || !el._bwComponentHandle) {
6501
+ el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
6502
+ }
6503
+ // Then try CSS class (user tag)
6504
+ if (!el || !el._bwComponentHandle) {
5835
6505
  el = bw.$('.' + target)[0];
5836
6506
  }
5837
6507
  if (!el || !el._bwComponentHandle) return false;
5838
6508
  var comp = el._bwComponentHandle;
5839
- if (typeof comp[action] !== 'function') {
5840
- console.warn('bw.message: unknown action "' + action + '" on component ' + target);
6509
+ if (!_is(comp[action], 'function')) {
6510
+ _cw('bw.message: unknown action "' + action + '" on component ' + target);
5841
6511
  return false;
5842
6512
  }
5843
6513
  comp[action](data);
@@ -5845,59 +6515,24 @@ bw.message = function(target, action, data) {
5845
6515
  };
5846
6516
 
5847
6517
  // ===================================================================================
5848
- // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
6518
+ // bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
5849
6519
  // ===================================================================================
5850
6520
 
5851
6521
  /**
5852
6522
  * Registry of named functions sent via register messages.
5853
- * Populated by clientApply({ type: 'register', name, body }).
5854
- * Invoked by clientApply({ type: 'call', name, args }).
6523
+ * Populated by bw.apply({ type: 'register', name, body }).
6524
+ * Invoked by bw.apply({ type: 'call', name, args }).
5855
6525
  * @private
5856
6526
  */
5857
6527
  bw._clientFunctions = {};
5858
6528
 
5859
6529
  /**
5860
- * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
6530
+ * Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
5861
6531
  * Default false — exec messages are rejected unless explicitly opted in.
5862
6532
  * @private
5863
6533
  */
5864
6534
  bw._allowExec = false;
5865
6535
 
5866
- /**
5867
- * Built-in client functions available via call() without registration.
5868
- * @private
5869
- */
5870
- bw._builtinClientFunctions = {
5871
- scrollTo: function(selector) {
5872
- var el = bw._el(selector);
5873
- if (el) el.scrollTop = el.scrollHeight;
5874
- },
5875
- focus: function(selector) {
5876
- var el = bw._el(selector);
5877
- if (el && typeof el.focus === 'function') el.focus();
5878
- },
5879
- download: function(filename, content, mimeType) {
5880
- if (typeof document === 'undefined') return;
5881
- var blob = new Blob([content], { type: mimeType || 'text/plain' });
5882
- var a = document.createElement('a');
5883
- a.href = URL.createObjectURL(blob);
5884
- a.download = filename;
5885
- a.click();
5886
- URL.revokeObjectURL(a.href);
5887
- },
5888
- clipboard: function(text) {
5889
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
5890
- navigator.clipboard.writeText(text);
5891
- }
5892
- },
5893
- redirect: function(url) {
5894
- if (typeof window !== 'undefined') window.location.href = url;
5895
- },
5896
- log: function() {
5897
- console.log.apply(console, arguments);
5898
- }
5899
- };
5900
-
5901
6536
  /**
5902
6537
  * Parse a bwserve protocol message string, supporting both strict JSON
5903
6538
  * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
@@ -5912,9 +6547,9 @@ bw._builtinClientFunctions = {
5912
6547
  * @param {string} str - JSON or r-prefixed relaxed JSON string
5913
6548
  * @returns {Object} Parsed message object
5914
6549
  * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
5915
- * @category Server
6550
+ * @category Core
5916
6551
  */
5917
- bw.clientParse = function(str) {
6552
+ bw.parseJSONFlex = function(str) {
5918
6553
  str = (str || '').trim();
5919
6554
  if (str.charAt(0) !== 'r') return JSON.parse(str);
5920
6555
  str = str.slice(1);
@@ -5999,10 +6634,10 @@ bw.clientParse = function(str) {
5999
6634
  * append — target.appendChild(bw.createDOM(node))
6000
6635
  * remove — bw.cleanup(target); target.remove()
6001
6636
  * patch — bw.patch(target, content, attr)
6002
- * batch — iterate ops, call clientApply for each
6637
+ * batch — iterate ops, call bw.apply for each
6003
6638
  * message — bw.message(target, action, data)
6004
6639
  * register — store a named function for later call()
6005
- * call — invoke a registered or built-in function
6640
+ * call — invoke a registered function
6006
6641
  * exec — execute arbitrary JS (requires allowExec)
6007
6642
  *
6008
6643
  * Target resolution:
@@ -6011,9 +6646,9 @@ bw.clientParse = function(str) {
6011
6646
  *
6012
6647
  * @param {Object} msg - Protocol message
6013
6648
  * @returns {boolean} true if the message was applied successfully
6014
- * @category Server
6649
+ * @category Core
6015
6650
  */
6016
- bw.clientApply = function(msg) {
6651
+ bw.apply = function(msg) {
6017
6652
  if (!msg || !msg.type) return false;
6018
6653
 
6019
6654
  var type = msg.type;
@@ -6039,15 +6674,15 @@ bw.clientApply = function(msg) {
6039
6674
  } else if (type === 'remove') {
6040
6675
  var toRemove = bw._el(target);
6041
6676
  if (!toRemove) return false;
6042
- if (typeof bw.cleanup === 'function') bw.cleanup(toRemove);
6677
+ if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
6043
6678
  toRemove.remove();
6044
6679
  return true;
6045
6680
 
6046
6681
  } else if (type === 'batch') {
6047
- if (!Array.isArray(msg.ops)) return false;
6682
+ if (!_isA(msg.ops)) return false;
6048
6683
  var allOk = true;
6049
6684
  msg.ops.forEach(function(op) {
6050
- if (!bw.clientApply(op)) allOk = false;
6685
+ if (!bw.apply(op)) allOk = false;
6051
6686
  });
6052
6687
  return allOk;
6053
6688
 
@@ -6060,26 +6695,26 @@ bw.clientApply = function(msg) {
6060
6695
  bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
6061
6696
  return true;
6062
6697
  } catch (e) {
6063
- console.error('[bw] register error:', msg.name, e);
6698
+ _ce('[bw] register error:', msg.name, e);
6064
6699
  return false;
6065
6700
  }
6066
6701
 
6067
6702
  } else if (type === 'call') {
6068
6703
  if (!msg.name) return false;
6069
- var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
6070
- if (typeof fn !== 'function') return false;
6704
+ var fn = bw._clientFunctions[msg.name];
6705
+ if (!_is(fn, 'function')) return false;
6071
6706
  try {
6072
- var args = Array.isArray(msg.args) ? msg.args : [];
6707
+ var args = _isA(msg.args) ? msg.args : [];
6073
6708
  fn.apply(null, args);
6074
6709
  return true;
6075
6710
  } catch (e) {
6076
- console.error('[bw] call error:', msg.name, e);
6711
+ _ce('[bw] call error:', msg.name, e);
6077
6712
  return false;
6078
6713
  }
6079
6714
 
6080
6715
  } else if (type === 'exec') {
6081
6716
  if (!bw._allowExec) {
6082
- console.warn('[bw] exec rejected: allowExec is not enabled');
6717
+ _cw('[bw] exec rejected: allowExec is not enabled');
6083
6718
  return false;
6084
6719
  }
6085
6720
  if (!msg.code) return false;
@@ -6087,7 +6722,7 @@ bw.clientApply = function(msg) {
6087
6722
  new Function(msg.code)();
6088
6723
  return true;
6089
6724
  } catch (e) {
6090
- console.error('[bw] exec error:', e);
6725
+ _ce('[bw] exec error:', e);
6091
6726
  return false;
6092
6727
  }
6093
6728
  }
@@ -6095,139 +6730,6 @@ bw.clientApply = function(msg) {
6095
6730
  return false;
6096
6731
  };
6097
6732
 
6098
- /**
6099
- * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
6100
- *
6101
- * Returns a connection object with sendAction(), on(), and close() methods.
6102
- *
6103
- * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
6104
- * @param {Object} [opts] - Connection options
6105
- * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
6106
- * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
6107
- * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
6108
- * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
6109
- * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
6110
- * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
6111
- * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
6112
- * @returns {Object} Connection object { sendAction, on, close, status }
6113
- * @category Server
6114
- */
6115
- bw.clientConnect = function(url, opts) {
6116
- opts = opts || {};
6117
- var transport = opts.transport || 'sse';
6118
- var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
6119
- var reconnect = opts.reconnect !== false;
6120
- var onStatus = opts.onStatus || function() {};
6121
- var onMessage = opts.onMessage || null;
6122
- var handlers = {};
6123
- // Set the global allowExec flag from connection options
6124
- bw._allowExec = !!opts.allowExec;
6125
- var conn = {
6126
- status: 'connecting',
6127
- _es: null,
6128
- _pollTimer: null
6129
- };
6130
-
6131
- function setStatus(s) {
6132
- conn.status = s;
6133
- onStatus(s);
6134
- }
6135
-
6136
- function handleMessage(data) {
6137
- try {
6138
- var msg = typeof data === 'string' ? bw.clientParse(data) : data;
6139
- if (onMessage) onMessage(msg);
6140
- if (handlers.message) handlers.message(msg);
6141
- bw.clientApply(msg);
6142
- } catch (e) {
6143
- if (handlers.error) handlers.error(e);
6144
- }
6145
- }
6146
-
6147
- if (transport === 'sse' && typeof EventSource !== 'undefined') {
6148
- setStatus('connecting');
6149
- var es = new EventSource(url);
6150
- conn._es = es;
6151
-
6152
- es.onopen = function() {
6153
- setStatus('connected');
6154
- if (handlers.open) handlers.open();
6155
- };
6156
-
6157
- es.onmessage = function(e) {
6158
- handleMessage(e.data);
6159
- };
6160
-
6161
- es.onerror = function() {
6162
- if (conn.status === 'connected') {
6163
- setStatus('disconnected');
6164
- }
6165
- if (handlers.error) handlers.error(new Error('SSE connection error'));
6166
- if (!reconnect) {
6167
- es.close();
6168
- }
6169
- // EventSource auto-reconnects by default when reconnect=true
6170
- };
6171
- } else if (transport === 'poll') {
6172
- var interval = opts.interval || 2000;
6173
- setStatus('connected');
6174
- conn._pollTimer = setInterval(function() {
6175
- fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
6176
- if (Array.isArray(msgs)) {
6177
- msgs.forEach(handleMessage);
6178
- } else if (msgs && msgs.type) {
6179
- handleMessage(msgs);
6180
- }
6181
- }).catch(function(e) {
6182
- if (handlers.error) handlers.error(e);
6183
- });
6184
- }, interval);
6185
- }
6186
-
6187
- /**
6188
- * Send an action to the server via POST.
6189
- * @param {string} action - Action name
6190
- * @param {Object} [data] - Action payload
6191
- */
6192
- conn.sendAction = function(action, data) {
6193
- var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
6194
- fetch(actionUrl, {
6195
- method: 'POST',
6196
- headers: { 'Content-Type': 'application/json' },
6197
- body: body
6198
- }).catch(function(e) {
6199
- if (handlers.error) handlers.error(e);
6200
- });
6201
- };
6202
-
6203
- /**
6204
- * Register an event handler.
6205
- * @param {string} event - 'open'|'message'|'error'|'close'
6206
- * @param {Function} handler
6207
- */
6208
- conn.on = function(event, handler) {
6209
- handlers[event] = handler;
6210
- return conn;
6211
- };
6212
-
6213
- /**
6214
- * Close the connection.
6215
- */
6216
- conn.close = function() {
6217
- if (conn._es) {
6218
- conn._es.close();
6219
- conn._es = null;
6220
- }
6221
- if (conn._pollTimer) {
6222
- clearInterval(conn._pollTimer);
6223
- conn._pollTimer = null;
6224
- }
6225
- setStatus('disconnected');
6226
- if (handlers.close) handlers.close();
6227
- };
6228
-
6229
- return conn;
6230
- };
6231
6733
 
6232
6734
  // ===================================================================================
6233
6735
  // bw.inspect() — Debug utility
@@ -6255,33 +6757,33 @@ bw.inspect = function(target) {
6255
6757
  el = target.element;
6256
6758
  comp = target;
6257
6759
  } else {
6258
- if (typeof target === 'string') {
6760
+ if (_is(target, 'string')) {
6259
6761
  el = bw.$(target)[0];
6260
6762
  }
6261
6763
  if (!el) {
6262
- console.warn('bw.inspect: element not found');
6764
+ _cw('bw.inspect: element not found');
6263
6765
  return null;
6264
6766
  }
6265
6767
  comp = el._bwComponentHandle;
6266
6768
  }
6267
6769
  if (!comp) {
6268
- console.log('bw.inspect: no ComponentHandle on this element');
6269
- console.log(' Tag:', el.tagName);
6270
- console.log(' Classes:', el.className);
6271
- console.log(' _bw_state:', el._bw_state || '(none)');
6770
+ _cl('bw.inspect: no ComponentHandle on this element');
6771
+ _cl(' Tag:', el.tagName);
6772
+ _cl(' Classes:', el.className);
6773
+ _cl(' _bw_state:', el._bw_state || '(none)');
6272
6774
  return null;
6273
6775
  }
6274
6776
  var deps = comp._bindings.reduce(function(s, b) {
6275
6777
  return s.concat(b.deps || []);
6276
6778
  }, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
6277
6779
  console.group('Component: ' + comp._bwId);
6278
- console.log('State:', comp._state);
6279
- console.log('Bindings:', comp._bindings.length, '(deps:', deps, ')');
6280
- console.log('Methods:', Object.keys(comp._methods));
6281
- console.log('Actions:', Object.keys(comp._actions));
6282
- console.log('User tag:', comp._userTag || '(none)');
6283
- console.log('Mounted:', comp.mounted);
6284
- console.log('Element:', comp.element);
6780
+ _cl('State:', comp._state);
6781
+ _cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
6782
+ _cl('Methods:', _keys(comp._methods));
6783
+ _cl('Actions:', _keys(comp._actions));
6784
+ _cl('User tag:', comp._userTag || '(none)');
6785
+ _cl('Mounted:', comp.mounted);
6786
+ _cl('Element:', comp.element);
6285
6787
  console.groupEnd();
6286
6788
  return comp;
6287
6789
  };
@@ -6304,8 +6806,8 @@ bw.compile = function(taco) {
6304
6806
  // Pre-extract all binding expressions
6305
6807
  var precompiled = [];
6306
6808
  function walkExpressions(node) {
6307
- if (!node || typeof node !== 'object') return;
6308
- if (typeof node.c === 'string' && node.c.indexOf('${') >= 0) {
6809
+ if (!_is(node, 'object')) return;
6810
+ if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
6309
6811
  var parsed = bw._parseBindings(node.c);
6310
6812
  for (var i = 0; i < parsed.length; i++) {
6311
6813
  try {
@@ -6320,9 +6822,9 @@ bw.compile = function(taco) {
6320
6822
  }
6321
6823
  if (node.a) {
6322
6824
  for (var key in node.a) {
6323
- if (Object.prototype.hasOwnProperty.call(node.a, key)) {
6825
+ if (_hop.call(node.a, key)) {
6324
6826
  var v = node.a[key];
6325
- if (typeof v === 'string' && v.indexOf('${') >= 0) {
6827
+ if (_is(v, 'string') && v.indexOf('${') >= 0) {
6326
6828
  var parsed2 = bw._parseBindings(v);
6327
6829
  for (var j = 0; j < parsed2.length; j++) {
6328
6830
  try {
@@ -6338,9 +6840,9 @@ bw.compile = function(taco) {
6338
6840
  }
6339
6841
  }
6340
6842
  }
6341
- if (Array.isArray(node.c)) {
6843
+ if (_isA(node.c)) {
6342
6844
  for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
6343
- } else if (node.c && typeof node.c === 'object' && node.c.t) {
6845
+ } else if (_is(node.c, 'object') && node.c.t) {
6344
6846
  walkExpressions(node.c);
6345
6847
  }
6346
6848
  }
@@ -6352,7 +6854,7 @@ bw.compile = function(taco) {
6352
6854
  handle._precompiledBindings = precompiled;
6353
6855
  if (initialState) {
6354
6856
  for (var k in initialState) {
6355
- if (Object.prototype.hasOwnProperty.call(initialState, k)) {
6857
+ if (_hop.call(initialState, k)) {
6356
6858
  handle._state[k] = initialState[k];
6357
6859
  }
6358
6860
  }
@@ -6383,18 +6885,18 @@ bw.compile = function(taco) {
6383
6885
  bw.css = function(rules, options = {}) {
6384
6886
  const { minify = false, pretty = !minify } = options;
6385
6887
 
6386
- if (typeof rules === 'string') return rules;
6888
+ if (_is(rules, 'string')) return rules;
6387
6889
 
6388
6890
  let css = '';
6389
6891
  const indent = pretty ? ' ' : '';
6390
6892
  const newline = pretty ? '\n' : '';
6391
6893
  const space = pretty ? ' ' : '';
6392
6894
 
6393
- if (Array.isArray(rules)) {
6895
+ if (_isA(rules)) {
6394
6896
  css = rules.map(rule => bw.css(rule, options)).join(newline);
6395
- } else if (typeof rules === 'object') {
6897
+ } else if (_is(rules, 'object')) {
6396
6898
  Object.entries(rules).forEach(([selector, styles]) => {
6397
- if (typeof styles === 'object' && !Array.isArray(styles)) {
6899
+ if (_is(styles, 'object')) {
6398
6900
  // Handle @media, @keyframes, @supports — recurse into nested block
6399
6901
  if (selector.charAt(0) === '@') {
6400
6902
  const inner = bw.css(styles, options);
@@ -6436,14 +6938,14 @@ bw.css = function(rules, options = {}) {
6436
6938
  * @returns {Element} The style element
6437
6939
  * @category CSS & Styling
6438
6940
  * @see bw.css
6439
- * @see bw.loadDefaultStyles
6941
+ * @see bw.loadStyles
6440
6942
  * @example
6441
6943
  * bw.injectCSS('.my-class { color: red; }');
6442
6944
  * bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
6443
6945
  */
6444
6946
  bw.injectCSS = function(css, options = {}) {
6445
6947
  if (!bw._isBrowser) {
6446
- console.warn('bw.injectCSS requires a DOM environment');
6948
+ _cw('bw.injectCSS requires a DOM environment');
6447
6949
  return null;
6448
6950
  }
6449
6951
 
@@ -6460,7 +6962,7 @@ bw.injectCSS = function(css, options = {}) {
6460
6962
  }
6461
6963
 
6462
6964
  // Convert CSS if needed
6463
- const cssStr = typeof css === 'string' ? css : bw.css(css, options);
6965
+ const cssStr = _is(css, 'string') ? css : bw.css(css, options);
6464
6966
 
6465
6967
  // Set or append CSS
6466
6968
  if (append && styleEl.textContent) {
@@ -6481,113 +6983,19 @@ bw.injectCSS = function(css, options = {}) {
6481
6983
  * @param {...Object} styles - Style objects to merge (left-to-right)
6482
6984
  * @returns {Object} Merged style object
6483
6985
  * @category CSS & Styling
6484
- * @see bw.u
6485
6986
  * @example
6486
- * var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
6987
+ * var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
6487
6988
  * // => { display: 'flex', gap: '1rem', color: 'red' }
6488
6989
  */
6489
6990
  bw.s = function() {
6490
6991
  var result = {};
6491
6992
  for (var i = 0; i < arguments.length; i++) {
6492
6993
  var arg = arguments[i];
6493
- if (arg && typeof arg === 'object') Object.assign(result, arg);
6994
+ if (_is(arg, 'object')) Object.assign(result, arg);
6494
6995
  }
6495
6996
  return result;
6496
6997
  };
6497
6998
 
6498
- /**
6499
- * Pre-built CSS utility objects (like Tailwind utilities, but in JS).
6500
- *
6501
- * Compose with `bw.s()` to build inline styles without writing raw CSS strings.
6502
- * Includes flex, padding, margin, typography, color, border, and transition utilities.
6503
- *
6504
- * @category CSS & Styling
6505
- * @see bw.s
6506
- * @example
6507
- * { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
6508
- * c: 'Flexbox with 1rem gap and padding' }
6509
- */
6510
- bw.u = {
6511
- // Display
6512
- flex: { display: 'flex' },
6513
- flexCol: { display: 'flex', flexDirection: 'column' },
6514
- flexRow: { display: 'flex', flexDirection: 'row' },
6515
- flexWrap: { display: 'flex', flexWrap: 'wrap' },
6516
- block: { display: 'block' },
6517
- inline: { display: 'inline' },
6518
- hidden: { display: 'none' },
6519
-
6520
- // Flex alignment
6521
- justifyCenter: { justifyContent: 'center' },
6522
- justifyBetween: { justifyContent: 'space-between' },
6523
- justifyEnd: { justifyContent: 'flex-end' },
6524
- alignCenter: { alignItems: 'center' },
6525
- alignStart: { alignItems: 'flex-start' },
6526
- alignEnd: { alignItems: 'flex-end' },
6527
-
6528
- // Gap (0.25rem increments)
6529
- gap1: { gap: '0.25rem' },
6530
- gap2: { gap: '0.5rem' },
6531
- gap3: { gap: '0.75rem' },
6532
- gap4: { gap: '1rem' },
6533
- gap6: { gap: '1.5rem' },
6534
- gap8: { gap: '2rem' },
6535
-
6536
- // Padding
6537
- p0: { padding: '0' },
6538
- p1: { padding: '0.25rem' },
6539
- p2: { padding: '0.5rem' },
6540
- p3: { padding: '0.75rem' },
6541
- p4: { padding: '1rem' },
6542
- p6: { padding: '1.5rem' },
6543
- p8: { padding: '2rem' },
6544
- px4: { paddingLeft: '1rem', paddingRight: '1rem' },
6545
- py2: { paddingTop: '0.5rem', paddingBottom: '0.5rem' },
6546
- py4: { paddingTop: '1rem', paddingBottom: '1rem' },
6547
-
6548
- // Margin (same scale)
6549
- m0: { margin: '0' },
6550
- m4: { margin: '1rem' },
6551
- mt2: { marginTop: '0.5rem' },
6552
- mt4: { marginTop: '1rem' },
6553
- mb2: { marginBottom: '0.5rem' },
6554
- mb4: { marginBottom: '1rem' },
6555
- mx_auto: { marginLeft: 'auto', marginRight: 'auto' },
6556
-
6557
- // Typography
6558
- textSm: { fontSize: '0.875rem' },
6559
- textBase: { fontSize: '1rem' },
6560
- textLg: { fontSize: '1.125rem' },
6561
- textXl: { fontSize: '1.25rem' },
6562
- text2xl: { fontSize: '1.5rem' },
6563
- text3xl: { fontSize: '1.875rem' },
6564
- bold: { fontWeight: '700' },
6565
- semibold: { fontWeight: '600' },
6566
- italic: { fontStyle: 'italic' },
6567
- textCenter: { textAlign: 'center' },
6568
- textRight: { textAlign: 'right' },
6569
-
6570
- // Colors (from design tokens)
6571
- bgWhite: { background: '#ffffff' },
6572
- bgTeal: { background: '#006666', color: '#ffffff' },
6573
- textWhite: { color: '#ffffff' },
6574
- textTeal: { color: '#006666' },
6575
- textMuted: { color: '#888' },
6576
-
6577
- // Borders
6578
- rounded: { borderRadius: '0.375rem' },
6579
- roundedLg: { borderRadius: '0.5rem' },
6580
- roundedFull: { borderRadius: '9999px' },
6581
- border: { border: '1px solid #d8d8d8' },
6582
-
6583
- // Sizing
6584
- wFull: { width: '100%' },
6585
- hFull: { height: '100%' },
6586
-
6587
- // Transitions
6588
- transition: { transition: 'all 0.2s ease' }
6589
- };
6590
-
6591
6999
  /**
6592
7000
  * Generate responsive CSS with media query breakpoints.
6593
7001
  *
@@ -6613,7 +7021,7 @@ bw.u = {
6613
7021
  bw.responsive = function(selector, breakpoints) {
6614
7022
  var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
6615
7023
  var parts = [];
6616
- Object.keys(breakpoints).forEach(function(key) {
7024
+ _keys(breakpoints).forEach(function(key) {
6617
7025
  var rules = {};
6618
7026
  if (key === 'base') {
6619
7027
  rules[selector] = breakpoints[key];
@@ -6685,18 +7093,18 @@ if (bw._isBrowser) {
6685
7093
  if (!selector) return [];
6686
7094
 
6687
7095
  // Already an array
6688
- if (Array.isArray(selector)) return selector;
7096
+ if (_isA(selector)) return selector;
6689
7097
 
6690
7098
  // Single element
6691
7099
  if (selector.nodeType) return [selector];
6692
7100
 
6693
7101
  // NodeList or HTMLCollection
6694
- if (selector.length !== undefined && typeof selector !== 'string') {
7102
+ if (selector.length !== undefined && !_is(selector, 'string')) {
6695
7103
  return Array.from(selector);
6696
7104
  }
6697
7105
 
6698
7106
  // CSS selector string
6699
- if (typeof selector === 'string') {
7107
+ if (_is(selector, 'string')) {
6700
7108
  return Array.from(document.querySelectorAll(selector));
6701
7109
  }
6702
7110
 
@@ -6709,103 +7117,49 @@ if (bw._isBrowser) {
6709
7117
  };
6710
7118
  }
6711
7119
 
6712
- /**
6713
- * Load the built-in Bootstrap-inspired default stylesheet.
6714
- *
6715
- * Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
6716
- * alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
6717
- * Returns null in Node.js (no DOM).
6718
- *
6719
- * @param {Object} [options] - Style loading options
6720
- * @param {boolean} [options.minify=true] - Minify the CSS output
6721
- * @returns {Element|null} Style element if in browser, null in Node.js
6722
- * @category CSS & Styling
6723
- * @see bw.setTheme
6724
- * @see bw.applyTheme
6725
- * @see bw.toggleTheme
6726
- * @example
6727
- * bw.loadDefaultStyles(); // inject all default CSS
6728
- */
6729
- bw.loadDefaultStyles = function(options = {}) {
6730
- const { minify = true, palette } = options;
6731
-
6732
- // 1. Inject structural CSS (layout, sizing — never changes with theme)
6733
- if (bw._isBrowser) {
6734
- var structuralCSS = bw.css(getStructuralStyles());
6735
- bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
6736
- }
6737
7120
 
6738
- // 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
6739
- var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
6740
- var result = bw.generateTheme('', Object.assign({}, paletteConfig, { inject: true }));
6741
- return result;
6742
- };
7121
+ // =========================================================================
7122
+ // v2.0.18 Clean Styles API makeStyles / applyStyles / loadStyles / etc.
7123
+ // =========================================================================
6743
7124
 
7125
+ /**
7126
+ * Convert a scope selector to a <style> element id.
7127
+ * @private
7128
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
7129
+ * @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
7130
+ */
7131
+ function _scopeToStyleId(scope) {
7132
+ if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
7133
+ if (scope === 'reset') return 'bw_style_reset';
7134
+ // Strip leading # or . and convert - to _
7135
+ var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
7136
+ return 'bw_style_' + clean;
7137
+ }
6744
7138
 
6745
7139
  /**
6746
- * Generate a complete, scoped theme from seed colors.
7140
+ * Generate a complete styles object from seed colors and layout config.
7141
+ * Pure function — no DOM, no state, no side effects.
6747
7142
  *
6748
- * Produces CSS for all themed components (buttons, alerts, badges, cards,
6749
- * forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
6750
- * scoped under `.name` class. Multiple themes can coexist in the stylesheet.
6751
- * Swap themes by changing the class on a container element.
7143
+ * All parameters are optional. Defaults to the bitwrench default palette.
6752
7144
  *
6753
- * @param {string} name - CSS scope class (e.g. 'ocean'). Empty string = unscoped global.
6754
- * @param {Object} config - Theme configuration
6755
- * @param {string} config.primary - Primary brand color hex
6756
- * @param {string} config.secondary - Secondary color hex
6757
- * @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
6758
- * @param {string} [config.success='#198754'] - Success color hex
6759
- * @param {string} [config.danger='#dc3545'] - Danger color hex
6760
- * @param {string} [config.warning='#ffc107'] - Warning color hex
6761
- * @param {string} [config.info='#0dcaf0'] - Info color hex
6762
- * @param {string} [config.light='#f8f9fa'] - Light color hex
6763
- * @param {string} [config.dark='#212529'] - Dark color hex
6764
- * @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
6765
- * @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
7145
+ * @param {Object} [config] - Style configuration
7146
+ * @param {string} [config.primary='#006666'] - Primary brand color hex
7147
+ * @param {string} [config.secondary='#6c757d'] - Secondary color hex
7148
+ * @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
6766
7149
  * @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
6767
7150
  * @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
6768
- * @param {number} [config.fontSize=1.0] - Base font size scale factor
6769
- * @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
6770
- * @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
6771
- * @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
6772
- * @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
6773
- * @param {boolean} [config.inject=true] - Inject into DOM (browser only)
6774
- * @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
7151
+ * @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
6775
7152
  * @category CSS & Styling
6776
- * @see bw.applyTheme
6777
- * @see bw.toggleTheme
6778
- * @see bw.loadDefaultStyles
7153
+ * @see bw.applyStyles
7154
+ * @see bw.loadStyles
6779
7155
  * @example
6780
- * // Generate and inject an ocean theme (primary + alternate)
6781
- * var theme = bw.generateTheme('ocean', {
6782
- * primary: '#0077b6',
6783
- * secondary: '#90e0ef',
6784
- * tertiary: '#00b4d8'
6785
- * });
6786
- *
6787
- * // Apply to a container
6788
- * document.getElementById('app').classList.add('ocean');
6789
- *
6790
- * // Toggle to alternate palette
6791
- * bw.toggleTheme();
6792
- *
6793
- * // Generate CSS for static export (Node.js)
6794
- * var result = bw.generateTheme('sunset', {
6795
- * primary: '#e76f51',
6796
- * secondary: '#264653',
6797
- * inject: false
6798
- * });
6799
- * fs.writeFileSync('sunset.css', result.css + result.alternate.css);
7156
+ * var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
7157
+ * console.log(styles.palette.primary.base); // '#4f46e5'
7158
+ * // styles.css contains all themed CSS — nothing injected
6800
7159
  */
6801
- bw.generateTheme = function(name, config) {
6802
- if (!config || !config.primary || !config.secondary) {
6803
- throw new Error('bw.generateTheme requires config.primary and config.secondary');
6804
- }
6805
-
6806
- // Merge with defaults; if user didn't supply tertiary, default to their primary
6807
- var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
6808
- if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
7160
+ bw.makeStyles = function(config) {
7161
+ var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
7162
+ if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
6809
7163
 
6810
7164
  // Derive primary palette
6811
7165
  var palette = derivePalette(fullConfig);
@@ -6813,131 +7167,207 @@ bw.generateTheme = function(name, config) {
6813
7167
  // Resolve layout
6814
7168
  var layout = resolveLayout(fullConfig);
6815
7169
 
6816
- // Generate primary themed CSS rules
6817
- var themedRules = generateThemedCSS(name, palette, layout);
7170
+ // Generate primary themed CSS rules (unscoped)
7171
+ var themedRules = generateThemedCSS('', palette, layout);
6818
7172
  var cssStr = bw.css(themedRules);
6819
7173
 
6820
7174
  // Derive alternate palette (luminance-inverted)
6821
7175
  var altConfig = deriveAlternateConfig(fullConfig);
6822
7176
  var altPalette = derivePalette(altConfig);
6823
7177
 
6824
- // Generate alternate CSS scoped under .bw_theme_alt
6825
- var altRules = generateAlternateCSS(name, altPalette, layout);
6826
- var altCssStr = bw.css(altRules);
7178
+ // Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
7179
+ // applyStyles() wraps them appropriately based on scope
7180
+ var altRawRules = generateThemedCSS('', altPalette, layout);
7181
+
7182
+ // Add body-level surface overrides for the alternate palette.
7183
+ // When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
7184
+ altRawRules['body'] = {
7185
+ 'color': altPalette.dark.base,
7186
+ 'background-color': altPalette.surface || altPalette.light.base
7187
+ };
7188
+
7189
+ var altCssStr = bw.css(altRawRules);
6827
7190
 
6828
7191
  // Determine if primary is light-flavored
6829
7192
  var lightPrimary = isLightPalette(fullConfig);
6830
7193
 
6831
- // Inject both CSS sets into DOM if requested
6832
- var shouldInject = config.inject !== false;
6833
- if (shouldInject && bw._isBrowser) {
6834
- var safeName = name ? name.replace(/-/g, '_') : '';
6835
- var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
6836
- var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
6837
-
6838
- bw.injectCSS(cssStr, { id: styleId, append: false });
6839
- bw.injectCSS(altCssStr, { id: altStyleId, append: false });
7194
+ return {
7195
+ css: cssStr,
7196
+ alternateCss: altCssStr,
7197
+ rules: themedRules,
7198
+ alternateRules: altRawRules,
7199
+ palette: palette,
7200
+ alternatePalette: altPalette,
7201
+ isLightPrimary: lightPrimary
7202
+ };
7203
+ };
6840
7204
 
6841
- bw._activeThemeStyleIds = [styleId, altStyleId];
7205
+ /**
7206
+ * Inject styles into the DOM with optional scoping.
7207
+ *
7208
+ * Takes a styles object from `makeStyles()` and creates a single `<style>`
7209
+ * element in `<head>`. If a scope selector is provided, all CSS rules are
7210
+ * wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
7211
+ *
7212
+ * @param {Object} styles - Result of `bw.makeStyles()`
7213
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
7214
+ * @returns {Element|null} The `<style>` element, or null in Node.js
7215
+ * @category CSS & Styling
7216
+ * @see bw.makeStyles
7217
+ * @see bw.loadStyles
7218
+ * @see bw.clearStyles
7219
+ * @example
7220
+ * var styles = bw.makeStyles({ primary: '#4f46e5' });
7221
+ * bw.applyStyles(styles); // global
7222
+ * bw.applyStyles(styles, '#my-dashboard'); // scoped
7223
+ */
7224
+ bw.applyStyles = function(styles, scope) {
7225
+ if (!bw._isBrowser) return null;
7226
+ if (!styles || !styles.rules) {
7227
+ _cw('bw.applyStyles: invalid styles object');
7228
+ return null;
6842
7229
  }
6843
7230
 
6844
- // Update bw.u color entries to reflect the palette
6845
- if (!name) {
6846
- bw.u.bgTeal = { background: palette.primary.base, color: palette.primary.textOn };
6847
- bw.u.textTeal = { color: palette.primary.base };
6848
- bw.u.bgWhite = { background: '#ffffff' };
6849
- bw.u.textWhite = { color: '#ffffff' };
7231
+ var styleId = _scopeToStyleId(scope);
7232
+
7233
+ // Scope the primary rules if a scope is provided
7234
+ var primaryRules = styles.rules;
7235
+ if (scope) {
7236
+ primaryRules = scopeRulesUnder(primaryRules, scope);
6850
7237
  }
6851
7238
 
6852
- // Store active theme state
6853
- var result = {
6854
- css: cssStr,
6855
- palette: palette,
6856
- name: name,
6857
- isLightPrimary: lightPrimary,
6858
- alternate: {
6859
- css: altCssStr,
6860
- palette: altPalette
7239
+ // Wrap alternate rules with .bw_theme_alt
7240
+ var altRules = styles.alternateRules;
7241
+ if (altRules) {
7242
+ if (scope) {
7243
+ // Scoped compound: #scope.bw_theme_alt .bw_card
7244
+ altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
7245
+ } else {
7246
+ // Global: .bw_theme_alt .bw_card
7247
+ altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
6861
7248
  }
6862
- };
6863
- bw._activeTheme = result;
6864
- bw._activeThemeMode = 'primary';
7249
+ }
6865
7250
 
6866
- return result;
7251
+ // Combine primary + alternate into one CSS string
7252
+ var combined = bw.css(primaryRules);
7253
+ if (altRules) {
7254
+ combined += '\n' + bw.css(altRules);
7255
+ }
7256
+
7257
+ return bw.injectCSS(combined, { id: styleId, append: false });
6867
7258
  };
6868
7259
 
6869
7260
  /**
6870
- * Apply a theme mode. Switches between primary and alternate palettes
6871
- * by adding/removing the `bw_theme_alt` class on `<html>`.
7261
+ * Generate and apply styles in one call. Convenience wrapper.
7262
+ *
7263
+ * Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
6872
7264
  *
6873
- * @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
6874
- * @returns {string} Active mode: 'primary' or 'alternate'
7265
+ * @param {Object} [config] - Style configuration (same as `makeStyles`)
7266
+ * @param {string} [scope] - Scope selector (same as `applyStyles`)
7267
+ * @returns {Element|null} The `<style>` element, or null in Node.js
6875
7268
  * @category CSS & Styling
6876
- * @see bw.generateTheme
6877
- * @see bw.toggleTheme
7269
+ * @see bw.makeStyles
7270
+ * @see bw.applyStyles
6878
7271
  * @example
6879
- * bw.applyTheme('alternate'); // switch to alternate palette
6880
- * bw.applyTheme('dark'); // switch to whichever palette is darker
6881
- * bw.applyTheme('primary'); // switch back to primary palette
6882
- */
6883
- bw.applyTheme = function(mode) {
6884
- if (!bw._isBrowser) return mode || 'primary';
6885
- var root = document.documentElement;
6886
- var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
6887
-
6888
- var wantAlt;
6889
- if (mode === 'primary') wantAlt = false;
6890
- else if (mode === 'alternate') wantAlt = true;
6891
- else if (mode === 'light') wantAlt = !isLight;
6892
- else if (mode === 'dark') wantAlt = isLight;
6893
- else wantAlt = false;
6894
-
6895
- if (wantAlt) {
6896
- root.classList.add('bw_theme_alt');
6897
- } else {
6898
- root.classList.remove('bw_theme_alt');
7272
+ * bw.loadStyles(); // defaults, global
7273
+ * bw.loadStyles({ primary: '#4f46e5' }); // custom, global
7274
+ * bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
7275
+ */
7276
+ bw.loadStyles = function(config, scope) {
7277
+ // Also inject structural CSS first (only once)
7278
+ if (bw._isBrowser) {
7279
+ var existing = document.getElementById('bw_structural');
7280
+ if (!existing) {
7281
+ var structuralCSS = bw.css(getStructuralStyles());
7282
+ bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
7283
+ }
6899
7284
  }
7285
+ return bw.applyStyles(bw.makeStyles(config), scope);
7286
+ };
6900
7287
 
6901
- bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
6902
- return bw._activeThemeMode;
7288
+ /**
7289
+ * Inject the CSS reset (box-sizing, html/body font, reduced-motion).
7290
+ * Idempotent — if already injected, returns the existing `<style>` element.
7291
+ *
7292
+ * @returns {Element|null} The `<style>` element, or null in Node.js
7293
+ * @category CSS & Styling
7294
+ * @see bw.loadStyles
7295
+ * @see bw.clearStyles
7296
+ * @example
7297
+ * bw.loadReset(); // inject once, safe to call multiple times
7298
+ */
7299
+ bw.loadReset = function() {
7300
+ if (!bw._isBrowser) return null;
7301
+ var existing = document.getElementById('bw_style_reset');
7302
+ if (existing) return existing;
7303
+ return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
6903
7304
  };
6904
7305
 
6905
7306
  /**
6906
- * Toggle between primary and alternate theme palettes.
7307
+ * Toggle between primary and alternate palettes.
7308
+ *
7309
+ * Adds/removes the `bw_theme_alt` class on the scoping element.
7310
+ * Without a scope, toggles on `<html>` (global).
7311
+ * With a scope, toggles on the first matching element.
6907
7312
  *
7313
+ * @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
6908
7314
  * @returns {string} Active mode after toggle: 'primary' or 'alternate'
6909
7315
  * @category CSS & Styling
6910
- * @see bw.applyTheme
6911
- * @see bw.generateTheme
7316
+ * @see bw.applyStyles
7317
+ * @see bw.clearStyles
6912
7318
  * @example
6913
- * bw.toggleTheme(); // flip between primary and alternate
7319
+ * bw.toggleStyles(); // global toggle on <html>
7320
+ * bw.toggleStyles('#my-dashboard'); // scoped toggle
6914
7321
  */
6915
- bw.toggleTheme = function() {
6916
- var current = bw._activeThemeMode || 'primary';
6917
- return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
7322
+ bw.toggleStyles = function(scope) {
7323
+ if (!bw._isBrowser) return 'primary';
7324
+ var target;
7325
+ if (scope) {
7326
+ var els = bw.$(scope);
7327
+ target = els[0];
7328
+ } else {
7329
+ target = document.documentElement;
7330
+ }
7331
+ if (!target) return 'primary';
7332
+
7333
+ var hasAlt = target.classList.contains('bw_theme_alt');
7334
+ if (hasAlt) {
7335
+ target.classList.remove('bw_theme_alt');
7336
+ return 'primary';
7337
+ } else {
7338
+ target.classList.add('bw_theme_alt');
7339
+ return 'alternate';
7340
+ }
6918
7341
  };
6919
7342
 
6920
7343
  /**
6921
- * Remove the currently active theme's injected style elements from the DOM.
6922
- * Use this before generating a new theme with a different name to prevent
6923
- * stale CSS accumulation.
7344
+ * Remove injected styles for a given scope.
7345
+ *
7346
+ * Finds the `<style>` element by id and removes it. Also removes
7347
+ * the `bw_theme_alt` class from the relevant element.
6924
7348
  *
7349
+ * @param {string} [scope] - Scope selector. Omit to remove global styles.
6925
7350
  * @category CSS & Styling
6926
- * @see bw.generateTheme
7351
+ * @see bw.applyStyles
7352
+ * @see bw.loadStyles
6927
7353
  * @example
6928
- * bw.clearTheme(); // remove current theme styles
6929
- * bw.generateTheme('sunset', conf); // inject fresh theme
6930
- */
6931
- bw.clearTheme = function() {
6932
- if (bw._activeThemeStyleIds && bw._isBrowser) {
6933
- bw._activeThemeStyleIds.forEach(function(id) {
6934
- var el = document.getElementById(id);
6935
- if (el) el.remove();
6936
- });
6937
- bw._activeThemeStyleIds = null;
7354
+ * bw.clearStyles(); // remove global styles
7355
+ * bw.clearStyles('#my-dashboard'); // remove scoped styles
7356
+ * bw.clearStyles('reset'); // remove the CSS reset
7357
+ */
7358
+ bw.clearStyles = function(scope) {
7359
+ if (!bw._isBrowser) return;
7360
+ var styleId = _scopeToStyleId(scope);
7361
+ var el = document.getElementById(styleId);
7362
+ if (el) el.remove();
7363
+
7364
+ // Also remove bw_theme_alt from the relevant element
7365
+ if (scope && scope !== 'reset' && scope !== 'global') {
7366
+ var targets = bw.$(scope);
7367
+ if (targets[0]) targets[0].classList.remove('bw_theme_alt');
7368
+ } else if (!scope || scope === 'global') {
7369
+ document.documentElement.classList.remove('bw_theme_alt');
6938
7370
  }
6939
- bw._activeTheme = null;
6940
- bw._activeThemeMode = 'primary';
6941
7371
  };
6942
7372
 
6943
7373
  // Expose color utility functions on bw namespace
@@ -7160,10 +7590,15 @@ bw.copyToClipboard = function(text) {
7160
7590
  * @param {Object} config - Table configuration
7161
7591
  * @param {Array<Object>} config.data - Array of row objects to display
7162
7592
  * @param {Array<Object>} [config.columns] - Column definitions with key, label, render
7163
- * @param {string} [config.className='table'] - CSS class for table element
7593
+ * @param {string} [config.className=''] - Additional CSS classes for table element
7164
7594
  * @param {boolean} [config.sortable=true] - Enable click-to-sort headers
7165
7595
  * @param {Function} [config.onSort] - Sort callback (column, direction)
7166
- * @returns {Object} TACO object for table
7596
+ * @param {boolean} [config.selectable=false] - Enable row selection on click
7597
+ * @param {Function} [config.onRowClick] - Row click callback (row, index, event)
7598
+ * @param {number} [config.pageSize] - Rows per page (enables pagination when set)
7599
+ * @param {number} [config.currentPage=1] - Current page number (1-based)
7600
+ * @param {Function} [config.onPageChange] - Page change callback (newPage)
7601
+ * @returns {Object} TACO object for table (with optional pagination controls)
7167
7602
  * @category Component Builders
7168
7603
  * @see bw.makeDataTable
7169
7604
  * @example
@@ -7175,7 +7610,12 @@ bw.copyToClipboard = function(text) {
7175
7610
  * columns: [
7176
7611
  * { key: 'name', label: 'Name' },
7177
7612
  * { key: 'age', label: 'Age' }
7178
- * ]
7613
+ * ],
7614
+ * selectable: true,
7615
+ * onRowClick: function(row, i) { console.log('clicked', row.name); },
7616
+ * pageSize: 10,
7617
+ * currentPage: 1,
7618
+ * onPageChange: function(page) { console.log('page', page); }
7179
7619
  * });
7180
7620
  */
7181
7621
  bw.makeTable = function(config) {
@@ -7188,41 +7628,47 @@ bw.makeTable = function(config) {
7188
7628
  sortable = true,
7189
7629
  onSort,
7190
7630
  sortColumn,
7191
- sortDirection = 'asc'
7631
+ sortDirection = 'asc',
7632
+ selectable = false,
7633
+ onRowClick,
7634
+ pageSize,
7635
+ currentPage = 1,
7636
+ onPageChange
7192
7637
  } = config;
7193
7638
 
7194
- // Build class list: always include bw_table, add striped/hover, append user className
7639
+ // Build class list: always include bw_table, add striped/hover/selectable, append user className
7195
7640
  let cls = 'bw_table';
7196
7641
  if (striped) cls += ' bw_table_striped';
7197
- if (hover) cls += ' bw_table_hover';
7642
+ if (hover || selectable) cls += ' bw_table_hover';
7643
+ if (selectable) cls += ' bw_table_selectable';
7198
7644
  if (className) cls += ' ' + className;
7199
7645
  cls = cls.trim();
7200
-
7646
+
7201
7647
  // Auto-detect columns if not provided
7202
- const cols = columns || (data.length > 0
7203
- ? Object.keys(data[0]).map(key => ({ key, label: key }))
7648
+ const cols = columns || (data.length > 0
7649
+ ? _keys(data[0]).map(key => ({ key, label: key }))
7204
7650
  : []);
7205
-
7651
+
7206
7652
  // Current sort state
7207
7653
  let currentSortColumn = sortColumn || null;
7208
7654
  let currentSortDirection = sortDirection;
7209
-
7655
+
7210
7656
  // Sort data if column specified
7211
7657
  let sortedData = [...data];
7212
7658
  if (currentSortColumn) {
7213
7659
  sortedData.sort((a, b) => {
7214
7660
  const aVal = a[currentSortColumn];
7215
7661
  const bVal = b[currentSortColumn];
7216
-
7662
+
7217
7663
  // Handle different types
7218
- if (typeof aVal === 'number' && typeof bVal === 'number') {
7664
+ if (_is(aVal, 'number') && _is(bVal, 'number')) {
7219
7665
  return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
7220
7666
  }
7221
-
7667
+
7222
7668
  // String comparison
7223
7669
  const aStr = String(aVal || '').toLowerCase();
7224
7670
  const bStr = String(bVal || '').toLowerCase();
7225
-
7671
+
7226
7672
  if (currentSortDirection === 'asc') {
7227
7673
  return aStr.localeCompare(bStr);
7228
7674
  } else {
@@ -7230,23 +7676,32 @@ bw.makeTable = function(config) {
7230
7676
  }
7231
7677
  });
7232
7678
  }
7233
-
7679
+
7680
+ // Pagination
7681
+ const totalRows = sortedData.length;
7682
+ const totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
7683
+ const page = Math.max(1, Math.min(currentPage, totalPages));
7684
+ if (pageSize) {
7685
+ const start = (page - 1) * pageSize;
7686
+ sortedData = sortedData.slice(start, start + pageSize);
7687
+ }
7688
+
7234
7689
  // Create sort handler
7235
7690
  const handleSort = (column) => {
7236
7691
  if (!sortable) return;
7237
-
7692
+
7238
7693
  if (currentSortColumn === column) {
7239
7694
  currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
7240
7695
  } else {
7241
7696
  currentSortColumn = column;
7242
7697
  currentSortDirection = 'asc';
7243
7698
  }
7244
-
7699
+
7245
7700
  if (onSort) {
7246
7701
  onSort(column, currentSortDirection);
7247
7702
  }
7248
7703
  };
7249
-
7704
+
7250
7705
  // Build table header
7251
7706
  const thead = {
7252
7707
  t: 'thead',
@@ -7269,24 +7724,87 @@ bw.makeTable = function(config) {
7269
7724
  }))
7270
7725
  }
7271
7726
  };
7272
-
7273
- // Build table body
7727
+
7728
+ // Build table body with selectable/onRowClick support
7274
7729
  const tbody = {
7275
7730
  t: 'tbody',
7276
- c: sortedData.map(row => ({
7277
- t: 'tr',
7278
- c: cols.map(col => ({
7279
- t: 'td',
7280
- c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
7281
- }))
7282
- }))
7731
+ c: sortedData.map((row, idx) => {
7732
+ const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
7733
+ const rowAttrs = {};
7734
+ if (selectable || onRowClick) {
7735
+ rowAttrs.style = 'cursor:pointer;';
7736
+ rowAttrs.onclick = function(e) {
7737
+ if (selectable) {
7738
+ // Toggle selected class on this row
7739
+ var tr = e.currentTarget;
7740
+ tr.classList.toggle('bw_table_row_selected');
7741
+ }
7742
+ if (onRowClick) {
7743
+ onRowClick(row, globalIdx, e);
7744
+ }
7745
+ };
7746
+ }
7747
+ return {
7748
+ t: 'tr',
7749
+ a: rowAttrs,
7750
+ c: cols.map(col => ({
7751
+ t: 'td',
7752
+ c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
7753
+ }))
7754
+ };
7755
+ })
7283
7756
  };
7284
-
7285
- return {
7757
+
7758
+ const table = {
7286
7759
  t: 'table',
7287
7760
  a: { class: cls },
7288
7761
  c: [thead, tbody]
7289
7762
  };
7763
+
7764
+ // If no pagination, return table directly
7765
+ if (!pageSize) return table;
7766
+
7767
+ // Build pagination controls
7768
+ const pageButtons = [];
7769
+ // Previous button
7770
+ pageButtons.push({
7771
+ t: 'button',
7772
+ a: {
7773
+ class: 'bw_btn bw_btn_sm',
7774
+ disabled: page <= 1 ? 'disabled' : undefined,
7775
+ onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
7776
+ },
7777
+ c: 'Prev'
7778
+ });
7779
+ // Page info
7780
+ pageButtons.push({
7781
+ t: 'span',
7782
+ a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
7783
+ c: 'Page ' + page + ' of ' + totalPages
7784
+ });
7785
+ // Next button
7786
+ pageButtons.push({
7787
+ t: 'button',
7788
+ a: {
7789
+ class: 'bw_btn bw_btn_sm',
7790
+ disabled: page >= totalPages ? 'disabled' : undefined,
7791
+ onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
7792
+ },
7793
+ c: 'Next'
7794
+ });
7795
+
7796
+ return {
7797
+ t: 'div',
7798
+ a: { class: 'bw_table_paginated' },
7799
+ c: [
7800
+ table,
7801
+ {
7802
+ t: 'div',
7803
+ a: { class: 'bw_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
7804
+ c: pageButtons
7805
+ }
7806
+ ]
7807
+ };
7290
7808
  };
7291
7809
 
7292
7810
  /**
@@ -7325,7 +7843,7 @@ bw.makeTable = function(config) {
7325
7843
  bw.makeTableFromArray = function(config) {
7326
7844
  const { data = [], headerRow = true, columns, ...rest } = config;
7327
7845
 
7328
- if (!Array.isArray(data) || data.length === 0) {
7846
+ if (!_isA(data) || data.length === 0) {
7329
7847
  return bw.makeTable({ data: [], columns: columns || [], ...rest });
7330
7848
  }
7331
7849
 
@@ -7407,7 +7925,7 @@ bw.makeBarChart = function(config) {
7407
7925
  className = ''
7408
7926
  } = config;
7409
7927
 
7410
- if (!Array.isArray(data) || data.length === 0) {
7928
+ if (!_isA(data) || data.length === 0) {
7411
7929
  return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
7412
7930
  }
7413
7931
 
@@ -7556,7 +8074,7 @@ bw._componentRegistry = new Map();
7556
8074
  */
7557
8075
  bw.render = function(element, position, taco) {
7558
8076
  // Get target element
7559
- const targetEl = typeof element === 'string'
8077
+ const targetEl = _is(element, 'string')
7560
8078
  ? document.querySelector(element)
7561
8079
  : element;
7562
8080
 
@@ -7706,7 +8224,7 @@ bw.render = function(element, position, taco) {
7706
8224
  setContent(content) {
7707
8225
  this._taco.c = content;
7708
8226
  if (this.element) {
7709
- if (typeof content === 'string') {
8227
+ if (_is(content, 'string')) {
7710
8228
  this.element.textContent = content;
7711
8229
  } else {
7712
8230
  // Re-render for complex content