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,177 @@
1
+ <!-- Generated from docs/src/content/docs/recipes by scripts/copy-guides.mjs; online: https://sygnal.js.org/recipes/ -->
2
+ # Charts (Chart.js, ECharts)
3
+
4
+ [Chart.js](https://www.chartjs.org/) draws charts on a `<canvas>`. It doesn't know about Sygnal, and doesn't need to: [`defineWidget`](../widgets.md) turns it into a JSX tag. The data lives in your state, the chart is redrawn when it changes, and a click on a bar comes back as an event the intent reads.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install chart.js
10
+ ```
11
+
12
+ ## The widget
13
+
14
+ ```js live-file=./SalesChart.js
15
+ // SalesChart.js
16
+ import { defineWidget } from 'sygnal'
17
+ import { Chart, BarController, BarElement, CategoryScale, LinearScale, Tooltip, Colors } from 'chart.js'
18
+
19
+ Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Colors)
20
+
21
+ export const SalesChart = defineWidget({
22
+ name: 'SalesChart',
23
+ tag: 'canvas',
24
+ mount: (el, props, dispatch) => new Chart(el, {
25
+ type: 'bar',
26
+ data: { labels: props.labels, datasets: [{ label: props.series, data: props.values }] },
27
+ options: {
28
+ maintainAspectRatio: false,
29
+ onClick: (event, bars) => { if (bars.length) dispatch('bar-select', bars[0].index) },
30
+ },
31
+ }),
32
+ update: (chart, props) => {
33
+ chart.data.labels = props.labels
34
+ chart.data.datasets[0].label = props.series
35
+ chart.data.datasets[0].data = props.values
36
+ chart.update()
37
+ },
38
+ unmount: (chart) => chart.destroy(),
39
+ events: ['bar-select'],
40
+ })
41
+ ```
42
+
43
+ - The `Colors` plugin gives each dataset a colour from Chart.js's palette. Without it, a dataset with no `backgroundColor` is drawn in a 10% black that is hard to see on a dark page.
44
+ - `mount` creates the chart on the `<canvas>` host and returns it: that is the instance `update` and `unmount` get.
45
+ - `update` runs when the props change. It changes the chart's data in place and calls `chart.update()`, so the chart animates to the new values instead of being rebuilt.
46
+ - `unmount` runs when the chart leaves the page and frees the canvas.
47
+ - A click on a bar calls `dispatch('bar-select', index)`, a bubbling DOM event on the canvas with the bar's index as its `detail`.
48
+
49
+ ## Using it
50
+
51
+ ```jsx live
52
+ // Sales.jsx
53
+ import { SalesChart } from './SalesChart.js'
54
+
55
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr']
56
+
57
+ export function Sales({ state }) {
58
+ const picked = state.selected === null ? 'Select a bar' : `${MONTHS[state.selected]}: ${state.values[state.selected]}`
59
+ return (
60
+ <section>
61
+ <div className="chart-box">
62
+ <SalesChart className="sales" role="img" aria-label="Sales by month"
63
+ labels={MONTHS} series="Sales" values={state.values} />
64
+ </div>
65
+ <p className="picked">{picked}</p>
66
+ <button className="add-sale">Add a sale in April</button>
67
+ </section>
68
+ )
69
+ }
70
+
71
+ Sales.initialState = { values: [12, 19, 7, 4], selected: null }
72
+
73
+ Sales.intent = ({ DOM }) => ({
74
+ SELECT: DOM.select('.sales').events('bar-select').detail(),
75
+ ADD_SALE: DOM.click('.add-sale'),
76
+ })
77
+
78
+ Sales.model = {
79
+ SELECT: (state, index) => ({ ...state, selected: index }),
80
+ ADD_SALE: (state) => ({ ...state, values: [...state.values.slice(0, 3), state.values[3] + 1] }),
81
+ }
82
+ ```
83
+
84
+ ```css live
85
+ .chart-box { position: relative; height: 240px; }
86
+ ```
87
+
88
+ Chart.js sizes the canvas to its parent, so the parent needs a size (and `position: relative`). With `maintainAspectRatio: false` the chart fills that box.
89
+
90
+ ## Testing
91
+
92
+ `renderComponent`'s mock DOM doesn't draw the chart. `t.widget('.sales')` sends the widget's events and shows the props the view passed it, which is what the component is responsible for:
93
+
94
+ ```jsx
95
+ // Sales.test.jsx
96
+ import { test, expect } from 'vitest'
97
+ import { renderComponent } from 'sygnal'
98
+ import { Sales } from './Sales.jsx'
99
+
100
+ test('a clicked bar is selected, and a new sale reaches the chart', async () => {
101
+ const t = renderComponent(Sales)
102
+ await t.ready()
103
+ t.widget('.sales').dispatch('bar-select', 1)
104
+ await t.next((state) => state.selected === 1)
105
+ expect(t.query('.picked').textContent).toBe('Feb: 19')
106
+
107
+ t.simulateEvent('.add-sale', 'click')
108
+ await t.next((state) => state.values[3] === 5)
109
+ expect(t.widget('.sales').props.values).toEqual([12, 19, 7, 5])
110
+ t.dispose()
111
+ })
112
+ ```
113
+
114
+ To test the chart itself (that a click on the second bar selects it), run the component in a real browser with `renderComponent(Sales, { dom: 'real' })`: `t.widget('.sales').instance` is the Chart.js object, and `Chart.getChart(canvas)` is `undefined` after `t.dispose()`. jsdom has no canvas, so Chart.js can't draw there.
115
+
116
+ ## ECharts
117
+
118
+ [Apache ECharts](https://echarts.apache.org/) works the same way. It draws into a `<div>` (the default host), takes one `option` object for everything, and reports clicks through `chart.on('click')`:
119
+
120
+ ```js
121
+ // SalesEChart.js
122
+ import { defineWidget } from 'sygnal'
123
+ import * as echarts from 'echarts/core'
124
+ import { BarChart } from 'echarts/charts'
125
+ import { GridComponent, TooltipComponent } from 'echarts/components'
126
+ import { CanvasRenderer } from 'echarts/renderers'
127
+
128
+ echarts.use([BarChart, GridComponent, TooltipComponent, CanvasRenderer])
129
+
130
+ const option = (props) => ({
131
+ xAxis: { type: 'category', data: props.labels },
132
+ yAxis: { type: 'value' },
133
+ tooltip: {},
134
+ series: [{ type: 'bar', data: props.values }],
135
+ })
136
+
137
+ export const SalesEChart = defineWidget({
138
+ name: 'SalesEChart',
139
+ mount: (el, props, dispatch) => {
140
+ const chart = echarts.init(el)
141
+ chart.setOption(option(props))
142
+ chart.on('click', (params) => dispatch('bar-select', params.dataIndex))
143
+ // ECharts measures the element once: redraw at the new size when the host resizes
144
+ const observer = new ResizeObserver(() => chart.resize())
145
+ observer.observe(el)
146
+ return { chart, observer }
147
+ },
148
+ update: ({ chart }, props) => chart.setOption(option(props)),
149
+ unmount: ({ chart, observer }) => {
150
+ observer.disconnect()
151
+ chart.dispose()
152
+ },
153
+ events: ['bar-select'],
154
+ })
155
+ ```
156
+
157
+ `setOption` merges the new option into the chart, so `update` can pass the whole option every time. Give the host a height in CSS (`.sales { height: 240px; }`): ECharts measures the element when it starts, and only again on `chart.resize()`, which the `ResizeObserver` calls when the host changes size (a resized window, a sidebar that opens). The instance is `{ chart, observer }`, so `unmount` can stop both. (Chart.js watches its canvas's container itself; with `maintainAspectRatio: false` it follows the container's height.)
158
+
159
+ ## Size
160
+
161
+ Measured with Vite, minified and gzipped, Sygnal not included:
162
+
163
+ | | Adds |
164
+ |---|---|
165
+ | Chart.js bar chart (the registrations above) | 60 KB |
166
+ | ECharts bar chart (the `use` list above) | 180 KB |
167
+ | `defineWidget` (the first widget in an app) | 1.1 KB |
168
+
169
+ Register only what you draw: `import Chart from 'chart.js/auto'` registers every chart type, scale and plugin: 75 KB instead of 60.
170
+
171
+ ## Pitfalls
172
+
173
+ - **Register what the chart uses.** Chart.js and ECharts are tree-shaken: a chart type, scale or plugin you don't register isn't there (`"bar" is not a registered controller`).
174
+ - **Pass new arrays.** `update` runs when a prop is a different value. A reducer that changes `state.values` in place passes the same array, and the chart doesn't change. The usual immutable update (`[...values]`, `map`) is enough.
175
+ - **A canvas has no text.** Give the host `role="img"` and an `aria-label`, and show the numbers somewhere a screen reader can reach (a table, a summary line), as `picked` does here.
176
+ - **Clicks on bars are mouse-only.** A keyboard user can't click a bar. Offer the same choice with buttons or a `<select>` that dispatch the same action.
177
+ - **Animations.** `chart.update()` animates and `chart.update('none')` doesn't. To respect `prefers-reduced-motion`, set `options.animation: false` in `mount` when `matchMedia('(prefers-reduced-motion: reduce)').matches`.
@@ -0,0 +1,135 @@
1
+ <!-- Generated from docs/src/content/docs/recipes by scripts/copy-guides.mjs; online: https://sygnal.js.org/recipes/ -->
2
+ # Code Editor (CodeMirror)
3
+
4
+ [CodeMirror 6](https://codemirror.net/) is a code editor made of an `EditorView` and extensions. [`defineWidget`](../widgets.md) turns it into a JSX tag: the code is a string in your state, each change comes back as an event, and a `focus` command focuses the editor from the model.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install codemirror @codemirror/state @codemirror/lang-javascript
10
+ ```
11
+
12
+ ## The widget
13
+
14
+ ```js live-file=./CodeEditor.js
15
+ // CodeEditor.js
16
+ import { defineWidget } from 'sygnal'
17
+ import { EditorView, basicSetup } from 'codemirror'
18
+ import { Compartment } from '@codemirror/state'
19
+ import { javascript } from '@codemirror/lang-javascript'
20
+
21
+ // the label can change (another language): it lives in a compartment that update() reconfigures
22
+ const labelling = new Compartment()
23
+ const label = (props) => EditorView.contentAttributes.of(props.label == null ? {} : { 'aria-label': props.label })
24
+
25
+ export const CodeEditor = defineWidget({
26
+ name: 'CodeEditor',
27
+ mount: (el, props, dispatch) => new EditorView({
28
+ parent: el,
29
+ doc: props.code,
30
+ extensions: [
31
+ basicSetup,
32
+ javascript(),
33
+ labelling.of(label(props)),
34
+ EditorView.updateListener.of((update) => {
35
+ if (update.docChanged) dispatch('edit', update.state.doc.toString())
36
+ }),
37
+ ],
38
+ }),
39
+ update: (view, props) => {
40
+ const code = view.state.doc.toString()
41
+ if (props.code !== code) view.dispatch({ changes: { from: 0, to: code.length, insert: props.code } })
42
+ // getAttribute gives null for no label
43
+ if ((props.label ?? null) !== view.contentDOM.getAttribute('aria-label')) view.dispatch({ effects: labelling.reconfigure(label(props)) })
44
+ },
45
+ unmount: (view) => view.destroy(),
46
+ events: ['edit'],
47
+ commands: { focus: (view) => view.focus() },
48
+ })
49
+ ```
50
+
51
+ - `mount` creates the view inside the host `<div>` (`parent: el`). `basicSetup` brings line numbers, history, bracket matching, search and autocompletion.
52
+ - The update listener sends the whole document as the `edit` event after each change.
53
+ - `update` gets the code from state. When the user typed it, it equals the editor's document and nothing happens; otherwise one transaction replaces the document.
54
+ - `view.dispatch` is CodeMirror's own method for applying a transaction. It has nothing to do with the widget's `dispatch`, the third parameter of `mount`, which sends events to the intent.
55
+ - The `focus` command focuses the editable area. A command the widget declares wins over the host element's method of the same name, so `{ focus: '.code' }` runs this one, not the `<div>`'s.
56
+
57
+ ## Using it
58
+
59
+ ```jsx live
60
+ // Snippet.jsx
61
+ import { CodeEditor } from './CodeEditor.js'
62
+
63
+ const START = 'const answer = 42\n'
64
+
65
+ export function Snippet({ state }) {
66
+ return (
67
+ <section>
68
+ <CodeEditor className="code" label="Snippet" code={state.code} />
69
+ <p className="lines">{state.code.split('\n').length} lines</p>
70
+ <button type="button" className="edit-code">Edit</button>
71
+ <button type="button" className="reset">Reset</button>
72
+ </section>
73
+ )
74
+ }
75
+
76
+ Snippet.initialState = { code: START }
77
+
78
+ Snippet.intent = ({ DOM }) => ({
79
+ EDIT: DOM.select('.code').events('edit').detail(),
80
+ FOCUS_CODE: DOM.click('.edit-code'),
81
+ RESET: DOM.click('.reset'),
82
+ })
83
+
84
+ Snippet.model = {
85
+ EDIT: (state, code) => ({ ...state, code }),
86
+ FOCUS_CODE: { ELEMENT: { focus: '.code' } },
87
+ RESET: (state) => ({ ...state, code: START }),
88
+ }
89
+ ```
90
+
91
+ ```css live
92
+ .code .cm-editor { background: white; color: black; border: 1px solid #8888; }
93
+ ```
94
+
95
+ CodeMirror's default theme is a light one: dark syntax colours and a light gutter, on whatever background the page has. On a page that can be dark, give the editor a light background, as here, or add a dark theme extension (`@codemirror/theme-one-dark`) and switch it with a compartment.
96
+
97
+ ## Testing
98
+
99
+ ```jsx
100
+ // Snippet.test.jsx
101
+ import { test, expect } from 'vitest'
102
+ import { renderComponent } from 'sygnal'
103
+ import { Snippet } from './Snippet.jsx'
104
+
105
+ test('edits reach the state, Reset puts the start code back', async () => {
106
+ const t = renderComponent(Snippet)
107
+ await t.ready()
108
+ t.widget('.code').dispatch('edit', 'const answer = 42\nconsole.log(answer)\n')
109
+ await t.next((state) => state.code.includes('console.log'))
110
+ expect(t.query('.lines').textContent).toBe('3 lines')
111
+
112
+ t.simulateEvent('.reset', 'click')
113
+ await t.next((state) => state.code === 'const answer = 42\n')
114
+ expect(t.widget('.code').props.code).toBe('const answer = 42\n')
115
+
116
+ t.simulateEvent('.edit-code', 'click')
117
+ await t.settle()
118
+ expect(t.commands()).toEqual([{ focus: '.code' }])
119
+ t.dispose()
120
+ })
121
+ ```
122
+
123
+ CodeMirror measures text positions with layout APIs jsdom doesn't have, so test typing in a real browser: with `renderComponent(Snippet, { dom: 'real' })` under Playwright, click **Edit**, type, and `t.waitForState((state) => ...)` sees the new code; `t.widget('.code').instance` is the `EditorView`.
124
+
125
+ ## Size
126
+
127
+ Measured with Vite, minified and gzipped, Sygnal not included: `basicSetup` with the JavaScript language adds **177 KB**. For a smaller editor, replace `basicSetup` with `minimalSetup` (history, default keymap, special characters) and the extensions you want from `@codemirror/view`, `@codemirror/commands` and the language packages. `defineWidget` adds 1.1 KB for the first widget in an app.
128
+
129
+ ## Pitfalls
130
+
131
+ - **Keep the comparison in `update`.** Replacing the document on every render would reset the cursor and the undo history while the user types.
132
+ - **A reset is also an edit.** The transaction `update` dispatches changes the document, so the listener sends it back as an `edit` with the same code. That costs one extra action and changes nothing. To skip it, add an annotation to the transaction (`annotations: Transaction.remote.of(true)` from `@codemirror/state`) and ignore updates that carry it.
133
+ - **Change configuration with compartments.** `mount` builds the extensions once. The label is in a `Compartment` that `update` reconfigures when the `label` prop changes (a translated app switching language); do the same to switch the language mode, the theme or `readOnly` from props (`view.dispatch({ effects: compartment.reconfigure(...) })`). `@codemirror/state` is installed with `codemirror`; list it yourself so there is one copy (two copies break compartments).
134
+ - **Label the editable element.** The host `<div>` is not what receives the keyboard. `EditorView.contentAttributes` puts `aria-label` on CodeMirror's `.cm-content`, the element a screen reader announces.
135
+ - **Tab.** CodeMirror doesn't trap Tab by default, so keyboard users can leave the editor. Adding `indentWithTab` changes that; if you do, tell users that Escape then Tab leaves the editor.
@@ -0,0 +1,155 @@
1
+ <!-- Generated from docs/src/content/docs/recipes by scripts/copy-guides.mjs; online: https://sygnal.js.org/recipes/ -->
2
+ # Data Grid (AG Grid)
3
+
4
+ [AG Grid](https://www.ag-grid.com/) is a full spreadsheet-like grid: virtual scrolling for large data sets, column resizing and reordering, in-cell editing, keyboard navigation, CSV export. It renders everything itself, so [`defineWidget`](../widgets.md) turns it into a JSX tag. The rows stay in your state: the grid reports edits and selection as events, and the model decides what changes.
5
+
6
+ This recipe uses the free Community edition (`ag-grid-community`). For a table you style yourself, with a few hundred rows, the lighter [TanStack Table recipe](./data-table.md) is enough.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ npm install ag-grid-community
12
+ ```
13
+
14
+ ## The widget
15
+
16
+ ```js live-file=./Grid.js
17
+ // Grid.js
18
+ import { defineWidget } from 'sygnal'
19
+ import {
20
+ createGrid, ModuleRegistry, ClientSideRowModelModule, RowSelectionModule,
21
+ TextEditorModule, NumberEditorModule, CsvExportModule,
22
+ } from 'ag-grid-community'
23
+
24
+ ModuleRegistry.registerModules([
25
+ ClientSideRowModelModule, RowSelectionModule, TextEditorModule, NumberEditorModule, CsvExportModule,
26
+ ])
27
+
28
+ export const Grid = defineWidget({
29
+ name: 'Grid',
30
+ mount: (el, props, dispatch) => createGrid(el, {
31
+ columnDefs: props.columns,
32
+ rowData: props.rows,
33
+ getRowId: (params) => params.data.id,
34
+ // edits become events; the grid shows them once the state has them
35
+ readOnlyEdit: true,
36
+ onCellEditRequest: (event) =>
37
+ dispatch('cell-edit', { id: event.data.id, field: event.colDef.field, value: event.newValue }),
38
+ rowSelection: { mode: 'singleRow', checkboxes: false, enableClickSelection: true },
39
+ onSelectionChanged: (event) => dispatch('row-select', event.api.getSelectedRows()[0]?.id ?? null),
40
+ }),
41
+ update: (grid, props) => {
42
+ grid.setGridOption('columnDefs', props.columns)
43
+ grid.setGridOption('rowData', props.rows)
44
+ },
45
+ unmount: (grid) => grid.destroy(),
46
+ events: ['cell-edit', 'row-select'],
47
+ commands: {
48
+ exportCsv: (grid, { fileName }) => grid.exportDataAsCsv({ fileName }),
49
+ },
50
+ })
51
+ ```
52
+
53
+ - `readOnlyEdit: true` keeps the grid from writing an edit into the row object. It fires `onCellEditRequest` instead, the widget sends that as a `cell-edit` event, and the edit shows up in the grid when the new rows come back through `update`.
54
+ - `getRowId` gives each row its identity, so a new `rowData` array updates only the rows that changed, and keeps the selection, the scroll position and the focused cell.
55
+ - The `exportCsv` command gets the options of the element command (`fileName`).
56
+
57
+ ## Using it
58
+
59
+ ```jsx live
60
+ // Stock.jsx
61
+ import { Grid } from './Grid.js'
62
+
63
+ const COLUMNS = [
64
+ { field: 'name', headerName: 'Item', editable: true, flex: 2 },
65
+ { field: 'count', headerName: 'In stock', editable: true, cellDataType: 'number', flex: 1 },
66
+ ]
67
+
68
+ export function Stock({ state }) {
69
+ const selected = state.items.find((item) => item.id === state.selected)
70
+ return (
71
+ <section>
72
+ <Grid className="grid" columns={COLUMNS} rows={state.items} />
73
+ <p className="selected">{selected ? `${selected.name}: ${selected.count} in stock` : 'No item selected'}</p>
74
+ <button className="restock" disabled={!selected}>Restock 10</button>
75
+ <button className="export">Export CSV</button>
76
+ </section>
77
+ )
78
+ }
79
+
80
+ Stock.initialState = {
81
+ items: [
82
+ { id: 'a1', name: 'Bolts', count: 120 },
83
+ { id: 'a2', name: 'Nuts', count: 80 },
84
+ { id: 'a3', name: 'Washers', count: 0 },
85
+ ],
86
+ selected: null,
87
+ }
88
+
89
+ Stock.intent = ({ DOM }) => ({
90
+ EDIT: DOM.select('.grid').events('cell-edit').detail(),
91
+ SELECT: DOM.select('.grid').events('row-select').detail(),
92
+ RESTOCK: DOM.click('.restock'),
93
+ EXPORT: DOM.click('.export'),
94
+ })
95
+
96
+ const change = (items, id, values) => items.map((item) => (item.id === id ? { ...item, ...values } : item))
97
+
98
+ Stock.model = {
99
+ EDIT: (state, { id, field, value }) => ({ ...state, items: change(state.items, id, { [field]: value }) }),
100
+ SELECT: (state, id) => ({ ...state, selected: id }),
101
+ RESTOCK: (state) => {
102
+ const item = state.items.find((it) => it.id === state.selected)
103
+ return { ...state, items: change(state.items, item.id, { count: item.count + 10 }) }
104
+ },
105
+ EXPORT: { ELEMENT: { exportCsv: '.grid', fileName: 'stock.csv' } },
106
+ }
107
+ ```
108
+
109
+ ```css live
110
+ .grid { height: 400px; }
111
+ ```
112
+
113
+ `COLUMNS` is a module constant, so the `columns` prop keeps its identity and only a change to `items` runs `update`. The grid draws its own theme (Quartz by default); it needs a height to fill.
114
+
115
+ ## Testing
116
+
117
+ ```jsx
118
+ // Stock.test.jsx
119
+ import { test, expect } from 'vitest'
120
+ import { renderComponent } from 'sygnal'
121
+ import { Stock } from './Stock.jsx'
122
+
123
+ test('edits and restocking change the rows the grid gets', async () => {
124
+ const t = renderComponent(Stock)
125
+ await t.ready()
126
+ t.widget('.grid').dispatch('cell-edit', { id: 'a2', field: 'count', value: 75 })
127
+ await t.next((state) => state.items[1].count === 75)
128
+
129
+ t.widget('.grid').dispatch('row-select', 'a3')
130
+ await t.next((state) => state.selected === 'a3')
131
+ t.simulateEvent('.restock', 'click')
132
+ await t.next((state) => state.items[2].count === 10)
133
+ expect(t.query('.selected').textContent).toBe('Washers: 10 in stock')
134
+ expect(t.widget('.grid').props.rows.map((item) => item.count)).toEqual([120, 75, 10])
135
+
136
+ t.simulateEvent('.export', 'click')
137
+ await t.settle()
138
+ expect(t.commands()).toEqual([{ exportCsv: '.grid', fileName: 'stock.csv' }])
139
+ t.dispose()
140
+ })
141
+ ```
142
+
143
+ In a real browser (`renderComponent(Stock, { dom: 'real' })` under Playwright), double-click a cell (`.grid [row-id="a2"] [col-id="count"]`), type and press Enter: the state gets the new count, and the cell shows it once the new rows reach the grid. `t.widget('.grid').instance` is the grid API (`getSelectedRows()`, `isDestroyed()`).
144
+
145
+ ## Size
146
+
147
+ Measured with Vite, minified and gzipped, Sygnal not included: **240 KB** with the five modules above. `AllCommunityModule` registers every Community feature and makes it 333 KB. `defineWidget` adds 1.1 KB for the first widget in an app.
148
+
149
+ ## Pitfalls
150
+
151
+ - **Keep `readOnlyEdit: true`.** Without it, AG Grid writes each edit into the row object it was given, which is the object in your state: the state changes without an action, and the next render can't tell anything changed.
152
+ - **Give rows an id.** Without `getRowId`, each new `rowData` array replaces every row: the selection and the focused cell are lost after each edit.
153
+ - **Register the modules you use.** AG Grid 33 and later are modular. An option whose module isn't registered doesn't work, and the grid logs an error with a link; register `ValidationModule` in development to get messages that name the missing module.
154
+ - **Don't recreate `columns` in the view.** A new array on every render runs `update` every time and makes the grid rebuild its columns. Keep column definitions in a constant, or in state if the user can change them.
155
+ - **Keyboard and screen readers.** The grid has its own keyboard navigation and ARIA grid roles: don't wrap it in elements that capture arrow keys, and label the page section it sits in.
@@ -0,0 +1,196 @@
1
+ <!-- Generated from docs/src/content/docs/recipes by scripts/copy-guides.mjs; online: https://sygnal.js.org/recipes/ -->
2
+ # Data Table (TanStack Table)
3
+
4
+ [TanStack Table](https://tanstack.com/table) is headless: it computes which rows to show (sorted, filtered, paged) and renders nothing. That fits Sygnal without a widget. Keep the table's settings (sorting, search, page) in your state, build the table from that state in the view, and render its rows with JSX. Sygnal still renders every element, so the table works in the mock DOM, on the server, and with sygnal-check.
5
+
6
+ This recipe uses **v9** (`@tanstack/table-core` 9.x), whose API differs from v8: `constructTable` and `tableFeatures` replace `createTable` and the `get*RowModel` options.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ npm install @tanstack/table-core
12
+ ```
13
+
14
+ ## The table
15
+
16
+ ```js live-file=./peopleTable.js
17
+ // peopleTable.js
18
+ import {
19
+ constructTable, createColumnHelper, tableFeatures,
20
+ rowSortingFeature, createSortedRowModel, sortFn_alphanumeric, sortFn_basic,
21
+ columnFilteringFeature, globalFilteringFeature, createFilteredRowModel, filterFn_includesString,
22
+ rowPaginationFeature, createPaginatedRowModel,
23
+ } from '@tanstack/table-core'
24
+ import { storeReactivityBindings } from '@tanstack/table-core/store-reactivity-bindings'
25
+
26
+ const features = tableFeatures({
27
+ coreReactivityFeature: storeReactivityBindings(),
28
+ rowSortingFeature,
29
+ sortedRowModel: createSortedRowModel(),
30
+ sortFns: { alphanumeric: sortFn_alphanumeric, basic: sortFn_basic },
31
+ columnFilteringFeature,
32
+ globalFilteringFeature,
33
+ filteredRowModel: createFilteredRowModel(),
34
+ filterFns: { includesString: filterFn_includesString },
35
+ rowPaginationFeature,
36
+ paginatedRowModel: createPaginatedRowModel(),
37
+ })
38
+
39
+ const column = createColumnHelper()
40
+ const columns = column.columns([
41
+ column.accessor('name', { header: 'Name', sortFn: 'alphanumeric' }),
42
+ column.accessor('team', { header: 'Team', sortFn: 'alphanumeric' }),
43
+ column.accessor('points', { header: 'Points', sortFn: 'basic' }),
44
+ ])
45
+
46
+ export const PAGE_SIZE = 3
47
+
48
+ // A pure function of the component's state: the table never holds state of its own
49
+ export const peopleTable = (state) => constructTable({
50
+ features,
51
+ columns,
52
+ data: state.people,
53
+ getRowId: (person) => person.id,
54
+ globalFilterFn: 'includesString',
55
+ state: {
56
+ sorting: state.sorting,
57
+ globalFilter: state.search,
58
+ pagination: { pageIndex: state.page, pageSize: PAGE_SIZE },
59
+ },
60
+ })
61
+
62
+ // click a column: ascending, then descending, then unsorted
63
+ export const nextSorting = (sorting, id) =>
64
+ sorting[0]?.id !== id ? [{ id, desc: false }] : sorting[0].desc ? [] : [{ id, desc: true }]
65
+ ```
66
+
67
+ - `features`, `columns` and the sort and filter functions are created once, at module level. v9 registers each feature explicitly; an API you didn't register (say `getCanNextPage` without `rowPaginationFeature`) doesn't exist.
68
+ - `peopleTable(state)` passes every setting through the `state` option. The table owns no state and nobody calls its setters (`setSorting`, `nextPage`): actions change Sygnal's state, and the next render builds the table from it.
69
+
70
+ ## Using it
71
+
72
+ ```jsx live
73
+ // People.jsx
74
+ import { peopleTable, nextSorting } from './peopleTable.js'
75
+
76
+ const ARIA_SORT = { asc: 'ascending', desc: 'descending' }
77
+
78
+ export function People({ state }) {
79
+ const table = peopleTable(state)
80
+ return (
81
+ <section>
82
+ <label>Search <input className="search" value={state.search} /></label>
83
+ <table>
84
+ <thead>
85
+ {table.getHeaderGroups().map((group) => (
86
+ <tr key={group.id}>
87
+ {group.headers.map((header) => (
88
+ <th key={header.id} aria-sort={ARIA_SORT[header.column.getIsSorted()] || 'none'}>
89
+ <button className="sort" data={{ column: header.id }}>{header.column.columnDef.header}</button>
90
+ </th>
91
+ ))}
92
+ </tr>
93
+ ))}
94
+ </thead>
95
+ <tbody>
96
+ {table.getRowModel().rows.map((row) => (
97
+ <tr key={row.id}>
98
+ {row.getAllCells().map((cell) => <td key={cell.id}>{cell.getValue()}</td>)}
99
+ </tr>
100
+ ))}
101
+ </tbody>
102
+ </table>
103
+ <button className="prev" disabled={!table.getCanPreviousPage()}>Previous</button>
104
+ <span className="page">Page {state.page + 1} of {Math.max(table.getPageCount(), 1)}</span>
105
+ <button className="next" disabled={!table.getCanNextPage()}>Next</button>
106
+ </section>
107
+ )
108
+ }
109
+
110
+ People.initialState = {
111
+ people: [
112
+ { id: 'p1', name: 'Ada', team: 'Engines', points: 36 },
113
+ { id: 'p2', name: 'Grace', team: 'Compilers', points: 45 },
114
+ { id: 'p3', name: 'Alan', team: 'Engines', points: 41 },
115
+ { id: 'p4', name: 'Edsger', team: 'Compilers', points: 12 },
116
+ { id: 'p5', name: 'Barbara', team: 'Languages', points: 28 },
117
+ ],
118
+ sorting: [],
119
+ search: '',
120
+ page: 0,
121
+ }
122
+
123
+ People.intent = ({ DOM }) => ({
124
+ SEARCH: DOM.input('.search').value(),
125
+ SORT: DOM.click('.sort').data('column'),
126
+ PREV: DOM.click('.prev'),
127
+ NEXT: DOM.click('.next'),
128
+ })
129
+
130
+ People.model = {
131
+ SEARCH: (state, search) => ({ ...state, search, page: 0 }),
132
+ SORT: (state, id) => ({ ...state, sorting: nextSorting(state.sorting, id), page: 0 }),
133
+ PREV: (state) => ({ ...state, page: state.page - 1 }),
134
+ NEXT: (state) => ({ ...state, page: state.page + 1 }),
135
+ }
136
+ ```
137
+
138
+ The column headers are buttons inside the `<th>`, so they work with the keyboard, and `aria-sort` tells screen readers which column is sorted and how.
139
+
140
+ ## Testing
141
+
142
+ Everything is plain markup, so the mock DOM shows the result directly:
143
+
144
+ ```jsx
145
+ // People.test.jsx
146
+ import { test, expect } from 'vitest'
147
+ import { renderComponent } from 'sygnal'
148
+ import { People } from './People.jsx'
149
+
150
+ const names = (t) => t.queryAll('tbody tr').map((row) => row.querySelector('td').textContent)
151
+
152
+ test('sorts, searches and pages', async () => {
153
+ const t = renderComponent(People)
154
+ await t.ready()
155
+ expect(names(t)).toEqual(['Ada', 'Grace', 'Alan'])
156
+ expect(t.query('.page').textContent).toBe('Page 1 of 2')
157
+
158
+ t.simulateEvent('.sort[data-column="points"]', 'click')
159
+ t.simulateEvent('.sort[data-column="points"]', 'click')
160
+ await t.next((state) => state.sorting[0]?.desc === true)
161
+ expect(names(t)).toEqual(['Grace', 'Alan', 'Ada'])
162
+ expect(t.queryAll('th')[2].getAttribute('aria-sort')).toBe('descending')
163
+
164
+ t.simulateEvent('.next', 'click')
165
+ await t.next((state) => state.page === 1)
166
+ expect(names(t)).toEqual(['Barbara', 'Edsger'])
167
+ expect(t.query('.next').disabled).toBe(true)
168
+
169
+ t.simulateEvent('.search', 'input', { value: 'compilers' })
170
+ await t.next((state) => state.search === 'compilers')
171
+ expect(names(t)).toEqual(['Grace', 'Edsger'])
172
+ expect(t.query('.page').textContent).toBe('Page 1 of 1')
173
+ t.dispose()
174
+ })
175
+ ```
176
+
177
+ `peopleTable` is a plain function too: a test can call it with a state object and check `getRowModel().rows` without rendering anything.
178
+
179
+ ## Size
180
+
181
+ Measured with Vite, minified and gzipped, Sygnal not included: **16 KB** for `table-core` with sorting, filtering and pagination. Each feature you register adds to it; `stockFeatures` registers all of them.
182
+
183
+ ## Performance
184
+
185
+ The view builds a new table on every render. In a browser that took about 1.5–3 ms for 1,000 rows and 9–23 ms for 10,000 rows (sorted, filtered and paged, depending on the engine). That is fine up to a few thousand rows. Beyond that:
186
+
187
+ - page or filter on the server and pass only the visible rows as `data` (`manualPagination`, `manualSorting`, `manualFiltering`);
188
+ - or render all rows with a [`VirtualCollection`](../virtual-collections.md) and sort and filter the array in the model.
189
+
190
+ ## Pitfalls
191
+
192
+ - **Register a reactivity binding.** v9's `constructTable` needs `coreReactivityFeature` in `features`. Without it, it throws `Cannot read properties of undefined (reading 'wrapExternalAtoms')`. `storeReactivityBindings()` is the framework-neutral one; the table's own atoms aren't used here, because the state comes from Sygnal.
193
+ - **Create features and columns once.** Build them at module level, not in the view: TanStack expects them to keep their identity, and creating them per render also costs time.
194
+ - **One owner per setting.** Pass each setting through `state` and change it in the model. Calling the table's own setters (`table.setSorting`, `table.nextPage`) changes nothing that lasts: the next render builds a new table from Sygnal's state.
195
+ - **Reset the page.** A new search or sort can leave the current page past the end. The reducers here go back to page 0, as most tables do.
196
+ - **Call methods on their objects.** `row.getValue('name')` works; `const { getValue } = row` loses `this`.