@mmstack/primitives 21.2.1 → 21.2.2

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
@@ -19,7 +19,7 @@ npm install @mmstack/primitives
19
19
  - [Timing & propagation](#timing--propagation) — `debounced`, `throttled`, `until`
20
20
  - [Reactive collections](#reactive-collections) — `indexArray`, `keyArray`, `mapObject`
21
21
  - [Effects](#effects) — `nestedEffect`
22
- - [Concurrency & transitions](#concurrency--transitions) — `keepPrevious`, keep-alive (`MmActivity`), `pausable*`, Suspense (`mm-suspense`), `startTransition` / `startTransaction`, `holdUntilReady`
22
+ - [Concurrency & transitions](#concurrency--transitions) — `keepPrevious`, keep-alive (`MmActivity`), `pausable*` / `providePausableOptions`, Suspense (`mm-suspense`), `startTransition` / `startTransaction`, `holdUntilReady`
23
23
  - [History & persistence](#history--persistence) — `withHistory`, `stored`, `tabSync`
24
24
  - [Performance helpers](#performance-helpers) — `chunked`, `pooled` / `pooledArray` / `pooledMap` / `pooledSet`
25
25
  - [Sensors](#sensors) — `sensor()` facade + browser-state signals
@@ -117,14 +117,16 @@ Each level's shape is resolved from what's known: a value that is currently an o
117
117
 
118
118
  Top-level array support isn't exposed yet — use `indexArray` / `keyArray` for those.
119
119
 
120
- ### `extend` (scoped overlay)
120
+ ### `extendStore` (scoped overlay)
121
121
 
122
- `store.extend(seed)` (on any store kind) creates a **scoped overlay** — a child store that **shares** the parent's signals for inherited keys (the same `WritableSignal`: writes go through to the parent and parent changes flow down) while keeping the seed and any new keys in a **local layer** that never propagates upward. No diffing, no syncing — local keys simply aren't wired to the parent.
122
+ `extendStore(store, seed)` (on any store kind) creates a **scoped overlay** — a child store that **shares** the parent's signals for inherited keys (the same `WritableSignal`: writes go through to the parent and parent changes flow down) while keeping the seed and any new keys in a **local layer** that never propagates upward. No diffing, no syncing — local keys simply aren't wired to the parent.
123
123
 
124
124
  ```typescript
125
+ import { extendStore, store } from '@mmstack/primitives';
126
+
125
127
  const app = store({ user: { name: 'Alice' }, theme: 'dark' });
126
128
 
127
- const scope = app.extend({ draft: '' }); // inherits user + theme, adds a local draft
129
+ const scope = extendStore(app, { draft: '' }); // inherits user + theme, adds a local draft
128
130
 
129
131
  scope.user === app.user; // true — the same signal (shared, two-way)
130
132
  scope.user.name.set('Bob'); // writes through to the parent
@@ -132,19 +134,19 @@ scope.draft.set('hello'); // local only — `app` never gains `draft`
132
134
  scope(); // { user: { name: 'Bob' }, theme: 'dark', draft: 'hello' }
133
135
  ```
134
136
 
135
- Resolution per key is **local → parent → local**: a seed key (or one set on the scope before it exists on the parent) is local and _shadows_ the parent — and keeps shadowing even if the parent later grows that key; a key that exists only on the parent writes through to it; a brand-new key lands locally. `scope()` is the merged view (local shadowing), and `Object.keys(scope)` / `key in scope` are the union of both layers. `extend` composes — `a.extend(x).extend(y)` chains parents.
137
+ Resolution per key is **local → parent → local**: a seed key (or one set on the scope before it exists on the parent) is local and _shadows_ the parent — and keeps shadowing even if the parent later grows that key; a key that exists only on the parent writes through to it; a brand-new key lands locally. `scope()` is the merged view (local shadowing), and `Object.keys(scope)` / `key in scope` are the union of both layers. It composes — `extendStore(extendStore(app, x), y)` chains parents.
136
138
 
137
139
  The seed may also be a **signal** of the matching kind, so an existing (externally-owned, reactive) signal becomes the local layer:
138
140
 
139
141
  ```typescript
140
142
  const draft = signal({ title: '' });
141
- const scope = app.extend(draft); // writes to scope.title flow out to `draft`, and back in
143
+ const scope = extendStore(app, draft); // writes to scope.title flow out to `draft`, and back in
142
144
  ```
143
145
 
144
146
  A few release notes:
145
147
 
146
- - The local layer is a plain store (vivify off). Inherited paths vivify when the _parent_ was created with `vivify`; to autovivify local keys, seed with a vivify-enabled store — `app.extend(store(seed, { vivify: 'auto' }))`.
147
- - Reserved names — `extend`, `asReadonlyStore`, and the signal methods (`set` / `update` / `mutate` / `inline` / `asReadonly`) — shadow same-named data keys, as on any store.
148
+ - The local layer is a plain store (vivify off). Inherited paths vivify when the _parent_ was created with `vivify`; to autovivify local keys, seed with a vivify-enabled store — `extendStore(app, store(seed, { vivify: 'auto' }))`.
149
+ - Reserved names — `asReadonlyStore` and the signal methods (`set` / `update` / `mutate` / `inline` / `asReadonly`) — shadow same-named data keys, as on any store.
148
150
  - `scope.asReadonlyStore()` returns a read-only **snapshot view** of the merge (reactive reads, no writes); it does not share sub-store identity.
149
151
 
150
152
  ### `forkStore`
@@ -165,7 +167,7 @@ base.user.name(); // 'Bob'
165
167
  // draft.discard(); // …or throw the edits away
166
168
  ```
167
169
 
168
- The fork is a full store (`draft.store.user.name(...)`, `extend`, deep reads/writes — everything `store` gives you). It's built on `linkedSignal`: it holds local writes until the **base changes underneath it**, then runs a `strategy` to reconcile:
170
+ The fork is a full store (`draft.store.user.name(...)`, `extendStore`, deep reads/writes — everything `store` gives you). It's built on `linkedSignal`: it holds local writes until the **base changes underneath it**, then runs a `strategy` to reconcile:
169
171
 
170
172
  - **`'fine'`** (default for immutable stores) — per-path 3-way merge: keep the paths the fork edited, take the base's live values for the paths it didn't. Survives concurrent base changes. Relies on copy-on-write reference identity, so it's **unsupported on a mutable base** (in-place mutation defeats it — `fork` warns and falls back to `'coarse'`).
171
173
  - **`'coarse'`** — any base change resets the whole fork. Cheapest; correct when the base is held for the fork's lifetime (e.g. a transition). The default for a mutable base.
@@ -365,6 +367,19 @@ pausableEffect(() => poll(url())); // body skipped while paused; deps collapse s
365
367
 
366
368
  While paused each one **collapses its dependency set to just the pause predicate**, so an upstream change can't trigger work; on resume it re-tracks and re-runs / recomputes with the latest values. SSR never pauses.
367
369
 
370
+ #### `providePausableOptions`
371
+
372
+ Sets an app-wide default pause source for every pausable-aware primitive — the `pausable*` family above plus the opt-in integrations (`stored`, `chunked`). A call-site `pause` always wins; this only fills in when the call didn't specify one. Use it to make everything honour the ambient `*mmActivity` boundary from one place:
373
+
374
+ ```typescript
375
+ import { providePausableOptions } from '@mmstack/primitives';
376
+
377
+ // e.g. in app.config.ts
378
+ providers: [providePausableOptions({ pause: true })];
379
+ ```
380
+
381
+ With this provided, `stored(...)` / `chunked(...)` (off by default) start reading the ambient paused context; pass `pause: false` at an individual call site to opt that one back out.
382
+
368
383
  ### Suspense — `<mm-suspense>` and the transition scope
369
384
 
370
385
  A **transition scope** is a per-boundary registry of resources whose async state a boundary coordinates. `<mm-suspense>` provides its own scope, so resources created in its subtree register into it automatically (via `@mmstack/resource`'s `register` option, or `registerResource(ref)` for a hand-rolled `ResourceRef`):
@@ -631,6 +646,7 @@ const mouse = sensor('mousePosition', {
631
646
  | `windowSize` | `windowSize()` | `Signal<{ width, height }>` + `.unthrottled` | Throttled to 100ms by default. |
632
647
  | `scrollPosition` | `scrollPosition()` | `Signal<{ x, y }>` + `.unthrottled` | Window or element scroll, throttled 100ms. |
633
648
  | `mousePosition` | `mousePosition()` | `Signal<{ x, y }>` + `.unthrottled` | Throttled 100ms. `coordinateSpace: 'client' \| 'page'`, optional `touch`. |
649
+ | `pointerDrag` | `pointerDrag()` | `Signal<PointerDragState>` + `.unthrottled` + `.cancel()` | Pointer gesture (down→move→up) with `activationThreshold`, `delta`, modifiers, pointer capture, Escape-cancel. |
634
650
  | `elementVisibility` | `elementVisibility(target?)` | `Signal<IntersectionObserverEntry?>` + `.visible` | IntersectionObserver-based, `.visible` is a boolean shorthand. |
635
651
  | `elementSize` | `elementSize(target?)` | `Signal<{ width, height }?>` | ResizeObserver-based. Defaults to `border-box`. |
636
652
  | `geolocation` | `geolocation(opt?)` | `Signal<GeolocationPosition?>` + `.error` + `.loading` | One-shot by default; pass `watch: true` for `watchPosition`. |
@@ -640,7 +656,30 @@ const mouse = sensor('mousePosition', {
640
656
  | `idle` | `idle({ ms })` | `Signal<boolean>` + `.since` | Flips to `true` after `ms` of inactivity. Configurable activity events. |
641
657
  | `focusWithin` | `focusWithin(target?)` | `Signal<boolean>` | Mirrors the `:focus-within` CSS pseudo-class. |
642
658
 
643
- Element-targeting sensors (`elementSize`, `elementVisibility`, `focusWithin`) default `target` to `inject(ElementRef)` so they're drop-in inside a component.
659
+ Element-targeting sensors (`elementSize`, `elementVisibility`, `focusWithin`, `pointerDrag`) default `target` to `inject(ElementRef)` so they're drop-in inside a component.
660
+
661
+ ### `pointerDrag`
662
+
663
+ Tracks a pointer **gesture** (pointerdown → capture → move → up) as a signal — the
664
+ foundation for pointer-based move/resize/marquee on a canvas. Unlike native HTML5
665
+ drag, pointer events fire continuously and coordinates stay reliable; `delta` is
666
+ computed on the same update as `current` (never torn). `active` only flips true
667
+ once the pointer travels past `activationThreshold`, so the same element stays
668
+ clickable. Uses `setPointerCapture`, supports a delegated `handleSelector`, and
669
+ cancels on Escape or via `.cancel()`.
670
+
671
+ ```typescript
672
+ import { sensor } from '@mmstack/primitives';
673
+
674
+ const drag = sensor('pointerDrag', { activationThreshold: 4 });
675
+
676
+ // derive position from the gesture — no effects
677
+ const position = computed(() => {
678
+ const d = drag();
679
+ return d.active ? { x: base.x + d.delta.x, y: base.y + d.delta.y } : base;
680
+ });
681
+ // drag().modifiers.shift → e.g. constrain axis · drag.cancel() → revert
682
+ ```
644
683
 
645
684
  ### `signalFromEvent`
646
685