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
@@ -1,76 +1,110 @@
1
1
 
2
2
  import * as is from './is'
3
- import * as fn from './fn'
4
3
 
5
- const createTextElement = (text: any): any => !is.text(text) ? undefined : {
6
- text,
7
- sel: undefined,
8
- data: undefined,
9
- children: undefined,
10
- elm: undefined,
11
- key: undefined
4
+ // P45-B: a vnode this pragma made whose subtree has no component (or other marker the view
5
+ // walk handles), no form field and no vnode from elsewhere. the core's view walk skips it
6
+ // (and, at the root, the whole walk). The flag is on the prototype, not enumerable, so the
7
+ // vnode's own fields are snabbdom's six. Checked by name (`$p`), not by class (a second copy of
8
+ // the core, e.g. Astro's SSR bundle, makes its own).
9
+ function Plain(this: any, sel: any, data: any, children: any, text: any, key: any) {
10
+ this.sel = sel
11
+ this.data = data
12
+ this.children = children
13
+ this.text = text
14
+ this.elm = undefined
15
+ this.key = key
12
16
  }
17
+ Object.defineProperty(Plain.prototype, '$p', { value: 1 })
13
18
 
14
- const applySvg = (vnode: any): any => {
15
- // Skip text vnodes (sel is undefined) and nullish values
16
- if (!vnode || is.undefinedv(vnode.sel)) return vnode
19
+ const createTextElement = (text: any): any => is.text(text) ? new (Plain as any)(undefined, undefined, undefined, text, undefined) : undefined
20
+ // A tag whose tree isn't plain: a form field (a superset of isField: a re-run view with the same
21
+ // output keeps its last vnode (P46-P sameTree), which would skip the controlled-input module's
22
+ // re-sync of the DOM value) or a host the view walk knows by its string tag (<collection>,
23
+ // <switchable>). D196: a custom element (a hyphenated tag) may be a form-associated field
24
+ const FIELD = /^(input|textarea|select)|-/i
25
+ // P46-P: by tag: 1 a form field (FIELD: a prefix), 4 a host (<collection>, <switchable>), 2 an SVG
26
+ // tag. G-350: keyed by the tag part of the selector (before its first '#' or '.'), so dynamic
27
+ // selectors (`li#row-${id}`) don't grow it. A selector with an id or class keeps only bit 1, as
28
+ // P46-P's full-selector tests did (the host and SVG tags match the whole selector)
29
+ const tags: Record<string, number> = Object.create(null)
30
+ const bits = (tag: string): number => tags[tag] = +FIELD.test(tag) | (/^(collection|switchable)$/i.test(tag) ? 4 : 0) | (tag in svgTags ? 2 : 0) | (/-/.test(tag) ? 8 : 0)
31
+ const tagBits = (sel: string): number => {
32
+ const t = tags[sel]
33
+ if (t !== undefined) return t
34
+ let i = 0
35
+ for (let c; i < sel.length && (c = sel.charCodeAt(i)) != 35 && c != 46; i++);
36
+ if (i == sel.length) return bits(sel)
37
+ const tag = sel.slice(0, i)
38
+ return (tags[tag] ?? bits(tag)) & 9
39
+ }
17
40
 
41
+ // Mutates the vnode (and its SVG children, not below a foreignObject: that contains HTML):
42
+ // the props become attributes (className as class, attrs win), and the SVG namespace is set
43
+ const applySvg = (vnode: any): void => {
44
+ // Skip text vnodes (sel is undefined) and nullish values; a vnode this pragma made in the
45
+ // SVG namespace was done, with its subtree, when it was made
46
+ if (!vnode || vnode.sel === undefined || (vnode.$p && vnode.data.ns)) return
18
47
  const data = vnode.data || {}
19
48
  const props = data.props || {}
20
- const propsWithoutClassName = fn.omit('className', props)
21
- const classAttr = props.className !== undefined ? { class: props.className } : {}
22
- const mergedAttrs = fn.assign({}, propsWithoutClassName, classAttr, data.attrs || {})
23
-
24
- return fn.assign(vnode,
25
- { data: fn.omit('props', fn.assign({}, data,
26
- { ns: 'http://www.w3.org/2000/svg', attrs: mergedAttrs }
27
- )) },
28
- // foreignObject contains HTML, not SVG — do not recurse into its children
29
- { children: (!Array.isArray(vnode.children) || vnode.sel === 'foreignObject')
30
- ? vnode.children
31
- : vnode.children.map((child: any) => applySvg(child))
32
- }
33
- )
49
+ const attrs: any = {}
50
+ for (const k in props) if (k != 'className' && props[k] !== undefined) attrs[k] = props[k]
51
+ if (props.className !== undefined) attrs.class = props.className
52
+ for (const k in data.attrs) if (data.attrs[k] !== undefined) attrs[k] = data.attrs[k]
53
+ const svgData = { ...data, ns: 'http://www.w3.org/2000/svg', attrs }
54
+ delete svgData.props
55
+ vnode.data = svgData
56
+ if (Array.isArray(vnode.children) && vnode.sel !== 'foreignObject') vnode.children.forEach(applySvg)
34
57
  }
35
58
 
36
- const considerSvg = (vnode: any): any => !is.svg(vnode) ? vnode : applySvg(vnode)
37
-
38
- const rewrites: Record<string, string | null> = {
39
- for: 'attrs',
40
- role: 'attrs',
41
- tabindex: 'attrs',
42
- 'aria-*': 'attrs',
43
- key: null
59
+ const svgTags: Record<string, number> = {
60
+ // Container / structural
61
+ svg: 1, g: 1, defs: 1, symbol: 1, use: 1,
62
+ // Shape
63
+ circle: 1, ellipse: 1, line: 1, path: 1, polygon: 1, polyline: 1, rect: 1,
64
+ // Text (no HTML collision: HTML has no <text>, <tspan>, or <textPath>)
65
+ text: 1, tspan: 1, textPath: 1,
66
+ // Gradient / paint
67
+ linearGradient: 1, radialGradient: 1, stop: 1, pattern: 1,
68
+ // Clipping / masking
69
+ clipPath: 1, mask: 1,
70
+ // Marker
71
+ marker: 1,
72
+ // Filter primitives
73
+ filter: 1, feBlend: 1, feColorMatrix: 1, feComponentTransfer: 1,
74
+ feComposite: 1, feConvolveMatrix: 1, feDiffuseLighting: 1,
75
+ feDisplacementMap: 1, feDropShadow: 1, feFlood: 1, feGaussianBlur: 1,
76
+ feImage: 1, feMerge: 1, feMergeNode: 1, feMorphology: 1, feOffset: 1,
77
+ fePointLight: 1, feSpecularLighting: 1, feSpotLight: 1, feTile: 1,
78
+ feTurbulence: 1, feFuncR: 1, feFuncG: 1, feFuncB: 1, feFuncA: 1,
79
+ // Descriptive (excluding 'title' — collides with HTML <title>)
80
+ desc: 1, metadata: 1,
81
+ // Other (excluding 'a', 'image', 'style', 'script' — collide with HTML)
82
+ foreignObject: 1, switch: 1,
83
+ // Animation
84
+ animate: 1, animateMotion: 1, animateTransform: 1, set: 1, mpath: 1,
44
85
  }
45
86
 
46
- const rewriteModules = (data: any, modules: Record<string, any>): any => fn.mapObject(data, (key: string, val: any) => {
47
- const inner = { [key]: val }
48
- if (rewrites[key] && modules[rewrites[key] as string] !== undefined) {
49
- return { [rewrites[key] as string]: inner }
50
- }
51
- if (rewrites[key] === null) {
52
- return {}
53
- }
54
- const keys = Object.keys(rewrites)
55
- for (let i = 0; i < keys.length; i++) {
56
- const k = keys[i]
57
- if (k.charAt(k.length - 1) === '*' && key.indexOf(k.slice(0, -1)) === 0 && modules[rewrites[k] as string] !== undefined) {
58
- return { [rewrites[k] as string]: inner }
87
+ // Adds snabbdom hooks to data.hook (a copy); a hook already there runs first (also PLAN-5
88
+ // widget hosts: extra/widget.ts)
89
+ export const chainHooks = (data: any, hooks: Record<string, (...args: any[]) => void>): void => {
90
+ const existing = data.hook || {}
91
+ const hook = { ...existing }
92
+ // G-485 / G-521: a chained hook function is marked `s` (hydration adopts an element whose
93
+ // chained insert has a postpatch: a ref, autoFocus, a widget); `u`, an insert hook of the
94
+ // user's own under it (its element is made again so that insert runs, as an unchained one is)
95
+ if (existing.insert && !existing.insert.s) hook.u = 1
96
+ for (const name in hooks) {
97
+ const f: any = hook[name] = (...args: any[]) => {
98
+ if (existing[name]) existing[name](...args)
99
+ hooks[name](...args)
59
100
  }
101
+ f.s = 1
60
102
  }
61
- if (modules[key] !== undefined) {
62
- return { [modules[key] ? modules[key] : key]: val }
63
- }
64
- if (modules.props !== undefined) {
65
- return { props: inner }
66
- }
67
- return inner
68
- })
103
+ data.hook = hook
104
+ }
69
105
 
70
- const applyFocusProps = (data: any): any => {
71
- if (!data.props) return data
106
+ const applyFocusProps = (data: any): void => {
72
107
  const { autoFocus, autoSelect, ...rest } = data.props
73
- if (!autoFocus && !autoSelect) return data
74
108
 
75
109
  data.props = rest
76
110
  data._autoFocus = !!autoFocus
@@ -87,27 +121,17 @@ const applyFocusProps = (data: any): any => {
87
121
  }
88
122
  }
89
123
 
90
- const existingInsert = data.hook?.insert
91
- const existingPostpatch = data.hook?.postpatch
92
- data.hook = {
93
- ...data.hook,
94
- insert: (vnode: any) => {
95
- if (existingInsert) existingInsert(vnode)
96
- if (autoFocus || autoSelect) doFocus(vnode.elm)
97
- },
124
+ chainHooks(data, {
125
+ insert: (vnode: any) => doFocus(vnode.elm),
98
126
  postpatch: (oldVnode: any, vnode: any) => {
99
- if (existingPostpatch) existingPostpatch(oldVnode, vnode)
100
127
  const wasFocused = oldVnode.data?._autoFocus
101
128
  const isFocused = vnode.data?._autoFocus
102
129
  if (!wasFocused && isFocused) doFocus(vnode.elm)
103
130
  },
104
- }
105
- return data
131
+ })
106
132
  }
107
133
 
108
- const applyRefProps = (data: any, ref: any): any => {
109
- if (!ref) return data
110
-
134
+ const applyRefProps = (data: any, ref: any): void => {
111
135
  const setRef = (elm: any) => {
112
136
  if (typeof ref === 'function') {
113
137
  ref(elm)
@@ -116,41 +140,135 @@ const applyRefProps = (data: any, ref: any): any => {
116
140
  }
117
141
  }
118
142
 
119
- const existingInsert = data.hook?.insert
120
- const existingDestroy = data.hook?.destroy
121
- const existingPostpatch = data.hook?.postpatch
122
- data.hook = {
123
- ...data.hook,
124
- insert: (vnode: any) => {
125
- if (existingInsert) existingInsert(vnode)
126
- setRef(vnode.elm)
127
- },
128
- postpatch: (_oldVnode: any, vnode: any) => {
129
- if (existingPostpatch) existingPostpatch(_oldVnode, vnode)
130
- setRef(vnode.elm)
131
- },
132
- destroy: (vnode: any) => {
133
- if (existingDestroy) existingDestroy(vnode)
134
- setRef(null)
135
- },
143
+ chainHooks(data, {
144
+ insert: (vnode: any) => setRef(vnode.elm),
145
+ postpatch: (_oldVnode: any, vnode: any) => setRef(vnode.elm),
146
+ destroy: () => setRef(null),
147
+ })
148
+ }
149
+
150
+ // B-014: snabbdom's classModule wants a { name: boolean } map. A string ("a b") or an
151
+ // array (['a', cond && 'b']) would be iterated by index, adding the classes "0", "1", ...
152
+ // G-096: clsx-style: strings, nested arrays, objects (truthy keys); falsy entries skipped
153
+ const addClasses = (map: Record<string, boolean>, klass: any): any => {
154
+ if (klass && typeof klass === 'object') for (const k in klass) Array.isArray(klass) ? addClasses(map, klass[k as any]) : klass[k] && addClasses(map, k)
155
+ else if (klass) for (const name of String(klass).split(/\s+/)) if (name) map[name] = true
156
+ return map
157
+ }
158
+
159
+ const toClassMap = (klass: any): any =>
160
+ typeof klass !== 'string' && !Array.isArray(klass) ? klass : addClasses({}, klass)
161
+
162
+ const isObject = (v: any): boolean => is.object(v) && !Array.isArray(v)
163
+
164
+ // P45-B (D146): nested objects are passed by reference. An object with an undefined entry is
165
+ // copied without it (the DOM modules would write "undefined"; the old deep copy dropped it)
166
+ const clean = (obj: any): any => {
167
+ if (isObject(obj)) for (const k in obj) if (obj[k] === undefined) {
168
+ const out: any = {}
169
+ for (const j in obj) if (obj[j] !== undefined) out[j] = obj[j]
170
+ return out
136
171
  }
137
- return data
172
+ return obj
138
173
  }
139
174
 
140
- const sanitizeData = (data: any, modules: Record<string, any>): any => {
141
- const { ref, ...rest } = data
142
- const sanitized = applyFocusProps(rewriteModules(fn.deepifyKeys(rest, modules), modules))
143
- return applyRefProps(sanitized, ref)
175
+ // One pass over the JSX props into snabbdom's module buckets (audit rec 6):
176
+ // - `<module>-<name>` (on-click, attrs-href, data-task-id → dataset.taskId) and `<module>={...}`
177
+ // go to that module's bucket (`data` is snabbdom's `dataset`)
178
+ // - on an element tag, for, role, tabindex, aria-*, form and list (G-463: getter-only DOM
179
+ // properties, which the props module would throw on) and the popover / invoker / anchor attributes
180
+ // (ATTRS) go to attrs (G-370: a component placeholder keeps them as props); an aria-* true is
181
+ // written as "true" (D196: snabbdom writes true as "", which ARIA doesn't read as true), false
182
+ // as "false" for a state whose values include false (G-372: ARIA_FALSE), else (an IDREF, a
183
+ // string, a number) it removes the attribute, as null does; key is dropped; anything else is a
184
+ // prop
185
+ // - an undefined value is skipped (a `<module>-<name>` one still makes its bucket)
186
+ // A bucket is the object passed (`lent`) until a second source adds to it; then it is a copy.
187
+ // sanitizeData doesn't re-enter, so `lent` is per call (null until a bucket is lent).
188
+ let lent: any
189
+ const bucket = (out: any, b: string): any => {
190
+ const o = out[b]
191
+ if (is.object(o) && !lent?.[b]) return o
192
+ if (lent) lent[b] = 0
193
+ return out[b] = is.object(o) ? { ...o } : {}
194
+ }
195
+ const add = (out: any, b: string, v: any): void => {
196
+ if (isObject(out[b]) && isObject(v)) {
197
+ const o = bucket(out, b)
198
+ for (const k in v) if (v[k] !== undefined) o[k] = v[k]
199
+ } else {
200
+ out[b] = clean(v);
201
+ (lent ||= {})[b] = 1
202
+ }
203
+ }
204
+ // P46-P: where a key goes, by key (per modules map; 0: skipped): [bucket, name, kind]. kind 1: a
205
+ // `<module>-<name>` key (its bucket is made even for undefined); 2: a module's own object
206
+ // (3: `class`, made a map first); else the bucket's `name` (no bucket: the data's own key; 4:
207
+ // aria-*, a boolean stringified)
208
+ const ATTRS = /^(for|form|list|role|tabindex|popovertarget(action)?|command(for)?|closedby|interestfor|anchor|aria-.*)$/
209
+ // G-372: the WAI-ARIA 1.2 states and properties whose value set includes "false" (true/false,
210
+ // tristate, and the tokens aria-current / aria-invalid / aria-haspopup), by a prefix no other
211
+ // ARIA 1.2/1.3 attribute has: atomic, busy, checked, current, disabled, expanded, grabbed,
212
+ // haspopup, hidden, invalid, modal, multiline, multiselectable, pressed, readonly, required,
213
+ // selected (sel: not setsize)
214
+ const ARIA_FALSE = /^aria-(at|bu|ch|cu|di|ex|gr|ha|hi|in|mo|mu|pr|re[aq]|sel)/
215
+ // `c`: a component placeholder (G-370: ATTRS stay props)
216
+ const route = (key: string, modules: Record<string, any>, c?: any): any => {
217
+ if (key == 'ref' || key == 'key' || key == 'children') return 0
218
+ const dash = key.indexOf('-')
219
+ const prefix = dash > -1 && key.slice(0, dash)
220
+ // G-152: data-task-id → dataset key taskId (a hyphenated dataset key makes the DOM throw)
221
+ if (prefix && modules[prefix] !== undefined) return [modules[prefix] || prefix, prefix == 'data' ? key.slice(dash + 1).replace(/-([a-z])/g, (_, c) => c.toUpperCase()) : key.slice(dash + 1), 1]
222
+ // G-492: on a custom element (c 2), list is its own property (Lit, sygnal/element). G-519: form
223
+ // stays an attribute (a form-associated element's `form` is a getter; the attribute links it).
224
+ // G-542: there, only a string form (an object is the element's own property: r[3])
225
+ if (c != 1 && modules.attrs !== undefined && ATTRS.test(key) && !(c && key == 'list')) return ['attrs', key, prefix == 'aria' && 4, c && key == 'form']
226
+ if (modules[key] !== undefined) return [modules[key] || key, 0, key == 'class' && modules.class !== undefined ? 3 : 2]
227
+ return [modules.props !== undefined && 'props', key]
228
+ }
229
+ const own = Object.prototype.hasOwnProperty
230
+ const sanitizeData = (data: any, modules: Record<string, any>, routes: Map<string, any>, c?: any): any => {
231
+ const out: any = {}
232
+ lent = null
233
+ for (const key in data) {
234
+ // G-354: own properties only, as the rest-spread copy had (about 1 ns a call)
235
+ if (!own.call(data, key)) continue
236
+ const val = data[key]
237
+ let r = routes.get(key)
238
+ // G-350: capped (data-dependent keys, e.g. `data-${id}`, are routed but not kept)
239
+ if (r === undefined && (r = route(key, modules, c), routes.size < 1024)) routes.set(key, r)
240
+ if (!r) continue
241
+ const [b, name, kind] = r
242
+ if (kind == 1) {
243
+ const o = bucket(out, b)
244
+ if (val !== undefined) o[name] = val
245
+ } else if (val === undefined) continue
246
+ else if (kind & 2) add(out, b, kind == 3 ? toClassMap(val) : val)
247
+ else if (b) bucket(out, r[3] && typeof val != 'string' ? 'props' : b)[name] = kind ? val === !!val ? (val || ARIA_FALSE.test(key)) && '' + val : val ?? false : val
248
+ else out[key] = val
249
+ }
250
+ const props = out.props
251
+ if (props && (props.autoFocus || props.autoSelect)) applyFocusProps(out)
252
+ if (data.ref) applyRefProps(out, data.ref)
253
+ return out
144
254
  }
145
255
 
146
256
  const sanitizeText = (children: any[]): string | undefined => children.length > 1 || !is.text(children[0]) ? undefined : children[0].toString()
147
257
 
148
- const sanitizeChildren = (children: any[]): any[] => fn.reduceDeep(children, (acc: any[], child: any) => {
149
- const vnode = is.vnode(child) ? child : createTextElement(child)
150
- acc.push(vnode)
151
- return acc
258
+ // Flattens the children into `out` (a non-vnode child becomes a text vnode, or undefined);
259
+ // 1 when every child is plain: none, text, or a plain vnode
260
+ const flatten = (children: any[], out: any[]): number => {
261
+ let plain = 1
262
+ for (let i = 0; i < children.length; i++) {
263
+ const child = children[i]
264
+ if (is.array(child)) plain &= flatten(child, out)
265
+ else if (is.vnode(child)) {
266
+ out.push(child)
267
+ if (!child.$p && (child.sel !== undefined || child.children)) plain = 0
268
+ } else out.push(createTextElement(child))
269
+ }
270
+ return plain
152
271
  }
153
- , [])
154
272
 
155
273
  const defaultModules: Record<string, string> = {
156
274
  attrs: '',
@@ -163,59 +281,64 @@ const defaultModules: Record<string, string> = {
163
281
  }
164
282
 
165
283
  export const createElementWithModules = (modules: Record<string, any>) => {
166
- return (sel: any, data: any, ...children: any[]) => {
284
+ // G-370: a component placeholder's keys route without ATTRS, so in their own cache
285
+ const routes = new Map<string, any>(), croutes = new Map<string, any>(), xroutes = new Map<string, any>()
286
+ // the children as one array (`k`: a key that wins over data.key: the JSX runtime's own)
287
+ const ca = (sel: any, data: any, children: any[], k?: any): any => {
167
288
  if (typeof sel === 'undefined') {
168
289
  sel = 'UNDEFINED'
169
- // Pre-formatted (formatDiagnostic output) instead of diagnostics/legacy: the JSX
170
- // runtime entries bundle this file standalone and must not carry a second diagnostics core.
171
- console.error('[Sygnal SYG420] JSX: A JSX tag is undefined, so <UNDEFINED> is rendered instead. Import or define the component in this file. https://sygnal.js.org/reference/errors#syg420')
290
+ // Through the diagnostics bridge (G-044: collected like the other codes), or, with
291
+ // diagnostics not loaded, the same pre-formatted text.
292
+ const msg = 'A JSX tag is undefined, so <UNDEFINED> is rendered instead', fix = 'Import or define the component in this file'
293
+ const core = (globalThis as any).__SYGNAL_DIAGNOSTICS__
294
+ if (core && core.error) core.error('SYG420', 'JSX', msg, fix)
295
+ else console.error(`[Sygnal SYG420] JSX: ${msg}. ${fix}. https://sygnal.js.org/reference/errors#syg420`)
172
296
  }
297
+ // CT-1: a control (src/extra/controls.ts) renders its element itself and stamps its marker.
298
+ // It gets this createElement (the JSX runtime entries use the core's, D188).
173
299
  const isComponent = is.fun(sel)
300
+ if (isComponent && (sel as any).__sygnalControl) return (sel as any).__sygnalControl(data, children, ce)
301
+ // the core instantiates a component from the vnode's `data.c` (the component function, with
302
+ // its statics); `sel` is its name
303
+ let fn: any
174
304
  if (isComponent) {
175
305
  if ((sel as any).__sygnalFragment || sel.name === 'Fragment') {
176
306
  return sel(data || {}, children)
177
307
  }
178
308
  data ||= {}
179
- if (!(sel as any).isSygnalComponent) {
180
- const name = (sel as any).componentName || (sel as any).label || sel.name || 'FUNCTION_COMPONENT'
181
- const view = sel
182
- const { model, intent, hmrActions, context, peers, components, initialState, isolatedState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug, preventInstantiation } = sel as any
183
- if (preventInstantiation) {
184
- // children always stay an array here: Portal/Suspense/... read vnode.children
185
- return considerSvg({
186
- sel: name,
187
- data: data ? sanitizeData(data, modules) : {},
188
- children: sanitizeChildren(children),
189
- text: undefined,
190
- elm: undefined,
191
- key: data ? data.key : undefined
192
- })
193
- }
194
- const options = { name, view, model, intent, hmrActions, context, peers, components, initialState, isolatedState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug }
195
- data.sygnalOptions = options
196
- sel = name
197
- } else {
198
- const factory = sel
199
- sel = (sel as any).componentName || (sel as any).label || sel.name || 'sygnal-factory'
200
- data.sygnalFactory = factory
201
- }
309
+ // preventInstantiation (Portal/Suspense/...): a marker vnode, its function in `data.m` (G-470:
310
+ // how a check tells it from a plain element of that tag), not `data.c`; its children
311
+ // stay an array (they read vnode.children), as for every component below
312
+ fn = sel
313
+ sel = (sel as any).componentName || sel.name || 'FUNCTION_COMPONENT'
202
314
  }
203
315
  // B-011: a vnode is either text-only (`text`, no children) or has a children array,
204
316
  // never both; snabbdom's diff mishandles a vnode with both. A component placeholder
205
317
  // keeps an array so the component receives its text child via `children`.
206
318
  const text = isComponent ? undefined : sanitizeText(children)
207
- return considerSvg({
208
- sel,
209
- data: data ? sanitizeData(data, modules) : {},
210
- children: typeof text !== 'undefined' ? undefined : sanitizeChildren(children),
211
- text,
212
- elm: undefined,
213
- key: data ? data.key : undefined
214
- })
319
+ let kids: any
320
+ const t = tagBits(sel)
321
+ let plain = !isComponent && is.string(sel) && !(t & 5)
322
+ if (typeof text === 'undefined') plain = !!(flatten(children, kids = []) & +plain)
323
+ // G-492: a custom element (bit 8) routes list as a prop, in its own cache
324
+ const d = data ? sanitizeData(data, modules, isComponent ? croutes : t & 8 ? xroutes : routes, isComponent || t & 8 && 2) : {}
325
+ if (fn) d[fn.preventInstantiation ? 'm' : 'c'] = fn
326
+ const key = k !== undefined ? k : data ? data.key : undefined
327
+ const vnode = plain ? new (Plain as any)(sel, d, kids, text, key) : { sel, data: d, children: kids, text, elm: undefined, key }
328
+ if (t & 2) applySvg(vnode)
329
+ return vnode
215
330
  }
331
+ const ce = (sel: any, data: any, ...children: any[]): any => ca(sel, data, children)
332
+ // P46-P: the JSX runtime's element path (its props object is the data as is: sanitizeData
333
+ // skips `children`)
334
+ ;(ce as any).a = ca
335
+ ;(ce as any).$r = routes
336
+ return ce
216
337
  }
217
338
 
218
339
  export const createElement = createElementWithModules(defaultModules)
340
+ /** G-350 (tests): the sizes of the tag cache and of a createElement's route cache */
341
+ export const __cacheSizes = (ce: any) => ({ tags: Object.keys(tags).length, routes: ce.$r.size })
219
342
 
220
343
  export default {
221
344
  createElement,
@@ -0,0 +1,5 @@
1
+ // Build-time declarations for the optional peer dependencies of the 'sygnal/react' entry, so the
2
+ // build doesn't need @types/react. The entry's public types are in react.d.ts.
3
+ declare module 'react' { export const createElement: (type: any, props?: any, ...children: any[]) => any }
4
+ declare module 'react-dom' { export const flushSync: <R>(fn: () => R) => R }
5
+ declare module 'react-dom/client' { export const createRoot: (el: Element) => { render(node: any): void; unmount(): void } }
package/src/react.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ // 'sygnal/react' (PLAN-5 W-2, D203): fromReact, a React component as a widget tag. Needs react
2
+ // and react-dom (or preact/compat through a bundler alias).
3
+ import type { Widget, WidgetDefinition } from 'sygnal'
4
+
5
+ /** The adapter instance (`t.widget(sel).instance` with `dom: 'real'`) */
6
+ export interface ReactInstance {
7
+ /** The React root on the host */
8
+ root: { render(node: any): void; unmount(): void }
9
+ /** The host element */
10
+ el: HTMLElement
11
+ /** The props of the last render */
12
+ props: Record<string, any>
13
+ }
14
+
15
+ export interface FromReactOptions<P = any> {
16
+ /**
17
+ * The callbacks that become DOM events on the host: `{ rate: 'onChange' }` (event name →
18
+ * callback prop), or `['onChange']` (dispatched under the callback's own name). The detail is
19
+ * the callback's argument (an array when it gets more than one).
20
+ */
21
+ events?: Record<string, string> | readonly string[]
22
+ /** The component's props from the widget's (default: all but the host's className, class, id, style, attrs, tabIndex, hidden; D215) */
23
+ props?: (props: P) => Record<string, any>
24
+ /** Element commands, called with the instance */
25
+ commands?: Record<string, (instance: ReactInstance, options: Record<string, any>, el: HTMLElement) => unknown>
26
+ /** The host element (default 'div') */
27
+ tag?: string
28
+ /** A name for diagnostics (default: the component's displayName or name) */
29
+ name?: string
30
+ /** What server rendering puts in the host */
31
+ fallback?: WidgetDefinition['fallback']
32
+ /** More prop names for the host element (also a component-only one: aria-*, role, title) */
33
+ hostProps?: string[]
34
+ /** Prop names that stay off the host and go only to the component (aria-*, role and title always do) */
35
+ ownProps?: string[]
36
+ }
37
+
38
+ /**
39
+ * A React component as a widget tag (an escape hatch): `fromReact(StarRating, { events: { rate: 'onChange' } })`.
40
+ * Render it with a class (`<Stars className="rating" value={state.rating} />`) and read its events
41
+ * in the intent (`DOM.select('.rating').events('rate').detail()`).
42
+ */
43
+ export function fromReact<P = any>(component: any, options?: FromReactOptions<P>): Widget<P, ReactInstance>
package/src/react.ts ADDED
@@ -0,0 +1,110 @@
1
+ /*
2
+ * 'sygnal/react' (PLAN-5 W-2, D203): `fromReact(Component, { events })`, a React component as a
3
+ * widget tag (defineWidget). An escape hatch for the one component you can't replace: context
4
+ * and providers don't cross the boundary, a React portal renders outside the host, and the mock
5
+ * DOM can't run it (test with `dom: 'real'`).
6
+ *
7
+ * const Stars = fromReact(StarRating, { events: { rate: 'onChange' } })
8
+ * // view: <Stars className="rating" value={state.rating} max={5} />
9
+ * // intent: RATE: DOM.select('.rating').events('rate').detail()
10
+ *
11
+ * - Each host is a React root (react-dom/client createRoot). `mount` and `update` render the
12
+ * component with the newest props in flushSync, so the DOM is up to date when they return.
13
+ * - `events`: `{ eventName: 'onCallback' }`, or an array of callback names, each dispatched under
14
+ * its own name (`['onChange']` → 'onChange'; React's names never collide with native events).
15
+ * The detail is the callback's argument (an array when it gets more than one). The component
16
+ * gets a callback for each, and a callback passed as a prop runs first.
17
+ * - Props (D215): `className`, `class`, `id`, `style`, `attrs`, `tabIndex` and `hidden` go to the
18
+ * host only (one tab stop); `aria-*`, `role` and `title` to the component only (a labelled
19
+ * React control gets its own name; the host div is generic); `data-*` to both; the rest to the
20
+ * component (`name`, `placeholder`, `lang`, `dir` also to the host, as for any widget).
21
+ * `ownProps: ['tabIndex']` sends a host prop to the component instead; `hostProps: ['role']`
22
+ * puts a component-only one on the host too. `props` maps the component's props:
23
+ * `props: (p) => ({ ...p, size: 'small' })`.
24
+ * - Unmount: `root.unmount()` in a microtask (React refuses a synchronous unmount while it is
25
+ * rendering, which a callback → action → re-render chain can be in), and only once the host
26
+ * has left the document (G-412: inside a <Transition>, the content stays during the leave),
27
+ * seen in shadow roots too (G-437); a destroyed host still in the DOM after 10 s unmounts then.
28
+ * - SSR: the host with the `fallback` (no React on the server here).
29
+ * - preact/compat: alias `react` and `react-dom` to `preact/compat`, and `react-dom/client` to
30
+ * `preact/compat/client`, in the bundler; the adapter only uses createElement, createRoot and
31
+ * flushSync.
32
+ *
33
+ * Separate entry: './index' becomes the external 'sygnal'; react and react-dom stay external
34
+ * (optional peerDependencies, D209).
35
+ */
36
+ import {createElement} from 'react'
37
+ import {flushSync} from 'react-dom'
38
+ import {createRoot} from 'react-dom/client'
39
+ import {defineWidget} from './index'
40
+
41
+ // D215: the host's own props (the component doesn't get them): tabIndex / hidden too, so a
42
+ // focusable host isn't doubled inside (two tab stops)
43
+ const HOST = /^(className|class|id|style|attrs|tab[iI]ndex|hidden)$/
44
+ // D215 (G-436): the component's own (kept off the host): a React control names itself
45
+ const OWN = /^(aria-|role$|title$)/
46
+
47
+ const fail = (m: string): never => {
48
+ throw new Error(`[Sygnal SYG667] ${m}. https://sygnal.js.org/reference/errors#syg667`)
49
+ }
50
+
51
+ // G-412: run f once the host has left the document. A host still in it is leaving with a delay
52
+ // (a <Transition> leave animation): its React content stays until the element is removed.
53
+ // G-437: the removal can happen inside a shadow root (sygnal/element `shadow: true`), which a
54
+ // document observer doesn't see: every root on the way up (shadow roots, then the document) is
55
+ // observed. A destroyed host that stays in the DOM (a leave whose transitionend never comes, a
56
+ // tree left in place) is unmounted after LATE ms anyway, so no React root or observer leaks.
57
+ const LATE = 10000
58
+ const gone = (el: any, f: () => void) => {
59
+ if (!el.isConnected) return f()
60
+ let t: any
61
+ const done = () => { o.disconnect(); clearTimeout(t); f() }
62
+ const o: MutationObserver = new MutationObserver(() => { if (!el.isConnected) done() })
63
+ for (let r = el.getRootNode(); r; r = r.host?.getRootNode()) o.observe(r, {childList: true, subtree: true})
64
+ t = setTimeout(done, LATE)
65
+ }
66
+
67
+ /**
68
+ * A React component as a widget tag: `fromReact(Comp, { events, props, tag, name, fallback,
69
+ * hostProps, commands })`. `commands` are called with the instance `{ root, el, props }`.
70
+ */
71
+ export function fromReact(Comp: any, options: any = {}): any {
72
+ if (typeof Comp != 'function' && !(Comp && typeof Comp == 'object' && Comp.$$typeof)) {
73
+ fail('fromReact(Component): the first argument is not a React component')
74
+ }
75
+ const o = options, ev = o.events || {}
76
+ // event name → callback prop name
77
+ const events: Record<string, string> = Array.isArray(ev) ? Object.fromEntries(ev.map((c: string) => [c, c])) : ev
78
+ const draw = (i: any, p: any) => {
79
+ const q: any = {}
80
+ for (const k in p) if (!HOST.test(k) || o.ownProps?.includes(k)) q[k] = p[k]
81
+ const rp = o.props ? o.props(q) : q
82
+ for (const name in events) {
83
+ const cb = events[name], own = rp[cb]
84
+ rp[cb] = (...a: any[]) => {
85
+ own?.(...a)
86
+ i.d(name, a.length > 1 ? a : a[0])
87
+ }
88
+ }
89
+ i.props = rp
90
+ flushSync(() => i.root.render(createElement(Comp, rp)))
91
+ }
92
+ return defineWidget({
93
+ tag: o.tag,
94
+ name: o.name || Comp.displayName || Comp.name,
95
+ fallback: o.fallback,
96
+ hostProps: o.hostProps,
97
+ // D215: aria-* / role / title stay off the host unless hostProps names one (the list is the
98
+ // option's; its includes() also matches those patterns, which is all defineWidget asks of it)
99
+ ownProps: Object.assign([...(o.ownProps || [])], {includes: (k: string) => !!o.ownProps?.includes(k) || OWN.test(k) && !o.hostProps?.includes(k)}),
100
+ events: Object.keys(events),
101
+ commands: o.commands,
102
+ mount: (el: any, p: any, d: any) => {
103
+ const i = {root: createRoot(el), el, d, props: null}
104
+ draw(i, p)
105
+ return i
106
+ },
107
+ update: draw,
108
+ unmount: (i: any) => { queueMicrotask(() => gone(i.el, () => i.root.unmount())) },
109
+ })
110
+ }