snice 7.5.0 → 7.7.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 (265) hide show
  1. package/adapters/react/SniceProvider.js +1 -1
  2. package/adapters/react/SniceRouter.js +1 -1
  3. package/adapters/react/avatar.d.ts +1 -0
  4. package/adapters/react/avatar.d.ts.map +1 -1
  5. package/adapters/react/avatar.js +1 -1
  6. package/adapters/react/avatar.js.map +1 -1
  7. package/adapters/react/avatar.tsx +2 -1
  8. package/adapters/react/calendar.d.ts +4 -0
  9. package/adapters/react/calendar.d.ts.map +1 -1
  10. package/adapters/react/calendar.js +1 -1
  11. package/adapters/react/calendar.js.map +1 -1
  12. package/adapters/react/calendar.tsx +5 -1
  13. package/adapters/react/matchRoute.js +68 -2
  14. package/adapters/react/matchRoute.js.map +1 -1
  15. package/adapters/react/route-match.d.ts +7 -0
  16. package/adapters/react/useRequestHandler.js +1 -1
  17. package/bin/analyzer-contracts.js +60 -2
  18. package/bin/project-analyzer.js +1262 -118
  19. package/bin/snice.js +95 -5
  20. package/custom-elements.json +112 -0
  21. package/dist/cdn/accordion/snice-accordion.js +1 -1
  22. package/dist/cdn/action-bar/snice-action-bar.js +1 -1
  23. package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
  24. package/dist/cdn/alert/snice-alert.js +1 -1
  25. package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
  26. package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
  27. package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
  28. package/dist/cdn/availability/snice-availability.js +1 -1
  29. package/dist/cdn/avatar/README.md +1 -1
  30. package/dist/cdn/avatar/snice-avatar.js +9 -3
  31. package/dist/cdn/avatar/snice-avatar.js.map +1 -1
  32. package/dist/cdn/avatar/snice-avatar.min.js +2 -2
  33. package/dist/cdn/avatar/snice-avatar.min.js.map +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 +2 -2
  42. package/dist/cdn/button/snice-button.js.map +1 -1
  43. package/dist/cdn/button/snice-button.min.js +1 -1
  44. package/dist/cdn/button/snice-button.min.js.map +1 -1
  45. package/dist/cdn/calendar/README.md +2 -2
  46. package/dist/cdn/calendar/snice-calendar.js +562 -40
  47. package/dist/cdn/calendar/snice-calendar.js.map +1 -1
  48. package/dist/cdn/calendar/snice-calendar.min.js +28 -1
  49. package/dist/cdn/calendar/snice-calendar.min.js.map +1 -1
  50. package/dist/cdn/camera/snice-camera.js +1 -1
  51. package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
  52. package/dist/cdn/candlestick/snice-candlestick.js +1 -1
  53. package/dist/cdn/card/snice-card.js +1 -1
  54. package/dist/cdn/carousel/snice-carousel.js +1 -1
  55. package/dist/cdn/cart/snice-cart.js +1 -1
  56. package/dist/cdn/chart/snice-chart.js +1 -1
  57. package/dist/cdn/chat/snice-chat.js +1 -1
  58. package/dist/cdn/checkbox/snice-checkbox.js +1 -1
  59. package/dist/cdn/chip/snice-chip.js +1 -1
  60. package/dist/cdn/code-block/snice-code-block.js +1 -1
  61. package/dist/cdn/color-display/snice-color-display.js +1 -1
  62. package/dist/cdn/color-picker/snice-color-picker.js +1 -1
  63. package/dist/cdn/command-palette/snice-command-palette.js +1 -1
  64. package/dist/cdn/comments/snice-comments.js +1 -1
  65. package/dist/cdn/countdown/snice-countdown.js +1 -1
  66. package/dist/cdn/cropper/snice-cropper.js +1 -1
  67. package/dist/cdn/data-card/snice-data-card.js +1 -1
  68. package/dist/cdn/date-picker/snice-date-picker.js +1 -1
  69. package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
  70. package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
  71. package/dist/cdn/diff/snice-diff.js +1 -1
  72. package/dist/cdn/divider/snice-divider.js +1 -1
  73. package/dist/cdn/doc/snice-doc.js +1 -1
  74. package/dist/cdn/draw/snice-draw.js +1 -1
  75. package/dist/cdn/drawer/snice-drawer.js +1 -1
  76. package/dist/cdn/empty-state/snice-empty-state.js +1 -1
  77. package/dist/cdn/estimate/snice-estimate.js +1 -1
  78. package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
  79. package/dist/cdn/file-upload/snice-file-upload.js +1 -1
  80. package/dist/cdn/flip-card/snice-flip-card.js +1 -1
  81. package/dist/cdn/flow/snice-flow.js +1 -1
  82. package/dist/cdn/form-layout/snice-form-layout.js +1 -1
  83. package/dist/cdn/funnel/snice-funnel.js +1 -1
  84. package/dist/cdn/gantt/snice-gantt.js +1 -1
  85. package/dist/cdn/gauge/snice-gauge.js +1 -1
  86. package/dist/cdn/grid/snice-grid.js +1 -1
  87. package/dist/cdn/heatmap/snice-heatmap.js +1 -1
  88. package/dist/cdn/image/snice-image.js +1 -1
  89. package/dist/cdn/input/snice-input.js +1 -1
  90. package/dist/cdn/invoice/snice-invoice.js +1 -1
  91. package/dist/cdn/kanban/snice-kanban.js +1 -1
  92. package/dist/cdn/key-value/snice-key-value.js +1 -1
  93. package/dist/cdn/kpi/snice-kpi.js +1 -1
  94. package/dist/cdn/layout/snice-layout.js +1 -1
  95. package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
  96. package/dist/cdn/link/snice-link.js +1 -1
  97. package/dist/cdn/link-preview/snice-link-preview.js +1 -1
  98. package/dist/cdn/list/snice-list.js +1 -1
  99. package/dist/cdn/location/snice-location.js +1 -1
  100. package/dist/cdn/login/README.md +1 -1
  101. package/dist/cdn/login/snice-login.js +2 -2
  102. package/dist/cdn/login/snice-login.js.map +1 -1
  103. package/dist/cdn/login/snice-login.min.js +1 -1
  104. package/dist/cdn/login/snice-login.min.js.map +1 -1
  105. package/dist/cdn/map/snice-map.js +1 -1
  106. package/dist/cdn/markdown/snice-markdown.js +1 -1
  107. package/dist/cdn/masonry/snice-masonry.js +1 -1
  108. package/dist/cdn/menu/snice-menu.js +1 -1
  109. package/dist/cdn/message-strip/snice-message-strip.js +1 -1
  110. package/dist/cdn/modal/snice-modal.js +1 -1
  111. package/dist/cdn/music-player/snice-music-player.js +1 -1
  112. package/dist/cdn/nav/snice-nav.js +1 -1
  113. package/dist/cdn/network-graph/snice-network-graph.js +1 -1
  114. package/dist/cdn/notification-center/snice-notification-center.js +1 -1
  115. package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
  116. package/dist/cdn/org-chart/snice-org-chart.js +1 -1
  117. package/dist/cdn/pagination/snice-pagination.js +1 -1
  118. package/dist/cdn/paint/snice-paint.js +1 -1
  119. package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
  120. package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
  121. package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
  122. package/dist/cdn/popover/snice-popover.js +1 -1
  123. package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
  124. package/dist/cdn/product-card/snice-product-card.js +1 -1
  125. package/dist/cdn/progress/snice-progress.js +1 -1
  126. package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
  127. package/dist/cdn/qr-code/snice-qr-code.js +1 -1
  128. package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
  129. package/dist/cdn/radio/snice-radio.js +1 -1
  130. package/dist/cdn/range-slider/snice-range-slider.js +1 -1
  131. package/dist/cdn/rating/snice-rating.js +1 -1
  132. package/dist/cdn/receipt/snice-receipt.js +1 -1
  133. package/dist/cdn/recipe/snice-recipe.js +1 -1
  134. package/dist/cdn/runtime/README.md +2 -2
  135. package/dist/cdn/runtime/snice-runtime.esm.js +706 -267
  136. package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
  137. package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
  138. package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
  139. package/dist/cdn/runtime/snice-runtime.js +706 -267
  140. package/dist/cdn/runtime/snice-runtime.js.map +1 -1
  141. package/dist/cdn/runtime/snice-runtime.min.js +1 -1
  142. package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
  143. package/dist/cdn/sankey/snice-sankey.js +1 -1
  144. package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
  145. package/dist/cdn/select/snice-select.js +3 -1
  146. package/dist/cdn/select/snice-select.js.map +1 -1
  147. package/dist/cdn/select/snice-select.min.js +2 -2
  148. package/dist/cdn/select/snice-select.min.js.map +1 -1
  149. package/dist/cdn/skeleton/snice-skeleton.js +1 -1
  150. package/dist/cdn/slider/snice-slider.js +1 -1
  151. package/dist/cdn/sortable/snice-sortable.js +1 -1
  152. package/dist/cdn/sparkline/snice-sparkline.js +1 -1
  153. package/dist/cdn/spinner/snice-spinner.js +1 -1
  154. package/dist/cdn/split-button/snice-split-button.js +1 -1
  155. package/dist/cdn/split-pane/snice-split-pane.js +1 -1
  156. package/dist/cdn/spotlight/snice-spotlight.js +1 -1
  157. package/dist/cdn/stat-group/snice-stat-group.js +1 -1
  158. package/dist/cdn/step-input/snice-step-input.js +1 -1
  159. package/dist/cdn/stepper/snice-stepper.js +1 -1
  160. package/dist/cdn/switch/README.md +1 -1
  161. package/dist/cdn/switch/snice-switch.js +8 -4
  162. package/dist/cdn/switch/snice-switch.js.map +1 -1
  163. package/dist/cdn/switch/snice-switch.min.js +8 -4
  164. package/dist/cdn/switch/snice-switch.min.js.map +1 -1
  165. package/dist/cdn/table/README.md +2 -2
  166. package/dist/cdn/table/snice-table.esm.js +77 -5
  167. package/dist/cdn/table/snice-table.esm.js.map +1 -1
  168. package/dist/cdn/table/snice-table.esm.min.js +68 -47
  169. package/dist/cdn/table/snice-table.esm.min.js.map +1 -1
  170. package/dist/cdn/table/snice-table.js +76 -4
  171. package/dist/cdn/table/snice-table.js.map +1 -1
  172. package/dist/cdn/table/snice-table.min.js +53 -32
  173. package/dist/cdn/table/snice-table.min.js.map +1 -1
  174. package/dist/cdn/tabs/snice-tabs.js +1 -1
  175. package/dist/cdn/tag/snice-tag.js +1 -1
  176. package/dist/cdn/tag-input/snice-tag-input.js +1 -1
  177. package/dist/cdn/terminal/snice-terminal.js +1 -1
  178. package/dist/cdn/testimonial/snice-testimonial.js +1 -1
  179. package/dist/cdn/textarea/snice-textarea.js +1 -1
  180. package/dist/cdn/time-picker/snice-time-picker.js +1 -1
  181. package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
  182. package/dist/cdn/timeline/snice-timeline.js +1 -1
  183. package/dist/cdn/timer/snice-timer.js +1 -1
  184. package/dist/cdn/toast/snice-toast.js +1 -1
  185. package/dist/cdn/tooltip/snice-tooltip.js +1 -1
  186. package/dist/cdn/tree/snice-tree.js +1 -1
  187. package/dist/cdn/treemap/snice-treemap.js +1 -1
  188. package/dist/cdn/user-card/snice-user-card.js +1 -1
  189. package/dist/cdn/video-player/snice-video-player.js +1 -1
  190. package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
  191. package/dist/cdn/waterfall/snice-waterfall.js +1 -1
  192. package/dist/cdn/weather/snice-weather.js +1 -1
  193. package/dist/cdn/work-order/snice-work-order.js +1 -1
  194. package/dist/components/avatar/snice-avatar.d.ts +1 -0
  195. package/dist/components/avatar/snice-avatar.js +8 -2
  196. package/dist/components/avatar/snice-avatar.js.map +1 -1
  197. package/dist/components/avatar/snice-avatar.types.d.ts +2 -0
  198. package/dist/components/button/snice-button.js +1 -1
  199. package/dist/components/button/snice-button.js.map +1 -1
  200. package/dist/components/calendar/snice-calendar.d.ts +38 -1
  201. package/dist/components/calendar/snice-calendar.js +390 -40
  202. package/dist/components/calendar/snice-calendar.js.map +1 -1
  203. package/dist/components/calendar/snice-calendar.types.d.ts +63 -0
  204. package/dist/components/select/snice-select.js +2 -0
  205. package/dist/components/select/snice-select.js.map +1 -1
  206. package/dist/components/switch/snice-switch.js +7 -3
  207. package/dist/components/switch/snice-switch.js.map +1 -1
  208. package/dist/components/table/snice-table.d.ts +9 -0
  209. package/dist/components/table/snice-table.js +72 -2
  210. package/dist/components/table/snice-table.js.map +1 -1
  211. package/dist/components/table/table-keyboard.js +1 -1
  212. package/dist/components/table/table-keyboard.js.map +1 -1
  213. package/dist/index.cjs +703 -264
  214. package/dist/index.cjs.map +1 -1
  215. package/dist/index.d.ts +3 -1
  216. package/dist/index.esm.js +703 -264
  217. package/dist/index.esm.js.map +1 -1
  218. package/dist/index.iife.js +703 -264
  219. package/dist/index.iife.js.map +1 -1
  220. package/dist/parts.d.ts +14 -4
  221. package/dist/react/SniceProvider.js +1 -1
  222. package/dist/react/SniceRouter.js +1 -1
  223. package/dist/react/index.js +1 -1
  224. package/dist/react/matchRoute.js +68 -2
  225. package/dist/react/matchRoute.js.map +1 -1
  226. package/dist/react/route-match.d.ts +7 -0
  227. package/dist/react/useRequestHandler.js +1 -1
  228. package/dist/render-errors.d.ts +9 -0
  229. package/dist/route-match.d.ts +7 -0
  230. package/dist/symbols.cjs +1 -1
  231. package/dist/symbols.esm.js +1 -1
  232. package/dist/template-marker.d.ts +5 -0
  233. package/dist/testing-dom.cjs +1 -1
  234. package/dist/testing-dom.esm.js +1 -1
  235. package/dist/testing.d.ts +3 -0
  236. package/dist/transitions.cjs +1 -1
  237. package/dist/transitions.esm.js +1 -1
  238. package/dist/types/dispatch-options.d.ts +5 -4
  239. package/dist/types/event-timing.d.ts +7 -0
  240. package/dist/types/index.d.ts +1 -0
  241. package/dist/types/on-options.d.ts +21 -6
  242. package/dist/utils.d.ts +3 -0
  243. package/docs/ai/DEVELOPMENT.md +1 -1
  244. package/docs/ai/api.md +7 -2
  245. package/docs/ai/cli.md +14 -0
  246. package/docs/ai/components/avatar.md +1 -0
  247. package/docs/ai/components/calendar.md +24 -1
  248. package/docs/ai/components/switch.md +3 -0
  249. package/docs/ai/components/table.md +1 -1
  250. package/docs/ai/decorators.md +3 -2
  251. package/docs/ai/events.md +45 -8
  252. package/docs/ai/rendering.md +7 -0
  253. package/docs/ai/routing.md +6 -1
  254. package/docs/ai/testing.md +17 -0
  255. package/docs/cli.md +19 -0
  256. package/docs/components/avatar.md +1 -0
  257. package/docs/components/calendar.md +112 -2
  258. package/docs/components/switch.md +7 -1
  259. package/docs/components/table.md +2 -1
  260. package/docs/events.md +90 -10
  261. package/docs/rendering.md +10 -0
  262. package/docs/routing.md +22 -1
  263. package/docs/testing.md +19 -0
  264. package/package.json +2 -2
  265. package/vscode.html-custom-data.json +28 -0
package/docs/ai/events.md CHANGED
@@ -142,7 +142,8 @@ handleTextInput(event: Event) {}
142
142
  ```
143
143
 
144
144
  - `currentTarget` is the listener's HOST, not the matched element — derive the match with `event.target.closest(selector)`.
145
- - Shadow retargeting: an event crossing a shadow boundary retargets to the shadow host, so a selector stops matching when rows move into a child component. Listen on the container; carry identity in `detail`.
145
+ - Delegation matches in both the shadow tree and the light DOM by default; narrow with `light`/`shadow` (see OnOptions). Clicks on content slotted into a matching shadow wrapper match that wrapper.
146
+ - Shadow retargeting: an event crossing a shadow boundary retargets to the shadow host, so a selector never matches a child component's internals. Listen on the container; carry identity in `detail`.
146
147
 
147
148
  Keyboard events with `@on` (`:` notation):
148
149
 
@@ -176,7 +177,7 @@ class MyButton extends HTMLElement {
176
177
  interface OnOptions {
177
178
  // Standard event listener options
178
179
  capture?: boolean; // Use capture phase instead of bubble phase
179
- once?: boolean; // Remove listener after first trigger
180
+ once?: boolean; // Handler runs exactly once; non-matching selector/key events don't consume it
180
181
  passive?: boolean; // Passive listener (can't preventDefault)
181
182
 
182
183
  // Automatic event handling
@@ -184,17 +185,32 @@ interface OnOptions {
184
185
  stopPropagation?: boolean; // Automatically call stopPropagation on the event
185
186
 
186
187
  // Timing controls
187
- debounce?: number; // Debounce the handler by specified milliseconds
188
- throttle?: number; // Throttle the handler by specified milliseconds
188
+ debounce?: EventTiming;
189
+ throttle?: EventTiming;
189
190
 
190
- // Shadow DOM delegation
191
- target?: string; // CSS selector to target specific elements within shadow root
191
+ // Delegation
192
+ target?: string; // CSS selector for delegation; same as the positional selector argument
193
+
194
+ // Tree toggles — same light/shadow pair as @query; both default to true.
195
+ // Direct: shadow → shadow-root listener, light → host listener.
196
+ // Delegated: which tree(s) the selector matches in.
197
+ // Both false → warn + skip. Ignored (warned) with scope/daemon.
198
+ light?: boolean;
199
+ shadow?: boolean;
192
200
 
193
201
  // Where to attach the listener (see scope below)
194
202
  scope?: 'global' | string | EventTarget | ((this: HTMLElement) => EventTarget | null);
195
203
  }
204
+ type EventTiming = number | ((this: any) => number);
196
205
  ```
197
206
 
207
+ - Number intervals remain supported.
208
+ - Resolver: called with the decorated element/controller as `this` when the
209
+ listener is set up (and set up again after reconnect).
210
+ - Result must be finite and non-negative. `0` disables; invalid/negative/`NaN`
211
+ throws `TypeError`.
212
+ - Use method/function syntax, not an arrow, to read `this`.
213
+
198
214
  ### scope — listener attachment target
199
215
 
200
216
  Default: host element. `scope` redirects attachment to another target — how Snice expresses cross-cutting events.
@@ -240,6 +256,11 @@ handleScroll(event: Event) {}
240
256
  // Debounce input events by 300ms
241
257
  @on('input', 'input[type="search"]', { debounce: 300 })
242
258
  handleSearch(event: Event) {}
259
+
260
+ @on('input', 'input[type="search"]', {
261
+ debounce() { return this.searchDebounce; }
262
+ })
263
+ handleAdaptiveSearch(event: Event) {}
243
264
  ```
244
265
 
245
266
  ## @dispatch decorator
@@ -276,13 +297,26 @@ updateStatus(status: string) {
276
297
  ```typescript
277
298
  interface DispatchOptions extends EventInit {
278
299
  dispatchOnUndefined?: boolean; // Undefined return still dispatches unless false (default: true)
279
- debounce?: number; // Debounce dispatch by ms
280
- throttle?: number; // Throttle dispatch by ms
300
+ debounce?: EventTiming;
301
+ throttle?: EventTiming;
281
302
  // Where to dispatch the event (see scope below)
282
303
  scope?: 'global' | string | EventTarget | ((this: HTMLElement) => EventTarget | null);
283
304
  }
284
305
  ```
285
306
 
307
+ `EventTiming = number | ((this: any) => number)`. A resolver runs against the
308
+ decorated element/controller on every method invocation. Result validation is
309
+ the same as `@on`. Async methods dispatch only after resolution. Disconnect
310
+ drops work queued before disconnect and decorated async invocations that began
311
+ before disconnect. Teardown hooks keep the real element as `this`; calls they
312
+ make after cancellation are new invocations, using current timing state after
313
+ reconnect.
314
+
315
+ Each invocation supersedes that method's pending timed work. Resolved `0`
316
+ cancels an older timer and dispatches the new result immediately. Throttle
317
+ trailing detail is latest-wins; its deadline is last actual dispatch + the
318
+ newly resolved interval.
319
+
286
320
  ### scope — dispatch target
287
321
 
288
322
  Default: `this.dispatchEvent(event)` — event originates from the host element. `scope` redirects the dispatch to another target so the event behaves as if it originated there. Use with `@on({ scope })` to express cross-cutting events without bubbling.
@@ -321,6 +355,9 @@ If `scope` cannot resolve (selector matches no ancestor, resolver returns `null`
321
355
  ```typescript
322
356
  @dispatch('search-query', { debounce: 300 })
323
357
  emitSearch(query: string) { return { query }; }
358
+
359
+ @dispatch('search-query', { debounce() { return this.searchDebounce; } })
360
+ emitAdaptiveSearch(query: string) { return { query }; }
324
361
  ```
325
362
 
326
363
  ### Async methods
@@ -73,6 +73,13 @@ html`${asyncIterable}`
73
73
  - Promise/AsyncIterable values work directly in node expressions; stale replaced sources are ignored.
74
74
  - Async iterators receive a best-effort `return()` on replacement or disconnect. Promise cancellation stays caller-owned.
75
75
 
76
+ ## Authoring diagnostics
77
+
78
+ - Template parse/authoring failures rendered by a Snice element identify the authoritative registered `<tag>` and, when safely available, the class, plus a nearby static-template excerpt when available. Minified CDN constructors may have no class name, so tag-only attribution is expected. Identity is attached to the exact constructor/immediate prototype only after successful `@element`, `@layout`, or Router registration (or an exact existing registration); undecorated subclasses stay generic, while document adoption preserves that identity.
79
+ - Context follows nested/iterable/async `TemplateResult` values in open/closed shadow roots and light DOM.
80
+ - Host-free preparation stays generic; runtime diagnostics do not invent a source filename or component.
81
+ - Contextual errors retain the original error as `cause` (and therefore its stack).
82
+
76
83
  ## Control flow
77
84
 
78
85
  ```typescript
@@ -192,7 +192,12 @@ class OverridePage extends HTMLElement {}
192
192
 
193
193
  ## Route parameters
194
194
 
195
- - `:param` segments in `routes` auto-map to `@property()` fields of the same name on the page element, set before `@ready()` fires. `@property({ attribute: false })` opts OUT the Router binds through attributes, so such a field silently keeps its initializer.
195
+ - Named `:param` segments and named splats (`*path`, including optional splats) in string routes and `{ path, order }` routes bind through attributes before `@ready()` fires; normally declare a plain `@property()` field of the same name.
196
+ - The route-param spelling must match the observed attribute after HTML lowercasing. `HTMLElement`: `:articleId` -> plain `articleId` (`articleid`). `SniceElement`: plain `articleId` observes `article-id`, so use `:article-id` or `@property({ attribute: 'articleId' }) articleId`.
197
+ - Explicit alias: `@property({ attribute: 'article-id' }) articleId` binds from `:article-id`, not `:articleId`.
198
+ - `@property({ attribute: false })` opts OUT — Router cannot set it, so it silently keeps its initializer.
199
+ - A reflected native HTMLElement attribute such as `id` is already a binding target when no Snice property overrides it. An explicit `@property({ attribute: false }) id` or differently aliased `id` overrides the native channel and does not bind from `:id`. A custom element can also consume a statically declared `observedAttributes` entry in `attributeChangedCallback`; do not redeclare native IDL properties merely to satisfy the analyzer.
200
+ - A subclass `@state()` member disables an inherited `@property()` channel of the same name. With Snice's stage-3 field transformation, a plain subclass field initializer or authored accessor still runs through the inherited transformed property accessor and remains bindable.
196
201
  - Multiple `:param`s all populate independently, e.g. `/posts/:postId/comments/:commentId` → `postId`, `commentId` properties.
197
202
  - Query params: declare directly in the route pattern, e.g. `routes: ['/search?q=:query']` — extracted as a route param (`ctx.navigation.params.query`), not parsed from `location.search`.
198
203
 
@@ -42,6 +42,23 @@ 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
+ ## Strict render errors
46
+
47
+ ```typescript
48
+ try {
49
+ setStrictRenderErrors(true);
50
+ expect(() => { element.invalid = true; }).toThrow(/<my-element> \(MyElement\)/);
51
+ } finally {
52
+ setStrictRenderErrors(false);
53
+ }
54
+ ```
55
+
56
+ - Default: log render failure, retain previous DOM. Strict: synchronously rethrow synchronous render failures.
57
+ - Template authoring errors include the authoritative registered tag, an optional safely available class name, and a nearby static-template excerpt when a host exists. Minified CDN output may correctly report only the tag. Attribution is exact-constructor/immediate-prototype metadata recorded after successful Snice registration; undecorated subclasses are generic, and adopted instances retain the registered identity.
58
+ - Host-free preparation stays generic. Runtime does not claim a source filename it cannot know.
59
+ - Original error is `cause`; inspect `cause.stack` for the underlying stack.
60
+ - Promise/AsyncIterable failures happen after the render call and remain `console.error` reports in strict mode; await settlement and assert the `Error` argument.
61
+
45
62
  ## Partial DOM compatibility
46
63
 
47
64
  In a simulated DOM, opt into Snice's standards compatibility layer from the
package/docs/cli.md CHANGED
@@ -52,6 +52,25 @@ npx snice validate # source analyzer only
52
52
  - deep imports that were never released package paths, such as `snice/decorators`
53
53
  - a Router without `target`, `type`, or a project-wide `initialize()` call
54
54
  - a routed class combining `@page` with redundant `@element`
55
+ - a path/query `:param` or named `*splat` whose page has no reachable attribute
56
+ target (`snice/route-param-has-no-binding-target`, warning), including
57
+ `attribute: false` and mismatched explicit aliases
58
+ - a controller `@context()` handler that starts load/reload/refresh/fetch work
59
+ without `{ once: true }`, a first-delivery guard, or an update diff
60
+ (`snice/unguarded-context-load`, warning)
61
+
62
+ The route-param check follows proven local declarations, direct relative
63
+ re-exports, and named or namespace Snice imports, so an inherited bindable
64
+ property satisfies it. It deliberately defers a missing-target warning for an
65
+ unresolved/ambiguous base or dynamic route/attribute contract; locally visible
66
+ disabled or mismatched properties are still diagnosed. Native reflected IDL
67
+ attributes such as an unmodified `id`, and statically known custom
68
+ `observedAttributes` handlers, also satisfy the attribute target.
69
+
70
+ The route-param rule is non-blocking because its dependency-free static lexer
71
+ cannot model every valid JavaScript/TypeScript grammar edge. Treat the warning
72
+ as a strong prompt to verify the Router attribute channel; `validate` and
73
+ `check` fail only when another error-level diagnostic is present.
55
74
 
56
75
  It also gives non-blocking architecture suggestions: keep `@page`, `@element`,
57
76
  `@controller`, and `@daemon` classes under `src/pages`, `src/components`,
@@ -22,6 +22,7 @@ Displays a user profile image with automatic fallback to name-based initials or
22
22
  | `name` | `string` | `''` | User's name (used for initials and color generation) |
23
23
  | `size` | `'xs' \| 'small' \| 'medium' \| 'large' \| 'xl' \| 'xxl'` | `'medium'` | Avatar size |
24
24
  | `shape` | `'circle' \| 'square' \| 'rounded'` | `'circle'` | Avatar shape |
25
+ | `loading` | `'lazy' \| 'eager'` | `'lazy'` | Image loading strategy; use `eager` for above-the-fold or tiny thumbnail avatars |
25
26
  | `fallbackColor` (attr: `fallback-color`) | `string` | `'#ffffff'` | Text color for initials fallback |
26
27
  | `fallbackBackground` (attr: `fallback-background`) | `string` | `''` | Background color for initials fallback (overrides auto-color) |
27
28
 
@@ -25,6 +25,8 @@ Display and select dates with event support, date restrictions, and locale-aware
25
25
  | `maxDate` (attr: `max-date`) | `Date \| string` | `''` | Maximum selectable date |
26
26
  | `disabledDates` | `(Date \| string)[]` | `[]` | Disabled dates (JS-only; no attribute) |
27
27
  | `highlightToday` (attr: `highlight-today`) | `boolean` | `true` | Highlight today's date |
28
+ | `noDaySelect` (attr: `no-day-select`) | `boolean` | `false` | Display-only mode: day clicks neither select/highlight nor fire `calendar-change`; event bars stay interactive |
29
+ | `cellSizing` (attr: `cell-sizing`) | `'square' \| 'stretch'` | `'square'` | `square` keeps day cells as tall as the column is wide; `stretch` lets rows collapse to their content and event-lane reservation |
28
30
  | `showWeekNumbers` (attr: `show-week-numbers`) | `boolean` | `false` | Show week numbers |
29
31
  | `firstDayOfWeek` (attr: `first-day-of-week`) | `number` | `0` | First day of week (0=Sun, 1=Mon) |
30
32
  | `locale` | `string` | `'en-US'` | Locale for formatting |
@@ -37,9 +39,19 @@ interface CalendarEvent {
37
39
  title: string;
38
40
  start: Date | string;
39
41
  end?: Date | string;
40
- color?: string;
42
+ color?: string; // bar background
43
+ className?: string; // extra class(es) on the bars, also exposed as ::part names
44
+ avatar?: string | CalendarEventAvatar; // string is shorthand for { src }
45
+ tooltip?: string; // static tooltip text for the bars
46
+ popover?: boolean | string | Node | (() => Node); // click-to-open details card (see Event Popovers)
41
47
  data?: any;
42
48
  }
49
+
50
+ interface CalendarEventAvatar {
51
+ src?: string; // image URL
52
+ name?: string; // initials fallback (rendered with <snice-avatar>)
53
+ alt?: string;
54
+ }
43
55
  ```
44
56
 
45
57
  ## Methods
@@ -109,10 +121,108 @@ Use the `events` property to display events on the calendar.
109
121
  ```typescript
110
122
  calendar.events = [
111
123
  { id: 1, title: 'Team Meeting', start: new Date(2024, 5, 15, 10, 0), color: '#2196f3' },
112
- { id: 2, title: 'Project Deadline', start: new Date(2024, 5, 20), color: '#f44336' }
124
+ { id: 2, title: 'Project Deadline', start: new Date(2024, 5, 20), color: '#f44336' },
125
+ { id: 3, title: 'Conference', start: new Date(2024, 5, 18), end: new Date(2024, 5, 24), color: '#16a34a' }
113
126
  ];
114
127
  ```
115
128
 
129
+ Events render as continuous stripes, the way professional calendars draw them:
130
+ an event with an `end` date spans all of its days as one bar per week row,
131
+ chopped at week boundaries with squared corners so consecutive rows read as a
132
+ single bar (the title repeats on each row). Concurrent events stack into
133
+ lanes — earlier start first, longer event first on ties. Up to three lanes are
134
+ shown; days with deeper stacks get a `+N more` chip. Each bar exposes a
135
+ `part="event-bar"` for styling and dispatches `calendar-event-click` when
136
+ clicked.
137
+
138
+ Bars are styleable per event: `color` sets the background, `avatar` renders a
139
+ small `<snice-avatar>` at the start of each bar (`part="event-avatar"`) — an
140
+ image when `src` is given, initials from `name` otherwise — and `className` is
141
+ added to the bar's classes **and** its part list, so a specific kind of event
142
+ can be themed from outside the component:
143
+
144
+ ```css
145
+ snice-calendar::part(event-bar) { font-weight: 500; }
146
+ snice-calendar::part(urgent) { background: crimson; }
147
+ ```
148
+
149
+ ```typescript
150
+ calendar.events = [
151
+ { id: 1, title: 'Incident review', start: '2024-06-18', className: 'urgent',
152
+ avatar: { src: '/avatars/sre-lead.png', name: 'Robin Kim' } }
153
+ ];
154
+ ```
155
+
156
+ ### Event Tooltips
157
+
158
+ A per-event `tooltip` string shows on hover. For rich or lazily-loaded
159
+ content, set the `eventTooltip` provider on the calendar — it runs when the
160
+ pointer enters a bar and may return text, a DOM node, or a promise of either;
161
+ results that resolve after the pointer left are discarded. The provider wins
162
+ over `event.tooltip`. The overlay exposes `part="event-tooltip"`.
163
+
164
+ ```typescript
165
+ calendar.eventTooltip = async (event) => {
166
+ const details = await fetchEventDetails(event.id);
167
+ const node = document.createElement('div');
168
+ node.innerHTML = `<strong>${event.title}</strong><p>${details.attendees} attendees</p>`;
169
+ return node;
170
+ };
171
+ ```
172
+
173
+ ### Event Popovers
174
+
175
+ Tooltips are for glancing; popovers are for interacting. An event that sets
176
+ `popover` opens a click-anchored details card (`role="dialog"`,
177
+ `part="event-popover"`) that holds focus, and closes on Escape (focus returns
178
+ to the bar) or an outside click. `calendar-event-click` still dispatches.
179
+ Popovers are **strictly per-event opt-in** — events without `popover` never
180
+ open a card and never issue a request.
181
+
182
+ Content resolves in this order:
183
+
184
+ 1. **Inline** — `popover` is a string, a Node, or a Node factory.
185
+ 2. **Provider** — `popover: true` and the element's `eventPopover` callback:
186
+ `(event) => string | Node | Promise<string | Node>`. A loading state shows
187
+ while a promise is pending; results arriving after close are discarded.
188
+ 3. **Request channel** — `popover: true` with no provider issues
189
+ `@request('calendar/event-popover')` with `{ event }`; any
190
+ `@respond('calendar/event-popover')` controller can return the content.
191
+ With no responder either, the card closes and a dev warning is logged.
192
+
193
+ ```typescript
194
+ // Inline
195
+ calendar.events = [
196
+ { id: 1, title: 'Standup', start: '2024-06-18', popover: 'Room 4, 10:00' }
197
+ ];
198
+
199
+ // Lazy provider
200
+ calendar.events = [{ id: 2, title: 'Conf', start: '2024-06-20', popover: true }];
201
+ calendar.eventPopover = async (event) => detailsCard(await fetchEvent(event.id));
202
+
203
+ // Request channel — the calendar stays generic, a controller owns the data
204
+ @controller('agenda-controller')
205
+ class AgendaController implements IController {
206
+ element: HTMLElement | null = null;
207
+ private ctx!: Context;
208
+ async attach() {}
209
+ async detach() {}
210
+
211
+ @context()
212
+ receiveContext(ctx: Context) { this.ctx = ctx; }
213
+
214
+ @respond('calendar/event-popover')
215
+ async details({ event }: { event: CalendarEvent }) {
216
+ const data = await this.ctx.fetch(`/api/events/${event.id}`).then(r => r.json());
217
+ return renderDetailsCard(data);
218
+ }
219
+ }
220
+ ```
221
+
222
+ Bars with a popover are keyboard-operable (`role="button"`, `tabindex="0"`,
223
+ Enter/Space opens). Close programmatically with
224
+ `calendar.closeEventPopover()`.
225
+
116
226
  ### Date Restrictions
117
227
 
118
228
  Use `min-date` and `max-date` to constrain the selectable date range.
@@ -64,6 +64,8 @@ A toggle switch input for boolean selections. Supports on/off labels, form integ
64
64
  | `thumb` | Switch thumb |
65
65
  | `spinner` | Loading spinner |
66
66
  | `label` | Label text |
67
+ | `label-on` | On-state text inside the track (renders when `label-on` is set) |
68
+ | `label-off` | Off-state text inside the track (renders when `label-off` is set) |
67
69
 
68
70
  ## Basic Usage
69
71
 
@@ -102,10 +104,14 @@ Use `size` to change the switch dimensions.
102
104
 
103
105
  ### With On/Off Labels
104
106
 
105
- Use `label-on` and `label-off` for text inside the switch track.
107
+ Use `label-on` and `label-off` for text inside the switch track. The track
108
+ sizes itself to the widest label (medium and large sizes; small hides state
109
+ labels), and the thumb's travel follows the actual track width — so long
110
+ labels and `::part(track)` width overrides both stay aligned.
106
111
 
107
112
  ```html
108
113
  <snice-switch label="Dark mode" label-on="ON" label-off="OFF"></snice-switch>
114
+ <snice-switch label="Door" label-on="Open" label-off="Closed"></snice-switch>
109
115
  ```
110
116
 
111
117
  ### Form Integration
@@ -153,7 +153,8 @@ Column capability flags default to enabled unless explicitly set to `false` when
153
153
  `formatter` is the row-aware display override for every built-in cell.
154
154
  `valueFormatter` is the fallback display formatter and is also used by the
155
155
  editing pipeline, aggregate output, and formatted clipboard export.
156
- `valueGetter` participates in local sorting and aggregation. `valueParser` and
156
+ `valueGetter` derives the working value for cell display, local sorting, and
157
+ aggregation. `valueParser` and
157
158
  `valueSetter` run during editing; a setter may return either the final field
158
159
  value or an updated row object.
159
160
 
package/docs/events.md CHANGED
@@ -354,17 +354,23 @@ class ListController implements IController {
354
354
  }
355
355
  ```
356
356
 
357
- Two delegation rules worth knowing:
357
+ Three delegation rules worth knowing:
358
358
 
359
359
  - **`currentTarget` is the listener's host, not the matched element.** Inside
360
360
  `@on('click', '.delete-button')` the handler receives the raw event, so
361
361
  derive the match with `event.target.closest('.delete-button')` when you need
362
362
  the element itself (as above).
363
+ - **Delegation matches in both trees by default.** The selector matches
364
+ elements in the component's shadow tree and its light-DOM children. A click
365
+ on content slotted into a matching shadow wrapper matches that wrapper.
366
+ Narrow the search with the `light`/`shadow` options — the same tree toggles
367
+ `@query` uses (see [@on Options](#on-options)).
363
368
  - **Shadow DOM retargeting changes what the selector matches.** An event
364
369
  crossing a shadow boundary is retargeted to the shadow host, so
365
370
  `@on('row-clicked', 'my-row')` stops matching when rows move into a list
366
- component — from outside, the event's target is the list host. Listen on the
367
- container and carry the row identity in the event `detail` instead.
371
+ component — from outside, the event's target is the list host. A selector
372
+ never matches a child component's internals. Listen on the container and
373
+ carry the row identity in the event `detail` instead.
368
374
 
369
375
  ### Keyboard Events with @on
370
376
 
@@ -410,7 +416,7 @@ class EditorController implements IController {
410
416
  interface OnOptions {
411
417
  // Standard event listener options
412
418
  capture?: boolean; // Use capture phase instead of bubble phase
413
- once?: boolean; // Remove listener after first trigger
419
+ once?: boolean; // Handler runs exactly once; non-matching selector/key events don't consume it
414
420
  passive?: boolean; // Passive listener (can't preventDefault)
415
421
 
416
422
  // Automatic event handling
@@ -418,11 +424,15 @@ interface OnOptions {
418
424
  stopPropagation?: boolean; // Automatically call stopPropagation on the event
419
425
 
420
426
  // Timing controls
421
- debounce?: number; // Debounce the handler by specified milliseconds
422
- throttle?: number; // Throttle the handler by specified milliseconds
427
+ debounce?: EventTiming; // Debounce the handler
428
+ throttle?: EventTiming; // Throttle the handler
423
429
 
424
- // Shadow DOM delegation
425
- target?: string; // CSS selector to target specific elements within shadow root
430
+ // Delegation
431
+ target?: string; // CSS selector for delegation; same as the positional selector argument
432
+
433
+ // Tree toggles — the same light/shadow pair @query uses; both default to true
434
+ light?: boolean; // Listen in the light DOM (host element + light children)
435
+ shadow?: boolean; // Listen in the shadow tree (the component's shadow root)
426
436
 
427
437
  // Where to attach the listener (see scope section below)
428
438
  scope?: 'global' | string | EventTarget | ((this: HTMLElement) => EventTarget | null);
@@ -430,8 +440,39 @@ interface OnOptions {
430
440
  // Named daemon from the nearest provided app context
431
441
  daemon?: string;
432
442
  }
443
+
444
+ type EventTiming = number | ((this: any) => number);
445
+ ```
446
+
447
+ Numeric intervals remain supported. A resolver can instead read a value from
448
+ the decorated element or controller instance. For `@on`, it runs when the
449
+ managed listener is set up and again when it is set up after reconnect. The
450
+ resolved value must be a finite, non-negative number; `0` disables that timing
451
+ option, while invalid, negative, and `NaN` values throw `TypeError`.
452
+
453
+ #### light / shadow — choosing the tree
454
+
455
+ `light` and `shadow` are the same tree toggles `@query` uses. Both default to
456
+ `true`, so `@on` hears events from the shadow tree and the light DOM alike.
457
+ Set one to `false` to narrow the listener:
458
+
459
+ ```typescript
460
+ // Only the component's shadow tree
461
+ @on('click', '.item', { light: false })
462
+ onShadowItem(e: MouseEvent) { /* ... */ }
463
+
464
+ // Only light-DOM children (and the host itself)
465
+ @on('click', '.item', { shadow: false })
466
+ onLightItem(e: MouseEvent) { /* ... */ }
433
467
  ```
434
468
 
469
+ For direct handlers the flags choose where the listener attaches: `shadow`
470
+ controls the shadow-root listener, `light` controls the host listener. For
471
+ delegated handlers they choose which tree(s) the selector matches in. Setting
472
+ both to `false` warns and skips the listener, and the flags are ignored (with
473
+ a warning) when `scope` or `daemon` is set — those own the listener target
474
+ outright.
475
+
435
476
  #### scope — controlling the listener target
436
477
 
437
478
  By default, `@on` attaches the listener to the host element. The `scope` option redirects
@@ -522,6 +563,19 @@ class SearchController implements IController {
522
563
  }
523
564
  ```
524
565
 
566
+ Per-instance interval:
567
+
568
+ ```typescript
569
+ @on('input', 'input[type="search"]', {
570
+ debounce() { return this.searchDebounce; }
571
+ })
572
+ handleSearch(event: Event) { /* ... */ }
573
+ ```
574
+
575
+ Use method syntax (or a normal function), not an arrow, when reading `this`.
576
+ Controllers receive the controller instance as `this`; elements receive the
577
+ element instance.
578
+
525
579
  ### Using @on in Elements (Alternative)
526
580
 
527
581
  While template syntax is preferred, `@on` can also be used in elements:
@@ -623,8 +677,8 @@ class StatusIndicator extends HTMLElement {
623
677
  ```typescript
624
678
  interface DispatchOptions extends EventInit {
625
679
  dispatchOnUndefined?: boolean; // Undefined return still dispatches unless false (default: true)
626
- debounce?: number; // Debounce dispatch by ms
627
- throttle?: number; // Throttle dispatch by ms
680
+ debounce?: EventTiming; // Debounce dispatch
681
+ throttle?: EventTiming; // Throttle dispatch
628
682
  // Where to dispatch the event (see scope section below)
629
683
  scope?: 'global' | string | EventTarget | ((this: HTMLElement) => EventTarget | null);
630
684
  // Named daemon from the nearest provided app context
@@ -632,6 +686,23 @@ interface DispatchOptions extends EventInit {
632
686
  }
633
687
  ```
634
688
 
689
+ `EventTiming` is the same `number | ((this: any) => number)` type used by
690
+ `@on`. `@dispatch` resolves it against the decorated instance on every method
691
+ invocation, so an element or controller can change its interval at runtime.
692
+ The same finite, non-negative validation applies. Debounced async methods still
693
+ dispatch only after the method resolves. Disconnect synchronously drops timed
694
+ dispatches already queued and decorated async invocations that started before
695
+ disconnect. Lifecycle hooks always run with the real element as `this`; an
696
+ `@dispatch` method called by teardown code after that cancellation is a new,
697
+ ordinary invocation (and, after reconnect, participates in the current timing
698
+ state).
699
+
700
+ Each invocation supersedes pending timed work for that decorated method. A
701
+ resolved `0` therefore cancels an older debounce/throttle timer and dispatches
702
+ the new result without delay. For throttle, a suppressed invocation replaces
703
+ the trailing detail and recalculates its deadline as the last actual dispatch
704
+ plus the newly resolved interval.
705
+
635
706
  #### scope — controlling the dispatch target
636
707
 
637
708
  By default, `@dispatch` originates from the element or a controller's host; on a
@@ -696,6 +767,15 @@ class SearchBox extends HTMLElement {
696
767
  }
697
768
  ```
698
769
 
770
+ ```typescript
771
+ @dispatch('search-query', {
772
+ debounce() { return this.searchDebounce; }
773
+ })
774
+ emitSearch(query: string) {
775
+ return { query };
776
+ }
777
+ ```
778
+
699
779
  ### Async Methods
700
780
 
701
781
  `@dispatch` works with async methods — the event dispatches after the promise resolves:
package/docs/rendering.md CHANGED
@@ -51,6 +51,16 @@ isSafeUrl(objectUrl, { allowed: ['blob:'] });
51
51
 
52
52
  By default, relative references and absolute `http:`, `https:`, `mailto:`, and `tel:` URLs are accepted. Network-path references must resolve to an allowed protocol. Malformed URLs, raw ASCII control characters, and every other explicit scheme are rejected. Passing `allowed` replaces the absolute-protocol list but does not disable relative references. `snice-button` applies this policy automatically to its `href` property.
53
53
 
54
+ ### Authoring diagnostics
55
+
56
+ Malformed declarative syntax fails when its `TemplateResult` is prepared for rendering. When the template belongs to a Snice element, the error identifies the owning host by its authoritative registered tag and, when safely available, its JavaScript class, then includes a nearby static-template excerpt where possible:
57
+
58
+ ```text
59
+ snice: render failed for <user-editor> (UserEditor): ... Near "<button ${…}>...".
60
+ ```
61
+
62
+ Minified CDN builds commonly strip constructor names, so a tag-only identity such as `<user-editor>` is normal and intentional. Identity is recorded only after a successful Snice `@element`, `@layout`, or Router page registration (or when that exact constructor is already registered). It belongs to that exact constructor and immediate prototype: an undecorated subclass stays generic, while an instance adopted into another document keeps its original registered identity. This context follows nested templates, keyed/iterable templates, promises, and async iterables, including templates rendered into open or closed shadow roots and light DOM. A template prepared without a component render host keeps the generic authoring error and nearby excerpt; Snice does not invent a component, source filename, or callsite it cannot know at runtime. Contextual errors retain the original error as `cause`, so its stack remains available for debugging.
63
+
54
64
  ## Bindings
55
65
 
56
66
  This section is the quick syntax overview. See [Binding Channels](./bindings.md) for the complete value, cleanup, event, spread, sentinel, and form-control semantics.
package/docs/routing.md CHANGED
@@ -447,11 +447,32 @@ window.history.go(-2);
447
447
  ### Accessing Parameters
448
448
 
449
449
  Route parameters are automatically mapped to element properties — a `:param`
450
- segment binds to a **plain `@property()`** of the same name. A field declared
450
+ segment or named splat such as `*path` (including an optional splat) normally
451
+ binds to a **plain `@property()`** of the same name. A field declared
451
452
  `@property({ attribute: false })` is opted OUT of route-param binding: the
452
453
  Router never sets it and it silently keeps its initializer. (The Router sets
453
454
  params through attributes; `attribute: false` fields have none.)
454
455
 
456
+ The parameter spelling must reach the property's observed attribute. On an
457
+ `HTMLElement` page, `:articleId` reaches a plain `articleId` property through
458
+ the lowercased `articleid` attribute. A `SniceElement` page uses kebab-case
459
+ implicit attributes, so its plain `articleId` property needs `:article-id` (or
460
+ an explicit `@property({ attribute: 'articleId' })`). Explicit aliases follow
461
+ the same rule: `@property({ attribute: 'article-id' }) articleId` binds from
462
+ `:article-id`, not `:articleId`.
463
+
464
+ A reflected native HTMLElement attribute such as `id` is already reachable
465
+ when no Snice property overrides it. An explicit
466
+ `@property({ attribute: false }) id` or differently aliased `id` overrides that
467
+ native channel, so `:id` no longer populates the property. A statically declared
468
+ `observedAttributes` entry paired with `attributeChangedCallback` can also
469
+ consume a route attribute.
470
+
471
+ Inheritance follows Snice's decorator transformation: a subclass `@state()`
472
+ member disables an inherited `@property()` channel of the same name, while a
473
+ plain field initializer or authored accessor still uses the inherited
474
+ transformed property accessor and remains bindable.
475
+
455
476
  ```typescript
456
477
  @page({
457
478
  tag: 'article-page',
package/docs/testing.md CHANGED
@@ -47,6 +47,25 @@ 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
+ ## Strict Render Errors
51
+
52
+ Rendering logs failures by default and retains the previous DOM. Tests that need a synchronous render failure to fail the assertion directly can temporarily enable strict mode:
53
+
54
+ ```typescript
55
+ import { setStrictRenderErrors } from 'snice';
56
+
57
+ try {
58
+ setStrictRenderErrors(true);
59
+ expect(() => { element.invalid = true; }).toThrow(/<my-element> \(MyElement\)/);
60
+ } finally {
61
+ setStrictRenderErrors(false);
62
+ }
63
+ ```
64
+
65
+ Template parse and authoring errors identify the owning component by its authoritative registered tag and, when safely available, its class, and include a nearby static-template excerpt. Minified CDN constructors may have no class name, so tests should accept tag-only attribution. Attribution requires the exact constructor and immediate prototype successfully registered by `@element`, `@layout`, or Router (or that exact constructor already present in the registry): undecorated subclasses stay generic, and document adoption does not change an instance's captured registration identity. The original error is retained as `cause`, so assertions and debugging can inspect its stack. Snice cannot recover a source filename from a runtime tagged-template value and does not fabricate one. A template prepared outside a component render therefore keeps the generic nearby-template diagnostic.
66
+
67
+ Promise and async-iterable values settle after the synchronous render call has returned, so their failures are reported through `console.error` even while strict mode is enabled. Spy on `console.error`, wait for the deferred value to settle, and assert against the `Error` argument; it carries the same owning-component context.
68
+
50
69
  ## Partial DOM Compatibility
51
70
 
52
71
  In a simulated DOM, opt into Snice's standards compatibility layer from your
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snice",
3
- "version": "7.5.0",
3
+ "version": "7.7.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",
@@ -117,7 +117,7 @@
117
117
  "build:distribution": "rm -rf dist && npm run generate:metadata && rollup -c && npm run build:types && node tooling/build/embed-sourcemap-sources.mjs dist",
118
118
  "build:types": "tsc --project packages/components/tsconfig.json --emitDeclarationOnly && cp packages/components/custom-elements.d.ts dist/components/custom-elements.d.ts",
119
119
  "prebuild:cdn": "test -f dist/index.esm.js || npm run build:distribution",
120
- "build:cdn": "rollup -c rollup.config.cdn.js && node tooling/website/sync-component-assets.js && node tooling/website/update-cdn-sizes.js",
120
+ "build:cdn": "node tooling/build/parallel-cdn-build.mjs && node tooling/website/sync-component-assets.js && node tooling/website/update-cdn-sizes.js",
121
121
  "prebuild:react": "test -f dist/index.d.ts || npm run build:distribution",
122
122
  "build:react": "npm run generate:react-adapters && tsc --project adapters/react/tsconfig.json && node tooling/build/embed-sourcemap-sources.mjs adapters/react",
123
123
  "build:testing": "rollup -c rollup.config.test.js && node tooling/build/embed-sourcemap-sources.mjs dist",