@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,176 @@
1
+ /**
2
+ * The DOM's **Auto-scroll** (ADR-0020, grill decision 16): Motion's
3
+ * (Reorder/utils/auto-scroll.ts at 16c61222c4166bbef5ff6ab0051192faa79fa8c8,
4
+ * https://github.com/motiondivision/motion/blob/16c61222c4166bbef5ff6ab0051192faa79fa8c8/packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts)
5
+ * over core's pure `AutoScroll.step`. While a **Reorder group**'s item is dragged near
6
+ * the visible edge of the group's scroller, moving toward it, each drag update scrolls
7
+ * the scroller; the scroll then moves the held pointer (a synthetic move), whose update
8
+ * scrolls again, so it keeps scrolling while the pointer holds still in the zone.
9
+ */
10
+ import * as AutoScroll from "@wardic/core/AutoScroll"
11
+ import type * as Point from "@wardic/core/Point"
12
+ import * as Arr from "effect/Array"
13
+ import * as Bool from "effect/Boolean"
14
+ import * as Equal from "effect/Equal"
15
+ import { constVoid } from "effect/Function"
16
+ import * as Match from "effect/Match"
17
+ import * as Option from "effect/Option"
18
+ import * as UndefinedOr from "effect/UndefinedOr"
19
+ import { heldScrollShift } from "./input.ts"
20
+
21
+ /** The overflow values Motion's auto-scroll scrolls (auto-scroll.ts:4). */
22
+ const SCROLLS: ReadonlySet<string> = new Set(["auto", "scroll"])
23
+
24
+ type Axis = "x" | "y"
25
+
26
+ /** A scroller on one axis: an element, or the document (the window's scroll). */
27
+ type Scroller = { readonly kind: "element"; readonly el: HTMLElement } | { readonly kind: "document"; readonly win: Window }
28
+
29
+ /** A scroller's extent on one axis: its visible edges in viewport px, its scroll, and how far it can scroll. */
30
+ interface View {
31
+ readonly start: number
32
+ readonly end: number
33
+ readonly scroll: number
34
+ readonly maxScroll: () => number
35
+ }
36
+
37
+ /**
38
+ * The **Auto-scroll** of one group. `update(pointer, axis, velocity, item?)` takes the
39
+ * pointer in viewport px, the axis to scroll and the pointer's velocity on it; given
40
+ * the dragged item's element, `pointer` may instead be its drag point, which carries
41
+ * the scroll since the hold began (ADR-0019 *Amendment (Reorder)*), and the scroller
42
+ * takes that back out. `reset()` ends the gesture: the next update finds the scroller
43
+ * afresh and needs velocity toward an edge again.
44
+ */
45
+ export interface AutoScroller {
46
+ readonly update: (pointer: Point.Point, axis: Axis, velocity: number, item?: Element) => void
47
+ readonly reset: () => void
48
+ }
49
+
50
+ const ZERO: Point.Point = { x: 0, y: 0 }
51
+
52
+ /**
53
+ * The nearest ancestor of `group` that scrolls on `axis` (`overflow` `auto | scroll`),
54
+ * or the document once the walk reaches `body` (Motion's `findScrollableAncestor`).
55
+ */
56
+ const findScroller = (group: Element, axis: Axis): Option.Option<Scroller> => {
57
+ const doc = group.ownerDocument
58
+ const from = (cur: HTMLElement | null): Option.Option<Scroller> =>
59
+ Option.flatMap(Option.fromNullOr(cur), (el) =>
60
+ Bool.match(el === doc.body || el === doc.documentElement, {
61
+ onTrue: () => Option.map(Option.fromNullOr(doc.defaultView), (win): Scroller => ({ kind: "document", win })),
62
+ onFalse: () => {
63
+ const style = getComputedStyle(el)
64
+ return Bool.match(SCROLLS.has({ x: style.overflowX, y: style.overflowY }[axis]), {
65
+ onTrue: () => Option.some<Scroller>({ kind: "element", el }),
66
+ onFalse: () => from(el.parentElement),
67
+ })
68
+ },
69
+ })
70
+ )
71
+ return from(group.parentElement)
72
+ }
73
+
74
+ /** `scroller`'s extent on `axis`: an element's visible edges are clamped to the viewport, as Motion's `getScrollAmount`. */
75
+ const viewOf = (scroller: Scroller, axis: Axis): View =>
76
+ Match.value(scroller).pipe(
77
+ Match.when({ kind: "document" }, ({ win }): View => {
78
+ const root = win.document.documentElement
79
+ return {
80
+ start: 0,
81
+ end: { x: win.innerWidth, y: win.innerHeight }[axis],
82
+ scroll: { x: win.scrollX, y: win.scrollY }[axis],
83
+ maxScroll: { x: () => root.scrollWidth - win.innerWidth, y: () => root.scrollHeight - win.innerHeight }[axis],
84
+ }
85
+ }),
86
+ Match.when({ kind: "element" }, ({ el }): View => {
87
+ const rect = el.getBoundingClientRect()
88
+ const win = Option.fromNullOr(el.ownerDocument.defaultView)
89
+ const viewport = {
90
+ x: Option.match(win, { onNone: () => rect.right, onSome: (w) => w.innerWidth }),
91
+ y: Option.match(win, { onNone: () => rect.bottom, onSome: (w) => w.innerHeight }),
92
+ }
93
+ return {
94
+ start: Math.max(0, { x: rect.left, y: rect.top }[axis]),
95
+ end: Math.min(viewport[axis], { x: rect.right, y: rect.bottom }[axis]),
96
+ scroll: { x: el.scrollLeft, y: el.scrollTop }[axis],
97
+ maxScroll: { x: () => el.scrollWidth - el.clientWidth, y: () => el.scrollHeight - el.clientHeight }[axis],
98
+ }
99
+ }),
100
+ Match.exhaustive,
101
+ )
102
+
103
+ /** Scroll `scroller` by `amount` on `axis`. */
104
+ const scrollBy = (scroller: Scroller, axis: Axis, amount: number): void =>
105
+ Match.value(scroller).pipe(
106
+ Match.when({ kind: "document" }, ({ win }) => win.scrollBy({ x: { left: amount }, y: { top: amount } }[axis])),
107
+ Match.when({ kind: "element" }, ({ el }) =>
108
+ Match.value(axis).pipe(
109
+ Match.when("x", () => {
110
+ el.scrollLeft += amount
111
+ }),
112
+ Match.orElse(() => {
113
+ el.scrollTop += amount
114
+ }),
115
+ )
116
+ ),
117
+ Match.exhaustive,
118
+ )
119
+
120
+ const edgeOf = (state: AutoScroll.State): AutoScroll.State["edge"] => state.edge
121
+
122
+ /**
123
+ * Build the **Auto-scroll** for `group` (the Reorder group's element). The scroller on
124
+ * each axis is found once per gesture, at its first update. Each update reads the
125
+ * scroller's visible edges (clamped to the viewport, as Motion's `getScrollAmount`), runs
126
+ * core's `AutoScroll.step`, and scrolls by its amount. `debug` receives `auto-scroll
127
+ * start` and `auto-scroll stop` { axis, edge } when an edge starts and ends scrolling.
128
+ */
129
+ export const makeAutoScroller = (
130
+ group: Element,
131
+ debug?: (event: string, annotations: Record<string, unknown>) => void,
132
+ ): AutoScroller => {
133
+ const log = UndefinedOr.match(debug, { onUndefined: () => constVoid, onDefined: (d) => d })
134
+ // Each axis's scroller, found at the gesture's first update (`None` when there is none).
135
+ const scrollers = new Map<Axis, Option.Option<Scroller>>()
136
+ const states = new Map<Axis, AutoScroll.State>()
137
+
138
+ const setState = (axis: Axis, next: Option.Option<AutoScroll.State>): void => {
139
+ const prev = Option.fromUndefinedOr(states.get(axis))
140
+ const edgeChanged = Bool.not(Equal.equals(Option.map(prev, edgeOf), Option.map(next, edgeOf)))
141
+ if (edgeChanged && Option.isSome(prev)) log("auto-scroll stop", { axis, edge: prev.value.edge })
142
+ Option.match(next, {
143
+ onNone: () => states.delete(axis),
144
+ onSome: (state) => states.set(axis, state),
145
+ })
146
+ if (edgeChanged && Option.isSome(next)) log("auto-scroll start", { axis, edge: next.value.edge })
147
+ }
148
+
149
+ return {
150
+ update: (pointer, axis, velocity, item) => {
151
+ const scroller = Option.getOrElse(Option.fromUndefinedOr(scrollers.get(axis)), () => {
152
+ const found = findScroller(group, axis)
153
+ scrollers.set(axis, found)
154
+ return found
155
+ })
156
+ if (Option.isNone(scroller)) return
157
+ const shift = UndefinedOr.match(item, { onUndefined: () => ZERO, onDefined: heldScrollShift })
158
+ const view = viewOf(scroller.value, axis)
159
+ const step = AutoScroll.step(Option.fromUndefinedOr(states.get(axis)), {
160
+ pointer: pointer[axis] - shift[axis],
161
+ start: view.start,
162
+ end: view.end,
163
+ velocity,
164
+ scroll: view.scroll,
165
+ maxScroll: view.maxScroll,
166
+ })
167
+ setState(axis, step.state)
168
+ if (step.amount === 0) return
169
+ scrollBy(scroller.value, axis, step.amount)
170
+ },
171
+ reset: () => {
172
+ Arr.forEach(Arr.fromIterable(states.keys()), (axis) => setState(axis, Option.none()))
173
+ scrollers.clear()
174
+ },
175
+ }
176
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `ClockDomLive` — the browser realization of `Clock` and of the Environment's
3
+ * `RefreshFrameSource` (ADR-0031): `requestAnimationFrame`, whose callbacks are the browser's
4
+ * Refresh frames, as the member, and core's shared `Clock.layer` built from it. The flush gate
5
+ * requests its flushes from the same member, so the Clock and the gate share the browser's
6
+ * frames. Everything else — the ref-counted Refresh frame loop, the `dt` computation and the
7
+ * `wardic.clock` span — lives in core, so every Environment's Clock behaves the same (ADR-0004).
8
+ */
9
+ import * as Clock from "@wardic/core/Clock"
10
+ import * as RefreshFrameSource from "@wardic/core/RefreshFrameSource"
11
+ import * as Effect from "effect/Effect"
12
+ import * as Layer from "effect/Layer"
13
+ import * as Predicate from "effect/Predicate"
14
+ import { requireGlobal } from "./require-global.ts"
15
+
16
+ /**
17
+ * The browser's Refresh frames: `requestAnimationFrame` itself, so the Clock and the gate make
18
+ * the browser's own calls, in the order they make them (ADR-0031, decision 7). `cancelFrame`
19
+ * wraps `cancelAnimationFrame` for the member's `unknown` handle; it runs only on a cancel. Both
20
+ * are read when the Layer builds, after the client-only check.
21
+ */
22
+ const frames: Layer.Layer<RefreshFrameSource.RefreshFrameSource> = Layer.effect(RefreshFrameSource.RefreshFrameSource)(
23
+ requireGlobal("DomClock.layer", "requestAnimationFrame").pipe(
24
+ Effect.map((): RefreshFrameSource.Shape => {
25
+ const cancel = cancelAnimationFrame
26
+ return {
27
+ requestFrame: requestAnimationFrame,
28
+ cancelFrame: (handle: unknown) => {
29
+ if (Predicate.isNumber(handle)) cancel(handle)
30
+ },
31
+ }
32
+ }),
33
+ ),
34
+ )
35
+
36
+ /**
37
+ * Core's `Clock` and the Environment's `RefreshFrameSource`, from one `requestAnimationFrame`, needing
38
+ * nothing: the frame loop runs in the Layer's own scope. It reads `requestAnimationFrame` when the Layer
39
+ * is built, not when `@wardic/dom` is imported, so a test that stubs it first drives this Clock. Building
40
+ * it on a server fails, with an error that says it's client-only.
41
+ */
42
+ // ADR-0021, decision 7: the client-only error.
43
+ export const ClockDomLive: Layer.Layer<Clock.Clock | RefreshFrameSource.RefreshFrameSource> = Clock.layer.pipe(
44
+ Layer.provideMerge(frames),
45
+ )
@@ -0,0 +1,258 @@
1
+ /**
2
+ * The DOM Environment's colours (ADR-0036; ADR-0026 for the Interpolator's shape): CSS Color syntax,
3
+ * parsed through culori, and blended in sRGB by Wardic's own lerp and format, which write culori's
4
+ * strings (ADR-0026 *Amendment (#156)*). Core names no colour syntax; `@wardic/dom` registers its
5
+ * colour Interpolator in its registry (`interpolators.ts`) and names a colour inside a complex value
6
+ * as a kind of token (`COLOR_TOKEN`, #61).
7
+ *
8
+ * Read through `culori/fn`, which registers no colour space by itself (#511, ADR-0067 decision 5):
9
+ * sRGB's syntaxes, a hex, `rgb()`, `hsl()` and the colour names, are registered here, and every other
10
+ * CSS colour space (`oklch()`, `lab()`, `color(display-p3 …)`, `hwb()` …) by {@link registerSpaces},
11
+ * which the bindings' `colors` Feature calls. culori keeps its spaces in one registry for the page, so
12
+ * a space registered once is read everywhere from then on.
13
+ *
14
+ * @module
15
+ */
16
+ /// <reference path="./culori.d.ts" />
17
+ import {
18
+ formatRgb,
19
+ getMode,
20
+ interpolate as culoriInterpolate,
21
+ modeA98,
22
+ modeHsl,
23
+ modeHwb,
24
+ modeLab,
25
+ modeLch,
26
+ modeLrgb,
27
+ modeOklab,
28
+ modeOklch,
29
+ modeP3,
30
+ modeProphoto,
31
+ modeRec2020,
32
+ modeRgb,
33
+ modeXyz50,
34
+ modeXyz65,
35
+ parse as culoriParse,
36
+ useMode,
37
+ } from "culori/fn"
38
+ import type * as Interpolator from "@wardic/core/Interpolator"
39
+ import * as Scalar from "@wardic/core/Scalar"
40
+ import * as Arr from "effect/Array"
41
+ import * as Bool from "effect/Boolean"
42
+ import { identity } from "effect/Function"
43
+ import * as Option from "effect/Option"
44
+ import * as Predicate from "effect/Predicate"
45
+ import * as Str from "effect/String"
46
+ import * as UndefinedOr from "effect/UndefinedOr"
47
+
48
+ /** A colour as culori holds it: its colour space (`mode`) and its channels. */
49
+ // The DOM's own type, so the declarations it emits don't lean on `culori.d.ts`.
50
+ export type Color = { readonly mode: string; readonly [channel: string]: number | string | undefined }
51
+
52
+ /** A culori colour: an object tagged with its colour space. */
53
+ const isColor = (value: unknown): value is Color => Predicate.hasProperty(value, "mode")
54
+
55
+ /**
56
+ * culori's `parse`, with a throw read as no colour. It throws on a CSS function holding a length,
57
+ * `blur(4px)`, `inset(10px)`, `drop-shadow(0px 0px 4px red)`, and the throw escaped the colour's
58
+ * decode at `resolve`, so the leg failed (#154).
59
+ */
60
+ const culoriParseOption = Option.liftThrowable(culoriParse)
61
+
62
+ /** Parse a colour string, trimmed, or `None` when culori rejects it (`"auto"`, `"#zzz"`) or throws on it (`"blur(4px)"`). */
63
+ const parseColorString = (input: string): Option.Option<Color> =>
64
+ Option.filter(Option.flatMap(culoriParseOption(Str.trim(input)), Option.fromUndefinedOr), isColor)
65
+
66
+ /** A colour string read: a culori colour, or `None` for any other value. A string is a colour when culori reads it, trimmed. */
67
+ // ADR-0026; CONTEXT.md's "color string". `Color.parse`, and the colour Interpolator's `parse` (plain
68
+ // functions since #511, where they were a Schema codec, `ColorString`).
69
+ export const parseColor = (value: unknown): Option.Option<Color> => {
70
+ if (!Predicate.isString(value)) return Option.none()
71
+ return parseColorString(value)
72
+ }
73
+
74
+ /**
75
+ * A colour written as `rgb(…)` or `rgba(…)`, as culori's `formatRgb` writes it, so `"#fff"` reads and
76
+ * writes back as `"rgb(255, 255, 255)"`: what a blend's every frame writes.
77
+ */
78
+ // `Color.format`, and the colour Interpolator's `format`.
79
+ export const formatColor = (color: Color): string => formatRgb(color)
80
+
81
+ // --- sRGB, Wardic's own (#156, ADR-0026 *Amendment (#156)*) ------------------------------------
82
+ //
83
+ // culori's `formatRgb(interpolate([from, to], "rgb")(t))`, string for string, without a colour
84
+ // object per frame. Its path, at culori 4.0.2: `t` clamped to [0, 1]; at `t <= 0` the start as
85
+ // converted; a channel one end lacks (CSS `none`) takes the other end's; an alpha one end lacks is
86
+ // 1 when the other has one (`fixupAlpha`); each channel lerped `a + t * (b - a)` (`Scalar.lerp`'s
87
+ // `a + (b - a) * t`, the same bits: multiplication commutes); then each channel byte is
88
+ // `round(clamp(v || 0) * 255)`, and `rgb()` is written when alpha is absent or exactly 1, else
89
+ // `rgba()` with alpha clamped and rounded to two places, so 0.999 writes `rgba(…, 1)`. A seeded
90
+ // differential test against culori pins it (`colors.node.test.ts`).
91
+
92
+ // sRGB's syntaxes, read by every app (#511): `rgb`'s parsers take a hex, `rgb()` and `rgba()`, the
93
+ // colour names and `transparent`; `hsl`'s take `hsl()` and `hsla()`. Registered once, when this module
94
+ // loads, as culori's own bundle registers its spaces.
95
+ const toRgb = useMode(modeRgb)
96
+ useMode(modeHsl)
97
+
98
+ /**
99
+ * Registers every CSS colour space beyond sRGB's syntaxes: `lab()`, `lch()`, `oklab()`, `oklch()`,
100
+ * `hwb()`, and `color()`'s `display-p3`, `rec2020`, `a98-rgb`, `prophoto-rgb`, `srgb-linear`, `xyz`,
101
+ * `xyz-d50` and `xyz-d65`. From then on such a string is a colour everywhere on the page; before, it
102
+ * isn't one, and a `lab()` value's numbers blend as written. Calling it again does nothing new.
103
+ */
104
+ // #511, ADR-0067 decision 5: the spaces culori's `culori/css` registers but for its non-CSS `hsv`, `lab65`
105
+ // and `lch65`. The `colors` Feature's.
106
+ export const registerSpaces = (): void => {
107
+ Arr.forEach(CSS_SPACES, (mode) => {
108
+ useMode(mode)
109
+ })
110
+ }
111
+
112
+ /**
113
+ * Whether the spaces `registerSpaces` registers are registered on the page: by it, or by culori's own
114
+ * bundle, which registers every space as it loads.
115
+ */
116
+ // Asked of culori's registry, the page's.
117
+ export const spacesRegistered = (): boolean => Predicate.isNotUndefined(getMode("oklch"))
118
+
119
+ const CSS_SPACES = [modeA98, modeHwb, modeLab, modeLch, modeLrgb, modeOklab, modeOklch, modeP3, modeProphoto, modeRec2020, modeXyz50, modeXyz65]
120
+
121
+ /** A colour's four channels, resolved once: what the frame lerps. */
122
+ interface Channels {
123
+ readonly r: number
124
+ readonly g: number
125
+ readonly b: number
126
+ readonly alpha: number
127
+ }
128
+
129
+ /**
130
+ * One channel of a pair, as culori's piecewise interpolator reads it: both ends, or the end that
131
+ * has it, twice. One neither end has is 0 on both: culori leaves it out, and its format writes 0.
132
+ */
133
+ const channelPair = (a: number | undefined, b: number | undefined): readonly [number, number] =>
134
+ UndefinedOr.match(a, {
135
+ onUndefined: () => UndefinedOr.match(b, { onUndefined: () => [0, 0] as const, onDefined: (y) => [y, y] as const }),
136
+ onDefined: (x) => UndefinedOr.match(b, { onUndefined: () => [x, x] as const, onDefined: (y) => [x, y] as const }),
137
+ })
138
+
139
+ /** An alpha one end lacks is 1, culori's `fixupAlpha`; with neither, 1 on both writes `rgb()`, as culori does. */
140
+ const alphaOf = (alpha: number | undefined): number => UndefinedOr.match(alpha, { onUndefined: () => 1, onDefined: identity })
141
+
142
+ /** A channel's value at 0–1, as culori's format reads it: NaN as 0, then clamped. Per frame: an early return. */
143
+ const unitOf = (value: number): number => {
144
+ if (Number.isNaN(value)) return 0
145
+ return Math.max(0, Math.min(1, value))
146
+ }
147
+
148
+ /** A channel as a byte, as culori's `formatRgb` writes it. */
149
+ const byteOf = (value: number): number => Math.round(unitOf(value) * 255)
150
+
151
+ /** An alpha as culori's `formatRgb` writes it: clamped, two decimal places. */
152
+ const alphaText = (alpha: number): number => Math.round(unitOf(alpha) * 100) / 100
153
+
154
+ /** Four channels as culori's `formatRgb` writes them. Per frame: an early return. */
155
+ const formatChannels = (r: number, g: number, b: number, alpha: number): string => {
156
+ if (alpha === 1) return `rgb(${byteOf(r)}, ${byteOf(g)}, ${byteOf(b)})`
157
+ return `rgba(${byteOf(r)}, ${byteOf(g)}, ${byteOf(b)}, ${alphaText(alpha)})`
158
+ }
159
+
160
+ /**
161
+ * Two colours' blend in sRGB, on the per-frame path (ADR-0025): `t` clamped as culori clamps it,
162
+ * the start as written at `t <= 0`, and the four lerps formatted. Nothing allocated but the string.
163
+ */
164
+ const blendChannels =
165
+ (atStart: string, from: Channels, to: Channels) =>
166
+ (t: number): string => {
167
+ const at = Math.min(Math.max(0, t), 1)
168
+ if (at <= 0) return atStart
169
+ return formatChannels(
170
+ Scalar.lerp(from.r, to.r, at),
171
+ Scalar.lerp(from.g, to.g, at),
172
+ Scalar.lerp(from.b, to.b, at),
173
+ Scalar.lerp(from.alpha, to.alpha, at),
174
+ )
175
+ }
176
+
177
+ /**
178
+ * Two ends with the same channels: every `t` past 0 writes one string, since `a + t * (a - a)` is
179
+ * `a` (#156). Per frame: an early return.
180
+ */
181
+ const holdChannels =
182
+ (atStart: string, same: string) =>
183
+ (t: number): string => {
184
+ if (t <= 0) return atStart
185
+ return same
186
+ }
187
+
188
+ /** Two channels that are the same on both ends. */
189
+ const isSame = ([a, b]: readonly [number, number]): boolean => a === b
190
+
191
+ /**
192
+ * Two colours' sRGB blend, resolved once when a stream is built: each end converted to sRGB by
193
+ * culori, its channels paired as culori pairs them, and the start's own string for `t <= 0`, by
194
+ * culori's `formatRgb` itself. Two ends with the same channels hold one string.
195
+ */
196
+ const srgb = (from: Color, to: Color): ((t: number) => string) => {
197
+ const a = toRgb(from)
198
+ const b = toRgb(to)
199
+ const pairs = [
200
+ channelPair(a.r as number | undefined, b.r as number | undefined),
201
+ channelPair(a.g as number | undefined, b.g as number | undefined),
202
+ channelPair(a.b as number | undefined, b.b as number | undefined),
203
+ [alphaOf(a.alpha as number | undefined), alphaOf(b.alpha as number | undefined)] as const,
204
+ ] as const
205
+ const [[fr, tr], [fg, tg], [fb, tb], [fa, ta]] = pairs
206
+ const atStart = formatRgb(a)
207
+ return Bool.match(Arr.every(pairs, isSame), {
208
+ onTrue: () => holdChannels(atStart, formatChannels(fr, fg, fb, fa)),
209
+ onFalse: () => blendChannels(atStart, { r: fr, g: fg, b: fb, alpha: fa }, { r: tr, g: tg, b: tb, alpha: ta }),
210
+ })
211
+ }
212
+
213
+ /** Two colours' blend through culori in `mode`: `interpolate` once, then a colour object and `formatRgb` per frame. */
214
+ const throughCulori = (mode: string, from: Color, to: Color): ((t: number) => string) => {
215
+ const at = culoriInterpolate([from, to], mode)
216
+ return (t: number) => formatRgb(at(t))
217
+ }
218
+
219
+ /**
220
+ * Blends colours in the colour space `mode`: `"rgb"` by default, sRGB, as the browser's own animations
221
+ * blend. `"oklch"` or `"lab"` blend perceptually, once `registerSpaces` has registered them. A string is
222
+ * a colour when culori reads it, trimmed, so a keyword like `"auto"` isn't, and named colours and the
223
+ * modern forms culori knows are. Past the end of a blend a colour holds at its target.
224
+ *
225
+ * @example
226
+ * Color.registerSpaces()
227
+ * yield* registry.register(Color.interpolator("oklch")) // colours now blend in OKLCH
228
+ */
229
+ // ADR-0026. A factory, so an app can register the space it prefers over the one `parseColor` and
230
+ // `formatColor` use; the DOM's registry uses `"rgb"` (`cssColor`). `blend` resolves the pair once, when a
231
+ // stream is built, and the frame writes the string `format` (`formatRgb`) would:
232
+ // - `"rgb"` blends with Wardic's own lerp and format (#156, ADR-0026 *Amendment (#156)*), the strings
233
+ // culori writes, with no colour object per frame; two ends of the same colour write one string.
234
+ // - Any other mode goes through culori's interpolator, which returns a new colour object on each call.
235
+ // Either clamps `t` to [0, 1], as culori does (ADR-0024, decision 5).
236
+ export const color = (mode = "rgb"): Interpolator.Entry<Color, string> => ({
237
+ name: `color(${mode})`,
238
+ parse: parseColor,
239
+ format: formatColor,
240
+ blend: (from, to) =>
241
+ Option.some(
242
+ Bool.match(mode === "rgb", {
243
+ onTrue: () => srgb(from, to),
244
+ onFalse: () => throughCulori(mode, from, to),
245
+ }),
246
+ ),
247
+ })
248
+
249
+ /**
250
+ * A colour inside a longer value: a hex, or an `rgb()`, `rgba()`, `hsl()` or `hsla()`, as Motion's colour
251
+ * token is. A named colour, `red`, isn't one: it stays part of the text around the tokens.
252
+ */
253
+ // Motion's motion-dom value/types/utils/color-regex.ts at 16c6122; #61.
254
+ export const COLOR_TOKEN = /#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\)/
255
+
256
+ /** The DOM's colours: CSS's colour syntax, blended in sRGB, as the browser's own animations blend. Its registry's colour kind. */
257
+ // ADR-0024: as WAAPI blends.
258
+ export const cssColor: Interpolator.Entry<Color, string> = color("rgb")
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The DOM Environment's complex values (ADR-0035 *Amendment (#158)*, ADR-0036): core's complex tier,
3
+ * given CSS's `var()` as a span of text (#158) and a colour as a kind of token (#61, #160). Core
4
+ * names neither.
5
+ *
6
+ * @module
7
+ */
8
+ import * as Interpolator from "@wardic/core/Interpolator"
9
+ import { COLOR_TOKEN, cssColor } from "./colors.ts"
10
+
11
+ /**
12
+ * A reference to a custom property, `var(--name)`, with a fallback one bracket level deep (Motion's
13
+ * var token, motion-dom value/types/complex/index.ts `complexRegex` at 16c6122). Inside a complex
14
+ * value it's text: the digits in its name (`--gray-500`) are never numbers (#158).
15
+ */
16
+ export const VARIABLE_REFERENCE = /var\s*\(\s*--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)/
17
+
18
+ /**
19
+ * Core's complex Interpolator with the DOM's syntax:
20
+ * - a `var()` is text (#158): a lone `var()` isn't a complex value, and two blend only with the same
21
+ * count of `var()`s, the target's written as it stands;
22
+ * - a colour is a token (#61, #160) that blends through `cssColor`, in sRGB, so a shadow's colour
23
+ * fades: two blend only with the same count of colours, the n-th with the n-th, and a lone colour
24
+ * is `cssColor`'s, not a complex value.
25
+ */
26
+ export const cssComplex: Interpolator.Entry<Interpolator.Complex, string> = Interpolator.complexWith({
27
+ text: [VARIABLE_REFERENCE],
28
+ tokens: [{ pattern: COLOR_TOKEN, entry: cssColor }],
29
+ })
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Watching a container's content for size changes: its direct children on a `ResizeObserver`, each
3
+ * observed now and as it's added, and let go as it's removed. A deep change counts once it resizes
4
+ * the direct child it sits in.
5
+ *
6
+ * - `Probe.scroll` watches a container's (the page's: the body's), so its range follows its content
7
+ * (#239, ADR-0043 *Amendment (#239)*).
8
+ * - The Projection system watches the page's, so a node that content elsewhere moved is measured again
9
+ * (#261, ADR-0018 *Amendment (#261)*).
10
+ *
11
+ * The observer is shared by count: a box watched for two reasons (a scroll target that is also a
12
+ * child, a layout node that is also a page box) stays watched until both let go.
13
+ */
14
+ import * as Arr from "effect/Array"
15
+ import { constVoid } from "effect/Function"
16
+ import * as Option from "effect/Option"
17
+
18
+ /** Observing and letting go of boxes on one observer, each counted by the reasons it's watched for. */
19
+ export interface Watched {
20
+ readonly observe: (box: Element) => void
21
+ readonly unobserve: (box: Element) => void
22
+ }
23
+
24
+ /** {@link Watched} over `observer`, every box observed with `options`. */
25
+ export const counted = (observer: ResizeObserver, options?: ResizeObserverOptions): Watched => {
26
+ const counts = new Map<Element, number>()
27
+ const countOf = (box: Element): number => Option.getOrElse(Option.fromUndefinedOr(counts.get(box)), () => 0)
28
+ return {
29
+ observe: (box) => {
30
+ const count = countOf(box)
31
+ counts.set(box, count + 1)
32
+ if (count === 0) observer.observe(box, options)
33
+ },
34
+ unobserve: (box) => {
35
+ const count = countOf(box)
36
+ if (count === 0) return
37
+ if (count > 1) {
38
+ counts.set(box, count - 1)
39
+ return
40
+ }
41
+ counts.delete(box)
42
+ observer.unobserve(box)
43
+ },
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Watch `parent`'s direct children on `watched`, kept current by a `MutationObserver` on its child list.
49
+ * `onChildren` runs after each change to the list, its children watched by then. Returns the stop; the
50
+ * observer's own disconnect lets go of the boxes.
51
+ */
52
+ export const watchChildren = (parent: Element, watched: Watched, onChildren: () => void): (() => void) => {
53
+ Arr.forEach(Array.from(parent.children), watched.observe)
54
+ const children = new MutationObserver((records) => {
55
+ Arr.forEach(records, (record) => {
56
+ Arr.forEach(Array.from(record.addedNodes), (node) => {
57
+ if (node instanceof Element) watched.observe(node)
58
+ })
59
+ Arr.forEach(Array.from(record.removedNodes), (node) => {
60
+ if (node instanceof Element) watched.unobserve(node)
61
+ })
62
+ })
63
+ onChildren()
64
+ })
65
+ children.observe(parent, { childList: true })
66
+ return () => children.disconnect()
67
+ }
68
+
69
+ /**
70
+ * Watch the page's content on `watched`: the body and its direct children (a body sized to the window
71
+ * doesn't grow with what overflows it), as {@link watchChildren} watches a container's. None with no
72
+ * body (a server, a document not parsed that far). Returns the stop.
73
+ */
74
+ export const watchPage = (watched: Watched, onChildren: () => void): (() => void) =>
75
+ Option.match(
76
+ Option.flatMap(Option.fromUndefinedOr((globalThis as { readonly document?: Document }).document), (page) => Option.fromNullOr(page.body)),
77
+ {
78
+ onNone: () => constVoid,
79
+ onSome: (body) => {
80
+ watched.observe(body)
81
+ return watchChildren(body, watched, onChildren)
82
+ },
83
+ },
84
+ )
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Minimal ambient types for the subset of `culori` the color interpolator uses: `culori/fn` (#511),
3
+ * which registers no colour space by itself, and the `culori` bundle the tests compare against.
4
+ * `culori` v4 ships no TypeScript types and `@types/culori` lags its API, so we
5
+ * declare exactly what we touch here. Widen this if we start using more of culori.
6
+ */
7
+ declare module "culori" {
8
+ export type Color = Record<string, number | string | undefined>
9
+
10
+ export function interpolate(
11
+ colors: ReadonlyArray<string | Color>,
12
+ mode?: string,
13
+ overrides?: unknown,
14
+ ): (t: number) => Color
15
+
16
+ export function formatRgb(color: Color | string): string
17
+ export function formatHex(color: Color | string): string
18
+ export function parse(color: string): Color | undefined
19
+ export function converter(mode: string): (color: Color) => Color
20
+ }
21
+
22
+ declare module "culori/fn" {
23
+ export type Color = Record<string, number | string | undefined>
24
+
25
+ /** A colour space's definition, as `useMode` takes it. */
26
+ export interface Mode {
27
+ readonly mode: string
28
+ }
29
+
30
+ export function interpolate(
31
+ colors: ReadonlyArray<string | Color>,
32
+ mode?: string,
33
+ overrides?: unknown,
34
+ ): (t: number) => Color
35
+
36
+ export function formatRgb(color: Color | string): string
37
+ export function parse(color: string): Color | undefined
38
+ export function converter(mode: string): (color: Color) => Color
39
+ /** A registered colour space's definition, or `undefined` for one not registered. */
40
+ export function getMode(mode: string): Mode | undefined
41
+ /** Register a colour space (its parsers, its conversions): a converter to it. */
42
+ export function useMode(mode: Mode): (color: Color) => Color
43
+
44
+ export const modeA98: Mode
45
+ export const modeHsl: Mode
46
+ export const modeHwb: Mode
47
+ export const modeLab: Mode
48
+ export const modeLch: Mode
49
+ export const modeLrgb: Mode
50
+ export const modeOklab: Mode
51
+ export const modeOklch: Mode
52
+ export const modeP3: Mode
53
+ export const modeProphoto: Mode
54
+ export const modeRec2020: Mode
55
+ export const modeRgb: Mode
56
+ export const modeXyz50: Mode
57
+ export const modeXyz65: Mode
58
+ }