sygnal 4.6.1 → 5.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/README.md CHANGED
@@ -1,25 +1,25 @@
1
1
  # Sygnal
2
2
 
3
- An intuitive framework for building fast, small and composable components or applications.
3
+ A reactive component framework with pure functions, zero side effects, and automatic state management.
4
4
 
5
- Sygnal is built on top of [Cycle.js](https://cycle.js.org/), and allows you to write functional reactive, Observable based, components with fully isolated side-effects without having to worry about the complex plumbing usually associated with functional reactive programming.
5
+ [![npm version](https://img.shields.io/npm/v/sygnal.svg?style=flat-square)](https://www.npmjs.com/package/sygnal)
6
+ [![npm downloads](https://img.shields.io/npm/dm/sygnal.svg?style=flat-square)](https://www.npmjs.com/package/sygnal)
7
+ [![license](https://img.shields.io/npm/l/sygnal.svg?style=flat-square)](https://github.com/tpresley/sygnal/blob/main/LICENSE)
8
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/sygnal?style=flat-square&label=bundle%20size)](https://pkg-size.dev/sygnal)
6
9
 
7
- Components and applications written using Sygnal look similar to React functional components, and can be nested just as easily, but have many benefits including:
8
- - 100% pure components with absolutely no side effects
9
- - No need for component state management (it's handled automatically at the application level)
10
- - Small bundle sizes
11
- - Fast build times
12
- - Fast rendering
13
- - Close to zero boiler plate code
10
+ ---
14
11
 
15
- ## Documentation
12
+ ## Why Sygnal?
16
13
 
17
- - **[Getting Started](./docs/getting-started.md)** — Installation, setup, and your first interactive component
18
- - **[Guide](./docs/guide.md)** — In-depth coverage of all features: state, MVI, collections, drivers, context, TypeScript, and more
19
- - **[API Reference](./docs/api-reference.md)** — Complete reference for all exports, types, and configuration options
14
+ - **Pure components** — Views are plain functions. All side effects are handled by drivers, outside your code.
15
+ - **Automatic state management** — Monolithic state tree with no store setup, no providers, no hooks. Trivial undo/redo and time-travel debugging.
16
+ - **Model-View-Intent** — Cleanly separate *what* happens (Model), *when* it happens (Intent), and *how* it looks (View).
17
+ - **Tiny footprint** — Three runtime dependencies: [snabbdom](https://github.com/snabbdom/snabbdom), [xstream](https://github.com/staltz/xstream), and [extend](https://github.com/nicjohnson145/extend).
20
18
 
21
19
  ## Quick Start
22
20
 
21
+ **Scaffold a new project:**
22
+
23
23
  ```bash
24
24
  npx degit tpresley/sygnal-template my-app
25
25
  cd my-app
@@ -27,15 +27,15 @@ npm install
27
27
  npm run dev
28
28
  ```
29
29
 
30
- Or install manually:
30
+ **Or add to an existing project:**
31
31
 
32
32
  ```bash
33
33
  npm install sygnal
34
34
  ```
35
35
 
36
- ## In a Nutshell
36
+ ## A Sygnal Component
37
37
 
38
- A Sygnal component is a function (the **view**) with static properties that define **when** things happen (`.intent`) and **what** happens (`.model`):
38
+ A component is a function (the **view**) with static properties that define **when** things happen (`.intent`) and **what** happens (`.model`):
39
39
 
40
40
  ```jsx
41
41
  function Counter({ state }) {
@@ -50,20 +50,18 @@ function Counter({ state }) {
50
50
 
51
51
  Counter.initialState = { count: 0 }
52
52
 
53
- // Intent: WHEN things happen
54
53
  Counter.intent = ({ DOM }) => ({
55
54
  INCREMENT: DOM.select('.increment').events('click'),
56
- DECREMENT: DOM.select('.decrement').events('click')
55
+ DECREMENT: DOM.select('.decrement').events('click'),
57
56
  })
58
57
 
59
- // Model: WHAT happens
60
58
  Counter.model = {
61
- INCREMENT: (state) => ({ count: state.count + 1 }),
62
- DECREMENT: (state) => ({ count: state.count - 1 })
59
+ INCREMENT: (state) => ({ ...state, count: state.count + 1 }),
60
+ DECREMENT: (state) => ({ ...state, count: state.count - 1 }),
63
61
  }
64
62
  ```
65
63
 
66
- Start it with `run()`:
64
+ Start it:
67
65
 
68
66
  ```javascript
69
67
  import { run } from 'sygnal'
@@ -72,54 +70,105 @@ import Counter from './Counter.jsx'
72
70
  run(Counter)
73
71
  ```
74
72
 
75
- That's it. No store setup, no providers, no hooks — just a function and some properties.
73
+ No store setup, no providers, no hooks — just a function and some properties.
76
74
 
77
- ## Built on Cycle.js
75
+ ## Features
78
76
 
79
- Sygnal is built on top of Cycle.js which is a functional reactive coding framework that asks "what if the user was a function?"
77
+ ### Collections
80
78
 
81
- It is worth reading the summary on the [Cycle.js homepage](https://cycle.js.org/), but essentially Cycle.js allows you to write simple, concise, extensible, and testable code using a functional reactive style, and helps ensure that all side-effects are isolated away from your component code.
79
+ Render dynamic lists with built-in filtering and sorting:
82
80
 
83
- Sygnal takes it a step further, and makes it easy to write arbitrarily complex applications that have all of the Cycle.js benefits, but with a much easier learning curve, and virtually no complex plumbing or boiler plate code.
81
+ ```jsx
82
+ <Collection of={TodoItem} from="items" filter={item => !item.done} sort="name" />
83
+ ```
84
84
 
85
- ## Key Features
85
+ ### Switchable
86
86
 
87
- ### Model-View-Intent Architecture
87
+ Swap between components based on state:
88
88
 
89
- Separate **what** your component does (Model), **when** it does it (Intent), and **how** it's displayed (View). All side effects are delegated to drivers, keeping your components 100% pure.
89
+ ```jsx
90
+ <Switchable of={{ home: HomePage, settings: SettingsPage }} current={state.activeTab} />
91
+ ```
90
92
 
91
- ### Monolithic State
93
+ ### Context
92
94
 
93
- Automatic application-level state management with no setup. Trivial undo/redo, state restoration, and time-travel debugging.
95
+ Top-down data propagation without prop drilling:
94
96
 
95
- ### Collections
97
+ ```jsx
98
+ App.context = {
99
+ theme: (state) => state.settings.theme,
100
+ currentUser: (state) => state.auth.user,
101
+ }
102
+
103
+ function Child({ state, context }) {
104
+ return <div className={context.theme}>{context.currentUser.name}</div>
105
+ }
106
+ ```
107
+
108
+ ### Parent-Child Communication
96
109
 
97
- Render dynamic lists with filtering and sorting built in:
110
+ Structured message passing between components:
98
111
 
99
112
  ```jsx
100
- <collection of={TodoItem} from="items" filter={item => !item.done} sort="name" />
113
+ // Child emits
114
+ TaskCard.model = {
115
+ DELETE: { PARENT: (state) => ({ type: 'DELETE', taskId: state.id }) }
116
+ }
117
+
118
+ // Parent receives (use component reference — minification-safe)
119
+ Lane.intent = ({ CHILD }) => ({
120
+ TASK_DELETED: CHILD.select(TaskCard).filter(e => e.type === 'DELETE'),
121
+ })
101
122
  ```
102
123
 
103
- ### Switchable Components
124
+ ### Event Bus
104
125
 
105
- Swap between components based on state:
126
+ Global broadcast for cross-component communication:
127
+
128
+ ```jsx
129
+ // Any component can emit
130
+ Publisher.model = {
131
+ NOTIFY: { EVENTS: (state) => ({ type: 'notification', data: state.message }) }
132
+ }
133
+
134
+ // Any component can subscribe
135
+ Subscriber.intent = ({ EVENTS }) => ({
136
+ HANDLE: EVENTS.select('notification'),
137
+ })
138
+ ```
139
+
140
+ ### Calculated Fields
141
+
142
+ Derived state with optional dependency tracking:
106
143
 
107
144
  ```jsx
108
- <switchable of={{ home: HomePage, settings: SettingsPage }} current={state.activeTab} />
145
+ Invoice.calculated = {
146
+ subtotal: [['items'], (state) => sum(state.items.map(i => i.price))],
147
+ tax: [['subtotal'], (state) => state.subtotal * 0.08],
148
+ total: [['subtotal', 'tax'], (state) => state.subtotal + state.tax],
149
+ }
109
150
  ```
110
151
 
111
152
  ### Form Handling
112
153
 
113
- Extract form values without the plumbing:
154
+ Extract form values without the boilerplate:
114
155
 
115
156
  ```jsx
116
- import { processForm } from 'sygnal'
117
-
118
157
  MyForm.intent = ({ DOM }) => ({
119
- SUBMITTED: processForm(DOM.select('.my-form'), { events: 'submit' })
158
+ SUBMITTED: processForm(DOM.select('.my-form'), { events: 'submit' }),
120
159
  })
121
160
  ```
122
161
 
162
+ ### Drag and Drop
163
+
164
+ HTML5 drag-and-drop with a dedicated driver:
165
+
166
+ ```javascript
167
+ import { makeDragDriver } from 'sygnal'
168
+
169
+ run(RootComponent, { DND: makeDragDriver() })
170
+ ```
171
+
123
172
  ### Custom Drivers
124
173
 
125
174
  Wrap any async operation as a driver:
@@ -137,7 +186,7 @@ run(RootComponent, { API: apiDriver })
137
186
 
138
187
  ### Error Boundaries
139
188
 
140
- Catch and recover from errors in child component rendering without crashing the app:
189
+ Catch and recover from rendering errors:
141
190
 
142
191
  ```jsx
143
192
  BrokenComponent.onError = (error, { componentName }) => (
@@ -145,31 +194,39 @@ BrokenComponent.onError = (error, { componentName }) => (
145
194
  )
146
195
  ```
147
196
 
148
- ### Refs (DOM Access)
197
+ ### Portals
149
198
 
150
- Access DOM elements declaratively:
199
+ Render children into a different DOM container:
151
200
 
152
201
  ```jsx
153
- import { createRef } from 'sygnal'
154
- const myRef = createRef()
155
-
156
- function MyComponent({ state }) {
157
- return <div ref={myRef}>Measured: {state.width}px</div>
158
- }
202
+ <Portal target="#modal-root">
203
+ <div className="modal">Modal content</div>
204
+ </Portal>
159
205
  ```
160
206
 
161
- ### Portals
207
+ ### Slots
162
208
 
163
- Render children into a different DOM container — essential for modals, tooltips, and dropdowns:
209
+ Pass named content regions to child components:
164
210
 
165
211
  ```jsx
166
- import { Portal } from 'sygnal'
212
+ import { Slot } from 'sygnal'
213
+
214
+ <Card state="card">
215
+ <Slot name="header"><h2>Title</h2></Slot>
216
+ <Slot name="actions"><button>Save</button></Slot>
217
+ <p>Default content</p>
218
+ </Card>
167
219
 
168
- {state.showModal && (
169
- <Portal target="#modal-root">
170
- <div className="modal">Modal content here</div>
171
- </Portal>
172
- )}
220
+ // In Card's view:
221
+ function Card({ state, slots }) {
222
+ return (
223
+ <div>
224
+ <header>{...(slots.header || [])}</header>
225
+ <main>{...(slots.default || [])}</main>
226
+ <footer>{...(slots.actions || [])}</footer>
227
+ </div>
228
+ )
229
+ }
173
230
  ```
174
231
 
175
232
  ### Transitions
@@ -177,49 +234,93 @@ import { Portal } from 'sygnal'
177
234
  CSS-based enter/leave animations:
178
235
 
179
236
  ```jsx
180
- import { Transition } from 'sygnal'
181
-
182
- <Transition enter="fade-in" leave="fade-out">
237
+ <Transition name="fade" duration={300}>
183
238
  {state.visible && <div>Animated content</div>}
184
239
  </Transition>
185
240
  ```
186
241
 
187
- ### Lazy Loading
242
+ ### Lazy Loading & Suspense
188
243
 
189
- Code-split components with automatic placeholder rendering:
244
+ Code-split components with loading boundaries:
190
245
 
191
246
  ```jsx
192
- import { lazy } from 'sygnal'
193
247
  const HeavyChart = lazy(() => import('./HeavyChart.jsx'))
248
+
249
+ <Suspense fallback={<div>Loading...</div>}>
250
+ <HeavyChart />
251
+ </Suspense>
194
252
  ```
195
253
 
196
- ### Suspense
254
+ ### Refs
197
255
 
198
- Show fallback UI while async children resolve:
256
+ Access DOM elements declaratively:
199
257
 
200
258
  ```jsx
201
- import { Suspense } from 'sygnal'
259
+ const inputRef = createRef()
260
+ <input ref={inputRef} />
261
+ // inputRef.current.focus()
262
+ ```
202
263
 
203
- <Suspense fallback={<div>Loading...</div>}>
204
- <SlowComponent />
205
- </Suspense>
264
+ ### Commands
265
+
266
+ Send imperative commands from parent to child:
267
+
268
+ ```jsx
269
+ import { createCommand } from 'sygnal'
270
+
271
+ const playerCmd = createCommand()
272
+ <VideoPlayer commands={playerCmd} />
273
+
274
+ // Parent sends commands with optional data
275
+ playerCmd.send('seek', { time: 30 })
276
+
277
+ // Child receives via commands$ source
278
+ VideoPlayer.intent = ({ commands$ }) => ({
279
+ SEEK: commands$.select('seek'), // emits { time: 30 }
280
+ })
206
281
  ```
207
282
 
208
- Components signal readiness via the built-in `READY` sink. Components without explicit `READY` model entries are ready immediately.
283
+ ### Effect Handlers
284
+
285
+ Run side effects without state changes — no more `ABORT` workarounds:
286
+
287
+ ```jsx
288
+ App.model = {
289
+ SEND_COMMAND: {
290
+ EFFECT: () => playerCmd.send('play'),
291
+ },
292
+ ROUTE: {
293
+ EFFECT: (state, data, next) => {
294
+ if (state.mode === 'a') next('DO_A', data)
295
+ else next('DO_B', data)
296
+ },
297
+ },
298
+ }
299
+ ```
300
+
301
+ ### Model Shorthand
302
+
303
+ Compact syntax for single-driver model entries:
304
+
305
+ ```jsx
306
+ App.model = {
307
+ 'SEND_CMD | EFFECT': () => playerCmd.send('play'),
308
+ 'NOTIFY | EVENTS': (state) => ({ type: 'alert', data: state.message }),
309
+ 'DELETE | PARENT': (state) => ({ type: 'DELETE', id: state.id }),
310
+ }
311
+ ```
209
312
 
210
313
  ### Disposal Hooks
211
314
 
212
- Run cleanup logic when components unmount — close connections, clear timers:
315
+ Cleanup on unmount:
213
316
 
214
317
  ```jsx
215
- MyComponent.intent = ({ DOM, dispose$ }) => ({
216
- CLEANUP: dispose$, // Emits once on unmount
318
+ MyComponent.intent = ({ dispose$ }) => ({
319
+ CLEANUP: dispose$,
217
320
  })
218
321
 
219
322
  MyComponent.model = {
220
- CLEANUP: {
221
- WEBSOCKET: () => ({ type: 'close' }),
222
- },
323
+ CLEANUP: { WEBSOCKET: () => ({ type: 'close' }) },
223
324
  }
224
325
  ```
225
326
 
@@ -238,6 +339,8 @@ if (import.meta.hot) {
238
339
 
239
340
  ### Astro Integration
240
341
 
342
+ First-class Astro support with server rendering and client hydration:
343
+
241
344
  ```javascript
242
345
  // astro.config.mjs
243
346
  import sygnal from 'sygnal/astro'
@@ -251,24 +354,24 @@ import Counter from '../components/Counter.jsx'
251
354
  <Counter client:load />
252
355
  ```
253
356
 
254
- ### TypeScript Support
357
+ ### TypeScript
255
358
 
256
359
  Full type definitions included:
257
360
 
258
361
  ```tsx
259
362
  import type { RootComponent } from 'sygnal'
260
363
 
261
- type AppState = { count: number }
262
- type AppActions = { INCREMENT: null }
364
+ type State = { count: number }
365
+ type Actions = { INCREMENT: null }
263
366
 
264
- const App: RootComponent<AppState, {}, AppActions> = ({ state }) => (
367
+ const App: RootComponent<State, {}, Actions> = ({ state }) => (
265
368
  <div>{state.count}</div>
266
369
  )
267
370
  ```
268
371
 
269
- ## Bundler Setup (JSX)
372
+ ## Bundler Setup
270
373
 
271
- For Vite:
374
+ **Vite** (recommended):
272
375
 
273
376
  ```javascript
274
377
  // vite.config.js
@@ -276,11 +379,11 @@ export default defineConfig({
276
379
  esbuild: {
277
380
  jsx: 'automatic',
278
381
  jsxImportSource: 'sygnal',
279
- }
382
+ },
280
383
  })
281
384
  ```
282
385
 
283
- For TypeScript projects, also add to `tsconfig.json`:
386
+ For TypeScript projects, add to `tsconfig.json`:
284
387
 
285
388
  ```json
286
389
  {
@@ -291,28 +394,34 @@ For TypeScript projects, also add to `tsconfig.json`:
291
394
  }
292
395
  ```
293
396
 
294
- Without JSX, use `h()`:
397
+ Without JSX, use `h()` directly:
295
398
 
296
399
  ```javascript
297
400
  import { h } from 'sygnal'
298
401
  h('div', [h('h1', 'Hello'), h('button.btn', 'Click')])
299
402
  ```
300
403
 
301
- See the [Guide](./docs/guide.md#bundler-configuration) for other bundlers.
404
+ ## Documentation
405
+
406
+ 📖 **[sygnal.js.org](https://sygnal.js.org)** — Full guide, API reference, and examples.
302
407
 
303
408
  ## Examples
304
409
 
305
- - **[Getting Started](./examples/getting-started)** Interactive guide with live demos (Astro)
306
- - **[Kanban Board](./examples/kanban)** — Drag-and-drop with Collections and cross-component communication
307
- - **[Advanced Feature Tests](./examples/advanced-feature-tests)** Portals, disposal, suspense, lazy loading
308
- - **[TypeScript 2048](./examples/ts-example-2048)** Full game in TypeScript
309
- - **[AI Discussion Panel](./examples/ai-panel-spa)** Complex SPA with custom drivers
310
- - **[Astro Integration](./examples/astro-smoke)** Sygnal in Astro
311
- - **[HMR Smoke Test](./examples/hmr-smoke)** Minimal counter with HMR
312
- - **[Sygnal ToDoMVC](https://github.com/tpresley/sygnal-todomvc)** ([Live Demo](https://tpresley.github.io/sygnal-todomvc/)) — TodoMVC implementation
313
- - **[Sygnal 2048](https://github.com/tpresley/sygnal-2048)** ([Live Demo](https://tpresley.github.io/sygnal-2048/)) — 2048 game
314
- - **[Sygnal Calculator](https://github.com/tpresley/sygnal-calculator)** ([Live Demo](https://tpresley.github.io/sygnal-calculator/)) — Simple calculator
410
+ | Example | Description |
411
+ |---------|-------------|
412
+ | [Getting Started](./examples/getting-started) | Interactive guide with live demos (Astro) |
413
+ | [Kanban Board](./examples/kanban) | Drag-and-drop with Collections and cross-component communication |
414
+ | [Advanced Features](./examples/advanced-feature-tests) | Portals, slots, disposal, suspense, lazy loading |
415
+ | [TypeScript 2048](./examples/ts-example-2048) | Full game in TypeScript |
416
+ | [AI Discussion Panel](./examples/ai-panel-spa) | Complex SPA with custom drivers |
417
+ | [Sygnal ToDoMVC](https://github.com/tpresley/sygnal-todomvc) | [Live Demo](https://tpresley.github.io/sygnal-todomvc/) |
418
+ | [Sygnal 2048](https://github.com/tpresley/sygnal-2048) | [Live Demo](https://tpresley.github.io/sygnal-2048/) |
419
+ | [Sygnal Calculator](https://github.com/tpresley/sygnal-calculator) | [Live Demo](https://tpresley.github.io/sygnal-calculator/) |
420
+
421
+ ## Acknowledgments
422
+
423
+ Sygnal's reactive architecture is built on patterns from [Cycle.js](https://cycle.js.org/) by [André Staltz](https://github.com/staltz). The Cycle.js runtime, DOM driver, state management, and isolation modules have been absorbed into the library — snabbdom, xstream, and extend are the only external dependencies.
315
424
 
316
425
  ## License
317
426
 
318
- MIT
427
+ [MIT](./LICENSE)