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/ai/bindings.md
CHANGED
|
@@ -48,7 +48,7 @@ Ordinary attributes/comments support multiple expressions. Property, boolean, ev
|
|
|
48
48
|
- Direct JS assignment; preserves type and identity.
|
|
49
49
|
- null/undefined/false assigned unchanged; `nothing` assigns undefined; `noChange` skips.
|
|
50
50
|
- Same committed value skips write.
|
|
51
|
-
- `live(value)` compares value to the current DOM property instead of last commit; use only in property bindings when controlled native state must be reasserted after
|
|
51
|
+
- `live(value)` compares value to the current DOM property instead of last commit; use only in property bindings when controlled native or custom-element state must be reasserted after the user or element mutates it.
|
|
52
52
|
- `live()` does not observe DOM mutations or schedule a render. It reasserts only when the owning template renders. A modal open-state change can supply that render when reopening should restore an unchanged seed value.
|
|
53
53
|
|
|
54
54
|
```typescript
|
package/docs/ai/cli.md
CHANGED
|
@@ -52,10 +52,35 @@ Non-blocking architecture suggestions:
|
|
|
52
52
|
- `@element` -> `src/components/`
|
|
53
53
|
- `@controller` -> `src/controllers/`
|
|
54
54
|
- `@daemon` -> `src/daemons/`
|
|
55
|
-
-
|
|
55
|
+
- visual behavior -> element
|
|
56
|
+
- application behavior specific to a set of elements -> controller
|
|
57
|
+
- element orchestration -> page; do not attach a controller to the page host
|
|
58
|
+
- host-free reusable function -> plain module in the project's chosen location
|
|
56
59
|
|
|
57
60
|
Both `doctor` and `validate` accept `--json`.
|
|
58
61
|
|
|
62
|
+
## Diagnostic codes and `.sniceignore`
|
|
63
|
+
|
|
64
|
+
Every error, warning, and suggestion has a stable `code` in JSON and text
|
|
65
|
+
output. Create `.sniceignore` at the project root to suppress an accepted
|
|
66
|
+
diagnostic. Entries are exact; glob patterns are not used.
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
# One exact diagnostic (preferred)
|
|
70
|
+
snice/prefer-dispatch-decorator src/components/legacy-filter.ts:42:5
|
|
71
|
+
|
|
72
|
+
# Every instance in one file
|
|
73
|
+
snice/prefer-dispatch-decorator src/components/legacy-filter.ts
|
|
74
|
+
|
|
75
|
+
# The rule everywhere in the project
|
|
76
|
+
snice/prefer-dispatch-decorator
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`code path:line` is also accepted. Paths are project-relative. A code-only
|
|
80
|
+
entry suppresses the rule globally, including its effect on the command's exit
|
|
81
|
+
status; use it only when the project intentionally rejects that rule. The same
|
|
82
|
+
file applies to `check`, `doctor`, and `validate`.
|
|
83
|
+
|
|
59
84
|
## init-ai
|
|
60
85
|
|
|
61
86
|
```bash
|
|
@@ -19,6 +19,10 @@ showZero: boolean = false; // attr: show-zero — render 0 instead of h
|
|
|
19
19
|
offset: number = 0;
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
The indicator is hidden when there is no dot, non-empty `content`, positive
|
|
23
|
+
`count`, or `showZero`. Slotted content remains available as the element the
|
|
24
|
+
badge would overlay.
|
|
25
|
+
|
|
22
26
|
## Methods
|
|
23
27
|
|
|
24
28
|
- `setBadgeContent(content)` - Set text content
|
|
@@ -57,9 +57,11 @@ variant: 'default'|'rounded'|'text' = 'default';
|
|
|
57
57
|
|
|
58
58
|
```typescript
|
|
59
59
|
import 'snice/components/pagination/snice-pagination';
|
|
60
|
+
import type { PaginationChangeDetail } from 'snice/components/pagination/snice-pagination.types';
|
|
60
61
|
|
|
61
|
-
pagination.addEventListener('pagination-change', (
|
|
62
|
-
|
|
62
|
+
pagination.addEventListener('pagination-change', (event) => {
|
|
63
|
+
const { detail } = event as CustomEvent<PaginationChangeDetail>;
|
|
64
|
+
console.log('Page:', detail.page);
|
|
63
65
|
});
|
|
64
66
|
```
|
|
65
67
|
|
|
@@ -5,7 +5,7 @@ Multi-option switcher with sliding indicator. One selected at a time.
|
|
|
5
5
|
## Properties
|
|
6
6
|
|
|
7
7
|
```typescript
|
|
8
|
-
value: string = '';
|
|
8
|
+
value: string = ''; // user selection updates this before value-change dispatches
|
|
9
9
|
options: SegmentedControlOption[] = []; // JS only: { value, label, icon?, disabled? }
|
|
10
10
|
size: 'small'|'medium'|'large' = 'medium';
|
|
11
11
|
disabled: boolean = false;
|
|
@@ -15,6 +15,10 @@ disabled: boolean = false;
|
|
|
15
15
|
|
|
16
16
|
- `value-change` → `{ value: string, previousValue: string, option: SegmentedControlOption, control }`
|
|
17
17
|
|
|
18
|
+
`value` is self-mutating: selecting an option assigns it before the event is
|
|
19
|
+
dispatched. An owner that must reassert an unchanged value on a later render
|
|
20
|
+
should use a `.value=${live(value)}` property binding.
|
|
21
|
+
|
|
18
22
|
## CSS Parts
|
|
19
23
|
|
|
20
24
|
- `base` - Root container
|
package/docs/ai/controllers.md
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Mirrors `docs/controllers.md`.
|
|
4
4
|
|
|
5
|
-
Controllers
|
|
5
|
+
Controllers hold application behavior specific to a set of elements, including
|
|
6
|
+
their data fetching, business rules, and server communication. Attach to any
|
|
7
|
+
HTML element, including native elements.
|
|
8
|
+
|
|
9
|
+
Visual behavior belongs in elements, application behavior specific to a set of
|
|
10
|
+
elements belongs in a controller, and element orchestration belongs in pages.
|
|
11
|
+
Do not attach a controller to the page host. A host-free reusable function may
|
|
12
|
+
stay a plain module wherever the project keeps it. URL/query parsing belongs in
|
|
13
|
+
`@page({ routes })`, not in a controller.
|
|
6
14
|
|
|
7
15
|
## Basic Usage
|
|
8
16
|
|
|
@@ -67,18 +75,21 @@ el.controller = UserController; // snice elements
|
|
|
67
75
|
**Attachment flow:**
|
|
68
76
|
1. Controller instance created
|
|
69
77
|
2. `element` property set
|
|
70
|
-
3. Router context passed (if available)
|
|
78
|
+
3. Router application context passed (if available)
|
|
71
79
|
4. Element's `ready` promise awaited
|
|
72
80
|
5. `attach()` called
|
|
73
|
-
6.
|
|
74
|
-
7.
|
|
75
|
-
8.
|
|
76
|
-
9.
|
|
81
|
+
6. `@context` handlers registered and caught up with the current Router context
|
|
82
|
+
7. Observers set up
|
|
83
|
+
8. Channel/response handlers set up
|
|
84
|
+
9. Event handlers set up
|
|
85
|
+
10. `controller-attached` event dispatched
|
|
77
86
|
|
|
78
87
|
Exception to step 4: `await attachController(this, ControllerClass)` inside the
|
|
79
88
|
host's own `@ready` handler attaches immediately because initial render is
|
|
80
89
|
already complete. Awaiting that same host's `ready` would self-deadlock. An
|
|
81
90
|
attachment targeting any other element still waits for the target's `ready`.
|
|
91
|
+
This runtime safeguard does not make attaching a controller to a routed page a
|
|
92
|
+
good architecture; pages should orchestrate directly.
|
|
82
93
|
|
|
83
94
|
**Detachment flow:**
|
|
84
95
|
1. `detach()` called
|
|
@@ -86,8 +97,9 @@ attachment targeting any other element still waits for the target's `ready`.
|
|
|
86
97
|
3. Observers cleaned up
|
|
87
98
|
4. Channel/response handlers cleaned up
|
|
88
99
|
5. Event handlers cleaned up
|
|
89
|
-
6.
|
|
90
|
-
7.
|
|
100
|
+
6. `@context` handlers cleaned up
|
|
101
|
+
7. Controller scope cleaned up
|
|
102
|
+
8. `controller-detached` event dispatched
|
|
91
103
|
|
|
92
104
|
## Native Element Controllers
|
|
93
105
|
|
|
@@ -103,7 +115,7 @@ Controllers provide specific behaviors (fetch/sort/filter) to generic visual com
|
|
|
103
115
|
|
|
104
116
|
## Resource Cleanup
|
|
105
117
|
|
|
106
|
-
Framework auto-cleans `@on`, `@observe`, `@respond` handlers. Clean up your own resources (WebSockets, timers, manual listeners) in `detach`:
|
|
118
|
+
Framework auto-cleans `@on`, `@observe`, `@respond`, and `@context` handlers. Clean up your own resources (WebSockets, timers, manual listeners) in `detach`:
|
|
107
119
|
|
|
108
120
|
```typescript
|
|
109
121
|
@controller('resource-controller')
|
|
@@ -182,6 +194,8 @@ the element's reactive public API. Dispatch outcome events, never mutate the
|
|
|
182
194
|
element's rendered DOM.
|
|
183
195
|
|
|
184
196
|
```typescript
|
|
197
|
+
import { context, type Context } from 'snice';
|
|
198
|
+
|
|
185
199
|
interface DataHost<T> extends HTMLElement {
|
|
186
200
|
loading: boolean;
|
|
187
201
|
error: string;
|
|
@@ -192,17 +206,24 @@ interface DataHost<T> extends HTMLElement {
|
|
|
192
206
|
@controller('data-controller')
|
|
193
207
|
class DataController<T> implements IController<DataHost<T>> {
|
|
194
208
|
element: DataHost<T> | null = null;
|
|
209
|
+
private ctx?: Context;
|
|
195
210
|
private abort?: AbortController;
|
|
196
211
|
private version = 0;
|
|
197
212
|
|
|
198
|
-
|
|
213
|
+
attach(element: DataHost<T>) {
|
|
199
214
|
this.element = element;
|
|
200
|
-
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@context()
|
|
218
|
+
receiveContext(ctx: Context) {
|
|
219
|
+
this.ctx = ctx;
|
|
220
|
+
void this.reload();
|
|
201
221
|
}
|
|
202
222
|
|
|
203
223
|
async detach() {
|
|
204
224
|
this.version++;
|
|
205
225
|
this.abort?.abort();
|
|
226
|
+
this.ctx = undefined;
|
|
206
227
|
}
|
|
207
228
|
|
|
208
229
|
async reload() {
|
|
@@ -216,7 +237,9 @@ class DataController<T> implements IController<DataHost<T>> {
|
|
|
216
237
|
host.empty = false;
|
|
217
238
|
|
|
218
239
|
try {
|
|
219
|
-
const
|
|
240
|
+
const ctx = this.ctx;
|
|
241
|
+
if (!ctx) throw new Error('DataController requires Router context');
|
|
242
|
+
const response = await ctx.fetch('/api/data', { signal: abort.signal });
|
|
220
243
|
if (!response.ok) throw new Error(`Request failed (${response.status})`);
|
|
221
244
|
const data = await response.json() as T[];
|
|
222
245
|
if (version !== this.version || this.element !== host) return;
|
|
@@ -241,6 +264,12 @@ class DataController<T> implements IController<DataHost<T>> {
|
|
|
241
264
|
}
|
|
242
265
|
```
|
|
243
266
|
|
|
267
|
+
`@context()` works on controllers as well as elements. It receives the same
|
|
268
|
+
long-lived `Context` instance, including `application`, navigation state, and
|
|
269
|
+
the Router's middleware-aware `fetch`. Managed decorators are activated after
|
|
270
|
+
`attach()`, so start context-dependent work in the `@context()` handler (or in
|
|
271
|
+
an event handled later), not in `attach()`.
|
|
272
|
+
|
|
244
273
|
Required pieces: abort previous work, increment a version for stale-response
|
|
245
274
|
guarding, reset loading/error/empty before fetch, check version + host identity
|
|
246
275
|
before every commit, expose retry as `reload()`, and render all four states in
|
package/docs/ai/decorators.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Class
|
|
4
4
|
- `@element('tag-name', options?)` - Custom element. Options: `{ formAssociated?, renderRoot?: 'shadow'|'light', shadow?: 'open'|'closed'|false, delegatesFocus? }`
|
|
5
|
-
- `@page({ tag, routes, guards?, placard? })` - Routable page
|
|
5
|
+
- `@page({ tag, routes, guards?, placard? })` - Routable page. `routes` normally uses ordered strings; optional `{ path, order? }` entries explicitly break specificity ties.
|
|
6
6
|
- `@controller('name')` - Swappable behavior. Required on every controller class. Attach by class (preferred): `controller=${MyController}` in templates, `attachController(el, MyController)`, or `el.controller = MyController`; by string in raw HTML: `controller="name"`. A class attachment reflects this name in the DOM as a diagnostic marker without attaching through the registry.
|
|
7
7
|
- `@daemon` - Marks an explicitly constructed app-context daemon. No name argument; context key is address. Never constructs/registers globally.
|
|
8
8
|
- `@layout('tag-name')` - Page wrapper for routing system
|
|
@@ -66,8 +66,8 @@ Resolver re-runs on reconnect — listeners track DOM moves.
|
|
|
66
66
|
## Communication
|
|
67
67
|
- `@request(channel, options?)` - Async generator request pattern; `{ daemon: 'name' }` targets an app-context daemon
|
|
68
68
|
- `@respond(channel, options?)` - Handle requests from `@request`; `{ daemon: 'name' }` installs on that daemon target
|
|
69
|
-
- `@context(options?)` - Receive router navigation context updates
|
|
70
|
-
- `provideContext(root, context)` / `getContext(participant)` - Explicit
|
|
69
|
+
- `@context(options?)` - Receive router navigation context updates on pages, descendant elements, and attached controllers
|
|
70
|
+
- `provideContext(root, context, { fetch? })` / `getContext(participant)` / `getContextFetch(participant)` - Explicit app-context and transport provisioning/lookup
|
|
71
71
|
|
|
72
72
|
## Observers
|
|
73
73
|
- `@observe(target, selector?, options?)` - Watch intersection, resize, media query, mutation
|
package/docs/ai/events.md
CHANGED
|
@@ -272,7 +272,7 @@ updateStatus(status: string) {
|
|
|
272
272
|
|
|
273
273
|
```typescript
|
|
274
274
|
interface DispatchOptions extends EventInit {
|
|
275
|
-
dispatchOnUndefined?: boolean; //
|
|
275
|
+
dispatchOnUndefined?: boolean; // Undefined return still dispatches unless false (default: true)
|
|
276
276
|
debounce?: number; // Debounce dispatch by ms
|
|
277
277
|
throttle?: number; // Throttle dispatch by ms
|
|
278
278
|
// Where to dispatch the event (see scope below)
|
|
@@ -379,7 +379,13 @@ methods themselves default to their own private target. See
|
|
|
379
379
|
|
|
380
380
|
## Custom events
|
|
381
381
|
|
|
382
|
-
|
|
382
|
+
Prefer `@dispatch` for a static custom event emitted from a Snice element or a
|
|
383
|
+
controller's host. It supplies the standard bubbling/composed behavior, uses
|
|
384
|
+
the method return value as `detail`, and keeps event timing options declarative.
|
|
385
|
+
|
|
386
|
+
Manual dispatch remains a valid low-level escape hatch when code needs the
|
|
387
|
+
Event object, a dynamic event name, `dispatchEvent()`'s cancellation boolean,
|
|
388
|
+
or a non-host target:
|
|
383
389
|
|
|
384
390
|
```typescript
|
|
385
391
|
notify() {
|
package/docs/ai/fetcher.md
CHANGED
|
@@ -30,7 +30,13 @@ const router = Router({ target: '#app', context: { auth: null }, fetcher });
|
|
|
30
30
|
router.initialize();
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Pages, descendant elements, and attached controllers receive the Router's
|
|
34
|
+
long-lived `Context` through `@context()`; its `ctx.fetch` is the bound,
|
|
35
|
+
middleware-aware function. `getContextFetch(this)` remains the lower-level
|
|
36
|
+
lookup for code using an explicit non-router
|
|
37
|
+
`provideContext(root, appContext, { fetch })` boundary.
|
|
38
|
+
|
|
39
|
+
## `ctx.fetch` in pages, elements, and controllers
|
|
34
40
|
|
|
35
41
|
Available on any `Context` obtained via `@context()`; middleware applies automatically:
|
|
36
42
|
|
|
@@ -45,6 +51,31 @@ class UserPage extends HTMLElement {
|
|
|
45
51
|
}
|
|
46
52
|
```
|
|
47
53
|
|
|
54
|
+
Controllers use the same decorator. Managed decorators are activated after
|
|
55
|
+
`attach()`, so start context-dependent work from the handler:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
@controller('user-data')
|
|
59
|
+
class UserDataController implements IController<HTMLElement> {
|
|
60
|
+
element: HTMLElement | null = null;
|
|
61
|
+
private ctx?: Context;
|
|
62
|
+
|
|
63
|
+
attach(element: HTMLElement) { this.element = element; }
|
|
64
|
+
detach() { this.ctx = undefined; }
|
|
65
|
+
|
|
66
|
+
@context()
|
|
67
|
+
receiveContext(ctx: Context) {
|
|
68
|
+
this.ctx = ctx;
|
|
69
|
+
void this.load();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async load() {
|
|
73
|
+
const response = await this.ctx!.fetch('/api/users');
|
|
74
|
+
// Commit through the host's public API.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
48
79
|
## Middleware types
|
|
49
80
|
|
|
50
81
|
### Request middleware — runs before the `fetch()` call
|
package/docs/ai/lifecycle.md
CHANGED
|
@@ -120,7 +120,10 @@ Options object always comes after the property names — works with multiple wat
|
|
|
120
120
|
|
|
121
121
|
## @context()
|
|
122
122
|
|
|
123
|
-
Receives router context updates
|
|
123
|
+
Receives router context updates on pages, descendant elements, and attached
|
|
124
|
+
controllers. Called whenever the router context changes (navigation, app
|
|
125
|
+
context update, etc.). A controller's managed decorators activate after
|
|
126
|
+
`attach()`, so context-dependent startup belongs in this handler:
|
|
124
127
|
|
|
125
128
|
```typescript
|
|
126
129
|
@property({ type: Array }) placards: Placard[] = [];
|
package/docs/ai/patterns.md
CHANGED
|
@@ -170,17 +170,17 @@ import { page } from '../router'; // NOT from 'snice'!
|
|
|
170
170
|
@page({ tag: 'user-page', routes: ['/users/:id'], guards: [isAuthenticated] })
|
|
171
171
|
class UserPage extends HTMLElement {
|
|
172
172
|
@property() id = '';
|
|
173
|
-
private
|
|
173
|
+
private ctx?: Context;
|
|
174
174
|
|
|
175
175
|
@context()
|
|
176
176
|
handleContext(ctx: Context) {
|
|
177
|
-
this.
|
|
177
|
+
this.ctx = ctx;
|
|
178
178
|
this.requestRender();
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
@ready()
|
|
182
182
|
async load() {
|
|
183
|
-
const user = await fetch(`/api/users/${this.id}`).then(r => r.json());
|
|
183
|
+
const user = await this.ctx!.fetch(`/api/users/${this.id}`).then(r => r.json());
|
|
184
184
|
// ...
|
|
185
185
|
}
|
|
186
186
|
}
|
|
@@ -219,9 +219,14 @@ Mock controller for tests, real API controller in production — same element.
|
|
|
219
219
|
// Controller responds to requests (receives payload, returns result directly)
|
|
220
220
|
@controller('api')
|
|
221
221
|
class API {
|
|
222
|
+
private ctx!: Context;
|
|
223
|
+
|
|
224
|
+
@context()
|
|
225
|
+
receiveContext(ctx: Context) { this.ctx = ctx; }
|
|
226
|
+
|
|
222
227
|
@respond('fetch-user')
|
|
223
228
|
async handleFetchUser(payload: { id: string }) {
|
|
224
|
-
const user = await fetch(`/api/users/${payload.id}`).then(r => r.json());
|
|
229
|
+
const user = await this.ctx.fetch(`/api/users/${payload.id}`).then(r => r.json());
|
|
225
230
|
return user; // Direct return, not callback
|
|
226
231
|
}
|
|
227
232
|
}
|
|
@@ -33,6 +33,7 @@ Deep import: `import { useRequestHandler } from 'snice/react/useRequestHandler'`
|
|
|
33
33
|
## Route Props
|
|
34
34
|
|
|
35
35
|
- `path`: Route pattern (e.g., `/users/:id`)
|
|
36
|
+
- `order`: Optional numeric specificity tie-break; lower values match first. Equal/omitted values preserve declaration order.
|
|
36
37
|
- `page`: React component (receives params as props) OR Snice tag name string (params set as attributes)
|
|
37
38
|
- `guard`: `(ctx, params) => boolean | Promise<boolean>`
|
|
38
39
|
- `guards`: Array of guard functions (AND logic, sequential, short-circuit)
|
|
@@ -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 })
|
|
@@ -144,7 +151,10 @@ changeTheme(theme: 'light' | 'dark') {
|
|
|
144
151
|
```typescript
|
|
145
152
|
interface PageOptions {
|
|
146
153
|
tag: string; // custom element tag name
|
|
147
|
-
routes: string
|
|
154
|
+
routes: Array<string | { // strings are the normal form
|
|
155
|
+
path: string;
|
|
156
|
+
order?: number; // lower wins on a specificity tie
|
|
157
|
+
}>;
|
|
148
158
|
transition?: Transition; // page-specific transition
|
|
149
159
|
guards?: Guard | Guard[]; // route guards
|
|
150
160
|
layout?: string | false; // layout tag, or false to disable
|
|
@@ -154,6 +164,25 @@ interface PageOptions {
|
|
|
154
164
|
|
|
155
165
|
- Multiple routes: `routes: ['/user', '/users', '/profile']` — all resolve to the same page.
|
|
156
166
|
- Route params: `:name` segments inside a route pattern (see Route Parameters below).
|
|
167
|
+
- Routes are sorted by specificity first. When specificity ties, string entries
|
|
168
|
+
keep registration order, including their order in one `routes` array.
|
|
169
|
+
- Object notation is optional. Use `{ path, order }` only for an explicit tie-break
|
|
170
|
+
across registrations; lower `order` values match first. Equal or omitted values
|
|
171
|
+
still preserve registration order.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
@page({
|
|
175
|
+
tag: 'work-orders-page',
|
|
176
|
+
routes: ['/work-orders?status=:status', '/work-orders']
|
|
177
|
+
})
|
|
178
|
+
class WorkOrdersPage extends HTMLElement {}
|
|
179
|
+
|
|
180
|
+
@page({
|
|
181
|
+
tag: 'override-page',
|
|
182
|
+
routes: [{ path: '/:section/:item', order: -10 }]
|
|
183
|
+
})
|
|
184
|
+
class OverridePage extends HTMLElement {}
|
|
185
|
+
```
|
|
157
186
|
|
|
158
187
|
## Navigation
|
|
159
188
|
|
|
@@ -183,7 +212,7 @@ function Router(options: RouterOptions): {
|
|
|
183
212
|
page: (pageOptions: PageOptions) => ClassDecorator;
|
|
184
213
|
initialize: () => void;
|
|
185
214
|
navigate: (path: string) => Promise<void>;
|
|
186
|
-
register: (route: string, tag: string, transition?: Transition, guards?: Guard | Guard[]) => void;
|
|
215
|
+
register: (route: string, tag: string, transition?: Transition, guards?: Guard | Guard[], layout?: string | false, placard?: Placard | ((ctx: AppContext) => Placard), order?: number) => void;
|
|
187
216
|
}
|
|
188
217
|
```
|
|
189
218
|
|
|
@@ -191,4 +220,4 @@ function Router(options: RouterOptions): {
|
|
|
191
220
|
- `initialize(): void` — starts listening for route changes; must be called after all pages are defined.
|
|
192
221
|
- Do not add `@element` to an `@page` class. The Router-returned page decorator
|
|
193
222
|
already registers the custom element and applies Snice element behavior.
|
|
194
|
-
- `register(route
|
|
223
|
+
- `register(route, tag, transition?, guards?, layout?, placard?, order?): void` — manually registers a route without `@page`. Lower `order` values win only after specificity ties; omitted/equal order preserves registration order.
|
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
|
@@ -55,11 +55,36 @@ npx snice validate # source analyzer only
|
|
|
55
55
|
|
|
56
56
|
It also gives non-blocking architecture suggestions: keep `@page`, `@element`,
|
|
57
57
|
`@controller`, and `@daemon` classes under `src/pages`, `src/components`,
|
|
58
|
-
`src/controllers`, and `src/daemons`;
|
|
59
|
-
|
|
58
|
+
`src/controllers`, and `src/daemons`; keep visual behavior in elements,
|
|
59
|
+
application behavior specific to a set of elements in controllers, and element
|
|
60
|
+
orchestration in pages. A host-free reusable function may remain a plain module
|
|
61
|
+
wherever the project convention places it.
|
|
60
62
|
|
|
61
63
|
Both accept `--json` for CI.
|
|
62
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
|
+
|
|
63
88
|
## AI Setup
|
|
64
89
|
|
|
65
90
|
```bash
|