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,222 @@
1
+ <!-- Generated from docs/src/content/docs/recipes by scripts/copy-guides.mjs; online: https://sygnal.js.org/recipes/ -->
2
+ # Translations (i18n)
3
+
4
+ [i18next](https://www.i18next.com/) looks up translated strings, fills in values and picks plural forms. In a Sygnal app the chosen locale is ordinary state: the root keeps it, [`.context`](https://sygnal.js.org/guide/context/) hands every component a `t()` for that locale, and [`persist()`](../persistence.md) remembers it for the next visit. Switching the language is an action like any other, and every component re-renders in the new language.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install i18next
10
+ ```
11
+
12
+ ## The i18next instance
13
+
14
+ ```js live-file=./i18n.js
15
+ // i18n.js
16
+ import i18next from 'i18next'
17
+
18
+ export const LOCALES = { en: 'English', fr: 'Français' }
19
+
20
+ export const i18n = i18next.createInstance()
21
+ i18n.init({
22
+ initAsync: false, // the resources are here: ready before the first render
23
+ interpolation: { escapeValue: false }, // Sygnal renders strings as text, never as HTML
24
+ fallbackLng: 'en',
25
+ supportedLngs: Object.keys(LOCALES),
26
+ resources: {
27
+ en: {
28
+ translation: {
29
+ language: 'Language',
30
+ greeting: 'Hello, {{name}}!',
31
+ cart: {
32
+ title: 'Your cart',
33
+ add: 'Add a coffee',
34
+ items_one: '{{count}} item',
35
+ items_other: '{{count}} items',
36
+ total: 'Total: {{total, currency(EUR)}}',
37
+ },
38
+ },
39
+ },
40
+ fr: {
41
+ translation: {
42
+ language: 'Langue',
43
+ greeting: 'Bonjour, {{name}} !',
44
+ cart: {
45
+ title: 'Votre panier',
46
+ add: 'Ajouter un café',
47
+ items_one: '{{count}} article',
48
+ items_other: '{{count}} articles',
49
+ total: 'Total : {{total, currency(EUR)}}',
50
+ },
51
+ },
52
+ },
53
+ },
54
+ })
55
+
56
+ // One t() per locale, made once: the context value changes only when the locale does
57
+ const fixed = {}
58
+ export const translator = (locale) => (fixed[locale] ||= i18n.getFixedT(locale))
59
+
60
+ // The browser's language; 'en' on the server (Node 21+ has a navigator too, with the server's)
61
+ export function detectLocale() {
62
+ const lang = typeof window === 'undefined' ? 'en' : (navigator.language || '').slice(0, 2)
63
+ return lang in LOCALES ? lang : 'en'
64
+ }
65
+ ```
66
+
67
+ - `initAsync: false` with the translations inline makes `t()` work as soon as the module is loaded, so the first render is already translated.
68
+ - `escapeValue: false`: i18next escapes the values it fills in for HTML by default (`&` becomes `&amp;`). Sygnal puts strings into the page as text, which is already safe, so escaping would show the entities.
69
+ - `getFixedT(locale)` returns a `t` bound to one locale. The app never calls `i18n.changeLanguage()`: the locale lives in Sygnal's state, not in the i18next instance, so several apps (or tests) can show different languages at once.
70
+ - `translator` keeps one `t` per locale, so the function only changes when the locale does.
71
+
72
+ ## The app
73
+
74
+ ```jsx live
75
+ // App.jsx
76
+ import { persist } from 'sygnal'
77
+ import { LOCALES, translator, detectLocale } from './i18n.js'
78
+ import { Cart } from './Cart.jsx'
79
+
80
+ export function App({ state }) {
81
+ const t = translator(state.locale)
82
+ return (
83
+ <main lang={state.locale}>
84
+ <label>
85
+ {t('language')}
86
+ <select className="locale" value={state.locale}>
87
+ {Object.entries(LOCALES).map(([code, name]) => <option value={code}>{name}</option>)}
88
+ </select>
89
+ </label>
90
+ <h1>{t('greeting', { name: state.name })}</h1>
91
+ <Cart state="cart" />
92
+ </main>
93
+ )
94
+ }
95
+
96
+ App.initialState = { locale: detectLocale(), name: 'Ada', cart: { items: 0 } }
97
+
98
+ App.context = { t: (state) => translator(state.locale) }
99
+
100
+ App.intent = ({ DOM }) => ({
101
+ LOCALE: DOM.change('.locale').value(),
102
+ })
103
+
104
+ App.model = {
105
+ LOCALE: (state, locale) => ({ ...state, locale }),
106
+ }
107
+
108
+ App.persist = persist({ key: 'locale', pick: ['locale'] })
109
+ ```
110
+
111
+ ```jsx live-file=./Cart.jsx
112
+ // Cart.jsx
113
+ const PRICE = 4.5
114
+
115
+ export function Cart({ state, context }) {
116
+ const { t } = context
117
+ return (
118
+ <section>
119
+ <h2>{t('cart.title')}</h2>
120
+ <p className="count">{t('cart.items', { count: state.items })}</p>
121
+ <p className="total">{t('cart.total', { total: state.items * PRICE })}</p>
122
+ <button className="add">{t('cart.add')}</button>
123
+ </section>
124
+ )
125
+ }
126
+
127
+ Cart.intent = ({ DOM }) => ({
128
+ ADD: DOM.click('.add'),
129
+ })
130
+
131
+ Cart.model = {
132
+ ADD: (state) => ({ ...state, items: state.items + 1 }),
133
+ }
134
+ ```
135
+
136
+ - **The locale** is a key of the root's state. On the first visit it comes from the browser (`detectLocale()`); after that, `persist()` restores the saved one before the first render, so there is no flash of the wrong language.
137
+ - **`t` for every component**: `App.context.t` is computed from the root's state, and every component below reads it as `context.t`, however deep. When the locale changes, the context changes, and the components that use it render again. The root itself calls `translator(state.locale)`, since the locale is in its own state.
138
+ - **Reducers** get the context too, on their fourth argument: `(state, data, next, { context }) => ... context.t('saved') ...`, for a message the model builds.
139
+
140
+ ## Plurals, values and formats
141
+
142
+ - **Plurals**: pass `count`, and i18next picks the key with the language's plural suffix (`items_one`, `items_other`; some languages have `_zero`, `_two`, `_few`, `_many`). The rules are the language's own, from `Intl.PluralRules`: French uses the singular for 0 ("0 article"), English the plural ("0 items").
143
+ - **Values**: `{{name}}` in a string is filled from the options: `t('greeting', { name: state.name })`.
144
+ - **Formats**: `{{total, currency(EUR)}}` formats with `Intl.NumberFormat` for the locale: "€4.50" in English, "4,50 €" in French. `number`, `datetime`, `relativetime` and `list` work the same way.
145
+
146
+ ## Accessibility
147
+
148
+ - Put the locale in a `lang` attribute (here on `<main>`), so screen readers pronounce the text in the right language. For the whole page, set `document.documentElement.lang` from an `EFFECT` in the `LOCALE` entry and in `BOOTSTRAP`.
149
+ - Translated labels are fine. sygnal-check's accessibility rules ([SYG702](https://sygnal.js.org/reference/errors/#syg702), [SYG705](https://sygnal.js.org/reference/errors/#syg705)) check that a field has a label and a button has a name; a `{t('...')}` expression counts as one. They can't check what the translation says, so keep the keys for labels and names present in every language (a missing key shows the key itself).
150
+ - Translate `aria-label`s, `alt` text and `title`s too: `aria-label={t('cart.remove')}`.
151
+
152
+ ## Testing
153
+
154
+ `renderComponent` gives a persisting root a fake storage; fill it to start in a saved locale, and read what was saved with `t.storage()`:
155
+
156
+ ```jsx
157
+ // App.test.jsx
158
+ import { test, expect } from 'vitest'
159
+ import { renderComponent } from 'sygnal'
160
+ import { App } from './App.jsx'
161
+
162
+ const text = (t, selector) => t.query(selector).textContent.replace(/\s/g, ' ')
163
+
164
+ test('restores the saved locale, pluralises, switches language and saves it', async () => {
165
+ const t = renderComponent(App, { storage: { locale: { version: 1, state: { locale: 'fr' } } } })
166
+ await t.ready()
167
+ expect(text(t, 'h1')).toBe('Bonjour, Ada !')
168
+ expect(text(t, '.count')).toBe('0 article')
169
+
170
+ t.simulateEvent('.add', 'click')
171
+ t.simulateEvent('.add', 'click')
172
+ await t.next((state) => state.cart.items === 2)
173
+ expect(text(t, '.count')).toBe('2 articles')
174
+ expect(text(t, '.total')).toBe('Total : 9,00 €')
175
+
176
+ t.simulateEvent('.locale', 'change', { value: 'en' })
177
+ await t.next((state) => state.locale === 'en')
178
+ expect(text(t, 'h2')).toBe('Your cart')
179
+ expect(text(t, '.count')).toBe('2 items')
180
+ expect(text(t, '.total')).toBe('Total: €9.00')
181
+ expect(t.query('main').getAttribute('lang')).toBe('en')
182
+
183
+ await t.settle()
184
+ expect(t.storage('locale')).toEqual({ version: 1, state: { locale: 'en' } })
185
+ t.dispose()
186
+ })
187
+ ```
188
+
189
+ `Intl` puts non-breaking spaces in French numbers and punctuation ("9,00 €", "Ada !"); the `text` helper turns them into plain spaces before comparing.
190
+
191
+ A component further down reads `t` from the context, which comes from its ancestors. To test it alone, pass its state and the context its ancestors would give it ([`renderComponent`'s `context`](https://sygnal.js.org/integration/testing/#context)):
192
+
193
+ ```jsx
194
+ // Cart.test.jsx
195
+ import { test, expect } from 'vitest'
196
+ import { renderComponent } from 'sygnal'
197
+ import { Cart } from './Cart.jsx'
198
+ import { translator } from './i18n.js'
199
+
200
+ test('the cart in French', async () => {
201
+ // the state and the context the app would give it
202
+ const t = renderComponent(Cart, { initialState: { items: 1 }, context: { t: translator('fr') } })
203
+ await t.ready()
204
+ expect(t.query('.count').textContent).toBe('1 article')
205
+ t.simulateEvent('.add', 'click')
206
+ await t.next((state) => state.items === 2)
207
+ expect(t.query('.count').textContent).toBe('2 articles')
208
+ t.dispose()
209
+ })
210
+ ```
211
+
212
+ ## Size
213
+
214
+ Measured with Vite, minified and gzipped, Sygnal not included: **15.5 KB** for i18next and this recipe (the translations are part of it). Larger apps keep each language in its own JSON file and load only the chosen one (below).
215
+
216
+ ## Pitfalls
217
+
218
+ - **Don't call `i18n.changeLanguage()`.** It changes a global inside i18next that Sygnal doesn't see, so nothing re-renders. Keep the locale in state and use `getFixedT`.
219
+ - **Keep `t` stable per locale.** `getFixedT` returns a new function on each call. Called directly in `.context`, it gives the context a new value whenever the root's state changes, not only when the locale does; `translator` hands out the same function until the locale changes.
220
+ - **Load translations before switching.** With translations in separate files, fetch the new language first (a `makeFetchDriver` request on the `LOCALE` action), add it with `i18n.addResourceBundle(locale, 'translation', data)` in an `EFFECT` of the reply's action (a reducer only returns state), and only then put the new locale in state (the same action's `STATE` entry: the `EFFECT` runs synchronously and the view renders after the whole action, so the strings are there). A locale whose strings aren't loaded shows the keys.
221
+ - **Server rendering.** Take the locale from the request (the URL or `Accept-Language`) and pass it in the server's state. A locale saved only in `localStorage` reaches a server-rendered page after hydration, as a `RESTORE` action: the page shows the server's language first.
222
+ - **Missing keys show the key.** `t('cart.titel')` renders "cart.titel". Set `saveMissing` with a `missingKeyHandler` in development to log them.
@@ -0,0 +1,115 @@
1
+ <!-- Generated from docs/src/content/docs/recipes by scripts/copy-guides.mjs; online: https://sygnal.js.org/recipes/ -->
2
+ # Icons (Lucide)
3
+
4
+ [Lucide](https://lucide.dev/) has about 1,600 icons. Its framework-neutral package, `lucide`, exports each icon as plain data: a list of SVG elements and their attributes. A small helper turns that data into Sygnal SVG vnodes, so an icon is part of the view like any other markup: it is patched, server-rendered and tested in the mock DOM.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install lucide
10
+ ```
11
+
12
+ ## The helper
13
+
14
+ ```jsx live-file=./icon.jsx
15
+ // icon.jsx
16
+ // A Lucide icon (an array of [tag, attributes, children?]) as Sygnal SVG vnodes
17
+ const parts = (node) => node.map(([Tag, attrs, children]) => <Tag {...attrs}>{children && parts(children)}</Tag>)
18
+
19
+ export function icon(node, { label, size = 24 } = {}) {
20
+ return (
21
+ <svg className="icon" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
22
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
23
+ role={label ? 'img' : undefined} aria-label={label} aria-hidden={label ? undefined : 'true'}>
24
+ {parts(node)}
25
+ </svg>
26
+ )
27
+ }
28
+ ```
29
+
30
+ `icon` is a plain function that returns vnodes, not a component: call it in the view (`{icon(Plus)}`), don't render it as a tag. A component per icon would cost an instance, a state and a stream for each one, for markup that never changes.
31
+
32
+ The `<svg>` gets Lucide's default attributes (a 24 × 24 view box, a 2-pixel round stroke in `currentColor`, so the icon takes the text colour). Inside an `<svg>`, Sygnal's JSX sets every prop as an attribute in the SVG namespace, so the data's `d`, `cx` or `points` work as written.
33
+
34
+ ## Using it
35
+
36
+ ```jsx live
37
+ // Toolbar.jsx
38
+ import { Plus, Trash2, CloudCheck, CloudOff } from 'lucide'
39
+ import { icon } from './icon.jsx'
40
+
41
+ export function Toolbar({ state }) {
42
+ return (
43
+ <div role="toolbar" aria-label="Items">
44
+ <button className="add">{icon(Plus)} Add</button>
45
+ <button className="remove" aria-label="Remove the last item">{icon(Trash2)}</button>
46
+ <span className="status">
47
+ {state.online ? icon(CloudCheck, { label: 'Saved' }) : icon(CloudOff, { label: 'Offline' })}
48
+ </span>
49
+ <span className="count">{state.count} items</span>
50
+ </div>
51
+ )
52
+ }
53
+
54
+ Toolbar.initialState = { count: 0, online: true }
55
+
56
+ Toolbar.intent = ({ DOM }) => ({
57
+ ADD: DOM.click('.add'),
58
+ REMOVE: DOM.click('.remove'),
59
+ })
60
+
61
+ Toolbar.model = {
62
+ ADD: (state) => ({ ...state, count: state.count + 1 }),
63
+ REMOVE: (state) => ({ ...state, count: Math.max(0, state.count - 1) }),
64
+ }
65
+ ```
66
+
67
+ Three cases, three ways to make the icon accessible:
68
+
69
+ | Icon | Example | How |
70
+ |---|---|---|
71
+ | Next to text that says the same | **Add** | Decorative: no label, so the helper sets `aria-hidden="true"`. |
72
+ | The only content of a button or link | **Remove** | The button carries the name (`aria-label`); the icon stays hidden. |
73
+ | Meaning on its own, no text | **Saved / Offline** | Pass `label`: the icon gets `role="img"` and an `aria-label`. |
74
+
75
+ sygnal-check reports a button with no accessible name ([SYG705](https://sygnal.js.org/reference/errors/#syg705)) when the `<svg>` is written inside it, and when its only content is an `icon(...)` call with an icon imported from `lucide` (or another icon package) and no `label`: `<button>{icon(Trash2)}</button>` is reported, `<button aria-label="Remove">{icon(Trash2)}</button>` is not. Only `label` names the icon, as in the helper above (`title` or `aria-label` in the options are ignored by the helper, so they don't count). It knows only a function named `icon` (or `renderIcon`); a helper with another name, or an icon passed through a variable, isn't checked, so assert the button's name in a test as below. If your own `icon` helper renders screen-reader text itself, put the name on the button instead, or silence the line with `// sygnal-ignore SYG705`.
76
+
77
+ ```css live
78
+ .icon { vertical-align: -0.125em; width: 1.25em; height: 1.25em; }
79
+ ```
80
+
81
+ ## Testing
82
+
83
+ ```jsx
84
+ // Toolbar.test.jsx
85
+ import { test, expect } from 'vitest'
86
+ import { renderComponent } from 'sygnal'
87
+ import { Toolbar } from './Toolbar.jsx'
88
+
89
+ test('decorative icons are hidden, a meaningful icon has a name', async () => {
90
+ const t = renderComponent(Toolbar)
91
+ await t.ready()
92
+ expect(t.query('.add svg').getAttribute('aria-hidden')).toBe('true')
93
+ expect(t.query('.remove svg').getAttribute('aria-hidden')).toBe('true')
94
+ expect(t.query('.remove').getAttribute('aria-label')).toBe('Remove the last item')
95
+ expect(t.query('.status svg').getAttribute('role')).toBe('img')
96
+ expect(t.query('.status svg').getAttribute('aria-label')).toBe('Saved')
97
+ expect(t.queryAll('.add svg path').length).toBe(2)
98
+
99
+ t.simulateEvent('.add', 'click')
100
+ await t.next((state) => state.count === 1)
101
+ expect(t.query('.count').textContent).toBe('1 items')
102
+ t.dispose()
103
+ })
104
+ ```
105
+
106
+ ## Size
107
+
108
+ Each icon is a few hundred bytes. Measured with Vite, minified and gzipped, Sygnal not included: the four icons and the helper above add **0.9 KB**. Import icons by name (`import { Plus } from 'lucide'`) so the bundler drops the rest.
109
+
110
+ ## Pitfalls
111
+
112
+ - **Don't look icons up by name at runtime.** `import { icons } from 'lucide'` and `icons[name]` keep every icon in the bundle: 94 KB instead of 0.9. Map the names you need yourself: `const STATUS = { saved: CloudCheck, offline: CloudOff }`.
113
+ - **Don't use `createIcons()`.** It replaces `<i data-lucide="...">` elements in the page with SVG after the fact; Sygnal owns those elements and puts its own back on the next render. `createElement(Plus)` from `lucide` returns a DOM element, which is useful inside a [widget](../widgets.md)'s `mount`, not in a view.
114
+ - **Icon-only buttons need a name.** The icon is hidden from screen readers, so the button must say what it does.
115
+ - **Not `lucide-react`.** The React package returns React elements; use `lucide`, whose icons are plain data.
@@ -0,0 +1,33 @@
1
+ <!-- Generated from docs/src/content/docs/recipes by scripts/copy-guides.mjs; online: https://sygnal.js.org/recipes/ -->
2
+ # Recipes
3
+
4
+ Most JavaScript UI libraries don't need a Sygnal version. A library that builds its UI inside an element you give it becomes a JSX tag with [`defineWidget`](../widgets.md); a library that only computes (a table model, a translation) is called from the view or the context, as a pure function of state. Each recipe shows the code, how to test it, what it adds to the bundle, and the mistakes to avoid.
5
+
6
+ | Recipe | Library | How |
7
+ |---|---|---|
8
+ | [Charts](./charts.md) | Chart.js, ECharts | Widget |
9
+ | [Rich text](./rich-text.md) | Tiptap | Widget, toolbar as element commands |
10
+ | [Code editor](./code-editor.md) | CodeMirror 6 | Widget, `focus` command |
11
+ | [Carousel](./carousel.md) | Embla Carousel | Widget, previous/next as element commands |
12
+ | [Data table](./data-table.md) | TanStack Table v9 | A pure function of state in the view |
13
+ | [Data grid](./data-grid.md) | AG Grid Community | Widget, immutable row data |
14
+ | [Icons](./icons.md) | Lucide | A helper that returns Sygnal SVG vnodes |
15
+ | [Translations (i18n)](./i18n.md) | i18next | `t()` in `.context`, the locale saved with `persist()` |
16
+
17
+ Elsewhere in the docs:
18
+
19
+ - **Positioning** (tooltips, popovers, menus) with Floating UI: [Floating UI for older browsers](../ui/overview.md#floating-ui-for-older-browsers). Current browsers position `sygnal/ui` parts with CSS anchor positioning and need no library.
20
+ - **List animations** with AutoAnimate and Motion: [View Transitions](https://sygnal.js.org/guide/view-transitions/#recipe-autoanimate).
21
+ - **Date pickers** (flatpickr): the [Widgets](../widgets.md) guide's example.
22
+ - **Web components** (Web Awesome, Shoelace): no wrapper needed, see [Web components](../web-components.md).
23
+ - **Forms with validation** (zod, valibot): the [`form` behavior](../forms.md).
24
+
25
+ ## Choosing a pattern
26
+
27
+ - **The library owns some DOM** (a canvas, an editor, a slider): `defineWidget`. Sygnal renders the host element and never touches what the library builds inside it. Props flow in through `update`, events flow out through `dispatch` and the intent, and the model calls the library's methods through the widget's `commands`.
28
+ - **The library only computes** (rows to show, a translated string, an icon's shape): call it in the view or in `.context`, and render the result with JSX. Sygnal keeps rendering the DOM, so the output is ordinary markup: it is server-rendered, tested in the mock DOM, and checked by sygnal-check.
29
+ - **The library is a framework component** (React, Vue): see [Adapters](../adapters.md), and look for a framework-agnostic core first (most headless libraries have one).
30
+
31
+ ## Size numbers
32
+
33
+ Each recipe gives what it adds to an app, measured with Vite (minified, gzipped), Sygnal itself not included. `defineWidget` adds about 1.1 KB the first time an app uses it.
@@ -0,0 +1,154 @@
1
+ <!-- Generated from docs/src/content/docs/recipes by scripts/copy-guides.mjs; online: https://sygnal.js.org/recipes/ -->
2
+ # Rich Text (Tiptap)
3
+
4
+ [Tiptap](https://tiptap.dev/) is a rich-text editor built on ProseMirror. Its core doesn't depend on a framework, so [`defineWidget`](../widgets.md) turns it into a JSX tag: the document is HTML in your state, each edit comes back as an event, and toolbar buttons run the editor's commands through [element commands](../element-commands.md).
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install @tiptap/core @tiptap/pm @tiptap/starter-kit
10
+ ```
11
+
12
+ ## The widget
13
+
14
+ ```js live-file=./RichText.js
15
+ // RichText.js
16
+ import { defineWidget } from 'sygnal'
17
+ import { Editor } from '@tiptap/core'
18
+ import StarterKit from '@tiptap/starter-kit'
19
+
20
+ const editorProps = (props) => ({ attributes: { role: 'textbox', 'aria-multiline': 'true', 'aria-label': props.label } })
21
+
22
+ // the state gets the HTML as Tiptap writes it ('Hi' → '<p>Hi</p>'), so update() doesn't see a
23
+ // difference and reset the content (and the selection) on the next prop change
24
+ const dispatches = new WeakMap()
25
+ const normalise = (editor, html) => {
26
+ if (editor.getHTML() !== html) dispatches.get(editor)('edit', editor.getHTML())
27
+ }
28
+
29
+ export const RichText = defineWidget({
30
+ name: 'RichText',
31
+ mount: (el, props, dispatch) => {
32
+ const editor = new Editor({
33
+ element: el,
34
+ extensions: [StarterKit],
35
+ content: props.html,
36
+ editorProps: editorProps(props),
37
+ onUpdate: ({ editor }) => dispatch('edit', editor.getHTML()),
38
+ })
39
+ dispatches.set(editor, dispatch)
40
+ normalise(editor, props.html)
41
+ return editor
42
+ },
43
+ update: (editor, props) => {
44
+ if (props.html !== editor.getHTML()) {
45
+ editor.commands.setContent(props.html, { emitUpdate: false })
46
+ normalise(editor, props.html)
47
+ }
48
+ // a new label (another language): Tiptap's options are read at mount, so set them again
49
+ if (props.label !== editor.options.editorProps.attributes['aria-label']) editor.setOptions({ editorProps: editorProps(props) })
50
+ },
51
+ unmount: (editor) => editor.destroy(),
52
+ events: ['edit'],
53
+ commands: {
54
+ bold: (editor) => editor.chain().focus().toggleBold().run(),
55
+ italic: (editor) => editor.chain().focus().toggleItalic().run(),
56
+ },
57
+ })
58
+ ```
59
+
60
+ - `mount` creates the editor inside the host `<div>`. Tiptap adds its own editable element there; `editorProps.attributes` labels it.
61
+ - `onUpdate` runs after each change the user makes and sends the new HTML as the `edit` event.
62
+ - `update` gets the HTML from state. When the user typed it, it is what the editor already holds and nothing happens. When the state changed it some other way (a reset, a loaded draft), `setContent` replaces the document, with `emitUpdate: false` so the change isn't sent back as an edit.
63
+ - The `bold` and `italic` commands run Tiptap commands on the instance. `focus()` comes first because clicking a toolbar button moves the focus to the button.
64
+
65
+ ## Using it
66
+
67
+ ```jsx live
68
+ // Notes.jsx
69
+ import { RichText } from './RichText.js'
70
+
71
+ export function Notes({ state }) {
72
+ return (
73
+ <section>
74
+ <div role="toolbar" aria-label="Formatting">
75
+ <button type="button" className="make-bold">Bold</button>
76
+ <button type="button" className="make-italic">Italic</button>
77
+ <button type="button" className="clear">Clear</button>
78
+ </div>
79
+ <RichText className="body" label="Notes" html={state.html} />
80
+ <p className="saved">{state.html.length} characters saved</p>
81
+ </section>
82
+ )
83
+ }
84
+
85
+ Notes.initialState = { html: '<p>Hello</p>' }
86
+
87
+ Notes.intent = ({ DOM }) => ({
88
+ EDIT: DOM.select('.body').events('edit').detail(),
89
+ BOLD: DOM.click('.make-bold'),
90
+ ITALIC: DOM.click('.make-italic'),
91
+ CLEAR: DOM.click('.clear'),
92
+ })
93
+
94
+ Notes.model = {
95
+ EDIT: (state, html) => ({ ...state, html }),
96
+ BOLD: { ELEMENT: { bold: '.body' } },
97
+ ITALIC: { ELEMENT: { italic: '.body' } },
98
+ CLEAR: (state) => ({ ...state, html: '<p></p>' }),
99
+ }
100
+ ```
101
+
102
+ ```css live
103
+ .body .tiptap { border: 1px solid #8888; border-radius: 4px; padding: 0 0.75rem; min-height: 6rem; }
104
+ ```
105
+
106
+ Tiptap brings no styles of its own. Its editable element has the class `tiptap`: give it a border and a height, so it looks like a place to type.
107
+
108
+ The label is a prop of its own (`label`), not `aria-label`: an `aria-label` prop would go on the host `<div>`, and the element a screen reader lands on is Tiptap's editable element inside it. Tiptap reads its options when it starts, so `update` sets them again when the label changes (a translated label after a language switch).
109
+
110
+ Tiptap writes HTML its own way (`Hello` becomes `<p>Hello</p>`). `mount` sends the editor's HTML back as an `edit` when it differs from the `html` prop, so the state holds what the editor holds; otherwise the next prop change (a new label) would see a difference and reset the content.
111
+
112
+ To save the notes across reloads, add [`persist()`](../persistence.md) to the root and pick `html`.
113
+
114
+ ## Testing
115
+
116
+ In the mock DOM, `t.widget('.body')` sends the editor's events, and `t.commands()` lists the element commands the toolbar sent:
117
+
118
+ ```jsx
119
+ // Notes.test.jsx
120
+ import { test, expect } from 'vitest'
121
+ import { renderComponent } from 'sygnal'
122
+ import { Notes } from './Notes.jsx'
123
+
124
+ test('edits reach the state, and the toolbar sends commands to the editor', async () => {
125
+ const t = renderComponent(Notes)
126
+ await t.ready()
127
+ t.widget('.body').dispatch('edit', '<p>Hello <strong>you</strong></p>')
128
+ await t.next((state) => state.html === '<p>Hello <strong>you</strong></p>')
129
+
130
+ t.simulateEvent('.make-bold', 'click')
131
+ t.simulateEvent('.make-italic', 'click')
132
+ await t.settle()
133
+ expect(t.commands()).toEqual([{ bold: '.body' }, { italic: '.body' }])
134
+
135
+ t.simulateEvent('.clear', 'click')
136
+ await t.next((state) => state.html === '<p></p>')
137
+ expect(t.widget('.body').props.html).toBe('<p></p>')
138
+ t.dispose()
139
+ })
140
+ ```
141
+
142
+ Tiptap also starts in jsdom: with `renderComponent(Notes, { dom: 'real' })`, `t.widget('.body').instance` is the editor, and `t.dispose()` destroys it (`editor.isDestroyed`). Typing and selecting text need a real browser (Playwright); there, select a word, click **Bold**, and the state's HTML gets a `<strong>`.
143
+
144
+ ## Size
145
+
146
+ Measured with Vite, minified and gzipped, Sygnal not included: Tiptap with `StarterKit` (paragraphs, headings, lists, code, quotes, marks, history) adds **128 KB**. Pass only the extensions you need instead of `StarterKit` to make it smaller. `defineWidget` adds 1.1 KB for the first widget in an app.
147
+
148
+ ## Pitfalls
149
+
150
+ - **Compare before `setContent`.** Without the `props.html !== editor.getHTML()` check, every keystroke would set the content again and move the cursor to the end.
151
+ - **Tiptap normalises HTML.** `getHTML()` returns the editor's own serialisation (`<p>Hello</p>`, an empty document is `<p></p>`), which may differ from the HTML you started with. Store what `getHTML()` returns; then the comparison in `update` holds. The widget does this for HTML that comes from state: after `mount` and after a `setContent` in `update`, it sends an `edit` with the normalised HTML when it differs, so a later prop change (a new label) doesn't set the content again and lose the selection.
152
+ - **Keep `emitUpdate: false`.** Without it, a reset from state comes back as an `edit` event: harmless here (the same HTML), but a second action for every external change.
153
+ - **Toolbar state.** To show which marks are active (`aria-pressed` on the Bold button), add an `onSelectionUpdate` / `onTransaction` callback that dispatches `editor.isActive('bold')` as an event of its own, and keep it in state.
154
+ - **Untrusted HTML.** Tiptap only keeps what its extensions know, but HTML you save and show elsewhere (`innerHTML`) still needs sanitising there.