snice 7.3.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 (209) hide show
  1. package/.agents/skills/snice/SKILL.md +12 -16
  2. package/README.md +5 -1
  3. package/adapters/react/SniceProvider.js +1 -1
  4. package/adapters/react/SniceRouter.js +1 -1
  5. package/adapters/react/matchRoute.js +1 -1
  6. package/adapters/react/useRequestHandler.js +1 -1
  7. package/bin/analyzer-contracts.js +3 -1
  8. package/bin/project-analyzer.js +219 -6
  9. package/bin/snice.js +77 -13
  10. package/dist/app-context.d.ts +10 -10
  11. package/dist/cdn/accordion/snice-accordion.js +1 -1
  12. package/dist/cdn/action-bar/snice-action-bar.js +1 -1
  13. package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
  14. package/dist/cdn/alert/snice-alert.js +1 -1
  15. package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
  16. package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
  17. package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
  18. package/dist/cdn/availability/snice-availability.js +1 -1
  19. package/dist/cdn/avatar/snice-avatar.js +1 -1
  20. package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
  21. package/dist/cdn/badge/snice-badge.js +1 -1
  22. package/dist/cdn/banner/snice-banner.js +1 -1
  23. package/dist/cdn/binpack/snice-binpack.js +1 -1
  24. package/dist/cdn/book/snice-book.js +1 -1
  25. package/dist/cdn/booking/snice-booking.js +1 -1
  26. package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
  27. package/dist/cdn/button/snice-button.js +1 -1
  28. package/dist/cdn/calendar/snice-calendar.js +1 -1
  29. package/dist/cdn/camera/snice-camera.js +1 -1
  30. package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
  31. package/dist/cdn/candlestick/snice-candlestick.js +1 -1
  32. package/dist/cdn/card/snice-card.js +1 -1
  33. package/dist/cdn/carousel/snice-carousel.js +1 -1
  34. package/dist/cdn/cart/snice-cart.js +1 -1
  35. package/dist/cdn/chart/snice-chart.js +1 -1
  36. package/dist/cdn/chat/snice-chat.js +1 -1
  37. package/dist/cdn/checkbox/snice-checkbox.js +1 -1
  38. package/dist/cdn/chip/snice-chip.js +1 -1
  39. package/dist/cdn/code-block/snice-code-block.js +1 -1
  40. package/dist/cdn/color-display/snice-color-display.js +1 -1
  41. package/dist/cdn/color-picker/snice-color-picker.js +1 -1
  42. package/dist/cdn/command-palette/snice-command-palette.js +1 -1
  43. package/dist/cdn/comments/snice-comments.js +1 -1
  44. package/dist/cdn/countdown/snice-countdown.js +1 -1
  45. package/dist/cdn/cropper/snice-cropper.js +1 -1
  46. package/dist/cdn/data-card/snice-data-card.js +1 -1
  47. package/dist/cdn/date-picker/snice-date-picker.js +1 -1
  48. package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
  49. package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
  50. package/dist/cdn/diff/snice-diff.js +1 -1
  51. package/dist/cdn/divider/snice-divider.js +1 -1
  52. package/dist/cdn/doc/snice-doc.js +1 -1
  53. package/dist/cdn/draw/snice-draw.js +1 -1
  54. package/dist/cdn/drawer/snice-drawer.js +1 -1
  55. package/dist/cdn/empty-state/snice-empty-state.js +1 -1
  56. package/dist/cdn/estimate/snice-estimate.js +1 -1
  57. package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
  58. package/dist/cdn/file-upload/snice-file-upload.js +1 -1
  59. package/dist/cdn/flip-card/snice-flip-card.js +1 -1
  60. package/dist/cdn/flow/snice-flow.js +1 -1
  61. package/dist/cdn/form-layout/snice-form-layout.js +1 -1
  62. package/dist/cdn/funnel/snice-funnel.js +1 -1
  63. package/dist/cdn/gantt/snice-gantt.js +1 -1
  64. package/dist/cdn/gauge/snice-gauge.js +1 -1
  65. package/dist/cdn/grid/snice-grid.js +1 -1
  66. package/dist/cdn/heatmap/snice-heatmap.js +1 -1
  67. package/dist/cdn/image/snice-image.js +1 -1
  68. package/dist/cdn/input/snice-input.js +1 -1
  69. package/dist/cdn/invoice/snice-invoice.js +1 -1
  70. package/dist/cdn/kanban/snice-kanban.js +1 -1
  71. package/dist/cdn/key-value/snice-key-value.js +1 -1
  72. package/dist/cdn/kpi/snice-kpi.js +1 -1
  73. package/dist/cdn/layout/snice-layout.js +1 -1
  74. package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
  75. package/dist/cdn/link/snice-link.js +1 -1
  76. package/dist/cdn/link-preview/snice-link-preview.js +1 -1
  77. package/dist/cdn/list/snice-list.js +1 -1
  78. package/dist/cdn/location/snice-location.js +1 -1
  79. package/dist/cdn/login/snice-login.js +1 -1
  80. package/dist/cdn/map/snice-map.js +1 -1
  81. package/dist/cdn/markdown/snice-markdown.js +1 -1
  82. package/dist/cdn/masonry/snice-masonry.js +1 -1
  83. package/dist/cdn/menu/snice-menu.js +1 -1
  84. package/dist/cdn/message-strip/snice-message-strip.js +1 -1
  85. package/dist/cdn/modal/snice-modal.js +1 -1
  86. package/dist/cdn/music-player/snice-music-player.js +1 -1
  87. package/dist/cdn/nav/snice-nav.js +1 -1
  88. package/dist/cdn/network-graph/snice-network-graph.js +1 -1
  89. package/dist/cdn/notification-center/snice-notification-center.js +1 -1
  90. package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
  91. package/dist/cdn/org-chart/snice-org-chart.js +1 -1
  92. package/dist/cdn/pagination/snice-pagination.js +1 -1
  93. package/dist/cdn/paint/snice-paint.js +1 -1
  94. package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
  95. package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
  96. package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
  97. package/dist/cdn/popover/snice-popover.js +1 -1
  98. package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
  99. package/dist/cdn/product-card/snice-product-card.js +1 -1
  100. package/dist/cdn/progress/snice-progress.js +1 -1
  101. package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
  102. package/dist/cdn/qr-code/snice-qr-code.js +1 -1
  103. package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
  104. package/dist/cdn/radio/snice-radio.js +1 -1
  105. package/dist/cdn/range-slider/snice-range-slider.js +1 -1
  106. package/dist/cdn/rating/snice-rating.js +1 -1
  107. package/dist/cdn/receipt/snice-receipt.js +1 -1
  108. package/dist/cdn/recipe/snice-recipe.js +1 -1
  109. package/dist/cdn/runtime/README.md +2 -2
  110. package/dist/cdn/runtime/snice-runtime.esm.js +309 -188
  111. package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
  112. package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
  113. package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
  114. package/dist/cdn/runtime/snice-runtime.js +309 -187
  115. package/dist/cdn/runtime/snice-runtime.js.map +1 -1
  116. package/dist/cdn/runtime/snice-runtime.min.js +1 -1
  117. package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
  118. package/dist/cdn/sankey/snice-sankey.js +1 -1
  119. package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
  120. package/dist/cdn/select/snice-select.js +1 -1
  121. package/dist/cdn/skeleton/snice-skeleton.js +1 -1
  122. package/dist/cdn/slider/snice-slider.js +1 -1
  123. package/dist/cdn/sortable/snice-sortable.js +1 -1
  124. package/dist/cdn/sparkline/snice-sparkline.js +1 -1
  125. package/dist/cdn/spinner/snice-spinner.js +1 -1
  126. package/dist/cdn/split-button/snice-split-button.js +1 -1
  127. package/dist/cdn/split-pane/snice-split-pane.js +1 -1
  128. package/dist/cdn/spotlight/snice-spotlight.js +1 -1
  129. package/dist/cdn/stat-group/snice-stat-group.js +1 -1
  130. package/dist/cdn/step-input/snice-step-input.js +1 -1
  131. package/dist/cdn/stepper/snice-stepper.js +1 -1
  132. package/dist/cdn/switch/snice-switch.js +1 -1
  133. package/dist/cdn/table/snice-table.esm.js +1 -1
  134. package/dist/cdn/table/snice-table.js +1 -1
  135. package/dist/cdn/tabs/snice-tabs.js +1 -1
  136. package/dist/cdn/tag/snice-tag.js +1 -1
  137. package/dist/cdn/tag-input/snice-tag-input.js +1 -1
  138. package/dist/cdn/terminal/snice-terminal.js +1 -1
  139. package/dist/cdn/testimonial/snice-testimonial.js +1 -1
  140. package/dist/cdn/textarea/snice-textarea.js +1 -1
  141. package/dist/cdn/time-picker/snice-time-picker.js +1 -1
  142. package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
  143. package/dist/cdn/timeline/snice-timeline.js +1 -1
  144. package/dist/cdn/timer/snice-timer.js +1 -1
  145. package/dist/cdn/toast/snice-toast.js +1 -1
  146. package/dist/cdn/tooltip/snice-tooltip.js +1 -1
  147. package/dist/cdn/tree/snice-tree.js +1 -1
  148. package/dist/cdn/treemap/snice-treemap.js +1 -1
  149. package/dist/cdn/user-card/snice-user-card.js +1 -1
  150. package/dist/cdn/video-player/snice-video-player.js +1 -1
  151. package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
  152. package/dist/cdn/waterfall/snice-waterfall.js +1 -1
  153. package/dist/cdn/weather/snice-weather.js +1 -1
  154. package/dist/cdn/work-order/snice-work-order.js +1 -1
  155. package/dist/components/pagination/snice-pagination.types.d.ts +5 -0
  156. package/dist/context-provider.d.ts +11 -1
  157. package/dist/context.d.ts +5 -6
  158. package/dist/index.cjs +306 -184
  159. package/dist/index.cjs.map +1 -1
  160. package/dist/index.d.ts +2 -1
  161. package/dist/index.esm.js +306 -185
  162. package/dist/index.esm.js.map +1 -1
  163. package/dist/index.iife.js +306 -184
  164. package/dist/index.iife.js.map +1 -1
  165. package/dist/react/SniceProvider.js +1 -1
  166. package/dist/react/SniceRouter.js +1 -1
  167. package/dist/react/index.js +1 -1
  168. package/dist/react/matchRoute.js +1 -1
  169. package/dist/react/useRequestHandler.js +1 -1
  170. package/dist/symbols.cjs +1 -1
  171. package/dist/symbols.esm.js +1 -1
  172. package/dist/testing-dom.cjs +48 -0
  173. package/dist/testing-dom.cjs.map +1 -0
  174. package/dist/testing-dom.d.ts +23 -0
  175. package/dist/testing-dom.esm.js +45 -0
  176. package/dist/testing-dom.esm.js.map +1 -0
  177. package/dist/transitions.cjs +1 -1
  178. package/dist/transitions.esm.js +1 -1
  179. package/dist/types/context.d.ts +8 -8
  180. package/dist/utils.d.ts +3 -1
  181. package/docs/ai/api.md +12 -1
  182. package/docs/ai/architecture.md +7 -3
  183. package/docs/ai/bindings.md +1 -1
  184. package/docs/ai/cli.md +22 -0
  185. package/docs/ai/components/badge.md +4 -0
  186. package/docs/ai/components/pagination.md +4 -2
  187. package/docs/ai/components/segmented-control.md +5 -1
  188. package/docs/ai/controllers.md +30 -11
  189. package/docs/ai/decorators.md +2 -2
  190. package/docs/ai/events.md +8 -2
  191. package/docs/ai/fetcher.md +32 -1
  192. package/docs/ai/lifecycle.md +4 -1
  193. package/docs/ai/patterns.md +9 -4
  194. package/docs/ai/request-response.md +20 -6
  195. package/docs/ai/routing.md +8 -1
  196. package/docs/ai/testing.md +71 -0
  197. package/docs/bindings.md +1 -1
  198. package/docs/cli.md +23 -0
  199. package/docs/components/badge.md +4 -0
  200. package/docs/components/pagination.md +8 -5
  201. package/docs/components/segmented-control.md +5 -1
  202. package/docs/controllers.md +30 -11
  203. package/docs/events.md +11 -2
  204. package/docs/fetcher.md +32 -1
  205. package/docs/lifecycle.md +5 -1
  206. package/docs/request-response.md +25 -7
  207. package/docs/routing.md +10 -1
  208. package/docs/testing.md +71 -0
  209. package/package.json +6 -1
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v7.2.0
2
+ * snice v7.3.0
3
3
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -232,7 +232,7 @@ let contextIdCounter = 0;
232
232
  */
233
233
  class Context {
234
234
  constructor(context = {}, placards = [], currentRoute = '', routeParams = {}, fetcher) {
235
- // Stores the SET of wrapped-method names per element so that classes with
235
+ // Stores the SET of wrapped-method names per subscriber so that classes with
236
236
  // multiple @context() handlers register every method, not just the last one.
237
237
  this[_a] = new WeakMap();
238
238
  this[_b] = new Set();
@@ -256,7 +256,7 @@ class Context {
256
256
  }
257
257
  }
258
258
  /**
259
- * Register an element to receive context updates
259
+ * Register an element or controller to receive context updates
260
260
  * @internal Used by @context decorator
261
261
  */
262
262
  [(_a = REGISTERED_ELEMENTS, _b = REGISTERED_ELEMENTS_SET, _c = IS_UPDATING, CONTEXT_REGISTER)](element, methodName) {
@@ -270,10 +270,10 @@ class Context {
270
270
  this[REGISTERED_ELEMENTS_SET].add(element);
271
271
  }
272
272
  /**
273
- * Unregister an element from receiving context updates. With a methodName,
273
+ * Unregister a subscriber from context updates. With a methodName,
274
274
  * removes only that one handler (so a `once` handler tearing itself down
275
275
  * doesn't kill the element's other @context handlers); without one, removes
276
- * the element entirely (used on disconnect).
276
+ * the subscriber entirely (used on disconnect/detach).
277
277
  * @internal Used by @context decorator cleanup
278
278
  */
279
279
  [CONTEXT_UNREGISTER](element, methodName) {
@@ -314,7 +314,7 @@ class Context {
314
314
  return;
315
315
  }
316
316
  this[IS_UPDATING] = true;
317
- // Notify all registered elements by calling EVERY registered method
317
+ // Notify all registered subscribers by calling EVERY registered method
318
318
  // name on each. Classes with multiple @context() methods get all fired.
319
319
  const elementsSet = this[REGISTERED_ELEMENTS_SET];
320
320
  const elementsMap = this[REGISTERED_ELEMENTS];
@@ -336,7 +336,7 @@ class Context {
336
336
  this[IS_UPDATING] = false;
337
337
  }
338
338
  /**
339
- * Notify a specific element of the current context state
339
+ * Notify a specific subscriber of the current context state
340
340
  * @internal Used by @context decorator
341
341
  */
342
342
  [CONTEXT_NOTIFY_ELEMENT](element) {
@@ -345,7 +345,12 @@ class Context {
345
345
  return;
346
346
  for (const methodName of names) {
347
347
  if (typeof element[methodName] === 'function') {
348
- element[methodName](this);
348
+ try {
349
+ element[methodName](this);
350
+ }
351
+ catch (error) {
352
+ console.error(`Error calling @context method ${methodName}:`, error);
353
+ }
349
354
  }
350
355
  }
351
356
  }
@@ -677,11 +682,17 @@ function invokeImmediateWatchers(instance, constructor) {
677
682
  */
678
683
  function createDebounced(fn, delay) {
679
684
  let timer = null;
680
- return function (...args) {
685
+ const debounced = function (...args) {
681
686
  if (timer !== null)
682
687
  clearTimeout(timer);
683
688
  timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay);
684
689
  };
690
+ debounced.cancel = () => {
691
+ if (timer !== null)
692
+ clearTimeout(timer);
693
+ timer = null;
694
+ };
695
+ return debounced;
685
696
  }
686
697
  /**
687
698
  * Create a throttled version of a function (leading + trailing edge)
@@ -1138,7 +1149,7 @@ function contextTarget(source) {
1138
1149
  * boundary. Public callers use provideContext(), which also activates the
1139
1150
  * daemon instances declared by the context.
1140
1151
  */
1141
- function installContextProvider(root, context) {
1152
+ function installContextProvider(root, context, fetch, navigation) {
1142
1153
  if (!root || typeof root.addEventListener !== 'function' || typeof root.removeEventListener !== 'function') {
1143
1154
  throw new TypeError('provideContext() requires an EventTarget root.');
1144
1155
  }
@@ -1150,9 +1161,26 @@ function installContextProvider(root, context) {
1150
1161
  }
1151
1162
  const listener = (event) => {
1152
1163
  const request = event;
1153
- if (!request.detail || request.detail.context !== undefined)
1164
+ if (!request.detail)
1165
+ return;
1166
+ if (request.detail.kind === 'application') {
1167
+ if (request.detail.context !== undefined)
1168
+ return;
1169
+ request.detail.context = context;
1170
+ }
1171
+ else if (request.detail.kind === 'fetch') {
1172
+ if (!fetch || request.detail.fetch !== undefined)
1173
+ return;
1174
+ request.detail.fetch = fetch;
1175
+ }
1176
+ else if (request.detail.kind === 'navigation') {
1177
+ if (!navigation || request.detail.navigation !== undefined)
1178
+ return;
1179
+ request.detail.navigation = navigation;
1180
+ }
1181
+ else {
1154
1182
  return;
1155
- request.detail.context = context;
1183
+ }
1156
1184
  request.preventDefault();
1157
1185
  request.stopPropagation();
1158
1186
  };
@@ -1182,7 +1210,7 @@ function getContext(source) {
1182
1210
  }
1183
1211
  const target = contextTarget(source);
1184
1212
  if (target) {
1185
- const detail = {};
1213
+ const detail = { kind: 'application' };
1186
1214
  target.dispatchEvent(new CustomEvent(CONTEXT_REQUEST_EVENT, {
1187
1215
  bubbles: true,
1188
1216
  composed: true,
@@ -1201,6 +1229,39 @@ function getContext(source) {
1201
1229
  }
1202
1230
  return undefined;
1203
1231
  }
1232
+ /**
1233
+ * Resolve the fetch function supplied by the nearest context provider.
1234
+ * Router providers expose their ContextAwareFetcher-bound function here;
1235
+ * explicit providers may supply the same dependency for non-router apps and
1236
+ * tests without adding a reserved property to AppContext.
1237
+ */
1238
+ function getContextFetch(source) {
1239
+ const target = contextTarget(source);
1240
+ if (!target)
1241
+ return undefined;
1242
+ const detail = { kind: 'fetch' };
1243
+ target.dispatchEvent(new CustomEvent(CONTEXT_REQUEST_EVENT, {
1244
+ bubbles: true,
1245
+ composed: true,
1246
+ cancelable: true,
1247
+ detail,
1248
+ }));
1249
+ return detail.fetch;
1250
+ }
1251
+ /** @internal Resolve the router Context visible to an element or controller. */
1252
+ function getNavigationContext(source) {
1253
+ const target = contextTarget(source);
1254
+ if (!target)
1255
+ return undefined;
1256
+ const detail = { kind: 'navigation' };
1257
+ target.dispatchEvent(new CustomEvent(CONTEXT_REQUEST_EVENT, {
1258
+ bubbles: true,
1259
+ composed: true,
1260
+ cancelable: true,
1261
+ detail,
1262
+ }));
1263
+ return detail.navigation;
1264
+ }
1204
1265
 
1205
1266
  function isDaemonInstance(value) {
1206
1267
  return !!value
@@ -1864,12 +1925,13 @@ function setupEventHandlers(instance, targetElement) {
1864
1925
  }
1865
1926
  // Initialize cleanup object if needed
1866
1927
  if (!instance[CLEANUP]) {
1867
- instance[CLEANUP] = { events: [], channels: [], observers: [] };
1928
+ instance[CLEANUP] = { events: [], eventCancels: [], channels: [], observers: [] };
1868
1929
  }
1869
- else if (!instance[CLEANUP].events) {
1930
+ if (!instance[CLEANUP].events)
1870
1931
  instance[CLEANUP].events = [];
1871
- }
1872
- else if (instance[CLEANUP].events.length > 0) {
1932
+ if (!instance[CLEANUP].eventCancels)
1933
+ instance[CLEANUP].eventCancels = [];
1934
+ if (instance[CLEANUP].events.length > 0 || instance[CLEANUP].eventCancels.length > 0) {
1873
1935
  // Events already set up - clean them up first to avoid duplicates
1874
1936
  cleanupEventHandlers(instance);
1875
1937
  }
@@ -1899,7 +1961,9 @@ function setupEventHandlers(instance, targetElement) {
1899
1961
  warnIfModifierMisuse(handler.eventName);
1900
1962
  // Apply debounce (takes precedence over throttle)
1901
1963
  if (handlerOptions.debounce && handlerOptions.debounce > 0) {
1902
- boundMethod = createDebounced(boundMethod, handlerOptions.debounce);
1964
+ const debounced = createDebounced(boundMethod, handlerOptions.debounce);
1965
+ instance[CLEANUP].eventCancels.push(() => debounced.cancel());
1966
+ boundMethod = debounced;
1903
1967
  }
1904
1968
  else if (handlerOptions.throttle && handlerOptions.throttle > 0) {
1905
1969
  boundMethod = throttle$1(boundMethod, handlerOptions.throttle);
@@ -2072,12 +2136,190 @@ function setupEventHandlers(instance, targetElement) {
2072
2136
  function cleanupEventHandlers(instance) {
2073
2137
  if (!instance[CLEANUP]?.events)
2074
2138
  return;
2139
+ for (const cancel of instance[CLEANUP].eventCancels ?? [])
2140
+ cancel();
2141
+ instance[CLEANUP].eventCancels = [];
2075
2142
  for (const { target, eventName, handler, options } of instance[CLEANUP].events) {
2076
2143
  target.removeEventListener(eventName, handler, options);
2077
2144
  }
2078
2145
  instance[CLEANUP].events = [];
2079
2146
  }
2080
2147
 
2148
+ /**
2149
+ * @context decorator for receiving router context updates
2150
+ */
2151
+ const CONTEXT_HANDLERS = getSymbol('context-handlers');
2152
+ /**
2153
+ * @context decorator for receiving router context updates
2154
+ *
2155
+ * @example
2156
+ * ```typescript
2157
+ * @element('my-layout')
2158
+ * class MyLayout extends HTMLElement {
2159
+ * @context
2160
+ * handleContext(ctx: Context) {
2161
+ * this.renderNav(ctx.placards, ctx.currentRoute);
2162
+ * }
2163
+ *
2164
+ * @context({ debounce: 300 })
2165
+ * handleContextDebounced(ctx: Context) {
2166
+ * // Called after 300ms of no updates
2167
+ * }
2168
+ * }
2169
+ * ```
2170
+ */
2171
+ function context$1(options = {}) {
2172
+ return function (originalMethod, context) {
2173
+ const methodName = context.name;
2174
+ context.addInitializer(function () {
2175
+ const constructor = this.constructor;
2176
+ // hasOwnProperty guards so subclasses don't mutate parent state via
2177
+ // the prototype chain.
2178
+ if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_METHODS)) {
2179
+ constructor[CONTEXT_METHODS] = new Set();
2180
+ }
2181
+ if (constructor[CONTEXT_METHODS].has(originalMethod))
2182
+ return;
2183
+ constructor[CONTEXT_METHODS].add(originalMethod);
2184
+ if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_HANDLERS)) {
2185
+ const inherited = constructor[CONTEXT_HANDLERS];
2186
+ constructor[CONTEXT_HANDLERS] = inherited ? [...inherited] : [];
2187
+ }
2188
+ constructor[CONTEXT_HANDLERS].push({
2189
+ methodName,
2190
+ method: originalMethod,
2191
+ options,
2192
+ });
2193
+ });
2194
+ return originalMethod;
2195
+ };
2196
+ }
2197
+ /**
2198
+ * Set up context handlers for an element or attached controller.
2199
+ * Called automatically by the corresponding managed lifecycle.
2200
+ */
2201
+ function setupContextHandler(element, catchUp = false) {
2202
+ const handlers = element.constructor[CONTEXT_HANDLERS];
2203
+ if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2204
+ return;
2205
+ }
2206
+ // Get the Context instance from the router
2207
+ const ctx = element[CONTEXT_HANDLER] ?? getNavigationContext(element);
2208
+ if (!ctx) {
2209
+ return;
2210
+ }
2211
+ // Store the Context instance for cleanup
2212
+ element[NAVIGATION_CONTEXT_INSTANCE] = ctx;
2213
+ // A controller can finish attaching after the Router has already announced
2214
+ // the current navigation. Track whether an update arrives during this turn;
2215
+ // if none does, catch it up once from the Context's current state.
2216
+ let receivedContextUpdate = false;
2217
+ // Register each handler with the Context
2218
+ for (const handler of handlers) {
2219
+ const { methodName, method, options } = handler;
2220
+ const wrappedMethodName = `__wrapped_${methodName}`;
2221
+ // Create wrapped method with timing controls
2222
+ element[wrappedMethodName] = function (context) {
2223
+ receivedContextUpdate = true;
2224
+ // `once` is tracked PER method (not a single element-level flag) so
2225
+ // multiple once handlers on the same element each fire.
2226
+ const called = element[CONTEXT_CALLED] ||
2227
+ (element[CONTEXT_CALLED] = new Set());
2228
+ if (options.once && called.has(methodName)) {
2229
+ return;
2230
+ }
2231
+ const callMethod = () => {
2232
+ method.call(element, context);
2233
+ // Handle once option
2234
+ if (options.once) {
2235
+ called.add(methodName);
2236
+ // Unregister ONLY this handler, not the whole element — otherwise the
2237
+ // element's other @context handlers would stop receiving updates.
2238
+ const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2239
+ if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2240
+ ctx[CONTEXT_UNREGISTER](element, wrappedMethodName);
2241
+ }
2242
+ }
2243
+ };
2244
+ // Per-handler timer slot to avoid debounce/throttle handlers on the same
2245
+ // element overwriting each other's state.
2246
+ const timerSlot = element[CONTEXT_TIMER] ||
2247
+ (element[CONTEXT_TIMER] = {});
2248
+ const timerKey = methodName;
2249
+ if (options.debounce) {
2250
+ clearTimeout(timerSlot[timerKey]?.timeout);
2251
+ timerSlot[timerKey] = {
2252
+ timeout: setTimeout(callMethod, options.debounce),
2253
+ };
2254
+ return;
2255
+ }
2256
+ if (options.throttle) {
2257
+ const now = Date.now();
2258
+ const lastCall = timerSlot[timerKey]?.lastCall || 0;
2259
+ if (now - lastCall >= options.throttle) {
2260
+ timerSlot[timerKey] = { lastCall: now };
2261
+ callMethod();
2262
+ }
2263
+ return;
2264
+ }
2265
+ callMethod();
2266
+ };
2267
+ // Register with the Context using the wrapped method name
2268
+ if (typeof ctx[CONTEXT_REGISTER] === 'function') {
2269
+ ctx[CONTEXT_REGISTER](element, wrappedMethodName);
2270
+ }
2271
+ }
2272
+ if (catchUp && typeof ctx[CONTEXT_NOTIFY_ELEMENT] === 'function') {
2273
+ return new Promise((resolve) => {
2274
+ const notifyIfNeeded = () => {
2275
+ try {
2276
+ if (!receivedContextUpdate
2277
+ && element[NAVIGATION_CONTEXT_INSTANCE] === ctx) {
2278
+ ctx[CONTEXT_NOTIFY_ELEMENT](element);
2279
+ }
2280
+ }
2281
+ finally {
2282
+ resolve();
2283
+ }
2284
+ };
2285
+ if (catchUp === 'task')
2286
+ setTimeout(notifyIfNeeded, 0);
2287
+ else
2288
+ queueMicrotask(notifyIfNeeded);
2289
+ });
2290
+ }
2291
+ }
2292
+ /**
2293
+ * Clean up context handlers for an element or attached controller.
2294
+ */
2295
+ function cleanupContextHandler(element) {
2296
+ const handlers = element.constructor[CONTEXT_HANDLERS];
2297
+ if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2298
+ return;
2299
+ }
2300
+ // Clear any pending debounce timers (per-handler slots)
2301
+ const timerSlot = element[CONTEXT_TIMER];
2302
+ if (timerSlot && typeof timerSlot === 'object') {
2303
+ for (const key of Object.keys(timerSlot)) {
2304
+ if (timerSlot[key]?.timeout)
2305
+ clearTimeout(timerSlot[key].timeout);
2306
+ }
2307
+ delete element[CONTEXT_TIMER];
2308
+ }
2309
+ for (const handler of handlers) {
2310
+ // Clean up wrapped method
2311
+ const wrappedMethodName = `__wrapped_${handler.methodName}`;
2312
+ delete element[wrappedMethodName];
2313
+ }
2314
+ // Unregister from Context if available
2315
+ const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2316
+ if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2317
+ ctx[CONTEXT_UNREGISTER](element);
2318
+ }
2319
+ delete element[NAVIGATION_CONTEXT_INSTANCE];
2320
+ delete element[CONTEXT_CALLED];
2321
+ }
2322
+
2081
2323
  // Controller-scoped cleanup registry
2082
2324
  class ControllerScope {
2083
2325
  constructor() {
@@ -2411,6 +2653,10 @@ async function attachController(element, controller) {
2411
2653
  await scope.runOperation(async () => {
2412
2654
  await controllerInstance.attach(element);
2413
2655
  });
2656
+ // @context has the same managed lifecycle on controllers as @on,
2657
+ // @observe and @respond. Catch up controllers whose async attach completed
2658
+ // after the Router announced the current navigation.
2659
+ await setupContextHandler(controllerInstance, 'microtask');
2414
2660
  // Setup @observe observers for controller
2415
2661
  setupObservers(controllerInstance, element);
2416
2662
  // Setup @channel handlers for controller
@@ -2476,6 +2722,8 @@ async function detachController(element) {
2476
2722
  cleanupResponseHandlers(controllerInstance);
2477
2723
  // Cleanup @on event handlers for controller
2478
2724
  cleanupEventHandlers(controllerInstance);
2725
+ // Cleanup @context handlers for controller
2726
+ cleanupContextHandler(controllerInstance);
2479
2727
  // Cleanup the controller scope
2480
2728
  if (scope) {
2481
2729
  await scope.cleanup();
@@ -2609,158 +2857,6 @@ function useNativeElementControllers() {
2609
2857
  globalThis.sniceNativeControllerObserver = observer;
2610
2858
  }
2611
2859
 
2612
- /**
2613
- * @context decorator for receiving router context updates
2614
- */
2615
- const CONTEXT_HANDLERS = getSymbol('context-handlers');
2616
- /**
2617
- * @context decorator for receiving router context updates
2618
- *
2619
- * @example
2620
- * ```typescript
2621
- * @element('my-layout')
2622
- * class MyLayout extends HTMLElement {
2623
- * @context
2624
- * handleContext(ctx: Context) {
2625
- * this.renderNav(ctx.placards, ctx.currentRoute);
2626
- * }
2627
- *
2628
- * @context({ debounce: 300 })
2629
- * handleContextDebounced(ctx: Context) {
2630
- * // Called after 300ms of no updates
2631
- * }
2632
- * }
2633
- * ```
2634
- */
2635
- function context$1(options = {}) {
2636
- return function (originalMethod, context) {
2637
- const methodName = context.name;
2638
- context.addInitializer(function () {
2639
- const constructor = this.constructor;
2640
- // hasOwnProperty guards so subclasses don't mutate parent state via
2641
- // the prototype chain.
2642
- if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_METHODS)) {
2643
- constructor[CONTEXT_METHODS] = new Set();
2644
- }
2645
- if (constructor[CONTEXT_METHODS].has(originalMethod))
2646
- return;
2647
- constructor[CONTEXT_METHODS].add(originalMethod);
2648
- if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_HANDLERS)) {
2649
- const inherited = constructor[CONTEXT_HANDLERS];
2650
- constructor[CONTEXT_HANDLERS] = inherited ? [...inherited] : [];
2651
- }
2652
- constructor[CONTEXT_HANDLERS].push({
2653
- methodName,
2654
- method: originalMethod,
2655
- options,
2656
- });
2657
- });
2658
- return originalMethod;
2659
- };
2660
- }
2661
- /**
2662
- * Setup context handler for an element instance
2663
- * Called automatically during element connection
2664
- */
2665
- function setupContextHandler(element) {
2666
- const handlers = element.constructor[CONTEXT_HANDLERS];
2667
- if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2668
- return;
2669
- }
2670
- // Get the Context instance from the router
2671
- const ctx = element[CONTEXT_HANDLER];
2672
- if (!ctx) {
2673
- return;
2674
- }
2675
- // Store the Context instance for cleanup
2676
- element[NAVIGATION_CONTEXT_INSTANCE] = ctx;
2677
- // Register each handler with the Context
2678
- for (const handler of handlers) {
2679
- const { methodName, method, options } = handler;
2680
- const wrappedMethodName = `__wrapped_${methodName}`;
2681
- // Create wrapped method with timing controls
2682
- element[wrappedMethodName] = function (context) {
2683
- // `once` is tracked PER method (not a single element-level flag) so
2684
- // multiple once handlers on the same element each fire.
2685
- const called = element[CONTEXT_CALLED] ||
2686
- (element[CONTEXT_CALLED] = new Set());
2687
- if (options.once && called.has(methodName)) {
2688
- return;
2689
- }
2690
- const callMethod = () => {
2691
- method.call(element, context);
2692
- // Handle once option
2693
- if (options.once) {
2694
- called.add(methodName);
2695
- // Unregister ONLY this handler, not the whole element — otherwise the
2696
- // element's other @context handlers would stop receiving updates.
2697
- const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2698
- if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2699
- ctx[CONTEXT_UNREGISTER](element, wrappedMethodName);
2700
- }
2701
- }
2702
- };
2703
- // Per-handler timer slot to avoid debounce/throttle handlers on the same
2704
- // element overwriting each other's state.
2705
- const timerSlot = element[CONTEXT_TIMER] ||
2706
- (element[CONTEXT_TIMER] = {});
2707
- const timerKey = methodName;
2708
- if (options.debounce) {
2709
- clearTimeout(timerSlot[timerKey]?.timeout);
2710
- timerSlot[timerKey] = {
2711
- timeout: setTimeout(callMethod, options.debounce),
2712
- };
2713
- return;
2714
- }
2715
- if (options.throttle) {
2716
- const now = Date.now();
2717
- const lastCall = timerSlot[timerKey]?.lastCall || 0;
2718
- if (now - lastCall >= options.throttle) {
2719
- timerSlot[timerKey] = { lastCall: now };
2720
- callMethod();
2721
- }
2722
- return;
2723
- }
2724
- callMethod();
2725
- };
2726
- // Register with the Context using the wrapped method name
2727
- if (typeof ctx[CONTEXT_REGISTER] === 'function') {
2728
- ctx[CONTEXT_REGISTER](element, wrappedMethodName);
2729
- }
2730
- }
2731
- }
2732
- /**
2733
- * Cleanup context handler for an element instance
2734
- * Called automatically during element disconnection
2735
- */
2736
- function cleanupContextHandler(element) {
2737
- const handlers = element.constructor[CONTEXT_HANDLERS];
2738
- if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2739
- return;
2740
- }
2741
- // Clear any pending debounce timers (per-handler slots)
2742
- const timerSlot = element[CONTEXT_TIMER];
2743
- if (timerSlot && typeof timerSlot === 'object') {
2744
- for (const key of Object.keys(timerSlot)) {
2745
- if (timerSlot[key]?.timeout)
2746
- clearTimeout(timerSlot[key].timeout);
2747
- }
2748
- delete element[CONTEXT_TIMER];
2749
- }
2750
- for (const handler of handlers) {
2751
- // Clean up wrapped method
2752
- const wrappedMethodName = `__wrapped_${handler.methodName}`;
2753
- delete element[wrappedMethodName];
2754
- }
2755
- // Unregister from Context if available
2756
- const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2757
- if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2758
- ctx[CONTEXT_UNREGISTER](element);
2759
- }
2760
- delete element[NAVIGATION_CONTEXT_INSTANCE];
2761
- delete element[CONTEXT_CALLED];
2762
- }
2763
-
2764
2860
  /**
2765
2861
  * Template system for Snice v3.0.0
2766
2862
  * Provides html`` and css`` tagged template processors with differential rendering
@@ -2996,14 +3092,32 @@ const NOT_COMMITTED = Symbol('not-committed');
2996
3092
  function markPreUpgradePropertyBinding(element, propertyName) {
2997
3093
  const tagName = element.localName;
2998
3094
  if (!tagName?.includes('-'))
2999
- return;
3095
+ return false;
3000
3096
  const registry = element.ownerDocument?.defaultView?.customElements ?? globalThis.customElements;
3001
3097
  if (registry?.get(tagName))
3002
- return;
3098
+ return false;
3003
3099
  const target = element;
3004
3100
  if (!target[PRE_UPGRADE_PROPERTY_BINDINGS])
3005
3101
  target[PRE_UPGRADE_PROPERTY_BINDINGS] = new Set();
3006
3102
  target[PRE_UPGRADE_PROPERTY_BINDINGS].add(propertyName);
3103
+ return true;
3104
+ }
3105
+ function commitPropertyValue(element, propertyName, value) {
3106
+ if (!markPreUpgradePropertyBinding(element, propertyName)) {
3107
+ element[propertyName] = value;
3108
+ return;
3109
+ }
3110
+ // An undefined custom element can inherit a native IDL setter with the same
3111
+ // name. Assignment would invoke that setter (for example HTMLElement.role),
3112
+ // stringify structured data into an attribute, and leave no own value for
3113
+ // @property's upgrade adoption. Park every pre-upgrade binding as the own,
3114
+ // configurable value that the initializer already knows how to replay.
3115
+ Object.defineProperty(element, propertyName, {
3116
+ configurable: true,
3117
+ enumerable: true,
3118
+ writable: true,
3119
+ value,
3120
+ });
3007
3121
  }
3008
3122
  // noChange sentinel - preserves the currently committed value
3009
3123
  const noChange = Symbol.for('snice:no-change');
@@ -4640,15 +4754,13 @@ class SpreadPart extends Part {
4640
4754
  commitProperties(next) {
4641
4755
  for (const key of Object.keys(this.committed)) {
4642
4756
  if (!Object.prototype.hasOwnProperty.call(next, key)) {
4643
- markPreUpgradePropertyBinding(this.element, key);
4644
- this.element[key] = undefined;
4757
+ commitPropertyValue(this.element, key, undefined);
4645
4758
  }
4646
4759
  }
4647
4760
  for (const [key, value] of Object.entries(next)) {
4648
4761
  if (Object.is(this.committed[key], value))
4649
4762
  continue;
4650
- markPreUpgradePropertyBinding(this.element, key);
4651
- this.element[key] = value === nothing ? undefined : value;
4763
+ commitPropertyValue(this.element, key, value === nothing ? undefined : value);
4652
4764
  }
4653
4765
  }
4654
4766
  commitAttributes(next) {
@@ -4775,8 +4887,7 @@ class PropertyPart extends Part {
4775
4887
  return;
4776
4888
  }
4777
4889
  this._committedValue = value;
4778
- markPreUpgradePropertyBinding(this.element, this.name);
4779
- this.element[this.name] = value === nothing ? undefined : value;
4890
+ commitPropertyValue(this.element, this.name, value === nothing ? undefined : value);
4780
4891
  }
4781
4892
  clear() {
4782
4893
  // This part is being destroyed with its owning template. The element is
@@ -6808,7 +6919,7 @@ function applyElementFunctionality(constructor) {
6808
6919
  // Re-establish handlers that get cleaned up on disconnect
6809
6920
  setupEventHandlers(this, this);
6810
6921
  setupResponseHandlers(this, this);
6811
- setupContextHandler(this);
6922
+ void setupContextHandler(this, 'task');
6812
6923
  // Re-establish observers that get cleaned up on disconnect
6813
6924
  try {
6814
6925
  setupObservers(this, this);
@@ -6933,7 +7044,7 @@ function applyElementFunctionality(constructor) {
6933
7044
  }
6934
7045
  setupEventHandlers(this, this);
6935
7046
  setupResponseHandlers(this, this);
6936
- setupContextHandler(this);
7047
+ void setupContextHandler(this, 'task');
6937
7048
  this[INITIALIZED] = true;
6938
7049
  // One-time (per class) sanity check: a @watch name with no matching
6939
7050
  // @property never fires — surface the typo instead of staying silent.
@@ -8638,10 +8749,11 @@ function bindContextDaemons(context) {
8638
8749
  *
8639
8750
  * Daemon instances must already exist in context.daemons. The returned cleanup
8640
8751
  * removes the provider and deactivates framework communication for daemons no
8641
- * longer provided by any context.
8752
+ * longer provided by any context. An optional fetch function is exposed on the
8753
+ * same boundary through getContextFetch() without modifying the context object.
8642
8754
  */
8643
- function provideContext(root, context) {
8644
- const releaseProvider = installContextProvider(root, context);
8755
+ function provideContextBoundary(root, context, options, navigation) {
8756
+ const releaseProvider = installContextProvider(root, context, options.fetch, navigation);
8645
8757
  let releaseDaemons;
8646
8758
  try {
8647
8759
  releaseDaemons = bindContextDaemons(context);
@@ -8659,6 +8771,15 @@ function provideContext(root, context) {
8659
8771
  releaseDaemons?.();
8660
8772
  };
8661
8773
  }
8774
+ function provideContext(root, context, options = {}) {
8775
+ return provideContextBoundary(root, context, options);
8776
+ }
8777
+ /** @internal Router-owned provider including live navigation state. */
8778
+ function provideRouterContext(root, navigation) {
8779
+ return provideContextBoundary(root, navigation.application, {
8780
+ fetch: navigation.fetch,
8781
+ }, navigation);
8782
+ }
8662
8783
 
8663
8784
  // Router historically allows a later Router instance to take over the same
8664
8785
  // target. Track only Router-owned provider releases so that handoff remains
@@ -8702,7 +8823,7 @@ function Router(options) {
8702
8823
  return;
8703
8824
  releaseContextProvider?.();
8704
8825
  routerContextProviders.get(target)?.();
8705
- const releaseProvidedContext = provideContext(target, context);
8826
+ const releaseProvidedContext = provideRouterContext(target, navigationContext);
8706
8827
  let released = false;
8707
8828
  const release = () => {
8708
8829
  if (released)
@@ -9616,6 +9737,7 @@ exports.escapeAttr = escapeAttr;
9616
9737
  exports.escapeHtml = escapeHtml;
9617
9738
  exports.getBodyScrollLockCount = getBodyScrollLockCount;
9618
9739
  exports.getContext = getContext;
9740
+ exports.getContextFetch = getContextFetch;
9619
9741
  exports.getController = getController;
9620
9742
  exports.getSymbol = getSymbol;
9621
9743
  exports.html = html;