@adukiorg/anza 0.2.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 (349) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +215 -0
  3. package/bin/anza.js +63 -0
  4. package/bin/create.js +150 -0
  5. package/importmap.json +72 -0
  6. package/package.json +100 -0
  7. package/src/core/animations/index.js +55 -0
  8. package/src/core/animations/play.js +111 -0
  9. package/src/core/animations/registry.js +54 -0
  10. package/src/core/animations/scroll.js +50 -0
  11. package/src/core/animations/tokens.js +58 -0
  12. package/src/core/animations/usage.md +301 -0
  13. package/src/core/animations/waapi.js +86 -0
  14. package/src/core/api/cache.js +120 -0
  15. package/src/core/api/caches/glob.js +24 -0
  16. package/src/core/api/caches/index.js +118 -0
  17. package/src/core/api/events/index.js +75 -0
  18. package/src/core/api/fetch.js +99 -0
  19. package/src/core/api/index.js +158 -0
  20. package/src/core/api/pipeline.js +98 -0
  21. package/src/core/api/plan.md +209 -0
  22. package/src/core/api/prefixes/index.js +66 -0
  23. package/src/core/api/retry.js +69 -0
  24. package/src/core/api/stream.js +127 -0
  25. package/src/core/api/upload.js +180 -0
  26. package/src/core/api/usage.md +206 -0
  27. package/src/core/events/bus.js +38 -0
  28. package/src/core/events/delegate.js +79 -0
  29. package/src/core/events/index.js +26 -0
  30. package/src/core/events/listen.js +50 -0
  31. package/src/core/events/missing.md +103 -0
  32. package/src/core/events/once.js +49 -0
  33. package/src/core/events/plan.md +177 -0
  34. package/src/core/events/types/index.js +34 -0
  35. package/src/core/events/usage.md +107 -0
  36. package/src/core/offline/bridge.js +51 -0
  37. package/src/core/offline/clock.js +100 -0
  38. package/src/core/offline/connectivity.js +116 -0
  39. package/src/core/offline/index.js +41 -0
  40. package/src/core/offline/missing.md +89 -0
  41. package/src/core/offline/plan.md +143 -0
  42. package/src/core/offline/queue.js +168 -0
  43. package/src/core/offline/state.js +18 -0
  44. package/src/core/offline/sync.js +106 -0
  45. package/src/core/offline/usage.md +273 -0
  46. package/src/core/platform/guard.js +104 -0
  47. package/src/core/platform/index.js +42 -0
  48. package/src/core/platform/missing.md +119 -0
  49. package/src/core/platform/platform.d.ts +88 -0
  50. package/src/core/platform/polyfills/anchor.js +79 -0
  51. package/src/core/platform/polyfills/navigation.js +142 -0
  52. package/src/core/platform/polyfills/popover.js +142 -0
  53. package/src/core/platform/polyfills/scheduler.js +194 -0
  54. package/src/core/platform/polyfills/shadow.js +35 -0
  55. package/src/core/platform/polyfills/urlpattern.js +119 -0
  56. package/src/core/platform/supports.js +186 -0
  57. package/src/core/platform/usage.md +287 -0
  58. package/src/core/router/cache.js +95 -0
  59. package/src/core/router/container.js +146 -0
  60. package/src/core/router/handler.js +52 -0
  61. package/src/core/router/history.js +120 -0
  62. package/src/core/router/index.js +158 -0
  63. package/src/core/router/intercept.js +376 -0
  64. package/src/core/router/match.js +145 -0
  65. package/src/core/router/missing.md +716 -0
  66. package/src/core/router/outlet.js +139 -0
  67. package/src/core/router/plan.md +370 -0
  68. package/src/core/router/sync/index.js +16 -0
  69. package/src/core/router/sync/tab.js +115 -0
  70. package/src/core/router/sync/transport.js +139 -0
  71. package/src/core/router/transitions.js +59 -0
  72. package/src/core/router/usage.md +773 -0
  73. package/src/core/security/crypto.js +159 -0
  74. package/src/core/security/index.js +49 -0
  75. package/src/core/security/missing.md +97 -0
  76. package/src/core/security/permissions.js +64 -0
  77. package/src/core/security/sanitize.js +100 -0
  78. package/src/core/security/usage.md +283 -0
  79. package/src/core/state/derived.js +117 -0
  80. package/src/core/state/index.js +23 -0
  81. package/src/core/state/missing.md +165 -0
  82. package/src/core/state/persist.js +284 -0
  83. package/src/core/state/store.js +308 -0
  84. package/src/core/state/sync.js +46 -0
  85. package/src/core/state/usage.md +440 -0
  86. package/src/core/storage/cache.js +83 -0
  87. package/src/core/storage/idb.js +196 -0
  88. package/src/core/storage/index.js +373 -0
  89. package/src/core/storage/lru.js +107 -0
  90. package/src/core/storage/missing.md +165 -0
  91. package/src/core/storage/opfs.js +190 -0
  92. package/src/core/storage/plan.md +69 -0
  93. package/src/core/storage/quota.js +69 -0
  94. package/src/core/storage/usage.md +226 -0
  95. package/src/core/ui/base.js +50 -0
  96. package/src/core/ui/define/container.js +82 -0
  97. package/src/core/ui/define/define.js +12 -0
  98. package/src/core/ui/define/element.js +390 -0
  99. package/src/core/ui/define/index.js +9 -0
  100. package/src/core/ui/define/orchestrator.js +105 -0
  101. package/src/core/ui/define/proxy.js +644 -0
  102. package/src/core/ui/define/state.js +6 -0
  103. package/src/core/ui/define/utils.js +134 -0
  104. package/src/core/ui/implementation.md +170 -0
  105. package/src/core/ui/index.js +41 -0
  106. package/src/core/ui/observe.js +117 -0
  107. package/src/core/ui/plan.md +510 -0
  108. package/src/core/ui/schedule.js +60 -0
  109. package/src/core/ui/template.js +37 -0
  110. package/src/core/ui/transitions.js +37 -0
  111. package/src/core/ui/ui.types.md +890 -0
  112. package/src/core/ui/usage.md +1124 -0
  113. package/src/core/ui/watch.md +346 -0
  114. package/src/core/workers/broadcast.js +138 -0
  115. package/src/core/workers/dedicated.js +153 -0
  116. package/src/core/workers/index.js +169 -0
  117. package/src/core/workers/locks.js +160 -0
  118. package/src/core/workers/offscreen.js +166 -0
  119. package/src/core/workers/plan.md +381 -0
  120. package/src/core/workers/pool.js +267 -0
  121. package/src/core/workers/shared.js +137 -0
  122. package/src/core/workers/usage.md +622 -0
  123. package/src/elements/base.js +12 -0
  124. package/src/elements/data/card/index.html +9 -0
  125. package/src/elements/data/card/index.js +19 -0
  126. package/src/elements/data/card/index.tags.json +1 -0
  127. package/src/elements/data/card/style.css +46 -0
  128. package/src/elements/data/chart/index.html +1 -0
  129. package/src/elements/data/chart/index.js +143 -0
  130. package/src/elements/data/chart/index.tags.json +1 -0
  131. package/src/elements/data/chart/style.css +13 -0
  132. package/src/elements/data/list/index.html +3 -0
  133. package/src/elements/data/list/index.js +19 -0
  134. package/src/elements/data/list/index.tags.json +1 -0
  135. package/src/elements/data/list/style.css +39 -0
  136. package/src/elements/data/stat/index.html +9 -0
  137. package/src/elements/data/stat/index.js +19 -0
  138. package/src/elements/data/stat/index.tags.json +1 -0
  139. package/src/elements/data/stat/style.css +50 -0
  140. package/src/elements/data/table/index.html +1 -0
  141. package/src/elements/data/table/index.js +16 -0
  142. package/src/elements/data/table/index.tags.json +1 -0
  143. package/src/elements/data/table/style.css +50 -0
  144. package/src/elements/feedback/alert/index.html +11 -0
  145. package/src/elements/feedback/alert/index.js +28 -0
  146. package/src/elements/feedback/alert/index.tags.json +1 -0
  147. package/src/elements/feedback/alert/style.css +75 -0
  148. package/src/elements/feedback/empty/index.html +13 -0
  149. package/src/elements/feedback/empty/index.js +34 -0
  150. package/src/elements/feedback/empty/index.tags.json +1 -0
  151. package/src/elements/feedback/empty/style.css +45 -0
  152. package/src/elements/feedback/progress/index.html +7 -0
  153. package/src/elements/feedback/progress/index.js +46 -0
  154. package/src/elements/feedback/progress/index.tags.json +1 -0
  155. package/src/elements/feedback/progress/style.css +36 -0
  156. package/src/elements/feedback/skeleton/index.html +1 -0
  157. package/src/elements/feedback/skeleton/index.js +78 -0
  158. package/src/elements/feedback/skeleton/index.tags.json +1 -0
  159. package/src/elements/feedback/skeleton/style.css +28 -0
  160. package/src/elements/feedback/toast/index.html +3 -0
  161. package/src/elements/feedback/toast/index.js +65 -0
  162. package/src/elements/feedback/toast/index.tags.json +1 -0
  163. package/src/elements/feedback/toast/style.css +36 -0
  164. package/src/elements/forms/checkbox/index.html +7 -0
  165. package/src/elements/forms/checkbox/index.js +104 -0
  166. package/src/elements/forms/checkbox/index.tags.json +1 -0
  167. package/src/elements/forms/checkbox/style.css +86 -0
  168. package/src/elements/forms/field/index.html +13 -0
  169. package/src/elements/forms/field/index.js +42 -0
  170. package/src/elements/forms/field/index.tags.json +1 -0
  171. package/src/elements/forms/field/style.css +42 -0
  172. package/src/elements/forms/form/index.html +3 -0
  173. package/src/elements/forms/form/index.js +122 -0
  174. package/src/elements/forms/form/index.tags.json +1 -0
  175. package/src/elements/forms/form/style.css +11 -0
  176. package/src/elements/forms/input/index.html +4 -0
  177. package/src/elements/forms/input/index.js +103 -0
  178. package/src/elements/forms/input/index.tags.json +1 -0
  179. package/src/elements/forms/input/style.css +39 -0
  180. package/src/elements/forms/radio/index.html +4 -0
  181. package/src/elements/forms/radio/index.js +109 -0
  182. package/src/elements/forms/radio/index.tags.json +1 -0
  183. package/src/elements/forms/radio/style.css +65 -0
  184. package/src/elements/forms/select/index.html +9 -0
  185. package/src/elements/forms/select/index.js +114 -0
  186. package/src/elements/forms/select/index.tags.json +1 -0
  187. package/src/elements/forms/select/style.css +95 -0
  188. package/src/elements/forms/textarea/index.html +4 -0
  189. package/src/elements/forms/textarea/index.js +115 -0
  190. package/src/elements/forms/textarea/index.tags.json +1 -0
  191. package/src/elements/forms/textarea/style.css +46 -0
  192. package/src/elements/forms/toggle/index.html +4 -0
  193. package/src/elements/forms/toggle/index.js +89 -0
  194. package/src/elements/forms/toggle/index.tags.json +1 -0
  195. package/src/elements/forms/toggle/style.css +63 -0
  196. package/src/elements/forms/upload/index.html +13 -0
  197. package/src/elements/forms/upload/index.js +120 -0
  198. package/src/elements/forms/upload/index.tags.json +1 -0
  199. package/src/elements/forms/upload/style.css +61 -0
  200. package/src/elements/index.js +71 -0
  201. package/src/elements/layout/app/index.html +7 -0
  202. package/src/elements/layout/app/index.js +16 -0
  203. package/src/elements/layout/app/index.tags.json +1 -0
  204. package/src/elements/layout/app/style.css +41 -0
  205. package/src/elements/layout/grid/index.html +3 -0
  206. package/src/elements/layout/grid/index.js +41 -0
  207. package/src/elements/layout/grid/index.tags.json +1 -0
  208. package/src/elements/layout/grid/style.css +12 -0
  209. package/src/elements/layout/header/index.html +8 -0
  210. package/src/elements/layout/header/index.js +16 -0
  211. package/src/elements/layout/header/index.tags.json +1 -0
  212. package/src/elements/layout/header/style.css +28 -0
  213. package/src/elements/layout/scroll/index.html +3 -0
  214. package/src/elements/layout/scroll/index.js +19 -0
  215. package/src/elements/layout/scroll/index.tags.json +1 -0
  216. package/src/elements/layout/scroll/style.css +24 -0
  217. package/src/elements/layout/sidebar/index.html +3 -0
  218. package/src/elements/layout/sidebar/index.js +24 -0
  219. package/src/elements/layout/sidebar/index.tags.json +1 -0
  220. package/src/elements/layout/sidebar/style.css +30 -0
  221. package/src/elements/layout/split/index.html +3 -0
  222. package/src/elements/layout/split/index.js +18 -0
  223. package/src/elements/layout/split/index.tags.json +1 -0
  224. package/src/elements/layout/split/style.css +28 -0
  225. package/src/elements/layout/stack/index.html +3 -0
  226. package/src/elements/layout/stack/index.js +31 -0
  227. package/src/elements/layout/stack/index.tags.json +1 -0
  228. package/src/elements/layout/stack/style.css +15 -0
  229. package/src/elements/layout/surface/index.html +3 -0
  230. package/src/elements/layout/surface/index.js +19 -0
  231. package/src/elements/layout/surface/index.tags.json +1 -0
  232. package/src/elements/layout/surface/style.css +29 -0
  233. package/src/elements/navigation/breadcrumb/index.html +5 -0
  234. package/src/elements/navigation/breadcrumb/index.js +16 -0
  235. package/src/elements/navigation/breadcrumb/index.tags.json +1 -0
  236. package/src/elements/navigation/breadcrumb/style.css +36 -0
  237. package/src/elements/navigation/nav/index.html +3 -0
  238. package/src/elements/navigation/nav/index.js +24 -0
  239. package/src/elements/navigation/nav/index.tags.json +1 -0
  240. package/src/elements/navigation/nav/style.css +38 -0
  241. package/src/elements/navigation/pagination/index.html +3 -0
  242. package/src/elements/navigation/pagination/index.js +94 -0
  243. package/src/elements/navigation/pagination/index.tags.json +1 -0
  244. package/src/elements/navigation/pagination/style.css +39 -0
  245. package/src/elements/navigation/steps/index.html +6 -0
  246. package/src/elements/navigation/steps/index.js +64 -0
  247. package/src/elements/navigation/steps/index.tags.json +1 -0
  248. package/src/elements/navigation/steps/style.css +78 -0
  249. package/src/elements/navigation/tabs/index.html +6 -0
  250. package/src/elements/navigation/tabs/index.js +132 -0
  251. package/src/elements/navigation/tabs/index.tags.json +1 -0
  252. package/src/elements/navigation/tabs/style.css +52 -0
  253. package/src/elements/overlay/dialog/index.html +5 -0
  254. package/src/elements/overlay/dialog/index.js +57 -0
  255. package/src/elements/overlay/dialog/index.tags.json +1 -0
  256. package/src/elements/overlay/dialog/style.css +31 -0
  257. package/src/elements/overlay/drawer/index.html +3 -0
  258. package/src/elements/overlay/drawer/index.js +56 -0
  259. package/src/elements/overlay/drawer/index.tags.json +1 -0
  260. package/src/elements/overlay/drawer/style.css +48 -0
  261. package/src/elements/overlay/menu/index.html +3 -0
  262. package/src/elements/overlay/menu/index.js +107 -0
  263. package/src/elements/overlay/menu/index.tags.json +1 -0
  264. package/src/elements/overlay/menu/style.css +43 -0
  265. package/src/elements/overlay/popover/index.html +3 -0
  266. package/src/elements/overlay/popover/index.js +44 -0
  267. package/src/elements/overlay/popover/index.tags.json +1 -0
  268. package/src/elements/overlay/popover/style.css +21 -0
  269. package/src/elements/overlay/sheet/index.html +8 -0
  270. package/src/elements/overlay/sheet/index.js +105 -0
  271. package/src/elements/overlay/sheet/index.tags.json +1 -0
  272. package/src/elements/overlay/sheet/style.css +64 -0
  273. package/src/elements/overlay/tooltip/index.html +6 -0
  274. package/src/elements/overlay/tooltip/index.js +16 -0
  275. package/src/elements/overlay/tooltip/index.tags.json +1 -0
  276. package/src/elements/overlay/tooltip/style.css +41 -0
  277. package/src/elements/primitives/avatar/index.html +2 -0
  278. package/src/elements/primitives/avatar/index.js +79 -0
  279. package/src/elements/primitives/avatar/index.tags.json +1 -0
  280. package/src/elements/primitives/avatar/style.css +36 -0
  281. package/src/elements/primitives/badge/index.html +3 -0
  282. package/src/elements/primitives/badge/index.js +20 -0
  283. package/src/elements/primitives/badge/index.tags.json +1 -0
  284. package/src/elements/primitives/badge/style.css +67 -0
  285. package/src/elements/primitives/button/index.html +3 -0
  286. package/src/elements/primitives/button/index.js +61 -0
  287. package/src/elements/primitives/button/index.tags.json +1 -0
  288. package/src/elements/primitives/button/style.css +66 -0
  289. package/src/elements/primitives/divider/index.html +1 -0
  290. package/src/elements/primitives/divider/index.js +43 -0
  291. package/src/elements/primitives/divider/index.tags.json +1 -0
  292. package/src/elements/primitives/divider/style.css +39 -0
  293. package/src/elements/primitives/icon/index.html +3 -0
  294. package/src/elements/primitives/icon/index.js +66 -0
  295. package/src/elements/primitives/icon/index.tags.json +1 -0
  296. package/src/elements/primitives/icon/style.css +20 -0
  297. package/src/elements/primitives/link/index.html +3 -0
  298. package/src/elements/primitives/link/index.js +129 -0
  299. package/src/elements/primitives/link/index.tags.json +1 -0
  300. package/src/elements/primitives/link/style.css +40 -0
  301. package/src/elements/primitives/spinner/index.html +1 -0
  302. package/src/elements/primitives/spinner/index.js +62 -0
  303. package/src/elements/primitives/spinner/index.tags.json +1 -0
  304. package/src/elements/primitives/spinner/style.css +20 -0
  305. package/src/elements/primitives/text/index.html +1 -0
  306. package/src/elements/primitives/text/index.js +79 -0
  307. package/src/elements/primitives/text/index.tags.json +1 -0
  308. package/src/elements/primitives/text/style.css +25 -0
  309. package/src/index.js +23 -0
  310. package/src/styles/base.css +66 -0
  311. package/src/styles/index.css +10 -0
  312. package/src/styles/layers.css +9 -0
  313. package/src/styles/reset.css +66 -0
  314. package/src/sw/activate.js +51 -0
  315. package/src/sw/expire.js +47 -0
  316. package/src/sw/index.js +28 -0
  317. package/src/sw/install.js +35 -0
  318. package/src/sw/push.js +58 -0
  319. package/src/sw/queue.js +60 -0
  320. package/src/sw/routes.js +71 -0
  321. package/src/sw/strategies.js +247 -0
  322. package/src/sw/sync.js +80 -0
  323. package/src/tokens/index.css +26 -0
  324. package/src/tokens/primitives/colors.css +54 -0
  325. package/src/tokens/primitives/motion.css +34 -0
  326. package/src/tokens/primitives/radius.css +16 -0
  327. package/src/tokens/primitives/shadow.css +34 -0
  328. package/src/tokens/primitives/spacing.css +27 -0
  329. package/src/tokens/primitives/typography.css +46 -0
  330. package/src/tokens/primitives/zindex.css +18 -0
  331. package/src/tokens/registered/colors.css +133 -0
  332. package/src/tokens/registered/dimensions.css +31 -0
  333. package/src/tokens/semantic/components.css +125 -0
  334. package/src/tokens/semantic/contrast.css +33 -0
  335. package/src/tokens/semantic/dark.css +61 -0
  336. package/src/tokens/semantic/light.css +64 -0
  337. package/types/core/animations/index.d.ts +52 -0
  338. package/types/core/api/index.d.ts +68 -0
  339. package/types/core/events/index.d.ts +50 -0
  340. package/types/core/offline/index.d.ts +68 -0
  341. package/types/core/platform/index.d.ts +60 -0
  342. package/types/core/router/index.d.ts +203 -0
  343. package/types/core/security/index.d.ts +33 -0
  344. package/types/core/state/index.d.ts +68 -0
  345. package/types/core/storage/index.d.ts +40 -0
  346. package/types/core/ui/index.d.ts +446 -0
  347. package/types/core/workers/index.d.ts +221 -0
  348. package/types/elements/index.d.ts +150 -0
  349. package/types/index.d.ts +18 -0
@@ -0,0 +1,346 @@
1
+ # Scoped Mutation Observation API: `watch`
2
+
3
+ `watch` is the component-scoped mutation observation helper injected alongside `tags`, `refs`, and `on`. It wraps `MutationObserver` with lifecycle cleanup, shadow-root scoping, selector/direct-ref targeting, one-shot handlers, and mutation-specific handler signatures.
4
+
5
+ ## 1. Injection Shape
6
+
7
+ ```javascript
8
+ mount({ el, ctrl, tags, on, refs, watch }) {}
9
+
10
+ update({ el, ctrl, tags, on, refs, watch, name, val, prev }) {}
11
+ ```
12
+
13
+ `watch` is created once per connected lifecycle. If the component disconnects, `ctrl.abort()` disconnects the shared observer and removes every registration.
14
+
15
+ ## 2. API Summary
16
+
17
+ ```javascript
18
+ watch.attr(target, attrName, handler, signalOrOptions?)
19
+ watch.attr(target, attrNames, handler, signalOrOptions?)
20
+ watch.attr(target, '*', handler, signalOrOptions?)
21
+
22
+ watch.kids(target, handler, signalOrOptions?)
23
+ watch.kids(target, { deep: true }, handler, signalOrOptions?)
24
+
25
+ watch.text(target, handler, signalOrOptions?)
26
+
27
+ watch.tree(target, handler, signalOrOptions?)
28
+
29
+ watch.attr.once(...)
30
+ watch.kids.once(...)
31
+ watch.text.once(...)
32
+ watch.tree.once(...)
33
+ ```
34
+
35
+ `target` can be:
36
+
37
+ - A selector string scoped to the component shadow root.
38
+ - A direct `Element` reference from `refs.*` or `tags.one(...)`.
39
+
40
+ The final argument can be:
41
+
42
+ - An `AbortSignal`.
43
+ - An options object: `{ signal, once }`.
44
+ - Omitted, in which case `ctrl.signal` is used.
45
+
46
+ Every call returns a disposer function.
47
+
48
+ ```javascript
49
+ const stop = watch.attr(refs.submit, 'disabled', handler);
50
+ stop();
51
+ ```
52
+
53
+ ## 3. Attribute Watching: `watch.attr`
54
+
55
+ Observe one, many, or all attributes.
56
+
57
+ ```javascript
58
+ watch.attr('button', 'disabled', (attr, next, prev, el) => {})
59
+ watch.attr('.card', ['aria-expanded', 'data-state'], handler)
60
+ watch.attr('.card', '*', handler)
61
+ watch.attr(refs.submit, 'disabled', handler)
62
+ watch.attr(tags.one('#toggle'), 'aria-pressed', handler)
63
+ watch.attr.once('details', 'open', handler)
64
+ ```
65
+
66
+ Handler:
67
+
68
+ ```javascript
69
+ (attrName, newValue, oldValue, element) => void
70
+ ```
71
+
72
+ Example:
73
+
74
+ ```javascript
75
+ mount({ refs, watch }) {
76
+ watch.attr(refs.submit, 'disabled', (attr, next, prev, button) => {
77
+ button.dataset.wasDisabled = String(prev !== null);
78
+ });
79
+ }
80
+ ```
81
+
82
+ Notes:
83
+
84
+ - `newValue` is read with `element.getAttribute(attrName)`, so removed attributes produce `null`.
85
+ - `oldValue` comes from the mutation record and is always requested internally.
86
+ - Boolean attributes usually have `''` when present and `null` when absent.
87
+
88
+ ## 4. Child Watching: `watch.kids`
89
+
90
+ Observe child additions and removals.
91
+
92
+ ```javascript
93
+ watch.kids('ul', ({ added, removed }, list) => {})
94
+ watch.kids(refs.list, handler)
95
+ watch.kids('ul', { deep: true }, handler)
96
+ watch.kids.once('ul', handler)
97
+ ```
98
+
99
+ Handler:
100
+
101
+ ```javascript
102
+ ({ added, removed }, element) => void
103
+ ```
104
+
105
+ Example:
106
+
107
+ ```javascript
108
+ mount({ refs, watch }) {
109
+ watch.kids(refs.list, ({ added }) => {
110
+ for (const node of added) {
111
+ if (node.nodeType === Node.ELEMENT_NODE) {
112
+ node.classList.add('entry-new');
113
+ }
114
+ }
115
+ });
116
+ }
117
+ ```
118
+
119
+ Rules:
120
+
121
+ - `added` and `removed` are arrays, never `NodeList`.
122
+ - Without `{ deep: true }`, only direct child mutations on the target match.
123
+ - With `{ deep: true }`, descendant child mutations inside the target subtree match.
124
+
125
+ ## 5. Text Watching: `watch.text`
126
+
127
+ Observe text-node changes inside a target.
128
+
129
+ ```javascript
130
+ watch.text('.label', (next, prev, el) => {})
131
+ watch.text(refs.counter, handler)
132
+ watch.text.once('.status', handler)
133
+ ```
134
+
135
+ Handler:
136
+
137
+ ```javascript
138
+ (newText, oldText, element) => void
139
+ ```
140
+
141
+ Example:
142
+
143
+ ```javascript
144
+ mount({ refs, watch }) {
145
+ watch.text(refs.counter, (next, prev, el) => {
146
+ const direction = Number(next) > Number(prev) ? 'up' : 'down';
147
+ el.dataset.direction = direction;
148
+ });
149
+ }
150
+ ```
151
+
152
+ Notes:
153
+
154
+ - `newText` is the current `textContent` of the watched element.
155
+ - `oldText` is the previous text node value from the mutation record.
156
+ - Multiple text nodes may produce multiple records in the same microtask batch.
157
+
158
+ ## 6. Subtree Watching: `watch.tree`
159
+
160
+ Observe all mutation types under a target and receive raw records.
161
+
162
+ ```javascript
163
+ watch.tree('.editor', (records) => {})
164
+ watch.tree(refs.canvas, handler)
165
+ watch.tree.once('.editor', handler)
166
+ ```
167
+
168
+ Handler:
169
+
170
+ ```javascript
171
+ (records, element) => void
172
+ ```
173
+
174
+ Example:
175
+
176
+ ```javascript
177
+ mount({ refs, watch }) {
178
+ watch.tree(refs.editor, (records) => {
179
+ for (const record of records) {
180
+ if (record.type === 'childList') syncOutline();
181
+ if (record.type === 'attributes') syncToolbar();
182
+ }
183
+ });
184
+ }
185
+ ```
186
+
187
+ `watch.tree` is the escape hatch. Prefer `attr`, `kids`, or `text` when the mutation type is known.
188
+
189
+ ## 7. Scoping Rules
190
+
191
+ `watch` never observes outside the component shadow root.
192
+
193
+ - Selector targets are resolved with `shadowRoot.querySelectorAll`.
194
+ - Direct element targets must satisfy `shadowRoot.contains(element)`.
195
+ - A selector with no matches returns a no-op disposer and warns in development.
196
+ - A direct target outside the shadow root throws in development and returns a no-op disposer in production.
197
+ - Cross-component observation must use `CustomEvent`, shared state, or parent-owned refs.
198
+
199
+ ## 8. Shared Observer Model
200
+
201
+ Each component instance owns one `MutationObserver`.
202
+
203
+ Registrations are stored internally:
204
+
205
+ ```javascript
206
+ {
207
+ id,
208
+ kind,
209
+ target,
210
+ selector,
211
+ attrs,
212
+ deep,
213
+ handler,
214
+ signal,
215
+ once
216
+ }
217
+ ```
218
+
219
+ When a registration is added or removed, the observer options are recomputed:
220
+
221
+ ```javascript
222
+ {
223
+ attributes: hasAttrWatch || hasTreeWatch,
224
+ attributeOldValue: hasAttrWatch || hasTreeWatch,
225
+ attributeFilter: unionOfSpecificAttrsOrUndefined,
226
+ childList: hasKidsWatch || hasTreeWatch,
227
+ characterData: hasTextWatch || hasTreeWatch,
228
+ characterDataOldValue: hasTextWatch || hasTreeWatch,
229
+ subtree: true
230
+ }
231
+ ```
232
+
233
+ If any `watch.attr(..., '*', ...)` or `watch.tree(...)` registration exists, `attributeFilter` is omitted.
234
+
235
+ ## 9. Matching Rules
236
+
237
+ Attribute records:
238
+
239
+ - Match `record.type === 'attributes'`.
240
+ - Match when `record.target === registration.target`.
241
+ - For selector targets with multiple elements, match any registered target element.
242
+ - Attribute name must be included in `attrs`, unless `attrs === '*'`.
243
+
244
+ Child records:
245
+
246
+ - Match `record.type === 'childList'`.
247
+ - Without `deep`, match only `record.target === registration.target`.
248
+ - With `deep`, match `registration.target.contains(record.target)`.
249
+
250
+ Text records:
251
+
252
+ - Match `record.type === 'characterData'`.
253
+ - Use `record.target.parentElement` as the element.
254
+ - Match if the parent is the registration target or is contained by it.
255
+
256
+ Tree records:
257
+
258
+ - Match any record where the mutation target is the watched element or inside it.
259
+ - Handler receives the filtered record batch.
260
+
261
+ ## 10. Cleanup
262
+
263
+ Cleanup must be deterministic:
264
+
265
+ - Default signal is the component `ctrl.signal`.
266
+ - A custom signal removes only that registration.
267
+ - `.once` removes only that registration after the first matching mutation.
268
+ - Component abort disconnects the shared observer and clears the registry.
269
+ - The mutation callback checks `defaultSignal.aborted` before dispatching because queued observer callbacks can still run after `disconnect()`.
270
+
271
+ ## 11. Implementation Pseudocode
272
+
273
+ ```javascript
274
+ export function createMutationWatcher(shadowRoot, defaultSignal) {
275
+ const registry = new Map();
276
+ let observer = null;
277
+ let nextId = 0;
278
+
279
+ function add(kind, args, once = false) {
280
+ const reg = normalizeRegistration(kind, args, once, defaultSignal, shadowRoot);
281
+ if (!reg) return () => {};
282
+
283
+ registry.set(reg.id, reg);
284
+ reg.signal?.addEventListener('abort', () => remove(reg.id), { once: true });
285
+ refreshObserver();
286
+
287
+ return () => remove(reg.id);
288
+ }
289
+
290
+ function remove(id) {
291
+ registry.delete(id);
292
+ refreshObserver();
293
+ }
294
+
295
+ function refreshObserver() {
296
+ if (observer) observer.disconnect();
297
+ if (!registry.size || defaultSignal.aborted) return;
298
+ observer ||= new MutationObserver(dispatch);
299
+ observer.observe(shadowRoot, computeOptions(registry.values()));
300
+ }
301
+
302
+ function dispatch(records) {
303
+ if (defaultSignal.aborted) return;
304
+ for (const reg of registry.values()) {
305
+ const matches = filterRecords(records, reg);
306
+ if (!matches.length) continue;
307
+ callHandler(matches, reg);
308
+ if (reg.once) remove(reg.id);
309
+ }
310
+ }
311
+
312
+ defaultSignal.addEventListener('abort', () => {
313
+ observer?.disconnect();
314
+ registry.clear();
315
+ }, { once: true });
316
+
317
+ return createWatchProxy(add);
318
+ }
319
+ ```
320
+
321
+ ## 12. Usage with `tags`, `refs`, and `on`
322
+
323
+ ```javascript
324
+ mount({ refs, tags, on, watch }) {
325
+ const toggle = refs.toggle ?? tags.one('[data-toggle]');
326
+ const panel = refs.panel;
327
+
328
+ watch.attr(toggle, 'aria-expanded', (attr, next) => {
329
+ panel.hidden = next !== 'true';
330
+ });
331
+
332
+ on.click('[data-toggle]', (event, target) => {
333
+ const open = target.getAttribute('aria-expanded') === 'true';
334
+ target.setAttribute('aria-expanded', String(!open));
335
+ });
336
+ }
337
+ ```
338
+
339
+ The click mutates an attribute. `watch.attr` reacts without any manual event dispatch.
340
+
341
+ ## 13. What `watch` Is Not
342
+
343
+ - It is not cross-component state management.
344
+ - It is not a replacement for `on` for user interactions.
345
+ - It is not a render scheduler. Heavy handlers should schedule work with `ui.schedule` or `ui.scheduleFrame`.
346
+ - It is not for slotted light DOM changes. Use `slotchange` on a `<slot>` element for that.
@@ -0,0 +1,138 @@
1
+ /**
2
+ * src/core/workers/broadcast.js
3
+ *
4
+ * BroadcastChannel Manager.
5
+ * Reference-counted channels: open on first subscriber, close when the last
6
+ * subscriber leaves. Supports explicit close(name) and clear() for lifecycle
7
+ * control. Cleans up both the message listener and the abort listener on
8
+ * disposal to prevent memory leaks.
9
+ *
10
+ * Source: plan.md Phase 5
11
+ */
12
+
13
+ class BroadcastManager {
14
+ #channels = new Map(); // name → BroadcastChannel
15
+ #listeners = new Map(); // name → Set<{ wrapped, fn, abortCleanup }>
16
+
17
+ /**
18
+ * Dispatches a message to a named channel.
19
+ *
20
+ * BroadcastChannel messages are NOT echoed to the posting instance — only
21
+ * other channel instances with the same name receive them. So we always post
22
+ * from a short-lived peer channel and close it immediately. This correctly
23
+ * delivers to all subscriber channel instances, including the one managed
24
+ * by this BroadcastManager.
25
+ *
26
+ * @param {string} name
27
+ * @param {any} payload
28
+ */
29
+ broadcast(name, payload) {
30
+ // Always use a peer sender so the subscriber channel receives the message.
31
+ // (A BroadcastChannel instance never fires its own onmessage for messages it posts.)
32
+ const sender = new BroadcastChannel(name);
33
+ sender.postMessage(payload);
34
+ // Close asynchronously to allow same-process delivery before cleanup.
35
+ setTimeout(() => sender.close(), 0);
36
+ }
37
+
38
+ /**
39
+ * Subscribes to a named channel.
40
+ * Returns a dispose function that removes the listener and cleans up the
41
+ * abort listener.
42
+ *
43
+ * @param {string} name
44
+ * @param {Function} fn
45
+ * @param {AbortSignal} [signal]
46
+ * @returns {() => void} dispose
47
+ */
48
+ subscribe(name, fn, signal) {
49
+ if (signal?.aborted) return () => {};
50
+
51
+ // Open channel on first subscriber
52
+ if (!this.#channels.has(name)) {
53
+ const ch = new BroadcastChannel(name);
54
+ ch.onmessageerror = (e) => {
55
+ console.error(`BroadcastChannel "${name}" deserialization error:`, e);
56
+ };
57
+ this.#channels.set(name, ch);
58
+ this.#listeners.set(name, new Set());
59
+ }
60
+
61
+ const channel = this.#channels.get(name);
62
+ const group = this.#listeners.get(name);
63
+
64
+ const wrapped = (event) => {
65
+ try {
66
+ fn(event.data);
67
+ } catch (err) {
68
+ console.error(`Error in BroadcastChannel "${name}" subscriber:`, err);
69
+ }
70
+ };
71
+
72
+ channel.addEventListener('message', wrapped);
73
+
74
+ let abortCleanup = null;
75
+ const entry = { wrapped, fn, abortCleanup };
76
+ group.add(entry);
77
+
78
+ const dispose = () => {
79
+ channel.removeEventListener('message', wrapped);
80
+ group.delete(entry);
81
+
82
+ // Remove the abort listener we registered
83
+ if (entry.abortCleanup) {
84
+ signal?.removeEventListener('abort', entry.abortCleanup);
85
+ entry.abortCleanup = null;
86
+ }
87
+
88
+ // Auto-close when no listeners remain
89
+ if (group.size === 0) {
90
+ channel.close();
91
+ this.#channels.delete(name);
92
+ this.#listeners.delete(name);
93
+ }
94
+ };
95
+
96
+ if (signal) {
97
+ const onAbort = () => dispose();
98
+ entry.abortCleanup = onAbort;
99
+ signal.addEventListener('abort', onAbort, { once: true });
100
+ }
101
+
102
+ return dispose;
103
+ }
104
+
105
+ /**
106
+ * Closes one named channel and removes all its listeners.
107
+ *
108
+ * @param {string} name
109
+ */
110
+ close(name) {
111
+ const channel = this.#channels.get(name);
112
+ if (!channel) return;
113
+
114
+ const group = this.#listeners.get(name) ?? new Set();
115
+ for (const entry of group) {
116
+ channel.removeEventListener('message', entry.wrapped);
117
+ if (entry.abortCleanup) {
118
+ // We don't have the signal reference here, so just null the cleanup
119
+ entry.abortCleanup = null;
120
+ }
121
+ }
122
+
123
+ channel.close();
124
+ this.#channels.delete(name);
125
+ this.#listeners.delete(name);
126
+ }
127
+
128
+ /**
129
+ * Closes all open channels and removes all listeners.
130
+ */
131
+ clear() {
132
+ for (const name of [...this.#channels.keys()]) {
133
+ this.close(name);
134
+ }
135
+ }
136
+ }
137
+
138
+ export const broadcast = new BroadcastManager();
@@ -0,0 +1,153 @@
1
+ /**
2
+ * src/core/workers/dedicated.js
3
+ *
4
+ * Dedicated Worker Wrapper.
5
+ * Manages the lifecycle of standard background dedicated Workers, using
6
+ * native MessageChannel per request for isolated, concurrent execution corridors
7
+ * without message cross-contamination.
8
+ *
9
+ * Message contract:
10
+ * Request → { task, payload, meta }
11
+ * Response → { ok, value, error }
12
+ *
13
+ * Source: plan.md Phase 1, Phase 2
14
+ */
15
+
16
+ export class Dedicated {
17
+ #script;
18
+ #worker;
19
+ #pending = new Map(); // port1 → { resolve, reject, timer }
20
+ #closed = false;
21
+
22
+ constructor(script) {
23
+ this.#script = script;
24
+ this.#worker = new Worker(script, { type: 'module' });
25
+ this.#worker.addEventListener('error', (e) => this.#fail(e));
26
+ this.#worker.addEventListener('messageerror', (e) => this.#fail(e));
27
+ }
28
+
29
+ get closed() { return this.#closed; }
30
+
31
+ /**
32
+ * Dispatches a task to the worker.
33
+ *
34
+ * Options:
35
+ * payload – data to send (structured-clone safe)
36
+ * transferables – Transferable[] transferred with the message
37
+ * signal – AbortSignal to cancel the request
38
+ * timeout – milliseconds before automatic abort
39
+ * meta – arbitrary structured data attached to the request
40
+ */
41
+ run(task, options = {}) {
42
+ if (this.#closed) {
43
+ return Promise.reject(new Error(`Worker "${this.#script}" is closed`));
44
+ }
45
+
46
+ const {
47
+ payload = null,
48
+ transferables = [],
49
+ signal,
50
+ timeout,
51
+ meta
52
+ } = options;
53
+
54
+ // Abort before anything starts
55
+ if (signal?.aborted) {
56
+ return Promise.reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
57
+ }
58
+
59
+ return new Promise((resolve, reject) => {
60
+ const channel = new MessageChannel();
61
+
62
+ // Combine caller signal and optional timeout into one controller
63
+ let combined = null;
64
+ let timer = null;
65
+
66
+ if (signal || timeout) {
67
+ combined = new AbortController();
68
+
69
+ if (signal) {
70
+ if (signal.aborted) {
71
+ combined.abort(signal.reason);
72
+ } else {
73
+ signal.addEventListener('abort', () => combined.abort(signal.reason), { once: true });
74
+ }
75
+ }
76
+
77
+ if (timeout) {
78
+ timer = setTimeout(
79
+ () => combined.abort(new Error(`Worker task "${task}" timed out after ${timeout}ms`)),
80
+ timeout
81
+ );
82
+ }
83
+ }
84
+
85
+ const abort = combined?.signal;
86
+
87
+ const cleanup = () => {
88
+ if (timer) clearTimeout(timer);
89
+ channel.port1.close();
90
+ this.#pending.delete(channel.port1);
91
+ };
92
+
93
+ const fail = (reason) => {
94
+ cleanup();
95
+ reject(reason);
96
+ };
97
+
98
+ this.#pending.set(channel.port1, { resolve, reject: fail });
99
+
100
+ channel.port1.onmessage = (e) => {
101
+ const { ok, value, error } = e.data;
102
+ cleanup();
103
+ if (ok) {
104
+ resolve(value);
105
+ } else {
106
+ reject(new Error(error ?? 'Worker task failed'));
107
+ }
108
+ };
109
+
110
+ channel.port1.onmessageerror = () => {
111
+ fail(new Error('Deserialization error on message channel'));
112
+ };
113
+
114
+ if (abort) {
115
+ abort.addEventListener('abort', () => {
116
+ fail(abort.reason ?? new DOMException('Aborted', 'AbortError'));
117
+ }, { once: true });
118
+ }
119
+
120
+ this.#worker.postMessage(
121
+ { task, payload, meta, port: channel.port2 },
122
+ [channel.port2, ...transferables]
123
+ );
124
+ });
125
+ }
126
+
127
+ /** Route a worker-level error into all pending requests. */
128
+ #fail(event) {
129
+ const reason = event instanceof ErrorEvent
130
+ ? new Error(event.message ?? 'Worker error')
131
+ : new Error('Worker message deserialization error');
132
+
133
+ for (const { reject } of this.#pending.values()) {
134
+ reject(reason);
135
+ }
136
+ this.#pending.clear();
137
+ }
138
+
139
+ /** Terminate the worker. Subsequent `run` calls will reject immediately. */
140
+ terminate() {
141
+ if (this.#closed) return;
142
+ this.#closed = true;
143
+ this.#worker.terminate();
144
+ const reason = new Error(`Worker "${this.#script}" terminated`);
145
+ for (const { reject } of this.#pending.values()) {
146
+ reject(reason);
147
+ }
148
+ this.#pending.clear();
149
+ }
150
+ }
151
+
152
+ // Compatibility alias
153
+ export { Dedicated as DedicatedWorker };