snice 7.2.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 +36 -13
- package/README.md +11 -6
- package/adapters/react/SniceProvider.js +1 -1
- package/adapters/react/SniceRouter.d.ts +2 -0
- package/adapters/react/SniceRouter.js +3 -2
- package/adapters/react/SniceRouter.js.map +1 -1
- package/adapters/react/matchRoute.d.ts +4 -1
- package/adapters/react/matchRoute.js +13 -3
- package/adapters/react/matchRoute.js.map +1 -1
- package/adapters/react/useRequestHandler.js +1 -1
- package/bin/analyzer-contracts.js +4 -1
- package/bin/project-analyzer.js +521 -20
- package/bin/snice.js +77 -13
- package/bin/templates/AI_GUIDANCE.md +6 -1
- package/bin/templates/default/README.md +1 -1
- package/bin/templates/default/src/components/app-header.ts +11 -16
- package/bin/templates/default/src/components/notification-badge.ts +5 -14
- package/bin/templates/default/src/components/search-bar.ts +21 -65
- package/bin/templates/default/src/pages/dashboard.ts +15 -11
- package/bin/templates/default/src/pages/data.ts +10 -13
- package/bin/templates/default/src/pages/notifications.ts +4 -9
- package/bin/templates/default/src/pages/profile.ts +2 -8
- package/bin/templates/default/src/pages/settings.ts +16 -16
- 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 +333 -192
- 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 +333 -191
- 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 +330 -188
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.esm.js +330 -189
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +330 -188
- package/dist/index.iife.js.map +1 -1
- package/dist/react/SniceProvider.js +1 -1
- package/dist/react/SniceRouter.d.ts +2 -0
- package/dist/react/SniceRouter.js +3 -2
- package/dist/react/SniceRouter.js.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/matchRoute.d.ts +4 -1
- package/dist/react/matchRoute.js +13 -3
- package/dist/react/matchRoute.js.map +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/types/page-options.d.ts +15 -1
- package/dist/types/router-instance.d.ts +2 -1
- package/dist/utils.d.ts +3 -1
- package/docs/ai/README.md +6 -4
- package/docs/ai/api.md +13 -2
- package/docs/ai/architecture.md +28 -11
- package/docs/ai/bindings.md +1 -1
- package/docs/ai/cli.md +26 -1
- 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 +41 -12
- package/docs/ai/decorators.md +3 -3
- 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/react-integration.md +1 -0
- package/docs/ai/request-response.md +20 -6
- package/docs/ai/routing.md +33 -4
- package/docs/ai/testing.md +71 -0
- package/docs/bindings.md +1 -1
- package/docs/cli.md +27 -2
- 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 +46 -15
- package/docs/events.md +11 -2
- package/docs/fetcher.md +32 -1
- package/docs/lifecycle.md +5 -1
- package/docs/react-integration.md +1 -0
- package/docs/request-response.md +25 -7
- package/docs/routing.md +39 -3
- package/docs/testing.md +71 -0
- package/package.json +6 -1
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
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
<!-- AI: For the AI-optimized version of this doc, see docs/ai/api.md -->
|
|
2
2
|
# Controllers API Documentation
|
|
3
3
|
|
|
4
|
-
Controllers
|
|
4
|
+
Controllers hold application behavior specific to a set of elements, including
|
|
5
|
+
their data fetching, business rules, and server communication. They can be
|
|
6
|
+
attached to any HTML element, including native elements.
|
|
7
|
+
|
|
8
|
+
Visual behavior belongs in elements, application behavior specific to a set of
|
|
9
|
+
elements belongs in a controller, and element orchestration belongs in pages.
|
|
10
|
+
Do not attach a controller to the page host. A host-free reusable function may
|
|
11
|
+
stay a plain module wherever the project keeps it. URL/query parsing belongs in
|
|
12
|
+
`@page({ routes })`, not in a controller.
|
|
5
13
|
|
|
6
14
|
## Table of Contents
|
|
7
15
|
- [Basic Usage](#basic-usage)
|
|
@@ -111,13 +119,14 @@ interface IController<T extends HTMLElement = HTMLElement> {
|
|
|
111
119
|
|
|
112
120
|
1. Controller instance is created
|
|
113
121
|
2. `element` property is set
|
|
114
|
-
3. Router context is passed (if available)
|
|
122
|
+
3. Router application context is passed (if available)
|
|
115
123
|
4. Element's `ready` promise is awaited
|
|
116
124
|
5. `attach()` method is called
|
|
117
|
-
6.
|
|
118
|
-
7.
|
|
119
|
-
8.
|
|
120
|
-
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
|
|
121
130
|
|
|
122
131
|
The step-4 wait has one safe exception: when an element calls
|
|
123
132
|
`await attachController(this, ControllerClass)` from its own `@ready` handler,
|
|
@@ -125,6 +134,8 @@ Snice attaches immediately. Initial rendering has already completed at that
|
|
|
125
134
|
point, and waiting for `ready` would otherwise create a self-deadlock because
|
|
126
135
|
`ready` cannot settle until the current handler returns. Attaching to any
|
|
127
136
|
other element still awaits that element's `ready` promise.
|
|
137
|
+
This runtime safeguard does not make attaching a controller to a routed page a
|
|
138
|
+
good architecture; pages should orchestrate directly.
|
|
128
139
|
|
|
129
140
|
### Detachment Flow
|
|
130
141
|
|
|
@@ -133,8 +144,9 @@ other element still awaits that element's `ready` promise.
|
|
|
133
144
|
3. Observers are cleaned up
|
|
134
145
|
4. Channel/response handlers are cleaned up
|
|
135
146
|
5. Event handlers are cleaned up
|
|
136
|
-
6.
|
|
137
|
-
7.
|
|
147
|
+
6. `@context` handlers are cleaned up
|
|
148
|
+
7. Controller scope is cleaned up
|
|
149
|
+
8. `controller-detached` event is dispatched
|
|
138
150
|
|
|
139
151
|
### Example with Lifecycle Logging
|
|
140
152
|
|
|
@@ -230,7 +242,7 @@ class TableController implements IController<HTMLTableElement> {
|
|
|
230
242
|
|
|
231
243
|
## Resource Cleanup
|
|
232
244
|
|
|
233
|
-
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`:
|
|
234
246
|
|
|
235
247
|
```typescript
|
|
236
248
|
import { controller, IController } from 'snice';
|
|
@@ -355,11 +367,15 @@ class FormValidationController implements IController<HTMLFormElement> {
|
|
|
355
367
|
|
|
356
368
|
### Data Fetching Controller
|
|
357
369
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
370
|
+
A data-fetching controller is appropriate when the fetch is application behavior
|
|
371
|
+
specific to the elements it controls. Pass state through the element's public
|
|
372
|
+
API and dispatch outcome events — do not manipulate its rendered DOM. A
|
|
373
|
+
production controller should also prevent an older response from overwriting a
|
|
374
|
+
newer one:
|
|
361
375
|
|
|
362
376
|
```typescript
|
|
377
|
+
import { context, type Context } from 'snice';
|
|
378
|
+
|
|
363
379
|
interface Order { id: string; total: number }
|
|
364
380
|
|
|
365
381
|
interface OrdersView extends HTMLElement {
|
|
@@ -372,12 +388,18 @@ interface OrdersView extends HTMLElement {
|
|
|
372
388
|
@controller('orders-data')
|
|
373
389
|
export class OrdersDataController implements IController<OrdersView> {
|
|
374
390
|
element: OrdersView | null = null;
|
|
391
|
+
private ctx?: Context;
|
|
375
392
|
private abortController?: AbortController;
|
|
376
393
|
private requestVersion = 0;
|
|
377
394
|
|
|
378
|
-
|
|
395
|
+
attach(element: OrdersView) {
|
|
379
396
|
this.element = element;
|
|
380
|
-
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
@context()
|
|
400
|
+
receiveContext(ctx: Context) {
|
|
401
|
+
this.ctx = ctx;
|
|
402
|
+
void this.reload();
|
|
381
403
|
}
|
|
382
404
|
|
|
383
405
|
async detach() {
|
|
@@ -385,6 +407,7 @@ export class OrdersDataController implements IController<OrdersView> {
|
|
|
385
407
|
this.requestVersion++;
|
|
386
408
|
this.abortController?.abort();
|
|
387
409
|
this.abortController = undefined;
|
|
410
|
+
this.ctx = undefined;
|
|
388
411
|
}
|
|
389
412
|
|
|
390
413
|
async reload() {
|
|
@@ -401,7 +424,9 @@ export class OrdersDataController implements IController<OrdersView> {
|
|
|
401
424
|
host.empty = false;
|
|
402
425
|
|
|
403
426
|
try {
|
|
404
|
-
const
|
|
427
|
+
const ctx = this.ctx;
|
|
428
|
+
if (!ctx) throw new Error('OrdersDataController requires Router context');
|
|
429
|
+
const response = await ctx.fetch('/api/orders', {
|
|
405
430
|
signal: abortController.signal
|
|
406
431
|
});
|
|
407
432
|
if (!response.ok) throw new Error(`Orders request failed (${response.status})`);
|
|
@@ -439,6 +464,12 @@ export class OrdersDataController implements IController<OrdersView> {
|
|
|
439
464
|
}
|
|
440
465
|
```
|
|
441
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
|
+
|
|
442
473
|
The element owns presentation for every state:
|
|
443
474
|
|
|
444
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';
|
|
@@ -147,6 +147,7 @@ Defines a route within `<SniceRouter>`. The `<Route>` component itself renders n
|
|
|
147
147
|
| Prop | Type | Description |
|
|
148
148
|
|------|------|-------------|
|
|
149
149
|
| `path` | `string` | URL pattern. Supports dynamic segments: `/users/:id`, `/posts/:slug`. |
|
|
150
|
+
| `order` | `number` | Optional specificity tie-break. Lower values match first; equal or omitted values preserve declaration order. |
|
|
150
151
|
| `page` | `Component \| string` | What to render. React component receives route params as props. String = Snice web component tag name (params set as attributes). |
|
|
151
152
|
| `guard` | `(ctx, params) => boolean \| Promise<boolean>` | Single guard function. |
|
|
152
153
|
| `guards` | `function[]` | Multiple guards — all must pass (AND logic). |
|
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';
|
|
@@ -294,7 +303,10 @@ class SettingsPage extends HTMLElement {
|
|
|
294
303
|
```typescript
|
|
295
304
|
interface PageOptions {
|
|
296
305
|
tag: string; // Custom element tag name
|
|
297
|
-
routes: string
|
|
306
|
+
routes: Array<string | { // Strings are the normal form
|
|
307
|
+
path: string;
|
|
308
|
+
order?: number; // Lower wins on a specificity tie
|
|
309
|
+
}>;
|
|
298
310
|
transition?: Transition; // Page-specific transition
|
|
299
311
|
guards?: Guard | Guard[]; // Route guards
|
|
300
312
|
layout?: string | false; // Layout tag, or false to disable
|
|
@@ -317,6 +329,30 @@ class UserPage extends HTMLElement {
|
|
|
317
329
|
}
|
|
318
330
|
```
|
|
319
331
|
|
|
332
|
+
Routes are sorted by specificity first. If specificity ties, registration
|
|
333
|
+
order wins, including the order of plain strings in one `routes` array. Keep
|
|
334
|
+
that compact syntax for normal pages:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
@page({
|
|
338
|
+
tag: 'work-orders-page',
|
|
339
|
+
routes: ['/work-orders?status=:status', '/work-orders']
|
|
340
|
+
})
|
|
341
|
+
class WorkOrdersPage extends HTMLElement {}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Object notation is optional. Use it only when a route needs an explicit
|
|
345
|
+
tie-break across registrations; lower `order` values match first. Equal or
|
|
346
|
+
omitted values still preserve registration order.
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
@page({
|
|
350
|
+
tag: 'override-page',
|
|
351
|
+
routes: [{ path: '/:section/:item', order: -10 }]
|
|
352
|
+
})
|
|
353
|
+
class OverridePage extends HTMLElement {}
|
|
354
|
+
```
|
|
355
|
+
|
|
320
356
|
### Route with Parameters
|
|
321
357
|
|
|
322
358
|
```typescript
|
|
@@ -815,7 +851,7 @@ function Router(options: RouterOptions): {
|
|
|
815
851
|
page: (pageOptions: PageOptions) => ClassDecorator;
|
|
816
852
|
initialize: () => void;
|
|
817
853
|
navigate: (path: string) => Promise<void>;
|
|
818
|
-
register: (route: string, tag: string, transition?: Transition, guards?: Guard | Guard[]) => void;
|
|
854
|
+
register: (route: string, tag: string, transition?: Transition, guards?: Guard | Guard[], layout?: string | false, placard?: Placard | ((ctx: AppContext) => Placard), order?: number) => void;
|
|
819
855
|
}
|
|
820
856
|
```
|
|
821
857
|
|
package/docs/testing.md
CHANGED
|
@@ -47,6 +47,27 @@ element. Snice form controls also retain their native-input/proxy fallback in
|
|
|
47
47
|
DOM runners such as jsdom that expose only the ARIA subset of
|
|
48
48
|
`ElementInternals`.
|
|
49
49
|
|
|
50
|
+
## Partial DOM Compatibility
|
|
51
|
+
|
|
52
|
+
In a simulated DOM, opt into Snice's standards compatibility layer from your
|
|
53
|
+
test setup file:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import 'snice/testing/dom';
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The module capability-tests the current DOM and fills only missing IDL
|
|
60
|
+
behavior. For example, jsdom and some happy-dom versions omit the browser's
|
|
61
|
+
reflective `HTMLElement.autofocus` property for generic custom-element hosts;
|
|
62
|
+
the adapter adds that property without replacing an implementation already
|
|
63
|
+
provided by the runner. Application tests can then use
|
|
64
|
+
`element.autofocus = true` exactly as browser code does.
|
|
65
|
+
|
|
66
|
+
The adapter does not simulate layout, paint, or real focus navigation. Keep a
|
|
67
|
+
browser-test lane for behavior that depends on those capabilities. If a runner
|
|
68
|
+
creates its DOM after module evaluation, import and call
|
|
69
|
+
`installDOMTestingCompatibility(scope)` explicitly.
|
|
70
|
+
|
|
50
71
|
## Reading the DOM
|
|
51
72
|
|
|
52
73
|
Use the element's render root directly:
|
|
@@ -87,6 +108,56 @@ await attachController(el, UserController);
|
|
|
87
108
|
|
|
88
109
|
Swapping a controller is the supported way to test an element in isolation: attach a fixture controller that responds on the same channel the production controller uses, and the element itself never changes. See [Request / Response](./request-response.md) and [Controllers](./controllers.md).
|
|
89
110
|
|
|
111
|
+
When a controller calls `getContext(this)`, install the same application
|
|
112
|
+
context on its host (or an ancestor) before attachment, then release it during
|
|
113
|
+
cleanup:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { attachController, detachController, provideContext } from 'snice';
|
|
117
|
+
|
|
118
|
+
const host = document.createElement('user-list');
|
|
119
|
+
document.body.append(host);
|
|
120
|
+
const releaseContext = provideContext(host, {
|
|
121
|
+
api: { listUsers: async () => [{ id: 'u1' }] }
|
|
122
|
+
}, {
|
|
123
|
+
fetch: async () => new Response(JSON.stringify([{ id: 'u1' }]))
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await attachController(host, UserController);
|
|
128
|
+
// assert controller effects
|
|
129
|
+
} finally {
|
|
130
|
+
await detachController(host);
|
|
131
|
+
releaseContext();
|
|
132
|
+
host.remove();
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Assigning the host's own `@context` field does not create a provider;
|
|
137
|
+
`getContext()` resolves the nearest `provideContext()` boundary.
|
|
138
|
+
When production code uses `getContextFetch()`, pass the test fetch function in
|
|
139
|
+
the same provider's third argument as shown above. `Router` supplies its
|
|
140
|
+
middleware-bound fetch function there in an application.
|
|
141
|
+
|
|
142
|
+
If the controller uses `@context()`, test it beneath a real `Router` target.
|
|
143
|
+
`provideContext()` intentionally provides application state and transport, not
|
|
144
|
+
navigation notifications; using `Router` exercises the same registration,
|
|
145
|
+
initial catch-up, middleware-aware `ctx.fetch`, updates, and detach cleanup as
|
|
146
|
+
production:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const router = Router({ target: '#fixture', context: { accountId: 'a1' }, fetcher });
|
|
150
|
+
|
|
151
|
+
@router.page({ tag: 'fixture-page', routes: ['/fixture'] })
|
|
152
|
+
class FixturePage extends HTMLElement {}
|
|
153
|
+
|
|
154
|
+
router.initialize();
|
|
155
|
+
await router.navigate('/fixture');
|
|
156
|
+
const host = document.createElement('user-list');
|
|
157
|
+
document.querySelector('fixture-page')!.append(host);
|
|
158
|
+
await attachController(host, UserController);
|
|
159
|
+
```
|
|
160
|
+
|
|
90
161
|
## Validating Source
|
|
91
162
|
|
|
92
163
|
Beyond unit tests, the analyzer catches Snice-specific mistakes that still compile:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snice",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
"import": "./dist/transitions.esm.js",
|
|
25
25
|
"require": "./dist/transitions.cjs"
|
|
26
26
|
},
|
|
27
|
+
"./testing/dom": {
|
|
28
|
+
"types": "./dist/testing-dom.d.ts",
|
|
29
|
+
"import": "./dist/testing-dom.esm.js",
|
|
30
|
+
"require": "./dist/testing-dom.cjs"
|
|
31
|
+
},
|
|
27
32
|
"./react": {
|
|
28
33
|
"types": "./adapters/react/index.d.ts",
|
|
29
34
|
"import": "./adapters/react/index.js"
|