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
@@ -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.
@@ -233,7 +233,7 @@ var Snice = (function (exports) {
233
233
  */
234
234
  class Context {
235
235
  constructor(context = {}, placards = [], currentRoute = '', routeParams = {}, fetcher) {
236
- // Stores the SET of wrapped-method names per element so that classes with
236
+ // Stores the SET of wrapped-method names per subscriber so that classes with
237
237
  // multiple @context() handlers register every method, not just the last one.
238
238
  this[_a] = new WeakMap();
239
239
  this[_b] = new Set();
@@ -257,7 +257,7 @@ var Snice = (function (exports) {
257
257
  }
258
258
  }
259
259
  /**
260
- * Register an element to receive context updates
260
+ * Register an element or controller to receive context updates
261
261
  * @internal Used by @context decorator
262
262
  */
263
263
  [(_a = REGISTERED_ELEMENTS, _b = REGISTERED_ELEMENTS_SET, _c = IS_UPDATING, CONTEXT_REGISTER)](element, methodName) {
@@ -271,10 +271,10 @@ var Snice = (function (exports) {
271
271
  this[REGISTERED_ELEMENTS_SET].add(element);
272
272
  }
273
273
  /**
274
- * Unregister an element from receiving context updates. With a methodName,
274
+ * Unregister a subscriber from context updates. With a methodName,
275
275
  * removes only that one handler (so a `once` handler tearing itself down
276
276
  * doesn't kill the element's other @context handlers); without one, removes
277
- * the element entirely (used on disconnect).
277
+ * the subscriber entirely (used on disconnect/detach).
278
278
  * @internal Used by @context decorator cleanup
279
279
  */
280
280
  [CONTEXT_UNREGISTER](element, methodName) {
@@ -315,7 +315,7 @@ var Snice = (function (exports) {
315
315
  return;
316
316
  }
317
317
  this[IS_UPDATING] = true;
318
- // Notify all registered elements by calling EVERY registered method
318
+ // Notify all registered subscribers by calling EVERY registered method
319
319
  // name on each. Classes with multiple @context() methods get all fired.
320
320
  const elementsSet = this[REGISTERED_ELEMENTS_SET];
321
321
  const elementsMap = this[REGISTERED_ELEMENTS];
@@ -337,7 +337,7 @@ var Snice = (function (exports) {
337
337
  this[IS_UPDATING] = false;
338
338
  }
339
339
  /**
340
- * Notify a specific element of the current context state
340
+ * Notify a specific subscriber of the current context state
341
341
  * @internal Used by @context decorator
342
342
  */
343
343
  [CONTEXT_NOTIFY_ELEMENT](element) {
@@ -346,7 +346,12 @@ var Snice = (function (exports) {
346
346
  return;
347
347
  for (const methodName of names) {
348
348
  if (typeof element[methodName] === 'function') {
349
- element[methodName](this);
349
+ try {
350
+ element[methodName](this);
351
+ }
352
+ catch (error) {
353
+ console.error(`Error calling @context method ${methodName}:`, error);
354
+ }
350
355
  }
351
356
  }
352
357
  }
@@ -678,11 +683,17 @@ var Snice = (function (exports) {
678
683
  */
679
684
  function createDebounced(fn, delay) {
680
685
  let timer = null;
681
- return function (...args) {
686
+ const debounced = function (...args) {
682
687
  if (timer !== null)
683
688
  clearTimeout(timer);
684
689
  timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay);
685
690
  };
691
+ debounced.cancel = () => {
692
+ if (timer !== null)
693
+ clearTimeout(timer);
694
+ timer = null;
695
+ };
696
+ return debounced;
686
697
  }
687
698
  /**
688
699
  * Create a throttled version of a function (leading + trailing edge)
@@ -1139,7 +1150,7 @@ var Snice = (function (exports) {
1139
1150
  * boundary. Public callers use provideContext(), which also activates the
1140
1151
  * daemon instances declared by the context.
1141
1152
  */
1142
- function installContextProvider(root, context) {
1153
+ function installContextProvider(root, context, fetch, navigation) {
1143
1154
  if (!root || typeof root.addEventListener !== 'function' || typeof root.removeEventListener !== 'function') {
1144
1155
  throw new TypeError('provideContext() requires an EventTarget root.');
1145
1156
  }
@@ -1151,9 +1162,26 @@ var Snice = (function (exports) {
1151
1162
  }
1152
1163
  const listener = (event) => {
1153
1164
  const request = event;
1154
- if (!request.detail || request.detail.context !== undefined)
1165
+ if (!request.detail)
1166
+ return;
1167
+ if (request.detail.kind === 'application') {
1168
+ if (request.detail.context !== undefined)
1169
+ return;
1170
+ request.detail.context = context;
1171
+ }
1172
+ else if (request.detail.kind === 'fetch') {
1173
+ if (!fetch || request.detail.fetch !== undefined)
1174
+ return;
1175
+ request.detail.fetch = fetch;
1176
+ }
1177
+ else if (request.detail.kind === 'navigation') {
1178
+ if (!navigation || request.detail.navigation !== undefined)
1179
+ return;
1180
+ request.detail.navigation = navigation;
1181
+ }
1182
+ else {
1155
1183
  return;
1156
- request.detail.context = context;
1184
+ }
1157
1185
  request.preventDefault();
1158
1186
  request.stopPropagation();
1159
1187
  };
@@ -1183,7 +1211,7 @@ var Snice = (function (exports) {
1183
1211
  }
1184
1212
  const target = contextTarget(source);
1185
1213
  if (target) {
1186
- const detail = {};
1214
+ const detail = { kind: 'application' };
1187
1215
  target.dispatchEvent(new CustomEvent(CONTEXT_REQUEST_EVENT, {
1188
1216
  bubbles: true,
1189
1217
  composed: true,
@@ -1202,6 +1230,39 @@ var Snice = (function (exports) {
1202
1230
  }
1203
1231
  return undefined;
1204
1232
  }
1233
+ /**
1234
+ * Resolve the fetch function supplied by the nearest context provider.
1235
+ * Router providers expose their ContextAwareFetcher-bound function here;
1236
+ * explicit providers may supply the same dependency for non-router apps and
1237
+ * tests without adding a reserved property to AppContext.
1238
+ */
1239
+ function getContextFetch(source) {
1240
+ const target = contextTarget(source);
1241
+ if (!target)
1242
+ return undefined;
1243
+ const detail = { kind: 'fetch' };
1244
+ target.dispatchEvent(new CustomEvent(CONTEXT_REQUEST_EVENT, {
1245
+ bubbles: true,
1246
+ composed: true,
1247
+ cancelable: true,
1248
+ detail,
1249
+ }));
1250
+ return detail.fetch;
1251
+ }
1252
+ /** @internal Resolve the router Context visible to an element or controller. */
1253
+ function getNavigationContext(source) {
1254
+ const target = contextTarget(source);
1255
+ if (!target)
1256
+ return undefined;
1257
+ const detail = { kind: 'navigation' };
1258
+ target.dispatchEvent(new CustomEvent(CONTEXT_REQUEST_EVENT, {
1259
+ bubbles: true,
1260
+ composed: true,
1261
+ cancelable: true,
1262
+ detail,
1263
+ }));
1264
+ return detail.navigation;
1265
+ }
1205
1266
 
1206
1267
  function isDaemonInstance(value) {
1207
1268
  return !!value
@@ -1865,12 +1926,13 @@ var Snice = (function (exports) {
1865
1926
  }
1866
1927
  // Initialize cleanup object if needed
1867
1928
  if (!instance[CLEANUP]) {
1868
- instance[CLEANUP] = { events: [], channels: [], observers: [] };
1929
+ instance[CLEANUP] = { events: [], eventCancels: [], channels: [], observers: [] };
1869
1930
  }
1870
- else if (!instance[CLEANUP].events) {
1931
+ if (!instance[CLEANUP].events)
1871
1932
  instance[CLEANUP].events = [];
1872
- }
1873
- else if (instance[CLEANUP].events.length > 0) {
1933
+ if (!instance[CLEANUP].eventCancels)
1934
+ instance[CLEANUP].eventCancels = [];
1935
+ if (instance[CLEANUP].events.length > 0 || instance[CLEANUP].eventCancels.length > 0) {
1874
1936
  // Events already set up - clean them up first to avoid duplicates
1875
1937
  cleanupEventHandlers(instance);
1876
1938
  }
@@ -1900,7 +1962,9 @@ var Snice = (function (exports) {
1900
1962
  warnIfModifierMisuse(handler.eventName);
1901
1963
  // Apply debounce (takes precedence over throttle)
1902
1964
  if (handlerOptions.debounce && handlerOptions.debounce > 0) {
1903
- boundMethod = createDebounced(boundMethod, handlerOptions.debounce);
1965
+ const debounced = createDebounced(boundMethod, handlerOptions.debounce);
1966
+ instance[CLEANUP].eventCancels.push(() => debounced.cancel());
1967
+ boundMethod = debounced;
1904
1968
  }
1905
1969
  else if (handlerOptions.throttle && handlerOptions.throttle > 0) {
1906
1970
  boundMethod = throttle$1(boundMethod, handlerOptions.throttle);
@@ -2073,12 +2137,190 @@ var Snice = (function (exports) {
2073
2137
  function cleanupEventHandlers(instance) {
2074
2138
  if (!instance[CLEANUP]?.events)
2075
2139
  return;
2140
+ for (const cancel of instance[CLEANUP].eventCancels ?? [])
2141
+ cancel();
2142
+ instance[CLEANUP].eventCancels = [];
2076
2143
  for (const { target, eventName, handler, options } of instance[CLEANUP].events) {
2077
2144
  target.removeEventListener(eventName, handler, options);
2078
2145
  }
2079
2146
  instance[CLEANUP].events = [];
2080
2147
  }
2081
2148
 
2149
+ /**
2150
+ * @context decorator for receiving router context updates
2151
+ */
2152
+ const CONTEXT_HANDLERS = getSymbol('context-handlers');
2153
+ /**
2154
+ * @context decorator for receiving router context updates
2155
+ *
2156
+ * @example
2157
+ * ```typescript
2158
+ * @element('my-layout')
2159
+ * class MyLayout extends HTMLElement {
2160
+ * @context
2161
+ * handleContext(ctx: Context) {
2162
+ * this.renderNav(ctx.placards, ctx.currentRoute);
2163
+ * }
2164
+ *
2165
+ * @context({ debounce: 300 })
2166
+ * handleContextDebounced(ctx: Context) {
2167
+ * // Called after 300ms of no updates
2168
+ * }
2169
+ * }
2170
+ * ```
2171
+ */
2172
+ function context$1(options = {}) {
2173
+ return function (originalMethod, context) {
2174
+ const methodName = context.name;
2175
+ context.addInitializer(function () {
2176
+ const constructor = this.constructor;
2177
+ // hasOwnProperty guards so subclasses don't mutate parent state via
2178
+ // the prototype chain.
2179
+ if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_METHODS)) {
2180
+ constructor[CONTEXT_METHODS] = new Set();
2181
+ }
2182
+ if (constructor[CONTEXT_METHODS].has(originalMethod))
2183
+ return;
2184
+ constructor[CONTEXT_METHODS].add(originalMethod);
2185
+ if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_HANDLERS)) {
2186
+ const inherited = constructor[CONTEXT_HANDLERS];
2187
+ constructor[CONTEXT_HANDLERS] = inherited ? [...inherited] : [];
2188
+ }
2189
+ constructor[CONTEXT_HANDLERS].push({
2190
+ methodName,
2191
+ method: originalMethod,
2192
+ options,
2193
+ });
2194
+ });
2195
+ return originalMethod;
2196
+ };
2197
+ }
2198
+ /**
2199
+ * Set up context handlers for an element or attached controller.
2200
+ * Called automatically by the corresponding managed lifecycle.
2201
+ */
2202
+ function setupContextHandler(element, catchUp = false) {
2203
+ const handlers = element.constructor[CONTEXT_HANDLERS];
2204
+ if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2205
+ return;
2206
+ }
2207
+ // Get the Context instance from the router
2208
+ const ctx = element[CONTEXT_HANDLER] ?? getNavigationContext(element);
2209
+ if (!ctx) {
2210
+ return;
2211
+ }
2212
+ // Store the Context instance for cleanup
2213
+ element[NAVIGATION_CONTEXT_INSTANCE] = ctx;
2214
+ // A controller can finish attaching after the Router has already announced
2215
+ // the current navigation. Track whether an update arrives during this turn;
2216
+ // if none does, catch it up once from the Context's current state.
2217
+ let receivedContextUpdate = false;
2218
+ // Register each handler with the Context
2219
+ for (const handler of handlers) {
2220
+ const { methodName, method, options } = handler;
2221
+ const wrappedMethodName = `__wrapped_${methodName}`;
2222
+ // Create wrapped method with timing controls
2223
+ element[wrappedMethodName] = function (context) {
2224
+ receivedContextUpdate = true;
2225
+ // `once` is tracked PER method (not a single element-level flag) so
2226
+ // multiple once handlers on the same element each fire.
2227
+ const called = element[CONTEXT_CALLED] ||
2228
+ (element[CONTEXT_CALLED] = new Set());
2229
+ if (options.once && called.has(methodName)) {
2230
+ return;
2231
+ }
2232
+ const callMethod = () => {
2233
+ method.call(element, context);
2234
+ // Handle once option
2235
+ if (options.once) {
2236
+ called.add(methodName);
2237
+ // Unregister ONLY this handler, not the whole element — otherwise the
2238
+ // element's other @context handlers would stop receiving updates.
2239
+ const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2240
+ if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2241
+ ctx[CONTEXT_UNREGISTER](element, wrappedMethodName);
2242
+ }
2243
+ }
2244
+ };
2245
+ // Per-handler timer slot to avoid debounce/throttle handlers on the same
2246
+ // element overwriting each other's state.
2247
+ const timerSlot = element[CONTEXT_TIMER] ||
2248
+ (element[CONTEXT_TIMER] = {});
2249
+ const timerKey = methodName;
2250
+ if (options.debounce) {
2251
+ clearTimeout(timerSlot[timerKey]?.timeout);
2252
+ timerSlot[timerKey] = {
2253
+ timeout: setTimeout(callMethod, options.debounce),
2254
+ };
2255
+ return;
2256
+ }
2257
+ if (options.throttle) {
2258
+ const now = Date.now();
2259
+ const lastCall = timerSlot[timerKey]?.lastCall || 0;
2260
+ if (now - lastCall >= options.throttle) {
2261
+ timerSlot[timerKey] = { lastCall: now };
2262
+ callMethod();
2263
+ }
2264
+ return;
2265
+ }
2266
+ callMethod();
2267
+ };
2268
+ // Register with the Context using the wrapped method name
2269
+ if (typeof ctx[CONTEXT_REGISTER] === 'function') {
2270
+ ctx[CONTEXT_REGISTER](element, wrappedMethodName);
2271
+ }
2272
+ }
2273
+ if (catchUp && typeof ctx[CONTEXT_NOTIFY_ELEMENT] === 'function') {
2274
+ return new Promise((resolve) => {
2275
+ const notifyIfNeeded = () => {
2276
+ try {
2277
+ if (!receivedContextUpdate
2278
+ && element[NAVIGATION_CONTEXT_INSTANCE] === ctx) {
2279
+ ctx[CONTEXT_NOTIFY_ELEMENT](element);
2280
+ }
2281
+ }
2282
+ finally {
2283
+ resolve();
2284
+ }
2285
+ };
2286
+ if (catchUp === 'task')
2287
+ setTimeout(notifyIfNeeded, 0);
2288
+ else
2289
+ queueMicrotask(notifyIfNeeded);
2290
+ });
2291
+ }
2292
+ }
2293
+ /**
2294
+ * Clean up context handlers for an element or attached controller.
2295
+ */
2296
+ function cleanupContextHandler(element) {
2297
+ const handlers = element.constructor[CONTEXT_HANDLERS];
2298
+ if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2299
+ return;
2300
+ }
2301
+ // Clear any pending debounce timers (per-handler slots)
2302
+ const timerSlot = element[CONTEXT_TIMER];
2303
+ if (timerSlot && typeof timerSlot === 'object') {
2304
+ for (const key of Object.keys(timerSlot)) {
2305
+ if (timerSlot[key]?.timeout)
2306
+ clearTimeout(timerSlot[key].timeout);
2307
+ }
2308
+ delete element[CONTEXT_TIMER];
2309
+ }
2310
+ for (const handler of handlers) {
2311
+ // Clean up wrapped method
2312
+ const wrappedMethodName = `__wrapped_${handler.methodName}`;
2313
+ delete element[wrappedMethodName];
2314
+ }
2315
+ // Unregister from Context if available
2316
+ const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2317
+ if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2318
+ ctx[CONTEXT_UNREGISTER](element);
2319
+ }
2320
+ delete element[NAVIGATION_CONTEXT_INSTANCE];
2321
+ delete element[CONTEXT_CALLED];
2322
+ }
2323
+
2082
2324
  // Controller-scoped cleanup registry
2083
2325
  class ControllerScope {
2084
2326
  constructor() {
@@ -2412,6 +2654,10 @@ var Snice = (function (exports) {
2412
2654
  await scope.runOperation(async () => {
2413
2655
  await controllerInstance.attach(element);
2414
2656
  });
2657
+ // @context has the same managed lifecycle on controllers as @on,
2658
+ // @observe and @respond. Catch up controllers whose async attach completed
2659
+ // after the Router announced the current navigation.
2660
+ await setupContextHandler(controllerInstance, 'microtask');
2415
2661
  // Setup @observe observers for controller
2416
2662
  setupObservers(controllerInstance, element);
2417
2663
  // Setup @channel handlers for controller
@@ -2477,6 +2723,8 @@ var Snice = (function (exports) {
2477
2723
  cleanupResponseHandlers(controllerInstance);
2478
2724
  // Cleanup @on event handlers for controller
2479
2725
  cleanupEventHandlers(controllerInstance);
2726
+ // Cleanup @context handlers for controller
2727
+ cleanupContextHandler(controllerInstance);
2480
2728
  // Cleanup the controller scope
2481
2729
  if (scope) {
2482
2730
  await scope.cleanup();
@@ -2610,158 +2858,6 @@ var Snice = (function (exports) {
2610
2858
  globalThis.sniceNativeControllerObserver = observer;
2611
2859
  }
2612
2860
 
2613
- /**
2614
- * @context decorator for receiving router context updates
2615
- */
2616
- const CONTEXT_HANDLERS = getSymbol('context-handlers');
2617
- /**
2618
- * @context decorator for receiving router context updates
2619
- *
2620
- * @example
2621
- * ```typescript
2622
- * @element('my-layout')
2623
- * class MyLayout extends HTMLElement {
2624
- * @context
2625
- * handleContext(ctx: Context) {
2626
- * this.renderNav(ctx.placards, ctx.currentRoute);
2627
- * }
2628
- *
2629
- * @context({ debounce: 300 })
2630
- * handleContextDebounced(ctx: Context) {
2631
- * // Called after 300ms of no updates
2632
- * }
2633
- * }
2634
- * ```
2635
- */
2636
- function context$1(options = {}) {
2637
- return function (originalMethod, context) {
2638
- const methodName = context.name;
2639
- context.addInitializer(function () {
2640
- const constructor = this.constructor;
2641
- // hasOwnProperty guards so subclasses don't mutate parent state via
2642
- // the prototype chain.
2643
- if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_METHODS)) {
2644
- constructor[CONTEXT_METHODS] = new Set();
2645
- }
2646
- if (constructor[CONTEXT_METHODS].has(originalMethod))
2647
- return;
2648
- constructor[CONTEXT_METHODS].add(originalMethod);
2649
- if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_HANDLERS)) {
2650
- const inherited = constructor[CONTEXT_HANDLERS];
2651
- constructor[CONTEXT_HANDLERS] = inherited ? [...inherited] : [];
2652
- }
2653
- constructor[CONTEXT_HANDLERS].push({
2654
- methodName,
2655
- method: originalMethod,
2656
- options,
2657
- });
2658
- });
2659
- return originalMethod;
2660
- };
2661
- }
2662
- /**
2663
- * Setup context handler for an element instance
2664
- * Called automatically during element connection
2665
- */
2666
- function setupContextHandler(element) {
2667
- const handlers = element.constructor[CONTEXT_HANDLERS];
2668
- if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2669
- return;
2670
- }
2671
- // Get the Context instance from the router
2672
- const ctx = element[CONTEXT_HANDLER];
2673
- if (!ctx) {
2674
- return;
2675
- }
2676
- // Store the Context instance for cleanup
2677
- element[NAVIGATION_CONTEXT_INSTANCE] = ctx;
2678
- // Register each handler with the Context
2679
- for (const handler of handlers) {
2680
- const { methodName, method, options } = handler;
2681
- const wrappedMethodName = `__wrapped_${methodName}`;
2682
- // Create wrapped method with timing controls
2683
- element[wrappedMethodName] = function (context) {
2684
- // `once` is tracked PER method (not a single element-level flag) so
2685
- // multiple once handlers on the same element each fire.
2686
- const called = element[CONTEXT_CALLED] ||
2687
- (element[CONTEXT_CALLED] = new Set());
2688
- if (options.once && called.has(methodName)) {
2689
- return;
2690
- }
2691
- const callMethod = () => {
2692
- method.call(element, context);
2693
- // Handle once option
2694
- if (options.once) {
2695
- called.add(methodName);
2696
- // Unregister ONLY this handler, not the whole element — otherwise the
2697
- // element's other @context handlers would stop receiving updates.
2698
- const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2699
- if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2700
- ctx[CONTEXT_UNREGISTER](element, wrappedMethodName);
2701
- }
2702
- }
2703
- };
2704
- // Per-handler timer slot to avoid debounce/throttle handlers on the same
2705
- // element overwriting each other's state.
2706
- const timerSlot = element[CONTEXT_TIMER] ||
2707
- (element[CONTEXT_TIMER] = {});
2708
- const timerKey = methodName;
2709
- if (options.debounce) {
2710
- clearTimeout(timerSlot[timerKey]?.timeout);
2711
- timerSlot[timerKey] = {
2712
- timeout: setTimeout(callMethod, options.debounce),
2713
- };
2714
- return;
2715
- }
2716
- if (options.throttle) {
2717
- const now = Date.now();
2718
- const lastCall = timerSlot[timerKey]?.lastCall || 0;
2719
- if (now - lastCall >= options.throttle) {
2720
- timerSlot[timerKey] = { lastCall: now };
2721
- callMethod();
2722
- }
2723
- return;
2724
- }
2725
- callMethod();
2726
- };
2727
- // Register with the Context using the wrapped method name
2728
- if (typeof ctx[CONTEXT_REGISTER] === 'function') {
2729
- ctx[CONTEXT_REGISTER](element, wrappedMethodName);
2730
- }
2731
- }
2732
- }
2733
- /**
2734
- * Cleanup context handler for an element instance
2735
- * Called automatically during element disconnection
2736
- */
2737
- function cleanupContextHandler(element) {
2738
- const handlers = element.constructor[CONTEXT_HANDLERS];
2739
- if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2740
- return;
2741
- }
2742
- // Clear any pending debounce timers (per-handler slots)
2743
- const timerSlot = element[CONTEXT_TIMER];
2744
- if (timerSlot && typeof timerSlot === 'object') {
2745
- for (const key of Object.keys(timerSlot)) {
2746
- if (timerSlot[key]?.timeout)
2747
- clearTimeout(timerSlot[key].timeout);
2748
- }
2749
- delete element[CONTEXT_TIMER];
2750
- }
2751
- for (const handler of handlers) {
2752
- // Clean up wrapped method
2753
- const wrappedMethodName = `__wrapped_${handler.methodName}`;
2754
- delete element[wrappedMethodName];
2755
- }
2756
- // Unregister from Context if available
2757
- const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2758
- if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2759
- ctx[CONTEXT_UNREGISTER](element);
2760
- }
2761
- delete element[NAVIGATION_CONTEXT_INSTANCE];
2762
- delete element[CONTEXT_CALLED];
2763
- }
2764
-
2765
2861
  /**
2766
2862
  * Template system for Snice v3.0.0
2767
2863
  * Provides html`` and css`` tagged template processors with differential rendering
@@ -2997,14 +3093,32 @@ var Snice = (function (exports) {
2997
3093
  function markPreUpgradePropertyBinding(element, propertyName) {
2998
3094
  const tagName = element.localName;
2999
3095
  if (!tagName?.includes('-'))
3000
- return;
3096
+ return false;
3001
3097
  const registry = element.ownerDocument?.defaultView?.customElements ?? globalThis.customElements;
3002
3098
  if (registry?.get(tagName))
3003
- return;
3099
+ return false;
3004
3100
  const target = element;
3005
3101
  if (!target[PRE_UPGRADE_PROPERTY_BINDINGS])
3006
3102
  target[PRE_UPGRADE_PROPERTY_BINDINGS] = new Set();
3007
3103
  target[PRE_UPGRADE_PROPERTY_BINDINGS].add(propertyName);
3104
+ return true;
3105
+ }
3106
+ function commitPropertyValue(element, propertyName, value) {
3107
+ if (!markPreUpgradePropertyBinding(element, propertyName)) {
3108
+ element[propertyName] = value;
3109
+ return;
3110
+ }
3111
+ // An undefined custom element can inherit a native IDL setter with the same
3112
+ // name. Assignment would invoke that setter (for example HTMLElement.role),
3113
+ // stringify structured data into an attribute, and leave no own value for
3114
+ // @property's upgrade adoption. Park every pre-upgrade binding as the own,
3115
+ // configurable value that the initializer already knows how to replay.
3116
+ Object.defineProperty(element, propertyName, {
3117
+ configurable: true,
3118
+ enumerable: true,
3119
+ writable: true,
3120
+ value,
3121
+ });
3008
3122
  }
3009
3123
  // noChange sentinel - preserves the currently committed value
3010
3124
  const noChange = Symbol.for('snice:no-change');
@@ -4641,15 +4755,13 @@ var Snice = (function (exports) {
4641
4755
  commitProperties(next) {
4642
4756
  for (const key of Object.keys(this.committed)) {
4643
4757
  if (!Object.prototype.hasOwnProperty.call(next, key)) {
4644
- markPreUpgradePropertyBinding(this.element, key);
4645
- this.element[key] = undefined;
4758
+ commitPropertyValue(this.element, key, undefined);
4646
4759
  }
4647
4760
  }
4648
4761
  for (const [key, value] of Object.entries(next)) {
4649
4762
  if (Object.is(this.committed[key], value))
4650
4763
  continue;
4651
- markPreUpgradePropertyBinding(this.element, key);
4652
- this.element[key] = value === nothing ? undefined : value;
4764
+ commitPropertyValue(this.element, key, value === nothing ? undefined : value);
4653
4765
  }
4654
4766
  }
4655
4767
  commitAttributes(next) {
@@ -4776,8 +4888,7 @@ var Snice = (function (exports) {
4776
4888
  return;
4777
4889
  }
4778
4890
  this._committedValue = value;
4779
- markPreUpgradePropertyBinding(this.element, this.name);
4780
- this.element[this.name] = value === nothing ? undefined : value;
4891
+ commitPropertyValue(this.element, this.name, value === nothing ? undefined : value);
4781
4892
  }
4782
4893
  clear() {
4783
4894
  // This part is being destroyed with its owning template. The element is
@@ -6809,7 +6920,7 @@ var Snice = (function (exports) {
6809
6920
  // Re-establish handlers that get cleaned up on disconnect
6810
6921
  setupEventHandlers(this, this);
6811
6922
  setupResponseHandlers(this, this);
6812
- setupContextHandler(this);
6923
+ void setupContextHandler(this, 'task');
6813
6924
  // Re-establish observers that get cleaned up on disconnect
6814
6925
  try {
6815
6926
  setupObservers(this, this);
@@ -6934,7 +7045,7 @@ var Snice = (function (exports) {
6934
7045
  }
6935
7046
  setupEventHandlers(this, this);
6936
7047
  setupResponseHandlers(this, this);
6937
- setupContextHandler(this);
7048
+ void setupContextHandler(this, 'task');
6938
7049
  this[INITIALIZED] = true;
6939
7050
  // One-time (per class) sanity check: a @watch name with no matching
6940
7051
  // @property never fires — surface the typo instead of staying silent.
@@ -8639,10 +8750,11 @@ var Snice = (function (exports) {
8639
8750
  *
8640
8751
  * Daemon instances must already exist in context.daemons. The returned cleanup
8641
8752
  * removes the provider and deactivates framework communication for daemons no
8642
- * longer provided by any context.
8753
+ * longer provided by any context. An optional fetch function is exposed on the
8754
+ * same boundary through getContextFetch() without modifying the context object.
8643
8755
  */
8644
- function provideContext(root, context) {
8645
- const releaseProvider = installContextProvider(root, context);
8756
+ function provideContextBoundary(root, context, options, navigation) {
8757
+ const releaseProvider = installContextProvider(root, context, options.fetch, navigation);
8646
8758
  let releaseDaemons;
8647
8759
  try {
8648
8760
  releaseDaemons = bindContextDaemons(context);
@@ -8660,6 +8772,15 @@ var Snice = (function (exports) {
8660
8772
  releaseDaemons?.();
8661
8773
  };
8662
8774
  }
8775
+ function provideContext(root, context, options = {}) {
8776
+ return provideContextBoundary(root, context, options);
8777
+ }
8778
+ /** @internal Router-owned provider including live navigation state. */
8779
+ function provideRouterContext(root, navigation) {
8780
+ return provideContextBoundary(root, navigation.application, {
8781
+ fetch: navigation.fetch,
8782
+ }, navigation);
8783
+ }
8663
8784
 
8664
8785
  // Router historically allows a later Router instance to take over the same
8665
8786
  // target. Track only Router-owned provider releases so that handoff remains
@@ -8703,7 +8824,7 @@ var Snice = (function (exports) {
8703
8824
  return;
8704
8825
  releaseContextProvider?.();
8705
8826
  routerContextProviders.get(target)?.();
8706
- const releaseProvidedContext = provideContext(target, context);
8827
+ const releaseProvidedContext = provideRouterContext(target, navigationContext);
8707
8828
  let released = false;
8708
8829
  const release = () => {
8709
8830
  if (released)
@@ -9617,6 +9738,7 @@ var Snice = (function (exports) {
9617
9738
  exports.escapeHtml = escapeHtml;
9618
9739
  exports.getBodyScrollLockCount = getBodyScrollLockCount;
9619
9740
  exports.getContext = getContext;
9741
+ exports.getContextFetch = getContextFetch;
9620
9742
  exports.getController = getController;
9621
9743
  exports.getSymbol = getSymbol;
9622
9744
  exports.html = html;