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,11 +1,11 @@
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.
6
6
  */
7
7
  /*!
8
- * snice v7.2.0
8
+ * snice v7.3.0
9
9
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
10
10
  * (c) 2024
11
11
  * Released under the MIT License.
@@ -236,7 +236,7 @@ let contextIdCounter = 0;
236
236
  */
237
237
  class Context {
238
238
  constructor(context = {}, placards = [], currentRoute = '', routeParams = {}, fetcher) {
239
- // Stores the SET of wrapped-method names per element so that classes with
239
+ // Stores the SET of wrapped-method names per subscriber so that classes with
240
240
  // multiple @context() handlers register every method, not just the last one.
241
241
  this[_a] = new WeakMap();
242
242
  this[_b] = new Set();
@@ -260,7 +260,7 @@ class Context {
260
260
  }
261
261
  }
262
262
  /**
263
- * Register an element to receive context updates
263
+ * Register an element or controller to receive context updates
264
264
  * @internal Used by @context decorator
265
265
  */
266
266
  [(_a = REGISTERED_ELEMENTS, _b = REGISTERED_ELEMENTS_SET, _c = IS_UPDATING, CONTEXT_REGISTER)](element, methodName) {
@@ -274,10 +274,10 @@ class Context {
274
274
  this[REGISTERED_ELEMENTS_SET].add(element);
275
275
  }
276
276
  /**
277
- * Unregister an element from receiving context updates. With a methodName,
277
+ * Unregister a subscriber from context updates. With a methodName,
278
278
  * removes only that one handler (so a `once` handler tearing itself down
279
279
  * doesn't kill the element's other @context handlers); without one, removes
280
- * the element entirely (used on disconnect).
280
+ * the subscriber entirely (used on disconnect/detach).
281
281
  * @internal Used by @context decorator cleanup
282
282
  */
283
283
  [CONTEXT_UNREGISTER](element, methodName) {
@@ -318,7 +318,7 @@ class Context {
318
318
  return;
319
319
  }
320
320
  this[IS_UPDATING] = true;
321
- // Notify all registered elements by calling EVERY registered method
321
+ // Notify all registered subscribers by calling EVERY registered method
322
322
  // name on each. Classes with multiple @context() methods get all fired.
323
323
  const elementsSet = this[REGISTERED_ELEMENTS_SET];
324
324
  const elementsMap = this[REGISTERED_ELEMENTS];
@@ -340,7 +340,7 @@ class Context {
340
340
  this[IS_UPDATING] = false;
341
341
  }
342
342
  /**
343
- * Notify a specific element of the current context state
343
+ * Notify a specific subscriber of the current context state
344
344
  * @internal Used by @context decorator
345
345
  */
346
346
  [CONTEXT_NOTIFY_ELEMENT](element) {
@@ -349,7 +349,12 @@ class Context {
349
349
  return;
350
350
  for (const methodName of names) {
351
351
  if (typeof element[methodName] === 'function') {
352
- element[methodName](this);
352
+ try {
353
+ element[methodName](this);
354
+ }
355
+ catch (error) {
356
+ console.error(`Error calling @context method ${methodName}:`, error);
357
+ }
353
358
  }
354
359
  }
355
360
  }
@@ -681,11 +686,17 @@ function invokeImmediateWatchers(instance, constructor) {
681
686
  */
682
687
  function createDebounced(fn, delay) {
683
688
  let timer = null;
684
- return function (...args) {
689
+ const debounced = function (...args) {
685
690
  if (timer !== null)
686
691
  clearTimeout(timer);
687
692
  timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay);
688
693
  };
694
+ debounced.cancel = () => {
695
+ if (timer !== null)
696
+ clearTimeout(timer);
697
+ timer = null;
698
+ };
699
+ return debounced;
689
700
  }
690
701
  /**
691
702
  * Create a throttled version of a function (leading + trailing edge)
@@ -1142,7 +1153,7 @@ function contextTarget(source) {
1142
1153
  * boundary. Public callers use provideContext(), which also activates the
1143
1154
  * daemon instances declared by the context.
1144
1155
  */
1145
- function installContextProvider(root, context) {
1156
+ function installContextProvider(root, context, fetch, navigation) {
1146
1157
  if (!root || typeof root.addEventListener !== 'function' || typeof root.removeEventListener !== 'function') {
1147
1158
  throw new TypeError('provideContext() requires an EventTarget root.');
1148
1159
  }
@@ -1154,9 +1165,26 @@ function installContextProvider(root, context) {
1154
1165
  }
1155
1166
  const listener = (event) => {
1156
1167
  const request = event;
1157
- if (!request.detail || request.detail.context !== undefined)
1168
+ if (!request.detail)
1169
+ return;
1170
+ if (request.detail.kind === 'application') {
1171
+ if (request.detail.context !== undefined)
1172
+ return;
1173
+ request.detail.context = context;
1174
+ }
1175
+ else if (request.detail.kind === 'fetch') {
1176
+ if (!fetch || request.detail.fetch !== undefined)
1177
+ return;
1178
+ request.detail.fetch = fetch;
1179
+ }
1180
+ else if (request.detail.kind === 'navigation') {
1181
+ if (!navigation || request.detail.navigation !== undefined)
1182
+ return;
1183
+ request.detail.navigation = navigation;
1184
+ }
1185
+ else {
1158
1186
  return;
1159
- request.detail.context = context;
1187
+ }
1160
1188
  request.preventDefault();
1161
1189
  request.stopPropagation();
1162
1190
  };
@@ -1186,7 +1214,7 @@ function getContext(source) {
1186
1214
  }
1187
1215
  const target = contextTarget(source);
1188
1216
  if (target) {
1189
- const detail = {};
1217
+ const detail = { kind: 'application' };
1190
1218
  target.dispatchEvent(new CustomEvent(CONTEXT_REQUEST_EVENT, {
1191
1219
  bubbles: true,
1192
1220
  composed: true,
@@ -1205,6 +1233,39 @@ function getContext(source) {
1205
1233
  }
1206
1234
  return undefined;
1207
1235
  }
1236
+ /**
1237
+ * Resolve the fetch function supplied by the nearest context provider.
1238
+ * Router providers expose their ContextAwareFetcher-bound function here;
1239
+ * explicit providers may supply the same dependency for non-router apps and
1240
+ * tests without adding a reserved property to AppContext.
1241
+ */
1242
+ function getContextFetch(source) {
1243
+ const target = contextTarget(source);
1244
+ if (!target)
1245
+ return undefined;
1246
+ const detail = { kind: 'fetch' };
1247
+ target.dispatchEvent(new CustomEvent(CONTEXT_REQUEST_EVENT, {
1248
+ bubbles: true,
1249
+ composed: true,
1250
+ cancelable: true,
1251
+ detail,
1252
+ }));
1253
+ return detail.fetch;
1254
+ }
1255
+ /** @internal Resolve the router Context visible to an element or controller. */
1256
+ function getNavigationContext(source) {
1257
+ const target = contextTarget(source);
1258
+ if (!target)
1259
+ return undefined;
1260
+ const detail = { kind: 'navigation' };
1261
+ target.dispatchEvent(new CustomEvent(CONTEXT_REQUEST_EVENT, {
1262
+ bubbles: true,
1263
+ composed: true,
1264
+ cancelable: true,
1265
+ detail,
1266
+ }));
1267
+ return detail.navigation;
1268
+ }
1208
1269
 
1209
1270
  function isDaemonInstance(value) {
1210
1271
  return !!value
@@ -1868,12 +1929,13 @@ function setupEventHandlers(instance, targetElement) {
1868
1929
  }
1869
1930
  // Initialize cleanup object if needed
1870
1931
  if (!instance[CLEANUP]) {
1871
- instance[CLEANUP] = { events: [], channels: [], observers: [] };
1932
+ instance[CLEANUP] = { events: [], eventCancels: [], channels: [], observers: [] };
1872
1933
  }
1873
- else if (!instance[CLEANUP].events) {
1934
+ if (!instance[CLEANUP].events)
1874
1935
  instance[CLEANUP].events = [];
1875
- }
1876
- else if (instance[CLEANUP].events.length > 0) {
1936
+ if (!instance[CLEANUP].eventCancels)
1937
+ instance[CLEANUP].eventCancels = [];
1938
+ if (instance[CLEANUP].events.length > 0 || instance[CLEANUP].eventCancels.length > 0) {
1877
1939
  // Events already set up - clean them up first to avoid duplicates
1878
1940
  cleanupEventHandlers(instance);
1879
1941
  }
@@ -1903,7 +1965,9 @@ function setupEventHandlers(instance, targetElement) {
1903
1965
  warnIfModifierMisuse(handler.eventName);
1904
1966
  // Apply debounce (takes precedence over throttle)
1905
1967
  if (handlerOptions.debounce && handlerOptions.debounce > 0) {
1906
- boundMethod = createDebounced(boundMethod, handlerOptions.debounce);
1968
+ const debounced = createDebounced(boundMethod, handlerOptions.debounce);
1969
+ instance[CLEANUP].eventCancels.push(() => debounced.cancel());
1970
+ boundMethod = debounced;
1907
1971
  }
1908
1972
  else if (handlerOptions.throttle && handlerOptions.throttle > 0) {
1909
1973
  boundMethod = throttle$1(boundMethod, handlerOptions.throttle);
@@ -2076,12 +2140,190 @@ function setupEventHandlers(instance, targetElement) {
2076
2140
  function cleanupEventHandlers(instance) {
2077
2141
  if (!instance[CLEANUP]?.events)
2078
2142
  return;
2143
+ for (const cancel of instance[CLEANUP].eventCancels ?? [])
2144
+ cancel();
2145
+ instance[CLEANUP].eventCancels = [];
2079
2146
  for (const { target, eventName, handler, options } of instance[CLEANUP].events) {
2080
2147
  target.removeEventListener(eventName, handler, options);
2081
2148
  }
2082
2149
  instance[CLEANUP].events = [];
2083
2150
  }
2084
2151
 
2152
+ /**
2153
+ * @context decorator for receiving router context updates
2154
+ */
2155
+ const CONTEXT_HANDLERS = getSymbol('context-handlers');
2156
+ /**
2157
+ * @context decorator for receiving router context updates
2158
+ *
2159
+ * @example
2160
+ * ```typescript
2161
+ * @element('my-layout')
2162
+ * class MyLayout extends HTMLElement {
2163
+ * @context
2164
+ * handleContext(ctx: Context) {
2165
+ * this.renderNav(ctx.placards, ctx.currentRoute);
2166
+ * }
2167
+ *
2168
+ * @context({ debounce: 300 })
2169
+ * handleContextDebounced(ctx: Context) {
2170
+ * // Called after 300ms of no updates
2171
+ * }
2172
+ * }
2173
+ * ```
2174
+ */
2175
+ function context$1(options = {}) {
2176
+ return function (originalMethod, context) {
2177
+ const methodName = context.name;
2178
+ context.addInitializer(function () {
2179
+ const constructor = this.constructor;
2180
+ // hasOwnProperty guards so subclasses don't mutate parent state via
2181
+ // the prototype chain.
2182
+ if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_METHODS)) {
2183
+ constructor[CONTEXT_METHODS] = new Set();
2184
+ }
2185
+ if (constructor[CONTEXT_METHODS].has(originalMethod))
2186
+ return;
2187
+ constructor[CONTEXT_METHODS].add(originalMethod);
2188
+ if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_HANDLERS)) {
2189
+ const inherited = constructor[CONTEXT_HANDLERS];
2190
+ constructor[CONTEXT_HANDLERS] = inherited ? [...inherited] : [];
2191
+ }
2192
+ constructor[CONTEXT_HANDLERS].push({
2193
+ methodName,
2194
+ method: originalMethod,
2195
+ options,
2196
+ });
2197
+ });
2198
+ return originalMethod;
2199
+ };
2200
+ }
2201
+ /**
2202
+ * Set up context handlers for an element or attached controller.
2203
+ * Called automatically by the corresponding managed lifecycle.
2204
+ */
2205
+ function setupContextHandler(element, catchUp = false) {
2206
+ const handlers = element.constructor[CONTEXT_HANDLERS];
2207
+ if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2208
+ return;
2209
+ }
2210
+ // Get the Context instance from the router
2211
+ const ctx = element[CONTEXT_HANDLER] ?? getNavigationContext(element);
2212
+ if (!ctx) {
2213
+ return;
2214
+ }
2215
+ // Store the Context instance for cleanup
2216
+ element[NAVIGATION_CONTEXT_INSTANCE] = ctx;
2217
+ // A controller can finish attaching after the Router has already announced
2218
+ // the current navigation. Track whether an update arrives during this turn;
2219
+ // if none does, catch it up once from the Context's current state.
2220
+ let receivedContextUpdate = false;
2221
+ // Register each handler with the Context
2222
+ for (const handler of handlers) {
2223
+ const { methodName, method, options } = handler;
2224
+ const wrappedMethodName = `__wrapped_${methodName}`;
2225
+ // Create wrapped method with timing controls
2226
+ element[wrappedMethodName] = function (context) {
2227
+ receivedContextUpdate = true;
2228
+ // `once` is tracked PER method (not a single element-level flag) so
2229
+ // multiple once handlers on the same element each fire.
2230
+ const called = element[CONTEXT_CALLED] ||
2231
+ (element[CONTEXT_CALLED] = new Set());
2232
+ if (options.once && called.has(methodName)) {
2233
+ return;
2234
+ }
2235
+ const callMethod = () => {
2236
+ method.call(element, context);
2237
+ // Handle once option
2238
+ if (options.once) {
2239
+ called.add(methodName);
2240
+ // Unregister ONLY this handler, not the whole element — otherwise the
2241
+ // element's other @context handlers would stop receiving updates.
2242
+ const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2243
+ if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2244
+ ctx[CONTEXT_UNREGISTER](element, wrappedMethodName);
2245
+ }
2246
+ }
2247
+ };
2248
+ // Per-handler timer slot to avoid debounce/throttle handlers on the same
2249
+ // element overwriting each other's state.
2250
+ const timerSlot = element[CONTEXT_TIMER] ||
2251
+ (element[CONTEXT_TIMER] = {});
2252
+ const timerKey = methodName;
2253
+ if (options.debounce) {
2254
+ clearTimeout(timerSlot[timerKey]?.timeout);
2255
+ timerSlot[timerKey] = {
2256
+ timeout: setTimeout(callMethod, options.debounce),
2257
+ };
2258
+ return;
2259
+ }
2260
+ if (options.throttle) {
2261
+ const now = Date.now();
2262
+ const lastCall = timerSlot[timerKey]?.lastCall || 0;
2263
+ if (now - lastCall >= options.throttle) {
2264
+ timerSlot[timerKey] = { lastCall: now };
2265
+ callMethod();
2266
+ }
2267
+ return;
2268
+ }
2269
+ callMethod();
2270
+ };
2271
+ // Register with the Context using the wrapped method name
2272
+ if (typeof ctx[CONTEXT_REGISTER] === 'function') {
2273
+ ctx[CONTEXT_REGISTER](element, wrappedMethodName);
2274
+ }
2275
+ }
2276
+ if (catchUp && typeof ctx[CONTEXT_NOTIFY_ELEMENT] === 'function') {
2277
+ return new Promise((resolve) => {
2278
+ const notifyIfNeeded = () => {
2279
+ try {
2280
+ if (!receivedContextUpdate
2281
+ && element[NAVIGATION_CONTEXT_INSTANCE] === ctx) {
2282
+ ctx[CONTEXT_NOTIFY_ELEMENT](element);
2283
+ }
2284
+ }
2285
+ finally {
2286
+ resolve();
2287
+ }
2288
+ };
2289
+ if (catchUp === 'task')
2290
+ setTimeout(notifyIfNeeded, 0);
2291
+ else
2292
+ queueMicrotask(notifyIfNeeded);
2293
+ });
2294
+ }
2295
+ }
2296
+ /**
2297
+ * Clean up context handlers for an element or attached controller.
2298
+ */
2299
+ function cleanupContextHandler(element) {
2300
+ const handlers = element.constructor[CONTEXT_HANDLERS];
2301
+ if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2302
+ return;
2303
+ }
2304
+ // Clear any pending debounce timers (per-handler slots)
2305
+ const timerSlot = element[CONTEXT_TIMER];
2306
+ if (timerSlot && typeof timerSlot === 'object') {
2307
+ for (const key of Object.keys(timerSlot)) {
2308
+ if (timerSlot[key]?.timeout)
2309
+ clearTimeout(timerSlot[key].timeout);
2310
+ }
2311
+ delete element[CONTEXT_TIMER];
2312
+ }
2313
+ for (const handler of handlers) {
2314
+ // Clean up wrapped method
2315
+ const wrappedMethodName = `__wrapped_${handler.methodName}`;
2316
+ delete element[wrappedMethodName];
2317
+ }
2318
+ // Unregister from Context if available
2319
+ const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2320
+ if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2321
+ ctx[CONTEXT_UNREGISTER](element);
2322
+ }
2323
+ delete element[NAVIGATION_CONTEXT_INSTANCE];
2324
+ delete element[CONTEXT_CALLED];
2325
+ }
2326
+
2085
2327
  // Controller-scoped cleanup registry
2086
2328
  class ControllerScope {
2087
2329
  constructor() {
@@ -2415,6 +2657,10 @@ async function attachController(element, controller) {
2415
2657
  await scope.runOperation(async () => {
2416
2658
  await controllerInstance.attach(element);
2417
2659
  });
2660
+ // @context has the same managed lifecycle on controllers as @on,
2661
+ // @observe and @respond. Catch up controllers whose async attach completed
2662
+ // after the Router announced the current navigation.
2663
+ await setupContextHandler(controllerInstance, 'microtask');
2418
2664
  // Setup @observe observers for controller
2419
2665
  setupObservers(controllerInstance, element);
2420
2666
  // Setup @channel handlers for controller
@@ -2480,6 +2726,8 @@ async function detachController(element) {
2480
2726
  cleanupResponseHandlers(controllerInstance);
2481
2727
  // Cleanup @on event handlers for controller
2482
2728
  cleanupEventHandlers(controllerInstance);
2729
+ // Cleanup @context handlers for controller
2730
+ cleanupContextHandler(controllerInstance);
2483
2731
  // Cleanup the controller scope
2484
2732
  if (scope) {
2485
2733
  await scope.cleanup();
@@ -2613,158 +2861,6 @@ function useNativeElementControllers() {
2613
2861
  globalThis.sniceNativeControllerObserver = observer;
2614
2862
  }
2615
2863
 
2616
- /**
2617
- * @context decorator for receiving router context updates
2618
- */
2619
- const CONTEXT_HANDLERS = getSymbol('context-handlers');
2620
- /**
2621
- * @context decorator for receiving router context updates
2622
- *
2623
- * @example
2624
- * ```typescript
2625
- * @element('my-layout')
2626
- * class MyLayout extends HTMLElement {
2627
- * @context
2628
- * handleContext(ctx: Context) {
2629
- * this.renderNav(ctx.placards, ctx.currentRoute);
2630
- * }
2631
- *
2632
- * @context({ debounce: 300 })
2633
- * handleContextDebounced(ctx: Context) {
2634
- * // Called after 300ms of no updates
2635
- * }
2636
- * }
2637
- * ```
2638
- */
2639
- function context$1(options = {}) {
2640
- return function (originalMethod, context) {
2641
- const methodName = context.name;
2642
- context.addInitializer(function () {
2643
- const constructor = this.constructor;
2644
- // hasOwnProperty guards so subclasses don't mutate parent state via
2645
- // the prototype chain.
2646
- if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_METHODS)) {
2647
- constructor[CONTEXT_METHODS] = new Set();
2648
- }
2649
- if (constructor[CONTEXT_METHODS].has(originalMethod))
2650
- return;
2651
- constructor[CONTEXT_METHODS].add(originalMethod);
2652
- if (!Object.prototype.hasOwnProperty.call(constructor, CONTEXT_HANDLERS)) {
2653
- const inherited = constructor[CONTEXT_HANDLERS];
2654
- constructor[CONTEXT_HANDLERS] = inherited ? [...inherited] : [];
2655
- }
2656
- constructor[CONTEXT_HANDLERS].push({
2657
- methodName,
2658
- method: originalMethod,
2659
- options,
2660
- });
2661
- });
2662
- return originalMethod;
2663
- };
2664
- }
2665
- /**
2666
- * Setup context handler for an element instance
2667
- * Called automatically during element connection
2668
- */
2669
- function setupContextHandler(element) {
2670
- const handlers = element.constructor[CONTEXT_HANDLERS];
2671
- if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2672
- return;
2673
- }
2674
- // Get the Context instance from the router
2675
- const ctx = element[CONTEXT_HANDLER];
2676
- if (!ctx) {
2677
- return;
2678
- }
2679
- // Store the Context instance for cleanup
2680
- element[NAVIGATION_CONTEXT_INSTANCE] = ctx;
2681
- // Register each handler with the Context
2682
- for (const handler of handlers) {
2683
- const { methodName, method, options } = handler;
2684
- const wrappedMethodName = `__wrapped_${methodName}`;
2685
- // Create wrapped method with timing controls
2686
- element[wrappedMethodName] = function (context) {
2687
- // `once` is tracked PER method (not a single element-level flag) so
2688
- // multiple once handlers on the same element each fire.
2689
- const called = element[CONTEXT_CALLED] ||
2690
- (element[CONTEXT_CALLED] = new Set());
2691
- if (options.once && called.has(methodName)) {
2692
- return;
2693
- }
2694
- const callMethod = () => {
2695
- method.call(element, context);
2696
- // Handle once option
2697
- if (options.once) {
2698
- called.add(methodName);
2699
- // Unregister ONLY this handler, not the whole element — otherwise the
2700
- // element's other @context handlers would stop receiving updates.
2701
- const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2702
- if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2703
- ctx[CONTEXT_UNREGISTER](element, wrappedMethodName);
2704
- }
2705
- }
2706
- };
2707
- // Per-handler timer slot to avoid debounce/throttle handlers on the same
2708
- // element overwriting each other's state.
2709
- const timerSlot = element[CONTEXT_TIMER] ||
2710
- (element[CONTEXT_TIMER] = {});
2711
- const timerKey = methodName;
2712
- if (options.debounce) {
2713
- clearTimeout(timerSlot[timerKey]?.timeout);
2714
- timerSlot[timerKey] = {
2715
- timeout: setTimeout(callMethod, options.debounce),
2716
- };
2717
- return;
2718
- }
2719
- if (options.throttle) {
2720
- const now = Date.now();
2721
- const lastCall = timerSlot[timerKey]?.lastCall || 0;
2722
- if (now - lastCall >= options.throttle) {
2723
- timerSlot[timerKey] = { lastCall: now };
2724
- callMethod();
2725
- }
2726
- return;
2727
- }
2728
- callMethod();
2729
- };
2730
- // Register with the Context using the wrapped method name
2731
- if (typeof ctx[CONTEXT_REGISTER] === 'function') {
2732
- ctx[CONTEXT_REGISTER](element, wrappedMethodName);
2733
- }
2734
- }
2735
- }
2736
- /**
2737
- * Cleanup context handler for an element instance
2738
- * Called automatically during element disconnection
2739
- */
2740
- function cleanupContextHandler(element) {
2741
- const handlers = element.constructor[CONTEXT_HANDLERS];
2742
- if (!handlers || !Array.isArray(handlers) || handlers.length === 0) {
2743
- return;
2744
- }
2745
- // Clear any pending debounce timers (per-handler slots)
2746
- const timerSlot = element[CONTEXT_TIMER];
2747
- if (timerSlot && typeof timerSlot === 'object') {
2748
- for (const key of Object.keys(timerSlot)) {
2749
- if (timerSlot[key]?.timeout)
2750
- clearTimeout(timerSlot[key].timeout);
2751
- }
2752
- delete element[CONTEXT_TIMER];
2753
- }
2754
- for (const handler of handlers) {
2755
- // Clean up wrapped method
2756
- const wrappedMethodName = `__wrapped_${handler.methodName}`;
2757
- delete element[wrappedMethodName];
2758
- }
2759
- // Unregister from Context if available
2760
- const ctx = element[NAVIGATION_CONTEXT_INSTANCE];
2761
- if (ctx && typeof ctx[CONTEXT_UNREGISTER] === 'function') {
2762
- ctx[CONTEXT_UNREGISTER](element);
2763
- }
2764
- delete element[NAVIGATION_CONTEXT_INSTANCE];
2765
- delete element[CONTEXT_CALLED];
2766
- }
2767
-
2768
2864
  /**
2769
2865
  * Template system for Snice v3.0.0
2770
2866
  * Provides html`` and css`` tagged template processors with differential rendering
@@ -3000,14 +3096,32 @@ const NOT_COMMITTED = Symbol('not-committed');
3000
3096
  function markPreUpgradePropertyBinding(element, propertyName) {
3001
3097
  const tagName = element.localName;
3002
3098
  if (!tagName?.includes('-'))
3003
- return;
3099
+ return false;
3004
3100
  const registry = element.ownerDocument?.defaultView?.customElements ?? globalThis.customElements;
3005
3101
  if (registry?.get(tagName))
3006
- return;
3102
+ return false;
3007
3103
  const target = element;
3008
3104
  if (!target[PRE_UPGRADE_PROPERTY_BINDINGS])
3009
3105
  target[PRE_UPGRADE_PROPERTY_BINDINGS] = new Set();
3010
3106
  target[PRE_UPGRADE_PROPERTY_BINDINGS].add(propertyName);
3107
+ return true;
3108
+ }
3109
+ function commitPropertyValue(element, propertyName, value) {
3110
+ if (!markPreUpgradePropertyBinding(element, propertyName)) {
3111
+ element[propertyName] = value;
3112
+ return;
3113
+ }
3114
+ // An undefined custom element can inherit a native IDL setter with the same
3115
+ // name. Assignment would invoke that setter (for example HTMLElement.role),
3116
+ // stringify structured data into an attribute, and leave no own value for
3117
+ // @property's upgrade adoption. Park every pre-upgrade binding as the own,
3118
+ // configurable value that the initializer already knows how to replay.
3119
+ Object.defineProperty(element, propertyName, {
3120
+ configurable: true,
3121
+ enumerable: true,
3122
+ writable: true,
3123
+ value,
3124
+ });
3011
3125
  }
3012
3126
  // noChange sentinel - preserves the currently committed value
3013
3127
  const noChange = Symbol.for('snice:no-change');
@@ -4644,15 +4758,13 @@ class SpreadPart extends Part {
4644
4758
  commitProperties(next) {
4645
4759
  for (const key of Object.keys(this.committed)) {
4646
4760
  if (!Object.prototype.hasOwnProperty.call(next, key)) {
4647
- markPreUpgradePropertyBinding(this.element, key);
4648
- this.element[key] = undefined;
4761
+ commitPropertyValue(this.element, key, undefined);
4649
4762
  }
4650
4763
  }
4651
4764
  for (const [key, value] of Object.entries(next)) {
4652
4765
  if (Object.is(this.committed[key], value))
4653
4766
  continue;
4654
- markPreUpgradePropertyBinding(this.element, key);
4655
- this.element[key] = value === nothing ? undefined : value;
4767
+ commitPropertyValue(this.element, key, value === nothing ? undefined : value);
4656
4768
  }
4657
4769
  }
4658
4770
  commitAttributes(next) {
@@ -4779,8 +4891,7 @@ class PropertyPart extends Part {
4779
4891
  return;
4780
4892
  }
4781
4893
  this._committedValue = value;
4782
- markPreUpgradePropertyBinding(this.element, this.name);
4783
- this.element[this.name] = value === nothing ? undefined : value;
4894
+ commitPropertyValue(this.element, this.name, value === nothing ? undefined : value);
4784
4895
  }
4785
4896
  clear() {
4786
4897
  // This part is being destroyed with its owning template. The element is
@@ -6812,7 +6923,7 @@ function applyElementFunctionality(constructor) {
6812
6923
  // Re-establish handlers that get cleaned up on disconnect
6813
6924
  setupEventHandlers(this, this);
6814
6925
  setupResponseHandlers(this, this);
6815
- setupContextHandler(this);
6926
+ void setupContextHandler(this, 'task');
6816
6927
  // Re-establish observers that get cleaned up on disconnect
6817
6928
  try {
6818
6929
  setupObservers(this, this);
@@ -6937,7 +7048,7 @@ function applyElementFunctionality(constructor) {
6937
7048
  }
6938
7049
  setupEventHandlers(this, this);
6939
7050
  setupResponseHandlers(this, this);
6940
- setupContextHandler(this);
7051
+ void setupContextHandler(this, 'task');
6941
7052
  this[INITIALIZED] = true;
6942
7053
  // One-time (per class) sanity check: a @watch name with no matching
6943
7054
  // @property never fires — surface the typo instead of staying silent.
@@ -8642,10 +8753,11 @@ function bindContextDaemons(context) {
8642
8753
  *
8643
8754
  * Daemon instances must already exist in context.daemons. The returned cleanup
8644
8755
  * removes the provider and deactivates framework communication for daemons no
8645
- * longer provided by any context.
8756
+ * longer provided by any context. An optional fetch function is exposed on the
8757
+ * same boundary through getContextFetch() without modifying the context object.
8646
8758
  */
8647
- function provideContext(root, context) {
8648
- const releaseProvider = installContextProvider(root, context);
8759
+ function provideContextBoundary(root, context, options, navigation) {
8760
+ const releaseProvider = installContextProvider(root, context, options.fetch, navigation);
8649
8761
  let releaseDaemons;
8650
8762
  try {
8651
8763
  releaseDaemons = bindContextDaemons(context);
@@ -8663,6 +8775,15 @@ function provideContext(root, context) {
8663
8775
  releaseDaemons?.();
8664
8776
  };
8665
8777
  }
8778
+ function provideContext(root, context, options = {}) {
8779
+ return provideContextBoundary(root, context, options);
8780
+ }
8781
+ /** @internal Router-owned provider including live navigation state. */
8782
+ function provideRouterContext(root, navigation) {
8783
+ return provideContextBoundary(root, navigation.application, {
8784
+ fetch: navigation.fetch,
8785
+ }, navigation);
8786
+ }
8666
8787
 
8667
8788
  // Router historically allows a later Router instance to take over the same
8668
8789
  // target. Track only Router-owned provider releases so that handoff remains
@@ -8706,7 +8827,7 @@ function Router(options) {
8706
8827
  return;
8707
8828
  releaseContextProvider?.();
8708
8829
  routerContextProviders.get(target)?.();
8709
- const releaseProvidedContext = provideContext(target, context);
8830
+ const releaseProvidedContext = provideRouterContext(target, navigationContext);
8710
8831
  let released = false;
8711
8832
  const release = () => {
8712
8833
  if (released)
@@ -9590,7 +9711,7 @@ if (typeof document !== 'undefined') {
9590
9711
  }
9591
9712
 
9592
9713
  /*!
9593
- * snice v7.2.0
9714
+ * snice v7.3.0
9594
9715
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
9595
9716
  * (c) 2024
9596
9717
  * Released under the MIT License.
@@ -9612,7 +9733,7 @@ if (!globalThis.snice) {
9612
9733
  globalThis.snice.pendingControllerAttachments ??= new Map();
9613
9734
 
9614
9735
  /*!
9615
- * snice v7.2.0
9736
+ * snice v7.3.0
9616
9737
  * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
9617
9738
  * (c) 2024
9618
9739
  * Released under the MIT License.
@@ -9630,5 +9751,5 @@ var TransitionMode;
9630
9751
  TransitionMode["SIMULTANEOUS"] = "simultaneous";
9631
9752
  })(TransitionMode || (TransitionMode = {}));
9632
9753
 
9633
- export { Context, ContextAwareFetcher, IS_CONTROLLER_INSTANCE, Router, SimpleArray, SniceElement, TransitionMode$1 as TransitionMode, adopted, applyElementFunctionality, attachController, classMap, clearDebounceTimers, clearMemoizeCache, clearThrottleTimers, context$1 as context, context as contextProperty, controller, createRequestHandler, css, daemon, debounce, detachController, dispatch, dispose, element, escapeAttr, escapeHtml, getBodyScrollLockCount, getContext, getController, getSymbol, html, isSafeUrl, layout, live, lockBodyScroll, memoize, moved, noChange, nothing, observe, on, once, parseDuration, property, provideContext, query, queryAll, ready, reconnect, render, repeat, request, resetOnce, respond, setDisableElementReadyWarnings, setStrictRenderErrors, state, styleMap, styles, svg, throttle, trackRenders, unlockBodyScroll, unsafeHTML, useNativeElementControllers, waitForAllCustomElements, waitForElementDefined, waitForElementReady, watch };
9754
+ export { Context, ContextAwareFetcher, IS_CONTROLLER_INSTANCE, Router, SimpleArray, SniceElement, TransitionMode$1 as TransitionMode, adopted, applyElementFunctionality, attachController, classMap, clearDebounceTimers, clearMemoizeCache, clearThrottleTimers, context$1 as context, context as contextProperty, controller, createRequestHandler, css, daemon, debounce, detachController, dispatch, dispose, element, escapeAttr, escapeHtml, getBodyScrollLockCount, getContext, getContextFetch, getController, getSymbol, html, isSafeUrl, layout, live, lockBodyScroll, memoize, moved, noChange, nothing, observe, on, once, parseDuration, property, provideContext, query, queryAll, ready, reconnect, render, repeat, request, resetOnce, respond, setDisableElementReadyWarnings, setStrictRenderErrors, state, styleMap, styles, svg, throttle, trackRenders, unlockBodyScroll, unsafeHTML, useNativeElementControllers, waitForAllCustomElements, waitForElementDefined, waitForElementReady, watch };
9634
9755
  //# sourceMappingURL=snice-runtime.esm.js.map