bitwrench 2.0.20 → 2.0.21

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 -2
  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 +84 -84
  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 +2 -2
  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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitwrench",
3
- "version": "2.0.20",
3
+ "version": "2.0.21",
4
4
  "description": "A library for javascript UI functions.",
5
5
  "main": "./dist/bitwrench.umd.js",
6
6
  "repository": {
@@ -43,6 +43,7 @@
43
43
  "dist/*.js",
44
44
  "dist/*.css",
45
45
  "dist/*.json",
46
+ "dist/*.d.ts",
46
47
  "bin/",
47
48
  "src/",
48
49
  "README.md",
@@ -92,10 +93,9 @@
92
93
  "build:readme": "node tools/build-readme.js",
93
94
  "build:watch": "rollup --config --watch",
94
95
  "update_rm": "echo 'DEPRECATED: use build:index instead'",
95
- "build_1_x": "./tools/update-bw-package.js package.json package.json && ./tools/export-bw-default-css.js bitwrench.css && uglifyjs bitwrench.js -o bitwrench.min.js && uglifyjs bitwrench_ESM.js -o bitwrench_ESM.min.js && ./tools/umd2ModuleHack.js && npm pack",
96
96
  "cleanbuild": "npm run clean && npm run build && npm run build:generated",
97
97
  "oldtest": "./node_modules/mocha/bin/mocha test/bitwrench_test.js --reporter spec",
98
- "test": "c8 --reporter=text mocha ./test/bitwrench_ci.js ./test/bitwrench_test_coverage.js ./test/bitwrench_test_pubsub.js ./test/bitwrench_test_theme.js ./test/bitwrench_test_nodemap.js ./test/bitwrench_test_components.js ./test/bitwrench_test_coverage_gaps.js ./test/bitwrench_test_bwserve.js ./test/bitwrench_test_attach.js ./test/bitwrench_test_serve.js ./test/bitwrench_test_code_edit.js ./test/bitwrench_test_html_page.js ./test/bitwrench_test_util_css.js ./test/bitwrench_test_handle.js ./test/bitwrench_test_debug.js -r jsdom-global/register",
98
+ "test": "c8 --reporter=text mocha ./test/bitwrench_ci.js ./test/bitwrench_test_coverage.js ./test/bitwrench_test_pubsub.js ./test/bitwrench_test_theme.js ./test/bitwrench_test_nodemap.js ./test/bitwrench_test_components.js ./test/bitwrench_test_coverage_gaps.js ./test/bitwrench_test_bwserve.js ./test/bitwrench_test_attach.js ./test/bitwrench_test_serve.js ./test/bitwrench_test_code_edit.js ./test/bitwrench_test_html_page.js ./test/bitwrench_test_util_css.js ./test/bitwrench_test_handle.js ./test/bitwrench_test_debug.js ./test/bitwrench_test_router.js -r jsdom-global/register",
99
99
  "test:bwserve": "mocha ./test/bitwrench_test_bwserve.js -r jsdom-global/register",
100
100
  "test:attach": "mocha ./test/bitwrench_test_attach.js -r jsdom-global/register",
101
101
  "test:serve": "mocha ./test/bitwrench_test_serve.js -r jsdom-global/register --exit",
@@ -114,6 +114,7 @@
114
114
  "test:nodemap": "mocha ./test/bitwrench_test_nodemap.js -r jsdom-global/register",
115
115
  "test:cli": "mocha ./test/bitwrench_test_cli.js",
116
116
  "test:debug": "mocha ./test/bitwrench_test_debug.js -r jsdom-global/register",
117
+ "test:router": "mocha ./test/bitwrench_test_router.js -r jsdom-global/register",
117
118
  "test:code-edit": "mocha ./test/bitwrench_test_code_edit.js -r jsdom-global/register",
118
119
  "test:html-page": "mocha ./test/bitwrench_test_html_page.js -r jsdom-global/register",
119
120
  "test:e2e": "playwright test",
@@ -141,9 +142,7 @@
141
142
  "include": [
142
143
  "src/**"
143
144
  ],
144
- "exclude": [
145
- "src_1x/**"
146
- ],
145
+ "exclude": [],
147
146
  "check-coverage": false,
148
147
  "branches": 80,
149
148
  "lines": 80,
package/readme.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta name="generator" content="bitwrench v2.0.20">
6
+ <meta name="generator" content="bitwrench v2.0.21">
7
7
  <title>bitwrench.js - README</title>
8
8
  <link rel="icon" type="image/x-icon" href="images/favicon.ico">
9
9
  <script src="dist/bitwrench.umd.min.js"></script>
@@ -381,7 +381,7 @@ curl -X POST http://localhost:9000 -d &#39;{&quot;type&quot;:&quot;patch&quot;,&
381
381
  <li class="quikdown-li"><a class="quikdown-a" href="examples/client-server/">bwserve Counter</a> -- server-driven UI demo</li>
382
382
  <li class="quikdown-li"><a class="quikdown-a" href="examples/llm-chat/">LLM Chat</a> -- streaming chat via bwserve + Ollama/OpenAI</li>
383
383
  </ul><h2 class="quikdown-h2">FAQ</h2>
384
- <strong class="quikdown-strong">Is this a framework?</strong> -- No. Bitwrench is a library (~38KB gzipped). No lifecycle to learn, no project structure to follow. Import it, call functions, done.</p><p><strong class="quikdown-strong">Does it scale to large apps?</strong> -- Bitwrench targets single-page tools, dashboards, prototypes, embedded UIs, and content sites -- apps where a single HTML file or a handful of files is the right form factor. For a 500-route SPA with team-scale state management, React or Vue is a better fit.</p><p><strong class="quikdown-strong">How does bitwrench compare to React/Vue?</strong> -- They solve different problems at different scales. React and Vue provide a component model, virtual DOM, and ecosystem for large team-built SPAs. Bitwrench provides rendering and state primitives in a single file with no build step, aimed at single-page tools, dashboards, embedded devices, and server-driven UIs. They coexist fine -- use whichever fits the job.</p><p><strong class="quikdown-strong">How does CSS work?</strong> -- Bitwrench doesn&#39;t own your CSS. Use any external stylesheet, Tailwind, or CSS file you want -- bitwrench doesn&#39;t interfere. On top of that, <code class="quikdown-code">bw.css()</code> generates CSS from JS objects (with <code class="quikdown-code">@media</code>, <code class="quikdown-code">@keyframes</code>, pseudo-classes), <code class="quikdown-code">bw.s()</code> composes inline style objects, and <code class="quikdown-code">bw.loadStyles()</code> derives a complete design system from 2 seed colors. You can use all three together or none at all.</p><p><strong class="quikdown-strong">What&#39;s the difference between <code class="quikdown-code">bw.DOM()</code> and <code class="quikdown-code">bw.html()</code>?</strong> -- Same TACO input, two outputs. <code class="quikdown-code">bw.DOM(&#39;#app&#39;, taco)</code> mounts live DOM elements in a browser. <code class="quikdown-code">bw.html(taco)</code> returns an HTML string -- use it in Node.js scripts, email generators, static site builds, or anywhere you need markup without a browser. One object format, two rendering modes.</p><p><strong class="quikdown-strong">What is bwserve?</strong> -- bwserve lets any server push UI updates to a browser over SSE. The server sends TACO objects as JSON; the browser renders them. It&#39;s language-agnostic -- the server can be Python, Go, Rust, C, or a shell script. Anything that can write JSON to an HTTP response can drive a bitwrench UI. See the <a class="quikdown-a" href="docs/bwserve.md">bwserve docs</a>.</p><p><strong class="quikdown-strong">Can I use bitwrench on embedded devices?</strong> -- Yes -- this is a primary use case. An ESP32 or Raspberry Pi serves one HTML page with bitwrench loaded, then pushes sensor data as JSON patches over SSE. The device never generates HTML. See the <a class="quikdown-a" href="docs/tutorial-embedded.md">ESP32 tutorial</a>.</p><p><strong class="quikdown-strong">Can I use it with TypeScript?</strong> -- Yes. Type declarations are included. TACO objects are plain JSON-compatible objects that TypeScript infers naturally.</p><p><strong class="quikdown-strong">What about accessibility?</strong> -- BCCL components emit semantic HTML with ARIA attributes where applicable. You can add any <code class="quikdown-code">aria-*</code> attribute via <code class="quikdown-code">a: { &#39;aria-label&#39;: &#39;...&#39; }</code>.</p><h2 class="quikdown-h2">Development</h2>
384
+ <strong class="quikdown-strong">Is this a framework?</strong> -- No. Bitwrench is a library (~38KB gzipped). No lifecycle to learn, no project structure to follow. Import it, call functions, done.</p><p><strong class="quikdown-strong">How does bitwrench compare to React/Vue?</strong> -- They solve different problems at different scales. React and Vue provide a component model, virtual DOM, and ecosystem for large team-built SPAs. Bitwrench provides rendering and state primitives in a single file with no build step, aimed at single-page tools, dashboards, embedded devices, and server-driven UIs. They coexist fine -- use whichever fits the job.</p><p><strong class="quikdown-strong">How does CSS work?</strong> -- Bitwrench doesn&#39;t own your CSS. Use any external stylesheet, Tailwind, or CSS file you want -- bitwrench doesn&#39;t interfere. On top of that, <code class="quikdown-code">bw.css()</code> generates CSS from JS objects (with <code class="quikdown-code">@media</code>, <code class="quikdown-code">@keyframes</code>, pseudo-classes), <code class="quikdown-code">bw.s()</code> composes inline style objects, and <code class="quikdown-code">bw.loadStyles()</code> derives a complete design system from 2 seed colors. You can use all three together or none at all.</p><p><strong class="quikdown-strong">What&#39;s the difference between <code class="quikdown-code">bw.DOM()</code> and <code class="quikdown-code">bw.html()</code>?</strong> -- Same TACO input, two outputs. <code class="quikdown-code">bw.DOM(&#39;#app&#39;, taco)</code> mounts live DOM elements in a browser. <code class="quikdown-code">bw.html(taco)</code> returns an HTML string -- use it in Node.js scripts, email generators, static site builds, or anywhere you need markup without a browser. One object format, two rendering modes.</p><p><strong class="quikdown-strong">What is bwserve?</strong> -- bwserve lets any server push UI updates to a browser over SSE. The server sends TACO objects as JSON; the browser renders them. It&#39;s language-agnostic -- the server can be Python, Go, Rust, C, or a shell script. Anything that can write JSON to an HTTP response can drive a bitwrench UI. See the <a class="quikdown-a" href="docs/bwserve.md">bwserve docs</a>.</p><p><strong class="quikdown-strong">Can I use bitwrench on embedded devices?</strong> -- Yes -- this is a primary use case. An ESP32 or Raspberry Pi serves one HTML page with bitwrench loaded, then pushes sensor data as JSON patches over SSE. The device never generates HTML. See the <a class="quikdown-a" href="docs/tutorial-embedded.md">ESP32 tutorial</a>.</p><p><strong class="quikdown-strong">Can I use it with TypeScript?</strong> -- Yes. Type declarations are included. TACO objects are plain JSON-compatible objects that TypeScript infers naturally.</p><p><strong class="quikdown-strong">What about accessibility?</strong> -- BCCL components emit semantic HTML with ARIA attributes where applicable. You can add any <code class="quikdown-code">aria-*</code> attribute via <code class="quikdown-code">a: { &#39;aria-label&#39;: &#39;...&#39; }</code>.</p><h2 class="quikdown-h2">Development</h2>
385
385
  <p><pre class="quikdown-pre"><code class="language-bash">npm install # install dev dependencies
386
386
  npm run build # build all dist formats (UMD, ESM, CJS, ES5)
387
387
  npm test # run unit tests (1400+ tests, 96% coverage)
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Bitwrench Router -- client-side URL routing for SPAs
3
+ *
4
+ * Single export: initRouter(bw) attaches bw.router(), bw.navigate(), bw.link()
5
+ *
6
+ * @license BSD-2-Clause
7
+ */
8
+
9
+ // -- internal helpers --
10
+
11
+ function normalizePath(p) {
12
+ // strip query string (handled separately)
13
+ var qi = p.indexOf('?');
14
+ if (qi >= 0) p = p.substring(0, qi);
15
+ // collapse double slashes, strip trailing slash
16
+ p = p.replace(/\/\/+/g, '/');
17
+ if (p.length > 1 && p.charAt(p.length - 1) === '/') p = p.substring(0, p.length - 1);
18
+ return p || '/';
19
+ }
20
+
21
+ function parseQuery(fullPath) {
22
+ var qi = fullPath.indexOf('?');
23
+ if (qi < 0) return {};
24
+ var qs = fullPath.substring(qi + 1);
25
+ var result = {};
26
+ var pairs = qs.split('&');
27
+ for (var i = 0; i < pairs.length; i++) {
28
+ var kv = pairs[i].split('=');
29
+ if (kv[0]) result[decodeURIComponent(kv[0])] = kv.length > 1 ? decodeURIComponent(kv[1]) : '';
30
+ }
31
+ return result;
32
+ }
33
+
34
+ function matchRoute(routes, rawPath) {
35
+ var query = parseQuery(rawPath);
36
+ var path = normalizePath(rawPath);
37
+ var segs = path === '/' ? [''] : path.split('/');
38
+
39
+ var globalWild = null;
40
+
41
+ for (var i = 0; i < routes.length; i++) {
42
+ var r = routes[i];
43
+ var pattern = r.pattern;
44
+
45
+ // global wildcard -- save for last
46
+ if (pattern === '*') { globalWild = r; continue; }
47
+
48
+ // catch-all: ends with /*
49
+ if (pattern.length > 1 && pattern.substring(pattern.length - 2) === '/*') {
50
+ var prefix = pattern.substring(0, pattern.length - 2);
51
+ var prefixSegs = prefix === '' ? [''] : prefix.split('/');
52
+ if (segs.length < prefixSegs.length) continue;
53
+ var params = {};
54
+ var ok = true;
55
+ for (var j = 0; j < prefixSegs.length; j++) {
56
+ if (prefixSegs[j].charAt(0) === ':') {
57
+ params[prefixSegs[j].substring(1)] = segs[j];
58
+ } else if (prefixSegs[j] !== segs[j]) {
59
+ ok = false; break;
60
+ }
61
+ }
62
+ if (ok) {
63
+ params._rest = segs.slice(prefixSegs.length).join('/');
64
+ params._query = query;
65
+ return { handler: r.handler, params: params };
66
+ }
67
+ continue;
68
+ }
69
+
70
+ // exact / parameterized match
71
+ var rSegs = pattern === '/' ? [''] : pattern.split('/');
72
+ if (rSegs.length !== segs.length) continue;
73
+ var params2 = {};
74
+ var match = true;
75
+ for (var k = 0; k < rSegs.length; k++) {
76
+ if (rSegs[k].charAt(0) === ':') {
77
+ params2[rSegs[k].substring(1)] = segs[k];
78
+ } else if (rSegs[k] !== segs[k]) {
79
+ match = false; break;
80
+ }
81
+ }
82
+ if (match) {
83
+ params2._query = query;
84
+ return { handler: r.handler, params: params2 };
85
+ }
86
+ }
87
+
88
+ // global wildcard fallback
89
+ if (globalWild) {
90
+ return { handler: globalWild.handler, params: { _query: query } };
91
+ }
92
+ return null;
93
+ }
94
+
95
+
96
+ // -- public API factory --
97
+
98
+ export function initRouter(bw) {
99
+ var _activeRouter = null;
100
+
101
+ bw.router = function(config) {
102
+ if (!config || !config.routes) throw new Error('bw.router: config.routes is required');
103
+ if (!bw._isBrowser) throw new Error('bw.router: requires a browser environment');
104
+
105
+ var mode = config.mode || 'hash';
106
+ var base = config.base || '/';
107
+ if (base.length > 1 && base.charAt(base.length - 1) === '/') base = base.substring(0, base.length - 1);
108
+ var target = config.target || null;
109
+
110
+ // compile routes (preserve registration order)
111
+ var routes = [];
112
+ var keys = Object.keys(config.routes);
113
+ for (var i = 0; i < keys.length; i++) {
114
+ routes.push({ pattern: keys[i], handler: config.routes[keys[i]] });
115
+ }
116
+
117
+ var currentPath = '/';
118
+ var destroyed = false;
119
+
120
+ function getPath() {
121
+ if (mode === 'hash') {
122
+ var h = window.location.hash.replace(/^#/, '');
123
+ return h || '/';
124
+ }
125
+ var p = window.location.pathname;
126
+ if (base !== '/' && p.indexOf(base) === 0) {
127
+ p = p.substring(base.length) || '/';
128
+ }
129
+ var s = window.location.search || '';
130
+ return p + s;
131
+ }
132
+
133
+ function handleRoute(toRaw, opts) {
134
+ if (destroyed) return;
135
+ var fromPath = currentPath;
136
+ var toPath = normalizePath(toRaw);
137
+
138
+ // before guard
139
+ if (config.before) {
140
+ var result = config.before(toPath, fromPath);
141
+ if (result === false) return;
142
+ if (typeof result === 'string') {
143
+ toPath = normalizePath(result);
144
+ toRaw = result;
145
+ }
146
+ }
147
+
148
+ currentPath = toPath;
149
+
150
+ // match route
151
+ var m = matchRoute(routes, toRaw);
152
+ if (m) {
153
+ var rendered = m.handler(m.params);
154
+ if (rendered != null && target) {
155
+ bw.DOM(target, rendered);
156
+ }
157
+ }
158
+
159
+ // pub/sub
160
+ var query = parseQuery(toRaw);
161
+ bw.pub('bw:route', {
162
+ path: toPath,
163
+ params: m ? m.params : {},
164
+ query: query,
165
+ from: fromPath
166
+ });
167
+
168
+ // after hook
169
+ if (config.after) config.after(toPath, fromPath);
170
+ }
171
+
172
+ function navigate(path, opts) {
173
+ if (destroyed) return;
174
+ opts = opts || {};
175
+ if (mode === 'hash') {
176
+ if (opts.replace) {
177
+ var loc = window.location;
178
+ loc.replace(loc.pathname + loc.search + '#' + path);
179
+ } else {
180
+ window.location.hash = path;
181
+ }
182
+ // hashchange listener will fire handleRoute; but if same hash, trigger manually
183
+ var currentHash = window.location.hash.replace(/^#/, '') || '/';
184
+ if (normalizePath(currentHash) === normalizePath(path)) {
185
+ handleRoute(path, opts);
186
+ }
187
+ } else {
188
+ var url = (base === '/' ? '' : base) + path;
189
+ if (opts.replace) {
190
+ window.history.replaceState(null, '', url);
191
+ } else {
192
+ window.history.pushState(null, '', url);
193
+ }
194
+ handleRoute(path, opts);
195
+ }
196
+ }
197
+
198
+ function onHashChange() {
199
+ if (destroyed) return;
200
+ handleRoute(getPath());
201
+ }
202
+
203
+ function onPopState() {
204
+ if (destroyed) return;
205
+ handleRoute(getPath());
206
+ }
207
+
208
+ // listen
209
+ if (mode === 'hash') {
210
+ window.addEventListener('hashchange', onHashChange);
211
+ } else {
212
+ window.addEventListener('popstate', onPopState);
213
+ }
214
+
215
+ // initial render
216
+ handleRoute(getPath());
217
+
218
+ var routerObj = {
219
+ navigate: navigate,
220
+ current: function() {
221
+ var raw = getPath();
222
+ var m = matchRoute(routes, raw);
223
+ return {
224
+ path: currentPath,
225
+ params: m ? m.params : {},
226
+ query: parseQuery(raw)
227
+ };
228
+ },
229
+ destroy: function() {
230
+ destroyed = true;
231
+ if (mode === 'hash') {
232
+ window.removeEventListener('hashchange', onHashChange);
233
+ } else {
234
+ window.removeEventListener('popstate', onPopState);
235
+ }
236
+ if (_activeRouter === routerObj) _activeRouter = null;
237
+ }
238
+ };
239
+
240
+ _activeRouter = routerObj;
241
+ return routerObj;
242
+ };
243
+
244
+ bw.navigate = function(path, opts) {
245
+ if (_activeRouter) {
246
+ _activeRouter.navigate(path, opts);
247
+ } else {
248
+ if (typeof console !== 'undefined' && console.warn) {
249
+ console.warn('bw.navigate: no active router');
250
+ }
251
+ }
252
+ };
253
+
254
+ bw.link = function(path, content, attrs) {
255
+ var a = {};
256
+ if (attrs) {
257
+ var keys = Object.keys(attrs);
258
+ for (var i = 0; i < keys.length; i++) a[keys[i]] = attrs[keys[i]];
259
+ }
260
+ if (_activeRouter) {
261
+ // determine href based on mode -- check hash by looking at current location
262
+ var isHash = window.location.hash !== undefined; // always true, but we default hash
263
+ a.href = '#' + path;
264
+ } else {
265
+ a.href = path;
266
+ }
267
+ a.onclick = function(e) {
268
+ e.preventDefault();
269
+ bw.navigate(path);
270
+ };
271
+ return { t: 'a', a: a, c: content };
272
+ };
273
+
274
+ // expose for testing: internal helpers
275
+ bw._router = {
276
+ matchRoute: matchRoute,
277
+ normalizePath: normalizePath,
278
+ parseQuery: parseQuery,
279
+ getActiveRouter: function() { return _activeRouter; },
280
+ resetActiveRouter: function() { _activeRouter = null; }
281
+ };
282
+ }
@@ -108,10 +108,10 @@ export var ELEVATION_PRESETS = {
108
108
  xl: '0 4px 12px rgba(0,0,0,0.12)'
109
109
  },
110
110
  md: {
111
- sm: '0 1px 3px rgba(0,0,0,0.08)',
112
- md: '0 2px 6px rgba(0,0,0,0.12)',
113
- lg: '0 4px 12px rgba(0,0,0,0.16)',
114
- xl: '0 8px 24px rgba(0,0,0,0.20)'
111
+ sm: '0 1px 3px rgba(0,0,0,0.10), 0 1px 2px rgba(0,0,0,0.06)',
112
+ md: '0 4px 6px rgba(0,0,0,0.10), 0 2px 4px rgba(0,0,0,0.06)',
113
+ lg: '0 10px 15px rgba(0,0,0,0.12), 0 4px 6px rgba(0,0,0,0.08)',
114
+ xl: '0 20px 25px rgba(0,0,0,0.15), 0 8px 10px rgba(0,0,0,0.10)'
115
115
  },
116
116
  lg: {
117
117
  sm: '0 2px 4px rgba(0,0,0,0.10)',
@@ -305,6 +305,9 @@ function generateCards(scope, palette, layout) {
305
305
  rules[_sx(scope, '.bw_card:hover')] = {
306
306
  'box-shadow': elev.md
307
307
  };
308
+ rules[_sx(scope, '.bw_card_hoverable')] = {
309
+ 'transition': 'box-shadow ' + motion.slow + ' ' + motion.easing + ', transform ' + motion.slow + ' ' + motion.easing
310
+ };
308
311
  rules[_sx(scope, '.bw_card_hoverable:hover')] = {
309
312
  'box-shadow': elev.lg
310
313
  };
@@ -405,7 +408,8 @@ function generateNavigation(scope, palette, layout) {
405
408
  };
406
409
  rules[_sx(scope, '.bw_navbar_nav .bw_nav_link')] = {
407
410
  'color': palette.secondary.base,
408
- 'border-radius': layout.radius.btn
411
+ 'border-radius': layout.radius.btn,
412
+ 'transition': 'color ' + layout.motion.fast + ' ' + layout.motion.easing + ', background-color ' + layout.motion.fast + ' ' + layout.motion.easing
409
413
  };
410
414
  rules[_sx(scope, '.bw_navbar_nav .bw_nav_link:hover')] = {
411
415
  'color': palette.dark.base,
@@ -568,15 +572,18 @@ function generatePagination(scope, palette, layout) {
568
572
  return rules;
569
573
  }
570
574
 
571
- function generateProgress(scope, palette) {
575
+ function generateProgress(scope, palette, layout) {
572
576
  var rules = {};
577
+ var rd = layout ? layout.radius : { badge: '.375rem' };
573
578
  rules[_sx(scope, '.bw_progress')] = {
574
579
  'background-color': palette.surfaceAlt,
580
+ 'border-radius': rd.badge,
575
581
  'box-shadow': 'inset 0 1px 2px rgba(0,0,0,.1)'
576
582
  };
577
583
  rules[_sx(scope, '.bw_progress_bar')] = {
578
584
  'color': palette.primary.textOn,
579
585
  'background-color': palette.primary.base,
586
+ 'border-radius': 'inherit',
580
587
  'box-shadow': 'inset 0 -1px 0 rgba(0,0,0,.15)'
581
588
  };
582
589
  // Variant progress bar colors handled by palette class
@@ -704,7 +711,8 @@ function generateCarouselThemed(scope, palette) {
704
711
  };
705
712
  rules[_sx(scope, '.bw_carousel_control')] = {
706
713
  'background-color': palette.dark.base,
707
- 'color': palette.dark.textOn
714
+ 'color': palette.dark.textOn,
715
+ 'transition': 'background-color 0.15s ease-out'
708
716
  };
709
717
  rules[_sx(scope, '.bw_carousel_control:hover')] = {
710
718
  'background-color': palette.dark.hover
@@ -718,9 +726,11 @@ function generateCarouselThemed(scope, palette) {
718
726
 
719
727
  function generateModalThemed(scope, palette, layout) {
720
728
  var rules = {};
729
+ var rd = layout ? layout.radius : { card: '8px' };
721
730
  rules[_sx(scope, '.bw_modal_content')] = {
722
731
  'background-color': palette.surface || '#fff',
723
732
  'border-color': palette.light.border,
733
+ 'border-radius': rd.card,
724
734
  'box-shadow': layout.elevation.lg
725
735
  };
726
736
  rules[_sx(scope, '.bw_modal_header')] = {
@@ -737,9 +747,11 @@ function generateModalThemed(scope, palette, layout) {
737
747
 
738
748
  function generateToastThemed(scope, palette, layout) {
739
749
  var rules = {};
750
+ var rd = layout ? layout.radius : { card: '8px' };
740
751
  rules[_sx(scope, '.bw_toast')] = {
741
752
  'background-color': palette.surface || '#fff',
742
753
  'border-color': palette.light.border,
754
+ 'border-radius': rd.card,
743
755
  'box-shadow': layout.elevation.lg
744
756
  };
745
757
  rules[_sx(scope, '.bw_toast_header')] = {
@@ -751,9 +763,11 @@ function generateToastThemed(scope, palette, layout) {
751
763
 
752
764
  function generateDropdownThemed(scope, palette, layout) {
753
765
  var rules = {};
766
+ var rd = layout ? layout.radius : { card: '8px' };
754
767
  rules[_sx(scope, '.bw_dropdown_menu')] = {
755
768
  'background-color': palette.surface || '#fff',
756
769
  'border-color': palette.light.border,
770
+ 'border-radius': rd.card,
757
771
  'box-shadow': layout.elevation.md
758
772
  };
759
773
  rules[_sx(scope, '.bw_dropdown_item')] = {
@@ -786,6 +800,10 @@ function generateSwitchThemed(scope, palette) {
786
800
  rules[_sx(scope, '.bw_form_switch .bw_switch_input:focus')] = {
787
801
  'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus
788
802
  };
803
+ rules[_sx(scope, '.bw_form_switch .bw_switch_input:focus-visible')] = {
804
+ 'box-shadow': '0 0 0 0.25rem ' + palette.primary.focus,
805
+ 'outline': 'none'
806
+ };
789
807
  return rules;
790
808
  }
791
809
 
@@ -849,12 +867,14 @@ function generateStepperThemed(scope, palette) {
849
867
  return rules;
850
868
  }
851
869
 
852
- function generateChipInputThemed(scope, palette) {
870
+ function generateChipInputThemed(scope, palette, layout) {
853
871
  var rules = {};
872
+ var rd = layout ? layout.radius : { input: '6px' };
854
873
  rules[_sx(scope, '.bw_chip_input')] = {
855
874
  'border-color': palette.light.border,
856
875
  'background-color': palette.surface || '#fff',
857
- 'color': palette.dark.base
876
+ 'color': palette.dark.base,
877
+ 'border-radius': rd.input
858
878
  };
859
879
  rules[_sx(scope, '.bw_chip_input:focus-within')] = {
860
880
  'border-color': palette.primary.base,
@@ -1139,7 +1159,7 @@ export function generateThemedCSS(scopeName, palette, layout) {
1139
1159
  generateTabs(scopeName, palette, layout),
1140
1160
  generateListGroups(scopeName, palette, layout),
1141
1161
  generatePagination(scopeName, palette, layout),
1142
- generateProgress(scopeName, palette),
1162
+ generateProgress(scopeName, palette, layout),
1143
1163
  generateBreadcrumbThemed(scopeName, palette, layout),
1144
1164
  generateCloseButtonThemed(scopeName, palette),
1145
1165
  generateSectionsThemed(scopeName, palette),
@@ -1153,7 +1173,7 @@ export function generateThemedCSS(scopeName, palette, layout) {
1153
1173
  generateStatCardThemed(scopeName, palette, layout),
1154
1174
  generateTimelineThemed(scopeName, palette),
1155
1175
  generateStepperThemed(scopeName, palette),
1156
- generateChipInputThemed(scopeName, palette),
1176
+ generateChipInputThemed(scopeName, palette, layout),
1157
1177
  generateFileUploadThemed(scopeName, palette, layout),
1158
1178
  generateRangeThemed(scopeName, palette),
1159
1179
  generateSearchThemed(scopeName, palette, layout),
@@ -1301,7 +1321,7 @@ var structuralRules = {
1301
1321
  '.bw_card_text': { 'margin-bottom': '0', 'font-size': '0.9375rem', 'line-height': '1.6' },
1302
1322
  '.bw_card_header': { 'margin-bottom': '0', 'font-weight': '600', 'font-size': '0.875rem' },
1303
1323
  '.bw_card_footer': { 'font-size': '0.875rem' },
1304
- '.bw_card_hoverable': { 'transition': 'all 0.3s ease-out' },
1324
+ '.bw_card_hoverable': {},
1305
1325
  '.bw_card_hoverable:hover': { 'transform': 'translateY(-4px)' },
1306
1326
  '.bw_card_img_top': { 'width': '100%' },
1307
1327
  '.bw_card_img_bottom': { 'width': '100%' },
@@ -1316,7 +1336,8 @@ var structuralRules = {
1316
1336
  'display': 'block', 'width': '100%',
1317
1337
  'font-size': '0.9375rem', 'font-weight': '400', 'line-height': '1.5',
1318
1338
  'background-clip': 'padding-box', 'appearance': 'none',
1319
- 'border': '1px solid transparent', 'font-family': 'inherit'
1339
+ 'border': '1px solid transparent', 'font-family': 'inherit',
1340
+ 'transition': 'border-color 0.15s ease-out, box-shadow 0.15s ease-out'
1320
1341
  },
1321
1342
  '.bw_form_control:focus': { 'outline': '2px solid currentColor', 'outline-offset': '-1px' },
1322
1343
  '.bw_form_control::placeholder': { 'opacity': '1' },
@@ -1443,6 +1464,7 @@ var structuralRules = {
1443
1464
  'text-decoration': 'none', 'cursor': 'pointer',
1444
1465
  'border': 'none', 'background': 'transparent', 'font-family': 'inherit'
1445
1466
  },
1467
+ '.bw_nav_link:focus-visible': { 'outline': '2px solid currentColor', 'outline-offset': '-2px' },
1446
1468
  '.bw_nav_tabs .bw_nav_link': { 'border': 'none', 'border-bottom': '2px solid transparent', 'border-radius': '0', 'background-color': 'transparent' },
1447
1469
  '.bw_nav_vertical': { 'flex-direction': 'column' },
1448
1470
  '.bw_tab_content': { 'padding': '1.25rem 0' },
@@ -1560,9 +1582,11 @@ var structuralRules = {
1560
1582
  'display': 'inline-flex', 'align-items': 'center', 'justify-content': 'center',
1561
1583
  'width': '1.5rem', 'height': '1.5rem', 'padding': '0',
1562
1584
  'font-size': '1.25rem', 'font-weight': '700', 'line-height': '1',
1563
- 'background': 'transparent', 'border': '0', 'border-radius': '0.25rem', 'cursor': 'pointer'
1585
+ 'background': 'transparent', 'border': '0', 'border-radius': '0.25rem', 'cursor': 'pointer',
1586
+ 'transition': 'opacity 0.15s ease-out'
1564
1587
  },
1565
- '.bw_close:hover': { 'opacity': '0.75' }
1588
+ '.bw_close:hover': { 'opacity': '0.75' },
1589
+ '.bw_close:focus-visible': { 'outline': '2px solid currentColor', 'outline-offset': '2px' }
1566
1590
  },
1567
1591
 
1568
1592
  // ---- Stacks ----
@@ -1644,7 +1668,8 @@ var structuralRules = {
1644
1668
  'flex-shrink': '0', 'width': '1.25rem', 'height': '1.25rem', 'margin-left': 'auto',
1645
1669
  'content': '""',
1646
1670
  '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\")",
1647
- 'background-repeat': 'no-repeat', 'background-size': '1.25rem'
1671
+ 'background-repeat': 'no-repeat', 'background-size': '1.25rem',
1672
+ 'transition': 'transform 0.2s ease-out'
1648
1673
  },
1649
1674
  '.bw_accordion_button:not(.bw_collapsed)::after': { 'transform': 'rotate(-180deg)' },
1650
1675
  '.bw_accordion_body': { 'padding': '1rem 1.25rem' },
@@ -1669,6 +1694,7 @@ var structuralRules = {
1669
1694
  'z-index': '2', 'padding': '0'
1670
1695
  },
1671
1696
  '.bw_carousel_control img': { 'width': '20px', 'height': '20px', 'pointer-events': 'none' },
1697
+ '.bw_carousel_control:focus-visible': { 'outline': '2px solid currentColor', 'outline-offset': '2px' },
1672
1698
  '.bw_carousel_control_prev': { 'left': '10px' },
1673
1699
  '.bw_carousel_control_next': { 'right': '10px' },
1674
1700
  '.bw_carousel_indicators': {
@@ -1688,12 +1714,14 @@ var structuralRules = {
1688
1714
  'display': 'flex', 'align-items': 'center', 'justify-content': 'center',
1689
1715
  'position': 'fixed', 'top': '0', 'left': '0', 'width': '100%', 'height': '100%',
1690
1716
  'z-index': '1050', 'overflow-x': 'hidden', 'overflow-y': 'auto',
1691
- 'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none'
1717
+ 'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none',
1718
+ 'transition': 'opacity 0.2s ease-out, visibility 0.2s ease-out'
1692
1719
  },
1693
1720
  '.bw_modal.bw_modal_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
1694
1721
  '.bw_modal_dialog': {
1695
1722
  'position': 'relative', 'width': 'calc(100% - 1rem)', 'max-width': '500px', 'margin': '1.75rem auto',
1696
- 'pointer-events': 'none'
1723
+ 'pointer-events': 'none', 'transform': 'translateY(-16px)',
1724
+ 'transition': 'transform 0.2s ease-out'
1697
1725
  },
1698
1726
  '.bw_modal.bw_modal_show .bw_modal_dialog': { 'transform': 'translateY(0)' },
1699
1727
  '.bw_modal_sm': { 'max-width': '300px' },
@@ -1723,10 +1751,11 @@ var structuralRules = {
1723
1751
  '.bw_toast_container.bw_toast_bottom_center': { 'bottom': '0', 'left': '50%', 'transform': 'translateX(-50%)' },
1724
1752
  '.bw_toast': {
1725
1753
  'pointer-events': 'auto', 'width': '350px', 'max-width': 'calc(100vw - 2rem)', 'background-clip': 'padding-box',
1726
- 'opacity': '0'
1754
+ 'opacity': '0', 'transform': 'translateY(-8px)',
1755
+ 'transition': 'opacity 0.2s ease-out, transform 0.2s ease-out'
1727
1756
  },
1728
1757
  '.bw_toast.bw_toast_show': { 'opacity': '1', 'transform': 'translateY(0)' },
1729
- '.bw_toast.bw_toast_hiding': { 'opacity': '0' },
1758
+ '.bw_toast.bw_toast_hiding': { 'opacity': '0', 'transform': 'translateY(-8px)' },
1730
1759
  '.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' },
1731
1760
  '.bw_toast_body': { 'padding': '0.5rem 0.75rem', 'font-size': '0.9375rem' }
1732
1761
  },
@@ -1743,9 +1772,11 @@ var structuralRules = {
1743
1772
  'position': 'absolute', 'top': '100%', 'left': '0', 'z-index': '1000', 'display': 'block',
1744
1773
  'min-width': '10rem', 'padding': '0.5rem 0', 'margin': '0.125rem 0 0',
1745
1774
  'background-clip': 'padding-box', 'border': '1px solid transparent',
1746
- 'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none'
1775
+ 'opacity': '0', 'visibility': 'hidden', 'pointer-events': 'none',
1776
+ 'transform': 'translateY(-4px)',
1777
+ 'transition': 'opacity 0.15s ease-out, transform 0.15s ease-out, visibility 0.15s ease-out'
1747
1778
  },
1748
- '.bw_dropdown_menu.bw_dropdown_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
1779
+ '.bw_dropdown_menu.bw_dropdown_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto', 'transform': 'translateY(0)' },
1749
1780
  '.bw_dropdown_menu_end': { 'left': 'auto', 'right': '0' },
1750
1781
  '.bw_dropdown_item': {
1751
1782
  'display': 'block', 'width': '100%', 'padding': '0.4rem 1rem', 'clear': 'both',
@@ -1764,7 +1795,8 @@ var structuralRules = {
1764
1795
  'appearance': 'none',
1765
1796
  '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\")",
1766
1797
  'background-position': 'left center', 'background-repeat': 'no-repeat',
1767
- 'background-size': 'contain', 'cursor': 'pointer'
1798
+ 'background-size': 'contain', 'cursor': 'pointer',
1799
+ 'transition': 'background-color 0.15s ease-out, background-position 0.15s ease-out, border-color 0.15s ease-out'
1768
1800
  },
1769
1801
  '.bw_form_switch .bw_switch_input:checked': { 'background-position': 'right center' },
1770
1802
  '.bw_form_switch .bw_switch_input:disabled': { 'opacity': '0.5', 'cursor': 'not-allowed' }
@@ -1798,9 +1830,7 @@ var structuralRules = {
1798
1830
  '.bw_stat_card': {
1799
1831
  'padding': '1.25rem',
1800
1832
  'border-left': '4px solid transparent',
1801
- 'border-radius': '0.375rem',
1802
- 'background-color': 'inherit',
1803
- 'transition': 'transform 0.15s ease'
1833
+ 'background-color': 'inherit'
1804
1834
  },
1805
1835
  '.bw_stat_card:hover': { 'transform': 'translateY(-1px)' },
1806
1836
  '.bw_stat_icon': { 'font-size': '1.5rem', 'margin-bottom': '0.5rem' },
@@ -1860,7 +1890,8 @@ var structuralRules = {
1860
1890
  'width': '1.5rem', 'height': '1.5rem',
1861
1891
  'border': 'none', 'background': 'none',
1862
1892
  'font-size': '1.25rem', 'cursor': 'pointer', 'padding': '0', 'border-radius': '50%'
1863
- }
1893
+ },
1894
+ '.bw_search_clear:focus-visible': { 'outline': '2px solid currentColor', 'outline-offset': '2px' }
1864
1895
  },
1865
1896
 
1866
1897
  // ---- Range ----
@@ -1952,6 +1983,7 @@ var structuralRules = {
1952
1983
  'width': '1rem', 'height': '1rem', 'border': 'none', 'background': 'none',
1953
1984
  'font-size': '0.875rem', 'cursor': 'pointer', 'padding': '0', 'border-radius': '50%'
1954
1985
  },
1986
+ '.bw_chip_remove:focus-visible': { 'outline': '2px solid currentColor', 'outline-offset': '1px' },
1955
1987
  '.bw_chip_field': { 'flex': '1', 'min-width': '80px', 'border': 'none', 'outline': 'none', 'font-size': '0.875rem', 'padding': '0.125rem 0', 'background': 'transparent' }
1956
1988
  },
1957
1989
 
package/src/bitwrench.js CHANGED
@@ -3566,6 +3566,12 @@ bw.getAllComponents = function() {
3566
3566
  return new Map(bw._componentRegistry);
3567
3567
  };
3568
3568
 
3569
+ // =========================================================================
3570
+ // Import and register router
3571
+ // =========================================================================
3572
+ import { initRouter } from './bitwrench-router.js';
3573
+ initRouter(bw);
3574
+
3569
3575
  // =========================================================================
3570
3576
  // Import and register all components
3571
3577
  // =========================================================================