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
@@ -12,22 +12,11 @@ reconstruct Snice APIs from Lit or generic web-component conventions.
12
12
 
13
13
  Make this decision before creating files or choosing decorators:
14
14
 
15
- - **Visual behavior belongs in the element.** Rendering, internal DOM, focus,
16
- keyboard behavior, visual state, and translation of internal-part events into
17
- the element's semantic events stay in the `@element` class.
18
- - **Application behavior specific to a set of elements belongs in a
19
- controller.** API/storage access, business rules, and app-specific reactions
20
- that exist for those elements stay in an `@controller` class.
21
- - **Element orchestration belongs in the page.** A `@page` composes elements,
22
- passes properties, handles their events, binds their controllers, and
23
- coordinates the screen. Routing is one page concern; it is not the definition
24
- of the page's role.
25
-
26
- Do not attach a controller to the page host or move orchestration into a “page
27
- controller.” Do not move visual behavior into a controller merely to make an
28
- element smaller. A host-free reusable function may stay a plain module wherever
29
- the project convention puts it. State plus lifecycle belongs in an explicitly
30
- constructed `@daemon`, not a singleton.
15
+ - **Element:** visual behavior, rendering, internal DOM, focus, keyboard behavior, visual state, and semantic events.
16
+ - **Controller:** application behavior specific to a set of elements, including API/storage access and business rules.
17
+ - **Page:** element orchestration: composition, properties, events, controller binding, and screen coordination. Routing is only one page concern.
18
+
19
+ Do not attach a controller to a page host, create a “page controller,” or move visual behavior into a controller to shrink an element. Host-free reusable functions may stay plain modules. State plus lifecycle belongs in an explicitly constructed `@daemon`, never a singleton.
31
20
 
32
21
  Examples: focus/internal buttons → element; element-specific API behavior → controller;
33
22
  coordinating search, results, and dialog elements → page; state plus lifecycle → daemon.
@@ -76,6 +65,11 @@ type-checks with `npx tsc -p references/tsconfig.kitchen-sink.json` after
76
65
  - Import React wrappers from `snice/react`; also register each underlying
77
66
  custom element through its documented deep side-effect import.
78
67
  - Pass objects and arrays through property bindings, not string attributes.
68
+ - In Router-backed controllers, receive `Context` with `@context()` and use
69
+ `ctx.fetch` for middleware-aware transport. Managed decorators activate
70
+ after `attach()`, so context-dependent startup belongs in the `@context()`
71
+ handler. Do not call bare `fetch`, import the Router into the controller, or
72
+ invent another context-injection channel.
79
73
  - Preserve native form, keyboard, focus, and accessibility behavior.
80
74
  - Use the project compiler configuration for TC39 decorators.
81
75
  - Do not edit generated distribution, adapter, metadata, or website output.
@@ -85,6 +79,8 @@ configuration, import, hallucination, and component-recommendation checks.
85
79
  Use `npx snice doctor` or `npx snice validate` only when isolating the
86
80
  configuration or source half of a failed check.
87
81
 
82
+ Diagnostics expose stable codes. After reading the referenced AI docs and verifying intentional acceptance, the agent may record the narrowest suppression in project-root `.sniceignore`: `code path:line:column` (one diagnostic), `code path` (one file), or `code` (global). Use global only when the project rejects the rule. Never suppress merely to pass a gate; comment non-obvious reasons.
83
+
88
84
  ## Verify
89
85
 
90
86
  Run the consuming project's available checks in this order:
package/README.md CHANGED
@@ -433,7 +433,11 @@ setValue(val: string) {
433
433
 
434
434
  ### Global State
435
435
 
436
- **`@context(options?)`** - Receive router context updates (global state)
436
+ **`@context(options?)`** - Receive Router context updates on pages, descendant
437
+ elements, and attached controllers. Controllers can use the same
438
+ middleware-aware `ctx.fetch` as pages; context-dependent controller startup
439
+ belongs in the `@context()` handler because managed decorators activate after
440
+ `attach()`.
437
441
  ```typescript
438
442
  // Method decorator that receives context updates
439
443
  @context()
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -21,7 +21,7 @@ export const ANALYZER_CONTRACTS = {
21
21
  "componentModules": 188,
22
22
  "componentUtilityModules": 31,
23
23
  "reactWrappers": 193,
24
- "rootExports": 116
24
+ "rootExports": 118
25
25
  },
26
26
  "rootExports": [
27
27
  "AdoptedOptions",
@@ -30,6 +30,7 @@ export const ANALYZER_CONTRACTS = {
30
30
  "Context",
31
31
  "ContextAwareFetcher",
32
32
  "ContextOptions",
33
+ "ContextProviderOptions",
33
34
  "ControllerClass",
34
35
  "CreateRequestHandlerOptions",
35
36
  "DaemonMap",
@@ -99,6 +100,7 @@ export const ANALYZER_CONTRACTS = {
99
100
  "escapeHtml",
100
101
  "getBodyScrollLockCount",
101
102
  "getContext",
103
+ "getContextFetch",
102
104
  "getController",
103
105
  "getSymbol",
104
106
  "html",
@@ -47,6 +47,11 @@ const LEGACY_LIT_LIFECYCLE = new Set([
47
47
  'performUpdate'
48
48
  ]);
49
49
 
50
+ const NATIVE_ELEMENT_IDL_MEMBERS = new Set([
51
+ 'autofocus', 'dir', 'hidden', 'id', 'inert', 'lang', 'role', 'slot',
52
+ 'title', 'translate'
53
+ ]);
54
+
50
55
  const SNICE_REACT_EVENT_PROPS = new Set([
51
56
  'onInputChange', 'onSelectChange', 'onTextareaChange', 'onCheckboxChange',
52
57
  'onRadioChange', 'onSliderChange', 'onDateChange', 'onTimeChange',
@@ -525,6 +530,44 @@ const RULE_DEFINITIONS = [
525
530
  }
526
531
  }
527
532
  },
533
+ {
534
+ id: 'snice/imperative-controller-attach',
535
+ severity: 'suggestion',
536
+ category: 'architecture',
537
+ description: 'Prefer declarative controller bindings in application templates.',
538
+ check(context) {
539
+ if (isFrameworkImplementation(context.filename) || isTestFilename(context.filename)) return;
540
+ for (const localName of localsFor(context.provenance.rootBindings, 'attachController')) {
541
+ const pattern = new RegExp(`\\b${escapeRegExp(localName)}\\s*\\(`, 'g');
542
+ for (const match of context.source.matchAll(pattern)) {
543
+ context.report(match.index, {
544
+ message: `${localName}() imperatively attaches a controller in application code even though controller bindings own this lifecycle declaratively.`,
545
+ fix: 'Bind the decorated class in the owning template with controller=${ControllerClass}. Keep imperative attachment for focused framework tests only. Review docs/ai/controllers.md.'
546
+ });
547
+ }
548
+ }
549
+ }
550
+ },
551
+ {
552
+ id: 'snice/element-member-shadows-native-idl',
553
+ severity: 'warning',
554
+ category: 'properties',
555
+ description: 'Warn when a Snice element redeclares an inherited HTMLElement IDL member.',
556
+ check(context) {
557
+ if (isFrameworkImplementation(context.filename)) return;
558
+ for (const declaration of findSniceElementClasses(context.source)) {
559
+ const members = inspectElementMembers(declaration.body);
560
+ for (const name of new Set([...members.fields, ...members.accessors, ...members.reactive])) {
561
+ if (!NATIVE_ELEMENT_IDL_MEMBERS.has(name)) continue;
562
+ const relativeIndex = members.memberIndices.get(name) ?? 0;
563
+ context.report(declaration.bodyStart + relativeIndex, {
564
+ message: `${declaration.name}.${name} shadows HTMLElement.${name}, so native reflection/accessibility behavior and Snice property binding can conflict.`,
565
+ fix: `Rename the application-owned member. If native ${name} semantics are intended, use the inherited HTMLElement property/attribute without redeclaring it.`
566
+ });
567
+ }
568
+ }
569
+ }
570
+ },
528
571
  {
529
572
  id: 'snice/self-ready-await',
530
573
  severity: 'error',
@@ -806,6 +849,51 @@ const RULE_DEFINITIONS = [
806
849
  }
807
850
  }
808
851
  },
852
+ {
853
+ id: 'snice/live-control-value-binding',
854
+ severity: 'suggestion',
855
+ category: 'rendering',
856
+ description: 'Recommend live property bindings for controlled values on verified self-mutating Snice controls.',
857
+ check(context) {
858
+ const components = {
859
+ 'snice-input': 'input',
860
+ 'snice-textarea': 'textarea',
861
+ 'snice-segmented-control': 'segmented-control'
862
+ };
863
+ for (const opening of findOpeningTags(context.source, Object.keys(components))) {
864
+ const binding = /(?:^|\s)value\s*=\s*(?:['"])?\$\{/.exec(opening.text);
865
+ if (!binding) continue;
866
+ const valueIndex = opening.index + binding.index + binding[0].indexOf('value');
867
+ context.report(valueIndex, {
868
+ message: `${opening.name} receives a dynamic value attribute, but its live value can diverge after user interaction.`,
869
+ fix: `For a controlled value, import live from 'snice' and use .value=\${live(value)} so an owner render compares with the control's current property. Keep value= only for an authored/reset default. Review docs/ai/components/${components[opening.name]}.md and docs/ai/bindings.md.`
870
+ });
871
+ }
872
+ }
873
+ },
874
+ {
875
+ id: 'snice/prefer-dispatch-decorator',
876
+ severity: 'suggestion',
877
+ category: 'events',
878
+ description: 'Prefer @dispatch for static CustomEvents emitted from a Snice host.',
879
+ check(context) {
880
+ if (isFrameworkImplementation(context.filename)) return;
881
+
882
+ const declarations = [
883
+ ...findSniceElementClasses(context.source).map(declaration => ({ declaration, role: 'element' })),
884
+ ...findDecoratedClasses(context.source, 'controller').map(declaration => ({ declaration, role: 'controller' }))
885
+ ];
886
+
887
+ for (const { declaration, role } of declarations) {
888
+ for (const dispatch of findManualCustomEventDispatches(declaration.body, role)) {
889
+ context.report(declaration.bodyStart + dispatch.index, {
890
+ message: `Manual host dispatch of "${dispatch.eventName}" duplicates the standard @dispatch CustomEvent path.`,
891
+ fix: `Prefer an emitter method decorated with @dispatch('${dispatch.eventName}') and return the detail payload directly. Keep dispatchEvent() when code needs the Event object, a dynamic event name, or the cancellation boolean. Review docs/ai/events.md.`
892
+ });
893
+ }
894
+ }
895
+ }
896
+ },
809
897
  {
810
898
  id: 'snice/custom-event-contract',
811
899
  severity: 'error',
@@ -1424,6 +1512,14 @@ const RULE_DEFINITIONS = [
1424
1512
  projectOnly: true,
1425
1513
  check() {}
1426
1514
  },
1515
+ {
1516
+ id: 'snice/shadowed-query-route',
1517
+ severity: 'warning',
1518
+ category: 'router',
1519
+ description: 'Warn when same-page string route order makes a query-parameter variant unreachable.',
1520
+ projectOnly: true,
1521
+ check() {}
1522
+ },
1427
1523
  {
1428
1524
  id: 'snice/template-reads-nonreactive-field',
1429
1525
  severity: 'warning',
@@ -1465,7 +1561,7 @@ const RULE_DEFINITIONS = [
1465
1561
  */
1466
1562
  export const PROJECT_ANALYZER_RULES = Object.freeze(
1467
1563
  RULE_DEFINITIONS.map(({ id, severity, category, description }) =>
1468
- Object.freeze({ id, severity, category, description })
1564
+ Object.freeze({ id, code: id, severity, category, description })
1469
1565
  )
1470
1566
  );
1471
1567
 
@@ -1476,6 +1572,7 @@ export const PROJECT_ANALYZER_RULES = Object.freeze(
1476
1572
  * @param {string} [filename=''] Optional filename included in every diagnostic.
1477
1573
  * @returns {Array<{
1478
1574
  * severity: 'error'|'warning'|'suggestion',
1575
+ * code: string,
1479
1576
  * ruleId: string,
1480
1577
  * message: string,
1481
1578
  * fix: string,
@@ -1499,6 +1596,7 @@ export function analyzeSource(source, filename = '') {
1499
1596
  const location = sourceLocation(source, Math.max(0, nonSource.index));
1500
1597
  return [{
1501
1598
  severity: 'error',
1599
+ code: 'snice/non-source-content',
1502
1600
  ruleId: 'snice/non-source-content',
1503
1601
  message: nonSource.message,
1504
1602
  fix: nonSource.fix,
@@ -1526,6 +1624,7 @@ export function analyzeSource(source, filename = '') {
1526
1624
  seen.add(key);
1527
1625
  const diagnostic = {
1528
1626
  severity: rule.severity,
1627
+ code: rule.id,
1529
1628
  ruleId: rule.id,
1530
1629
  message: detail.message,
1531
1630
  fix: detail.fix,
@@ -1868,6 +1967,27 @@ export function analyzeProject(files) {
1868
1967
  column: location.column
1869
1968
  });
1870
1969
  }
1970
+
1971
+ const precedingBareRoutes = new Set();
1972
+ for (const route of findPageStringRoutes(page.file.source, page.index)) {
1973
+ const queryIndex = route.spec.indexOf('?');
1974
+ const pathname = queryIndex >= 0 ? route.spec.slice(0, queryIndex) : route.spec;
1975
+ if (queryIndex < 0) {
1976
+ precedingBareRoutes.add(pathname);
1977
+ continue;
1978
+ }
1979
+ if (!precedingBareRoutes.has(pathname)) continue;
1980
+ const location = sourceLocation(page.file.source, route.index);
1981
+ diagnostics.push({
1982
+ severity: 'warning',
1983
+ ruleId: 'snice/shadowed-query-route',
1984
+ message: `${page.name}'s query route "${route.spec}" is shadowed by an earlier "${pathname}" entry with the same specificity, so its query properties never bind.`,
1985
+ fix: `Move "${route.spec}" before "${pathname}" in the routes array, or use route objects with explicit order values when a more complex tie-break is intentional. Review docs/ai/routing.md.`,
1986
+ file: page.file.filename,
1987
+ line: location.line,
1988
+ column: location.column
1989
+ });
1990
+ }
1871
1991
  }
1872
1992
  for (const [key, declarations] of declarationsByClass) {
1873
1993
  const page = declarations.find(declaration => declaration.decorator === 'page');
@@ -2040,7 +2160,9 @@ export function analyzeProject(files) {
2040
2160
  }
2041
2161
  }
2042
2162
 
2043
- return diagnostics.sort(compareProjectDiagnostics);
2163
+ return diagnostics
2164
+ .map(diagnostic => diagnostic.code ? diagnostic : { ...diagnostic, code: diagnostic.ruleId })
2165
+ .sort(compareProjectDiagnostics);
2044
2166
  }
2045
2167
 
2046
2168
  /**
@@ -2290,10 +2412,91 @@ function findSniceElementClasses(source) {
2290
2412
  return declarations;
2291
2413
  }
2292
2414
 
2415
+ /**
2416
+ * Find standalone, static CustomEvent dispatches that @dispatch can express
2417
+ * without changing control flow. Calls whose return value is consumed, whose
2418
+ * event name is dynamic, or whose target is not the Snice host are deliberately
2419
+ * left alone as valid low-level event code.
2420
+ */
2421
+ function findManualCustomEventDispatches(body, role) {
2422
+ const dispatches = [];
2423
+ const pattern = role === 'controller'
2424
+ ? /\bthis\s*\.\s*element\s*(?:\?\.\s*|\.\s*)dispatchEvent\s*\(/g
2425
+ : /\bthis\s*\.\s*dispatchEvent\s*\(/g;
2426
+
2427
+ for (const match of body.matchAll(pattern)) {
2428
+ const lineStart = body.lastIndexOf('\n', match.index) + 1;
2429
+ if (body.slice(lineStart, match.index).trim()) continue;
2430
+
2431
+ const dispatchOpen = body.indexOf('(', match.index);
2432
+ const dispatchClose = findMatchingDelimiter(body, dispatchOpen, '(', ')');
2433
+ if (dispatchOpen < 0 || dispatchClose < 0) continue;
2434
+
2435
+ let afterDispatch = dispatchClose + 1;
2436
+ while (body[afterDispatch] === ' ' || body[afterDispatch] === '\t') afterDispatch++;
2437
+ if (body[afterDispatch] !== ';') continue;
2438
+ const lineEnd = body.indexOf('\n', afterDispatch);
2439
+ const remainder = body.slice(afterDispatch + 1, lineEnd < 0 ? body.length : lineEnd);
2440
+ if (remainder.trim()) continue;
2441
+
2442
+ let cursor = dispatchOpen + 1;
2443
+ while (/\s/.test(body[cursor] ?? '')) cursor++;
2444
+ if (!body.startsWith('new', cursor) || /[\w$]/.test(body[cursor + 3] ?? '')) continue;
2445
+ cursor += 3;
2446
+ while (/\s/.test(body[cursor] ?? '')) cursor++;
2447
+ if (!body.startsWith('CustomEvent', cursor) || /[\w$]/.test(body[cursor + 11] ?? '')) continue;
2448
+ cursor += 11;
2449
+ while (/\s/.test(body[cursor] ?? '')) cursor++;
2450
+
2451
+ if (body[cursor] === '<') {
2452
+ const genericClose = findMatchingDelimiter(body, cursor, '<', '>');
2453
+ if (genericClose < 0) continue;
2454
+ cursor = genericClose + 1;
2455
+ while (/\s/.test(body[cursor] ?? '')) cursor++;
2456
+ }
2457
+
2458
+ if (body[cursor] !== '(') continue;
2459
+ cursor++;
2460
+ while (/\s/.test(body[cursor] ?? '')) cursor++;
2461
+ const eventName = /^(['"])([^'"]+)\1/.exec(body.slice(cursor));
2462
+ if (!eventName) continue;
2463
+
2464
+ dispatches.push({ index: match.index, eventName: eventName[2] });
2465
+ }
2466
+
2467
+ return dispatches;
2468
+ }
2469
+
2293
2470
  function findElementDecoratorTag(source, index) {
2294
2471
  return /^@element\s*\(\s*['"]([^'"]+)['"]/.exec(source.slice(index, index + 500))?.[1] ?? null;
2295
2472
  }
2296
2473
 
2474
+ function findPageStringRoutes(source, decoratorIndex) {
2475
+ const open = source.indexOf('(', decoratorIndex);
2476
+ if (open < 0) return [];
2477
+ const close = findMatchingDelimiter(source, open, '(', ')');
2478
+ if (close < 0) return [];
2479
+ const argumentsSource = source.slice(open + 1, close);
2480
+ const routesProperty = /\broutes\s*:\s*\[/.exec(argumentsSource);
2481
+ if (!routesProperty) return [];
2482
+ const arrayOpen = open + 1 + routesProperty.index + routesProperty[0].lastIndexOf('[');
2483
+ const arrayClose = findMatchingDelimiter(source, arrayOpen, '[', ']');
2484
+ if (arrayClose < 0 || arrayClose > close) return [];
2485
+
2486
+ const body = source.slice(arrayOpen + 1, arrayClose);
2487
+ const routes = [];
2488
+ let searchFrom = 0;
2489
+ for (const item of splitTopLevelArguments(body)) {
2490
+ const relative = body.indexOf(item, searchFrom);
2491
+ if (relative < 0) continue;
2492
+ searchFrom = relative + item.length;
2493
+ const literal = /^(['"])([^'"]+)\1$/.exec(item);
2494
+ if (!literal) continue;
2495
+ routes.push({ spec: literal[2], index: arrayOpen + 1 + relative });
2496
+ }
2497
+ return routes;
2498
+ }
2499
+
2297
2500
  function inspectElementMembers(body) {
2298
2501
  const code = blankStringContents(maskComments(body));
2299
2502
  const depths = new Uint16Array(code.length + 1);
@@ -2309,11 +2512,13 @@ function inspectElementMembers(body) {
2309
2512
  const readonly = new Set();
2310
2513
  const functionFields = new Set();
2311
2514
  const reactive = new Set();
2312
- const fieldPattern = /(?:^|\n)\s*((?:(?:public|private|protected|static|readonly|override|declare|abstract)\s+)*)([A-Za-z_$][\w$]*)\s*[!?]?\s*(?::[^=\n;{]+)?\s*(?==|;)/g;
2515
+ const memberIndices = new Map();
2516
+ const fieldPattern = /(?:^|\n)\s*((?:(?:public|private|protected|static|readonly|override|declare|abstract)\s+)*)([A-Za-z_$][\w$]*)\s*[!?]?\s*(?::[^=\n;]+)?\s*(?==|;)/g;
2313
2517
  for (const match of code.matchAll(fieldPattern)) {
2314
2518
  const nameIndex = match.index + match[0].lastIndexOf(match[2]);
2315
2519
  if (depths[nameIndex] !== 0) continue;
2316
2520
  fields.add(match[2]);
2521
+ if (!memberIndices.has(match[2])) memberIndices.set(match[2], nameIndex);
2317
2522
  if (/\breadonly\b/.test(match[1])) readonly.add(match[2]);
2318
2523
  }
2319
2524
  const functionFieldPattern = /(?:^|\n)\s*(?:(?:public|private|protected|static|readonly|override|declare|abstract)\s+)*([A-Za-z_$][\w$]*)\s*[!?]?\s*(?::[^=\n;{]+)?\s*=\s*(?:async\s*)?(?:\([^\n)]*\)|[A-Za-z_$][\w$]*)\s*=>/g;
@@ -2324,7 +2529,10 @@ function inspectElementMembers(body) {
2324
2529
  const accessorPattern = /(?:^|\n)\s*(?:(?:public|private|protected|static|readonly|override|declare|abstract)\s+)*(?:get|set|accessor)\s+([A-Za-z_$][\w$]*)\b/g;
2325
2530
  for (const match of code.matchAll(accessorPattern)) {
2326
2531
  const nameIndex = match.index + match[0].lastIndexOf(match[1]);
2327
- if (depths[nameIndex] === 0) accessors.add(match[1]);
2532
+ if (depths[nameIndex] === 0) {
2533
+ accessors.add(match[1]);
2534
+ if (!memberIndices.has(match[1])) memberIndices.set(match[1], nameIndex);
2535
+ }
2328
2536
  }
2329
2537
  const decoratedPattern = /@(property|state)\b/g;
2330
2538
  for (const match of code.matchAll(decoratedPattern)) {
@@ -2337,7 +2545,12 @@ function inspectElementMembers(body) {
2337
2545
  cursor = close + 1;
2338
2546
  }
2339
2547
  const declaration = /^\s*(?:(?:public|private|protected|static|readonly|override|declare|abstract)\s+)*(?:accessor\s+)?([A-Za-z_$][\w$]*)/.exec(code.slice(cursor));
2340
- if (declaration) reactive.add(declaration[1]);
2548
+ if (declaration) {
2549
+ reactive.add(declaration[1]);
2550
+ if (!memberIndices.has(declaration[1])) {
2551
+ memberIndices.set(declaration[1], cursor + declaration.index + declaration[0].lastIndexOf(declaration[1]));
2552
+ }
2553
+ }
2341
2554
  }
2342
2555
 
2343
2556
  // An undecorated native-style accessor can still be reactive when its
@@ -2366,7 +2579,7 @@ function inspectElementMembers(body) {
2366
2579
  if (close >= 0 && invalidates(body.slice(open + 1, close))) invalidatingAccessors.add(match[1]);
2367
2580
  }
2368
2581
 
2369
- return { fields, accessors, readonly, functionFields, reactive, invalidatingAccessors };
2582
+ return { fields, accessors, readonly, functionFields, reactive, invalidatingAccessors, memberIndices };
2370
2583
  }
2371
2584
 
2372
2585
  function findDecoratedClassMethods(body, decoratorName, bodyStart = 0) {
package/bin/snice.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  import { execFile } from 'node:child_process';
15
15
  import { createRequire } from 'node:module';
16
16
  import { tmpdir } from 'node:os';
17
- import { basename, dirname, join, resolve } from 'node:path';
17
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
18
18
  import { fileURLToPath, pathToFileURL } from 'node:url';
19
19
  import { promisify } from 'node:util';
20
20
  import { analyzeProject, findImports, isTypeOnlyImport, maskComments } from './project-analyzer.js';
@@ -26,6 +26,7 @@ const packageRoot = join(__dirname, '..');
26
26
  const packageJson = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
27
27
  const argv = process.argv.slice(2);
28
28
  const command = argv[0];
29
+ const DIAGNOSTIC_IGNORE_FILENAME = '.sniceignore';
29
30
 
30
31
  function help() {
31
32
  console.log(`Snice CLI ${packageJson.version}
@@ -52,6 +53,9 @@ Commands:
52
53
  AI setup:
53
54
  npx snice init-ai Install version-matched guidance before an agent writes Snice code.
54
55
 
56
+ Diagnostics:
57
+ .sniceignore Suppress diagnostic codes globally or at an exact source location.
58
+
55
59
  Build options:
56
60
  --output=<dir> Output directory (default: ./dist/cdn)
57
61
  --format=iife,es Output formats (default: iife; table: iife,es)
@@ -207,21 +211,79 @@ function walkSource(root) {
207
211
  }
208
212
 
209
213
  function validateProject(targetDir, json = false) {
210
- const issues = collectValidationIssues(targetDir);
214
+ const ignored = readDiagnosticIgnores(targetDir);
215
+ const issues = collectValidationIssues(targetDir, ignored);
211
216
  renderValidation(issues, json);
212
217
  return issues;
213
218
  }
214
219
 
215
- function collectValidationIssues(targetDir) {
220
+ function collectValidationIssues(targetDir, ignored = readDiagnosticIgnores(targetDir)) {
216
221
  const sourceRoot = existsSync(join(targetDir, 'src')) ? join(targetDir, 'src') : targetDir;
217
222
  const projectFiles = ['package.json', 'tsconfig.json', 'index.html']
218
223
  .map(filename => join(targetDir, filename))
219
224
  .filter(existsSync);
220
225
  const files = [...new Set([...projectFiles, ...walkSource(sourceRoot)])].sort();
221
- return analyzeProject(files.map(file => ({
226
+ const issues = analyzeProject(files.map(file => ({
222
227
  filename: file,
223
228
  source: readFileSync(file, 'utf8')
224
229
  })));
230
+ return filterIgnoredDiagnostics(issues, ignored, targetDir);
231
+ }
232
+
233
+ function readDiagnosticIgnores(targetDir) {
234
+ const path = join(targetDir, DIAGNOSTIC_IGNORE_FILENAME);
235
+ if (!existsSync(path)) return [];
236
+
237
+ const entries = [];
238
+ for (const authoredLine of readFileSync(path, 'utf8').replace(/^\uFEFF/, '').split(/\r?\n/)) {
239
+ const line = authoredLine.replace(/\s+#.*$/, '').trim();
240
+ if (!line || line.startsWith('#')) continue;
241
+ const separator = line.search(/\s/);
242
+ const code = separator < 0 ? line : line.slice(0, separator);
243
+ const locator = separator < 0 ? '' : line.slice(separator).trim();
244
+ if (!locator) {
245
+ entries.push({ code });
246
+ continue;
247
+ }
248
+
249
+ let file = locator;
250
+ let lineNumber;
251
+ let column;
252
+ const exact = /^(.*):(\d+):(\d+)$/.exec(locator);
253
+ const lineOnly = exact ? null : /^(.*):(\d+)$/.exec(locator);
254
+ if (exact) {
255
+ file = exact[1];
256
+ lineNumber = Number(exact[2]);
257
+ column = Number(exact[3]);
258
+ } else if (lineOnly) {
259
+ file = lineOnly[1];
260
+ lineNumber = Number(lineOnly[2]);
261
+ }
262
+ if (!file) continue;
263
+ entries.push({ code, file: normalizeDiagnosticPath(file), line: lineNumber, column });
264
+ }
265
+ return entries;
266
+ }
267
+
268
+ function filterIgnoredDiagnostics(diagnostics, ignored, targetDir) {
269
+ if (!ignored.length) return diagnostics;
270
+ return diagnostics.filter(diagnostic => !ignored.some(entry => {
271
+ const code = diagnostic.code ?? diagnostic.ruleId;
272
+ if (entry.code !== code) return false;
273
+ if (!entry.file) return true;
274
+ if (!diagnostic.file) return false;
275
+ const absolute = isAbsolute(diagnostic.file)
276
+ ? diagnostic.file
277
+ : resolve(targetDir, diagnostic.file);
278
+ if (normalizeDiagnosticPath(relative(targetDir, absolute)) !== entry.file) return false;
279
+ if (entry.line !== undefined && diagnostic.line !== entry.line) return false;
280
+ if (entry.column !== undefined && diagnostic.column !== entry.column) return false;
281
+ return true;
282
+ }));
283
+ }
284
+
285
+ function normalizeDiagnosticPath(path) {
286
+ return path.replaceAll('\\', '/').replace(/^\.\//, '');
225
287
  }
226
288
 
227
289
  function renderValidation(issues, json = false) {
@@ -230,7 +292,7 @@ function renderValidation(issues, json = false) {
230
292
  else {
231
293
  for (const issue of issues) {
232
294
  console.log(
233
- `[${issue.severity.toUpperCase()}] ${issue.file ?? ''}:${issue.line}:${issue.column} ${issue.ruleId}\n` +
295
+ `[${issue.severity.toUpperCase()}] ${issue.file ?? ''}:${issue.line}:${issue.column} ${issue.code}\n` +
234
296
  ` ${issue.message}\n ${issue.fix}`
235
297
  );
236
298
  }
@@ -280,7 +342,8 @@ function resolveTsconfigChain(dir, entry = 'tsconfig.json', seen = new Set()) {
280
342
  }
281
343
 
282
344
  async function doctor(targetDir, json = false) {
283
- const findings = await collectDoctorFindings(targetDir);
345
+ const ignored = readDiagnosticIgnores(targetDir);
346
+ const findings = await collectDoctorFindings(targetDir, ignored);
284
347
  renderDoctor(findings, json);
285
348
  return findings;
286
349
  }
@@ -381,7 +444,7 @@ async function probeDecoratorTransform(targetDir, add, projectPackage) {
381
444
  }
382
445
  }
383
446
 
384
- async function collectDoctorFindings(targetDir) {
447
+ async function collectDoctorFindings(targetDir, ignored = readDiagnosticIgnores(targetDir)) {
385
448
  const findings = [];
386
449
  const add = (severity, code, message) => findings.push({ severity, code, message });
387
450
  const projectPackage = readJson(join(targetDir, 'package.json'));
@@ -443,7 +506,7 @@ async function collectDoctorFindings(targetDir) {
443
506
 
444
507
  await probeDecoratorTransform(targetDir, add, projectPackage);
445
508
 
446
- return findings;
509
+ return filterIgnoredDiagnostics(findings, ignored, targetDir);
447
510
  }
448
511
 
449
512
  function renderDoctor(findings, json = false) {
@@ -451,15 +514,16 @@ function renderDoctor(findings, json = false) {
451
514
  const warnings = findings.filter(item => item.severity === 'warning').length;
452
515
  if (json) console.log(JSON.stringify({ ok: errors === 0, findings }, null, 2));
453
516
  else {
454
- for (const finding of findings) console.log(`[${finding.severity.toUpperCase()}] ${finding.message}`);
517
+ for (const finding of findings) console.log(`[${finding.severity.toUpperCase()}] ${finding.code} ${finding.message}`);
455
518
  console.log(`Doctor finished with ${errors} error(s) and ${warnings} warning(s).`);
456
519
  }
457
520
  if (errors) process.exitCode = 1;
458
521
  }
459
522
 
460
523
  async function checkProject(targetDir, json = false) {
461
- const findings = await collectDoctorFindings(targetDir);
462
- const issues = collectValidationIssues(targetDir);
524
+ const ignored = readDiagnosticIgnores(targetDir);
525
+ const findings = await collectDoctorFindings(targetDir, ignored);
526
+ const issues = collectValidationIssues(targetDir, ignored);
463
527
  const ok = !findings.some(item => item.severity === 'error') &&
464
528
  !issues.some(item => item.severity === 'error');
465
529
 
@@ -467,12 +531,12 @@ async function checkProject(targetDir, json = false) {
467
531
  console.log(JSON.stringify({ ok, findings, issues }, null, 2));
468
532
  } else {
469
533
  console.log('Project configuration:');
470
- for (const finding of findings) console.log(`[${finding.severity.toUpperCase()}] ${finding.message}`);
534
+ for (const finding of findings) console.log(`[${finding.severity.toUpperCase()}] ${finding.code} ${finding.message}`);
471
535
  console.log('\nSnice source:');
472
536
  if (!issues.length) console.log('No Snice authoring issues found.');
473
537
  for (const issue of issues) {
474
538
  console.log(
475
- `[${issue.severity.toUpperCase()}] ${issue.file ?? ''}:${issue.line}:${issue.column} ${issue.ruleId}\n` +
539
+ `[${issue.severity.toUpperCase()}] ${issue.file ?? ''}:${issue.line}:${issue.column} ${issue.code}\n` +
476
540
  ` ${issue.message}\n ${issue.fix}`
477
541
  );
478
542
  }
@@ -1,11 +1,11 @@
1
- import { getContext } from './context-provider';
1
+ import { getContext, getContextFetch } from './context-provider';
2
2
  import type { AppContext } from './types/app-context';
3
- /**
4
- * Makes one explicit application context visible beneath a root.
5
- *
6
- * Daemon instances must already exist in context.daemons. The returned cleanup
7
- * removes the provider and deactivates framework communication for daemons no
8
- * longer provided by any context.
9
- */
10
- export declare function provideContext(root: EventTarget, context: AppContext): () => void;
11
- export { getContext };
3
+ import type { Context } from './types/context';
4
+ export interface ContextProviderOptions {
5
+ /** Fetch function exposed to descendants through getContextFetch(). */
6
+ fetch?: typeof globalThis.fetch;
7
+ }
8
+ export declare function provideContext(root: EventTarget, context: AppContext, options?: ContextProviderOptions): () => void;
9
+ /** @internal Router-owned provider including live navigation state. */
10
+ export declare function provideRouterContext(root: EventTarget, navigation: Context): () => void;
11
+ export { getContext, getContextFetch };
@@ -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.