bireactive 0.3.0 → 0.3.1

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.
Files changed (46) hide show
  1. package/dist/automerge/doc-cell.d.ts +20 -0
  2. package/dist/automerge/doc-cell.js +80 -0
  3. package/dist/automerge/index.d.ts +3 -0
  4. package/dist/automerge/index.js +12 -0
  5. package/dist/automerge/reconcile.d.ts +5 -0
  6. package/dist/automerge/reconcile.js +63 -0
  7. package/dist/core/_counts.d.ts +48 -0
  8. package/dist/core/_counts.js +58 -0
  9. package/dist/core/cell.d.ts +148 -112
  10. package/dist/core/cell.js +946 -768
  11. package/dist/core/debug.d.ts +25 -0
  12. package/dist/core/debug.js +121 -0
  13. package/dist/core/index.d.ts +6 -1
  14. package/dist/core/index.js +5 -0
  15. package/dist/core/lenses/closed-form-policies.js +8 -3
  16. package/dist/core/lenses/index.d.ts +1 -0
  17. package/dist/core/lenses/index.js +1 -0
  18. package/dist/core/lenses/snap.d.ts +18 -0
  19. package/dist/core/lenses/snap.js +145 -0
  20. package/dist/core/optic.d.ts +13 -0
  21. package/dist/core/optic.js +44 -0
  22. package/dist/core/optics.d.ts +10 -0
  23. package/dist/core/optics.js +30 -0
  24. package/dist/core/store.d.ts +10 -0
  25. package/dist/core/store.js +85 -0
  26. package/dist/core/values/audio.js +4 -5
  27. package/dist/core/values/canvas.js +15 -18
  28. package/dist/core/values/str.js +8 -8
  29. package/dist/formats/lens.js +6 -9
  30. package/dist/jsx-dev-runtime.d.ts +2 -0
  31. package/dist/jsx-dev-runtime.js +5 -0
  32. package/dist/jsx-runtime.d.ts +54 -0
  33. package/dist/jsx-runtime.js +219 -0
  34. package/dist/schema/lens.js +5 -5
  35. package/dist/shapes/drag-behaviors.d.ts +56 -0
  36. package/dist/shapes/drag-behaviors.js +102 -0
  37. package/dist/shapes/drag-spec.d.ts +52 -0
  38. package/dist/shapes/drag-spec.js +112 -0
  39. package/dist/shapes/index.d.ts +3 -1
  40. package/dist/shapes/index.js +3 -1
  41. package/dist/shapes/interaction.d.ts +2 -3
  42. package/dist/shapes/interaction.js +77 -56
  43. package/dist/shapes/label.js +6 -0
  44. package/dist/shapes/layout.d.ts +47 -1
  45. package/dist/shapes/layout.js +59 -1
  46. package/package.json +24 -2
@@ -213,21 +213,20 @@ export class Canvas extends Cell {
213
213
  return c;
214
214
  };
215
215
  const self = this;
216
- return Canvas.lens([self], {
217
- init: ([s]) => chromaOf(s),
218
- step: ([s], c, external) => (external ? chromaOf(s) : c),
219
- fwd: ([s]) => {
216
+ return Canvas.lens(self, {
217
+ init: s => chromaOf(s),
218
+ fwd: s => {
220
219
  const out = sf(s.w, s.h);
221
220
  pass(LUMA, out, x => x.tex("u_s", 0, s.tex));
222
221
  return stamp(out.tex, s.w, s.h);
223
222
  },
224
- bwd: (target, [s], c) => {
223
+ bwd: (target, s, c) => {
225
224
  const out = sb(s.w, s.h);
226
225
  pass(RECOLOR, out, x => {
227
226
  x.tex("u_t", 0, target.tex);
228
227
  x.tex("u_c", 1, c.tex);
229
228
  });
230
- return { updates: [stamp(out.tex, s.w, s.h)], complement: c };
229
+ return { update: stamp(out.tex, s.w, s.h), complement: c };
231
230
  },
232
231
  });
233
232
  }
@@ -244,21 +243,20 @@ export class Canvas extends Cell {
244
243
  return c;
245
244
  };
246
245
  const self = this;
247
- return Canvas.lens([self], {
248
- init: ([s]) => lumaOf(s),
249
- step: ([s], c, external) => (external ? lumaOf(s) : c),
250
- fwd: ([s]) => {
246
+ return Canvas.lens(self, {
247
+ init: s => lumaOf(s),
248
+ fwd: s => {
251
249
  const out = sf(s.w, s.h);
252
250
  pass(CHROMA_VIEW, out, x => x.tex("u_s", 0, s.tex));
253
251
  return stamp(out.tex, s.w, s.h);
254
252
  },
255
- bwd: (target, [s], c) => {
253
+ bwd: (target, s, c) => {
256
254
  const out = sb(s.w, s.h);
257
255
  pass(DELUMA, out, x => {
258
256
  x.tex("u_t", 0, target.tex);
259
257
  x.tex("u_c", 1, c.tex);
260
258
  });
261
- return { updates: [stamp(out.tex, s.w, s.h)], complement: c };
259
+ return { update: stamp(out.tex, s.w, s.h), complement: c };
262
260
  },
263
261
  });
264
262
  }
@@ -332,14 +330,13 @@ export class Canvas extends Cell {
332
330
  return res;
333
331
  };
334
332
  const self = this;
335
- return Canvas.lens([self], {
336
- init: ([s]) => residualOf(s),
337
- step: ([s], c, external) => (external ? residualOf(s) : c),
338
- fwd: ([s]) => {
333
+ return Canvas.lens(self, {
334
+ init: s => residualOf(s),
335
+ fwd: s => {
339
336
  const small = down(sdF, s.tex, s.w, s.h);
340
337
  return stamp(small.tex, small.w, small.h);
341
338
  },
342
- bwd: (target, [s], c) => {
339
+ bwd: (target, s, c) => {
343
340
  const up = suB(s.w, s.h);
344
341
  pass(UP, up, x => {
345
342
  x.tex("u_small", 0, target.tex);
@@ -351,7 +348,7 @@ export class Canvas extends Cell {
351
348
  x.tex("u_a", 0, up.tex);
352
349
  x.tex("u_b", 1, c.tex);
353
350
  });
354
- return { updates: [stamp(out.tex, s.w, s.h)], complement: c };
351
+ return { update: stamp(out.tex, s.w, s.h), complement: c };
355
352
  },
356
353
  });
357
354
  }
@@ -22,16 +22,16 @@ import { Cell } from "../cell.js";
22
22
  // The complement is state recorded forward from the source and consumed
23
23
  // on write-back. It persists across the lens's own writes (so `trim`
24
24
  // keeps its padding even when the view is emptied) and refreshes on
25
- // external source changes — the engine's `external` flag drives `step`.
25
+ // external source changes — the engine re-runs `init` (the default `step`)
26
+ // only when the source actually moves.
26
27
  /** Endo lens backed by a complement recorded from the source. `record`
27
- * rebuilds the complement (kept on the lens's own writes), `project`
28
- * is the forward view, `reconstruct` is the backward source. */
28
+ * rebuilds the complement (kept on the lens's own writes; re-run on external
29
+ * source changes), `project` is the forward view, `reconstruct` the source. */
29
30
  function complementLens(parent, record, project, reconstruct) {
30
- return Str.lens([parent], {
31
- init: ([s]) => record(s),
32
- step: ([s], c, external) => (external ? record(s) : c),
33
- fwd: ([s]) => project(s),
34
- bwd: (target, _s, c) => ({ updates: [reconstruct(target, c)], complement: c }),
31
+ return Str.lens(parent, {
32
+ init: (s) => record(s),
33
+ fwd: (s) => project(s),
34
+ bwd: (target, _s, c) => ({ update: reconstruct(target, c), complement: c }),
35
35
  });
36
36
  }
37
37
  export const equals = (a, b) => a === b;
@@ -36,19 +36,16 @@ export function valueHub(initial) {
36
36
  * the error regions. */
37
37
  export function formatSpoke(hub, adapter) {
38
38
  return lens(hub, {
39
- init: ([v]) => fromValue(adapter, v),
40
- step: ([v], c) => (v === c.synced ? c : absorb(adapter, c, v)),
41
- fwd: (_vals, c) => c.text,
42
- bwd: (target, _vals, c) => {
39
+ init: v => fromValue(adapter, v),
40
+ step: (v, c) => (v === c.synced ? c : absorb(adapter, c, v)),
41
+ fwd: (_v, c) => c.text,
42
+ bwd: (target, _v, c) => {
43
43
  const { tree, errors } = adapter.parse(target);
44
44
  if (errors.length === 0) {
45
45
  const v = valueOf(tree);
46
- return { updates: [v], complement: { text: target, tree, errors, synced: v } };
46
+ return { update: v, complement: { text: target, tree, errors, synced: v } };
47
47
  }
48
- return {
49
- updates: [SKIP],
50
- complement: { text: target, tree, errors, synced: c.synced },
51
- };
48
+ return { update: SKIP, complement: { text: target, tree, errors, synced: c.synced } };
52
49
  },
53
50
  });
54
51
  }
@@ -0,0 +1,2 @@
1
+ export * from "./jsx-runtime.js";
2
+ export { jsx as jsxDEV } from "./jsx-runtime.js";
@@ -0,0 +1,5 @@
1
+ // Development entry for the automatic JSX runtime. esbuild/tsc import this when
2
+ // `jsxDev` is enabled; the dev signature carries extra debug args we ignore, so
3
+ // `jsxDEV` just forwards to `jsx`.
4
+ export * from "./jsx-runtime.js";
5
+ export { jsx as jsxDEV } from "./jsx-runtime.js";
@@ -0,0 +1,54 @@
1
+ import { Cell } from "./core/cell.js";
2
+ /** Marker for `<>…</>`; lowered to `jsx(Fragment, …)`. */
3
+ export declare const Fragment: unique symbol;
4
+ type Disposer = () => void;
5
+ /** Register a teardown with the active scope (`mount` / `scope` / an `each`
6
+ * item) — for raw `effect`s or listeners created in a component body, which
7
+ * the JSX helpers otherwise track for you. No-op outside a scope. */
8
+ export declare function onCleanup(fn: Disposer): void;
9
+ type Props = Record<string, unknown> & {
10
+ children?: unknown;
11
+ };
12
+ type Component = (props: Props) => Node;
13
+ /** Build a DOM node for one JSX element. */
14
+ export declare function jsx(type: string | symbol | Component, props?: Props): Node;
15
+ /** Static-children variant; behaviourally identical in a runtime builder. */
16
+ export declare const jsxs: typeof jsx;
17
+ /** Render `component` into `host`, collecting reactive teardowns. The returned
18
+ * disposer releases them — call it on unmount (e.g. `disconnectedCallback`). */
19
+ export declare function mount(component: () => Node, host: Node): Disposer;
20
+ /** Run `fn` under a fresh reactive scope, returning its result and a disposer
21
+ * for every effect created during it — `mount` without a host. `each` gives
22
+ * each keyed item its own scope so its effects die when the item leaves. */
23
+ export declare function scope<T>(fn: () => T): [T, Disposer];
24
+ /** Keyed list rendering: keep `parent`'s children in sync with a reactive array,
25
+ * reusing and reordering nodes by key, disposing those that leave. Each item is
26
+ * rendered in its own `scope` (untracked from the list effect, so item-internal
27
+ * reads don't retrigger the whole list). Attach via `ref`:
28
+ * `<div ref={el => each(el, items, s => s.id, render)} />`. */
29
+ export declare function each<T>(parent: Element, items: Cell<T[]> | (() => readonly T[]), key: (item: T, index: number) => string, render: (item: T, index: number) => Node): void;
30
+ type Reactive<T> = T | Cell<T> | (() => T);
31
+ export declare namespace JSX {
32
+ export type Element = Node;
33
+ export interface ElementChildrenAttribute {
34
+ children: unknown;
35
+ }
36
+ export interface IntrinsicAttributes {
37
+ children?: unknown;
38
+ }
39
+ export interface CommonProps {
40
+ class?: Reactive<string>;
41
+ style?: Reactive<string | Partial<CSSStyleDeclaration>>;
42
+ id?: Reactive<string>;
43
+ lens?: Cell<any>;
44
+ ref?: (el: Element) => void;
45
+ children?: unknown;
46
+ [attr: string]: any;
47
+ }
48
+ type Tag = keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap;
49
+ export type IntrinsicElements = {
50
+ [K in Tag]: CommonProps;
51
+ };
52
+ export {};
53
+ }
54
+ export {};
@@ -0,0 +1,219 @@
1
+ // jsx-runtime.ts — minimal runtime JSX for bireactive (no compiler step).
2
+ //
3
+ // esbuild's automatic runtime (`jsxImportSource: "@bireactive"`) lowers JSX to
4
+ // `jsx`/`jsxs`/`Fragment` calls; this module builds real DOM nodes and wires
5
+ // reactive props and children through `effect`. The one binding with no React
6
+ // analogue is the `lens` prop: a single bidirectional terminal — the dual of
7
+ // the value-plus-onInput pair — that reads a writable cell forward into the
8
+ // control and writes edits back, so a chain of lenses can be driven from the
9
+ // leaf. Components are plain `props → Node`; reactive expressions are passed as
10
+ // thunks (`{() => expr}`) or cells, since there is no compile step to wrap them.
11
+ import { Cell, effect, untracked } from "./core/cell.js";
12
+ /** Marker for `<>…</>`; lowered to `jsx(Fragment, …)`. */
13
+ export const Fragment = Symbol.for("bireactive.jsx.Fragment");
14
+ // The active mount scope: reactive teardowns created while building a tree are
15
+ // collected here so `mount` can release them all on unmount. Non-reentrant —
16
+ // `mount` saves/restores the previous owner around the render.
17
+ let currentOwner = null;
18
+ function track(d) {
19
+ currentOwner?.push(d);
20
+ }
21
+ /** Register a teardown with the active scope (`mount` / `scope` / an `each`
22
+ * item) — for raw `effect`s or listeners created in a component body, which
23
+ * the JSX helpers otherwise track for you. No-op outside a scope. */
24
+ export function onCleanup(fn) {
25
+ track(fn);
26
+ }
27
+ /** Build a DOM node for one JSX element. */
28
+ export function jsx(type, props) {
29
+ if (typeof type === "function")
30
+ return type(props ?? {});
31
+ if (type === Fragment) {
32
+ const frag = document.createDocumentFragment();
33
+ append(frag, props?.children);
34
+ return frag;
35
+ }
36
+ const el = document.createElement(type);
37
+ if (props)
38
+ for (const key in props)
39
+ applyProp(el, key, props[key]);
40
+ return el;
41
+ }
42
+ /** Static-children variant; behaviourally identical in a runtime builder. */
43
+ export const jsxs = jsx;
44
+ function applyProp(el, key, value) {
45
+ if (key === "children")
46
+ return append(el, value);
47
+ if (key === "ref") {
48
+ if (typeof value === "function")
49
+ value(el);
50
+ return;
51
+ }
52
+ if (key === "lens")
53
+ return bindLens(el, value);
54
+ if (key.startsWith("on") && typeof value === "function") {
55
+ el.addEventListener(key.slice(2).toLowerCase(), value);
56
+ return;
57
+ }
58
+ if (value instanceof Cell) {
59
+ track(effect(() => setProp(el, key, value.value)));
60
+ return;
61
+ }
62
+ if (typeof value === "function") {
63
+ track(effect(() => setProp(el, key, value())));
64
+ return;
65
+ }
66
+ setProp(el, key, value);
67
+ }
68
+ function setProp(el, key, value) {
69
+ if (key === "class" || key === "className") {
70
+ el.setAttribute("class", value == null ? "" : String(value));
71
+ }
72
+ else if (key === "style") {
73
+ if (value && typeof value === "object")
74
+ Object.assign(el.style, value);
75
+ else
76
+ el.setAttribute("style", value == null ? "" : String(value));
77
+ }
78
+ else if (key === "value") {
79
+ el.value = value == null ? "" : String(value);
80
+ }
81
+ else if (key === "checked" || key === "disabled" || key === "selected") {
82
+ // biome-ignore lint/suspicious/noExplicitAny: boolean DOM properties
83
+ el[key] = !!value;
84
+ }
85
+ else if (value == null || value === false) {
86
+ el.removeAttribute(key);
87
+ }
88
+ else {
89
+ el.setAttribute(key, value === true ? "" : String(value));
90
+ }
91
+ }
92
+ /** Append a child (array / Node / text / cell / thunk) to `parent`. */
93
+ function append(parent, child) {
94
+ if (Array.isArray(child)) {
95
+ for (const c of child)
96
+ append(parent, c);
97
+ }
98
+ else if (child instanceof Node) {
99
+ parent.appendChild(child);
100
+ }
101
+ else if (child instanceof Cell) {
102
+ parent.appendChild(reactiveText(() => child.value));
103
+ }
104
+ else if (typeof child === "function") {
105
+ parent.appendChild(reactiveText(child));
106
+ }
107
+ else if (child != null && child !== false && child !== true) {
108
+ parent.appendChild(document.createTextNode(String(child)));
109
+ }
110
+ }
111
+ /** A text node whose content tracks `get()`. Primitive children only — the
112
+ * minimal runtime does not reconcile dynamic element children. */
113
+ function reactiveText(get) {
114
+ const node = document.createTextNode("");
115
+ track(effect(() => {
116
+ const v = get();
117
+ node.data = v == null ? "" : String(v);
118
+ }));
119
+ return node;
120
+ }
121
+ /** Two-way bind a form control to a writable cell: read forward into the
122
+ * control, write back on input. The forward write is skipped while the
123
+ * control is focused, so a live edit is never clobbered mid-drag (the
124
+ * controlled-input focus guard, written once). */
125
+ function bindLens(el, lens) {
126
+ const checkbox = el.type === "checkbox";
127
+ track(effect(() => {
128
+ const v = lens.value;
129
+ if (checkbox) {
130
+ el.checked = !!v;
131
+ return;
132
+ }
133
+ const next = v == null ? "" : String(v);
134
+ const root = el.getRootNode();
135
+ if (root.activeElement !== el && el.value !== next)
136
+ el.value = next;
137
+ }));
138
+ const evt = checkbox || el.tagName === "SELECT" ? "change" : "input";
139
+ el.addEventListener(evt, () => {
140
+ lens.value = checkbox
141
+ ? el.checked
142
+ : el.type === "range" || el.type === "number"
143
+ ? Number(el.value)
144
+ : el.value;
145
+ });
146
+ }
147
+ /** Render `component` into `host`, collecting reactive teardowns. The returned
148
+ * disposer releases them — call it on unmount (e.g. `disconnectedCallback`). */
149
+ export function mount(component, host) {
150
+ const [node, dispose] = scope(component);
151
+ host.appendChild(node);
152
+ return dispose;
153
+ }
154
+ /** Run `fn` under a fresh reactive scope, returning its result and a disposer
155
+ * for every effect created during it — `mount` without a host. `each` gives
156
+ * each keyed item its own scope so its effects die when the item leaves. */
157
+ export function scope(fn) {
158
+ const prev = currentOwner;
159
+ const owner = [];
160
+ currentOwner = owner;
161
+ try {
162
+ return [
163
+ fn(),
164
+ () => {
165
+ for (const d of owner)
166
+ d();
167
+ owner.length = 0;
168
+ },
169
+ ];
170
+ }
171
+ finally {
172
+ currentOwner = prev;
173
+ }
174
+ }
175
+ /** Keyed list rendering: keep `parent`'s children in sync with a reactive array,
176
+ * reusing and reordering nodes by key, disposing those that leave. Each item is
177
+ * rendered in its own `scope` (untracked from the list effect, so item-internal
178
+ * reads don't retrigger the whole list). Attach via `ref`:
179
+ * `<div ref={el => each(el, items, s => s.id, render)} />`. */
180
+ export function each(parent, items, key, render) {
181
+ const read = typeof items === "function" ? items : () => items.value;
182
+ const cache = new Map();
183
+ const stop = effect(() => {
184
+ const arr = read();
185
+ const seen = new Set();
186
+ const nodes = [];
187
+ arr.forEach((item, i) => {
188
+ const k = key(item, i);
189
+ seen.add(k);
190
+ let entry = cache.get(k);
191
+ if (entry === undefined) {
192
+ const [node, dispose] = untracked(() => scope(() => render(item, i)));
193
+ entry = { node, dispose };
194
+ cache.set(k, entry);
195
+ }
196
+ nodes.push(entry.node);
197
+ });
198
+ for (const [k, entry] of cache) {
199
+ if (!seen.has(k)) {
200
+ entry.dispose();
201
+ cache.delete(k);
202
+ }
203
+ }
204
+ // Only touch the DOM when the ordered node set actually changed — re-inserting
205
+ // identical children mid-interaction would steal focus and reset clicks.
206
+ const cur = parent.childNodes;
207
+ let same = cur.length === nodes.length;
208
+ for (let i = 0; same && i < nodes.length; i++)
209
+ same = cur[i] === nodes[i];
210
+ if (!same)
211
+ parent.replaceChildren(...nodes);
212
+ });
213
+ track(() => {
214
+ stop();
215
+ for (const entry of cache.values())
216
+ entry.dispose();
217
+ cache.clear();
218
+ });
219
+ }
@@ -372,12 +372,12 @@ export function recurse(build) {
372
372
  /** Lift a value-lens onto a reactive cell. */
373
373
  export function toStep(vl) {
374
374
  return src => lens(src, {
375
- init: ([v]) => vl.init(v),
376
- step: ([v], c) => (vl.step ? vl.step(v, c) : c),
377
- fwd: ([v], c) => vl.fwd(v, c),
378
- bwd: (t, [v], c) => {
375
+ init: v => vl.init(v),
376
+ step: (v, c) => (vl.step ? vl.step(v, c) : c),
377
+ fwd: (v, c) => vl.fwd(v, c),
378
+ bwd: (t, v, c) => {
379
379
  const r = vl.bwd(t, v, c);
380
- return { updates: [r.s], complement: r.c };
380
+ return { update: r.s, complement: r.c };
381
381
  },
382
382
  });
383
383
  }
@@ -0,0 +1,56 @@
1
+ import { type Animator, type SpringOpts } from "../animation/index.js";
2
+ import { type Cell, type Inner, type Read, Vec, type Writable } from "../core/index.js";
3
+ import type { Drag } from "./drag-spec.js";
4
+ import type { AnyShape } from "./shape.js";
5
+ export interface FloatingResult {
6
+ /** True between pointerdown and release. Drive ghosting/elevation off this. */
7
+ dragging: Cell<boolean>;
8
+ /** Start this on the diagram's `Anim` (`this.anim.start(anim)`). */
9
+ anim: Animator<void>;
10
+ dispose: () => void;
11
+ }
12
+ export interface FloatingOpts extends SpringOpts<{
13
+ x: number;
14
+ y: number;
15
+ }> {
16
+ }
17
+ /** Dragology's `withFloating`: while held, `pos` follows the pointer
18
+ * directly (via the robust `drag` wiring — grab offset, touch, capture);
19
+ * on release it springs to `home` (the resolved target, e.g. a `closest`
20
+ * snap position or a layout slot). `pos` is the caller-owned display cell
21
+ * the shape renders from.
22
+ *
23
+ * const pos = vec(home.peek());
24
+ * const dot = s(circle(pos, 10));
25
+ * const { anim } = floating(dot, pos, home);
26
+ * this.anim.start(anim);
27
+ *
28
+ * While dragging, the settle spring is frozen (rate 0) so it never fights
29
+ * the pointer; on release it re-engages and eases `pos` home. */
30
+ export declare function floating(shape: AnyShape, pos: Writable<Vec>, home: Read<{
31
+ x: number;
32
+ y: number;
33
+ }>, opts?: FloatingOpts): FloatingResult;
34
+ /** Run `grab`/`drop` on the rising/falling edge of `active`. */
35
+ export declare function onGesture(active: Read<boolean>, edges: {
36
+ grab?: () => void;
37
+ drop?: () => void;
38
+ }): () => void;
39
+ /** Re-append shapes to raise them above siblings (z-order). */
40
+ export declare function raise(...shapes: readonly AnyShape[]): void;
41
+ export interface DragModel<M, Id> {
42
+ /** Which element is being dragged (null when idle). */
43
+ active: Cell<Id | null>;
44
+ /** The free pointer for the active drag (bound by `grip`). */
45
+ pointer: Writable<Vec>;
46
+ /** Previewed model while dragging, else the committed model. */
47
+ preview: Read<M>;
48
+ /** Where the dragged handle sits this frame (float the dragged element here). */
49
+ at: Vec;
50
+ /** Wire a handle: seed + claim on press, commit `drop` on release. */
51
+ grip(handle: AnyShape, id: Id, seed: () => Inner<Vec>, onGrab?: () => void): () => void;
52
+ }
53
+ /** Bind a committed `model` cell to a `Drag<M>` spec built at grab time. Owns
54
+ * the transient drag state (which element, the free pointer, the live preview)
55
+ * and commits the spec's drop on release — the demo only renders `preview`/`at`. */
56
+ export declare function dragModel<M, Id>(model: Writable<Cell<M>>, spec: (id: Id, pointer: Read<Inner<Vec>>) => Drag<M>): DragModel<M, Id>;
@@ -0,0 +1,102 @@
1
+ // drag-behaviors.ts — Dragology-style drag *modifiers* layered over the
2
+ // scene graph. The model-driven cores (`closest`, `between`, `whenFar`)
3
+ // live in `core/lenses/snap.ts`; these wire them to pointer input and the
4
+ // animation clock.
5
+ //
6
+ // The key idea, and the answer to "drag-and-drop complicates state": the
7
+ // floating offset and the spring-settle are TRANSIENT drag state, held in
8
+ // the animator, never written to the model. The model only ever sees the
9
+ // committed drop.
10
+ import { spring } from "../animation/index.js";
11
+ import { cell, derive, effect, Vec, vec, } from "../core/index.js";
12
+ import { drag } from "./interaction.js";
13
+ /** Dragology's `withFloating`: while held, `pos` follows the pointer
14
+ * directly (via the robust `drag` wiring — grab offset, touch, capture);
15
+ * on release it springs to `home` (the resolved target, e.g. a `closest`
16
+ * snap position or a layout slot). `pos` is the caller-owned display cell
17
+ * the shape renders from.
18
+ *
19
+ * const pos = vec(home.peek());
20
+ * const dot = s(circle(pos, 10));
21
+ * const { anim } = floating(dot, pos, home);
22
+ * this.anim.start(anim);
23
+ *
24
+ * While dragging, the settle spring is frozen (rate 0) so it never fights
25
+ * the pointer; on release it re-engages and eases `pos` home. */
26
+ export function floating(shape, pos, home, opts = {}) {
27
+ const dragging = cell(false);
28
+ const dispose = drag(shape, pos, dragging);
29
+ const anim = spring(pos, home, {
30
+ omega: opts.omega ?? 24,
31
+ zeta: opts.zeta ?? 0.9,
32
+ ...opts,
33
+ // Never completes (re-engages every release) and yields to the pointer
34
+ // while held.
35
+ precision: 0,
36
+ rate: () => (dragging.value ? 0 : (opts.rate?.() ?? 1)),
37
+ });
38
+ return { dragging, anim, dispose };
39
+ }
40
+ // ── drag lifecycle ──────────────────────────────────────────────────
41
+ // The `was`-flag edge and z-raise every demo hand-rolls, factored out, plus a
42
+ // model-driven driver that ties a `Drag<M>` spec (drag-spec.ts) to the
43
+ // grab→preview→commit lifecycle — the spec is built once per grab (like
44
+ // Dragology's `dragologyOnDrag`), so candidate states are enumerated then.
45
+ /** Run `grab`/`drop` on the rising/falling edge of `active`. */
46
+ export function onGesture(active, edges) {
47
+ let was = false;
48
+ return effect(() => {
49
+ const now = active.value;
50
+ if (now && !was)
51
+ edges.grab?.();
52
+ else if (!now && was)
53
+ edges.drop?.();
54
+ was = now;
55
+ });
56
+ }
57
+ /** Re-append shapes to raise them above siblings (z-order). */
58
+ export function raise(...shapes) {
59
+ for (const s of shapes)
60
+ s.el.parentElement?.appendChild(s.el);
61
+ }
62
+ /** Bind a committed `model` cell to a `Drag<M>` spec built at grab time. Owns
63
+ * the transient drag state (which element, the free pointer, the live preview)
64
+ * and commits the spec's drop on release — the demo only renders `preview`/`at`. */
65
+ export function dragModel(model, spec) {
66
+ const active = cell(null);
67
+ const pointer = vec(0, 0);
68
+ const live = cell(null);
69
+ const preview = derive(() => {
70
+ const s = live.value;
71
+ return s ? s.preview.value : model.value;
72
+ });
73
+ const at = Vec.derive(() => {
74
+ const s = live.value;
75
+ return s ? s.at.value : pointer.value;
76
+ });
77
+ const grip = (handle, id, seed, onGrab) => {
78
+ const dragging = cell(false);
79
+ const offDown = handle.on("pointerdown", () => {
80
+ pointer.value = seed();
81
+ active.value = id;
82
+ live.value = spec(id, pointer);
83
+ onGrab?.();
84
+ });
85
+ const offDrag = drag(handle, pointer, dragging);
86
+ const offEdge = onGesture(dragging, {
87
+ drop: () => {
88
+ const s = live.peek();
89
+ if (s)
90
+ model.value = s.drop.peek();
91
+ active.value = null;
92
+ live.value = null;
93
+ },
94
+ });
95
+ return () => {
96
+ offDown();
97
+ offDrag();
98
+ offEdge();
99
+ };
100
+ };
101
+ return { active, pointer, preview, at, grip };
102
+ }
@@ -0,0 +1,52 @@
1
+ import { type Read } from "../core/index.js";
2
+ type V = {
3
+ x: number;
4
+ y: number;
5
+ };
6
+ /** A drag behavior: Dragology's `DragBehavior`, reactive and parametric in the
7
+ * MODEL `M` (positions are just `M = Vec`). */
8
+ export interface Drag<M> {
9
+ /** Model rendered this frame (non-dragged elements reflow toward this). */
10
+ preview: Read<M>;
11
+ /** Model committed on release. */
12
+ drop: Read<M>;
13
+ /** Where the dragged handle sits this frame (the renderer floats it here). */
14
+ at: Read<V>;
15
+ /** Residual |pointer − achievable|; combinators arbitrate on this. */
16
+ gap: Read<number>;
17
+ }
18
+ /** `d.fixed`: a single reachable model; `locate` reads where the dragged handle
19
+ * lands in it (a layout cell, or a pure layout fn). */
20
+ declare function fixed<M>(pointer: Read<V>, state: M, locate: (m: M) => V): Drag<M>;
21
+ /** `d.vary`: a continuous family; `place` is the BACKWARD map pointer→model (a
22
+ * lens / `argminVec`, not numerical search), `gap` the residual off the family. */
23
+ declare function vary<M>(pointer: Read<V>, place: (p: V) => M, locate: (m: M) => V): Drag<M>;
24
+ /** `d.closest`: the behavior with the smallest `gap` (discrete snapping and
25
+ * continuous tracks both pick with it). */
26
+ declare function closest<M>(bs: readonly Drag<M>[]): Drag<M>;
27
+ /** `d.between`: free motion in the candidates' convex hull; preview is their
28
+ * barycentric blend (`mix` any `Lerp`/`Linear` model). Unlike `closest` it does
29
+ * NOT snap — it rests at the blend, so `drop` is the previewed mix. */
30
+ declare function between<M>(pointer: Read<V>, bs: readonly Drag<M>[], mix: (ms: readonly M[], ws: readonly number[]) => M): Drag<M>;
31
+ /** `d.whenFar`: use `near` unless its gap exceeds `radius`, then `far` (snap
32
+ * into a port, else float free). */
33
+ declare function whenFar<M>(near: Drag<M>, far: Drag<M>, radius: number): Drag<M>;
34
+ /** `d.withFloating`: the dragged handle follows the pointer while the rest
35
+ * reflow (they already do — `preview` is reactive); just an `at` override. */
36
+ declare function withFloating<M>(pointer: Read<V>, b: Drag<M>): Drag<M>;
37
+ /** `d.onDrop`: transform the committed model (create/destroy, snap-to-grid),
38
+ * the escape hatch beyond repositional drags. */
39
+ declare function onDrop<M>(b: Drag<M>, f: (m: M) => M): Drag<M>;
40
+ /** The drag-behavior DSL (Dragology's `d.`): primitives `fixed`/`vary`,
41
+ * combinators `closest`/`between`/`whenFar`, modifiers `withFloating`/`onDrop`.
42
+ * Build a `Drag<M>` once at grab; the renderer reads `preview`/`at`/`drop`. */
43
+ export declare const d: {
44
+ readonly fixed: typeof fixed;
45
+ readonly vary: typeof vary;
46
+ readonly closest: typeof closest;
47
+ readonly between: typeof between;
48
+ readonly whenFar: typeof whenFar;
49
+ readonly withFloating: typeof withFloating;
50
+ readonly onDrop: typeof onDrop;
51
+ };
52
+ export {};