sygnal 5.4.0 → 6.1.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 (360) hide show
  1. package/CHANGELOG.md +716 -0
  2. package/README.md +24 -9
  3. package/dist/ai.cjs.js +115 -0
  4. package/dist/ai.cjs.js.map +1 -0
  5. package/dist/ai.esm.js +2 -0
  6. package/dist/ai.esm.js.map +1 -0
  7. package/dist/astro/client.cjs.js +16 -9
  8. package/dist/astro/client.cjs.js.map +1 -1
  9. package/dist/astro/client.mjs +16 -9
  10. package/dist/astro/client.mjs.map +1 -1
  11. package/dist/astro/index.cjs.js +958 -92
  12. package/dist/astro/index.cjs.js.map +1 -1
  13. package/dist/astro/index.mjs +958 -93
  14. package/dist/astro/index.mjs.map +1 -1
  15. package/dist/astro/server.cjs.js +3396 -165
  16. package/dist/astro/server.cjs.js.map +1 -1
  17. package/dist/astro/server.mjs +3396 -165
  18. package/dist/astro/server.mjs.map +1 -1
  19. package/dist/devtools.cjs.js +1840 -0
  20. package/dist/devtools.cjs.js.map +1 -0
  21. package/dist/devtools.esm.js +1827 -0
  22. package/dist/devtools.esm.js.map +1 -0
  23. package/dist/diagnostics.cjs.js +3190 -296
  24. package/dist/diagnostics.cjs.js.map +1 -1
  25. package/dist/diagnostics.esm.js +3190 -296
  26. package/dist/diagnostics.esm.js.map +1 -1
  27. package/dist/element.cjs.js +230 -0
  28. package/dist/element.cjs.js.map +1 -0
  29. package/dist/element.esm.js +228 -0
  30. package/dist/element.esm.js.map +1 -0
  31. package/dist/guide/accessibility.md +310 -0
  32. package/dist/guide/adapters.md +221 -0
  33. package/dist/guide/agent.md +588 -0
  34. package/dist/guide/ai-chat.md +693 -0
  35. package/dist/guide/ai-decisions.md +330 -0
  36. package/dist/guide/behaviors.md +312 -0
  37. package/dist/guide/browser-sources.md +203 -0
  38. package/dist/guide/drag-and-drop.md +342 -0
  39. package/dist/guide/element-commands.md +306 -0
  40. package/dist/guide/error-boundaries.md +169 -0
  41. package/dist/guide/forms-reference.md +409 -0
  42. package/dist/guide/forms.md +251 -0
  43. package/dist/guide/http.md +301 -0
  44. package/dist/guide/inputs.md +173 -0
  45. package/dist/guide/mcp-apps.md +272 -0
  46. package/dist/guide/persistence.md +245 -0
  47. package/dist/guide/recipes/ai-form-fill.md +163 -0
  48. package/dist/guide/recipes/ai-summarize.md +155 -0
  49. package/dist/guide/recipes/ai-support-inbox.md +190 -0
  50. package/dist/guide/recipes/carousel.md +155 -0
  51. package/dist/guide/recipes/charts.md +177 -0
  52. package/dist/guide/recipes/code-editor.md +135 -0
  53. package/dist/guide/recipes/data-grid.md +155 -0
  54. package/dist/guide/recipes/data-table.md +196 -0
  55. package/dist/guide/recipes/i18n.md +222 -0
  56. package/dist/guide/recipes/icons.md +115 -0
  57. package/dist/guide/recipes/overview.md +43 -0
  58. package/dist/guide/recipes/rich-text.md +154 -0
  59. package/dist/guide/resources.md +471 -0
  60. package/dist/guide/ssr.md +255 -0
  61. package/dist/guide/timers.md +178 -0
  62. package/dist/guide/ui/accordion.md +93 -0
  63. package/dist/guide/ui/combobox.md +144 -0
  64. package/dist/guide/ui/dialog.md +124 -0
  65. package/dist/guide/ui/disclosure.md +60 -0
  66. package/dist/guide/ui/menu.md +125 -0
  67. package/dist/guide/ui/overview.md +166 -0
  68. package/dist/guide/ui/popover.md +101 -0
  69. package/dist/guide/ui/select.md +114 -0
  70. package/dist/guide/ui/tabs.md +161 -0
  71. package/dist/guide/ui/toaster.md +152 -0
  72. package/dist/guide/ui/tooltip.md +103 -0
  73. package/dist/guide/undo.md +178 -0
  74. package/dist/guide/virtual-collections.md +183 -0
  75. package/dist/guide/web-components.md +179 -0
  76. package/dist/guide/webmcp.md +226 -0
  77. package/dist/guide/widgets.md +217 -0
  78. package/dist/index.cjs.js +19288 -7408
  79. package/dist/index.cjs.js.map +1 -1
  80. package/dist/index.d.ts +4466 -552
  81. package/dist/index.esm.js +19274 -7461
  82. package/dist/index.esm.js.map +1 -1
  83. package/dist/jsx-dev-runtime.cjs.js +26 -334
  84. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  85. package/dist/jsx-dev-runtime.esm.js +19 -327
  86. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  87. package/dist/jsx-runtime.cjs.js +26 -334
  88. package/dist/jsx-runtime.cjs.js.map +1 -1
  89. package/dist/jsx-runtime.esm.js +19 -327
  90. package/dist/jsx-runtime.esm.js.map +1 -1
  91. package/dist/jsx.cjs.js +10 -314
  92. package/dist/jsx.cjs.js.map +1 -1
  93. package/dist/jsx.esm.js +8 -315
  94. package/dist/jsx.esm.js.map +1 -1
  95. package/dist/react.cjs.js +117 -0
  96. package/dist/react.cjs.js.map +1 -0
  97. package/dist/react.esm.js +115 -0
  98. package/dist/react.esm.js.map +1 -0
  99. package/dist/shims/globalthis.cjs +20 -0
  100. package/dist/sygnal.min.js +1 -1
  101. package/dist/sygnal.min.js.map +1 -1
  102. package/dist/ui-combobox.cjs.js +169 -0
  103. package/dist/ui-combobox.cjs.js.map +1 -0
  104. package/dist/ui-combobox.esm.js +148 -0
  105. package/dist/ui-combobox.esm.js.map +1 -0
  106. package/dist/ui-menu.cjs.js +87 -0
  107. package/dist/ui-menu.cjs.js.map +1 -0
  108. package/dist/ui-menu.esm.js +66 -0
  109. package/dist/ui-menu.esm.js.map +1 -0
  110. package/dist/ui-select.cjs.js +104 -0
  111. package/dist/ui-select.cjs.js.map +1 -0
  112. package/dist/ui-select.esm.js +83 -0
  113. package/dist/ui-select.esm.js.map +1 -0
  114. package/dist/ui.cjs.js +738 -0
  115. package/dist/ui.cjs.js.map +1 -0
  116. package/dist/ui.esm.js +727 -0
  117. package/dist/ui.esm.js.map +1 -0
  118. package/dist/vike/ClientOnly.cjs.js +4 -14
  119. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  120. package/dist/vike/ClientOnly.mjs +4 -14
  121. package/dist/vike/ClientOnly.mjs.map +1 -1
  122. package/dist/vike/{+config.js → config/+config.js} +9 -1
  123. package/dist/vike/config/+config.js.map +1 -0
  124. package/dist/vike/config/package.json +6 -0
  125. package/dist/vike/onRenderClient.cjs.js +165 -95
  126. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  127. package/dist/vike/onRenderClient.mjs +165 -95
  128. package/dist/vike/onRenderClient.mjs.map +1 -1
  129. package/dist/vike/onRenderHtml.cjs.js +59 -24
  130. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  131. package/dist/vike/onRenderHtml.mjs +60 -25
  132. package/dist/vike/onRenderHtml.mjs.map +1 -1
  133. package/dist/vite/plugin.cjs.js +919 -89
  134. package/dist/vite/plugin.cjs.js.map +1 -1
  135. package/dist/vite/plugin.mjs +919 -90
  136. package/dist/vite/plugin.mjs.map +1 -1
  137. package/dist/zag.cjs.js +408 -0
  138. package/dist/zag.cjs.js.map +1 -0
  139. package/dist/zag.esm.js +405 -0
  140. package/dist/zag.esm.js.map +1 -0
  141. package/llms.txt +172 -103
  142. package/package.json +112 -14
  143. package/src/ai.d.ts +1186 -0
  144. package/src/ai.ts +23 -0
  145. package/src/astro/client.ts +27 -16
  146. package/src/astro/index.d.ts +24 -0
  147. package/src/astro/index.ts +69 -3
  148. package/src/astro/server.ts +8 -2
  149. package/src/collection.ts +6 -93
  150. package/src/core/actions.ts +134 -0
  151. package/src/core/cell.ts +202 -0
  152. package/src/core/debug.ts +23 -0
  153. package/src/core/define.ts +242 -0
  154. package/src/core/hooks.ts +314 -0
  155. package/src/core/hosts/collection.ts +325 -0
  156. package/src/core/hosts/switchable.ts +146 -0
  157. package/src/core/instance.ts +592 -0
  158. package/src/core/markers/clientonly.ts +13 -0
  159. package/src/core/markers/lazy.ts +40 -0
  160. package/src/core/markers/portal.ts +96 -0
  161. package/src/core/markers/suspense.ts +41 -0
  162. package/src/core/markers/transition.ts +70 -0
  163. package/src/core/registry.ts +25 -0
  164. package/src/core/runtime.ts +573 -0
  165. package/src/core/statics.ts +104 -0
  166. package/src/core/teardown.ts +60 -0
  167. package/src/core/view.ts +37 -0
  168. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  169. package/src/cycle/dom/ElementFinder.ts +7 -9
  170. package/src/cycle/dom/EventDelegator.ts +174 -223
  171. package/src/cycle/dom/IsolateModule.ts +65 -35
  172. package/src/cycle/dom/MainDOMSource.ts +9 -2
  173. package/src/cycle/dom/PriorityQueue.ts +4 -2
  174. package/src/cycle/dom/SymbolTree.ts +18 -47
  175. package/src/cycle/dom/classNameModule.ts +9 -7
  176. package/src/cycle/dom/controlledInputModule.ts +23 -6
  177. package/src/cycle/dom/enrichEventStream.ts +25 -47
  178. package/src/cycle/dom/fragment.ts +7 -0
  179. package/src/cycle/dom/isolate.ts +30 -27
  180. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  181. package/src/cycle/dom/mockDOMSource.ts +21 -1
  182. package/src/cycle/dom/modules.ts +16 -3
  183. package/src/cycle/dom/propsModule.ts +65 -0
  184. package/src/cycle/dom/selectModule.ts +22 -17
  185. package/src/cycle/dom/snabbdom.ts +1 -6
  186. package/src/cycle/dom/thunk.ts +3 -0
  187. package/src/cycle/dom/utils.ts +98 -0
  188. package/src/cycle/dom/viewTransition.ts +43 -0
  189. package/src/cycle/state/StateSource.ts +19 -3
  190. package/src/cycle/state/objIsEqual.ts +50 -0
  191. package/src/cycle/state/types.ts +0 -10
  192. package/src/defineComponent.ts +34 -0
  193. package/src/devtools.d.ts +170 -0
  194. package/src/devtools.ts +37 -0
  195. package/src/element.d.ts +59 -0
  196. package/src/element.ts +235 -0
  197. package/src/extra/ai/agent/index.ts +484 -0
  198. package/src/extra/ai/answers.ts +115 -0
  199. package/src/extra/ai/chat/behavior.ts +336 -0
  200. package/src/extra/ai/chat/driver.ts +271 -0
  201. package/src/extra/ai/chat/memoryTransport.ts +61 -0
  202. package/src/extra/ai/chat/output.ts +37 -0
  203. package/src/extra/ai/commandBar.ts +281 -0
  204. package/src/extra/ai/decide.ts +104 -0
  205. package/src/extra/ai/index.ts +42 -0
  206. package/src/extra/ai/link.ts +90 -0
  207. package/src/extra/ai/mcpApp.ts +314 -0
  208. package/src/extra/ai/messages.ts +51 -0
  209. package/src/extra/ai/schema/index.ts +166 -0
  210. package/src/extra/ai/schema/jsonSchema.ts +72 -0
  211. package/src/extra/ai/schema/strict.ts +152 -0
  212. package/src/extra/ai/transports/agui.ts +177 -0
  213. package/src/extra/ai/transports/anthropicMessages.ts +175 -0
  214. package/src/extra/ai/transports/chatCompletions.ts +105 -0
  215. package/src/extra/ai/transports/chromePrompt.ts +77 -0
  216. package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
  217. package/src/extra/ai/transports/fromAISDK.ts +126 -0
  218. package/src/extra/ai/transports/openResponses.ts +105 -0
  219. package/src/extra/ai/transports/shared.ts +125 -0
  220. package/src/extra/ai/transports/tools.ts +59 -0
  221. package/src/extra/ai/transports/uiMessageStream.ts +107 -0
  222. package/src/extra/ai/webmcp.ts +260 -0
  223. package/src/extra/backoff.ts +9 -0
  224. package/src/extra/behaviors.ts +169 -0
  225. package/src/extra/browserSignals.ts +23 -0
  226. package/src/extra/browserSources.ts +326 -0
  227. package/src/extra/command.ts +3 -3
  228. package/src/extra/controls.ts +47 -0
  229. package/src/extra/copyAsTest.ts +286 -0
  230. package/src/extra/devMcp.ts +386 -0
  231. package/src/extra/devtools.ts +101 -31
  232. package/src/extra/devtoolsActions.ts +379 -0
  233. package/src/extra/devtoolsHook.ts +9 -0
  234. package/src/extra/devtoolsNext.ts +83 -0
  235. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  236. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  237. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  238. package/src/extra/diagnostics/checks/chat.ts +71 -0
  239. package/src/extra/diagnostics/checks/collections.ts +1 -1
  240. package/src/extra/diagnostics/checks/controls.ts +148 -0
  241. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  242. package/src/extra/diagnostics/checks/dom.ts +20 -11
  243. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  244. package/src/extra/diagnostics/checks/events.ts +17 -2
  245. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  246. package/src/extra/diagnostics/checks/forms.ts +171 -0
  247. package/src/extra/diagnostics/checks/index.ts +103 -4
  248. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  249. package/src/extra/diagnostics/checks/next.ts +417 -0
  250. package/src/extra/diagnostics/checks/persist.ts +54 -0
  251. package/src/extra/diagnostics/checks/props.ts +10 -5
  252. package/src/extra/diagnostics/checks/public.d.ts +115 -7
  253. package/src/extra/diagnostics/checks/replies.ts +128 -0
  254. package/src/extra/diagnostics/checks/router.ts +100 -0
  255. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  256. package/src/extra/diagnostics/checks/shared.ts +86 -2
  257. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  258. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  259. package/src/extra/diagnostics/checks/state.ts +100 -7
  260. package/src/extra/diagnostics/checks/statics.ts +48 -0
  261. package/src/extra/diagnostics/checks/strict.ts +10 -67
  262. package/src/extra/diagnostics/checks/timers.ts +80 -0
  263. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  264. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  265. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  266. package/src/extra/diagnostics/checks/wiring.ts +69 -10
  267. package/src/extra/diagnostics/codes.ts +335 -23
  268. package/src/extra/diagnostics/index.ts +34 -9
  269. package/src/extra/diagnostics/legacy.ts +17 -0
  270. package/src/extra/driverFactories.ts +83 -19
  271. package/src/extra/elementCommands.ts +53 -0
  272. package/src/extra/fetchDriver.ts +519 -0
  273. package/src/extra/focusWithin.ts +27 -0
  274. package/src/extra/form.ts +284 -0
  275. package/src/extra/formHelpers.ts +139 -0
  276. package/src/extra/formTool.ts +150 -0
  277. package/src/extra/head.ts +106 -0
  278. package/src/extra/hmr.ts +2 -3
  279. package/src/extra/owned.ts +24 -0
  280. package/src/extra/pager.ts +50 -0
  281. package/src/extra/persist.ts +133 -0
  282. package/src/extra/pwa.ts +1 -1
  283. package/src/extra/queryCache.ts +107 -0
  284. package/src/extra/reducers.ts +1 -1
  285. package/src/extra/reduxDevtools.ts +92 -0
  286. package/src/extra/replies.ts +54 -0
  287. package/src/extra/router.ts +323 -0
  288. package/src/extra/run.ts +90 -217
  289. package/src/extra/selection.ts +76 -0
  290. package/src/extra/socketDriver.ts +265 -0
  291. package/src/extra/sortable.ts +377 -0
  292. package/src/extra/ssr.ts +427 -160
  293. package/src/extra/standardSchema.ts +27 -0
  294. package/src/extra/testing.ts +2550 -175
  295. package/src/extra/timers.ts +95 -0
  296. package/src/extra/undo.ts +261 -0
  297. package/src/extra/viewTransitions.ts +38 -0
  298. package/src/extra/virtual.ts +513 -0
  299. package/src/extra/widget.ts +201 -0
  300. package/src/index.d.ts +2754 -127
  301. package/src/index.ts +45 -4
  302. package/src/jsx-runtime.ts +15 -1
  303. package/src/jsx.ts +2 -1
  304. package/src/lazy.ts +50 -7
  305. package/src/portal.ts +3 -1
  306. package/src/pragma/index.ts +256 -133
  307. package/src/react-peers.d.ts +5 -0
  308. package/src/react.d.ts +43 -0
  309. package/src/react.ts +110 -0
  310. package/src/shared.ts +57 -0
  311. package/src/slot.ts +1 -1
  312. package/src/suspense.ts +3 -1
  313. package/src/switchable.ts +6 -121
  314. package/src/transition.ts +3 -1
  315. package/src/ui/accordion.ts +64 -0
  316. package/src/ui/dialog.ts +129 -0
  317. package/src/ui/disclosure.ts +35 -0
  318. package/src/ui/popover.ts +45 -0
  319. package/src/ui/shared.ts +94 -0
  320. package/src/ui/tabs.ts +82 -0
  321. package/src/ui/toaster.ts +198 -0
  322. package/src/ui/tooltip.ts +70 -0
  323. package/src/ui/zag/combobox.ts +115 -0
  324. package/src/ui/zag/menu.ts +40 -0
  325. package/src/ui/zag/select.ts +54 -0
  326. package/src/ui/zag/shared.ts +43 -0
  327. package/src/ui-combobox.d.ts +20 -0
  328. package/src/ui-combobox.ts +6 -0
  329. package/src/ui-menu.d.ts +30 -0
  330. package/src/ui-menu.ts +6 -0
  331. package/src/ui-select.d.ts +17 -0
  332. package/src/ui-select.ts +6 -0
  333. package/src/ui-zag-types.d.ts +42 -0
  334. package/src/ui.d.ts +208 -0
  335. package/src/ui.ts +15 -0
  336. package/src/vike/+config.ts +9 -1
  337. package/src/vike/ClientOnly.ts +1 -1
  338. package/src/vike/onRenderClient.ts +144 -96
  339. package/src/vike/onRenderHtml.ts +69 -26
  340. package/src/vike/types.ts +7 -1
  341. package/src/vite/globalthis-shim.ts +18 -0
  342. package/src/vite/globalthis.ts +56 -0
  343. package/src/vite/mcp.ts +568 -0
  344. package/src/vite/plugin.d.ts +47 -3
  345. package/src/vite/plugin.ts +262 -25
  346. package/src/zag.d.ts +64 -0
  347. package/src/zag.ts +238 -0
  348. package/dist/vike/+config.cjs.js +0 -66
  349. package/dist/vike/+config.cjs.js.map +0 -1
  350. package/dist/vike/+config.js.map +0 -1
  351. package/src/component.ts +0 -2301
  352. package/src/cycle/isolate/index.ts +0 -196
  353. package/src/cycle/run/index.ts +0 -151
  354. package/src/cycle/run/internals.ts +0 -143
  355. package/src/cycle/state/Collection.ts +0 -184
  356. package/src/cycle/state/index.ts +0 -5
  357. package/src/cycle/state/pickCombine.ts +0 -201
  358. package/src/cycle/state/pickMerge.ts +0 -122
  359. package/src/cycle/state/withState.ts +0 -47
  360. package/src/pragma/fn.ts +0 -55
@@ -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,43 @@
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
+ ## AI
26
+
27
+ Recipes for [`sygnal/ai`](../ai-chat.md): models through the chat driver and decision requests, no library beyond a schema validator.
28
+
29
+ | Recipe | Model | How |
30
+ |---|---|---|
31
+ | [Support inbox](./ai-support-inbox.md) | A decision model, a chat model for the unsure tickets | A `decide()` resource per Collection item, escalation with structured output |
32
+ | [Structured output into a form](./ai-form-fill.md) | A chat model | `output` schema, `form.CHANGE` per extracted field |
33
+ | [On-device summaries](./ai-summarize.md) | Chrome's built-in model | `chromePrompt()`, an excerpt where it is unavailable |
34
+
35
+ ## Choosing a pattern
36
+
37
+ - **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`.
38
+ - **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.
39
+ - **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).
40
+
41
+ ## Size numbers
42
+
43
+ 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.