@wardic/dom 0.0.0-stage → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/THIRD_PARTY_NOTICES.md +145 -0
  4. package/dist/Appear.d.ts +24 -0
  5. package/dist/Appear.d.ts.map +1 -0
  6. package/dist/Appear.js +25 -0
  7. package/dist/Appear.js.map +1 -0
  8. package/dist/Color.d.ts +14 -0
  9. package/dist/Color.d.ts.map +1 -0
  10. package/dist/Color.js +16 -0
  11. package/dist/Color.js.map +1 -0
  12. package/dist/DomAnnouncer.d.ts +13 -0
  13. package/dist/DomAnnouncer.d.ts.map +1 -0
  14. package/dist/DomAnnouncer.js +14 -0
  15. package/dist/DomAnnouncer.js.map +1 -0
  16. package/dist/DomClock.d.ts +11 -0
  17. package/dist/DomClock.d.ts.map +1 -0
  18. package/dist/DomClock.js +12 -0
  19. package/dist/DomClock.js.map +1 -0
  20. package/dist/DomDefaultTransition.d.ts +12 -0
  21. package/dist/DomDefaultTransition.d.ts.map +1 -0
  22. package/dist/DomDefaultTransition.js +13 -0
  23. package/dist/DomDefaultTransition.js.map +1 -0
  24. package/dist/DomInput.d.ts +13 -0
  25. package/dist/DomInput.d.ts.map +1 -0
  26. package/dist/DomInput.js +15 -0
  27. package/dist/DomInput.js.map +1 -0
  28. package/dist/DomInterpolators.d.ts +13 -0
  29. package/dist/DomInterpolators.d.ts.map +1 -0
  30. package/dist/DomInterpolators.js +14 -0
  31. package/dist/DomInterpolators.js.map +1 -0
  32. package/dist/DomKeyRules.d.ts +13 -0
  33. package/dist/DomKeyRules.d.ts.map +1 -0
  34. package/dist/DomKeyRules.js +14 -0
  35. package/dist/DomKeyRules.js.map +1 -0
  36. package/dist/DomPositionalKeys.d.ts +13 -0
  37. package/dist/DomPositionalKeys.d.ts.map +1 -0
  38. package/dist/DomPositionalKeys.js +14 -0
  39. package/dist/DomPositionalKeys.js.map +1 -0
  40. package/dist/DomProbe.d.ts +13 -0
  41. package/dist/DomProbe.d.ts.map +1 -0
  42. package/dist/DomProbe.js +14 -0
  43. package/dist/DomProbe.js.map +1 -0
  44. package/dist/DomProjectionSystem.d.ts +28 -0
  45. package/dist/DomProjectionSystem.d.ts.map +1 -0
  46. package/dist/DomProjectionSystem.js +41 -0
  47. package/dist/DomProjectionSystem.js.map +1 -0
  48. package/dist/DomRenderer.d.ts +13 -0
  49. package/dist/DomRenderer.d.ts.map +1 -0
  50. package/dist/DomRenderer.js +15 -0
  51. package/dist/DomRenderer.js.map +1 -0
  52. package/dist/FlushGate.d.ts +48 -0
  53. package/dist/FlushGate.d.ts.map +1 -0
  54. package/dist/FlushGate.js +62 -0
  55. package/dist/FlushGate.js.map +1 -0
  56. package/dist/MeasuredPair.d.ts +14 -0
  57. package/dist/MeasuredPair.d.ts.map +1 -0
  58. package/dist/MeasuredPair.js +16 -0
  59. package/dist/MeasuredPair.js.map +1 -0
  60. package/dist/OwnValues.d.ts +14 -0
  61. package/dist/OwnValues.d.ts.map +1 -0
  62. package/dist/OwnValues.js +15 -0
  63. package/dist/OwnValues.js.map +1 -0
  64. package/dist/ProjectionSystem.d.ts +34 -0
  65. package/dist/ProjectionSystem.d.ts.map +1 -0
  66. package/dist/ProjectionSystem.js +29 -0
  67. package/dist/ProjectionSystem.js.map +1 -0
  68. package/dist/ReorderController.d.ts +41 -0
  69. package/dist/ReorderController.d.ts.map +1 -0
  70. package/dist/ReorderController.js +44 -0
  71. package/dist/ReorderController.js.map +1 -0
  72. package/dist/Report.d.ts +12 -0
  73. package/dist/Report.d.ts.map +1 -0
  74. package/dist/Report.js +13 -0
  75. package/dist/Report.js.map +1 -0
  76. package/dist/ScrollOffset.d.ts +34 -0
  77. package/dist/ScrollOffset.d.ts.map +1 -0
  78. package/dist/ScrollOffset.js +42 -0
  79. package/dist/ScrollOffset.js.map +1 -0
  80. package/dist/Style.d.ts +15 -0
  81. package/dist/Style.d.ts.map +1 -0
  82. package/dist/Style.js +17 -0
  83. package/dist/Style.js.map +1 -0
  84. package/dist/Svg.d.ts +13 -0
  85. package/dist/Svg.d.ts.map +1 -0
  86. package/dist/Svg.js +14 -0
  87. package/dist/Svg.js.map +1 -0
  88. package/dist/Variables.d.ts +13 -0
  89. package/dist/Variables.d.ts.map +1 -0
  90. package/dist/Variables.js +14 -0
  91. package/dist/Variables.js.map +1 -0
  92. package/dist/index.d.ts +72 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.js +60 -0
  95. package/dist/index.js.map +1 -0
  96. package/dist/internal/announcer.d.ts +50 -0
  97. package/dist/internal/announcer.d.ts.map +1 -0
  98. package/dist/internal/announcer.js +115 -0
  99. package/dist/internal/announcer.js.map +1 -0
  100. package/dist/internal/appear.d.ts +169 -0
  101. package/dist/internal/appear.d.ts.map +1 -0
  102. package/dist/internal/appear.js +499 -0
  103. package/dist/internal/appear.js.map +1 -0
  104. package/dist/internal/auto-scroll.d.ts +24 -0
  105. package/dist/internal/auto-scroll.d.ts.map +1 -0
  106. package/dist/internal/auto-scroll.js +124 -0
  107. package/dist/internal/auto-scroll.js.map +1 -0
  108. package/dist/internal/clock.d.ts +19 -0
  109. package/dist/internal/clock.d.ts.map +1 -0
  110. package/dist/internal/clock.js +39 -0
  111. package/dist/internal/clock.js.map +1 -0
  112. package/dist/internal/colors.d.ts +45 -0
  113. package/dist/internal/colors.d.ts.map +1 -0
  114. package/dist/internal/colors.js +193 -0
  115. package/dist/internal/colors.js.map +1 -0
  116. package/dist/internal/complex.d.ts +24 -0
  117. package/dist/internal/complex.d.ts.map +1 -0
  118. package/dist/internal/complex.js +28 -0
  119. package/dist/internal/complex.js.map +1 -0
  120. package/dist/internal/content-watch.d.ts +20 -0
  121. package/dist/internal/content-watch.d.ts.map +1 -0
  122. package/dist/internal/content-watch.js +76 -0
  123. package/dist/internal/content-watch.js.map +1 -0
  124. package/dist/internal/default-transition.d.ts +25 -0
  125. package/dist/internal/default-transition.d.ts.map +1 -0
  126. package/dist/internal/default-transition.js +41 -0
  127. package/dist/internal/default-transition.js.map +1 -0
  128. package/dist/internal/drag-defaults.d.ts +18 -0
  129. package/dist/internal/drag-defaults.d.ts.map +1 -0
  130. package/dist/internal/drag-defaults.js +38 -0
  131. package/dist/internal/drag-defaults.js.map +1 -0
  132. package/dist/internal/gate.d.ts +186 -0
  133. package/dist/internal/gate.d.ts.map +1 -0
  134. package/dist/internal/gate.js +453 -0
  135. package/dist/internal/gate.js.map +1 -0
  136. package/dist/internal/input.d.ts +41 -0
  137. package/dist/internal/input.d.ts.map +1 -0
  138. package/dist/internal/input.js +476 -0
  139. package/dist/internal/input.js.map +1 -0
  140. package/dist/internal/interpolators.d.ts +19 -0
  141. package/dist/internal/interpolators.d.ts.map +1 -0
  142. package/dist/internal/interpolators.js +30 -0
  143. package/dist/internal/interpolators.js.map +1 -0
  144. package/dist/internal/key-rules.d.ts +11 -0
  145. package/dist/internal/key-rules.d.ts.map +1 -0
  146. package/dist/internal/key-rules.js +244 -0
  147. package/dist/internal/key-rules.js.map +1 -0
  148. package/dist/internal/measure.d.ts +150 -0
  149. package/dist/internal/measure.d.ts.map +1 -0
  150. package/dist/internal/measure.js +278 -0
  151. package/dist/internal/measure.js.map +1 -0
  152. package/dist/internal/measured-pairs.d.ts +38 -0
  153. package/dist/internal/measured-pairs.d.ts.map +1 -0
  154. package/dist/internal/measured-pairs.js +251 -0
  155. package/dist/internal/measured-pairs.js.map +1 -0
  156. package/dist/internal/own-values.d.ts +35 -0
  157. package/dist/internal/own-values.d.ts.map +1 -0
  158. package/dist/internal/own-values.js +117 -0
  159. package/dist/internal/own-values.js.map +1 -0
  160. package/dist/internal/pass-ends.d.ts +21 -0
  161. package/dist/internal/pass-ends.d.ts.map +1 -0
  162. package/dist/internal/pass-ends.js +14 -0
  163. package/dist/internal/pass-ends.js.map +1 -0
  164. package/dist/internal/positional-keys.d.ts +16 -0
  165. package/dist/internal/positional-keys.d.ts.map +1 -0
  166. package/dist/internal/positional-keys.js +21 -0
  167. package/dist/internal/positional-keys.js.map +1 -0
  168. package/dist/internal/probe.d.ts +10 -0
  169. package/dist/internal/probe.d.ts.map +1 -0
  170. package/dist/internal/probe.js +117 -0
  171. package/dist/internal/probe.js.map +1 -0
  172. package/dist/internal/projection-defaults.d.ts +34 -0
  173. package/dist/internal/projection-defaults.d.ts.map +1 -0
  174. package/dist/internal/projection-defaults.js +21 -0
  175. package/dist/internal/projection-defaults.js.map +1 -0
  176. package/dist/internal/projection.d.ts +529 -0
  177. package/dist/internal/projection.d.ts.map +1 -0
  178. package/dist/internal/projection.js +4990 -0
  179. package/dist/internal/projection.js.map +1 -0
  180. package/dist/internal/renderer.d.ts +86 -0
  181. package/dist/internal/renderer.d.ts.map +1 -0
  182. package/dist/internal/renderer.js +616 -0
  183. package/dist/internal/renderer.js.map +1 -0
  184. package/dist/internal/reorder.d.ts +159 -0
  185. package/dist/internal/reorder.d.ts.map +1 -0
  186. package/dist/internal/reorder.js +437 -0
  187. package/dist/internal/reorder.js.map +1 -0
  188. package/dist/internal/report.d.ts +30 -0
  189. package/dist/internal/report.d.ts.map +1 -0
  190. package/dist/internal/report.js +60 -0
  191. package/dist/internal/report.js.map +1 -0
  192. package/dist/internal/require-global.d.ts +11 -0
  193. package/dist/internal/require-global.d.ts.map +1 -0
  194. package/dist/internal/require-global.js +31 -0
  195. package/dist/internal/require-global.js.map +1 -0
  196. package/dist/internal/reveal.d.ts +16 -0
  197. package/dist/internal/reveal.d.ts.map +1 -0
  198. package/dist/internal/reveal.js +66 -0
  199. package/dist/internal/reveal.js.map +1 -0
  200. package/dist/internal/runner.d.ts +21 -0
  201. package/dist/internal/runner.d.ts.map +1 -0
  202. package/dist/internal/runner.js +48 -0
  203. package/dist/internal/runner.js.map +1 -0
  204. package/dist/internal/scale-correction.d.ts +80 -0
  205. package/dist/internal/scale-correction.d.ts.map +1 -0
  206. package/dist/internal/scale-correction.js +227 -0
  207. package/dist/internal/scale-correction.js.map +1 -0
  208. package/dist/internal/scroll.d.ts +35 -0
  209. package/dist/internal/scroll.d.ts.map +1 -0
  210. package/dist/internal/scroll.js +244 -0
  211. package/dist/internal/scroll.js.map +1 -0
  212. package/dist/internal/scrollers.d.ts +35 -0
  213. package/dist/internal/scrollers.d.ts.map +1 -0
  214. package/dist/internal/scrollers.js +38 -0
  215. package/dist/internal/scrollers.js.map +1 -0
  216. package/dist/internal/starting-style.d.ts +141 -0
  217. package/dist/internal/starting-style.d.ts.map +1 -0
  218. package/dist/internal/starting-style.js +300 -0
  219. package/dist/internal/starting-style.js.map +1 -0
  220. package/dist/internal/svg.d.ts +60 -0
  221. package/dist/internal/svg.d.ts.map +1 -0
  222. package/dist/internal/svg.js +257 -0
  223. package/dist/internal/svg.js.map +1 -0
  224. package/dist/internal/variables.d.ts +45 -0
  225. package/dist/internal/variables.d.ts.map +1 -0
  226. package/dist/internal/variables.js +92 -0
  227. package/dist/internal/variables.js.map +1 -0
  228. package/package.json +65 -4
  229. package/src/Appear.ts +45 -0
  230. package/src/Color.ts +24 -0
  231. package/src/DomAnnouncer.ts +22 -0
  232. package/src/DomClock.ts +13 -0
  233. package/src/DomDefaultTransition.ts +16 -0
  234. package/src/DomInput.ts +22 -0
  235. package/src/DomInterpolators.ts +16 -0
  236. package/src/DomKeyRules.ts +17 -0
  237. package/src/DomPositionalKeys.ts +16 -0
  238. package/src/DomProbe.ts +15 -0
  239. package/src/DomProjectionSystem.ts +44 -0
  240. package/src/DomRenderer.ts +26 -0
  241. package/src/FlushGate.ts +81 -0
  242. package/src/MeasuredPair.ts +15 -0
  243. package/src/OwnValues.ts +19 -0
  244. package/src/ProjectionSystem.ts +51 -0
  245. package/src/ReorderController.ts +72 -0
  246. package/src/Report.ts +17 -0
  247. package/src/ScrollOffset.ts +54 -0
  248. package/src/Style.ts +37 -0
  249. package/src/Svg.ts +21 -0
  250. package/src/Variables.ts +17 -0
  251. package/src/index.ts +106 -0
  252. package/src/internal/announcer.ts +132 -0
  253. package/src/internal/appear.ts +739 -0
  254. package/src/internal/auto-scroll.ts +176 -0
  255. package/src/internal/clock.ts +45 -0
  256. package/src/internal/colors.ts +258 -0
  257. package/src/internal/complex.ts +29 -0
  258. package/src/internal/content-watch.ts +84 -0
  259. package/src/internal/culori.d.ts +58 -0
  260. package/src/internal/default-transition.ts +50 -0
  261. package/src/internal/drag-defaults.ts +54 -0
  262. package/src/internal/gate.ts +593 -0
  263. package/src/internal/input.ts +579 -0
  264. package/src/internal/interpolators.ts +33 -0
  265. package/src/internal/key-rules.ts +357 -0
  266. package/src/internal/measure.ts +357 -0
  267. package/src/internal/measured-pairs.ts +297 -0
  268. package/src/internal/own-values.ts +166 -0
  269. package/src/internal/pass-ends.ts +23 -0
  270. package/src/internal/positional-keys.ts +24 -0
  271. package/src/internal/probe.ts +176 -0
  272. package/src/internal/projection-defaults.ts +45 -0
  273. package/src/internal/projection.ts +5999 -0
  274. package/src/internal/renderer.ts +646 -0
  275. package/src/internal/reorder.ts +604 -0
  276. package/src/internal/report.ts +69 -0
  277. package/src/internal/require-global.ts +46 -0
  278. package/src/internal/reveal.ts +69 -0
  279. package/src/internal/runner.ts +64 -0
  280. package/src/internal/scale-correction.ts +310 -0
  281. package/src/internal/scroll.ts +356 -0
  282. package/src/internal/scrollers.ts +51 -0
  283. package/src/internal/starting-style.ts +396 -0
  284. package/src/internal/svg.ts +300 -0
  285. package/src/internal/variables.ts +145 -0
@@ -0,0 +1,357 @@
1
+ /**
2
+ * The DOM reads the Projection system takes at measure time (ADR-0005 / ADR-0009 /
3
+ * ADR-0012). Two box readers: {@link readLayoutBox} is the raw rect the projection
4
+ * pass uses (it owns the transform reset at pass level), {@link measureLayoutBox}
5
+ * a self-resetting single-element read for a lone caller. No framework or Effect dependency — pure DOM — so
6
+ * this lives in `@wardic/dom` alongside the Projection system that consumes it (a
7
+ * binding imports it from here).
8
+ *
9
+ * It's the surface of the port the engine would move into `@wardic/core` behind (#16, parked
10
+ * until a non-DOM Environment exists): a DOM read the engine newly needs is added here, named,
11
+ * rather than written inline in `projection.ts`, measure-time or not: the tree's `contains` too
12
+ * (#618). #16 maps the reads that file made inline before this rule.
13
+ */
14
+ import * as Drag from "@wardic/core/Drag"
15
+ import type * as Point from "@wardic/core/Point"
16
+ import type * as Probe from "@wardic/core/Probe"
17
+ import * as Arr from "effect/Array"
18
+ import * as Bool from "effect/Boolean"
19
+ import { constVoid } from "effect/Function"
20
+ import * as Option from "effect/Option"
21
+ import * as Predicate from "effect/Predicate"
22
+ import * as Str from "effect/String"
23
+ import { pending as appearRuns } from "./appear.ts"
24
+
25
+ /**
26
+ * Read `el`'s box AS IT CURRENTLY RENDERS — a raw `getBoundingClientRect`, no
27
+ * transform reset. The projection measure pass uses this because it resets the
28
+ * whole tree's transforms in its own WRITE phase first, so by the time it reads,
29
+ * nothing is transformed (see `runMeasurePass`). A lone caller wants
30
+ * {@link measureLayoutBox} instead.
31
+ */
32
+ export const readLayoutBox = (el: HTMLElement): Probe.Bounds => {
33
+ const r = el.getBoundingClientRect()
34
+ return {
35
+ x: r.x,
36
+ y: r.y,
37
+ width: r.width,
38
+ height: r.height,
39
+ top: r.top,
40
+ right: r.right,
41
+ bottom: r.bottom,
42
+ left: r.left,
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Measures `el`'s box where the layout puts it: its own `transform` is cleared for the read and put
48
+ * back, so an animation running on it doesn't move the reading. Its ancestors' transforms still
49
+ * count. The box is in the viewport: `x`, `y`, `width`, `height`, and its edges.
50
+ *
51
+ * @example
52
+ * const box = measureLayoutBox(card)
53
+ * @see https://wardic.dev/reference/solid/layout/measure-layout-box
54
+ */
55
+ // The inline transform is blanked, the rect read, and the exact prior value restored in a `finally`, so
56
+ // a throwing getBoundingClientRect can't leave the element reset. For one element only: inside a tree
57
+ // pass an ancestor's transform pollutes the reading too, and the `finally` restores before the next
58
+ // element is read, which is why the projection pass hoists the reset out to pass level instead.
59
+ export const measureLayoutBox = (el: HTMLElement): Probe.Bounds => {
60
+ const saved = el.style.transform
61
+ el.style.transform = ""
62
+ // An Appear animation not yet handed off (#245): its CSS transform beats an inline one, not an important one.
63
+ if (appearRuns(el)) el.style.setProperty("transform", "none", "important")
64
+ try {
65
+ return readLayoutBox(el)
66
+ } finally {
67
+ el.style.transform = saved
68
+ }
69
+ }
70
+
71
+ /**
72
+ * The bounds on `element`'s x/y that keep it inside `container` (Motion's element `dragConstraints`):
73
+ * `element` measured without its own transform (its layout box, as Motion's projection measures it),
74
+ * `container` as it renders. With `mapPoint` (Motion's `transformPagePoint`), each box's corners are
75
+ * mapped through it first, as Motion's `measurePageBox` maps them: a container inside a zoomed parent
76
+ * bounds the element in its own pixels.
77
+ */
78
+ // #273, ADR-0047; #443.
79
+ export const dragBoundsWithin = (element: HTMLElement, container: Element, mapPoint?: (point: Point.Point) => Point.Point): Drag.Bounds =>
80
+ Drag.boundsWithin(mappedEdges(measureLayoutBox(element), mapPoint), mappedEdges(container.getBoundingClientRect(), mapPoint))
81
+
82
+ /** A box's edges, its top-left and bottom-right corners mapped through `mapPoint` when given (#443, Motion's `transformBoxPoints`). */
83
+ const mappedEdges = (box: Drag.Edges, mapPoint: ((point: Point.Point) => Point.Point) | undefined): Drag.Edges => {
84
+ if (Predicate.isUndefined(mapPoint)) return box
85
+ const topLeft = mapPoint({ x: box.left, y: box.top })
86
+ const bottomRight = mapPoint({ x: box.right, y: box.bottom })
87
+ return { left: topLeft.x, top: topLeft.y, right: bottomRight.x, bottom: bottomRight.y }
88
+ }
89
+
90
+ /** A box's size, in CSS px. */
91
+ export interface BoxSize {
92
+ readonly width: number
93
+ readonly height: number
94
+ }
95
+
96
+ /**
97
+ * A page box's size as it renders (#261, ADR-0018 *Amendment (#261)*): a raw `getBoundingClientRect`,
98
+ * so a transformed one reads as painted, which the observer's border box doesn't (its echo then reads
99
+ * as a change, and costs one refresh).
100
+ */
101
+ export const readBoxSize = (el: Element): BoxSize => {
102
+ const r = el.getBoundingClientRect()
103
+ return { width: r.width, height: r.height }
104
+ }
105
+
106
+ /** The positions that take a box out of its parent's flow, where its size moves nothing else. */
107
+ const OUT_OF_FLOW: ReadonlyArray<string> = ["absolute", "fixed"]
108
+
109
+ /** Whether `el` takes space in its parent's flow (#261): it isn't absolutely or fixed positioned. */
110
+ export const takesFlowSpace = (el: Element): boolean => Bool.not(Arr.contains(OUT_OF_FLOW, getComputedStyle(el).position))
111
+
112
+ /** An element's scroll offset (ADR-0012, Motion's `measureScroll`). */
113
+ export const measureScroll = (el: HTMLElement): Point.Point => ({ x: el.scrollLeft, y: el.scrollTop })
114
+
115
+ /**
116
+ * Whether `el` is a scroll ROOT (ADR-0012, Motion's `checkIsScrollRoot`): a
117
+ * `position: fixed` element establishes a viewport-relative frame, so its subtree
118
+ * doesn't fold in page (or outer-container) scroll.
119
+ */
120
+ export const isScrollRoot = (el: HTMLElement): boolean => getComputedStyle(el).position === "fixed"
121
+
122
+
123
+ /** The properties that move an element on screen without moving its layout (ADR-0012, *Amendment (#136)*). */
124
+ const TRANSFORM_PROPERTIES = ["transform", "translate", "scale", "rotate"] as const
125
+ type TransformProperty = (typeof TRANSFORM_PROPERTIES)[number]
126
+
127
+ /**
128
+ * Each property's identity that isn't `none`. An element held at it keeps the containing
129
+ * block and stacking context its transform gives it, so nothing inside lays out
130
+ * differently while it's held.
131
+ */
132
+ const IDENTITY: Readonly<Record<TransformProperty, string>> = {
133
+ transform: "translate(0px)",
134
+ translate: "0px",
135
+ scale: "1",
136
+ rotate: "0deg",
137
+ }
138
+
139
+ const isTransformProperty = (property: string): property is TransformProperty =>
140
+ Arr.contains(TRANSFORM_PROPERTIES as ReadonlyArray<string>, property)
141
+
142
+ /** A transition on a transform property: `all`, or one of them by name. */
143
+ const transitionsTransform = (property: string): boolean => Bool.or(property === "all", isTransformProperty(property))
144
+
145
+ /** A CSS time (`0.3s`, `200ms`) in seconds. */
146
+ const seconds = (time: string): number =>
147
+ Bool.match(Str.endsWith("ms")(time), { onTrue: () => Number.parseFloat(time) / 1000, onFalse: () => Number.parseFloat(time) })
148
+
149
+ /** The list a computed `transition-*` value holds, one entry per transition. */
150
+ const entries = (value: string): Array<string> => Arr.map(value.split(","), Str.trim)
151
+
152
+ /**
153
+ * Whether `style` declares a transition that would run on one of the transform
154
+ * properties: its name or `all`, with a combined duration (`max(duration, 0) + delay`)
155
+ * above 0. The durations and delays repeat to the properties' length, as CSS does.
156
+ */
157
+ const declaresTransformTransition = (style: CSSStyleDeclaration): boolean => {
158
+ const durations = entries(style.transitionDuration)
159
+ const delays = entries(style.transitionDelay)
160
+ return Arr.some(entries(style.transitionProperty), (property, i) =>
161
+ Bool.and(
162
+ transitionsTransform(property),
163
+ Math.max(seconds(durations[i % durations.length]), 0) + seconds(delays[i % delays.length]) > 0,
164
+ ))
165
+ }
166
+
167
+ /** A computed transform property that moves its element: set, and not `none` (`""` is unsupported, or no style). */
168
+ const isMoving = (value: string): boolean => Bool.and(Str.isNonEmpty(value), value !== "none")
169
+
170
+ /** What the Measure pass needs from an element's style to hold its transform. */
171
+ export interface TransformStyle {
172
+ /** The transform properties that aren't `none`. Empty: nothing moves it. */
173
+ readonly moved: ReadonlyArray<TransformProperty>
174
+ /** Whether a CSS transition is declared on one of them. */
175
+ readonly transitioned: boolean
176
+ }
177
+
178
+ /**
179
+ * Whether one of `style`'s transform properties moves its element. Read through the
180
+ * properties' accessors, which cost less than `getPropertyValue` (0.57 against 0.70 µs for
181
+ * the four in Chromium 147), and nothing is allocated.
182
+ */
183
+ const movesAny = (style: CSSStyleDeclaration): boolean =>
184
+ Bool.or(Bool.or(isMoving(style.transform), isMoving(style.translate)), Bool.or(isMoving(style.scale), isMoving(style.rotate)))
185
+
186
+ /**
187
+ * {@link TransformStyle} from an element's computed style. `getComputedStyle` returns a
188
+ * live declaration, so a caller that keeps one per element reads the current values each
189
+ * time without calling it again.
190
+ */
191
+ export const transformStyleFrom = (style: CSSStyleDeclaration): TransformStyle => {
192
+ // Most elements carry none: they cost the four reads, and nothing else.
193
+ if (Bool.not(movesAny(style))) return UNMOVED
194
+ return {
195
+ moved: Arr.filter(TRANSFORM_PROPERTIES, (property) => isMoving(style[property])),
196
+ transitioned: declaresTransformTransition(style),
197
+ }
198
+ }
199
+
200
+ /** The answer for an element no transform moves, shared. */
201
+ const UNMOVED: TransformStyle = { moved: [], transitioned: false }
202
+
203
+ /**
204
+ * Whether a CSS transition is running on one of `el`'s transform properties. Holding it
205
+ * would cancel or restart that transition, so the pass leaves it as it is: its offset
206
+ * stays in the box (the documented limit).
207
+ */
208
+ export const transitionRunning = (el: Element): boolean =>
209
+ Arr.some(el.getAnimations(), (animation) => Bool.and(animation instanceof CSSTransition, isTransformProperty((animation as CSSTransition).transitionProperty)))
210
+
211
+ /** An element the pass can hold: one with an inline style. */
212
+ export type Holdable = Element & ElementCSSInlineStyle
213
+
214
+ /** Whether `el` has an inline style the pass can hold its transform through. */
215
+ export const isHoldable = (el: Element): el is Holdable => Predicate.hasProperty(el, "style")
216
+
217
+ /** An element held at its transform's identity for a read, and what to put back. */
218
+ export interface TransformHold {
219
+ readonly element: Holdable
220
+ /** Each held property's own inline value and priority (`""` for none). */
221
+ readonly saved: ReadonlyArray<readonly [TransformProperty, string, string]>
222
+ /** The inline `transition-property` and priority, when the hold turned transitions off. */
223
+ readonly transition: Option.Option<readonly [string, string]>
224
+ }
225
+
226
+ /** Put an inline property back as it was: removed when it had no inline value. */
227
+ const restoreProperty = (inline: CSSStyleDeclaration, property: string, value: string, priority: string): void =>
228
+ Bool.match(Str.isEmpty(value), {
229
+ onTrue: () => {
230
+ inline.removeProperty(property)
231
+ },
232
+ onFalse: () => inline.setProperty(property, value, priority),
233
+ })
234
+
235
+ /**
236
+ * Hold `el`'s transform properties at their identity (ADR-0012, *Amendment (#136)*):
237
+ * inline and `!important`, so a stylesheet rule, a CSS animation and a Web Animation
238
+ * all give way. An element with a transition declared on one of them also gets
239
+ * `transition-property: none` for the hold, so it starts none.
240
+ */
241
+ export const holdTransform = (el: Holdable, style: TransformStyle): TransformHold => {
242
+ const inline = el.style
243
+ const saved = Arr.map(style.moved, (property) => [property, inline.getPropertyValue(property), inline.getPropertyPriority(property)] as const)
244
+ const transition = Bool.match(style.transitioned, {
245
+ onFalse: () => Option.none<readonly [string, string]>(),
246
+ onTrue: () => Option.some([inline.getPropertyValue("transition-property"), inline.getPropertyPriority("transition-property")] as const),
247
+ })
248
+ if (Option.isSome(transition)) inline.setProperty("transition-property", "none", "important")
249
+ Arr.forEach(style.moved, (property) => inline.setProperty(property, IDENTITY[property], "important"))
250
+ return { element: el, saved, transition }
251
+ }
252
+
253
+ /**
254
+ * Put back what {@link holdTransform} replaced. The elements whose transitions it turned
255
+ * off get their transform back first, then a style read, then their transition, so the
256
+ * return from identity starts no transition either.
257
+ */
258
+ export const releaseTransforms = (holds: ReadonlyArray<TransformHold>): void => {
259
+ Arr.forEach(holds, (hold) => Arr.forEach(hold.saved, ([property, value, priority]) => restoreProperty(hold.element.style, property, value, priority)))
260
+ const transitioned = Arr.filter(holds, (hold) => Option.isSome(hold.transition))
261
+ Arr.forEach(transitioned, (hold) => getComputedStyle(hold.element).getPropertyValue("transform"))
262
+ Arr.forEach(transitioned, (hold) =>
263
+ Option.match(hold.transition, {
264
+ onNone: constVoid,
265
+ onSome: ([value, priority]) => restoreProperty(hold.element.style, "transition-property", value, priority),
266
+ }))
267
+ }
268
+
269
+ /** The inline properties a sticky element is held unstuck through (#578). */
270
+ const UNSTUCK: ReadonlyArray<readonly [string, string]> = [
271
+ ["position", "relative"],
272
+ ["top", "auto"],
273
+ ["right", "auto"],
274
+ ["bottom", "auto"],
275
+ ["left", "auto"],
276
+ ]
277
+
278
+ /** A `position: sticky` element held at its static place for a read (#578), and what to put back. */
279
+ export interface StickyHold {
280
+ readonly element: Holdable
281
+ /** Each held property's own inline value and priority (`""` for none). */
282
+ readonly saved: ReadonlyArray<readonly [string, string, string]>
283
+ }
284
+
285
+ /**
286
+ * Hold a `position: sticky` element at its static place (ADR-0012, *Amendment (#578)*):
287
+ * `position: relative` with every inset `auto`, inline and `!important`, so it's offset by
288
+ * nothing and still positioned, the containing block of what's absolute inside it. Neither
289
+ * the flow around it nor the scroll moves: a sticky element's offset takes no space.
290
+ */
291
+ export const holdUnstuck = (el: Holdable): StickyHold => {
292
+ const inline = el.style
293
+ const saved = Arr.map(UNSTUCK, ([property]) => [property, inline.getPropertyValue(property), inline.getPropertyPriority(property)] as const)
294
+ Arr.forEach(UNSTUCK, ([property, value]) => inline.setProperty(property, value, "important"))
295
+ return { element: el, saved }
296
+ }
297
+
298
+ /** Put back what {@link holdUnstuck} replaced. */
299
+ export const releaseUnstuck = (holds: ReadonlyArray<StickyHold>): void =>
300
+ Arr.forEach(holds, (hold) => Arr.forEach(hold.saved, ([property, value, priority]) => restoreProperty(hold.element.style, property, value, priority)))
301
+
302
+ /**
303
+ * The document's scroll offset (the page/window scroll, ADR-0012, Motion's
304
+ * `DocumentProjectionNode` root). Folded into every box unless it sits in a scroll
305
+ * root. SSR-safe (returns origin when there is no document).
306
+ */
307
+ export const measureDocumentScroll = (): Point.Point => {
308
+ const page = (globalThis as { readonly document?: Document }).document
309
+ if (Predicate.isUndefined(page)) return { x: 0, y: 0 }
310
+ const doc = page.documentElement
311
+ // In quirks mode the body scrolls, and `documentElement` reads 0. Per pass: plain reads.
312
+ const body = page.body as HTMLElement | null
313
+ if (Predicate.isNull(body)) return { x: doc.scrollLeft, y: doc.scrollTop }
314
+ return { x: doc.scrollLeft || body.scrollLeft, y: doc.scrollTop || body.scrollTop }
315
+ }
316
+
317
+ export { readScrollStyle, scrollersOf, scrollStyleFrom, type ScrollStyle } from "./scrollers.ts"
318
+
319
+ /** Whether `inner` is `outer` or inside it, in the DOM's tree: the engine's one question of the tree (#618). */
320
+ export const contains = (outer: Node, inner: Node): boolean => outer.contains(inner)
321
+
322
+ /** `el`'s computed opacity, as a number: a Crossfade's fade from where its leaver is drawn (#618). */
323
+ export const readOpacity = (el: Element): number => Number(getComputedStyle(el).opacity)
324
+
325
+ /** A scale corrector's key and the CSS property it reads (#443). */
326
+ export interface CorrectedKey {
327
+ readonly key: string
328
+ readonly cssKey: string
329
+ }
330
+
331
+ /** The computed values of `keys` on `el` (#443), by key: an empty one, or `none`, left out. One style read. */
332
+ export const readCustomBases = (el: Element, keys: ReadonlyArray<CorrectedKey>): Readonly<Record<string, string>> => {
333
+ const style = getComputedStyle(el)
334
+ const out: Record<string, string> = {}
335
+ for (const corrected of keys) {
336
+ const value = style.getPropertyValue(corrected.cssKey).trim()
337
+ if (value !== "" && value !== "none") out[corrected.key] = value
338
+ }
339
+ return out
340
+ }
341
+
342
+ /**
343
+ * Each of `els`' offsets from its static place (#578): its box as it's drawn, less its box held
344
+ * unstuck. All held at once, so it costs one layout for them all, then one more after the release.
345
+ */
346
+ export const readStickyShifts = (els: Iterable<HTMLElement>): Map<Element, Point.Point> => {
347
+ const list = Arr.fromIterable(els)
348
+ const stuck = Arr.map(list, (el) => el.getBoundingClientRect())
349
+ const holds = Arr.map(list, holdUnstuck)
350
+ let unstuck: ReadonlyArray<DOMRect> = []
351
+ try {
352
+ unstuck = Arr.map(list, (el) => el.getBoundingClientRect())
353
+ } finally {
354
+ releaseUnstuck(holds)
355
+ }
356
+ return new Map(Arr.map(list, (el, i) => [el, { x: stuck[i].left - unstuck[i].left, y: stuck[i].top - unstuck[i].top }] as const))
357
+ }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * **Measured pairs** (#430, CONTEXT.md; Motion's `DOMKeyframesResolver`, `.reference/motion` at
3
+ * `16c6122`): a positional key whose start and target differ in type (`"auto"`, `"50%"` against a
4
+ * length in px, a `calc()` on one side) is measured in px at both ends, its leg runs between them,
5
+ * and it lands on its target as given.
6
+ *
7
+ * - **Which keys:** `width`, `height`, `top`, `left`, `right`, `bottom`, and the translates `x`, `y`,
8
+ * `translateX`, `translateY`: Motion's `positionalKeys` with a measurement (`positionalValues`).
9
+ * - **When** (#430's Q2): every pair a task asks for is measured in one batch, in a microtask at the
10
+ * end of that task, before its paint (ADR-0018's timing): each start read, each target written
11
+ * inline, the page laid out once, each target read, every style restored. Motion's
12
+ * `measureAllKeyframes`. Nothing paints the target, and a `ResizeObserver` never reports it.
13
+ * - **An end already in px** (a number, a px length) is taken as given: only `auto`, a percentage, a
14
+ * `calc()` or another unit is read from the layout.
15
+ * - **The reads:** a size from the computed style, which a transform doesn't change, else from the
16
+ * box (an element with none of its own, inline: Motion's `measureDimension`); an inset from the
17
+ * computed style, else, for `right` and `bottom`, Motion's sum from `top` or `left` and the box,
18
+ * read with the element's transform off; a translate from the computed transform's matrix.
19
+ *
20
+ * A binding asks with {@link measure} at a Retarget, which returns each pair as asked
21
+ * ({@link Requested}), and its leg reads each one {@link measured} at its first step, a tick after the
22
+ * batch: a new record with its px, none if the batch couldn't read it.
23
+ */
24
+ import * as Interpolator from "@wardic/core/Interpolator"
25
+ import type * as Values from "@wardic/core/Values"
26
+ import * as Arr from "effect/Array"
27
+ import * as Bool from "effect/Boolean"
28
+ import * as Option from "effect/Option"
29
+ import * as Predicate from "effect/Predicate"
30
+ import * as Rec from "effect/Record"
31
+ import * as UndefinedOr from "effect/UndefinedOr"
32
+ import { project } from "./renderer.ts"
33
+
34
+ /** The keys a Measured pair can be on (Motion's positional keys with a measurement). */
35
+ const SIZES: ReadonlySet<string> = new Set(["width", "height"])
36
+ const INSETS: ReadonlySet<string> = new Set(["top", "left", "right", "bottom"])
37
+ const TRANSLATES: ReadonlySet<string> = new Set(["x", "y", "translateX", "translateY"])
38
+
39
+ /** Every key a Measured pair can be on: one lookup per key, per Retarget (#460). */
40
+ const MEASURABLE: ReadonlySet<string> = new Set([...SIZES, ...INSETS, ...TRANSLATES])
41
+
42
+ /** Whether `key` can be a Measured pair. */
43
+ const isMeasurable = (key: string): boolean => MEASURABLE.has(key)
44
+
45
+ /**
46
+ * A value's type, as a Measured pair compares them: a number or a px length is `px`; `auto`; a
47
+ * `calc()` or `var()`; any other unit; `undefined` for what isn't a length. Per key, per Retarget: a
48
+ * number, nearly every translate's, makes nothing.
49
+ */
50
+ const kindOf = (value: number | string): string | undefined => {
51
+ if (Predicate.isNumber(value)) return "px"
52
+ const text = value.trim()
53
+ if (text === "auto") return "auto"
54
+ if (/^(calc|var|min|max|clamp)\(/.test(text)) return "calc"
55
+ return Option.getOrUndefined(Option.map(Interpolator.unitOf(text), (unit) => Bool.match(Bool.or(unit === "", unit === "px"), { onTrue: () => "px", onFalse: () => unit })))
56
+ }
57
+
58
+ /**
59
+ * Whether `key` from `from` to `to` needs measuring, a Measured pair: a key that can be measured, whose
60
+ * two ends are lengths, `auto` or a `calc()`, of different types. Two of one type blend as ever.
61
+ */
62
+ // #430; `needs`, not a type guard's `is…` name (#618, A3).
63
+ export const needs = (key: string, from: number | string, to: number | string): boolean => {
64
+ if (Bool.not(isMeasurable(key))) return false
65
+ const a = kindOf(from)
66
+ if (Predicate.isUndefined(a)) return false
67
+ const b = kindOf(to)
68
+ if (Predicate.isUndefined(b)) return false
69
+ return a !== b
70
+ }
71
+
72
+ /** One Measured pair as asked for: its key, and its ends as given. */
73
+ // #430; split from the measured one (#618, A1): the batch wrote its px into the asker's record.
74
+ export interface Requested {
75
+ /** The key. */
76
+ readonly key: string
77
+ /** Its start, as given. */
78
+ readonly from: number | string
79
+ /** Its target, as given. */
80
+ readonly to: number | string
81
+ }
82
+
83
+ /** One Measured pair its batch has read: as asked, with each end in px. */
84
+ export interface Measured extends Requested {
85
+ /** Its start in px. */
86
+ readonly fromPx: number
87
+ /** Its target in px. */
88
+ readonly toPx: number
89
+ }
90
+
91
+ /** A pair as its batch reads it: each end's px so far. */
92
+ interface Slot {
93
+ readonly pair: Requested
94
+ fromPx: number | undefined
95
+ toPx: number | undefined
96
+ }
97
+
98
+ /** An element's pairs in a batch, and the values it draws now, which a translate's transform is built from. */
99
+ interface Request {
100
+ readonly element: HTMLElement
101
+ readonly slots: ReadonlyArray<Slot>
102
+ readonly values: Values.Values
103
+ }
104
+
105
+ // Module state (#460, Q7): each pair its batch has read, by the pair as asked, kept as long as its asker
106
+ // keeps that pair.
107
+ const read = new WeakMap<Requested, Measured>()
108
+
109
+ /**
110
+ * `pair` with its px, once its end-of-task batch has read both its ends: none before that batch, and
111
+ * none if it couldn't read one, which the animation takes as a switch.
112
+ */
113
+ // #618 (A1).
114
+ export const measured = (pair: Requested): Option.Option<Measured> => Option.fromUndefinedOr(read.get(pair))
115
+
116
+ /** The pairs this task asked for, measured at its end. */
117
+ // Module state (#460, Q7): the page's one batch, so every provider's pairs share one read pass a task.
118
+ let queued: Array<Request> = []
119
+ // Module state (#460, Q7): the page's one end-of-task batch across providers.
120
+ let scheduled = false
121
+
122
+ /**
123
+ * Measures `pairs` on `element` at the end of this task, with every other element's in the same batch:
124
+ * each pair, as asked, {@link measured} before the next tick. `values` are what the element draws now,
125
+ * its other transforms among them.
126
+ */
127
+ // #430's Q2.
128
+ export const measure = (
129
+ element: HTMLElement,
130
+ pairs: Readonly<Record<string, { readonly from: number | string; readonly to: number | string }>>,
131
+ values: Values.Values,
132
+ ): Readonly<Record<string, Requested>> => {
133
+ const made = Rec.map(pairs, ({ from, to }, key): Requested => ({ key, from, to }))
134
+ queued.push({ element, slots: Arr.map(Rec.values(made), (pair): Slot => ({ pair, fromPx: undefined, toPx: undefined })), values })
135
+ if (Bool.not(scheduled)) {
136
+ scheduled = true
137
+ queueMicrotask(flush)
138
+ }
139
+ return made
140
+ }
141
+
142
+ /** A length's CSS text, as a style property takes it: a number in px. */
143
+ const cssOf = (value: number | string): string => Bool.match(Predicate.isNumber(value), { onTrue: () => `${value as number}px`, onFalse: () => value as string })
144
+
145
+ /**
146
+ * An end already in px (#430): a number, or a px length, taken as given, not read from the layout.
147
+ * So an explicit first stop (`[0, "auto"]`) starts at it, where Motion's `measureInitialState` reads
148
+ * the element over it, and a target in px is never written to be read.
149
+ */
150
+ const givenPx = (value: number | string): number | undefined => {
151
+ if (kindOf(value) !== "px") return undefined
152
+ if (Predicate.isNumber(value)) return value
153
+ return Option.getOrUndefined(pxOf(value))
154
+ }
155
+
156
+ /** A number read from CSS text, `None` when it isn't one (`auto`). */
157
+ const pxOf = (text: string): Option.Option<number> => Option.liftPredicate(Number.parseFloat(text), Number.isFinite)
158
+
159
+ /** A size read from the box, as Motion's `measureDimension`: the border box, less the padding for a content box. */
160
+ const sizeFromBox = (element: HTMLElement, style: CSSStyleDeclaration, key: string): number => {
161
+ const box = element.getBoundingClientRect()
162
+ const size = Bool.match(key === "width", { onTrue: () => box.width, onFalse: () => box.height })
163
+ if (style.boxSizing === "border-box") return size
164
+ const [start, end] = Bool.match(key === "width", {
165
+ onTrue: () => [style.paddingLeft, style.paddingRight] as const,
166
+ onFalse: () => [style.paddingTop, style.paddingBottom] as const,
167
+ })
168
+ return size - Option.getOrElse(pxOf(start), () => 0) - Option.getOrElse(pxOf(end), () => 0)
169
+ }
170
+
171
+ /** An inset Motion reads from the box (`right`, `bottom`): its opposite inset and the box's size, the element's transform off. */
172
+ const insetFromBox = (element: HTMLElement, style: CSSStyleDeclaration, key: string): number => {
173
+ const transform = element.style.transform
174
+ element.style.transform = "none"
175
+ const box = element.getBoundingClientRect()
176
+ element.style.transform = transform
177
+ const [opposite, size] = Bool.match(key === "right", { onTrue: () => [style.left, box.width] as const, onFalse: () => [style.top, box.height] as const })
178
+ return Option.getOrElse(pxOf(opposite), () => 0) + size
179
+ }
180
+
181
+ /** A translate read from the computed transform's matrix. */
182
+ const translateOf = (style: CSSStyleDeclaration, key: string): number => {
183
+ const matrix = new DOMMatrix(Bool.match(style.transform === "none", { onTrue: () => "matrix(1, 0, 0, 1, 0, 0)", onFalse: () => style.transform }))
184
+ return Bool.match(Bool.or(key === "x", key === "translateX"), { onTrue: () => matrix.m41, onFalse: () => matrix.m42 })
185
+ }
186
+
187
+ /** `key`'s value in px as `element` lays out now. */
188
+ const readPx = (element: HTMLElement, style: CSSStyleDeclaration, key: string): number => {
189
+ if (TRANSLATES.has(key)) return translateOf(style, key)
190
+ const read = pxOf(style.getPropertyValue(key))
191
+ if (Option.isSome(read)) return read.value
192
+ if (SIZES.has(key)) return sizeFromBox(element, style, key)
193
+ if (Bool.or(key === "right", key === "bottom")) return insetFromBox(element, style, key)
194
+ return 0
195
+ }
196
+
197
+ /** An inline property as it was before a batch wrote its target, to put back. */
198
+ interface Saved {
199
+ readonly element: HTMLElement
200
+ readonly property: string
201
+ readonly value: string
202
+ readonly priority: string
203
+ }
204
+
205
+ /** An inline property as it is now, to put back. */
206
+ const savedOf = (element: HTMLElement, property: string): Saved => ({ element, property, value: element.style.getPropertyValue(property), priority: element.style.getPropertyPriority(property) })
207
+
208
+ /**
209
+ * Write `pairs`' translates at their `end` through the element's whole transform, keeping what was
210
+ * there: the browser lays out any unit, `-100%` included (#594).
211
+ */
212
+ const writeTranslates = (request: Request, slots: ReadonlyArray<Slot>, end: "from" | "to"): ReadonlyArray<Saved> => {
213
+ if (slots.length === 0) return []
214
+ const { element, values } = request
215
+ const saved = savedOf(element, "transform")
216
+ const transform = project({ ...values, ...Rec.fromEntries(Arr.map(slots, ({ pair }) => [pair.key, pair[end]] as const)) })["transform"]
217
+ element.style.setProperty("transform", UndefinedOr.match(transform as string | undefined, { onUndefined: () => "none", onDefined: String }))
218
+ return [saved]
219
+ }
220
+
221
+ /** Write each pair's target inline on its element, keeping what was there; a translate through the element's whole transform. */
222
+ const writeTargets = (request: Request): ReadonlyArray<Saved> => {
223
+ const unread = Arr.filter(request.slots, (slot) => Predicate.isUndefined(slot.toPx))
224
+ const saved: Array<Saved> = [...writeTranslates(request, Arr.filter(unread, (slot) => TRANSLATES.has(slot.pair.key)), "to")]
225
+ Arr.forEach(unread, ({ pair }) => {
226
+ if (TRANSLATES.has(pair.key)) return
227
+ saved.push(savedOf(request.element, pair.key))
228
+ request.element.style.setProperty(pair.key, cssOf(pair.to))
229
+ })
230
+ return saved
231
+ }
232
+
233
+ /**
234
+ * Write each translate pair's start not in px, as a target is (#594): a layout element's transform at
235
+ * this moment is its projection's, or held off for its Measure pass, so reading it as it is gave 0.
236
+ */
237
+ const writeTranslateStarts = (request: Request): ReadonlyArray<Saved> =>
238
+ writeTranslates(request, Arr.filter(request.slots, (slot) => Bool.and(TRANSLATES.has(slot.pair.key), Predicate.isUndefined(slot.fromPx))), "from")
239
+
240
+ /** Put back what {@link writeTargets} replaced. */
241
+ const restore = (saved: ReadonlyArray<Saved>): void =>
242
+ Arr.forEach(saved, ({ element, property, value, priority }) =>
243
+ Bool.match(value === "", {
244
+ onTrue: () => void element.style.removeProperty(property),
245
+ onFalse: () => element.style.setProperty(property, value, priority),
246
+ }))
247
+
248
+ /**
249
+ * The batch (#430's Q2): every start read, every target written, every target read, every style
250
+ * restored, so the page is laid out once for all of them and paints none. A page scrolled by a target
251
+ * (a height growing above the view) is scrolled back, as Motion's `suspendedScrollY`.
252
+ */
253
+ const flush = (): void => {
254
+ const batch = queued
255
+ queued = []
256
+ scheduled = false
257
+ const styles = Arr.map(batch, ({ element }) => getComputedStyle(element))
258
+ // Each end in px as given.
259
+ Arr.forEach(batch, (request) => Arr.forEach(request.slots, (slot) => {
260
+ slot.toPx = givenPx(slot.pair.to)
261
+ slot.fromPx = givenPx(slot.pair.from)
262
+ }))
263
+ // WRITE, READ, WRITE: each translate's start not in px, written alone and read (#594).
264
+ const starts = Arr.flatMap(batch, writeTranslateStarts)
265
+ if (Arr.isArrayNonEmpty(starts)) {
266
+ try {
267
+ Arr.forEach(batch, (request, i) => Arr.forEach(request.slots, (slot) => {
268
+ if (Bool.and(TRANSLATES.has(slot.pair.key), Predicate.isUndefined(slot.fromPx))) slot.fromPx = readPx(request.element, UndefinedOr.getOrThrow(styles[i]), slot.pair.key)
269
+ }))
270
+ } finally {
271
+ restore(starts)
272
+ }
273
+ }
274
+ // READ: each other start not already in px, as the element lays out now.
275
+ Arr.forEach(batch, (request, i) => Arr.forEach(request.slots, (slot) => {
276
+ if (Predicate.isUndefined(slot.fromPx)) slot.fromPx = readPx(request.element, UndefinedOr.getOrThrow(styles[i]), slot.pair.key)
277
+ }))
278
+ const scrollY = globalThis.scrollY
279
+ // WRITE: each target, inline.
280
+ const saved = Arr.flatMap(batch, writeTargets)
281
+ try {
282
+ // READ: each target not already in px, laid out once.
283
+ Arr.forEach(batch, (request, i) => Arr.forEach(request.slots, (slot) => {
284
+ if (Predicate.isUndefined(slot.toPx)) slot.toPx = readPx(request.element, UndefinedOr.getOrThrow(styles[i]), slot.pair.key)
285
+ }))
286
+ } finally {
287
+ // WRITE: every style as it was.
288
+ restore(saved)
289
+ if (globalThis.scrollY !== scrollY) globalThis.scrollTo(globalThis.scrollX, scrollY)
290
+ }
291
+ // Each pair read at both ends, as a record of its own.
292
+ Arr.forEach(batch, (request) => Arr.forEach(request.slots, ({ pair, fromPx, toPx }) => {
293
+ if (Predicate.isUndefined(fromPx)) return
294
+ if (Predicate.isUndefined(toPx)) return
295
+ if (Bool.and(Number.isFinite(fromPx), Number.isFinite(toPx))) read.set(pair, { ...pair, fromPx, toPx })
296
+ }))
297
+ }