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
@@ -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
- <script type="module">
90
- document.querySelector('snice-pagination').addEventListener('pagination-change', (e) => {
91
- console.log('Page:', e.detail.page, 'Previous:', e.detail.previousPage);
92
- });
93
- </script>
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 |
@@ -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 handle data fetching, business logic, and server communication separately from visual components. They can be attached to any HTML element, including native elements.
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. Observers are set up
118
- 7. Channel/response handlers are set up
119
- 8. Event handlers are set up
120
- 9. `controller-attached` event is dispatched
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. Controller scope is cleaned up
137
- 7. `controller-detached` event is dispatched
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 `@respond` handlers. Clean up your own resources (WebSockets, timers, manual listeners) in `detach`:
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
- Controllers own data fetching. Pass state through the element's public API and
359
- dispatch outcome events do not manipulate its rendered DOM. A production
360
- controller should also prevent an older response from overwriting a newer one:
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
- async attach(element: OrdersView) {
395
+ attach(element: OrdersView) {
379
396
  this.element = element;
380
- await this.reload();
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 response = await fetch('/api/orders', {
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; // Skip dispatch when return is undefined (default: true)
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
- ### Dispatching Manually
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
- ## Using Context.fetch in Pages
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. The decorated method is called whenever the router context changes (navigation, app context update, etc.):
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). |
@@ -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 never call `fetch()` or manage data directly. They yield requests upward and render whatever comes back. Controllers own the data layer.
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 updates from the router. The method is called whenever navigation occurs, with a Context object containing application state and navigation data.
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[]; // Route patterns
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.2.0",
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"