@mhmo91/schmancy 0.10.19 → 0.10.21

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 (371) hide show
  1. package/custom-elements.json +0 -62
  2. package/dist/agent/{overlay.confirm-body-D3jQyXgA.js → overlay.confirm-body-DXus8d-w.js} +1 -1
  3. package/dist/agent/{overlay.confirm-body-D3jQyXgA.js.map → overlay.confirm-body-DXus8d-w.js.map} +1 -1
  4. package/dist/agent/schmancy.agent.js +2043 -2083
  5. package/dist/agent/schmancy.agent.js.map +1 -1
  6. package/dist/agent/schmancy.manifest.json +1 -48
  7. package/dist/area-Cbkt0NX4.cjs +21 -0
  8. package/dist/area-Cbkt0NX4.cjs.map +1 -0
  9. package/dist/{area-BIipuSyO.js → area-Ddk7P5wD.js} +101 -131
  10. package/dist/area-Ddk7P5wD.js.map +1 -0
  11. package/dist/area.cjs +1 -1
  12. package/dist/area.js +1 -1
  13. package/dist/{autocomplete-B8CE5vGw.cjs → autocomplete-CfBFDSc3.cjs} +1 -1
  14. package/dist/{autocomplete-B8CE5vGw.cjs.map → autocomplete-CfBFDSc3.cjs.map} +1 -1
  15. package/dist/{autocomplete-Mrb3koUN.js → autocomplete-Ds3Q2cwR.js} +2 -2
  16. package/dist/{autocomplete-Mrb3koUN.js.map → autocomplete-Ds3Q2cwR.js.map} +1 -1
  17. package/dist/autocomplete.cjs +1 -1
  18. package/dist/autocomplete.js +1 -1
  19. package/dist/avatar.cjs +1 -1
  20. package/dist/avatar.js +1 -1
  21. package/dist/badge.cjs +1 -1
  22. package/dist/badge.js +1 -1
  23. package/dist/{boat-CNWIQPA1.js → boat-BF5P6p_f.js} +1 -1
  24. package/dist/{boat-CNWIQPA1.js.map → boat-BF5P6p_f.js.map} +1 -1
  25. package/dist/{boat-OatK_MGh.cjs → boat-BPN8HLzZ.cjs} +1 -1
  26. package/dist/{boat-OatK_MGh.cjs.map → boat-BPN8HLzZ.cjs.map} +1 -1
  27. package/dist/boat.cjs +1 -1
  28. package/dist/boat.js +1 -1
  29. package/dist/breadcrumb.cjs +1 -1
  30. package/dist/breadcrumb.js +1 -1
  31. package/dist/{busy-Cetzws-m.js → busy-BuACDJy6.js} +1 -1
  32. package/dist/{busy-Cetzws-m.js.map → busy-BuACDJy6.js.map} +1 -1
  33. package/dist/{busy-CMKX4oQf.cjs → busy-C7ejPa-Q.cjs} +1 -1
  34. package/dist/{busy-CMKX4oQf.cjs.map → busy-C7ejPa-Q.cjs.map} +1 -1
  35. package/dist/busy.cjs +1 -1
  36. package/dist/busy.js +1 -1
  37. package/dist/button.cjs +15 -9
  38. package/dist/button.cjs.map +1 -1
  39. package/dist/button.js +15 -9
  40. package/dist/button.js.map +1 -1
  41. package/dist/{card-8VXoo2C_.cjs → card-BIzaLuEg.cjs} +1 -1
  42. package/dist/{card-8VXoo2C_.cjs.map → card-BIzaLuEg.cjs.map} +1 -1
  43. package/dist/{card-D2k3dRL0.js → card-CgQwXO8L.js} +1 -1
  44. package/dist/{card-D2k3dRL0.js.map → card-CgQwXO8L.js.map} +1 -1
  45. package/dist/card.cjs +1 -1
  46. package/dist/card.js +1 -1
  47. package/dist/{checkbox-Cq5wzeaY.cjs → checkbox-BAqE3sTx.cjs} +1 -1
  48. package/dist/{checkbox-Cq5wzeaY.cjs.map → checkbox-BAqE3sTx.cjs.map} +1 -1
  49. package/dist/{checkbox-8hNsBejz.js → checkbox-BNdg57Om.js} +1 -1
  50. package/dist/{checkbox-8hNsBejz.js.map → checkbox-BNdg57Om.js.map} +1 -1
  51. package/dist/checkbox.cjs +1 -1
  52. package/dist/checkbox.js +1 -1
  53. package/dist/{chips-Dx_WvOGk.cjs → chips-DS3y4Lbn.cjs} +2 -4
  54. package/dist/chips-DS3y4Lbn.cjs.map +1 -0
  55. package/dist/{chips-D1kJrbzo.js → chips-DnqLaOb1.js} +3 -5
  56. package/dist/chips-DnqLaOb1.js.map +1 -0
  57. package/dist/chips.cjs +1 -1
  58. package/dist/chips.js +2 -2
  59. package/dist/connectivity.cjs +1 -1
  60. package/dist/connectivity.js +1 -1
  61. package/dist/content-drawer.cjs +1 -1
  62. package/dist/content-drawer.js +1 -1
  63. package/dist/{date-range-H903Vt_r.cjs → date-range-CsJfjbmi.cjs} +1 -1
  64. package/dist/{date-range-H903Vt_r.cjs.map → date-range-CsJfjbmi.cjs.map} +1 -1
  65. package/dist/{date-range-Dv-DM6mB.js → date-range-aPSmSBhk.js} +2 -2
  66. package/dist/{date-range-Dv-DM6mB.js.map → date-range-aPSmSBhk.js.map} +1 -1
  67. package/dist/{date-range-inline-Bvs2ZvEY.cjs → date-range-inline-CAa0_4EI.cjs} +1 -1
  68. package/dist/{date-range-inline-Bvs2ZvEY.cjs.map → date-range-inline-CAa0_4EI.cjs.map} +1 -1
  69. package/dist/{date-range-inline-TWWnTZlw.js → date-range-inline-PeRt1iIF.js} +1 -1
  70. package/dist/{date-range-inline-TWWnTZlw.js.map → date-range-inline-PeRt1iIF.js.map} +1 -1
  71. package/dist/date-range-inline.cjs +1 -1
  72. package/dist/date-range-inline.js +1 -1
  73. package/dist/date-range.cjs +1 -1
  74. package/dist/date-range.js +1 -1
  75. package/dist/delay.cjs +1 -1
  76. package/dist/delay.js +1 -1
  77. package/dist/{details-CwSDur6j.cjs → details-BnXbDpt7.cjs} +2 -2
  78. package/dist/details-BnXbDpt7.cjs.map +1 -0
  79. package/dist/{details-Cpg8sH2F.js → details-BpFjVclg.js} +2 -2
  80. package/dist/details-BpFjVclg.js.map +1 -0
  81. package/dist/details.cjs +1 -1
  82. package/dist/details.js +1 -1
  83. package/dist/directives.cjs +3 -3
  84. package/dist/directives.cjs.map +1 -1
  85. package/dist/directives.js +290 -203
  86. package/dist/directives.js.map +1 -1
  87. package/dist/{divider-BNdVLE0H.cjs → divider-B84lt1A3.cjs} +1 -1
  88. package/dist/{divider-BNdVLE0H.cjs.map → divider-B84lt1A3.cjs.map} +1 -1
  89. package/dist/{divider-Be833gGZ.js → divider-D8cBBkdG.js} +1 -1
  90. package/dist/{divider-Be833gGZ.js.map → divider-D8cBBkdG.js.map} +1 -1
  91. package/dist/divider.cjs +1 -1
  92. package/dist/divider.js +1 -1
  93. package/dist/dropdown.cjs +1 -1
  94. package/dist/dropdown.js +1 -1
  95. package/dist/{expand-CtoffNNj.js → expand-BJiKggfg.js} +2 -2
  96. package/dist/{expand-CtoffNNj.js.map → expand-BJiKggfg.js.map} +1 -1
  97. package/dist/{expand-BP6RLzHw.cjs → expand-DK-O37-j.cjs} +1 -1
  98. package/dist/{expand-BP6RLzHw.cjs.map → expand-DK-O37-j.cjs.map} +1 -1
  99. package/dist/expand.cjs +1 -1
  100. package/dist/expand.js +1 -1
  101. package/dist/{float-KmbhaQHA.js → float-B4FDN40h.js} +1 -1
  102. package/dist/{float-KmbhaQHA.js.map → float-B4FDN40h.js.map} +1 -1
  103. package/dist/{float-CfbQM_2v.cjs → float-RWR6Q1Hh.cjs} +1 -1
  104. package/dist/{float-CfbQM_2v.cjs.map → float-RWR6Q1Hh.cjs.map} +1 -1
  105. package/dist/float.cjs +1 -1
  106. package/dist/float.js +1 -1
  107. package/dist/{form-8IcmP8uV.js → form-Bz5WamuM.js} +8 -8
  108. package/dist/{form-8IcmP8uV.js.map → form-Bz5WamuM.js.map} +1 -1
  109. package/dist/{form-CuBIrKOA.cjs → form-PioZDvzA.cjs} +1 -1
  110. package/dist/{form-CuBIrKOA.cjs.map → form-PioZDvzA.cjs.map} +1 -1
  111. package/dist/form.cjs +1 -1
  112. package/dist/form.js +6 -6
  113. package/dist/handover/agent-runtime-followups.md +1 -1
  114. package/dist/handover/agent-runtime-v1.md +3 -3
  115. package/dist/{icons-D7df1ysG.js → icons-BgUbHwy8.js} +1 -1
  116. package/dist/{icons-D7df1ysG.js.map → icons-BgUbHwy8.js.map} +1 -1
  117. package/dist/{icons-BJld4JHp.cjs → icons-morK4hHz.cjs} +1 -1
  118. package/dist/{icons-BJld4JHp.cjs.map → icons-morK4hHz.cjs.map} +1 -1
  119. package/dist/icons.cjs +1 -1
  120. package/dist/icons.js +1 -1
  121. package/dist/{iframe-GT6D8l5Z.cjs → iframe-BXe1TPx1.cjs} +1 -1
  122. package/dist/{iframe-GT6D8l5Z.cjs.map → iframe-BXe1TPx1.cjs.map} +1 -1
  123. package/dist/{iframe-DAbgW9tT.js → iframe-CByrVlZy.js} +1 -1
  124. package/dist/{iframe-DAbgW9tT.js.map → iframe-CByrVlZy.js.map} +1 -1
  125. package/dist/iframe.cjs +1 -1
  126. package/dist/iframe.js +1 -1
  127. package/dist/index.cjs +1 -1
  128. package/dist/index.js +55 -56
  129. package/dist/{input-BE9wEEw4.cjs → input-BY9OCQWr.cjs} +1 -1
  130. package/dist/{input-BE9wEEw4.cjs.map → input-BY9OCQWr.cjs.map} +1 -1
  131. package/dist/{input-DC6ap_uN.js → input-Q0fm34Co.js} +2 -2
  132. package/dist/{input-DC6ap_uN.js.map → input-Q0fm34Co.js.map} +1 -1
  133. package/dist/{input-chip-MsiMu-b5.cjs → input-chip-BwNf3GD0.cjs} +1 -1
  134. package/dist/{input-chip-MsiMu-b5.cjs.map → input-chip-BwNf3GD0.cjs.map} +1 -1
  135. package/dist/{input-chip-c5n547tg.js → input-chip-CytUirVS.js} +1 -1
  136. package/dist/{input-chip-c5n547tg.js.map → input-chip-CytUirVS.js.map} +1 -1
  137. package/dist/input.cjs +1 -1
  138. package/dist/input.js +1 -1
  139. package/dist/json.cjs +1 -1
  140. package/dist/json.js +2 -2
  141. package/dist/kbd.cjs +1 -1
  142. package/dist/kbd.js +1 -1
  143. package/dist/layout.cjs +26 -1
  144. package/dist/layout.cjs.map +1 -0
  145. package/dist/layout.js +115 -2
  146. package/dist/layout.js.map +1 -0
  147. package/dist/{lightbox-CNX9Eg3U.js → lightbox-Ckvn5YNF.js} +1 -1
  148. package/dist/{lightbox-CNX9Eg3U.js.map → lightbox-Ckvn5YNF.js.map} +1 -1
  149. package/dist/{lightbox-HqJBBjAT.cjs → lightbox-p2E0oVR0.cjs} +1 -1
  150. package/dist/{lightbox-HqJBBjAT.cjs.map → lightbox-p2E0oVR0.cjs.map} +1 -1
  151. package/dist/lightbox.cjs +1 -1
  152. package/dist/lightbox.js +1 -1
  153. package/dist/{list-C76Pb-c1.js → list-CsrPVvmm.js} +1 -1
  154. package/dist/{list-C76Pb-c1.js.map → list-CsrPVvmm.js.map} +1 -1
  155. package/dist/{list-bhyuQSyO.cjs → list-r57UFHu3.cjs} +1 -1
  156. package/dist/{list-bhyuQSyO.cjs.map → list-r57UFHu3.cjs.map} +1 -1
  157. package/dist/list.cjs +1 -1
  158. package/dist/list.js +1 -1
  159. package/dist/{menu-BqKQ-s0C.cjs → menu-BOZ2iwed.cjs} +1 -1
  160. package/dist/{menu-BqKQ-s0C.cjs.map → menu-BOZ2iwed.cjs.map} +1 -1
  161. package/dist/{menu-C5ksITpG.js → menu-CxE16xur.js} +2 -2
  162. package/dist/{menu-C5ksITpG.js.map → menu-CxE16xur.js.map} +1 -1
  163. package/dist/menu.cjs +1 -1
  164. package/dist/menu.js +1 -1
  165. package/dist/mixins-DTCHPEd4.cjs +254 -0
  166. package/dist/{mixins-Du9HMrIG.cjs.map → mixins-DTCHPEd4.cjs.map} +1 -1
  167. package/dist/mixins-pU53qf6R.js +636 -0
  168. package/dist/{mixins-DCVXqL1Q.js.map → mixins-pU53qf6R.js.map} +1 -1
  169. package/dist/mixins.cjs +1 -1
  170. package/dist/mixins.js +1 -1
  171. package/dist/nav-drawer.cjs +1 -1
  172. package/dist/nav-drawer.js +1 -1
  173. package/dist/navigation-bar.cjs +1 -1
  174. package/dist/navigation-bar.js +1 -1
  175. package/dist/navigation-rail.cjs +1 -1
  176. package/dist/navigation-rail.js +1 -1
  177. package/dist/{notification-DR3gvWt8.cjs → notification-58tkVys8.cjs} +1 -1
  178. package/dist/{notification-DR3gvWt8.cjs.map → notification-58tkVys8.cjs.map} +1 -1
  179. package/dist/{notification-eZxtr3NN.js → notification-CgTBiAdf.js} +2 -2
  180. package/dist/{notification-eZxtr3NN.js.map → notification-CgTBiAdf.js.map} +1 -1
  181. package/dist/notification.cjs +1 -1
  182. package/dist/notification.js +1 -1
  183. package/dist/{option-BDOKUqTy.cjs → option-61YE3gub.cjs} +1 -1
  184. package/dist/{option-BDOKUqTy.cjs.map → option-61YE3gub.cjs.map} +1 -1
  185. package/dist/{option-CBEHYG4U.js → option-Bicf6xpI.js} +1 -1
  186. package/dist/{option-CBEHYG4U.js.map → option-Bicf6xpI.js.map} +1 -1
  187. package/dist/option.cjs +1 -1
  188. package/dist/option.js +1 -1
  189. package/dist/{overlay-DG6EeyKt.cjs → overlay-B3gKPWhu.cjs} +2 -2
  190. package/dist/overlay-B3gKPWhu.cjs.map +1 -0
  191. package/dist/{overlay-oxM9OLXP.js → overlay-D3mdWOLS.js} +12 -11
  192. package/dist/overlay-D3mdWOLS.js.map +1 -0
  193. package/dist/overlay.cjs +1 -1
  194. package/dist/{overlay.confirm-body-D_P2e7l6.js → overlay.confirm-body-Czi6cMZq.js} +1 -1
  195. package/dist/{overlay.confirm-body-D_P2e7l6.js.map → overlay.confirm-body-Czi6cMZq.js.map} +1 -1
  196. package/dist/{overlay.confirm-body-78e1WrN9.cjs → overlay.confirm-body-yr0HzS_d.cjs} +1 -1
  197. package/dist/{overlay.confirm-body-78e1WrN9.cjs.map → overlay.confirm-body-yr0HzS_d.cjs.map} +1 -1
  198. package/dist/overlay.js +3 -3
  199. package/dist/{overlay.service-C8NwO4Bx.js → overlay.service-BfZf3xoD.js} +2 -2
  200. package/dist/{overlay.service-C8NwO4Bx.js.map → overlay.service-BfZf3xoD.js.map} +1 -1
  201. package/dist/{overlay.service-DQkGPUY7.cjs → overlay.service-DNs3AWqp.cjs} +1 -1
  202. package/dist/{overlay.service-DQkGPUY7.cjs.map → overlay.service-DNs3AWqp.cjs.map} +1 -1
  203. package/dist/{progress-CMSst_2U.cjs → progress-D8XZJVl5.cjs} +1 -1
  204. package/dist/{progress-CMSst_2U.cjs.map → progress-D8XZJVl5.cjs.map} +1 -1
  205. package/dist/{progress-C4kDZfb7.js → progress-Zqx-S9NZ.js} +1 -1
  206. package/dist/{progress-C4kDZfb7.js.map → progress-Zqx-S9NZ.js.map} +1 -1
  207. package/dist/progress.cjs +1 -1
  208. package/dist/progress.js +1 -1
  209. package/dist/radio-group-D9MU1Mxz.js +71 -0
  210. package/dist/radio-group-D9MU1Mxz.js.map +1 -0
  211. package/dist/radio-group-bl8K4Gls.cjs +19 -0
  212. package/dist/radio-group-bl8K4Gls.cjs.map +1 -0
  213. package/dist/radio-group.cjs +1 -1
  214. package/dist/radio-group.js +1 -1
  215. package/dist/range.cjs +1 -1
  216. package/dist/range.js +1 -1
  217. package/dist/{rxjs-utils-Cs6XGwF6.js.map → rxjs-utils-BK8VMe3K.js.map} +1 -1
  218. package/dist/{rxjs-utils-Dsj75cJy.cjs.map → rxjs-utils-DhOKenkS.cjs.map} +1 -1
  219. package/dist/rxjs-utils.cjs +1 -1
  220. package/dist/rxjs-utils.js +1 -1
  221. package/dist/{select-UU2pB67h.js → select-CMwkl-D6.js} +3 -3
  222. package/dist/select-CMwkl-D6.js.map +1 -0
  223. package/dist/select-COIfVtZl.cjs +56 -0
  224. package/dist/select-COIfVtZl.cjs.map +1 -0
  225. package/dist/select.cjs +1 -1
  226. package/dist/select.js +1 -1
  227. package/dist/skeleton.cjs +1 -1
  228. package/dist/skeleton.js +1 -1
  229. package/dist/skills/SKILL.md +18 -2
  230. package/dist/skills/area.md +13 -0
  231. package/dist/skills/overlay.md +13 -0
  232. package/dist/skills/page.md +71 -29
  233. package/dist/skills/schmancy/SKILL.md +18 -2
  234. package/dist/skills/schmancy/area.md +13 -0
  235. package/dist/skills/schmancy/overlay.md +13 -0
  236. package/dist/skills/schmancy/page.md +71 -29
  237. package/dist/slider.cjs +1 -1
  238. package/dist/slider.js +1 -1
  239. package/dist/{splash-screen-BvaDkvJU.cjs → splash-screen-2hxq8Sft.cjs} +1 -1
  240. package/dist/{splash-screen-BvaDkvJU.cjs.map → splash-screen-2hxq8Sft.cjs.map} +1 -1
  241. package/dist/{splash-screen-ChMkAPLU.js → splash-screen-xrMNpzkm.js} +1 -1
  242. package/dist/{splash-screen-ChMkAPLU.js.map → splash-screen-xrMNpzkm.js.map} +1 -1
  243. package/dist/splash-screen.cjs +1 -1
  244. package/dist/splash-screen.js +1 -1
  245. package/dist/{src-DnunCC4X.js → src-CHd-U-w4.js} +34 -35
  246. package/dist/{src-DnunCC4X.js.map → src-CHd-U-w4.js.map} +1 -1
  247. package/dist/{src-BIlD63Cz.cjs → src-ggWtvpDr.cjs} +1 -1
  248. package/dist/{src-BIlD63Cz.cjs.map → src-ggWtvpDr.cjs.map} +1 -1
  249. package/dist/steps.cjs +1 -1
  250. package/dist/steps.js +1 -1
  251. package/dist/{surface-DCRy-EyT.js → surface-3nnvlxeE.js} +1 -1
  252. package/dist/{surface-DCRy-EyT.js.map → surface-3nnvlxeE.js.map} +1 -1
  253. package/dist/{surface-DWwQDX9r.cjs → surface-BkQ44Wuo.cjs} +1 -1
  254. package/dist/{surface-DWwQDX9r.cjs.map → surface-BkQ44Wuo.cjs.map} +1 -1
  255. package/dist/surface.cjs +1 -1
  256. package/dist/surface.js +1 -1
  257. package/dist/switch.cjs +1 -1
  258. package/dist/switch.js +1 -1
  259. package/dist/table.cjs +1 -1
  260. package/dist/table.js +1 -1
  261. package/dist/{tabs-CkDNLbiS.js → tabs-CnLIe8nE.js} +1 -1
  262. package/dist/{tabs-CkDNLbiS.js.map → tabs-CnLIe8nE.js.map} +1 -1
  263. package/dist/{tabs-lxQHWEb7.cjs → tabs-Dql0rcqZ.cjs} +1 -1
  264. package/dist/{tabs-lxQHWEb7.cjs.map → tabs-Dql0rcqZ.cjs.map} +1 -1
  265. package/dist/tabs.cjs +1 -1
  266. package/dist/tabs.js +1 -1
  267. package/dist/teleport.cjs +1 -1
  268. package/dist/teleport.js +1 -1
  269. package/dist/{textarea-DkfGmRSI.js → textarea-BAogS_Ff.js} +1 -1
  270. package/dist/{textarea-DkfGmRSI.js.map → textarea-BAogS_Ff.js.map} +1 -1
  271. package/dist/{textarea-CNa4dSvF.cjs → textarea-CGD6lAEe.cjs} +1 -1
  272. package/dist/{textarea-CNa4dSvF.cjs.map → textarea-CGD6lAEe.cjs.map} +1 -1
  273. package/dist/textarea.cjs +1 -1
  274. package/dist/textarea.js +1 -1
  275. package/dist/{theme-CNWRYdfn.js → theme-CUK0HrS3.js} +1 -1
  276. package/dist/{theme-CNWRYdfn.js.map → theme-CUK0HrS3.js.map} +1 -1
  277. package/dist/{theme-CMyXTDht.cjs → theme-DKrrQ-ic.cjs} +1 -1
  278. package/dist/{theme-CMyXTDht.cjs.map → theme-DKrrQ-ic.cjs.map} +1 -1
  279. package/dist/{theme-button-CixloLin.js → theme-button-Bb8qW2IH.js} +1 -1
  280. package/dist/{theme-button-CixloLin.js.map → theme-button-Bb8qW2IH.js.map} +1 -1
  281. package/dist/{theme-button-kMhsX5Oe.cjs → theme-button-CmTwFm3l.cjs} +1 -1
  282. package/dist/{theme-button-kMhsX5Oe.cjs.map → theme-button-CmTwFm3l.cjs.map} +1 -1
  283. package/dist/theme-button.cjs +1 -1
  284. package/dist/theme-button.js +1 -1
  285. package/dist/theme.cjs +1 -1
  286. package/dist/theme.js +2 -2
  287. package/dist/tree.cjs +1 -1
  288. package/dist/tree.js +1 -1
  289. package/dist/typography.cjs +1 -1
  290. package/dist/typography.js +1 -1
  291. package/dist/{utils-DXE5fBBd.js.map → utils-Cxg0Kfy5.js.map} +1 -1
  292. package/dist/{utils-C-Q8ePtG.cjs.map → utils-aCJYAGUr.cjs.map} +1 -1
  293. package/dist/utils.cjs +1 -1
  294. package/dist/utils.js +1 -1
  295. package/dist/visually-hidden.cjs +1 -1
  296. package/dist/visually-hidden.js +1 -1
  297. package/dist/{window-qaGFMn_4.cjs → window-BbWlaPZv.cjs} +1 -1
  298. package/dist/{window-qaGFMn_4.cjs.map → window-BbWlaPZv.cjs.map} +1 -1
  299. package/dist/{window-BcvDNi9D.js → window-DuDAQa6y.js} +1 -1
  300. package/dist/{window-BcvDNi9D.js.map → window-DuDAQa6y.js.map} +1 -1
  301. package/dist/window.cjs +1 -1
  302. package/dist/window.js +1 -1
  303. package/package.json +1 -1
  304. package/skills/schmancy/SKILL.md +18 -2
  305. package/skills/schmancy/area.md +13 -0
  306. package/skills/schmancy/overlay.md +13 -0
  307. package/skills/schmancy/page.md +71 -29
  308. package/src/CLAUDE.md +20 -0
  309. package/src/area/area.component.ts +168 -343
  310. package/src/button/button.ts +8 -5
  311. package/src/button/icon-button.ts +8 -5
  312. package/src/chips/filter-chip.ts +1 -3
  313. package/src/details/details.ts +1 -1
  314. package/src/directives/fill.ts +137 -0
  315. package/src/directives/index.ts +2 -0
  316. package/src/directives/overflow-within.ts +186 -0
  317. package/src/form/fields/radio-group/radio-button.ts +22 -44
  318. package/src/form/fields/radio-group/radio-group.ts +20 -75
  319. package/src/form/fields/select/select.ts +3 -2
  320. package/src/index.ts +0 -1
  321. package/src/overlay/index.ts +1 -0
  322. package/src/overlay/overlay.component.ts +38 -39
  323. package/src/overlay/overlay.positioning.ts +10 -2
  324. package/src/overlay/overlay.types.ts +10 -3
  325. package/src/state/schmancy-state.html +897 -0
  326. package/src/state/schmancy-state.md +981 -0
  327. package/types/src/area/area.component.d.ts +0 -15
  328. package/types/src/button/icon-button.d.ts +1 -1
  329. package/types/src/directives/fill.d.ts +46 -0
  330. package/types/src/directives/index.d.ts +2 -0
  331. package/types/src/directives/overflow-within.d.ts +77 -0
  332. package/types/src/form/fields/radio-group/radio-button.d.ts +2 -5
  333. package/types/src/form/fields/radio-group/radio-group.d.ts +2 -10
  334. package/types/src/index.d.ts +0 -1
  335. package/types/src/overlay/index.d.ts +1 -1
  336. package/types/src/overlay/overlay.positioning.d.ts +9 -1
  337. package/types/src/overlay/overlay.types.d.ts +9 -3
  338. package/dist/area-BIipuSyO.js.map +0 -1
  339. package/dist/area-C-EMiNEE.cjs +0 -12
  340. package/dist/area-C-EMiNEE.cjs.map +0 -1
  341. package/dist/chips-D1kJrbzo.js.map +0 -1
  342. package/dist/chips-Dx_WvOGk.cjs.map +0 -1
  343. package/dist/details-Cpg8sH2F.js.map +0 -1
  344. package/dist/details-CwSDur6j.cjs.map +0 -1
  345. package/dist/mixins-DCVXqL1Q.js +0 -636
  346. package/dist/mixins-Du9HMrIG.cjs +0 -254
  347. package/dist/overlay-DG6EeyKt.cjs.map +0 -1
  348. package/dist/overlay-oxM9OLXP.js.map +0 -1
  349. package/dist/page.cjs +0 -20
  350. package/dist/page.cjs.map +0 -1
  351. package/dist/page.js +0 -74
  352. package/dist/page.js.map +0 -1
  353. package/dist/radio-group-DB9D2ZkA.js +0 -108
  354. package/dist/radio-group-DB9D2ZkA.js.map +0 -1
  355. package/dist/radio-group-dVUvYFq7.cjs +0 -40
  356. package/dist/radio-group-dVUvYFq7.cjs.map +0 -1
  357. package/dist/scroll-C1klVgSQ.js +0 -115
  358. package/dist/scroll-C1klVgSQ.js.map +0 -1
  359. package/dist/scroll-S-bXF2u6.cjs +0 -26
  360. package/dist/scroll-S-bXF2u6.cjs.map +0 -1
  361. package/dist/select-UU2pB67h.js.map +0 -1
  362. package/dist/select-fu_-rZyn.cjs +0 -56
  363. package/dist/select-fu_-rZyn.cjs.map +0 -1
  364. package/src/page/index.ts +0 -1
  365. package/src/page/page.ts +0 -137
  366. package/types/src/page/index.d.ts +0 -1
  367. package/types/src/page/page.d.ts +0 -37
  368. /package/dist/{rxjs-utils-Cs6XGwF6.js → rxjs-utils-BK8VMe3K.js} +0 -0
  369. /package/dist/{rxjs-utils-Dsj75cJy.cjs → rxjs-utils-DhOKenkS.cjs} +0 -0
  370. /package/dist/{utils-DXE5fBBd.js → utils-Cxg0Kfy5.js} +0 -0
  371. /package/dist/{utils-C-Q8ePtG.cjs → utils-aCJYAGUr.cjs} +0 -0
@@ -0,0 +1,981 @@
1
+ # Schmancy State — Complete Developer Reference
2
+
3
+ > Source of truth. `@mhmo91/schmancy/state` · `packages/schmancy/src/state/` · 2026-05-08
4
+ > Covers: foundational libraries, design patterns with mental models, full technical system design.
5
+
6
+ ---
7
+
8
+ ## How to read this document
9
+
10
+ Read **Part 1** first — it explains the two external libraries schmancy state is built on. Without understanding TC39 Signals and `@lit/context`, the design patterns are mechanisms without motivation.
11
+
12
+ Read **Part 2** to understand *why* the code is shaped the way it is — each pattern is named, given a mental model, shown visually, and grounded in actual code.
13
+
14
+ Read **Part 3** as a reference — low-level technical detail for every file, every function, every invariant.
15
+
16
+ ---
17
+
18
+ # Part 1 — Foundational Libraries
19
+
20
+ ## A. TC39 Signals — `signal-polyfill` · `@lit-labs/signals`
21
+
22
+ ### The problem
23
+
24
+ A spreadsheet: cell `C1 = A1 + B1`. When `A1` changes, `C1` recomputes automatically — without you calling "recompute C1" explicitly. JavaScript has no built-in version of this. Every reactive system that ever existed (MobX, Vue reactivity, SolidJS signals, Svelte stores) solves the same problem with the same idea: *track which computations read which values, then invalidate those computations when the values change.*
25
+
26
+ TC39 Signals standardises this mechanism. `signal-polyfill` ships it today. `@lit-labs/signals` wraps it for Lit components.
27
+
28
+ ### Three primitives
29
+
30
+ **`Signal.State<T>` — the mutable leaf**
31
+
32
+ Holds a value. Notifies dependents when replaced. This is the bottom of the graph.
33
+
34
+ ```ts
35
+ const n = new Signal.State(0)
36
+ n.get() // → 0
37
+ n.set(1) // notifies all dependents
38
+ n.get() // → 1
39
+ ```
40
+
41
+ **`Signal.Computed<T>` — the derived cell**
42
+
43
+ Lazy. Runs a callback to produce a value. Caches the result until any dependency changes. Re-runs only when `.get()` is called after a dependency was set.
44
+
45
+ ```ts
46
+ const doubled = new Signal.Computed(() => n.get() * 2)
47
+ doubled.get() // runs fn → 2
48
+ doubled.get() // cached — fn NOT run → 2
49
+ n.set(5)
50
+ doubled.get() // dependency changed — re-runs → 10
51
+ ```
52
+
53
+ **`Signal.subtle.Watcher` — the push hook**
54
+
55
+ Low-level. Fires a callback the moment any watched signal or computed *becomes dirty* (i.e. its value may have changed). This is what bridges the pull model to external push systems — RxJS observables, Lit's `requestUpdate()`.
56
+
57
+ ```ts
58
+ const w = new Signal.subtle.Watcher(() => {
59
+ // fires synchronously when any watched signal is set
60
+ w.watch() // must re-arm — Watcher disarms itself after each notification
61
+ })
62
+ w.watch(n) // start watching
63
+ ```
64
+
65
+ ### How auto-tracking works — the core mechanism
66
+
67
+ The polyfill maintains a single module-level variable: **`activeConsumer`**. When a `Computed`'s callback runs, `activeConsumer` is set to that computed. Every `signal.get()` call checks `activeConsumer` and registers itself as a dependency of the current consumer. No explicit subscribe call — **the read is the registration**.
68
+
69
+ ```
70
+ // Trace: what happens inside Computed.get()
71
+
72
+ activeConsumer = doubled_computed ← set before running the callback
73
+
74
+ doubled_computed.fn():
75
+ n.get() ← n checks activeConsumer
76
+ n._subscribers.add(doubled) ← n records doubled as a dependent
77
+ return n._value (0)
78
+
79
+ activeConsumer = null ← cleared after callback
80
+
81
+ // Later: n.set(5)
82
+ n._value = 5
83
+ doubled_computed.markDirty() ← n notifies all subscribers
84
+ doubled_computed._dirty = true
85
+ Watcher callback fires ← if a Watcher was watching doubled
86
+ ```
87
+
88
+ ### SignalWatcher — how Lit plugs in
89
+
90
+ `SignalWatcher` (from `@lit-labs/signals`) is a class mixin that intercepts Lit's `performUpdate()` and wraps it in a `Signal.Computed`. A `Signal.subtle.Watcher` watches that computed. When any signal read during the last render changes, the Watcher fires and calls `requestUpdate()`.
91
+
92
+ ```ts
93
+ // Reconstructed from minified source (signal-watcher.js)
94
+ class SignalWatcher extends LitElement {
95
+ _renderComputed = new Signal.Computed(() => {
96
+ super.performUpdate() // your render() runs inside here
97
+ })
98
+
99
+ _watcher = new Signal.subtle.Watcher(() => {
100
+ this.requestUpdate() // schedule Lit re-render
101
+ this._watcher.watch() // re-arm
102
+ })
103
+
104
+ performUpdate() {
105
+ this._watcher.watch(this._renderComputed)
106
+ this._renderComputed.get() // runs render(), auto-tracking all signal reads
107
+ }
108
+ }
109
+ ```
110
+
111
+ `SchmancyElement` already includes `SignalWatcher`. Reading `cart.value` in `render()` IS the subscription. No decorator, no setup.
112
+
113
+ ### Full cycle — trace by hand
114
+
115
+ ```
116
+ 1. const cart = new Signal.State({ items: [], total: 0 })
117
+
118
+ 2. render() runs (inside SignalWatcher's Computed)
119
+ activeConsumer = renderComputed
120
+ cart.get() → cart._subscribers.add(renderComputed)
121
+ renders "Total: 0"
122
+ activeConsumer = null
123
+
124
+ 3. cart.set({ items: ['x'], total: 12 }) — called anywhere in the app
125
+ cart._value = { items: ['x'], total: 12 }
126
+ renderComputed.markDirty()
127
+ _watcher callback fires (synchronous)
128
+ queueMicrotask(() => component.requestUpdate())
129
+
130
+ 4. microtask: Lit re-renders
131
+ renderComputed.get() → re-runs render(), re-tracks deps
132
+ renders "Total: 12" ✓
133
+ ```
134
+
135
+ ---
136
+
137
+ ## B. `@lit/context` — tree-scoped value injection
138
+
139
+ ### The problem
140
+
141
+ You have a value many nested components need — a theme, a cart, a user session. Passing it as a property through every intermediate element ("prop drilling") is brittle and coupling. **Context** is the alternative: a provider at any tree position announces a value; any descendant requests it without any intermediate element knowing.
142
+
143
+ React has `createContext / useContext`. `@lit/context` is the same idea implemented with the DOM's own event system — no framework runtime required.
144
+
145
+ ### The mechanism — one event
146
+
147
+ The entire protocol is a single custom DOM event: `"context-request"`. It bubbles up the DOM. A provider listens for it and calls the requester's callback with the value.
148
+
149
+ ```ts
150
+ // The event — from actual source (context-request-event.js)
151
+ class ContextRequestEvent extends Event {
152
+ constructor(context, contextTarget, callback) {
153
+ super('context-request', { bubbles: true, composed: true })
154
+ this.context = context // identity key (a Symbol)
155
+ this.contextTarget = contextTarget
156
+ this.callback = callback // "call me with the value"
157
+ }
158
+ }
159
+
160
+ // The provider — from context-provider.js
161
+ host.addEventListener('context-request', (e) => {
162
+ if (e.context !== this.context) return // wrong key — ignore
163
+ if (e.contextTarget === host) return // don't serve yourself
164
+ e.stopPropagation() // closest provider wins
165
+ e.callback(this.value) // deliver the value
166
+ })
167
+ ```
168
+
169
+ ### Why `Symbol.for()` is critical
170
+
171
+ Provider and consumer must use the *exact same key object*. `Symbol('x') !== Symbol('x')` — each call creates a unique symbol. `Symbol.for('x') === Symbol.for('x')` — the runtime-global registry returns the same symbol for the same string, even across separate bundle copies. Schmancy uses `Symbol.for('schmancy.state:' + namespace)` so Bundle A's provider matches Bundle B's request.
172
+
173
+ ### Full resolution — trace by hand
174
+
175
+ ```
176
+ DOM tree:
177
+ <app>
178
+ <schmancy-context .provides=${[cart]}> ← ContextProvider installed here
179
+ <checkout-page>
180
+ <cart-view> ← wants isolated copy
181
+
182
+ Step 1 — cart-view calls cart.value inside render()
183
+ resolveContextual('hannah/cart', globalFallback)
184
+ host = resolveActiveHost() → <cart-view> element
185
+
186
+ Step 2 — dispatch ContextRequestEvent from <cart-view>
187
+ let resolved
188
+ cartViewEl.dispatchEvent(new ContextRequestEvent(
189
+ Symbol.for('schmancy.state:hannah/cart'),
190
+ cartViewEl,
191
+ value => { resolved = value }
192
+ ))
193
+ // bubbles: cart-view → checkout-page → schmancy-context
194
+
195
+ Step 3 — ContextProvider on <schmancy-context> intercepts
196
+ e.context === our key? yes
197
+ e.contextTarget === host? no (it's cart-view)
198
+ e.stopPropagation() ← outer providers never see this
199
+ e.callback(isolatedCopy) ← resolved = isolatedCopy ✓
200
+
201
+ Step 4 — resolveContextual caches and returns
202
+ hostResolverCache[cartViewEl]['hannah/cart'] = isolatedCopy
203
+ cart.value → isolatedCopy.value ✓
204
+
205
+ Step 5 — same render() again
206
+ cache hit → skip event entirely → O(1) ✓
207
+ ```
208
+
209
+ **`bubbles: true`** — event travels up the DOM so a nested consumer reaches a distant provider.
210
+ **`composed: true`** — event crosses shadow DOM boundaries. Without it, shadow roots silently block the event.
211
+ **`stopPropagation()`** — closest provider wins. Outer providers never see the event.
212
+
213
+ ---
214
+
215
+ # Part 2 — Design Patterns
216
+
217
+ ## Pattern 01 — Module-Scoped Singleton
218
+
219
+ **Named pattern:** Singleton · Module system
220
+
221
+ > *"One name, one value, everywhere the module is imported."*
222
+
223
+ **Mental model:** A plain JavaScript `const` at the top of a file. ES module semantics guarantee it is evaluated once and the same object is returned to every importer. That is the singleton. The state factory makes this the *default* by producing an object that lives on the module's scope, never inside a class or function.
224
+
225
+ ```
226
+ // cart.state.ts
227
+ export const cart = state<CartState>('hannah/cart').session(initial)
228
+
229
+ ┌────────────────────┼────────────────────┐
230
+ ▼ ▼ ▼
231
+ CartView.ts CartSummary.ts CheckoutPage.ts
232
+ import {cart} import {cart} import {cart}
233
+ ← same object ─────────────────────────────────────────
234
+ ```
235
+
236
+ The singleton is not managed by a service locator or DI container. It is the JS module system itself. No registry to look up, no `getInstance()` to call — just an import.
237
+
238
+ **The one rule:** Never use `using cart = state(...)` at module scope. The `using` keyword disposes on scope exit — which for a module-level variable is never until the tab closes. Reserve `using` for test scope where you want explicit teardown.
239
+
240
+ ---
241
+
242
+ ## Pattern 02 — Transparent Proxy
243
+
244
+ **Named pattern:** Proxy · Structural
245
+
246
+ > *"The same `cart.value` call routes to a different object depending on where you are in the DOM tree — and you never know."*
247
+
248
+ **Mental model:** A receptionist who receives every call. Most of the time she routes it to the main office (module-scoped signal). But if a call comes in while the caller is sitting inside a special meeting room (`<schmancy-context>`), she routes it to the room's private whiteboard. The caller never dials a different number — they always call the receptionist — but who actually picks up changes based on physical location.
249
+
250
+ ```
251
+ DOM tree:
252
+ <app>
253
+ ├── <schmancy-context .provides=${[cart]}> ← installs isolated copy A
254
+ │ ├── <cart-view>
255
+ │ │ cart.value → isolated copy A
256
+ │ │ cart.set() → isolated copy A
257
+ │ └── <cart-summary>
258
+ │ cart.value → isolated copy A
259
+
260
+ └── <sidebar>
261
+ cart.value → module-scoped global
262
+ cart.set() → module-scoped global
263
+
264
+ Same variable. Different target. No code change in consumers.
265
+ ```
266
+
267
+ **How the proxy is built — without ES `Proxy`:**
268
+
269
+ ```ts
270
+ // Every public accessor on the global instance is a live getter
271
+ Object.defineProperty(instance, 'value', {
272
+ get: () => {
273
+ const target = resolveContextual(namespace, isolatedTarget)
274
+ return target.value // reads from whichever target resolved
275
+ }
276
+ })
277
+
278
+ // Every write method also routes through resolveContextual
279
+ instance['set'] = (...args) => {
280
+ const target = resolveContextual(namespace, isolatedTarget)
281
+ return target['set'](...args)
282
+ }
283
+ ```
284
+
285
+ `Object.defineProperty` live getters are used instead of `ES Proxy` because the interception needs to know the *caller's DOM position*, which requires the active-host tracking system (Pattern 06) as an orthogonal concern.
286
+
287
+ ---
288
+
289
+ ## Pattern 03 — Adapter / Storage Port
290
+
291
+ **Named pattern:** Adapter · Ports & Adapters · Strategy
292
+
293
+ > *"The state logic never touches a storage API directly. It talks to a uniform interface, and the backend is swapped by configuration."*
294
+
295
+ **Mental model:** A document that needs to be filed. The document doesn't care whether the filing cabinet is in the same room (memory), the local office (sessionStorage), an office that survives overnight (localStorage), or a central warehouse that never forgets (IndexedDB). It hands the document to a filing clerk. The interface to the clerk is always the same: *file this, retrieve this, destroy this*.
296
+
297
+ ```ts
298
+ interface StorageAdapter<T> {
299
+ load(): Promise<T | null>
300
+ save(value: T): Promise<void>
301
+ clear(): Promise<void>
302
+ close?(): Promise<void> // IDB only
303
+ }
304
+
305
+ // Four implementations, one interface:
306
+ MemoryAdapter → .memory() JS heap, no I/O
307
+ WebStorageAdapter → .local() localStorage
308
+ WebStorageAdapter → .session() sessionStorage
309
+ IndexedDBAdapter → .idb() IndexedDB 'SchmancyState'/'states'
310
+ ```
311
+
312
+ **The Map/Set JSON tunnel** (a nested adapter inside `WebStorageAdapter`):
313
+ `localStorage` only stores strings. `Map` and `Set` aren't JSON-serialisable by default. A custom replacer/reviver pair handles the translation:
314
+
315
+ ```ts
316
+ // On save:
317
+ Map → { $kind: '__schmancy_state_Map', entries: [[k, v], …] }
318
+ Set → { $kind: '__schmancy_state_Set', values: [v, …] }
319
+ // On load: reviver reconstructs. IDB stores native objects — no tunnel needed.
320
+ ```
321
+
322
+ Adding a new backend (e.g. OPFS) requires one new class implementing `StorageAdapter<T>` and one case in `createAdapter()`. Nothing else changes.
323
+
324
+ ---
325
+
326
+ ## Pattern 04 — Dual Observer (Signal + Observable)
327
+
328
+ **Named pattern:** Observer · Pull + Push · Bridge
329
+
330
+ > *"One value, two reactive surfaces — pull (signals) for the render loop, push (Observables) for pipelines."*
331
+
332
+ **Mental model:** Signals are a thermometer on the wall. You glance at it whenever you need the temperature — you *pull* the value. Observables are like weather alerts — the system *pushes* a notification at the moment something changes. Both surfaces sit over the same measurement.
333
+
334
+ ```
335
+ Signal.State<T>
336
+ signal.set(next) ← writes
337
+
338
+ ┌───────────────┼───────────────────────┐
339
+ ▼ ▼
340
+ PULL SURFACE PUSH SURFACE
341
+ signal.get() / state.value Observable<T> / state.$
342
+ (TC39 signals) (RxJS)
343
+ │ │
344
+ SignalWatcher auto-tracks Signal.subtle.Watcher
345
+ reads inside render() + queueMicrotask bridge
346
+ │ │
347
+ component.requestUpdate() subscriber.next(signal.get())
348
+ ```
349
+
350
+ **The bridge — how Signal becomes Observable:**
351
+
352
+ ```ts
353
+ function signalToObservable<T>(signal): Observable<T> {
354
+ return new Observable(subscriber => {
355
+ subscriber.next(signal.get()) // ① immediate initial emission
356
+ let scheduled = false
357
+ const watcher = new Signal.subtle.Watcher(() => { // ② fires on signal.set()
358
+ if (scheduled) return
359
+ scheduled = true
360
+ queueMicrotask(() => {
361
+ scheduled = false
362
+ if (subscriber.closed) return
363
+ subscriber.next(signal.get()) // ③ push latest, coalesced
364
+ watcher.watch(signal) // ④ re-arm
365
+ })
366
+ })
367
+ watcher.watch(signal)
368
+ return () => watcher.unwatch(signal) // ⑤ cleanup on unsubscribe
369
+ })
370
+ }
371
+ ```
372
+
373
+ Use **signals / `.value`** when: reading inside `render()`, feeding `computed()`, running `effect()`, needing synchronous access.
374
+ Use **Observable / `.$`** when: composing with other streams (`combineLatest`, `switchMap`), debouncing/throttling, piping with `takeUntil(this.disconnecting)`.
375
+
376
+ ---
377
+
378
+ ## Pattern 05 — Type-Driven Command Dispatch
379
+
380
+ **Named pattern:** Command · Strategy · Discriminated Union
381
+
382
+ > *"The shape of your data determines which write commands exist. TypeScript's type system makes the wrong operation inexpressible."*
383
+
384
+ **Mental model:** A chef's knife block. The shape of the slot tells you exactly which knife fits. If you have a `Map`, you get `set(k,v)`, `delete(k)`, `clear()`. You cannot accidentally call `push()` on a Map — the type system prevents it.
385
+
386
+ ```
387
+ T = Map<K,V> → Kind = 'map' → MapAPI<T>
388
+ T = Set<U> → Kind = 'set' → SetAPI<T>
389
+ T = U[] → Kind = 'array' → ArrayAPI<T>
390
+ T = string|number|… → Kind = 'prim' → ScalarAPI<T>
391
+ T = Foo | null → Kind = 'null' → ScalarAPI<T>
392
+ T = { … } → Kind = 'obj' → ObjectAPI<T>
393
+
394
+ const sel = state<Set<string>>('ui/sel').memory(new Set())
395
+ sel.add('id') ✓
396
+ sel.toggle('id') ✓
397
+ sel.push('id') ✗ TypeScript error — push does not exist on SetAPI
398
+ ```
399
+
400
+ **All writes funnel into one `commit()` function:**
401
+
402
+ ```ts
403
+ const commit = (next: unknown): void => {
404
+ internal.signal.set(next) // synchronous — visible immediately
405
+ scheduleWrite(internal) // persist via adapter, microtask-debounced
406
+ }
407
+
408
+ // Every write method is syntactic sugar over commit():
409
+ // ObjectAPI:
410
+ set(patch, merge = true) { commit(merge ? {...current, ...patch} : patch) }
411
+ update(recipe) { commit(produce(current, recipe)) } // immer
412
+
413
+ // SetAPI:
414
+ toggle(value) {
415
+ const next = new Set(current)
416
+ next.has(value) ? next.delete(value) : next.add(value)
417
+ commit(next)
418
+ }
419
+ ```
420
+
421
+ **Immutability by construction:** every write produces a new value — spread for objects, `new Map(current)` for maps, `new Set(current)` for sets, spread for arrays, or immer's `produce()` for recipes. Nothing mutates in place. Reference inequality = change.
422
+
423
+ ---
424
+
425
+ ## Pattern 06 — Zone / Execution Context Propagation
426
+
427
+ **Named pattern:** Zone · AsyncContext polyfill · Ambient state
428
+
429
+ > *"Knowing which DOM element is 'currently running code' — even across async boundaries — without passing it as a parameter."*
430
+
431
+ **Mental model:** A call centre where every agent wears a badge saying which client file they are working on. The badge tells the system which client's records to look up. Zone.js is the classic implementation. Schmancy implements a minimal version — a per-call-stack "badge" identifying which DOM element is currently executing — using a global stack and a Promise patch.
432
+
433
+ **Four-tier fallback chain:**
434
+
435
+ ```
436
+ Tier 1: Explicit stack
437
+ _activeHost.run(host, fn): pushes host, calls fn, pops on exit
438
+ SchmancyElement prototype-wrap calls .run(this, fn) around every
439
+ concrete method: render(), connectedCallback(), firstUpdated(),
440
+ class methods called from any of those.
441
+
442
+ Tier 2: Promise.prototype.then patch
443
+ Captures stack head at .then() chain-time; restores when callback fires.
444
+ fetch('/api').then(data => { cart.update(…) }) ← works ✓
445
+ async method: const data = await fetch()
446
+ cart.update(…) ← does NOT work (V8 opt) ✗
447
+
448
+ Tier 3: Event-host slot
449
+ <schmancy-context> installs capture-phase listeners for 16 event types.
450
+ When a DOM event fires, it publishes the deepest HTMLElement as host.
451
+ Slot self-clears via queueMicrotask.
452
+ <button @click=${() => cart.set(…)}> ← works ✓
453
+
454
+ Tier 4: document.activeElement
455
+ Keyboard / focus handlers.
456
+
457
+ Tier 5 (implicit): undefined → module-scoped global
458
+ ```
459
+
460
+ **The Promise patch:**
461
+
462
+ ```ts
463
+ const _origThen = Promise.prototype.then
464
+ Promise.prototype.then = function(onfulfilled, onrejected) {
465
+ const captured = _stack[_stack.length - 1] // capture NOW at chain time
466
+ const wrapFulfilled = v => {
467
+ _stack.push(captured) // restore at CALLBACK time
468
+ try { return onfulfilled(v) }
469
+ finally { _stack.pop() }
470
+ }
471
+ return _origThen.call(this, wrapFulfilled, wrapRejected)
472
+ }
473
+ ```
474
+
475
+ **Known limitation:** V8's native `await` optimization (since v7.x) bypasses `Promise.resolve(x).then(continuation)` — the patch never fires for `await`-resumed continuations. Mutations after the first `await` fall back to the module-scoped global. Fix: keep mutations before the `await`, or chain explicitly with `.then()`.
476
+
477
+ ---
478
+
479
+ ## Pattern 07 — Provider / Consumer (Context Protocol)
480
+
481
+ **Named pattern:** Context · @lit/context · Tree-scoped DI
482
+
483
+ > *"A provider in the DOM tree answers requests from consumers below it, without knowing who they are."*
484
+
485
+ **Mental model:** React Context. A `<Provider value={x}>` wraps a subtree; any `useContext()` inside it receives `x`. `<schmancy-context>` is the same idea for Web Components, implemented with DOM events instead of a VDOM tree walk.
486
+
487
+ ```
488
+ Consumer dispatches:
489
+ new ContextRequestEvent(
490
+ Symbol.for('schmancy.state:hannah/cart'),
491
+ consumerElement,
492
+ value => { resolved = value }
493
+ )
494
+ // bubbles up the DOM
495
+
496
+ Provider (<schmancy-context>) intercepts:
497
+ ContextProvider listens for 'context-request'
498
+ checks: event.context === our key?
499
+ yes → event.callback(isolated_copy)
500
+ event.stopPropagation() ← closest provider wins
501
+
502
+ Nested:
503
+ <schmancy-context .provides=${[cart]}> ← outer
504
+ <schmancy-context .provides=${[cart]}> ← inner
505
+ <cart-view>
506
+ cart.value → inner's copy ✓ closest wins
507
+ ```
508
+
509
+ **`<schmancy-context>` on connect:**
510
+
511
+ ```ts
512
+ for (const tmpl of this.provides) {
513
+ const isolated = tmpl._isolatedInstance()
514
+ // ^ createInstance({ storage: 'memory', initial: tmpl.value }, { isolated: true })
515
+ // ^ seeded from global's current value; always memory-backed
516
+ const ctx = createContext(Symbol.for('schmancy.state:' + tmpl.namespace))
517
+ const provider = new ContextProvider(this, { context: ctx, initialValue: isolated })
518
+ this._scoped.push({ isolated, provider })
519
+ }
520
+ // + capture-phase listeners for 16 event types → Tier 3 of active-host chain
521
+ ```
522
+
523
+ **On disconnect:** `isolated.destroy()` for each — flushes pending writes, releases namespace claim.
524
+
525
+ Isolated copies are **always memory-backed** — they share the element's lifetime, never persist to localStorage/sessionStorage/IDB.
526
+
527
+ ---
528
+
529
+ ## Pattern 08 — Prototype Decoration (ReactiveController)
530
+
531
+ **Named pattern:** Decorator · ReactiveController · Template Method
532
+
533
+ > *"Attach subscription lifecycle to a component's class without modifying the class body."*
534
+
535
+ **Mental model:** A Decorator Pattern attaches new behaviour to an object without changing its source. Lit's `addInitializer` is the hook. The `@observe` decorator and `bindState` both use `ReactiveController` as the attachment — a standardised plugin interface: *tell me when the host connects and disconnects, and I'll wire subscriptions.*
536
+
537
+ **Three binding options — decreasing magic:**
538
+
539
+ ```ts
540
+ // Option 1: Direct render() read — SignalWatcher auto-tracks (zero code)
541
+ class CartView extends SchmancyElement {
542
+ render() { return html`${cart.value.items.length}` }
543
+ }
544
+
545
+ // Option 2: @observe — field updated on each emission
546
+ class CartView extends SchmancyElement {
547
+ @observe(cart) cart!: CartState
548
+ onClick() { console.log(this.cart) } // safe in event handlers
549
+ }
550
+
551
+ // Option 3: bindState — same controller, no decorator
552
+ class CustomHost extends LitElement { // not a SchmancyElement
553
+ cart = bindState(this, cart)
554
+ render() { return html`${this.cart.value.items.length}` }
555
+ }
556
+ ```
557
+
558
+ **How `@observe` works — two hooks:**
559
+
560
+ ```ts
561
+ export function observe<T>(source) {
562
+ return function(proto, propertyKey) {
563
+ const storageKey = Symbol(`__observe_${propertyKey}`)
564
+
565
+ // Hook 1: per-PROTOTYPE accessor (runs on every instance via prototype chain)
566
+ Object.defineProperty(proto, propertyKey, {
567
+ get(this) { return this[storageKey] ?? source.value }, // latest or fallback
568
+ set(_) { console.warn('@observe: read-only') },
569
+ })
570
+
571
+ // Hook 2: per-INSTANCE subscription (runs at construction via addInitializer)
572
+ proto.constructor.addInitializer(host => {
573
+ let sub
574
+ host.addController({
575
+ hostConnected() { sub = source.$.subscribe(v => { host[storageKey] = v; host.requestUpdate() }) },
576
+ hostDisconnected() { sub?.unsubscribe() },
577
+ })
578
+ })
579
+ }
580
+ }
581
+ ```
582
+
583
+ `ReactiveController` is Lit's **Template Method** pattern: the framework defines *when* (`hostConnected`, `hostDisconnected`); the controller fills in the *what*.
584
+
585
+ ---
586
+
587
+ ## Pattern 09 — Microtask Debounce Gate
588
+
589
+ **Named pattern:** Debounce · Event coalescing
590
+
591
+ > *"Many writes in one synchronous task collapse to one storage flush — the gate only lets the last one through."*
592
+
593
+ **Mental model:** A postal worker who collects letters all day but makes one trip to the post office at day's end. Ten letters in the outbox → one trip carries all ten. The debounce works at JS-microtask granularity: many `set()`/`update()` calls in the same synchronous task collapse to one `adapter.save()`, which always carries the *latest* value.
594
+
595
+ ```
596
+ cart.set({ total: 10 }) → signal updated immediately ✓
597
+ cart.set({ total: 20 }) → signal updated immediately ✓
598
+ cart.set({ total: 30 }) → signal updated immediately ✓
599
+
600
+ scheduleWrite():
601
+ if (scheduledWrite) return ← 2nd and 3rd calls exit here
602
+ scheduledWrite = true
603
+ queueMicrotask(() => {
604
+ scheduledWrite = false
605
+ adapter.save(signal.get()) ← reads 30, the latest value
606
+ })
607
+
608
+ Result: one adapter.save(30) — not three.
609
+
610
+ Timeline:
611
+ sync task │ set(10) set(20) set(30) │microtask│ save(30) │
612
+ │ signal always current │boundary │ one I/O │
613
+ ```
614
+
615
+ **Two microtask drains on dispose** (to guarantee the latest value is flushed):
616
+
617
+ ```ts
618
+ async flushAndClose() {
619
+ if (internal.pendingWrite) await internal.pendingWrite // ① in-flight save
620
+ await new Promise(resolve => queueMicrotask(resolve)) // ② let scheduled task run
621
+ if (internal.pendingWrite) await internal.pendingWrite // ③ catch write from ②
622
+ if (adapter.close) await adapter.close() // ④ IDB close
623
+ }
624
+ ```
625
+
626
+ ---
627
+
628
+ ## Pattern 10 — RAII Lifecycle
629
+
630
+ **Named pattern:** RAII · Symbol.dispose · TC39 Explicit Resource Management
631
+
632
+ > *"Acquire the resource when you create the object; release it automatically when the object goes out of scope — even if an exception is thrown."*
633
+
634
+ **Mental model:** C++ RAII, now in JavaScript via TC39. `using x = acquireResource()` guarantees `x[Symbol.dispose]()` at block exit — even on thrown exceptions. No `try/finally`, no `afterEach(() => cleanup())`.
635
+
636
+ ```ts
637
+ // WITHOUT RAII — easy to forget cleanup
638
+ it('test', () => {
639
+ const cart = state('test/cart').memory({}) // plain const
640
+ cart.set({ total: 10 })
641
+ expect(cart.value.total).toBe(10)
642
+ // forgot to call cart.destroy()
643
+ // namespace 'test/cart' is permanently claimed
644
+ // next test: state('test/cart') throws or returns stale instance
645
+ })
646
+
647
+ // WITH RAII — cleanup is structural
648
+ it('test', () => {
649
+ using cart = state('test/cart').memory({})
650
+ cart.set({ total: 10 })
651
+ expect(cart.value.total).toBe(10)
652
+ // [Symbol.dispose]() called automatically here — even if expect() threw
653
+ }) // namespace released, next test starts clean
654
+
655
+ // IDB — async variant
656
+ it('test', async () => {
657
+ await using cart = state('test/cart').idb({})
658
+ // [Symbol.asyncDispose]() awaited on exit — IDB fully closed before next test
659
+ })
660
+ ```
661
+
662
+ **What dispose does:**
663
+
664
+ ```ts
665
+ [Symbol.dispose]():
666
+ if (disposed) return // idempotent
667
+ disposed = true
668
+ void flushAndClose() // fire-and-forget (sync backends)
669
+ claimed.delete(namespace) // releases namespace → re-registrable
670
+
671
+ [Symbol.asyncDispose]() (IDB only):
672
+ if (disposed) return
673
+ disposed = true
674
+ await flushAndClose() // fully awaited
675
+ claimed.delete(namespace)
676
+ ```
677
+
678
+ **Module-scope footgun:** `using` at module scope disposes when the module record is GC'd — effectively never. Module-scope state uses plain `const`.
679
+
680
+ ---
681
+
682
+ ## Pattern 11 — Flyweight / Global Registry
683
+
684
+ **Named pattern:** Flyweight · Global registry · Module Federation
685
+
686
+ > *"Share one instance across all importers — even across separate bundle copies — by parking it in a key-value store keyed off a process-global Symbol."*
687
+
688
+ **Mental model:** The Flyweight pattern avoids creating many copies of the same object by sharing one canonical instance. In a browser app with Module Federation, two separate bundles (host and remote) each import `schmancy/state`. Without coordination, each bundle creates its own `claimed` Set and `instances` Map. They produce different `cart` objects — writing through Bundle A doesn't update Bundle B's signal. Fix: park everything on `globalThis` under `Symbol.for()` keys so all bundles share the same store.
689
+
690
+ ```
691
+ // WITHOUT globalThis registry (broken)
692
+ Bundle A: claimed: Set, instances: Map, cart₁ (signal A)
693
+ Bundle B: claimed: Set, instances: Map, cart₂ (signal B) ← different!
694
+ cart₁.set({x:1}) → cart₂.value === {} ← B never saw the write
695
+
696
+ // WITH globalThis registry (correct)
697
+ globalThis[Symbol.for('schmancy.state.claimed')] = Set<string>
698
+ globalThis[Symbol.for('schmancy.state.instances')] = Map<'ns@storage', instance>
699
+ globalThis[Symbol.for('schmancy.state.hostResolverCache')]= WeakMap
700
+ globalThis[Symbol.for('schmancy.state.activeHost.stack')] = Array
701
+ globalThis[Symbol.for('schmancy.state.activeHost.eventHost')] = slot
702
+
703
+ Bundle A loads first → creates all five structures
704
+ Bundle B loads later → finds existing structures, reuses them
705
+ cart₁.set({x:1}) → cart₂.value === {x:1} ✓
706
+ ```
707
+
708
+ **The idempotent make pattern:**
709
+
710
+ ```ts
711
+ // ??= operator: "assign only if not already set"
712
+ __claimedSlot[CLAIMED_KEY] ??= new Set<string>()
713
+
714
+ // makeHandle: create-or-return
715
+ const ensure = (storage) => (initial) => {
716
+ const key = `${namespace}@${storage}`
717
+ const cached = instances.get(key)
718
+ if (cached !== undefined) return cached // second caller gets same instance
719
+ const instance = createInstance({ namespace, initial, storage })
720
+ instances.set(key, instance)
721
+ return instance
722
+ }
723
+ ```
724
+
725
+ ---
726
+
727
+ ## Pattern 12 — How the Patterns Assemble
728
+
729
+ Each pattern solves a specific problem. They compose so each layer's output is the next layer's input.
730
+
731
+ ```
732
+ ┌──────────────────────────────────────────────────────────────────┐
733
+ │ Consumer layer (what you write) │
734
+ │ cart.value / cart.set() / cart.$ / await cart.ready │
735
+ │ @observe(cart) / bindState(this, cart) / computed(...) │
736
+ ├──────────────────────────────────────────────────────────────────┤
737
+ │ Pattern 08 Prototype Decoration │
738
+ │ @observe and bindState attach ReactiveControllers to Lit hosts │
739
+ ├──────────────────────────────────────────────────────────────────┤
740
+ │ Pattern 02 Transparent Proxy │
741
+ │ Every read/write on the global routes through resolveContextual │
742
+ ├──────────────────────────────────────────────────────────────────┤
743
+ │ Pattern 06 Zone / Execution Context │
744
+ │ resolveActiveHost() answers "who is calling right now?" │
745
+ │ stack / Promise.then patch / event-host slot / activeElement │
746
+ ├──────────────────────────────────────────────────────────────────┤
747
+ │ Pattern 07 Provider / Consumer (Context Protocol) │
748
+ │ ContextRequestEvent dispatched from host — nearest wins │
749
+ ├──────────────────────────────────────────────────────────────────┤
750
+ │ Pattern 01 Module-Scoped Singleton (the fallback) │
751
+ │ If no context provider found — use the module-scoped global │
752
+ ├──────────────────────────────────────────────────────────────────┤
753
+ │ Pattern 04 Dual Observer (Signal + Observable) │
754
+ │ Signal for pull (Lit render), Observable for push (RxJS) │
755
+ ├──────────────────────────────────────────────────────────────────┤
756
+ │ Pattern 05 Type-Driven Command Dispatch │
757
+ │ Kind<T> routes to MapAPI / SetAPI / ArrayAPI / ObjectAPI │
758
+ │ All commits go through one commit(next) function │
759
+ ├──────────────────────────────────────────────────────────────────┤
760
+ │ Pattern 09 Microtask Debounce Gate │
761
+ │ Many writes collapse to one adapter.save() per microtask │
762
+ ├──────────────────────────────────────────────────────────────────┤
763
+ │ Pattern 03 Adapter / Storage Port │
764
+ │ Memory / WebStorage / IndexedDB behind uniform load/save/clear │
765
+ ├──────────────────────────────────────────────────────────────────┤
766
+ │ Pattern 10 RAII Lifecycle │
767
+ │ Symbol.dispose / Symbol.asyncDispose — flush + release + close │
768
+ ├──────────────────────────────────────────────────────────────────┤
769
+ │ Pattern 11 Flyweight / Global Registry │
770
+ │ globalThis[Symbol.for(...)] shares instances across bundles │
771
+ └──────────────────────────────────────────────────────────────────┘
772
+ ```
773
+
774
+ | Pattern | Problem it solves | Alternative rejected |
775
+ |---|---|---|
776
+ | 01 Singleton | State survives component remounts | Class-instance state, Redux |
777
+ | 02 Transparent Proxy | Consumer code unchanged whether scoped or global | Props, different variable name per scope |
778
+ | 03 Adapter | Swap storage backend without touching state logic | Hardcoded localStorage calls |
779
+ | 04 Dual Observer | Both Lit (pull) and RxJS (push) need reactivity | RxJS-only, signals-only |
780
+ | 05 Command Dispatch | Ergonomic writes per shape, no type casting | One generic `setState()` |
781
+ | 06 Zone | Know which DOM element is executing, across async | Pass host as parameter, Zone.js (~50 KB) |
782
+ | 07 Provider/Consumer | Subtree-scoped state, no consumer code changes | React context, custom event protocol |
783
+ | 08 Prototype Decoration | State bound to field with lifecycle guarantees | Manual subscribe/unsubscribe per component |
784
+ | 09 Debounce Gate | Burst writes → one I/O call | Write on every set(), batch manually |
785
+ | 10 RAII | Guaranteed cleanup even on test failure | afterEach(), try/finally |
786
+ | 11 Flyweight | One instance across bundle copies | Module-level singleton (breaks MF) |
787
+
788
+ ---
789
+
790
+ # Part 3 — Technical Reference
791
+
792
+ ## File map
793
+
794
+ ```
795
+ packages/schmancy/src/state/
796
+ index.ts — factory, types, write APIs, context resolution,
797
+ observe decorator, bindState, stateFromObservable, effect
798
+ persist.ts — StorageAdapter interface + four implementations
799
+ active-host.ts — AsyncContext polyfill (stack + Promise.then patch + event-host slot)
800
+ schmancy-context.ts — <schmancy-context> element (scoping primitive)
801
+ ```
802
+
803
+ ## Factory — three TypeScript overloads
804
+
805
+ ```ts
806
+ // Overload A — registry augmentation (typo-safe, autocomplete)
807
+ declare module '@mhmo91/schmancy/state' {
808
+ interface SchmancyStateRegistry { 'hannah/cart': CartState }
809
+ }
810
+ const cart = state('hannah/cart').session({ items: [], total: 0 })
811
+
812
+ // Overload B — explicit T arg (inline literals)
813
+ const cart = state<CartState>('hannah/cart').session({ items: [], total: 0 })
814
+
815
+ // Overload C — typed const (T inferred, no cast needed)
816
+ const initial: CartState = { items: [], total: 0 }
817
+ const cart = state('hannah/cart').session(initial)
818
+ ```
819
+
820
+ All return a `NamespaceHandle` with four backend methods. Each returns `State<NS, T, StorageBackend>` composed of `BaseAPI + WriteAPI<T>`.
821
+
822
+ **Namespace constraint:** `FeatureNamespace = \`${string}/${string}\`` — enforced at compile time and runtime.
823
+
824
+ ## Instance construction (`createInstance`)
825
+
826
+ ```
827
+ createInstance({ namespace, initial, storage }, { isolated? })
828
+
829
+ ├─ createAdapter(storage, namespace) → StorageAdapter
830
+ ├─ new Signal.State<T>(initial) → TC39 signal
831
+ ├─ adapter.load() → signal.set(stored) if not null
832
+ │ .then(markLoaded) → loaded = true; ready resolves
833
+ ├─ signalToObservable(signal) → Observable via Signal.subtle.Watcher
834
+ └─ buildWriteApi(internal, detectKind(initial))→ variant write methods
835
+ ```
836
+
837
+ Global instances route all reads/writes through `resolveContextual`. Isolated instances (`{ isolated: true }`) read/write their own signal directly — no context resolution, no recursion.
838
+
839
+ ## Storage backends
840
+
841
+ | Method | Backing | Survives refresh | Survives close |
842
+ |---|---|---|---|
843
+ | `.memory(initial)` | JS heap | ❌ | ❌ |
844
+ | `.session(initial)` | `sessionStorage` | ✅ | ❌ (per-tab) |
845
+ | `.local(initial)` | `localStorage` | ✅ | ✅ |
846
+ | `.idb(initial)` | IndexedDB | ✅ | ✅ |
847
+
848
+ IDB additionally implements `AsyncDisposable` (`[Symbol.asyncDispose]`).
849
+
850
+ ## Context resolution (`resolveContextual`)
851
+
852
+ ```
853
+ resolveContextual(namespace, fallback):
854
+ 1. host = resolveActiveHost() — may return undefined
855
+ 2. if undefined → return fallback — module-scoped global
856
+ 3. check hostResolverCache[host][namespace]
857
+ → cache hit → return immediately (O(1))
858
+ 4. dispatch ContextRequestEvent from host
859
+ → provider responds → resolved = isolated copy
860
+ → no response → resolved = fallback
861
+ 5. cache result in hostResolverCache[host][namespace]
862
+ 6. return resolved
863
+ ```
864
+
865
+ `hostResolverCache` is a `WeakMap<HTMLElement, Map<string, unknown>>` on `globalThis`.
866
+
867
+ ## Active-host resolution chain
868
+
869
+ ```
870
+ resolveActiveHost():
871
+ 1. _activeHost.get() ← explicit stack (SchmancyElement prototype-wrap)
872
+ 2. _eventHostSlot.host ← capture-phase event on <schmancy-context>
873
+ 3. document.activeElement ← keyboard / focus
874
+ 4. undefined ← caller uses module-scoped global
875
+ ```
876
+
877
+ ## Component binding options
878
+
879
+ | Option | When to use | Mechanism |
880
+ |---|---|---|
881
+ | `cart.value` in `render()` | Default — 80% of cases | SignalWatcher auto-tracking |
882
+ | `@observe(cart) field!: T` | Need value as class field (handlers, devtools) | addInitializer + ReactiveController |
883
+ | `bindState(this, cart)` | Non-SchmancyElement Lit host | ReactiveController directly |
884
+
885
+ ## `computed()`, `effect()`, `stateFromObservable()`
886
+
887
+ **`computed(fn)`** — re-export of `@lit-labs/signals/computed`. Reading `state.value` inside the callback auto-tracks it. Reference-equality dedup by default.
888
+
889
+ **`effect(fn)`** — runs `fn` immediately (registers deps), then re-runs (microtask-coalesced) whenever any read signal changes. Returns `Disposable`.
890
+
891
+ **`stateFromObservable(obs$, namespace, initial)`** — creates a state, subscribes to the observable, calls `signal.set(value)` directly on each emission (bypasses `resolveContextual`). Wraps `[Symbol.dispose]` to also unsubscribe.
892
+
893
+ ## Lifecycle — disposal sequence
894
+
895
+ ```
896
+ using cart = state('test/x').memory(initial)
897
+
898
+ └── [Symbol.dispose]() at block exit
899
+ if (disposed) return
900
+ disposed = true
901
+ void flushAndClose()
902
+ await pendingWrite
903
+ await queueMicrotask ← drain scheduled write
904
+ await pendingWrite ← catch write from microtask
905
+ await adapter.close() ← IDB only
906
+ claimed.delete(namespace) ← namespace re-registrable
907
+ ```
908
+
909
+ ## Key invariants
910
+
911
+ | Invariant | Where enforced |
912
+ |---|---|
913
+ | Namespace must contain `/` | TypeScript template literal + runtime throw |
914
+ | One global per namespace | `claimed` Set + `instances` Map on `globalThis` |
915
+ | Context dispatch is O(1) after first | `hostResolverCache` WeakMap |
916
+ | Isolated copy never calls `resolveContextual` | `{ isolated: true }` branch |
917
+ | Write persists async, never blocks signal | `scheduleWrite` → `queueMicrotask` |
918
+ | `Signal.State.set()` is synchronous | TC39 signal polyfill contract |
919
+ | Multiple bundle copies share one registry | All global state under `Symbol.for(...)` |
920
+ | No in-place mutation | Spread / `new Map()` / `new Set()` / immer `produce()` |
921
+
922
+ ## Full data-flow diagram
923
+
924
+ ```
925
+ User calls cart.set({ total: 12 })
926
+
927
+
928
+ resolveContextual(namespace, isolatedTarget)
929
+
930
+ ├─ resolveActiveHost()
931
+ │ ├─ 1. _activeHost stack (SchmancyElement prototype-wrap)
932
+ │ ├─ 2. _eventHostSlot (capture-phase event)
933
+ │ ├─ 3. document.activeElement
934
+ │ └─ 4. undefined → use isolatedTarget (module global)
935
+
936
+ ├─ cache hit? → return cached target
937
+
938
+ └─ dispatch ContextRequestEvent from host
939
+ ├─ <schmancy-context> responds → isolated copy
940
+ └─ no response → module-scoped global
941
+
942
+
943
+ target.set({ total: 12 })
944
+
945
+ ObjectAPI.set(patch, merge=true)
946
+ commit({ ...current, ...patch })
947
+
948
+ ┌─────────────┴──────────────────────┐
949
+ ▼ ▼
950
+ signal.set(next) scheduleWrite(internal)
951
+ (synchronous) │
952
+ │ queueMicrotask
953
+ │ │
954
+ ▼ ▼
955
+ Signal.subtle.Watcher fires adapter.save(signal.get())
956
+ │ (localStorage / IDB / …)
957
+
958
+ signalToObservable: queueMicrotask → emit
959
+
960
+ ┌──────┴────────────────────────┐
961
+ ▼ ▼
962
+ SignalWatcher (Lit) RxJS subscribers
963
+ → requestUpdate() → re-render pipe(takeUntil(disconnecting))
964
+ ```
965
+
966
+ ## Quick-decision guide
967
+
968
+ | I need… | Use |
969
+ |---|---|
970
+ | State in a Lit component template | `cart.value` in `render()` |
971
+ | State as a class field (event handlers) | `@observe(cart) cart!: CartState` |
972
+ | State in a non-SchmancyElement Lit host | `bindState(this, cart)` |
973
+ | Derived / computed value | `computed(() => cart.value.items.length)` |
974
+ | Side effect that reruns on change | `effect(() => { … cart.value … })` |
975
+ | Lift an Observable into state | `stateFromObservable(obs$, 'ns/key', initial)` |
976
+ | Isolated subtree copy | `<schmancy-context .provides=${[cart]}>` |
977
+ | Per-test isolation | `using cart = state('test/x').memory(initial)` |
978
+ | Persist across page refresh (same tab) | `.session(initial)` |
979
+ | Persist across tab close | `.local(initial)` or `.idb(initial)` |
980
+ | Large collections (>100 entries) | `.idb(initial)` |
981
+ | Wait for async hydration | `await cart.ready` / `if (cart.loaded)` |