snice 7.0.3 → 7.1.1

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 (223) 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 +64 -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 +3825 -3337
  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 +3827 -3336
  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/controller.d.ts +5 -0
  170. package/dist/daemon-target.d.ts +10 -0
  171. package/dist/daemon.d.ts +10 -0
  172. package/dist/index.cjs +3821 -3330
  173. package/dist/index.cjs.map +1 -1
  174. package/dist/index.d.ts +3 -0
  175. package/dist/index.esm.js +3819 -3331
  176. package/dist/index.esm.js.map +1 -1
  177. package/dist/index.iife.js +3821 -3330
  178. package/dist/index.iife.js.map +1 -1
  179. package/dist/keyboard-filter.d.ts +25 -0
  180. package/dist/on.d.ts +1 -1
  181. package/dist/parts.d.ts +2 -19
  182. package/dist/react/SniceProvider.js +2 -2
  183. package/dist/react/SniceRouter.js +2 -2
  184. package/dist/react/index.js +2 -2
  185. package/dist/react/matchRoute.js +2 -2
  186. package/dist/react/useRequestHandler.js +2 -2
  187. package/dist/request-response.d.ts +9 -1
  188. package/dist/symbols.cjs +19 -2
  189. package/dist/symbols.cjs.map +1 -1
  190. package/dist/symbols.d.ts +5 -0
  191. package/dist/symbols.esm.js +15 -3
  192. package/dist/symbols.esm.js.map +1 -1
  193. package/dist/testing.d.ts +1 -1
  194. package/dist/transitions.cjs +2 -2
  195. package/dist/transitions.esm.js +2 -2
  196. package/dist/types/app-context.d.ts +9 -58
  197. package/dist/types/daemon.d.ts +8 -0
  198. package/dist/types/dispatch-options.d.ts +2 -0
  199. package/dist/types/index.d.ts +1 -0
  200. package/dist/types/on-options.d.ts +5 -0
  201. package/dist/types/request-options.d.ts +2 -0
  202. package/dist/types/respond-options.d.ts +2 -0
  203. package/docs/ai/DEVELOPMENT.md +15 -1
  204. package/docs/ai/README.md +5 -1
  205. package/docs/ai/api.md +13 -4
  206. package/docs/ai/architecture.md +51 -2
  207. package/docs/ai/bindings.md +1 -1
  208. package/docs/ai/cli.md +9 -0
  209. package/docs/ai/controllers.md +1 -1
  210. package/docs/ai/daemons.md +80 -0
  211. package/docs/ai/decorators.md +7 -5
  212. package/docs/ai/events.md +6 -0
  213. package/docs/ai/patterns.md +49 -4
  214. package/docs/ai/request-response.md +26 -9
  215. package/docs/ai/routing.md +16 -1
  216. package/docs/bindings.md +1 -1
  217. package/docs/cli.md +7 -0
  218. package/docs/controllers.md +8 -2
  219. package/docs/daemons.md +200 -0
  220. package/docs/events.md +19 -2
  221. package/docs/request-response.md +20 -13
  222. package/docs/routing.md +25 -1
  223. package/package.json +6 -4
package/docs/ai/README.md CHANGED
@@ -41,6 +41,7 @@ for depth; read the aggregates when you want the whole surface at once.
41
41
  - `bindings.md` - Exact node, attribute, property, event, class, style, spread, controller, sentinel, and form semantics
42
42
  - `events.md` - Template event syntax, `@on`, `@dispatch`, delegation, keyboard filters
43
43
  - `controllers.md` - Attaching by class or registry name, lifecycle, cleanup
44
+ - `daemons.md` - Explicit app-context daemon instances and communication
44
45
  - `routing.md` - Router setup, pages, params, navigation
45
46
  - `guards-and-layouts.md` - Route guards, layouts, page transitions
46
47
 
@@ -104,6 +105,9 @@ Runtime first, then one bundle per component (any order after runtime):
104
105
  - Elements receive data via properties, emit events for actions
105
106
  - **Pages orchestrate** - handle routing, call APIs, coordinate elements
106
107
  - **Controllers add behavior** - attach to elements for reusable non-visual logic; bind by class in templates (`controller=${MyController}`, preferred) or by name in raw HTML (`controller="name"`)
108
+ - **Daemons hold app-owned state/lifecycle** - construct explicitly, provide through `context.daemons`, communicate by address without importing implementations
109
+ - Conventional folders: `src/pages`, `src/components`, `src/controllers`, `src/daemons`; construct the Router in `src/router.ts` and initialize it from `src/main.ts`
110
+ - Several server/storage/timer operations in one page, or substantial logic copied across pages, belongs in a controller; `snice check` suggests the extraction conservatively
107
111
  - Put API calls in pages/controllers/services, not in elements
108
112
 
109
113
  **Properties:**
@@ -198,7 +202,7 @@ class MyCounter extends HTMLElement {
198
202
  - Factory: `const hasRole = (role) => (ctx, params) => ctx.user?.role === role`
199
203
 
200
204
  **Custom AppContext Types:**
201
- - Snice's `AppContext` interface has: `theme?`, `locale?`, `principal?`, `config?`
205
+ - Snice assigns meaning only to `AppContext.daemons?`; other keys are `unknown`
202
206
  - For custom fields (like `user`), extend snice's AppContext:
203
207
  ```typescript
204
208
  import type { AppContext as SniceAppContext } from 'snice';
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
 
@@ -71,9 +92,18 @@ Mock controller for tests, real API controller in production — same element.
71
92
  4. Attach new controller (class refs skip the registry; reference-deduped)
72
93
  5. setupEventHandlers for controller
73
94
  6. On detach: cleanupEventHandlers
74
- - Class bindings own the element: attribute writes ignored while a class is bound
95
+ - Class bindings own the element; their decorator name is reflected as a diagnostic-only `controller="name"` marker
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) {
@@ -59,7 +59,7 @@ Ordinary attributes/comments support multiple expressions. Property, boolean, ev
59
59
 
60
60
  ### Controller `controller=${value}`
61
61
 
62
- - Class decorated with `@controller` (decorator required): attaches directly, registry skipped. Reference-deduped — same class re-render is a no-op; different class or null detaches first. Removes the `controller` attribute; attribute writes ignored while a class is bound.
62
+ - Class decorated with `@controller` (decorator required): attaches directly, registry skipped. Reference-deduped — same class re-render is a no-op; different class or null detaches first. Reflects the decorator name as a read-only diagnostic `controller="name"` marker; the marker never creates a registry attachment.
63
63
  - Works on snice elements AND native elements in templates. Not-yet-upgraded custom elements park the class until connectedCallback.
64
64
  - String: delegates to `setAttribute` — identical to static `controller="name"`. Interpolated `controller="a-${x}"` is plain attribute interpolation, not this channel.
65
65
  - nothing/null/undefined/false/`''`: detach. `noChange`: keep current controller.
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
 
@@ -40,7 +40,7 @@ html`<div controller=${UserController}></div>` // native elements work too
40
40
  - `@controller('name')` decorator is still required: registers the class, marks it, flushes pending attachments.
41
41
  - Re-binding the same class reference is a no-op.
42
42
  - Binding a different class (or `null`) detaches the old controller first.
43
- - While a class is bound, the class binding owns the element `controller` attribute writes are ignored until unbound.
43
+ - While a class is bound, its decorator name is reflected as `controller="name"` for DOM inspection. This is a read-only diagnostic marker, not a registry attachment; the class reference owns the element until unbound.
44
44
 
45
45
  Imperative equivalents:
46
46
 
@@ -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.
@@ -3,7 +3,8 @@
3
3
  ## Class
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
- - `@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"`
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"`. A class attachment reflects this name in the DOM as a diagnostic marker without attaching through the registry.
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:
@@ -80,9 +80,10 @@ class DataLoader implements IController {
80
80
  ```
81
81
 
82
82
  **Class vs string:** class binding is reference-deduped (re-render no-op, swap
83
- detaches old), removes the `controller` attribute, and owns the element
84
- attribute writes are ignored while a class is bound. Strings stay fully
85
- supported and behave exactly like the static attribute.
83
+ detaches old) and owns the element. It reflects the decorator name as a
84
+ diagnostic `controller="name"` marker, but that marker does not attach through
85
+ the registry. Strings stay fully supported and behave exactly like the static
86
+ attribute.
86
87
 
87
88
  **Native element controllers** — attach controllers to any HTML element (div, form, table, etc.).
88
89
  Enabled automatically when Snice loads — no setup needed.
@@ -94,6 +95,50 @@ Enabled automatically when Snice loads — no setup needed.
94
95
  // <table controller="table-loader"></table>
95
96
  ```
96
97
 
98
+ ## Daemon
99
+
100
+ Ordinary state/lifecycle object owned by the application. Construction and
101
+ teardown stay explicit; consumers use a context address instead of importing
102
+ the implementation class.
103
+
104
+ ```typescript
105
+ @daemon
106
+ class SessionDaemon {
107
+ session: Session | null = null;
108
+
109
+ @respond('session/get')
110
+ getSession() { return this.session; }
111
+
112
+ @on('session/set')
113
+ setSession(event: CustomEvent<Session>) {
114
+ this.session = event.detail;
115
+ this.changed();
116
+ }
117
+
118
+ @dispatch('session/changed')
119
+ changed() { return this.session; }
120
+ }
121
+
122
+ const session = new SessionDaemon();
123
+ const release = provideContext(appRoot, { daemons: { session } });
124
+
125
+ @request('session/get', { daemon: 'session' })
126
+ async *getSession(): Response<Session | null> { return yield {}; }
127
+
128
+ @dispatch('session/set', { daemon: 'session' })
129
+ setSession(session: Session) { return session; }
130
+
131
+ @on('session/changed', { daemon: 'session' })
132
+ sessionChanged(event: CustomEvent<Session | null>) {}
133
+ ```
134
+
135
+ - Provide before element connection/controller attachment.
136
+ - Router `context.daemons` uses the same provider automatically.
137
+ - `@request`/`@respond` = one reply; `@dispatch`/`@on` = notifications.
138
+ - `daemon` cannot be combined with DOM `scope`; daemon listeners cannot delegate selectors.
139
+ - Call `release()` during app/test teardown.
140
+ - See `docs/ai/daemons.md`.
141
+
97
142
  ## Page + Router
98
143
 
99
144
  **Module Structure (avoids circular imports):**
@@ -185,7 +230,7 @@ class API {
185
230
  @element('user-profile')
186
231
  class UserProfile extends HTMLElement {
187
232
  @request('fetch-user')
188
- async *fetchUser(id: string): any {
233
+ async *fetchUser(id: string): Response<User> {
189
234
  const user = await (yield { id }); // yield = send request, await = get response
190
235
  return user;
191
236
  }
@@ -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/bindings.md CHANGED
@@ -163,7 +163,7 @@ html`<user-list controller=${DataLoader}></user-list>`;
163
163
  html`<div controller=${DataLoader}></div>`; // native elements too
164
164
  ```
165
165
 
166
- Class values skip the registry and are deduped by reference: re-rendering with the same class is a no-op; a different class (or `null`) detaches the previous controller first. The `@controller('name')` decorator is still required on the class. While a class is bound it owns the element `controller` attribute writes are ignored and no attribute is reflected. Custom elements that have not upgraded yet hold the class until their `connectedCallback` runs.
166
+ Class values skip the registry and are deduped by reference: re-rendering with the same class is a no-op; a different class (or `null`) detaches the previous controller first. The `@controller('name')` decorator is still required on the class. While a class is bound it owns the element. Snice reflects its decorator name as a diagnostic `controller="name"` attribute for DevTools, but that marker never resolves the registry or creates another attachment; treat it as read-only. Custom elements that have not upgraded yet hold the class until their `connectedCallback` runs.
167
167
 
168
168
  String values delegate to the attribute channel and behave exactly like a static `controller="name"` attribute. Interpolated forms (`controller="user-${kind}"`) are ordinary attribute interpolation, not this channel.
169
169
 
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
 
@@ -54,7 +54,14 @@ The `@controller('name')` decorator is still required — it registers the class
54
54
  marks it, and flushes pending attachments. Re-binding the same class reference
55
55
  is a no-op; binding a different class (or `null`) detaches the old controller
56
56
  first. While a class is bound, the class binding owns the element: `controller`
57
- attribute writes are ignored until the class is unbound.
57
+ attribute writes cannot switch it until the class is unbound.
58
+
59
+ For inspection in DevTools, Snice reflects the decorator name as a
60
+ `controller="name"` attribute while the class is attached. This marker is
61
+ diagnostic only: it does not resolve the registry or create a second
62
+ attachment. Treat it as read-only; the class reference remains authoritative.
63
+ Snice removes the marker when the class detaches or replaces it when another
64
+ controller is bound.
58
65
 
59
66
  Imperative equivalents:
60
67
 
@@ -467,4 +474,3 @@ element.addEventListener('controller-attached', (e: CustomEvent) => {
467
474
  ### Auto-Cleanup
468
475
 
469
476
  The framework automatically cleans up `@on` handlers, observers, and `@respond` handlers during detach. Manual cleanup in `detach()` is only needed for resources you manage yourself (WebSockets, intervals, manual event listeners).
470
-