preact-sigma 2.2.2 → 2.3.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 +11 -291
- package/dist/index.d.mts +122 -34
- package/dist/index.mjs +68 -18
- package/docs/context.md +104 -0
- package/examples/async-commit.ts +42 -0
- package/examples/basic-counter.ts +23 -0
- package/examples/command-palette.tsx +211 -0
- package/examples/observe-and-restore.ts +27 -0
- package/examples/setup-act.ts +34 -0
- package/examples/sigma-target.ts +26 -0
- package/examples/signal-access.ts +28 -0
- package/package.json +3 -2
- package/llms.txt +0 -437
package/dist/index.mjs
CHANGED
|
@@ -198,7 +198,7 @@ function getSigmaInternals(context) {
|
|
|
198
198
|
function isAutoFreeze() {
|
|
199
199
|
return autoFreezeEnabled;
|
|
200
200
|
}
|
|
201
|
-
/** Controls whether sigma deep-freezes published public state. Auto-freezing starts enabled. */
|
|
201
|
+
/** Controls whether sigma deep-freezes published public state. Auto-freezing starts enabled and the setting is shared across instances. */
|
|
202
202
|
function setAutoFreeze(autoFreeze) {
|
|
203
203
|
autoFreezeEnabled = autoFreeze;
|
|
204
204
|
immer.setAutoFreeze(autoFreeze);
|
|
@@ -446,8 +446,9 @@ function publishState(instance, finalized) {
|
|
|
446
446
|
* Returns a shallow snapshot of an instance's committed public state.
|
|
447
447
|
*
|
|
448
448
|
* The snapshot includes one own property for each top-level state key and reads
|
|
449
|
-
* the current committed value for that key.
|
|
450
|
-
* instance's sigma-state
|
|
449
|
+
* the current committed value for that key. Nested sigma states remain as
|
|
450
|
+
* referenced values. Its type is inferred from the instance's sigma-state
|
|
451
|
+
* definition.
|
|
451
452
|
*/
|
|
452
453
|
function snapshot(publicInstance) {
|
|
453
454
|
return snapshotState(getSigmaInternals(publicInstance));
|
|
@@ -456,8 +457,9 @@ function snapshot(publicInstance) {
|
|
|
456
457
|
* Replaces an instance's committed public state from a snapshot object.
|
|
457
458
|
*
|
|
458
459
|
* The replacement snapshot must be a plain object with exactly the instance's
|
|
459
|
-
* top-level state keys.
|
|
460
|
-
*
|
|
460
|
+
* top-level state keys. It updates committed state outside action semantics and
|
|
461
|
+
* notifies observers when the committed state changes. Its type is inferred
|
|
462
|
+
* from the instance's sigma-state definition.
|
|
461
463
|
*/
|
|
462
464
|
function replaceState(publicInstance, nextState) {
|
|
463
465
|
const instance = getSigmaInternals(publicInstance);
|
|
@@ -537,17 +539,26 @@ var Sigma = class extends EventTarget {
|
|
|
537
539
|
Object.defineProperty(Sigma.prototype, sigmaStateBrand, { value: true });
|
|
538
540
|
//#endregion
|
|
539
541
|
//#region src/framework.ts
|
|
540
|
-
/** Checks whether a value is a sigma
|
|
542
|
+
/** Checks whether a value is an instance created by a configured sigma type. */
|
|
541
543
|
function isSigmaState(value) {
|
|
542
|
-
return Boolean(value
|
|
544
|
+
return Boolean(value[sigmaStateBrand]);
|
|
543
545
|
}
|
|
544
|
-
/**
|
|
546
|
+
/**
|
|
547
|
+
* Creates a standalone tracked query helper with the same signature as `fn`.
|
|
548
|
+
*
|
|
549
|
+
* Each call is reactive at the call site and does not memoize results across
|
|
550
|
+
* invocations, which makes `query(fn)` a good fit for local tracked helpers
|
|
551
|
+
* that do not need to live on the sigma-state instance.
|
|
552
|
+
*/
|
|
545
553
|
function query(fn) {
|
|
546
554
|
return ((...args) => computed$1(() => fn(...args)).value);
|
|
547
555
|
}
|
|
548
556
|
/**
|
|
549
|
-
* Builds sigma-state constructors by accumulating default state, computeds,
|
|
550
|
-
* observers, actions, and setup handlers.
|
|
557
|
+
* Builds sigma-state constructors by accumulating default state, computeds,
|
|
558
|
+
* queries, observers, actions, and setup handlers.
|
|
559
|
+
*
|
|
560
|
+
* State and event inference starts from `new SigmaType<TState, TEvents>()`.
|
|
561
|
+
* Later builder methods infer names and types from the objects you pass to them.
|
|
551
562
|
*/
|
|
552
563
|
var SigmaType = class extends Function {
|
|
553
564
|
constructor(name = "Sigma") {
|
|
@@ -618,17 +629,51 @@ var SigmaType = class extends Function {
|
|
|
618
629
|
};
|
|
619
630
|
//#endregion
|
|
620
631
|
//#region src/listener.ts
|
|
621
|
-
/**
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
632
|
+
/**
|
|
633
|
+
* A standalone typed event hub with `emit(...)` and `on(...)` methods and full
|
|
634
|
+
* `EventTarget`, `listen(...)`, and `useListener(...)` compatibility.
|
|
635
|
+
*/
|
|
636
|
+
var SigmaTarget = class extends EventTarget {
|
|
637
|
+
/**
|
|
638
|
+
* Emits a typed event from the hub.
|
|
639
|
+
*
|
|
640
|
+
* Void events dispatch a plain `Event`. Payload events dispatch a
|
|
641
|
+
* `CustomEvent` whose `detail` holds the payload.
|
|
642
|
+
*/
|
|
643
|
+
emit(name, ...args) {
|
|
644
|
+
this.dispatchEvent(args.length === 0 ? new Event(name) : new CustomEvent(name, { detail: args[0] }));
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Registers a typed event listener and returns an unsubscribe function.
|
|
648
|
+
*
|
|
649
|
+
* Payload events pass their payload directly to the listener. Void events
|
|
650
|
+
* call the listener with no arguments.
|
|
651
|
+
*/
|
|
652
|
+
on(name, listener) {
|
|
653
|
+
const adapter = (event) => listener(event.detail);
|
|
654
|
+
this.addEventListener(name, adapter);
|
|
655
|
+
return () => {
|
|
656
|
+
this.removeEventListener(name, adapter);
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
/** Adds a listener to a sigma state or DOM target and returns a cleanup function that removes it. */
|
|
661
|
+
function listen(target, name, listener) {
|
|
662
|
+
const adapter = isSigmaState(target) || target instanceof SigmaTarget ? (event) => listener(event.detail) : listener;
|
|
663
|
+
target.addEventListener(name, adapter);
|
|
625
664
|
return () => {
|
|
626
|
-
target.removeEventListener(name,
|
|
665
|
+
target.removeEventListener(name, adapter);
|
|
627
666
|
};
|
|
628
667
|
}
|
|
629
668
|
//#endregion
|
|
630
669
|
//#region src/hooks.ts
|
|
631
|
-
/**
|
|
670
|
+
/**
|
|
671
|
+
* Creates one sigma-state instance for a component.
|
|
672
|
+
*
|
|
673
|
+
* `create()` runs once per mounted component instance. When the sigma state
|
|
674
|
+
* defines setup, `useSigma(...)` also runs `setup(...setupArgs)` in an effect
|
|
675
|
+
* and cleans it up when the setup arguments change or the component unmounts.
|
|
676
|
+
*/
|
|
632
677
|
function useSigma(create, setupArgs) {
|
|
633
678
|
const sigmaState = useState(create)[0];
|
|
634
679
|
if (shouldSetup(sigmaState)) {
|
|
@@ -637,7 +682,12 @@ function useSigma(create, setupArgs) {
|
|
|
637
682
|
}
|
|
638
683
|
return sigmaState;
|
|
639
684
|
}
|
|
640
|
-
/**
|
|
685
|
+
/**
|
|
686
|
+
* Attaches an event listener in a component and cleans it up automatically.
|
|
687
|
+
*
|
|
688
|
+
* Passing `null` disables the listener. The latest callback is used without
|
|
689
|
+
* forcing the effect to resubscribe on every render.
|
|
690
|
+
*/
|
|
641
691
|
function useListener(target, name, listener) {
|
|
642
692
|
const listenerRef = useRef(listener);
|
|
643
693
|
listenerRef.current = listener;
|
|
@@ -647,4 +697,4 @@ function useListener(target, name, listener) {
|
|
|
647
697
|
}, [target, name]);
|
|
648
698
|
}
|
|
649
699
|
//#endregion
|
|
650
|
-
export { SigmaType, action, batch, computed, effect, freeze, immerable, isSigmaState, listen, query, replaceState, setAutoFreeze, snapshot, untracked, useListener, useSigma };
|
|
700
|
+
export { SigmaTarget, SigmaType, action, batch, computed, effect, freeze, immerable, isSigmaState, listen, query, replaceState, setAutoFreeze, snapshot, untracked, useListener, useSigma };
|
package/docs/context.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Overview
|
|
2
|
+
|
|
3
|
+
`preact-sigma` builds reusable state models from one definition. A configured `SigmaType` owns top-level state, derived reads, writes, setup handlers, and typed events. Each top-level state property is exposed as a reactive public property backed by its own Preact signal, while actions use Immer-style mutation semantics to publish committed state. For event-only flows, `SigmaTarget` provides a standalone typed event hub with no managed state.
|
|
4
|
+
|
|
5
|
+
# When to Use
|
|
6
|
+
|
|
7
|
+
- State, derived reads, mutations, and lifecycle need to stay together.
|
|
8
|
+
- You need multiple instances of the same model.
|
|
9
|
+
- Public reads should stay reactive and readonly while writes stay explicit.
|
|
10
|
+
- A model needs to own timers, subscriptions, listeners, nested setup, or other cleanup-aware resources.
|
|
11
|
+
- Components should consume the same model shape used outside Preact.
|
|
12
|
+
|
|
13
|
+
# When Not to Use
|
|
14
|
+
|
|
15
|
+
- A few plain signals already cover the state without extra coordination.
|
|
16
|
+
- You want side effects to start implicitly during construction.
|
|
17
|
+
- The main problem is remote caching, normalization, or cross-app store tooling rather than local state behavior.
|
|
18
|
+
- You need ad hoc mutable objects with no benefit from typed actions, setup, or signal-backed reads.
|
|
19
|
+
|
|
20
|
+
# Core Abstractions
|
|
21
|
+
|
|
22
|
+
- Sigma type: the builder returned by `new SigmaType<TState, TEvents>()`. After configuration, it is also the constructor for instances.
|
|
23
|
+
- Sigma state: an instance created from a configured sigma type.
|
|
24
|
+
- Sigma target: a standalone typed event hub created with `new SigmaTarget<TEvents>()` when you need typed events without managed state.
|
|
25
|
+
- State property: a top-level key from `TState`. Each one becomes a readonly reactive public property and gets its own signal.
|
|
26
|
+
- Computed: an argument-free derived getter declared with `.computed(...)`.
|
|
27
|
+
- Query: a reactive read that accepts arguments, declared with `.queries(...)` or built locally with `query(fn)`.
|
|
28
|
+
- Action: a method declared with `.actions(...)` that reads and writes through sigma's draft and commit semantics.
|
|
29
|
+
- Setup handler: a function declared with `.setup(...)` that owns side effects and cleanup resources explicitly.
|
|
30
|
+
- Event: a typed notification emitted through `this.emit(...)` and observed through `.on(...)`, `listen(...)`, or `useListener(...)`.
|
|
31
|
+
|
|
32
|
+
# Data Flow / Lifecycle
|
|
33
|
+
|
|
34
|
+
1. Define a sigma type with `new SigmaType<TState, TEvents>()`. Let later builder methods infer names and types from the objects you pass to them.
|
|
35
|
+
2. Add `defaultState(...)` for top-level public state and optional per-instance initializers.
|
|
36
|
+
3. Add `computed(...)`, `queries(...)`, and `actions(...)` for derived reads and writes.
|
|
37
|
+
4. Instantiate the configured type. Constructor input shallowly overrides `defaultState(...)`.
|
|
38
|
+
5. Read state, computeds, and queries reactively from the public instance.
|
|
39
|
+
6. Mutate state inside actions. Sync nested actions on the same instance share one draft. Boundaries like `await`, `emit(...)`, or separate action invocations may require `this.commit()` before the boundary.
|
|
40
|
+
7. Run `setup(...)` explicitly when the instance should start owning side effects. `useSigma(...)` does this automatically for component-owned instances that define setup.
|
|
41
|
+
8. Dispose the cleanup returned from `setup(...)` when the owned resources should stop.
|
|
42
|
+
|
|
43
|
+
# Common Tasks -> Recommended APIs
|
|
44
|
+
|
|
45
|
+
- Define reusable model state: `new SigmaType<TState, TEvents>().defaultState(...)`
|
|
46
|
+
- Derive an argument-free value: `.computed(...)`
|
|
47
|
+
- Derive a reactive read with arguments: `.queries(...)`
|
|
48
|
+
- Keep a tracked helper local to one consumer module: `query(fn)`
|
|
49
|
+
- Mutate state and emit typed notifications: `.actions(...)`
|
|
50
|
+
- Publish before `await`, `emit(...)`, or another action boundary: `this.commit()`
|
|
51
|
+
- React to committed state changes: `.observe(...)`
|
|
52
|
+
- Own timers, listeners, subscriptions, or nested setup: `.setup(...)`
|
|
53
|
+
- Use a sigma state inside a component: `useSigma(...)`
|
|
54
|
+
- Subscribe to sigma or DOM events in a component: `useListener(...)`
|
|
55
|
+
- Create a standalone typed event hub with no managed state: `new SigmaTarget<TEvents>()`, `hub.emit(...)`, and `hub.on(...)`
|
|
56
|
+
- Subscribe outside components: `.on(...)` or `listen(...)`
|
|
57
|
+
- Read or restore committed top-level state: `snapshot(...)` and `replaceState(...)`
|
|
58
|
+
|
|
59
|
+
# Practical Guidelines
|
|
60
|
+
|
|
61
|
+
- Put explicit type arguments on `new SigmaType<TState, TEvents>()` and let later builder methods infer from the objects you pass.
|
|
62
|
+
- Keep frequently read values as separate top-level state properties. Each top-level key gets its own signal.
|
|
63
|
+
- Use `.computed(...)` for argument-free derived reads.
|
|
64
|
+
- Use `.queries(...)` for tracked reads with arguments.
|
|
65
|
+
- Keep one-off calculations local until they become reusable model behavior.
|
|
66
|
+
- Reach for `instance.get(key)` only when code specifically needs the underlying `ReadonlySignal`.
|
|
67
|
+
- Treat `emit(...)`, `await`, and any action call other than a same-instance synchronous nested action call as draft boundaries. Call `this.commit()` only when pending changes need to become public before one of those boundaries.
|
|
68
|
+
- Use ordinary actions for routine writes. Reserve `snapshot(...)` and `replaceState(...)` for replay, reset, or undo-like flows on committed top-level state.
|
|
69
|
+
- Put owned side effects in `.setup(...)`.
|
|
70
|
+
- Use `this.act(function () { ... })` for setup-owned callbacks that need action semantics.
|
|
71
|
+
- Call Immer's `enablePatches()` before relying on `.observe(..., { patches: true })`.
|
|
72
|
+
|
|
73
|
+
# Invariants and Constraints
|
|
74
|
+
|
|
75
|
+
- Sigma only tracks top-level state properties. Each top-level key gets its own signal.
|
|
76
|
+
- Public state is readonly outside actions and `this.act(...)` inside setup.
|
|
77
|
+
- Duplicate names across state properties, computeds, queries, and actions are rejected at runtime. Reserved public names include `act`, `emit`, `get`, `on`, and `setup`.
|
|
78
|
+
- Query calls are reactive at the call site but do not memoize across invocations.
|
|
79
|
+
- Setup handlers return arrays of cleanup resources, and cleanup runs in reverse order.
|
|
80
|
+
- `replaceState(...)` works on committed top-level state and requires the exact state-key shape.
|
|
81
|
+
- Published draftable public state is deep-frozen by default. `setAutoFreeze(false)` disables that behavior globally.
|
|
82
|
+
|
|
83
|
+
# Error Model
|
|
84
|
+
|
|
85
|
+
- Crossing an action boundary with unpublished changes throws until `this.commit()` publishes them. Async actions also reject when they finish with unpublished changes.
|
|
86
|
+
- If another invocation crosses a boundary while unpublished changes still exist, sigma warns and discards those changes before continuing.
|
|
87
|
+
- Calling `setup(...)` on a sigma state without registered setup handlers throws.
|
|
88
|
+
- Cleanup rethrows an `AggregateError` when more than one cleanup resource fails.
|
|
89
|
+
- `replaceState(...)` throws when the replacement value is not a plain object, has the wrong top-level keys, or runs while an action still owns unpublished changes.
|
|
90
|
+
|
|
91
|
+
# Terminology
|
|
92
|
+
|
|
93
|
+
- Draft boundary: a point where sigma cannot keep reusing the current unpublished draft.
|
|
94
|
+
- Committed state: the published top-level public state visible outside the current action draft.
|
|
95
|
+
- Signal access: reading the underlying `ReadonlySignal` for a top-level state key or computed through `instance.get(key)`.
|
|
96
|
+
- Cleanup resource: a cleanup function, `AbortController`, object with `dispose()`, or object with `[Symbol.dispose]()`.
|
|
97
|
+
- Nested sigma state: a sigma-state instance stored in top-level state as a value; it stays usable as a value rather than exposing its internals through parent actions.
|
|
98
|
+
|
|
99
|
+
# Non-Goals
|
|
100
|
+
|
|
101
|
+
- Replacing every plain-signal use case with a builder abstraction.
|
|
102
|
+
- Hiding lifecycle behind implicit setup or constructor side effects.
|
|
103
|
+
- Memoizing every query call or turning queries into a global cache.
|
|
104
|
+
- Acting as a large tutorial framework or hand-maintained API reference. Exact signatures come from declaration output, and factual behavior lives beside source.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SigmaType } from "preact-sigma";
|
|
2
|
+
|
|
3
|
+
const SaveIndicator = new SigmaType<
|
|
4
|
+
{
|
|
5
|
+
savedCount: number;
|
|
6
|
+
saving: boolean;
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
saved: {
|
|
10
|
+
count: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
>("SaveIndicator")
|
|
14
|
+
.defaultState({
|
|
15
|
+
savedCount: 0,
|
|
16
|
+
saving: false,
|
|
17
|
+
})
|
|
18
|
+
.actions({
|
|
19
|
+
async save() {
|
|
20
|
+
this.saving = true;
|
|
21
|
+
this.commit(); // Publish before the async boundary.
|
|
22
|
+
|
|
23
|
+
await Promise.resolve();
|
|
24
|
+
|
|
25
|
+
this.savedCount += 1;
|
|
26
|
+
this.saving = false;
|
|
27
|
+
this.commit(); // Publish before emitting the event boundary.
|
|
28
|
+
|
|
29
|
+
this.emit("saved", { count: this.savedCount });
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const indicator = new SaveIndicator();
|
|
34
|
+
|
|
35
|
+
indicator.on("saved", ({ count }) => {
|
|
36
|
+
console.log(`Saved ${count} times`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await indicator.save();
|
|
40
|
+
|
|
41
|
+
console.log(indicator.saving); // false
|
|
42
|
+
console.log(indicator.savedCount); // 1
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SigmaType } from "preact-sigma";
|
|
2
|
+
|
|
3
|
+
const Counter = new SigmaType<{ count: number }>("Counter")
|
|
4
|
+
.defaultState({
|
|
5
|
+
count: 0,
|
|
6
|
+
})
|
|
7
|
+
.computed({
|
|
8
|
+
doubled() {
|
|
9
|
+
return this.count * 2;
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
.actions({
|
|
13
|
+
increment() {
|
|
14
|
+
this.count += 1;
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const counter = new Counter();
|
|
19
|
+
|
|
20
|
+
counter.increment();
|
|
21
|
+
|
|
22
|
+
console.log(counter.count); // 1
|
|
23
|
+
console.log(counter.doubled); // 2
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { useState } from "preact/hooks";
|
|
2
|
+
|
|
3
|
+
import { listen, query, SigmaType, useListener, useSigma } from "preact-sigma";
|
|
4
|
+
|
|
5
|
+
type Command = {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
keywords: readonly string[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class UsageLedger {
|
|
12
|
+
counts = new Map<string, number>();
|
|
13
|
+
|
|
14
|
+
get(id: string) {
|
|
15
|
+
return this.counts.get(id) ?? 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
increment(id: string) {
|
|
19
|
+
this.counts.set(id, this.get(id) + 1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const matchesText = query((command: Command, draft: string) => {
|
|
24
|
+
const needle = draft.trim().toLowerCase();
|
|
25
|
+
if (!needle) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
command.title.toLowerCase().includes(needle) ||
|
|
31
|
+
command.keywords.some((keyword) => keyword.toLowerCase().includes(needle))
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const SearchHistory = new SigmaType<{
|
|
36
|
+
items: string[];
|
|
37
|
+
}>("SearchHistory")
|
|
38
|
+
.defaultState({
|
|
39
|
+
items: [],
|
|
40
|
+
})
|
|
41
|
+
.actions({
|
|
42
|
+
remember(query: string) {
|
|
43
|
+
const value = query.trim();
|
|
44
|
+
if (!value) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.items = [value, ...this.items.filter((item) => item !== value)].slice(0, 5);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
interface SearchHistory extends InstanceType<typeof SearchHistory> {}
|
|
53
|
+
|
|
54
|
+
const CommandPalette = new SigmaType<
|
|
55
|
+
{
|
|
56
|
+
commands: Command[];
|
|
57
|
+
cursor: number;
|
|
58
|
+
draft: string;
|
|
59
|
+
history: SearchHistory;
|
|
60
|
+
usage: UsageLedger;
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
ran: Command;
|
|
64
|
+
}
|
|
65
|
+
>("CommandPalette")
|
|
66
|
+
.defaultState({
|
|
67
|
+
commands: [
|
|
68
|
+
{ id: "inbox", title: "Open inbox", keywords: ["mail", "messages", "triage"] },
|
|
69
|
+
{ id: "capture", title: "Capture note", keywords: ["write", "quick", "idea"] },
|
|
70
|
+
{ id: "focus", title: "Start focus timer", keywords: ["pomodoro", "deep work"] },
|
|
71
|
+
{ id: "theme", title: "Toggle theme", keywords: ["appearance", "dark", "light"] },
|
|
72
|
+
],
|
|
73
|
+
cursor: 0,
|
|
74
|
+
draft: "",
|
|
75
|
+
history: () => new SearchHistory(),
|
|
76
|
+
usage: () => new UsageLedger(),
|
|
77
|
+
})
|
|
78
|
+
.computed({
|
|
79
|
+
visibleCommands() {
|
|
80
|
+
return this.commands.filter((command) => matchesText(command, this.draft));
|
|
81
|
+
},
|
|
82
|
+
activeCommand() {
|
|
83
|
+
return this.visibleCommands[this.cursor] ?? null;
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
.queries({
|
|
87
|
+
canRun() {
|
|
88
|
+
return this.activeCommand !== null;
|
|
89
|
+
},
|
|
90
|
+
usageCount(id: string) {
|
|
91
|
+
return this.usage.get(id);
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
.actions({
|
|
95
|
+
setDraft(draft: string) {
|
|
96
|
+
this.draft = draft;
|
|
97
|
+
this.cursor = 0;
|
|
98
|
+
},
|
|
99
|
+
move(step: number) {
|
|
100
|
+
if (this.visibleCommands.length === 0) {
|
|
101
|
+
this.cursor = 0;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const lastIndex = this.visibleCommands.length - 1;
|
|
106
|
+
this.cursor = Math.max(0, Math.min(lastIndex, this.cursor + step));
|
|
107
|
+
},
|
|
108
|
+
seedDraftFromHistory() {
|
|
109
|
+
const latest = this.history.items[0];
|
|
110
|
+
if (latest) {
|
|
111
|
+
this.setDraft(latest);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
runActive() {
|
|
115
|
+
const command = this.activeCommand;
|
|
116
|
+
if (!command || !this.canRun()) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.history.remember(this.draft || command.title);
|
|
121
|
+
this.usage.increment(command.id);
|
|
122
|
+
this.emit("ran", command);
|
|
123
|
+
this.draft = "";
|
|
124
|
+
this.cursor = 0;
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
.setup(function () {
|
|
128
|
+
return [
|
|
129
|
+
listen(window, "keydown", (event) => {
|
|
130
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "k") {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
this.seedDraftFromHistory();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (event.key === "ArrowDown") {
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
this.move(1);
|
|
139
|
+
} else if (event.key === "ArrowUp") {
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
this.move(-1);
|
|
142
|
+
} else if (event.key === "Enter") {
|
|
143
|
+
this.runActive();
|
|
144
|
+
}
|
|
145
|
+
}),
|
|
146
|
+
];
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
interface CommandPalette extends InstanceType<typeof CommandPalette> {}
|
|
150
|
+
|
|
151
|
+
export function CommandPaletteExample() {
|
|
152
|
+
const palette = useSigma(() => new CommandPalette());
|
|
153
|
+
const [lastRun, setLastRun] = useState<string>("Nothing yet");
|
|
154
|
+
|
|
155
|
+
useListener(palette, "ran", (command) => {
|
|
156
|
+
setLastRun(`${command.title} (${palette.usageCount(command.id)} runs)`);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<section>
|
|
161
|
+
<p>
|
|
162
|
+
<strong>Command palette</strong>: setup-owned keyboard shortcuts, computed getters, tracked
|
|
163
|
+
queries with args, typed events, nested sigma state, and a mutable custom class instance.
|
|
164
|
+
</p>
|
|
165
|
+
|
|
166
|
+
<label>
|
|
167
|
+
Search
|
|
168
|
+
<input
|
|
169
|
+
value={palette.draft}
|
|
170
|
+
onInput={(event) => palette.setDraft((event.currentTarget as HTMLInputElement).value)}
|
|
171
|
+
placeholder="Try: note, timer, inbox"
|
|
172
|
+
/>
|
|
173
|
+
</label>
|
|
174
|
+
|
|
175
|
+
<div>
|
|
176
|
+
<button type="button" onClick={() => palette.move(-1)}>
|
|
177
|
+
Up
|
|
178
|
+
</button>
|
|
179
|
+
<button type="button" onClick={() => palette.move(1)}>
|
|
180
|
+
Down
|
|
181
|
+
</button>
|
|
182
|
+
<button type="button" onClick={() => palette.runActive()} disabled={!palette.canRun()}>
|
|
183
|
+
Run
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<p>Last run: {lastRun}</p>
|
|
188
|
+
|
|
189
|
+
<ul>
|
|
190
|
+
{palette.visibleCommands.map((command, index) => (
|
|
191
|
+
<li key={command.id}>
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={() => {
|
|
195
|
+
palette.setDraft(command.title);
|
|
196
|
+
palette.runActive();
|
|
197
|
+
}}
|
|
198
|
+
style={{
|
|
199
|
+
fontWeight: index === palette.cursor ? "700" : "400",
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{command.title} · used {palette.usageCount(command.id)} times
|
|
203
|
+
</button>
|
|
204
|
+
</li>
|
|
205
|
+
))}
|
|
206
|
+
</ul>
|
|
207
|
+
|
|
208
|
+
<p>History: {palette.history.items.join(" / ") || "empty"}</p>
|
|
209
|
+
</section>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { replaceState, SigmaType, snapshot } from "preact-sigma";
|
|
2
|
+
|
|
3
|
+
const TodoList = new SigmaType<{
|
|
4
|
+
todos: string[];
|
|
5
|
+
}>("TodoList")
|
|
6
|
+
.defaultState({
|
|
7
|
+
todos: [],
|
|
8
|
+
})
|
|
9
|
+
.observe(function (change) {
|
|
10
|
+
console.log(`${change.oldState.todos.length} -> ${change.newState.todos.length}`);
|
|
11
|
+
})
|
|
12
|
+
.actions({
|
|
13
|
+
add(title: string) {
|
|
14
|
+
this.todos.push(title);
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const todoList = new TodoList();
|
|
19
|
+
|
|
20
|
+
todoList.add("Write docs");
|
|
21
|
+
|
|
22
|
+
const saved = snapshot(todoList);
|
|
23
|
+
|
|
24
|
+
todoList.add("Ship release");
|
|
25
|
+
replaceState(todoList, saved);
|
|
26
|
+
|
|
27
|
+
console.log(snapshot(todoList).todos); // ["Write docs"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { listen, SigmaType } from "preact-sigma";
|
|
2
|
+
|
|
3
|
+
const ClickTracker = new SigmaType<{
|
|
4
|
+
clicks: number;
|
|
5
|
+
status: "idle" | "ready";
|
|
6
|
+
}>("ClickTracker")
|
|
7
|
+
.defaultState({
|
|
8
|
+
clicks: 0,
|
|
9
|
+
status: "idle",
|
|
10
|
+
})
|
|
11
|
+
.setup(function (target: EventTarget) {
|
|
12
|
+
this.act(function () {
|
|
13
|
+
this.status = "ready";
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return [
|
|
17
|
+
listen(target, "click", () => {
|
|
18
|
+
this.act(function () {
|
|
19
|
+
this.clicks += 1;
|
|
20
|
+
});
|
|
21
|
+
}),
|
|
22
|
+
];
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const target = new EventTarget();
|
|
26
|
+
const tracker = new ClickTracker();
|
|
27
|
+
const cleanup = tracker.setup(target);
|
|
28
|
+
|
|
29
|
+
target.dispatchEvent(new Event("click"));
|
|
30
|
+
target.dispatchEvent(new Event("click"));
|
|
31
|
+
|
|
32
|
+
console.log(tracker.status, tracker.clicks); // ready 2
|
|
33
|
+
|
|
34
|
+
cleanup();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { listen, SigmaTarget } from "preact-sigma";
|
|
2
|
+
|
|
3
|
+
const notifications = new SigmaTarget<{
|
|
4
|
+
saved: {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
};
|
|
8
|
+
reset: void;
|
|
9
|
+
}>();
|
|
10
|
+
|
|
11
|
+
const stopSaved = notifications.on("saved", ({ id, title }) => {
|
|
12
|
+
console.log(`Saved ${id}: ${title}`);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const stopReset = listen(notifications, "reset", () => {
|
|
16
|
+
console.log("Reset");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
notifications.emit("saved", {
|
|
20
|
+
id: "note-1",
|
|
21
|
+
title: "Draft post",
|
|
22
|
+
});
|
|
23
|
+
notifications.emit("reset");
|
|
24
|
+
|
|
25
|
+
stopSaved();
|
|
26
|
+
stopReset();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { effect, SigmaType } from "preact-sigma";
|
|
2
|
+
|
|
3
|
+
const Counter = new SigmaType<{
|
|
4
|
+
count: number;
|
|
5
|
+
}>("Counter")
|
|
6
|
+
.defaultState({
|
|
7
|
+
count: 0,
|
|
8
|
+
})
|
|
9
|
+
.computed({
|
|
10
|
+
doubled() {
|
|
11
|
+
return this.count * 2;
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
.actions({
|
|
15
|
+
increment() {
|
|
16
|
+
this.count += 1;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const counter = new Counter();
|
|
21
|
+
|
|
22
|
+
const stop = effect(() => {
|
|
23
|
+
console.log(counter.get("count").value, counter.get("doubled").value);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
counter.increment();
|
|
27
|
+
|
|
28
|
+
stop();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "preact-sigma",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alec Larson",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"dist",
|
|
13
|
-
"
|
|
13
|
+
"docs",
|
|
14
|
+
"examples"
|
|
14
15
|
],
|
|
15
16
|
"type": "module",
|
|
16
17
|
"exports": {
|