sygnal 4.6.0 → 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:
80
+
81
+ ```jsx
82
+ <Collection of={TodoItem} from="items" filter={item => !item.done} sort="name" />
83
+ ```
82
84
 
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.
85
+ ### Switchable
84
86
 
85
- ## Key Features
87
+ Swap between components based on state:
86
88
 
87
- ### Model-View-Intent Architecture
89
+ ```jsx
90
+ <Switchable of={{ home: HomePage, settings: SettingsPage }} current={state.activeTab} />
91
+ ```
88
92
 
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.
93
+ ### Context
90
94
 
91
- ### Monolithic State
95
+ Top-down data propagation without prop drilling:
92
96
 
93
- Automatic application-level state management with no setup. Trivial undo/redo, state restoration, and time-travel debugging.
97
+ ```jsx
98
+ App.context = {
99
+ theme: (state) => state.settings.theme,
100
+ currentUser: (state) => state.auth.user,
101
+ }
94
102
 
95
- ### Collections
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:
106
127
 
107
128
  ```jsx
108
- <switchable of={{ home: HomePage, settings: SettingsPage }} current={state.activeTab} />
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
+ })
109
138
  ```
110
139
 
111
- ### Form Handling
140
+ ### Calculated Fields
112
141
 
113
- Extract form values without the plumbing:
142
+ Derived state with optional dependency tracking:
114
143
 
115
144
  ```jsx
116
- import { processForm } from 'sygnal'
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
+ }
150
+ ```
151
+
152
+ ### Form Handling
117
153
 
154
+ Extract form values without the boilerplate:
155
+
156
+ ```jsx
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:
@@ -135,6 +184,146 @@ const apiDriver = driverFromAsync(async (url) => {
135
184
  run(RootComponent, { API: apiDriver })
136
185
  ```
137
186
 
187
+ ### Error Boundaries
188
+
189
+ Catch and recover from rendering errors:
190
+
191
+ ```jsx
192
+ BrokenComponent.onError = (error, { componentName }) => (
193
+ <div>Something went wrong in {componentName}</div>
194
+ )
195
+ ```
196
+
197
+ ### Portals
198
+
199
+ Render children into a different DOM container:
200
+
201
+ ```jsx
202
+ <Portal target="#modal-root">
203
+ <div className="modal">Modal content</div>
204
+ </Portal>
205
+ ```
206
+
207
+ ### Slots
208
+
209
+ Pass named content regions to child components:
210
+
211
+ ```jsx
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>
219
+
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
+ }
230
+ ```
231
+
232
+ ### Transitions
233
+
234
+ CSS-based enter/leave animations:
235
+
236
+ ```jsx
237
+ <Transition name="fade" duration={300}>
238
+ {state.visible && <div>Animated content</div>}
239
+ </Transition>
240
+ ```
241
+
242
+ ### Lazy Loading & Suspense
243
+
244
+ Code-split components with loading boundaries:
245
+
246
+ ```jsx
247
+ const HeavyChart = lazy(() => import('./HeavyChart.jsx'))
248
+
249
+ <Suspense fallback={<div>Loading...</div>}>
250
+ <HeavyChart />
251
+ </Suspense>
252
+ ```
253
+
254
+ ### Refs
255
+
256
+ Access DOM elements declaratively:
257
+
258
+ ```jsx
259
+ const inputRef = createRef()
260
+ <input ref={inputRef} />
261
+ // inputRef.current.focus()
262
+ ```
263
+
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
+ })
281
+ ```
282
+
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
+ ```
312
+
313
+ ### Disposal Hooks
314
+
315
+ Cleanup on unmount:
316
+
317
+ ```jsx
318
+ MyComponent.intent = ({ dispose$ }) => ({
319
+ CLEANUP: dispose$,
320
+ })
321
+
322
+ MyComponent.model = {
323
+ CLEANUP: { WEBSOCKET: () => ({ type: 'close' }) },
324
+ }
325
+ ```
326
+
138
327
  ### Hot Module Replacement
139
328
 
140
329
  State-preserving HMR out of the box:
@@ -150,6 +339,8 @@ if (import.meta.hot) {
150
339
 
151
340
  ### Astro Integration
152
341
 
342
+ First-class Astro support with server rendering and client hydration:
343
+
153
344
  ```javascript
154
345
  // astro.config.mjs
155
346
  import sygnal from 'sygnal/astro'
@@ -163,24 +354,24 @@ import Counter from '../components/Counter.jsx'
163
354
  <Counter client:load />
164
355
  ```
165
356
 
166
- ### TypeScript Support
357
+ ### TypeScript
167
358
 
168
359
  Full type definitions included:
169
360
 
170
361
  ```tsx
171
362
  import type { RootComponent } from 'sygnal'
172
363
 
173
- type AppState = { count: number }
174
- type AppActions = { INCREMENT: null }
364
+ type State = { count: number }
365
+ type Actions = { INCREMENT: null }
175
366
 
176
- const App: RootComponent<AppState, {}, AppActions> = ({ state }) => (
367
+ const App: RootComponent<State, {}, Actions> = ({ state }) => (
177
368
  <div>{state.count}</div>
178
369
  )
179
370
  ```
180
371
 
181
- ## Bundler Setup (JSX)
372
+ ## Bundler Setup
182
373
 
183
- For Vite:
374
+ **Vite** (recommended):
184
375
 
185
376
  ```javascript
186
377
  // vite.config.js
@@ -188,11 +379,11 @@ export default defineConfig({
188
379
  esbuild: {
189
380
  jsx: 'automatic',
190
381
  jsxImportSource: 'sygnal',
191
- }
382
+ },
192
383
  })
193
384
  ```
194
385
 
195
- For TypeScript projects, also add to `tsconfig.json`:
386
+ For TypeScript projects, add to `tsconfig.json`:
196
387
 
197
388
  ```json
198
389
  {
@@ -203,25 +394,34 @@ For TypeScript projects, also add to `tsconfig.json`:
203
394
  }
204
395
  ```
205
396
 
206
- Without JSX, use `h()`:
397
+ Without JSX, use `h()` directly:
207
398
 
208
399
  ```javascript
209
400
  import { h } from 'sygnal'
210
401
  h('div', [h('h1', 'Hello'), h('button.btn', 'Click')])
211
402
  ```
212
403
 
213
- 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.
214
407
 
215
408
  ## Examples
216
409
 
217
- - **[HMR Smoke Test](./examples/hmr-smoke)** — Minimal counter with HMR
218
- - **[TypeScript 2048](./examples/ts-example-2048)** — Full game in TypeScript
219
- - **[AI Discussion Panel](./examples/ai-panel-spa)** Complex SPA with custom drivers
220
- - **[Astro Integration](./examples/astro-smoke)** Sygnal in Astro
221
- - **[Sygnal ToDoMVC](https://github.com/tpresley/sygnal-todomvc)** ([Live Demo](https://tpresley.github.io/sygnal-todomvc/)) TodoMVC implementation
222
- - **[Sygnal 2048](https://github.com/tpresley/sygnal-2048)** ([Live Demo](https://tpresley.github.io/sygnal-2048/)) 2048 game
223
- - **[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.
224
424
 
225
425
  ## License
226
426
 
227
- MIT
427
+ [MIT](./LICENSE)