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.
Files changed (232) hide show
  1. package/.agents/skills/snice/SKILL.md +36 -13
  2. package/README.md +11 -6
  3. package/adapters/react/SniceProvider.js +1 -1
  4. package/adapters/react/SniceRouter.d.ts +2 -0
  5. package/adapters/react/SniceRouter.js +3 -2
  6. package/adapters/react/SniceRouter.js.map +1 -1
  7. package/adapters/react/matchRoute.d.ts +4 -1
  8. package/adapters/react/matchRoute.js +13 -3
  9. package/adapters/react/matchRoute.js.map +1 -1
  10. package/adapters/react/useRequestHandler.js +1 -1
  11. package/bin/analyzer-contracts.js +4 -1
  12. package/bin/project-analyzer.js +521 -20
  13. package/bin/snice.js +77 -13
  14. package/bin/templates/AI_GUIDANCE.md +6 -1
  15. package/bin/templates/default/README.md +1 -1
  16. package/bin/templates/default/src/components/app-header.ts +11 -16
  17. package/bin/templates/default/src/components/notification-badge.ts +5 -14
  18. package/bin/templates/default/src/components/search-bar.ts +21 -65
  19. package/bin/templates/default/src/pages/dashboard.ts +15 -11
  20. package/bin/templates/default/src/pages/data.ts +10 -13
  21. package/bin/templates/default/src/pages/notifications.ts +4 -9
  22. package/bin/templates/default/src/pages/profile.ts +2 -8
  23. package/bin/templates/default/src/pages/settings.ts +16 -16
  24. package/dist/app-context.d.ts +10 -10
  25. package/dist/cdn/accordion/snice-accordion.js +1 -1
  26. package/dist/cdn/action-bar/snice-action-bar.js +1 -1
  27. package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
  28. package/dist/cdn/alert/snice-alert.js +1 -1
  29. package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
  30. package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
  31. package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
  32. package/dist/cdn/availability/snice-availability.js +1 -1
  33. package/dist/cdn/avatar/snice-avatar.js +1 -1
  34. package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
  35. package/dist/cdn/badge/snice-badge.js +1 -1
  36. package/dist/cdn/banner/snice-banner.js +1 -1
  37. package/dist/cdn/binpack/snice-binpack.js +1 -1
  38. package/dist/cdn/book/snice-book.js +1 -1
  39. package/dist/cdn/booking/snice-booking.js +1 -1
  40. package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
  41. package/dist/cdn/button/snice-button.js +1 -1
  42. package/dist/cdn/calendar/snice-calendar.js +1 -1
  43. package/dist/cdn/camera/snice-camera.js +1 -1
  44. package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
  45. package/dist/cdn/candlestick/snice-candlestick.js +1 -1
  46. package/dist/cdn/card/snice-card.js +1 -1
  47. package/dist/cdn/carousel/snice-carousel.js +1 -1
  48. package/dist/cdn/cart/snice-cart.js +1 -1
  49. package/dist/cdn/chart/snice-chart.js +1 -1
  50. package/dist/cdn/chat/snice-chat.js +1 -1
  51. package/dist/cdn/checkbox/snice-checkbox.js +1 -1
  52. package/dist/cdn/chip/snice-chip.js +1 -1
  53. package/dist/cdn/code-block/snice-code-block.js +1 -1
  54. package/dist/cdn/color-display/snice-color-display.js +1 -1
  55. package/dist/cdn/color-picker/snice-color-picker.js +1 -1
  56. package/dist/cdn/command-palette/snice-command-palette.js +1 -1
  57. package/dist/cdn/comments/snice-comments.js +1 -1
  58. package/dist/cdn/countdown/snice-countdown.js +1 -1
  59. package/dist/cdn/cropper/snice-cropper.js +1 -1
  60. package/dist/cdn/data-card/snice-data-card.js +1 -1
  61. package/dist/cdn/date-picker/snice-date-picker.js +1 -1
  62. package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
  63. package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
  64. package/dist/cdn/diff/snice-diff.js +1 -1
  65. package/dist/cdn/divider/snice-divider.js +1 -1
  66. package/dist/cdn/doc/snice-doc.js +1 -1
  67. package/dist/cdn/draw/snice-draw.js +1 -1
  68. package/dist/cdn/drawer/snice-drawer.js +1 -1
  69. package/dist/cdn/empty-state/snice-empty-state.js +1 -1
  70. package/dist/cdn/estimate/snice-estimate.js +1 -1
  71. package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
  72. package/dist/cdn/file-upload/snice-file-upload.js +1 -1
  73. package/dist/cdn/flip-card/snice-flip-card.js +1 -1
  74. package/dist/cdn/flow/snice-flow.js +1 -1
  75. package/dist/cdn/form-layout/snice-form-layout.js +1 -1
  76. package/dist/cdn/funnel/snice-funnel.js +1 -1
  77. package/dist/cdn/gantt/snice-gantt.js +1 -1
  78. package/dist/cdn/gauge/snice-gauge.js +1 -1
  79. package/dist/cdn/grid/snice-grid.js +1 -1
  80. package/dist/cdn/heatmap/snice-heatmap.js +1 -1
  81. package/dist/cdn/image/snice-image.js +1 -1
  82. package/dist/cdn/input/snice-input.js +1 -1
  83. package/dist/cdn/invoice/snice-invoice.js +1 -1
  84. package/dist/cdn/kanban/snice-kanban.js +1 -1
  85. package/dist/cdn/key-value/snice-key-value.js +1 -1
  86. package/dist/cdn/kpi/snice-kpi.js +1 -1
  87. package/dist/cdn/layout/snice-layout.js +1 -1
  88. package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
  89. package/dist/cdn/link/snice-link.js +1 -1
  90. package/dist/cdn/link-preview/snice-link-preview.js +1 -1
  91. package/dist/cdn/list/snice-list.js +1 -1
  92. package/dist/cdn/location/snice-location.js +1 -1
  93. package/dist/cdn/login/snice-login.js +1 -1
  94. package/dist/cdn/map/snice-map.js +1 -1
  95. package/dist/cdn/markdown/snice-markdown.js +1 -1
  96. package/dist/cdn/masonry/snice-masonry.js +1 -1
  97. package/dist/cdn/menu/snice-menu.js +1 -1
  98. package/dist/cdn/message-strip/snice-message-strip.js +1 -1
  99. package/dist/cdn/modal/snice-modal.js +1 -1
  100. package/dist/cdn/music-player/snice-music-player.js +1 -1
  101. package/dist/cdn/nav/snice-nav.js +1 -1
  102. package/dist/cdn/network-graph/snice-network-graph.js +1 -1
  103. package/dist/cdn/notification-center/snice-notification-center.js +1 -1
  104. package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
  105. package/dist/cdn/org-chart/snice-org-chart.js +1 -1
  106. package/dist/cdn/pagination/snice-pagination.js +1 -1
  107. package/dist/cdn/paint/snice-paint.js +1 -1
  108. package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
  109. package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
  110. package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
  111. package/dist/cdn/popover/snice-popover.js +1 -1
  112. package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
  113. package/dist/cdn/product-card/snice-product-card.js +1 -1
  114. package/dist/cdn/progress/snice-progress.js +1 -1
  115. package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
  116. package/dist/cdn/qr-code/snice-qr-code.js +1 -1
  117. package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
  118. package/dist/cdn/radio/snice-radio.js +1 -1
  119. package/dist/cdn/range-slider/snice-range-slider.js +1 -1
  120. package/dist/cdn/rating/snice-rating.js +1 -1
  121. package/dist/cdn/receipt/snice-receipt.js +1 -1
  122. package/dist/cdn/recipe/snice-recipe.js +1 -1
  123. package/dist/cdn/runtime/README.md +2 -2
  124. package/dist/cdn/runtime/snice-runtime.esm.js +333 -192
  125. package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
  126. package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
  127. package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
  128. package/dist/cdn/runtime/snice-runtime.js +333 -191
  129. package/dist/cdn/runtime/snice-runtime.js.map +1 -1
  130. package/dist/cdn/runtime/snice-runtime.min.js +1 -1
  131. package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
  132. package/dist/cdn/sankey/snice-sankey.js +1 -1
  133. package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
  134. package/dist/cdn/select/snice-select.js +1 -1
  135. package/dist/cdn/skeleton/snice-skeleton.js +1 -1
  136. package/dist/cdn/slider/snice-slider.js +1 -1
  137. package/dist/cdn/sortable/snice-sortable.js +1 -1
  138. package/dist/cdn/sparkline/snice-sparkline.js +1 -1
  139. package/dist/cdn/spinner/snice-spinner.js +1 -1
  140. package/dist/cdn/split-button/snice-split-button.js +1 -1
  141. package/dist/cdn/split-pane/snice-split-pane.js +1 -1
  142. package/dist/cdn/spotlight/snice-spotlight.js +1 -1
  143. package/dist/cdn/stat-group/snice-stat-group.js +1 -1
  144. package/dist/cdn/step-input/snice-step-input.js +1 -1
  145. package/dist/cdn/stepper/snice-stepper.js +1 -1
  146. package/dist/cdn/switch/snice-switch.js +1 -1
  147. package/dist/cdn/table/snice-table.esm.js +1 -1
  148. package/dist/cdn/table/snice-table.js +1 -1
  149. package/dist/cdn/tabs/snice-tabs.js +1 -1
  150. package/dist/cdn/tag/snice-tag.js +1 -1
  151. package/dist/cdn/tag-input/snice-tag-input.js +1 -1
  152. package/dist/cdn/terminal/snice-terminal.js +1 -1
  153. package/dist/cdn/testimonial/snice-testimonial.js +1 -1
  154. package/dist/cdn/textarea/snice-textarea.js +1 -1
  155. package/dist/cdn/time-picker/snice-time-picker.js +1 -1
  156. package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
  157. package/dist/cdn/timeline/snice-timeline.js +1 -1
  158. package/dist/cdn/timer/snice-timer.js +1 -1
  159. package/dist/cdn/toast/snice-toast.js +1 -1
  160. package/dist/cdn/tooltip/snice-tooltip.js +1 -1
  161. package/dist/cdn/tree/snice-tree.js +1 -1
  162. package/dist/cdn/treemap/snice-treemap.js +1 -1
  163. package/dist/cdn/user-card/snice-user-card.js +1 -1
  164. package/dist/cdn/video-player/snice-video-player.js +1 -1
  165. package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
  166. package/dist/cdn/waterfall/snice-waterfall.js +1 -1
  167. package/dist/cdn/weather/snice-weather.js +1 -1
  168. package/dist/cdn/work-order/snice-work-order.js +1 -1
  169. package/dist/components/pagination/snice-pagination.types.d.ts +5 -0
  170. package/dist/context-provider.d.ts +11 -1
  171. package/dist/context.d.ts +5 -6
  172. package/dist/index.cjs +330 -188
  173. package/dist/index.cjs.map +1 -1
  174. package/dist/index.d.ts +2 -1
  175. package/dist/index.esm.js +330 -189
  176. package/dist/index.esm.js.map +1 -1
  177. package/dist/index.iife.js +330 -188
  178. package/dist/index.iife.js.map +1 -1
  179. package/dist/react/SniceProvider.js +1 -1
  180. package/dist/react/SniceRouter.d.ts +2 -0
  181. package/dist/react/SniceRouter.js +3 -2
  182. package/dist/react/SniceRouter.js.map +1 -1
  183. package/dist/react/index.js +1 -1
  184. package/dist/react/matchRoute.d.ts +4 -1
  185. package/dist/react/matchRoute.js +13 -3
  186. package/dist/react/matchRoute.js.map +1 -1
  187. package/dist/react/useRequestHandler.js +1 -1
  188. package/dist/symbols.cjs +1 -1
  189. package/dist/symbols.esm.js +1 -1
  190. package/dist/testing-dom.cjs +48 -0
  191. package/dist/testing-dom.cjs.map +1 -0
  192. package/dist/testing-dom.d.ts +23 -0
  193. package/dist/testing-dom.esm.js +45 -0
  194. package/dist/testing-dom.esm.js.map +1 -0
  195. package/dist/transitions.cjs +1 -1
  196. package/dist/transitions.esm.js +1 -1
  197. package/dist/types/context.d.ts +8 -8
  198. package/dist/types/page-options.d.ts +15 -1
  199. package/dist/types/router-instance.d.ts +2 -1
  200. package/dist/utils.d.ts +3 -1
  201. package/docs/ai/README.md +6 -4
  202. package/docs/ai/api.md +13 -2
  203. package/docs/ai/architecture.md +28 -11
  204. package/docs/ai/bindings.md +1 -1
  205. package/docs/ai/cli.md +26 -1
  206. package/docs/ai/components/badge.md +4 -0
  207. package/docs/ai/components/pagination.md +4 -2
  208. package/docs/ai/components/segmented-control.md +5 -1
  209. package/docs/ai/controllers.md +41 -12
  210. package/docs/ai/decorators.md +3 -3
  211. package/docs/ai/events.md +8 -2
  212. package/docs/ai/fetcher.md +32 -1
  213. package/docs/ai/lifecycle.md +4 -1
  214. package/docs/ai/patterns.md +9 -4
  215. package/docs/ai/react-integration.md +1 -0
  216. package/docs/ai/request-response.md +20 -6
  217. package/docs/ai/routing.md +33 -4
  218. package/docs/ai/testing.md +71 -0
  219. package/docs/bindings.md +1 -1
  220. package/docs/cli.md +27 -2
  221. package/docs/components/badge.md +4 -0
  222. package/docs/components/pagination.md +8 -5
  223. package/docs/components/segmented-control.md +5 -1
  224. package/docs/controllers.md +46 -15
  225. package/docs/events.md +11 -2
  226. package/docs/fetcher.md +32 -1
  227. package/docs/lifecycle.md +5 -1
  228. package/docs/react-integration.md +1 -0
  229. package/docs/request-response.md +25 -7
  230. package/docs/routing.md +39 -3
  231. package/docs/testing.md +71 -0
  232. package/package.json +6 -1
@@ -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 DOM/user mutation.
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
- - several external-effect methods or substantial logic repeated across pages -> extract a controller
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', (e) => {
62
- console.log('Page:', e.detail.page);
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
@@ -2,7 +2,15 @@
2
2
 
3
3
  Mirrors `docs/controllers.md`.
4
4
 
5
- Controllers handle data fetching, business logic, and server communication separately from visual components. Attach to any HTML element, including native elements.
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. Observers set up
74
- 7. Channel/response handlers set up
75
- 8. Event handlers set up
76
- 9. `controller-attached` event dispatched
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. Controller scope cleaned up
90
- 7. `controller-detached` event dispatched
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
- async attach(element: DataHost<T>) {
213
+ attach(element: DataHost<T>) {
199
214
  this.element = element;
200
- await this.reload();
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 response = await fetch('/api/data', { signal: abort.signal });
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
@@ -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 raw app-context provisioning and lookup
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; // Skip dispatch when return is undefined (default: true)
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
- Dispatching manually:
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() {
@@ -30,7 +30,13 @@ const router = Router({ target: '#app', context: { auth: null }, fetcher });
30
30
  router.initialize();
31
31
  ```
32
32
 
33
- ## `ctx.fetch` in pages
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
@@ -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. Called whenever the router context changes (navigation, app context update, etc.):
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[] = [];
@@ -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 appContext?: AppContext;
173
+ private ctx?: Context;
174
174
 
175
175
  @context()
176
176
  handleContext(ctx: Context) {
177
- this.appContext = ctx.application;
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 never call `fetch()` or manage data directly — they yield requests upward and render whatever comes back. Controllers own the data layer.
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
  }
@@ -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; called on navigation and on `ctx.update()`.
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[]; // route patterns
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: string, tag: string, transition?: Transition, guards?: Guard | Guard[], layout?: string | false, placard?: Placard | ((ctx: AppContext) => Placard)): void` — manually registers a route without the `@page` decorator. Note: the return-type shape above lists `register` with 4 params; this fuller 6-param signature (adding `layout`, `placard`) is documented separately in the source.
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.
@@ -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 component state must be reasserted even if the bound value itself is unchanged:
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`; extract clearly service-heavy or repeated
59
- page logic into a controller. Small route orchestration remains valid page code.
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