sygnal 5.3.7 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +646 -0
- package/README.md +77 -47
- package/dist/astro/client.cjs.js +33 -8114
- package/dist/astro/client.cjs.js.map +1 -0
- package/dist/astro/client.mjs +33 -8114
- package/dist/astro/client.mjs.map +1 -0
- package/dist/astro/index.cjs.js +1039 -9
- package/dist/astro/index.cjs.js.map +1 -0
- package/dist/astro/index.mjs +1038 -9
- package/dist/astro/index.mjs.map +1 -0
- package/dist/astro/server.cjs.js +3400 -166
- package/dist/astro/server.cjs.js.map +1 -0
- package/dist/astro/server.mjs +3400 -166
- package/dist/astro/server.mjs.map +1 -0
- package/dist/devtools.cjs.js +1431 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.esm.js +1419 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/diagnostics.cjs.js +4033 -0
- package/dist/diagnostics.cjs.js.map +1 -0
- package/dist/diagnostics.esm.js +4003 -0
- package/dist/diagnostics.esm.js.map +1 -0
- package/dist/element.cjs.js +230 -0
- package/dist/element.cjs.js.map +1 -0
- package/dist/element.esm.js +228 -0
- package/dist/element.esm.js.map +1 -0
- package/dist/guide/accessibility.md +257 -0
- package/dist/guide/adapters.md +221 -0
- package/dist/guide/behaviors.md +312 -0
- package/dist/guide/browser-sources.md +203 -0
- package/dist/guide/drag-and-drop.md +342 -0
- package/dist/guide/element-commands.md +306 -0
- package/dist/guide/error-boundaries.md +169 -0
- package/dist/guide/forms-reference.md +381 -0
- package/dist/guide/forms.md +250 -0
- package/dist/guide/http.md +299 -0
- package/dist/guide/inputs.md +173 -0
- package/dist/guide/persistence.md +245 -0
- package/dist/guide/recipes/carousel.md +155 -0
- package/dist/guide/recipes/charts.md +177 -0
- package/dist/guide/recipes/code-editor.md +135 -0
- package/dist/guide/recipes/data-grid.md +155 -0
- package/dist/guide/recipes/data-table.md +196 -0
- package/dist/guide/recipes/i18n.md +222 -0
- package/dist/guide/recipes/icons.md +115 -0
- package/dist/guide/recipes/overview.md +33 -0
- package/dist/guide/recipes/rich-text.md +154 -0
- package/dist/guide/resources.md +471 -0
- package/dist/guide/ssr.md +255 -0
- package/dist/guide/timers.md +178 -0
- package/dist/guide/ui/accordion.md +93 -0
- package/dist/guide/ui/combobox.md +144 -0
- package/dist/guide/ui/dialog.md +124 -0
- package/dist/guide/ui/disclosure.md +60 -0
- package/dist/guide/ui/menu.md +125 -0
- package/dist/guide/ui/overview.md +166 -0
- package/dist/guide/ui/popover.md +101 -0
- package/dist/guide/ui/select.md +114 -0
- package/dist/guide/ui/tabs.md +161 -0
- package/dist/guide/ui/toaster.md +152 -0
- package/dist/guide/ui/tooltip.md +103 -0
- package/dist/guide/undo.md +178 -0
- package/dist/guide/virtual-collections.md +183 -0
- package/dist/guide/web-components.md +179 -0
- package/dist/guide/widgets.md +217 -0
- package/dist/index.cjs.js +15283 -6279
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +3739 -370
- package/dist/index.esm.js +15230 -6274
- package/dist/index.esm.js.map +1 -0
- package/dist/jsx-dev-runtime.cjs.js +27 -329
- package/dist/jsx-dev-runtime.cjs.js.map +1 -0
- package/dist/jsx-dev-runtime.esm.js +20 -322
- package/dist/jsx-dev-runtime.esm.js.map +1 -0
- package/dist/jsx-runtime.cjs.js +27 -329
- package/dist/jsx-runtime.cjs.js.map +1 -0
- package/dist/jsx-runtime.esm.js +20 -322
- package/dist/jsx-runtime.esm.js.map +1 -0
- package/dist/jsx.cjs.js +11 -309
- package/dist/jsx.cjs.js.map +1 -0
- package/dist/jsx.esm.js +9 -310
- package/dist/jsx.esm.js.map +1 -0
- package/dist/react.cjs.js +117 -0
- package/dist/react.cjs.js.map +1 -0
- package/dist/react.esm.js +115 -0
- package/dist/react.esm.js.map +1 -0
- package/dist/shims/globalthis.cjs +20 -0
- package/dist/sygnal.min.js +2 -1
- package/dist/sygnal.min.js.map +1 -0
- package/dist/ui-combobox.cjs.js +169 -0
- package/dist/ui-combobox.cjs.js.map +1 -0
- package/dist/ui-combobox.esm.js +148 -0
- package/dist/ui-combobox.esm.js.map +1 -0
- package/dist/ui-menu.cjs.js +87 -0
- package/dist/ui-menu.cjs.js.map +1 -0
- package/dist/ui-menu.esm.js +66 -0
- package/dist/ui-menu.esm.js.map +1 -0
- package/dist/ui-select.cjs.js +104 -0
- package/dist/ui-select.cjs.js.map +1 -0
- package/dist/ui-select.esm.js +83 -0
- package/dist/ui-select.esm.js.map +1 -0
- package/dist/ui.cjs.js +738 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.esm.js +727 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vike/ClientOnly.cjs.js +5 -14
- package/dist/vike/ClientOnly.cjs.js.map +1 -0
- package/dist/vike/ClientOnly.mjs +5 -14
- package/dist/vike/ClientOnly.mjs.map +1 -0
- package/dist/vike/{+config.js → config/+config.js} +10 -1
- package/dist/vike/config/+config.js.map +1 -0
- package/dist/vike/config/package.json +6 -0
- package/dist/vike/onRenderClient.cjs.js +166 -95
- package/dist/vike/onRenderClient.cjs.js.map +1 -0
- package/dist/vike/onRenderClient.mjs +166 -95
- package/dist/vike/onRenderClient.mjs.map +1 -0
- package/dist/vike/onRenderHtml.cjs.js +60 -24
- package/dist/vike/onRenderHtml.cjs.js.map +1 -0
- package/dist/vike/onRenderHtml.mjs +61 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -0
- package/dist/vite/plugin.cjs.js +926 -65
- package/dist/vite/plugin.cjs.js.map +1 -0
- package/dist/vite/plugin.mjs +925 -65
- package/dist/vite/plugin.mjs.map +1 -0
- package/dist/zag.cjs.js +408 -0
- package/dist/zag.cjs.js.map +1 -0
- package/dist/zag.esm.js +405 -0
- package/dist/zag.esm.js.map +1 -0
- package/llms.txt +313 -0
- package/package.json +109 -16
- package/src/astro/client.ts +40 -18
- package/src/astro/index.d.ts +48 -1
- package/src/astro/index.ts +106 -9
- package/src/astro/server.ts +11 -3
- package/src/collection.ts +6 -90
- package/src/core/actions.ts +134 -0
- package/src/core/cell.ts +202 -0
- package/src/core/debug.ts +23 -0
- package/src/core/define.ts +242 -0
- package/src/core/hooks.ts +314 -0
- package/src/core/hosts/collection.ts +325 -0
- package/src/core/hosts/switchable.ts +146 -0
- package/src/core/instance.ts +592 -0
- package/src/core/markers/clientonly.ts +13 -0
- package/src/core/markers/lazy.ts +40 -0
- package/src/core/markers/portal.ts +96 -0
- package/src/core/markers/suspense.ts +41 -0
- package/src/core/markers/transition.ts +70 -0
- package/src/core/registry.ts +25 -0
- package/src/core/runtime.ts +573 -0
- package/src/core/statics.ts +104 -0
- package/src/core/teardown.ts +60 -0
- package/src/core/view.ts +37 -0
- package/src/cycle/dom/DocumentDOMSource.ts +13 -8
- package/src/cycle/dom/ElementFinder.ts +7 -9
- package/src/cycle/dom/EventDelegator.ts +174 -223
- package/src/cycle/dom/IsolateModule.ts +65 -35
- package/src/cycle/dom/MainDOMSource.ts +13 -2
- package/src/cycle/dom/PriorityQueue.ts +4 -2
- package/src/cycle/dom/SymbolTree.ts +18 -47
- package/src/cycle/dom/classNameModule.ts +76 -0
- package/src/cycle/dom/controlledInputModule.ts +50 -0
- package/src/cycle/dom/enrichEventStream.ts +29 -45
- package/src/cycle/dom/fragment.ts +7 -0
- package/src/cycle/dom/isolate.ts +30 -27
- package/src/cycle/dom/makeDOMDriver.ts +115 -57
- package/src/cycle/dom/mockDOMSource.ts +83 -9
- package/src/cycle/dom/modules.ts +21 -4
- package/src/cycle/dom/propsModule.ts +65 -0
- package/src/cycle/dom/selectModule.ts +22 -17
- package/src/cycle/dom/snabbdom.ts +1 -6
- package/src/cycle/dom/thunk.ts +3 -0
- package/src/cycle/dom/utils.ts +98 -0
- package/src/cycle/dom/viewTransition.ts +43 -0
- package/src/cycle/state/StateSource.ts +20 -4
- package/src/cycle/state/objIsEqual.ts +50 -0
- package/src/cycle/state/types.ts +0 -10
- package/src/defineComponent.ts +34 -0
- package/src/devtools.d.ts +151 -0
- package/src/devtools.ts +34 -0
- package/src/element.d.ts +59 -0
- package/src/element.ts +235 -0
- package/src/extra/backoff.ts +9 -0
- package/src/extra/behaviors.ts +169 -0
- package/src/extra/browserSignals.ts +23 -0
- package/src/extra/browserSources.ts +326 -0
- package/src/extra/command.ts +3 -3
- package/src/extra/controls.ts +47 -0
- package/src/extra/copyAsTest.ts +286 -0
- package/src/extra/devtools.ts +109 -31
- package/src/extra/devtoolsActions.ts +379 -0
- package/src/extra/devtoolsHook.ts +9 -0
- package/src/extra/devtoolsNext.ts +83 -0
- package/src/extra/diagnostics/checks/actionLog.ts +108 -0
- package/src/extra/diagnostics/checks/behaviors.ts +49 -0
- package/src/extra/diagnostics/checks/browserSources.ts +96 -0
- package/src/extra/diagnostics/checks/collections.ts +67 -0
- package/src/extra/diagnostics/checks/controls.ts +148 -0
- package/src/extra/diagnostics/checks/dataset.ts +57 -0
- package/src/extra/diagnostics/checks/dom.ts +250 -0
- package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
- package/src/extra/diagnostics/checks/events.ts +83 -0
- package/src/extra/diagnostics/checks/fetch.ts +135 -0
- package/src/extra/diagnostics/checks/forms.ts +163 -0
- package/src/extra/diagnostics/checks/index.ts +202 -0
- package/src/extra/diagnostics/checks/inspect.ts +427 -0
- package/src/extra/diagnostics/checks/next.ts +417 -0
- package/src/extra/diagnostics/checks/persist.ts +54 -0
- package/src/extra/diagnostics/checks/props.ts +56 -0
- package/src/extra/diagnostics/checks/public.d.ts +302 -0
- package/src/extra/diagnostics/checks/replies.ts +128 -0
- package/src/extra/diagnostics/checks/router.ts +100 -0
- package/src/extra/diagnostics/checks/rxjsHints.ts +88 -0
- package/src/extra/diagnostics/checks/shared.ts +203 -0
- package/src/extra/diagnostics/checks/shorthand.ts +115 -0
- package/src/extra/diagnostics/checks/sortable.ts +129 -0
- package/src/extra/diagnostics/checks/state.ts +148 -0
- package/src/extra/diagnostics/checks/statics.ts +48 -0
- package/src/extra/diagnostics/checks/strict.ts +55 -0
- package/src/extra/diagnostics/checks/timers.ts +80 -0
- package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
- package/src/extra/diagnostics/checks/virtual.ts +73 -0
- package/src/extra/diagnostics/checks/widgets.ts +109 -0
- package/src/extra/diagnostics/checks/wiring.ts +121 -0
- package/src/extra/diagnostics/codes.ts +477 -0
- package/src/extra/diagnostics/index.ts +353 -0
- package/src/extra/diagnostics/legacy.ts +76 -0
- package/src/extra/driverFactories.ts +172 -60
- package/src/extra/elementCommands.ts +53 -0
- package/src/extra/eventDriver.ts +4 -0
- package/src/extra/fetchDriver.ts +519 -0
- package/src/extra/flatten.ts +75 -0
- package/src/extra/focusWithin.ts +27 -0
- package/src/extra/form.ts +268 -0
- package/src/extra/formHelpers.ts +139 -0
- package/src/extra/head.ts +106 -0
- package/src/extra/hmr.ts +2 -3
- package/src/extra/owned.ts +24 -0
- package/src/extra/pager.ts +50 -0
- package/src/extra/persist.ts +133 -0
- package/src/extra/pwa.ts +1 -1
- package/src/extra/queryCache.ts +107 -0
- package/src/extra/reducers.ts +13 -6
- package/src/extra/reduxDevtools.ts +92 -0
- package/src/extra/replies.ts +54 -0
- package/src/extra/router.ts +323 -0
- package/src/extra/run.ts +94 -210
- package/src/extra/selection.ts +76 -0
- package/src/extra/socketDriver.ts +265 -0
- package/src/extra/sortable.ts +377 -0
- package/src/extra/ssr.ts +427 -160
- package/src/extra/standardSchema.ts +27 -0
- package/src/extra/testing.ts +3158 -190
- package/src/extra/timers.ts +95 -0
- package/src/extra/undo.ts +261 -0
- package/src/extra/viewTransitions.ts +38 -0
- package/src/extra/virtual.ts +513 -0
- package/src/extra/widget.ts +201 -0
- package/src/extra/xstreamExtras.ts +269 -0
- package/src/index.d.ts +3012 -87
- package/src/index.ts +32 -10
- package/src/jsx-runtime.ts +15 -1
- package/src/jsx.ts +2 -1
- package/src/lazy.ts +50 -7
- package/src/portal.ts +3 -1
- package/src/pragma/index.ts +262 -134
- package/src/react-peers.d.ts +5 -0
- package/src/react.d.ts +43 -0
- package/src/react.ts +110 -0
- package/src/shared.ts +48 -0
- package/src/slot.ts +1 -1
- package/src/suspense.ts +3 -1
- package/src/switchable.ts +6 -119
- package/src/transition.ts +3 -1
- package/src/ui/accordion.ts +64 -0
- package/src/ui/dialog.ts +129 -0
- package/src/ui/disclosure.ts +35 -0
- package/src/ui/popover.ts +45 -0
- package/src/ui/shared.ts +94 -0
- package/src/ui/tabs.ts +82 -0
- package/src/ui/toaster.ts +198 -0
- package/src/ui/tooltip.ts +70 -0
- package/src/ui/zag/combobox.ts +115 -0
- package/src/ui/zag/menu.ts +40 -0
- package/src/ui/zag/select.ts +54 -0
- package/src/ui/zag/shared.ts +43 -0
- package/src/ui-combobox.d.ts +20 -0
- package/src/ui-combobox.ts +6 -0
- package/src/ui-menu.d.ts +30 -0
- package/src/ui-menu.ts +6 -0
- package/src/ui-select.d.ts +17 -0
- package/src/ui-select.ts +6 -0
- package/src/ui-zag-types.d.ts +42 -0
- package/src/ui.d.ts +208 -0
- package/src/ui.ts +15 -0
- package/src/vike/+config.ts +9 -1
- package/src/vike/ClientOnly.ts +1 -1
- package/src/vike/onRenderClient.ts +144 -96
- package/src/vike/onRenderHtml.ts +69 -26
- package/src/vike/types.ts +27 -4
- package/src/vite/globalthis-shim.ts +18 -0
- package/src/vite/globalthis.ts +56 -0
- package/src/vite/plugin.d.ts +114 -2
- package/src/vite/plugin.ts +920 -56
- package/src/zag.d.ts +64 -0
- package/src/zag.ts +238 -0
- package/dist/astro/astro/client.d.ts +0 -5
- package/dist/astro/astro/index.d.ts +0 -16
- package/dist/astro/astro/server.d.ts +0 -12
- package/dist/astro/client.d.ts +0 -5
- package/dist/astro/client.esm.js +0 -4665
- package/dist/astro/collection.d.ts +0 -12
- package/dist/astro/component.d.ts +0 -22
- package/dist/astro/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/astro/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/astro/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/astro/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/astro/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/astro/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/astro/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/astro/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/astro/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/astro/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/astro/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/astro/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/astro/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/astro/cycle/dom/index.d.ts +0 -8
- package/dist/astro/cycle/dom/isolate.d.ts +0 -9
- package/dist/astro/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/astro/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/astro/cycle/dom/modules.d.ts +0 -5
- package/dist/astro/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/astro/cycle/dom/styleModule.d.ts +0 -13
- package/dist/astro/cycle/dom/thunk.d.ts +0 -11
- package/dist/astro/cycle/dom/utils.d.ts +0 -8
- package/dist/astro/cycle/isolate/index.d.ts +0 -42
- package/dist/astro/cycle/run/adapt.d.ts +0 -6
- package/dist/astro/cycle/run/index.d.ts +0 -36
- package/dist/astro/cycle/run/internals.d.ts +0 -8
- package/dist/astro/cycle/run/types.d.ts +0 -49
- package/dist/astro/cycle/state/Collection.d.ts +0 -29
- package/dist/astro/cycle/state/StateSource.d.ts +0 -20
- package/dist/astro/cycle/state/index.d.ts +0 -5
- package/dist/astro/cycle/state/pickCombine.d.ts +0 -3
- package/dist/astro/cycle/state/pickMerge.d.ts +0 -3
- package/dist/astro/cycle/state/types.d.ts +0 -18
- package/dist/astro/cycle/state/withState.d.ts +0 -17
- package/dist/astro/extra/classes.d.ts +0 -7
- package/dist/astro/extra/devtools.d.ts +0 -78
- package/dist/astro/extra/dragDriver.d.ts +0 -20
- package/dist/astro/extra/driverFactories.d.ts +0 -12
- package/dist/astro/extra/eventDriver.d.ts +0 -9
- package/dist/astro/extra/exactState.d.ts +0 -1
- package/dist/astro/extra/hmr.d.ts +0 -17
- package/dist/astro/extra/logDriver.d.ts +0 -2
- package/dist/astro/extra/processDrag.d.ts +0 -19
- package/dist/astro/extra/processForm.d.ts +0 -15
- package/dist/astro/extra/run.d.ts +0 -13
- package/dist/astro/extra/xstreamCompat.d.ts +0 -5
- package/dist/astro/index.d.ts +0 -19
- package/dist/astro/index.esm.js +0 -27
- package/dist/astro/jsx-dev-runtime.d.ts +0 -1
- package/dist/astro/jsx-runtime.d.ts +0 -4
- package/dist/astro/jsx.d.ts +0 -2
- package/dist/astro/pragma/fn.d.ts +0 -7
- package/dist/astro/pragma/index.d.ts +0 -7
- package/dist/astro/pragma/is.d.ts +0 -10
- package/dist/astro/server.d.ts +0 -12
- package/dist/astro/server.esm.js +0 -25
- package/dist/astro/switchable.d.ts +0 -7
- package/dist/collection.d.ts +0 -12
- package/dist/component.d.ts +0 -22
- package/dist/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/cycle/dom/index.d.ts +0 -8
- package/dist/cycle/dom/isolate.d.ts +0 -9
- package/dist/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/cycle/dom/modules.d.ts +0 -5
- package/dist/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/cycle/dom/styleModule.d.ts +0 -13
- package/dist/cycle/dom/thunk.d.ts +0 -11
- package/dist/cycle/dom/utils.d.ts +0 -8
- package/dist/cycle/isolate/index.d.ts +0 -42
- package/dist/cycle/run/adapt.d.ts +0 -6
- package/dist/cycle/run/index.d.ts +0 -36
- package/dist/cycle/run/internals.d.ts +0 -8
- package/dist/cycle/run/types.d.ts +0 -49
- package/dist/cycle/state/Collection.d.ts +0 -29
- package/dist/cycle/state/StateSource.d.ts +0 -20
- package/dist/cycle/state/index.d.ts +0 -5
- package/dist/cycle/state/pickCombine.d.ts +0 -3
- package/dist/cycle/state/pickMerge.d.ts +0 -3
- package/dist/cycle/state/types.d.ts +0 -18
- package/dist/cycle/state/withState.d.ts +0 -17
- package/dist/extra/classes.d.ts +0 -7
- package/dist/extra/devtools.d.ts +0 -78
- package/dist/extra/dragDriver.d.ts +0 -20
- package/dist/extra/driverFactories.d.ts +0 -12
- package/dist/extra/eventDriver.d.ts +0 -9
- package/dist/extra/exactState.d.ts +0 -1
- package/dist/extra/hmr.d.ts +0 -17
- package/dist/extra/logDriver.d.ts +0 -2
- package/dist/extra/processDrag.d.ts +0 -19
- package/dist/extra/processForm.d.ts +0 -15
- package/dist/extra/run.d.ts +0 -13
- package/dist/extra/xstreamCompat.d.ts +0 -5
- package/dist/jsx-dev-runtime.d.ts +0 -1
- package/dist/jsx-dev-runtime.js +0 -224
- package/dist/jsx-runtime.d.ts +0 -4
- package/dist/jsx-runtime.js +0 -224
- package/dist/jsx.d.ts +0 -2
- package/dist/pragma/fn.d.ts +0 -7
- package/dist/pragma/index.d.ts +0 -7
- package/dist/pragma/is.d.ts +0 -10
- package/dist/switchable.d.ts +0 -7
- package/dist/vike/+config.cjs.js +0 -65
- package/src/component.ts +0 -2256
- package/src/cycle/isolate/index.ts +0 -196
- package/src/cycle/run/index.ts +0 -151
- package/src/cycle/run/internals.ts +0 -143
- package/src/cycle/state/Collection.ts +0 -174
- package/src/cycle/state/index.ts +0 -5
- package/src/cycle/state/pickCombine.ts +0 -167
- package/src/cycle/state/pickMerge.ts +0 -122
- package/src/cycle/state/withState.ts +0 -47
- package/src/pragma/fn.ts +0 -55
package/llms.txt
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Sygnal
|
|
2
|
+
|
|
3
|
+
> Reactive component framework (Cycle.js Model-View-Intent, JSX views, xstream streams). `npm i sygnal`; Vite: `import sygnal from 'sygnal/vite'`, `plugins: [sygnal()]`; entry: `run(App)`. This file shows only the canonical forms. Strict mode (`sygnal-check --strict`, runtime `strict: true`) flags other forms as SYG5xx; forms 6.0 removed (positional views, `'A | SINK'` keys, `CHILD.select('Name')`, `.components`, `.peers`, `component()`) are SYG612: https://sygnal.js.org/guide/migrating-to-6/
|
|
4
|
+
|
|
5
|
+
## 1. Mental model
|
|
6
|
+
- A component is a pure view function plus static properties: `.intent` (WHEN: sources → named action streams), `.model` (WHAT: action → one reducer per sink), `.initialState`.
|
|
7
|
+
- There is one state tree. A child sees its parent's whole state, or a slice with `state="key"`. A Collection item sees one array element.
|
|
8
|
+
- Every side effect is a sink of a model entry (STATE, EVENTS, PARENT, EFFECT, custom drivers). JSX never has event handlers (`onClick`).
|
|
9
|
+
- Streams are xstream, not RxJS. An intent's DOM selectors only see the component's own JSX, never a child component's.
|
|
10
|
+
|
|
11
|
+
## 2. Component anatomy
|
|
12
|
+
```jsx
|
|
13
|
+
import { ABORT } from 'sygnal'
|
|
14
|
+
function Counter({ state, context, label }) { // 1st arg: parent props + state, context, children, slots
|
|
15
|
+
return (
|
|
16
|
+
<div className="counter">
|
|
17
|
+
<span>{label} {state.count} (x2 = {state.double})</span>
|
|
18
|
+
<button className="inc">+</button>
|
|
19
|
+
<button className="reset">reset</button>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
Counter.initialState = { count: 0 } // root only: a child gets state from its parent (SYG405)
|
|
24
|
+
Counter.calculated = { double: state => state.count * 2 } // or [['count'], fn]; read as state.double
|
|
25
|
+
Counter.context = { total: state => state.count } // itself + descendants: ({ context }) => context.total
|
|
26
|
+
Counter.intent = ({ DOM }) => ({ // sources: DOM STATE EVENTS CHILD props$ commands$ dispose$ + drivers
|
|
27
|
+
INC: DOM.click('.inc'),
|
|
28
|
+
RESET: DOM.click('.reset'),
|
|
29
|
+
})
|
|
30
|
+
Counter.model = {
|
|
31
|
+
INC: (state, data, next, props) => ({ ...state, count: state.count + 1 }),
|
|
32
|
+
RESET: (state) => (state.count === 0 ? ABORT : { ...state, count: 0 }),
|
|
33
|
+
}
|
|
34
|
+
Counter.onError = (error, { componentName }) => <div className="error">{componentName} failed</div>
|
|
35
|
+
export default Counter
|
|
36
|
+
```
|
|
37
|
+
- Reducer `(state, data, next, props)`. `data` is the action stream's value. `next('ACTION', data?, delayMs = 10)` dispatches another action of this component `delayMs` after the call, whenever it is called: also later, from a timer or callback in an EFFECT (HTTP goes through `makeFetchDriver`, not `fetch` + `next()`). `props` holds the parent's props plus `state`, `context`, `children`, `slots`.
|
|
38
|
+
- Model entry: a function is the STATE reducer. An object `{ STATE, EVENTS, PARENT, EFFECT, LOG, <DRIVER>: fn }` maps each sink to a `(state, data, next, props)` function whose return value goes to that sink. `<SINK>: true` forwards `data` as-is.
|
|
39
|
+
- **Every sink of one entry sees the state from before this action**: EVENTS, PARENT, EFFECT and drivers never see what STATE returns. Compute the new value from `(state, data)` inside the sink: `INC: { STATE: s => ({ ...s, n: s.n + 1 }), PARENT: s => ({ n: s.n + 1 }) }`.
|
|
40
|
+
- Built-in actions (model only): `BOOTSTRAP` (once, just after mount), `INITIALIZE` (automatic: sets initialState), `DISPOSE` (unmount). `HYDRATE` is not built in (6.0): SSR data comes from Vike `+data` / `hydrateState`. Declaration statics (`connections`, `resources`, `route`, `head`) are computed from state, with or without a model; a root that declares one needs `initialState` (SYG132). Also reserved as statics: `timers`, `uses`, `persist`, `viewTransitions` (§3).
|
|
41
|
+
|
|
42
|
+
## 3. Canonical examples
|
|
43
|
+
### State update, ABORT, set / toggle, controlled input
|
|
44
|
+
```jsx
|
|
45
|
+
import { ABORT, set, toggle } from 'sygnal'
|
|
46
|
+
function AddTodo({ state }) {
|
|
47
|
+
return (
|
|
48
|
+
<div>
|
|
49
|
+
<input className="draft" aria-label="New todo" value={state.draft} />
|
|
50
|
+
<button className="add">Add</button>
|
|
51
|
+
<button className="help">?</button>
|
|
52
|
+
{state.showHelp && <p className="hint">Type, then Add</p>}
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
AddTodo.initialState = { draft: '', todos: [], nextId: 1, showHelp: false }
|
|
57
|
+
AddTodo.intent = ({ DOM }) => ({
|
|
58
|
+
DRAFT: DOM.input('.draft').value(), // a bound value={...} needs an input listener (SYG111)
|
|
59
|
+
ADD: DOM.click('.add'),
|
|
60
|
+
HELP: DOM.click('.help'),
|
|
61
|
+
})
|
|
62
|
+
AddTodo.model = {
|
|
63
|
+
DRAFT: set((state, draft) => ({ draft })), // merges: ({ ...state, draft }); never set('draft') (SYG221)
|
|
64
|
+
ADD: (state) => {
|
|
65
|
+
const text = state.draft.trim()
|
|
66
|
+
if (!text) return ABORT // "no change" (returning the same `state` works too)
|
|
67
|
+
return { ...state, draft: '', nextId: state.nextId + 1, todos: [...state.todos, { id: state.nextId, text }] }
|
|
68
|
+
},
|
|
69
|
+
HELP: toggle('showHelp'),
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
### Multi-sink entry + EVENTS between non-adjacent components
|
|
73
|
+
```jsx
|
|
74
|
+
import { event } from 'sygnal'
|
|
75
|
+
function SaveButton({ state }) {
|
|
76
|
+
return <button className="save">{state.saving ? 'Saving' : 'Save'}</button>
|
|
77
|
+
}
|
|
78
|
+
SaveButton.intent = ({ DOM }) => ({ SAVE: DOM.click('.save') })
|
|
79
|
+
SaveButton.model = {
|
|
80
|
+
SAVE: {
|
|
81
|
+
STATE: (state) => ({ ...state, saving: true }),
|
|
82
|
+
EVENTS: event('DOC_SAVED', (state) => ({ id: state.id })), // or event('RESET'), event('MODE', 'dark')
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
function Toast({ state }) { // anywhere in the tree: EVENTS is a global bus
|
|
86
|
+
return <p className="toast">{state.lastSaved}</p>
|
|
87
|
+
}
|
|
88
|
+
Toast.intent = ({ EVENTS }) => ({ SAVED: EVENTS.select('DOC_SAVED') }) // emits the payload only
|
|
89
|
+
Toast.model = { SAVED: (state, payload) => ({ ...state, lastSaved: payload.id }) }
|
|
90
|
+
```
|
|
91
|
+
### Child → parent (PARENT + CHILD.select), Collection, item removal
|
|
92
|
+
```jsx
|
|
93
|
+
import { Collection } from 'sygnal'
|
|
94
|
+
function TaskItem({ state }) {
|
|
95
|
+
return (
|
|
96
|
+
<li className="task" data={{ id: state.id }}>
|
|
97
|
+
{state.title} <button className="pick">pick</button> <button className="remove">x</button>
|
|
98
|
+
</li>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
TaskItem.intent = ({ DOM }) => ({ PICK: DOM.click('.pick'), REMOVE: DOM.click('.remove') })
|
|
102
|
+
TaskItem.model = {
|
|
103
|
+
PICK: { PARENT: (state) => ({ taskId: state.id }) }, // the parent receives exactly this value
|
|
104
|
+
REMOVE: () => undefined, // a Collection item removes itself
|
|
105
|
+
}
|
|
106
|
+
function TaskList({ state }) {
|
|
107
|
+
return (
|
|
108
|
+
<div>
|
|
109
|
+
<p className="picked">{state.picked}</p>
|
|
110
|
+
<ul className="tasks">
|
|
111
|
+
<Collection of={TaskItem} from="tasks" />
|
|
112
|
+
</ul>
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
TaskList.initialState = { picked: null, tasks: [{ id: 1, title: 'Write' }, { id: 2, title: 'Test' }] }
|
|
117
|
+
TaskList.intent = ({ CHILD }) => ({ PICKED: CHILD.select(TaskItem) }) // the function, not a string
|
|
118
|
+
TaskList.model = { PICKED: (state, { taskId }) => ({ ...state, picked: taskId }) }
|
|
119
|
+
```
|
|
120
|
+
- `from` names an array field. Items are keyed by `.id` (by index if they have none). `filter={t => !t.done}`; `sort="title"`, `{ title: 'desc' }`, an array of those, or a compare function. Sorting and filtering only change what renders: the state array keeps its order, and an item's edit (`{ ...state, done: true }`) is written back to its element by key. Items render directly into the parent element, with no wrapper (`<Collection className>` is SYG612: put the class on your own `<ul>`). Switchable: `<Switchable of={{ home: Home, settings: Settings }} current={state.tab} />` (optional `state="slice"`); hidden pages stay alive, and `instance={key}` re-creates the current page with fresh state when the key changes (a router: `instance={state.route.path}`).
|
|
121
|
+
### Behaviors (`uses`): shared state + actions
|
|
122
|
+
- `TaskList.uses = { pager: pager({ pageSize: 10, next: '.newer', prev: '.older' }) }` adds `state.pager` (`{ page, pageSize, total }` + calculated `offset`, `pages`, `hasPrev`, `hasNext`) and the actions `pager.NEXT`, `pager.PREV`, `pager.GOTO`, `pager.SET_TOTAL`; a click on what `next`/`prev` name triggers them. Options set the slice's start values; the key must not also be in `initialState` (SYG127). Also from `'sygnal'`: `selection({ multi: true, item: '.pick', from: 'mails' })` (`data-id` on each item; `isSelected(state.sel, id)`) and `undo({ key: 'doc', undo: '.undo', redo: '.redo' })` (`state.history.canUndo`/`canRedo`; `coalesce: ['TYPE'], coalesceMs: 500` groups only typing, each other action is its own step; `key: ['todo', 'done']` records several keys as one step; `node_modules/sygnal/dist/guide/undo.md`).
|
|
123
|
+
- Your own: `const disclosure = defineBehavior({ initialState: { open: false }, intent: ({ DOM }, { toggle }) => ({ TOGGLE: DOM.click(toggle) }), model: { TOGGLE: (slice) => ({ ...slice, open: !slice.open }) } })`, used as `uses = { more: disclosure({ toggle: '.more' }) }`. Its reducers and `calculated` get and return the slice (`state.more`), its action names have no key, and any sink works. Options: `persist: false` (UI state `persist()` never saves), `undoStep: ['DONE']` (its other actions form one gesture: one undo entry when `DONE` completes it).
|
|
124
|
+
- The host names behavior actions with the key: an intent action `'more.TOGGLE': DOM.keydown(…)` replaces the behavior's trigger; a model entry `'more.TOGGLE': (state) => …` runs after the behavior's, on the full state; tests use `t.simulateAction('more.TOGGLE')`. Guide: `node_modules/sygnal/dist/guide/behaviors.md`
|
|
125
|
+
### Forms with validation (the `form` behavior)
|
|
126
|
+
- `Signup.uses = { form: form(schema, { values: { email: '', addresses: [{ id: 1, city: '' }] }, submit: 'SIGN_UP' }) }` (`form` from `'sygnal'`), any Standard Schema (zod, valibot, or `{ '~standard': { version: 1, vendor, validate } }`). Fields inside the `<form>` are matched by `name`, a path in `values` (array rows by id: `addresses.7.city`): no intent per field (SYG230: a name not in `values`).
|
|
127
|
+
- View: `const f = state.form.fields`; `<input id={uid('email')} name="email" value={f.email.value} aria-invalid={f.email.invalid} aria-describedby={uid('email-error')} />`, `<p id={uid('email-error')}>{f.email.error}</p>`, a `<label for={uid('email')}>`. `f.x.error` is what to show (after blur or a submit); also `state.form.submitting`, `state.form.valid`, `state.form.error` (form-level).
|
|
128
|
+
- An invalid submit shows every error and focuses the first invalid field; a valid one dispatches `SIGN_UP` with the schema's output: `SIGN_UP: { HTTP: (state, values) => ({ url, method: 'POST', json: values, ok: 'form.DONE', error: 'form.ERRORS' }) }`; `state.form.submitting` is `true` until the reply (a second submit meanwhile is dropped); a submit entry without `HTTP` (a wizard step's `NEXT: { PARENT: … }`) is done at once, no `form.DONE` needed; `form.ERRORS` puts a 422's `{ errors: { field: msg } }` or `{ errors: [{ path: ['items', 0, 'qty'], message }] }` (an index is the row's position in what was sent) on the fields and focuses the first; a reply with no field message shows in `state.form.error` (`Request failed (500)`); a host entry `'form.DONE': (state, body) => …` runs after the form's. Two forms in one component: `form: '.login'` on each (SYG237).
|
|
129
|
+
- **The `form` slice lives in the host's state**, and a Switchable page (no `state=`) shares its parent's, so the values survive leaving the page and coming back, even when `instance=` re-creates it. When the form must start empty on each visit, say so: `form(schema, { values: EMPTY, submit: 'SAVE', resetOnShow: true })` starts over (start values; touched, errors and the submit cleared) each time the form is shown: the page shown again, the component mounted again. Off by default (a wizard step keeps its values). A start value that comes from state (a default category from the settings) goes in a `values` function of the host's state, called at each start: `values: (state) => ({ ...EMPTY, category: state.settings.defaultCategory })`. When removing a feature that fed a start value, keep `resetOnShow`.
|
|
130
|
+
- Rows, inline (no child component): `state.form.values.addresses.map((row, i) => …)` with `const c = f['addresses.' + row.id + '.city']`, `name={c.name} value={c.value}`, ids `uid('city-' + row.id)`, `<button type="button" className="remove" data-id={row.id}>`; `'form.ADD': DOM.click('.add').mapTo({ field: 'addresses', value: { city: '' } })`, `'form.REMOVE': DOM.click('.remove').map((e) => ({ field: 'addresses', id: e.target.dataset.id }))`. Rows as components: `<Collection of={Row} from={{ get: (s) => s.form.values.addresses }} fields={f} />`, Row `REMOVE: { PARENT: (s) => ({ field: 'addresses', id: s.id }) }` + host `'form.REMOVE': CHILD.select(Row)`.
|
|
131
|
+
- Async check: `check: { email: { request: (email) => ({ url: '/api/email-free', query: { email } }), error: (body) => !body.free && 'Taken' } }` (on blur; a submit waits for it; `f.email.pending`). Tests: `t.simulateEvent('[name="email"]', 'input', { value })`, `'focusout'`, `'submit'` on the form; `t.state.form.fields.email.error`. Guide (recipe first): `node_modules/sygnal/dist/guide/forms.md`; details `forms-reference.md`
|
|
132
|
+
### Commands (parent → child), element commands, timers, persistence
|
|
133
|
+
- `const player = createCommand()` (from `'sygnal'`); the parent passes it as a prop, `<Player commands={player} state="player" />`, and calls it from an EFFECT (side effect only, returns nothing): `PLAY: { EFFECT: () => player.send('play', data) }`. The child's intent listens with `PLAY: commands$.select('play')`, which emits `send()`'s data.
|
|
134
|
+
- **ELEMENT** (built in, no driver) calls a method on an element of the sending instance's own view, after the render the action causes: `SUBMIT: { STATE: (s) => ({ ...s, errors: validate(s) }), ELEMENT: (s) => (validate(s).email ? { focus: '.email' } : ABORT) }`. The first key is the method (`focus blur select click scrollIntoView showModal show close showPopover hidePopover togglePopover`) and its value the target (a selector); other keys are options (`{ scrollIntoView: '.row', block: 'nearest' }`, `{ close: '.help', returnValue: 'done' }`); an array sends several; `{ focus: focusWithin('.title') }` (from `'sygnal'`) reaches into children and Collection items. `DOM.close('.help')` hears a dialog close. SYG640: no target; SYG641: unknown method. Tests: `t.commands('ELEMENT')`; `dom: 'real'` also runs them. Guide: `node_modules/sygnal/dist/guide/element-commands.md`
|
|
135
|
+
- **Timers**, from state like `connections` (falsy stops, a changed spec restarts): `Clock.timers = (state) => ({ tick: state.running && { every: 1000, action: 'TICK' } })` (also `{ after: ms, action }`, `{ frame: 'FRAME' }`); `TICK` gets `{ n, t }`. Needs `run(App, { TIMER: makeTimerDriver() })` (SYG643 without it); no `setInterval`. Tests need no driver: fake timers, `await vi.advanceTimersByTimeAsync(ms)`, `t.timers()`. Guide: `node_modules/sygnal/dist/guide/timers.md`
|
|
136
|
+
- **Persistence** (root only, SYG224): `App.persist = persist({ key: 'todo-app', pick: ['todos', 'filter'] })` (also `version`, `migrate: (old, fromVersion) => keys`, `omit`, `storage: 'local' | 'session' | { getItem, setItem, removeItem }`, `sync: true`, `debounceMs`) restores those keys into `initialState` before the first render and saves them after each change (100 ms debounce) as JSON `{ version, state }` (`format: 'plain'`: the picked keys alone, no `version`/`migrate`); `PERSIST: { clear: true }` in an entry removes the copy. Tests: `renderComponent(App, { storage: { 'todo-app': { version: 1, state: { todos: [] } } } })`, `await t.settle()`, `t.storage('todo-app')`. Guide: `node_modules/sygnal/dist/guide/persistence.md`
|
|
137
|
+
### Third-party widgets (`defineWidget`) and web components
|
|
138
|
+
- `defineWidget({ tag, mount(el, props, dispatch), update(instance, props), unmount(instance), events, commands })` (from `'sygnal'`) makes a JSX tag: `const DatePicker = defineWidget({ tag: 'input', mount: (el, props, dispatch) => flatpickr(el, { defaultDate: props.value, onChange: ([d]) => dispatch('pick', d) }), update: (fp, props) => fp.setDate(props.value, false), unmount: (fp) => fp.destroy(), events: ['pick'], commands: { open: (fp) => fp.open() } })`. Also `fallback` (SSR content), `hostProps` / `ownProps` (props on / kept off the host), `mount`'s 4th argument `error(e)` (a failure the library reports later).
|
|
139
|
+
- View: `<label>Due <DatePicker className="due" value={state.due} /></label>` (the host element; the widget owns its content and keeps its instance across renders). Intent: `DUE: DOM.select('.due').events('pick').detail()` (`.detail(fn?)` = e.detail). Model: `OPEN: { ELEMENT: { open: '.due' } }` runs the widget's command (it wins over a native method of the same name).
|
|
140
|
+
- List every dispatched name in `events` and name them yourself (`'pick'`, not `'change'`: SYG140/144); `update` gets the newest props when they change (without it, a change remounts); a throwing `mount`/`update` renders the component's `onError` fallback in its place (`onError` phase `'widget'`). Never `DOM.select(DatePicker)`: select its class (SYG143).
|
|
141
|
+
- Tests: `t.widget('.due').dispatch('pick', date)`, `t.widget('.due').props.value`, `t.commands('ELEMENT')`; `dom: 'real'` mounts it (`.instance`). Guide: `node_modules/sygnal/dist/guide/widgets.md`
|
|
142
|
+
- Web components (Web Awesome, Shoelace) are elements, no wrapper: render the tag (`<wa-rating className="food" label="Food" value={state.food} />`; props set properties: `withClear`, not `with-clear`; attributes via `attrs={{ name: 'stars' }}`) and select it by class: `DOM.select('.food').events('change').value(Number)`, library events with any name: `DOM.select('.food').events('wa-hover').detail()`. Guide: `node_modules/sygnal/dist/guide/web-components.md`
|
|
143
|
+
### HTTP (makeFetchDriver + reply actions), latest response only
|
|
144
|
+
```jsx
|
|
145
|
+
import { ABORT, debounce } from 'sygnal'
|
|
146
|
+
function Search({ state }) {
|
|
147
|
+
return <div><input className="q" aria-label="Search" value={state.query} /><p className="status">{state.status}</p><ul>{state.results.map(r => <li>{r.title}</li>)}</ul></div>
|
|
148
|
+
}
|
|
149
|
+
Search.initialState = { query: '', status: '', results: [] }
|
|
150
|
+
Search.intent = ({ DOM }) => ({ // no intent line for the reply
|
|
151
|
+
TYPE: DOM.input('.q').value(),
|
|
152
|
+
SEARCH: DOM.input('.q').value().compose(debounce(300)).filter(q => q !== ''),
|
|
153
|
+
})
|
|
154
|
+
Search.model = {
|
|
155
|
+
TYPE: {
|
|
156
|
+
STATE: (state, query) => (query === '' ? { ...state, query, status: '', results: [] } : { ...state, query }),
|
|
157
|
+
HTTP: (state, query) => (query === '' ? { abort: 'RESULTS' } : ABORT), // clearing cancels the request in flight
|
|
158
|
+
},
|
|
159
|
+
SEARCH: {
|
|
160
|
+
STATE: (state) => ({ ...state, status: 'Searching…' }),
|
|
161
|
+
HTTP: (state, q) => ({ url: '/api/search', query: { q }, ok: 'RESULTS', error: 'FAILED', latest: true }),
|
|
162
|
+
},
|
|
163
|
+
RESULTS: (state, body) => ({ ...state, status: '', results: body.results }), // ok: the parsed body
|
|
164
|
+
FAILED: (state, { status, error }) => ({ ...state, status: status === 404 ? 'Not found.' : 'Search failed.', results: [] }),
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
- main.js: `run(Search, { HTTP: makeFetchDriver() })` (import both from `'sygnal'`; options `baseUrl headers init timeoutMs`). Request: `{ url, ok, error, key, query, json, body, method (POST with json/body, else GET), headers, latest, timeoutMs, parse: 'auto' | 'json' | 'text' | 'response', init: { credentials, mode, cache, ... } }`; other keys (an id) aren't sent and come back on `request`. The `ok` action gets the parsed body; the `error` action gets `{ error, status, body, request }` (non-2xx: `status` + parsed `body`; network error or timeout: `status` undefined, a timeout has `error.name === 'TimeoutError'`). The reply reaches exactly the instance that sent it (two `<Search>`es, every Collection item), so no request ids. Never `HTTP.select`/`HTTP.errors` for your own request (SYG508), never `then`/`catch` keys (SYG610: not sent); an `ok`/`error` name with no model entry is SYG112. Don't call `fetch` in a component, intent or EFFECT. Guide: `node_modules/sygnal/dist/guide/http.md` (online: https://sygnal.js.org/guide/http/)
|
|
168
|
+
- `latest: true`: a newer request in the same lane (`key`: a constant name, never an id or URL; default the `ok` action) from the same instance aborts the older ones once it is sent: their replies never arrive after that, but one that lands before it (while the newer input still waits out a debounce) does. `{ abort: 'RESULTS' }` (it names the lane: with a custom `key`, abort that key; or `{ abort: true, key }`) cancels without sending; a removed instance's requests are aborted. **Build the request from `(state, data)`**: sinks see the state before the action, so `SHOW: { STATE: (s, id) => ({ ...s, id }), HTTP: (s, id) => ({ url: '/api/q/' + id, ok: 'LOADED' }) }`, not `s.id`.
|
|
169
|
+
- Other promise APIs: `driverFromAsync(fn)` takes the same reply actions: `{ value, ok: 'DONE', error: 'FAILED' }` calls `fn(value)`; `DONE` gets the result, `FAILED` `{ error, request }` (no `latest`/abort). One-off async work: `EFFECT: async (state, data, next, { signal }) => { ...; next('DONE', result) }` (rejection: SYG214; `signal` aborts on unmount; no concurrency control). A driver is any function `sink$ => source`. Page-wide events: `DOM.select('document' | 'body').events(type)`, CSS-filtered with `DOM.select('document').select('.overlay').events('click')`.
|
|
170
|
+
### WebSocket / SSE (makeSocketDriver + connections)
|
|
171
|
+
```jsx
|
|
172
|
+
import { ABORT } from 'sygnal'
|
|
173
|
+
function Chat({ state }) {
|
|
174
|
+
return <div><p className="status">{state.status}</p><ul>{state.messages.map(m => <li>{m.text}</li>)}</ul><input className="draft" aria-label="Message" value={state.draft} /><button className="send">Send</button><button className="leave">Leave</button></div>
|
|
175
|
+
}
|
|
176
|
+
Chat.initialState = { room: 'general', status: 'connecting', messages: [], draft: '' }
|
|
177
|
+
Chat.connections = (state) => ({ // from state, diffed by name: new opens, falsy/removed closes, changed URL reconnects
|
|
178
|
+
room: state.room && { socket: `/ws/rooms/${state.room}`, message: 'RECEIVED', open: 'CONNECTED', close: 'DROPPED',
|
|
179
|
+
reconnect: { delayMs: 1000, maxDelayMs: 1000, jitter: false } }, // fixed 1 s retry (default: 500 ms doubling to 10 s, ±20%)
|
|
180
|
+
})
|
|
181
|
+
Chat.intent = ({ DOM }) => ({ DRAFT: DOM.input('.draft').value(), SEND: DOM.click('.send'), LEAVE: DOM.click('.leave') })
|
|
182
|
+
Chat.model = {
|
|
183
|
+
DRAFT: (state, draft) => ({ ...state, draft }),
|
|
184
|
+
SEND: {
|
|
185
|
+
STATE: (state) => (state.draft.trim() ? { ...state, draft: '' } : ABORT),
|
|
186
|
+
WS: (state) => (state.draft.trim() ? { to: 'room', json: { text: state.draft } } : ABORT), // queued while (re)connecting
|
|
187
|
+
},
|
|
188
|
+
LEAVE: (state) => ({ ...state, room: null, status: 'offline', messages: [] }), // closes it: no DROPPED
|
|
189
|
+
RECEIVED: (state, msg) => ({ ...state, messages: [...state.messages, msg] }), // the JSON-parsed frame
|
|
190
|
+
CONNECTED: (state) => ({ ...state, status: 'online' }), // data: { reconnected }
|
|
191
|
+
DROPPED: (state, { willReconnect }) => ({ ...state, status: willReconnect ? 'reconnecting' : 'offline' }), // { code, reason, willReconnect }
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
- main.js: `run(Chat, { WS: makeSocketDriver() })` (options `baseUrl reconnect queueLimit`). Without it the static opens nothing and nothing reports it. Spec keys: `socket` (a path gets ws:/wss: from the page) or `sse` (read-only; `events: { 'price-update': 'PRICE' }` for named events), `message open close error` (all optional reply actions, to this instance only), `reconnect` (`false` = never), `share` (default true: one socket per URL, ref-counted), `protocols`, `withCredentials`. `close` fires only for drops the app didn't cause; leaving, a URL change, unmount and dispose close silently, so no connection-generation ids. A room switch must `ABORT` when the room is unchanged (an unchanged connection fires no new `open`). `error` gets `{ error }`. `{ to, json | text | binary }` on a name this instance didn't declare, a closed-for-good or an SSE connection is SYG611. Use the static OR a model-sent `{ connections }`, never both (each is the whole set). Guide: https://sygnal.js.org/guide/sockets/
|
|
195
|
+
### Reads that follow state: `resources`
|
|
196
|
+
```jsx
|
|
197
|
+
function Quote({ state }) {
|
|
198
|
+
const { status, data, error, refreshing } = state.quote // status: 'idle' | 'loading' | 'success' | 'error'
|
|
199
|
+
const text = status === 'loading' ? 'Loading…' : status === 'error' ? `Failed (${error.status ?? 'network'})` : refreshing ? 'Updating…' : ''
|
|
200
|
+
return <div><button className="next">Next</button><button className="refresh">Refresh</button><p className="status">{text}</p><p className="text">{data?.text}</p></div>
|
|
201
|
+
}
|
|
202
|
+
Quote.initialState = { id: 1 }
|
|
203
|
+
Quote.resources = { quote: (state) => state.id && `/api/quotes/${state.id}` } // a URL or a request ({ url, query, … }); falsy = idle
|
|
204
|
+
Quote.intent = ({ DOM }) => ({ NEXT: DOM.click('.next'), REFRESH: DOM.click('.refresh') })
|
|
205
|
+
Quote.model = {
|
|
206
|
+
NEXT: (state) => ({ ...state, id: state.id + 1 }), // new request: 'loading' without data; the old one is aborted
|
|
207
|
+
REFRESH: { HTTP: { refresh: 'quote' } }, // same request: data kept, refreshing: true
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
- `resources` is the form for data a component shows (reads derived from state); writes and click-triggered one-off requests keep reply actions (above). Needs `run(Quote, { HTTP: makeFetchDriver() })`. The core keeps `state.quote` (named by the resource; don't set it in initialState: it reads as `{ status: 'idle' }`) and writes it with the built-in `RESOURCE` action. When the request changes (by value) it is fetched and the older one aborted, so a stale reply is never shown: no ids, `latest`, ok/error actions or loading flags to write. `data` is the parsed body; `error` is the Error (`error.status`, `error.body` for a non-2xx). Falsy aborts and goes idle. TS: `quote: Resource<Quote>` in the state type.
|
|
211
|
+
- **A component that reads `state.items` declares `items` in its own `.resources`**, even when another page (or its parent) declares the same request: without it `state.items` is undefined on a direct visit (or holds whatever another page loaded last), and a test that starts on the declaring page hides this. Each declaration fetches; with a `queryCache()` the repeat shows the cached data at once and identical requests in flight share one fetch.
|
|
212
|
+
- **Refetch rule**: a new request (another id) is `'loading'` with no data; a refetch of the same request (`{ refresh: 'quote' }`, an invalidation, focus, polling) keeps `status`, `data` and `error` with `refreshing: true`, and a failed refetch keeps `data`. When the old value must not show (Refresh shows "Loading…"), treat `refreshing` as loading. `keepPrevious: true` on the request keeps the old data across a request change too (pagination). `ok: 'FOUND'` / `error: 'FAILED'` on the request also dispatch those actions after the write (append a page, fill a field).
|
|
213
|
+
- Invalidation: tag reads `{ url, tags: ['quotes'] }`; a write with `invalidates: ['quotes']` (tags, a `'/api/quotes'` URL prefix, or a predicate) refetches the matching mounted reads after a 2xx, keeping their data; `{ invalidate: 'quotes' }` on the HTTP sink does it at once. Works with or without the cache. The refetch aborts the read already in flight, so a reply sent before the write never lands (no sequence numbers); a shared cache fetch from before never writes the cache.
|
|
214
|
+
- `updates: 'item'` on a write (React Query's `setQueryData`): after a 2xx, before the `ok` action and `invalidates`, the reply becomes `state.item.data` and its cache entry at once, and the item's read in flight is aborted; `updates: { items: (list, reply) => newList }` derives the data. With `invalidates` the read then refetches keeping the written data (`refreshing`). An idle resource is skipped; the `ok` action still gets the reply.
|
|
215
|
+
- Query cache (opt-in, its own export): `run(App, { HTTP: makeFetchDriver({ cache: queryCache({ staleTime: 30000, gcTime: 300000 }) }) })`. A request fetched before shows its cached data at once (`'success'`, `refreshing: true` while it revalidates; nothing is sent while younger than `staleTime`, default 0), identical requests in flight share one fetch, and stale mounted resources refetch on window focus and reconnect. Reply-action GETs use it with `cache: true` or `staleTime`. Per request: `retry: 3` or `{ count, delayMs }` (default 0; network errors, 408, 429, 5xx), `validate: schema` (Standard Schema: zod, valibot), `refetchEvery: 10000` (polling). `{ prefetch: '/api/quotes/2' }` on the sink warms the cache. SSR: `cache.set(request, data)`, `renderToString(App, { cache })`, `cache.dehydrate()` → `queryCache({ initial: snapshot })` (Vike: `pageContext.queryCache` from `+data`). Guide and recipes (list/detail + save, optimistic update, pagination, infinite list, save status): `node_modules/sygnal/dist/guide/resources.md` (online: https://sygnal.js.org/guide/resources/)
|
|
216
|
+
- List/detail + save, cached: `items: (s) => s.view === 'list' && { url: '/api/items', staleTime: 2000 }`, `item: (s) => s.view === 'detail' && { url: '/api/items/' + s.id, staleTime: 2000 }` (declared only while shown; shown again: cached data at once, refetched once stale), `makeFetchDriver({ cache: queryCache() })` in main.js **and** `renderComponent(App, { http: { cache: queryCache() } })` in every test (a `staleTime` without a queryCache is SYG635), the save `{ url, method: 'PUT', json, ok: 'SAVED', error: 'SAVE_FAILED', updates: 'item', invalidates: '/api/items' }`, status `r?.status === 'loading' ? 'Loading…' : r?.refreshing ? 'Updating…' : ''`.
|
|
217
|
+
- In a hidden Switchable page, resources and `connections` pause (fetches abort, sockets close) and resume on show; an entry with `background: true` (`{ url, background: true }`, `{ socket, background: true }`) stays live.
|
|
218
|
+
### Router (makeRouter + route) and HEAD
|
|
219
|
+
```jsx
|
|
220
|
+
// src/routes.js: one module the views import
|
|
221
|
+
import { makeRouter } from 'sygnal'
|
|
222
|
+
export const router = makeRouter({ routes: { home: '/', task: '/tasks/:id', admin: '/admin', notFound: '*' } }) // first match wins
|
|
223
|
+
export const { href } = router // href('task', { id: 2 }, { tab: 'notes' }) → '/tasks/2?tab=notes'
|
|
224
|
+
```
|
|
225
|
+
```jsx
|
|
226
|
+
// src/App.jsx; main.js: run(App, { ROUTER: router.driver, HEAD: makeHeadDriver({ titleTemplate: '%s · Tasks' }) })
|
|
227
|
+
import { ABORT, Switchable } from 'sygnal'
|
|
228
|
+
import { router, href } from './routes.js'
|
|
229
|
+
function App({ state }) {
|
|
230
|
+
return <main><nav><a href={href('home')}>All</a></nav><Switchable of={{ home: Home, task: TaskPage, admin: Admin, notFound: NotFound }} current={state.route.name} instance={state.route.path} /></main>
|
|
231
|
+
}
|
|
232
|
+
App.route = 'ROUTE' // ROUTE gets { name, params, query, hash, path } on start and every change
|
|
233
|
+
App.initialState = { route: router.current(), user: null, draft: '', leaving: null } // seeded: the first paint shows the right page
|
|
234
|
+
App.head = (state) => ({ title: state.route.name === 'task' ? `Task ${state.route.params.id}` : 'Tasks' }) // { title, meta, link }
|
|
235
|
+
App.model = {
|
|
236
|
+
ROUTE: { // a guard: refuse the route and redirect
|
|
237
|
+
STATE: (state, route) => (route.name === 'admin' && !state.user ? ABORT : { ...state, route, draft: '', leaving: null }),
|
|
238
|
+
ROUTER: (state, route) => (route.name === 'admin' && !state.user ? { to: 'home', replace: true } : ABORT),
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
TaskPage.model = { // a page shares the parent's state: no initialState (SYG405)
|
|
242
|
+
EDIT: { STATE: (state, draft) => ({ ...state, draft }), ROUTER: () => ({ block: 'CONFIRM_LEAVE' }) }, // unsaved changes
|
|
243
|
+
CONFIRM_LEAVE: (state, { proceed }) => ({ ...state, leaving: proceed }), // a blocked attempt: { to, route, proceed }
|
|
244
|
+
LEAVE: { STATE: (state) => ({ ...state, leaving: null }), ROUTER: (state) => ({ ...state.leaving, block: false }) },
|
|
245
|
+
DONE: { ROUTER: () => ({ to: 'home', block: false }) },
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
- Links are plain `<a href={href(...)}>`, never click handlers: the driver intercepts same-origin left clicks at the document (not modified clicks, `target`, `download`, `rel="external"`, `data-router-ignore`, other origins). `params` and `query` values are strings; `href()` is typed from the route table (an unknown name or a missing param is a type error; SYG130/SYG131 at run time). `instance={state.route.path}` re-creates the page for `/tasks/1` → `/tasks/2`. Pages derive `resources` from `state.route.params`.
|
|
249
|
+
- ROUTER commands: `{ to, params, query, hash, replace, scroll }`, `{ url }`, `{ back: true }`, `{ forward: true }`, `{ go: n }`, `{ block: 'ACTION' | false }` (also stops back/forward; `proceed` navigates anyway), `{ prefetch: 'task', params }` (calls `makeRouter({ prefetch })`). `route` is reserved for the declaration. A bad command is SYG620. The first declarer (the root) gets each route first, so keep guards there.
|
|
250
|
+
- Options: `makeRouter({ routes, base: '/app', mode: 'hash', scroll, focus })`. SSR: `router.current(req.url)` in the seeded state; `renderToString(App, { head: list })` + `renderHead(list)`. Vike: `makeRouter({ routes, navigate })` with `navigate` from `'vike/client/router'`, registered in `pages/+drivers.js`. HEAD: a later-mounted component's title wins; `meta`/`link` merge; a model entry can also send `HEAD: (state, data) => ({ title })`. Guides: https://sygnal.js.org/guide/router/, https://sygnal.js.org/guide/head/
|
|
251
|
+
|
|
252
|
+
## 4. API facts
|
|
253
|
+
- **Child props**: `<Rating name="food" value={state.food} />` → `function Rating({ state, name, value })`; reducers read `props.name` (4th arg); intent gets the `props$` stream. Reserved: `state` (lens: `"key"` or `{ get, set }`), `children`, `slots`, `context`, `uid` (SYG106; an error in strict mode). Without `state=` a child shares its parent's whole state. A child rendered by tag (`<Stopwatch state="stopwatch" />`) has no `initialState`: its start values go in the parent's (`stopwatch: { ms: 0 }`); its own throws SYG405 and the parent renders its error fallback (a test rendering the child as the root doesn't show it). Test a child alone with `renderComponent(Child, { initialState, context: { lang: 'fr' } })` (the ancestors' context). `uid(name?)` (a view prop, also on reducers' `props`) is a stable per-instance id for `id`/`for`/`aria-*` pairs: `<label for={uid('email')}>`, `<input id={uid('email')} />`.
|
|
254
|
+
- **A sliced child with its own start values**: `Editor.isolatedState = true` lets `<Editor state="draft" />` keep an `initialState`, used only while `state.draft` is `undefined` (an existing slice is kept; SYG425 warns when it lacks keys the `initialState` has). To start it from its `initialState` on every mount: `<Editor state="draft" resetState />`.
|
|
255
|
+
- **CHILD.select(Comp)** emits exactly what the child's `PARENT` function returned, for every instance (Collection items too). Put an id in the payload.
|
|
256
|
+
- **Extract a component without changing markup**: snapshot `t.html()` on the unchanged code first; move the elements verbatim into the child (a component adds no wrapper element and no attributes, so the HTML stays identical); props in, `PARENT` out; the parent drops its old selectors and uses `CHILD.select(Child)`; the snapshot must still match. Recipe: https://sygnal.js.org/guide/parent-child/#recipe-extract-a-component-without-changing-the-markup
|
|
257
|
+
- **run(App, drivers = {}, { mountPoint = '#root', diagnostics, onError })** returns `{ sources, sinks, dispose, hmr }`. `onError: (error, { componentName, action, phase }) => …` reports every error to a tracker (reporting only; a component's `.onError` still picks the fallback). Phase `'patch'`: a DOM patch threw, the app's DOM stops updating and the mount point gets `data-sygnal-error="patch"` (show a reload banner). DOM, EVENTS, LOG and STATE are built in. `app.sources.STATE.stream` is the state stream; `app.dispose()` fires DISPOSE.
|
|
258
|
+
- **ABORT**: from a STATE reducer, the state is unchanged (returning the identical `state` object also is: no emission, no render; except for an action triggered by an `input`/`change` event, which re-renders so a refused controlled field shows the state's value again); from any other sink, nothing is sent.
|
|
259
|
+
- **React to a state change** (save a draft, reload when a filter changes): `SAVE: STATE.watch(state => state.text).compose(debounce(1000))` in intent emits the selected value each time it changes (compared structurally; `{ immediate: true }` emits the current one first; a Collection item watches its own state). Not `STATE.stream` + `dropRepeats()`.
|
|
260
|
+
- **Shorthands**: `DOM.<event>('.sel')` = `DOM.select('.sel').events('<event>')` for every real DOM event name (`DOM.key`, `DOM.enter` never fire: SYG115). `blur`/`focus` don't bubble, but `DOM.blur('.field')` / `DOM.focus('.field')` work (listener on the element); for any field inside a container use the bubbling `DOM.focusout('.form')` / `DOM.focusin`. Escape anywhere: `DOM.keydown('document').key().filter(k => k === 'Escape')`. `preventDefault`: `DOM.select('form').events('submit', { preventDefault: true })`; a function picks the events: `DOM.select('document').events('keydown', { preventDefault: (e) => e.ctrlKey && e.key === 'z' })`. **Enriched streams** (chainable, optional mapper): `.value(fn?)` e.target.value; `.checked(fn?)` boolean; `.key(fn?)` e.key; `.target(fn?)`; `.data('id', Number)` reads `data-id` on the target or its nearest ancestor that has it (JSX: `data={{ id: 7 }}`). camelCase names map to kebab-case attributes: `.data('taskId')` reads `data-task-id` (`data={{ taskId }}`); a `data-task-id="1"` attribute works too, but a kebab key in the prop (`data={{ 'task-id': 1 }}`) throws a DOMException (SYG421).
|
|
261
|
+
- **TypeScript**: `const intent = ({ DOM }: IntentSources<State>) => ({ ... })`, then `const C: Component<State, Props, {}, ActionsOf<typeof intent>, Calculated, Context, { PARENT: Payload }> = ({ state, context }) => ...` (root: `RootComponent<State, {}, Actions>`; unused parameters `{}`). Without `{ PARENT: Payload }` the parent's `CHILD.select(C)` is a `Stream<unknown>`. In JSX pass `state="slice"`, a lens, or nothing, plus the declared props. Register EVENTS names in `declare module 'sygnal' { interface SygnalEvents { DOC_SAVED: { id: string } } }` in a file that keeps `export {}` (without it the block replaces the 'sygnal' types: "has no exported member"): with an empty registry no event names are type-checked. Guide: https://sygnal.js.org/integration/typescript/
|
|
262
|
+
- **xstream**: `xs.merge/combine/of/periodic/never/fromPromise`; methods `map mapTo filter startWith fold take drop last endWhen flatten compose remember replaceError debug`. From `'sygnal'`, used with `.compose(...)`: `debounce(ms) throttle(ms) delay(ms) dropRepeats() sampleCombine(other$) flattenConcurrently flattenSequentially`; plus `concat(a$, b$)`. **RxJS → xstream**: `pipe(a, b)` → chain `.a().b()` / `.compose(op)`; `switchMap(f)` → `.map(f).flatten()`; `mergeMap(f)` → `.map(f).compose(flattenConcurrently)`; `concatMap(f)` → `.map(f).compose(flattenSequentially)`; `debounceTime(ms)` → `.compose(debounce(ms))`; `throttleTime(ms)` → `.compose(throttle(ms))`; `distinctUntilChanged()` → `.compose(dropRepeats())`; `withLatestFrom(b$)` → `.compose(sampleCombine(b$))`; `combineLatest(a$, b$)` → `xs.combine(a$, b$)`; `merge` → `xs.merge`; `scan(f, seed)` → `.fold(f, seed)`; `skip(n)` / `first()` → `.drop(n)` / `.take(1)`; `takeUntil(b$)` → `.endWhen(b$)`; `tap(f)` → `.debug(f)`; `catchError(f)` → `.replaceError(f)`; `shareReplay(1)` → `.remember()`.
|
|
263
|
+
- More (guides): View Transitions (`viewTransitions = ['MOVE']` + `makeViewTransitionDOMDriver`; `<Collection viewTransitionName="card" />` names each item `card-<id>`, so moves and reorders animate, also across Collections with the same prefix) https://sygnal.js.org/guide/view-transitions/; SSR: the first client render adopts the server's elements (focus and typed text kept) `node_modules/sygnal/dist/guide/ssr.md`; a component as a custom element (`defineElement`, `'sygnal/element'`) https://sygnal.js.org/reference/api/#sygnalelement; DevTools action log and "Copy as test" https://sygnal.js.org/integration/debugging/; app-level error reporting `node_modules/sygnal/dist/guide/error-boundaries.md`; Testing Library https://sygnal.js.org/integration/testing/#testing-with-testing-library; `controls()` element tokens, an alternative to class selectors https://sygnal.js.org/guide/controls/
|
|
264
|
+
- Browser sources: `Card.browser = (state) => ({ seen: !state.seen && { intersection: '.cover', action: 'SEEN' }, dark: { media: '(prefers-color-scheme: dark)', action: 'DARK' } })` + `run(App, { BROWSER: makeBrowserDriver() })` (also `resize`, `storage`, `visibility`, `online`, `geolocation`; clipboard: `{ BROWSER: (s) => ({ copy: s.url, ok: 'COPIED', error: 'COPY_FAILED' }) }`; tests need no driver: `t.browser.intersect('.cover', true)`, `t.browser.media(query, true)`); `lazy(() => import('./Chart'), { when: 'visible' })` (or `'idle'`) defers the import. Guide: `node_modules/sygnal/dist/guide/browser-sources.md`
|
|
265
|
+
- Long lists (from ~1,000 rows): `<VirtualCollection of={Row} from="rows" className="rows" estimateSize={32} aria-label="Rows" />` (Collection's props; it is itself the scroll container, a `div` with `role="list"`: give `.rows` a bounded height in CSS and put `className`, `role`, `aria-label` on it, never wrap it in your own scrolling `<ul>`; SYG430 means fix that height; only rows in view have components, so keep row state in the array); jump with `ELEMENT: (s, index) => ({ scrollToIndex: '.rows', index })` or `{ scrollToId: '.rows', id }`. Guide: `node_modules/sygnal/dist/guide/virtual-collections.md`
|
|
266
|
+
- Reorderable list: `List.uses = { sort: sortable({ from: 'tasks', item: '.task', handle: '.grip' }) }` (mouse, touch, keyboard); items `<li className="task" data-id={state.id}><button className="grip" aria-label=… aria-describedby={context.sort.helpId}>` (context `sort: (s) => s.sort`; style from `context.sort.dragging` / `over` / `after`), host `<p id={state.sort.helpId} hidden>` instructions and `<p role="status" aria-live="assertive">{state.sort.message}</p>` (English defaults: "Dropped X at position 2 of 5.", "Reorder cancelled. X is back at position…"; a spec's exact wording: `messages: { lift, move, drop, cancel, stay }`, each `(label, position, count) => text`), save in `'sort.DROPPED': (state, { id, index, fromIndex }) => …` (the list is already reordered); two lists `from: ['todo', 'done']` + `data-list="todo"` on each container. HTML5 / file drops: `makeDragDriver`. Guide: `node_modules/sygnal/dist/guide/drag-and-drop.md`
|
|
267
|
+
- UI parts (`import { dialog, popover, tooltip, tabs, tabsAttrs, accordion, accordionAttrs, disclosure, disclosureAttrs, Toaster } from 'sygnal/ui'`; native HTML, unstyled): behaviors over your own markup in `uses` (`Profile.uses = { profile: dialog({ dialog: '.profile', trigger: '.edit', close: '.cancel' }) }` → `state.profile.open`, `'profile.CLOSED'`; for tabs/accordion/disclosure spread `tabsAttrs(state.tabs, uid).tab('general')` etc. for roles, ids and ARIA), and `<Toaster />` once + `EVENTS: event('TOAST', { text, kind, timeoutMs })` from anywhere (needs `makeTimerDriver()`). Guides: `node_modules/sygnal/dist/guide/ui/overview.md` (and `ui/dialog.md`, `popover`, `tooltip`, `tabs`, `accordion`, `disclosure`, `toaster`)
|
|
268
|
+
- Menu / Select / Combobox: `import { Select } from 'sygnal/ui/select'` (`sygnal/ui/menu`, `sygnal/ui/combobox`; each needs `@zag-js/vanilla` + its machine, e.g. `@zag-js/select@~1.45.0`): widget tags, `<Select className="size" label="Size" items={['S', 'M']} value={state.size} />` + `SIZE: DOM.select('.size').events('value-change').detail()` (Menu: `'select'`; Combobox also `'input-change'`), commands `{ open: '.size' }`. Escape hatch for one React/Preact component or a Zag machine: `fromReact(Comp, { events: { rate: 'onChange' } })` (`sygnal/react`) / `fromZag(menu, (api, props) => …, { events })` (`sygnal/zag`, `import * as menu from '@zag-js/menu'`) return widget tags; test them with `dom: 'real'`. Guides: `node_modules/sygnal/dist/guide/ui/menu.md` (`ui/select.md`, `ui/combobox.md`), `node_modules/sygnal/dist/guide/adapters.md`
|
|
269
|
+
- Third-party libraries (tested recipes): charts (Chart.js, ECharts), rich text (Tiptap), code editor (CodeMirror), carousel (Embla), data grid (AG Grid) are `defineWidget` tags; TanStack Table v9 runs in the view as a pure function of state (`constructTable` per render, settings in state); icons: `lucide` data → SVG vnodes with a helper called in the view (`{icon(Plus)}`, not a component); i18n: i18next `getFixedT(locale)` once per locale in `App.context = { t }`, the locale in state saved with `persist()` (never `changeLanguage()`). Recipes: `node_modules/sygnal/dist/guide/recipes/overview.md` (and `recipes/charts.md`, `code-editor`, `rich-text`, `carousel`, `data-table`, `data-grid`, `icons`, `i18n`)
|
|
270
|
+
|
|
271
|
+
## 5. Wiring rules (silent failures, and what catches them)
|
|
272
|
+
- **Selectors are scoped to the component's own JSX.** A parent's `DOM.click('.remove')` never fires for `.remove` rendered by a child or a Collection item. Handle the event in the child and send it up with `PARENT` (read with `CHILD.select(Child)`) or `EVENTS`. Caught as SYG104; a selector the view never renders is SYG110. Events still bubble like in the browser: a listener on an element the parent rendered itself (`<div className="slot"><Child /></div>` with `DOM.click('.slot')`) hears clicks from inside the child, after the child's own listeners.
|
|
273
|
+
- **Every intent action needs a model entry (SYG101), and every model entry needs a trigger (SYG102)**: an intent action, a built-in, or `next('X')`.
|
|
274
|
+
- **EVENTS types must match exactly** between `event('X')` and `EVENTS.select('X')` (SYG105). `event()` is the sink entry itself: `EVENTS: () => event('X')` sends a function, which nothing receives (SYG116).
|
|
275
|
+
- **Collection `from` must name an array field of the state** (SYG401). Over a calculated field it is read-only (item writes and removal are discarded): use `from="items"` with `sort=`/`filter=`.
|
|
276
|
+
- **A controlled input needs an input listener**: `value={state.x}` plus `DOM.input('.x').value()`, otherwise a re-render resets the text (SYG111). `value={null}` clears the field; leaving `value` out makes it uncontrolled.
|
|
277
|
+
- Also: reducers return the complete new state (`{ ...state, ... }`); never mutate (a mutated `state` returned as-is is ignored: SYG222); no side effects in views or STATE reducers (use EFFECT or a driver).
|
|
278
|
+
|
|
279
|
+
## 6. Diagnostics
|
|
280
|
+
- Format: `[Sygnal SYG104] Lane: <what is wrong>. <how to fix> https://sygnal.js.org/reference/errors#syg104`. Severities error/warn/info. 1xx wiring, 2xx model/state, 3xx streams (SYG301: RxJS operator on an xstream stream), 4xx components (Collection, Switchable, context), 5xx strict, 6xx drivers and setup, 7xx accessibility (static, in `sygnal-check` and the Vite dev checker: a clicked `div`, a form field without a label, `<img>` without `alt`, a button with no name; warn, also under `--strict`; `--a11y=error` makes them errors), 9xx internal.
|
|
281
|
+
- `npx --no-install sygnal-check` runs the locally installed checker, never a download (a `create-sygnal-app` project already has the `sygnal-check` dev dependency; elsewhere `npm i -D sygnal-check`; still not installed? skip it and rely on the runtime diagnostics in the tests). It checks `src` statically; pass other paths instead, e.g. `npx --no-install sygnal-check pages --strict` (Vike). Use `--strict` for canonical forms, `--fix` to apply the mechanical rewrites (implies `--strict`), and `--json` / `--verbose` for output. `npx --no-install sygnal-check explain SYG104` explains a code and its fix. Suppress one line with `// sygnal-ignore SYG110`.
|
|
282
|
+
- App graph: `npx --no-install sygnal-check --graph --json` (static), `t.inspect()` (test), `getDevTools()?.inspect()` (running dev app with `sygnal/devtools`, injected by `sygnal/vite`). All return the same `InspectGraph`: components, actions and their triggers, selectors (`matched`, `isolationHit`), EVENTS emitters/selectors, diagnostics.
|
|
283
|
+
- Vite plugin in dev (`vite`, never `vite build`): runtime checks print warnings to the console; when `sygnal-check` is installed, the plugin also runs it on start and on every save (terminal + browser console). Stricter: `sygnal({ diagnostics: { mode: 'error', strict: true }, check: { strict: true } })`.
|
|
284
|
+
- Without the plugin: `run(App, drivers, { diagnostics: 'warn' })` (`'collect'` → `getDiagnostics()`; `'error'` throws) and `import 'sygnal/diagnostics'` for the full checks. Runtime strict: `run(App, drivers, { diagnostics: { strict: true } })`, `configureStrict(true)` from `'sygnal/diagnostics'`, or `renderComponent(C, { strict: true })` (all need `'sygnal/diagnostics'`).
|
|
285
|
+
|
|
286
|
+
## 7. Testing your change
|
|
287
|
+
```js
|
|
288
|
+
import { it, expect, afterEach } from 'vitest'
|
|
289
|
+
import { renderComponent } from 'sygnal'
|
|
290
|
+
import TaskList from './TaskList.jsx'
|
|
291
|
+
let t
|
|
292
|
+
afterEach(() => t?.dispose())
|
|
293
|
+
it('picks, then removes a task', async () => {
|
|
294
|
+
t = renderComponent(TaskList, { strict: true }) // also: initialState, context, drivers, mockConfig, diagnostics, timeoutMs, settleMs, eventWaitMs
|
|
295
|
+
t.simulateEvent('.pick', 'click') // first match; bubbles; reaches child components too
|
|
296
|
+
await t.next(s => s.picked === 1) // a state emitted after this call
|
|
297
|
+
t.simulateEvent('.task[data-id="2"] .remove', 'click')
|
|
298
|
+
await t.next(s => s.tasks.length === 1)
|
|
299
|
+
await t.settle() // nothing pending anywhere in the tree
|
|
300
|
+
expect(t.html()).not.toContain('Test')
|
|
301
|
+
t.expectNoDiagnostics() // throws on any warn/error diagnostic
|
|
302
|
+
})
|
|
303
|
+
```
|
|
304
|
+
- `simulateEvent(sel, type, init?)`: init `{ value }`, `{ checked }`, `{ key: 'Enter' }`, `{ data: { id: 2 } }`; `'document'` targets `DOM.select('document')` listeners. Calls made before the component is ready are buffered (`await t.ready()` is optional). `sel` matches the rendered tree like the real DOM: tag, `.class`, `#id`, `[attr="v"]`, `:first-child`, `:last-child`, `:nth-child(n)`, `:nth-of-type(n)`, `:not()`, descendant and `>` (e.g. `.list:nth-child(2) .card:first-child .next`). If it matches nothing (after waiting up to 300ms for a render), the test fails with an error naming the selector (check `t.html()`, or select by attribute: `[data-id="3"]`); `{ allowMissing: true }` drops the event instead. `:has()`, `+`, `~` throw "unsupported selector syntax". `sel` is always a selector string, never an element (`t.query()` returns snapshots: select by attribute, `'.remove[data-id="2"]'`).
|
|
305
|
+
- `t.next(pred)` matches only future states (right after `await t.ready()`, also those of the buffered calls). `t.waitForState(pred)` also matches states already recorded (e.g. the initial one), so use it only for a state that can't already exist. Both resolve after the whole tree has rendered. `await t.respond(…)` / `t.fail(…)` / `t.settle()` resolve once the state is there: assert on `t.state` next; a `t.next(pred)` after them waits for a further state and times out ("a state recorded before this next() call already matches"). `t.settle()` doesn't wait out a model `next('X', data, ms)` longer than its 20ms window: `await t.next(pred)` instead (timeout errors name the pending `next()`). `t.state` is the latest state (`t.states.at(-1)`), with current calculated fields (also after a child or Collection item writes); `t.states`, `t.emitted` (EVENTS sent) and `t.sinkValues('PARENT')` / `t.sinkValues('API')` (any sink, children's too) are live arrays. `t.html()` is serialised like `innerHTML` (text `Couldn't`, not `'`) and throws before the first render: `await t.ready()` (or a `t.next`) first. Timer tests (debounce/delay): `vi.useFakeTimers()` before `renderComponent`, then `await t.ready()`; `await vi.advanceTimersByTimeAsync(ms)` moves time (e.g. 299 ms: nothing sent yet), and `t.next`/`t.waitForState`/`t.settle`/`t.ready` advance the fake clock themselves until they resolve, so no real waiting. Use it instead of sleeping out a real debounce. `vi.useRealTimers()` in `afterEach`.
|
|
306
|
+
- `t.actions` logs every action of the tree, `{ type, data, component, instance, sinks, cause, at }` (`cause`: `'intent' | 'next' | 'reply' | 'built-in' | 'simulateAction' | 'behavior'`; `sinks`: those that produced a value): when "nothing happened", it shows whether the action ran and what it produced. `t.explain(s => s.count === 5)` returns the action that produced a state.
|
|
307
|
+
- `t.simulateAction('LOADED', data)` pushes an action straight into intent → model (all sinks run). Use it for actions without a DOM trigger. **Drivers need no wiring in tests**: the HTTP fake is the real `makeFetchDriver` over an in-memory fetch, so every request stays pending until the test answers it. `t.requests('HTTP')` lists the requests as objects (a string URL is `{ url }`; never `{ abort }`/`{ refresh }`/`{ resources }` values). `await t.respond('HTTP', body, 'RESULTS')` delivers `body` as that request's `ok` action and resolves once it is reduced and rendered; `await t.fail('HTTP', 404 | error, target?)` sends `{ error, status, body, request }` to its `error` action. Target: an action or key name, a resource name, a URL, a partial request (`{ url: '/items/2' }`), or a predicate; none = the newest pending. A request superseded by `latest: true`, aborted or already answered isn't pending, and answering it **throws at the call**: `expect(() => t.respond('HTTP', {}, { query: { q: 'du' } })).toThrow()`. Identical requests (a resource refetch, a second click) match the same targets: `{ nth: 0 }` answers exactly `t.requests('HTTP')[0]` (`-1` the newest; with `request`, counting only its matches), and throws if it isn't pending. A call made while earlier input is still queued (a debounce) waits up to 1s for its request instead. Each send is its own request. The fake can't see `makeFetchDriver({ latest: true })` from main.js: write `latest: true` on the request.
|
|
308
|
+
- Sockets need no driver either: a component with `connections` gets a fake `WS` sink running the real driver on in-memory sockets (`{ socketSink: 'SOCKET' }` for another name). Connections open by themselves; `renderComponent(C, { autoConnect: false })` holds them in `'connecting'` until `await t.open('WS')`. `await t.push('WS', { text: 'hi' })` is a server frame (SSE named event: `t.push('WS', data, { event: 'price-update' })`); `await t.drop('WS', { code: 1011 })` is a close the app didn't make (`close` fires, the retry runs on the test's timers: `await vi.advanceTimersByTimeAsync(1000)`); `t.sent('WS')` lists the `{ to, json }` values; `t.connections('WS')` the declared connections (`name`, `url`, `state`). They throw too when nothing matches.
|
|
309
|
+
- Resources: each fetch is listed as `{ url, ...request, resource: 'quote' }` and answered by resource name, URL or partial request: `await t.respond('HTTP', { text: 'Hi' }, 'quote')`, `await t.fail('HTTP', 500, 'quote')`. A resource request is sent after the state changes; `t.respond` by resource name right after the `simulate*` call that changes it waits for that fetch. Cache: `renderComponent(C, { http: { cache: queryCache({ staleTime: 2000 }) } })` (any driver options); `t.cache('HTTP')` lists `{ key, age, stale, subscribers, data, tags }`; `t.focus()` / `t.online()` fire the refetch triggers; retries and `refetchEvery` run on the test's timers.
|
|
310
|
+
- Router: `renderComponent(App, { router, url: '/tasks/2' })` runs the router's real driver over an in-memory history (a component that declares `route` needs the `router` option). `await t.navigate('/admin')` (as a link click) or `await t.navigate({ to: 'task', params: { id: 1 } })`, `await t.back()` / `t.forward()`, `t.location` (`{ path, search, hash, href }`), `t.sent('ROUTER')` (the commands, not the declarations); a `simulateEvent` click on an `<a>` goes through the router's link handling; `{ block }` stops all of them. HEAD: `t.head()` returns the merged `{ title, meta, link }` (`{ titleTemplate }` option).
|
|
311
|
+
- TypeScript tests: `renderComponent` infers the state type; for a handle assigned in `beforeEach`, declare `let t: RenderResult<State>` (import the type from `'sygnal'`), not `any`. With the Vite plugin, Vitest gets `sygnal/diagnostics` in its setupFiles automatically; otherwise `import 'sygnal/diagnostics'` in the test.
|
|
312
|
+
- Real DOM (state the user typed or clicked, focus, refs, any CSS selector): `renderComponent(C, { dom: 'real' })` (`'real'`, not `'jsdom'`) in a jsdom test (`// @vitest-environment jsdom`, `npm i -D jsdom`), same `t.*` API in ONE suite (no separate `run()` + jsdom suite). `simulateEvent` then dispatches real events on any CSS selector (`{ value }` types, `'click'` toggles a checkbox/radio and skips disabled buttons, `'focus'`/`'blur'` move focus).
|
|
313
|
+
- Assert on elements: `t.query(sel)` / `t.queryAll(sel)` also work on the default mock DOM, as read-only snapshots of what the view rendered (`textContent`, `value`, `checked`, `disabled`, `getAttribute`, `classList`, `dataset`, `querySelector`, `closest`; `focus()` throws): `expect(t.query('input[name="plan"]:checked').value).toBe('team')`, `t.query('.save').disabled`, `t.queryAll('.row')`; with `dom: 'real'` they are real elements (`document.activeElement === t.query('input[name="city"]')`). Every wait (`ready`, `next`, `waitForState`, `settle`) resolves once its state is rendered, so query right after the `await` (before the first render, `t.query` throws: `await t.ready()`). `await t.next(a); await t.next(b)` also sees a `b` that arrived while the DOM showed `a`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sygnal",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "An intuitive framework for building fast and small components or applications based on Cycle.js",
|
|
5
5
|
"main": "./dist/index.cjs.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -26,6 +26,52 @@
|
|
|
26
26
|
"import": "./dist/jsx-dev-runtime.esm.js",
|
|
27
27
|
"require": "./dist/jsx-dev-runtime.cjs.js"
|
|
28
28
|
},
|
|
29
|
+
"./diagnostics": {
|
|
30
|
+
"types": "./src/extra/diagnostics/checks/public.d.ts",
|
|
31
|
+
"import": "./dist/diagnostics.esm.js",
|
|
32
|
+
"require": "./dist/diagnostics.cjs.js"
|
|
33
|
+
},
|
|
34
|
+
"./devtools": {
|
|
35
|
+
"types": "./src/devtools.d.ts",
|
|
36
|
+
"import": "./dist/devtools.esm.js",
|
|
37
|
+
"require": "./dist/devtools.cjs.js"
|
|
38
|
+
},
|
|
39
|
+
"./element": {
|
|
40
|
+
"types": "./src/element.d.ts",
|
|
41
|
+
"import": "./dist/element.esm.js",
|
|
42
|
+
"require": "./dist/element.cjs.js"
|
|
43
|
+
},
|
|
44
|
+
"./ui": {
|
|
45
|
+
"types": "./src/ui.d.ts",
|
|
46
|
+
"import": "./dist/ui.esm.js",
|
|
47
|
+
"require": "./dist/ui.cjs.js"
|
|
48
|
+
},
|
|
49
|
+
"./ui/menu": {
|
|
50
|
+
"types": "./src/ui-menu.d.ts",
|
|
51
|
+
"import": "./dist/ui-menu.esm.js",
|
|
52
|
+
"require": "./dist/ui-menu.cjs.js"
|
|
53
|
+
},
|
|
54
|
+
"./ui/select": {
|
|
55
|
+
"types": "./src/ui-select.d.ts",
|
|
56
|
+
"import": "./dist/ui-select.esm.js",
|
|
57
|
+
"require": "./dist/ui-select.cjs.js"
|
|
58
|
+
},
|
|
59
|
+
"./ui/combobox": {
|
|
60
|
+
"types": "./src/ui-combobox.d.ts",
|
|
61
|
+
"import": "./dist/ui-combobox.esm.js",
|
|
62
|
+
"require": "./dist/ui-combobox.cjs.js"
|
|
63
|
+
},
|
|
64
|
+
"./zag": {
|
|
65
|
+
"types": "./src/zag.d.ts",
|
|
66
|
+
"import": "./dist/zag.esm.js",
|
|
67
|
+
"require": "./dist/zag.cjs.js"
|
|
68
|
+
},
|
|
69
|
+
"./react": {
|
|
70
|
+
"types": "./src/react.d.ts",
|
|
71
|
+
"import": "./dist/react.esm.js",
|
|
72
|
+
"require": "./dist/react.cjs.js"
|
|
73
|
+
},
|
|
74
|
+
"./shims/globalthis": "./dist/shims/globalthis.cjs",
|
|
29
75
|
"./vite": {
|
|
30
76
|
"types": "./src/vite/plugin.d.ts",
|
|
31
77
|
"import": "./dist/vite/plugin.mjs",
|
|
@@ -48,18 +94,15 @@
|
|
|
48
94
|
},
|
|
49
95
|
"./vike": {
|
|
50
96
|
"types": "./src/vike/types.ts",
|
|
51
|
-
"
|
|
52
|
-
"require": "./dist/vike/+config.cjs.js"
|
|
97
|
+
"default": "./dist/vike/config/+config.js"
|
|
53
98
|
},
|
|
54
99
|
"./vike/config": {
|
|
55
100
|
"types": "./src/vike/types.ts",
|
|
56
|
-
"
|
|
57
|
-
"require": "./dist/vike/+config.cjs.js"
|
|
101
|
+
"default": "./dist/vike/config/+config.js"
|
|
58
102
|
},
|
|
59
103
|
"./config": {
|
|
60
104
|
"types": "./src/vike/types.ts",
|
|
61
|
-
"
|
|
62
|
-
"require": "./dist/vike/+config.cjs.js"
|
|
105
|
+
"default": "./dist/vike/config/+config.js"
|
|
63
106
|
},
|
|
64
107
|
"./vike/onRenderHtml": {
|
|
65
108
|
"import": "./dist/vike/onRenderHtml.mjs",
|
|
@@ -80,25 +123,32 @@
|
|
|
80
123
|
},
|
|
81
124
|
"files": [
|
|
82
125
|
"dist",
|
|
83
|
-
"src"
|
|
126
|
+
"src",
|
|
127
|
+
"llms.txt",
|
|
128
|
+
"CHANGELOG.md"
|
|
84
129
|
],
|
|
85
130
|
"scripts": {
|
|
86
|
-
"test": "vitest run && npm run test:types && npm run test:browser",
|
|
87
|
-
"test:
|
|
131
|
+
"test": "vitest run && npm run test:examples && npm run test:types && npm run test:browser && npm run test:perf-gate",
|
|
132
|
+
"test:perf-gate": "node scripts/perf-gate.mjs",
|
|
133
|
+
"test:examples": "node scripts/test-examples.mjs",
|
|
134
|
+
"test:recipes": "node scripts/test-recipes.mjs",
|
|
135
|
+
"test:types": "npx tsc --project tsconfig.json --noEmit && npx tsc --project type-tests/tsconfig.json",
|
|
88
136
|
"test:browser": "npm test --prefix browser-tests",
|
|
89
137
|
"test:watch": "vitest",
|
|
90
|
-
"
|
|
138
|
+
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
139
|
+
"build": "npm run clean && npx rollup -c && npx rollup -c rollup.config.dts.mjs && node scripts/copy-guides.mjs",
|
|
91
140
|
"build:types": "npx rollup -c rollup.config.dts.mjs",
|
|
92
|
-
"build:all": "npm run build
|
|
141
|
+
"build:all": "npm run build",
|
|
93
142
|
"build:devtools": "node devtools/build.mjs",
|
|
94
|
-
"start": "npx rollup -c -w"
|
|
143
|
+
"start": "npx rollup -c -w",
|
|
144
|
+
"prepublishOnly": "npm run build:all"
|
|
95
145
|
},
|
|
96
146
|
"repository": {
|
|
97
147
|
"type": "git",
|
|
98
148
|
"url": "git+https://github.com/tpresley/sygnal.git"
|
|
99
149
|
},
|
|
100
150
|
"engines": {
|
|
101
|
-
"node": ">=12.0
|
|
151
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
102
152
|
},
|
|
103
153
|
"keywords": [
|
|
104
154
|
"cycle",
|
|
@@ -114,19 +164,62 @@
|
|
|
114
164
|
},
|
|
115
165
|
"homepage": "https://github.com/tpresley/sygnal#readme",
|
|
116
166
|
"dependencies": {
|
|
117
|
-
"
|
|
167
|
+
"@tanstack/virtual-core": "^3.17.11",
|
|
118
168
|
"snabbdom": "^3.6.3",
|
|
119
169
|
"xstream": "^11.14.0"
|
|
120
170
|
},
|
|
171
|
+
"peerDependencies": {
|
|
172
|
+
"@zag-js/combobox": "~1.45.0",
|
|
173
|
+
"@zag-js/menu": "~1.45.0",
|
|
174
|
+
"@zag-js/select": "~1.45.0",
|
|
175
|
+
"@zag-js/vanilla": "~1.45.0",
|
|
176
|
+
"react": ">=18.0.0",
|
|
177
|
+
"react-dom": ">=18.0.0"
|
|
178
|
+
},
|
|
179
|
+
"peerDependenciesMeta": {
|
|
180
|
+
"@zag-js/combobox": {
|
|
181
|
+
"optional": true
|
|
182
|
+
},
|
|
183
|
+
"@zag-js/menu": {
|
|
184
|
+
"optional": true
|
|
185
|
+
},
|
|
186
|
+
"@zag-js/select": {
|
|
187
|
+
"optional": true
|
|
188
|
+
},
|
|
189
|
+
"@zag-js/vanilla": {
|
|
190
|
+
"optional": true
|
|
191
|
+
},
|
|
192
|
+
"react": {
|
|
193
|
+
"optional": true
|
|
194
|
+
},
|
|
195
|
+
"react-dom": {
|
|
196
|
+
"optional": true
|
|
197
|
+
}
|
|
198
|
+
},
|
|
121
199
|
"devDependencies": {
|
|
122
200
|
"@rollup/plugin-commonjs": "^25.0.4",
|
|
123
201
|
"@rollup/plugin-node-resolve": "^15.2.1",
|
|
124
202
|
"@rollup/plugin-terser": "^0.4.3",
|
|
125
203
|
"@rollup/plugin-typescript": "^11.1.6",
|
|
204
|
+
"@zag-js/combobox": "1.45.0",
|
|
205
|
+
"@zag-js/dialog": "1.45.0",
|
|
206
|
+
"@zag-js/menu": "1.45.0",
|
|
207
|
+
"@zag-js/select": "1.45.0",
|
|
208
|
+
"@zag-js/vanilla": "1.45.0",
|
|
209
|
+
"flatpickr": "4.6.13",
|
|
210
|
+
"jsdom": "^29.1.1",
|
|
211
|
+
"preact": "11.0.0",
|
|
212
|
+
"react": "19.3.0",
|
|
213
|
+
"react-dom": "19.3.0",
|
|
126
214
|
"rollup": "^3.28.1",
|
|
127
215
|
"rollup-plugin-dts": "^6.1.1",
|
|
128
216
|
"tslib": "^2.6.2",
|
|
129
217
|
"typescript": "^5.4.5",
|
|
130
|
-
"
|
|
218
|
+
"valibot": "^1.5.0",
|
|
219
|
+
"vitest": "^4.0.18",
|
|
220
|
+
"zod": "^4.6.5"
|
|
221
|
+
},
|
|
222
|
+
"overrides": {
|
|
223
|
+
"vite": "^7.3.6"
|
|
131
224
|
}
|
|
132
225
|
}
|