bitwrench 2.0.20 → 2.0.22

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 (53) hide show
  1. package/README.md +0 -4
  2. package/dist/bitwrench-bccl.cjs.js +1 -1
  3. package/dist/bitwrench-bccl.cjs.min.js +1 -1
  4. package/dist/bitwrench-bccl.esm.js +1 -1
  5. package/dist/bitwrench-bccl.esm.min.js +1 -1
  6. package/dist/bitwrench-bccl.umd.js +1 -1
  7. package/dist/bitwrench-bccl.umd.min.js +1 -1
  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-debug.js +1 -1
  17. package/dist/bitwrench-debug.min.js +1 -1
  18. package/dist/bitwrench-lean.cjs.js +344 -30
  19. package/dist/bitwrench-lean.cjs.min.js +14 -6
  20. package/dist/bitwrench-lean.es5.js +379 -29
  21. package/dist/bitwrench-lean.es5.min.js +12 -4
  22. package/dist/bitwrench-lean.esm.js +344 -30
  23. package/dist/bitwrench-lean.esm.min.js +14 -6
  24. package/dist/bitwrench-lean.umd.js +344 -30
  25. package/dist/bitwrench-lean.umd.min.js +14 -6
  26. package/dist/bitwrench-util-css.cjs.js +1 -1
  27. package/dist/bitwrench-util-css.cjs.min.js +1 -1
  28. package/dist/bitwrench-util-css.es5.js +1 -1
  29. package/dist/bitwrench-util-css.es5.min.js +1 -1
  30. package/dist/bitwrench-util-css.esm.js +1 -1
  31. package/dist/bitwrench-util-css.esm.min.js +1 -1
  32. package/dist/bitwrench-util-css.umd.js +1 -1
  33. package/dist/bitwrench-util-css.umd.min.js +1 -1
  34. package/dist/bitwrench.cjs.js +344 -30
  35. package/dist/bitwrench.cjs.min.js +14 -6
  36. package/dist/bitwrench.css +65 -14
  37. package/dist/bitwrench.es5.js +379 -29
  38. package/dist/bitwrench.es5.min.js +13 -5
  39. package/dist/bitwrench.esm.js +344 -30
  40. package/dist/bitwrench.esm.min.js +15 -7
  41. package/dist/bitwrench.min.css +1 -1
  42. package/dist/bitwrench.umd.js +344 -30
  43. package/dist/bitwrench.umd.min.js +14 -6
  44. package/dist/builds.json +87 -87
  45. package/dist/bwserve.cjs.js +2 -2
  46. package/dist/bwserve.esm.js +2 -2
  47. package/dist/sri.json +46 -46
  48. package/package.json +5 -6
  49. package/readme.html +3 -3
  50. package/src/bitwrench-router.js +282 -0
  51. package/src/bitwrench-styles.js +59 -27
  52. package/src/bitwrench.js +6 -0
  53. package/src/version.js +3 -3
@@ -1,4 +1,4 @@
1
- /*! bitwrench v2.0.20 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench v2.0.22 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
@@ -12,14 +12,14 @@
12
12
  */
13
13
 
14
14
  const VERSION_INFO = {
15
- version: '2.0.20',
15
+ version: '2.0.22',
16
16
  name: 'bitwrench',
17
17
  description: 'A library for javascript UI functions.',
18
18
  license: 'BSD-2-Clause',
19
19
  homepage: 'https://deftio.github.com/bitwrench/pages',
20
20
  repository: 'git+https://github.com/deftio/bitwrench.git',
21
21
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
22
- buildDate: '2026-03-23T05:19:31.951Z'
22
+ buildDate: '2026-03-24T05:38:12.852Z'
23
23
  };
24
24
 
25
25
  /**
@@ -585,10 +585,10 @@
585
585
  xl: '0 4px 12px rgba(0,0,0,0.12)'
586
586
  },
587
587
  md: {
588
- sm: '0 1px 3px rgba(0,0,0,0.08)',
589
- md: '0 2px 6px rgba(0,0,0,0.12)',
590
- lg: '0 4px 12px rgba(0,0,0,0.16)',
591
- xl: '0 8px 24px rgba(0,0,0,0.20)'
588
+ sm: '0 1px 3px rgba(0,0,0,0.10), 0 1px 2px rgba(0,0,0,0.06)',
589
+ md: '0 4px 6px rgba(0,0,0,0.10), 0 2px 4px rgba(0,0,0,0.06)',
590
+ lg: '0 10px 15px rgba(0,0,0,0.12), 0 4px 6px rgba(0,0,0,0.08)',
591
+ xl: '0 20px 25px rgba(0,0,0,0.15), 0 8px 10px rgba(0,0,0,0.10)'
592
592
  },
593
593
  lg: {
594
594
  sm: '0 2px 4px rgba(0,0,0,0.10)',
@@ -782,6 +782,9 @@
782
782
  rules[_sx(scope, '.bw_card:hover')] = {
783
783
  'box-shadow': elev.md
784
784
  };
785
+ rules[_sx(scope, '.bw_card_hoverable')] = {
786
+ 'transition': 'box-shadow ' + motion.slow + ' ' + motion.easing + ', transform ' + motion.slow + ' ' + motion.easing
787
+ };
785
788
  rules[_sx(scope, '.bw_card_hoverable:hover')] = {
786
789
  'box-shadow': elev.lg
787
790
  };
@@ -882,7 +885,8 @@
882
885
  };
883
886
  rules[_sx(scope, '.bw_navbar_nav .bw_nav_link')] = {
884
887
  'color': palette.secondary.base,
885
- 'border-radius': layout.radius.btn
888
+ 'border-radius': layout.radius.btn,
889
+ 'transition': 'color ' + layout.motion.fast + ' ' + layout.motion.easing + ', background-color ' + layout.motion.fast + ' ' + layout.motion.easing
886
890
  };
887
891
  rules[_sx(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
888
892
  'color': palette.dark.base,
@@ -1045,15 +1049,18 @@
1045
1049
  return rules;
1046
1050
  }
1047
1051
 
1048
- function generateProgress(scope, palette) {
1052
+ function generateProgress(scope, palette, layout) {
1049
1053
  var rules = {};
1054
+ var rd = layout ? layout.radius : { badge: '.375rem' };
1050
1055
  rules[_sx(scope, '.bw_progress')] = {
1051
1056
  'background-color': palette.surfaceAlt,
1057
+ 'border-radius': rd.badge,
1052
1058
  'box-shadow': 'inset 0 1px 2px rgba(0,0,0,.1)'
1053
1059
  };
1054
1060
  rules[_sx(scope, '.bw_progress_bar')] = {
1055
1061
  'color': palette.primary.textOn,
1056
1062
  'background-color': palette.primary.base,
1063
+ 'border-radius': 'inherit',
1057
1064
  'box-shadow': 'inset 0 -1px 0 rgba(0,0,0,.15)'
1058
1065
  };
1059
1066
  // Variant progress bar colors handled by palette class
@@ -1177,7 +1184,8 @@
1177
1184
  };
1178
1185
  rules[_sx(scope, '.bw_carousel_control')] = {
1179
1186
  'background-color': palette.dark.base,
1180
- 'color': palette.dark.textOn
1187
+ 'color': palette.dark.textOn,
1188
+ 'transition': 'background-color 0.15s ease-out'
1181
1189
  };
1182
1190
  rules[_sx(scope, '.bw_carousel_control:hover')] = {
1183
1191
  'background-color': palette.dark.hover
@@ -1191,9 +1199,11 @@
1191
1199
 
1192
1200
  function generateModalThemed(scope, palette, layout) {
1193
1201
  var rules = {};
1202
+ var rd = layout ? layout.radius : { card: '8px' };
1194
1203
  rules[_sx(scope, '.bw_modal_content')] = {
1195
1204
  'background-color': palette.surface || '#fff',
1196
1205
  'border-color': palette.light.border,
1206
+ 'border-radius': rd.card,
1197
1207
  'box-shadow': layout.elevation.lg
1198
1208
  };
1199
1209
  rules[_sx(scope, '.bw_modal_header')] = {
@@ -1210,9 +1220,11 @@
1210
1220
 
1211
1221
  function generateToastThemed(scope, palette, layout) {
1212
1222
  var rules = {};
1223
+ var rd = layout ? layout.radius : { card: '8px' };
1213
1224
  rules[_sx(scope, '.bw_toast')] = {
1214
1225
  'background-color': palette.surface || '#fff',
1215
1226
  'border-color': palette.light.border,
1227
+ 'border-radius': rd.card,
1216
1228
  'box-shadow': layout.elevation.lg
1217
1229
  };
1218
1230
  rules[_sx(scope, '.bw_toast_header')] = {
@@ -1224,9 +1236,11 @@
1224
1236
 
1225
1237
  function generateDropdownThemed(scope, palette, layout) {
1226
1238
  var rules = {};
1239
+ var rd = layout ? layout.radius : { card: '8px' };
1227
1240
  rules[_sx(scope, '.bw_dropdown_menu')] = {
1228
1241
  'background-color': palette.surface || '#fff',
1229
1242
  'border-color': palette.light.border,
1243
+ 'border-radius': rd.card,
1230
1244
  'box-shadow': layout.elevation.md
1231
1245
  };
1232
1246
  rules[_sx(scope, '.bw_dropdown_item')] = {
@@ -1259,6 +1273,10 @@
1259
1273
  rules[_sx(scope, '.bw_form_switch .bw_switch_input:focus')] = {
1260
1274
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
1261
1275
  };
1276
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:focus-visible')] = {
1277
+ 'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus,
1278
+ 'outline': 'none'
1279
+ };
1262
1280
  return rules;
1263
1281
  }
1264
1282
 
@@ -1322,12 +1340,14 @@
1322
1340
  return rules;
1323
1341
  }
1324
1342
 
1325
- function generateChipInputThemed(scope, palette) {
1343
+ function generateChipInputThemed(scope, palette, layout) {
1326
1344
  var rules = {};
1345
+ var rd = layout ? layout.radius : { input: '6px' };
1327
1346
  rules[_sx(scope, '.bw_chip_input')] = {
1328
1347
  'border-color': palette.light.border,
1329
1348
  'background-color': palette.surface || '#fff',
1330
- 'color': palette.dark.base
1349
+ 'color': palette.dark.base,
1350
+ 'border-radius': rd.input
1331
1351
  };
1332
1352
  rules[_sx(scope, '.bw_chip_input:focus-within')] = {
1333
1353
  'border-color': palette.primary.base,
@@ -1612,7 +1632,7 @@
1612
1632
  generateTabs(scopeName, palette, layout),
1613
1633
  generateListGroups(scopeName, palette, layout),
1614
1634
  generatePagination(scopeName, palette, layout),
1615
- generateProgress(scopeName, palette),
1635
+ generateProgress(scopeName, palette, layout),
1616
1636
  generateBreadcrumbThemed(scopeName, palette, layout),
1617
1637
  generateCloseButtonThemed(scopeName, palette),
1618
1638
  generateSectionsThemed(scopeName, palette),
@@ -1626,7 +1646,7 @@
1626
1646
  generateStatCardThemed(scopeName, palette, layout),
1627
1647
  generateTimelineThemed(scopeName, palette),
1628
1648
  generateStepperThemed(scopeName, palette),
1629
- generateChipInputThemed(scopeName, palette),
1649
+ generateChipInputThemed(scopeName, palette, layout),
1630
1650
  generateFileUploadThemed(scopeName, palette, layout),
1631
1651
  generateRangeThemed(scopeName, palette),
1632
1652
  generateSearchThemed(scopeName, palette, layout),
@@ -1774,7 +1794,7 @@
1774
1794
  '.bw_card_text': { 'margin-bottom': '0', 'font-size': '0.9375rem', 'line-height': '1.6' },
1775
1795
  '.bw_card_header': { 'margin-bottom': '0', 'font-weight': '600', 'font-size': '0.875rem' },
1776
1796
  '.bw_card_footer': { 'font-size': '0.875rem' },
1777
- '.bw_card_hoverable': { 'transition': 'all 0.3s ease-out' },
1797
+ '.bw_card_hoverable': {},
1778
1798
  '.bw_card_hoverable:hover': { 'transform': 'translateY(-4px)' },
1779
1799
  '.bw_card_img_top': { 'width': '100%' },
1780
1800
  '.bw_card_img_bottom': { 'width': '100%' },
@@ -1789,7 +1809,8 @@
1789
1809
  'display': 'block', 'width': '100%',
1790
1810
  'font-size': '0.9375rem', 'font-weight': '400', 'line-height': '1.5',
1791
1811
  'background-clip': 'padding-box', 'appearance': 'none',
1792
- 'border': '1px solid transparent', 'font-family': 'inherit'
1812
+ 'border': '1px solid transparent', 'font-family': 'inherit',
1813
+ 'transition': 'border-color 0.15s ease-out, box-shadow 0.15s ease-out'
1793
1814
  },
1794
1815
  '.bw_form_control:focus': { 'outline': '2px solid currentColor', 'outline-offset': '-1px' },
1795
1816
  '.bw_form_control::placeholder': { 'opacity': '1' },
@@ -1916,6 +1937,7 @@
1916
1937
  'text-decoration': 'none', 'cursor': 'pointer',
1917
1938
  'border': 'none', 'background': 'transparent', 'font-family': 'inherit'
1918
1939
  },
1940
+ '.bw_nav_link:focus-visible': { 'outline': '2px solid currentColor', 'outline-offset': '-2px' },
1919
1941
  '.bw_nav_tabs .bw_nav_link': { 'border': 'none', 'border-bottom': '2px solid transparent', 'border-radius': '0', 'background-color': 'transparent' },
1920
1942
  '.bw_nav_vertical': { 'flex-direction': 'column' },
1921
1943
  '.bw_tab_content': { 'padding': '1.25rem 0' },
@@ -2033,9 +2055,11 @@
2033
2055
  'display': 'inline-flex', 'align-items': 'center', 'justify-content': 'center',
2034
2056
  'width': '1.5rem', 'height': '1.5rem', 'padding': '0',
2035
2057
  'font-size': '1.25rem', 'font-weight': '700', 'line-height': '1',
2036
- 'background': 'transparent', 'border': '0', 'border-radius': '0.25rem', 'cursor': 'pointer'
2058
+ 'background': 'transparent', 'border': '0', 'border-radius': '0.25rem', 'cursor': 'pointer',
2059
+ 'transition': 'opacity 0.15s ease-out'
2037
2060
  },
2038
- '.bw_close:hover': { 'opacity': '0.75' }
2061
+ '.bw_close:hover': { 'opacity': '0.75' },
2062
+ '.bw_close:focus-visible': { 'outline': '2px solid currentColor', 'outline-offset': '2px' }
2039
2063
  },
2040
2064
 
2041
2065
  // ---- Stacks ----
@@ -2117,7 +2141,8 @@
2117
2141
  'flex-shrink': '0', 'width': '1.25rem', 'height': '1.25rem', 'margin-left': 'auto',
2118
2142
  'content': '""',
2119
2143
  'background-image': "url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\")",
2120
- 'background-repeat': 'no-repeat', 'background-size': '1.25rem'
2144
+ 'background-repeat': 'no-repeat', 'background-size': '1.25rem',
2145
+ 'transition': 'transform 0.2s ease-out'
2121
2146
  },
2122
2147
  '.bw_accordion_button:not(.bw_collapsed)::after': { 'transform': 'rotate(-180deg)' },
2123
2148
  '.bw_accordion_body': { 'padding': '1rem 1.25rem' },
@@ -2142,6 +2167,7 @@
2142
2167
  'z-index': '2', 'padding': '0'
2143
2168
  },
2144
2169
  '.bw_carousel_control img': { 'width': '20px', 'height': '20px', 'pointer-events': 'none' },
2170
+ '.bw_carousel_control:focus-visible': { 'outline': '2px solid currentColor', 'outline-offset': '2px' },
2145
2171
  '.bw_carousel_control_prev': { 'left': '10px' },
2146
2172
  '.bw_carousel_control_next': { 'right': '10px' },
2147
2173
  '.bw_carousel_indicators': {
@@ -2161,12 +2187,14 @@
2161
2187
  'display': 'flex', 'align-items': 'center', 'justify-content': 'center',
2162
2188
  'position': 'fixed', 'top': '0', 'left': '0', 'width': '100%', 'height': '100%',
2163
2189
  'z-index': '1050', 'overflow-x': 'hidden', 'overflow-y': 'auto',
2164
- 'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none'
2190
+ 'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none',
2191
+ 'transition': 'opacity 0.2s ease-out, visibility 0.2s ease-out'
2165
2192
  },
2166
2193
  '.bw_modal.bw_modal_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
2167
2194
  '.bw_modal_dialog': {
2168
2195
  'position': 'relative', 'width': 'calc(100% - 1rem)', 'max-width': '500px', 'margin': '1.75rem auto',
2169
- 'pointer-events': 'none'
2196
+ 'pointer-events': 'none', 'transform': 'translateY(-16px)',
2197
+ 'transition': 'transform 0.2s ease-out'
2170
2198
  },
2171
2199
  '.bw_modal.bw_modal_show .bw_modal_dialog': { 'transform': 'translateY(0)' },
2172
2200
  '.bw_modal_sm': { 'max-width': '300px' },
@@ -2196,10 +2224,11 @@
2196
2224
  '.bw_toast_container.bw_toast_bottom_center': { 'bottom': '0', 'left': '50%', 'transform': 'translateX(-50%)' },
2197
2225
  '.bw_toast': {
2198
2226
  'pointer-events': 'auto', 'width': '350px', 'max-width': 'calc(100vw - 2rem)', 'background-clip': 'padding-box',
2199
- 'opacity': '0'
2227
+ 'opacity': '0', 'transform': 'translateY(-8px)',
2228
+ 'transition': 'opacity 0.2s ease-out, transform 0.2s ease-out'
2200
2229
  },
2201
2230
  '.bw_toast.bw_toast_show': { 'opacity': '1', 'transform': 'translateY(0)' },
2202
- '.bw_toast.bw_toast_hiding': { 'opacity': '0' },
2231
+ '.bw_toast.bw_toast_hiding': { 'opacity': '0', 'transform': 'translateY(-8px)' },
2203
2232
  '.bw_toast_header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'padding': '0.5rem 0.75rem', 'font-size': '0.875rem', 'border-bottom': '1px solid transparent' },
2204
2233
  '.bw_toast_body': { 'padding': '0.5rem 0.75rem', 'font-size': '0.9375rem' }
2205
2234
  },
@@ -2216,9 +2245,11 @@
2216
2245
  'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'block',
2217
2246
  'min-width': '10rem', 'padding': '0.5rem 0', 'margin': '0.125rem 0 0',
2218
2247
  'background-clip': 'padding-box', 'border': '1px solid transparent',
2219
- 'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none'
2248
+ 'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none',
2249
+ 'transform': 'translateY(-4px)',
2250
+ 'transition': 'opacity 0.15s ease-out, transform 0.15s ease-out, visibility 0.15s ease-out'
2220
2251
  },
2221
- '.bw_dropdown_menu.bw_dropdown_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
2252
+ '.bw_dropdown_menu.bw_dropdown_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto', 'transform': 'translateY(0)' },
2222
2253
  '.bw_dropdown_menu_end': { 'left': 'auto', 'right': '0' },
2223
2254
  '.bw_dropdown_item': {
2224
2255
  'display': 'block', 'width': '100%', 'padding': '0.4rem 1rem', 'clear': 'both',
@@ -2237,7 +2268,8 @@
2237
2268
  'appearance': 'none',
2238
2269
  'background-image': "url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba(255,255,255,1)'/%3e%3c/svg%3e\")",
2239
2270
  'background-position': 'left center', 'background-repeat': 'no-repeat',
2240
- 'background-size': 'contain', 'cursor': 'pointer'
2271
+ 'background-size': 'contain', 'cursor': 'pointer',
2272
+ 'transition': 'background-color 0.15s ease-out, background-position 0.15s ease-out, border-color 0.15s ease-out'
2241
2273
  },
2242
2274
  '.bw_form_switch .bw_switch_input:checked': { 'background-position': 'right center' },
2243
2275
  '.bw_form_switch .bw_switch_input:disabled': { 'opacity': '0.5', 'cursor': 'not-allowed' }
@@ -2271,9 +2303,7 @@
2271
2303
  '.bw_stat_card': {
2272
2304
  'padding': '1.25rem',
2273
2305
  'border-left': '4px solid transparent',
2274
- 'border-radius': '0.375rem',
2275
- 'background-color': 'inherit',
2276
- 'transition': 'transform 0.15s ease'
2306
+ 'background-color': 'inherit'
2277
2307
  },
2278
2308
  '.bw_stat_card:hover': { 'transform': 'translateY(-1px)' },
2279
2309
  '.bw_stat_icon': { 'font-size': '1.5rem', 'margin-bottom': '0.5rem' },
@@ -2333,7 +2363,8 @@
2333
2363
  'width': '1.5rem', 'height': '1.5rem',
2334
2364
  'border': 'none', 'background': 'none',
2335
2365
  'font-size': '1.25rem', 'cursor': 'pointer', 'padding': '0', 'border-radius': '50%'
2336
- }
2366
+ },
2367
+ '.bw_search_clear:focus-visible': { 'outline': '2px solid currentColor', 'outline-offset': '2px' }
2337
2368
  },
2338
2369
 
2339
2370
  // ---- Range ----
@@ -2425,6 +2456,7 @@
2425
2456
  'width': '1rem', 'height': '1rem', 'border': 'none', 'background': 'none',
2426
2457
  'font-size': '0.875rem', 'cursor': 'pointer', 'padding': '0', 'border-radius': '50%'
2427
2458
  },
2459
+ '.bw_chip_remove:focus-visible': { 'outline': '2px solid currentColor', 'outline-offset': '1px' },
2428
2460
  '.bw_chip_field': { 'flex': '1', 'min-width': '80px', 'border': 'none', 'outline': 'none', 'font-size': '0.875rem', 'padding': '0.125rem 0', 'background': 'transparent' }
2429
2461
  },
2430
2462
 
@@ -3464,6 +3496,287 @@
3464
3496
  return intervalID;
3465
3497
  }
3466
3498
 
3499
+ /**
3500
+ * Bitwrench Router -- client-side URL routing for SPAs
3501
+ *
3502
+ * Single export: initRouter(bw) attaches bw.router(), bw.navigate(), bw.link()
3503
+ *
3504
+ * @license BSD-2-Clause
3505
+ */
3506
+
3507
+ // -- internal helpers --
3508
+
3509
+ function normalizePath(p) {
3510
+ // strip query string (handled separately)
3511
+ var qi = p.indexOf('?');
3512
+ if (qi >= 0) p = p.substring(0, qi);
3513
+ // collapse double slashes, strip trailing slash
3514
+ p = p.replace(/\/\/+/g, '/');
3515
+ if (p.length > 1 && p.charAt(p.length - 1) === '/') p = p.substring(0, p.length - 1);
3516
+ return p || '/';
3517
+ }
3518
+
3519
+ function parseQuery(fullPath) {
3520
+ var qi = fullPath.indexOf('?');
3521
+ if (qi < 0) return {};
3522
+ var qs = fullPath.substring(qi + 1);
3523
+ var result = {};
3524
+ var pairs = qs.split('&');
3525
+ for (var i = 0; i < pairs.length; i++) {
3526
+ var kv = pairs[i].split('=');
3527
+ if (kv[0]) result[decodeURIComponent(kv[0])] = kv.length > 1 ? decodeURIComponent(kv[1]) : '';
3528
+ }
3529
+ return result;
3530
+ }
3531
+
3532
+ function matchRoute(routes, rawPath) {
3533
+ var query = parseQuery(rawPath);
3534
+ var path = normalizePath(rawPath);
3535
+ var segs = path === '/' ? [''] : path.split('/');
3536
+
3537
+ var globalWild = null;
3538
+
3539
+ for (var i = 0; i < routes.length; i++) {
3540
+ var r = routes[i];
3541
+ var pattern = r.pattern;
3542
+
3543
+ // global wildcard -- save for last
3544
+ if (pattern === '*') { globalWild = r; continue; }
3545
+
3546
+ // catch-all: ends with /*
3547
+ if (pattern.length > 1 && pattern.substring(pattern.length - 2) === '/*') {
3548
+ var prefix = pattern.substring(0, pattern.length - 2);
3549
+ var prefixSegs = prefix === '' ? [''] : prefix.split('/');
3550
+ if (segs.length < prefixSegs.length) continue;
3551
+ var params = {};
3552
+ var ok = true;
3553
+ for (var j = 0; j < prefixSegs.length; j++) {
3554
+ if (prefixSegs[j].charAt(0) === ':') {
3555
+ params[prefixSegs[j].substring(1)] = segs[j];
3556
+ } else if (prefixSegs[j] !== segs[j]) {
3557
+ ok = false; break;
3558
+ }
3559
+ }
3560
+ if (ok) {
3561
+ params._rest = segs.slice(prefixSegs.length).join('/');
3562
+ params._query = query;
3563
+ return { handler: r.handler, params: params };
3564
+ }
3565
+ continue;
3566
+ }
3567
+
3568
+ // exact / parameterized match
3569
+ var rSegs = pattern === '/' ? [''] : pattern.split('/');
3570
+ if (rSegs.length !== segs.length) continue;
3571
+ var params2 = {};
3572
+ var match = true;
3573
+ for (var k = 0; k < rSegs.length; k++) {
3574
+ if (rSegs[k].charAt(0) === ':') {
3575
+ params2[rSegs[k].substring(1)] = segs[k];
3576
+ } else if (rSegs[k] !== segs[k]) {
3577
+ match = false; break;
3578
+ }
3579
+ }
3580
+ if (match) {
3581
+ params2._query = query;
3582
+ return { handler: r.handler, params: params2 };
3583
+ }
3584
+ }
3585
+
3586
+ // global wildcard fallback
3587
+ if (globalWild) {
3588
+ return { handler: globalWild.handler, params: { _query: query } };
3589
+ }
3590
+ return null;
3591
+ }
3592
+
3593
+
3594
+ // -- public API factory --
3595
+
3596
+ function initRouter(bw) {
3597
+ var _activeRouter = null;
3598
+
3599
+ bw.router = function(config) {
3600
+ if (!config || !config.routes) throw new Error('bw.router: config.routes is required');
3601
+ if (!bw._isBrowser) throw new Error('bw.router: requires a browser environment');
3602
+
3603
+ var mode = config.mode || 'hash';
3604
+ var base = config.base || '/';
3605
+ if (base.length > 1 && base.charAt(base.length - 1) === '/') base = base.substring(0, base.length - 1);
3606
+ var target = config.target || null;
3607
+
3608
+ // compile routes (preserve registration order)
3609
+ var routes = [];
3610
+ var keys = Object.keys(config.routes);
3611
+ for (var i = 0; i < keys.length; i++) {
3612
+ routes.push({ pattern: keys[i], handler: config.routes[keys[i]] });
3613
+ }
3614
+
3615
+ var currentPath = '/';
3616
+ var destroyed = false;
3617
+
3618
+ function getPath() {
3619
+ if (mode === 'hash') {
3620
+ var h = window.location.hash.replace(/^#/, '');
3621
+ return h || '/';
3622
+ }
3623
+ var p = window.location.pathname;
3624
+ if (base !== '/' && p.indexOf(base) === 0) {
3625
+ p = p.substring(base.length) || '/';
3626
+ }
3627
+ var s = window.location.search || '';
3628
+ return p + s;
3629
+ }
3630
+
3631
+ function handleRoute(toRaw, opts) {
3632
+ if (destroyed) return;
3633
+ var fromPath = currentPath;
3634
+ var toPath = normalizePath(toRaw);
3635
+
3636
+ // before guard
3637
+ if (config.before) {
3638
+ var result = config.before(toPath, fromPath);
3639
+ if (result === false) return;
3640
+ if (typeof result === 'string') {
3641
+ toPath = normalizePath(result);
3642
+ toRaw = result;
3643
+ }
3644
+ }
3645
+
3646
+ currentPath = toPath;
3647
+
3648
+ // match route
3649
+ var m = matchRoute(routes, toRaw);
3650
+ if (m) {
3651
+ var rendered = m.handler(m.params);
3652
+ if (rendered != null && target) {
3653
+ bw.DOM(target, rendered);
3654
+ }
3655
+ }
3656
+
3657
+ // pub/sub
3658
+ var query = parseQuery(toRaw);
3659
+ bw.pub('bw:route', {
3660
+ path: toPath,
3661
+ params: m ? m.params : {},
3662
+ query: query,
3663
+ from: fromPath
3664
+ });
3665
+
3666
+ // after hook
3667
+ if (config.after) config.after(toPath, fromPath);
3668
+ }
3669
+
3670
+ function navigate(path, opts) {
3671
+ if (destroyed) return;
3672
+ opts = opts || {};
3673
+ if (mode === 'hash') {
3674
+ if (opts.replace) {
3675
+ var loc = window.location;
3676
+ loc.replace(loc.pathname + loc.search + '#' + path);
3677
+ } else {
3678
+ window.location.hash = path;
3679
+ }
3680
+ // hashchange listener will fire handleRoute; but if same hash, trigger manually
3681
+ var currentHash = window.location.hash.replace(/^#/, '') || '/';
3682
+ if (normalizePath(currentHash) === normalizePath(path)) {
3683
+ handleRoute(path);
3684
+ }
3685
+ } else {
3686
+ var url = (base === '/' ? '' : base) + path;
3687
+ if (opts.replace) {
3688
+ window.history.replaceState(null, '', url);
3689
+ } else {
3690
+ window.history.pushState(null, '', url);
3691
+ }
3692
+ handleRoute(path);
3693
+ }
3694
+ }
3695
+
3696
+ function onHashChange() {
3697
+ if (destroyed) return;
3698
+ handleRoute(getPath());
3699
+ }
3700
+
3701
+ function onPopState() {
3702
+ if (destroyed) return;
3703
+ handleRoute(getPath());
3704
+ }
3705
+
3706
+ // listen
3707
+ if (mode === 'hash') {
3708
+ window.addEventListener('hashchange', onHashChange);
3709
+ } else {
3710
+ window.addEventListener('popstate', onPopState);
3711
+ }
3712
+
3713
+ // initial render
3714
+ handleRoute(getPath());
3715
+
3716
+ var routerObj = {
3717
+ navigate: navigate,
3718
+ current: function() {
3719
+ var raw = getPath();
3720
+ var m = matchRoute(routes, raw);
3721
+ return {
3722
+ path: currentPath,
3723
+ params: m ? m.params : {},
3724
+ query: parseQuery(raw)
3725
+ };
3726
+ },
3727
+ destroy: function() {
3728
+ destroyed = true;
3729
+ if (mode === 'hash') {
3730
+ window.removeEventListener('hashchange', onHashChange);
3731
+ } else {
3732
+ window.removeEventListener('popstate', onPopState);
3733
+ }
3734
+ if (_activeRouter === routerObj) _activeRouter = null;
3735
+ }
3736
+ };
3737
+
3738
+ _activeRouter = routerObj;
3739
+ return routerObj;
3740
+ };
3741
+
3742
+ bw.navigate = function(path, opts) {
3743
+ if (_activeRouter) {
3744
+ _activeRouter.navigate(path, opts);
3745
+ } else {
3746
+ if (typeof console !== 'undefined' && console.warn) {
3747
+ console.warn('bw.navigate: no active router');
3748
+ }
3749
+ }
3750
+ };
3751
+
3752
+ bw.link = function(path, content, attrs) {
3753
+ var a = {};
3754
+ if (attrs) {
3755
+ var keys = Object.keys(attrs);
3756
+ for (var i = 0; i < keys.length; i++) a[keys[i]] = attrs[keys[i]];
3757
+ }
3758
+ if (_activeRouter) {
3759
+ a.href = '#' + path;
3760
+ } else {
3761
+ a.href = path;
3762
+ }
3763
+ a.onclick = function(e) {
3764
+ e.preventDefault();
3765
+ bw.navigate(path);
3766
+ };
3767
+ return { t: 'a', a: a, c: content };
3768
+ };
3769
+
3770
+ // expose for testing: internal helpers
3771
+ bw._router = {
3772
+ matchRoute: matchRoute,
3773
+ normalizePath: normalizePath,
3774
+ parseQuery: parseQuery,
3775
+ getActiveRouter: function() { return _activeRouter; },
3776
+ resetActiveRouter: function() { _activeRouter = null; }
3777
+ };
3778
+ }
3779
+
3467
3780
  /**
3468
3781
  * Bitwrench v2 Components
3469
3782
  *
@@ -10860,6 +11173,7 @@
10860
11173
  bw.getAllComponents = function() {
10861
11174
  return new Map(bw._componentRegistry);
10862
11175
  };
11176
+ initRouter(bw);
10863
11177
 
10864
11178
  // Register all make functions
10865
11179
  Object.entries(components).forEach(([name, fn]) => {