snice 7.0.3 → 7.1.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 (218) hide show
  1. package/.agents/skills/snice/SKILL.md +4 -0
  2. package/.agents/skills/snice/references/core-kitchen-sink.ts +39 -1
  3. package/README.md +59 -14
  4. package/adapters/react/SniceProvider.js +2 -2
  5. package/adapters/react/SniceRouter.js +2 -2
  6. package/adapters/react/matchRoute.js +2 -2
  7. package/adapters/react/useRequestHandler.js +2 -2
  8. package/bin/analyzer-contracts.js +6 -1
  9. package/bin/postinstall.js +59 -0
  10. package/bin/project-analyzer.js +466 -5
  11. package/bin/snice.js +3 -0
  12. package/dist/app-context.d.ts +11 -0
  13. package/dist/cdn/accordion/snice-accordion.js +2 -2
  14. package/dist/cdn/action-bar/snice-action-bar.js +2 -2
  15. package/dist/cdn/activity-feed/snice-activity-feed.js +2 -2
  16. package/dist/cdn/alert/snice-alert.js +2 -2
  17. package/dist/cdn/app-tiles/snice-app-tiles.js +2 -2
  18. package/dist/cdn/approval-flow/snice-approval-flow.js +2 -2
  19. package/dist/cdn/audio-recorder/snice-audio-recorder.js +2 -2
  20. package/dist/cdn/availability/snice-availability.js +2 -2
  21. package/dist/cdn/avatar/snice-avatar.js +2 -2
  22. package/dist/cdn/avatar-group/snice-avatar-group.js +2 -2
  23. package/dist/cdn/badge/snice-badge.js +2 -2
  24. package/dist/cdn/banner/snice-banner.js +2 -2
  25. package/dist/cdn/binpack/snice-binpack.js +2 -2
  26. package/dist/cdn/book/snice-book.js +2 -2
  27. package/dist/cdn/booking/snice-booking.js +2 -2
  28. package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +2 -2
  29. package/dist/cdn/button/snice-button.js +2 -2
  30. package/dist/cdn/calendar/snice-calendar.js +2 -2
  31. package/dist/cdn/camera/snice-camera.js +2 -2
  32. package/dist/cdn/camera-annotate/snice-camera-annotate.js +2 -2
  33. package/dist/cdn/candlestick/snice-candlestick.js +2 -2
  34. package/dist/cdn/card/snice-card.js +2 -2
  35. package/dist/cdn/carousel/snice-carousel.js +2 -2
  36. package/dist/cdn/cart/snice-cart.js +2 -2
  37. package/dist/cdn/chart/snice-chart.js +2 -2
  38. package/dist/cdn/chat/snice-chat.js +2 -2
  39. package/dist/cdn/checkbox/snice-checkbox.js +2 -2
  40. package/dist/cdn/chip/snice-chip.js +2 -2
  41. package/dist/cdn/code-block/snice-code-block.js +2 -2
  42. package/dist/cdn/color-display/snice-color-display.js +2 -2
  43. package/dist/cdn/color-picker/snice-color-picker.js +2 -2
  44. package/dist/cdn/command-palette/snice-command-palette.js +2 -2
  45. package/dist/cdn/comments/snice-comments.js +2 -2
  46. package/dist/cdn/countdown/snice-countdown.js +2 -2
  47. package/dist/cdn/cropper/snice-cropper.js +2 -2
  48. package/dist/cdn/data-card/snice-data-card.js +2 -2
  49. package/dist/cdn/date-picker/snice-date-picker.js +2 -2
  50. package/dist/cdn/date-range-picker/snice-date-range-picker.js +2 -2
  51. package/dist/cdn/date-time-picker/snice-date-time-picker.js +2 -2
  52. package/dist/cdn/diff/snice-diff.js +2 -2
  53. package/dist/cdn/divider/snice-divider.js +2 -2
  54. package/dist/cdn/doc/snice-doc.js +2 -2
  55. package/dist/cdn/draw/snice-draw.js +2 -2
  56. package/dist/cdn/drawer/snice-drawer.js +2 -2
  57. package/dist/cdn/empty-state/snice-empty-state.js +2 -2
  58. package/dist/cdn/estimate/snice-estimate.js +2 -2
  59. package/dist/cdn/file-gallery/snice-file-gallery.js +2 -2
  60. package/dist/cdn/file-upload/snice-file-upload.js +2 -2
  61. package/dist/cdn/flip-card/snice-flip-card.js +2 -2
  62. package/dist/cdn/flow/snice-flow.js +2 -2
  63. package/dist/cdn/form-layout/snice-form-layout.js +2 -2
  64. package/dist/cdn/funnel/snice-funnel.js +2 -2
  65. package/dist/cdn/gantt/snice-gantt.js +2 -2
  66. package/dist/cdn/gauge/snice-gauge.js +2 -2
  67. package/dist/cdn/grid/snice-grid.js +2 -2
  68. package/dist/cdn/heatmap/snice-heatmap.js +2 -2
  69. package/dist/cdn/image/snice-image.js +2 -2
  70. package/dist/cdn/input/snice-input.js +2 -2
  71. package/dist/cdn/invoice/snice-invoice.js +2 -2
  72. package/dist/cdn/kanban/snice-kanban.js +2 -2
  73. package/dist/cdn/key-value/snice-key-value.js +2 -2
  74. package/dist/cdn/kpi/snice-kpi.js +2 -2
  75. package/dist/cdn/layout/README.md +17 -2
  76. package/dist/cdn/layout/snice-layout.js +1656 -11
  77. package/dist/cdn/layout/snice-layout.js.map +1 -1
  78. package/dist/cdn/layout/snice-layout.min.js +379 -2
  79. package/dist/cdn/layout/snice-layout.min.js.map +1 -1
  80. package/dist/cdn/leaderboard/snice-leaderboard.js +2 -2
  81. package/dist/cdn/link/snice-link.js +2 -2
  82. package/dist/cdn/link-preview/snice-link-preview.js +2 -2
  83. package/dist/cdn/list/snice-list.js +2 -2
  84. package/dist/cdn/location/snice-location.js +2 -2
  85. package/dist/cdn/login/snice-login.js +2 -2
  86. package/dist/cdn/map/snice-map.js +2 -2
  87. package/dist/cdn/markdown/snice-markdown.js +2 -2
  88. package/dist/cdn/masonry/snice-masonry.js +2 -2
  89. package/dist/cdn/menu/snice-menu.js +2 -2
  90. package/dist/cdn/message-strip/snice-message-strip.js +2 -2
  91. package/dist/cdn/modal/snice-modal.js +2 -2
  92. package/dist/cdn/music-player/snice-music-player.js +2 -2
  93. package/dist/cdn/nav/snice-nav.js +2 -2
  94. package/dist/cdn/network-graph/snice-network-graph.js +2 -2
  95. package/dist/cdn/notification-center/snice-notification-center.js +2 -2
  96. package/dist/cdn/order-tracker/snice-order-tracker.js +2 -2
  97. package/dist/cdn/org-chart/snice-org-chart.js +2 -2
  98. package/dist/cdn/pagination/snice-pagination.js +2 -2
  99. package/dist/cdn/paint/snice-paint.js +2 -2
  100. package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +2 -2
  101. package/dist/cdn/permission-matrix/snice-permission-matrix.js +2 -2
  102. package/dist/cdn/podcast-player/snice-podcast-player.js +2 -2
  103. package/dist/cdn/popover/snice-popover.js +2 -2
  104. package/dist/cdn/pricing-table/snice-pricing-table.js +2 -2
  105. package/dist/cdn/product-card/snice-product-card.js +2 -2
  106. package/dist/cdn/progress/snice-progress.js +2 -2
  107. package/dist/cdn/progress-ring/snice-progress-ring.js +2 -2
  108. package/dist/cdn/qr-code/snice-qr-code.js +2 -2
  109. package/dist/cdn/qr-reader/snice-qr-reader.js +2 -2
  110. package/dist/cdn/radio/snice-radio.js +2 -2
  111. package/dist/cdn/range-slider/snice-range-slider.js +2 -2
  112. package/dist/cdn/rating/snice-rating.js +2 -2
  113. package/dist/cdn/receipt/snice-receipt.js +2 -2
  114. package/dist/cdn/recipe/snice-recipe.js +2 -2
  115. package/dist/cdn/runtime/README.md +2 -2
  116. package/dist/cdn/runtime/snice-runtime.esm.js +3732 -3343
  117. package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
  118. package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
  119. package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
  120. package/dist/cdn/runtime/snice-runtime.js +3727 -3335
  121. package/dist/cdn/runtime/snice-runtime.js.map +1 -1
  122. package/dist/cdn/runtime/snice-runtime.min.js +1 -1
  123. package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
  124. package/dist/cdn/sankey/snice-sankey.js +2 -2
  125. package/dist/cdn/segmented-control/snice-segmented-control.js +2 -2
  126. package/dist/cdn/select/snice-select.js +2 -2
  127. package/dist/cdn/skeleton/snice-skeleton.js +2 -2
  128. package/dist/cdn/slider/snice-slider.js +2 -2
  129. package/dist/cdn/sortable/snice-sortable.js +2 -2
  130. package/dist/cdn/sparkline/snice-sparkline.js +2 -2
  131. package/dist/cdn/spinner/snice-spinner.js +2 -2
  132. package/dist/cdn/split-button/snice-split-button.js +2 -2
  133. package/dist/cdn/split-pane/snice-split-pane.js +2 -2
  134. package/dist/cdn/spotlight/snice-spotlight.js +2 -2
  135. package/dist/cdn/stat-group/snice-stat-group.js +2 -2
  136. package/dist/cdn/step-input/snice-step-input.js +2 -2
  137. package/dist/cdn/stepper/snice-stepper.js +2 -2
  138. package/dist/cdn/switch/snice-switch.js +2 -2
  139. package/dist/cdn/table/README.md +6 -2
  140. package/dist/cdn/table/snice-table.esm.js +347 -54
  141. package/dist/cdn/table/snice-table.esm.js.map +1 -1
  142. package/dist/cdn/table/snice-table.esm.min.js +105 -44
  143. package/dist/cdn/table/snice-table.esm.min.js.map +1 -1
  144. package/dist/cdn/table/snice-table.js +348 -57
  145. package/dist/cdn/table/snice-table.js.map +1 -1
  146. package/dist/cdn/table/snice-table.min.js +153 -92
  147. package/dist/cdn/table/snice-table.min.js.map +1 -1
  148. package/dist/cdn/tabs/snice-tabs.js +2 -2
  149. package/dist/cdn/tag/snice-tag.js +2 -2
  150. package/dist/cdn/tag-input/snice-tag-input.js +2 -2
  151. package/dist/cdn/terminal/snice-terminal.js +2 -2
  152. package/dist/cdn/testimonial/snice-testimonial.js +2 -2
  153. package/dist/cdn/textarea/snice-textarea.js +2 -2
  154. package/dist/cdn/time-picker/snice-time-picker.js +2 -2
  155. package/dist/cdn/time-range-picker/snice-time-range-picker.js +2 -2
  156. package/dist/cdn/timeline/snice-timeline.js +2 -2
  157. package/dist/cdn/timer/snice-timer.js +2 -2
  158. package/dist/cdn/toast/snice-toast.js +2 -2
  159. package/dist/cdn/tooltip/snice-tooltip.js +2 -2
  160. package/dist/cdn/tree/snice-tree.js +2 -2
  161. package/dist/cdn/treemap/snice-treemap.js +2 -2
  162. package/dist/cdn/user-card/snice-user-card.js +2 -2
  163. package/dist/cdn/video-player/snice-video-player.js +2 -2
  164. package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +2 -2
  165. package/dist/cdn/waterfall/snice-waterfall.js +2 -2
  166. package/dist/cdn/weather/snice-weather.js +2 -2
  167. package/dist/cdn/work-order/snice-work-order.js +2 -2
  168. package/dist/context-provider.d.ts +16 -0
  169. package/dist/daemon-target.d.ts +10 -0
  170. package/dist/daemon.d.ts +10 -0
  171. package/dist/index.cjs +3721 -3329
  172. package/dist/index.cjs.map +1 -1
  173. package/dist/index.d.ts +3 -0
  174. package/dist/index.esm.js +3719 -3330
  175. package/dist/index.esm.js.map +1 -1
  176. package/dist/index.iife.js +3721 -3329
  177. package/dist/index.iife.js.map +1 -1
  178. package/dist/keyboard-filter.d.ts +25 -0
  179. package/dist/on.d.ts +1 -1
  180. package/dist/parts.d.ts +0 -17
  181. package/dist/react/SniceProvider.js +2 -2
  182. package/dist/react/SniceRouter.js +2 -2
  183. package/dist/react/index.js +2 -2
  184. package/dist/react/matchRoute.js +2 -2
  185. package/dist/react/useRequestHandler.js +2 -2
  186. package/dist/request-response.d.ts +9 -1
  187. package/dist/symbols.cjs +11 -2
  188. package/dist/symbols.cjs.map +1 -1
  189. package/dist/symbols.d.ts +3 -0
  190. package/dist/symbols.esm.js +9 -3
  191. package/dist/symbols.esm.js.map +1 -1
  192. package/dist/testing.d.ts +1 -1
  193. package/dist/transitions.cjs +2 -2
  194. package/dist/transitions.esm.js +2 -2
  195. package/dist/types/app-context.d.ts +9 -58
  196. package/dist/types/daemon.d.ts +8 -0
  197. package/dist/types/dispatch-options.d.ts +2 -0
  198. package/dist/types/index.d.ts +1 -0
  199. package/dist/types/on-options.d.ts +5 -0
  200. package/dist/types/request-options.d.ts +2 -0
  201. package/dist/types/respond-options.d.ts +2 -0
  202. package/docs/ai/DEVELOPMENT.md +15 -1
  203. package/docs/ai/README.md +5 -1
  204. package/docs/ai/api.md +13 -4
  205. package/docs/ai/architecture.md +50 -1
  206. package/docs/ai/cli.md +9 -0
  207. package/docs/ai/daemons.md +80 -0
  208. package/docs/ai/decorators.md +6 -4
  209. package/docs/ai/events.md +6 -0
  210. package/docs/ai/patterns.md +45 -1
  211. package/docs/ai/request-response.md +26 -9
  212. package/docs/ai/routing.md +16 -1
  213. package/docs/cli.md +7 -0
  214. package/docs/daemons.md +200 -0
  215. package/docs/events.md +19 -2
  216. package/docs/request-response.md +20 -13
  217. package/docs/routing.md +25 -1
  218. package/package.json +6 -4
package/docs/ai/api.md CHANGED
@@ -15,6 +15,7 @@
15
15
  // attachController(el, MyController) | el.controller = MyController
16
16
  // controller="name" string attr (only channel in raw HTML; still supported everywhere)
17
17
  // Class binding: reference-deduped, removes controller attr, attr writes ignored while bound
18
+ @daemon // Explicitly constructed app-context state/lifecycle object; no name or global registration
18
19
  @layout('name') // Define page wrapper
19
20
  ```
20
21
 
@@ -112,13 +113,13 @@ html`
112
113
  ```typescript
113
114
  @on(event: string | string[], selector?: string, options?: OnOptions)
114
115
  // Works in elements + controllers
115
- // Options: { debounce?, throttle?, preventDefault?, stopPropagation?, once?, capture?, passive?, target?, scope? }
116
+ // Options: { debounce?, throttle?, preventDefault?, stopPropagation?, once?, capture?, passive?, target?, scope?, daemon? }
116
117
  // target: CSS selector for shadow DOM event delegation
117
118
  // scope: 'global' | selector | EventTarget | () => EventTarget | null — redirects listener attachment
118
119
  // Keyboard: 'keydown:Enter', 'keydown.escape', 'keydown:ctrl+s', 'keydown:~Space'
119
120
  // Supports both ':' and '.' notation
120
121
 
121
- @dispatch(eventName: string, options?: { debounce?, throttle?, dispatchOnUndefined?, scope?, ...EventInit })
122
+ @dispatch(eventName: string, options?: { debounce?, throttle?, dispatchOnUndefined?, scope?, daemon?, ...EventInit })
122
123
  // Fires CustomEvent after method, detail = return value
123
124
  // Supports async methods (dispatches after promise resolves)
124
125
  // dispatchOnUndefined: false (default) — skips dispatch if method returns undefined
@@ -142,7 +143,7 @@ html`
142
143
  ## Communication
143
144
 
144
145
  ```typescript
145
- @request(channel: string, options?: { timeout?, discoveryTimeout?, debounce?, throttle?, bubbles?, cancelable? })
146
+ @request(channel: string, options?: { daemon?, timeout?, discoveryTimeout?, debounce?, throttle?, bubbles?, cancelable? })
146
147
  // Request pattern using async generator syntax
147
148
  // Method must be async generator that yields payload and receives response
148
149
  // Returns Promise<T>
@@ -166,7 +167,7 @@ html`
166
167
  // }
167
168
  // // Usage: const user = await this.fetchUser('123');
168
169
 
169
- @respond(channel: string, options?: { debounce?, throttle? })
170
+ @respond(channel: string, options?: { daemon?, debounce?, throttle? })
170
171
  // Respond to requests from @request decorators
171
172
  // Method receives payload and returns response
172
173
  // Works in both elements and controllers
@@ -182,6 +183,14 @@ html`
182
183
  // }
183
184
  ```
184
185
 
186
+ Daemon addressing:
187
+ ```typescript
188
+ const release = provideContext(appRoot, { daemons: { session: new SessionDaemon() } });
189
+ const context = getContext(elementOrController);
190
+ // Consumers use { daemon: 'session' }; daemon methods default to their own private target.
191
+ // Provide before connect/attach. Call release() during app/test teardown.
192
+ ```
193
+
185
194
  ## Observers
186
195
 
187
196
  ```typescript
@@ -7,6 +7,7 @@
7
7
  - **Pages:** Orchestrate elements, handle URLs
8
8
  - **Elements:** Generic visual building blocks — no fetch(), no API calls, no business logic
9
9
  - **Controllers:** Specific behavior (data, APIs, business rules) — swappable per element
10
+ - **Daemons:** Explicitly constructed app-owned state/lifecycle objects — addressable through context, never global singletons
10
11
 
11
12
  **Generic vs Specific:** Elements say *what* they need, controllers decide *how*.
12
13
  Swap controllers to change behavior without touching the component.
@@ -16,6 +17,26 @@ Mock controller for tests, real API controller in production — same element.
16
17
  - Down: Properties
17
18
  - Up: Events
18
19
  - Sideways: Request/Response channels
20
+ - App services: daemon-addressed request/response or dispatch/on
21
+
22
+ ## Project Structure
23
+
24
+ Conventional application layout:
25
+
26
+ ```text
27
+ src/
28
+ main.ts
29
+ router.ts
30
+ pages/ # @page classes; route orchestration and presentation
31
+ components/ # reusable @element visual components
32
+ controllers/ # @controller data, API, and reusable business behavior
33
+ daemons/ # explicitly constructed @daemon state/lifecycle services
34
+ ```
35
+
36
+ Pages may coordinate a route and perform a small one-off load. Move behavior to
37
+ a controller when a page owns several storage/server/timer operations or when
38
+ substantial non-visual logic is copied across pages. `snice check` reports these
39
+ as architecture suggestions, not errors.
19
40
 
20
41
  ## Rendering System
21
42
 
@@ -74,6 +95,15 @@ Mock controller for tests, real API controller in production — same element.
74
95
  - Class bindings own the element: attribute writes ignored while a class is bound
75
96
  - Native elements: class bindings attach via ControllerPart; string attrs via MutationObserver
76
97
 
98
+ **Daemon lifecycle:**
99
+ 1. Application constructs an `@daemon` class with `new`
100
+ 2. `provideContext(root, { daemons })` activates its private communication target
101
+ 3. Elements/controllers resolve the nearest context by string address
102
+ 4. Provider release removes daemon handlers and deactivates that target
103
+ - Router provides its `context` through the same mechanism
104
+ - Multiple instances of the same class remain independent
105
+ - No implicit construction, registry scan, singleton, start, or stop hook
106
+
77
107
  ## Router System
78
108
 
79
109
  **Registration:**
@@ -136,7 +166,7 @@ onChange() { return { value: this.value }; }
136
166
  ```typescript
137
167
  // Element requests (async generator: yield sends, await receives)
138
168
  @request('fetch-data')
139
- async *fetchData(): any {
169
+ async *fetchData(): Response<Data> {
140
170
  return await (yield { id: this.dataId }); // single yield per call
141
171
  }
142
172
 
@@ -148,6 +178,25 @@ async handleFetch(payload: { id: string }) {
148
178
  // Wiring: html`<my-element controller=${MyController}></my-element>` (or controller="my-controller" in raw HTML)
149
179
  ```
150
180
 
181
+ **Element/Controller ↔ Daemon:** Addressed communication
182
+ ```typescript
183
+ @daemon
184
+ class SessionDaemon {
185
+ @respond('session/get') get() { return this.session; }
186
+ @dispatch('session/changed') changed() { return this.session; }
187
+ }
188
+
189
+ const session = new SessionDaemon();
190
+ const release = provideContext(appRoot, { daemons: { session } });
191
+
192
+ // Consumer imports no daemon implementation class.
193
+ @request('session/get', { daemon: 'session' })
194
+ async *getSession(): Response<Session | null> { return yield {}; }
195
+
196
+ @on('session/changed', { daemon: 'session' })
197
+ sessionChanged(event: CustomEvent<Session | null>) {}
198
+ ```
199
+
151
200
  **Global State:** Context
152
201
  ```typescript
153
202
  @context() handleContext(ctx: Context) {
package/docs/ai/cli.md CHANGED
@@ -44,6 +44,15 @@ npx snice validate # source analyzer only
44
44
  `validate` runs the analyzer over source. Catches mistakes that compile but never work:
45
45
  - an `@element`-decorated class that does not extend `HTMLElement` (or a Snice element subclass) — Snice registers/renders only element subclasses
46
46
  - deep imports that were never released package paths, e.g. `snice/decorators`
47
+ - Router missing `target`, `type`, or a project-wide `initialize()` call
48
+ - a routed class combining `@page` with redundant `@element`
49
+
50
+ Non-blocking architecture suggestions:
51
+ - `@page` -> `src/pages/`
52
+ - `@element` -> `src/components/`
53
+ - `@controller` -> `src/controllers/`
54
+ - `@daemon` -> `src/daemons/`
55
+ - several external-effect methods or substantial logic repeated across pages -> extract a controller
47
56
 
48
57
  Both `doctor` and `validate` accept `--json`.
49
58
 
@@ -0,0 +1,80 @@
1
+ # Daemons
2
+
3
+ Human reference: `docs/daemons.md`.
4
+
5
+ Ordinary explicitly constructed stateful objects with app-owned lifecycles.
6
+ `@daemon` adds per-instance Snice communication; it never constructs, caches,
7
+ globally registers, starts, or stops an instance.
8
+
9
+ ```typescript
10
+ @daemon
11
+ class SessionDaemon {
12
+ session: Session | null = null;
13
+
14
+ @respond('get-session')
15
+ getSession() { return this.session; }
16
+
17
+ @on('set-session')
18
+ setSession(e: CustomEvent<Session>) {
19
+ this.session = e.detail;
20
+ this.changed();
21
+ }
22
+
23
+ @dispatch('session-changed')
24
+ changed() { return this.session; }
25
+ }
26
+
27
+ const session = new SessionDaemon();
28
+ const release = provideContext(appRoot, { daemons: { session } });
29
+ ```
30
+
31
+ Context contract:
32
+
33
+ ```typescript
34
+ type DaemonMap = Readonly<Record<string, object>>;
35
+
36
+ interface AppContext {
37
+ readonly daemons?: DaemonMap;
38
+ [key: string]: unknown;
39
+ }
40
+ ```
41
+
42
+ Extend `AppContext` to type application-specific fields; Snice assigns meaning
43
+ only to `daemons`.
44
+
45
+ Router automatically provides its `context` beneath `target`. Non-router apps
46
+ call `provideContext(root, context)`. `getContext(elementOrController)` returns
47
+ the raw visible app context.
48
+
49
+ Consumer; no daemon implementation import:
50
+
51
+ ```typescript
52
+ @request<Session | null>('get-session', { daemon: 'session' })
53
+ async *load(): Response<Session | null> { return yield {}; }
54
+
55
+ @dispatch('set-session', { daemon: 'session' })
56
+ set(session: Session) { return session; }
57
+
58
+ @on('session-changed', { daemon: 'session' })
59
+ changed(e: CustomEvent<Session | null>) {}
60
+
61
+ @respond('confirm-logout', { daemon: 'session' })
62
+ confirm() { return true; }
63
+ ```
64
+
65
+ Daemon methods without `{ daemon }` use that instance's private target.
66
+ Controllers resolve through their attached host.
67
+
68
+ Resolution: participant -> nearest provided app context ->
69
+ `context.daemons[name]` -> instance target. No globals, implicit construction,
70
+ fallback, scanning, or late binding.
71
+
72
+ Rules:
73
+ - Provide before element connect/controller attach (`@on`/`@respond` bind then).
74
+ - `@request`/`@dispatch` resolve at invocation.
75
+ - `daemon` and `scope` are mutually exclusive.
76
+ - No selector delegation on daemon targets.
77
+ - Release function is idempotent and makes daemon traffic inert.
78
+ - Multiple instances of the same class and same address under different roots
79
+ are independent.
80
+ - Missing/undecorated/inactive entries fail explicitly.
@@ -4,6 +4,7 @@
4
4
  - `@element('tag-name', options?)` - Custom element. Options: `{ formAssociated?, renderRoot?: 'shadow'|'light', shadow?: 'open'|'closed'|false, delegatesFocus? }`
5
5
  - `@page({ tag, routes, guards?, placard? })` - Routable page
6
6
  - `@controller('name')` - Swappable behavior. Required on every controller class. Attach by class (preferred): `controller=${MyController}` in templates, `attachController(el, MyController)`, or `el.controller = MyController`; by string in raw HTML: `controller="name"`
7
+ - `@daemon` - Marks an explicitly constructed app-context daemon. No name argument; context key is address. Never constructs/registers globally.
7
8
  - `@layout('tag-name')` - Page wrapper for routing system
8
9
 
9
10
  ## Rendering
@@ -30,8 +31,8 @@
30
31
  - `@queryAll('selector', { light? })` - NodeList
31
32
 
32
33
  ## Events
33
- - `@on('event', 'selector?', options?)` - Delegation, auto-bound. Options: `{ capture?, once?, passive?, preventDefault?, stopPropagation?, debounce?, throttle?, scope? }`
34
- - `@dispatch('event-name', options?)` - Emit CustomEvent, detail = return value. Supports async. Options: `{ debounce?, throttle?, dispatchOnUndefined?, scope?, ...EventInit }`
34
+ - `@on('event', 'selector?', options?)` - Delegation, auto-bound. Options: `{ capture?, once?, passive?, preventDefault?, stopPropagation?, debounce?, throttle?, scope?, daemon? }`
35
+ - `@dispatch('event-name', options?)` - Emit CustomEvent, detail = return value. Supports async. Options: `{ debounce?, throttle?, dispatchOnUndefined?, scope?, daemon?, ...EventInit }`
35
36
 
36
37
  ### scope (on both `@on` and `@dispatch`)
37
38
 
@@ -63,9 +64,10 @@ Unresolved scope → `console.warn`, listener not attached / event not dispatche
63
64
  Resolver re-runs on reconnect — listeners track DOM moves.
64
65
 
65
66
  ## Communication
66
- - `@request(channel, options?)` - Async generator request pattern
67
- - `@respond(channel, options?)` - Handle requests from `@request`
67
+ - `@request(channel, options?)` - Async generator request pattern; `{ daemon: 'name' }` targets an app-context daemon
68
+ - `@respond(channel, options?)` - Handle requests from `@request`; `{ daemon: 'name' }` installs on that daemon target
68
69
  - `@context(options?)` - Receive router navigation context updates
70
+ - `provideContext(root, context)` / `getContext(participant)` - Explicit raw app-context provisioning and lookup
69
71
 
70
72
  ## Observers
71
73
  - `@observe(target, selector?, options?)` - Watch intersection, resize, media query, mutation
package/docs/ai/events.md CHANGED
@@ -371,6 +371,12 @@ Prefer the narrowest scope that works; `'global'` means every instance hears eve
371
371
 
372
372
  Need a reply rather than a broadcast → [@request / @respond](./request-response.md).
373
373
 
374
+ For stateful objects explicitly supplied by an application context, use
375
+ `{ daemon: 'name' }`. Both halves must target the same daemon address; daemon
376
+ methods themselves default to their own private target. See
377
+ [daemons](./daemons.md). Daemon targets are not DOM scopes: `scope` and
378
+ `daemon` cannot be combined, and selector delegation is unavailable.
379
+
374
380
  ## Custom events
375
381
 
376
382
  Dispatching manually:
@@ -94,6 +94,50 @@ Enabled automatically when Snice loads — no setup needed.
94
94
  // <table controller="table-loader"></table>
95
95
  ```
96
96
 
97
+ ## Daemon
98
+
99
+ Ordinary state/lifecycle object owned by the application. Construction and
100
+ teardown stay explicit; consumers use a context address instead of importing
101
+ the implementation class.
102
+
103
+ ```typescript
104
+ @daemon
105
+ class SessionDaemon {
106
+ session: Session | null = null;
107
+
108
+ @respond('session/get')
109
+ getSession() { return this.session; }
110
+
111
+ @on('session/set')
112
+ setSession(event: CustomEvent<Session>) {
113
+ this.session = event.detail;
114
+ this.changed();
115
+ }
116
+
117
+ @dispatch('session/changed')
118
+ changed() { return this.session; }
119
+ }
120
+
121
+ const session = new SessionDaemon();
122
+ const release = provideContext(appRoot, { daemons: { session } });
123
+
124
+ @request('session/get', { daemon: 'session' })
125
+ async *getSession(): Response<Session | null> { return yield {}; }
126
+
127
+ @dispatch('session/set', { daemon: 'session' })
128
+ setSession(session: Session) { return session; }
129
+
130
+ @on('session/changed', { daemon: 'session' })
131
+ sessionChanged(event: CustomEvent<Session | null>) {}
132
+ ```
133
+
134
+ - Provide before element connection/controller attachment.
135
+ - Router `context.daemons` uses the same provider automatically.
136
+ - `@request`/`@respond` = one reply; `@dispatch`/`@on` = notifications.
137
+ - `daemon` cannot be combined with DOM `scope`; daemon listeners cannot delegate selectors.
138
+ - Call `release()` during app/test teardown.
139
+ - See `docs/ai/daemons.md`.
140
+
97
141
  ## Page + Router
98
142
 
99
143
  **Module Structure (avoids circular imports):**
@@ -185,7 +229,7 @@ class API {
185
229
  @element('user-profile')
186
230
  class UserProfile extends HTMLElement {
187
231
  @request('fetch-user')
188
- async *fetchUser(id: string): any {
232
+ async *fetchUser(id: string): Response<User> {
189
233
  const user = await (yield { id }); // yield = send request, await = get response
190
234
  return user;
191
235
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  Mirrors `docs/request-response.md`.
4
4
 
5
- Request/response communication between elements and controllers using async generators.
5
+ Request/response communication between elements, controllers, and provided daemons using async generators.
6
6
 
7
7
  ## Why
8
8
 
@@ -27,6 +27,7 @@ function request(requestName: string, options?: RequestOptions): MethodDecorator
27
27
  function respond(requestName: string, options?: RespondOptions): MethodDecorator
28
28
 
29
29
  interface RequestOptions extends EventInit {
30
+ daemon?: string; // named daemon from nearest provided app context
30
31
  timeout?: number; // response timeout ms, default 120000 (2 min)
31
32
  discoveryTimeout?: number; // handler discovery timeout ms, default 50
32
33
  debounce?: number;
@@ -36,14 +37,20 @@ interface RequestOptions extends EventInit {
36
37
  }
37
38
 
38
39
  interface RespondOptions {
40
+ daemon?: string; // install responder on named daemon target
39
41
  debounce?: number;
40
42
  throttle?: number;
41
43
  }
42
44
 
43
- // Recommended type helper for request generator return types (define in your project):
44
- type RequestResult<T> = AsyncGenerator<any, T, any> | Promise<T>;
45
+ // Public return type for methods decorated with @request:
46
+ type Response<T = any> = T | any;
45
47
  ```
46
48
 
49
+ TypeScript cannot model a method decorator changing an async generator into a
50
+ promise-returning method. This is a deliberate pragmatic annotation: it keeps
51
+ strict consumers usable and documents `T`; the decorated runtime method
52
+ returns a promise for `T`.
53
+
47
54
  ### Response debounce/throttle
48
55
 
49
56
  ```typescript
@@ -65,6 +72,16 @@ class ProcessingController implements IController {
65
72
  }
66
73
  ```
67
74
 
75
+ Daemon communication uses the same protocol without DOM bubbling:
76
+
77
+ ```typescript
78
+ @request('get-session', { daemon: 'session' })
79
+ async *getSession() { return yield {}; }
80
+ ```
81
+
82
+ The daemon's plain `@respond('get-session')` handles it. See
83
+ [daemons](./daemons.md).
84
+
68
85
  ## Element-Side Requests
69
86
 
70
87
  ```typescript
@@ -75,7 +92,7 @@ class ProductCard extends HTMLElement {
75
92
  @property() price = '';
76
93
 
77
94
  @request('fetch-product')
78
- async *loadProduct(): RequestResult<void> {
95
+ async *loadProduct(): Response<void> {
79
96
  const product = await (yield { id: this.productId });
80
97
  this.name = product.name;
81
98
  this.price = product.price;
@@ -121,17 +138,17 @@ Elements never call `fetch()` or manage data directly — they yield requests up
121
138
 
122
139
  ```typescript
123
140
  @request('heavy-computation', { discoveryTimeout: 50, timeout: 30000 })
124
- async *compute(): RequestResult<any> { return await (yield data); }
141
+ async *compute(): Response<any> { return await (yield data); }
125
142
  ```
126
143
 
127
144
  ### Debounce/Throttle
128
145
 
129
146
  ```typescript
130
147
  @request('search', { debounce: 300 })
131
- async *search(): RequestResult<any[]> { return await (yield { query: this.searchTerm }); }
148
+ async *search(): Response<any[]> { return await (yield { query: this.searchTerm }); }
132
149
 
133
150
  @request('track', { throttle: 1000 })
134
- async *trackEvent(): RequestResult<void> { await (yield { event: 'scroll', position: window.scrollY }); }
151
+ async *trackEvent(): Response<void> { await (yield { event: 'scroll', position: window.scrollY }); }
135
152
  ```
136
153
 
137
154
  ## Error Handling
@@ -140,7 +157,7 @@ async *trackEvent(): RequestResult<void> { await (yield { event: 'scroll', posit
140
157
 
141
158
  ```typescript
142
159
  @request('load-data', { timeout: 5000 })
143
- async *loadData(): RequestResult<void> {
160
+ async *loadData(): Response<void> {
144
161
  try {
145
162
  this.data = await (yield { id: this.dataId });
146
163
  this.error = '';
@@ -201,7 +218,7 @@ class LiveTicker extends HTMLElement {
201
218
  @property() symbol = 'BTC';
202
219
 
203
220
  @request('subscribe-ticker')
204
- async *subscribe(): RequestResult<void> { await (yield { symbol: this.symbol }); }
221
+ async *subscribe(): Response<void> { await (yield { symbol: this.symbol }); }
205
222
 
206
223
  @on('ticker-update')
207
224
  onUpdate(e: CustomEvent) { this.price = e.detail.price; }
@@ -25,7 +25,7 @@ interface RouterOptions {
25
25
  document?: Document; // override document object (testing)
26
26
  transition?: Transition; // global transition config
27
27
  layout?: string; // default layout tag for all pages
28
- context?: any; // shared context object across pages/layouts
28
+ context?: any; // app context; may expose daemons
29
29
  fetcher?: Fetcher; // optional fetch middleware — see docs/ai/fetcher.md
30
30
  }
31
31
  ```
@@ -45,6 +45,19 @@ class AppContext {
45
45
  const { page, initialize } = Router({ target: '#app', type: 'hash', context: new AppContext() });
46
46
  ```
47
47
 
48
+ Router also provides the raw app context beneath `target`, using the same
49
+ `provideContext()` mechanism available to non-router applications. Explicitly
50
+ constructed `@daemon` instances may be addressed through `context.daemons`:
51
+
52
+ ```typescript
53
+ const session = new SessionDaemon();
54
+ Router({
55
+ target: '#app',
56
+ type: 'hash',
57
+ context: { daemons: { session } }
58
+ });
59
+ ```
60
+
48
61
  ### Module structure (avoids circular imports)
49
62
 
50
63
  ```typescript
@@ -176,4 +189,6 @@ function Router(options: RouterOptions): {
176
189
 
177
190
  - `navigate(path: string): Promise<void>` — navigates to `path`; uses hash or pushstate depending on router `type`.
178
191
  - `initialize(): void` — starts listening for route changes; must be called after all pages are defined.
192
+ - Do not add `@element` to an `@page` class. The Router-returned page decorator
193
+ already registers the custom element and applies Snice element behavior.
179
194
  - `register(route: string, tag: string, transition?: Transition, guards?: Guard | Guard[], layout?: string | false, placard?: Placard | ((ctx: AppContext) => Placard)): void` — manually registers a route without the `@page` decorator. Note: the return-type shape above lists `register` with 4 params; this fuller 6-param signature (adding `layout`, `placard`) is documented separately in the source.
package/docs/cli.md CHANGED
@@ -50,6 +50,13 @@ npx snice validate # source analyzer only
50
50
 
51
51
  - an `@element`-decorated class that does not extend `HTMLElement` (or a Snice element subclass) — Snice registers and renders only element subclasses
52
52
  - deep imports that were never released package paths, such as `snice/decorators`
53
+ - a Router without `target`, `type`, or a project-wide `initialize()` call
54
+ - a routed class combining `@page` with redundant `@element`
55
+
56
+ It also gives non-blocking architecture suggestions: keep `@page`, `@element`,
57
+ `@controller`, and `@daemon` classes under `src/pages`, `src/components`,
58
+ `src/controllers`, and `src/daemons`; extract clearly service-heavy or repeated
59
+ page logic into a controller. Small route orchestration remains valid page code.
53
60
 
54
61
  Both accept `--json` for CI.
55
62