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.
- package/README.md +2 -2
- package/bin/snice.js +4 -5
- package/bin/templates/CLAUDE.md +25 -3
- package/bin/templates/pwa/README.md +188 -0
- package/bin/templates/pwa/global.d.ts +10 -0
- package/bin/templates/pwa/index.html +16 -0
- package/bin/templates/pwa/package.json +32 -0
- package/bin/templates/pwa/public/icons/.gitkeep +6 -0
- package/bin/templates/pwa/src/daemons/notifications.ts +148 -0
- package/bin/templates/pwa/src/fetcher.ts +15 -0
- package/bin/templates/pwa/src/guards/auth.ts +12 -0
- package/bin/templates/pwa/src/main.ts +42 -0
- package/bin/templates/pwa/src/middleware/auth.ts +16 -0
- package/bin/templates/pwa/src/middleware/error.ts +36 -0
- package/bin/templates/pwa/src/middleware/retry.ts +31 -0
- package/bin/templates/pwa/src/pages/dashboard.ts +143 -0
- package/bin/templates/pwa/src/pages/login.ts +161 -0
- package/bin/templates/pwa/src/pages/notifications.ts +156 -0
- package/bin/templates/pwa/src/pages/profile.ts +164 -0
- package/bin/templates/pwa/src/router.ts +20 -0
- package/bin/templates/pwa/src/services/auth.ts +48 -0
- package/bin/templates/pwa/src/services/jwt.ts +35 -0
- package/bin/templates/pwa/src/services/storage.ts +24 -0
- package/bin/templates/pwa/src/styles/global.css +55 -0
- package/bin/templates/pwa/src/types/auth.ts +21 -0
- package/bin/templates/pwa/src/types/notifications.ts +9 -0
- package/bin/templates/pwa/tests/helpers/test-utils.ts +84 -0
- package/bin/templates/pwa/tests/middleware/auth.test.ts +67 -0
- package/bin/templates/pwa/tests/middleware/error.test.ts +105 -0
- package/bin/templates/pwa/tests/middleware/retry.test.ts +103 -0
- package/bin/templates/pwa/tests/services/auth.test.ts +89 -0
- package/bin/templates/pwa/tests/services/jwt.test.ts +76 -0
- package/bin/templates/pwa/tests/services/storage.test.ts +69 -0
- package/bin/templates/{social → pwa}/tsconfig.json +11 -10
- package/bin/templates/pwa/vite.config.ts +94 -0
- package/bin/templates/{social/vite.config.ts → pwa/vitest.config.ts} +12 -17
- package/dist/components/music-player/snice-music-player.d.ts +72 -0
- package/dist/components/music-player/snice-music-player.js +730 -0
- package/dist/components/music-player/snice-music-player.js.map +1 -0
- package/dist/components/music-player/snice-music-player.types.d.ts +43 -0
- package/dist/components/timer/snice-timer.d.ts +27 -0
- package/dist/components/timer/snice-timer.js +197 -0
- package/dist/components/timer/snice-timer.js.map +1 -0
- package/dist/components/timer/snice-timer.types.d.ts +10 -0
- package/dist/fetcher.d.ts +65 -0
- package/dist/index.cjs +92 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +92 -4
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +92 -3
- package/dist/index.iife.js.map +1 -1
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/dist/types/context.d.ts +7 -1
- package/dist/types/router-options.d.ts +6 -0
- package/docs/ai/api.md +33 -1
- package/docs/ai/components/music-player.md +134 -0
- package/docs/ai/components/timer.md +43 -0
- package/docs/ai/patterns.md +48 -1
- package/docs/components/music-player.md +314 -0
- package/docs/components/timer.md +143 -0
- package/docs/fetcher.md +447 -0
- package/docs/routing.md +11 -8
- package/package.json +2 -1
- package/bin/templates/social/README.md +0 -42
- package/bin/templates/social/global.d.ts +0 -14
- package/bin/templates/social/index.html +0 -13
- package/bin/templates/social/package.json +0 -21
- package/bin/templates/social/src/main.ts +0 -33
- package/bin/templates/social/src/pages/feed-page.ts +0 -111
- package/bin/templates/social/src/pages/messages-page.ts +0 -102
- package/bin/templates/social/src/pages/not-found-page.ts +0 -46
- package/bin/templates/social/src/pages/profile-page.ts +0 -99
- package/bin/templates/social/src/pages/settings-page.ts +0 -119
- package/bin/templates/social/src/router.ts +0 -9
- package/bin/templates/social/src/styles/global.css +0 -156
- /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
|
package/docs/fetcher.md
ADDED
|
@@ -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
|
|
209
|
-
application:
|
|
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
|
|
227
|
+
private ctx?: Context;
|
|
225
228
|
|
|
226
229
|
@context()
|
|
227
|
-
handleContext(ctx: Context
|
|
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
|
|
254
|
+
private ctx?: Context;
|
|
252
255
|
|
|
253
256
|
@context()
|
|
254
|
-
handleContext(ctx: Context
|
|
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
|
|
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
|
|
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.
|
|
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",
|