snice 7.3.0 → 7.4.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/.agents/skills/snice/SKILL.md +12 -16
- package/README.md +5 -1
- package/adapters/react/SniceProvider.js +1 -1
- package/adapters/react/SniceRouter.js +1 -1
- package/adapters/react/matchRoute.js +1 -1
- package/adapters/react/useRequestHandler.js +1 -1
- package/bin/analyzer-contracts.js +3 -1
- package/bin/project-analyzer.js +219 -6
- package/bin/snice.js +77 -13
- package/dist/app-context.d.ts +10 -10
- package/dist/cdn/accordion/snice-accordion.js +1 -1
- package/dist/cdn/action-bar/snice-action-bar.js +1 -1
- package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
- package/dist/cdn/alert/snice-alert.js +1 -1
- package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
- package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
- package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
- package/dist/cdn/availability/snice-availability.js +1 -1
- package/dist/cdn/avatar/snice-avatar.js +1 -1
- package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
- package/dist/cdn/badge/snice-badge.js +1 -1
- package/dist/cdn/banner/snice-banner.js +1 -1
- package/dist/cdn/binpack/snice-binpack.js +1 -1
- package/dist/cdn/book/snice-book.js +1 -1
- package/dist/cdn/booking/snice-booking.js +1 -1
- package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
- package/dist/cdn/button/snice-button.js +1 -1
- package/dist/cdn/calendar/snice-calendar.js +1 -1
- package/dist/cdn/camera/snice-camera.js +1 -1
- package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
- package/dist/cdn/candlestick/snice-candlestick.js +1 -1
- package/dist/cdn/card/snice-card.js +1 -1
- package/dist/cdn/carousel/snice-carousel.js +1 -1
- package/dist/cdn/cart/snice-cart.js +1 -1
- package/dist/cdn/chart/snice-chart.js +1 -1
- package/dist/cdn/chat/snice-chat.js +1 -1
- package/dist/cdn/checkbox/snice-checkbox.js +1 -1
- package/dist/cdn/chip/snice-chip.js +1 -1
- package/dist/cdn/code-block/snice-code-block.js +1 -1
- package/dist/cdn/color-display/snice-color-display.js +1 -1
- package/dist/cdn/color-picker/snice-color-picker.js +1 -1
- package/dist/cdn/command-palette/snice-command-palette.js +1 -1
- package/dist/cdn/comments/snice-comments.js +1 -1
- package/dist/cdn/countdown/snice-countdown.js +1 -1
- package/dist/cdn/cropper/snice-cropper.js +1 -1
- package/dist/cdn/data-card/snice-data-card.js +1 -1
- package/dist/cdn/date-picker/snice-date-picker.js +1 -1
- package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
- package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
- package/dist/cdn/diff/snice-diff.js +1 -1
- package/dist/cdn/divider/snice-divider.js +1 -1
- package/dist/cdn/doc/snice-doc.js +1 -1
- package/dist/cdn/draw/snice-draw.js +1 -1
- package/dist/cdn/drawer/snice-drawer.js +1 -1
- package/dist/cdn/empty-state/snice-empty-state.js +1 -1
- package/dist/cdn/estimate/snice-estimate.js +1 -1
- package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
- package/dist/cdn/file-upload/snice-file-upload.js +1 -1
- package/dist/cdn/flip-card/snice-flip-card.js +1 -1
- package/dist/cdn/flow/snice-flow.js +1 -1
- package/dist/cdn/form-layout/snice-form-layout.js +1 -1
- package/dist/cdn/funnel/snice-funnel.js +1 -1
- package/dist/cdn/gantt/snice-gantt.js +1 -1
- package/dist/cdn/gauge/snice-gauge.js +1 -1
- package/dist/cdn/grid/snice-grid.js +1 -1
- package/dist/cdn/heatmap/snice-heatmap.js +1 -1
- package/dist/cdn/image/snice-image.js +1 -1
- package/dist/cdn/input/snice-input.js +1 -1
- package/dist/cdn/invoice/snice-invoice.js +1 -1
- package/dist/cdn/kanban/snice-kanban.js +1 -1
- package/dist/cdn/key-value/snice-key-value.js +1 -1
- package/dist/cdn/kpi/snice-kpi.js +1 -1
- package/dist/cdn/layout/snice-layout.js +1 -1
- package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
- package/dist/cdn/link/snice-link.js +1 -1
- package/dist/cdn/link-preview/snice-link-preview.js +1 -1
- package/dist/cdn/list/snice-list.js +1 -1
- package/dist/cdn/location/snice-location.js +1 -1
- package/dist/cdn/login/snice-login.js +1 -1
- package/dist/cdn/map/snice-map.js +1 -1
- package/dist/cdn/markdown/snice-markdown.js +1 -1
- package/dist/cdn/masonry/snice-masonry.js +1 -1
- package/dist/cdn/menu/snice-menu.js +1 -1
- package/dist/cdn/message-strip/snice-message-strip.js +1 -1
- package/dist/cdn/modal/snice-modal.js +1 -1
- package/dist/cdn/music-player/snice-music-player.js +1 -1
- package/dist/cdn/nav/snice-nav.js +1 -1
- package/dist/cdn/network-graph/snice-network-graph.js +1 -1
- package/dist/cdn/notification-center/snice-notification-center.js +1 -1
- package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
- package/dist/cdn/org-chart/snice-org-chart.js +1 -1
- package/dist/cdn/pagination/snice-pagination.js +1 -1
- package/dist/cdn/paint/snice-paint.js +1 -1
- package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
- package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
- package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
- package/dist/cdn/popover/snice-popover.js +1 -1
- package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
- package/dist/cdn/product-card/snice-product-card.js +1 -1
- package/dist/cdn/progress/snice-progress.js +1 -1
- package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
- package/dist/cdn/qr-code/snice-qr-code.js +1 -1
- package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
- package/dist/cdn/radio/snice-radio.js +1 -1
- package/dist/cdn/range-slider/snice-range-slider.js +1 -1
- package/dist/cdn/rating/snice-rating.js +1 -1
- package/dist/cdn/receipt/snice-receipt.js +1 -1
- package/dist/cdn/recipe/snice-recipe.js +1 -1
- package/dist/cdn/runtime/README.md +2 -2
- package/dist/cdn/runtime/snice-runtime.esm.js +309 -188
- package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.js +309 -187
- package/dist/cdn/runtime/snice-runtime.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
- package/dist/cdn/sankey/snice-sankey.js +1 -1
- package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
- package/dist/cdn/select/snice-select.js +1 -1
- package/dist/cdn/skeleton/snice-skeleton.js +1 -1
- package/dist/cdn/slider/snice-slider.js +1 -1
- package/dist/cdn/sortable/snice-sortable.js +1 -1
- package/dist/cdn/sparkline/snice-sparkline.js +1 -1
- package/dist/cdn/spinner/snice-spinner.js +1 -1
- package/dist/cdn/split-button/snice-split-button.js +1 -1
- package/dist/cdn/split-pane/snice-split-pane.js +1 -1
- package/dist/cdn/spotlight/snice-spotlight.js +1 -1
- package/dist/cdn/stat-group/snice-stat-group.js +1 -1
- package/dist/cdn/step-input/snice-step-input.js +1 -1
- package/dist/cdn/stepper/snice-stepper.js +1 -1
- package/dist/cdn/switch/snice-switch.js +1 -1
- package/dist/cdn/table/snice-table.esm.js +1 -1
- package/dist/cdn/table/snice-table.js +1 -1
- package/dist/cdn/tabs/snice-tabs.js +1 -1
- package/dist/cdn/tag/snice-tag.js +1 -1
- package/dist/cdn/tag-input/snice-tag-input.js +1 -1
- package/dist/cdn/terminal/snice-terminal.js +1 -1
- package/dist/cdn/testimonial/snice-testimonial.js +1 -1
- package/dist/cdn/textarea/snice-textarea.js +1 -1
- package/dist/cdn/time-picker/snice-time-picker.js +1 -1
- package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
- package/dist/cdn/timeline/snice-timeline.js +1 -1
- package/dist/cdn/timer/snice-timer.js +1 -1
- package/dist/cdn/toast/snice-toast.js +1 -1
- package/dist/cdn/tooltip/snice-tooltip.js +1 -1
- package/dist/cdn/tree/snice-tree.js +1 -1
- package/dist/cdn/treemap/snice-treemap.js +1 -1
- package/dist/cdn/user-card/snice-user-card.js +1 -1
- package/dist/cdn/video-player/snice-video-player.js +1 -1
- package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
- package/dist/cdn/waterfall/snice-waterfall.js +1 -1
- package/dist/cdn/weather/snice-weather.js +1 -1
- package/dist/cdn/work-order/snice-work-order.js +1 -1
- package/dist/components/pagination/snice-pagination.types.d.ts +5 -0
- package/dist/context-provider.d.ts +11 -1
- package/dist/context.d.ts +5 -6
- package/dist/index.cjs +306 -184
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.esm.js +306 -185
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +306 -184
- package/dist/index.iife.js.map +1 -1
- package/dist/react/SniceProvider.js +1 -1
- package/dist/react/SniceRouter.js +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/matchRoute.js +1 -1
- package/dist/react/useRequestHandler.js +1 -1
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/testing-dom.cjs +48 -0
- package/dist/testing-dom.cjs.map +1 -0
- package/dist/testing-dom.d.ts +23 -0
- package/dist/testing-dom.esm.js +45 -0
- package/dist/testing-dom.esm.js.map +1 -0
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/dist/types/context.d.ts +8 -8
- package/dist/utils.d.ts +3 -1
- package/docs/ai/api.md +12 -1
- package/docs/ai/architecture.md +7 -3
- package/docs/ai/bindings.md +1 -1
- package/docs/ai/cli.md +22 -0
- package/docs/ai/components/badge.md +4 -0
- package/docs/ai/components/pagination.md +4 -2
- package/docs/ai/components/segmented-control.md +5 -1
- package/docs/ai/controllers.md +30 -11
- package/docs/ai/decorators.md +2 -2
- package/docs/ai/events.md +8 -2
- package/docs/ai/fetcher.md +32 -1
- package/docs/ai/lifecycle.md +4 -1
- package/docs/ai/patterns.md +9 -4
- package/docs/ai/request-response.md +20 -6
- package/docs/ai/routing.md +8 -1
- package/docs/ai/testing.md +71 -0
- package/docs/bindings.md +1 -1
- package/docs/cli.md +23 -0
- package/docs/components/badge.md +4 -0
- package/docs/components/pagination.md +8 -5
- package/docs/components/segmented-control.md +5 -1
- package/docs/controllers.md +30 -11
- package/docs/events.md +11 -2
- package/docs/fetcher.md +32 -1
- package/docs/lifecycle.md +5 -1
- package/docs/request-response.md +25 -7
- package/docs/routing.md +10 -1
- package/docs/testing.md +71 -0
- package/package.json +6 -1
|
@@ -57,17 +57,21 @@ returns a promise for `T`.
|
|
|
57
57
|
@controller('processing-controller')
|
|
58
58
|
class ProcessingController implements IController {
|
|
59
59
|
element: HTMLElement | null = null;
|
|
60
|
+
private ctx!: Context;
|
|
60
61
|
async attach() {}
|
|
61
62
|
async detach() {}
|
|
62
63
|
|
|
64
|
+
@context()
|
|
65
|
+
receiveContext(ctx: Context) { this.ctx = ctx; }
|
|
66
|
+
|
|
63
67
|
@respond('search', { debounce: 300 })
|
|
64
68
|
async handleSearch(query: { term: string }) {
|
|
65
|
-
return await fetch(`/api/search?q=${encodeURIComponent(query.term)}`).then(r => r.json());
|
|
69
|
+
return await this.ctx.fetch(`/api/search?q=${encodeURIComponent(query.term)}`).then(r => r.json());
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
@respond('analytics', { throttle: 1000 })
|
|
69
73
|
async handleAnalytics(event: any) {
|
|
70
|
-
return await fetch('/api/track', { method: 'POST', body: JSON.stringify(event) });
|
|
74
|
+
return await this.ctx.fetch('/api/track', { method: 'POST', body: JSON.stringify(event) });
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
```
|
|
@@ -117,17 +121,23 @@ How it works:
|
|
|
117
121
|
@controller('product-controller')
|
|
118
122
|
class ProductController implements IController {
|
|
119
123
|
element: HTMLElement | null = null;
|
|
124
|
+
private ctx!: Context;
|
|
120
125
|
async attach() {}
|
|
121
126
|
async detach() {}
|
|
122
127
|
|
|
128
|
+
@context()
|
|
129
|
+
receiveContext(ctx: Context) { this.ctx = ctx; }
|
|
130
|
+
|
|
123
131
|
@respond('fetch-product')
|
|
124
132
|
async handleFetchProduct(request: { id: string }) {
|
|
125
|
-
return await fetch(`/api/products/${request.id}`).then(r => r.json());
|
|
133
|
+
return await this.ctx.fetch(`/api/products/${request.id}`).then(r => r.json());
|
|
126
134
|
}
|
|
127
135
|
}
|
|
128
136
|
```
|
|
129
137
|
|
|
130
|
-
Elements
|
|
138
|
+
Elements stay visual and yield requests upward. A controller owns application
|
|
139
|
+
behavior specific to the elements it controls; the page still owns element
|
|
140
|
+
orchestration.
|
|
131
141
|
|
|
132
142
|
## Options
|
|
133
143
|
|
|
@@ -175,7 +185,7 @@ async *loadData(): Response<void> {
|
|
|
175
185
|
@respond('load-data')
|
|
176
186
|
async handleLoadData(request: { id: string }) {
|
|
177
187
|
if (!request.id) throw new Error('ID is required');
|
|
178
|
-
const response = await fetch(`/api/data/${request.id}`);
|
|
188
|
+
const response = await this.ctx.fetch(`/api/data/${request.id}`);
|
|
179
189
|
if (!response.ok) throw new Error(`API error: ${response.status}`);
|
|
180
190
|
return await response.json();
|
|
181
191
|
}
|
|
@@ -189,18 +199,22 @@ async handleLoadData(request: { id: string }) {
|
|
|
189
199
|
@controller('cached-controller')
|
|
190
200
|
class CachedController implements IController {
|
|
191
201
|
element: HTMLElement | null = null;
|
|
202
|
+
private ctx!: Context;
|
|
192
203
|
private cache = new Map<string, { data: any; timestamp: number }>();
|
|
193
204
|
private ttl = 60000; // 1 minute
|
|
194
205
|
async attach() {}
|
|
195
206
|
async detach() {}
|
|
196
207
|
|
|
208
|
+
@context()
|
|
209
|
+
receiveContext(ctx: Context) { this.ctx = ctx; }
|
|
210
|
+
|
|
197
211
|
@respond('fetch-cached')
|
|
198
212
|
async handleFetch(request: { key: string; forceRefresh?: boolean }) {
|
|
199
213
|
const cached = this.cache.get(request.key);
|
|
200
214
|
if (!request.forceRefresh && cached && Date.now() - cached.timestamp < this.ttl) {
|
|
201
215
|
return { data: cached.data, fromCache: true };
|
|
202
216
|
}
|
|
203
|
-
const data = await fetch(`/api/${request.key}`).then(r => r.json());
|
|
217
|
+
const data = await this.ctx.fetch(`/api/${request.key}`).then(r => r.json());
|
|
204
218
|
this.cache.set(request.key, { data, timestamp: Date.now() });
|
|
205
219
|
return { data, fromCache: false };
|
|
206
220
|
}
|
package/docs/ai/routing.md
CHANGED
|
@@ -58,6 +58,12 @@ Router({
|
|
|
58
58
|
});
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
The same boundary supplies the full Router `Context` to `@context()` methods on
|
|
62
|
+
descendant elements and attached controllers. They use `ctx.fetch` with the
|
|
63
|
+
configured request/response middleware without importing the router or adding
|
|
64
|
+
a reserved field to the application context. `getContextFetch()` is the
|
|
65
|
+
lower-level transport-only lookup for explicit non-router providers.
|
|
66
|
+
|
|
61
67
|
### Module structure (avoids circular imports)
|
|
62
68
|
|
|
63
69
|
```typescript
|
|
@@ -88,7 +94,8 @@ class HomePage extends HTMLElement {
|
|
|
88
94
|
|
|
89
95
|
### `@context()` — method decorator
|
|
90
96
|
|
|
91
|
-
Receives context updates from the router
|
|
97
|
+
Receives context updates from the router on pages, descendant elements, and
|
|
98
|
+
attached controllers; called on navigation and on `ctx.update()`.
|
|
92
99
|
|
|
93
100
|
```typescript
|
|
94
101
|
@context(options?: { debounce?: number; throttle?: number; once?: boolean })
|
package/docs/ai/testing.md
CHANGED
|
@@ -42,6 +42,27 @@ Gotcha: awaiting `ready` on an element that never connects hangs — append it t
|
|
|
42
42
|
- `ready` rejects with the first thrown/rejected `@ready()` failure; always await it.
|
|
43
43
|
- jsdom's partial `ElementInternals` is supported; form controls keep native-input/proxy fallback behavior when `setFormValue`/`setValidity` are absent.
|
|
44
44
|
|
|
45
|
+
## Partial DOM compatibility
|
|
46
|
+
|
|
47
|
+
In a simulated DOM, opt into Snice's standards compatibility layer from the
|
|
48
|
+
test setup file:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import 'snice/testing/dom';
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
It capability-tests the current DOM and fills only missing IDL behavior. For
|
|
55
|
+
example, jsdom and some happy-dom versions omit the browser's reflective
|
|
56
|
+
`HTMLElement.autofocus` property for generic custom-element hosts; the adapter
|
|
57
|
+
adds that property without replacing implementations already supplied by the
|
|
58
|
+
runner. Application tests can then use `element.autofocus = true` exactly as
|
|
59
|
+
browser code does.
|
|
60
|
+
|
|
61
|
+
This adapter does not simulate layout, paint, or real focus navigation. Keep a
|
|
62
|
+
browser-test lane for behavior that depends on those capabilities. For a DOM
|
|
63
|
+
created after module evaluation, call `installDOMTestingCompatibility(scope)`
|
|
64
|
+
from the same module explicitly.
|
|
65
|
+
|
|
45
66
|
## Reading the DOM
|
|
46
67
|
|
|
47
68
|
```typescript
|
|
@@ -80,6 +101,56 @@ await attachController(el, UserController);
|
|
|
80
101
|
|
|
81
102
|
To test an element in isolation, swap in a fixture controller that responds on the same channel the production controller uses; the element itself never changes. See request-response.md, controllers.md.
|
|
82
103
|
|
|
104
|
+
If the controller calls `getContext(this)`, provide its host (or an ancestor)
|
|
105
|
+
with the same application-context shape before attaching it. Release both
|
|
106
|
+
resources after the assertion:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { attachController, detachController, provideContext } from 'snice';
|
|
110
|
+
|
|
111
|
+
const host = document.createElement('user-list');
|
|
112
|
+
document.body.append(host);
|
|
113
|
+
const releaseContext = provideContext(host, {
|
|
114
|
+
api: { listUsers: async () => [{ id: 'u1' }] }
|
|
115
|
+
}, {
|
|
116
|
+
fetch: async () => new Response(JSON.stringify([{ id: 'u1' }]))
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await attachController(host, UserController);
|
|
121
|
+
// assert controller effects
|
|
122
|
+
} finally {
|
|
123
|
+
await detachController(host);
|
|
124
|
+
releaseContext();
|
|
125
|
+
host.remove();
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Assigning a value to the host's own `@context` field is not a provider;
|
|
130
|
+
`getContext()` resolves the nearest `provideContext()` boundary.
|
|
131
|
+
When production code uses `getContextFetch()`, pass the test fetch function in
|
|
132
|
+
the same provider's third argument as shown above; `Router` supplies its
|
|
133
|
+
middleware-bound fetch function there in an application.
|
|
134
|
+
|
|
135
|
+
If the controller uses `@context()`, test it beneath a real `Router` target.
|
|
136
|
+
`provideContext()` intentionally provides application state and transport, not
|
|
137
|
+
navigation notifications; using `Router` exercises the same registration,
|
|
138
|
+
initial catch-up, middleware-aware `ctx.fetch`, updates, and detach cleanup as
|
|
139
|
+
production:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const router = Router({ target: '#fixture', context: { accountId: 'a1' }, fetcher });
|
|
143
|
+
|
|
144
|
+
@router.page({ tag: 'fixture-page', routes: ['/fixture'] })
|
|
145
|
+
class FixturePage extends HTMLElement {}
|
|
146
|
+
|
|
147
|
+
router.initialize();
|
|
148
|
+
await router.navigate('/fixture');
|
|
149
|
+
const host = document.createElement('user-list');
|
|
150
|
+
document.querySelector('fixture-page')!.append(host);
|
|
151
|
+
await attachController(host, UserController);
|
|
152
|
+
```
|
|
153
|
+
|
|
83
154
|
## Validating source
|
|
84
155
|
|
|
85
156
|
```bash
|
package/docs/bindings.md
CHANGED
|
@@ -128,7 +128,7 @@ Property bindings preserve type and identity. They are the correct channel for a
|
|
|
128
128
|
|
|
129
129
|
### Live property values
|
|
130
130
|
|
|
131
|
-
Native controls can mutate their own properties after Snice writes them. Normally, if the bound value has not changed, a later render leaves that user-edited DOM value alone. Wrap a property value in `live()` when the
|
|
131
|
+
Native controls and stateful custom elements can mutate their own properties after Snice writes them. Normally, if the bound value has not changed, a later render leaves that user- or element-edited DOM value alone. Wrap a property value in `live()` when the owner state must be reasserted even if the bound value itself is unchanged:
|
|
132
132
|
|
|
133
133
|
```typescript
|
|
134
134
|
html`<input .value=${live(this.canonicalValue)}>`;
|
package/docs/cli.md
CHANGED
|
@@ -62,6 +62,29 @@ wherever the project convention places it.
|
|
|
62
62
|
|
|
63
63
|
Both accept `--json` for CI.
|
|
64
64
|
|
|
65
|
+
### Diagnostic codes and `.sniceignore`
|
|
66
|
+
|
|
67
|
+
Every error, warning, and suggestion has a stable code in human and JSON
|
|
68
|
+
output. When a project intentionally accepts a finding, create `.sniceignore`
|
|
69
|
+
in the project root. Prefer the narrowest entry:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
# One exact diagnostic
|
|
73
|
+
snice/prefer-dispatch-decorator src/components/legacy-filter.ts:42:5
|
|
74
|
+
|
|
75
|
+
# Every instance in one file
|
|
76
|
+
snice/prefer-dispatch-decorator src/components/legacy-filter.ts
|
|
77
|
+
|
|
78
|
+
# Every instance in the project
|
|
79
|
+
snice/prefer-dispatch-decorator
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`code path:line` is also accepted. Paths are project-relative and entries are
|
|
83
|
+
exact rather than glob patterns. Suppressed diagnostics are omitted from both
|
|
84
|
+
text and JSON output and do not affect the exit status. The file applies to
|
|
85
|
+
`check`, `doctor`, and `validate`, including doctor codes such as
|
|
86
|
+
`snice-skill`.
|
|
87
|
+
|
|
65
88
|
## AI Setup
|
|
66
89
|
|
|
67
90
|
```bash
|
package/docs/components/badge.md
CHANGED
|
@@ -31,6 +31,10 @@ The badge component displays notification indicators, status markers, and counts
|
|
|
31
31
|
| `showZero` (attr: `show-zero`) | `boolean` | `false` | Render a zero count instead of hiding the badge |
|
|
32
32
|
| `offset` | `number` | `0` | Offset in pixels from default position |
|
|
33
33
|
|
|
34
|
+
The indicator is hidden when there is no dot, non-empty `content`, positive
|
|
35
|
+
`count`, or `showZero`. Slotted content remains available as the element the
|
|
36
|
+
badge would overlay.
|
|
37
|
+
|
|
34
38
|
## Methods
|
|
35
39
|
|
|
36
40
|
| Method | Arguments | Description |
|
|
@@ -81,16 +81,19 @@ snice-pagination::part(button) {
|
|
|
81
81
|
|
|
82
82
|
```typescript
|
|
83
83
|
import 'snice/components/pagination/snice-pagination';
|
|
84
|
+
import type { PaginationChangeDetail } from 'snice/components/pagination/snice-pagination.types';
|
|
84
85
|
```
|
|
85
86
|
|
|
86
87
|
```html
|
|
87
88
|
<snice-pagination current="1" total="10"></snice-pagination>
|
|
89
|
+
```
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
91
|
+
```typescript
|
|
92
|
+
const pagination = document.querySelector('snice-pagination')!;
|
|
93
|
+
pagination.addEventListener('pagination-change', (event) => {
|
|
94
|
+
const { detail } = event as CustomEvent<PaginationChangeDetail>;
|
|
95
|
+
console.log('Page:', detail.page, 'Previous:', detail.previousPage);
|
|
96
|
+
});
|
|
94
97
|
```
|
|
95
98
|
|
|
96
99
|
## Examples
|
|
@@ -17,7 +17,7 @@ A multi-option switcher with a sliding indicator. One option is selected at a ti
|
|
|
17
17
|
|
|
18
18
|
| Property | Type | Default | Description |
|
|
19
19
|
|----------|------|---------|-------------|
|
|
20
|
-
| `value` | `string` | `''` | Selected option value |
|
|
20
|
+
| `value` | `string` | `''` | Selected option value; user selection updates it before `value-change` |
|
|
21
21
|
| `options` | `SegmentedControlOption[]` | `[]` | Array of `{ value, label, icon?, disabled? }` (JS only) |
|
|
22
22
|
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Control size |
|
|
23
23
|
| `disabled` | `boolean` | `false` | Disables the entire control |
|
|
@@ -39,6 +39,10 @@ interface SegmentedControlOption {
|
|
|
39
39
|
|-------|--------|-------------|
|
|
40
40
|
| `value-change` | `{ value: string, previousValue: string, option: SegmentedControlOption, control }` | Selection changed |
|
|
41
41
|
|
|
42
|
+
`value` is self-mutating. If an owning template must reassert the same
|
|
43
|
+
controlled value after user selection, bind it as `.value=${live(value)}` so
|
|
44
|
+
the next owner render compares against the control's current property.
|
|
45
|
+
|
|
42
46
|
## CSS Parts
|
|
43
47
|
|
|
44
48
|
| Part | Description |
|
package/docs/controllers.md
CHANGED
|
@@ -119,13 +119,14 @@ interface IController<T extends HTMLElement = HTMLElement> {
|
|
|
119
119
|
|
|
120
120
|
1. Controller instance is created
|
|
121
121
|
2. `element` property is set
|
|
122
|
-
3. Router context is passed (if available)
|
|
122
|
+
3. Router application context is passed (if available)
|
|
123
123
|
4. Element's `ready` promise is awaited
|
|
124
124
|
5. `attach()` method is called
|
|
125
|
-
6.
|
|
126
|
-
7.
|
|
127
|
-
8.
|
|
128
|
-
9.
|
|
125
|
+
6. `@context` handlers are registered and caught up with the current Router context
|
|
126
|
+
7. Observers are set up
|
|
127
|
+
8. Channel/response handlers are set up
|
|
128
|
+
9. Event handlers are set up
|
|
129
|
+
10. `controller-attached` event is dispatched
|
|
129
130
|
|
|
130
131
|
The step-4 wait has one safe exception: when an element calls
|
|
131
132
|
`await attachController(this, ControllerClass)` from its own `@ready` handler,
|
|
@@ -143,8 +144,9 @@ good architecture; pages should orchestrate directly.
|
|
|
143
144
|
3. Observers are cleaned up
|
|
144
145
|
4. Channel/response handlers are cleaned up
|
|
145
146
|
5. Event handlers are cleaned up
|
|
146
|
-
6.
|
|
147
|
-
7.
|
|
147
|
+
6. `@context` handlers are cleaned up
|
|
148
|
+
7. Controller scope is cleaned up
|
|
149
|
+
8. `controller-detached` event is dispatched
|
|
148
150
|
|
|
149
151
|
### Example with Lifecycle Logging
|
|
150
152
|
|
|
@@ -240,7 +242,7 @@ class TableController implements IController<HTMLTableElement> {
|
|
|
240
242
|
|
|
241
243
|
## Resource Cleanup
|
|
242
244
|
|
|
243
|
-
The framework auto-cleans `@on`, `@observe`, and `@
|
|
245
|
+
The framework auto-cleans `@on`, `@observe`, `@respond`, and `@context` handlers. Clean up your own resources (WebSockets, timers, manual listeners) in `detach`:
|
|
244
246
|
|
|
245
247
|
```typescript
|
|
246
248
|
import { controller, IController } from 'snice';
|
|
@@ -372,6 +374,8 @@ production controller should also prevent an older response from overwriting a
|
|
|
372
374
|
newer one:
|
|
373
375
|
|
|
374
376
|
```typescript
|
|
377
|
+
import { context, type Context } from 'snice';
|
|
378
|
+
|
|
375
379
|
interface Order { id: string; total: number }
|
|
376
380
|
|
|
377
381
|
interface OrdersView extends HTMLElement {
|
|
@@ -384,12 +388,18 @@ interface OrdersView extends HTMLElement {
|
|
|
384
388
|
@controller('orders-data')
|
|
385
389
|
export class OrdersDataController implements IController<OrdersView> {
|
|
386
390
|
element: OrdersView | null = null;
|
|
391
|
+
private ctx?: Context;
|
|
387
392
|
private abortController?: AbortController;
|
|
388
393
|
private requestVersion = 0;
|
|
389
394
|
|
|
390
|
-
|
|
395
|
+
attach(element: OrdersView) {
|
|
391
396
|
this.element = element;
|
|
392
|
-
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
@context()
|
|
400
|
+
receiveContext(ctx: Context) {
|
|
401
|
+
this.ctx = ctx;
|
|
402
|
+
void this.reload();
|
|
393
403
|
}
|
|
394
404
|
|
|
395
405
|
async detach() {
|
|
@@ -397,6 +407,7 @@ export class OrdersDataController implements IController<OrdersView> {
|
|
|
397
407
|
this.requestVersion++;
|
|
398
408
|
this.abortController?.abort();
|
|
399
409
|
this.abortController = undefined;
|
|
410
|
+
this.ctx = undefined;
|
|
400
411
|
}
|
|
401
412
|
|
|
402
413
|
async reload() {
|
|
@@ -413,7 +424,9 @@ export class OrdersDataController implements IController<OrdersView> {
|
|
|
413
424
|
host.empty = false;
|
|
414
425
|
|
|
415
426
|
try {
|
|
416
|
-
const
|
|
427
|
+
const ctx = this.ctx;
|
|
428
|
+
if (!ctx) throw new Error('OrdersDataController requires Router context');
|
|
429
|
+
const response = await ctx.fetch('/api/orders', {
|
|
417
430
|
signal: abortController.signal
|
|
418
431
|
});
|
|
419
432
|
if (!response.ok) throw new Error(`Orders request failed (${response.status})`);
|
|
@@ -451,6 +464,12 @@ export class OrdersDataController implements IController<OrdersView> {
|
|
|
451
464
|
}
|
|
452
465
|
```
|
|
453
466
|
|
|
467
|
+
`@context()` works on controllers as well as elements. It receives the same
|
|
468
|
+
long-lived `Context` instance, including `application`, navigation state, and
|
|
469
|
+
the Router's middleware-aware `fetch`. Managed decorators are activated after
|
|
470
|
+
`attach()`, so start context-dependent work in the `@context()` handler (or in
|
|
471
|
+
an event handled later), not in `attach()`.
|
|
472
|
+
|
|
454
473
|
The element owns presentation for every state:
|
|
455
474
|
|
|
456
475
|
```typescript
|
package/docs/events.md
CHANGED
|
@@ -610,7 +610,7 @@ class StatusIndicator extends HTMLElement {
|
|
|
610
610
|
|
|
611
611
|
```typescript
|
|
612
612
|
interface DispatchOptions extends EventInit {
|
|
613
|
-
dispatchOnUndefined?: boolean; //
|
|
613
|
+
dispatchOnUndefined?: boolean; // Undefined return still dispatches unless false (default: true)
|
|
614
614
|
debounce?: number; // Debounce dispatch by ms
|
|
615
615
|
throttle?: number; // Throttle dispatch by ms
|
|
616
616
|
// Where to dispatch the event (see scope section below)
|
|
@@ -807,7 +807,16 @@ See [Daemons](./daemons.md).
|
|
|
807
807
|
|
|
808
808
|
## Custom Events
|
|
809
809
|
|
|
810
|
-
|
|
810
|
+
Prefer `@dispatch` for a static custom event emitted from a Snice element or a
|
|
811
|
+
controller's host. The decorator supplies the standard bubbling and composed
|
|
812
|
+
behavior, uses the method return value as `detail`, and keeps debounce,
|
|
813
|
+
throttle, and scope declarative.
|
|
814
|
+
|
|
815
|
+
Manual dispatch remains valid when code needs direct access to the Event
|
|
816
|
+
object, a dynamic event name, the cancellation boolean returned by
|
|
817
|
+
`dispatchEvent()`, or a target other than the Snice host.
|
|
818
|
+
|
|
819
|
+
### Manual Escape Hatch
|
|
811
820
|
|
|
812
821
|
```typescript
|
|
813
822
|
@element('manual-dispatcher')
|
package/docs/fetcher.md
CHANGED
|
@@ -51,7 +51,13 @@ const router = Router({
|
|
|
51
51
|
router.initialize();
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
Pages, descendant elements, and attached controllers receive the Router's
|
|
55
|
+
long-lived `Context` through `@context()`; its `ctx.fetch` is the bound,
|
|
56
|
+
middleware-aware function. `getContextFetch(this)` remains the lower-level
|
|
57
|
+
lookup for code using an explicit non-router
|
|
58
|
+
`provideContext(root, appContext, { fetch })` boundary.
|
|
59
|
+
|
|
60
|
+
## Using Context.fetch in Pages, Elements, and Controllers
|
|
55
61
|
|
|
56
62
|
Once configured, `ctx.fetch` is available in all pages and components that use the `@context` decorator:
|
|
57
63
|
|
|
@@ -80,6 +86,31 @@ class UserPage extends HTMLElement {
|
|
|
80
86
|
}
|
|
81
87
|
```
|
|
82
88
|
|
|
89
|
+
Controllers use the same decorator. Managed decorators are activated after
|
|
90
|
+
`attach()`, so start context-dependent work from the handler:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
@controller('user-data')
|
|
94
|
+
class UserDataController implements IController<HTMLElement> {
|
|
95
|
+
element: HTMLElement | null = null;
|
|
96
|
+
private ctx?: Context;
|
|
97
|
+
|
|
98
|
+
attach(element: HTMLElement) { this.element = element; }
|
|
99
|
+
detach() { this.ctx = undefined; }
|
|
100
|
+
|
|
101
|
+
@context()
|
|
102
|
+
receiveContext(ctx: Context) {
|
|
103
|
+
this.ctx = ctx;
|
|
104
|
+
void this.load();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async load() {
|
|
108
|
+
const response = await this.ctx!.fetch('/api/users');
|
|
109
|
+
// Commit through the host's public API.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
83
114
|
## Middleware Types
|
|
84
115
|
|
|
85
116
|
### Request Middleware
|
package/docs/lifecycle.md
CHANGED
|
@@ -198,7 +198,11 @@ The options object always comes after the property names, so it works with multi
|
|
|
198
198
|
|
|
199
199
|
## @context() Decorator
|
|
200
200
|
|
|
201
|
-
Receive router context updates
|
|
201
|
+
Receive router context updates on pages, descendant elements, and attached
|
|
202
|
+
controllers. The decorated method is called whenever the router context
|
|
203
|
+
changes (navigation, app context update, etc.). A controller's managed
|
|
204
|
+
decorators activate after `attach()`, so context-dependent startup belongs in
|
|
205
|
+
this handler:
|
|
202
206
|
|
|
203
207
|
```typescript
|
|
204
208
|
import { element, context, property, render, html } from 'snice';
|
package/docs/request-response.md
CHANGED
|
@@ -79,17 +79,21 @@ Response handlers can be debounced or throttled:
|
|
|
79
79
|
@controller('processing-controller')
|
|
80
80
|
class ProcessingController implements IController {
|
|
81
81
|
element: HTMLElement | null = null;
|
|
82
|
+
private ctx!: Context;
|
|
82
83
|
async attach() {}
|
|
83
84
|
async detach() {}
|
|
84
85
|
|
|
86
|
+
@context()
|
|
87
|
+
receiveContext(ctx: Context) { this.ctx = ctx; }
|
|
88
|
+
|
|
85
89
|
@respond('search', { debounce: 300 })
|
|
86
90
|
async handleSearch(query: { term: string }) {
|
|
87
|
-
return await fetch(`/api/search?q=${encodeURIComponent(query.term)}`).then(r => r.json());
|
|
91
|
+
return await this.ctx.fetch(`/api/search?q=${encodeURIComponent(query.term)}`).then(r => r.json());
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
@respond('analytics', { throttle: 1000 })
|
|
91
95
|
async handleAnalytics(event: any) {
|
|
92
|
-
return await fetch('/api/track', { method: 'POST', body: JSON.stringify(event) });
|
|
96
|
+
return await this.ctx.fetch('/api/track', { method: 'POST', body: JSON.stringify(event) });
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
```
|
|
@@ -139,23 +143,29 @@ class ProductCard extends HTMLElement {
|
|
|
139
143
|
Controllers handle requests — this is where business logic, API calls, and data management belong:
|
|
140
144
|
|
|
141
145
|
```typescript
|
|
142
|
-
import { controller, respond, IController } from 'snice';
|
|
146
|
+
import { context, controller, respond, IController, type Context } from 'snice';
|
|
143
147
|
|
|
144
148
|
@controller('product-controller')
|
|
145
149
|
class ProductController implements IController {
|
|
146
150
|
element: HTMLElement | null = null;
|
|
151
|
+
private ctx!: Context;
|
|
147
152
|
async attach() {}
|
|
148
153
|
async detach() {}
|
|
149
154
|
|
|
155
|
+
@context()
|
|
156
|
+
receiveContext(ctx: Context) { this.ctx = ctx; }
|
|
157
|
+
|
|
150
158
|
@respond('fetch-product')
|
|
151
159
|
async handleFetchProduct(request: { id: string }) {
|
|
152
|
-
const response = await fetch(`/api/products/${request.id}`);
|
|
160
|
+
const response = await this.ctx.fetch(`/api/products/${request.id}`);
|
|
153
161
|
return await response.json();
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
```
|
|
157
165
|
|
|
158
|
-
**Architecture:** Elements
|
|
166
|
+
**Architecture:** Elements stay visual and yield requests upward. A controller
|
|
167
|
+
owns application behavior specific to the elements it controls; the page still
|
|
168
|
+
owns element orchestration.
|
|
159
169
|
|
|
160
170
|
## Request/Response Options
|
|
161
171
|
|
|
@@ -238,16 +248,20 @@ class SafeLoader extends HTMLElement {
|
|
|
238
248
|
@controller('resilient-controller')
|
|
239
249
|
class ResilientController implements IController {
|
|
240
250
|
element: HTMLElement | null = null;
|
|
251
|
+
private ctx!: Context;
|
|
241
252
|
async attach() {}
|
|
242
253
|
async detach() {}
|
|
243
254
|
|
|
255
|
+
@context()
|
|
256
|
+
receiveContext(ctx: Context) { this.ctx = ctx; }
|
|
257
|
+
|
|
244
258
|
@respond('load-data')
|
|
245
259
|
async handleLoadData(request: { id: string }) {
|
|
246
260
|
if (!request.id) {
|
|
247
261
|
throw new Error('ID is required');
|
|
248
262
|
}
|
|
249
263
|
|
|
250
|
-
const response = await fetch(`/api/data/${request.id}`);
|
|
264
|
+
const response = await this.ctx.fetch(`/api/data/${request.id}`);
|
|
251
265
|
if (!response.ok) {
|
|
252
266
|
throw new Error(`API error: ${response.status}`);
|
|
253
267
|
}
|
|
@@ -265,12 +279,16 @@ class ResilientController implements IController {
|
|
|
265
279
|
@controller('cached-controller')
|
|
266
280
|
class CachedController implements IController {
|
|
267
281
|
element: HTMLElement | null = null;
|
|
282
|
+
private ctx!: Context;
|
|
268
283
|
private cache = new Map<string, { data: any; timestamp: number }>();
|
|
269
284
|
private ttl = 60000; // 1 minute
|
|
270
285
|
|
|
271
286
|
async attach() {}
|
|
272
287
|
async detach() {}
|
|
273
288
|
|
|
289
|
+
@context()
|
|
290
|
+
receiveContext(ctx: Context) { this.ctx = ctx; }
|
|
291
|
+
|
|
274
292
|
@respond('fetch-cached')
|
|
275
293
|
async handleFetch(request: { key: string; forceRefresh?: boolean }) {
|
|
276
294
|
const cached = this.cache.get(request.key);
|
|
@@ -278,7 +296,7 @@ class CachedController implements IController {
|
|
|
278
296
|
return { data: cached.data, fromCache: true };
|
|
279
297
|
}
|
|
280
298
|
|
|
281
|
-
const data = await fetch(`/api/${request.key}`).then(r => r.json());
|
|
299
|
+
const data = await this.ctx.fetch(`/api/${request.key}`).then(r => r.json());
|
|
282
300
|
this.cache.set(request.key, { data, timestamp: Date.now() });
|
|
283
301
|
return { data, fromCache: false };
|
|
284
302
|
}
|
package/docs/routing.md
CHANGED
|
@@ -88,6 +88,12 @@ without Router. Descendant elements and attached controllers address the instanc
|
|
|
88
88
|
as `{ daemon: 'session' }`; they do not import `SessionDaemon`. See
|
|
89
89
|
[Daemons](./daemons.md).
|
|
90
90
|
|
|
91
|
+
That provider boundary supplies the full Router `Context` to `@context()`
|
|
92
|
+
methods on descendant elements and attached controllers. They use `ctx.fetch`
|
|
93
|
+
with the configured request/response middleware without importing the router
|
|
94
|
+
or adding a reserved field to the application context. `getContextFetch()` is
|
|
95
|
+
the lower-level transport-only lookup for explicit non-router providers.
|
|
96
|
+
|
|
91
97
|
## Page Components
|
|
92
98
|
|
|
93
99
|
### Basic Page
|
|
@@ -135,7 +141,10 @@ class HomePage extends HTMLElement {
|
|
|
135
141
|
|
|
136
142
|
### Page with Context
|
|
137
143
|
|
|
138
|
-
The `@context()` decorator is a **method decorator** that receives context
|
|
144
|
+
The `@context()` decorator is a **method decorator** that receives context
|
|
145
|
+
updates from the router on pages, descendant elements, and attached
|
|
146
|
+
controllers. The method is called whenever navigation occurs, with a Context
|
|
147
|
+
object containing application state and navigation data.
|
|
139
148
|
|
|
140
149
|
```typescript
|
|
141
150
|
import { context, render, html, Context } from 'snice';
|