@mhmo91/schmancy 0.10.19 → 0.10.20

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 (326) hide show
  1. package/custom-elements.json +0 -13
  2. package/dist/agent/{overlay.confirm-body-D3jQyXgA.js → overlay.confirm-body-mYm0zq4k.js} +1 -1
  3. package/dist/agent/{overlay.confirm-body-D3jQyXgA.js.map → overlay.confirm-body-mYm0zq4k.js.map} +1 -1
  4. package/dist/agent/schmancy.agent.js +808 -882
  5. package/dist/agent/schmancy.agent.js.map +1 -1
  6. package/dist/agent/schmancy.manifest.json +1 -9
  7. package/dist/area-C7MNn-3e.cjs +12 -0
  8. package/dist/area-C7MNn-3e.cjs.map +1 -0
  9. package/dist/{area-BIipuSyO.js → area-CRe41aIG.js} +91 -130
  10. package/dist/area-CRe41aIG.js.map +1 -0
  11. package/dist/area.cjs +1 -1
  12. package/dist/area.js +1 -1
  13. package/dist/{autocomplete-Mrb3koUN.js → autocomplete-CRDFL4Ul.js} +2 -2
  14. package/dist/{autocomplete-Mrb3koUN.js.map → autocomplete-CRDFL4Ul.js.map} +1 -1
  15. package/dist/{autocomplete-B8CE5vGw.cjs → autocomplete-CqUl7o0e.cjs} +1 -1
  16. package/dist/{autocomplete-B8CE5vGw.cjs.map → autocomplete-CqUl7o0e.cjs.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-OatK_MGh.cjs → boat-BHV5kOlN.cjs} +1 -1
  24. package/dist/{boat-OatK_MGh.cjs.map → boat-BHV5kOlN.cjs.map} +1 -1
  25. package/dist/{boat-CNWIQPA1.js → boat-XajM8A3M.js} +1 -1
  26. package/dist/{boat-CNWIQPA1.js.map → boat-XajM8A3M.js.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-CMKX4oQf.cjs → busy-BlBZ5ZOs.cjs} +1 -1
  32. package/dist/{busy-CMKX4oQf.cjs.map → busy-BlBZ5ZOs.cjs.map} +1 -1
  33. package/dist/{busy-Cetzws-m.js → busy-D8YsqVBf.js} +1 -1
  34. package/dist/{busy-Cetzws-m.js.map → busy-D8YsqVBf.js.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-D2k3dRL0.js → card-C9TljY2Z.js} +1 -1
  42. package/dist/{card-D2k3dRL0.js.map → card-C9TljY2Z.js.map} +1 -1
  43. package/dist/{card-8VXoo2C_.cjs → card-yT_St83D.cjs} +1 -1
  44. package/dist/{card-8VXoo2C_.cjs.map → card-yT_St83D.cjs.map} +1 -1
  45. package/dist/card.cjs +1 -1
  46. package/dist/card.js +1 -1
  47. package/dist/{checkbox-8hNsBejz.js → checkbox-BDgh4rge.js} +1 -1
  48. package/dist/{checkbox-8hNsBejz.js.map → checkbox-BDgh4rge.js.map} +1 -1
  49. package/dist/{checkbox-Cq5wzeaY.cjs → checkbox-Dz2lkJs0.cjs} +1 -1
  50. package/dist/{checkbox-Cq5wzeaY.cjs.map → checkbox-Dz2lkJs0.cjs.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-M7Dr2npv.cjs} +2 -4
  54. package/dist/chips-M7Dr2npv.cjs.map +1 -0
  55. package/dist/{chips-D1kJrbzo.js → chips-N7fu0hA4.js} +3 -5
  56. package/dist/chips-N7fu0hA4.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-D2vxD814.cjs} +1 -1
  64. package/dist/{date-range-H903Vt_r.cjs.map → date-range-D2vxD814.cjs.map} +1 -1
  65. package/dist/{date-range-Dv-DM6mB.js → date-range-DFWOMgI3.js} +2 -2
  66. package/dist/{date-range-Dv-DM6mB.js.map → date-range-DFWOMgI3.js.map} +1 -1
  67. package/dist/{date-range-inline-Bvs2ZvEY.cjs → date-range-inline-C5JuZ_Kw.cjs} +1 -1
  68. package/dist/{date-range-inline-Bvs2ZvEY.cjs.map → date-range-inline-C5JuZ_Kw.cjs.map} +1 -1
  69. package/dist/{date-range-inline-TWWnTZlw.js → date-range-inline-D3q1OoKk.js} +1 -1
  70. package/dist/{date-range-inline-TWWnTZlw.js.map → date-range-inline-D3q1OoKk.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-Cpg8sH2F.js → details-BrUPmd92.js} +2 -2
  78. package/dist/details-BrUPmd92.js.map +1 -0
  79. package/dist/{details-CwSDur6j.cjs → details-DmDEInaL.cjs} +2 -2
  80. package/dist/details-DmDEInaL.cjs.map +1 -0
  81. package/dist/details.cjs +1 -1
  82. package/dist/details.js +1 -1
  83. package/dist/{divider-Be833gGZ.js → divider-BLijs8ba.js} +1 -1
  84. package/dist/{divider-Be833gGZ.js.map → divider-BLijs8ba.js.map} +1 -1
  85. package/dist/{divider-BNdVLE0H.cjs → divider-B_Ts_-qz.cjs} +1 -1
  86. package/dist/{divider-BNdVLE0H.cjs.map → divider-B_Ts_-qz.cjs.map} +1 -1
  87. package/dist/divider.cjs +1 -1
  88. package/dist/divider.js +1 -1
  89. package/dist/dropdown.cjs +1 -1
  90. package/dist/dropdown.js +1 -1
  91. package/dist/{expand-CtoffNNj.js → expand-C-xSpg7M.js} +2 -2
  92. package/dist/{expand-CtoffNNj.js.map → expand-C-xSpg7M.js.map} +1 -1
  93. package/dist/{expand-BP6RLzHw.cjs → expand-DV5sWUB6.cjs} +1 -1
  94. package/dist/{expand-BP6RLzHw.cjs.map → expand-DV5sWUB6.cjs.map} +1 -1
  95. package/dist/expand.cjs +1 -1
  96. package/dist/expand.js +1 -1
  97. package/dist/{float-CfbQM_2v.cjs → float-LyKef0LY.cjs} +1 -1
  98. package/dist/{float-CfbQM_2v.cjs.map → float-LyKef0LY.cjs.map} +1 -1
  99. package/dist/{float-KmbhaQHA.js → float-Y22yVBE2.js} +1 -1
  100. package/dist/{float-KmbhaQHA.js.map → float-Y22yVBE2.js.map} +1 -1
  101. package/dist/float.cjs +1 -1
  102. package/dist/float.js +1 -1
  103. package/dist/{form-CuBIrKOA.cjs → form-C_smXI2-.cjs} +1 -1
  104. package/dist/{form-CuBIrKOA.cjs.map → form-C_smXI2-.cjs.map} +1 -1
  105. package/dist/{form-8IcmP8uV.js → form-LFkEQkOX.js} +8 -8
  106. package/dist/{form-8IcmP8uV.js.map → form-LFkEQkOX.js.map} +1 -1
  107. package/dist/form.cjs +1 -1
  108. package/dist/form.js +6 -6
  109. package/dist/handover/agent-runtime-followups.md +1 -1
  110. package/dist/handover/agent-runtime-v1.md +3 -3
  111. package/dist/{icons-D7df1ysG.js → icons-B3pFrwKC.js} +1 -1
  112. package/dist/{icons-D7df1ysG.js.map → icons-B3pFrwKC.js.map} +1 -1
  113. package/dist/{icons-BJld4JHp.cjs → icons-CCNy4Egc.cjs} +1 -1
  114. package/dist/{icons-BJld4JHp.cjs.map → icons-CCNy4Egc.cjs.map} +1 -1
  115. package/dist/icons.cjs +1 -1
  116. package/dist/icons.js +1 -1
  117. package/dist/{iframe-DAbgW9tT.js → iframe-BbFlCEyP.js} +1 -1
  118. package/dist/{iframe-DAbgW9tT.js.map → iframe-BbFlCEyP.js.map} +1 -1
  119. package/dist/{iframe-GT6D8l5Z.cjs → iframe-CCcmLZ_K.cjs} +1 -1
  120. package/dist/{iframe-GT6D8l5Z.cjs.map → iframe-CCcmLZ_K.cjs.map} +1 -1
  121. package/dist/iframe.cjs +1 -1
  122. package/dist/iframe.js +1 -1
  123. package/dist/index.cjs +1 -1
  124. package/dist/index.js +27 -27
  125. package/dist/{input-DC6ap_uN.js → input-Dkneo4uA.js} +2 -2
  126. package/dist/{input-DC6ap_uN.js.map → input-Dkneo4uA.js.map} +1 -1
  127. package/dist/{input-chip-c5n547tg.js → input-chip-C1-TYu4v.js} +1 -1
  128. package/dist/{input-chip-c5n547tg.js.map → input-chip-C1-TYu4v.js.map} +1 -1
  129. package/dist/{input-chip-MsiMu-b5.cjs → input-chip-F5NEkkBU.cjs} +1 -1
  130. package/dist/{input-chip-MsiMu-b5.cjs.map → input-chip-F5NEkkBU.cjs.map} +1 -1
  131. package/dist/{input-BE9wEEw4.cjs → input-sBZ89wz1.cjs} +1 -1
  132. package/dist/{input-BE9wEEw4.cjs.map → input-sBZ89wz1.cjs.map} +1 -1
  133. package/dist/input.cjs +1 -1
  134. package/dist/input.js +1 -1
  135. package/dist/json.cjs +1 -1
  136. package/dist/json.js +2 -2
  137. package/dist/kbd.cjs +1 -1
  138. package/dist/kbd.js +1 -1
  139. package/dist/layout.cjs +1 -1
  140. package/dist/layout.js +1 -1
  141. package/dist/{lightbox-HqJBBjAT.cjs → lightbox-B4m5lxGs.cjs} +1 -1
  142. package/dist/{lightbox-HqJBBjAT.cjs.map → lightbox-B4m5lxGs.cjs.map} +1 -1
  143. package/dist/{lightbox-CNX9Eg3U.js → lightbox-D7hYFspE.js} +1 -1
  144. package/dist/{lightbox-CNX9Eg3U.js.map → lightbox-D7hYFspE.js.map} +1 -1
  145. package/dist/lightbox.cjs +1 -1
  146. package/dist/lightbox.js +1 -1
  147. package/dist/{list-bhyuQSyO.cjs → list-C2ycz-yr.cjs} +1 -1
  148. package/dist/{list-bhyuQSyO.cjs.map → list-C2ycz-yr.cjs.map} +1 -1
  149. package/dist/{list-C76Pb-c1.js → list-Ou72tSeq.js} +1 -1
  150. package/dist/{list-C76Pb-c1.js.map → list-Ou72tSeq.js.map} +1 -1
  151. package/dist/list.cjs +1 -1
  152. package/dist/list.js +1 -1
  153. package/dist/{menu-BqKQ-s0C.cjs → menu-ComSx-T0.cjs} +1 -1
  154. package/dist/{menu-BqKQ-s0C.cjs.map → menu-ComSx-T0.cjs.map} +1 -1
  155. package/dist/{menu-C5ksITpG.js → menu-YHbpRa7x.js} +2 -2
  156. package/dist/{menu-C5ksITpG.js.map → menu-YHbpRa7x.js.map} +1 -1
  157. package/dist/menu.cjs +1 -1
  158. package/dist/menu.js +1 -1
  159. package/dist/mixins-B9kY_60p.js +636 -0
  160. package/dist/{mixins-DCVXqL1Q.js.map → mixins-B9kY_60p.js.map} +1 -1
  161. package/dist/mixins-BwGJwK7X.cjs +254 -0
  162. package/dist/{mixins-Du9HMrIG.cjs.map → mixins-BwGJwK7X.cjs.map} +1 -1
  163. package/dist/mixins.cjs +1 -1
  164. package/dist/mixins.js +1 -1
  165. package/dist/nav-drawer.cjs +1 -1
  166. package/dist/nav-drawer.js +1 -1
  167. package/dist/navigation-bar.cjs +1 -1
  168. package/dist/navigation-bar.js +1 -1
  169. package/dist/navigation-rail.cjs +1 -1
  170. package/dist/navigation-rail.js +1 -1
  171. package/dist/{notification-DR3gvWt8.cjs → notification-DZhL0ZEg.cjs} +1 -1
  172. package/dist/{notification-DR3gvWt8.cjs.map → notification-DZhL0ZEg.cjs.map} +1 -1
  173. package/dist/{notification-eZxtr3NN.js → notification-O4Q5pyio.js} +2 -2
  174. package/dist/{notification-eZxtr3NN.js.map → notification-O4Q5pyio.js.map} +1 -1
  175. package/dist/notification.cjs +1 -1
  176. package/dist/notification.js +1 -1
  177. package/dist/{option-CBEHYG4U.js → option-BCks0a4i.js} +1 -1
  178. package/dist/{option-CBEHYG4U.js.map → option-BCks0a4i.js.map} +1 -1
  179. package/dist/{option-BDOKUqTy.cjs → option-C2VKw8Yt.cjs} +1 -1
  180. package/dist/{option-BDOKUqTy.cjs.map → option-C2VKw8Yt.cjs.map} +1 -1
  181. package/dist/option.cjs +1 -1
  182. package/dist/option.js +1 -1
  183. package/dist/{overlay-oxM9OLXP.js → overlay-C0YSnxoV.js} +8 -10
  184. package/dist/overlay-C0YSnxoV.js.map +1 -0
  185. package/dist/{overlay-DG6EeyKt.cjs → overlay-CG1gc1Jw.cjs} +2 -2
  186. package/dist/overlay-CG1gc1Jw.cjs.map +1 -0
  187. package/dist/overlay.cjs +1 -1
  188. package/dist/{overlay.confirm-body-78e1WrN9.cjs → overlay.confirm-body-B-Kmn7LF.cjs} +1 -1
  189. package/dist/{overlay.confirm-body-78e1WrN9.cjs.map → overlay.confirm-body-B-Kmn7LF.cjs.map} +1 -1
  190. package/dist/{overlay.confirm-body-D_P2e7l6.js → overlay.confirm-body-BmOnrKbF.js} +1 -1
  191. package/dist/{overlay.confirm-body-D_P2e7l6.js.map → overlay.confirm-body-BmOnrKbF.js.map} +1 -1
  192. package/dist/overlay.js +3 -3
  193. package/dist/{overlay.service-DQkGPUY7.cjs → overlay.service-BPKV2a8w.cjs} +1 -1
  194. package/dist/{overlay.service-DQkGPUY7.cjs.map → overlay.service-BPKV2a8w.cjs.map} +1 -1
  195. package/dist/{overlay.service-C8NwO4Bx.js → overlay.service-CRoq9Gu-.js} +2 -2
  196. package/dist/{overlay.service-C8NwO4Bx.js.map → overlay.service-CRoq9Gu-.js.map} +1 -1
  197. package/dist/page.cjs +1 -1
  198. package/dist/page.js +2 -2
  199. package/dist/{progress-CMSst_2U.cjs → progress-B9RWAFv5.cjs} +1 -1
  200. package/dist/{progress-CMSst_2U.cjs.map → progress-B9RWAFv5.cjs.map} +1 -1
  201. package/dist/{progress-C4kDZfb7.js → progress-CEEl7vdd.js} +1 -1
  202. package/dist/{progress-C4kDZfb7.js.map → progress-CEEl7vdd.js.map} +1 -1
  203. package/dist/progress.cjs +1 -1
  204. package/dist/progress.js +1 -1
  205. package/dist/radio-group-C2y6H5YY.cjs +19 -0
  206. package/dist/radio-group-C2y6H5YY.cjs.map +1 -0
  207. package/dist/radio-group-VERF_8rC.js +71 -0
  208. package/dist/radio-group-VERF_8rC.js.map +1 -0
  209. package/dist/radio-group.cjs +1 -1
  210. package/dist/radio-group.js +1 -1
  211. package/dist/range.cjs +1 -1
  212. package/dist/range.js +1 -1
  213. package/dist/{scroll-C1klVgSQ.js → scroll-Bj7FsS08.js} +1 -1
  214. package/dist/{scroll-C1klVgSQ.js.map → scroll-Bj7FsS08.js.map} +1 -1
  215. package/dist/{scroll-S-bXF2u6.cjs → scroll-Djz3pJfX.cjs} +1 -1
  216. package/dist/{scroll-S-bXF2u6.cjs.map → scroll-Djz3pJfX.cjs.map} +1 -1
  217. package/dist/{select-UU2pB67h.js → select-ClJj_2AP.js} +3 -3
  218. package/dist/select-ClJj_2AP.js.map +1 -0
  219. package/dist/select-CngphfDB.cjs +56 -0
  220. package/dist/select-CngphfDB.cjs.map +1 -0
  221. package/dist/select.cjs +1 -1
  222. package/dist/select.js +1 -1
  223. package/dist/skeleton.cjs +1 -1
  224. package/dist/skeleton.js +1 -1
  225. package/dist/skills/SKILL.md +12 -0
  226. package/dist/skills/schmancy/SKILL.md +12 -0
  227. package/dist/slider.cjs +1 -1
  228. package/dist/slider.js +1 -1
  229. package/dist/{splash-screen-ChMkAPLU.js → splash-screen-BQsBy3O1.js} +1 -1
  230. package/dist/{splash-screen-ChMkAPLU.js.map → splash-screen-BQsBy3O1.js.map} +1 -1
  231. package/dist/{splash-screen-BvaDkvJU.cjs → splash-screen-CntIFk2h.cjs} +1 -1
  232. package/dist/{splash-screen-BvaDkvJU.cjs.map → splash-screen-CntIFk2h.cjs.map} +1 -1
  233. package/dist/splash-screen.cjs +1 -1
  234. package/dist/splash-screen.js +1 -1
  235. package/dist/{src-DnunCC4X.js → src-BAXhEv8f.js} +32 -32
  236. package/dist/{src-DnunCC4X.js.map → src-BAXhEv8f.js.map} +1 -1
  237. package/dist/{src-BIlD63Cz.cjs → src-ChFa-FDD.cjs} +1 -1
  238. package/dist/{src-BIlD63Cz.cjs.map → src-ChFa-FDD.cjs.map} +1 -1
  239. package/dist/steps.cjs +1 -1
  240. package/dist/steps.js +1 -1
  241. package/dist/{surface-DCRy-EyT.js → surface-CHUJSY1o.js} +1 -1
  242. package/dist/{surface-DCRy-EyT.js.map → surface-CHUJSY1o.js.map} +1 -1
  243. package/dist/{surface-DWwQDX9r.cjs → surface-CXmQuXun.cjs} +1 -1
  244. package/dist/{surface-DWwQDX9r.cjs.map → surface-CXmQuXun.cjs.map} +1 -1
  245. package/dist/surface.cjs +1 -1
  246. package/dist/surface.js +1 -1
  247. package/dist/switch.cjs +1 -1
  248. package/dist/switch.js +1 -1
  249. package/dist/table.cjs +1 -1
  250. package/dist/table.js +1 -1
  251. package/dist/{tabs-lxQHWEb7.cjs → tabs-Bku0sC0p.cjs} +1 -1
  252. package/dist/{tabs-lxQHWEb7.cjs.map → tabs-Bku0sC0p.cjs.map} +1 -1
  253. package/dist/{tabs-CkDNLbiS.js → tabs-DPVX21WM.js} +1 -1
  254. package/dist/{tabs-CkDNLbiS.js.map → tabs-DPVX21WM.js.map} +1 -1
  255. package/dist/tabs.cjs +1 -1
  256. package/dist/tabs.js +1 -1
  257. package/dist/teleport.cjs +1 -1
  258. package/dist/teleport.js +1 -1
  259. package/dist/{textarea-CNa4dSvF.cjs → textarea-CqJNviYi.cjs} +1 -1
  260. package/dist/{textarea-CNa4dSvF.cjs.map → textarea-CqJNviYi.cjs.map} +1 -1
  261. package/dist/{textarea-DkfGmRSI.js → textarea-D6z1UZzs.js} +1 -1
  262. package/dist/{textarea-DkfGmRSI.js.map → textarea-D6z1UZzs.js.map} +1 -1
  263. package/dist/textarea.cjs +1 -1
  264. package/dist/textarea.js +1 -1
  265. package/dist/{theme-CMyXTDht.cjs → theme-BpKVBJCr.cjs} +1 -1
  266. package/dist/{theme-CMyXTDht.cjs.map → theme-BpKVBJCr.cjs.map} +1 -1
  267. package/dist/{theme-CNWRYdfn.js → theme-DbHfINBV.js} +1 -1
  268. package/dist/{theme-CNWRYdfn.js.map → theme-DbHfINBV.js.map} +1 -1
  269. package/dist/{theme-button-CixloLin.js → theme-button-BeU8Nbs2.js} +1 -1
  270. package/dist/{theme-button-CixloLin.js.map → theme-button-BeU8Nbs2.js.map} +1 -1
  271. package/dist/{theme-button-kMhsX5Oe.cjs → theme-button-Cof9I85G.cjs} +1 -1
  272. package/dist/{theme-button-kMhsX5Oe.cjs.map → theme-button-Cof9I85G.cjs.map} +1 -1
  273. package/dist/theme-button.cjs +1 -1
  274. package/dist/theme-button.js +1 -1
  275. package/dist/theme.cjs +1 -1
  276. package/dist/theme.js +2 -2
  277. package/dist/tree.cjs +1 -1
  278. package/dist/tree.js +1 -1
  279. package/dist/typography.cjs +1 -1
  280. package/dist/typography.js +1 -1
  281. package/dist/visually-hidden.cjs +1 -1
  282. package/dist/visually-hidden.js +1 -1
  283. package/dist/{window-qaGFMn_4.cjs → window-Cql1aIX2.cjs} +1 -1
  284. package/dist/{window-qaGFMn_4.cjs.map → window-Cql1aIX2.cjs.map} +1 -1
  285. package/dist/{window-BcvDNi9D.js → window-DmMNsos0.js} +1 -1
  286. package/dist/{window-BcvDNi9D.js.map → window-DmMNsos0.js.map} +1 -1
  287. package/dist/window.cjs +1 -1
  288. package/dist/window.js +1 -1
  289. package/package.json +1 -1
  290. package/skills/schmancy/SKILL.md +12 -0
  291. package/src/CLAUDE.md +20 -0
  292. package/src/area/area.component.ts +155 -342
  293. package/src/button/button.ts +8 -5
  294. package/src/button/icon-button.ts +8 -5
  295. package/src/chips/filter-chip.ts +1 -3
  296. package/src/details/details.ts +1 -1
  297. package/src/form/fields/radio-group/radio-button.ts +22 -44
  298. package/src/form/fields/radio-group/radio-group.ts +20 -75
  299. package/src/form/fields/select/select.ts +3 -2
  300. package/src/overlay/overlay.component.ts +29 -39
  301. package/src/overlay/overlay.positioning.ts +10 -2
  302. package/src/state/schmancy-state.html +897 -0
  303. package/src/state/schmancy-state.md +981 -0
  304. package/types/src/area/area.component.d.ts +0 -15
  305. package/types/src/button/icon-button.d.ts +1 -1
  306. package/types/src/form/fields/radio-group/radio-button.d.ts +2 -5
  307. package/types/src/form/fields/radio-group/radio-group.d.ts +2 -10
  308. package/types/src/overlay/overlay.positioning.d.ts +9 -1
  309. package/dist/area-BIipuSyO.js.map +0 -1
  310. package/dist/area-C-EMiNEE.cjs +0 -12
  311. package/dist/area-C-EMiNEE.cjs.map +0 -1
  312. package/dist/chips-D1kJrbzo.js.map +0 -1
  313. package/dist/chips-Dx_WvOGk.cjs.map +0 -1
  314. package/dist/details-Cpg8sH2F.js.map +0 -1
  315. package/dist/details-CwSDur6j.cjs.map +0 -1
  316. package/dist/mixins-DCVXqL1Q.js +0 -636
  317. package/dist/mixins-Du9HMrIG.cjs +0 -254
  318. package/dist/overlay-DG6EeyKt.cjs.map +0 -1
  319. package/dist/overlay-oxM9OLXP.js.map +0 -1
  320. package/dist/radio-group-DB9D2ZkA.js +0 -108
  321. package/dist/radio-group-DB9D2ZkA.js.map +0 -1
  322. package/dist/radio-group-dVUvYFq7.cjs +0 -40
  323. package/dist/radio-group-dVUvYFq7.cjs.map +0 -1
  324. package/dist/select-UU2pB67h.js.map +0 -1
  325. package/dist/select-fu_-rZyn.cjs +0 -56
  326. package/dist/select-fu_-rZyn.cjs.map +0 -1
@@ -0,0 +1,897 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Schmancy State — Complete Developer Reference</title>
7
+ <style>
8
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
9
+ :root{
10
+ --ink:#0f1117;--ink2:#374151;--ink3:#6b7280;--rule:#e5e7eb;
11
+ --a:#1d4ed8;--ab:#eff6ff;--g:#15803d;--gb:#f0fdf4;
12
+ --o:#92400e;--ob:#fffbeb;--r:#be185d;--rb:#fdf2f8;
13
+ --v:#6d28d9;--vb:#f5f3ff;--t:#0f766e;--tb:#f0fdfa;
14
+ --mono:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,monospace;
15
+ --sans:'Inter','Helvetica Neue',Arial,sans-serif;
16
+ --w:920px;
17
+ }
18
+ html{font-size:13px}
19
+ body{font-family:var(--sans);color:var(--ink);background:#fff;padding:40px 28px;line-height:1.68}
20
+ .page{max-width:var(--w);margin:0 auto}
21
+
22
+ /* ── Typography ── */
23
+ h1{font-size:2rem;font-weight:900;letter-spacing:-.04em;line-height:1.1;border-bottom:4px solid var(--ink);padding-bottom:10px;margin-bottom:6px}
24
+ .meta{font-size:.72rem;color:var(--ink3);margin-bottom:8px;font-style:italic}
25
+ .intro{font-size:.83rem;color:var(--ink2);margin-bottom:32px;line-height:1.8;max-width:800px}
26
+
27
+ /* Part titles */
28
+ .part{font-size:1.5rem;font-weight:900;letter-spacing:-.03em;border-bottom:3px solid var(--ink);padding-bottom:8px;margin:44px 0 6px}
29
+ .part-sub{font-size:.75rem;color:var(--ink3);margin-bottom:28px;font-style:italic}
30
+
31
+ /* Section headers */
32
+ h2{font-size:1rem;font-weight:800;letter-spacing:-.02em;margin:32px 0 4px;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
33
+ .tag{font-size:.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;padding:2px 8px;border-radius:100px;white-space:nowrap}
34
+ h3{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--ink3);margin:16px 0 7px}
35
+ p{font-size:.82rem;margin-bottom:9px;line-height:1.72}
36
+ p:last-child{margin-bottom:0}
37
+ strong{font-weight:700}
38
+
39
+ /* ── Code ── */
40
+ code{font-family:var(--mono);font-size:.76em;background:#f3f4f6;color:var(--r);padding:1px 4px;border-radius:3px}
41
+ pre{font-family:var(--mono);font-size:.69rem;background:#f3f4f6;border:1px solid var(--rule);border-left:3px solid var(--ink3);border-radius:0 4px 4px 0;padding:11px 14px;overflow-x:auto;line-height:1.58;margin:9px 0;white-space:pre;color:var(--ink)}
42
+
43
+ /* ── Dark visual blocks ── */
44
+ .viz{font-family:var(--mono);font-size:.67rem;background:#0f1117;color:#e2e8f0;border-radius:6px;padding:14px 17px;margin:9px 0;white-space:pre;line-height:1.62;overflow-x:auto}
45
+ .d{color:#475569}.h{color:#38bdf8;font-weight:700}.g{color:#4ade80;font-weight:600}
46
+ .ac{color:#a78bfa}.y{color:#fde047}.re{color:#f87171}.or{color:#fb923c}
47
+
48
+ /* ── Callouts ── */
49
+ .box{border-radius:0 4px 4px 0;padding:9px 14px;margin:9px 0;font-size:.79rem;line-height:1.65}
50
+ .box strong{display:block;font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px}
51
+ .note{border-left:3px solid var(--a);background:var(--ab)}
52
+ .warn{border-left:3px solid #f59e0b;background:var(--ob)}
53
+ .good{border-left:3px solid var(--g);background:var(--gb)}
54
+ .limit{border-left:3px solid var(--r);background:var(--rb)}
55
+
56
+ /* ── Grid ── */
57
+ .g2{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:9px 0}
58
+ .card{border:1px solid var(--rule);border-radius:4px;padding:11px 13px;font-size:.79rem}
59
+ .card h4{font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--ink3);margin-bottom:7px}
60
+
61
+ /* ── Tables ── */
62
+ table{width:100%;border-collapse:collapse;font-size:.78rem;margin:10px 0}
63
+ thead tr{background:var(--ink);color:#fff}
64
+ thead th{padding:6px 10px;text-align:left;font-weight:600;white-space:nowrap}
65
+ tbody tr:nth-child(even){background:#f9fafb}
66
+ td{padding:5px 10px;border-bottom:1px solid var(--rule);vertical-align:top}
67
+
68
+ /* ── Pattern header ── */
69
+ .ph{display:flex;align-items:flex-start;gap:14px;margin-bottom:14px}
70
+ .pn{font-size:2rem;font-weight:900;color:var(--rule);line-height:1;min-width:40px;letter-spacing:-.04em}
71
+ .pm{flex:1}
72
+ .pname{font-size:1.1rem;font-weight:800;letter-spacing:-.02em;line-height:1.2}
73
+ .badges{display:flex;flex-wrap:wrap;gap:5px;margin-top:5px}
74
+ .tl{font-size:.79rem;color:var(--ink3);margin-top:4px;font-style:italic}
75
+
76
+ .mm{background:#fafafa;border:1px solid var(--rule);border-left:3px solid var(--ink);border-radius:0 5px 5px 0;padding:12px 14px;margin:10px 0;font-size:.81rem}
77
+ .mm strong{display:block;font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--ink3);margin-bottom:5px}
78
+
79
+ hr{border:none;border-top:1.5px solid var(--rule);margin:28px 0}
80
+ .thick{border-top:3px solid var(--ink);margin:40px 0 32px}
81
+
82
+ @media print{
83
+ body{padding:0;font-size:11px}
84
+ .ph,.g2{page-break-inside:avoid}
85
+ pre,.viz,table{page-break-inside:avoid}
86
+ h2,h3{page-break-after:avoid}
87
+ .part{page-break-before:always}
88
+ @page{size:A4;margin:14mm 13mm}
89
+ }
90
+ </style>
91
+ </head>
92
+ <body>
93
+ <div class="page">
94
+
95
+ <!-- ══ COVER ══ -->
96
+ <h1>Schmancy State — Complete Developer Reference</h1>
97
+ <p class="meta">@mhmo91/schmancy/state · packages/schmancy/src/state/ · 2026-05-08 · source of truth: schmancy-state.md</p>
98
+ <p class="intro">
99
+ Everything about the schmancy state module in one document: the external libraries it is built on, the design patterns it uses (with mental models and worked examples), and the full technical reference. Read Part 1 first — without understanding TC39 Signals and <code>@lit/context</code>, the patterns are mechanisms without motivation.
100
+ </p>
101
+
102
+ <!-- ════════════════════════════════════════════════════════ -->
103
+ <div class="part">Part 1 — Foundational Libraries</div>
104
+ <p class="part-sub">TC39 Signals (signal-polyfill · @lit-labs/signals) · @lit/context — how they actually work, from first principles</p>
105
+
106
+ <!-- ── A: TC39 Signals ── -->
107
+ <div>
108
+ <h2>A &nbsp; TC39 Signals <span class="tag" style="background:var(--ob);color:var(--o)">signal-polyfill</span> <span class="tag" style="background:var(--ob);color:var(--o)">@lit-labs/signals</span></h2>
109
+
110
+ <h3>The problem</h3>
111
+ <p>A spreadsheet: cell <code>C1 = A1 + B1</code>. When <code>A1</code> changes, <code>C1</code> recomputes automatically — without you calling "recompute C1." JavaScript has no built-in version of this. Every reactive system (MobX, Vue, SolidJS, Svelte) implements the same idea: <em>track which computations read which values, then invalidate those computations when the values change.</em> TC39 Signals standardises this. <code>signal-polyfill</code> ships it today.</p>
112
+
113
+ <h3>Three primitives</h3>
114
+ <div class="g2">
115
+ <div class="card">
116
+ <h4>Signal.State&lt;T&gt; — the mutable leaf</h4>
117
+ Holds a value. Notifies dependents when replaced. The bottom of the graph.
118
+ <pre>const n = new Signal.State(0)
119
+ n.get() // → 0
120
+ n.set(1) // notifies all dependents
121
+ n.get() // → 1</pre>
122
+ </div>
123
+ <div class="card">
124
+ <h4>Signal.Computed&lt;T&gt; — the derived cell</h4>
125
+ Lazy. Caches until a dependency changes. Re-runs only when <code>.get()</code> is called after dirty.
126
+ <pre>const x2 = new Signal.Computed(
127
+ () => n.get() * 2
128
+ )
129
+ x2.get() // runs fn → 2
130
+ x2.get() // cached → 2
131
+ n.set(5)
132
+ x2.get() // re-runs → 10</pre>
133
+ </div>
134
+ </div>
135
+ <div class="card" style="margin:9px 0">
136
+ <h4>Signal.subtle.Watcher — the push hook</h4>
137
+ Low-level. Fires a callback the moment any watched signal becomes dirty. This bridges the pull model to external push systems — RxJS, Lit's <code>requestUpdate()</code>.
138
+ <pre>const w = new Signal.subtle.Watcher(() => {
139
+ // fires synchronously when any watched signal is set
140
+ w.watch() // must re-arm — Watcher disarms after each notification
141
+ })
142
+ w.watch(n) // start watching</pre>
143
+ </div>
144
+
145
+ <h3>How auto-tracking works — the key mechanism</h3>
146
+ <p>The polyfill maintains a single module-level variable: <strong><code>activeConsumer</code></strong>. When a <code>Computed</code>'s callback runs, <code>activeConsumer</code> is set to that computed. Every <code>signal.get()</code> checks <code>activeConsumer</code> and registers itself as a dependency. <strong>The read is the registration. No explicit subscribe call.</strong></p>
147
+
148
+ <div class="viz"><span class="d">// Trace: what happens inside Computed.get()</span>
149
+
150
+ <span class="h">activeConsumer = x2_computed</span> <span class="d">← set before running the callback</span>
151
+
152
+ x2_computed.fn():
153
+ n.get() <span class="d">← n checks activeConsumer</span>
154
+ <span class="g">n._subscribers.add(x2_computed)</span> <span class="d">← n records x2 as a dependent</span>
155
+ return n._value (0)
156
+
157
+ <span class="h">activeConsumer = null</span> <span class="d">← cleared after callback</span>
158
+
159
+ <span class="d">// Later: n.set(5)</span>
160
+ n._value = 5
161
+ <span class="ac">x2_computed.markDirty()</span> <span class="d">← n notifies all subscribers</span>
162
+ x2_computed._dirty = true
163
+ Watcher callback fires <span class="d">← if a Watcher was watching x2</span></div>
164
+
165
+ <h3>SignalWatcher — how Lit plugs in</h3>
166
+ <p><code>SignalWatcher</code> (from <code>@lit-labs/signals</code>) wraps Lit's <code>performUpdate()</code> in a <code>Signal.Computed</code>. A <code>Watcher</code> watches that computed. When any signal read during the last render changes, the Watcher fires and calls <code>requestUpdate()</code>. Reading <code>cart.value</code> in <code>render()</code> IS the subscription. <code>SchmancyElement</code> already includes this mixin — no setup needed.</p>
167
+
168
+ <div class="viz"><span class="d">// Reconstructed from minified source (signal-watcher.js)</span>
169
+ class SignalWatcher extends LitElement {
170
+ <span class="h">_renderComputed</span> = new Signal.Computed(() =&gt; {
171
+ super.performUpdate() <span class="d">← your render() runs in here</span>
172
+ })
173
+
174
+ <span class="ac">_watcher</span> = new Signal.subtle.Watcher(() =&gt; {
175
+ this.requestUpdate() <span class="d">← schedule Lit re-render</span>
176
+ this._watcher.watch() <span class="d">← re-arm</span>
177
+ })
178
+
179
+ performUpdate() {
180
+ this._watcher.watch(this._renderComputed)
181
+ this._renderComputed.get() <span class="d">← runs render(), auto-tracking all signal reads</span>
182
+ }
183
+ }</div>
184
+
185
+ <h3>Full cycle — trace by hand</h3>
186
+ <div class="viz"><span class="y">1. define</span>
187
+ const cart = new Signal.State({ items: [], total: 0 })
188
+
189
+ <span class="y">2. render() runs (inside SignalWatcher's Computed)</span>
190
+ activeConsumer = renderComputed
191
+ cart.get() → <span class="g">cart._subscribers.add(renderComputed)</span>
192
+ renders "Total: 0"
193
+ activeConsumer = null
194
+
195
+ <span class="y">3. cart.set({ items: ['x'], total: 12 }) — called anywhere</span>
196
+ cart._value = { items: ['x'], total: 12 }
197
+ <span class="ac">renderComputed.markDirty()</span>
198
+ _watcher callback fires (synchronous)
199
+ queueMicrotask(() =&gt; component.requestUpdate())
200
+
201
+ <span class="y">4. microtask: Lit re-renders</span>
202
+ renderComputed.get() → re-runs render(), re-tracks deps
203
+ renders "Total: 12" <span class="g">✓</span></div>
204
+
205
+ <div class="box good">
206
+ <strong>What this means in practice</strong>
207
+ In a <code>SchmancyElement</code>, writing <code>cart.value.items.length</code> inside <code>render()</code> is all you need. No <code>@state</code>, no decorator, no subscription. When <code>cart.set()</code> is called anywhere in the app, the component re-renders automatically.
208
+ </div>
209
+ </div>
210
+
211
+ <hr>
212
+
213
+ <!-- ── B: @lit/context ── -->
214
+ <div>
215
+ <h2>B &nbsp; @lit/context <span class="tag" style="background:var(--rb);color:var(--r)">Provider / Consumer</span> <span class="tag" style="background:var(--rb);color:var(--r)">Bubbling event RPC</span></h2>
216
+
217
+ <h3>The problem</h3>
218
+ <p>You have a value many nested components need. Passing it as a property through every intermediate element ("prop drilling") is brittle and couples unrelated components. <strong>Context</strong> is the alternative: a provider at any tree position announces a value; any descendant requests it without any intermediate element knowing. React has <code>createContext / useContext</code>. <code>@lit/context</code> is the same idea implemented with the DOM's own event system — no framework runtime required.</p>
219
+
220
+ <h3>The mechanism — one event, one listener</h3>
221
+ <pre>
222
+ // The event — from actual source (context-request-event.js)
223
+ class ContextRequestEvent extends Event {
224
+ constructor(context, contextTarget, callback) {
225
+ super('context-request', { bubbles: true, composed: true })
226
+ this.context = context // identity key (a Symbol)
227
+ this.contextTarget = contextTarget
228
+ this.callback = callback // "call me with the value"
229
+ }
230
+ }
231
+
232
+ // The provider — reconstructed from context-provider.js
233
+ host.addEventListener('context-request', (e) => {
234
+ if (e.context !== this.context) return // wrong key — ignore
235
+ if (e.contextTarget === host) return // don't serve yourself
236
+ e.stopPropagation() // closest provider wins
237
+ e.callback(this.value) // deliver the value
238
+ })</pre>
239
+
240
+ <div class="g2">
241
+ <div class="box note" style="margin:0">
242
+ <strong>bubbles: true + composed: true</strong>
243
+ <code>bubbles</code> lets the event travel up so a nested consumer reaches a distant provider. <code>composed</code> lets it cross shadow DOM boundaries — without it, shadow roots silently block the event.
244
+ </div>
245
+ <div class="box warn" style="margin:0">
246
+ <strong>stopPropagation = closest provider wins</strong>
247
+ The nearest <code>ContextProvider</code> intercepts and stops the event. Outer providers never see it. This is identical to React's nearest-<code>Provider</code>-wins rule.
248
+ </div>
249
+ </div>
250
+
251
+ <h3>Why Symbol.for() — cross-bundle identity</h3>
252
+ <p><code>Symbol('x') !== Symbol('x')</code> — unique every call. <code>Symbol.for('x') === Symbol.for('x')</code> — the runtime-global registry returns the same symbol for the same string across all module copies. Schmancy uses <code>Symbol.for('schmancy.state:' + namespace)</code> so Bundle A's provider matches Bundle B's request even in a Module Federation setup.</p>
253
+
254
+ <h3>Full resolution — trace by hand</h3>
255
+ <div class="viz"><span class="d">// DOM tree</span>
256
+ &lt;app&gt;
257
+ &lt;<span class="h">schmancy-context</span> .provides=${[cart]}&gt; <span class="d">← ContextProvider installed here</span>
258
+ &lt;checkout-page&gt;
259
+ &lt;<span class="ac">cart-view</span>&gt; <span class="d">← wants isolated copy</span>
260
+
261
+ <span class="y">Step 1 — cart-view calls cart.value inside render()</span>
262
+ resolveContextual('hannah/cart', globalFallback)
263
+ host = resolveActiveHost() → &lt;cart-view&gt; element
264
+
265
+ <span class="y">Step 2 — dispatch ContextRequestEvent from &lt;cart-view&gt;</span>
266
+ cartViewEl.dispatchEvent(new ContextRequestEvent(
267
+ Symbol.for('schmancy.state:hannah/cart'),
268
+ cartViewEl, value =&gt; { resolved = value }
269
+ ))
270
+ <span class="d">// bubbles: cart-view → checkout-page → schmancy-context</span>
271
+
272
+ <span class="y">Step 3 — ContextProvider on &lt;schmancy-context&gt; intercepts</span>
273
+ e.context === our key? <span class="g">yes</span>
274
+ e.contextTarget === host? <span class="d">no (it's cart-view)</span>
275
+ <span class="g">e.stopPropagation()</span> <span class="d">← outer providers never see this</span>
276
+ <span class="g">e.callback(isolatedCopy)</span> <span class="d">← resolved = isolatedCopy ✓</span>
277
+
278
+ <span class="y">Step 4 — resolveContextual caches and returns</span>
279
+ hostResolverCache[cartViewEl]['hannah/cart'] = isolatedCopy
280
+ cart.value → isolatedCopy.value <span class="g">✓</span>
281
+
282
+ <span class="y">Step 5 — same render() called again</span>
283
+ cache hit → skip event dispatch entirely → O(1) <span class="g">✓</span></div>
284
+
285
+ <table>
286
+ <thead><tr><th>Library</th><th>Primitive used</th><th>What it enables in schmancy</th></tr></thead>
287
+ <tbody>
288
+ <tr><td><code>signal-polyfill</code></td><td><code>Signal.State</code>, <code>Signal.Computed</code></td><td>Reactive cell holding each state's value; <code>computed()</code> for derived state</td></tr>
289
+ <tr><td><code>signal-polyfill</code></td><td><code>Signal.subtle.Watcher</code></td><td>Signal change → RxJS Observable emission; used by <code>effect()</code></td></tr>
290
+ <tr><td><code>@lit-labs/signals</code></td><td><code>SignalWatcher</code> mixin</td><td>Auto-tracking inside Lit <code>render()</code> — reading <code>state.value</code> IS the subscription</td></tr>
291
+ <tr><td><code>@lit/context</code></td><td><code>ContextProvider</code></td><td>Installed by <code>&lt;schmancy-context&gt;</code> to serve isolated state copies to descendants</td></tr>
292
+ <tr><td><code>@lit/context</code></td><td><code>ContextRequestEvent</code></td><td>Dispatched by <code>resolveContextual()</code> to ask "is there an isolated copy for me?"</td></tr>
293
+ </tbody>
294
+ </table>
295
+ </div>
296
+
297
+ <!-- ════════════════════════════════════════════════════════ -->
298
+ <hr class="thick">
299
+ <div class="part">Part 2 — Design Patterns</div>
300
+ <p class="part-sub">11 named patterns with mental models, visual diagrams, and code. Pattern 12 shows how they assemble into the whole system.</p>
301
+
302
+ <!-- PATTERN 01 -->
303
+ <hr>
304
+ <div class="ph"><div class="pn">01</div><div class="pm">
305
+ <div class="pname">Module-Scoped Singleton</div>
306
+ <div class="badges">
307
+ <span class="tag" style="background:var(--vb);color:var(--v)">Singleton</span>
308
+ <span class="tag" style="background:var(--vb);color:var(--v)">Module system</span>
309
+ </div>
310
+ <div class="tl">"One name, one value, everywhere the module is imported."</div>
311
+ </div></div>
312
+ <div class="mm"><strong>Mental model</strong>A plain JavaScript <code>const</code> at the top of a file. ES module semantics guarantee it is evaluated once and the same object is returned to every importer. The state factory makes this the default: produce an object at module scope, never inside a class or function.</div>
313
+ <div class="viz">export const cart = state&lt;CartState&gt;('hannah/cart').session(initial)
314
+
315
+ ┌────────────────────┼────────────────────┐
316
+ ▼ ▼ ▼
317
+ <span class="h">CartView.ts</span> <span class="h">CartSummary.ts</span> <span class="h">CheckoutPage.ts</span>
318
+ import {cart} import {cart} import {cart}
319
+ <span class="g">← same object ────────────────────────────────────────</span></div>
320
+ <p>The singleton is not managed by a service locator or DI container — it is the JS module system itself. No registry lookup, no <code>getInstance()</code> call. State survives component remounts and navigation because it lives outside any component lifecycle.</p>
321
+ <div class="box warn"><strong>The one rule</strong>Never use <code>using cart = state(...)</code> at module scope. <code>using</code> disposes on block exit — which for a module-level variable is never. Reserve <code>using</code> for test scope.</div>
322
+
323
+ <!-- PATTERN 02 -->
324
+ <hr>
325
+ <div class="ph"><div class="pn">02</div><div class="pm">
326
+ <div class="pname">Transparent Proxy</div>
327
+ <div class="badges">
328
+ <span class="tag" style="background:var(--ab);color:var(--a)">Proxy</span>
329
+ <span class="tag" style="background:var(--ab);color:var(--a)">Structural</span>
330
+ </div>
331
+ <div class="tl">"The same <code>cart.value</code> call routes to a different object depending on where you are in the DOM tree — and you never know."</div>
332
+ </div></div>
333
+ <div class="mm"><strong>Mental model</strong>A receptionist who receives every call. Most of the time she routes it to the main office (module-scoped signal). If the call comes in from someone sitting inside a special meeting room (<code>&lt;schmancy-context&gt;</code>), she routes it to the room's private whiteboard. The caller never dials a different number — but who picks up changes based on physical location.</div>
334
+ <div class="viz">DOM tree:
335
+ &lt;app&gt;
336
+ ├── <span class="h">&lt;schmancy-context .provides=${[cart]}&gt;</span> <span class="d">← installs isolated copy</span>
337
+ │ cart.value → <span class="g">isolated copy</span>
338
+ │ cart.set() → <span class="g">isolated copy</span>
339
+
340
+ └── &lt;sidebar&gt;
341
+ cart.value → <span class="y">module-scoped global</span>
342
+ cart.set() → <span class="y">module-scoped global</span>
343
+
344
+ <span class="d">Same variable. Different target. No code change in consumers.</span></div>
345
+ <h3>How the proxy is built — without ES Proxy</h3>
346
+ <pre>
347
+ // Every public accessor on the global instance is a live Object.defineProperty getter
348
+ Object.defineProperty(instance, 'value', {
349
+ get: () => {
350
+ const target = resolveContextual(namespace, isolatedTarget)
351
+ return target.value // reads from whichever target resolved
352
+ }
353
+ })
354
+ // Every write method also routes through resolveContextual
355
+ instance['set'] = (...args) => {
356
+ const target = resolveContextual(namespace, isolatedTarget)
357
+ return target['set'](...args)
358
+ }</pre>
359
+ <div class="box note"><strong>Why not ES Proxy?</strong><code>ES Proxy</code> intercepts property access but cannot natively know the caller's DOM position. <code>Object.defineProperty</code> live getters achieve the same interception with the active-host tracking (Pattern 06) handling position resolution as a separate concern.</div>
360
+
361
+ <!-- PATTERN 03 -->
362
+ <hr>
363
+ <div class="ph"><div class="pn">03</div><div class="pm">
364
+ <div class="pname">Adapter / Storage Port</div>
365
+ <div class="badges">
366
+ <span class="tag" style="background:var(--gb);color:var(--g)">Adapter</span>
367
+ <span class="tag" style="background:var(--gb);color:var(--g)">Ports &amp; Adapters</span>
368
+ <span class="tag" style="background:var(--gb);color:var(--g)">Strategy</span>
369
+ </div>
370
+ <div class="tl">"The state logic never touches a storage API directly. It talks to a uniform interface, and the backend is swapped by configuration."</div>
371
+ </div></div>
372
+ <div class="mm"><strong>Mental model</strong>A document that needs to be filed. It hands the document to a filing clerk. The interface to the clerk is always the same — <em>file this, retrieve this, destroy this</em> — regardless of whether the cabinet is in the same room (memory), a local office (localStorage), or a central warehouse (IndexedDB).</div>
373
+ <pre>
374
+ interface StorageAdapter&lt;T&gt; {
375
+ load(): Promise&lt;T | null&gt;
376
+ save(value: T): Promise&lt;void&gt;
377
+ clear(): Promise&lt;void&gt;
378
+ close?(): Promise&lt;void&gt; // IDB only
379
+ }
380
+ // Four implementations:
381
+ MemoryAdapter → .memory() JS heap, no I/O
382
+ WebStorageAdapter → .local() localStorage (+ Map/Set JSON tunnel)
383
+ WebStorageAdapter → .session() sessionStorage (+ Map/Set JSON tunnel)
384
+ IndexedDBAdapter → .idb() IndexedDB 'SchmancyState'/'states'</pre>
385
+ <p><strong>Map/Set JSON tunnel</strong> (a nested adapter inside <code>WebStorageAdapter</code>): localStorage only stores strings. A custom replacer/reviver pair translates:</p>
386
+ <pre>
387
+ Map → { $kind: '__schmancy_state_Map', entries: [[k, v], …] }
388
+ Set → { $kind: '__schmancy_state_Set', values: [v, …] }
389
+ // Reviver reconstructs. IDB stores native JS objects — no tunnel needed.</pre>
390
+ <div class="box good"><strong>Extensibility</strong>Adding a new backend (e.g. OPFS) requires one new class implementing <code>StorageAdapter&lt;T&gt;</code> and one case in <code>createAdapter()</code>. Zero changes to the state core.</div>
391
+
392
+ <!-- PATTERN 04 -->
393
+ <hr>
394
+ <div class="ph"><div class="pn">04</div><div class="pm">
395
+ <div class="pname">Dual Observer — Signal + Observable</div>
396
+ <div class="badges">
397
+ <span class="tag" style="background:var(--ob);color:var(--o)">Observer</span>
398
+ <span class="tag" style="background:var(--ob);color:var(--o)">Pull + Push</span>
399
+ <span class="tag" style="background:var(--ob);color:var(--o)">Bridge</span>
400
+ </div>
401
+ <div class="tl">"One value, two reactive surfaces — pull (signals) for the render loop, push (Observables) for pipelines."</div>
402
+ </div></div>
403
+ <div class="mm"><strong>Mental model</strong>Signals are a thermometer on the wall — you glance at it (pull) whenever you need the temperature. Observables are weather alerts — the system pushes a notification when something changes. Both sit over the same measurement.</div>
404
+ <div class="viz"> <span class="h">Signal.State&lt;T&gt;</span>
405
+ signal.set(next) ← writes
406
+
407
+ ┌───────────────┼──────────────────────────┐
408
+ ▼ ▼
409
+ <span class="g">PULL state.value / signal.get()</span> <span class="ac">PUSH state.$ Observable&lt;T&gt;</span>
410
+ auto-tracked by SignalWatcher Signal.subtle.Watcher bridge
411
+ in render() → requestUpdate() subscriber.next(signal.get())
412
+ microtask-coalesced</div>
413
+ <h3>The bridge — how Signal becomes Observable</h3>
414
+ <pre>
415
+ new Observable(subscriber => {
416
+ subscriber.next(signal.get()) // ① immediate initial emission
417
+ let scheduled = false
418
+ const watcher = new Signal.subtle.Watcher(() => { // ② fires on signal.set()
419
+ if (scheduled) return
420
+ scheduled = true
421
+ queueMicrotask(() => {
422
+ scheduled = false
423
+ subscriber.next(signal.get()) // ③ push latest, coalesced
424
+ watcher.watch(signal) // ④ re-arm
425
+ })
426
+ })
427
+ watcher.watch(signal)
428
+ return () => watcher.unwatch(signal) // ⑤ cleanup on unsubscribe
429
+ })</pre>
430
+ <p>Use <strong>signals / <code>.value</code></strong> inside <code>render()</code>, <code>computed()</code>, <code>effect()</code>, or anywhere sync access is needed. Use <strong>Observable / <code>.$</code></strong> for RxJS pipelines: <code>combineLatest</code>, <code>switchMap</code>, debounce, <code>pipe(takeUntil(this.disconnecting))</code>.</p>
431
+
432
+ <!-- PATTERN 05 -->
433
+ <hr>
434
+ <div class="ph"><div class="pn">05</div><div class="pm">
435
+ <div class="pname">Type-Driven Command Dispatch</div>
436
+ <div class="badges">
437
+ <span class="tag" style="background:var(--gb);color:var(--g)">Command</span>
438
+ <span class="tag" style="background:var(--gb);color:var(--g)">Strategy</span>
439
+ <span class="tag" style="background:var(--gb);color:var(--g)">Discriminated Union</span>
440
+ </div>
441
+ <div class="tl">"The shape of your data determines which write commands exist. The wrong operation is inexpressible, not just a runtime error."</div>
442
+ </div></div>
443
+ <div class="mm"><strong>Mental model</strong>A chef's knife block. The shape of the slot tells you exactly which knife fits. If you have a <code>Map</code>, the slot is shaped for <code>MapAPI</code> — you get <code>set(k,v)</code>, <code>delete(k)</code>, <code>clear()</code>. You cannot accidentally call <code>push()</code> on a Map.</div>
444
+ <div class="viz">T = Map&lt;K,V&gt; → <span class="h">Kind = 'map'</span> → <span class="g">MapAPI</span> set(k,v) delete(k) replace clear
445
+ T = Set&lt;U&gt; → <span class="h">Kind = 'set'</span> → <span class="g">SetAPI</span> add toggle delete replace clear
446
+ T = U[] → <span class="h">Kind = 'array'</span> → <span class="g">ArrayAPI</span> push update(immer) replace clear
447
+ T = string|number|… → <span class="h">Kind = 'prim'</span> → <span class="g">ScalarAPI</span> set replace
448
+ T = Foo | null → <span class="h">Kind = 'null'</span> → <span class="g">ScalarAPI</span> set replace
449
+ T = { … } → <span class="h">Kind = 'obj'</span> → <span class="g">ObjectAPI</span> set(patch,merge?) update(immer) delete
450
+
451
+ const sel = state&lt;Set&lt;string&gt;&gt;('ui/sel').memory(new Set())
452
+ sel.add('id') <span class="g">✓</span>
453
+ sel.toggle('id') <span class="g">✓</span>
454
+ sel.push('id') <span class="re">✗ TypeScript error — push does not exist on SetAPI</span></div>
455
+ <h3>All writes funnel into one commit() — immutability by construction</h3>
456
+ <pre>
457
+ const commit = (next) => {
458
+ internal.signal.set(next) // synchronous — visible immediately
459
+ scheduleWrite(internal) // persist via adapter, microtask-debounced
460
+ }
461
+ // Every method is sugar over commit():
462
+ // ObjectAPI: set(patch, merge=true) { commit(merge ? {...cur, ...patch} : patch) }
463
+ // update(recipe) { commit(produce(cur, recipe)) } // immer
464
+ // SetAPI: toggle(v) { const n=new Set(cur); n.has(v)?n.delete(v):n.add(v); commit(n) }</pre>
465
+ <p>Every write produces a new value — spread, <code>new Map()</code>, <code>new Set()</code>, or immer <code>produce()</code>. Nothing mutates in place. Reference inequality = change.</p>
466
+
467
+ <!-- PATTERN 06 -->
468
+ <hr>
469
+ <div class="ph"><div class="pn">06</div><div class="pm">
470
+ <div class="pname">Zone / Execution Context Propagation</div>
471
+ <div class="badges">
472
+ <span class="tag" style="background:#f1f5f9;color:var(--ink2)">Zone</span>
473
+ <span class="tag" style="background:#f1f5f9;color:var(--ink2)">AsyncContext polyfill</span>
474
+ <span class="tag" style="background:#f1f5f9;color:var(--ink2)">Ambient state</span>
475
+ </div>
476
+ <div class="tl">"Knowing which DOM element is currently running code — even across async boundaries — without passing it as a parameter."</div>
477
+ </div></div>
478
+ <div class="mm"><strong>Mental model</strong>A call centre where every agent wears a badge showing which client file they are working on. The badge tells the system which client's records to look up. Zone.js is the classic full 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.</div>
479
+ <div class="viz"><span class="h">Tier 1: Explicit stack</span> <span class="d">← SchmancyElement prototype-wrap</span>
480
+ _activeHost.run(host, fn): pushes host → calls fn → pops on exit
481
+ Covers: render(), lifecycle hooks, all class methods
482
+
483
+ <span class="ac">Tier 2: Promise.prototype.then patch</span> <span class="d">← propagates across .then() chains</span>
484
+ fetch('/api').then(data =&gt; { cart.update(…) }) <span class="g">works ✓</span>
485
+ async fn: const data = await fetch()
486
+ cart.update(…) <span class="re">does NOT work (V8 opt) ✗</span>
487
+
488
+ <span class="g">Tier 3: Event-host slot</span> <span class="d">← &lt;schmancy-context&gt; capture-phase listener</span>
489
+ &lt;button @click=${() =&gt; cart.set(…)}&gt; <span class="g">works ✓</span>
490
+ 16 event types: click submit change input keydown…
491
+
492
+ <span class="y">Tier 4: document.activeElement</span> <span class="d">← keyboard / focus handlers</span>
493
+
494
+ <span class="d">Tier 5 (implicit): undefined → module-scoped global</span></div>
495
+ <h3>The Promise patch</h3>
496
+ <pre>
497
+ const _origThen = Promise.prototype.then
498
+ Promise.prototype.then = function(onfulfilled, onrejected) {
499
+ const captured = _stack[_stack.length - 1] // capture at chain time
500
+ const wrapFulfilled = v => {
501
+ _stack.push(captured) // restore at callback time
502
+ try { return onfulfilled(v) }
503
+ finally { _stack.pop() }
504
+ }
505
+ return _origThen.call(this, wrapFulfilled, wrapRejected)
506
+ }</pre>
507
+ <div class="box limit"><strong>Known limitation — native await</strong>V8's await optimization (since v7.x) bypasses <code>Promise.resolve(x).then(continuation)</code> — the patch never fires for <code>await</code>-resumed continuations. Mutations after the first <code>await</code> fall back to the module-scoped global. Fix: keep mutations before the <code>await</code>, or chain explicitly with <code>.then()</code>.</div>
508
+
509
+ <!-- PATTERN 07 -->
510
+ <hr>
511
+ <div class="ph"><div class="pn">07</div><div class="pm">
512
+ <div class="pname">Provider / Consumer (Context Protocol)</div>
513
+ <div class="badges">
514
+ <span class="tag" style="background:var(--rb);color:var(--r)">Context</span>
515
+ <span class="tag" style="background:var(--rb);color:var(--r)">@lit/context</span>
516
+ <span class="tag" style="background:var(--rb);color:var(--r)">Tree-scoped DI</span>
517
+ </div>
518
+ <div class="tl">"A provider in the DOM tree answers requests from consumers below it, without knowing who they are."</div>
519
+ </div></div>
520
+ <div class="mm"><strong>Mental model</strong>React Context. A <code>&lt;Provider value={x}&gt;</code> wraps a subtree; any <code>useContext()</code> inside receives <code>x</code>. <code>&lt;schmancy-context&gt;</code> is the same idea for Web Components, using DOM events instead of a VDOM tree walk.</div>
521
+ <pre>
522
+ // On <schmancy-context> connectedCallback:
523
+ for (const tmpl of this.provides) {
524
+ const isolated = tmpl._isolatedInstance()
525
+ // ^ createInstance({ storage: 'memory', initial: tmpl.value }, { isolated: true })
526
+ // ^ seeded from global's current value; always memory-backed
527
+ const ctx = createContext(Symbol.for('schmancy.state:' + tmpl.namespace))
528
+ new ContextProvider(this, { context: ctx, initialValue: isolated })
529
+ }
530
+ // + capture-phase listeners for 16 event types (Tier 3 of active-host chain)</pre>
531
+ <p>Isolated copies are <strong>always memory-backed</strong> — they share the element's lifetime, never persist to localStorage/sessionStorage/IDB. On disconnect, <code>isolated.destroy()</code> flushes pending writes and releases the namespace claim.</p>
532
+ <div class="box note"><strong>Nested contexts</strong>Two nested <code>&lt;schmancy-context .provides=${[cart]}&gt;</code> — the inner one wins. <code>stopPropagation()</code> in the provider stops the event at the closest provider.</div>
533
+
534
+ <!-- PATTERN 08 -->
535
+ <hr>
536
+ <div class="ph"><div class="pn">08</div><div class="pm">
537
+ <div class="pname">Prototype Decoration (ReactiveController)</div>
538
+ <div class="badges">
539
+ <span class="tag" style="background:var(--ab);color:var(--a)">Decorator</span>
540
+ <span class="tag" style="background:var(--ab);color:var(--a)">ReactiveController</span>
541
+ <span class="tag" style="background:var(--ab);color:var(--a)">Template Method</span>
542
+ </div>
543
+ <div class="tl">"Attach subscription lifecycle to a component's class without modifying the class body."</div>
544
+ </div></div>
545
+ <div class="mm"><strong>Mental model</strong>A Decorator Pattern attaches new behaviour to an object without changing its source. Lit's <code>addInitializer</code> is the hook. <code>ReactiveController</code> is the attachment mechanism — a plugin interface: <em>tell me when the host connects and disconnects, and I'll wire subscriptions.</em></div>
546
+ <pre>
547
+ // Option 1: direct render() read — zero code
548
+ class CartView extends SchmancyElement {
549
+ render() { return html`${cart.value.items.length}` }
550
+ }
551
+
552
+ // Option 2: @observe — value as class field
553
+ class CartView extends SchmancyElement {
554
+ @observe(cart) cart!: CartState
555
+ onClick() { console.log(this.cart) } // safe in handlers
556
+ }
557
+
558
+ // Option 3: bindState — no decorator needed
559
+ class CustomHost extends LitElement {
560
+ cart = bindState(this, cart)
561
+ render() { return html`${this.cart.value.items.length}` }
562
+ }</pre>
563
+ <h3>How @observe works — two separate hooks</h3>
564
+ <pre>
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 (via Lit's addInitializer)
572
+ proto.constructor.addInitializer(host => {
573
+ let sub
574
+ host.addController({
575
+ hostConnected() { sub = source.$.subscribe(v => { host[key] = v; host.requestUpdate() }) },
576
+ hostDisconnected() { sub?.unsubscribe() },
577
+ })
578
+ })</pre>
579
+ <p><code>ReactiveController</code> is Lit's <strong>Template Method</strong> pattern: the framework defines <em>when</em> (<code>hostConnected</code>, <code>hostDisconnected</code>); the controller fills in the <em>what</em>.</p>
580
+
581
+ <!-- PATTERN 09 -->
582
+ <hr>
583
+ <div class="ph"><div class="pn">09</div><div class="pm">
584
+ <div class="pname">Microtask Debounce Gate</div>
585
+ <div class="badges">
586
+ <span class="tag" style="background:var(--ob);color:var(--o)">Debounce</span>
587
+ <span class="tag" style="background:var(--ob);color:var(--o)">Event coalescing</span>
588
+ </div>
589
+ <div class="tl">"Many writes in one synchronous task collapse to one storage flush — the gate only lets the last one through."</div>
590
+ </div></div>
591
+ <div class="mm"><strong>Mental model</strong>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. The microtask debounce works at JS-microtask granularity: many <code>set()</code> calls in the same task collapse to one <code>adapter.save()</code>, always carrying the latest value.</div>
592
+ <div class="viz">cart.set({ total: 10 }) <span class="d">→ signal updated immediately ✓</span>
593
+ cart.set({ total: 20 }) <span class="d">→ signal updated immediately ✓</span>
594
+ cart.set({ total: 30 }) <span class="d">→ signal updated immediately ✓</span>
595
+
596
+ scheduleWrite():
597
+ <span class="g">if (scheduledWrite) return</span> <span class="d">← 2nd and 3rd calls exit here</span>
598
+ scheduledWrite = true
599
+ queueMicrotask(() =&gt; {
600
+ scheduledWrite = false
601
+ adapter.save(<span class="h">signal.get()</span>) <span class="d">← reads 30, the latest value</span>
602
+ })
603
+
604
+ Result: <span class="g">one adapter.save(30)</span> — not three.
605
+
606
+ Timeline:
607
+ sync task │ set(10) set(20) set(30) │microtask│ save(30) │
608
+ │ signal always current │boundary │ one I/O │</div>
609
+ <p>On <code>destroy()</code>, two microtask drains guarantee the latest value is flushed: wait for in-flight save → drain scheduled microtask → wait for any write from that microtask → close adapter.</p>
610
+
611
+ <!-- PATTERN 10 -->
612
+ <hr>
613
+ <div class="ph"><div class="pn">10</div><div class="pm">
614
+ <div class="pname">RAII Lifecycle</div>
615
+ <div class="badges">
616
+ <span class="tag" style="background:var(--tb);color:var(--t)">RAII</span>
617
+ <span class="tag" style="background:var(--tb);color:var(--t)">Symbol.dispose</span>
618
+ <span class="tag" style="background:var(--tb);color:var(--t)">TC39 Explicit Resource Mgmt</span>
619
+ </div>
620
+ <div class="tl">"Acquire the resource when you create the object; release it automatically when the object goes out of scope — even if an exception is thrown."</div>
621
+ </div></div>
622
+ <div class="mm"><strong>Mental model</strong>C++ RAII, now in JavaScript. <code>using x = acquireResource()</code> guarantees <code>x[Symbol.dispose]()</code> at block exit — even on thrown exceptions. No <code>try/finally</code>, no <code>afterEach(() => cleanup())</code>.</div>
623
+ <pre>
624
+ // WITHOUT RAII — easy to forget
625
+ it('test', () => {
626
+ const cart = state('test/cart').memory({}) // namespace permanently claimed if forgotten
627
+
628
+ })
629
+
630
+ // WITH RAII — cleanup is structural
631
+ it('test', () => {
632
+ using cart = state('test/cart').memory({})
633
+
634
+ }) // [Symbol.dispose]() fires here — even if the test threw
635
+
636
+ // IDB — async variant
637
+ it('test', async () => {
638
+ await using cart = state('test/cart').idb({})
639
+
640
+ }) // [Symbol.asyncDispose]() awaited — IDB fully closed before next test</pre>
641
+ <h3>Dispose sequence</h3>
642
+ <pre>
643
+ [Symbol.dispose]():
644
+ if (disposed) return // idempotent
645
+ disposed = true
646
+ void flushAndClose() // fire-and-forget sync backends
647
+ claimed.delete(namespace) // releases namespace → re-registrable
648
+
649
+ flushAndClose():
650
+ await pendingWrite // drain in-flight save
651
+ await new Promise(queueMicrotask) // drain scheduled microtask
652
+ await pendingWrite // catch write from microtask
653
+ await adapter.close() // IDB only</pre>
654
+
655
+ <!-- PATTERN 11 -->
656
+ <hr>
657
+ <div class="ph"><div class="pn">11</div><div class="pm">
658
+ <div class="pname">Flyweight / Global Registry</div>
659
+ <div class="badges">
660
+ <span class="tag" style="background:var(--vb);color:var(--v)">Flyweight</span>
661
+ <span class="tag" style="background:var(--vb);color:var(--v)">Global registry</span>
662
+ <span class="tag" style="background:var(--vb);color:var(--v)">Module Federation</span>
663
+ </div>
664
+ <div class="tl">"Share one instance across all importers — even across separate bundle copies — by parking it in a process-global store keyed by Symbol.for()."</div>
665
+ </div></div>
666
+ <div class="mm"><strong>Mental model</strong>In a Module Federation setup, Bundle A and Bundle B each import <code>schmancy/state</code>. Without coordination, each creates its own <code>instances</code> Map — producing two separate <code>cart</code> objects. Writing through A never updates B. Fix: park everything on <code>globalThis</code> under <code>Symbol.for()</code> keys, so the first bundle to load creates the store and every subsequent bundle reuses it.</div>
667
+ <div class="viz"><span class="d">// Five global slots — all under Symbol.for() for cross-bundle sharing</span>
668
+ globalThis[<span class="h">Symbol.for('schmancy.state.claimed')</span>] = Set&lt;string&gt;
669
+ globalThis[<span class="h">Symbol.for('schmancy.state.instances')</span>] = Map&lt;'ns@storage', instance&gt;
670
+ globalThis[<span class="h">Symbol.for('schmancy.state.hostResolverCache')</span>] = WeakMap
671
+ globalThis[<span class="h">Symbol.for('schmancy.state.activeHost.stack')</span>] = Array
672
+ globalThis[<span class="h">Symbol.for('schmancy.state.activeHost.eventHost')</span>] = slot
673
+
674
+ Bundle A loads first → creates all five
675
+ Bundle B loads later → <span class="g">finds existing, reuses</span>
676
+ cart₁.set({x:1}) → cart₂.value === {x:1} <span class="g">✓</span></div>
677
+
678
+ <!-- PATTERN 12 -->
679
+ <hr>
680
+ <div class="ph"><div class="pn">12</div><div class="pm">
681
+ <div class="pname">How the Patterns Assemble</div>
682
+ <div class="tl">The full stack — each layer's output is the next layer's input.</div>
683
+ </div></div>
684
+ <div class="viz"><span class="d">┌──────────────────────────────────────────────────────────────────┐</span>
685
+ <span class="d">│</span> <span class="h">Consumer layer</span> <span class="d">│</span>
686
+ <span class="d">│</span> cart.value / cart.set() / cart.$ / await cart.ready <span class="d">│</span>
687
+ <span class="d">│</span> @observe(cart) / bindState(this, cart) / computed(...) <span class="d">│</span>
688
+ <span class="d">├──────────────────────────────────────────────────────────────────┤</span>
689
+ <span class="d">│</span> <span class="ac">08 Prototype Decoration</span> @observe / bindState / ReactiveController <span class="d">│</span>
690
+ <span class="d">├──────────────────────────────────────────────────────────────────┤</span>
691
+ <span class="d">│</span> <span class="ac">02 Transparent Proxy</span> every read/write → resolveContextual <span class="d">│</span>
692
+ <span class="d">├──────────────────────────────────────────────────────────────────┤</span>
693
+ <span class="d">│</span> <span class="ac">06 Zone</span> resolveActiveHost(): stack / Promise patch / event slot <span class="d">│</span>
694
+ <span class="d">├──────────────────────────────────────────────────────────────────┤</span>
695
+ <span class="d">│</span> <span class="ac">07 Provider/Consumer</span> ContextRequestEvent → nearest context wins <span class="d">│</span>
696
+ <span class="d">├──────────────────────────────────────────────────────────────────┤</span>
697
+ <span class="d">│</span> <span class="ac">01 Singleton</span> module-scoped global — the fallback <span class="d">│</span>
698
+ <span class="d">├──────────────────────────────────────────────────────────────────┤</span>
699
+ <span class="d">│</span> <span class="ac">04 Dual Observer</span> Signal (pull) + Observable (push) over one value <span class="d">│</span>
700
+ <span class="d">├──────────────────────────────────────────────────────────────────┤</span>
701
+ <span class="d">│</span> <span class="ac">05 Command Dispatch</span> Kind&lt;T&gt; → MapAPI / SetAPI / ObjectAPI / … <span class="d">│</span>
702
+ <span class="d">│</span> all commits funnel through commit(next) <span class="d">│</span>
703
+ <span class="d">├──────────────────────────────────────────────────────────────────┤</span>
704
+ <span class="d">│</span> <span class="ac">09 Debounce Gate</span> burst writes → one adapter.save() per microtask <span class="d">│</span>
705
+ <span class="d">├──────────────────────────────────────────────────────────────────┤</span>
706
+ <span class="d">│</span> <span class="ac">03 Adapter</span> Memory / WebStorage / IndexedDB — uniform interface <span class="d">│</span>
707
+ <span class="d">├──────────────────────────────────────────────────────────────────┤</span>
708
+ <span class="d">│</span> <span class="ac">10 RAII</span> Symbol.dispose / Symbol.asyncDispose — flush + close <span class="d">│</span>
709
+ <span class="d">├──────────────────────────────────────────────────────────────────┤</span>
710
+ <span class="d">│</span> <span class="ac">11 Flyweight</span> globalThis[Symbol.for(...)] — cross-bundle sharing <span class="d">│</span>
711
+ <span class="d">└──────────────────────────────────────────────────────────────────┘</span></div>
712
+
713
+ <table style="margin-top:14px">
714
+ <thead><tr><th>Pattern</th><th>Problem it solves</th><th>Alternative rejected</th></tr></thead>
715
+ <tbody>
716
+ <tr><td><strong>01 Singleton</strong></td><td>State survives component remounts</td><td>Class-instance state, Redux</td></tr>
717
+ <tr><td><strong>02 Transparent Proxy</strong></td><td>Consumer code unchanged whether scoped or global</td><td>Props drilling, separate variable per scope</td></tr>
718
+ <tr><td><strong>03 Adapter</strong></td><td>Swap storage without touching state logic</td><td>Hardcoded localStorage calls</td></tr>
719
+ <tr><td><strong>04 Dual Observer</strong></td><td>Both Lit (pull) and RxJS (push) need reactivity</td><td>RxJS-only, signals-only</td></tr>
720
+ <tr><td><strong>05 Command Dispatch</strong></td><td>Ergonomic writes per shape, no type casting</td><td>One generic setState()</td></tr>
721
+ <tr><td><strong>06 Zone</strong></td><td>Know which DOM element is executing, across async</td><td>Pass host as parameter, Zone.js (~50 KB)</td></tr>
722
+ <tr><td><strong>07 Provider/Consumer</strong></td><td>Subtree-scoped state, no consumer code changes</td><td>React context (framework-specific)</td></tr>
723
+ <tr><td><strong>08 Prototype Decoration</strong></td><td>State bound to field with lifecycle guarantees</td><td>Manual subscribe/unsubscribe per component</td></tr>
724
+ <tr><td><strong>09 Debounce Gate</strong></td><td>Burst writes → one I/O call</td><td>Write on every set(), manual batching</td></tr>
725
+ <tr><td><strong>10 RAII</strong></td><td>Guaranteed cleanup even on test failure</td><td>afterEach(), try/finally</td></tr>
726
+ <tr><td><strong>11 Flyweight</strong></td><td>One instance across bundle copies</td><td>Module-level singleton (breaks MF)</td></tr>
727
+ </tbody>
728
+ </table>
729
+
730
+ <!-- ════════════════════════════════════════════════════════ -->
731
+ <hr class="thick">
732
+ <div class="part">Part 3 — Technical Reference</div>
733
+ <p class="part-sub">Low-level detail for every file, every function, every invariant. Use as a reference during debugging or code review.</p>
734
+
735
+ <h2>File map</h2>
736
+ <pre>
737
+ packages/schmancy/src/state/
738
+ index.ts — factory, types, write APIs, context resolution,
739
+ observe decorator, bindState, stateFromObservable, effect
740
+ persist.ts — StorageAdapter interface + four implementations
741
+ active-host.ts — AsyncContext polyfill (stack + Promise.then patch + event-host slot)
742
+ schmancy-context.ts — &lt;schmancy-context&gt; element (scoping primitive)</pre>
743
+
744
+ <h2>Factory — three TypeScript overloads</h2>
745
+ <pre>
746
+ // Overload A — registry augmentation (typo-safe, autocomplete)
747
+ declare module '@mhmo91/schmancy/state' {
748
+ interface SchmancyStateRegistry { 'hannah/cart': CartState }
749
+ }
750
+ const cart = state('hannah/cart').session({ items: [], total: 0 })
751
+
752
+ // Overload B — explicit T arg (inline literals)
753
+ const cart = state&lt;CartState&gt;('hannah/cart').session({ items: [], total: 0 })
754
+
755
+ // Overload C — typed const (T inferred, no cast)
756
+ const initial: CartState = { items: [], total: 0 }
757
+ const cart = state('hannah/cart').session(initial)</pre>
758
+ <p>All return a <code>NamespaceHandle</code> with four backend methods. Each returns <code>State&lt;NS, T, StorageBackend&gt;</code> = <code>BaseAPI + WriteAPI&lt;T&gt;</code>. Namespace constraint: <code>FeatureNamespace = `${string}/${string}`</code> — enforced at compile time and at runtime.</p>
759
+
760
+ <h2>Instance construction (createInstance)</h2>
761
+ <pre>
762
+ createInstance({ namespace, initial, storage }, { isolated? })
763
+
764
+ ├─ createAdapter(storage, namespace) → StorageAdapter
765
+ ├─ new Signal.State&lt;T&gt;(initial) → TC39 signal
766
+ ├─ adapter.load() → signal.set(stored) if not null
767
+ │ .then(markLoaded) → loaded = true; ready resolves
768
+ ├─ signalToObservable(signal) → Observable via Signal.subtle.Watcher
769
+ └─ buildWriteApi(internal, detectKind(initial))→ variant write methods</pre>
770
+ <p>Global instances route all reads/writes through <code>resolveContextual</code>. Isolated instances (<code>{ isolated: true }</code>) read/write their own signal directly — no context resolution, no recursion.</p>
771
+
772
+ <h2>Storage backends</h2>
773
+ <table>
774
+ <thead><tr><th>Method</th><th>Backing</th><th>Survives refresh</th><th>Survives close</th><th>Notes</th></tr></thead>
775
+ <tbody>
776
+ <tr><td><code>.memory()</code></td><td>JS heap</td><td>❌</td><td>❌</td><td>Default for isolated copies</td></tr>
777
+ <tr><td><code>.session()</code></td><td>sessionStorage</td><td>✅</td><td>❌ (per-tab)</td><td>Map/Set JSON tunnel</td></tr>
778
+ <tr><td><code>.local()</code></td><td>localStorage</td><td>✅</td><td>✅</td><td>Map/Set JSON tunnel</td></tr>
779
+ <tr><td><code>.idb()</code></td><td>IndexedDB</td><td>✅</td><td>✅</td><td>Also AsyncDisposable</td></tr>
780
+ </tbody>
781
+ </table>
782
+
783
+ <h2>Context resolution (resolveContextual)</h2>
784
+ <pre>
785
+ resolveContextual(namespace, fallback):
786
+ 1. host = resolveActiveHost() — may return undefined
787
+ 2. if undefined → return fallback — module-scoped global, done
788
+ 3. check hostResolverCache[host][namespace]
789
+ → cache hit → return immediately (O(1))
790
+ 4. dispatch ContextRequestEvent from host
791
+ → provider responds → resolved = isolated copy
792
+ → no response → resolved = fallback
793
+ 5. cache result in hostResolverCache[host][namespace]
794
+ 6. return resolved</pre>
795
+
796
+ <h2>Active-host resolution chain (resolveActiveHost)</h2>
797
+ <pre>
798
+ 1. _activeHost.get() ← explicit stack (SchmancyElement prototype-wrap)
799
+ 2. _eventHostSlot.host ← capture-phase event on &lt;schmancy-context&gt;
800
+ 3. document.activeElement ← keyboard / focus
801
+ 4. undefined ← caller uses module-scoped global</pre>
802
+
803
+ <h2>Component binding options</h2>
804
+ <table>
805
+ <thead><tr><th>Option</th><th>When to use</th><th>Mechanism</th></tr></thead>
806
+ <tbody>
807
+ <tr><td><code>cart.value</code> in <code>render()</code></td><td>Default — 80% of cases</td><td>SignalWatcher auto-tracking</td></tr>
808
+ <tr><td><code>@observe(cart) field!: T</code></td><td>Need value as class field (handlers, devtools)</td><td>addInitializer + ReactiveController</td></tr>
809
+ <tr><td><code>bindState(this, cart)</code></td><td>Non-SchmancyElement Lit host</td><td>ReactiveController directly</td></tr>
810
+ </tbody>
811
+ </table>
812
+
813
+ <h2>computed(), effect(), stateFromObservable()</h2>
814
+ <p><strong><code>computed(fn)</code></strong> — re-export of <code>@lit-labs/signals/computed</code>. Reading <code>state.value</code> inside the callback auto-tracks it. Reference-equality dedup by default.</p>
815
+ <p><strong><code>effect(fn)</code></strong> — runs <code>fn</code> immediately (registers deps), then re-runs microtask-coalesced whenever any read signal changes. Returns <code>Disposable</code>.</p>
816
+ <p><strong><code>stateFromObservable(obs$, namespace, initial)</code></strong> — creates a state, subscribes to the observable, calls <code>signal.set(value)</code> directly on each emission (bypasses <code>resolveContextual</code>). Wraps <code>[Symbol.dispose]</code> to also unsubscribe.</p>
817
+
818
+ <h2>Full data-flow diagram</h2>
819
+ <div class="viz">User calls <span class="h">cart.set({ total: 12 })</span>
820
+
821
+
822
+ <span class="ac">resolveContextual(namespace, isolatedTarget)</span>
823
+
824
+ ├─ resolveActiveHost()
825
+ │ ├─ 1. _activeHost stack <span class="d">(SchmancyElement prototype-wrap)</span>
826
+ │ ├─ 2. _eventHostSlot <span class="d">(capture-phase event)</span>
827
+ │ ├─ 3. document.activeElement
828
+ │ └─ 4. undefined → isolatedTarget <span class="d">(module global)</span>
829
+
830
+ ├─ cache hit? → return cached target
831
+
832
+ └─ dispatch ContextRequestEvent from host
833
+ ├─ &lt;schmancy-context&gt; responds → <span class="g">isolated copy</span>
834
+ └─ no response → <span class="y">module-scoped global</span>
835
+
836
+
837
+ target.set({ total: 12 })
838
+ ObjectAPI.set → commit({ ...current, ...patch })
839
+
840
+ ┌─────────────┴──────────────────────┐
841
+ ▼ ▼
842
+ <span class="h">signal.set(next)</span> <span class="ac">scheduleWrite(internal)</span>
843
+ synchronous │
844
+ │ queueMicrotask
845
+ │ │
846
+ ▼ ▼
847
+ Signal.subtle.Watcher fires <span class="g">adapter.save(signal.get())</span>
848
+ │ <span class="d">(localStorage / IDB / …)</span>
849
+
850
+ signalToObservable → queueMicrotask → emit
851
+
852
+ ┌──────┴────────────────────────┐
853
+ ▼ ▼
854
+ <span class="h">SignalWatcher → requestUpdate()</span> <span class="ac">RxJS subscribers</span>
855
+ <span class="g">component re-renders</span> <span class="d">pipe(takeUntil(disconnecting))</span></div>
856
+
857
+ <h2>Key design invariants</h2>
858
+ <table>
859
+ <thead><tr><th>Invariant</th><th>Where enforced</th></tr></thead>
860
+ <tbody>
861
+ <tr><td>Namespace must contain <code>/</code></td><td>TypeScript template literal type + runtime <code>TypeError</code></td></tr>
862
+ <tr><td>One global per namespace</td><td><code>claimed</code> Set + <code>instances</code> Map on <code>globalThis</code></td></tr>
863
+ <tr><td>Context dispatch is O(1) after first</td><td><code>hostResolverCache</code> WeakMap keyed by <code>(host, namespace)</code></td></tr>
864
+ <tr><td>Isolated copy never calls <code>resolveContextual</code></td><td><code>{ isolated: true }</code> branch in <code>createInstance</code></td></tr>
865
+ <tr><td>Write persists async, never blocks signal</td><td><code>scheduleWrite</code> → <code>queueMicrotask</code></td></tr>
866
+ <tr><td><code>Signal.State.set()</code> is synchronous</td><td>TC39 signal polyfill contract</td></tr>
867
+ <tr><td>Multiple bundle copies share one registry</td><td>All global state under <code>Symbol.for(...)</code> on <code>globalThis</code></td></tr>
868
+ <tr><td>No in-place mutation of held value</td><td>Spread / <code>new Map()</code> / <code>new Set()</code> / immer <code>produce()</code></td></tr>
869
+ </tbody>
870
+ </table>
871
+
872
+ <h2>Quick-decision guide</h2>
873
+ <table>
874
+ <thead><tr><th>I need…</th><th>Use</th></tr></thead>
875
+ <tbody>
876
+ <tr><td>State in a Lit component template</td><td><code>cart.value</code> in <code>render()</code></td></tr>
877
+ <tr><td>State as a class field (event handlers)</td><td><code>@observe(cart) cart!: CartState</code></td></tr>
878
+ <tr><td>State in a non-SchmancyElement Lit host</td><td><code>bindState(this, cart)</code></td></tr>
879
+ <tr><td>Derived / computed value</td><td><code>computed(() => cart.value.items.length)</code></td></tr>
880
+ <tr><td>Side effect that reruns on change</td><td><code>effect(() => { … cart.value … })</code></td></tr>
881
+ <tr><td>Lift an Observable into state</td><td><code>stateFromObservable(obs$, 'ns/key', initial)</code></td></tr>
882
+ <tr><td>Isolated subtree copy</td><td><code>&lt;schmancy-context .provides=${[cart]}&gt;</code></td></tr>
883
+ <tr><td>Per-test isolation</td><td><code>using cart = state('test/x').memory(initial)</code></td></tr>
884
+ <tr><td>Persist across page refresh (same tab)</td><td><code>.session(initial)</code></td></tr>
885
+ <tr><td>Persist across tab close</td><td><code>.local(initial)</code> or <code>.idb(initial)</code></td></tr>
886
+ <tr><td>Large collections (&gt;100 entries)</td><td><code>.idb(initial)</code></td></tr>
887
+ <tr><td>Wait for async hydration before first read</td><td><code>await cart.ready</code> / <code>if (cart.loaded)</code></td></tr>
888
+ </tbody>
889
+ </table>
890
+
891
+ <hr>
892
+ <p style="font-size:.68rem;color:#9ca3af;text-align:center;padding:6px 0">
893
+ Schmancy State — Complete Developer Reference · source of truth: <code>docs/schmancy-state.md</code> · 2026-05-08
894
+ </p>
895
+ </div>
896
+ </body>
897
+ </html>