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
@@ -0,0 +1,173 @@
1
+ <!-- Generated from docs/src/content/docs/guide by scripts/copy-guides.mjs; online: https://sygnal.js.org/guide/ -->
2
+ # Inputs, Labels and Focus
3
+
4
+ How Sygnal treats the value of a field, how to give labels and descriptions ids that stay unique, and how to focus a field when it appears. For forms with validation, see [Forms](./forms.md).
5
+
6
+ ## Controlled Inputs
7
+
8
+ An `<input>`, `<textarea>` or `<select>` with a `value` prop (or a checkbox/radio with `checked`) is **controlled**: on every render, Sygnal writes the value from your view into the element, like React does. That keeps the field in sync with state (clearing a field after "Add" works even when both happen in the same tick), but it means the field must update state as the user types. Otherwise any re-render resets what they typed.
9
+
10
+ A form-associated custom element (one whose class has `static formAssociated = true`, such as Web Awesome's `<wa-input>` or `<wa-rating>`) with a `value` or `checked` prop is controlled the same way.
11
+
12
+ To refuse what the user entered, return `ABORT` (or the state unchanged): the state stays as it was, and because the action came from an `input` or `change` event, the component still renders, so the field shows the state's value again. A digits-only field:
13
+
14
+ ```jsx live
15
+ import { ABORT } from 'sygnal'
16
+
17
+ function Pin({ state }) {
18
+ return <input className="pin" aria-label="PIN" inputMode="numeric" value={state.pin} />
19
+ }
20
+
21
+ Pin.initialState = { pin: '' }
22
+ Pin.intent = ({ DOM }) => ({ PIN: DOM.input('.pin').value() })
23
+ Pin.model = {
24
+ PIN: (state, pin) => /^\d{0,6}$/.test(pin) ? { ...state, pin } : ABORT,
25
+ }
26
+ ```
27
+
28
+ This applies only while the `input` or `change` event is being handled (an intent that delays the action, with `debounce` for example, gets no extra render). `ABORT` from any other action (a click, a timer, a reply) still renders nothing.
29
+
30
+ Pick one of two patterns:
31
+
32
+ **Controlled**: bind `value` to state and update state on `input`:
33
+
34
+ ```jsx live
35
+ import { ABORT } from 'sygnal'
36
+
37
+ function NewTodo({ state }) {
38
+ return (
39
+ <div>
40
+ <input className="new-todo" aria-label="New todo" value={state.draft} />
41
+ <button className="add">Add</button>
42
+ <ul>{state.items.map((item) => <li>{item}</li>)}</ul>
43
+ </div>
44
+ )
45
+ }
46
+
47
+ NewTodo.initialState = { draft: '', items: [] }
48
+
49
+ NewTodo.intent = ({ DOM }) => ({
50
+ DRAFT: DOM.input('.new-todo').value(),
51
+ ADD: DOM.click('.add'),
52
+ })
53
+
54
+ NewTodo.model = {
55
+ DRAFT: (state, draft) => ({ ...state, draft }),
56
+ ADD: (state) => state.draft.trim()
57
+ ? { ...state, items: [...state.items, state.draft.trim()], draft: '' }
58
+ : ABORT,
59
+ }
60
+ ```
61
+
62
+ **Uncontrolled**: leave out `value`, and read the element's value from the event when you need it (on blur, Enter or submit), or with [`processForm()`](./forms-reference.md#processform):
63
+
64
+ ```jsx live
65
+ import { ABORT } from 'sygnal'
66
+
67
+ function Rename({ state }) {
68
+ return <input className="rename" aria-label="Title" placeholder={state.title} />
69
+ }
70
+
71
+ Rename.initialState = { title: 'Untitled' }
72
+
73
+ Rename.intent = ({ DOM }) => ({
74
+ RENAME: DOM.keydown('.rename').filter(e => e.key === 'Enter').map(e => e.target.value),
75
+ })
76
+
77
+ Rename.model = {
78
+ RENAME: (state, title) => title ? { ...state, title } : ABORT,
79
+ }
80
+ ```
81
+
82
+ What doesn't work is a bound `value` with no `input`/`change` listener, for example a "save on blur" field that only listens to `blur`: the first re-render while the user types puts the old value back. `sygnal-check` reports that as [SYG111](https://sygnal.js.org/reference/errors/#syg111). Literal values (`value=""`) are controlled too, so they reset the field on every render as well.
83
+
84
+ `value={null}` (or `checked={null}`) clears the field and keeps it controlled; leaving the prop out makes the field uncontrolled, so whatever the user typed stays.
85
+
86
+ ## Labels and ids: uid()
87
+
88
+ Every field needs a label ([SYG702](./accessibility.md#syg702-form-field-without-a-label)). Wrapping the field in a `<label>` needs no id. When the label sits elsewhere, or a hint is attached with `aria-describedby`, the elements need ids, and a literal `id="email"` is repeated as soon as the component renders twice. Use the `uid` view prop instead:
89
+
90
+ ```jsx
91
+ function Signup({ state, uid }) {
92
+ return (
93
+ <form className="signup">
94
+ <label for={uid('email')}>Email</label>
95
+ <input id={uid('email')} type="email" className="email" value={state.email} aria-describedby={uid('email-help')} />
96
+ <p id={uid('email-help')}>We only use it to sign you in.</p>
97
+ </form>
98
+ )
99
+ }
100
+
101
+ Signup.initialState = { email: '' }
102
+ Signup.intent = ({ DOM }) => ({ EMAIL: DOM.input('.email').value() })
103
+ Signup.model = { EMAIL: (state, email) => ({ ...state, email }) }
104
+ ```
105
+
106
+ `uid()` returns an id for this component instance, and `uid('email')` one derived from it (for example `u-email` at the root, longer further down the tree). Ids come from the instance's position in the tree and its Collection item key, never from a counter, so they:
107
+
108
+ - differ between two instances of the component, and between Collection items;
109
+ - stay the same across renders, and move with their item when a Collection is reordered;
110
+ - are the same on the server and after hydration ([SSR](./ssr.md#stable-ids-uid)).
111
+
112
+ `uid` is also on the reducers' `props` argument. It is a reserved prop: a parent can't pass its own `uid` to a child ([SYG106](https://sygnal.js.org/reference/errors/#syg106)). `sygnal-check` matches `for={uid('email')}` with `id={uid('email')}` ([SYG708](./accessibility.md#syg708-label-or-aria-reference-to-an-id-that-isnt-rendered)).
113
+
114
+ ## Focus Management
115
+
116
+ Sygnal components are pure functions — they never touch real DOM elements. But web apps frequently need to focus an element programmatically, for example when an input appears for inline editing.
117
+
118
+ The `autoFocus` and `autoSelect` JSX props handle this declaratively. No imperative code in your view, no drivers, no hooks.
119
+
120
+ ### autoFocus
121
+
122
+ Add `autoFocus={true}` to any element. When that element enters the DOM, it receives focus automatically:
123
+
124
+ ```jsx
125
+ function SearchBar({ state }) {
126
+ return (
127
+ <div>
128
+ {state.isOpen &&
129
+ <input autoFocus={true} className="search-input" placeholder="Search..." />
130
+ }
131
+ </div>
132
+ )
133
+ }
134
+ ```
135
+
136
+ ### autoSelect
137
+
138
+ Add `autoSelect={true}` alongside `autoFocus` to select all text in the element after focusing. This is ideal for edit-in-place patterns where the user typically wants to replace the existing value:
139
+
140
+ ```jsx live
141
+ function EditableTitle({ state }) {
142
+ return (
143
+ <div>
144
+ {state.isEditing
145
+ ? <input autoFocus={true} autoSelect={true} value={state.draft} className="title-input" aria-label="Title" />
146
+ : <h2 className="title">{state.title}</h2>
147
+ }
148
+ </div>
149
+ )
150
+ }
151
+
152
+ EditableTitle.initialState = { title: 'Double-click to rename', isEditing: false, draft: '' }
153
+
154
+ EditableTitle.intent = ({ DOM }) => ({
155
+ EDIT: DOM.dblclick('.title'),
156
+ DRAFT: DOM.input('.title-input').value(),
157
+ SAVE: DOM.blur('.title-input'),
158
+ })
159
+
160
+ EditableTitle.model = {
161
+ EDIT: (state) => ({ ...state, isEditing: true, draft: state.title }),
162
+ DRAFT: (state, draft) => ({ ...state, draft }),
163
+ SAVE: (state) => ({ ...state, isEditing: false, title: state.draft }),
164
+ }
165
+ ```
166
+
167
+ When the user double-clicks to edit, the input appears focused with all text selected — ready to type a replacement.
168
+
169
+ ### How It Works
170
+
171
+ These props are intercepted by the JSX pragma before they reach the DOM. Under the hood, a snabbdom `insert` hook calls `.focus()` (and optionally `.select()`) when the element is first inserted. The props are never passed to the actual DOM element.
172
+
173
+ If you also set a manual `hook={{ insert: fn }}` on the same element, both hooks run — yours first, then the focus behavior.
@@ -0,0 +1,245 @@
1
+ <!-- Generated from docs/src/content/docs/guide by scripts/copy-guides.mjs; online: https://sygnal.js.org/guide/ -->
2
+ # Persistence
3
+
4
+ `persist()` saves part of the app's state in the browser's storage and puts it back when the app starts again. You declare it once, on the root component: which keys to keep, under which storage key, and in which version of their shape.
5
+
6
+ ```jsx live
7
+ // TodoApp.jsx
8
+ import { ABORT, persist } from 'sygnal'
9
+
10
+ export function TodoApp({ state }) {
11
+ const shown = state.filter === 'open' ? state.todos.filter((todo) => !todo.done) : state.todos
12
+ return (
13
+ <main>
14
+ <label>New todo <input className="draft" value={state.draft} /></label>
15
+ <button className="add">Add</button>
16
+ <button className="show-all">All</button>
17
+ <button className="show-open">Open</button>
18
+ <button className="start-over">Start over</button>
19
+ <ul>{shown.map((todo) => <li>{todo.title}</li>)}</ul>
20
+ </main>
21
+ )
22
+ }
23
+
24
+ TodoApp.initialState = { todos: [], filter: 'all', draft: '' }
25
+
26
+ TodoApp.intent = ({ DOM }) => ({
27
+ DRAFT: DOM.input('.draft').value(),
28
+ ADD: DOM.click('.add'),
29
+ SHOW_ALL: DOM.click('.show-all'),
30
+ SHOW_OPEN: DOM.click('.show-open'),
31
+ START_OVER: DOM.click('.start-over'),
32
+ })
33
+
34
+ TodoApp.model = {
35
+ DRAFT: (state, draft) => ({ ...state, draft }),
36
+ ADD: (state) => state.draft
37
+ ? { ...state, todos: [...state.todos, { title: state.draft, done: false }], draft: '' }
38
+ : ABORT,
39
+ SHOW_ALL: (state) => ({ ...state, filter: 'all' }),
40
+ SHOW_OPEN: (state) => ({ ...state, filter: 'open' }),
41
+ START_OVER: {
42
+ STATE: (state) => ({ ...state, todos: [], filter: 'all' }),
43
+ PERSIST: { clear: true },
44
+ },
45
+ }
46
+
47
+ // Version 1 saved { items: ['milk', ...] }; version 2 saves { todos: [{ title, done }], filter }
48
+ TodoApp.persist = persist({
49
+ key: 'todo-app',
50
+ pick: ['todos', 'filter'],
51
+ version: 2,
52
+ migrate: (old, fromVersion) => fromVersion === 1
53
+ ? { todos: old.items.map((title) => ({ title, done: false })), filter: 'all' }
54
+ : undefined,
55
+ })
56
+ ```
57
+
58
+ Reload the page and the todos and the filter are still there; the half-typed `draft` is not, because it isn't picked. **Start over** empties the list and removes the saved copy.
59
+
60
+ ## What is saved
61
+
62
+ The stored value is JSON, `{ version, state }`, under `key`:
63
+
64
+ ```json
65
+ {"version":2,"state":{"todos":[{"title":"milk","done":false}],"filter":"all"}}
66
+ ```
67
+
68
+ `state` has the top-level keys listed in `pick`, or, with `omit` instead, every key except those. [Calculated fields](https://sygnal.js.org/guide/calculated-fields/) are never saved: they are computed again from the restored state. Pick what has to survive a reload (the user's data, a chosen view) and leave out what doesn't (drafts, loading flags, server data you fetch again, an undo history). A [behavior](./behaviors.md#persisted-state-and-undo-steps) of the root component whose slice is UI state (`persist: false`, as [`sortable`](./drag-and-drop.md)'s drag state) is never saved or restored, even when `pick` names it. A sub-component's or Collection item's behavior slice is part of the data it lives in and is saved with it.
69
+
70
+ Values must survive `JSON.stringify`: plain objects, arrays, strings, numbers, booleans and `null`. A `Date` comes back as a string, and a `Map` or `Set` as `{}`.
71
+
72
+ ### A plain format
73
+
74
+ When something else reads or writes the same entry (an older version of the app, another script, a spec that fixes the format), `format: 'plain'` stores the picked keys themselves, with no envelope:
75
+
76
+ ```jsx
77
+ Note.persist = persist({ key: 'note-draft', pick: ['title', 'body'], format: 'plain' })
78
+ ```
79
+
80
+ ```json
81
+ {"title":"Groceries","body":"milk, eggs"}
82
+ ```
83
+
84
+ A plain entry has no version, so `version` and `migrate` don't apply: TypeScript rejects them with `format: 'plain'`, and they are ignored at runtime. A stored value that isn't an object (an array, a string, a number) is ignored, and the app starts from `initialState`. Everything else (`pick`/`omit`, `sync`, `storage`, clearing, the restore and the debounced writes) works as with the default `format: 'versioned'`.
85
+
86
+ ## When it is restored
87
+
88
+ When the app starts, the root component reads the stored entry synchronously, before its first state, and merges the saved keys into `initialState`. The first render already shows the saved todos, and the `INITIALIZE` action carries them: there is no separate "load" step and no flash of the empty list.
89
+
90
+ `persist` works on the **root component** only, the one passed to `run()`. It saves and restores the app's whole state tree, and a sub-component's state is a slice of it: to save a child's state, pick the key the root keeps it under. On any other component the static is ignored, which is [SYG224](https://sygnal.js.org/reference/errors/#syg224) in development and in `sygnal-check`.
91
+
92
+ :::note[Where persist works]
93
+ On any component that is its app's root:
94
+
95
+ - the component you pass to `run()`
96
+ - the component you pass to [`renderComponent`](#testing) in tests
97
+ - an [Astro](https://sygnal.js.org/integration/astro/) island (each island is its own app)
98
+ - a [Vike](https://sygnal.js.org/integration/vike/) page with no Layout or Wrapper
99
+
100
+ Not (yet) on a Vike page, Layout or Wrapper rendered in a Layout/Wrapper shell: the shell is the root there, so `persist` is ignored and SYG224 says so. Save that state yourself: write it with [`STATE.watch`](https://sygnal.js.org/guide/intent/#reacting-to-state-changes-statewatch) and read it back in `BOOTSTRAP`.
101
+ :::
102
+
103
+ ## When it is saved
104
+
105
+ A write happens after the state stops changing for `debounceMs` (100 ms by default), so typing or a burst of actions makes one write. A pending write is also made at once when the page is hidden for good (the `pagehide` event: closing the tab, navigating away) and when the app is disposed. Nothing is written while the picked keys are unchanged, and the initial state is not written back.
106
+
107
+ ## Versions and migrate
108
+
109
+ `version` (default 1) is saved with the state (not with `format: 'plain'`). When the stored entry has another version, `migrate(old, fromVersion)` turns the old `state` into this version's keys; the result is merged into `initialState` like any restore and saved under the new version on the next write. When `migrate` returns nothing (`undefined` or `null`), or there is no `migrate`, the stored entry is ignored and the app starts from `initialState`.
110
+
111
+ Bump `version` whenever the shape of a picked key changes, and keep the `migrate` branches for the versions your users may still have.
112
+
113
+ ## Clearing
114
+
115
+ `PERSIST: { clear: true }` in a model entry removes the stored copy. Like other sinks it can be a function of `(state, data)` that returns the command, or `ABORT` to do nothing:
116
+
117
+ ```jsx
118
+ Account.model = {
119
+ LOG_OUT: {
120
+ STATE: (state) => ({ ...state, user: null, cart: [] }),
121
+ PERSIST: { clear: true },
122
+ },
123
+ }
124
+ ```
125
+
126
+ The state the same action produces isn't saved; the next change is. `PERSIST` needs no driver.
127
+
128
+ ## Several tabs: sync
129
+
130
+ With `sync: true`, a write made in another tab (or window) of the same site is applied in this one, as a built-in `RESTORE` action whose data is the saved keys. The default `RESTORE` merges them into the state; a `RESTORE` entry in your model replaces it:
131
+
132
+ ```jsx
133
+ TodoApp.persist = persist({ key: 'todo-app', pick: ['todos', 'filter'], version: 2, migrate, sync: true })
134
+ ```
135
+
136
+ Clearing in another tab doesn't reset this one: it keeps its state and saves it again on its next change.
137
+
138
+ ## Storage
139
+
140
+ `storage` is `'local'` (`localStorage`, the default), `'session'` (`sessionStorage`: one tab, until it closes) or an object with synchronous `getItem`, `setItem` and `removeItem`. With `sync`, an object can also have `subscribe(fn)`: call `fn(key, newValue)` when another writer changes a key, and return a function that unsubscribes.
141
+
142
+ ```jsx
143
+ const memory = new Map()
144
+
145
+ export const memoryStorage = {
146
+ getItem: (key) => memory.get(key) ?? null,
147
+ setItem: (key, value) => { memory.set(key, value) },
148
+ removeItem: (key) => { memory.delete(key) },
149
+ }
150
+ ```
151
+
152
+ Asynchronous storages (IndexedDB, a server) don't fit the synchronous restore. Load from those with a request in `BOOTSTRAP` and save with [`STATE.watch`](https://sygnal.js.org/guide/intent/#reacting-to-state-changes-statewatch) instead.
153
+
154
+ During server rendering nothing is read or written: `renderToString` runs views only.
155
+
156
+ ## Server rendering: hydrate
157
+
158
+ When the client starts from server-rendered HTML (the [`hydrateState`](./ssr.md) state), restoring before the first render would make that render differ from the server's markup. So when the app hydrates, the first render uses the server's state, and the saved keys follow in a `RESTORE` action once it is on the page. The app hydrates when:
159
+
160
+ - `run()`'s mount point starts with markup from [`renderToString()`](./ssr.md): its root element carries a `data-sygnal-ssr` attribute, which the first client render removes. Other markup in the mount point, such as a client-only app's loading placeholder, isn't server markup: the saved keys are part of the first render
161
+ - an [Astro](https://sygnal.js.org/integration/astro/) island was rendered on the server (not `client:only`)
162
+ - a [Vike](https://sygnal.js.org/integration/vike/) page hydrates its server HTML (not a client-side navigation)
163
+
164
+ ```jsx
165
+ App.persist = persist({ key: 'app', pick: ['theme'] })
166
+ App.initialState = window.__SYGNAL_STATE__ || App.initialState
167
+
168
+ run(App, {}, { mountPoint: '#app' })
169
+ ```
170
+
171
+ The `hydrate` option overrides the detection: `hydrate: true` always restores after the first render, `hydrate: false` always before it. Use `hydrate: true` when the server's HTML doesn't come from `renderToString()` (another renderer, or markup you post-process without the attribute).
172
+
173
+ ## With undo
174
+
175
+ Keep an [undo history](./undo.md) out of the saved state: pick the document, not `history`. After a reload the document is back and the history starts empty.
176
+
177
+ ## Diagnostics
178
+
179
+ | Code | When |
180
+ |---|---|
181
+ | [SYG642](https://sygnal.js.org/reference/errors/#syg642) (warning) | The stored entry isn't JSON, `migrate` threw, or a write failed (the storage is full or blocked). The app continues: from `initialState` after a failed restore, unsaved after a failed write (retried at the next state change). Printed in production too, once per kind (restore, save, clear): a storage that refuses every write warns once, and again only after a save has worked |
182
+ | [SYG223](https://sygnal.js.org/reference/errors/#syg223) (warning) | A `pick` or `omit` key that isn't a key of `initialState` (a typo) |
183
+ | [SYG224](https://sygnal.js.org/reference/errors/#syg224) (error) | `persist` on a component that isn't the root |
184
+
185
+ ## Testing
186
+
187
+ `renderComponent` gives a persisting root a fake storage. The `storage` option fills it (key to stored entry, or a raw string), `t.storage(key)` reads an entry, and `t.settle()` makes the pending writes:
188
+
189
+ ```jsx
190
+ // TodoApp.test.jsx
191
+ import { it, expect } from 'vitest'
192
+ import { renderComponent } from 'sygnal'
193
+ import { TodoApp } from './TodoApp.jsx'
194
+
195
+ it('restores version 1 todos and saves version 2', async () => {
196
+ const t = renderComponent(TodoApp, {
197
+ storage: { 'todo-app': { version: 1, state: { items: ['milk'] } } },
198
+ })
199
+ await t.ready()
200
+ expect(t.state.todos).toEqual([{ title: 'milk', done: false }])
201
+
202
+ t.simulateAction('DRAFT', 'eggs')
203
+ t.simulateAction('ADD')
204
+ await t.settle()
205
+ expect(t.storage('todo-app')).toEqual({
206
+ version: 2,
207
+ state: { todos: [{ title: 'milk', done: false }, { title: 'eggs', done: false }], filter: 'all' },
208
+ })
209
+ t.dispose()
210
+ })
211
+ ```
212
+
213
+ The object you pass is used as the storage, not copied: writes land in it, and two `renderComponent` calls given the same object share it, so `sync: true` can be tested with two instances. The fake serves `'local'` and `'session'` alike; a component whose `storage` is an object uses that object.
214
+
215
+ With `format: 'plain'` the entries are the stored keys themselves, both in the `storage` option and from `t.storage(key)`:
216
+
217
+ ```jsx
218
+ it('saves the draft as { title, body }', async () => {
219
+ const t = renderComponent(Note, { storage: { 'note-draft': { title: 'Groceries', body: '' } } })
220
+ await t.ready()
221
+ expect(t.state.title).toBe('Groceries')
222
+
223
+ t.simulateAction('BODY', 'milk')
224
+ await t.settle()
225
+ expect(t.storage('note-draft')).toEqual({ title: 'Groceries', body: 'milk' })
226
+ t.dispose()
227
+ })
228
+ ```
229
+
230
+ ## Options
231
+
232
+ | Option | Default | |
233
+ |---|---|---|
234
+ | `key` | (required) | The storage key |
235
+ | `pick` | all keys | The top-level state keys to save |
236
+ | `omit` | `[]` | The top-level state keys not to save (instead of `pick`) |
237
+ | `version` | `1` | Saved with the state; another stored version goes through `migrate` |
238
+ | `migrate` | none | `(old, fromVersion) => keys` for a stored entry of another version; nothing discards it |
239
+ | `format` | `'versioned'` | `'versioned'` stores `{ version, state }`; `'plain'` stores the picked keys themselves (no `version` / `migrate`) |
240
+ | `storage` | `'local'` | `'local'`, `'session'` or a synchronous `{ getItem, setItem, removeItem, subscribe? }` |
241
+ | `sync` | `false` | Apply other tabs' writes (a `RESTORE` action) |
242
+ | `hydrate` | detected | Restore after the first render (in `RESTORE`): `true` / `false` override the detection of server-rendered HTML |
243
+ | `debounceMs` | `100` | Wait this long without a state change before writing |
244
+
245
+ In TypeScript, `pick` and `omit` are checked against the root component's state keys.
@@ -0,0 +1,155 @@
1
+ <!-- Generated from docs/src/content/docs/recipes by scripts/copy-guides.mjs; online: https://sygnal.js.org/recipes/ -->
2
+ # Carousel (Embla)
3
+
4
+ [Embla Carousel](https://www.embla-carousel.com/) handles the hard parts of a carousel: dragging and swiping, snapping, momentum. It renders no buttons and no styles of its own. [`defineWidget`](../widgets.md) turns it into a JSX tag; the buttons are ordinary Sygnal markup that send the carousel [element commands](../element-commands.md), and the slide Embla settles on comes back as state.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install embla-carousel
10
+ ```
11
+
12
+ ## The widget
13
+
14
+ ```js live-file=./Carousel.js
15
+ // Carousel.js
16
+ import { defineWidget } from 'sygnal'
17
+ import EmblaCarousel from 'embla-carousel'
18
+
19
+ // Embla moves a track inside the viewport (the host): the widget builds the track from the props
20
+ function fill(el, photos) {
21
+ const track = document.createElement('div')
22
+ track.className = 'slides'
23
+ photos.forEach((photo, i) => {
24
+ const slide = document.createElement('div')
25
+ slide.className = 'slide'
26
+ slide.setAttribute('role', 'group')
27
+ slide.setAttribute('aria-roledescription', 'slide')
28
+ slide.setAttribute('aria-label', `${i + 1} of ${photos.length}`)
29
+ slide.append(Object.assign(document.createElement('img'), { src: photo.src, alt: photo.alt }))
30
+ track.append(slide)
31
+ })
32
+ el.replaceChildren(track)
33
+ }
34
+
35
+ export const Carousel = defineWidget({
36
+ name: 'Carousel',
37
+ mount: (el, props, dispatch) => {
38
+ fill(el, props.photos)
39
+ const embla = EmblaCarousel(el, { loop: false })
40
+ // reInit (new photos) can move the selection without a select event: report both
41
+ const report = () => dispatch('slide', embla.selectedScrollSnap())
42
+ embla.on('select', report).on('reInit', report)
43
+ return embla
44
+ },
45
+ update: (embla, props, el) => {
46
+ fill(el, props.photos)
47
+ embla.reInit()
48
+ },
49
+ unmount: (embla) => embla.destroy(),
50
+ events: ['slide'],
51
+ commands: {
52
+ prev: (embla) => embla.scrollPrev(),
53
+ next: (embla) => embla.scrollNext(),
54
+ goTo: (embla, { index }) => embla.scrollTo(index),
55
+ },
56
+ })
57
+ ```
58
+
59
+ Embla expects a viewport element with one child, the track, whose children are the slides. The host `<div>` is the viewport, and the widget builds the track itself, because Sygnal never renders inside a widget's host. `update` rebuilds it when the photos change and `reInit()` makes Embla measure the new slides; the instance stays the same.
60
+
61
+ ## Using it
62
+
63
+ ```jsx live
64
+ // Gallery.jsx
65
+ import { Carousel } from './Carousel.js'
66
+
67
+ export function Gallery({ state }) {
68
+ const last = state.photos.length - 1
69
+ return (
70
+ <section aria-roledescription="carousel" aria-label="Photos">
71
+ <Carousel className="photos" photos={state.photos} />
72
+ <button className="prev" aria-label="Previous photo" aria-disabled={state.index === 0}>‹</button>
73
+ <button className="next" aria-label="Next photo" aria-disabled={state.index === last}>›</button>
74
+ {state.photos.map((photo, i) => (
75
+ <button className="dot" data={{ index: i }} aria-label={`Show photo ${i + 1}`}
76
+ aria-current={i === state.index ? 'true' : undefined} />
77
+ ))}
78
+ <p className="where" aria-live="polite">Photo {state.index + 1} of {state.photos.length}</p>
79
+ </section>
80
+ )
81
+ }
82
+
83
+ Gallery.initialState = {
84
+ photos: [
85
+ { src: '/photos/harbour.jpg', alt: 'Boats in the harbour at dawn' },
86
+ { src: '/photos/market.jpg', alt: 'The fish market' },
87
+ { src: '/photos/cliffs.jpg', alt: 'Cliffs north of the town' },
88
+ ],
89
+ index: 0,
90
+ }
91
+
92
+ Gallery.intent = ({ DOM }) => ({
93
+ SLIDE: DOM.select('.photos').events('slide').detail(),
94
+ PREV: DOM.click('.prev'),
95
+ NEXT: DOM.click('.next'),
96
+ GO_TO: DOM.click('.dot').data('index', Number),
97
+ })
98
+
99
+ Gallery.model = {
100
+ SLIDE: (state, index) => ({ ...state, index }),
101
+ PREV: { ELEMENT: { prev: '.photos' } },
102
+ NEXT: { ELEMENT: { next: '.photos' } },
103
+ GO_TO: { ELEMENT: (state, index) => ({ goTo: '.photos', index }) },
104
+ }
105
+ ```
106
+
107
+ ```css live
108
+ .photos { overflow: hidden; }
109
+ .photos .slides { display: flex; }
110
+ .photos .slide { flex: 0 0 100%; min-width: 0; }
111
+ .photos img { display: block; width: 100%; height: auto; }
112
+ ```
113
+
114
+ The buttons don't change `index` themselves. They ask Embla to scroll, and `index` follows from the `slide` event, the same way it does after a swipe: Embla decides where the carousel stops, and the state always says where it is. The widget also reports the slide after `reInit()`: when the photos shrink below the current one, Embla moves to the last slide without a `select` event.
115
+
116
+ At the ends, Previous and Next are marked `aria-disabled` rather than `disabled`: a `disabled` button loses focus as it is pressed (keyboard users land on the page body), while an `aria-disabled` one keeps it, is announced as unavailable, and a press does nothing (Embla doesn't scroll past the ends). Style it with `[aria-disabled="true"]`.
117
+
118
+ ## Testing
119
+
120
+ ```jsx
121
+ // Gallery.test.jsx
122
+ import { test, expect } from 'vitest'
123
+ import { renderComponent } from 'sygnal'
124
+ import { Gallery } from './Gallery.jsx'
125
+
126
+ test('the buttons send commands, and the shown slide comes back as state', async () => {
127
+ const t = renderComponent(Gallery)
128
+ await t.ready()
129
+ t.simulateEvent('.next', 'click')
130
+ t.simulateEvent('.dot[data-index="2"]', 'click')
131
+ await t.settle()
132
+ expect(t.commands()).toEqual([{ next: '.photos' }, { goTo: '.photos', index: 2 }])
133
+
134
+ t.widget('.photos').dispatch('slide', 2)
135
+ await t.next((state) => state.index === 2)
136
+ expect(t.query('.where').textContent).toBe('Photo 3 of 3')
137
+ expect(t.query('.next').getAttribute('aria-disabled')).toBe('true')
138
+ expect(t.query('.prev').getAttribute('aria-disabled')).toBe('false')
139
+ t.dispose()
140
+ })
141
+ ```
142
+
143
+ Dragging needs layout, so test it in a real browser: with `renderComponent(Gallery, { dom: 'real' })` under Playwright, drag across `.photos` and `t.waitForState((state) => state.index === 1)`; `t.widget('.photos').instance` is the Embla API (`selectedScrollSnap()`, `slideNodes()`).
144
+
145
+ ## Size
146
+
147
+ Measured with Vite, minified and gzipped, Sygnal not included: **9 KB** for Embla and this recipe. `defineWidget` adds 1.1 KB for the first widget in an app.
148
+
149
+ ## Pitfalls
150
+
151
+ - **Build the slides in the widget.** Children passed to a widget tag are ignored, and markup rendered next to the host isn't inside the viewport. Pass the slides' data as a prop and build them in `mount`/`update`, as `fill` does.
152
+ - **`reInit()` after the slides change.** Embla measures the slides when it starts; without `reInit()` it keeps scrolling over the old ones. `reInit()` emits `reInit`, not `select`, so listen to both to keep `index` right.
153
+ - **Initial state belongs to the page.** `Gallery` keeps its photos in `initialState`, which is fine for the root component or a page. A child component rendered by a parent can't have an `initialState` ([SYG405](https://sygnal.js.org/reference/errors/#syg405)): there, drop it, and pass `photos` and `index` down in the parent's state.
154
+ - **Name commands after what they do.** A command called `scrollTo` would replace the host `<div>`'s own `scrollTo()` method for element commands; `goTo` avoids the confusion.
155
+ - **Accessibility.** Give the carousel a name and each slide its position (`role="group"`, `aria-roledescription="slide"`, `aria-label="2 of 3"`), give the icon buttons labels, and announce the current slide (`aria-live="polite"`). Don't auto-play; if you add it, add a pause button and stop on focus or hover.