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.3.0
2
+ * snice v7.4.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.
@@ -8,7 +8,7 @@ var Snice = (function (exports) {
8
8
  'use strict';
9
9
 
10
10
  /*!
11
- * snice v7.2.0
11
+ * snice v7.3.0
12
12
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
13
13
  * (c) 2024
14
14
  * Released under the MIT License.
@@ -239,7 +239,7 @@ var Snice = (function (exports) {
239
239
  */
240
240
  class Context {
241
241
  constructor(context = {}, placards = [], currentRoute = '', routeParams = {}, fetcher) {
242
- // Stores the SET of wrapped-method names per element so that classes with
242
+ // Stores the SET of wrapped-method names per subscriber so that classes with
243
243
  // multiple @context() handlers register every method, not just the last one.
244
244
  this[_a] = new WeakMap();
245
245
  this[_b] = new Set();
@@ -263,7 +263,7 @@ var Snice = (function (exports) {
263
263
  }
264
264
  }
265
265
  /**
266
- * Register an element to receive context updates
266
+ * Register an element or controller to receive context updates
267
267
  * @internal Used by @context decorator
268
268
  */
269
269
  [(_a = REGISTERED_ELEMENTS, _b = REGISTERED_ELEMENTS_SET, _c = IS_UPDATING, CONTEXT_REGISTER)](element, methodName) {
@@ -277,10 +277,10 @@ var Snice = (function (exports) {
277
277
  this[REGISTERED_ELEMENTS_SET].add(element);
278
278
  }
279
279
  /**
280
- * Unregister an element from receiving context updates. With a methodName,
280
+ * Unregister a subscriber from context updates. With a methodName,
281
281
  * removes only that one handler (so a `once` handler tearing itself down
282
282
  * doesn't kill the element's other @context handlers); without one, removes
283
- * the element entirely (used on disconnect).
283
+ * the subscriber entirely (used on disconnect/detach).
284
284
  * @internal Used by @context decorator cleanup
285
285
  */
286
286
  [CONTEXT_UNREGISTER](element, methodName) {
@@ -321,7 +321,7 @@ var Snice = (function (exports) {
321
321
  return;
322
322
  }
323
323
  this[IS_UPDATING] = true;
324
- // Notify all registered elements by calling EVERY registered method
324
+ // Notify all registered subscribers by calling EVERY registered method
325
325
  // name on each. Classes with multiple @context() methods get all fired.
326
326
  const elementsSet = this[REGISTERED_ELEMENTS_SET];
327
327
  const elementsMap = this[REGISTERED_ELEMENTS];
@@ -343,7 +343,7 @@ var Snice = (function (exports) {
343
343
  this[IS_UPDATING] = false;
344
344
  }
345
345
  /**
346
- * Notify a specific element of the current context state
346
+ * Notify a specific subscriber of the current context state
347
347
  * @internal Used by @context decorator
348
348
  */
349
349
  [CONTEXT_NOTIFY_ELEMENT](element) {
@@ -352,7 +352,12 @@ var Snice = (function (exports) {
352
352
  return;
353
353
  for (const methodName of names) {
354
354
  if (typeof element[methodName] === 'function') {
355
- element[methodName](this);
355
+ try {
356
+ element[methodName](this);
357
+ }
358
+ catch (error) {
359
+ console.error(`Error calling @context method ${methodName}:`, error);
360
+ }
356
361
  }
357
362
  }
358
363
  }
@@ -684,11 +689,17 @@ var Snice = (function (exports) {
684
689
  */
685
690
  function createDebounced(fn, delay) {
686
691
  let timer = null;
687
- return function (...args) {
692
+ const debounced = function (...args) {
688
693
  if (timer !== null)
689
694
  clearTimeout(timer);
690
695
  timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay);
691
696
  };
697
+ debounced.cancel = () => {
698
+ if (timer !== null)
699
+ clearTimeout(timer);
700
+ timer = null;
701
+ };
702
+ return debounced;
692
703
  }
693
704
  /**
694
705
  * Create a throttled version of a function (leading + trailing edge)
@@ -1145,7 +1156,7 @@ var Snice = (function (exports) {
1145
1156
  * boundary. Public callers use provideContext(), which also activates the
1146
1157
  * daemon instances declared by the context.
1147
1158
  */
1148
- function installContextProvider(root, context) {
1159
+ function installContextProvider(root, context, fetch, navigation) {
1149
1160
  if (!root || typeof root.addEventListener !== 'function' || typeof root.removeEventListener !== 'function') {
1150
1161
  throw new TypeError('provideContext() requires an EventTarget root.');
1151
1162
  }
@@ -1157,9 +1168,26 @@ var Snice = (function (exports) {
1157
1168
  }
1158
1169
  const listener = (event) => {
1159
1170
  const request = event;
1160
- if (!request.detail || request.detail.context !== undefined)
1171
+ if (!request.detail)
1172
+ return;
1173
+ if (request.detail.kind === 'application') {
1174
+ if (request.detail.context !== undefined)
1175
+ return;
1176
+ request.detail.context = context;
1177
+ }
1178
+ else if (request.detail.kind === 'fetch') {
1179
+ if (!fetch || request.detail.fetch !== undefined)
1180
+ return;
1181
+ request.detail.fetch = fetch;
1182
+ }
1183
+ else if (request.detail.kind === 'navigation') {
1184
+ if (!navigation || request.detail.navigation !== undefined)
1185
+ return;
1186
+ request.detail.navigation = navigation;
1187
+ }
1188
+ else {
1161
1189
  return;
1162
- request.detail.context = context;
1190
+ }
1163
1191
  request.preventDefault();
1164
1192
  request.stopPropagation();
1165
1193
  };
@@ -1189,7 +1217,7 @@ var Snice = (function (exports) {
1189
1217
  }
1190
1218
  const target = contextTarget(source);
1191
1219
  if (target) {
1192
- const detail = {};
1220
+ const detail = { kind: 'application' };
1193
1221
  target.dispatchEvent(new CustomEvent(CONTEXT_REQUEST_EVENT, {
1194
1222
  bubbles: true,
1195
1223
  composed: true,
@@ -1208,6 +1236,39 @@ var Snice = (function (exports) {
1208
1236
  }
1209
1237
  return undefined;
1210
1238
  }
1239
+ /**
1240
+ * Resolve the fetch function supplied by the nearest context provider.
1241
+ * Router providers expose their ContextAwareFetcher-bound function here;
1242
+ * explicit providers may supply the same dependency for non-router apps and
1243
+ * tests without adding a reserved property to AppContext.
1244
+ */
1245
+ function getContextFetch(source) {
1246
+ const target = contextTarget(source);
1247
+ if (!target)
1248
+ return undefined;
1249
+ const detail = { kind: 'fetch' };
1250
+ target.dispatchEvent(new CustomEvent(CONTEXT_REQUEST_EVENT, {
1251
+ bubbles: true,
1252
+ composed: true,
1253
+ cancelable: true,
1254
+ detail,
1255
+ }));
1256
+ return detail.fetch;
1257
+ }
1258
+ /** @internal Resolve the router Context visible to an element or controller. */
1259
+ function getNavigationContext(source) {
1260
+ const target = contextTarget(source);
1261
+ if (!target)
1262
+ return undefined;
1263
+ const detail = { kind: 'navigation' };
1264
+ target.dispatchEvent(new CustomEvent(CONTEXT_REQUEST_EVENT, {
1265
+ bubbles: true,
1266
+ composed: true,
1267
+ cancelable: true,
1268
+ detail,
1269
+ }));
1270
+ return detail.navigation;
1271
+ }
1211
1272
 
1212
1273
  function isDaemonInstance(value) {
1213
1274
  return !!value
@@ -1871,12 +1932,13 @@ var Snice = (function (exports) {
1871
1932
  }
1872
1933
  // Initialize cleanup object if needed
1873
1934
  if (!instance[CLEANUP]) {
1874
- instance[CLEANUP] = { events: [], channels: [], observers: [] };
1935
+ instance[CLEANUP] = { events: [], eventCancels: [], channels: [], observers: [] };
1875
1936
  }
1876
- else if (!instance[CLEANUP].events) {
1937
+ if (!instance[CLEANUP].events)
1877
1938
  instance[CLEANUP].events = [];
1878
- }
1879
- else if (instance[CLEANUP].events.length > 0) {
1939
+ if (!instance[CLEANUP].eventCancels)
1940
+ instance[CLEANUP].eventCancels = [];
1941
+ if (instance[CLEANUP].events.length > 0 || instance[CLEANUP].eventCancels.length > 0) {
1880
1942
  // Events already set up - clean them up first to avoid duplicates
1881
1943
  cleanupEventHandlers(instance);
1882
1944
  }
@@ -1906,7 +1968,9 @@ var Snice = (function (exports) {
1906
1968
  warnIfModifierMisuse(handler.eventName);
1907
1969
  // Apply debounce (takes precedence over throttle)
1908
1970
  if (handlerOptions.debounce && handlerOptions.debounce > 0) {
1909
- boundMethod = createDebounced(boundMethod, handlerOptions.debounce);
1971
+ const debounced = createDebounced(boundMethod, handlerOptions.debounce);
1972
+ instance[CLEANUP].eventCancels.push(() => debounced.cancel());
1973
+ boundMethod = debounced;
1910
1974
  }
1911
1975
  else if (handlerOptions.throttle && handlerOptions.throttle > 0) {
1912
1976
  boundMethod = throttle$1(boundMethod, handlerOptions.throttle);
@@ -2079,12 +2143,190 @@ var Snice = (function (exports) {
2079
2143
  function cleanupEventHandlers(instance) {
2080
2144
  if (!instance[CLEANUP]?.events)
2081
2145
  return;
2146
+ for (const cancel of instance[CLEANUP].eventCancels ?? [])
2147
+ cancel();
2148
+ instance[CLEANUP].eventCancels = [];
2082
2149
  for (const { target, eventName, handler, options } of instance[CLEANUP].events) {
2083
2150
  target.removeEventListener(eventName, handler, options);
2084
2151
  }
2085
2152
  instance[CLEANUP].events = [];
2086
2153
  }
2087
2154
 
2155
+ /**
2156
+ * @context decorator for receiving router context updates
2157
+ */
2158
+ const CONTEXT_HANDLERS = getSymbol('context-handlers');
2159
+ /**
2160
+ * @context decorator for receiving router context updates
2161
+ *
2162
+ * @example
2163
+ * ```typescript
2164
+ * @element('my-layout')
2165
+ * class MyLayout extends HTMLElement {
2166
+ * @context
2167
+ * handleContext(ctx: Context) {
2168
+ * this.renderNav(ctx.placards, ctx.currentRoute);
2169
+ * }
2170
+ *
2171
+ * @context({ debounce: 300 })
2172
+ * handleContextDebounced(ctx: Context) {
2173
+ * // Called after 300ms of no updates
2174
+ * }
2175
+ * }
2176
+ * ```
2177
+ */
2178
+ function context$1(options = {}) {
2179
+ return function (originalMethod, context) {
2180
+ const methodName = context.name;
2181
+ context.addInitializer(function () {
2182
+ const constructor = this.constructor;
2183
+ // hasOwnProperty guards so subclasses don't mutate parent state via
2184
+ // the prototype chain.
2185
+ if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_METHODS)) {
2186
+ constructor[CONTEXT_METHODS] = new Set();
2187
+ }
2188
+ if (constructor[CONTEXT_METHODS].has(originalMethod))
2189
+ return;
2190
+ constructor[CONTEXT_METHODS].add(originalMethod);
2191
+ if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_HANDLERS)) {
2192
+ const inherited = constructor[CONTEXT_HANDLERS];
2193
+ constructor[CONTEXT_HANDLERS] = inherited ? [...inherited] : [];
2194
+ }
2195
+ constructor[CONTEXT_HANDLERS].push({
2196
+ methodName,
2197
+ method: originalMethod,
2198
+ options,
2199
+ });
2200
+ });
2201
+ return originalMethod;
2202
+ };
2203
+ }
2204
+ /**
2205
+ * Set up context handlers for an element or attached controller.
2206
+ * Called automatically by the corresponding managed lifecycle.
2207
+ */
2208
+ function setupContextHandler(element, catchUp = false) {
2209
+ const handlers = element.constructor[CONTEXT_HANDLERS];
2210
+ if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2211
+ return;
2212
+ }
2213
+ // Get the Context instance from the router
2214
+ const ctx = element[CONTEXT_HANDLER] ?? getNavigationContext(element);
2215
+ if (!ctx) {
2216
+ return;
2217
+ }
2218
+ // Store the Context instance for cleanup
2219
+ element[NAVIGATION_CONTEXT_INSTANCE] = ctx;
2220
+ // A controller can finish attaching after the Router has already announced
2221
+ // the current navigation. Track whether an update arrives during this turn;
2222
+ // if none does, catch it up once from the Context's current state.
2223
+ let receivedContextUpdate = false;
2224
+ // Register each handler with the Context
2225
+ for (const handler of handlers) {
2226
+ const { methodName, method, options } = handler;
2227
+ const wrappedMethodName = `__wrapped_${methodName}`;
2228
+ // Create wrapped method with timing controls
2229
+ element[wrappedMethodName] = function (context) {
2230
+ receivedContextUpdate = true;
2231
+ // `once` is tracked PER method (not a single element-level flag) so
2232
+ // multiple once handlers on the same element each fire.
2233
+ const called = element[CONTEXT_CALLED] ||
2234
+ (element[CONTEXT_CALLED] = new Set());
2235
+ if (options.once && called.has(methodName)) {
2236
+ return;
2237
+ }
2238
+ const callMethod = () => {
2239
+ method.call(element, context);
2240
+ // Handle once option
2241
+ if (options.once) {
2242
+ called.add(methodName);
2243
+ // Unregister ONLY this handler, not the whole element — otherwise the
2244
+ // element's other @context handlers would stop receiving updates.
2245
+ const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2246
+ if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2247
+ ctx[CONTEXT_UNREGISTER](element, wrappedMethodName);
2248
+ }
2249
+ }
2250
+ };
2251
+ // Per-handler timer slot to avoid debounce/throttle handlers on the same
2252
+ // element overwriting each other's state.
2253
+ const timerSlot = element[CONTEXT_TIMER] ||
2254
+ (element[CONTEXT_TIMER] = {});
2255
+ const timerKey = methodName;
2256
+ if (options.debounce) {
2257
+ clearTimeout(timerSlot[timerKey]?.timeout);
2258
+ timerSlot[timerKey] = {
2259
+ timeout: setTimeout(callMethod, options.debounce),
2260
+ };
2261
+ return;
2262
+ }
2263
+ if (options.throttle) {
2264
+ const now = Date.now();
2265
+ const lastCall = timerSlot[timerKey]?.lastCall || 0;
2266
+ if (now - lastCall >= options.throttle) {
2267
+ timerSlot[timerKey] = { lastCall: now };
2268
+ callMethod();
2269
+ }
2270
+ return;
2271
+ }
2272
+ callMethod();
2273
+ };
2274
+ // Register with the Context using the wrapped method name
2275
+ if (typeof ctx[CONTEXT_REGISTER] === 'function') {
2276
+ ctx[CONTEXT_REGISTER](element, wrappedMethodName);
2277
+ }
2278
+ }
2279
+ if (catchUp && typeof ctx[CONTEXT_NOTIFY_ELEMENT] === 'function') {
2280
+ return new Promise((resolve) => {
2281
+ const notifyIfNeeded = () => {
2282
+ try {
2283
+ if (!receivedContextUpdate
2284
+ && element[NAVIGATION_CONTEXT_INSTANCE] === ctx) {
2285
+ ctx[CONTEXT_NOTIFY_ELEMENT](element);
2286
+ }
2287
+ }
2288
+ finally {
2289
+ resolve();
2290
+ }
2291
+ };
2292
+ if (catchUp === 'task')
2293
+ setTimeout(notifyIfNeeded, 0);
2294
+ else
2295
+ queueMicrotask(notifyIfNeeded);
2296
+ });
2297
+ }
2298
+ }
2299
+ /**
2300
+ * Clean up context handlers for an element or attached controller.
2301
+ */
2302
+ function cleanupContextHandler(element) {
2303
+ const handlers = element.constructor[CONTEXT_HANDLERS];
2304
+ if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2305
+ return;
2306
+ }
2307
+ // Clear any pending debounce timers (per-handler slots)
2308
+ const timerSlot = element[CONTEXT_TIMER];
2309
+ if (timerSlot && typeof timerSlot === 'object') {
2310
+ for (const key of Object.keys(timerSlot)) {
2311
+ if (timerSlot[key]?.timeout)
2312
+ clearTimeout(timerSlot[key].timeout);
2313
+ }
2314
+ delete element[CONTEXT_TIMER];
2315
+ }
2316
+ for (const handler of handlers) {
2317
+ // Clean up wrapped method
2318
+ const wrappedMethodName = `__wrapped_${handler.methodName}`;
2319
+ delete element[wrappedMethodName];
2320
+ }
2321
+ // Unregister from Context if available
2322
+ const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2323
+ if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2324
+ ctx[CONTEXT_UNREGISTER](element);
2325
+ }
2326
+ delete element[NAVIGATION_CONTEXT_INSTANCE];
2327
+ delete element[CONTEXT_CALLED];
2328
+ }
2329
+
2088
2330
  // Controller-scoped cleanup registry
2089
2331
  class ControllerScope {
2090
2332
  constructor() {
@@ -2418,6 +2660,10 @@ var Snice = (function (exports) {
2418
2660
  await scope.runOperation(async () => {
2419
2661
  await controllerInstance.attach(element);
2420
2662
  });
2663
+ // @context has the same managed lifecycle on controllers as @on,
2664
+ // @observe and @respond. Catch up controllers whose async attach completed
2665
+ // after the Router announced the current navigation.
2666
+ await setupContextHandler(controllerInstance, 'microtask');
2421
2667
  // Setup @observe observers for controller
2422
2668
  setupObservers(controllerInstance, element);
2423
2669
  // Setup @channel handlers for controller
@@ -2483,6 +2729,8 @@ var Snice = (function (exports) {
2483
2729
  cleanupResponseHandlers(controllerInstance);
2484
2730
  // Cleanup @on event handlers for controller
2485
2731
  cleanupEventHandlers(controllerInstance);
2732
+ // Cleanup @context handlers for controller
2733
+ cleanupContextHandler(controllerInstance);
2486
2734
  // Cleanup the controller scope
2487
2735
  if (scope) {
2488
2736
  await scope.cleanup();
@@ -2616,158 +2864,6 @@ var Snice = (function (exports) {
2616
2864
  globalThis.sniceNativeControllerObserver = observer;
2617
2865
  }
2618
2866
 
2619
- /**
2620
- * @context decorator for receiving router context updates
2621
- */
2622
- const CONTEXT_HANDLERS = getSymbol('context-handlers');
2623
- /**
2624
- * @context decorator for receiving router context updates
2625
- *
2626
- * @example
2627
- * ```typescript
2628
- * @element('my-layout')
2629
- * class MyLayout extends HTMLElement {
2630
- * @context
2631
- * handleContext(ctx: Context) {
2632
- * this.renderNav(ctx.placards, ctx.currentRoute);
2633
- * }
2634
- *
2635
- * @context({ debounce: 300 })
2636
- * handleContextDebounced(ctx: Context) {
2637
- * // Called after 300ms of no updates
2638
- * }
2639
- * }
2640
- * ```
2641
- */
2642
- function context$1(options = {}) {
2643
- return function (originalMethod, context) {
2644
- const methodName = context.name;
2645
- context.addInitializer(function () {
2646
- const constructor = this.constructor;
2647
- // hasOwnProperty guards so subclasses don't mutate parent state via
2648
- // the prototype chain.
2649
- if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_METHODS)) {
2650
- constructor[CONTEXT_METHODS] = new Set();
2651
- }
2652
- if (constructor[CONTEXT_METHODS].has(originalMethod))
2653
- return;
2654
- constructor[CONTEXT_METHODS].add(originalMethod);
2655
- if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_HANDLERS)) {
2656
- const inherited = constructor[CONTEXT_HANDLERS];
2657
- constructor[CONTEXT_HANDLERS] = inherited ? [...inherited] : [];
2658
- }
2659
- constructor[CONTEXT_HANDLERS].push({
2660
- methodName,
2661
- method: originalMethod,
2662
- options,
2663
- });
2664
- });
2665
- return originalMethod;
2666
- };
2667
- }
2668
- /**
2669
- * Setup context handler for an element instance
2670
- * Called automatically during element connection
2671
- */
2672
- function setupContextHandler(element) {
2673
- const handlers = element.constructor[CONTEXT_HANDLERS];
2674
- if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2675
- return;
2676
- }
2677
- // Get the Context instance from the router
2678
- const ctx = element[CONTEXT_HANDLER];
2679
- if (!ctx) {
2680
- return;
2681
- }
2682
- // Store the Context instance for cleanup
2683
- element[NAVIGATION_CONTEXT_INSTANCE] = ctx;
2684
- // Register each handler with the Context
2685
- for (const handler of handlers) {
2686
- const { methodName, method, options } = handler;
2687
- const wrappedMethodName = `__wrapped_${methodName}`;
2688
- // Create wrapped method with timing controls
2689
- element[wrappedMethodName] = function (context) {
2690
- // `once` is tracked PER method (not a single element-level flag) so
2691
- // multiple once handlers on the same element each fire.
2692
- const called = element[CONTEXT_CALLED] ||
2693
- (element[CONTEXT_CALLED] = new Set());
2694
- if (options.once && called.has(methodName)) {
2695
- return;
2696
- }
2697
- const callMethod = () => {
2698
- method.call(element, context);
2699
- // Handle once option
2700
- if (options.once) {
2701
- called.add(methodName);
2702
- // Unregister ONLY this handler, not the whole element — otherwise the
2703
- // element's other @context handlers would stop receiving updates.
2704
- const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2705
- if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2706
- ctx[CONTEXT_UNREGISTER](element, wrappedMethodName);
2707
- }
2708
- }
2709
- };
2710
- // Per-handler timer slot to avoid debounce/throttle handlers on the same
2711
- // element overwriting each other's state.
2712
- const timerSlot = element[CONTEXT_TIMER] ||
2713
- (element[CONTEXT_TIMER] = {});
2714
- const timerKey = methodName;
2715
- if (options.debounce) {
2716
- clearTimeout(timerSlot[timerKey]?.timeout);
2717
- timerSlot[timerKey] = {
2718
- timeout: setTimeout(callMethod, options.debounce),
2719
- };
2720
- return;
2721
- }
2722
- if (options.throttle) {
2723
- const now = Date.now();
2724
- const lastCall = timerSlot[timerKey]?.lastCall || 0;
2725
- if (now - lastCall >= options.throttle) {
2726
- timerSlot[timerKey] = { lastCall: now };
2727
- callMethod();
2728
- }
2729
- return;
2730
- }
2731
- callMethod();
2732
- };
2733
- // Register with the Context using the wrapped method name
2734
- if (typeof ctx[CONTEXT_REGISTER] === 'function') {
2735
- ctx[CONTEXT_REGISTER](element, wrappedMethodName);
2736
- }
2737
- }
2738
- }
2739
- /**
2740
- * Cleanup context handler for an element instance
2741
- * Called automatically during element disconnection
2742
- */
2743
- function cleanupContextHandler(element) {
2744
- const handlers = element.constructor[CONTEXT_HANDLERS];
2745
- if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2746
- return;
2747
- }
2748
- // Clear any pending debounce timers (per-handler slots)
2749
- const timerSlot = element[CONTEXT_TIMER];
2750
- if (timerSlot && typeof timerSlot === 'object') {
2751
- for (const key of Object.keys(timerSlot)) {
2752
- if (timerSlot[key]?.timeout)
2753
- clearTimeout(timerSlot[key].timeout);
2754
- }
2755
- delete element[CONTEXT_TIMER];
2756
- }
2757
- for (const handler of handlers) {
2758
- // Clean up wrapped method
2759
- const wrappedMethodName = `__wrapped_${handler.methodName}`;
2760
- delete element[wrappedMethodName];
2761
- }
2762
- // Unregister from Context if available
2763
- const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2764
- if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2765
- ctx[CONTEXT_UNREGISTER](element);
2766
- }
2767
- delete element[NAVIGATION_CONTEXT_INSTANCE];
2768
- delete element[CONTEXT_CALLED];
2769
- }
2770
-
2771
2867
  /**
2772
2868
  * Template system for Snice v3.0.0
2773
2869
  * Provides html`` and css`` tagged template processors with differential rendering
@@ -3003,14 +3099,32 @@ var Snice = (function (exports) {
3003
3099
  function markPreUpgradePropertyBinding(element, propertyName) {
3004
3100
  const tagName = element.localName;
3005
3101
  if (!tagName?.includes('-'))
3006
- return;
3102
+ return false;
3007
3103
  const registry = element.ownerDocument?.defaultView?.customElements ?? globalThis.customElements;
3008
3104
  if (registry?.get(tagName))
3009
- return;
3105
+ return false;
3010
3106
  const target = element;
3011
3107
  if (!target[PRE_UPGRADE_PROPERTY_BINDINGS])
3012
3108
  target[PRE_UPGRADE_PROPERTY_BINDINGS] = new Set();
3013
3109
  target[PRE_UPGRADE_PROPERTY_BINDINGS].add(propertyName);
3110
+ return true;
3111
+ }
3112
+ function commitPropertyValue(element, propertyName, value) {
3113
+ if (!markPreUpgradePropertyBinding(element, propertyName)) {
3114
+ element[propertyName] = value;
3115
+ return;
3116
+ }
3117
+ // An undefined custom element can inherit a native IDL setter with the same
3118
+ // name. Assignment would invoke that setter (for example HTMLElement.role),
3119
+ // stringify structured data into an attribute, and leave no own value for
3120
+ // @property's upgrade adoption. Park every pre-upgrade binding as the own,
3121
+ // configurable value that the initializer already knows how to replay.
3122
+ Object.defineProperty(element, propertyName, {
3123
+ configurable: true,
3124
+ enumerable: true,
3125
+ writable: true,
3126
+ value,
3127
+ });
3014
3128
  }
3015
3129
  // noChange sentinel - preserves the currently committed value
3016
3130
  const noChange = Symbol.for('snice:no-change');
@@ -4647,15 +4761,13 @@ var Snice = (function (exports) {
4647
4761
  commitProperties(next) {
4648
4762
  for (const key of Object.keys(this.committed)) {
4649
4763
  if (!Object.prototype.hasOwnProperty.call(next, key)) {
4650
- markPreUpgradePropertyBinding(this.element, key);
4651
- this.element[key] = undefined;
4764
+ commitPropertyValue(this.element, key, undefined);
4652
4765
  }
4653
4766
  }
4654
4767
  for (const [key, value] of Object.entries(next)) {
4655
4768
  if (Object.is(this.committed[key], value))
4656
4769
  continue;
4657
- markPreUpgradePropertyBinding(this.element, key);
4658
- this.element[key] = value === nothing ? undefined : value;
4770
+ commitPropertyValue(this.element, key, value === nothing ? undefined : value);
4659
4771
  }
4660
4772
  }
4661
4773
  commitAttributes(next) {
@@ -4782,8 +4894,7 @@ var Snice = (function (exports) {
4782
4894
  return;
4783
4895
  }
4784
4896
  this._committedValue = value;
4785
- markPreUpgradePropertyBinding(this.element, this.name);
4786
- this.element[this.name] = value === nothing ? undefined : value;
4897
+ commitPropertyValue(this.element, this.name, value === nothing ? undefined : value);
4787
4898
  }
4788
4899
  clear() {
4789
4900
  // This part is being destroyed with its owning template. The element is
@@ -6815,7 +6926,7 @@ var Snice = (function (exports) {
6815
6926
  // Re-establish handlers that get cleaned up on disconnect
6816
6927
  setupEventHandlers(this, this);
6817
6928
  setupResponseHandlers(this, this);
6818
- setupContextHandler(this);
6929
+ void setupContextHandler(this, 'task');
6819
6930
  // Re-establish observers that get cleaned up on disconnect
6820
6931
  try {
6821
6932
  setupObservers(this, this);
@@ -6940,7 +7051,7 @@ var Snice = (function (exports) {
6940
7051
  }
6941
7052
  setupEventHandlers(this, this);
6942
7053
  setupResponseHandlers(this, this);
6943
- setupContextHandler(this);
7054
+ void setupContextHandler(this, 'task');
6944
7055
  this[INITIALIZED] = true;
6945
7056
  // One-time (per class) sanity check: a @watch name with no matching
6946
7057
  // @property never fires — surface the typo instead of staying silent.
@@ -8645,10 +8756,11 @@ var Snice = (function (exports) {
8645
8756
  *
8646
8757
  * Daemon instances must already exist in context.daemons. The returned cleanup
8647
8758
  * removes the provider and deactivates framework communication for daemons no
8648
- * longer provided by any context.
8759
+ * longer provided by any context. An optional fetch function is exposed on the
8760
+ * same boundary through getContextFetch() without modifying the context object.
8649
8761
  */
8650
- function provideContext(root, context) {
8651
- const releaseProvider = installContextProvider(root, context);
8762
+ function provideContextBoundary(root, context, options, navigation) {
8763
+ const releaseProvider = installContextProvider(root, context, options.fetch, navigation);
8652
8764
  let releaseDaemons;
8653
8765
  try {
8654
8766
  releaseDaemons = bindContextDaemons(context);
@@ -8666,6 +8778,15 @@ var Snice = (function (exports) {
8666
8778
  releaseDaemons?.();
8667
8779
  };
8668
8780
  }
8781
+ function provideContext(root, context, options = {}) {
8782
+ return provideContextBoundary(root, context, options);
8783
+ }
8784
+ /** @internal Router-owned provider including live navigation state. */
8785
+ function provideRouterContext(root, navigation) {
8786
+ return provideContextBoundary(root, navigation.application, {
8787
+ fetch: navigation.fetch,
8788
+ }, navigation);
8789
+ }
8669
8790
 
8670
8791
  // Router historically allows a later Router instance to take over the same
8671
8792
  // target. Track only Router-owned provider releases so that handoff remains
@@ -8709,7 +8830,7 @@ var Snice = (function (exports) {
8709
8830
  return;
8710
8831
  releaseContextProvider?.();
8711
8832
  routerContextProviders.get(target)?.();
8712
- const releaseProvidedContext = provideContext(target, context);
8833
+ const releaseProvidedContext = provideRouterContext(target, navigationContext);
8713
8834
  let released = false;
8714
8835
  const release = () => {
8715
8836
  if (released)
@@ -9593,7 +9714,7 @@ var Snice = (function (exports) {
9593
9714
  }
9594
9715
 
9595
9716
  /*!
9596
- * snice v7.2.0
9717
+ * snice v7.3.0
9597
9718
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
9598
9719
  * (c) 2024
9599
9720
  * Released under the MIT License.
@@ -9615,7 +9736,7 @@ var Snice = (function (exports) {
9615
9736
  globalThis.snice.pendingControllerAttachments ??= new Map();
9616
9737
 
9617
9738
  /*!
9618
- * snice v7.2.0
9739
+ * snice v7.3.0
9619
9740
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
9620
9741
  * (c) 2024
9621
9742
  * Released under the MIT License.
@@ -9661,6 +9782,7 @@ var Snice = (function (exports) {
9661
9782
  exports.escapeHtml = escapeHtml;
9662
9783
  exports.getBodyScrollLockCount = getBodyScrollLockCount;
9663
9784
  exports.getContext = getContext;
9785
+ exports.getContextFetch = getContextFetch;
9664
9786
  exports.getController = getController;
9665
9787
  exports.getSymbol = getSymbol;
9666
9788
  exports.html = html;