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.
- package/dist/automerge/doc-cell.d.ts +20 -0
- package/dist/automerge/doc-cell.js +80 -0
- package/dist/automerge/index.d.ts +3 -0
- package/dist/automerge/index.js +12 -0
- package/dist/automerge/reconcile.d.ts +5 -0
- package/dist/automerge/reconcile.js +63 -0
- package/dist/core/_counts.d.ts +48 -0
- package/dist/core/_counts.js +58 -0
- package/dist/core/cell.d.ts +148 -112
- package/dist/core/cell.js +946 -768
- package/dist/core/debug.d.ts +25 -0
- package/dist/core/debug.js +121 -0
- package/dist/core/index.d.ts +6 -1
- package/dist/core/index.js +5 -0
- package/dist/core/lenses/closed-form-policies.js +8 -3
- package/dist/core/lenses/index.d.ts +1 -0
- package/dist/core/lenses/index.js +1 -0
- package/dist/core/lenses/snap.d.ts +18 -0
- package/dist/core/lenses/snap.js +145 -0
- package/dist/core/optic.d.ts +13 -0
- package/dist/core/optic.js +44 -0
- package/dist/core/optics.d.ts +10 -0
- package/dist/core/optics.js +30 -0
- package/dist/core/store.d.ts +10 -0
- package/dist/core/store.js +85 -0
- package/dist/core/values/audio.js +4 -5
- package/dist/core/values/canvas.js +15 -18
- package/dist/core/values/str.js +8 -8
- package/dist/formats/lens.js +6 -9
- package/dist/jsx-dev-runtime.d.ts +2 -0
- package/dist/jsx-dev-runtime.js +5 -0
- package/dist/jsx-runtime.d.ts +54 -0
- package/dist/jsx-runtime.js +219 -0
- package/dist/schema/lens.js +5 -5
- package/dist/shapes/drag-behaviors.d.ts +56 -0
- package/dist/shapes/drag-behaviors.js +102 -0
- package/dist/shapes/drag-spec.d.ts +52 -0
- package/dist/shapes/drag-spec.js +112 -0
- package/dist/shapes/index.d.ts +3 -1
- package/dist/shapes/index.js +3 -1
- package/dist/shapes/interaction.d.ts +2 -3
- package/dist/shapes/interaction.js +77 -56
- package/dist/shapes/label.js +6 -0
- package/dist/shapes/layout.d.ts +47 -1
- package/dist/shapes/layout.js +59 -1
- 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(
|
|
217
|
-
init:
|
|
218
|
-
|
|
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,
|
|
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 {
|
|
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(
|
|
248
|
-
init:
|
|
249
|
-
|
|
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,
|
|
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 {
|
|
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(
|
|
336
|
-
init:
|
|
337
|
-
|
|
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,
|
|
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 {
|
|
351
|
+
return { update: stamp(out.tex, s.w, s.h), complement: c };
|
|
355
352
|
},
|
|
356
353
|
});
|
|
357
354
|
}
|
package/dist/core/values/str.js
CHANGED
|
@@ -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
|
|
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
|
|
28
|
-
* is the forward view, `reconstruct`
|
|
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(
|
|
31
|
-
init: (
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
package/dist/formats/lens.js
CHANGED
|
@@ -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:
|
|
40
|
-
step: (
|
|
41
|
-
fwd: (
|
|
42
|
-
bwd: (target,
|
|
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 {
|
|
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,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
|
+
}
|
package/dist/schema/lens.js
CHANGED
|
@@ -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:
|
|
376
|
-
step: (
|
|
377
|
-
fwd: (
|
|
378
|
-
bwd: (t,
|
|
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 {
|
|
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 {};
|