sygnal 5.3.7 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (438) hide show
  1. package/CHANGELOG.md +646 -0
  2. package/README.md +77 -47
  3. package/dist/astro/client.cjs.js +33 -8114
  4. package/dist/astro/client.cjs.js.map +1 -0
  5. package/dist/astro/client.mjs +33 -8114
  6. package/dist/astro/client.mjs.map +1 -0
  7. package/dist/astro/index.cjs.js +1039 -9
  8. package/dist/astro/index.cjs.js.map +1 -0
  9. package/dist/astro/index.mjs +1038 -9
  10. package/dist/astro/index.mjs.map +1 -0
  11. package/dist/astro/server.cjs.js +3400 -166
  12. package/dist/astro/server.cjs.js.map +1 -0
  13. package/dist/astro/server.mjs +3400 -166
  14. package/dist/astro/server.mjs.map +1 -0
  15. package/dist/devtools.cjs.js +1431 -0
  16. package/dist/devtools.cjs.js.map +1 -0
  17. package/dist/devtools.esm.js +1419 -0
  18. package/dist/devtools.esm.js.map +1 -0
  19. package/dist/diagnostics.cjs.js +4033 -0
  20. package/dist/diagnostics.cjs.js.map +1 -0
  21. package/dist/diagnostics.esm.js +4003 -0
  22. package/dist/diagnostics.esm.js.map +1 -0
  23. package/dist/element.cjs.js +230 -0
  24. package/dist/element.cjs.js.map +1 -0
  25. package/dist/element.esm.js +228 -0
  26. package/dist/element.esm.js.map +1 -0
  27. package/dist/guide/accessibility.md +257 -0
  28. package/dist/guide/adapters.md +221 -0
  29. package/dist/guide/behaviors.md +312 -0
  30. package/dist/guide/browser-sources.md +203 -0
  31. package/dist/guide/drag-and-drop.md +342 -0
  32. package/dist/guide/element-commands.md +306 -0
  33. package/dist/guide/error-boundaries.md +169 -0
  34. package/dist/guide/forms-reference.md +381 -0
  35. package/dist/guide/forms.md +250 -0
  36. package/dist/guide/http.md +299 -0
  37. package/dist/guide/inputs.md +173 -0
  38. package/dist/guide/persistence.md +245 -0
  39. package/dist/guide/recipes/carousel.md +155 -0
  40. package/dist/guide/recipes/charts.md +177 -0
  41. package/dist/guide/recipes/code-editor.md +135 -0
  42. package/dist/guide/recipes/data-grid.md +155 -0
  43. package/dist/guide/recipes/data-table.md +196 -0
  44. package/dist/guide/recipes/i18n.md +222 -0
  45. package/dist/guide/recipes/icons.md +115 -0
  46. package/dist/guide/recipes/overview.md +33 -0
  47. package/dist/guide/recipes/rich-text.md +154 -0
  48. package/dist/guide/resources.md +471 -0
  49. package/dist/guide/ssr.md +255 -0
  50. package/dist/guide/timers.md +178 -0
  51. package/dist/guide/ui/accordion.md +93 -0
  52. package/dist/guide/ui/combobox.md +144 -0
  53. package/dist/guide/ui/dialog.md +124 -0
  54. package/dist/guide/ui/disclosure.md +60 -0
  55. package/dist/guide/ui/menu.md +125 -0
  56. package/dist/guide/ui/overview.md +166 -0
  57. package/dist/guide/ui/popover.md +101 -0
  58. package/dist/guide/ui/select.md +114 -0
  59. package/dist/guide/ui/tabs.md +161 -0
  60. package/dist/guide/ui/toaster.md +152 -0
  61. package/dist/guide/ui/tooltip.md +103 -0
  62. package/dist/guide/undo.md +178 -0
  63. package/dist/guide/virtual-collections.md +183 -0
  64. package/dist/guide/web-components.md +179 -0
  65. package/dist/guide/widgets.md +217 -0
  66. package/dist/index.cjs.js +15283 -6279
  67. package/dist/index.cjs.js.map +1 -0
  68. package/dist/index.d.ts +3739 -370
  69. package/dist/index.esm.js +15230 -6274
  70. package/dist/index.esm.js.map +1 -0
  71. package/dist/jsx-dev-runtime.cjs.js +27 -329
  72. package/dist/jsx-dev-runtime.cjs.js.map +1 -0
  73. package/dist/jsx-dev-runtime.esm.js +20 -322
  74. package/dist/jsx-dev-runtime.esm.js.map +1 -0
  75. package/dist/jsx-runtime.cjs.js +27 -329
  76. package/dist/jsx-runtime.cjs.js.map +1 -0
  77. package/dist/jsx-runtime.esm.js +20 -322
  78. package/dist/jsx-runtime.esm.js.map +1 -0
  79. package/dist/jsx.cjs.js +11 -309
  80. package/dist/jsx.cjs.js.map +1 -0
  81. package/dist/jsx.esm.js +9 -310
  82. package/dist/jsx.esm.js.map +1 -0
  83. package/dist/react.cjs.js +117 -0
  84. package/dist/react.cjs.js.map +1 -0
  85. package/dist/react.esm.js +115 -0
  86. package/dist/react.esm.js.map +1 -0
  87. package/dist/shims/globalthis.cjs +20 -0
  88. package/dist/sygnal.min.js +2 -1
  89. package/dist/sygnal.min.js.map +1 -0
  90. package/dist/ui-combobox.cjs.js +169 -0
  91. package/dist/ui-combobox.cjs.js.map +1 -0
  92. package/dist/ui-combobox.esm.js +148 -0
  93. package/dist/ui-combobox.esm.js.map +1 -0
  94. package/dist/ui-menu.cjs.js +87 -0
  95. package/dist/ui-menu.cjs.js.map +1 -0
  96. package/dist/ui-menu.esm.js +66 -0
  97. package/dist/ui-menu.esm.js.map +1 -0
  98. package/dist/ui-select.cjs.js +104 -0
  99. package/dist/ui-select.cjs.js.map +1 -0
  100. package/dist/ui-select.esm.js +83 -0
  101. package/dist/ui-select.esm.js.map +1 -0
  102. package/dist/ui.cjs.js +738 -0
  103. package/dist/ui.cjs.js.map +1 -0
  104. package/dist/ui.esm.js +727 -0
  105. package/dist/ui.esm.js.map +1 -0
  106. package/dist/vike/ClientOnly.cjs.js +5 -14
  107. package/dist/vike/ClientOnly.cjs.js.map +1 -0
  108. package/dist/vike/ClientOnly.mjs +5 -14
  109. package/dist/vike/ClientOnly.mjs.map +1 -0
  110. package/dist/vike/{+config.js → config/+config.js} +10 -1
  111. package/dist/vike/config/+config.js.map +1 -0
  112. package/dist/vike/config/package.json +6 -0
  113. package/dist/vike/onRenderClient.cjs.js +166 -95
  114. package/dist/vike/onRenderClient.cjs.js.map +1 -0
  115. package/dist/vike/onRenderClient.mjs +166 -95
  116. package/dist/vike/onRenderClient.mjs.map +1 -0
  117. package/dist/vike/onRenderHtml.cjs.js +60 -24
  118. package/dist/vike/onRenderHtml.cjs.js.map +1 -0
  119. package/dist/vike/onRenderHtml.mjs +61 -25
  120. package/dist/vike/onRenderHtml.mjs.map +1 -0
  121. package/dist/vite/plugin.cjs.js +926 -65
  122. package/dist/vite/plugin.cjs.js.map +1 -0
  123. package/dist/vite/plugin.mjs +925 -65
  124. package/dist/vite/plugin.mjs.map +1 -0
  125. package/dist/zag.cjs.js +408 -0
  126. package/dist/zag.cjs.js.map +1 -0
  127. package/dist/zag.esm.js +405 -0
  128. package/dist/zag.esm.js.map +1 -0
  129. package/llms.txt +313 -0
  130. package/package.json +109 -16
  131. package/src/astro/client.ts +40 -18
  132. package/src/astro/index.d.ts +48 -1
  133. package/src/astro/index.ts +106 -9
  134. package/src/astro/server.ts +11 -3
  135. package/src/collection.ts +6 -90
  136. package/src/core/actions.ts +134 -0
  137. package/src/core/cell.ts +202 -0
  138. package/src/core/debug.ts +23 -0
  139. package/src/core/define.ts +242 -0
  140. package/src/core/hooks.ts +314 -0
  141. package/src/core/hosts/collection.ts +325 -0
  142. package/src/core/hosts/switchable.ts +146 -0
  143. package/src/core/instance.ts +592 -0
  144. package/src/core/markers/clientonly.ts +13 -0
  145. package/src/core/markers/lazy.ts +40 -0
  146. package/src/core/markers/portal.ts +96 -0
  147. package/src/core/markers/suspense.ts +41 -0
  148. package/src/core/markers/transition.ts +70 -0
  149. package/src/core/registry.ts +25 -0
  150. package/src/core/runtime.ts +573 -0
  151. package/src/core/statics.ts +104 -0
  152. package/src/core/teardown.ts +60 -0
  153. package/src/core/view.ts +37 -0
  154. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  155. package/src/cycle/dom/ElementFinder.ts +7 -9
  156. package/src/cycle/dom/EventDelegator.ts +174 -223
  157. package/src/cycle/dom/IsolateModule.ts +65 -35
  158. package/src/cycle/dom/MainDOMSource.ts +13 -2
  159. package/src/cycle/dom/PriorityQueue.ts +4 -2
  160. package/src/cycle/dom/SymbolTree.ts +18 -47
  161. package/src/cycle/dom/classNameModule.ts +76 -0
  162. package/src/cycle/dom/controlledInputModule.ts +50 -0
  163. package/src/cycle/dom/enrichEventStream.ts +29 -45
  164. package/src/cycle/dom/fragment.ts +7 -0
  165. package/src/cycle/dom/isolate.ts +30 -27
  166. package/src/cycle/dom/makeDOMDriver.ts +115 -57
  167. package/src/cycle/dom/mockDOMSource.ts +83 -9
  168. package/src/cycle/dom/modules.ts +21 -4
  169. package/src/cycle/dom/propsModule.ts +65 -0
  170. package/src/cycle/dom/selectModule.ts +22 -17
  171. package/src/cycle/dom/snabbdom.ts +1 -6
  172. package/src/cycle/dom/thunk.ts +3 -0
  173. package/src/cycle/dom/utils.ts +98 -0
  174. package/src/cycle/dom/viewTransition.ts +43 -0
  175. package/src/cycle/state/StateSource.ts +20 -4
  176. package/src/cycle/state/objIsEqual.ts +50 -0
  177. package/src/cycle/state/types.ts +0 -10
  178. package/src/defineComponent.ts +34 -0
  179. package/src/devtools.d.ts +151 -0
  180. package/src/devtools.ts +34 -0
  181. package/src/element.d.ts +59 -0
  182. package/src/element.ts +235 -0
  183. package/src/extra/backoff.ts +9 -0
  184. package/src/extra/behaviors.ts +169 -0
  185. package/src/extra/browserSignals.ts +23 -0
  186. package/src/extra/browserSources.ts +326 -0
  187. package/src/extra/command.ts +3 -3
  188. package/src/extra/controls.ts +47 -0
  189. package/src/extra/copyAsTest.ts +286 -0
  190. package/src/extra/devtools.ts +109 -31
  191. package/src/extra/devtoolsActions.ts +379 -0
  192. package/src/extra/devtoolsHook.ts +9 -0
  193. package/src/extra/devtoolsNext.ts +83 -0
  194. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  195. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  196. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  197. package/src/extra/diagnostics/checks/collections.ts +67 -0
  198. package/src/extra/diagnostics/checks/controls.ts +148 -0
  199. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  200. package/src/extra/diagnostics/checks/dom.ts +250 -0
  201. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  202. package/src/extra/diagnostics/checks/events.ts +83 -0
  203. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  204. package/src/extra/diagnostics/checks/forms.ts +163 -0
  205. package/src/extra/diagnostics/checks/index.ts +202 -0
  206. package/src/extra/diagnostics/checks/inspect.ts +427 -0
  207. package/src/extra/diagnostics/checks/next.ts +417 -0
  208. package/src/extra/diagnostics/checks/persist.ts +54 -0
  209. package/src/extra/diagnostics/checks/props.ts +56 -0
  210. package/src/extra/diagnostics/checks/public.d.ts +302 -0
  211. package/src/extra/diagnostics/checks/replies.ts +128 -0
  212. package/src/extra/diagnostics/checks/router.ts +100 -0
  213. package/src/extra/diagnostics/checks/rxjsHints.ts +88 -0
  214. package/src/extra/diagnostics/checks/shared.ts +203 -0
  215. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  216. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  217. package/src/extra/diagnostics/checks/state.ts +148 -0
  218. package/src/extra/diagnostics/checks/statics.ts +48 -0
  219. package/src/extra/diagnostics/checks/strict.ts +55 -0
  220. package/src/extra/diagnostics/checks/timers.ts +80 -0
  221. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  222. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  223. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  224. package/src/extra/diagnostics/checks/wiring.ts +121 -0
  225. package/src/extra/diagnostics/codes.ts +477 -0
  226. package/src/extra/diagnostics/index.ts +353 -0
  227. package/src/extra/diagnostics/legacy.ts +76 -0
  228. package/src/extra/driverFactories.ts +172 -60
  229. package/src/extra/elementCommands.ts +53 -0
  230. package/src/extra/eventDriver.ts +4 -0
  231. package/src/extra/fetchDriver.ts +519 -0
  232. package/src/extra/flatten.ts +75 -0
  233. package/src/extra/focusWithin.ts +27 -0
  234. package/src/extra/form.ts +268 -0
  235. package/src/extra/formHelpers.ts +139 -0
  236. package/src/extra/head.ts +106 -0
  237. package/src/extra/hmr.ts +2 -3
  238. package/src/extra/owned.ts +24 -0
  239. package/src/extra/pager.ts +50 -0
  240. package/src/extra/persist.ts +133 -0
  241. package/src/extra/pwa.ts +1 -1
  242. package/src/extra/queryCache.ts +107 -0
  243. package/src/extra/reducers.ts +13 -6
  244. package/src/extra/reduxDevtools.ts +92 -0
  245. package/src/extra/replies.ts +54 -0
  246. package/src/extra/router.ts +323 -0
  247. package/src/extra/run.ts +94 -210
  248. package/src/extra/selection.ts +76 -0
  249. package/src/extra/socketDriver.ts +265 -0
  250. package/src/extra/sortable.ts +377 -0
  251. package/src/extra/ssr.ts +427 -160
  252. package/src/extra/standardSchema.ts +27 -0
  253. package/src/extra/testing.ts +3158 -190
  254. package/src/extra/timers.ts +95 -0
  255. package/src/extra/undo.ts +261 -0
  256. package/src/extra/viewTransitions.ts +38 -0
  257. package/src/extra/virtual.ts +513 -0
  258. package/src/extra/widget.ts +201 -0
  259. package/src/extra/xstreamExtras.ts +269 -0
  260. package/src/index.d.ts +3012 -87
  261. package/src/index.ts +32 -10
  262. package/src/jsx-runtime.ts +15 -1
  263. package/src/jsx.ts +2 -1
  264. package/src/lazy.ts +50 -7
  265. package/src/portal.ts +3 -1
  266. package/src/pragma/index.ts +262 -134
  267. package/src/react-peers.d.ts +5 -0
  268. package/src/react.d.ts +43 -0
  269. package/src/react.ts +110 -0
  270. package/src/shared.ts +48 -0
  271. package/src/slot.ts +1 -1
  272. package/src/suspense.ts +3 -1
  273. package/src/switchable.ts +6 -119
  274. package/src/transition.ts +3 -1
  275. package/src/ui/accordion.ts +64 -0
  276. package/src/ui/dialog.ts +129 -0
  277. package/src/ui/disclosure.ts +35 -0
  278. package/src/ui/popover.ts +45 -0
  279. package/src/ui/shared.ts +94 -0
  280. package/src/ui/tabs.ts +82 -0
  281. package/src/ui/toaster.ts +198 -0
  282. package/src/ui/tooltip.ts +70 -0
  283. package/src/ui/zag/combobox.ts +115 -0
  284. package/src/ui/zag/menu.ts +40 -0
  285. package/src/ui/zag/select.ts +54 -0
  286. package/src/ui/zag/shared.ts +43 -0
  287. package/src/ui-combobox.d.ts +20 -0
  288. package/src/ui-combobox.ts +6 -0
  289. package/src/ui-menu.d.ts +30 -0
  290. package/src/ui-menu.ts +6 -0
  291. package/src/ui-select.d.ts +17 -0
  292. package/src/ui-select.ts +6 -0
  293. package/src/ui-zag-types.d.ts +42 -0
  294. package/src/ui.d.ts +208 -0
  295. package/src/ui.ts +15 -0
  296. package/src/vike/+config.ts +9 -1
  297. package/src/vike/ClientOnly.ts +1 -1
  298. package/src/vike/onRenderClient.ts +144 -96
  299. package/src/vike/onRenderHtml.ts +69 -26
  300. package/src/vike/types.ts +27 -4
  301. package/src/vite/globalthis-shim.ts +18 -0
  302. package/src/vite/globalthis.ts +56 -0
  303. package/src/vite/plugin.d.ts +114 -2
  304. package/src/vite/plugin.ts +920 -56
  305. package/src/zag.d.ts +64 -0
  306. package/src/zag.ts +238 -0
  307. package/dist/astro/astro/client.d.ts +0 -5
  308. package/dist/astro/astro/index.d.ts +0 -16
  309. package/dist/astro/astro/server.d.ts +0 -12
  310. package/dist/astro/client.d.ts +0 -5
  311. package/dist/astro/client.esm.js +0 -4665
  312. package/dist/astro/collection.d.ts +0 -12
  313. package/dist/astro/component.d.ts +0 -22
  314. package/dist/astro/cycle/dom/BodyDOMSource.d.ts +0 -10
  315. package/dist/astro/cycle/dom/DOMSource.d.ts +0 -11
  316. package/dist/astro/cycle/dom/DocumentDOMSource.d.ts +0 -12
  317. package/dist/astro/cycle/dom/ElementFinder.d.ts +0 -8
  318. package/dist/astro/cycle/dom/EventDelegator.d.ts +0 -34
  319. package/dist/astro/cycle/dom/IsolateModule.d.ts +0 -23
  320. package/dist/astro/cycle/dom/MainDOMSource.d.ts +0 -32
  321. package/dist/astro/cycle/dom/PriorityQueue.d.ts +0 -7
  322. package/dist/astro/cycle/dom/ScopeChecker.d.ts +0 -9
  323. package/dist/astro/cycle/dom/SymbolTree.d.ts +0 -9
  324. package/dist/astro/cycle/dom/VNodeWrapper.d.ts +0 -8
  325. package/dist/astro/cycle/dom/enrichEventStream.d.ts +0 -24
  326. package/dist/astro/cycle/dom/fromEvent.d.ts +0 -6
  327. package/dist/astro/cycle/dom/index.d.ts +0 -8
  328. package/dist/astro/cycle/dom/isolate.d.ts +0 -9
  329. package/dist/astro/cycle/dom/makeDOMDriver.d.ts +0 -11
  330. package/dist/astro/cycle/dom/mockDOMSource.d.ts +0 -17
  331. package/dist/astro/cycle/dom/modules.d.ts +0 -5
  332. package/dist/astro/cycle/dom/snabbdom.d.ts +0 -22
  333. package/dist/astro/cycle/dom/styleModule.d.ts +0 -13
  334. package/dist/astro/cycle/dom/thunk.d.ts +0 -11
  335. package/dist/astro/cycle/dom/utils.d.ts +0 -8
  336. package/dist/astro/cycle/isolate/index.d.ts +0 -42
  337. package/dist/astro/cycle/run/adapt.d.ts +0 -6
  338. package/dist/astro/cycle/run/index.d.ts +0 -36
  339. package/dist/astro/cycle/run/internals.d.ts +0 -8
  340. package/dist/astro/cycle/run/types.d.ts +0 -49
  341. package/dist/astro/cycle/state/Collection.d.ts +0 -29
  342. package/dist/astro/cycle/state/StateSource.d.ts +0 -20
  343. package/dist/astro/cycle/state/index.d.ts +0 -5
  344. package/dist/astro/cycle/state/pickCombine.d.ts +0 -3
  345. package/dist/astro/cycle/state/pickMerge.d.ts +0 -3
  346. package/dist/astro/cycle/state/types.d.ts +0 -18
  347. package/dist/astro/cycle/state/withState.d.ts +0 -17
  348. package/dist/astro/extra/classes.d.ts +0 -7
  349. package/dist/astro/extra/devtools.d.ts +0 -78
  350. package/dist/astro/extra/dragDriver.d.ts +0 -20
  351. package/dist/astro/extra/driverFactories.d.ts +0 -12
  352. package/dist/astro/extra/eventDriver.d.ts +0 -9
  353. package/dist/astro/extra/exactState.d.ts +0 -1
  354. package/dist/astro/extra/hmr.d.ts +0 -17
  355. package/dist/astro/extra/logDriver.d.ts +0 -2
  356. package/dist/astro/extra/processDrag.d.ts +0 -19
  357. package/dist/astro/extra/processForm.d.ts +0 -15
  358. package/dist/astro/extra/run.d.ts +0 -13
  359. package/dist/astro/extra/xstreamCompat.d.ts +0 -5
  360. package/dist/astro/index.d.ts +0 -19
  361. package/dist/astro/index.esm.js +0 -27
  362. package/dist/astro/jsx-dev-runtime.d.ts +0 -1
  363. package/dist/astro/jsx-runtime.d.ts +0 -4
  364. package/dist/astro/jsx.d.ts +0 -2
  365. package/dist/astro/pragma/fn.d.ts +0 -7
  366. package/dist/astro/pragma/index.d.ts +0 -7
  367. package/dist/astro/pragma/is.d.ts +0 -10
  368. package/dist/astro/server.d.ts +0 -12
  369. package/dist/astro/server.esm.js +0 -25
  370. package/dist/astro/switchable.d.ts +0 -7
  371. package/dist/collection.d.ts +0 -12
  372. package/dist/component.d.ts +0 -22
  373. package/dist/cycle/dom/BodyDOMSource.d.ts +0 -10
  374. package/dist/cycle/dom/DOMSource.d.ts +0 -11
  375. package/dist/cycle/dom/DocumentDOMSource.d.ts +0 -12
  376. package/dist/cycle/dom/ElementFinder.d.ts +0 -8
  377. package/dist/cycle/dom/EventDelegator.d.ts +0 -34
  378. package/dist/cycle/dom/IsolateModule.d.ts +0 -23
  379. package/dist/cycle/dom/MainDOMSource.d.ts +0 -32
  380. package/dist/cycle/dom/PriorityQueue.d.ts +0 -7
  381. package/dist/cycle/dom/ScopeChecker.d.ts +0 -9
  382. package/dist/cycle/dom/SymbolTree.d.ts +0 -9
  383. package/dist/cycle/dom/VNodeWrapper.d.ts +0 -8
  384. package/dist/cycle/dom/enrichEventStream.d.ts +0 -24
  385. package/dist/cycle/dom/fromEvent.d.ts +0 -6
  386. package/dist/cycle/dom/index.d.ts +0 -8
  387. package/dist/cycle/dom/isolate.d.ts +0 -9
  388. package/dist/cycle/dom/makeDOMDriver.d.ts +0 -11
  389. package/dist/cycle/dom/mockDOMSource.d.ts +0 -17
  390. package/dist/cycle/dom/modules.d.ts +0 -5
  391. package/dist/cycle/dom/snabbdom.d.ts +0 -22
  392. package/dist/cycle/dom/styleModule.d.ts +0 -13
  393. package/dist/cycle/dom/thunk.d.ts +0 -11
  394. package/dist/cycle/dom/utils.d.ts +0 -8
  395. package/dist/cycle/isolate/index.d.ts +0 -42
  396. package/dist/cycle/run/adapt.d.ts +0 -6
  397. package/dist/cycle/run/index.d.ts +0 -36
  398. package/dist/cycle/run/internals.d.ts +0 -8
  399. package/dist/cycle/run/types.d.ts +0 -49
  400. package/dist/cycle/state/Collection.d.ts +0 -29
  401. package/dist/cycle/state/StateSource.d.ts +0 -20
  402. package/dist/cycle/state/index.d.ts +0 -5
  403. package/dist/cycle/state/pickCombine.d.ts +0 -3
  404. package/dist/cycle/state/pickMerge.d.ts +0 -3
  405. package/dist/cycle/state/types.d.ts +0 -18
  406. package/dist/cycle/state/withState.d.ts +0 -17
  407. package/dist/extra/classes.d.ts +0 -7
  408. package/dist/extra/devtools.d.ts +0 -78
  409. package/dist/extra/dragDriver.d.ts +0 -20
  410. package/dist/extra/driverFactories.d.ts +0 -12
  411. package/dist/extra/eventDriver.d.ts +0 -9
  412. package/dist/extra/exactState.d.ts +0 -1
  413. package/dist/extra/hmr.d.ts +0 -17
  414. package/dist/extra/logDriver.d.ts +0 -2
  415. package/dist/extra/processDrag.d.ts +0 -19
  416. package/dist/extra/processForm.d.ts +0 -15
  417. package/dist/extra/run.d.ts +0 -13
  418. package/dist/extra/xstreamCompat.d.ts +0 -5
  419. package/dist/jsx-dev-runtime.d.ts +0 -1
  420. package/dist/jsx-dev-runtime.js +0 -224
  421. package/dist/jsx-runtime.d.ts +0 -4
  422. package/dist/jsx-runtime.js +0 -224
  423. package/dist/jsx.d.ts +0 -2
  424. package/dist/pragma/fn.d.ts +0 -7
  425. package/dist/pragma/index.d.ts +0 -7
  426. package/dist/pragma/is.d.ts +0 -10
  427. package/dist/switchable.d.ts +0 -7
  428. package/dist/vike/+config.cjs.js +0 -65
  429. package/src/component.ts +0 -2256
  430. package/src/cycle/isolate/index.ts +0 -196
  431. package/src/cycle/run/index.ts +0 -151
  432. package/src/cycle/run/internals.ts +0 -143
  433. package/src/cycle/state/Collection.ts +0 -174
  434. package/src/cycle/state/index.ts +0 -5
  435. package/src/cycle/state/pickCombine.ts +0 -167
  436. package/src/cycle/state/pickMerge.ts +0 -122
  437. package/src/cycle/state/withState.ts +0 -47
  438. package/src/pragma/fn.ts +0 -55
package/src/index.d.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  import type { MainDOMSource } from './cycle/dom/MainDOMSource'
2
2
  import type { EnrichedEventStream } from './cycle/dom/enrichEventStream'
3
- import type { StateSource } from './cycle/state/index'
3
+ import type { DocumentDOMSource } from './cycle/dom/DocumentDOMSource'
4
+ import type { BodyDOMSource } from './cycle/dom/BodyDOMSource'
5
+ import type { EventsFnOptions } from './cycle/dom/DOMSource'
6
+ import type { VNode } from './cycle/dom/snabbdom'
7
+ import type { StateSource } from './cycle/state/StateSource'
4
8
  import xsDefault from 'xstream'
9
+ import type { InspectGraph, InspectOptions } from './extra/diagnostics/checks/public'
5
10
  import type { MemoryStream, Stream } from 'xstream'
6
11
 
7
12
  export declare const ABORT: unique symbol
@@ -25,15 +30,48 @@ export type DriverFactories<DRIVERS extends DriverSpecs = DriverSpecs> = {
25
30
 
26
31
  /**
27
32
  * A function that takes component properties and returns a JSX element.
28
- * State is always provided by the framework at runtime.
33
+ * State and context are always provided by the framework at runtime. In JSX the element
34
+ * takes the component's own props plus an optional `state` slice name or lens instead
35
+ * (see `JSX.LibraryManagedAttributes` / `ElementProps`).
29
36
  */
30
37
  type ComponentProps<STATE, PROPS, CONTEXT> = (
31
- props: PROPS & { state: STATE; children?: JSX.Element | JSX.Element[]; slots?: Record<string, JSX.Element[]>; context?: CONTEXT },
32
- state: STATE,
33
- context: CONTEXT,
34
- peers: { [peer: string]: JSX.Element | JSX.Element[] }
38
+ props: ViewProps<STATE, PROPS, CONTEXT>
35
39
  ) => JSX.Element
36
40
 
41
+ /**
42
+ * PLAN-4 GS-9: `uid()` is a stable id string for this component instance (from its position in
43
+ * the tree: the same in renderToString and after hydration); `uid('name')` derives one from it,
44
+ * e.g. `<input id={uid('email')} />` with `<label for={uid('email')}>`.
45
+ */
46
+ export type UidFunction = (name?: string) => string
47
+
48
+ /** The first argument of a component's view: its props plus `state`, `context`, `children`, `slots` and `uid`. */
49
+ export type ViewProps<STATE = any, PROPS = {}, CONTEXT = {}> =
50
+ PROPS & { state: STATE; context: CONTEXT; children?: JSX.Element | JSX.Element[]; slots?: Record<string, JSX.Element[]>; uid: UidFunction }
51
+
52
+ /**
53
+ * The `state` prop a parent passes to a sub-component in JSX: the name of a field of the
54
+ * parent's state (`state="editor"`) or a lens. Without it the child shares the parent's state.
55
+ */
56
+ export type StateProp = string | Lense<any, any>
57
+
58
+ /**
59
+ * The JSX attributes of a component whose view takes PROPS: `state` becomes an optional
60
+ * slice name or lens, and the framework-provided `context` and `slots` are not passed.
61
+ * `resetState` (6.0): for an `isolatedState` child bound with `state`, replace the slice with
62
+ * the child's `initialState` when it is created (by default an existing slice is kept and
63
+ * `initialState` only seeds a missing one). Read at creation, like `state`; not a prop of the child.
64
+ */
65
+ export type ElementProps<PROPS> =
66
+ 0 extends (1 & PROPS) ? PROPS
67
+ : 'state' extends keyof PROPS ? WithoutViewOnlyProps<PROPS> & { state?: StateProp; resetState?: boolean }
68
+ : PROPS
69
+
70
+ /** PROPS without `state`, `context`, `slots` and `uid` (keeps optionality and index signatures, unlike Omit). */
71
+ type WithoutViewOnlyProps<PROPS> = {
72
+ [KEY in keyof PROPS as KEY extends 'state' | 'context' | 'slots' | 'uid' ? never : KEY]: PROPS[KEY]
73
+ }
74
+
37
75
  type NextFunction<ACTIONS = any> = ACTIONS extends object
38
76
  ? <ACTION_KEY extends keyof ACTIONS>(
39
77
  action: ACTION_KEY,
@@ -42,7 +80,7 @@ type NextFunction<ACTIONS = any> = ACTIONS extends object
42
80
  ) => void
43
81
  : (action: string, data?: any, delay?: number) => void
44
82
 
45
- type ReducerExtras<PROPS, CONTEXT> = PROPS & { context: CONTEXT; children?: JSX.Element | JSX.Element[]; slots?: Record<string, JSX.Element[]> }
83
+ type ReducerExtras<PROPS, CONTEXT> = PROPS & { context: CONTEXT; children?: JSX.Element | JSX.Element[]; slots?: Record<string, JSX.Element[]>; uid: UidFunction }
46
84
 
47
85
  type Reducer<STATE, PROPS, ACTIONS = any, DATA = any, RETURN = any, CONTEXT = {}> = (
48
86
  state: STATE,
@@ -60,6 +98,76 @@ type StateOnlyReducer<STATE, RETURN = any> = (
60
98
 
61
99
  export type Event<DATA = any> = { type: string; data: DATA }
62
100
 
101
+ // ── EVENTS registry ────────────────────────────────────────────────
102
+
103
+ /**
104
+ * Registry of global EVENTS bus event names and their payload types.
105
+ *
106
+ * Empty by default, which keeps the EVENTS bus untyped (`any`). Augment it to
107
+ * type-check `EVENTS.select()`, `event()`, `emit()` and raw EVENTS sink returns:
108
+ *
109
+ * declare module 'sygnal' {
110
+ * interface SygnalEvents {
111
+ * DELETE_LANE: { laneId: string }
112
+ * RESET: void // no payload: event('RESET')
113
+ * }
114
+ * }
115
+ *
116
+ * Once the registry has at least one entry, unregistered event names are type errors.
117
+ * The augmenting file must be a module (have at least one import or export).
118
+ */
119
+ export interface SygnalEvents {}
120
+
121
+ /** Valid event names: `string` while the registry is empty, otherwise the registered names. */
122
+ export type EventName = keyof SygnalEvents extends never ? string : keyof SygnalEvents & string
123
+
124
+ /** Payload type of a registered event (`any` while the registry is empty). */
125
+ export type EventPayload<TYPE extends string = string> = keyof SygnalEvents extends never
126
+ ? any
127
+ : TYPE extends keyof SygnalEvents ? SygnalEvents[TYPE] : never
128
+
129
+ /**
130
+ * An event object as put on the EVENTS bus. While the registry is empty this is `Event<any>`;
131
+ * otherwise it is the union of `{ type, data }` for every registered event.
132
+ */
133
+ export type RegisteredEvent = keyof SygnalEvents extends never
134
+ ? Event<any>
135
+ : { [TYPE in keyof SygnalEvents & string]: { type: TYPE; data: SygnalEvents[TYPE] } }[keyof SygnalEvents & string]
136
+
137
+ /** The `{ type, data }` object produced by `event(type, ...)` / `emit(type, ...)`. */
138
+ export type EmittedEvent<TYPE extends string = string> = keyof SygnalEvents extends never
139
+ ? { type: TYPE; data: any }
140
+ // TYPE is every registered name when the name argument isn't registered (inference falls
141
+ // back to the constraint): any registered event, so only the clear "not assignable to
142
+ // parameter" error is reported, not a second one on the model entry
143
+ : [EventName] extends [TYPE] ? RegisteredEvent
144
+ : { type: TYPE; data: EventPayload<TYPE> }
145
+
146
+ /** The `next()` function as seen by an event payload function. */
147
+ type EventNextFunction = (action: string, data?: any, delay?: number) => void
148
+
149
+ /** Payload function for `event()` / `emit()`: receives the reducer arguments, returns the event data. */
150
+ export type EventPayloadFunction<TYPE extends string = string, STATE = any, DATA = any> =
151
+ (state: STATE, data: DATA, next: EventNextFunction, props: any) => EventPayload<TYPE>
152
+
153
+ /**
154
+ * Remaining arguments of `event()` / `emit()` when the payload is a static value.
155
+ * The payload may be omitted when the registry is empty or the event's payload
156
+ * type accepts `undefined` (e.g. `void`).
157
+ */
158
+ type StaticEventArgs<TYPE extends string> = keyof SygnalEvents extends never
159
+ ? [payload?: any]
160
+ : undefined extends EventPayload<TYPE>
161
+ ? [payload?: EventPayload<TYPE>]
162
+ : [payload: EventPayload<TYPE>]
163
+
164
+ /**
165
+ * The sink function returned by `event()`. Use it as the value of an `EVENTS` key in a
166
+ * model entry: `ACTION: { STATE: ..., EVENTS: event('TYPE', fn) }`.
167
+ */
168
+ export type EventSink<TYPE extends string = string, STATE = any, DATA = any> =
169
+ (state: STATE, data: DATA, next: any, props: any) => EmittedEvent<TYPE>
170
+
63
171
  export type NonStateSinkReturns = {
64
172
  EVENTS?: unknown;
65
173
  LOG?: unknown;
@@ -67,9 +175,11 @@ export type NonStateSinkReturns = {
67
175
  }
68
176
 
69
177
  type ResolvedNonStateSinkReturns<SINK_RETURNS extends NonStateSinkReturns = {}> = {
70
- EVENTS: SINK_RETURNS extends { EVENTS: infer EVENTS_RETURN } ? EVENTS_RETURN : Event<any>;
178
+ EVENTS: SINK_RETURNS extends { EVENTS: infer EVENTS_RETURN } ? EVENTS_RETURN : RegisteredEvent;
71
179
  LOG: SINK_RETURNS extends { LOG: infer LOG_RETURN } ? LOG_RETURN : any;
72
- PARENT: SINK_RETURNS extends { PARENT: infer PARENT_RETURN } ? PARENT_RETURN : any;
180
+ // `unknown` (any value may be sent) rather than `any`, so CHILD.select(Child) of a child
181
+ // annotated without `{ PARENT: T }` is a Stream<unknown>, not a silent Stream<any>
182
+ PARENT: SINK_RETURNS extends { PARENT: infer PARENT_RETURN } ? PARENT_RETURN : unknown;
73
183
  }
74
184
 
75
185
  /**
@@ -82,23 +192,55 @@ type SinkValue<STATE, PROPS, ACTIONS, DATA, RETURN, CALCULATED, CONTEXT = {}> =
82
192
  | true
83
193
  | Reducer<STATE & CALCULATED, PROPS, ACTIONS, DATA, RETURN, CONTEXT>
84
194
 
195
+ /**
196
+ * Valid values for a non-STATE sink (EVENTS, LOG, PARENT, custom drivers): a SinkValue, or
197
+ * a constant that is sent as-is every time the action fires (`LOG: 'saved'`). Not for
198
+ * STATE, whose values must be reducers. A constant can't be a function (that is a
199
+ * reducer) or `true` (that is pass-through).
200
+ */
201
+ type NonStateSinkValue<STATE, PROPS, ACTIONS, DATA, RETURN, CALCULATED, CONTEXT = {}> =
202
+ | SinkValue<STATE, PROPS, ACTIONS, DATA, RETURN, CALCULATED, CONTEXT>
203
+ | SinkConstant<RETURN>
204
+
205
+ /**
206
+ * A constant for a sink whose value type is `RETURN`. When RETURN is `any` or `unknown`
207
+ * (untyped sinks), any non-function value: a bare `any`/`unknown` would also accept reducers
208
+ * with wrong parameter types and switch off checking of the whole model entry.
209
+ */
210
+ type SinkConstant<RETURN> = unknown extends RETURN
211
+ ? AnySinkConstant
212
+ : RETURN extends (...args: any[]) => any ? never : RETURN
213
+
214
+ type AnySinkConstant =
215
+ | string | number | bigint | boolean | null
216
+ | readonly unknown[]
217
+ | { [key: string]: unknown; apply?: never; call?: never; bind?: never }
218
+
219
+ /**
220
+ * An EFFECT handler. It may be async: a returned promise is expected (no SYG219) and its
221
+ * rejection is reported as SYG214. `next()` after the component is disposed does nothing.
222
+ * `props.signal` aborts on DISPOSE (undefined where AbortController is missing).
223
+ */
85
224
  type EffectReducer<STATE, PROPS, ACTIONS, DATA, CALCULATED, CONTEXT = {}> =
86
- | ((state: STATE & CALCULATED, args: DATA, next: NextFunction<ACTIONS>, props: ReducerExtras<PROPS, CONTEXT>) => void)
225
+ | ((state: STATE & CALCULATED, args: DATA, next: NextFunction<ACTIONS>, props: ReducerExtras<PROPS, CONTEXT> & { signal?: AbortSignal }) => void)
87
226
 
88
227
  type DefaultSinks<STATE, PROPS, ACTIONS, DATA, CALCULATED, SINK_RETURNS extends NonStateSinkReturns = {}, CONTEXT = {}> = {
89
228
  STATE?: SinkValue<STATE, PROPS, ACTIONS, DATA, STATE, CALCULATED, CONTEXT>;
90
- EVENTS?: SinkValue<STATE, PROPS, ACTIONS, DATA, ResolvedNonStateSinkReturns<SINK_RETURNS>['EVENTS'], CALCULATED, CONTEXT>;
91
- LOG?: SinkValue<STATE, PROPS, ACTIONS, DATA, ResolvedNonStateSinkReturns<SINK_RETURNS>['LOG'], CALCULATED, CONTEXT>;
92
- PARENT?: SinkValue<STATE, PROPS, ACTIONS, DATA, ResolvedNonStateSinkReturns<SINK_RETURNS>['PARENT'], CALCULATED, CONTEXT>;
229
+ EVENTS?: NonStateSinkValue<STATE, PROPS, ACTIONS, DATA, ResolvedNonStateSinkReturns<SINK_RETURNS>['EVENTS'], CALCULATED, CONTEXT>;
230
+ LOG?: NonStateSinkValue<STATE, PROPS, ACTIONS, DATA, ResolvedNonStateSinkReturns<SINK_RETURNS>['LOG'], CALCULATED, CONTEXT>;
231
+ PARENT?: NonStateSinkValue<STATE, PROPS, ACTIONS, DATA, ResolvedNonStateSinkReturns<SINK_RETURNS>['PARENT'], CALCULATED, CONTEXT>;
93
232
  EFFECT?: EffectReducer<STATE, PROPS, ACTIONS, DATA, CALCULATED, CONTEXT>;
233
+ /** PLAN-4 GS-2: element commands (built in, no driver): `{ focus: Email }`, `[{ ... }, { ... }]` */
234
+ ELEMENT?: NonStateSinkValue<STATE, PROPS, ACTIONS, DATA, ElementCommands, CALCULATED, CONTEXT>;
94
235
  }
95
236
 
237
+ /** Keys a type declares by name (index signatures left out). */
96
238
  type CustomDriverSinks<STATE, PROPS, DRIVERS, ACTIONS, ACTION_ENTRY, CALCULATED, CONTEXT = {}> = keyof DRIVERS extends never
97
239
  ? {
98
- [driver: string]: SinkValue<STATE, PROPS, ACTIONS, any, any, CALCULATED, CONTEXT>
240
+ [driver: string]: NonStateSinkValue<STATE, PROPS, ACTIONS, any, any, CALCULATED, CONTEXT>
99
241
  }
100
242
  : {
101
- [DRIVER_KEY in keyof DRIVERS]: SinkValue<
243
+ [DRIVER_KEY in keyof DRIVERS]: NonStateSinkValue<
102
244
  STATE,
103
245
  PROPS,
104
246
  ACTIONS,
@@ -119,7 +261,6 @@ type ModelEntry<STATE, PROPS, DRIVERS, ACTIONS, ACTION_ENTRY, CALCULATED, SINK_R
119
261
  type WithDefaultActions<STATE, ACTIONS> = ACTIONS & {
120
262
  BOOTSTRAP?: never;
121
263
  INITIALIZE?: STATE;
122
- HYDRATE?: any;
123
264
  DISPOSE?: never;
124
265
  }
125
266
 
@@ -149,19 +290,965 @@ type ComponentModel<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, SINK_RETURNS ext
149
290
  >
150
291
  }
151
292
 
293
+ /** Value type produced by a PARENT sink value (a reducer's return, minus ABORT/undefined). */
294
+ type ParentSinkValueReturn<VALUE> =
295
+ // an expando model widens `PARENT: true` (pass-through) and `PARENT: false` to boolean:
296
+ // the payload can't be told apart there
297
+ boolean extends VALUE ? ParentConstantReturn<Exclude<VALUE, boolean>> : ParentConstantReturn<VALUE>
298
+
299
+ type ParentConstantReturn<VALUE> =
300
+ VALUE extends (...args: any[]) => infer RETURN ? Exclude<RETURN, ABORT | undefined | void>
301
+ // `true` is pass-through (payload unknown here)
302
+ : VALUE extends true ? never
303
+ // a constant (including `false`) is sent as-is
304
+ : VALUE
305
+
306
+ type ParentPayloadFromEntry<ENTRY> =
307
+ ENTRY extends (...args: any[]) => any ? never
308
+ : ENTRY extends object
309
+ ? 'PARENT' extends keyof ENTRY ? ParentSinkValueReturn<NonNullable<ENTRY['PARENT']>> : never
310
+ : never
311
+
312
+ type AnyIfNever<T> = [T] extends [never] ? any : T
313
+
314
+ /**
315
+ * The value type a component sends to its parent through the `PARENT` sink, inferred from the
316
+ * component's `model` (its `{ PARENT: fn }` entries),
317
+ * or for a `Component<...>` annotation, the `PARENT` entry of its `SINK_RETURNS`. A
318
+ * `Component<...>` annotation without one gives `unknown` (declare it: `{ PARENT: T }`); no model
319
+ * or `PARENT: true` pass-through entries only give `any`.
320
+ */
321
+ export type ParentPayloadOf<COMPONENT> =
322
+ COMPONENT extends { model?: infer MODEL }
323
+ // the union is written out here (not behind a helper alias) so hovers and errors print
324
+ // the payload (`Stream<{ taskId: number }>`), not `Stream<Helper<...the whole model...>>`
325
+ ? 0 extends (1 & MODEL) ? any : AnyIfNever<
326
+ ParentPayloadFromEntry<NonNullable<NonNullable<MODEL>[keyof NonNullable<MODEL>]>>
327
+ >
328
+ : any
329
+
152
330
  type ChildSource = {
331
+ /** Typed: the stream type is inferred from the child's PARENT sink (falls back to `any`). */
332
+ select<COMPONENT extends (...args: any[]) => any>(component: COMPONENT): Stream<ParentPayloadOf<COMPONENT>>;
153
333
  select<T = any>(component: (...args: any[]) => any): Stream<T>;
154
- select<T = any>(name: string): Stream<T>;
155
334
  }
156
335
 
157
- export type SygnalDOMSource = MainDOMSource & {
158
- [eventName: string]: (selector: string) => EnrichedEventStream<globalThis.Event>
336
+ /**
337
+ * `DOM.<event>(selector)` shorthands for the standard DOM events, typed like
338
+ * `DOM.select(selector).events('<event>')`: `DOM.keydown('.x')` is a stream of KeyboardEvent,
339
+ * `DOM.click('.x')` of MouseEvent (PointerEvent in newer DOM typings), and so on.
340
+ */
341
+ export type DOMEventShorthands = {
342
+ [EVENT in Exclude<keyof HTMLElementEventMap, keyof MainDOMSource>]: DOMEventShorthand<HTMLElementEventMap[EVENT]>
343
+ }
344
+
345
+ /**
346
+ * One `DOM.<event>` shorthand: a selector string gives a stream of the event; a control gives
347
+ * the event with `currentTarget` (and `ownerTarget`) typed as the control's element.
348
+ */
349
+ export interface DOMEventShorthand<EVENT> {
350
+ <CONTROL extends AnyControl>(control: CONTROL): EnrichedEventStream<ControlEvent<EVENT, ControlElementOf<CONTROL>>>
351
+ // last, so ReturnType<SygnalDOMSource['click']> stays the selector form
352
+ (selector: string): EnrichedEventStream<EVENT>
353
+ }
354
+
355
+ /**
356
+ * `select()` overloads added to the DOM source: a control selects its element, typed;
357
+ * `'document'` / `'body'` give sources that also take a control; a selector string gives a
358
+ * source whose `select()` takes controls too.
359
+ */
360
+ type ControlSelectOverlay = {
361
+ select<CONTROL extends AnyControl>(control: CONTROL): ControlDOMSource<ControlElementOf<CONTROL>>
362
+ select(selector: 'document'): SygnalDocumentDOMSource
363
+ select(selector: 'body'): SygnalBodyDOMSource
364
+ select(selector: string): SelectedDOMSource
365
+ }
366
+
367
+ /** What `DOM.select('<selector>')` returns: a MainDOMSource whose `select()` also takes controls. */
368
+ export type SelectedDOMSource = ControlSelectOverlay & MainDOMSource
369
+
370
+ /** `DOM.select('document')`: document-level listeners, optionally filtered to a selector or control. */
371
+ export type SygnalDocumentDOMSource = DocumentDOMSource & {
372
+ select(control: AnyControl): DocumentDOMSource
373
+ }
374
+
375
+ /** `DOM.select('body')`: body-level listeners, optionally filtered to a selector or control. */
376
+ export type SygnalBodyDOMSource = BodyDOMSource & {
377
+ select(control: AnyControl): BodyDOMSource
378
+ }
379
+
380
+ /**
381
+ * `DOM.select(control)`: a DOM source scoped to a control's element. `events(name)` is typed
382
+ * like a selector's, with `currentTarget` typed as the control's element; `element()` and
383
+ * `elements()` give that element type.
384
+ */
385
+ export type ControlDOMSource<ELEMENT extends Element = Element> = ControlSelectOverlay & Omit<MainDOMSource, 'select' | 'events' | 'element' | 'elements'> & {
386
+ events<K extends keyof HTMLElementEventMap>(eventType: K, options?: EventsFnOptions, bubbles?: boolean): EnrichedEventStream<ControlEvent<HTMLElementEventMap[K], ELEMENT>>
387
+ events(eventType: string, options?: EventsFnOptions, bubbles?: boolean): EnrichedEventStream<ControlEvent<globalThis.Event, ELEMENT>>
388
+ element(): MemoryStream<ELEMENT>
389
+ elements(): MemoryStream<ELEMENT[]>
390
+ }
391
+
392
+ export type SygnalDOMSource = ControlSelectOverlay & MainDOMSource & DOMEventShorthands & {
393
+ /** Any other event name (custom events): `DOM['my-event']('.x')`, `DOM['my-event'](Control)` */
394
+ [eventName: string]: DOMEventShorthand<globalThis.Event>
395
+ }
396
+
397
+ // ── Controls (PLAN-4 CT-1) ────────────────────────────────────────
398
+
399
+ /**
400
+ * The pragma's own createElement, passed to a spec's `vnode()` as `h` (D116):
401
+ * `h(tag, props, ...children)`. Spec authors build vnodes with it, never with an imported
402
+ * `createElement` (under the automatic JSX runtime that would bundle a second pragma).
403
+ */
404
+ export type ControlH = (tag: any, props?: Record<string, any> | null, ...children: unknown[]) => VNode
405
+
406
+ /**
407
+ * The spec-object form of a control spec (D101, amended D116): `controls({ DueDate: datePicker })`.
408
+ * `vnode(props, children, h)` returns the one element vnode the control renders (the pragma
409
+ * stamps `data-control` on it, keeping its key and hooks, and copies the props' `key` onto it
410
+ * when it has none). `commands` are looked up by element commands before native methods.
411
+ * `__props` is a phantom field (types only) that gives the control its props type.
412
+ */
413
+ export interface ControlSpecObject<P = any> {
414
+ /** Free-form kind ('widget' in PLAN-5), shown in inspect() and diagnostics */
415
+ kind: string;
416
+ /** Must return one element vnode (not a component, fragment or text), built with `h` */
417
+ vnode(props: P, children: unknown[], h: ControlH): VNode;
418
+ commands?: Record<string, (elm: Element, options: Record<string, unknown>) => void>;
419
+ /** Phantom, types only: the control's props */
420
+ __props?: P;
421
+ }
422
+
423
+ /**
424
+ * A control spec (frozen contract D101): an intrinsic tag name (`'button'`, `'input'`,
425
+ * `'wa-rating'`) or a spec object `{ kind, vnode(props, children, h), commands?, __props? }`.
426
+ */
427
+ export type ControlSpec<P = any> =
428
+ | keyof JSX.IntrinsicElements
429
+ | ControlSpecObject<P>
430
+
431
+ declare const CONTROL: unique symbol
432
+
433
+ /**
434
+ * A control (`controls({ Add: 'button' }).Add`): a JSX tag that renders its element with the
435
+ * props passed plus `data-control="<KEY>"`. Not a component (no state, intent or isolation).
436
+ * Anywhere a selector is accepted (`DOM.select`, `DOM.<event>`, `simulateEvent`, `query`,
437
+ * `queryAll`), a control selects its element; in a template string it is its selector:
438
+ * `` `li ${Add}` `` is `li [data-control="Add"]`.
439
+ *
440
+ * KEY is the control's name, ELEMENT the element type (`HTMLButtonElement` for 'button';
441
+ * `Element` for a spec object), PROPS its JSX props.
442
+ */
443
+ export interface Control<KEY extends string = string, ELEMENT extends Element = Element, PROPS = any> {
444
+ (props: PROPS): JSX.Element;
445
+ /** The control's selector: `[data-control="<KEY>"]` */
446
+ toString(): `[data-control="${KEY}"]`;
447
+ /** 'element' for a tag spec, else the spec object's `kind` */
448
+ readonly kind: string;
449
+ /** The spec the control was made from */
450
+ readonly spec: ControlSpec;
451
+ /** Phantom, types only */
452
+ readonly [CONTROL]: { key: KEY; element: ELEMENT; props: PROPS };
453
+ }
454
+
455
+ /** Any control, whatever its key, element and props. */
456
+ export type AnyControl = Control<string, any, any>
457
+
458
+ /** The element type a control (or a control spec) renders. */
459
+ export type ControlElementOf<C> =
460
+ C extends { readonly [CONTROL]: { element: infer ELEMENT } } ? ELEMENT
461
+ : C extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[C]
462
+ : C extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[C]
463
+ : C extends string ? HTMLElement
464
+ : Element
465
+
466
+ /** An event from a control's listener: `currentTarget` / `ownerTarget` are the control's element. */
467
+ export type ControlEvent<EVENT, ELEMENT extends Element = Element> = EVENT & {
468
+ readonly currentTarget: ELEMENT;
469
+ readonly ownerTarget: ELEMENT;
470
+ }
471
+
472
+ type IfEqual<X, Y, A, B> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? A : B
473
+
474
+ /** Keys of T that are not readonly. */
475
+ type WritableKeys<T> = {
476
+ [KEY in keyof T]-?: IfEqual<{ [Q in KEY]: T[KEY] }, { -readonly [Q in KEY]: T[KEY] }, KEY, never>
477
+ }[keyof T]
478
+
479
+ /** Writable, non-function, non-constant properties of an element: its settable DOM props. */
480
+ type SettableElementProps<ELEMENT> = {
481
+ [KEY in WritableKeys<ELEMENT> as KEY extends string
482
+ ? KEY extends Uppercase<KEY> ? never
483
+ : NonNullable<ELEMENT[KEY]> extends (...args: any[]) => any ? never
484
+ : KEY
485
+ : never
486
+ ]?: KEY extends 'value' ? ELEMENT[KEY] | number : ELEMENT[KEY]
487
+ }
488
+
489
+ /** JSX props every control takes, besides its element's own props (or the spec's props). */
490
+ export type ControlCommonProps<ELEMENT = Element> = {
491
+ key?: string | number;
492
+ children?: any;
493
+ ref?: Ref<any> | ((element: ELEMENT | null) => void);
494
+ }
495
+
496
+ /**
497
+ * JSX props of an intrinsic element as rendered by Sygnal: the element's settable DOM
498
+ * properties (no event handlers: listen in the intent), plus `class`, `style`, `attrs`,
499
+ * `props`, `hook`, `for`, `tabindex`, `autoFocus` / `autoSelect`. `data-*` / `aria-*` are
500
+ * accepted as hyphenated attributes. Custom elements (`'wa-rating'`) and SVG elements take any prop.
501
+ */
502
+ export type IntrinsicControlProps<TAG extends string> =
503
+ ControlCommonProps<ControlElementOf<TAG>> & {
504
+ class?: string | ReadonlyArray<unknown> | Record<string, boolean | null | undefined>;
505
+ style?: string | Record<string, any>;
506
+ attrs?: Record<string, any>;
507
+ props?: Record<string, any>;
508
+ hook?: Record<string, (...args: any[]) => any>;
509
+ for?: string;
510
+ tabindex?: number | string;
511
+ /** Focus the element when it enters the DOM */
512
+ autoFocus?: boolean;
513
+ /** Select the element's text after focusing (input/textarea) */
514
+ autoSelect?: boolean;
515
+ } & (TAG extends keyof HTMLElementTagNameMap
516
+ ? Omit<SettableElementProps<HTMLElementTagNameMap[TAG]>, 'style'>
517
+ : { [prop: string]: any })
518
+
519
+ /** The JSX props of a control made from a spec: a tag's IntrinsicControlProps, or a spec object's P. */
520
+ export type ControlPropsOf<SPEC> =
521
+ SPEC extends string ? IntrinsicControlProps<SPEC>
522
+ : SPEC extends { __props?: infer P }
523
+ ? unknown extends P
524
+ ? (SPEC extends { vnode(props: infer VP, ...rest: any[]): any } ? VP : any) & ControlCommonProps
525
+ : P & ControlCommonProps
526
+ : any
527
+
528
+ /** The controls `controls(spec)` returns: one per key, typed from its spec. */
529
+ export type ControlsOf<SPECS> = {
530
+ [KEY in keyof SPECS & string]: Control<
531
+ KEY,
532
+ SPECS[KEY] extends string ? ControlElementOf<SPECS[KEY]> : Element,
533
+ ControlPropsOf<SPECS[KEY]>
534
+ >
535
+ }
536
+
537
+ /**
538
+ * Element tokens for linking a view to its intent by identifier (CT-1):
539
+ *
540
+ * const { Draft, Add } = controls({ Draft: 'input', Add: 'button' })
541
+ * <Draft className="field" value={state.draft} /><Add>Add</Add>
542
+ * AddTodo.intent = ({ DOM }) => ({ DRAFT: DOM.input(Draft).value(), ADD: DOM.click(Add) })
543
+ *
544
+ * Each key becomes a control that renders its spec (a tag name, or a spec object) with
545
+ * `data-control="<Key>"`. The keys are the names, so keep them unique in a file.
546
+ */
547
+ export function controls<const SPECS extends Record<string, ControlSpec>>(spec: SPECS): ControlsOf<SPECS>
548
+
549
+ // ── Widgets (PLAN-5 W-1) ───────────────────────────────────────────
550
+
551
+ /**
552
+ * Props a widget tag puts on its host element (they reach the widget's `mount`/`update` too,
553
+ * except `key` and `ref`): id, className/class, style, title, name, placeholder, role, tabindex,
554
+ * hidden, lang, dir, attrs, aria-*, data-* (and the definition's `hostProps`). `ref` gets the
555
+ * host element (the widget's own API is reached through its `commands`).
556
+ */
557
+ export type WidgetHostProps = {
558
+ key?: string | number;
559
+ ref?: { current: Element | null } | ((el: Element | null) => void);
560
+ id?: string;
561
+ className?: string;
562
+ class?: string | ReadonlyArray<unknown> | Record<string, boolean | null | undefined>;
563
+ style?: string | Record<string, any>;
564
+ title?: string;
565
+ name?: string;
566
+ placeholder?: string;
567
+ role?: string;
568
+ tabindex?: number | string;
569
+ tabIndex?: number;
570
+ hidden?: boolean;
571
+ lang?: string;
572
+ dir?: string;
573
+ attrs?: Record<string, any>;
574
+ [aria: `aria-${string}`]: any;
575
+ [data: `data-${string}`]: any;
576
+ }
577
+
578
+ /** The element type of a widget's host tag (`'input'` → `HTMLInputElement`). */
579
+ export type WidgetElementOf<TAG extends string> =
580
+ TAG extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[TAG] : HTMLElement
581
+
582
+ /** A widget's `dispatch(name, detail)` (mount's third parameter): dispatches a bubbling `CustomEvent` named `name` on the host. */
583
+ export type WidgetDispatch<EV extends string = string> = (name: EV, detail?: unknown) => void
584
+ /** @deprecated the earlier name of `WidgetDispatch` */
585
+ export type WidgetEmit<EV extends string = string> = WidgetDispatch<EV>
586
+
587
+ /** The object passed to `defineWidget()`. */
588
+ export interface WidgetDefinition<P = {}, I = unknown, EV extends string = string, TAG extends string = 'div'> {
589
+ /** A name for diagnostics and inspect() (`'DatePicker'`); the host tag stands in without one */
590
+ name?: string;
591
+ /** The host element (default `'div'`). It has no children of its own: the widget owns its content. */
592
+ tag?: TAG;
593
+ /**
594
+ * Called once the host is in the document (or on the first client patch after SSR) with the
595
+ * props the view passed. Returns the instance that `update`, `unmount` and `commands` get.
596
+ * `error(e)` reports a later failure the widget catches itself (a library's own re-render): it
597
+ * is handled like a throwing `update` (SYG661, the owner's `onError` fallback in its place).
598
+ */
599
+ mount(el: WidgetElementOf<TAG>, props: P, dispatch: WidgetDispatch<EV>, error: (e: unknown) => void): I;
600
+ /** Called with the newest props when they change (shallow compare; `style`/`attrs` objects by their entries). Without it, a change remounts. */
601
+ update?(instance: I, props: P, el: WidgetElementOf<TAG>): void;
602
+ /** Called when the host leaves the DOM. */
603
+ unmount?(instance: I, el: WidgetElementOf<TAG>): void;
604
+ /** The events `dispatch` sends (bubbling CustomEvents on the host; read them with `.detail()`) */
605
+ events?: readonly EV[];
606
+ /**
607
+ * Element commands (`ELEMENT: { open: '.due' }` or `{ open: Due }`), called with the instance.
608
+ * A command wins over a native method of the same name (`focus`, `close`, `togglePopover`: it
609
+ * gets the options object). Register the names in `ElementCommandRegistry` to type the commands.
610
+ */
611
+ commands?: Record<string, (instance: I, options: Record<string, any>, el: WidgetElementOf<TAG>) => unknown>;
612
+ /** What SSR renders inside the host until the client mounts (not for void hosts like `input`) */
613
+ fallback?: VNode | string | ((props: P, h: ControlH) => VNode | string);
614
+ /** More prop names to put on the host element as well */
615
+ hostProps?: readonly string[];
616
+ /** Prop names that stay off the host (the widget applies them itself, e.g. `aria-label` on its own control) */
617
+ ownProps?: readonly string[];
618
+ }
619
+
620
+ /**
621
+ * A widget (`defineWidget()`): a JSX tag that renders its host element, selected like any
622
+ * element (`className`, `DOM.select('.due').events('pick').detail()`), and also a control spec
623
+ * (`controls({ Due: DatePicker })`, the alternative form).
624
+ */
625
+ export interface Widget<P = {}, I = unknown, EV extends string = string, TAG extends string = string> extends ControlSpecObject<P & WidgetHostProps> {
626
+ (props: P & WidgetHostProps): JSX.Element;
627
+ readonly kind: 'widget';
628
+ /** The declared events */
629
+ readonly events: readonly EV[];
630
+ /** The control-spec commands, `(hostElement, options)` (D102) */
631
+ readonly commands: Record<string, (elm: Element, options: Record<string, unknown>) => void>;
632
+ readonly def: WidgetDefinition<P, I, EV, any>;
633
+ /** Phantom, types only */
634
+ __props?: P & WidgetHostProps;
635
+ }
636
+
637
+ /**
638
+ * Wraps a framework-agnostic widget (a date picker, a chart, an editor) as a JSX tag:
639
+ *
640
+ * const DatePicker = defineWidget({
641
+ * tag: 'input',
642
+ * mount: (el, props: { value?: Date }, dispatch) =>
643
+ * flatpickr(el, { defaultDate: props.value, onChange: ([d]) => dispatch('pick', d) }),
644
+ * update: (fp, props) => fp.setDate(props.value ?? '', false),
645
+ * unmount: (fp) => fp.destroy(),
646
+ * events: ['pick'],
647
+ * commands: { open: (fp) => fp.open() },
648
+ * })
649
+ * // view: <label>Due <DatePicker className="due" value={state.due} /></label>
650
+ * // intent: DUE: DOM.select('.due').events('pick').detail()
651
+ * // model: OPEN: { ELEMENT: { open: '.due' } }
652
+ *
653
+ * The host keeps its widget instance across renders and keyed moves; `update` gets the newest
654
+ * props. A `mount`/`update` that throws is reported to `onError` with phase `'widget'` and the
655
+ * owner's `onError` fallback renders in its place.
656
+ */
657
+ export function defineWidget<P = {}, I = unknown, const EV extends string = string, const TAG extends string = 'div'>(definition: WidgetDefinition<P, I, EV, TAG>): Widget<P, I, EV, TAG>
658
+
659
+ /**
660
+ * The target of an element command: a control, or a selector. It is looked up in the view of the
661
+ * component instance that sends the command (a child's elements are isolated from its parent; a
662
+ * Collection item reaches only its own).
663
+ */
664
+ export type ElementTarget = AnyControl | string
665
+
666
+ /**
667
+ * Element commands beyond the built-in ones, by method name → options, for a control spec's
668
+ * `commands` (D102) or another method of the element. Augment it (the first key of a command is
669
+ * the method, the rest are the options):
670
+ *
671
+ * declare module 'sygnal' { interface ElementCommandRegistry { open: { at?: number }; play: {} } }
672
+ * // ELEMENT: { open: DueDate, at: 3 }
673
+ */
674
+ export interface ElementCommandRegistry {}
675
+
676
+ type RegisteredElementCommand = {
677
+ [METHOD in keyof ElementCommandRegistry & string]: { [K in METHOD]: ElementTarget } & ElementCommandRegistry[METHOD]
678
+ }[keyof ElementCommandRegistry & string]
679
+
680
+ /**
681
+ * One element command (the built-in `ELEMENT` sink, PLAN-4 GS-2): `{ <method>: target, ...options }`.
682
+ * The FIRST key is the method, the others are its options; `close` passes `returnValue` as its
683
+ * argument. A control whose spec declares `commands` is asked first (`{ open: DueDate }`), then
684
+ * the element's own method runs, after the next render reaches the page. Register other method
685
+ * names (a spec's commands, `play`, `reset`...) in `ElementCommandRegistry`.
686
+ */
687
+ export type ElementCommand =
688
+ | { focus: ElementTarget | WithinTarget; preventScroll?: boolean; focusVisible?: boolean }
689
+ | { blur: ElementTarget }
690
+ | { select: ElementTarget }
691
+ | { click: ElementTarget }
692
+ | { scrollIntoView: ElementTarget; block?: ScrollLogicalPosition; inline?: ScrollLogicalPosition; behavior?: ScrollBehavior }
693
+ | { showModal: ElementTarget }
694
+ | { show: ElementTarget }
695
+ | { close: ElementTarget; returnValue?: string }
696
+ | { showPopover: ElementTarget }
697
+ | { hidePopover: ElementTarget }
698
+ | { togglePopover: ElementTarget; force?: boolean }
699
+ /** A `<VirtualCollection>`'s container: scroll to the row at `index` (of the filtered, sorted rows) */
700
+ | { scrollToIndex: ElementTarget; index: number; align?: ScrollToAlign; behavior?: 'auto' | 'smooth' }
701
+ /** A `<VirtualCollection>`'s container: scroll to the row whose item has this `id` */
702
+ | { scrollToId: ElementTarget; id: string | number; align?: ScrollToAlign; behavior?: 'auto' | 'smooth' }
703
+ | RegisteredElementCommand
704
+
705
+ /** A `focusWithin(selector)` target (D194): `{ focus: focusWithin('.title') }`. */
706
+ export interface WithinTarget {
707
+ /** The selector, searched under the sender's root element (children included). */
708
+ readonly within: string
709
+ }
710
+
711
+ /**
712
+ * An ELEMENT target that reaches inside the sender's children (PLAN-5 D194): `{ focus:
713
+ * focusWithin(selector), ...options }` focuses the first element under the sender's root element
714
+ * that matches `selector`, a child component's or a Collection item's included (a plain `{ focus:
715
+ * '.x' }` stays in the sender's own isolated scope). It runs after the next patch, so an item the
716
+ * same action adds is there. No match: nothing happens.
717
+ *
718
+ * ADD: { STATE: addRow, ELEMENT: (s) => ({ focus: focusWithin(`[data-id="${s.next}"] .title`) }) }
719
+ */
720
+ export function focusWithin(selector: string): WithinTarget
721
+
722
+ /** What the `ELEMENT` sink takes: one command or several (run in order). */
723
+ export type ElementCommands = ElementCommand | readonly ElementCommand[]
724
+
725
+ // ── Behaviors (PLAN-4 GS-1) ────────────────────────────────────────
726
+
727
+ /**
728
+ * What a behavior's intent receives: the host component's sources (DOM is the host's isolated
729
+ * DOM source; CHILD, EVENTS, drivers as they are), with STATE lensed to the behavior's slice.
730
+ */
731
+ export type BehaviorSources<SLICE = any> = IntentSources<SLICE> & { [source: string]: any }
732
+
733
+ /** A behavior model handler's 4th argument: the host's props, context, uid and its whole state. */
734
+ export type BehaviorProps = ReducerExtras<Record<string, any>, any> & { state: any; signal?: AbortSignal }
735
+
736
+ /**
737
+ * A behavior model handler: `(slice, data, next, props, options, key)`. `options` are the use's
738
+ * options (`pager({ pageSize: 10 })`), `key` its key in `uses` (`'pager'`), e.g. to name a reply
739
+ * action `key + '.LOADED'` (D197).
740
+ */
741
+ export type BehaviorHandler<SLICE = any, OPTIONS = any, RETURN = any> = (
742
+ slice: SLICE,
743
+ data: any,
744
+ next: NextFunction<any>,
745
+ props: BehaviorProps,
746
+ options: OPTIONS,
747
+ key: string
748
+ ) => RETURN | ABORT | undefined
749
+
750
+ /** One behavior model entry: a slice reducer, or an object of sinks (STATE, HOST, EFFECT, ELEMENT, EVENTS, drivers). */
751
+ export type BehaviorModelEntry<SLICE = any, OPTIONS = any> =
752
+ | BehaviorHandler<SLICE, OPTIONS, SLICE>
753
+ | ABORT
754
+ | {
755
+ /** A reducer on the slice: ABORT, or the slice itself, means no change. */
756
+ STATE?: BehaviorHandler<SLICE, OPTIONS, SLICE> | ABORT;
757
+ /**
758
+ * D197: a reducer on the host's WHOLE state (for a behavior that edits a host field, e.g.
759
+ * reorders `state[options.from]`); return the new host state. ABORT, or the same state,
760
+ * means no change. Use it instead of STATE in an entry.
761
+ */
762
+ HOST?: (state: any, data: any, next: NextFunction<any>, props: BehaviorProps, options: OPTIONS, key: string) => any;
763
+ EFFECT?: (slice: SLICE, data: any, next: NextFunction<any>, props: BehaviorProps, options: OPTIONS, key: string) => void | Promise<void>;
764
+ ELEMENT?: BehaviorHandler<SLICE, OPTIONS, ElementCommands> | ElementCommands;
765
+ [sink: string]: BehaviorHandler<SLICE, OPTIONS, any> | string | number | boolean | object | null | undefined;
766
+ }
767
+
768
+ /** The object passed to `defineBehavior()`. Reducers and sinks get the slice, not the host state (HOST: the host state). */
769
+ export interface BehaviorDefinition<SLICE = any, ACTIONS = {}, CALCULATED = {}, OPTIONS = Record<string, any>> {
770
+ /** The slice a host starts with (`state[key]`); options naming one of its keys override it. */
771
+ initialState: SLICE;
772
+ /** Actions named without the key (`NEXT`); the host sees them as `'<key>.NEXT'`. `key`: the use's key in `uses`. */
773
+ intent?: (sources: BehaviorSources<SLICE>, options: OPTIONS, key: string) => { [ACTION in keyof ACTIONS]: Stream<ACTIONS[ACTION]> };
774
+ /**
775
+ * Model entries on the slice: ABORT, or the slice itself, means no change. `next('X')` names
776
+ * the behavior's own actions. Handlers get `(slice, data, next, props, options, key)`; a
777
+ * `HOST` entry gets the host's whole state. (The slice's calculated fields are there at
778
+ * runtime, but typed only on the host's state: TypeScript can't infer them while typing these
779
+ * functions.)
780
+ */
781
+ model?: { [action: string]: BehaviorModelEntry<SLICE, OPTIONS> };
782
+ /** Fields of the slice (`state.pager.offset`), stored on it and recomputed when it changes. */
783
+ calculated?: { [FIELD in keyof CALCULATED]: (slice: SLICE) => CALCULATED[FIELD] };
784
+ /**
785
+ * D197: timers for the host (`makeTimerDriver()`), from the slice: `{ name: spec }`, as the
786
+ * `timers` static. The host sees them as `'<key>.<name>'`; a spec's `action` (or `frame`)
787
+ * naming one of the behavior's actions is namespaced (`'SHOW'` → `'<key>.SHOW'`), any other
788
+ * name is sent to the host as written. They join the host's own `timers`.
789
+ */
790
+ timers?: (slice: SLICE, options: OPTIONS, key: string) => Timers;
791
+ /** `false`: the slice is UI state (sortable's drag): a root's `persist()` neither saves nor restores it (the root's own `uses` only: a sub-component's or Collection item's slice is saved with its data) */
792
+ persist?: false;
793
+ /**
794
+ * The actions that complete one undoable step (sortable: `['DROPPED']`). With `undo()` on the
795
+ * same host, the behavior's other actions are steps of a gesture: their changes to the undo
796
+ * key aren't recorded; the completing action records the value from before the gesture as
797
+ * one entry (none for a gesture that ends without it, or whose steps bring the value back: the
798
+ * same value, or an array / plain object with the same entries by identity), whatever the
799
+ * `uses` order. A recorded change, UNDO or REDO mid-gesture first records the value from
800
+ * before it; `track` / `coalesce` naming any of the behavior's actions cover its steps (a
801
+ * `track` naming none of them: the gesture is never recorded; drops join only when `coalesce`
802
+ * names one).
803
+ */
804
+ undoStep?: string[];
805
+ }
806
+
807
+ /** One use of a behavior (a `defineBehavior()` factory's result), for a component's `uses`. */
808
+ export interface Behavior<SLICE = any, ACTIONS = any, CALCULATED = {}, OPTIONS = any> {
809
+ readonly initialState: SLICE;
810
+ readonly options: OPTIONS;
811
+ /** The slice a host starts with: initialState, the options naming its keys, the calculated fields. */
812
+ readonly state: SLICE & CALCULATED;
813
+ /** Merges the behavior into a component instance under `key`; called by the core for each `uses` entry. */
814
+ merge(component: any, key: string): void;
815
+ /** Phantom, types only */
816
+ readonly __behavior?: { actions: ACTIONS };
817
+ }
818
+
819
+ /** What `defineBehavior()` returns: call it with the options of one use. */
820
+ export type BehaviorFactory<SLICE = any, ACTIONS = {}, CALCULATED = {}, OPTIONS = Record<string, any>> =
821
+ (options?: OPTIONS & Partial<SLICE>) => Behavior<SLICE, ACTIONS, CALCULATED, OPTIONS>
822
+
823
+ /**
824
+ * A reusable piece of state, intent and model (GS-1). A host uses it under a key:
825
+ *
826
+ * const pager = defineBehavior({
827
+ * initialState: { page: 0, pageSize: 20 },
828
+ * intent: ({ DOM }, { next, prev }) => ({ NEXT: DOM.click(next), PREV: DOM.click(prev) }),
829
+ * model: { NEXT: (p) => ({ ...p, page: p.page + 1 }), PREV: (p) => (p.page === 0 ? ABORT : { ...p, page: p.page - 1 }) },
830
+ * calculated: { offset: (p) => p.page * p.pageSize },
831
+ * })
832
+ * TaskList.uses = { pager: pager({ pageSize: 10, next: Newer, prev: Older }) } // state.pager, 'pager.NEXT'
833
+ *
834
+ * A host model entry for a behavior action ('pager.NEXT') runs after the behavior's: its STATE
835
+ * reducer gets the full state with the behavior's update, its EFFECT runs too, and its value
836
+ * sinks (EVENTS, PARENT, drivers) replace the behavior's. A host intent action of the same name
837
+ * replaces the behavior's trigger.
838
+ */
839
+ export function defineBehavior<SLICE extends Record<string, any>, ACTIONS = {}, CALCULATED = {}, OPTIONS = Record<string, any>>(
840
+ definition: BehaviorDefinition<SLICE, ACTIONS, CALCULATED, OPTIONS>
841
+ ): BehaviorFactory<SLICE, ACTIONS, CALCULATED, OPTIONS>
842
+
843
+ /** The slice a behavior gives its host (initialState & calculated fields). */
844
+ export type BehaviorState<BEHAVIOR> = BEHAVIOR extends Behavior<infer SLICE, any, infer CALCULATED, any> ? SLICE & CALCULATED : never
845
+
846
+ /** The state keys a `uses` object adds: `type State = { tasks: Task[] } & UsesState<typeof uses>`. */
847
+ export type UsesState<USES> = { [KEY in keyof USES]: BehaviorState<USES[KEY]> }
848
+
849
+ type UsesActionEntries<USES> = {
850
+ [KEY in keyof USES & string]: USES[KEY] extends Behavior<any, infer ACTIONS, any, any>
851
+ ? { [ACTION in keyof ACTIONS & string]: [`${KEY}.${ACTION}`, ACTIONS[ACTION]] }[keyof ACTIONS & string]
852
+ : never
853
+ }[keyof USES & string]
854
+
855
+ /** The namespaced actions a `uses` object adds ('pager.NEXT'), for a typed ACTIONS map. */
856
+ export type UsesActions<USES> = { [ENTRY in UsesActionEntries<USES> as ENTRY[0]]: ENTRY[1] }
857
+
858
+ // ── First-party behaviors (PLAN-4 GS-1, GS-8) ──────────────────────
859
+
860
+ /** Where a behavior option takes something to listen to: a control or a CSS selector. */
861
+ export type BehaviorTarget = AnyControl | string
862
+
863
+ /** A `pager` slice: `state.pager`. */
864
+ export interface PagerState { page: number; pageSize: number; total: number | null }
865
+ /** A `pager` slice's calculated fields. `pages` is null while `total` is unknown. */
866
+ export interface PagerCalculated { offset: number; pages: number | null; hasPrev: boolean; hasNext: boolean }
867
+ export interface PagerOptions {
868
+ /** Items per page (default 20) */
869
+ pageSize?: number;
870
+ /** Starting page, from 0 (default 0) */
871
+ page?: number;
872
+ /** Item count; null (default) = unknown, NEXT has no upper bound */
873
+ total?: number | null;
874
+ /** Its clicks dispatch NEXT */
875
+ next?: BehaviorTarget;
876
+ /** Its clicks dispatch PREV */
877
+ prev?: BehaviorTarget;
878
+ }
879
+ export interface PagerActions { NEXT: any; PREV: any; GOTO: number; SET_TOTAL: number }
880
+
881
+ /**
882
+ * A page cursor (GS-1): `List.uses = { pager: pager({ pageSize: 10, total, next: Newer, prev: Older }) }`
883
+ * gives `state.pager = { page, pageSize, total, offset, pages, hasPrev, hasNext }` and the actions
884
+ * 'pager.NEXT' / 'pager.PREV' (no change at the bounds), 'pager.GOTO' (a page number, clamped)
885
+ * and 'pager.SET_TOTAL' (the item count).
886
+ */
887
+ export function pager(options?: PagerOptions): Behavior<PagerState, PagerActions, PagerCalculated, PagerOptions>
888
+
889
+ // ── Forms (PLAN-5 F-1, D193) ───────────────────────────────────────
890
+
891
+ /** Field errors by field name (`'email'`, `'addresses.7.city'`; `''` = form-level). */
892
+ export type FieldErrors = Record<string, string>
893
+
894
+ /** One field of a `form` slice: `state.form.fields.email`, `state.form.fields['addresses.7.city']`. */
895
+ export interface FormField {
896
+ /** The field name, for `name={f.name}` (the path in `values`; rows of an array by id) */
897
+ name: string;
898
+ value: any;
899
+ /** What to show: a server or check error at once, a schema error once the field is touched (per `show`) or after a submit; '' when none */
900
+ error: string;
901
+ /** `!!error`, for `aria-invalid={f.email.invalid}` */
902
+ invalid: boolean;
903
+ touched: boolean;
904
+ /** The value differs from `initial` */
905
+ dirty: boolean;
906
+ /** Its async `check` is running */
907
+ pending: boolean;
908
+ }
909
+
910
+ /** A `form` slice's stored fields: `state.form`. */
911
+ export interface FormState<V = any> {
912
+ values: V;
913
+ /** The values the form started with (or was last saved / reset with) */
914
+ initial: V;
915
+ /** Every current schema error by field name, shown or not */
916
+ errors: FieldErrors;
917
+ touched: Record<string, boolean>;
918
+ /** Server errors (`form.ERRORS`) */
919
+ server: FieldErrors;
920
+ /** Async check results by field name ('' = passed) */
921
+ remote: FieldErrors;
922
+ /** Field name → the value its check is running for */
923
+ pending: Record<string, any>;
924
+ /** A valid submit sent its request (the submit entry has the `http` sink) and has no `form.DONE` / `form.ERRORS` yet; a submit that sends nothing is done at once */
925
+ submitting: boolean;
926
+ /** `form.DONE` arrived, or a submit that sends nothing was dispatched */
927
+ submitted: boolean;
928
+ submitCount: number;
929
+ /** A submit waits for an async check or an async schema */
930
+ queued: boolean;
931
+ /** The schema hasn't answered for the current values yet (an async one is running; also at the start) */
932
+ validating: boolean;
933
+ /** The schema has answered at least once since the start (or the last `form.RESET`) */
934
+ validated: boolean;
935
+ }
936
+
937
+ /** A `form` slice's calculated fields. */
938
+ export interface FormCalculated {
939
+ /** Every field of `values` by name (leaves, arrays, and array rows' fields by row id) */
940
+ fields: Record<string, FormField>;
941
+ /** No schema error and no failed check, as of the schema's last answer (false until its first; an async re-validation keeps the previous value) */
942
+ valid: boolean;
943
+ dirty: boolean;
944
+ /** The form-level message: a server error without a field, or a schema issue without a path after a submit */
945
+ error: string;
946
+ }
947
+
948
+ /** An async per-field check through a driver (a request with reply actions). */
949
+ export interface FormFieldCheck {
950
+ /** The driver request for a value (without `ok` / `error` / `latest`: the form sets them, SYG235) */
951
+ request: (value: any) => Record<string, any>;
952
+ /** The message for a reply body, or a falsy value when the value is fine */
953
+ error?: (body: any) => string | false | null | undefined;
954
+ }
955
+
956
+ export interface FormOptions<V = any> {
957
+ /**
958
+ * The start values; field names are paths in it (rows of an array of objects need an `id`, SYG236).
959
+ * Or a function of the host component's state, for start values that come from state (a default
960
+ * from the settings): called when the form starts, and each time it starts over (`resetOnShow`)
961
+ */
962
+ values: V | ((state: any) => V);
963
+ /** The host action a valid submit dispatches with the schema's output (SYG234 when it has no model entry). With an `http` sink in its entry the submit is pending until `form.DONE` / `form.ERRORS`; otherwise it is done at once */
964
+ submit: string;
965
+ /** Async checks by field name: run on blur and before a submit, which waits for them */
966
+ check?: Record<string, FormFieldCheck>;
967
+ /** When a schema error shows: after the field's blur (default), on input, or only after a submit */
968
+ show?: 'blur' | 'input' | 'submit';
969
+ /** The form element's selector (default 'form'): input, focusout and submit are heard on it */
970
+ form?: string;
971
+ /** The driver sink the checks' requests go to, and that makes a submit entry a request (default 'HTTP') */
972
+ http?: string;
973
+ /**
974
+ * Start over each time the form is shown (D239): when its host component starts (mounted again,
975
+ * a Switchable page made again) and each time the form element appears again (a Switchable page
976
+ * shown again; hidden pages stay alive). Back to the start values, touched, errors and the submit
977
+ * state cleared. Default false: the values live in state and survive a visit (a wizard step keeps
978
+ * them when the user comes back)
979
+ */
980
+ resetOnShow?: boolean;
981
+ }
982
+
983
+ export interface FormActions {
984
+ /** A field changed (the form element's input events): `{ name, value }`; a checkbox gives `checked` as `value` and its own value as `item` (on an array field: added or removed) */
985
+ CHANGE: { name: string; value: any; item?: any };
986
+ /** A field lost focus (focusout): its name */
987
+ BLUR: string;
988
+ /** The form element's submit (default prevented) */
989
+ SUBMIT: any;
990
+ /** Adds a row to an array field (with the next id): `{ field: 'addresses', value: { street: '' } }` */
991
+ ADD: { field: string; value?: Record<string, any> };
992
+ /** Removes a row by id: `{ field: 'addresses', id }` */
993
+ REMOVE: { field: string; id: any };
994
+ /** Server errors: an error reply (`{ status, body: { errors } }`) or a map `{ name: message }`; focuses the first */
995
+ ERRORS: any;
996
+ /** The submit was saved: `initial` becomes `values` */
997
+ DONE: any;
998
+ /** Back to `initial` (or to the values given) */
999
+ RESET: any;
1000
+ }
1001
+
1002
+ /**
1003
+ * A form with validation (PLAN-5 F-1), any Standard Schema validator (zod, valibot, arktype, a
1004
+ * hand-written object):
1005
+ *
1006
+ * Signup.uses = { form: form(signupSchema, { values: { email: '', password: '' }, submit: 'SIGN_UP' }) }
1007
+ * // view: const f = state.form.fields; <form><input name="email" value={f.email.value} aria-invalid={f.email.invalid} /></form>
1008
+ * // the host's SIGN_UP gets the schema's output
1009
+ *
1010
+ * Fields are matched by `name=` inside the form element (Collection items' fields too). A failed
1011
+ * submit focuses the first invalid field. A host that sends the submit as a request answers it with
1012
+ * `form.DONE` / `form.ERRORS` (reply actions `ok: 'form.DONE', error: 'form.ERRORS'`); a submit
1013
+ * that sends nothing (STATE, PARENT, EVENTS) is done at once. Throws SYG231 when
1014
+ * `schema` isn't a Standard Schema.
1015
+ */
1016
+ export function form<V = any>(schema: StandardSchemaLike, options: FormOptions<V>): Behavior<FormState<V>, FormActions, FormCalculated, FormOptions<V>>
1017
+
1018
+ /** A Standard Schema's result for `values`: errors by field name, and the output when there are none. A Promise for an async schema. */
1019
+ export function checkForm(schema: StandardSchemaLike, values: any): { errors: FieldErrors; value: any } | Promise<{ errors: FieldErrors; value: any }>
1020
+ /** The errors only ({} when valid); a Promise of them for an async schema. */
1021
+ export function formErrors(schema: StandardSchemaLike, values: any): FieldErrors | Promise<FieldErrors>
1022
+ /** `values` with the field at `name` set (immutable; rows of an array by id). */
1023
+ export function setField<V>(values: V, name: string, value: any): V
1024
+ /** The value at a field name. */
1025
+ export function getField(values: any, name: string): any
1026
+ /** Whether `name` is a field of `values` (its path exists, also when the value is undefined; rows by id). */
1027
+ export function hasField(values: any, name: string): boolean
1028
+ /** The field name of a schema issue path (array indexes become row ids). */
1029
+ export function fieldName(values: any, path?: ReadonlyArray<any>): string
1030
+ /** Every field name of `values` (leaves, arrays, rows' fields). */
1031
+ export function fieldNames(values: any): string[]
1032
+ /** Server errors (an error reply, a map, or a list of issues) as field errors; '' for a form-level message. */
1033
+ export function replyErrors(reply: any, values?: any): FieldErrors
1034
+ /** An ELEMENT command focusing the first field (DOM order) named in `names` (or with a message in an errors map), children included; with `within` (the form element's selector), only fields inside it; ABORT when none. */
1035
+ export function focusInvalid(names: string[] | FieldErrors, within?: string): ElementCommand | ABORT
1036
+
1037
+ /** A `selection` slice: the selected ids, as strings, in selection order. */
1038
+ export interface SelectionState { selected: string[] }
1039
+ export interface SelectionCalculated { count: number }
1040
+ export interface SelectionOptions {
1041
+ /** Several items at once: an item click toggles its id (default false: it replaces the selection) */
1042
+ multi?: boolean;
1043
+ /** The control on each item; its clicks dispatch SELECT with the item's `attr` */
1044
+ item?: BehaviorTarget;
1045
+ /** Select-all toggle: dispatches TOGGLE_ALL (needs `from`) */
1046
+ all?: BehaviorTarget;
1047
+ /** Its clicks dispatch CLEAR */
1048
+ clear?: BehaviorTarget;
1049
+ /** The item element's attribute that holds its id (default 'data-id') */
1050
+ attr?: string;
1051
+ /** The host state key of the item list, for SELECT_ALL / TOGGLE_ALL */
1052
+ from?: string;
1053
+ /** The id field of the `from` list's items (default 'id') */
1054
+ idField?: string;
1055
+ }
1056
+ export interface SelectionActions { SELECT: string | number | Event; SELECT_ALL: Array<string | number> | undefined; TOGGLE_ALL: Array<string | number> | Event | undefined; CLEAR: any }
1057
+
1058
+ /**
1059
+ * Single or multiple selection (GS-1): `uses = { sel: selection({ multi: true, item: Pick, all: All, from: 'mails' }) }`
1060
+ * gives `state.sel = { selected, count }` and 'sel.SELECT' (an id or an item click),
1061
+ * 'sel.SELECT_ALL' (ids, or every id of `state[from]`), 'sel.TOGGLE_ALL' and 'sel.CLEAR'.
1062
+ */
1063
+ export function selection(options?: SelectionOptions): Behavior<SelectionState, SelectionActions, SelectionCalculated, SelectionOptions>
1064
+
1065
+ /** Is `id` in a `selection` slice? Ids compare as strings. */
1066
+ export function isSelected(slice: SelectionState | undefined | null, id: string | number): boolean
1067
+
1068
+ /** A `sortable` slice: the drag state the view styles from, and the live-region text. */
1069
+ export interface SortableState {
1070
+ /** The id (as a string) of the item being moved, or null */
1071
+ dragging: string | null;
1072
+ /** The id the pointer is over (pointer drags), or null */
1073
+ over: string | null;
1074
+ /** true: the item lands after `over`; false: before it */
1075
+ after: boolean;
1076
+ /** The `from` key the item would land in (pointer drags; two lists) */
1077
+ list: string | null;
1078
+ mode: 'pointer' | 'keyboard' | null;
1079
+ /** The text for an ARIA live region: lift, move, drop and cancel announcements */
1080
+ message: string;
1081
+ /** A `uid()` id for the instructions element the handles' `aria-describedby` names, unique per host (null until the first focus, press or key inside the host) */
1082
+ helpId: string | null;
1083
+ /** Internal: the pointer press before the threshold (`n`: the instance that started it) */
1084
+ press: { id: string; x: number; y: number; n: number } | null;
1085
+ /** Internal: where the item started (`n`: a keyboard drag's instance) */
1086
+ origin: { list: string; index: number; n?: number } | null;
1087
+ }
1088
+ export interface SortableOptions {
1089
+ /** The host state key of the list; an array of keys allows moves between lists (each container marked `data-list="<key>"`) */
1090
+ from: string | string[];
1091
+ /** Selector of each item's element, which carries its id in `attr` (default `[data-id]`) */
1092
+ item?: string;
1093
+ /** Selector of the part that starts a drag and takes keyboard focus (default: the item) */
1094
+ handle?: string;
1095
+ /** 'y' (default): ArrowUp / ArrowDown move, ArrowLeft / ArrowRight change lists; 'x': the other way round */
1096
+ axis?: 'x' | 'y';
1097
+ /** Pixels a pointer moves before a drag starts (default 4) */
1098
+ threshold?: number;
1099
+ /** The item element's attribute that holds its id (default 'data-id') */
1100
+ attr?: string;
1101
+ /** The id field of the list's items (default 'id') */
1102
+ idField?: string;
1103
+ /** An item's name in announcements (default: its title, name, label or id) */
1104
+ label?: (item: any) => string;
1105
+ /**
1106
+ * Announcement texts. `extra`: lift, true for a keyboard lift; move / drop / stay, the list key when the item changed lists.
1107
+ * `cancel`: Escape put the item back; `stay`: Escape after something else changed the list, so the item stays where it is
1108
+ */
1109
+ messages?: Partial<Record<'lift' | 'move' | 'drop' | 'cancel' | 'stay', (label: string, position: number, count: number, extra?: any) => string>>;
1110
+ }
1111
+ /** 'sort.DROPPED': one completed move (pointer drop, or keyboard drop away from where it started) */
1112
+ export interface SortableDropped { id: string; list: string; index: number; fromList: string; fromIndex: number }
1113
+ export interface SortableActions {
1114
+ INIT: any; HELP: any; END: any; PRESS: any; MOVE: any; UP: any; CANCEL: any;
1115
+ KEY: { key: string; id?: string };
1116
+ DROPPED: SortableDropped;
1117
+ }
1118
+
1119
+ /**
1120
+ * Drag-and-drop reordering of `state[from]` by pointer (mouse, touch, pen) and keyboard (PLAN-5
1121
+ * B-1): `uses = { sort: sortable({ from: 'tasks', item: '.task', handle: '.grip' }) }`. Works on
1122
+ * Collection items (it listens on the host's root). 'sort.DROPPED' fires once per completed move;
1123
+ * `state.sort.message` is the live-region text.
1124
+ */
1125
+ export function sortable(options: SortableOptions): Behavior<SortableState, SortableActions, {}, SortableOptions>
1126
+
1127
+ /** `state.history` of `undoable()` / `undo()`: snapshots of `state[key]` (`key: ['a', 'b']`: of `{ a, b }`), newest last in `past`. */
1128
+ export interface UndoHistory<T = any> {
1129
+ past: T[]; future: T[];
1130
+ /** Internal (`undo()` with a gesture behavior, `undoStep`): [the value before the gesture in progress, the value after its last step, the behavior's key in `uses`] */
1131
+ base?: [T, T, string?];
1132
+ }
1133
+ export interface UndoOptions {
1134
+ /** The state key whose value is snapshotted; several keys (`['todo', 'done']`: a sortable's lists) are snapshotted together as `{ todo, done }` */
1135
+ key: string | string[];
1136
+ /** The most snapshots kept in `past` (default 100) */
1137
+ limit?: number;
1138
+ /** Only these actions are recorded (default: every action with a STATE reducer) */
1139
+ track?: string[];
1140
+ /**
1141
+ * Changes by one action within this many ms join one undo step (default 0: off; 500 when
1142
+ * `coalesce` is given). Without `coalesce`, every action's quick repeats join (not a gesture's
1143
+ * drops: those join only when `coalesce` names a gesture action)
1144
+ */
1145
+ coalesceMs?: number;
1146
+ /**
1147
+ * Only these actions' quick repeats join one step (typing); every other action is always its
1148
+ * own step: `undo({ key: 'poster', coalesce: ['HEADLINE'], coalesceMs: 1000 })`
1149
+ */
1150
+ coalesce?: string[];
1151
+ /** These actions clear the history (a load) and are not recorded */
1152
+ resetOn?: string[];
1153
+ }
1154
+
1155
+ /**
1156
+ * Undo / redo for `state[key]` (GS-8): wraps the model's STATE reducers so each change pushes
1157
+ * the old value onto `state.history.past`, and adds UNDO and REDO (no change when there is
1158
+ * nothing to undo or redo). `Editor.model = undoable({ TYPE: ... }, { key: 'doc', coalesceMs: 500 })`.
1159
+ */
1160
+ export function undoable<MODEL extends Record<string, any>>(model: MODEL, options: UndoOptions): MODEL & { UNDO: any; REDO: any }
1161
+
1162
+ /** `undo()` options: undoable()'s, plus the controls that trigger UNDO / REDO. */
1163
+ export interface UndoBehaviorOptions extends UndoOptions { undo?: BehaviorTarget; redo?: BehaviorTarget }
1164
+
1165
+ /**
1166
+ * undoable() as a behavior (GS-8): `uses = { history: undo({ key: 'doc', undo: UndoButton, redo: RedoButton }) }`
1167
+ * gives `state.history = { past, future, canUndo, canRedo }` and 'history.UNDO' / 'history.REDO'.
1168
+ */
1169
+ export function undo(options: UndoBehaviorOptions): Behavior<UndoHistory, { UNDO: any; REDO: any }, { canUndo: boolean; canRedo: boolean }, UndoBehaviorOptions>
1170
+
1171
+ /** The top-level state keys of STATE (any string while STATE is unknown) */
1172
+ type PersistKey<STATE> = 0 extends (1 & STATE) ? string : keyof STATE & string
1173
+
1174
+ /**
1175
+ * A synchronous storage for `persist({ storage })` (async storages are not supported).
1176
+ * `subscribe` (optional) is what `sync: true` listens to instead of the window `storage` event:
1177
+ * call `fn(key, newValue)` when another writer changes a key; return the unsubscribe function.
1178
+ */
1179
+ export interface PersistStorage {
1180
+ getItem(key: string): string | null
1181
+ setItem(key: string, value: string): void
1182
+ removeItem(key: string): void
1183
+ subscribe?(fn: (key: string, newValue: string | null) => void): () => void
1184
+ }
1185
+
1186
+ /** PLAN-4 GS-5: `persist()` options */
1187
+ export interface PersistOptions<STATE = any> {
1188
+ /** The storage key */
1189
+ key: string
1190
+ /** The top-level state keys to save (default: all but `omit`) */
1191
+ pick?: ReadonlyArray<PersistKey<STATE>>
1192
+ /** The top-level state keys not to save (calculated fields are never saved) */
1193
+ omit?: ReadonlyArray<PersistKey<STATE>>
1194
+ /** The version saved with the state (default 1). A stored entry with another version goes through `migrate` */
1195
+ version?: number
1196
+ /**
1197
+ * Turns a stored entry of another version into this version's picked keys; nothing (undefined
1198
+ * or null) discards it. Without migrate such an entry is ignored. If it throws: SYG642.
1199
+ */
1200
+ migrate?: (old: any, fromVersion: number) => Partial<STATE> | null | undefined | void
1201
+ /** 'local' (localStorage, the default), 'session' (sessionStorage) or a synchronous adapter */
1202
+ storage?: 'local' | 'session' | PersistStorage
1203
+ /** Apply other tabs' writes to this key (a RESTORE action) */
1204
+ sync?: boolean
1205
+ /**
1206
+ * Restore in a RESTORE action after the first render (so the first render matches the server's
1207
+ * HTML) instead of before INITIALIZE. Detected when omitted: run()'s mount point holds
1208
+ * renderToString markup (its root element has `data-sygnal-ssr`), a server-rendered Astro
1209
+ * island, a Vike hydration. true / false override it
1210
+ */
1211
+ hydrate?: boolean
1212
+ /** Writes wait for this many ms without a state change (default 100); flushed on pagehide and dispose */
1213
+ debounceMs?: number
1214
+ /**
1215
+ * 'versioned' (the default) stores `{ version, state }`; 'plain' stores the picked keys
1216
+ * themselves (`{ title, body }`), with no `version` / `migrate`
1217
+ */
1218
+ format?: 'versioned' | 'plain'
159
1219
  }
160
1220
 
161
- export type EventsSource<EVENTS = any> = Stream<Event<EVENTS>> & {
162
- select<T = any>(type: string): Stream<T>;
1221
+ /**
1222
+ * The options `persist()` takes: `format: 'plain'` rules out `version` and `migrate` (a plain
1223
+ * entry has no version to migrate from)
1224
+ */
1225
+ export type PersistOptionsFor<STATE = any> =
1226
+ | (PersistOptions<STATE> & { format?: 'versioned' })
1227
+ | (Omit<PersistOptions<STATE>, 'version' | 'migrate' | 'format'> & { format: 'plain'; version?: never; migrate?: never })
1228
+
1229
+ /** The value `persist()` returns: set it as the root component's `persist` static */
1230
+ export interface Persist<STATE = any> {
1231
+ readonly options: PersistOptions<STATE>
1232
+ /** Called by the core on the root component (internal) */
1233
+ setup(component: any): void
163
1234
  }
164
1235
 
1236
+ /**
1237
+ * PLAN-4 GS-5: save the root component's state and restore it at startup:
1238
+ * `TodoApp.persist = persist({ key: 'todo-app', pick: ['todos', 'filter'], version: 2, migrate })`.
1239
+ * Stored as JSON `{ version, state }` (`format: 'plain'`: the picked keys themselves). The restore is merged into initialState (part of
1240
+ * INITIALIZE); writes are debounced (`debounceMs`) and flushed on pagehide and dispose.
1241
+ * `PERSIST: { clear: true }` in a model entry removes the stored copy. Root component only
1242
+ * (SYG224); failures are SYG642 (warn) and the app continues on initialState.
1243
+ */
1244
+ export function persist<STATE = any>(options: PersistOptionsFor<[STATE] extends [infer S] ? S : never>): Persist<STATE>
1245
+
1246
+ type EventsSelect = keyof SygnalEvents extends never
1247
+ ? { select<T = any>(type: string): Stream<T>; }
1248
+ : { select<TYPE extends keyof SygnalEvents & string>(type: TYPE): Stream<SygnalEvents[TYPE]>; }
1249
+
1250
+ export type EventsSource<EVENTS = any> = Stream<Event<EVENTS>> & EventsSelect
1251
+
165
1252
  export type DefaultDrivers<STATE, EVENTS = any> = {
166
1253
  STATE: {
167
1254
  source: StateSource<STATE>;
@@ -217,21 +1304,67 @@ export type FixDrivers<DRIVERS> =
217
1304
 
218
1305
  type CombinedSources<STATE, DRIVERS> = Sources<DefaultDrivers<STATE> & DRIVERS> & { dispose$: Stream<boolean> }
219
1306
 
220
- interface ComponentIntent<STATE, DRIVERS, ACTIONS> {
221
- (args: CombinedSources<STATE, DRIVERS>): Partial<IntentActions<ACTIONS>>
1307
+ /**
1308
+ * The sources object an intent function receives (DOM, STATE, EVENTS, CHILD, dispose$, plus
1309
+ * any custom drivers). Use it to annotate an intent declared before its component:
1310
+ *
1311
+ * const intent = ({ DOM }: IntentSources<State>) => ({ INC: DOM.click('.inc') })
1312
+ */
1313
+ export type IntentSources<STATE = any, DRIVERS = {}> = CombinedSources<STATE, FixDrivers<DRIVERS>>
1314
+
1315
+ type StreamPayload<STREAM> = STREAM extends Stream<infer T> ? T : any
1316
+
1317
+ type IntentReturnToActions<RETURN> = {
1318
+ [ACTION_KEY in keyof RETURN & string]-?: StreamPayload<Exclude<RETURN[ACTION_KEY], undefined>>
1319
+ }
1320
+
1321
+ /**
1322
+ * Derives the ACTIONS map (action name → payload type) from an intent function's type
1323
+ * (or from its return object type): each key's `Stream<T>` becomes `T`.
1324
+ *
1325
+ * const intent = ({ DOM }: IntentSources<State>) => ({
1326
+ * INC: DOM.click('.inc').mapTo(1), // Stream<number>
1327
+ * NAME: DOM.input('.name').value(), // Stream<string>
1328
+ * })
1329
+ * const Counter: Component<State, {}, {}, ActionsOf<typeof intent>> = ...
1330
+ * Counter.intent = intent
1331
+ * Counter.model = { INC: (state, n) => ..., NAME: (state, name) => ... } // n: number, name: string
1332
+ *
1333
+ * With it, model keys not returned by the intent are type errors (the built-ins BOOTSTRAP,
1334
+ * INITIALIZE and DISPOSE stay allowed). Actions reached only through `next()` or as reply actions
1335
+ * of a request (`ok: 'LOADED'`, `error: 'FAILED'`) are added explicitly, typed by their data:
1336
+ *
1337
+ * type Actions = ActionsOf<typeof intent> & { SAVED: { id: string }; LOADED: Quote; FAILED: FetchFailure }
1338
+ */
1339
+ export type ActionsOf<INTENT> = INTENT extends (...args: any[]) => infer RETURN
1340
+ ? IntentReturnToActions<RETURN>
1341
+ : IntentReturnToActions<INTENT>
1342
+
1343
+ interface ComponentIntent<STATE, DRIVERS, ACTIONS, CALCULATED = {}> {
1344
+ (args: IntentArgs<STATE, DRIVERS, CALCULATED>): Partial<IntentActions<ACTIONS>>
222
1345
  }
223
1346
 
1347
+ /**
1348
+ * What a component's intent receives. With calculated fields, STATE also matches
1349
+ * `StateSource<STATE>` (a stream of STATE & CALCULATED is also one of STATE), so an intent
1350
+ * annotated with the documented `IntentSources<State>` is accepted as well as
1351
+ * `IntentSources<State & Calculated>`; an inline intent sees the calculated fields.
1352
+ */
1353
+ type IntentArgs<STATE, DRIVERS, CALCULATED> = keyof CALCULATED extends never
1354
+ ? CombinedSources<STATE, DRIVERS>
1355
+ : CombinedSources<STATE & CALCULATED, DRIVERS> & { STATE: StateSource<STATE> }
1356
+
224
1357
  type CalculatedFieldValue<FULL_STATE, RETURN> =
225
1358
  | StateOnlyReducer<FULL_STATE, RETURN>
226
1359
  | [ReadonlyArray<string & keyof FULL_STATE>, StateOnlyReducer<FULL_STATE, RETURN>]
227
1360
 
228
1361
  type Calculated<STATE, CALCULATED> = keyof CALCULATED extends never
229
- ? { [field: string]: boolean | CalculatedFieldValue<STATE, any> }
230
- : { [CALCULATED_KEY in keyof CALCULATED]: boolean | CalculatedFieldValue<STATE & CALCULATED, CALCULATED[CALCULATED_KEY]> }
1362
+ ? { [field: string]: CalculatedFieldValue<STATE, any> }
1363
+ : { [CALCULATED_KEY in keyof CALCULATED]: CalculatedFieldValue<STATE & CALCULATED, CALCULATED[CALCULATED_KEY]> }
231
1364
 
232
1365
  type Context<STATE, CONTEXT> = keyof CONTEXT extends never
233
- ? { [field: string]: boolean | StateOnlyReducer<STATE, any> }
234
- : { [CONTEXT_KEY in keyof CONTEXT]: boolean | StateOnlyReducer<STATE, CONTEXT[CONTEXT_KEY]> }
1366
+ ? { [field: string]: StateOnlyReducer<STATE, any> }
1367
+ : { [CONTEXT_KEY in keyof CONTEXT]: StateOnlyReducer<STATE, CONTEXT[CONTEXT_KEY]> }
235
1368
 
236
1369
  export type Lense<PARENT_STATE = any, CHILD_STATE = any> = {
237
1370
  get: (state: PARENT_STATE) => CHILD_STATE;
@@ -244,12 +1377,23 @@ export type Filter<ITEM = any> = (item: ITEM) => boolean
244
1377
 
245
1378
  export type SortFunction<ITEM = any> = (a: ITEM, b: ITEM) => number
246
1379
 
1380
+ /** Sort by one field: `{ name: 'asc' }`, `{ priority: -1 }` (one key; 1 = ascending). */
247
1381
  export type SortObject<ITEM = any> = {
248
- [field: string]: 'asc' | 'desc' | SortFunction<ITEM>
1382
+ [field: string]: 'asc' | 'desc' | 1 | -1
249
1383
  }
250
1384
 
251
1385
  /**
252
- * Sygnal Component
1386
+ * A Collection `sort`: 'asc'/'desc' (whole items), a field name (ascending), a comparator,
1387
+ * a SortObject, or an array of field names, SortObjects and comparators applied in order.
1388
+ */
1389
+ export type SortSpec<ITEM = any> =
1390
+ | string
1391
+ | SortFunction<ITEM>
1392
+ | SortObject<ITEM>
1393
+ | ReadonlyArray<string | SortFunction<ITEM> | SortObject<ITEM>>
1394
+
1395
+ /**
1396
+ * Sygnal Component
253
1397
  */
254
1398
  export type Component<
255
1399
  STATE = any,
@@ -258,26 +1402,123 @@ export type Component<
258
1402
  ACTIONS = {},
259
1403
  CALCULATED = {},
260
1404
  CONTEXT = {},
261
- SINK_RETURNS extends NonStateSinkReturns = {}
1405
+ SINK_RETURNS extends NonStateSinkReturns = {},
1406
+ PROVIDED_CONTEXT = CONTEXT
262
1407
  > = ComponentProps<STATE & CALCULATED, PROPS, CONTEXT> & {
263
- label?: string;
264
- DOMSourceName?: string;
265
- stateSourceName?: string;
266
- requestSourceName?: string;
1408
+ /** The component's name (diagnostics, devtools, uid()); defaults to the function's name. */
1409
+ componentName?: string;
267
1410
  model?: ComponentModel<STATE, PROPS, FixDrivers<DRIVERS>, ACTIONS, CALCULATED, SINK_RETURNS, CONTEXT>;
268
- intent?: ComponentIntent<STATE & CALCULATED, FixDrivers<DRIVERS>, ACTIONS>;
1411
+ intent?: ComponentIntent<STATE, FixDrivers<DRIVERS>, ACTIONS, CALCULATED>;
269
1412
  initialState?: STATE;
1413
+ /**
1414
+ * Give a sub-component its own state instead of the slice its parent passes in.
1415
+ * Required to use `initialState` on a sub-component (otherwise SYG405). Without a
1416
+ * `state` prop the state is local to the instance and never written to the parent.
1417
+ */
1418
+ isolatedState?: boolean;
270
1419
  calculated?: Calculated<STATE, CALCULATED>;
271
- storeCalculatedInState?: boolean;
272
- context?: Context<STATE & CALCULATED, CONTEXT>;
273
- peers?: { [name: string]: Component };
274
- components?: { [name: string]: Component };
1420
+ /**
1421
+ * Context this component provides to itself and its descendants. Typed by PROVIDED_CONTEXT,
1422
+ * which defaults to CONTEXT (the context the view and reducers see).
1423
+ */
1424
+ context?: Context<STATE & CALCULATED, PROVIDED_CONTEXT>;
275
1425
  onError?: (error: Error, info: { componentName: string }) => any;
276
1426
  debug?: boolean;
1427
+ /**
1428
+ * WebSocket / server-sent events connections derived from state (`makeSocketDriver()`):
1429
+ * `Chat.connections = (state) => ({ room: state.room && { socket: '/ws/rooms/' + state.room, message: 'RECEIVED' } })`.
1430
+ * Recomputed from the current state (after the action's reducer) and sent to the socket
1431
+ * driver's sink whenever the result changes structurally (once at startup too): a new name
1432
+ * opens, a removed or falsy one closes, a changed URL reconnects. Events arrive as the named
1433
+ * actions on this instance; send with `{ to: 'room', json }` from a model entry (a connection
1434
+ * the same action opens or changes is declared first). Dispose closes them.
1435
+ */
1436
+ connections?: (state: STATE & CALCULATED) => Connections;
1437
+ /**
1438
+ * PLAN-3: declarative reads (`makeFetchDriver()`). Each entry derives a
1439
+ * request from state; falsy means idle:
1440
+ * `Quote.resources = { quote: (state) => state.id && '/api/quotes/' + state.id }`.
1441
+ * `state.quote` is a `Resource`: `{ status: 'idle' | 'loading' | 'success' | 'error', data,
1442
+ * error, refreshing }`, written by the built-in RESOURCE action (idle until the first request).
1443
+ * A changed request is fetched with latest semantics (the stale one aborted, its reply never
1444
+ * shown): 'loading' with no data (unless `keepPrevious: true`). A refetch of the same request
1445
+ * (`{ refresh: 'quote' }` on the HTTP sink, `{ invalidate }`, focus, `refetchEvery`) keeps
1446
+ * `data`, `error` and `status` and sets `refreshing: true` (D78). `ok` / `error` on the
1447
+ * request also dispatch those actions after the write.
1448
+ */
1449
+ resources?: { [name: string]: (state: STATE & CALCULATED) => ResourceRequest | false | null | undefined | '' | 0 };
1450
+ /**
1451
+ * The router's reply action (`makeRouter()`): `App.route = 'ROUTE'`. The driver sends this
1452
+ * instance ROUTE with the `Route` (`{ name, params, query, hash, path }`) once declared and on
1453
+ * every change; the reducer stores it (`ROUTE: (state, route) => ({ ...state, route })`).
1454
+ * The first (outermost) declarer gets each route first and may redirect from that entry
1455
+ * (`ROUTER: { to: 'login', replace: true }`); the others get it right after its ROUTE ran, only
1456
+ * if no redirect happened. All in one flush: every declarer has its first route before the
1457
+ * first render is patched, and a navigation is one patch (G-555, G-565); past 32 navigations in
1458
+ * a row (a redirect loop) each next one waits a task (G-571). A component that
1459
+ * declares later gets the current route in the flush that declared it. A function of state may
1460
+ * return a falsy value to stop listening. Works
1461
+ * with or without a model; a root needs `initialState` (SYG132), seeded with `router.current()`.
1462
+ */
1463
+ route?: string | ((state: STATE & CALCULATED) => string | false | null | undefined);
1464
+ /**
1465
+ * Document head values for `makeHeadDriver()`, derived from state: `App.head = (state) =>
1466
+ * ({ title: state.task?.title })`. Recomputed when the result changes; removed on dispose.
1467
+ * A later-mounted component's `title` wins; `meta` keys and `link`s merge. Also collected by
1468
+ * `renderToString(App, { head: list })` for SSR (`renderHead(list)`). Like every declaration
1469
+ * static, it is sent with or without a model; a root needs `initialState` (SYG132).
1470
+ */
1471
+ head?: (state: STATE & CALCULATED) => HeadValue | false | null | undefined;
1472
+ /**
1473
+ * PLAN-4 GS-1: behaviors this component uses, each under its state key:
1474
+ * `TaskList.uses = { pager: pager({ pageSize: 10, next: Newer, prev: Older }) }` gives
1475
+ * `state.pager` and the actions 'pager.NEXT', 'pager.PREV'. See `defineBehavior`.
1476
+ */
1477
+ uses?: { [key: string]: Behavior<any, any, any, any> };
1478
+ /**
1479
+ * PLAN-4 GS-7: timers derived from state, run by `makeTimerDriver()` (registered:
1480
+ * `run(App, { TIMER: makeTimerDriver() })`; renderComponent provides it) and delivered as this
1481
+ * instance's own actions:
1482
+ * `Stopwatch.timers = (state) => ({ tick: state.running && { every: 100, action: 'TICK' } })`.
1483
+ * Diffed by name whenever the result changes structurally: a new name starts, a falsy or
1484
+ * removed one stops, a changed spec restarts. `every` is drift-free (data `{ n, t }`), `after`
1485
+ * fires once (`{ t }`), `frame` runs every animation frame (`{ t, dt }`). A hidden Switchable
1486
+ * page's timers stop unless `background: true`; dispose stops them; nothing runs during SSR.
1487
+ */
1488
+ timers?: (state: STATE & CALCULATED) => Timers<ActionNameOf<ACTIONS>>;
1489
+ /**
1490
+ * PLAN-5 B-3: browser sources derived from state, run by `makeBrowserDriver()` (registered:
1491
+ * `run(App, { BROWSER: makeBrowserDriver() })`; renderComponent provides a fake, `t.browser`)
1492
+ * and delivered as this instance's own actions:
1493
+ * `Card.browser = (state) => ({ seen: !state.seen && { intersection: '.cover', action: 'SEEN' } })`.
1494
+ * Diffed by name as `timers`: a new name starts, a falsy or removed one stops, a changed spec
1495
+ * restarts. A hidden Switchable page's sources stop unless `background: true`; dispose stops
1496
+ * them; nothing runs during SSR.
1497
+ */
1498
+ browser?: (state: STATE & CALCULATED) => BrowserSources<ActionNameOf<ACTIONS>>;
1499
+ /**
1500
+ * PLAN-4 GS-5: save this root component's state and restore it at startup:
1501
+ * `TodoApp.persist = persist({ key: 'todo-app', pick: ['todos', 'filter'] })`. `pick` / `omit`
1502
+ * are typed against STATE's keys. Root component only (SYG224 elsewhere).
1503
+ */
1504
+ persist?: Persist<STATE>;
1505
+ /**
1506
+ * PLAN-4 GS-12: actions whose state change animates as a View Transition:
1507
+ * `Board.viewTransitions = ['MOVE']`, `App.viewTransitions = ['ROUTE']` (the router's reply
1508
+ * action) for route changes. The DOM patch that the action's STATE reducer causes runs inside
1509
+ * `document.startViewTransition()`, which needs the app's DOM driver from
1510
+ * `makeViewTransitionDOMDriver()` (SYG645 in dev otherwise). Patched at once, without a
1511
+ * transition, under `prefers-reduced-motion: reduce` and where the browser has no API.
1512
+ */
1513
+ viewTransitions?: Array<ActionNameOf<ACTIONS>>;
277
1514
  }
278
1515
 
1516
+ /** The action names of an ACTIONS map (any string when it names none) */
1517
+ type ActionNameOf<ACTIONS> = keyof ACTIONS extends never ? string : keyof ACTIONS & string;
1518
+
279
1519
  /**
280
- * Sygnal Root Component
1520
+ * Sygnal Root Component (the one passed to `run()`): a Component without props, so the
1521
+ * view's `state` is typed by STATE (& CALCULATED).
281
1522
  */
282
1523
  export type RootComponent<
283
1524
  STATE = any,
@@ -285,8 +1526,9 @@ export type RootComponent<
285
1526
  ACTIONS = {},
286
1527
  CALCULATED = {},
287
1528
  CONTEXT = {},
288
- SINK_RETURNS extends NonStateSinkReturns = {}
289
- > = Component<STATE, any, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>
1529
+ SINK_RETURNS extends NonStateSinkReturns = {},
1530
+ PROVIDED_CONTEXT = CONTEXT
1531
+ > = Component<STATE, {}, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS, PROVIDED_CONTEXT>
290
1532
 
291
1533
  /**
292
1534
  * A component function that can be used in Collection/Switchable.
@@ -294,18 +1536,87 @@ export type RootComponent<
294
1536
  */
295
1537
  type AnyComponent = ((...args: any[]) => any) & Record<string, any>
296
1538
 
297
- export type CollectionProps<PROPS = any> = {
1539
+ /** Keys of STATE whose value is an array (optional/nullable arrays included). */
1540
+ export type ArrayKeysOf<STATE> = {
1541
+ [KEY in keyof STATE]-?: NonNullable<STATE[KEY]> extends ReadonlyArray<any> ? KEY : never
1542
+ }[keyof STATE] & string
1543
+
1544
+ /**
1545
+ * Valid `from` values for a Collection: any string or Lense while the parent STATE is unknown
1546
+ * (`any`); otherwise an array-valued key of STATE or a Lense over STATE.
1547
+ */
1548
+ export type CollectionFrom<STATE = any> = 0 extends (1 & STATE)
1549
+ ? string | Lense
1550
+ : ArrayKeysOf<STATE> | Lense<STATE, any>
1551
+
1552
+ /**
1553
+ * Collection props. Pass the parent component's state type as STATE to type-check `from`:
1554
+ *
1555
+ * const TaskCollection = Collection<{}, LaneState> // instantiation expression
1556
+ * <TaskCollection of={TaskCard} from="tasks" /> // 'tasks' must be an array key of LaneState
1557
+ */
1558
+ export type CollectionProps<PROPS = any, STATE = any> = {
298
1559
  of: AnyComponent;
299
- from: string | Lense;
1560
+ from: CollectionFrom<STATE>;
300
1561
  filter?: Filter;
301
- sort?: string | SortFunction | SortObject;
302
- } & Omit<PROPS, 'of' | 'from' | 'filter' | 'sort'>
1562
+ sort?: SortSpec;
1563
+ /**
1564
+ * PLAN-5 A-1: a CSS identifier such as `'card'`. Each item with an `id` gets
1565
+ * `view-transition-name: card-<id>` and `view-transition-class: card` on its root element (its
1566
+ * own style wins), so an action in a `viewTransitions` static animates the items between their
1567
+ * places, and between Collections with the same prefix. Items without an `id` get none.
1568
+ */
1569
+ viewTransitionName?: string;
1570
+ /**
1571
+ * Removed in 6.0 (D229, SYG612): a Collection has no element of its own; its items render
1572
+ * directly into the parent. Put the class on your own element: `<ul className="x"><Collection … /></ul>`
1573
+ */
1574
+ className?: never;
1575
+ } & Omit<PROPS, 'of' | 'from' | 'filter' | 'sort' | 'viewTransitionName' | 'className'>
1576
+
1577
+ /**
1578
+ * VirtualCollection props (PLAN-5 V-1): Collection's, plus the scroll container's. Pass the parent
1579
+ * component's state type as STATE to type-check `from`, as for Collection.
1580
+ */
1581
+ export type VirtualCollectionProps<PROPS = any, STATE = any> = {
1582
+ of: AnyComponent;
1583
+ from: CollectionFrom<STATE>;
1584
+ filter?: Filter;
1585
+ sort?: SortSpec;
1586
+ /** The scroll container's class; give it a bounded height (`height`, `max-height`, or a flex item with `min-height: 0`) */
1587
+ className?: string;
1588
+ /** A row's height in px before it is measured: a number (default 32) or `(item, index) => px` */
1589
+ estimateSize?: number | ((item: any, index: number) => number);
1590
+ /** Rows rendered beyond each edge of the view (default 5) */
1591
+ overscan?: number;
1592
+ /** The container's role (default `'list'`; its rows get `role="listitem"` unless they have a role). `null`: none */
1593
+ role?: string | null;
1594
+ /** The container's tabIndex (default 0: the keyboard scrolls it) */
1595
+ tabIndex?: number;
1596
+ /** The container's inline style, after `overflow-y: auto; overflow-anchor: none` */
1597
+ style?: Record<string, string | number>;
1598
+ id?: string;
1599
+ 'aria-label'?: string;
1600
+ 'aria-labelledby'?: string;
1601
+ 'aria-describedby'?: string;
1602
+ /** As Collection's (G-417): each row with an `id` gets `view-transition-name: <prefix>-<id>` and `view-transition-class: <prefix>` */
1603
+ viewTransitionName?: string;
1604
+ } & Omit<PROPS, 'of' | 'from' | 'filter' | 'sort' | 'className' | 'estimateSize' | 'overscan' | 'role' | 'tabIndex' | 'style' | 'id' | 'viewTransitionName'>
1605
+
1606
+ /** Where `scrollToIndex` / `scrollToId` put the row: 'auto' (default) scrolls only when it isn't in view */
1607
+ export type ScrollToAlign = 'auto' | 'start' | 'center' | 'end'
303
1608
 
304
1609
  export type SwitchableProps<PROPS = any> = {
305
1610
  of: Record<string, AnyComponent>;
306
1611
  current: string;
1612
+ /**
1613
+ * The current page's instance key. When it changes, the current page is disposed and created
1614
+ * again (fresh state); a hidden page shown with another key than it last had is re-created on
1615
+ * show. Switching `current` alone keeps pages alive. Router recipe: `instance={state.route.path}`
1616
+ */
1617
+ instance?: string | number;
307
1618
  state?: string | Lense;
308
- } & Omit<PROPS, 'of' | 'state' | 'current'>
1619
+ } & Omit<PROPS, 'of' | 'state' | 'current' | 'instance'>
309
1620
 
310
1621
  export type PortalProps = {
311
1622
  target: string;
@@ -319,6 +1630,16 @@ export type TransitionProps = {
319
1630
  children?: any;
320
1631
  }
321
1632
 
1633
+ export type SuspenseProps = {
1634
+ /**
1635
+ * Shown (wrapped in `<div data-sygnal-suspense="pending">`) while any child is not ready:
1636
+ * a `lazy()` component still loading, or a component with an explicit READY model entry
1637
+ * that hasn't emitted true. A string renders as text. Without it the children render as-is.
1638
+ */
1639
+ fallback?: JSX.Element | string;
1640
+ children?: any;
1641
+ }
1642
+
322
1643
  export type SlotProps = {
323
1644
  name?: string;
324
1645
  children?: any;
@@ -326,12 +1647,192 @@ export type SlotProps = {
326
1647
 
327
1648
  export type ClassesType = (string | string[] | { [className: string]: boolean | undefined })[]
328
1649
 
1650
+ /**
1651
+ * Diagnostics mode.
1652
+ * - 'off' — no checks, no collection (default in production)
1653
+ * - 'collect' — collect diagnostics silently (read with getDiagnostics())
1654
+ * - 'warn' — collect and print warn/error diagnostics to the console (default in Vite dev)
1655
+ * - 'error' — collect and throw on warn/error diagnostics
1656
+ */
1657
+ export type DiagnosticsMode = 'off' | 'collect' | 'warn' | 'error'
1658
+
1659
+ /** Stable diagnostic code, e.g. 'SYG101'. See https://sygnal.js.org/reference/errors */
1660
+ export type DiagnosticCode = `SYG${number}`
1661
+
1662
+ export type DiagnosticSeverity = 'error' | 'warn' | 'info'
1663
+
1664
+ export type Diagnostic = {
1665
+ code: DiagnosticCode;
1666
+ severity: DiagnosticSeverity;
1667
+ /** Name of the component the diagnostic is about */
1668
+ component?: string;
1669
+ /** What is wrong */
1670
+ message: string;
1671
+ /** How to fix it */
1672
+ fix?: string;
1673
+ /** Structured payload (check-specific) */
1674
+ data?: any;
1675
+ /** Link to the docs entry for this code */
1676
+ docsUrl: string;
1677
+ /** Fully formatted message: `[Sygnal SYG123] Component: message. fix docsUrl` */
1678
+ text: string;
1679
+ timestamp: number;
1680
+ }
1681
+
1682
+ export type DiagnosticsOptions = {
1683
+ mode?: DiagnosticsMode;
1684
+ /** Codes to ignore entirely */
1685
+ ignore?: DiagnosticCode[];
1686
+ /**
1687
+ * Strict (canonical-form, SYG5xx) runtime checks. Needs the 'sygnal/diagnostics' dev entry
1688
+ * (otherwise SYG608 is printed once). Without a `mode`, `strict: true` also turns diagnostics
1689
+ * on ('warn'). Omitted: an earlier `configureStrict()` setting is kept.
1690
+ */
1691
+ strict?: boolean;
1692
+ }
1693
+
1694
+ /**
1695
+ * Where an error reported to the app-level `onError` hook happened (PLAN-4 GS-11). `'intent'`: an
1696
+ * intent stream errored (it stops emitting; `action` is its action name). `'context'`: a
1697
+ * `.context` entry threw (it keeps its last value). `'widget'`: a `defineWidget` widget's `mount`,
1698
+ * `update` or `unmount` threw (`componentName` is the component that renders it). `'patch'`: the
1699
+ * DOM driver's patch threw (a vnode hook, a DOM module, flattening the tree); the app's DOM stops
1700
+ * updating (reported once) and the mount point is marked `data-sygnal-error="patch"` (already when
1701
+ * `onError` runs) until the app is disposed.
1702
+ */
1703
+ export type AppErrorPhase = 'view' | 'reducer' | 'effect' | 'intent' | 'context' | 'declaration' | 'driver' | 'instantiate' | 'dispose' | 'widget' | 'patch'
1704
+
1705
+ /** What the app-level `onError` hook gets with the error */
1706
+ export interface AppErrorInfo {
1707
+ /** The component whose view, reducer, EFFECT or sub-component threw (not for 'driver') */
1708
+ componentName?: string
1709
+ /** The action whose reducer or EFFECT threw ('reducer', 'effect'), or whose intent stream errored ('intent') */
1710
+ action?: string
1711
+ phase: AppErrorPhase
1712
+ /** The driver (sink) name, for 'driver' */
1713
+ driver?: string
1714
+ }
1715
+
1716
+ /**
1717
+ * App-level error hook: reporting only (e.g. to an error tracker). It is called after the
1718
+ * component's own `onError` boundary chose the fallback, once per error, in every diagnostics
1719
+ * mode. An exception thrown by the hook is logged with console.error and swallowed.
1720
+ */
1721
+ export type AppErrorHook = (error: any, info: AppErrorInfo) => void
1722
+
329
1723
  export type RunOptions = {
330
- mountPoint?: string;
1724
+ /** Where the app renders: a CSS selector (default '#root') or an element */
1725
+ mountPoint?: string | Element;
1726
+ /** @deprecated No effect since 6.0: fragments always work (the DOM driver splices them into their parent) */
331
1727
  fragments?: boolean;
332
1728
  useDefaultDrivers?: boolean;
1729
+ /**
1730
+ * Runtime diagnostics. Takes precedence over `globalThis.__SYGNAL_DEV__`
1731
+ * (set by the Sygnal Vite plugin in dev), which enables 'warn'. Default: 'off'.
1732
+ */
1733
+ diagnostics?: DiagnosticsMode | DiagnosticsOptions;
1734
+ /** App-level error hook for this app (each run() has its own); see AppErrorHook */
1735
+ onError?: AppErrorHook;
1736
+ /**
1737
+ * The root of this app's `uid()` strings (default 'u'). Give each app on one page its own
1738
+ * (`run(Signup, {}, { mountPoint: '#signup', uid: 'signup' })`), and pass the same value to
1739
+ * renderToString's `uid` when hydrating server markup.
1740
+ */
1741
+ uid?: string;
333
1742
  }
334
1743
 
1744
+ /** All diagnostics collected so far (most recent last). */
1745
+ export function getDiagnostics(): Diagnostic[]
1746
+
1747
+ /** Clear the collected diagnostics. */
1748
+ export function clearDiagnostics(): void
1749
+
1750
+ /** Subscribe to diagnostics as they are reported. Returns an unsubscribe function. */
1751
+ export function onDiagnostic(callback: (diagnostic: Diagnostic) => void): () => void
1752
+
1753
+ export type {
1754
+ InspectGraph,
1755
+ InspectComponent,
1756
+ InspectAction,
1757
+ InspectActionTrigger,
1758
+ InspectChild,
1759
+ InspectSelector,
1760
+ InspectDiagnostic,
1761
+ InspectRecentAction,
1762
+ InspectCommand,
1763
+ InspectTimer,
1764
+ } from './extra/diagnostics/checks/public'
1765
+
1766
+ /**
1767
+ * The Sygnal DevTools bridge (also `window.__SYGNAL_DEVTOOLS__`), installed in a
1768
+ * browser by the dev-only 'sygnal/devtools' entry, which sygnal/vite injects in dev.
1769
+ * Only the stable, documented members are typed.
1770
+ */
1771
+ export interface SygnalDevTools {
1772
+ /** true while the browser extension is connected */
1773
+ readonly connected: boolean
1774
+ /** Diagnostics collected so far (same as getDiagnostics()) */
1775
+ getDiagnostics(): Diagnostic[]
1776
+ /**
1777
+ * The machine-readable app graph of the live components. Present only when the
1778
+ * 'sygnal/diagnostics' dev entry is loaded (it attaches this method); needs diagnostics on.
1779
+ */
1780
+ inspect?(): InspectGraph
1781
+ /** PLAN-4 3-E (G-226): defaults for "Copy as test" from the extension panel (componentImport, drivers, ...) */
1782
+ configureCopyAsTest(options: DevToolsCopyAsTestOptions): void
1783
+ /**
1784
+ * PLAN-4 3-E (G-226): one instance's recorded session: undefined (the newest root), an
1785
+ * instance id, a component, or run()'s result
1786
+ */
1787
+ getSession(target?: string | number | ((...args: any[]) => any) | { sources: any }): DevToolsSessionRecording
1788
+ }
1789
+
1790
+ /** "Copy as test" options (the same as CopyAsTestOptions in 'sygnal/devtools') */
1791
+ export interface DevToolsCopyAsTestOptions {
1792
+ /** The import line(s) for the component (default: `import <Name> from './<Name>.js'`) */
1793
+ componentImport?: string
1794
+ /** The component's identifier in the test (default: its recorded name) */
1795
+ componentName?: string
1796
+ /** More import lines (drivers, helpers) */
1797
+ imports?: string[]
1798
+ /** Drivers for renderComponent, as source code by sink name: { DND: 'mockDragDriver().driver' } */
1799
+ drivers?: Record<string, string>
1800
+ /** More renderComponent options, as source code: 'strict: true' */
1801
+ renderOptions?: string
1802
+ /** The test's name */
1803
+ testName?: string
1804
+ /** Adds a `// @vitest-environment <env>` first line */
1805
+ environment?: string
1806
+ }
1807
+
1808
+ export type DevToolsActionCause = 'intent' | 'next' | 'reply' | 'built-in' | 'simulateAction' | 'behavior'
1809
+
1810
+ /** One instance's recorded session (the same as SessionRecording in 'sygnal/devtools') */
1811
+ export interface DevToolsSessionRecording {
1812
+ version: 1
1813
+ component: string
1814
+ instance: string
1815
+ /** The state when the session started for this instance */
1816
+ initialState?: any
1817
+ /** The component's own initialState (renderComponent's default) */
1818
+ definitionInitialState?: any
1819
+ finalState: any
1820
+ /** The action names the component can be sent (model keys, behavior actions) */
1821
+ actionNames?: string[]
1822
+ /** Source names beyond DOM / EVENTS / STATE / LOG / CHILD / PARENT / READY */
1823
+ drivers: string[]
1824
+ /** Those of `drivers` renderComponent fakes (makeFetchDriver sources) */
1825
+ fakeable: string[]
1826
+ /** The instance's own actions, in order */
1827
+ actions: Array<{ type: string; data: any; cause: DevToolsActionCause; sinks: string[]; at: number; replySink?: string; replyKind?: 'fetch' | 'other'; echo?: true }>
1828
+ /** State changes in descendant instances a replay at this instance can't reproduce */
1829
+ foreign: Array<{ type: string; component: string; instance: string; cause: DevToolsActionCause }>
1830
+ truncated?: boolean
1831
+ }
1832
+
1833
+ /** The installed DevTools bridge; undefined unless 'sygnal/devtools' was loaded (always in production builds). */
1834
+ export function getDevTools(): SygnalDevTools | undefined
1835
+
335
1836
  export type SygnalSinks<STATE = any, DRIVERS = {}> = {
336
1837
  [SINK_NAME in keyof (DefaultDrivers<STATE> & FixDrivers<DRIVERS>) | string]?: Stream<any>
337
1838
  }
@@ -394,9 +1895,11 @@ export function exactState<STATE>(): <ACTUAL extends STATE>(state: ExactShape<ST
394
1895
  * Dynamic form — function receives (state, data, next, props) and
395
1896
  * returns the partial update to merge:
396
1897
  * `set((state, title) => ({ title }))`
1898
+ *
1899
+ * Not a field name: `set('title')` is a type error (and SYG221 in the dev checks).
397
1900
  */
398
1901
  export function set<S = any>(
399
- partial: Partial<S> | ((state: S, data: any, next: Function, props: any) => Partial<S>)
1902
+ partial: (Partial<S> & object) | ((state: S, data: any, next: Function, props: any) => Partial<S>)
400
1903
  ): (state: S, data: any, next: Function, props: any) => S
401
1904
 
402
1905
  /**
@@ -406,16 +1909,58 @@ export function set<S = any>(
406
1909
  */
407
1910
  export function toggle<S = any>(field: keyof S & string): (state: S) => S
408
1911
 
1912
+ type EmitEntry<TYPE extends string> = keyof SygnalEvents extends never
1913
+ ? { EVENTS: (state: any, actionData: any, next: Function, props: any) => { type: string; data: any } }
1914
+ : { EVENTS: (state: any, actionData: any, next: Function, props: any) => EmittedEvent<TYPE> }
1915
+
409
1916
  /**
410
1917
  * Create a model entry that emits an EVENTS bus event.
1918
+ * Prefer `event()` inside the object form: `ACTION: { EVENTS: event('TYPE', fn) }`.
411
1919
  *
412
1920
  * `emit('DELETE_LANE', (state) => ({ laneId: state.id }))`
413
1921
  * `emit('REFRESH')`
1922
+ *
1923
+ * With a `SygnalEvents` registry, `type` and the payload are checked against it.
1924
+ */
1925
+ export function emit<TYPE extends EventName>(
1926
+ type: TYPE,
1927
+ data: (state: any, actionData: any, next: Function, props: any) => EventPayload<TYPE>
1928
+ ): EmitEntry<TYPE>
1929
+ export function emit<TYPE extends EventName>(
1930
+ type: TYPE,
1931
+ ...data: StaticEventArgs<TYPE>
1932
+ ): EmitEntry<TYPE>
1933
+
1934
+ /**
1935
+ * Create an EVENTS sink function that puts `{ type, data }` on the EVENTS bus.
1936
+ * Use it as the `EVENTS` value inside an object-form model entry:
1937
+ *
1938
+ * DELETE: {
1939
+ * STATE: (state) => ({ ...state, deleting: true }),
1940
+ * EVENTS: event('DELETE_LANE', (state) => ({ laneId: state.id })),
1941
+ * }
1942
+ *
1943
+ * The payload is either a function `(state, data, next, props) => payload` or a static value:
1944
+ * `event('RESET')`, `event('SET_MODE', 'dark')`.
1945
+ *
1946
+ * With a `SygnalEvents` registry, `type` must be a registered name and the payload must match
1947
+ * its type. When used inside a model, the payload function's `state` and `data` parameters are
1948
+ * typed from the component.
1949
+ */
1950
+ export function event<TYPE extends EventName, STATE = any, DATA = any>(
1951
+ type: TYPE,
1952
+ ...payload: EventArgs<TYPE, STATE, DATA>
1953
+ ): EventSink<TYPE, STATE, DATA>
1954
+
1955
+ /**
1956
+ * The payload argument of `event()`: a payload function or a static value (one signature, not
1957
+ * overloads, so an unregistered name is reported as one "not assignable to parameter" error).
414
1958
  */
415
- export function emit(
416
- type: string,
417
- data?: any | ((state: any, actionData: any, next: Function, props: any) => any)
418
- ): { EVENTS: (state: any, actionData: any, next: Function, props: any) => { type: string; data: any } }
1959
+ type EventArgs<TYPE extends string, STATE, DATA> = keyof SygnalEvents extends never
1960
+ ? [payload?: EventPayloadFunction<TYPE, STATE, DATA> | AnySinkConstant]
1961
+ : undefined extends EventPayload<TYPE>
1962
+ ? [payload?: EventPayloadFunction<TYPE, STATE, DATA> | EventPayload<TYPE>]
1963
+ : [payload: EventPayloadFunction<TYPE, STATE, DATA> | EventPayload<TYPE>]
419
1964
 
420
1965
  /**
421
1966
  * Any object with an events() method (e.g., DOM.select('form')).
@@ -510,60 +2055,144 @@ export type DragDriverSource = {
510
2055
 
511
2056
  export function makeDragDriver(): (sink$: Stream<DragDriverRegistration | DragDriverRegistration[]>) => DragDriverSource
512
2057
 
513
- export type ComponentFactoryOptions<
2058
+ /**
2059
+ * The options `defineComponent()` takes: the view plus the statics a function component carries
2060
+ * (`model`, `intent`, `initialState`, ...), and `name` (its `componentName`; defaults to the view's
2061
+ * name, or 'Component' for an anonymous inline view). Statics already on the view are copied; the
2062
+ * options override them.
2063
+ */
2064
+ export type DefineComponentOptions<
514
2065
  STATE = any,
515
- PROPS = any,
2066
+ PROPS = { [prop: string]: any },
516
2067
  DRIVERS = {},
517
2068
  ACTIONS = {},
518
2069
  CALCULATED = {},
519
2070
  CONTEXT = {},
520
2071
  SINK_RETURNS extends NonStateSinkReturns = {}
521
2072
  > = {
2073
+ /** The view: `({ state, context, ...props }) => vnode` */
2074
+ view: ComponentProps<STATE & CALCULATED, PROPS, CONTEXT>;
2075
+ /** The component's name (its `componentName`); defaults to the view function's name */
522
2076
  name?: string;
523
- view: Component<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>;
524
- model?: Component<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>['model'];
525
- intent?: Component<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>['intent'];
526
- hmrActions?: string | string[];
527
- context?: Component<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>['context'];
528
- peers?: { [name: string]: Component };
529
- components?: { [name: string]: Component };
530
- initialState?: STATE;
531
- calculated?: Component<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>['calculated'];
532
- storeCalculatedInState?: boolean;
533
- DOMSourceName?: string;
534
- stateSourceName?: string;
535
- requestSourceName?: string;
536
- debug?: boolean;
537
- }
2077
+ } & Omit<Component<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>, 'componentName' | keyof Function>
538
2078
 
539
- export function component<
2079
+ /**
2080
+ * Build a component from an options object (for code that makes components from data). Returns
2081
+ * an ordinary function component that calls `view`, with the other options as its statics:
2082
+ *
2083
+ * const Counter = defineComponent({ name: 'Counter', view, model, initialState: { count: 0 } })
2084
+ *
2085
+ * Writing the function and its statics directly is the usual form.
2086
+ */
2087
+ export function defineComponent<
540
2088
  STATE = any,
541
- PROPS = any,
2089
+ PROPS = { [prop: string]: any },
542
2090
  DRIVERS = {},
543
2091
  ACTIONS = {},
544
2092
  CALCULATED = {},
545
2093
  CONTEXT = {},
546
2094
  SINK_RETURNS extends NonStateSinkReturns = {}
547
2095
  >(
548
- options: ComponentFactoryOptions<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>
2096
+ options: DefineComponentOptions<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>
549
2097
  ): Component<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>
550
2098
 
551
- export function collection(...args: any[]): any
552
- export function switchable(...args: any[]): any
553
2099
  export function portal(...args: any[]): any
554
2100
 
555
- export function Collection<PROPS extends { [prop: string]: any }>(props: CollectionProps<PROPS>): JSX.Element
2101
+ export function Collection<PROPS extends { [prop: string]: any }, STATE = any>(props: CollectionProps<PROPS, STATE>): JSX.Element
2102
+ /**
2103
+ * PLAN-5 V-1: a Collection that renders only the rows in view (+ `overscan`). Same `of` / `from` /
2104
+ * `filter` / `sort`; the element is its own scroll container (give `className` a bounded height).
2105
+ * Rows scrolled out are disposed and made again when they come back: keep row state in the array.
2106
+ * Jump with element commands: `{ scrollToIndex: '.rows', index }`, `{ scrollToId: '.rows', id }`.
2107
+ *
2108
+ * <VirtualCollection of={Row} from="rows" className="rows" estimateSize={32} />
2109
+ */
2110
+ export function VirtualCollection<PROPS extends { [prop: string]: any }, STATE = any>(props: VirtualCollectionProps<PROPS, STATE>): JSX.Element
556
2111
  export function Switchable<PROPS extends { [prop: string]: any }>(props: SwitchableProps<PROPS>): JSX.Element
557
2112
  export function Portal(props: PortalProps): JSX.Element
558
2113
  export function Transition(props: TransitionProps): JSX.Element
2114
+ export function Suspense(props: SuspenseProps): JSX.Element
559
2115
  export function Slot(props: SlotProps): JSX.Element
560
2116
 
2117
+ /**
2118
+ * What `lazy()` returns: a sub-component used in JSX with its own props only
2119
+ * (`<Chart title="Sales" />`; `state` is optional, as for any sub-component), that
2120
+ * still carries the component statics (`model`, `intent`, …) once loaded.
2121
+ */
2122
+ export type LazyComponent<PROPS = any> = ((
2123
+ props: PROPS & { state?: any; children?: JSX.Element | JSX.Element[] }
2124
+ ) => JSX.Element) & Omit<Component<any, PROPS>, never> & {
2125
+ /** start the import now (a deferred `when` one too: preload on hover, or in a test); resolves once it has loaded or failed */
2126
+ load(): Promise<void>
2127
+ }
2128
+
2129
+ /**
2130
+ * PLAN-5 B-4: `when` defers the import until a placeholder is visible ('visible',
2131
+ * IntersectionObserver; `rootMargin` to start earlier) or the browser is idle after it is on the
2132
+ * page ('idle', requestIdleCallback with a 2 s timeout). The placeholder stays in its place: a
2133
+ * Suspense boundary waits for it (its fallback) only once the import has started. SSR renders the
2134
+ * placeholder and never loads.
2135
+ */
2136
+ export interface LazyOptions {
2137
+ when?: 'visible' | 'idle'
2138
+ rootMargin?: string
2139
+ /** the placeholder's min-height (a number: px; or a CSS length), e.g. the component's expected height */
2140
+ placeholderHeight?: number | string
2141
+ }
2142
+
561
2143
  export function lazy<PROPS = any>(
562
- loadFn: () => Promise<{ default: Component<any, PROPS> } | Component<any, PROPS>>
563
- ): Component<any, PROPS>
2144
+ loadFn: () => Promise<{ default: Component<any, PROPS> } | Component<any, PROPS>>,
2145
+ options?: LazyOptions
2146
+ ): LazyComponent<PROPS>
2147
+
2148
+ /**
2149
+ * The reply-action keys of a request to makeFetchDriver or driverFromAsync: the
2150
+ * outcome becomes an action on exactly the component instance that sent the request, instead
2151
+ * of reaching `select()` / `errors()`.
2152
+ *
2153
+ * LOAD: { HTTP: (state) => ({ url: `/api/q/${state.id}`, ok: 'LOADED', error: 'FAILED' }) },
2154
+ * LOADED: (state, quote) => ({ ...state, quote }), // data: the parsed body
2155
+ * FAILED: (state, { status }) => ({ ...state, status }), // data: { error, status?, body?, request }
2156
+ *
2157
+ * The action's data type is not inferred from the request: type it in the component's ACTIONS
2158
+ * (`{ LOADED: Quote; FAILED: FetchFailure }`). `ok` / `error` are plain strings in the types (D70);
2159
+ * a name with no model entry is SYG112 (sygnal-check and the dev entry).
2160
+ */
2161
+ export type ReplyRequest = {
2162
+ /** Action that receives the success value (fetch: the parsed body; driverFromAsync: the resolved value) */
2163
+ ok?: string;
2164
+ /** Action that receives the failure (`{ error, request }`, plus `status` / `body` for fetch) */
2165
+ error?: string;
2166
+ /** Not allowed: a `then` key makes the request a thenable (SYG610, not sent). Use `ok` */
2167
+ then?: never;
2168
+ /** Not allowed (SYG610, not sent). Use `error` */
2169
+ catch?: never;
2170
+ }
2171
+
2172
+ /**
2173
+ * A request to a driverFromAsync() sink: your own fields (`value`, the args, ...) plus the
2174
+ * reply-action keys `ok` / `error`. Type the driver's sink with it: `{ QUOTE: { source:
2175
+ * AsyncDriverFromFunction; sink: AsyncRequest<{ value: number }> } }`.
2176
+ */
2177
+ export type AsyncRequest<FIELDS = { [field: string]: any }> = FIELDS & ReplyRequest
2178
+
2179
+ /** Payload on `errors()` of a driverFromAsync source when a request fails */
2180
+ export type AsyncDriverError<INCOMING = any> = {
2181
+ /** The rejection reason (or what `post` threw) */
2182
+ error: any;
2183
+ /** The request that failed */
2184
+ request: INCOMING;
2185
+ /** The request's selector property (default 'category') is copied here (not for an `error` reply action) */
2186
+ [selectorProperty: string]: any;
2187
+ }
564
2188
 
565
2189
  export type AsyncDriverFromFunction<INCOMING = any, OUTGOING = any> = {
566
2190
  select: (selector?: string | ((value: OUTGOING) => boolean)) => Stream<OUTGOING>
2191
+ /**
2192
+ * Failed requests (rejected promise, rejected/throwing `post`). Filters like
2193
+ * `select()`. Failures are only console.error'd while nothing listens here.
2194
+ */
2195
+ errors: (selector?: string | ((error: AsyncDriverError<INCOMING>) => boolean)) => Stream<AsyncDriverError<INCOMING>>
567
2196
  }
568
2197
 
569
2198
  export type DriverFromAsyncOptions<INCOMING = any, OUTGOING = any, RETURN = any> = {
@@ -579,6 +2208,718 @@ export function driverFromAsync<INCOMING = any, RETURN = any, OUTGOING = any>(
579
2208
  options?: DriverFromAsyncOptions<INCOMING, OUTGOING, RETURN>
580
2209
  ): (fromApp$: Stream<INCOMING>) => AsyncDriverFromFunction<INCOMING, OUTGOING>
581
2210
 
2211
+ /** fetch() options a request (or the driver) may set under `init`; the driver owns `signal` */
2212
+ export type FetchInit = {
2213
+ method?: string;
2214
+ headers?: Record<string, string> | Headers;
2215
+ body?: any;
2216
+ mode?: string;
2217
+ credentials?: 'omit' | 'same-origin' | 'include';
2218
+ cache?: string;
2219
+ redirect?: string;
2220
+ referrer?: string;
2221
+ referrerPolicy?: string;
2222
+ integrity?: string;
2223
+ keepalive?: boolean;
2224
+ priority?: 'high' | 'low' | 'auto';
2225
+ window?: null;
2226
+ duplex?: 'half';
2227
+ }
2228
+
2229
+ /**
2230
+ * A request sent to a makeFetchDriver() sink. A plain string is a GET of that URL.
2231
+ * Other fetch() options go under `init` (`init: { credentials: 'include' }`). Any other key is
2232
+ * the app's own: not sent, but returned on the reply's `request`.
2233
+ *
2234
+ * Reply actions (canonical): `{ url, ok: 'LOADED', error: 'FAILED' }` delivers the parsed body as
2235
+ * LOADED, a failure as FAILED (`FetchFailure`), to exactly the sending instance (see
2236
+ * ReplyRequest). Without `ok` / `error` the reply goes to `select()` / `errors()`.
2237
+ *
2238
+ * Isolation: the replies, `latest` and `abort` of a component instance are its own (and its
2239
+ * descendants'): two instances, or Collection items, using the same category never see or
2240
+ * cancel each other's requests. The root component sees every reply.
2241
+ */
2242
+ export type FetchRequest = string | {
2243
+ /** Request URL (prefixed with the driver's `baseUrl`) */
2244
+ url: string;
2245
+ /** Reply action that receives the parsed body of a 2xx response (the Response with `parse: 'response'`) */
2246
+ ok?: string;
2247
+ /** Reply action that receives a failure, `{ error, status?, body?, request }` (FetchFailure) */
2248
+ error?: string;
2249
+ /**
2250
+ * Reply actions: the `latest` / `abort` group (default: the `ok` action, else `error`). Requests with
2251
+ * the same key from the same instance supersede each other under `latest: true`
2252
+ */
2253
+ key?: string;
2254
+ /** Without reply actions: tag read back with `select(category)` / `errors(category)`; also the `latest` / `abort` group */
2255
+ category?: string;
2256
+ /** Default: 'POST' when `json` or `body` is set, else 'GET' */
2257
+ method?: string;
2258
+ /** Merged over the driver's `headers`, case-insensitively (names are sent lowercased) */
2259
+ headers?: Record<string, string> | Headers;
2260
+ /**
2261
+ * Appended as a query string (`{ q: 'dune' }` → `?q=dune`), before any `#fragment`; null/undefined
2262
+ * values are skipped; an array repeats the key (`{ tag: ['a', 'b'] }` → `?tag=a&tag=b`)
2263
+ */
2264
+ query?: Record<string, string | number | boolean | null | undefined | Array<string | number | boolean | null | undefined>>;
2265
+ /** Sent as JSON.stringify(json), with `Content-Type: application/json` unless set */
2266
+ json?: any;
2267
+ /** Raw body (string, FormData, Blob, ...) */
2268
+ body?: any;
2269
+ /**
2270
+ * Latest only: sending this request aborts this component's requests still in flight in the
2271
+ * same category; their responses and errors are never delivered. Default: the driver's `latest`
2272
+ * option.
2273
+ */
2274
+ latest?: boolean;
2275
+ /** Fail with a TimeoutError (on `errors()`) after this many ms. Default: the driver's `timeoutMs` */
2276
+ timeoutMs?: number;
2277
+ /**
2278
+ * How the 2xx body becomes `value`: 'auto' (default; JSON when the content-type says json,
2279
+ * else text; 204 → null), 'json', 'text', 'response' (the Response), or a function.
2280
+ */
2281
+ parse?: 'auto' | 'json' | 'text' | 'response' | ((response: Response) => any);
2282
+ /** Other fetch() options (merged over the driver's `init`) */
2283
+ init?: FetchInit;
2284
+ /** Not allowed: a `then` key makes the request a thenable (SYG610, not sent). Use `ok` */
2285
+ then?: never;
2286
+ /** Not allowed (SYG610, not sent). Use `error` */
2287
+ catch?: never;
2288
+ /**
2289
+ * PLAN-3 5-3 (D79): cache this request's reply in the driver's `queryCache()` (any method; a
2290
+ * POST is SYG630; without a queryCache nothing is cached, SYG635). `false`: never cached (a
2291
+ * resource under `makeFetchDriver({ cache: queryCache() })` too). A request with reply actions
2292
+ * is otherwise one-send-one-request
2293
+ */
2294
+ cache?: boolean;
2295
+ /** How long a cached reply stays fresh, in ms (served without a fetch). Implies `cache` */
2296
+ staleTime?: number;
2297
+ /** Invalidation tags of this request (and its cache entry): `{ invalidate: 'quotes' }` matches `tags: ['quotes']` */
2298
+ tags?: string[];
2299
+ /**
2300
+ * After a 2xx reply: invalidate these tags / URL prefixes / the predicate's matches (like
2301
+ * `{ invalidate }`). A read of them already in flight is aborted, so an older reply never lands
2302
+ */
2303
+ invalidates?: FetchInvalidate;
2304
+ /**
2305
+ * PLAN-3 6-A (G-184; React Query's setQueryData): after a 2xx reply, write it into this
2306
+ * component's resources with these names (and their `queryCache()` entries) before the `ok`
2307
+ * action and before `invalidates`: `updates: 'item'` (the reply becomes `state.item.data`), or
2308
+ * `{ items: (list, reply) => newList }` to derive it. A read of them in flight is aborted;
2309
+ * other mounted resources on the same cache entry show it too. Resources without a request
2310
+ * (idle) are skipped. The `ok` action still gets the reply
2311
+ */
2312
+ updates?: string | string[] | Record<string, true | ((data: any, reply: any) => any)>;
2313
+ /**
2314
+ * Retries (default 0): a count, or a count with the backoff of makeSocketDriver's reconnect
2315
+ * (`{ count: 3, delayMs: 500, maxDelayMs: 10000, jitter: 0.2 }`; count defaults to 3).
2316
+ * Network errors, 408, 429 (a Retry-After in seconds wins) and 5xx are retried, never other
2317
+ * 4xx; the failure arrives once, after the last attempt, with `attempts`
2318
+ */
2319
+ retry?: number | FetchRetry;
2320
+ /**
2321
+ * A Standard Schema (zod, valibot, arktype, ...) the parsed 2xx body must pass; the reply is the
2322
+ * schema's (possibly transformed) value. A failure is an error with `issues`
2323
+ */
2324
+ validate?: StandardSchemaLike;
2325
+ /** Your own fields (an id, ...): not sent, returned on the reply's `request` */
2326
+ [appData: string]: any;
2327
+ } | {
2328
+ /**
2329
+ * Cancel. Reply actions: `{ abort: 'LOADED' }` aborts this instance's requests in flight whose key
2330
+ * (`key`, else `ok`, else `error`) is 'LOADED'; `{ abort: true, key: 'search' }` does the same
2331
+ * by key. Without reply actions: `{ category: 'search', abort: true }` aborts this component's requests in
2332
+ * that category (all of them, reply-action ones included, without a category or key). Nothing is
2333
+ * delivered for a cancelled request.
2334
+ */
2335
+ abort: true | string;
2336
+ key?: string;
2337
+ category?: string;
2338
+ } | {
2339
+ /** PLAN-3 3-A: refetch this instance's resource(s) by name, keeping data (nothing while idle) */
2340
+ refresh: string | string[];
2341
+ } | {
2342
+ /**
2343
+ * PLAN-3 5-3 (D80), from any component: matching cache entries go stale and matching mounted
2344
+ * resources refetch, keeping data. With or without the cache
2345
+ */
2346
+ invalidate: FetchInvalidate;
2347
+ } | {
2348
+ /**
2349
+ * PLAN-3 5-5 (H-7): fetch this request into the driver's `queryCache()` without a reply (a
2350
+ * fresh entry or the same fetch in flight: nothing new), so a resource that reads it later
2351
+ * renders 'success' at once. Without a queryCache it does nothing (SYG635)
2352
+ */
2353
+ prefetch: string | { url: string; [field: string]: any };
2354
+ }
2355
+
2356
+ /**
2357
+ * What `{ invalidate }` / `invalidates` match: a tag (`tags: ['quotes']` on the request), a URL
2358
+ * prefix of the request's `url` (a string starting with '/'), several of them, or a predicate
2359
+ */
2360
+ export type FetchInvalidate = string | string[] | ((request: any) => boolean);
2361
+
2362
+ /** Retry policy of a makeFetchDriver() request: the reconnect backoff of makeSocketDriver plus a count */
2363
+ export type FetchRetry = SocketReconnect & {
2364
+ /** Retries after the first attempt. Default 3 in this object form */
2365
+ count?: number;
2366
+ }
2367
+
2368
+ /** Any Standard Schema (https://standardschema.dev): an object with `~standard.validate` */
2369
+ export type StandardSchemaLike = {
2370
+ readonly '~standard': {
2371
+ validate: (value: unknown) => {value?: any; issues?: ReadonlyArray<{message: string; path?: ReadonlyArray<any>}>} | Promise<{value?: any; issues?: ReadonlyArray<{message: string; path?: ReadonlyArray<any>}>}>;
2372
+ [key: string]: any;
2373
+ };
2374
+ }
2375
+
2376
+ /** PLAN-3 5-3 (D79), D88: `queryCache(options)` */
2377
+ export type FetchCacheOptions = {
2378
+ /** How long a reply stays fresh, in ms: a fresh entry is served without a fetch. Default 0 (always refetch, showing the cached data meanwhile) */
2379
+ staleTime?: number;
2380
+ /** How long an entry no resource uses is kept, in ms. Default 300000 (5 min); Infinity keeps it */
2381
+ gcTime?: number;
2382
+ /** Refetch stale mounted resources when the window regains focus / the page becomes visible. Default true */
2383
+ refetchOnFocus?: boolean;
2384
+ /** Refetch stale mounted resources when the browser comes back online. Default true */
2385
+ refetchOnReconnect?: boolean;
2386
+ /** PLAN-3 5-5 (H-7): entries to start with, from `dehydrate()` (SSR seeding) */
2387
+ initial?: QueryCacheSnapshot;
2388
+ }
2389
+
2390
+ /** PLAN-3 5-5 (H-7): one entry of a `dehydrate()` snapshot (JSON-safe when `data` is) */
2391
+ export type QueryCacheSnapshotEntry = {
2392
+ /** method, URL as written (query sorted), body and parse: `'GET /api/quotes/1'` */
2393
+ key: string;
2394
+ /** the parsed (and validated) body */
2395
+ data: any;
2396
+ /** when it was fetched (ms since the epoch): it is fresh until `updatedAt + staleTime` */
2397
+ updatedAt: number;
2398
+ /** the request's invalidation tags */
2399
+ tags?: string[];
2400
+ }
2401
+ export type QueryCacheSnapshot = QueryCacheSnapshotEntry[];
2402
+
2403
+ /** PLAN-3 D88: the query cache of a makeFetchDriver (`makeFetchDriver({ cache: queryCache() })`) */
2404
+ export interface QueryCache {
2405
+ /** the entries with data, as a JSON-safe snapshot (serialise it into the page) */
2406
+ dehydrate(): QueryCacheSnapshot;
2407
+ /** writes a snapshot's entries (an entry newer than the snapshot's is kept) */
2408
+ hydrate(snapshot: QueryCacheSnapshot | null | undefined): void;
2409
+ /** writes one entry, keyed like the request (a loader on the server: `cache.set('/api/quotes/1', quote)`) */
2410
+ set(request: ResourceRequest, data: any): void;
2411
+ /** the driver given this cache fetches the request into it, without a reply (like the `{ prefetch }` command) */
2412
+ prefetch(request: ResourceRequest): void;
2413
+ }
2414
+
2415
+ /**
2416
+ * PLAN-3 D88: the opt-in query cache, passed to `makeFetchDriver({ cache: queryCache({ staleTime: 30000 }) })`.
2417
+ * Resources' GET/HEAD replies are cached (stale-while-revalidate: cached data shows at once,
2418
+ * `refreshing` while it refetches; an entry younger than `staleTime` is served without a fetch),
2419
+ * identical cacheable requests in flight share one fetch, focus / reconnect refetch stale
2420
+ * mounted resources, and unused entries are dropped after `gcTime`. SSR seeding:
2421
+ * `renderToString(App, { cache })` renders cached resources as 'success', `dehydrate()` /
2422
+ * `queryCache({ initial })` carry the entries to the client. One cache per driver
2423
+ */
2424
+ export function queryCache(options?: FetchCacheOptions): QueryCache
2425
+
2426
+ /** PLAN-3: a request a `resources` entry derives (a URL, or a request without `abort`) */
2427
+ export type ResourceRequest = string | (Exclude<FetchRequest, string | { abort: true | string } | { refresh: string | string[] }> & {
2428
+ /**
2429
+ * Keep this resource live while its component is in a hidden Switchable page (default: a
2430
+ * hidden page's resources are paused, keeping their last result, and refetched when it is shown)
2431
+ */
2432
+ background?: boolean;
2433
+ /** D78: on a new request (key change), keep the previous `data` (status unchanged, `refreshing: true`) instead of 'loading' (pagination) */
2434
+ keepPrevious?: boolean;
2435
+ /** Refetch every this many ms after each result (skipped while the document is hidden) */
2436
+ refetchEvery?: number;
2437
+ })
2438
+
2439
+ /**
2440
+ * PLAN-3: the state slot of a resource (`state.quote`), written by the built-in RESOURCE action.
2441
+ * `data` is the parsed (validated) body; `error` is the Error (`error.status` / `error.body` for
2442
+ * a non-2xx response, `error.issues` for a validation failure). D78: a refetch of the same
2443
+ * request keeps `data` and `error` with `refreshing: true`; a failed refetch keeps `data`.
2444
+ */
2445
+ export type Resource<DATA = any, ERROR = any> =
2446
+ | { status: 'idle' | 'loading'; data?: undefined; error?: undefined; refreshing?: undefined }
2447
+ | { status: 'success'; data: DATA; error?: undefined; refreshing?: boolean }
2448
+ | { status: 'error'; data?: DATA; error: ERROR; refreshing?: boolean }
2449
+
2450
+ /** The data of the `error` reply action of a makeFetchDriver() request (`error: 'FAILED'`) */
2451
+ export type FetchFailure<REQUEST = any> = {
2452
+ /**
2453
+ * 'HTTP 404 ...' for a non-2xx status (with `.status` and `.body`), the network error
2454
+ * (TypeError), the body parse error, a TimeoutError (`.name === 'TimeoutError'`), or
2455
+ * "fetch is not available"
2456
+ */
2457
+ error: any;
2458
+ /** HTTP status, for a non-2xx response (undefined for a network error or timeout) */
2459
+ status?: number;
2460
+ /** The non-2xx response's body, parsed like 'auto' */
2461
+ body?: any;
2462
+ /** The request as the app sent it */
2463
+ request: REQUEST;
2464
+ /** With `retry`: how many attempts were made */
2465
+ attempts?: number;
2466
+ /** With `validate`: the schema's issues (the body didn't validate) */
2467
+ issues?: ReadonlyArray<{message: string; path?: ReadonlyArray<any>}>;
2468
+ }
2469
+
2470
+ /** A successful (2xx) response on `select()` of a makeFetchDriver() source */
2471
+ export type FetchResponse<VALUE = any, REQUEST = any> = {
2472
+ /** The request's category */
2473
+ category: string | undefined;
2474
+ /** The parsed body (see `parse`) */
2475
+ value: VALUE;
2476
+ /** HTTP status */
2477
+ status: number;
2478
+ /** The request as the app sent it (any extra fields you put on it come back here) */
2479
+ request: REQUEST;
2480
+ }
2481
+
2482
+ /** A failure on `errors()` of a makeFetchDriver() source */
2483
+ export type FetchError<REQUEST = any> = {
2484
+ /**
2485
+ * 'HTTP 404 ...' for a non-2xx status (with `.status` and `.body`), the network error
2486
+ * (TypeError), the body parse error, a TimeoutError (`.name === 'TimeoutError'`), or
2487
+ * "fetch is not available"
2488
+ */
2489
+ error: any;
2490
+ category: string | undefined;
2491
+ request: REQUEST;
2492
+ /** HTTP status, for a non-2xx response (undefined for a network error or timeout) */
2493
+ status?: number;
2494
+ /** The non-2xx response's body, parsed like 'auto' */
2495
+ body?: any;
2496
+ }
2497
+
2498
+ export type FetchSource<VALUE = any> = {
2499
+ /** 2xx responses: all of them, one category, or those a predicate accepts */
2500
+ select: (category?: string | ((response: FetchResponse<VALUE>) => boolean)) => Stream<FetchResponse<VALUE>>
2501
+ /** Failures. Filters like select(). While nothing listens, failures are console.error'd */
2502
+ errors: (category?: string | ((failure: FetchError) => boolean)) => Stream<FetchError>
2503
+ }
2504
+
2505
+ export type FetchDriverOptions = {
2506
+ /** Prefix for every request URL, e.g. '/api' or 'https://api.example.com' */
2507
+ baseUrl?: string;
2508
+ /** Headers for every request (a request's own `headers` win, case-insensitively) */
2509
+ headers?: Record<string, string> | Headers;
2510
+ /** fetch() options for every request, e.g. `{ credentials: 'include' }` (a request's `init` wins) */
2511
+ init?: FetchInit;
2512
+ /**
2513
+ * Latest only for every request (a request's own `latest` wins). Default false. renderComponent's
2514
+ * fake can't see this option: write `latest: true` on the request (the canonical form)
2515
+ */
2516
+ latest?: boolean;
2517
+ /** Timeout for every request, in ms. Default: none */
2518
+ timeoutMs?: number;
2519
+ /** Default `parse` for every request. Default 'auto' */
2520
+ parse?: 'auto' | 'json' | 'text' | 'response' | ((response: Response) => any);
2521
+ /** The fetch implementation. Default: `globalThis.fetch`, read at each request (so test stubs apply) */
2522
+ fetch?: (input: string, init?: any) => Promise<any>;
2523
+ /**
2524
+ * PLAN-3 D88: the query cache, off by default: `cache: queryCache({ staleTime })`. On:
2525
+ * resources' GET/HEAD replies are cached (stale-while-revalidate: cached data shows at once,
2526
+ * `refreshing` while it refetches), identical cacheable requests in flight share one fetch,
2527
+ * and focus / reconnect refetch stale mounted resources
2528
+ */
2529
+ cache?: QueryCache;
2530
+ /** Default `retry` for GET/HEAD requests (a request's own `retry` applies to any method). Default 0 */
2531
+ retry?: number | FetchRetry;
2532
+ }
2533
+
2534
+ /**
2535
+ * An HTTP driver over `fetch`: `run(App, { HTTP: makeFetchDriver() })`. Canonical: a request with reply actions
2536
+ * (`HTTP: (state) => ({ url: '/api/quote', ok: 'LOADED', error: 'FAILED' })`) whose
2537
+ * outcome arrives as the LOADED (parsed body) or FAILED (FetchFailure) action of the sending
2538
+ * instance. Without reply actions: the model sends a
2539
+ * request (`HTTP: (state) => ({ category: 'quote', url: '/api/quote' })`); the intent reads
2540
+ * `HTTP.select('quote')` (`{ category, value, status, request }`) and `HTTP.errors('quote')`
2541
+ * (`{ error, category, request, status?, body? }`). Non-2xx statuses, network errors and
2542
+ * timeouts go to errors(), never select(). `latest: true` drops superseded requests;
2543
+ * `{ category, abort: true }` cancels; disposing the app aborts everything in flight.
2544
+ * During SSR no requests are made (server rendering runs views only). In renderComponent
2545
+ * tests, pass no driver and answer with `t.respond('HTTP', value)` / `t.fail('HTTP', 404)`.
2546
+ */
2547
+ export function makeFetchDriver(options?: FetchDriverOptions): ((request$: Stream<any>) => FetchSource) & { cache?: QueryCache }
2548
+
2549
+ /**
2550
+ * Reconnect policy of a makeSocketDriver() connection. Delay of retry n (from 0):
2551
+ * `min(maxDelayMs, delayMs * 2^n)`, varied by ±`jitter` (a fraction). A fixed 1 s retry:
2552
+ * `{ delayMs: 1000, maxDelayMs: 1000, jitter: false }`. Defaults: 500 ms, 10 s, 0.2.
2553
+ */
2554
+ export type SocketReconnect = {
2555
+ delayMs?: number;
2556
+ maxDelayMs?: number;
2557
+ /** false (or 0): no jitter; true: 0.2; a number: that fraction (0.2 = ±20%) */
2558
+ jitter?: boolean | number;
2559
+ }
2560
+
2561
+ /** The reply actions of a connection. Each is optional; an event without one goes to `select()` */
2562
+ export type SocketActions = {
2563
+ /** Action for each incoming message: the JSON-parsed frame when it parses, else the raw data (binary as is) */
2564
+ message?: string;
2565
+ /** Action when the connection opens, every time: `SocketOpen` (`{ reconnected }`) */
2566
+ open?: string;
2567
+ /**
2568
+ * Action when the connection closes or fails to open without the app closing it (never for a
2569
+ * connection the app removed or replaced, or on dispose): `SocketClose`
2570
+ */
2571
+ close?: string;
2572
+ /** Action on an error event: `{ error }` (`SocketError`) */
2573
+ error?: string;
2574
+ /**
2575
+ * Reconnect after a drop (default on, with jittered backoff; the driver's `reconnect` option
2576
+ * is the default). `false`: a drop closes the connection for good. SSE: EventSource retries
2577
+ * transient drops itself; this applies when it gives up
2578
+ */
2579
+ reconnect?: false | SocketReconnect;
2580
+ /** Default true: connections to the same URL (and protocols) share one socket. false: a socket of its own */
2581
+ share?: boolean;
2582
+ /**
2583
+ * Keep this connection open while its component is in a hidden Switchable page (default: a
2584
+ * hidden page's connections close, and open again as new ones when it is shown)
2585
+ */
2586
+ background?: boolean;
2587
+ /** Not allowed: a `then` key makes the value a thenable (SYG610). Use `message` / `open` */
2588
+ then?: never;
2589
+ catch?: never;
2590
+ }
2591
+
2592
+ /** A WebSocket connection of `{ connections }` */
2593
+ export type SocketConnection = SocketActions & {
2594
+ /** URL or path (`/ws/rooms/general`: resolved against the page, ws: for http:, wss: for https:), after `baseUrl` */
2595
+ socket: string;
2596
+ /** WebSocket subprotocols (part of the connection's identity: a change reconnects) */
2597
+ protocols?: string | string[];
2598
+ }
2599
+
2600
+ /** A server-sent events (EventSource) connection of `{ connections }`: read-only */
2601
+ export type SseConnection = SocketActions & {
2602
+ /** URL (after `baseUrl`) */
2603
+ sse: string;
2604
+ withCredentials?: boolean;
2605
+ /** Named events → actions: `{ 'price-update': 'PRICE' }` (data JSON-parsed when it parses) */
2606
+ events?: Record<string, string>;
2607
+ }
2608
+
2609
+ /**
2610
+ * A value sent to a makeSocketDriver() sink: the sender's whole set of connections (a falsy
2611
+ * entry or a missing name closes that connection), or a message to send on one of them.
2612
+ */
2613
+ /** A component's whole set of connections: a falsy entry (`state.room && { ... }`) means closed */
2614
+ export type Connections = Record<string, SocketConnection | SseConnection | false | null | undefined | '' | 0>
2615
+
2616
+ export type SocketRequest =
2617
+ | { connections: Connections }
2618
+ | {
2619
+ /** The name of one of this instance's own WebSocket connections (else SYG611, not sent) */
2620
+ to: string;
2621
+ /** Sent as JSON.stringify(json) */
2622
+ json?: any;
2623
+ /** Sent as is */
2624
+ text?: string;
2625
+ /** Sent as is (ArrayBuffer, Blob, typed array) */
2626
+ binary?: any;
2627
+ }
2628
+
2629
+ /** Data of a connection's `open` action */
2630
+ export type SocketOpen = { reconnected: boolean }
2631
+ /** Data of a connection's `close` action (SSE: no code / reason) */
2632
+ export type SocketClose = { code?: number; reason?: string; willReconnect: boolean }
2633
+ /** Data of a connection's `error` action: the error Event (or the constructor's exception) */
2634
+ export type SocketError = { error: any }
2635
+
2636
+ /** An event without a reply action, on `select(name?)` */
2637
+ export type SocketEvent<DATA = any> =
2638
+ | { name: string; type: 'message'; data: DATA }
2639
+ | { name: string; type: 'open'; data: SocketOpen }
2640
+ | { name: string; type: 'close'; data: SocketClose }
2641
+ | { name: string; type: 'error'; data: SocketError }
2642
+
2643
+ export type SocketSource = {
2644
+ /** Events of connections without an action name for that event type, for one connection name or all */
2645
+ select: (name?: string) => Stream<SocketEvent>
2646
+ }
2647
+
2648
+ export type SocketDriverOptions = {
2649
+ /** Prefix for relative URLs, e.g. '/api' or 'https://api.example.com' */
2650
+ baseUrl?: string;
2651
+ /** Default reconnect policy (a connection's `reconnect` wins, merged over it). Default on */
2652
+ reconnect?: false | SocketReconnect;
2653
+ /** Messages kept per connection while it (re)connects; the oldest are dropped. Default 100 */
2654
+ queueLimit?: number;
2655
+ /** The WebSocket class. Default: `globalThis.WebSocket`, read at connect time (so test stubs apply) */
2656
+ WebSocket?: any;
2657
+ /** The EventSource class. Default: `globalThis.EventSource`, read at connect time */
2658
+ EventSource?: any;
2659
+ }
2660
+
2661
+ /**
2662
+ * WebSocket and server-sent events: `run(App, { WS: makeSocketDriver() })`. A component sends
2663
+ * `{ connections: { room: { socket: '/ws/rooms/general', message: 'RECEIVED', open: 'ONLINE',
2664
+ * close: 'DROPPED' } } }` to declare its connections (diffed by name: new ones open, removed or
2665
+ * falsy ones close, a changed URL reconnects) and `{ to: 'room', json: { text } }` to send.
2666
+ * Events arrive as the named actions on exactly that instance. Drops reconnect with backoff;
2667
+ * connections to the same URL are shared; a disposed instance's connections close. No
2668
+ * connections during SSR.
2669
+ */
2670
+ export function makeSocketDriver(options?: SocketDriverOptions): (sink$: Stream<any>) => SocketSource
2671
+
2672
+ /** The names of the `:params` in a route pattern: ParamNames<'/tasks/:id'> = 'id' */
2673
+ export type ParamNames<P extends string> =
2674
+ P extends `${string}:${infer K}/${infer Rest}` ? K | ParamNames<`/${Rest}`> :
2675
+ P extends `${string}:${infer K}` ? K : never;
2676
+
2677
+ /** Route names of a route table, without the `'*'` not-found route (`string` when not literal) */
2678
+ export type RouteName<R extends Record<string, string>> =
2679
+ string extends keyof R ? string : { [K in keyof R & string]: R[K] extends '*' ? never : K }[keyof R & string];
2680
+
2681
+ /** href()'s params for one pattern: required when it has `:params`, none otherwise */
2682
+ export type RouteParamsArg<P extends string> =
2683
+ string extends P ? [params?: Record<string, string | number>] :
2684
+ [ParamNames<P>] extends [never] ? [params?: Record<string, never>] :
2685
+ [params: { [K in ParamNames<P>]: string | number }];
2686
+
2687
+ export type RouteQuery = Record<string, string | number | boolean | null | undefined>;
2688
+
2689
+ /** The route value the `route` reply action carries */
2690
+ export interface Route<NAME extends string = string> {
2691
+ /** the matched route's name (the `'*'` route's name when nothing matched; null without one) */
2692
+ name: NAME | null;
2693
+ /** decoded `:params` */
2694
+ params: Record<string, string>;
2695
+ /** the query string as an object (the last value of a repeated key) */
2696
+ query: Record<string, string>;
2697
+ /** the decoded fragment without '#' ('' when none) */
2698
+ hash: string;
2699
+ /** the path without `base`, normalised: no trailing slash, no empty segments */
2700
+ path: string;
2701
+ }
2702
+
2703
+ /** A value for the router's sink (`ROUTER`); `{ route }` is reserved for the `route` static */
2704
+ export type RouterCommand<R extends Record<string, string> = Record<string, string>> =
2705
+ | { to: RouteName<R>; params?: Record<string, string | number>; query?: RouteQuery; hash?: string; replace?: boolean; scroll?: boolean; force?: boolean; block?: string | false | null }
2706
+ | { url: string; replace?: boolean; scroll?: boolean; force?: boolean; block?: string | false | null }
2707
+ | { back: true; force?: boolean; block?: string | false | null } | { forward: true; force?: boolean; block?: string | false | null } | { go: number; force?: boolean; block?: string | false | null }
2708
+ | { block: string | false | null }
2709
+ | { prefetch: RouteName<R> | string; params?: Record<string, string | number>; query?: RouteQuery };
2710
+
2711
+ /**
2712
+ * The data of a block action (`{ block: 'CONFIRM_LEAVE' }`): the navigation that was not made.
2713
+ * Send `proceed` to the router's sink to make it anyway.
2714
+ */
2715
+ export interface RouterBlocked {
2716
+ /** the URL the navigation would go to */
2717
+ to: string;
2718
+ route: Route;
2719
+ proceed: RouterCommand;
2720
+ }
2721
+
2722
+ export interface RouterOptions<R extends Record<string, string> = Record<string, string>> {
2723
+ /** `{ name: '/path/:param' | '*' }`; first match wins; `'*'` is the not-found route */
2724
+ routes: R;
2725
+ /** path prefix the app lives under ('/app'); in hash mode, the page's path */
2726
+ base?: string;
2727
+ /** 'history' (default) or 'hash' (`/#/tasks/1`) */
2728
+ mode?: 'history' | 'hash';
2729
+ /** scroll to top on a push, restore on back/forward. Default true (false with `navigate`) */
2730
+ scroll?: boolean;
2731
+ /**
2732
+ * After a push or back/forward, once the DOM is quiet, focus the first match of these
2733
+ * comma-separated selectors, tried in order. Default '[data-router-focus],main h1,h1'; false disables
2734
+ */
2735
+ focus?: string | false;
2736
+ /** quiet time (ms) before scroll restore and focus. Default 30 */
2737
+ settleMs?: number;
2738
+ /**
2739
+ * called for `{ prefetch }` commands, e.g. to warm a route's data in the fetch driver's cache:
2740
+ * `prefetch: (route) => routeData[route.name]?.(route).forEach(cache.prefetch)`
2741
+ */
2742
+ prefetch?: (route: Route, url: string) => void;
2743
+ /** Vike: its `navigate()` (from 'vike/client/router'); the router then leaves links and history to Vike */
2744
+ navigate?: (url: string, options: { overwriteLastHistoryEntry: boolean }) => any;
2745
+ /** test seams: default the global window and its history, location and document */
2746
+ window?: any;
2747
+ history?: any;
2748
+ location?: any;
2749
+ document?: any;
2750
+ }
2751
+
2752
+ /** The ROUTER source: reply actions plus the latest route */
2753
+ export interface RouterSource {
2754
+ current(): Route | null;
2755
+ href: (name: string, params?: Record<string, string | number>, query?: RouteQuery, hash?: string) => string;
2756
+ dispose(): void;
2757
+ }
2758
+
2759
+ export interface Router<R extends Record<string, string> = Record<string, string>> {
2760
+ routes: R;
2761
+ /** a link to a named route (pure, SSR-safe): href('task', { id: 2 }, { tab: 'notes' }) */
2762
+ href<N extends RouteName<R>>(name: N, ...args: [...RouteParamsArg<R[N]>, query?: RouteQuery, hash?: string]): string;
2763
+ /** the Route of a URL (a path or an absolute URL); pure */
2764
+ match(url: string): Route<RouteName<R>>;
2765
+ /** the Route of the current location, or of `url` (SSR: the request URL). The `initialState.route` seed */
2766
+ current(url?: string): Route<RouteName<R>>;
2767
+ /** the driver: `run(App, { ROUTER: router.driver })` */
2768
+ driver: (sink$: Stream<any>) => RouterSource;
2769
+ options: RouterOptions<R>;
2770
+ }
2771
+
2772
+ /**
2773
+ * The SPA router: `export const router = makeRouter({ routes: { home: '/', task: '/tasks/:id',
2774
+ * notFound: '*' } })`, then `run(App, { ROUTER: router.driver })`. Components declare
2775
+ * `App.route = 'ROUTE'` and store the route; views link with `<a href={router.href('task', { id })}>`
2776
+ * (clicks are intercepted at the document); models navigate with `ROUTER: { to: 'task', params }`.
2777
+ */
2778
+ export function makeRouter<const R extends Record<string, string>>(options: RouterOptions<R>): Router<R>
2779
+
2780
+ /** `makeRouter(options).driver` */
2781
+ export function makeRouterDriver<const R extends Record<string, string>>(options: RouterOptions<R>): (sink$: Stream<any>) => RouterSource
2782
+
2783
+ /** A `head` static value or HEAD sink value */
2784
+ export interface HeadValue {
2785
+ title?: string;
2786
+ /** `{ description: '...', 'og:title': '...' }`: og:/article:/... keys use `property`, others `name`; null removes */
2787
+ meta?: Record<string, string | null | undefined>;
2788
+ /** link tags; merged by `key`, else by `rel` for canonical, else by `rel` + `href` */
2789
+ link?: Array<{ rel: string; href: string; key?: string; [attr: string]: any }>;
2790
+ }
2791
+
2792
+ /**
2793
+ * Document title, meta and link tags: `run(App, { HEAD: makeHeadDriver() })` with a `head`
2794
+ * static (`App.head = (state) => ({ title })`) or HEAD sink values from a model entry.
2795
+ * `titleTemplate: '%s · Tasks'` formats every title.
2796
+ */
2797
+ /**
2798
+ * PLAN-4 GS-12: a DOM driver that runs the patches asked for by a component's
2799
+ * `viewTransitions` static inside `document.startViewTransition()`:
2800
+ * `run(App, { DOM: makeViewTransitionDOMDriver('#root') })`. It is `makeDOMDriver(mountPoint,
2801
+ * options)` plus the hook: one action's patches (a
2802
+ * Collection move is several) are folded into one transition (20 ms quiet window, capped at
2803
+ * 200 ms); `prefers-reduced-motion: reduce` and browsers without the API patch at once.
2804
+ */
2805
+ export function makeViewTransitionDOMDriver(mountPoint?: string | Element | DocumentFragment, options?: import('./cycle/dom/makeDOMDriver').DOMDriverOptions): (vnode$: Stream<any>, name?: string) => MainDOMSource
2806
+
2807
+ export function makeHeadDriver(options?: { titleTemplate?: string; document?: any }): (sink$: Stream<any>) => { dispose(): void }
2808
+
2809
+ /**
2810
+ * PLAN-4 GS-7: one timer of a `timers` static. `every`: a positive interval in ms, drift-free
2811
+ * (tick n is due at start + n * every; a late tick coalesces the missed ones and n jumps);
2812
+ * `after`: once, after ms (0 or more); `frame`: every animation frame (requestAnimationFrame,
2813
+ * else every 16 ms). `background: true` keeps it running while its Switchable page is hidden.
2814
+ * An invalid spec is not started (SYG422 in dev).
2815
+ */
2816
+ export type TimerSpec<ACTION extends string = string> =
2817
+ | { every: number; action: ACTION; background?: boolean; after?: never; frame?: never }
2818
+ | { after: number; action: ACTION; background?: boolean; every?: never; frame?: never }
2819
+ | { frame: ACTION; background?: boolean; every?: never; after?: never; action?: never }
2820
+
2821
+ /** A component's whole set of timers, by name: a falsy entry (`state.running && { ... }`) is stopped */
2822
+ export type Timers<ACTION extends string = string> = { [name: string]: TimerSpec<ACTION> | false | null | undefined | 0 | '' }
2823
+
2824
+ /** The data of an `every` timer's action: the tick number from 1 (it jumps over coalesced ticks) and Date.now() */
2825
+ export interface TimerTick { n: number; t: number }
2826
+ /** The data of an `after` timer's action: Date.now() */
2827
+ export interface TimerAfter { t: number }
2828
+ /** The data of a `frame` timer's action: Date.now() and the ms since the previous frame (0 on the first) */
2829
+ export interface TimerFrame { t: number; dt: number }
2830
+
2831
+ /**
2832
+ * PLAN-4 GS-7: runs the components' `timers` statics: `run(App, { TIMER: makeTimerDriver() })`.
2833
+ * The key is free (the core finds the driver by the static it takes); `TIMER` by convention.
2834
+ * Each timer's action goes to the instance that declared it. A disposed instance's timers stop;
2835
+ * app dispose stops them all. A component declaring `timers` with no timer driver gets SYG643 in dev.
2836
+ */
2837
+ export function makeTimerDriver(): (sink$: Stream<any>) => { dispose(): void }
2838
+
2839
+ /** PLAN-5 B-3: fields every browser-source spec takes */
2840
+ interface BrowserSpecBase<ACTION extends string> {
2841
+ /** the action each event (or the current value) is delivered as */
2842
+ action: ACTION;
2843
+ /** keep it running while its Switchable page is hidden */
2844
+ background?: boolean;
2845
+ }
2846
+ /**
2847
+ * PLAN-5 B-3: one entry of a `browser` static; its one source key is its kind:
2848
+ * - `intersection`: the elements a selector matches in this component (`true`: its root element)
2849
+ * entering or leaving the viewport (IntersectionObserver), data `BrowserIntersection`;
2850
+ * - `resize`: their content-box size (ResizeObserver), data `BrowserResize`;
2851
+ * - `media`: a media query, data `{ matches, media }` (the current value first);
2852
+ * - `storage`: a localStorage key (`area: 'session'`: sessionStorage; `json: true`: parsed), read
2853
+ * and observed (other tabs' writes, and BROWSER `setItem` / `removeItem`), data `{ key, value }`;
2854
+ * for state that should survive a reload use `persist()`;
2855
+ * - `visibility: true`: the document's visibility, data `{ visible }`;
2856
+ * - `online: true`: the network, data `{ online }`;
2857
+ * - `geolocation`: watchPosition (permission-gated; `true` or PositionOptions), data
2858
+ * `BrowserPosition`, failures `{ code, message }` to `error`.
2859
+ * An invalid spec is not started (SYG663 in dev).
2860
+ */
2861
+ export type BrowserSpec<ACTION extends string = string> =
2862
+ | (BrowserSpecBase<ACTION> & { intersection: string | true; threshold?: number | number[]; rootMargin?: string })
2863
+ | (BrowserSpecBase<ACTION> & { resize: string | true })
2864
+ | (BrowserSpecBase<ACTION> & { media: string })
2865
+ | (BrowserSpecBase<ACTION> & { storage: string; area?: 'local' | 'session'; json?: boolean; error?: ACTION })
2866
+ | (BrowserSpecBase<ACTION> & { visibility: true })
2867
+ | (BrowserSpecBase<ACTION> & { online: true })
2868
+ | (BrowserSpecBase<ACTION> & { geolocation: true | { enableHighAccuracy?: boolean; maximumAge?: number; timeout?: number }; error?: ACTION })
2869
+
2870
+ /** A component's browser sources, by name: a falsy entry (`!state.seen && { ... }`) is stopped */
2871
+ export type BrowserSources<ACTION extends string = string> = { [name: string]: BrowserSpec<ACTION> | false | null | undefined | 0 | '' }
2872
+
2873
+ /** The data of an `intersection` action: visible (isIntersecting), the ratio, which matched element (its index and dataset) */
2874
+ export interface BrowserIntersection { visible: boolean; ratio: number; index: number; dataset: Record<string, string> }
2875
+ /** The data of a `resize` action: the content box, which matched element */
2876
+ export interface BrowserResize { width: number; height: number; index: number; dataset: Record<string, string> }
2877
+ /** The data of a `geolocation` action */
2878
+ export interface BrowserPosition { latitude: number; longitude: number; accuracy: number; altitude: number | null; altitudeAccuracy: number | null; heading: number | null; speed: number | null; timestamp: number }
2879
+
2880
+ /**
2881
+ * PLAN-5 B-3: a command for the browser driver's sink, from a model entry
2882
+ * (`COPY: { BROWSER: (state) => ({ copy: state.link, ok: 'COPIED' }) }`); the method is its
2883
+ * `copy`, `paste`, `setItem` or `removeItem` key, in any order. `ok` / `error` name reply actions (copy/paste: `{ text }`; a failure `{ name, message }`).
2884
+ */
2885
+ export type BrowserCommand =
2886
+ | { copy: string; ok?: string; error?: string }
2887
+ | { paste: true; ok: string; error?: string }
2888
+ | { setItem: string; value: any; area?: 'local' | 'session'; json?: boolean; ok?: string; error?: string }
2889
+ | { removeItem: string; area?: 'local' | 'session'; ok?: string; error?: string }
2890
+
2891
+ /** PLAN-5 B-3: a browser source for makeBrowserDriverWith (its declaration kinds and commands) */
2892
+ export interface BrowserSource { d?: Record<string, Function>; c?: Record<string, Function> }
2893
+ /** `intersection` declarations */
2894
+ export const intersectionSource: BrowserSource
2895
+ /** `resize` declarations */
2896
+ export const resizeSource: BrowserSource
2897
+ /** `media` declarations */
2898
+ export const mediaSource: BrowserSource
2899
+ /** `storage` declarations and the `setItem` / `removeItem` commands */
2900
+ export const storageSource: BrowserSource
2901
+ /** `visibility` declarations */
2902
+ export const visibilitySource: BrowserSource
2903
+ /** `online` declarations */
2904
+ export const onlineSource: BrowserSource
2905
+ /** `geolocation` declarations */
2906
+ export const geolocationSource: BrowserSource
2907
+ /** the `copy` / `paste` commands */
2908
+ export const clipboardSource: BrowserSource
2909
+
2910
+ /**
2911
+ * PLAN-5 B-3: runs the components' `browser` statics and the BROWSER sink commands, with every
2912
+ * source: `run(App, { BROWSER: makeBrowserDriver() })`. The key is free (the core finds the driver
2913
+ * by the static it takes); `BROWSER` by convention. A component declaring `browser` with no
2914
+ * browser driver gets SYG643 in dev.
2915
+ */
2916
+ export function makeBrowserDriver(): (sink$: Stream<any>, name?: string) => { dispose(): void }
2917
+ /** PLAN-5 B-3: makeBrowserDriver() with only the given sources (the others add no bytes): `makeBrowserDriverWith(intersectionSource, mediaSource)` */
2918
+ export function makeBrowserDriverWith(...sources: BrowserSource[]): (sink$: Stream<any>, name?: string) => { dispose(): void }
2919
+
2920
+ /** The tags for the head values `renderToString(App, { head: list })` collected (SSR) */
2921
+ export function renderHead(list: Array<HeadValue | null | undefined | false>, options?: { titleTemplate?: string }): string
2922
+
582
2923
  export interface Ref<T = HTMLElement> {
583
2924
  current: T | null;
584
2925
  }
@@ -629,18 +2970,305 @@ export interface InstallPrompt {
629
2970
 
630
2971
  export function createInstallPrompt(): InstallPrompt
631
2972
 
2973
+ export interface SimulatedEventInit {
2974
+ /** Merged into `event.target` (value, checked, dataset, ...) */
2975
+ target?: Record<string, any>;
2976
+ /** Shorthand for target.value */
2977
+ value?: any;
2978
+ /** Shorthand for target.checked */
2979
+ checked?: boolean;
2980
+ /** Merged into target.dataset (values become strings, like the DOM) */
2981
+ dataset?: Record<string, any>;
2982
+ /** Alias for dataset */
2983
+ data?: Record<string, any>;
2984
+ /** Keyboard key (e.key) */
2985
+ key?: string;
2986
+ /**
2987
+ * Don't fail when the selector matches no rendered element: wait up to 300ms for it, then
2988
+ * drop the event with SYG103 (info). Not copied onto the event.
2989
+ */
2990
+ allowMissing?: boolean;
2991
+ /**
2992
+ * Target the matching element inside the first element matching this selector or control
2993
+ * (e.g. one Collection item): `t.simulateEvent(Done, 'click', { within: '[data-id="2"]' })`.
2994
+ * Not copied onto the event.
2995
+ */
2996
+ within?: string | AnyControl;
2997
+ /** Any other event properties are copied onto the event */
2998
+ [prop: string]: any;
2999
+ }
3000
+
3001
+ /**
3002
+ * Which request a t.respond()/t.fail() answers, as options. An object with only these keys is
3003
+ * options; any other object is a request pattern (see `respond`).
3004
+ */
3005
+ export interface FakeReplyOptions {
3006
+ /** Only requests with this category */
3007
+ category?: string;
3008
+ /**
3009
+ * The request to answer, compared by value: a request object (e.g. an element of
3010
+ * t.requests(name), or the constant the model returns; among equal pending requests that very
3011
+ * object, else the newest), a partial request (`{ url: '/a' }`), a URL string, or a predicate
3012
+ * `(request) => boolean`. `null`: push the value without a request (for a source that emits
3013
+ * on its own); `category` then sets its category.
3014
+ */
3015
+ request?: any;
3016
+ /** respond(): the status (default 200). fail(): an HTTP error response with this status */
3017
+ status?: number;
3018
+ /** fail(): the parsed error body */
3019
+ body?: any;
3020
+ /**
3021
+ * That very request by its position in t.requests(name) (counting only those matching
3022
+ * `request`/`category`): 0 the first, -1 the newest. Answers the older of two identical
3023
+ * requests; throws at the call when it is no longer pending (answered, aborted, superseded).
3024
+ */
3025
+ nth?: number;
3026
+ }
3027
+
3028
+ /** A connection on a driverless socket sink, as `t.connections(name)` lists it: the spec as declared plus these */
3029
+ export interface FakeConnection {
3030
+ /** Its name in `{ connections: { [name]: spec } }` */
3031
+ name: string;
3032
+ /** The URL as declared (WebSocket) */
3033
+ socket?: string;
3034
+ /** The URL as declared (server-sent events) */
3035
+ sse?: string;
3036
+ /** The URL it opened (a socket path resolves to ws:/wss: on the page's host) */
3037
+ url: string;
3038
+ /** 'closed': dropped (t.drop), waiting for a retry, or gone (reconnect: false) */
3039
+ state: 'connecting' | 'open' | 'closed';
3040
+ /** The name of the component that declared it */
3041
+ sender: string;
3042
+ [key: string]: any;
3043
+ }
3044
+ /**
3045
+ * Which connections t.open / t.push / t.drop act on: a connection name or URL (as declared or
3046
+ * opened), a partial FakeConnection compared by value (`{ socket: '/ws/a' }`), or a predicate.
3047
+ * Nothing: the newest connection that can take the call.
3048
+ */
3049
+ export type FakeConnectionTarget = string | Record<string, any> | ((connection: FakeConnection) => boolean);
3050
+
3051
+ /** PLAN-3 5-3: a cache entry of an HTTP fake (t.cache) */
3052
+ export type FakeCacheEntry = {
3053
+ /** method, URL (query sorted), body and parse */
3054
+ key: string;
3055
+ /** ms since the reply was cached (undefined before data arrives) */
3056
+ age?: number;
3057
+ /** older than staleTime, or invalidated */
3058
+ stale: boolean;
3059
+ /** mounted resources using it */
3060
+ subscribers: number;
3061
+ /** the parsed body */
3062
+ data: any;
3063
+ }
3064
+
632
3065
  export interface RenderOptions {
633
3066
  /** Override initial state (defaults to component's .initialState) */
634
3067
  initialState?: any;
3068
+ /**
3069
+ * D214: context from the ancestors the rendered component would have, for testing a child
3070
+ * alone: `renderComponent(Greeting, { context: { lang: 'fr' } })`. The view, reducers and
3071
+ * descendants read it as `context.lang`; the component's own `.context` entries win over a key
3072
+ * of the same name. Fixed values for the test's lifetime.
3073
+ */
3074
+ context?: Record<string, any>;
635
3075
  /** Mock DOM configuration — maps selectors to event streams */
636
3076
  mockConfig?: Record<string, any>;
637
- /** Additional drivers beyond DOM, EVENTS, STATE, and LOG */
3077
+ /** The app-level error hook, as run()'s `onError` (PLAN-4 GS-11) */
3078
+ onError?: AppErrorHook;
3079
+ /**
3080
+ * Additional drivers beyond DOM, EVENTS, STATE, and LOG. A custom sink without a driver, in
3081
+ * the component or any child, gets a recording one (read it with sinkValues / requests), and
3082
+ * a source without a driver a scriptable fake (answer with respond / fail).
3083
+ */
638
3084
  drivers?: Record<string, any>;
3085
+ /**
3086
+ * Diagnostics mode while rendered. Default: 'collect' (error-severity messages still print),
3087
+ * or the current mode when diagnostics are already on. Restored when the last instance is
3088
+ * disposed. Dev checks require `import 'sygnal/diagnostics'` (the Vite plugin adds it under Vitest).
3089
+ */
3090
+ diagnostics?: DiagnosticsMode;
3091
+ /** Enable strict (canonical-form) runtime checks while rendered (requires 'sygnal/diagnostics') */
3092
+ strict?: boolean;
3093
+ /**
3094
+ * settle()'s quiet window in ms (default 20). A model `next('X', data, ms)` with a longer
3095
+ * delay fires after settle() resolved: raise this, or wait with `t.next(pred)`.
3096
+ */
3097
+ settleMs?: number;
3098
+ /** How long simulateEvent waits for a matching element / its listeners, in ms (default 300) */
3099
+ eventWaitMs?: number;
3100
+ /** Default timeout of next(), waitForState() and settle(), in ms (default 2000) */
3101
+ timeoutMs?: number;
3102
+ /**
3103
+ * 'mock' (default): the mock DOM. 'real': mount into a real container element (needs a DOM:
3104
+ * `// @vitest-environment jsdom`, or `environment: 'jsdom'` / 'happy-dom'), so `checked`,
3105
+ * `value`, `disabled`, focus, refs and Portals are real. simulateEvent then dispatches a real
3106
+ * event on the first element matching any CSS selector (a value/checked init is set on the
3107
+ * element first; 'click' runs the default action and skips disabled controls; 'focus'/'blur'
3108
+ * move focus). Read elements with `t.query(sel)` / `t.queryAll(sel)` / `t.container`. While
3109
+ * it runs, what jsdom lacks is added (and removed after): the `<dialog>` and popover methods,
3110
+ * scrollIntoView, and for Zag's machines ResizeObserver, CSS.escape and Element#scrollTo.
3111
+ */
3112
+ dom?: 'mock' | 'real';
3113
+ /**
3114
+ * Fake socket connections open by themselves (default true; reconnects too; a t.push / t.drop
3115
+ * right after the declaration opens it first). false: they stay 'connecting' until t.open(),
3116
+ * for "Connecting…" assertions and failures to open (t.drop on a connecting one).
3117
+ */
3118
+ autoConnect?: boolean;
3119
+ /**
3120
+ * PLAN-3 G-160: the driverless sink that receives the components' `connections` static
3121
+ * (default 'WS'). It is created even when no model entry names it. Pass a driver for it in
3122
+ * `drivers` to use a real one.
3123
+ */
3124
+ socketSink?: string;
3125
+ /**
3126
+ * PLAN-3: the driverless sink that receives the components' `resources`
3127
+ * static (default 'HTTP'); each resource fetch is pending until t.respond / t.fail, and is
3128
+ * listed in t.requests as `{ url, ...request, resource: name }`.
3129
+ */
3130
+ resourceSink?: string;
3131
+ /**
3132
+ * PLAN-3 5-3: options for the HTTP fakes' makeFetchDriver (all but `fetch`), e.g.
3133
+ * `{ cache: queryCache() }`. Focus / reconnect refetches come only from t.focus() / t.online()
3134
+ */
3135
+ http?: FetchDriverOptions;
3136
+ /**
3137
+ * The app's router (the object `makeRouter()` returns). With no driver for `routerSink` in
3138
+ * `drivers`, renderComponent runs its real driver over an in-memory window (location, history,
3139
+ * popstate a task later, document listeners): read with `t.location`, drive with
3140
+ * `t.navigate` / `t.back` / `t.forward`; the commands the app sent are `t.sent('ROUTER')`.
3141
+ * Required when the component declares `route` (renderComponent throws naming it otherwise).
3142
+ * The real `window.location` is never changed.
3143
+ */
3144
+ router?: Router<any>;
3145
+ /** The router fake's start URL (default '/'), e.g. '/tasks/2?tab=notes' */
3146
+ url?: string;
3147
+ /** The sink the router fake serves (default 'ROUTER') */
3148
+ routerSink?: string;
3149
+ /** Run the router's scroll handling in the fake (default false; positions are kept in memory) */
3150
+ routerScroll?: boolean;
3151
+ /** Run the router's focus handling (default false; true: the router's own `focus` selectors; a string: these selectors). Needs `dom: 'real'` */
3152
+ routerFocus?: boolean | string;
3153
+ /** The sink the HEAD fake serves (default 'HEAD'); with no driver for it, `t.head()` reads what the components declared */
3154
+ headSink?: string;
3155
+ /** The HEAD fake's `titleTemplate` (`'%s · App'`), as passed to makeHeadDriver */
3156
+ titleTemplate?: string;
3157
+ /** PLAN-4 GS-7: the sink the timer fake serves (default 'TIMER'); with no driver for it, the real makeTimerDriver() runs on the test's timers and `t.timers()` lists them */
3158
+ timerSink?: string;
3159
+ /** PLAN-5 B-3: the sink the browser fake serves (default 'BROWSER'); with no driver for it, the browser driver runs over fake sources `t.browser` drives */
3160
+ browserSink?: string;
3161
+ /** PLAN-5 B-3: the browser fake's environment at start (default: no media query matches, empty storage, visible, online, empty clipboard, no position, nothing denied) */
3162
+ browser?: BrowserFakeOptions;
3163
+ /**
3164
+ * PLAN-4 GS-5: the fake storage behind the root's `persist()` ('local' and 'session' alike), as
3165
+ * key -> stored entry (`{ version, state }`; with `format: 'plain'` the stored keys themselves;
3166
+ * or a raw string). Used as is, not copied: writes
3167
+ * land in it, and renderComponent calls given the same object share one storage (`sync: true`
3168
+ * applies one's writes in the other). Default: a new empty object.
3169
+ */
3170
+ storage?: Record<string, PersistedEntry | Record<string, any> | string>;
639
3171
  }
640
3172
 
641
- export interface RenderResult {
3173
+ /** PLAN-4 GS-5: a stored persist() entry (as `t.storage(key)` returns it) */
3174
+ export interface PersistedEntry<STATE = any> {
3175
+ version: number
3176
+ state: Partial<STATE>
3177
+ }
3178
+
3179
+ /** PLAN-5 B-3: renderComponent's `browser` option: the fake environment at start */
3180
+ export interface BrowserFakeOptions {
3181
+ /** media query -> matches */
3182
+ media?: Record<string, boolean>;
3183
+ /** localStorage, key -> stored string */
3184
+ storage?: Record<string, string>;
3185
+ /** sessionStorage, key -> stored string */
3186
+ sessionStorage?: Record<string, string>;
3187
+ visible?: boolean;
3188
+ online?: boolean;
3189
+ clipboard?: string;
3190
+ /** the position a geolocation declaration starts with (the coords; the rest default) */
3191
+ position?: Partial<BrowserPosition>;
3192
+ /** permissions denied from the start */
3193
+ deny?: Array<'geolocation' | 'clipboard'>;
3194
+ }
3195
+
3196
+ /** PLAN-5 B-3: `t.browser` */
3197
+ export interface BrowserFake {
3198
+ /** the declarations of `intersection: target` hear `{ visible, ratio: 1 | 0, index: 0, dataset: {}, ...data }`; `at`: only the at-th of them (start order). Throws when nothing declares it. (Each declaration also hears `{ visible: false, ratio: 0, index: 0, dataset: {} }` when it starts, and a resize one `{ width: 0, height: 0, index: 0, dataset: {} }`, as the observers report) */
3199
+ intersect(target: string | true, visible?: boolean, data?: Partial<BrowserIntersection> & { at?: number }): Promise<void>;
3200
+ /** the declarations of `resize: target` hear `{ width, height, index: 0, dataset: {}, ...size }`. Throws when nothing declares it */
3201
+ resize(target: string | true, size: Partial<BrowserResize> & { at?: number }): Promise<void>;
3202
+ /** a position (accuracy 0, the rest null, timestamp now unless given) or an error `{ code, message }` for the geolocation declarations */
3203
+ geolocation(position: Partial<BrowserPosition> | { code: number; message?: string }): Promise<void>;
3204
+ /** a media query now matches (or not) */
3205
+ media(query: string, matches: boolean): Promise<void>;
3206
+ visibility(visible: boolean): Promise<void>;
3207
+ online(online: boolean): Promise<void>;
3208
+ /** the stored string (null when absent) */
3209
+ storage(key: string): string | null;
3210
+ /** another tab writes the key (a non-string is stored as JSON; null removes it) */
3211
+ storage(key: string, value: any, area?: 'local' | 'session'): Promise<void>;
3212
+ /** the clipboard's text */
3213
+ clipboard(): string;
3214
+ /** the clipboard's text is now `text` */
3215
+ clipboard(text: string): Promise<void>;
3216
+ /** deny permissions: copy/paste fail with NotAllowedError, geolocation with code 1 (running ones too) */
3217
+ deny(...kinds: Array<'geolocation' | 'clipboard'>): void;
3218
+ /** the running declarations, in start order: `{ name, ...spec, component }` */
3219
+ active(): Array<Record<string, any>>;
3220
+ }
3221
+
3222
+ /** PLAN-4 GS-7: an active timer, as `t.timers()` lists it: the spec as declared plus these */
3223
+ export type ActiveTimer = TimerSpec & {
3224
+ /** Its name in the `timers` static */
3225
+ name: string;
3226
+ /** The action it sends (a `frame` timer's is its `frame`) */
3227
+ action: string;
3228
+ /** The declaring component's name */
3229
+ component: string;
3230
+ }
3231
+
3232
+ /**
3233
+ * What renderComponent() returns. STATE is the component's state type (calculated fields
3234
+ * included), inferred by renderComponent; name it for a handle declared before it is assigned:
3235
+ * `let t: RenderResult<State>`. Defaults to `any` (untyped tests compile as before).
3236
+ */
3237
+ /** PLAN-4 GS-10: where an action in `t.actions` came from */
3238
+ export type ActionCause = 'intent' | 'next' | 'reply' | 'built-in' | 'simulateAction' | 'behavior'
3239
+
3240
+ /**
3241
+ * PLAN-4 GS-10: one action in renderComponent's `t.actions`. `sinks` fills in as the action's
3242
+ * reducers run (STATE a microtask later), so an entry is live.
3243
+ */
3244
+ export interface TestAction {
3245
+ /** The action name (a behavior's actions are namespaced: `pager.NEXT`) */
3246
+ type: string
3247
+ /** Its data (the DOM event for a DOM intent stream) */
3248
+ data: any
3249
+ /** The name of the component that ran it */
3250
+ component: string
3251
+ /** That component instance's id (stable for its life; inspect()'s component id) */
3252
+ instance: string
3253
+ /** The sinks that produced a value: not ABORT; STATE not the unchanged state; EFFECT when it ran */
3254
+ sinks: string[]
3255
+ /** 'intent' | 'next' (a reducer's or EFFECT's next()) | 'reply' (reply actions) | 'built-in' (INITIALIZE, BOOTSTRAP, DISPOSE, RESOURCE) | 'simulateAction' | 'behavior' */
3256
+ cause: ActionCause
3257
+ /** ms since renderComponent() was called (the fake clock under fake timers) */
3258
+ at: number
3259
+ }
3260
+
3261
+ /** PLAN-4 GS-10: what `t.explain()` returns */
3262
+ export interface ExplainedAction<STATE = any> extends TestAction {
3263
+ /** The root state the action produced (the first recorded after its STATE reducer ran) */
3264
+ state: STATE
3265
+ /** Its STATE reducer: the model's function and its source text (JS exposes no source location) */
3266
+ reducer?: { action: string; sink: string; fn: Function; source: string }
3267
+ }
3268
+
3269
+ export interface RenderResult<STATE = any> {
642
3270
  /** Stream of state values */
643
- state$: Stream<any>;
3271
+ state$: Stream<STATE>;
644
3272
  /** Stream of rendered VNode trees */
645
3273
  dom$: Stream<any>;
646
3274
  /** Event bus source — call .select(type) to filter */
@@ -649,17 +3277,284 @@ export interface RenderResult {
649
3277
  sinks: Record<string, any>;
650
3278
  /** All source objects by driver name */
651
3279
  sources: Record<string, any>;
652
- /** Push an action directly into the intent→model pipeline */
3280
+ /** Push an action into the intent→model pipeline under its real name (all sinks of the entry run) */
653
3281
  simulateAction: (actionName: string, data?: any) => void;
654
- /** Wait for state to satisfy a predicate (resolves with the matching state) */
655
- waitForState: (predicate: (state: any) => boolean, timeoutMs?: number) => Promise<any>;
3282
+ /**
3283
+ * Dispatch a synthetic DOM event through the mock DOM source so the component's real intent
3284
+ * streams fire (DOM.click('.x'), DOM.select('.x').events('click'), .value(), .data()).
3285
+ * Targets the first rendered element matching `selector` and bubbles within its isolation scope.
3286
+ * Selectors match the rendered tree like the real DOM: tag, .class, #id, [attr], [attr="v"]
3287
+ * (^= $= *= ~=), :first-child, :last-child, :only-child, :nth-child(an+b|odd|even),
3288
+ * :nth-last-child(), :first/last/only/nth-of-type, :not(), descendant ' ' and child '>'
3289
+ * combinators, ',' lists. Other syntax (:has(), '+', '~', pseudo-elements...) throws.
3290
+ * If nothing matches yet, the event waits (up to 300ms, re-checked on every render) for a
3291
+ * matching element, then targets the first one. If none renders, the test fails with an
3292
+ * error naming the selector: it rejects the pending next()/waitForState()/settle(), or is
3293
+ * thrown by the next t.* call or dispose() (or by simulateEvent itself when nothing is
3294
+ * pending and the tree is quiet). Pass `{ allowMissing: true }` to drop the event with SYG103
3295
+ * instead. It is never sent to every listener with that selector string. It also waits until
3296
+ * the listeners it reaches are subscribed (a just-mounted child subscribes a few ms late).
3297
+ * `'document'` / `'body'` go to the DOM.select('document' | 'body') listeners.
3298
+ * simulateAction/simulateEvent calls are delivered in call order; a waiting event holds the
3299
+ * calls after it. Reports SYG104 (selector only matches inside a child component).
3300
+ */
3301
+ simulateEvent: (selector: string | AnyControl, eventType: string, eventInit?: SimulatedEventInit) => void;
3302
+ /**
3303
+ * Resolves once the component is subscribed (earlier simulate* calls are buffered and replayed).
3304
+ * Also a cursor: the first next() after `await t.ready()` also matches the states the replayed
3305
+ * calls produced, unless another t.* call came in between.
3306
+ */
3307
+ ready: () => Promise<void>;
3308
+ /**
3309
+ * Wait for a state that satisfies the predicate. Matches the recorded HISTORY too: a state
3310
+ * from before the call resolves it (e.g. `count === 0` right after a reset resolves at once
3311
+ * with the initial state). Resolves with the matching state once the whole tree (children
3312
+ * included) has rendered it. Use `next()` to wait for a new state.
3313
+ */
3314
+ waitForState: (predicate: (state: STATE) => boolean, timeoutMs?: number) => Promise<STATE>;
3315
+ /**
3316
+ * Wait for the next state emitted AFTER this call (right after `await t.ready()`: after the
3317
+ * component became ready) that satisfies the predicate (default: any state). Resolves with it
3318
+ * once the whole tree (children included) has rendered it; rejects after timeoutMs (default:
3319
+ * the timeoutMs option, 2000). The error names a model next() still scheduled, and a recorded
3320
+ * state that already matched. With `{ dom: 'real' }`, a next() right after another wait (no
3321
+ * input in between) starts after the state that wait resolved with, so
3322
+ * `await t.next(a); await t.next(b)` sees a `b` that arrived while the DOM showed `a`.
3323
+ */
3324
+ next: (predicate?: (state: STATE) => boolean, timeoutMs?: number) => Promise<STATE>;
3325
+ /**
3326
+ * Resolves once nothing is pending: the component is ready, no simulated input is waiting,
3327
+ * and nothing in the tree has rendered, reduced or changed state for settleMs (default 20,
3328
+ * longer than a model next()'s default delay). Rejects after timeoutMs if it never calms down.
3329
+ */
3330
+ settle: (timeoutMs?: number) => Promise<void>;
656
3331
  /** Collected state values — grows as new states are emitted */
657
- states: any[];
658
- /** Tear down the component and clean up all listeners */
3332
+ states: STATE[];
3333
+ /**
3334
+ * PLAN-4 GS-10: every action the rendered tree ran (children and Collection items included),
3335
+ * in order, as `{ type, data, component, instance, sinks, cause, at }`. Live array.
3336
+ */
3337
+ actions: TestAction[];
3338
+ /**
3339
+ * PLAN-4 GS-10: the first action whose resulting root state matches the predicate, with that
3340
+ * state and its STATE reducer; undefined when none did.
3341
+ */
3342
+ explain: (predicate: (state: STATE) => boolean) => ExplainedAction<STATE> | undefined;
3343
+ /** The latest recorded state (`t.states.at(-1)`; undefined before the first one), calculated fields current. Read-only */
3344
+ readonly state: STATE;
3345
+ /** Live array of values emitted on a sink (EVENTS as {type, data}, PARENT unwrapped, custom sinks of any component in the tree) */
3346
+ sinkValues: (sinkName: string) => any[];
3347
+ /**
3348
+ * Live array of the requests a sink was sent, as objects: a string request is `{ url }`, a
3349
+ * resource fetch `{ url, ...request, resource: name }`. Never the `{ abort }` commands,
3350
+ * `{ resources }` declarations or `{ refresh }` commands (sinkValues has every value, as sent)
3351
+ */
3352
+ requests: (sinkName: string) => any[];
3353
+ /**
3354
+ * Answer a pending request on a driverless sink/source (e.g. `HTTP` with no
3355
+ * `drivers: { HTTP }`, which runs makeFetchDriver over an in-memory fetch) with a response whose
3356
+ * body is `value` (JSON; text for a string): a request with reply actions (`ok: 'LOADED'`) gets
3357
+ * the parsed body as its `LOADED` action, on exactly the component that sent it; a plain one gets
3358
+ * `{ category, value, status: 200, request }` on `HTTP.select(category)`.
3359
+ * Which request (the newest pending one that matches): `target` is an `ok`/`error` action
3360
+ * name, key, category, resource name or URL (`'LOADED'`); a partial request compared by value
3361
+ * with its t.requests form (`{ url: '/a' }`,
3362
+ * the constant the model returns); a predicate `(request) => boolean`; or FakeReplyOptions.
3363
+ * Nothing: the newest pending request. Requests answered, superseded by `latest: true`,
3364
+ * aborted, or whose component is gone aren't pending.
3365
+ * Throws at the call when nothing matching is pending, unless simulateEvent/simulateAction/
3366
+ * respond/fail calls are still queued before it or the component isn't ready yet: then it is
3367
+ * delivered after them, waiting up to 1s (half of timeoutMs if lower) for the request.
3368
+ * Resolves once the reply has been reduced and the tree rendered; rejects (and, if not
3369
+ * awaited, fails the next wait) when no request comes or nothing receives a plain reply.
3370
+ */
3371
+ respond: (sinkName: string, value: any, target?: string | FakeReplyOptions | Record<string, any> | ((request: any) => boolean)) => Promise<void>;
3372
+ /**
3373
+ * Fail a pending request (chosen as in respond): one with reply actions (`error: 'FAILED'`) gets
3374
+ * `{ error, request, status?, body? }` as its `FAILED` action, on its sender; a plain one
3375
+ * `{ error, category, request, status, body }` on `HTTP.errors(category)`. `error`: an HTTP
3376
+ * status (an error response: 404 → the driver's Error 'HTTP 404: url', status 404), or an
3377
+ * Error / message (a network failure: no status).
3378
+ */
3379
+ fail: (sinkName: string, error: any, target?: string | FakeReplyOptions | Record<string, any> | ((request: any) => boolean)) => Promise<void>;
3380
+ /**
3381
+ * A driverless sink that gets `{ connections }` / `{ to }` values (e.g. `WS` with no
3382
+ * `drivers: { WS }`) is a fake makeSocketDriver: diffed per component and connection name,
3383
+ * `open`/`message`/`close`/`error` as reply actions of the sender (no `close` for closes the
3384
+ * app makes), other events on `WS.select(name)`, shared by URL, reconnect per the spec on
3385
+ * the test's timers (fake timers included). The connections declared now, in order.
3386
+ * t.open / t.push / t.drop throw at the call when no connection matches (unless input is still
3387
+ * queued before them, as for respond) and resolve once the result has been reduced and rendered.
3388
+ */
3389
+ connections: (sinkName: string) => FakeConnection[];
3390
+ /** PLAN-3 5-3: the cache entries of an HTTP fake (`renderComponent(C, { http: { cache: queryCache() } })`) */
3391
+ cache: (sinkName: string) => FakeCacheEntry[];
3392
+ /** PLAN-3 5-3: the window regains focus (queued like simulate*): stale mounted resources refetch (cache on) */
3393
+ focus: () => void;
3394
+ /** PLAN-3 5-3: the browser comes back online (queued like simulate*): stale mounted resources refetch (cache on) */
3395
+ online: () => void;
3396
+ /** Complete the open of connecting connection(s) (`autoConnect: false`, or a pending retry): `open` fires with `{ reconnected }` */
3397
+ open: (sinkName: string, target?: FakeConnectionTarget) => Promise<void>;
3398
+ /**
3399
+ * The server sends `data` (objects as JSON text) on open connection(s): `message` fires with
3400
+ * the data, JSON-parsed when it parses. `{ event, connection? }` sends an SSE named event.
3401
+ */
3402
+ push: (sinkName: string, data: any, target?: FakeConnectionTarget | { event?: string; connection?: FakeConnectionTarget }) => Promise<void>;
3403
+ /**
3404
+ * Connection(s) close without the app closing them: `close` fires with `{ code, reason,
3405
+ * willReconnect }` (default `{ code: 1006, reason: '' }`; a connecting one fails to open: `error`
3406
+ * first), then the fake reconnects per the spec's `reconnect`.
3407
+ */
3408
+ drop: (sinkName: string, close?: { code?: number; reason?: string } | FakeConnectionTarget, target?: FakeConnectionTarget) => Promise<void>;
3409
+ /**
3410
+ * Live array of the `{ to, json | text | binary }` values sent (`to`: only those to that connection).
3411
+ * For the router fake's sink (`t.sent('ROUTER')`): the commands the components sent
3412
+ * (`{ to, params }`, `{ back: true }`, `{ block }`...), not the `route` declarations
3413
+ */
3414
+ sent: (sinkName: string, to?: string) => any[];
3415
+ /**
3416
+ * Router fake (`renderComponent(App, { router })`): navigate as a click on a link with this
3417
+ * href (`'/tasks/2'`), or as the command `{ to: 'task', params: { id: 2 }, query?, hash?,
3418
+ * replace? }`. Goes through `{ block }` like the real thing. Throws at the call for an unknown
3419
+ * route name, a missing param or another origin; resolves once the route has been reduced
3420
+ * and the tree rendered.
3421
+ */
3422
+ navigate: (target: string | { to: string; params?: Record<string, string | number>; query?: Record<string, any>; hash?: string; replace?: boolean }) => Promise<void>;
3423
+ /** Router fake: the browser's back button (popstate a task later; a block undoes it). Throws with no entry to go back to */
3424
+ back: () => Promise<void>;
3425
+ /** Router fake: the browser's forward button. Throws with no entry to go forward to */
3426
+ forward: () => Promise<void>;
3427
+ /** Router fake: the in-memory location: `path` (pathname), `search` ('?tab=x' or ''), `hash` ('#c' or ''), `href` */
3428
+ readonly location: { path: string; search: string; hash: string; href: string };
3429
+ /**
3430
+ * HEAD fake (no HEAD driver passed): the head the components declare now (`head` statics and
3431
+ * HEAD sink values), merged as makeHeadDriver merges them: `{ title, meta: { name: content },
3432
+ * link: [{ rel, href }] }`, `titleTemplate` applied
3433
+ */
3434
+ head: () => { title: string | undefined; meta: Record<string, string>; link: Array<Record<string, any>> };
3435
+ /**
3436
+ * PLAN-4 GS-7: the timer fake's active timers, in start order (`{ name, every | after | frame,
3437
+ * action, background?, component }`): a fired `after`, a stopped one or an invalid spec is not
3438
+ * listed. The fake is the real makeTimerDriver(), so fake timers drive it
3439
+ * (`vi.useFakeTimers()`, then `await vi.advanceTimersByTimeAsync(ms)`). Throws when a driver
3440
+ * was passed for the timer sink.
3441
+ */
3442
+ timers: () => ActiveTimer[];
3443
+ /**
3444
+ * PLAN-5 B-3: the browser fake's controls (no browser driver passed): `await
3445
+ * t.browser.intersect('.cover', true)`, `await t.browser.media('(prefers-color-scheme: dark)',
3446
+ * true)`, `t.browser.active()`. Each input resolves once its actions are reduced and the tree
3447
+ * rendered. Throws when a driver was passed for the browser sink.
3448
+ */
3449
+ browser: BrowserFake;
3450
+ /**
3451
+ * PLAN-4 GS-5: the fake storage's entry for `key` (`{ version, state }`), undefined when none
3452
+ * (a raw string when what is stored isn't JSON). Pending persist() writes are flushed by t.settle().
3453
+ * A `format: 'plain'` entry is the stored keys: `t.storage<{ title: string }>('note-draft')`
3454
+ */
3455
+ storage: <ENTRY = PersistedEntry<STATE>>(key: string) => ENTRY | undefined;
3456
+ /** Live array of EVENTS sink emissions ({type, data}) */
3457
+ emitted: Array<{ type: string; data: any }>;
3458
+ /** Live array of diagnostics reported while rendered */
3459
+ diagnostics: Diagnostic[];
3460
+ /**
3461
+ * PLAN-4 GS-2: the element commands the tree's instances sent on `ELEMENT`, one entry per command
3462
+ * (arrays flattened), as sent: `expect(t.commands('ELEMENT')).toEqual([{ focus: Email }])`; a
3463
+ * `sygnal/ui` dialog's or popover's command with its selector: `{ showModal: '.profile' }`. The
3464
+ * mock DOM records them (and reports SYG640/SYG641); `dom: 'real'` also runs them (jsdom gets
3465
+ * `<dialog>` show/showModal/close, the popover methods and a no-op scrollIntoView). Another sink
3466
+ * name gives its sinkValues.
3467
+ */
3468
+ commands: {
3469
+ (sinkName?: 'ELEMENT'): ElementCommand[];
3470
+ (sinkName: string): any[];
3471
+ };
3472
+ /** Throws (with the formatted texts) if any warn/error diagnostics were collected */
3473
+ expectNoDiagnostics: () => void;
3474
+ /**
3475
+ * Latest rendered VNode serialized to HTML like innerHTML (text escapes only & < >, so
3476
+ * `Couldn't`, not `&#39;`). Throws if called before the first render: wait
3477
+ * with `await t.ready()` (or `t.next(...)`) first. '' for a component that renders nothing.
3478
+ */
3479
+ html: () => string;
3480
+ /** Tear down the component, clean up listeners and restore the diagnostics mode */
659
3481
  dispose: () => void;
3482
+ /**
3483
+ * The app graph of the rendered tree (components, actions, selectors with the mock DOM's
3484
+ * match / isolation results, EVENTS, diagnostics). Throws unless 'sygnal/diagnostics' is loaded.
3485
+ * `{ actions: true }` (or a number: the last n) adds `recentActions` (GS-10).
3486
+ */
3487
+ inspect: (options?: Pick<InspectOptions, 'actions'>) => InspectGraph;
3488
+ /** `{ dom: 'real' }`: the element the tree is mounted in (removed by dispose()); otherwise null */
3489
+ container: Element | null;
3490
+ /**
3491
+ * The first element matching a selector in the rendered tree (Portal content included), or
3492
+ * null: `expect(t.query('input[name="plan"]:checked').value).toBe('team')`. With `dom: 'real'`
3493
+ * a real element (any CSS selector); with the mock DOM a read-only snapshot of what the view
3494
+ * rendered (simulateEvent's selectors plus `:checked`/`:disabled`/`:enabled`; textContent,
3495
+ * value, checked, disabled, getAttribute, classList, dataset, querySelector, closest...; no
3496
+ * focus()). Throws before the first render (`await t.ready()` first). Right after
3497
+ * `await t.next(pred)` / `waitForState` / `settle()` / `ready()` it shows the state the wait resolved with.
3498
+ */
3499
+ query: {
3500
+ (selector: string): Element | null;
3501
+ /** A control's element (`t.query(Draft)` is an HTMLInputElement for an 'input' control), or null */
3502
+ <CONTROL extends AnyControl>(control: CONTROL): ControlElementOf<CONTROL> | null;
3503
+ };
3504
+ /** Every element matching a selector (or a control) in the rendered tree (Portals included), as query() */
3505
+ queryAll: {
3506
+ (selector: string): Element[];
3507
+ <CONTROL extends AnyControl>(control: CONTROL): Array<ControlElementOf<CONTROL>>;
3508
+ };
3509
+ /**
3510
+ * A widget's host (PLAN-5 W-1), by selector (`'.due'`) or control. `props`: what the view
3511
+ * passed the widget (mock DOM: the rendered host's; `dom: 'real'`: the mounted widget's).
3512
+ * `instance` (`dom: 'real'`): what `mount` returned. `emit(name, detail)`: the CustomEvent the
3513
+ * widget's dispatch() sends, sent like `simulateEvent`. Reading `props`/`instance` throws
3514
+ * when no widget host matches.
3515
+ */
3516
+ widget: {
3517
+ <P = any, I = any>(selector: string): WidgetHandle<P, I>;
3518
+ <CONTROL extends AnyControl>(control: CONTROL): WidgetHandle<CONTROL extends { readonly [CONTROL]: { props: infer P } } ? P : any>;
3519
+ };
660
3520
  }
661
3521
 
662
- export function renderComponent(componentDef: any, options?: RenderOptions): RenderResult
3522
+ /** `t.widget(target)` (PLAN-5 W-1) */
3523
+ export interface WidgetHandle<P = any, I = any> {
3524
+ readonly props: P;
3525
+ readonly instance: I | undefined;
3526
+ emit(name: string, detail?: unknown): void;
3527
+ }
3528
+
3529
+ /**
3530
+ * A component renderComponent() accepts. STATE is inferred from its view's `state` (a
3531
+ * `Component<State, ...>` annotation, or a typed `({ state }: { state: State })` parameter;
3532
+ * calculated fields included), else from its `initialState` (INITIAL).
3533
+ */
3534
+ export type RenderableComponent<STATE = any, INITIAL = STATE> =
3535
+ // a method signature: parameters are compared bivariantly, so views with their own required
3536
+ // props are accepted
3537
+ & { view(props: { state: STATE }, state: STATE, ...rest: any[]): any }['view']
3538
+ & { initialState?: INITIAL }
3539
+
3540
+ /**
3541
+ * STATE, or INITIAL when STATE is unknown (an untyped view with a typed `initialState`). `never`
3542
+ * (a generic call inlined as the argument, `renderComponent(defineComponent({...}))`) becomes `any`.
3543
+ */
3544
+ type RenderedState<STATE, INITIAL> =
3545
+ [STATE] extends [never] ? any
3546
+ : 0 extends (1 & STATE) ? AnyIfNever<INITIAL>
3547
+ : STATE
3548
+
3549
+ /**
3550
+ * Render a component in tests (mock DOM, fake drivers). The handle's state is typed from the
3551
+ * component, so `await t.next(s => s.count > 0)` needs no annotation. Pass the state type
3552
+ * explicitly for an untyped component: `renderComponent<State>(Counter)`.
3553
+ */
3554
+ export function renderComponent<STATE = any, INITIAL = STATE>(
3555
+ componentDef: RenderableComponent<STATE, INITIAL>,
3556
+ options?: RenderOptions
3557
+ ): RenderResult<RenderedState<STATE, INITIAL>>
663
3558
 
664
3559
  export interface RenderToStringOptions {
665
3560
  /** Initial state for the root component */
@@ -674,10 +3569,31 @@ export interface RenderToStringOptions {
674
3569
  * When a string, uses that as the variable name.
675
3570
  */
676
3571
  hydrateState?: boolean | string
3572
+ /** An array that receives each rendered component's `head` static value; pass it to `renderHead()` */
3573
+ head?: any[]
3574
+ /**
3575
+ * PLAN-3 5-5 (H-7): a seeded `queryCache()` the components' `resources` render from: a cached
3576
+ * entry as `{ status: 'success', data }`, any other request as `{ status: 'loading' }` (nothing
3577
+ * is fetched during SSR)
3578
+ */
3579
+ cache?: QueryCache
3580
+ /** The app-level error hook, as run()'s `onError`: phase 'view' only (PLAN-4 GS-11) */
3581
+ onError?: AppErrorHook
3582
+ /** The root of the `uid()` strings (default 'u'), as run()'s `uid`: the same value on both sides */
3583
+ uid?: string
677
3584
  }
678
3585
 
679
3586
  /**
680
3587
  * Render a Sygnal component to an HTML string on the server.
3588
+ *
3589
+ * ```ts
3590
+ * renderToString(App, { state: { count: 0 } })
3591
+ * // → '<div data-sygnal-ssr=""><h1>Count: 0</h1></div>'
3592
+ * ```
3593
+ *
3594
+ * The root element carries an empty `data-sygnal-ssr` attribute, which marks the markup as
3595
+ * Sygnal's server HTML (persist() under plain run() then restores after the first render); the
3596
+ * first client render removes it.
681
3597
  */
682
3598
  export function renderToString(componentDef: any, options?: RenderToStringOptions): string
683
3599
 
@@ -688,6 +3604,9 @@ export { default as throttle } from 'xstream/extra/throttle.js'
688
3604
  export { default as delay } from 'xstream/extra/delay.js'
689
3605
  export { default as dropRepeats } from 'xstream/extra/dropRepeats.js'
690
3606
  export { default as sampleCombine } from 'xstream/extra/sampleCombine.js'
3607
+ export { default as flattenConcurrently } from 'xstream/extra/flattenConcurrently.js'
3608
+ export { default as flattenSequentially } from 'xstream/extra/flattenSequentially.js'
3609
+ export { default as concat } from 'xstream/extra/concat.js'
691
3610
 
692
3611
  export * from './cycle/dom/index'
693
3612
  export type { MemoryStream, Stream }
@@ -715,5 +3634,11 @@ declare global {
715
3634
  interface ElementChildrenAttribute {
716
3635
  children: {};
717
3636
  }
3637
+
3638
+ /**
3639
+ * A component's view receives `state: STATE` and `context`, but in JSX the parent passes
3640
+ * an optional `state` slice name or lens (`<Editor state="editor" />`, `<Panel />`).
3641
+ */
3642
+ type LibraryManagedAttributes<C, P> = ElementProps<P>
718
3643
  }
719
3644
  }