sygnal 5.4.0 → 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 (316) hide show
  1. package/CHANGELOG.md +646 -0
  2. package/README.md +24 -9
  3. package/dist/astro/client.cjs.js +16 -9
  4. package/dist/astro/client.cjs.js.map +1 -1
  5. package/dist/astro/client.mjs +16 -9
  6. package/dist/astro/client.mjs.map +1 -1
  7. package/dist/astro/index.cjs.js +362 -91
  8. package/dist/astro/index.cjs.js.map +1 -1
  9. package/dist/astro/index.mjs +362 -92
  10. package/dist/astro/index.mjs.map +1 -1
  11. package/dist/astro/server.cjs.js +3396 -165
  12. package/dist/astro/server.cjs.js.map +1 -1
  13. package/dist/astro/server.mjs +3396 -165
  14. package/dist/astro/server.mjs.map +1 -1
  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 +3032 -296
  20. package/dist/diagnostics.cjs.js.map +1 -1
  21. package/dist/diagnostics.esm.js +3032 -296
  22. package/dist/diagnostics.esm.js.map +1 -1
  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 +13178 -5769
  67. package/dist/index.cjs.js.map +1 -1
  68. package/dist/index.d.ts +2908 -276
  69. package/dist/index.esm.js +13149 -5779
  70. package/dist/index.esm.js.map +1 -1
  71. package/dist/jsx-dev-runtime.cjs.js +26 -334
  72. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  73. package/dist/jsx-dev-runtime.esm.js +19 -327
  74. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  75. package/dist/jsx-runtime.cjs.js +26 -334
  76. package/dist/jsx-runtime.cjs.js.map +1 -1
  77. package/dist/jsx-runtime.esm.js +19 -327
  78. package/dist/jsx-runtime.esm.js.map +1 -1
  79. package/dist/jsx.cjs.js +10 -314
  80. package/dist/jsx.cjs.js.map +1 -1
  81. package/dist/jsx.esm.js +8 -315
  82. package/dist/jsx.esm.js.map +1 -1
  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 +1 -1
  89. package/dist/sygnal.min.js.map +1 -1
  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 +4 -14
  107. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  108. package/dist/vike/ClientOnly.mjs +4 -14
  109. package/dist/vike/ClientOnly.mjs.map +1 -1
  110. package/dist/vike/{+config.js → config/+config.js} +9 -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 +165 -95
  114. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  115. package/dist/vike/onRenderClient.mjs +165 -95
  116. package/dist/vike/onRenderClient.mjs.map +1 -1
  117. package/dist/vike/onRenderHtml.cjs.js +59 -24
  118. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  119. package/dist/vike/onRenderHtml.mjs +60 -25
  120. package/dist/vike/onRenderHtml.mjs.map +1 -1
  121. package/dist/vite/plugin.cjs.js +323 -88
  122. package/dist/vite/plugin.cjs.js.map +1 -1
  123. package/dist/vite/plugin.mjs +323 -89
  124. package/dist/vite/plugin.mjs.map +1 -1
  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 +166 -103
  130. package/package.json +98 -14
  131. package/src/astro/client.ts +27 -16
  132. package/src/astro/index.d.ts +24 -0
  133. package/src/astro/index.ts +69 -3
  134. package/src/astro/server.ts +8 -2
  135. package/src/collection.ts +6 -93
  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 +9 -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 +9 -7
  162. package/src/cycle/dom/controlledInputModule.ts +23 -6
  163. package/src/cycle/dom/enrichEventStream.ts +25 -47
  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 -56
  167. package/src/cycle/dom/mockDOMSource.ts +21 -1
  168. package/src/cycle/dom/modules.ts +16 -3
  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 +19 -3
  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 +101 -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 +1 -1
  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 +20 -11
  201. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  202. package/src/extra/diagnostics/checks/events.ts +17 -2
  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 +97 -4
  206. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  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 +10 -5
  210. package/src/extra/diagnostics/checks/public.d.ts +98 -7
  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 +1 -1
  214. package/src/extra/diagnostics/checks/shared.ts +86 -2
  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 +100 -7
  218. package/src/extra/diagnostics/checks/statics.ts +48 -0
  219. package/src/extra/diagnostics/checks/strict.ts +10 -67
  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 +66 -10
  225. package/src/extra/diagnostics/codes.ts +268 -23
  226. package/src/extra/diagnostics/index.ts +34 -9
  227. package/src/extra/diagnostics/legacy.ts +17 -0
  228. package/src/extra/driverFactories.ts +83 -19
  229. package/src/extra/elementCommands.ts +53 -0
  230. package/src/extra/fetchDriver.ts +519 -0
  231. package/src/extra/focusWithin.ts +27 -0
  232. package/src/extra/form.ts +268 -0
  233. package/src/extra/formHelpers.ts +139 -0
  234. package/src/extra/head.ts +106 -0
  235. package/src/extra/hmr.ts +2 -3
  236. package/src/extra/owned.ts +24 -0
  237. package/src/extra/pager.ts +50 -0
  238. package/src/extra/persist.ts +133 -0
  239. package/src/extra/pwa.ts +1 -1
  240. package/src/extra/queryCache.ts +107 -0
  241. package/src/extra/reducers.ts +1 -1
  242. package/src/extra/reduxDevtools.ts +92 -0
  243. package/src/extra/replies.ts +54 -0
  244. package/src/extra/router.ts +323 -0
  245. package/src/extra/run.ts +90 -217
  246. package/src/extra/selection.ts +76 -0
  247. package/src/extra/socketDriver.ts +265 -0
  248. package/src/extra/sortable.ts +377 -0
  249. package/src/extra/ssr.ts +427 -160
  250. package/src/extra/standardSchema.ts +27 -0
  251. package/src/extra/testing.ts +2433 -175
  252. package/src/extra/timers.ts +95 -0
  253. package/src/extra/undo.ts +261 -0
  254. package/src/extra/viewTransitions.ts +38 -0
  255. package/src/extra/virtual.ts +513 -0
  256. package/src/extra/widget.ts +201 -0
  257. package/src/index.d.ts +2631 -117
  258. package/src/index.ts +25 -4
  259. package/src/jsx-runtime.ts +15 -1
  260. package/src/jsx.ts +2 -1
  261. package/src/lazy.ts +50 -7
  262. package/src/portal.ts +3 -1
  263. package/src/pragma/index.ts +256 -133
  264. package/src/react-peers.d.ts +5 -0
  265. package/src/react.d.ts +43 -0
  266. package/src/react.ts +110 -0
  267. package/src/shared.ts +48 -0
  268. package/src/slot.ts +1 -1
  269. package/src/suspense.ts +3 -1
  270. package/src/switchable.ts +6 -121
  271. package/src/transition.ts +3 -1
  272. package/src/ui/accordion.ts +64 -0
  273. package/src/ui/dialog.ts +129 -0
  274. package/src/ui/disclosure.ts +35 -0
  275. package/src/ui/popover.ts +45 -0
  276. package/src/ui/shared.ts +94 -0
  277. package/src/ui/tabs.ts +82 -0
  278. package/src/ui/toaster.ts +198 -0
  279. package/src/ui/tooltip.ts +70 -0
  280. package/src/ui/zag/combobox.ts +115 -0
  281. package/src/ui/zag/menu.ts +40 -0
  282. package/src/ui/zag/select.ts +54 -0
  283. package/src/ui/zag/shared.ts +43 -0
  284. package/src/ui-combobox.d.ts +20 -0
  285. package/src/ui-combobox.ts +6 -0
  286. package/src/ui-menu.d.ts +30 -0
  287. package/src/ui-menu.ts +6 -0
  288. package/src/ui-select.d.ts +17 -0
  289. package/src/ui-select.ts +6 -0
  290. package/src/ui-zag-types.d.ts +42 -0
  291. package/src/ui.d.ts +208 -0
  292. package/src/ui.ts +15 -0
  293. package/src/vike/+config.ts +9 -1
  294. package/src/vike/ClientOnly.ts +1 -1
  295. package/src/vike/onRenderClient.ts +144 -96
  296. package/src/vike/onRenderHtml.ts +69 -26
  297. package/src/vike/types.ts +7 -1
  298. package/src/vite/globalthis-shim.ts +18 -0
  299. package/src/vite/globalthis.ts +56 -0
  300. package/src/vite/plugin.d.ts +22 -3
  301. package/src/vite/plugin.ts +222 -24
  302. package/src/zag.d.ts +64 -0
  303. package/src/zag.ts +238 -0
  304. package/dist/vike/+config.cjs.js +0 -66
  305. package/dist/vike/+config.cjs.js.map +0 -1
  306. package/dist/vike/+config.js.map +0 -1
  307. package/src/component.ts +0 -2301
  308. package/src/cycle/isolate/index.ts +0 -196
  309. package/src/cycle/run/index.ts +0 -151
  310. package/src/cycle/run/internals.ts +0 -143
  311. package/src/cycle/state/Collection.ts +0 -184
  312. package/src/cycle/state/index.ts +0 -5
  313. package/src/cycle/state/pickCombine.ts +0 -201
  314. package/src/cycle/state/pickMerge.ts +0 -122
  315. package/src/cycle/state/withState.ts +0 -47
  316. package/src/pragma/fn.ts +0 -55
package/src/ui/tabs.ts ADDED
@@ -0,0 +1,82 @@
1
+ /*
2
+ * PLAN-5 2-U (S-4): Tabs, a behavior that wires the host's own markup (WAI-ARIA APG tabs).
3
+ *
4
+ * Settings.uses = { tabs: tabs({ tab: '.tab', selected: 'general' }) }
5
+ * const a = tabsAttrs(state.tabs, uid)
6
+ * <div {...a.list} aria-label="Settings">
7
+ * <button className="tab" {...a.tab('general')}>General</button> …
8
+ * </div>
9
+ * <section {...a.panel('general')}>…</section> …
10
+ *
11
+ * The host renders the tabs and panels; `tabsAttrs` gives each its attributes from the slice:
12
+ * roles, ids (from `uid`, so every instance has its own), aria-selected / aria-controls /
13
+ * aria-labelledby, the roving tabindex, `hidden` on the other panels, and the hooks
14
+ * `data-value` / `data-state` ('active' | 'inactive'). A click on a tab selects it; the arrow
15
+ * keys move the focus between the tabs (and select, unless `activation: 'manual'`), Home / End
16
+ * go to the first / last; disabled tabs are skipped.
17
+ *
18
+ * Options: `tab` (required: the tabs' selector), `selected` (the start; none: the first tab
19
+ * rendered), `orientation` ('horizontal' | 'vertical': which arrow keys), `activation`
20
+ * ('automatic' | 'manual'), `loop` (true), `id` (the ids' prefix; default: the key in `uses`).
21
+ * State: { id, selected, orientation }. Actions: SELECT (a value, or a click), MOVE (a value:
22
+ * focus it, and select it unless manual).
23
+ * G-397: `tabsAttrs(slice, uid, values)` with the tabs' values shows the first one selected when
24
+ * `selected` isn't among them (a removed tab); a host that removes tabs re-selects a neighbour
25
+ * and moves the focus to it in its own reducer (the guide's closable tabs).
26
+ */
27
+ import {ABORT, defineBehavior} from '../index'
28
+ import {idsOf, keyNav, keyed, lit} from './shared'
29
+
30
+ const base = /*#__PURE__*/ defineBehavior({
31
+ initialState: {id: null as null | string, selected: null as null | string, orientation: 'horizontal'},
32
+ intent: ({DOM}: any, {tab, orientation, loop}: any) => {
33
+ const v = orientation == 'vertical'
34
+ return {
35
+ SELECT: DOM.click(tab).data('value'),
36
+ MOVE: keyNav(DOM, tab, [v ? 'ArrowDown' : 'ArrowRight'], [v ? 'ArrowUp' : 'ArrowLeft'], loop),
37
+ }
38
+ },
39
+ model: {
40
+ SELECT: (s: any, v: any) => (v == null || String(v) === s.selected ? ABORT : {...s, selected: String(v)}),
41
+ MOVE: {
42
+ STATE: (s: any, v: string, _n: any, _p: any, o: any) => (o.activation == 'manual' || v === s.selected ? ABORT : {...s, selected: v}),
43
+ ELEMENT: (_s: any, v: string, _n: any, _p: any, o: any) => ({focus: o.tab + '[data-value=' + lit(v) + ']'}),
44
+ },
45
+ },
46
+ })
47
+
48
+ /** Tabs as a behavior over the host's markup: `uses = { tabs: tabs({ tab: '.tab', selected: 'general' }) }`; render with `tabsAttrs`. */
49
+ export const tabs = (options: any = {}): any =>
50
+ keyed(base({...options, ...(options.selected != null && {selected: String(options.selected)})}))
51
+
52
+ /**
53
+ * The attributes of a tab set, from its slice: `list` (the tablist), `tab(value)` and
54
+ * `panel(value)`. Spread them on the host's elements: `<button className="tab" {...a.tab('general')}>`.
55
+ * `values` (optional): the tabs' values in order (leave disabled ones out). With it, a `selected`
56
+ * that isn't one of them (its tab was removed) shows the first one selected (G-397); without it,
57
+ * the first tab rendered is selected only while nothing is.
58
+ */
59
+ export const tabsAttrs = (s: any, uid: any, values?: any[]): any => {
60
+ const id = idsOf(s, uid, 'tabs'), state = (b: boolean) => (b ? 'active' : 'inactive')
61
+ const vs = values && values.map(String)
62
+ let first: any = vs?.[0], sel = s?.selected == null ? null : String(s.selected)
63
+ if (vs && sel != null && !vs.includes(sel)) sel = null
64
+ // no selection (or a removed one): the first value, or the first tab (or panel) rendered
65
+ const on = (v: any) => {
66
+ v = String(v)
67
+ if (sel != null) return v === sel
68
+ first ??= v
69
+ return v === first
70
+ }
71
+ return {
72
+ list: {role: 'tablist', 'aria-orientation': s?.orientation || 'horizontal'},
73
+ tab: (v: any) => {
74
+ const b = on(v)
75
+ return {type: 'button', id: id('tab', v), role: 'tab', 'aria-selected': b, 'aria-controls': id('panel', v), tabindex: b ? '0' : '-1', 'data-value': String(v), 'data-state': state(b)}
76
+ },
77
+ panel: (v: any) => {
78
+ const b = on(v)
79
+ return {id: id('panel', v), role: 'tabpanel', 'aria-labelledby': id('tab', v), tabindex: '0', hidden: !b, 'data-state': state(b)}
80
+ },
81
+ }
82
+ }
@@ -0,0 +1,198 @@
1
+ /*
2
+ * PLAN-5 2-U (T-1, D198, from spike 0-S4): the Toaster component.
3
+ *
4
+ * anywhere: SAVE: { EVENTS: event('TOAST', { text: 'Saved', kind: 'success' }) }
5
+ * once: <Toaster /> (main.js: run(App, { TIMER: makeTimerDriver() }))
6
+ *
7
+ * - TOAST adds { id, text, kind ('info' | 'success' | 'warning' | 'error'; default 'info'),
8
+ * timeoutMs (default 5000; 0 = until dismissed) }. A string is the text. A TOAST with the `id`
9
+ * of a shown toast replaces it in place and restarts its timer ('Saving…' → 'Saved').
10
+ * TOAST_DISMISS removes the toast with that id (no id: all of them). G-398: ids compare as
11
+ * strings (the Collection's keys); an automatic id is 't<n>', never one a shown toast has.
12
+ * - Each toast is a Collection item: its `timers` static auto-dismisses it (GS-7; fake-timer
13
+ * testable), its Dismiss button removes it, Transition animates it (`transition` names the
14
+ * classes, `duration` is how long a leaving toast stays: the leave animation's length).
15
+ * - Two persistent live regions: role="status" (polite) for every kind but errors, role="alert"
16
+ * for errors. They are rendered while empty, so a toast added later is announced.
17
+ * - While the pointer or the focus is in the region, the timers stop (and start over when both
18
+ * have left; G-399: `hover` and `focus` are tracked apart): `pauseOnHover={false}` turns this
19
+ * off. A focused Dismiss button moves the focus to the next toast's (else the previous one's,
20
+ * else where the focus came from) before its toast goes; a focused element removed any other
21
+ * way gets a focusout sent (Chromium sends none), so the region never stays paused.
22
+ * - The region is a `popover="manual"` element (top layer, above the page), re-parented into the
23
+ * topmost open modal <dialog> while one is open and back when it closes or is removed (a
24
+ * MutationObserver in the region's hooks, D198): outside the modal it would be drawn but inert
25
+ * (spike 0-S4). `__sygnalHome` keeps its events in this component (G-356). The region is the
26
+ * only child of a wrapper (`.toaster-home`), so the moved element has no siblings to patch.
27
+ * G-404: the dialogs of the region's own root (a shadow root) are looked at and observed too.
28
+ * G-426: the focus is that root's activeElement (in a shadow root the document's is the host).
29
+ *
30
+ * Class hooks: .toaster-home, .toaster (+ `className`), .toaster-status, .toaster-alert,
31
+ * .toaster-list, .toast, .toast-text, .toast-dismiss. Data attributes: data-kind on .toast,
32
+ * data-paused on .toaster.
33
+ */
34
+ import {ABORT, Collection, Transition, createElement as h, xs} from '../index'
35
+
36
+ // ── the region's placement (D198) ────────────────────────────────────
37
+ // the open modal dialogs, in the order they became modal (the last one is on top)
38
+ const order: any[] = []
39
+ const modal = (d: any) => { try { return d.matches(':modal') } catch (_) { return false } }
40
+ // G-404: the dialogs of the document and of the region's own root (a shadow root: sygnal/element)
41
+ const topModal = (root: any) => {
42
+ const all = [...document.querySelectorAll('dialog')]
43
+ if (root !== document && root?.querySelectorAll) all.push(...root.querySelectorAll('dialog'))
44
+ const open = all.filter(modal)
45
+ for (let i = order.length; i--;) if (!open.includes(order[i])) order.splice(i, 1)
46
+ for (const d of open) if (!order.includes(d)) order.push(d)
47
+ return order[order.length - 1]
48
+ }
49
+ const show = (el: any) => { try { el.matches(':popover-open') || el.showPopover() } catch (_) {} }
50
+ // the element with the focus in `el`'s root
51
+ const act = (el: any) => (el.getRootNode?.() || document).activeElement
52
+ // G-428: a focusout that leaves `t`: not to an element inside it, and with no relatedTarget not
53
+ // while the focus is still inside (the window lost the focus: it comes back to the same element)
54
+ const left = (t: any, e: any) => !(t && t.contains?.(e.relatedTarget || act(t)))
55
+ // a toast on its way out (its Transition's leave classes)
56
+ const leaving = (b: any) => /-leave-/.test(b.closest?.('.toast')?.className || '')
57
+
58
+ const placement = {
59
+ insert: (v: any) => {
60
+ const el = v.elm, home = el.parentNode, root = el.getRootNode?.() || document
61
+ el.__sygnalHome = home
62
+ show(el)
63
+ // G-399: the focus in the region, and where it came from (where it goes back to). G-432: a
64
+ // focus from nowhere (the region's own refocus after a move, the browser's after a modal
65
+ // closed, the window focused again) keeps the last element it came from. G-458: but not one
66
+ // a press in the region gave: the user comes back from nowhere after a click on the page took
67
+ // the focus, and a mouse Dismiss would focus (and scroll to) an element left long ago. (A focus
68
+ // that comes back from another element names that one; a focus that left to a modal comes
69
+ // back from nowhere when it closes and keeps it: G-432)
70
+ // G-499: the press is its time, and counts for a focus within a second (a press released
71
+ // outside, a right-click or a touch scroll gets no focusin or click to clear it); a cancelled
72
+ // one (the browser took it for a scroll) at once
73
+ el.addEventListener('pointerdown', (e: any) => { el._p = e.timeStamp })
74
+ el.addEventListener('pointercancel', () => { el._p = 0 })
75
+ el.addEventListener('focusin', (e: any) => {
76
+ if (!el.contains(e.relatedTarget)) { el._f = 1; el._from = e.relatedTarget || (el._p && e.timeStamp - el._p < 1e3 ? null : el._from) }
77
+ el._p = 0
78
+ })
79
+ el.addEventListener('focusout', (e: any) => { if (left(el, e)) el._f = 0 })
80
+ // a Dismiss button with the focus (keyboard; a click that focused it): the focus moves to the
81
+ // next toast's Dismiss button, else the previous one's, else back where it came from, before
82
+ // the button goes (a removed focused element gets no focusout in Chromium)
83
+ el.addEventListener('click', (e: any) => {
84
+ el._p = 0
85
+ const b = e.target?.closest?.('.toast-dismiss')
86
+ // G-426: the root's activeElement (in a shadow root, the document's is the host)
87
+ if (!b || !b.contains(act(el))) return
88
+ const bs = [...el.querySelectorAll('.toast-dismiss')].filter((x: any) => x === b || !leaving(x)), i = bs.indexOf(b)
89
+ const to: any = bs[i + 1] || bs[i - 1] || (el._from?.isConnected && !el.contains(el._from) && el._from)
90
+ to ? to.focus() : b.blur()
91
+ })
92
+ if (typeof MutationObserver == 'undefined') return
93
+ const place = () => {
94
+ const to = topModal(root) || home
95
+ if (el.parentNode !== to) {
96
+ // a move blurs a focused Dismiss button: give the focus back
97
+ const a = act(el), f: any = el.contains(a) && a
98
+ to.appendChild(el)
99
+ f && f.focus()
100
+ }
101
+ // a moved popover is closed: show it again (on top of the dialog it is in)
102
+ show(el)
103
+ }
104
+ // `open` flips on showModal / close; a dialog removed while open takes the region with it
105
+ const mo = el._mo = new MutationObserver((rs) => {
106
+ // G-399: the focused element was removed (a toast dismissed some other way): no focusout
107
+ // in Chromium, so the region would stay paused; send one
108
+ if (el._f && !el.contains(act(el))) { el._f = 0; el.dispatchEvent(new FocusEvent('focusout', {bubbles: true})) }
109
+ if (home.isConnected && (!el.isConnected || (el.parentNode !== home && !modal(el.parentNode)) || rs.some((r) => r.type == 'attributes'))) place()
110
+ })
111
+ const opts = {subtree: true, childList: true, attributes: true, attributeFilter: ['open']}
112
+ mo.observe(document.body, opts)
113
+ // G-404: a region in a shadow root sees that root's dialogs open and close too
114
+ if (root !== document && root.nodeType == 11) mo.observe(root, opts)
115
+ place()
116
+ },
117
+ // removed with its wrapper while in a dialog: take it out of the dialog too
118
+ destroy: (v: any) => {
119
+ const el = v.elm
120
+ el._mo?.disconnect()
121
+ el.parentNode !== el.__sygnalHome && el.remove()
122
+ },
123
+ }
124
+
125
+ // ── one toast ──────────────────────────────────────────────────────
126
+ // (statics through a /*#__PURE__*/ Object.assign, so every bundler drops an unused Toaster; a
127
+ // componentName, so diagnostics and DevTools name them in minified builds too)
128
+ const ToastItem = /*#__PURE__*/ Object.assign(function ToastItem({state, dismissLabel, transition, duration}: any) {
129
+ return h(Transition, {name: transition, duration},
130
+ h('div', {className: 'toast', 'data-kind': state.kind},
131
+ h('span', {className: 'toast-text'}, state.text),
132
+ h('button', {type: 'button', className: 'toast-dismiss', 'aria-label': dismissLabel + ': ' + state.text}, dismissLabel)))
133
+ }, {
134
+ componentName: 'ToastItem',
135
+ intent: ({DOM}: any) => ({DISMISS: DOM.click('.toast-dismiss')}),
136
+ // a replaced toast (rev) gets a new timer name, so its timer starts over
137
+ timers: (s: any) => ({['expire' + (s?.rev || '')]: s?.timeoutMs > 0 && !s.paused && {after: s.timeoutMs, action: 'EXPIRE'}}),
138
+ model: {DISMISS: () => undefined, EXPIRE: () => undefined},
139
+ })
140
+
141
+ // ── the toaster ──────────────────────────────────────────────────────
142
+ const polite = (t: any) => t.kind != 'error'
143
+ const urgent = (t: any) => t.kind == 'error'
144
+ // entering or leaving the region (not moving between its children)
145
+ const crossing = (e: any) => !e.ownerTarget?.contains?.(e.relatedTarget)
146
+ const paused = (s: any, p: boolean) => ({...s, paused: p, toasts: s.toasts.map((t: any) => ({...t, paused: p}))})
147
+ // the pointer or the focus came in or left: paused while either is in (unless pauseOnHover={false})
148
+ const pause = (s: any, k: string, on: boolean, p: any) => {
149
+ if (s[k] === on) return ABORT
150
+ const n = {...s, [k]: on}, z = p.pauseOnHover !== false && !!(n.hover || n.focus)
151
+ return z === s.paused ? n : paused(n, z)
152
+ }
153
+ // ids compare as strings (the Collection's keys do: 7 and '7' are one toast)
154
+ const same = (a: any, b: any) => '' + a === '' + b
155
+
156
+ /** Renders the toasts that `event('TOAST', { text, kind, timeoutMs })` sends from anywhere. Render it once: `<Toaster />`. */
157
+ export const Toaster = /*#__PURE__*/ Object.assign(function Toaster({state, label = 'Notifications', dismissLabel = 'Dismiss', transition = 'toast', duration = 200, className}: any) {
158
+ const list = (filter: any) => h('div', {className: 'toaster-list'}, h(Collection, {of: ToastItem, from: 'toasts', filter, dismissLabel, transition, duration}))
159
+ return h('div', {className: 'toaster-home'},
160
+ h('section', {
161
+ className: className ? 'toaster ' + className : 'toaster', 'aria-label': label, attrs: {popover: 'manual'},
162
+ 'data-paused': state.paused ? '' : undefined, hook: placement,
163
+ },
164
+ h('div', {className: 'toaster-status', role: 'status', 'aria-live': 'polite'}, list(polite)),
165
+ h('div', {className: 'toaster-alert', role: 'alert'}, list(urgent))))
166
+ }, {
167
+ componentName: 'Toaster',
168
+ isolatedState: true,
169
+ initialState: {toasts: [] as any[], next: 1, paused: false, hover: false, focus: false},
170
+ intent: ({EVENTS, DOM}: any) => ({
171
+ TOAST: EVENTS.select('TOAST'),
172
+ DISMISS: EVENTS.select('TOAST_DISMISS'),
173
+ // G-399: the pointer and the focus apart (the timers stop while either is in the region)
174
+ HOVER: xs.merge(DOM.pointerover('.toaster').filter(crossing).mapTo(true), DOM.pointerout('.toaster').filter(crossing).mapTo(false)),
175
+ FOCUS: xs.merge(DOM.focusin('.toaster').filter(crossing).mapTo(true), DOM.focusout('.toaster').filter((e: any) => left(e.ownerTarget, e)).mapTo(false)),
176
+ }),
177
+ model: {
178
+ TOAST: (s: any, d: any) => {
179
+ const t = typeof d == 'string' ? {text: d} : d || {}
180
+ // G-398: an automatic id is 't<n>', one no shown toast has (a user's 2 or '2' never meets it)
181
+ const i = t.id == null ? -1 : s.toasts.findIndex((x: any) => same(x.id, t.id))
182
+ let n = s.next, id = t.id
183
+ if (id == null) { while (s.toasts.some((x: any) => same(x.id, 't' + n))) n++; id = 't' + n++ }
184
+ const toast = {...t, id, kind: t.kind || 'info', timeoutMs: t.timeoutMs ?? 5000, paused: s.paused, rev: i < 0 ? 0 : s.toasts[i].rev + 1}
185
+ return {
186
+ ...s,
187
+ next: n,
188
+ toasts: i < 0 ? [...s.toasts, toast] : s.toasts.map((x: any, j: number) => (j == i ? toast : x)),
189
+ }
190
+ },
191
+ DISMISS: (s: any, id: any) => {
192
+ const toasts = id == null ? [] : s.toasts.filter((x: any) => !same(x.id, id))
193
+ return toasts.length == s.toasts.length ? ABORT : {...s, toasts}
194
+ },
195
+ HOVER: (s: any, on: boolean, _n: any, p: any) => pause(s, 'hover', on, p),
196
+ FOCUS: (s: any, on: boolean, _n: any, p: any) => pause(s, 'focus', on, p),
197
+ },
198
+ })
@@ -0,0 +1,70 @@
1
+ /*
2
+ * PLAN-5 2-U (S-3, from spike 0-S3): Tooltip, a manual popover placed by CSS anchor positioning,
3
+ * shown on hover / focus after a delay and hidden after a delay (D197 `timers`).
4
+ *
5
+ * Toolbar.uses = { saveTip: tooltip({ trigger: '.save', tip: '.save-tip' }) }
6
+ *
7
+ * The host renders the trigger with aria-describedby (the tip's id) and `anchor-name`, and the
8
+ * tip with role="tooltip", popover="manual", `position-anchor` and `position-area` (styles; see
9
+ * the guide). No JavaScript positioning.
10
+ *
11
+ * Options: `trigger`, `tip` (required), `showDelay` (500 ms), `hideDelay` (100 ms).
12
+ * State: { open, pending: null | 'show' | 'hide', hover, focus }. Actions: ENTER / LEAVE (data
13
+ * 'hover' | 'focus': the pointer on the trigger or the tip (it stays open while hovered, WCAG
14
+ * 1.4.13), or the focus on the trigger), SHOW / HIDE (the timers), ESCAPE (hides it at once),
15
+ * TOGGLED. Timers (joined to the host's `timers`): '<key>.show' / '<key>.hide'; one whose state
16
+ * goes away is cancelled, so leaving before the delay never shows the tip. Needs
17
+ * makeTimerDriver() (tests: fake timers).
18
+ * G-403: the pointer and the focus are tracked apart (it hides once both have left: a mouse
19
+ * leaving doesn't hide a tip the keyboard focus still shows); touch pointer events are ignored
20
+ * (a tap focuses the trigger instead); the document keydown listener for Escape is there only
21
+ * while the tip is shown or about to be, so a page of tooltips has at most the open one's.
22
+ */
23
+ import {ABORT, defineBehavior, dropRepeats, xs} from '../index'
24
+
25
+ const mouse = (e: any) => e?.pointerType != 'touch'
26
+ const which = (k: any) => (k == 'focus' ? 'focus' : 'hover')
27
+
28
+ const base = /*#__PURE__*/ defineBehavior({
29
+ initialState: {open: false, pending: null as null | string, hover: false, focus: false},
30
+ timers: (s: any, o: any) => ({
31
+ show: s?.pending == 'show' && {after: o.showDelay ?? 500, action: 'SHOW'},
32
+ hide: s?.pending == 'hide' && {after: o.hideDelay ?? 100, action: 'HIDE'},
33
+ }),
34
+ intent: ({DOM, STATE}: any, {trigger, tip}: any) => ({
35
+ ENTER: xs.merge(xs.merge(DOM.pointerenter(trigger), DOM.pointerenter(tip)).filter(mouse).mapTo('hover'), DOM.focus(trigger).mapTo('focus')),
36
+ LEAVE: xs.merge(xs.merge(DOM.pointerleave(trigger), DOM.pointerleave(tip)).filter(mouse).mapTo('hover'), DOM.blur(trigger).mapTo('focus')),
37
+ ESCAPE: (STATE ? STATE.stream.map((s: any) => !!(s?.open || s?.pending)).compose(dropRepeats()) : xs.of(true))
38
+ .map((on: boolean) => (on ? DOM.keydown('document').key().filter((k: string) => k == 'Escape') : xs.never()))
39
+ .flatten(),
40
+ TOGGLED: DOM.toggle(tip).map((e: any) => e.newState == 'open'),
41
+ }),
42
+ model: {
43
+ ENTER: (s: any, k: any) => {
44
+ const n = {...s, [which(k)]: true}, pending = s.open ? null : 'show'
45
+ return s[which(k)] && s.pending == pending ? ABORT : {...n, pending}
46
+ },
47
+ LEAVE: (s: any, k: any) => {
48
+ const n = {...s, [which(k)]: false}
49
+ // still hovered or focused: as it was
50
+ const pending = n.hover || n.focus ? s.pending : s.open ? 'hide' : null
51
+ return !s[which(k)] && s.pending == pending ? ABORT : {...n, pending}
52
+ },
53
+ SHOW: {
54
+ STATE: (s: any) => ({...s, pending: null}),
55
+ ELEMENT: (s: any, _d: any, _n: any, _p: any, o: any) => (s.open ? ABORT : {showPopover: o.tip}),
56
+ },
57
+ HIDE: {
58
+ STATE: (s: any) => ({...s, pending: null}),
59
+ ELEMENT: (s: any, _d: any, _n: any, _p: any, o: any) => (s.open ? {hidePopover: o.tip} : ABORT),
60
+ },
61
+ ESCAPE: {
62
+ STATE: (s: any) => (s.open || s.pending ? {...s, pending: null} : ABORT),
63
+ ELEMENT: (s: any, _d: any, _n: any, _p: any, o: any) => (s.open ? {hidePopover: o.tip} : ABORT),
64
+ },
65
+ TOGGLED: (s: any, open: boolean) => (s.open == open ? ABORT : {...s, open}),
66
+ },
67
+ })
68
+
69
+ /** A tooltip as a behavior: `uses = { tip: tooltip({ trigger: '.save', tip: '.save-tip', showDelay: 500 }) }`. */
70
+ export const tooltip = (options: any = {}): any => base({...options, open: false, pending: null, hover: false, focus: false})
@@ -0,0 +1,115 @@
1
+ /*
2
+ * PLAN-5 2-Z (U-1): Combobox, a Zag combobox machine (WAI-ARIA editable combobox with a listbox
3
+ * popup) as a widget tag.
4
+ *
5
+ * <Combobox className="city" label="City" items={CITIES} value={state.city} placeholder="Type a city" />
6
+ * intent: CITY: DOM.select('.city').events('value-change').detail() // 'Paris'
7
+ *
8
+ * Renders a label, the input (role="combobox", aria-autocomplete="list", aria-activedescendant
9
+ * on the highlighted option), a trigger button, and the listbox in a positioner. The items are
10
+ * filtered as the user types: by default the labels that contain the text (case-insensitive);
11
+ * `filter={(item, text) => boolean}` replaces the test, `filter={false}` leaves it to the app (it
12
+ * passes the `items` to show, from `input-change`). The filter text resets when the list closes.
13
+ * Keyboard is Zag's: ArrowDown / ArrowUp open and move, Enter selects, Escape closes, Home / End
14
+ * in the input move the caret. Styling hooks: `data-scope="combobox"` + `data-part` (root, label,
15
+ * control, input, trigger, positioner, content, item, item-text, item-indicator), `data-state`,
16
+ * `data-highlighted`.
17
+ *
18
+ * Forms (G-411): with `name`, hidden inputs submit the value (one per value when `multiple`);
19
+ * the visible input, which shows the label, has no name. `form` goes on the hidden inputs (G-434).
20
+ * With `allowCustomValue` (single), the selected value is submitted unless the user typed since
21
+ * the last selection; then the typed text (an item's exact label submits that item's value; G-435,
22
+ * G-468).
23
+ *
24
+ * Props: `label`, `items` (strings or { value, label?, disabled? }), `value` (controlled: a
25
+ * string, an array when `multiple`, null for none), `defaultValue`, `placeholder`, `filter`, and
26
+ * Zag's combobox props (`multiple`, `name`, `form`, `disabled`, `readOnly`, `required`,
27
+ * `invalid`, `allowCustomValue`, `openOnClick`, `inputBehavior`, `selectionBehavior`,
28
+ * `closeOnSelect`, `loopFocus`, `positioning`, `open`, `id`). Events: `value-change` (the value,
29
+ * or the values when multiple), `input-change` (the text the user typed), `open-change` (boolean).
30
+ * Commands: `open`, `close`, `clear`, `focus` (the input).
31
+ */
32
+ import * as combobox from '@zag-js/combobox'
33
+ import {fromZag} from '../../zag'
34
+ import {createElement as h} from '../../index'
35
+ import {NAMING, arr, collectionOf, named, norm, options, valueOut} from './shared'
36
+
37
+ const shown = (all: any[], f: any, q: string) => {
38
+ if (f === false || !q) return all
39
+ const l = q.toLowerCase()
40
+ return all.filter((i) => (typeof f == 'function' ? f(i, q) : String(i.label).toLowerCase().includes(l)))
41
+ }
42
+
43
+ // the filter text: what the user typed; '' when the list closes
44
+ const setText = (x: any, q: string) => { if (x.q !== q) x.q = q, x.refresh() }
45
+
46
+ // G-411: forms get the value, not the label the input shows: the visible input has no name; one
47
+ // hidden input per value (a single combobox: one, '' when empty) carries `name`.
48
+ // G-435 / G-468: a single combobox with `allowCustomValue` submits the selected value unless the
49
+ // user typed since the last selection (`x.t`: set by input-change, cleared by a selection, a value
50
+ // change or a new controlled value that isn't empty: G-494); then the value of an item whose label the text is, else the
51
+ // text itself. So `selectionBehavior` 'preserve' / 'clear', items relabelled after a pick and a
52
+ // `defaultValue` before the items load all submit the value.
53
+ const submitted = (api: any, x: any): string => {
54
+ const v = api.value[0] ?? ''
55
+ if (!x.custom || !x.t) return v
56
+ const q = api.inputValue
57
+ return x.items.find((i: any) => i.label === q)?.value ?? q
58
+ }
59
+ const hidden = (api: any, p: any, x: any) => {
60
+ if (p.name == null) return null
61
+ const vs = x.multiple ? api.value : [submitted(api, x)]
62
+ // G-434: `form` as an attribute (HTMLInputElement.form is a read-only property)
63
+ const attrs = p.form == null ? undefined : {form: p.form}
64
+ return vs.map((value: string, i: number) => h('input', {key: 'h' + i, type: 'hidden', name: p.name, attrs, disabled: !!p.disabled, value}))
65
+ }
66
+
67
+ export const Combobox: any = /*#__PURE__*/ fromZag(combobox, (api: any, p: any, x: any) => {
68
+ const input = named(api.getInputProps(), p, 1)
69
+ delete input.attrs.name
70
+ delete input.attrs.form
71
+ return h('div', api.getRootProps(),
72
+ p.label != null && h('label', api.getLabelProps(), p.label),
73
+ h('div', api.getControlProps(),
74
+ h('input', {...input, placeholder: p.placeholder}),
75
+ h('button', api.getTriggerProps(), '▾')),
76
+ h('div', api.getPositionerProps(), h('div', named(api.getContentProps(), p), options(h, api))),
77
+ hidden(api, p, x))
78
+ }, {
79
+ name: 'Combobox',
80
+ $own: 1,
81
+ ownProps: [...NAMING, 'name'],
82
+ props: ({items, label, placeholder, value, defaultValue, filter, 'aria-label': _l, 'aria-labelledby': _b, 'aria-describedby': _d, ...p}: any, x: any) => {
83
+ x.multiple = !!p.multiple
84
+ x.custom = !!p.allowCustomValue
85
+ x.items = norm(items)
86
+ // G-468: a new controlled value ends the typing. G-494: not an empty one (an app that clears
87
+ // its value on input-change: the typed text is still what is submitted)
88
+ const v = arr(value), k = v && JSON.stringify(v)
89
+ if (k !== x.v) x.v = k, v?.length && (x.t = 0)
90
+ return {
91
+ ...p,
92
+ collection: collectionOf(combobox, shown(x.items, filter, x.q || '')),
93
+ value: v,
94
+ defaultValue: arr(defaultValue),
95
+ // typing only (not the label a selection writes): the filter text, and input-change
96
+ // G-414: the app's own callbacks (p.onInputValueChange / p.onOpenChange) run first
97
+ onInputValueChange: (d: any) => { p.onInputValueChange?.(d); if (d.reason == 'input-change') x.t = 1, setText(x, d.inputValue), x.dispatch('input-change', d.inputValue) },
98
+ onSelect: (d: any) => { x.t = 0; p.onSelect?.(d) },
99
+ onValueChange: (d: any) => { x.t = 0; p.onValueChange?.(d) },
100
+ onOpenChange: (d: any) => { p.onOpenChange?.(d); d.open || setText(x, '') },
101
+ }
102
+ },
103
+ events: {
104
+ 'value-change': ['onValueChange', valueOut],
105
+ 'input-change': null,
106
+ 'open-change': ['onOpenChange', (d: any) => d.open],
107
+ },
108
+ commands: {
109
+ open: (api: any) => api.setOpen(true),
110
+ close: (api: any) => api.setOpen(false),
111
+ clear: (api: any) => api.clearValue(),
112
+ focus: (api: any) => api.focus(),
113
+ },
114
+ fallback: (p: any, hh: any) => hh('div', null, p.label != null && hh('label', null, p.label), hh('input', {placeholder: p.placeholder, disabled: true})),
115
+ })
@@ -0,0 +1,40 @@
1
+ /*
2
+ * PLAN-5 2-Z (U-1): Menu, a Zag menu machine (WAI-ARIA menu button) as a widget tag.
3
+ *
4
+ * <Menu className="actions" label="Actions" items={[{ value: 'edit', label: 'Edit' }, { value: 'delete', label: 'Delete', disabled: true }]} />
5
+ * intent: PICK: DOM.select('.actions').events('select').detail() // 'edit'
6
+ * model: OPEN_ACTIONS: { ELEMENT: { open: '.actions' } }
7
+ *
8
+ * Renders a trigger button (its text: `label`) and the menu (role="menu", items role="menuitem")
9
+ * in a positioner. Keyboard, focus and typeahead are Zag's: Enter / Space / ArrowDown open it on
10
+ * the first item (ArrowUp on the last), the arrows move, Home / End, typing jumps to an item,
11
+ * Enter / click selects and closes, Escape / Tab / a click outside closes; the focus returns to
12
+ * the trigger. Styling hooks: Zag's `data-scope="menu"` + `data-part` (trigger, positioner,
13
+ * content, item, separator), `data-state`, `data-highlighted`, `data-disabled`.
14
+ *
15
+ * Props: `label`, `items` (strings, or { value, label?, disabled? }, or { separator: true }), and
16
+ * Zag's menu props (`open` controlled, `defaultOpen`, `closeOnSelect`, `loopFocus`, `typeahead`,
17
+ * `positioning`, `dir`, `id`). Events: `select` (the item's value), `open-change` (boolean).
18
+ * Commands: `open`, `close`.
19
+ */
20
+ import * as menu from '@zag-js/menu'
21
+ import {fromZag} from '../../zag'
22
+ import {createElement as h} from '../../index'
23
+ import {NAMING, named, norm} from './shared'
24
+
25
+ export const Menu: any = /*#__PURE__*/ fromZag(menu, (api: any, p: any) => [
26
+ h('button', named(api.getTriggerProps(), {...p, label: 1}, 1), p.label),
27
+ h('div', api.getPositionerProps(),
28
+ h('div', api.getContentProps(),
29
+ norm(p.items).map((i: any) => (i.separator
30
+ ? h('hr', api.getSeparatorProps())
31
+ : h('div', {key: i.value, ...api.getItemProps({value: i.value, disabled: !!i.disabled})}, i.label))))),
32
+ ], {
33
+ name: 'Menu',
34
+ $own: 1,
35
+ ownProps: NAMING,
36
+ props: ({items, label, 'aria-label': _l, 'aria-labelledby': _b, 'aria-describedby': _d, ...p}: any) => p,
37
+ events: {select: ['onSelect', (d: any) => d.value], 'open-change': ['onOpenChange', (d: any) => d.open]},
38
+ commands: {open: (api: any) => api.setOpen(true), close: (api: any) => api.setOpen(false)},
39
+ fallback: (p: any, hh: any) => hh('button', {type: 'button', 'aria-haspopup': 'menu', 'aria-expanded': false}, p.label),
40
+ })
@@ -0,0 +1,54 @@
1
+ /*
2
+ * PLAN-5 2-Z (U-1): Select, a Zag select machine (WAI-ARIA select-only combobox + listbox) as a
3
+ * widget tag.
4
+ *
5
+ * <Select className="size" label="Size" items={['S', 'M', 'L']} value={state.size} placeholder="Pick a size" name="size" />
6
+ * intent: SIZE: DOM.select('.size').events('value-change').detail() // 'M' (multiple: ['M', 'L'])
7
+ *
8
+ * Renders a label, a trigger button (role="combobox", named by the label) showing the selected
9
+ * label or the placeholder, the listbox (items role="option", aria-selected, a ✓ indicator Zag
10
+ * hides when unselected) in a positioner, and a hidden native <select> for forms (`name`).
11
+ * Keyboard and typeahead are Zag's. Styling hooks: `data-scope="select"` + `data-part` (root,
12
+ * label, control, trigger, value-text, indicator, positioner, content, item, item-text,
13
+ * item-indicator), `data-state`, `data-highlighted`, `data-placeholder-shown`.
14
+ *
15
+ * Props: `label`, `items` (strings or { value, label?, disabled? }), `value` (controlled: a
16
+ * string, or an array when `multiple`; null for none), `defaultValue`, `placeholder`, and Zag's
17
+ * select props (`multiple`, `name`, `form`, `disabled`, `required`, `readOnly`, `invalid`,
18
+ * `deselectable`, `closeOnSelect`, `loopFocus`, `positioning`, `open`, `id`). Events:
19
+ * `value-change` (the value, or the values when multiple), `open-change` (boolean).
20
+ * Commands: `open`, `close`, `clear`, `focus` (the trigger).
21
+ */
22
+ import * as select from '@zag-js/select'
23
+ import {fromZag} from '../../zag'
24
+ import {createElement as h} from '../../index'
25
+ import {NAMING, arr, collectionOf, named, norm, options, valueOut} from './shared'
26
+
27
+ export const Select: any = /*#__PURE__*/ fromZag(select, (api: any, p: any, x: any) => {
28
+ const hidden = api.getHiddenSelectProps(), v = api.value
29
+ // multiple: the options' selected props carry the value (a select's value prop is one string)
30
+ if (x.multiple) delete hidden.props.value
31
+ if (p.label == null) delete hidden.attrs['aria-labelledby']
32
+ return h('div', api.getRootProps(),
33
+ p.label != null && h('label', api.getLabelProps(), p.label),
34
+ h('div', api.getControlProps(),
35
+ h('button', named(api.getTriggerProps(), p, 1),
36
+ h('span', api.getValueTextProps(), api.valueAsString || p.placeholder || ''),
37
+ h('span', api.getIndicatorProps(), '▾'))),
38
+ h('div', api.getPositionerProps(), h('div', named(api.getContentProps(), p), options(h, api))),
39
+ h('select', hidden,
40
+ h('option', {value: ''}),
41
+ x.items.map((i: any) => h('option', {key: i.value, value: i.value, selected: v.includes(i.value)}, i.label))))
42
+ }, {
43
+ name: 'Select',
44
+ $own: 1,
45
+ ownProps: [...NAMING, 'name'],
46
+ props: ({items, label, placeholder, value, defaultValue, 'aria-label': _l, 'aria-labelledby': _b, 'aria-describedby': _d, ...p}: any, x: any) => {
47
+ x.items = norm(items)
48
+ x.multiple = !!p.multiple
49
+ return {...p, collection: collectionOf(select, x.items), value: arr(value), defaultValue: arr(defaultValue)}
50
+ },
51
+ events: {'value-change': ['onValueChange', valueOut], 'open-change': ['onOpenChange', (d: any) => d.open]},
52
+ commands: {open: (api: any) => api.setOpen(true), close: (api: any) => api.setOpen(false), clear: (api: any) => api.clearValue(), focus: (api: any) => api.focus()},
53
+ fallback: (p: any, hh: any) => hh('div', null, p.label != null && hh('label', null, p.label), hh('button', {type: 'button', disabled: true}, p.placeholder || '')),
54
+ })
@@ -0,0 +1,43 @@
1
+ /*
2
+ * PLAN-5 2-Z: helpers the Zag-based parts (Menu, Select, Combobox) share. '../../zag' becomes the
3
+ * external 'sygnal/zag' and '../../index' the external 'sygnal' in the 'sygnal/ui/menu', 'sygnal/ui/select' and 'sygnal/ui/combobox' builds.
4
+ */
5
+
6
+ /** G-413: the naming props a part applies to its control (not the host) */
7
+ export const NAMING = ['aria-label', 'aria-labelledby', 'aria-describedby']
8
+
9
+ /**
10
+ * A control's (or listbox's) Zag props named from the part's props: without a label element, no
11
+ * aria-labelledby to it; the props' aria-label / aria-labelledby (/ aria-describedby) win
12
+ */
13
+ export const named = (bag: any, p: any, describe?: any) => {
14
+ const a = bag.attrs
15
+ if (p.label == null) delete a['aria-labelledby']
16
+ for (const k of NAMING) if (p[k] != null && (describe || k != 'aria-describedby')) a[k] = p[k]
17
+ if (p['aria-labelledby'] != null) delete a['aria-label']
18
+ return bag
19
+ }
20
+
21
+ /** An item as the parts take it: a string, or { value, label?, disabled? } (Menu also { separator: true }) */
22
+ export const norm = (items: any): any[] =>
23
+ (items || []).map((i: any) => (i && typeof i == 'object'
24
+ ? {...i, value: String(i.value ?? ''), label: i.label ?? String(i.value ?? '')}
25
+ : {value: String(i), label: String(i)}))
26
+
27
+ /** A Select / Combobox value prop as Zag's string[] (undefined: uncontrolled; null or '': none) */
28
+ export const arr = (v: any): any =>
29
+ v === undefined ? undefined : v == null || v === '' ? [] : Array.isArray(v) ? v.map(String) : [String(v)]
30
+
31
+ /** The listbox collection options for Zag's collection() */
32
+ export const collectionOf = (z: any, items: any[]) =>
33
+ z.collection({items, itemToValue: (i: any) => i.value, itemToString: (i: any) => i.label, isItemDisabled: (i: any) => !!i.disabled})
34
+
35
+ /** value-change's detail: the value (single) or the values (multiple) */
36
+ export const valueOut = (d: any, x: any) => (x.multiple ? d.value : d.value[0] ?? null)
37
+
38
+ /** Item rows of a listbox (Select, Combobox): text + a ✓ indicator Zag hides when unselected */
39
+ export const options = (h: any, api: any) =>
40
+ api.collection.items.map((item: any) =>
41
+ h('div', {key: item.value, ...api.getItemProps({item})},
42
+ h('span', api.getItemTextProps({item}), item.label),
43
+ h('span', api.getItemIndicatorProps({item}), '✓')))
@@ -0,0 +1,20 @@
1
+ // 'sygnal/ui/combobox' (PLAN-5 U-1, D202/D203/D211): Combobox on Zag.js's combobox machine.
2
+ // Needs @zag-js/vanilla and @zag-js/combobox (optional peer dependencies, ~1.45.0).
3
+ import type { Widget } from 'sygnal'
4
+ import type { ZagInstance } from 'sygnal/zag'
5
+ import type { UiItem, UiPositioning, ListboxProps } from './ui-zag-types'
6
+ export type { UiItem, UiPositioning, ListboxProps } from './ui-zag-types'
7
+
8
+ export interface ComboboxProps extends ListboxProps {
9
+ /** The items shown for the typed text: default, labels containing it (case-insensitive); false: none (the app filters `items` from `input-change`) */
10
+ filter?: false | ((item: { value: string; label: string; disabled?: boolean }, text: string) => boolean)
11
+ allowCustomValue?: boolean
12
+ openOnClick?: boolean
13
+ inputBehavior?: 'autohighlight' | 'autocomplete' | 'none'
14
+ selectionBehavior?: 'clear' | 'replace' | 'preserve'
15
+ }
16
+ /**
17
+ * An editable combobox (Zag combobox). Events: `value-change`, `input-change` (the typed text),
18
+ * `open-change`. Commands: `open`, `close`, `clear`, `focus`.
19
+ */
20
+ export const Combobox: Widget<ComboboxProps, ZagInstance, 'value-change' | 'input-change' | 'open-change'>
@@ -0,0 +1,6 @@
1
+ // 'sygnal/ui/combobox' (PLAN-5 U-1, D202/D203/D211): Combobox, a widget tag on Zag.js's combobox machine
2
+ // (fromZag). A subpath per part (D211), so an app that uses Combobox installs only @zag-js/vanilla and
3
+ // @zag-js/combobox (optional peer dependencies, ~1.45.0), and 'sygnal/ui' (the native parts) never
4
+ // needs Zag. Rollup turns '../../zag' into the external 'sygnal/zag' (fromZag ships once when an
5
+ // app uses several parts) and '../../index' into 'sygnal'.
6
+ export {Combobox} from './ui/zag/combobox'