bireactive 0.2.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.
Files changed (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/animation/anim.d.ts +57 -0
  4. package/dist/animation/anim.js +318 -0
  5. package/dist/animation/combinators.d.ts +39 -0
  6. package/dist/animation/combinators.js +113 -0
  7. package/dist/animation/easings.d.ts +5 -0
  8. package/dist/animation/easings.js +5 -0
  9. package/dist/animation/index.d.ts +3 -0
  10. package/dist/animation/index.js +3 -0
  11. package/dist/assert/algebra.d.ts +20 -0
  12. package/dist/assert/algebra.js +79 -0
  13. package/dist/assert/claim.d.ts +40 -0
  14. package/dist/assert/claim.js +129 -0
  15. package/dist/assert/index.d.ts +7 -0
  16. package/dist/assert/index.js +19 -0
  17. package/dist/assert/predicates.d.ts +18 -0
  18. package/dist/assert/predicates.js +43 -0
  19. package/dist/assert/record.d.ts +20 -0
  20. package/dist/assert/record.js +78 -0
  21. package/dist/assert/scope.d.ts +42 -0
  22. package/dist/assert/scope.js +233 -0
  23. package/dist/assert/span.d.ts +37 -0
  24. package/dist/assert/span.js +68 -0
  25. package/dist/assert/tree.d.ts +22 -0
  26. package/dist/assert/tree.js +65 -0
  27. package/dist/code/code.d.ts +70 -0
  28. package/dist/code/code.js +361 -0
  29. package/dist/code/index.d.ts +2 -0
  30. package/dist/code/index.js +9 -0
  31. package/dist/code/morph.d.ts +5 -0
  32. package/dist/code/morph.js +194 -0
  33. package/dist/code/tokenize.d.ts +8 -0
  34. package/dist/code/tokenize.js +51 -0
  35. package/dist/constraints/cluster.d.ts +83 -0
  36. package/dist/constraints/cluster.js +213 -0
  37. package/dist/constraints/drivers.d.ts +15 -0
  38. package/dist/constraints/drivers.js +40 -0
  39. package/dist/constraints/factories.d.ts +73 -0
  40. package/dist/constraints/factories.js +248 -0
  41. package/dist/constraints/index.d.ts +11 -0
  42. package/dist/constraints/index.js +39 -0
  43. package/dist/constraints/interaction.d.ts +21 -0
  44. package/dist/constraints/interaction.js +148 -0
  45. package/dist/constraints/linalg.d.ts +18 -0
  46. package/dist/constraints/linalg.js +141 -0
  47. package/dist/constraints/phases.d.ts +21 -0
  48. package/dist/constraints/phases.js +60 -0
  49. package/dist/constraints/physics.d.ts +34 -0
  50. package/dist/constraints/physics.js +128 -0
  51. package/dist/constraints/rigid.d.ts +210 -0
  52. package/dist/constraints/rigid.js +835 -0
  53. package/dist/constraints/solver.d.ts +107 -0
  54. package/dist/constraints/solver.js +510 -0
  55. package/dist/constraints/term.d.ts +50 -0
  56. package/dist/constraints/term.js +80 -0
  57. package/dist/constraints/terms.d.ts +80 -0
  58. package/dist/constraints/terms.js +302 -0
  59. package/dist/constraints/world.d.ts +31 -0
  60. package/dist/constraints/world.js +245 -0
  61. package/dist/core/aggregates.d.ts +64 -0
  62. package/dist/core/aggregates.js +198 -0
  63. package/dist/core/anim.d.ts +84 -0
  64. package/dist/core/anim.js +301 -0
  65. package/dist/core/index.d.ts +38 -0
  66. package/dist/core/index.js +38 -0
  67. package/dist/core/introspect.d.ts +5 -0
  68. package/dist/core/introspect.js +31 -0
  69. package/dist/core/lenses/closed-form-policies.d.ts +64 -0
  70. package/dist/core/lenses/closed-form-policies.js +452 -0
  71. package/dist/core/lenses/domain-aggregates.d.ts +54 -0
  72. package/dist/core/lenses/domain-aggregates.js +259 -0
  73. package/dist/core/lenses/factor-lens.d.ts +42 -0
  74. package/dist/core/lenses/factor-lens.js +419 -0
  75. package/dist/core/lenses/index.d.ts +5 -0
  76. package/dist/core/lenses/index.js +16 -0
  77. package/dist/core/lenses/memory.d.ts +47 -0
  78. package/dist/core/lenses/memory.js +102 -0
  79. package/dist/core/lenses/typed-factor.d.ts +45 -0
  80. package/dist/core/lenses/typed-factor.js +376 -0
  81. package/dist/core/network-utils.d.ts +14 -0
  82. package/dist/core/network-utils.js +62 -0
  83. package/dist/core/new-primitives.d.ts +33 -0
  84. package/dist/core/new-primitives.js +113 -0
  85. package/dist/core/signal.d.ts +254 -0
  86. package/dist/core/signal.js +1349 -0
  87. package/dist/core/traits.d.ts +61 -0
  88. package/dist/core/traits.js +56 -0
  89. package/dist/core/tree.d.ts +23 -0
  90. package/dist/core/tree.js +62 -0
  91. package/dist/core/values/anchor.d.ts +23 -0
  92. package/dist/core/values/anchor.js +23 -0
  93. package/dist/core/values/audio.d.ts +33 -0
  94. package/dist/core/values/audio.js +107 -0
  95. package/dist/core/values/bool.d.ts +37 -0
  96. package/dist/core/values/bool.js +75 -0
  97. package/dist/core/values/box.d.ts +77 -0
  98. package/dist/core/values/box.js +211 -0
  99. package/dist/core/values/canvas.d.ts +71 -0
  100. package/dist/core/values/canvas.js +495 -0
  101. package/dist/core/values/color.d.ts +49 -0
  102. package/dist/core/values/color.js +106 -0
  103. package/dist/core/values/flags.d.ts +18 -0
  104. package/dist/core/values/flags.js +50 -0
  105. package/dist/core/values/gpu.d.ts +74 -0
  106. package/dist/core/values/gpu.js +426 -0
  107. package/dist/core/values/matrix.d.ts +53 -0
  108. package/dist/core/values/matrix.js +140 -0
  109. package/dist/core/values/num.d.ts +62 -0
  110. package/dist/core/values/num.js +166 -0
  111. package/dist/core/values/pose.d.ts +31 -0
  112. package/dist/core/values/pose.js +83 -0
  113. package/dist/core/values/range.d.ts +83 -0
  114. package/dist/core/values/range.js +167 -0
  115. package/dist/core/values/str.d.ts +76 -0
  116. package/dist/core/values/str.js +346 -0
  117. package/dist/core/values/template.d.ts +49 -0
  118. package/dist/core/values/template.js +148 -0
  119. package/dist/core/values/transform.d.ts +49 -0
  120. package/dist/core/values/transform.js +115 -0
  121. package/dist/core/values/tri.d.ts +31 -0
  122. package/dist/core/values/tri.js +95 -0
  123. package/dist/core/values/vec.d.ts +72 -0
  124. package/dist/core/values/vec.js +219 -0
  125. package/dist/core/writable.d.ts +15 -0
  126. package/dist/core/writable.js +29 -0
  127. package/dist/ext/events.d.ts +10 -0
  128. package/dist/ext/events.js +31 -0
  129. package/dist/ext/index.d.ts +4 -0
  130. package/dist/ext/index.js +4 -0
  131. package/dist/ext/snapshot.d.ts +8 -0
  132. package/dist/ext/snapshot.js +29 -0
  133. package/dist/ext/timeline.d.ts +56 -0
  134. package/dist/ext/timeline.js +94 -0
  135. package/dist/ext/waapi.d.ts +25 -0
  136. package/dist/ext/waapi.js +198 -0
  137. package/dist/index.d.ts +8 -0
  138. package/dist/index.js +10 -0
  139. package/dist/propagators/index.d.ts +6 -0
  140. package/dist/propagators/index.js +6 -0
  141. package/dist/propagators/layout.d.ts +68 -0
  142. package/dist/propagators/layout.js +336 -0
  143. package/dist/propagators/network.d.ts +52 -0
  144. package/dist/propagators/network.js +185 -0
  145. package/dist/propagators/propagator.d.ts +12 -0
  146. package/dist/propagators/propagator.js +16 -0
  147. package/dist/propagators/range.d.ts +45 -0
  148. package/dist/propagators/range.js +147 -0
  149. package/dist/propagators/relations.d.ts +60 -0
  150. package/dist/propagators/relations.js +343 -0
  151. package/dist/shapes/annular-sector.d.ts +15 -0
  152. package/dist/shapes/annular-sector.js +64 -0
  153. package/dist/shapes/button.d.ts +14 -0
  154. package/dist/shapes/button.js +31 -0
  155. package/dist/shapes/choreographers.d.ts +22 -0
  156. package/dist/shapes/choreographers.js +69 -0
  157. package/dist/shapes/circle.d.ts +17 -0
  158. package/dist/shapes/circle.js +57 -0
  159. package/dist/shapes/clip.d.ts +5 -0
  160. package/dist/shapes/clip.js +31 -0
  161. package/dist/shapes/connect.d.ts +16 -0
  162. package/dist/shapes/connect.js +70 -0
  163. package/dist/shapes/curve.d.ts +60 -0
  164. package/dist/shapes/curve.js +285 -0
  165. package/dist/shapes/dashed.d.ts +16 -0
  166. package/dist/shapes/dashed.js +142 -0
  167. package/dist/shapes/debug.d.ts +43 -0
  168. package/dist/shapes/debug.js +97 -0
  169. package/dist/shapes/group.d.ts +5 -0
  170. package/dist/shapes/group.js +10 -0
  171. package/dist/shapes/handle.d.ts +32 -0
  172. package/dist/shapes/handle.js +88 -0
  173. package/dist/shapes/index.d.ts +23 -0
  174. package/dist/shapes/index.js +23 -0
  175. package/dist/shapes/interaction.d.ts +32 -0
  176. package/dist/shapes/interaction.js +187 -0
  177. package/dist/shapes/label.d.ts +20 -0
  178. package/dist/shapes/label.js +42 -0
  179. package/dist/shapes/layout.d.ts +29 -0
  180. package/dist/shapes/layout.js +74 -0
  181. package/dist/shapes/line.d.ts +21 -0
  182. package/dist/shapes/line.js +79 -0
  183. package/dist/shapes/list.d.ts +18 -0
  184. package/dist/shapes/list.js +51 -0
  185. package/dist/shapes/mount.d.ts +7 -0
  186. package/dist/shapes/mount.js +10 -0
  187. package/dist/shapes/path.d.ts +77 -0
  188. package/dist/shapes/path.js +227 -0
  189. package/dist/shapes/rect.d.ts +30 -0
  190. package/dist/shapes/rect.js +131 -0
  191. package/dist/shapes/shape.d.ts +132 -0
  192. package/dist/shapes/shape.js +306 -0
  193. package/dist/shapes/text.d.ts +24 -0
  194. package/dist/shapes/text.js +53 -0
  195. package/dist/shapes/tokens.d.ts +28 -0
  196. package/dist/shapes/tokens.js +27 -0
  197. package/dist/shapes/transitions.d.ts +23 -0
  198. package/dist/shapes/transitions.js +62 -0
  199. package/dist/tex/decorations.d.ts +26 -0
  200. package/dist/tex/decorations.js +116 -0
  201. package/dist/tex/index.d.ts +5 -0
  202. package/dist/tex/index.js +5 -0
  203. package/dist/tex/marker.d.ts +17 -0
  204. package/dist/tex/marker.js +63 -0
  205. package/dist/tex/motion.d.ts +43 -0
  206. package/dist/tex/motion.js +290 -0
  207. package/dist/tex/parts.d.ts +65 -0
  208. package/dist/tex/parts.js +149 -0
  209. package/dist/tex/tex.d.ts +45 -0
  210. package/dist/tex/tex.js +244 -0
  211. package/dist/web/attr.d.ts +16 -0
  212. package/dist/web/attr.js +98 -0
  213. package/dist/web/diagram.d.ts +49 -0
  214. package/dist/web/diagram.js +260 -0
  215. package/dist/web/index.d.ts +6 -0
  216. package/dist/web/index.js +6 -0
  217. package/dist/web/md-marker.d.ts +6 -0
  218. package/dist/web/md-marker.js +39 -0
  219. package/dist/web/md-tex.d.ts +6 -0
  220. package/dist/web/md-tex.js +61 -0
  221. package/dist/web/raf.d.ts +6 -0
  222. package/dist/web/raf.js +24 -0
  223. package/dist/web/viewport.d.ts +7 -0
  224. package/dist/web/viewport.js +13 -0
  225. package/package.json +87 -0
@@ -0,0 +1,198 @@
1
+ // WAAPI / scroll / viewport bridges. Scroll signals are lazy via the
2
+ // signal's `watched`/`unwatched` hooks; one shared capture-phase
3
+ // listener serves them all. Range names mirror CSS `view-timeline`.
4
+ import { suspend } from "../animation/index.js";
5
+ import { cell } from "../core/index.js";
6
+ /** WAAPI animation as a bireactive Animator. Bare-number `opts` is seconds;
7
+ * object `opts` passes through to `Element.animate` (ms). */
8
+ export function* native(el, keyframes, opts = {}) {
9
+ const native = typeof opts === "number" ? { duration: opts * 1000 } : opts;
10
+ const a = el.animate(keyframes, native);
11
+ try {
12
+ yield* untilAnimation(a);
13
+ try {
14
+ a.commitStyles();
15
+ }
16
+ catch {
17
+ /* disconnected or non-committable */
18
+ }
19
+ }
20
+ finally {
21
+ a.cancel();
22
+ }
23
+ }
24
+ /** Wake on the animation's `finish` event; resume with the event. */
25
+ export function untilAnimation(a) {
26
+ return suspend(wake => {
27
+ const handler = (e) => wake(e);
28
+ a.addEventListener("finish", handler, { once: true });
29
+ return () => a.removeEventListener("finish", handler);
30
+ });
31
+ }
32
+ /** Wake when `el` enters the viewport. Wakes immediately if already in. */
33
+ export function untilInView(el, opts) {
34
+ return suspend(wake => {
35
+ let woke = false;
36
+ const obs = new IntersectionObserver(entries => {
37
+ if (woke)
38
+ return;
39
+ if (entries.some(e => e.isIntersecting)) {
40
+ woke = true;
41
+ wake();
42
+ }
43
+ }, opts);
44
+ obs.observe(el);
45
+ return () => obs.disconnect();
46
+ });
47
+ }
48
+ /** Wake when `el` leaves the viewport. Wakes immediately if already out. */
49
+ export function untilOutOfView(el, opts) {
50
+ return suspend(wake => {
51
+ let woke = false;
52
+ const obs = new IntersectionObserver(entries => {
53
+ if (woke)
54
+ return;
55
+ if (entries.some(e => !e.isIntersecting)) {
56
+ woke = true;
57
+ wake();
58
+ }
59
+ }, opts);
60
+ obs.observe(el);
61
+ return () => obs.disconnect();
62
+ });
63
+ }
64
+ // Shared scroll plumbing: capture-phase picks up nested overflow
65
+ // containers (scroll doesn't bubble). `pageTotal` is cached and
66
+ // only refreshed on resize.
67
+ const subscribers = new Set();
68
+ let rafId = 0;
69
+ let attached = false;
70
+ let pageTotal = 0;
71
+ function refreshPageTotal() {
72
+ pageTotal = document.documentElement.scrollHeight - window.innerHeight;
73
+ }
74
+ function tick() {
75
+ rafId = 0;
76
+ // Snapshot: a callback may dispose another scroll signal mid-iter.
77
+ const snapshot = [...subscribers];
78
+ for (let i = 0; i < snapshot.length; i++)
79
+ snapshot[i]();
80
+ }
81
+ function schedule() {
82
+ if (rafId === 0)
83
+ rafId = requestAnimationFrame(tick);
84
+ }
85
+ function onResize() {
86
+ refreshPageTotal();
87
+ schedule();
88
+ }
89
+ function attach() {
90
+ if (attached)
91
+ return;
92
+ attached = true;
93
+ refreshPageTotal();
94
+ window.addEventListener("scroll", schedule, { passive: true, capture: true });
95
+ window.addEventListener("resize", onResize, { passive: true });
96
+ }
97
+ function detach() {
98
+ if (!attached)
99
+ return;
100
+ attached = false;
101
+ window.removeEventListener("scroll", schedule, { capture: true });
102
+ window.removeEventListener("resize", onResize);
103
+ if (rafId !== 0) {
104
+ cancelAnimationFrame(rafId);
105
+ rafId = 0;
106
+ }
107
+ }
108
+ function watchTick(cb) {
109
+ subscribers.add(cb);
110
+ if (subscribers.size === 1)
111
+ attach();
112
+ }
113
+ function unwatchTick(cb) {
114
+ subscribers.delete(cb);
115
+ if (subscribers.size === 0)
116
+ detach();
117
+ }
118
+ function scrollSignal(read, initial) {
119
+ let pull;
120
+ const sig = cell(initial, {
121
+ watched() {
122
+ pull = () => {
123
+ sig.value = read();
124
+ };
125
+ watchTick(pull);
126
+ pull();
127
+ },
128
+ unwatched() {
129
+ if (pull)
130
+ unwatchTick(pull);
131
+ pull = undefined;
132
+ },
133
+ });
134
+ return sig;
135
+ }
136
+ /** Global page scroll progress in `[0, 1]`; `0` if page doesn't scroll. */
137
+ export function scrollProgress() {
138
+ return scrollSignal(() => (pageTotal > 0 ? clamp01(window.scrollY / pageTotal) : 0), 0);
139
+ }
140
+ function clamp01(x) {
141
+ return x < 0 ? 0 : x > 1 ? 1 : x;
142
+ }
143
+ function rangeProgress(rect, vp, range) {
144
+ const top = rect.top;
145
+ const h = rect.height;
146
+ switch (range) {
147
+ case "cover": {
148
+ const total = vp + h;
149
+ return total > 0 ? clamp01((vp - top) / total) : 0;
150
+ }
151
+ case "entry": {
152
+ return h > 0 ? clamp01((vp - top) / h) : 1;
153
+ }
154
+ case "contain": {
155
+ // Taller than vp: range doesn't exist — pin at 0.5 for stability.
156
+ const total = vp - h;
157
+ if (total <= 0)
158
+ return 0.5;
159
+ return clamp01((vp - h - top) / total);
160
+ }
161
+ case "exit": {
162
+ return h > 0 ? clamp01(-top / h) : 1;
163
+ }
164
+ }
165
+ }
166
+ // Memoize `viewProgress` by (el, range) so N readers share one layout
167
+ // read per tick. WeakMap GCs when el is dropped.
168
+ const viewCache = new WeakMap();
169
+ /** Element view-progress in `[0, 1]` over `range` (default `cover`). */
170
+ export function viewProgress(el, range = "cover") {
171
+ let entry = viewCache.get(el);
172
+ if (!entry)
173
+ viewCache.set(el, (entry = {}));
174
+ return (entry[range] ??= scrollSignal(() => rangeProgress(el.getBoundingClientRect(), window.innerHeight, range), 0));
175
+ }
176
+ function elInViewport(el) {
177
+ const r = el.getBoundingClientRect();
178
+ return r.bottom > 0 && r.top < window.innerHeight && r.right > 0 && r.left < window.innerWidth;
179
+ }
180
+ /** Reactive boolean; `true` while `el` intersects the viewport. Seeded
181
+ * synchronously from rect, then maintained by IntersectionObserver. */
182
+ export function inView(el, opts) {
183
+ let observer;
184
+ const sig = cell(false, {
185
+ watched() {
186
+ sig.value = elInViewport(el);
187
+ observer = new IntersectionObserver(entries => {
188
+ sig.value = entries.some(e => e.isIntersecting);
189
+ }, opts);
190
+ observer.observe(el);
191
+ },
192
+ unwatched() {
193
+ observer?.disconnect();
194
+ observer = undefined;
195
+ },
196
+ });
197
+ return sig;
198
+ }
@@ -0,0 +1,8 @@
1
+ export * from "./animation/index.js";
2
+ export * from "./assert/index.js";
3
+ export { type CodeOpts, CodeShape, code, codeStyles, type Token, tokenize } from "./code/index.js";
4
+ export * from "./core/index.js";
5
+ export * from "./ext/index.js";
6
+ export * from "./shapes/index.js";
7
+ export * from "./tex/index.js";
8
+ export * from "./web/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export * from "./animation/index.js";
2
+ export * from "./assert/index.js";
3
+ // `code` and `tex` both export `Part`; re-export `code`'s other symbols
4
+ // explicitly so the wildcard below lets `tex`'s `Part` win.
5
+ export { CodeShape, code, codeStyles, tokenize } from "./code/index.js";
6
+ export * from "./core/index.js";
7
+ export * from "./ext/index.js";
8
+ export * from "./shapes/index.js";
9
+ export * from "./tex/index.js";
10
+ export * from "./web/index.js";
@@ -0,0 +1,6 @@
1
+ export { Box, box } from "../core/values/box.js";
2
+ export { attach, centerInside, follow, type GridOpts, grid, hstack, inset, lockSize, pinEdge, type Side, type StackItem, type StackOpts, vstack, } from "./layout.js";
3
+ export { PropagatorDivergedError, Propagators, type PropagatorsOpts, propagate, propagators, } from "./network.js";
4
+ export { type Propagator, propagator } from "./propagator.js";
5
+ export { constrain, intervalAdd, intervalAdder, intervalEq, intervalSub, intervalSum, RANGE_TOP, type Range, type RangeCell, RangeContradiction, rangeCell, rangeEq, rangeIsContradiction, rangeIsExact, rangeMeet, rangeMerge, rangeWidth, snap, } from "./range.js";
6
+ export { add, align, allDifferent, aspectRatio, between, centroid, constant, eq, keepDistance, mid, mul, onCircle, onLine, reflect, type SetCell, sub, sum, } from "./relations.js";
@@ -0,0 +1,6 @@
1
+ export { Box, box } from "../core/values/box.js";
2
+ export { attach, centerInside, follow, grid, hstack, inset, lockSize, pinEdge, vstack, } from "./layout.js";
3
+ export { PropagatorDivergedError, Propagators, propagate, propagators, } from "./network.js";
4
+ export { propagator } from "./propagator.js";
5
+ export { constrain, intervalAdd, intervalAdder, intervalEq, intervalSub, intervalSum, RANGE_TOP, RangeContradiction, rangeCell, rangeEq, rangeIsContradiction, rangeIsExact, rangeMeet, rangeMerge, rangeWidth, snap, } from "./range.js";
6
+ export { add, align, allDifferent, aspectRatio, between, centroid, constant, eq, keepDistance, mid, mul, onCircle, onLine, reflect, sub, sum, } from "./relations.js";
@@ -0,0 +1,68 @@
1
+ import { type Box, type Read } from "../core/index.js";
2
+ import { type Propagator } from "./propagator.js";
3
+ type ValOrSig = number | Read<number>;
4
+ /** Item in an `hstack` / `vstack`. Bare `Box` uses defaults (grow 1,
5
+ * shrink 1, no min/max); tag for per-item flex. Per-item opts are
6
+ * plain numbers (not reactive). */
7
+ export type StackItem = Box | {
8
+ box: Box;
9
+ grow?: number;
10
+ shrink?: number;
11
+ min?: number;
12
+ max?: number;
13
+ };
14
+ export interface StackOpts {
15
+ /** Space between adjacent items. Default 0. */
16
+ gap?: ValOrSig;
17
+ /** Padding inside container on each side. Default 0. */
18
+ padding?: ValOrSig;
19
+ /** Cross-axis alignment. Default "start". */
20
+ align?: "start" | "center" | "end" | "stretch";
21
+ /** "fit" (default): items grow/shrink to fill container.
22
+ * "hug": items keep their authored size; container resizes. */
23
+ mode?: "fit" | "hug";
24
+ }
25
+ /** Horizontal CSS-flex stack: per-item grow/shrink/min/max along the
26
+ * main axis, cross-axis handled by `align`. */
27
+ export declare function hstack(c: Box, items: readonly StackItem[], opts?: StackOpts): Propagator;
28
+ /** Vertical stack — top-to-bottom version of `hstack`. */
29
+ export declare function vstack(c: Box, items: readonly StackItem[], opts?: StackOpts): Propagator;
30
+ export interface GridOpts {
31
+ /** Cells per row. */
32
+ cols: number;
33
+ /** Gap between cells (both axes). Use `gapX` / `gapY` to differ. */
34
+ gap?: ValOrSig;
35
+ gapX?: ValOrSig;
36
+ gapY?: ValOrSig;
37
+ padding?: ValOrSig;
38
+ }
39
+ /** Regular grid: items placed in a `cols`-wide grid. Cells equal-size,
40
+ * computed from container minus padding and gaps. */
41
+ export declare function grid(c: Box, items: readonly Box[], opts: GridOpts): Propagator;
42
+ /** `inner` fills `outer` minus padding on all sides. Drag outer →
43
+ * inner follows. Default padding is 0 (inner == outer). */
44
+ export declare function inset(outer: Box, inner: Box, opts?: {
45
+ padding?: ValOrSig;
46
+ }): Propagator;
47
+ export type Side = "left" | "right" | "top" | "bottom";
48
+ /** Anchor `b`'s `bSide` to `a`'s `aSide` with optional gap.
49
+ *
50
+ * attach(panel, sidebar, "right", "left", { gap: 8 })
51
+ * // sidebar.left = panel.right + 8
52
+ *
53
+ * Bidirectional: drag a → b follows; drag b → a follows. */
54
+ export declare function attach(a: Box, b: Box, aSide: Side, bSide: Side, opts?: {
55
+ gap?: ValOrSig;
56
+ }): Propagator[];
57
+ /** Center `inner` inside `outer`. `inner.w/h` are preserved.
58
+ * Bidirectional: drag outer → inner re-centers; drag inner → outer
59
+ * shifts to keep inner centered. */
60
+ export declare function centerInside(outer: Box, inner: Box): Propagator[];
61
+ /** Pin one edge of a box to a fixed coordinate. The OPPOSITE edge
62
+ * stays put; size adjusts. */
63
+ export declare function pinEdge(b: Box, side: Side, target: ValOrSig): Propagator;
64
+ /** Lock a box's width or height to a fixed value (or signal). */
65
+ export declare function lockSize(b: Box, axis: "w" | "h", target: ValOrSig): Propagator;
66
+ /** One-way mirror: `follower` tracks `leader` exactly. */
67
+ export declare function follow(leader: Box, follower: Box): Propagator;
68
+ export {};
@@ -0,0 +1,336 @@
1
+ // layout.ts — Box-relational layout combinators.
2
+ //
3
+ // Every combinator operates on `Box` value-types. Reactive opts
4
+ // (`gap`, `padding`, …) accept a number or a Num signal.
5
+ //
6
+ // const c = box(0, 0, 300, 200);
7
+ // const items = [box(), box(), box()];
8
+ // p.add(hstack(c, items, { gap: 8, align: "stretch" }));
9
+ //
10
+ // `hstack` / `vstack` are CSS-flex-shaped (per-item grow/shrink vs
11
+ // min/max). For rigid edge-to-edge layouts use `attach`,
12
+ // `centerInside`, etc.
13
+ import { isCell, readNow } from "../core/index.js";
14
+ import { propagator } from "./propagator.js";
15
+ const asW = (n) => n;
16
+ function readDeps(...vs) {
17
+ return vs.filter(isCell);
18
+ }
19
+ function clamp(v, lo, hi) {
20
+ return Math.min(Math.max(v, lo), hi);
21
+ }
22
+ function specs(items) {
23
+ return items.map(it => "box" in it
24
+ ? {
25
+ box: it.box,
26
+ grow: it.grow ?? 1,
27
+ shrink: it.shrink ?? 1,
28
+ min: it.min ?? 0,
29
+ max: it.max ?? Number.POSITIVE_INFINITY,
30
+ }
31
+ : { box: it, grow: 1, shrink: 1, min: 0, max: Number.POSITIVE_INFINITY });
32
+ }
33
+ /** Horizontal CSS-flex stack: per-item grow/shrink/min/max along the
34
+ * main axis, cross-axis handled by `align`. */
35
+ export function hstack(c, items, opts = {}) {
36
+ return _stack(c, items, opts, "horizontal");
37
+ }
38
+ /** Vertical stack — top-to-bottom version of `hstack`. */
39
+ export function vstack(c, items, opts = {}) {
40
+ return _stack(c, items, opts, "vertical");
41
+ }
42
+ function _stack(c, rawItems, opts, dir) {
43
+ const items = specs(rawItems);
44
+ const horizontal = dir === "horizontal";
45
+ const mainPos = (b) => asW(horizontal ? b.x : b.y);
46
+ const mainSize = (b) => asW(horizontal ? b.w : b.h);
47
+ const crossPos = (b) => asW(horizontal ? b.y : b.x);
48
+ const crossSize = (b) => asW(horizontal ? b.h : b.w);
49
+ const mode = opts.mode ?? "fit";
50
+ const alignKind = opts.align ?? "start";
51
+ const reads = [
52
+ mainPos(c),
53
+ mainSize(c),
54
+ crossPos(c),
55
+ crossSize(c),
56
+ ...items.map(it => mainSize(it.box)),
57
+ ...items.map(it => crossSize(it.box)),
58
+ ...readDeps(opts.gap ?? 0, opts.padding ?? 0),
59
+ ];
60
+ const writes = [];
61
+ for (const it of items) {
62
+ writes.push(asW(mainPos(it.box)));
63
+ if (mode === "fit")
64
+ writes.push(asW(mainSize(it.box)));
65
+ writes.push(asW(crossPos(it.box)));
66
+ if (alignKind === "stretch")
67
+ writes.push(asW(crossSize(it.box)));
68
+ }
69
+ return propagator(reads, writes, () => {
70
+ const gap = readNow(opts.gap ?? 0);
71
+ const pad = readNow(opts.padding ?? 0);
72
+ const n = items.length;
73
+ let sizes;
74
+ if (mode === "fit") {
75
+ // Start each item at its hypothetical size (current main-size,
76
+ // clamped to [min, max]).
77
+ sizes = new Array(n);
78
+ let sumW = 0;
79
+ for (let i = 0; i < n; i++) {
80
+ const it = items[i];
81
+ const w0 = clamp(mainSize(it.box).value, it.min, it.max);
82
+ sizes[i] = w0;
83
+ sumW += w0;
84
+ }
85
+ const slack = mainSize(c).value - 2 * pad - (n - 1) * gap - sumW;
86
+ if (slack > 1e-9) {
87
+ // Grow.
88
+ let remaining = slack;
89
+ const eligible = new Set();
90
+ for (let i = 0; i < n; i++) {
91
+ if (items[i].grow > 0 && sizes[i] < items[i].max)
92
+ eligible.add(i);
93
+ }
94
+ while (remaining > 1e-9 && eligible.size > 0) {
95
+ let weights = 0;
96
+ for (const i of eligible)
97
+ weights += items[i].grow;
98
+ if (weights === 0)
99
+ break;
100
+ let absorbed = 0;
101
+ for (const i of [...eligible]) {
102
+ const share = (remaining * items[i].grow) / weights;
103
+ const newW = Math.min(sizes[i] + share, items[i].max);
104
+ absorbed += newW - sizes[i];
105
+ sizes[i] = newW;
106
+ if (newW >= items[i].max)
107
+ eligible.delete(i);
108
+ }
109
+ if (absorbed < 1e-9)
110
+ break;
111
+ remaining -= absorbed;
112
+ }
113
+ }
114
+ else if (slack < -1e-9) {
115
+ // Shrink.
116
+ let remaining = -slack;
117
+ const eligible = new Set();
118
+ for (let i = 0; i < n; i++) {
119
+ if (items[i].shrink > 0 && sizes[i] > items[i].min)
120
+ eligible.add(i);
121
+ }
122
+ while (remaining > 1e-9 && eligible.size > 0) {
123
+ let weights = 0;
124
+ for (const i of eligible)
125
+ weights += items[i].shrink;
126
+ if (weights === 0)
127
+ break;
128
+ let absorbed = 0;
129
+ for (const i of [...eligible]) {
130
+ const share = (remaining * items[i].shrink) / weights;
131
+ const newW = Math.max(sizes[i] - share, items[i].min);
132
+ absorbed += sizes[i] - newW;
133
+ sizes[i] = newW;
134
+ if (newW <= items[i].min)
135
+ eligible.delete(i);
136
+ }
137
+ if (absorbed < 1e-9)
138
+ break;
139
+ remaining -= absorbed;
140
+ }
141
+ }
142
+ }
143
+ else {
144
+ // hug: item sizes drive container.
145
+ sizes = items.map(it => clamp(mainSize(it.box).value, it.min, it.max));
146
+ let total = 2 * pad + (n - 1) * gap;
147
+ for (const s of sizes)
148
+ total += s;
149
+ mainSize(c).value = total;
150
+ }
151
+ // Place items along main axis.
152
+ let cursor = mainPos(c).value + pad;
153
+ for (let i = 0; i < n; i++) {
154
+ mainPos(items[i].box).value = cursor;
155
+ if (mode === "fit")
156
+ mainSize(items[i].box).value = sizes[i];
157
+ cursor += sizes[i] + gap;
158
+ }
159
+ // Cross-axis alignment.
160
+ const cBase = crossPos(c).value + pad;
161
+ const cAvail = crossSize(c).value - 2 * pad;
162
+ for (const it of items) {
163
+ const itSize = crossSize(it.box).value;
164
+ switch (alignKind) {
165
+ case "start":
166
+ crossPos(it.box).value = cBase;
167
+ break;
168
+ case "center":
169
+ crossPos(it.box).value = cBase + (cAvail - itSize) / 2;
170
+ break;
171
+ case "end":
172
+ crossPos(it.box).value = cBase + cAvail - itSize;
173
+ break;
174
+ case "stretch":
175
+ crossPos(it.box).value = cBase;
176
+ crossSize(it.box).value = cAvail;
177
+ break;
178
+ }
179
+ }
180
+ });
181
+ }
182
+ /** Regular grid: items placed in a `cols`-wide grid. Cells equal-size,
183
+ * computed from container minus padding and gaps. */
184
+ export function grid(c, items, opts) {
185
+ const cols = opts.cols;
186
+ const reads = [
187
+ c.x,
188
+ c.y,
189
+ c.w,
190
+ c.h,
191
+ ...readDeps(opts.gap ?? 0, opts.gapX ?? 0, opts.gapY ?? 0, opts.padding ?? 0),
192
+ ];
193
+ const writes = [];
194
+ for (const it of items)
195
+ writes.push(asW(it.x), asW(it.y), asW(it.w), asW(it.h));
196
+ return propagator(reads, writes, () => {
197
+ const pad = readNow(opts.padding ?? 0);
198
+ const gap = readNow(opts.gap ?? 0);
199
+ const gx = readNow(opts.gapX ?? gap);
200
+ const gy = readNow(opts.gapY ?? gap);
201
+ const rows = Math.ceil(items.length / cols);
202
+ const cellW = (c.w.value - 2 * pad - (cols - 1) * gx) / cols;
203
+ const cellH = (c.h.value - 2 * pad - (rows - 1) * gy) / rows;
204
+ for (let i = 0; i < items.length; i++) {
205
+ const row = Math.floor(i / cols);
206
+ const col = i % cols;
207
+ const it = items[i];
208
+ asW(it.x).value = c.x.value + pad + col * (cellW + gx);
209
+ asW(it.y).value = c.y.value + pad + row * (cellH + gy);
210
+ asW(it.w).value = cellW;
211
+ asW(it.h).value = cellH;
212
+ }
213
+ });
214
+ }
215
+ /** `inner` fills `outer` minus padding on all sides. Drag outer →
216
+ * inner follows. Default padding is 0 (inner == outer). */
217
+ export function inset(outer, inner, opts = {}) {
218
+ const reads = [outer.x, outer.y, outer.w, outer.h, ...readDeps(opts.padding ?? 0)];
219
+ const writes = [asW(inner.x), asW(inner.y), asW(inner.w), asW(inner.h)];
220
+ return propagator(reads, writes, () => {
221
+ const pad = readNow(opts.padding ?? 0);
222
+ asW(inner.x).value = outer.x.value + pad;
223
+ asW(inner.y).value = outer.y.value + pad;
224
+ asW(inner.w).value = outer.w.value - 2 * pad;
225
+ asW(inner.h).value = outer.h.value - 2 * pad;
226
+ });
227
+ }
228
+ /** Anchor `b`'s `bSide` to `a`'s `aSide` with optional gap.
229
+ *
230
+ * attach(panel, sidebar, "right", "left", { gap: 8 })
231
+ * // sidebar.left = panel.right + 8
232
+ *
233
+ * Bidirectional: drag a → b follows; drag b → a follows. */
234
+ export function attach(a, b, aSide, bSide, opts = {}) {
235
+ const gapDeps = readDeps(opts.gap ?? 0);
236
+ const gap = () => readNow(opts.gap ?? 0);
237
+ const sideValue = (box, side) => {
238
+ switch (side) {
239
+ case "left":
240
+ return box.x.value;
241
+ case "right":
242
+ return box.x.value + box.w.value;
243
+ case "top":
244
+ return box.y.value;
245
+ case "bottom":
246
+ return box.y.value + box.h.value;
247
+ }
248
+ };
249
+ const writeSide = (box, side, v) => {
250
+ switch (side) {
251
+ case "left":
252
+ asW(box.x).value = v;
253
+ break;
254
+ case "right":
255
+ asW(box.x).value = v - box.w.value;
256
+ break;
257
+ case "top":
258
+ asW(box.y).value = v;
259
+ break;
260
+ case "bottom":
261
+ asW(box.y).value = v - box.h.value;
262
+ break;
263
+ }
264
+ };
265
+ return [
266
+ propagator([a.x, a.y, a.w, a.h, b.w, b.h, ...gapDeps], [asW(b.x), asW(b.y)], () => writeSide(b, bSide, sideValue(a, aSide) + gap())),
267
+ propagator([b.x, b.y, b.w, b.h, a.w, a.h, ...gapDeps], [asW(a.x), asW(a.y)], () => writeSide(a, aSide, sideValue(b, bSide) - gap())),
268
+ ];
269
+ }
270
+ /** Center `inner` inside `outer`. `inner.w/h` are preserved.
271
+ * Bidirectional: drag outer → inner re-centers; drag inner → outer
272
+ * shifts to keep inner centered. */
273
+ export function centerInside(outer, inner) {
274
+ return [
275
+ propagator([outer.x, outer.y, outer.w, outer.h, inner.w, inner.h], [asW(inner.x), asW(inner.y)], () => {
276
+ asW(inner.x).value = outer.x.value + (outer.w.value - inner.w.value) / 2;
277
+ asW(inner.y).value = outer.y.value + (outer.h.value - inner.h.value) / 2;
278
+ }),
279
+ propagator([inner.x, inner.y], [asW(outer.x), asW(outer.y)], () => {
280
+ const targetX = inner.x.value - (outer.w.value - inner.w.value) / 2;
281
+ const targetY = inner.y.value - (outer.h.value - inner.h.value) / 2;
282
+ if (Math.abs(targetX - outer.x.value) > 1e-9)
283
+ asW(outer.x).value = targetX;
284
+ if (Math.abs(targetY - outer.y.value) > 1e-9)
285
+ asW(outer.y).value = targetY;
286
+ }),
287
+ ];
288
+ }
289
+ /** Pin one edge of a box to a fixed coordinate. The OPPOSITE edge
290
+ * stays put; size adjusts. */
291
+ export function pinEdge(b, side, target) {
292
+ const targetDeps = readDeps(target);
293
+ const t = () => readNow(target);
294
+ return propagator([b.x, b.y, b.w, b.h, ...targetDeps], [asW(b.x), asW(b.y), asW(b.w), asW(b.h)], () => {
295
+ const tv = t();
296
+ switch (side) {
297
+ case "left": {
298
+ const right = b.x.value + b.w.value;
299
+ asW(b.x).value = tv;
300
+ asW(b.w).value = right - tv;
301
+ break;
302
+ }
303
+ case "right":
304
+ asW(b.w).value = tv - b.x.value;
305
+ break;
306
+ case "top": {
307
+ const bot = b.y.value + b.h.value;
308
+ asW(b.y).value = tv;
309
+ asW(b.h).value = bot - tv;
310
+ break;
311
+ }
312
+ case "bottom":
313
+ asW(b.h).value = tv - b.y.value;
314
+ break;
315
+ }
316
+ });
317
+ }
318
+ /** Lock a box's width or height to a fixed value (or signal). */
319
+ export function lockSize(b, axis, target) {
320
+ const deps = readDeps(target);
321
+ const cell = axis === "w" ? asW(b.w) : asW(b.h);
322
+ return propagator([cell, ...deps], [cell], () => {
323
+ const v = readNow(target);
324
+ if (cell.value !== v)
325
+ cell.value = v;
326
+ });
327
+ }
328
+ /** One-way mirror: `follower` tracks `leader` exactly. */
329
+ export function follow(leader, follower) {
330
+ return propagator([leader.x, leader.y, leader.w, leader.h], [asW(follower.x), asW(follower.y), asW(follower.w), asW(follower.h)], () => {
331
+ asW(follower.x).value = leader.x.value;
332
+ asW(follower.y).value = leader.y.value;
333
+ asW(follower.w).value = leader.w.value;
334
+ asW(follower.h).value = leader.h.value;
335
+ });
336
+ }