snice 3.6.0 → 3.8.0

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 (80) hide show
  1. package/README.md +2 -2
  2. package/bin/snice.js +4 -5
  3. package/bin/templates/CLAUDE.md +25 -3
  4. package/bin/templates/pwa/README.md +188 -0
  5. package/bin/templates/pwa/global.d.ts +10 -0
  6. package/bin/templates/pwa/index.html +16 -0
  7. package/bin/templates/pwa/package.json +32 -0
  8. package/bin/templates/pwa/public/icons/.gitkeep +6 -0
  9. package/bin/templates/pwa/src/daemons/notifications.ts +148 -0
  10. package/bin/templates/pwa/src/fetcher.ts +15 -0
  11. package/bin/templates/pwa/src/guards/auth.ts +12 -0
  12. package/bin/templates/pwa/src/main.ts +42 -0
  13. package/bin/templates/pwa/src/middleware/auth.ts +16 -0
  14. package/bin/templates/pwa/src/middleware/error.ts +36 -0
  15. package/bin/templates/pwa/src/middleware/retry.ts +31 -0
  16. package/bin/templates/pwa/src/pages/dashboard.ts +143 -0
  17. package/bin/templates/pwa/src/pages/login.ts +161 -0
  18. package/bin/templates/pwa/src/pages/notifications.ts +156 -0
  19. package/bin/templates/pwa/src/pages/profile.ts +164 -0
  20. package/bin/templates/pwa/src/router.ts +20 -0
  21. package/bin/templates/pwa/src/services/auth.ts +48 -0
  22. package/bin/templates/pwa/src/services/jwt.ts +35 -0
  23. package/bin/templates/pwa/src/services/storage.ts +24 -0
  24. package/bin/templates/pwa/src/styles/global.css +55 -0
  25. package/bin/templates/pwa/src/types/auth.ts +21 -0
  26. package/bin/templates/pwa/src/types/notifications.ts +9 -0
  27. package/bin/templates/pwa/tests/helpers/test-utils.ts +84 -0
  28. package/bin/templates/pwa/tests/middleware/auth.test.ts +67 -0
  29. package/bin/templates/pwa/tests/middleware/error.test.ts +105 -0
  30. package/bin/templates/pwa/tests/middleware/retry.test.ts +103 -0
  31. package/bin/templates/pwa/tests/services/auth.test.ts +89 -0
  32. package/bin/templates/pwa/tests/services/jwt.test.ts +76 -0
  33. package/bin/templates/pwa/tests/services/storage.test.ts +69 -0
  34. package/bin/templates/{social → pwa}/tsconfig.json +11 -10
  35. package/bin/templates/pwa/vite.config.ts +94 -0
  36. package/bin/templates/{social/vite.config.ts → pwa/vitest.config.ts} +12 -17
  37. package/dist/components/music-player/snice-music-player.d.ts +72 -0
  38. package/dist/components/music-player/snice-music-player.js +730 -0
  39. package/dist/components/music-player/snice-music-player.js.map +1 -0
  40. package/dist/components/music-player/snice-music-player.types.d.ts +43 -0
  41. package/dist/components/timer/snice-timer.d.ts +27 -0
  42. package/dist/components/timer/snice-timer.js +197 -0
  43. package/dist/components/timer/snice-timer.js.map +1 -0
  44. package/dist/components/timer/snice-timer.types.d.ts +10 -0
  45. package/dist/fetcher.d.ts +65 -0
  46. package/dist/index.cjs +92 -3
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.esm.js +92 -4
  50. package/dist/index.esm.js.map +1 -1
  51. package/dist/index.iife.js +92 -3
  52. package/dist/index.iife.js.map +1 -1
  53. package/dist/symbols.cjs +1 -1
  54. package/dist/symbols.esm.js +1 -1
  55. package/dist/transitions.cjs +1 -1
  56. package/dist/transitions.esm.js +1 -1
  57. package/dist/types/context.d.ts +7 -1
  58. package/dist/types/router-options.d.ts +6 -0
  59. package/docs/ai/api.md +33 -1
  60. package/docs/ai/components/music-player.md +134 -0
  61. package/docs/ai/components/timer.md +43 -0
  62. package/docs/ai/patterns.md +48 -1
  63. package/docs/components/music-player.md +314 -0
  64. package/docs/components/timer.md +143 -0
  65. package/docs/fetcher.md +447 -0
  66. package/docs/routing.md +11 -8
  67. package/package.json +2 -1
  68. package/bin/templates/social/README.md +0 -42
  69. package/bin/templates/social/global.d.ts +0 -14
  70. package/bin/templates/social/index.html +0 -13
  71. package/bin/templates/social/package.json +0 -21
  72. package/bin/templates/social/src/main.ts +0 -33
  73. package/bin/templates/social/src/pages/feed-page.ts +0 -111
  74. package/bin/templates/social/src/pages/messages-page.ts +0 -102
  75. package/bin/templates/social/src/pages/not-found-page.ts +0 -46
  76. package/bin/templates/social/src/pages/profile-page.ts +0 -99
  77. package/bin/templates/social/src/pages/settings-page.ts +0 -119
  78. package/bin/templates/social/src/router.ts +0 -9
  79. package/bin/templates/social/src/styles/global.css +0 -156
  80. /package/bin/templates/{social → pwa}/public/vite.svg +0 -0
@@ -0,0 +1,143 @@
1
+ # Timer Component
2
+
3
+ The `<snice-timer>` component provides a stopwatch and countdown timer.
4
+
5
+ ## Basic Usage
6
+
7
+ ```html
8
+ <!-- Stopwatch -->
9
+ <snice-timer mode="stopwatch"></snice-timer>
10
+
11
+ <!-- Countdown Timer -->
12
+ <snice-timer mode="timer" initial-time="60"></snice-timer>
13
+ ```
14
+
15
+ ## Properties
16
+
17
+ | Property | Type | Default | Description |
18
+ |----------|------|---------|-------------|
19
+ | `mode` | `'stopwatch' \| 'timer'` | `'stopwatch'` | Timer mode |
20
+ | `initial-time` | `number` | `0` | Starting time in seconds (for timer mode) |
21
+ | `running` | `boolean` | `false` | Timer running state (read-only) |
22
+
23
+ ## Methods
24
+
25
+ | Method | Returns | Description |
26
+ |--------|---------|-------------|
27
+ | `start()` | `void` | Start the timer |
28
+ | `stop()` | `void` | Stop/pause the timer |
29
+ | `reset()` | `void` | Reset to initial state |
30
+ | `getTime()` | `number` | Get current time in seconds |
31
+
32
+ ## Events
33
+
34
+ | Event | Detail | Description |
35
+ |-------|--------|-------------|
36
+ | `@snice/timer-start` | `{ timer, time }` | Timer started |
37
+ | `@snice/timer-stop` | `{ timer, time }` | Timer stopped |
38
+ | `@snice/timer-reset` | `{ timer, time }` | Timer reset |
39
+ | `@snice/timer-complete` | `{ timer }` | Countdown completed (timer mode only) |
40
+
41
+ ## Examples
42
+
43
+ ### Stopwatch
44
+
45
+ ```html
46
+ <snice-timer id="stopwatch" mode="stopwatch"></snice-timer>
47
+
48
+ <script>
49
+ const stopwatch = document.getElementById('stopwatch');
50
+ stopwatch.start();
51
+
52
+ // Later...
53
+ stopwatch.stop();
54
+ console.log('Elapsed:', stopwatch.getTime(), 'seconds');
55
+ </script>
56
+ ```
57
+
58
+ ### Countdown Timer
59
+
60
+ ```html
61
+ <snice-timer id="timer" mode="timer" initial-time="300"></snice-timer>
62
+
63
+ <script>
64
+ const timer = document.getElementById('timer');
65
+
66
+ timer.addEventListener('@snice/timer-complete', () => {
67
+ console.log('Time is up!');
68
+ });
69
+
70
+ timer.start();
71
+ </script>
72
+ ```
73
+
74
+ ### Programmatic Control
75
+
76
+ ```html
77
+ <snice-timer id="my-timer"></snice-timer>
78
+
79
+ <button onclick="document.getElementById('my-timer').start()">Start</button>
80
+ <button onclick="document.getElementById('my-timer').stop()">Stop</button>
81
+ <button onclick="document.getElementById('my-timer').reset()">Reset</button>
82
+
83
+ <script>
84
+ const timer = document.getElementById('my-timer');
85
+
86
+ timer.addEventListener('@snice/timer-start', (e) => {
87
+ console.log('Timer started at', e.detail.time);
88
+ });
89
+
90
+ timer.addEventListener('@snice/timer-stop', (e) => {
91
+ console.log('Timer stopped at', e.detail.time);
92
+ });
93
+ </script>
94
+ ```
95
+
96
+ ### Workout Timer
97
+
98
+ ```html
99
+ <snice-timer id="workout" mode="timer" initial-time="45"></snice-timer>
100
+
101
+ <script>
102
+ const workout = document.getElementById('workout');
103
+
104
+ workout.addEventListener('@snice/timer-complete', () => {
105
+ alert('Rest time!');
106
+ // Start rest period
107
+ workout.initialTime = 15;
108
+ workout.reset();
109
+ workout.start();
110
+ });
111
+
112
+ workout.start();
113
+ </script>
114
+ ```
115
+
116
+ ## Styling
117
+
118
+ The timer uses CSS custom properties from the theme system:
119
+
120
+ ```css
121
+ snice-timer {
122
+ --snice-color-background-element: rgb(252 251 249);
123
+ --snice-color-border: rgb(226 226 226);
124
+ --snice-color-text: rgb(23 23 23);
125
+ --snice-color-success: rgb(22 163 74);
126
+ --snice-color-warning: rgb(202 138 4);
127
+ --snice-color-neutral: rgb(82 82 82);
128
+ }
129
+ ```
130
+
131
+ ## Accessibility
132
+
133
+ - Large, readable time display
134
+ - Clear button labels and icons
135
+ - Keyboard accessible controls
136
+ - High contrast buttons
137
+
138
+ ## Best Practices
139
+
140
+ 1. **Choose the right mode**: Use stopwatch for tracking elapsed time, timer for countdowns
141
+ 2. **Handle timer-complete**: Listen for completion events in timer mode
142
+ 3. **Provide context**: Add labels or descriptions near the timer
143
+ 4. **Reset appropriately**: Call `reset()` to return to initial state
@@ -0,0 +1,447 @@
1
+ # Fetch Middleware
2
+
3
+ Snice provides a context-aware fetch implementation with middleware support, allowing you to intercept and modify HTTP requests and responses globally across your application.
4
+
5
+ ## Overview
6
+
7
+ The `ContextAwareFetcher` class enables you to:
8
+
9
+ - Add authentication headers automatically to all requests
10
+ - Handle errors consistently across your application
11
+ - Log HTTP requests and responses
12
+ - Transform requests or responses
13
+ - Implement retry logic
14
+ - Add request/response timing metrics
15
+ - Access application and navigation state in middleware
16
+
17
+ ## Basic Usage
18
+
19
+ ```typescript
20
+ import { Router, ContextAwareFetcher } from 'snice';
21
+
22
+ // Create a fetcher instance
23
+ const fetcher = new ContextAwareFetcher();
24
+
25
+ // Add request middleware (runs before fetch)
26
+ fetcher.use('request', function(request, next) {
27
+ // `this` is bound to the Context instance
28
+ const token = this.application.user?.token;
29
+ if (token) {
30
+ request.headers.set('Authorization', `Bearer ${token}`);
31
+ }
32
+ return next();
33
+ });
34
+
35
+ // Add response middleware (runs after fetch)
36
+ fetcher.use('response', async function(response, next) {
37
+ if (!response.ok) {
38
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
39
+ }
40
+ return next();
41
+ });
42
+
43
+ // Pass fetcher to Router
44
+ const router = Router({
45
+ target: '#app',
46
+ context: { user: null },
47
+ fetcher
48
+ });
49
+
50
+ router.initialize();
51
+ ```
52
+
53
+ ## Using Context.fetch in Pages
54
+
55
+ Once configured, `ctx.fetch` is available in all pages and components that use the `@context` decorator:
56
+
57
+ ```typescript
58
+ @page({ tag: 'user-page', routes: ['/users/:id'] })
59
+ class UserPage extends HTMLElement {
60
+ private ctx: Context;
61
+
62
+ @context()
63
+ handleContext(ctx: Context) {
64
+ this.ctx = ctx;
65
+ }
66
+
67
+ @ready()
68
+ async loadUser() {
69
+ try {
70
+ // Middleware is automatically applied
71
+ const user = await this.ctx.fetch('/api/users/123')
72
+ .then(r => r.json());
73
+
74
+ console.log('User loaded:', user);
75
+ } catch (error) {
76
+ console.error('Failed to load user:', error);
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ ## Middleware Types
83
+
84
+ ### Request Middleware
85
+
86
+ Request middleware runs **before** the actual fetch call. It receives the `Request` object and can modify it before the request is sent.
87
+
88
+ **Signature:**
89
+ ```typescript
90
+ type RequestMiddleware = (
91
+ this: Context,
92
+ request: Request,
93
+ next: () => Promise<Response>
94
+ ) => Promise<Response>
95
+ ```
96
+
97
+ **Common use cases:**
98
+ - Adding authentication headers
99
+ - Modifying request URLs (e.g., adding base URL)
100
+ - Logging outgoing requests
101
+ - Adding custom headers (CSRF tokens, API keys, etc.)
102
+ - Request validation
103
+
104
+ **Example - Authentication:**
105
+ ```typescript
106
+ fetcher.use('request', function(request, next) {
107
+ const token = this.application.auth?.token;
108
+ if (token) {
109
+ request.headers.set('Authorization', `Bearer ${token}`);
110
+ }
111
+ return next();
112
+ });
113
+ ```
114
+
115
+ **Example - Base URL:**
116
+ ```typescript
117
+ fetcher.use('request', function(request, next) {
118
+ const url = new URL(request.url);
119
+ if (!url.hostname) {
120
+ // Relative URL, add base
121
+ const baseUrl = this.application.config?.apiBaseUrl || 'https://api.example.com';
122
+ const newRequest = new Request(`${baseUrl}${request.url}`, request);
123
+ return next(); // Note: modifying request in place doesn't work, would need to reconstruct
124
+ }
125
+ return next();
126
+ });
127
+ ```
128
+
129
+ **Example - Request Logging:**
130
+ ```typescript
131
+ fetcher.use('request', function(request, next) {
132
+ console.log(`[${this.navigation.route}] ${request.method} ${request.url}`);
133
+ return next();
134
+ });
135
+ ```
136
+
137
+ ### Response Middleware
138
+
139
+ Response middleware runs **after** the fetch call completes. It receives the `Response` object and can inspect or transform it.
140
+
141
+ **Signature:**
142
+ ```typescript
143
+ type ResponseMiddleware = (
144
+ this: Context,
145
+ response: Response,
146
+ next: () => Promise<Response>
147
+ ) => Promise<Response>
148
+ ```
149
+
150
+ **Common use cases:**
151
+ - Error handling based on status codes
152
+ - Response transformation
153
+ - Logging responses
154
+ - Caching
155
+ - Performance metrics
156
+ - Retry logic
157
+
158
+ **Example - Error Handling:**
159
+ ```typescript
160
+ fetcher.use('response', async function(response, next) {
161
+ if (!response.ok) {
162
+ const error = await response.text();
163
+ console.error(`[${this.navigation.route}] HTTP ${response.status}:`, error);
164
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
165
+ }
166
+ return next();
167
+ });
168
+ ```
169
+
170
+ **Example - Response Logging:**
171
+ ```typescript
172
+ fetcher.use('response', async function(response, next) {
173
+ console.log(`[${this.navigation.route}] Response ${response.status} from ${response.url}`);
174
+ return next();
175
+ });
176
+ ```
177
+
178
+ **Example - Performance Metrics:**
179
+ ```typescript
180
+ const timings = new WeakMap();
181
+
182
+ fetcher.use('request', function(request, next) {
183
+ timings.set(request, Date.now());
184
+ return next();
185
+ });
186
+
187
+ fetcher.use('response', async function(response, next) {
188
+ const request = timings.get(response); // Note: this won't work as response doesn't reference request
189
+ // Better approach: use a Map with URL as key
190
+ const duration = Date.now() - startTime;
191
+ console.log(`Request took ${duration}ms`);
192
+ return next();
193
+ });
194
+ ```
195
+
196
+ ## Accessing Context
197
+
198
+ Middleware functions have `this` bound to the `Context` instance, giving you access to:
199
+
200
+ - `this.application` - Application-wide state (user, config, theme, etc.)
201
+ - `this.navigation` - Navigation state (current route, route params, placards)
202
+ - `this.id` - Unique context instance ID
203
+
204
+ **Example - Context-Aware Error Handling:**
205
+ ```typescript
206
+ fetcher.use('response', async function(response, next) {
207
+ if (response.status === 401) {
208
+ // Unauthorized - clear user and redirect to login
209
+ this.application.user = null;
210
+ this.application.router?.navigate('/login');
211
+ throw new Error('Authentication required');
212
+ }
213
+
214
+ if (response.status === 403) {
215
+ // Forbidden - log and show error
216
+ console.error(`Access denied on route: ${this.navigation.route}`);
217
+ throw new Error('Access forbidden');
218
+ }
219
+
220
+ return next();
221
+ });
222
+ ```
223
+
224
+ ## Middleware Execution Order
225
+
226
+ Middleware executes in the order it's registered:
227
+
228
+ 1. Request middleware runs in registration order (first registered = first executed)
229
+ 2. Actual `fetch()` call happens
230
+ 3. Response middleware runs in registration order
231
+
232
+ **Example:**
233
+ ```typescript
234
+ const fetcher = new ContextAwareFetcher();
235
+
236
+ fetcher.use('request', function(request, next) {
237
+ console.log('Request middleware 1');
238
+ return next();
239
+ });
240
+
241
+ fetcher.use('request', function(request, next) {
242
+ console.log('Request middleware 2');
243
+ return next();
244
+ });
245
+
246
+ fetcher.use('response', async function(response, next) {
247
+ console.log('Response middleware 1');
248
+ return next();
249
+ });
250
+
251
+ fetcher.use('response', async function(response, next) {
252
+ console.log('Response middleware 2');
253
+ return next();
254
+ });
255
+
256
+ // Output when fetch is called:
257
+ // Request middleware 1
258
+ // Request middleware 2
259
+ // (actual fetch happens)
260
+ // Response middleware 1
261
+ // Response middleware 2
262
+ ```
263
+
264
+ ## Complete Example
265
+
266
+ Here's a complete example with authentication, error handling, and logging:
267
+
268
+ ```typescript
269
+ import { Router, ContextAwareFetcher } from 'snice';
270
+
271
+ interface AppContext {
272
+ user?: {
273
+ token: string;
274
+ id: string;
275
+ };
276
+ config: {
277
+ apiBaseUrl: string;
278
+ };
279
+ }
280
+
281
+ const fetcher = new ContextAwareFetcher();
282
+
283
+ // Add authentication token
284
+ fetcher.use('request', function(request, next) {
285
+ const token = this.application.user?.token;
286
+ if (token) {
287
+ request.headers.set('Authorization', `Bearer ${token}`);
288
+ }
289
+ return next();
290
+ });
291
+
292
+ // Log all requests
293
+ fetcher.use('request', function(request, next) {
294
+ const route = this.navigation.route;
295
+ console.log(`[${route}] ${request.method} ${request.url}`);
296
+ return next();
297
+ });
298
+
299
+ // Handle errors globally
300
+ fetcher.use('response', async function(response, next) {
301
+ if (response.status === 401) {
302
+ // Clear auth and redirect
303
+ this.application.user = undefined;
304
+ window.location.href = '/#/login';
305
+ throw new Error('Authentication required');
306
+ }
307
+
308
+ if (response.status >= 400) {
309
+ const errorText = await response.clone().text();
310
+ console.error(`HTTP ${response.status}:`, errorText);
311
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
312
+ }
313
+
314
+ return next();
315
+ });
316
+
317
+ // Log responses
318
+ fetcher.use('response', async function(response, next) {
319
+ console.log(`[${this.navigation.route}] Response ${response.status}`);
320
+ return next();
321
+ });
322
+
323
+ const router = Router({
324
+ target: '#app',
325
+ context: {
326
+ config: {
327
+ apiBaseUrl: 'https://api.example.com'
328
+ }
329
+ },
330
+ fetcher
331
+ });
332
+
333
+ router.initialize();
334
+ ```
335
+
336
+ ## Important Notes
337
+
338
+ ### Context is Long-Lived
339
+
340
+ The `Context` instance is created once per Router and persists for the entire application lifetime. This means:
341
+
342
+ - Middleware is configured once at application startup
343
+ - `ctx.fetch` is initialized once and reused
344
+ - Middleware can safely reference `this.application` and `this.navigation` as they update in place
345
+
346
+ ### Request Objects are Immutable
347
+
348
+ The `Request` object passed to middleware is immutable. To modify it, you need to create a new Request:
349
+
350
+ ```typescript
351
+ fetcher.use('request', function(request, next) {
352
+ // This won't work - headers are read-only on Request
353
+ // request.headers.set('X-Custom', 'value');
354
+
355
+ // Instead, create a new Request
356
+ const newRequest = new Request(request, {
357
+ headers: new Headers(request.headers)
358
+ });
359
+ newRequest.headers.set('X-Custom', 'value');
360
+
361
+ // But this is complex - better to set headers before creating Request
362
+ return next();
363
+ });
364
+ ```
365
+
366
+ In practice, it's better to set headers by modifying the `headers` property if it exists, or reconstructing the request entirely.
367
+
368
+ ### No Fetcher Means Native Fetch
369
+
370
+ If no `fetcher` is provided to the Router, `ctx.fetch` defaults to the native `fetch` function bound to the Context instance:
371
+
372
+ ```typescript
373
+ // No fetcher provided
374
+ const router = Router({
375
+ target: '#app',
376
+ context: {}
377
+ });
378
+
379
+ // In pages, ctx.fetch is just native fetch (with `this` bound to Context)
380
+ ```
381
+
382
+ ## API Reference
383
+
384
+ ### ContextAwareFetcher
385
+
386
+ **Constructor:**
387
+ ```typescript
388
+ new ContextAwareFetcher()
389
+ ```
390
+
391
+ **Methods:**
392
+
393
+ **`use(type: 'request', middleware: RequestMiddleware): void`**
394
+
395
+ Add request middleware that runs before the fetch call.
396
+
397
+ **`use(type: 'response', middleware: ResponseMiddleware): void`**
398
+
399
+ Add response middleware that runs after the fetch call.
400
+
401
+ **`create(ctx: Context): typeof globalThis.fetch`**
402
+
403
+ Create a fetch function bound to the given Context instance. This is called internally by the Router.
404
+
405
+ ### Type Definitions
406
+
407
+ ```typescript
408
+ type RequestMiddleware = (
409
+ this: Context,
410
+ request: Request,
411
+ next: () => Promise<Response>
412
+ ) => Promise<Response>
413
+
414
+ type ResponseMiddleware = (
415
+ this: Context,
416
+ response: Response,
417
+ next: () => Promise<Response>
418
+ ) => Promise<Response>
419
+
420
+ interface Fetcher {
421
+ use(type: 'request', middleware: RequestMiddleware): void;
422
+ use(type: 'response', middleware: ResponseMiddleware): void;
423
+ create(ctx: Context): typeof globalThis.fetch;
424
+ }
425
+ ```
426
+
427
+ ## Best Practices
428
+
429
+ 1. **Configure middleware at startup** - Don't add middleware inside pages or components, as this would add duplicate middleware on each navigation.
430
+
431
+ 2. **Keep middleware focused** - Each middleware should do one thing well (auth, logging, error handling, etc.).
432
+
433
+ 3. **Use `this.application` for app state** - Access user, config, and other app-wide state via the Context.
434
+
435
+ 4. **Clone responses if needed** - If you need to read the response body in middleware, clone it first as streams can only be read once:
436
+ ```typescript
437
+ fetcher.use('response', async function(response, next) {
438
+ const clone = response.clone();
439
+ const text = await clone.text();
440
+ console.log('Response body:', text);
441
+ return next(); // Original response still has readable body
442
+ });
443
+ ```
444
+
445
+ 5. **Handle errors gracefully** - Always consider what should happen when requests fail.
446
+
447
+ 6. **Call `next()`** - Every middleware must call and return `next()` to continue the chain.
package/docs/routing.md CHANGED
@@ -40,6 +40,7 @@ interface RouterOptions<T = any> {
40
40
  transition?: Transition; // Global transition config
41
41
  layout?: string; // Default layout for all pages
42
42
  context?: T; // Router context object (shared state)
43
+ fetcher?: Fetcher; // Optional fetch middleware (see docs/fetcher.md)
43
44
  }
44
45
  ```
45
46
 
@@ -130,6 +131,7 @@ class ProfilePage extends HTMLElement {
130
131
  // ctx.application is your router context (AppContext)
131
132
  this.appContext = ctx.application;
132
133
  // ctx.navigation contains { placards, route, params }
134
+ // ctx.fetch is available for HTTP requests (with middleware if configured)
133
135
  this.requestRender();
134
136
  }
135
137
 
@@ -205,13 +207,14 @@ class DashboardPage extends HTMLElement {
205
207
  The Context object passed to `@context()` methods has the following structure:
206
208
 
207
209
  ```typescript
208
- interface Context<T = any> {
209
- application: T; // Your router context (e.g., AppContext)
210
+ interface Context {
211
+ application: AppContext; // Your router context (e.g., { user, theme, config })
210
212
  navigation: {
211
213
  placards: Placard[]; // All page placards
212
214
  route: string; // Current route name
213
215
  params: Record<string, string>; // Route parameters
214
216
  };
217
+ fetch: typeof globalThis.fetch; // Fetch function with middleware support
215
218
  update(): void; // Notify all subscribers of changes
216
219
  }
217
220
  ```
@@ -221,10 +224,10 @@ interface Context<T = any> {
221
224
  ```typescript
222
225
  @page({ tag: 'user-page', routes: ['/users/:userId'] })
223
226
  class UserPage extends HTMLElement {
224
- private ctx?: Context<AppContext>;
227
+ private ctx?: Context;
225
228
 
226
229
  @context()
227
- handleContext(ctx: Context<AppContext>) {
230
+ handleContext(ctx: Context) {
228
231
  this.ctx = ctx;
229
232
 
230
233
  // Access application state
@@ -248,10 +251,10 @@ When you modify the application context, call `update()` to notify all subscribe
248
251
  ```typescript
249
252
  @page({ tag: 'settings-page', routes: ['/settings'] })
250
253
  class SettingsPage extends HTMLElement {
251
- private ctx?: Context<AppContext>;
254
+ private ctx?: Context;
252
255
 
253
256
  @context()
254
- handleContext(ctx: Context<AppContext>) {
257
+ handleContext(ctx: Context) {
255
258
  this.ctx = ctx;
256
259
  this.requestRender();
257
260
  }
@@ -1064,7 +1067,7 @@ class DashboardPage extends HTMLElement {
1064
1067
  private appContext?: AppContext;
1065
1068
 
1066
1069
  @context()
1067
- handleContext(ctx: Context<AppContext>) {
1070
+ handleContext(ctx: Context) {
1068
1071
  this.appContext = ctx.application;
1069
1072
  this.requestRender();
1070
1073
  }
@@ -1091,7 +1094,7 @@ class LoginPage extends HTMLElement {
1091
1094
  private appContext?: AppContext;
1092
1095
 
1093
1096
  @context()
1094
- handleContext(ctx: Context<AppContext>) {
1097
+ handleContext(ctx: Context) {
1095
1098
  this.appContext = ctx.application;
1096
1099
  }
1097
1100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snice",
3
- "version": "3.6.0",
3
+ "version": "3.8.0",
4
4
  "type": "module",
5
5
  "description": "Imperative TypeScript framework for building vanilla web components with decorators, differential rendering, routing, and controllers. No virtual DOM, no build complexity.",
6
6
  "main": "dist/index.cjs",
@@ -112,6 +112,7 @@
112
112
  "@vitest/ui": "^1.0.0",
113
113
  "clean-css": "^5.3.3",
114
114
  "happy-dom": "^12.0.0",
115
+ "qrcode": "^1.5.4",
115
116
  "rollup": "^4.50.2",
116
117
  "semantic-release": "^24.2.7",
117
118
  "typescript": "^5.9.2",