@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,166 @@
1
+ /**
2
+ * Base values in the DOM (ADR-0022): an element's Own value for a key, and the `style` link's
3
+ * lookup. A binding calls these and hands them to core's `BaseValue.of` / `BaseValue.start`, so a
4
+ * new binding writes no DOM code for them (the thin-bindings rule). Tier 2, for binding
5
+ * authors.
6
+ *
7
+ * Client-only: the read runs after mount, never during a server render (ADR-0021, "Nothing
8
+ * reads the DOM on a server"). `styleValueOf` and `valueLike` are pure.
9
+ *
10
+ * Observability: per element and cold (once per key), so quiet; the spine logs what it
11
+ * started from and returned to.
12
+ */
13
+ import * as BaseValue from "@wardic/core/BaseValue"
14
+ import * as Interpolator from "@wardic/core/Interpolator"
15
+ import * as Bool from "effect/Boolean"
16
+ import * as Match from "effect/Match"
17
+ import * as MutableRef from "effect/MutableRef"
18
+ import * as Option from "effect/Option"
19
+ import * as Predicate from "effect/Predicate"
20
+ import * as Str from "effect/String"
21
+ import { parseCssText } from "./starting-style.ts"
22
+ import { dashCase, isSvgInner, svgAttrNameOf } from "./svg.ts"
23
+
24
+ /** An element's own value for a key: the raw value read, a string, a transform key's identity, or `None`. */
25
+ export type OwnValue = (key: string) => Option.Option<number | string>
26
+
27
+ /**
28
+ * A reader of `element`'s own values, each read once, the first time it's needed, and kept, a miss too,
29
+ * so a later CSS change doesn't move it, as Motion's. For each key:
30
+ * 1. a transform key: its identity, with no read;
31
+ * 2. on an SVG element inside an `<svg>`, a key it takes as an attribute: that attribute, if it has one;
32
+ * 3. else its computed style, a custom property by its own name.
33
+ *
34
+ * Making a reader reads nothing, and a read from an element not in the document isn't kept. Values read
35
+ * are raw strings: `valueLike` reads one in a target's shape.
36
+ */
37
+ // Motion's C12. A transform key: Motion's projected branch, HTMLVisualElement.ts:51 at
38
+ // 16c61222c4166bbef5ff6ab0051192faa79fa8c8. An attribute: ADR-0014's routing.
39
+ export const ownValues = (element: Element): OwnValue => {
40
+ const cache = new Map<string, Option.Option<number | string>>()
41
+ // The element's computed style, read once, at the first key that needs it.
42
+ const computed = MutableRef.make(Option.none<CSSStyleDeclaration>())
43
+ const computedStyle = (): CSSStyleDeclaration =>
44
+ Option.getOrElse(MutableRef.get(computed), () => {
45
+ const style = getComputedStyle(element)
46
+ MutableRef.set(computed, Option.some(style))
47
+ return style
48
+ })
49
+ // On an svg-inner element, the attribute the key is written as, if it has one.
50
+ const attributeOf = (key: string): Option.Option<string> =>
51
+ Bool.match(isSvgInner(element), {
52
+ onTrue: () => Option.flatMap(svgAttrNameOf(key), (name) => nonEmpty(element.getAttribute(name))),
53
+ onFalse: () => Option.none(),
54
+ })
55
+ const read = (key: string): Option.Option<number | string> =>
56
+ Option.orElse(BaseValue.transformIdentity(key), () =>
57
+ Option.orElse(attributeOf(key), () => nonEmpty(computedStyle().getPropertyValue(dashCase(key)).trim()))
58
+ )
59
+ return (key) =>
60
+ Option.getOrElse(Option.fromUndefinedOr(cache.get(key)), () => {
61
+ const value = read(key)
62
+ // An element not in the document has an empty computed style: that miss isn't the
63
+ // element's, so it isn't kept, and the key is read again once the element is in.
64
+ if (element.isConnected) cache.set(key, value)
65
+ return value
66
+ })
67
+ }
68
+
69
+ /** A read string, `None` when there's none or it's empty. */
70
+ const nonEmpty = (value: string | null): Option.Option<string> => Option.filter(Option.fromNullOr(value), Str.isNonEmpty)
71
+
72
+ /**
73
+ * The app's `style` value for `key`: from an object style by the key, then by the key dash-cased; from a
74
+ * CSS-text style, parsed as `Style.over` parses it. `undefined`, `null` and an empty string are `None`. A
75
+ * transform key, `x`, `scale`, `rotate`, is Wardic's, as Motion's is: a binding draws it into `transform`.
76
+ * The value is as given, so a binding reads a live value, an Atom or an accessor, itself.
77
+ *
78
+ * @example
79
+ * OwnValues.styleValueOf({ opacity: 0.5 }, "opacity") // Some(0.5)
80
+ */
81
+ // The `style` link of a Base value. #130, ADR-0043 decision 4: through the flush gate.
82
+ export const styleValueOf = (style: object | string | undefined, key: string): Option.Option<unknown> =>
83
+ Option.flatMap(
84
+ Option.fromNullishOr(style),
85
+ (given) => {
86
+ const map = recordOf(given)
87
+ // A value at `k`: `undefined`, `null` and an empty string are misses.
88
+ const at = (k: string): Option.Option<unknown> =>
89
+ Option.filter(
90
+ Option.flatMap(Option.liftPredicate(k, (name: string) => Object.hasOwn(map, name)), (name) => Option.fromNullishOr(map[name])),
91
+ (value) => value !== "",
92
+ )
93
+ // A custom property by its own name only; any other key then dash-cased.
94
+ return Option.orElse(at(key), () =>
95
+ Option.flatMap(Option.liftPredicate(dashCase(key), (dashed: string) => Bool.not(key.startsWith("--")) && dashed !== key), at)
96
+ )
97
+ },
98
+ )
99
+
100
+ /** The last CSS text parsed, and its record. */
101
+ // Module state (#460, Q7): a one-entry cache keyed by the text it parsed, read only: a Retarget asks
102
+ // for each of a style's keys in a row, and parsed the whole text again for each (#618, F6).
103
+ let last: { readonly text: string; readonly record: Readonly<Record<string, unknown>> } = { text: "", record: {} }
104
+
105
+ /** `text` parsed, once for as long as it's the text asked for. */
106
+ const parsedOnce = (text: string): Readonly<Record<string, unknown>> => {
107
+ if (text === last.text) return last.record
108
+ last = { text, record: parseCssText(text) }
109
+ return last.record
110
+ }
111
+
112
+ /** A style given as CSS text, parsed; a record as it is. */
113
+ const recordOf: (style: object | string) => Readonly<Record<string, unknown>> = Match.type<object | string>().pipe(
114
+ Match.when(Predicate.isString, parsedOnce),
115
+ Match.orElse((record) => record as Readonly<Record<string, unknown>>),
116
+ )
117
+
118
+ /** A plain number, or a number in `px`. */
119
+ const NUMERIC = /^\s*-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?(px)?\s*$/i
120
+
121
+ /**
122
+ * `raw` in the shape of a key's target `like`, or `None` when it can't be:
123
+ * - **A number target** takes a finite number, or a plain number or a `px` string, parsed (`"0.5"` is
124
+ * 0.5, `"120px"` 120). Anything else misses, `auto`, `50%`, `1em`, a colour, so the key starts at its
125
+ * target. On a key that places or sizes the box, a miss of another type, `auto` or `50%`, is a Measured
126
+ * pair instead: both ends are measured in px, as Motion's are.
127
+ * - **A string target** takes a string as it is, or a finite number as a string, but not a length in a
128
+ * unit other than the target's (`"120px"` for `"0%"`), which can't blend with it.
129
+ */
130
+ // A **Measured pair** (#430, `measured-pairs.ts`): its binding keeps the start. The unit interpolator dies
131
+ // on a pair of two units, so it misses, as a numeric target misses `50%`.
132
+ export const valueLike = (raw: unknown, like: number | string): Option.Option<number | string> => valueLikeOf({ raw, like })
133
+
134
+ // The matchers are built once, at module load, not per call: a start reads every key's (#69).
135
+ /** A plain number, or a number in `px`, as a numeric target takes it. */
136
+ const isNumericString = (raw: unknown): raw is string => Predicate.isString(raw) && NUMERIC.test(raw)
137
+
138
+ /** `raw` for a numeric target: a finite number, or a plain-number or `px` string, parsed. */
139
+ const numericLike: (raw: unknown) => Option.Option<number> = Match.type<unknown>().pipe(
140
+ Match.when(Match.number, (n) => Option.liftPredicate(n, Number.isFinite)),
141
+ Match.when(isNumericString, (s) => Option.some(Number.parseFloat(s))),
142
+ Match.orElse(() => Option.none()),
143
+ )
144
+
145
+ /** `raw` for a string target `like`: a string as is unless its unit clashes with the target's, or a finite number as a string. */
146
+ const stringLike: (pair: { readonly raw: unknown; readonly like: string }) => Option.Option<string> = Match.type<{
147
+ readonly raw: unknown
148
+ readonly like: string
149
+ }>().pipe(
150
+ Match.when({ raw: Match.string }, ({ raw, like }) => Option.liftPredicate(raw, (given: string) => Bool.not(unitsClash(like, given)))),
151
+ Match.when({ raw: Match.number }, ({ raw }) => Option.map(Option.liftPredicate(raw, Number.isFinite), String)),
152
+ Match.orElse(() => Option.none()),
153
+ )
154
+
155
+ const valueLikeOf: (pair: { readonly raw: unknown; readonly like: number | string }) => Option.Option<number | string> = Match.type<{
156
+ readonly raw: unknown
157
+ readonly like: number | string
158
+ }>().pipe(
159
+ Match.when({ like: Match.number }, ({ raw }): Option.Option<number | string> => numericLike(raw)),
160
+ // The rest have a string target.
161
+ Match.orElse((pair): Option.Option<number | string> => stringLike(pair as { readonly raw: unknown; readonly like: string })),
162
+ )
163
+
164
+ /** Whether `raw` is a length in another unit than `like`'s (both have a unit, and they differ). */
165
+ const unitsClash = (like: string, raw: string): boolean =>
166
+ Option.exists(Interpolator.unitOf(like), (want) => want !== "" && Option.exists(Interpolator.unitOf(raw), (got) => got !== "" && got !== want))
@@ -0,0 +1,23 @@
1
+ /**
2
+ * One Projection system's Measure-pass ends, for the Reorder controller (ADR-0020): the engine's Layer
3
+ * provides it beside the engine, and `ReorderController.make` requires it. Not exported from
4
+ * `@wardic/dom`, so there's no public push hook (ADR-0020 decision 2).
5
+ *
6
+ * @internal
7
+ */
8
+ // #618 (decision 12): the controller found the engine's listeners in a WeakMap keyed by the engine
9
+ // object, so a wrapped or replaced ProjectionSystem in the context silently heard no pass end.
10
+ import * as Context from "effect/Context"
11
+
12
+ /** `onPassEnd(listener)` runs `listener` at the end of every Measure pass, and returns the unsubscribe. */
13
+ export class PassEnds extends Context.Service<
14
+ PassEnds,
15
+ {
16
+ /**
17
+ * Run `listener` at the end of every Measure pass: once per pass, after every round's boxes are
18
+ * stored (a blocked pass and one whose flush triggered more rounds included). A listener that
19
+ * throws is logged and doesn't stop the pass or the others.
20
+ */
21
+ readonly onPassEnd: (listener: () => void) => () => void
22
+ }
23
+ >()("@wardic/dom/PassEnds") {}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The DOM Environment's Positional keys (CONTEXT.md; ADR-0029, ADR-0036): the transforms, and the
3
+ * box's position and size as CSS names them, `width`, `height`, `top`, `left`, `right` and `bottom`
4
+ * (Motion's `positionalKeys`). Reduced motion Snaps these. Core names only the transforms; these
5
+ * box keys are CSS's, so they're the DOM's. `DomLive` provides the Layer.
6
+ *
7
+ * @module
8
+ */
9
+ import * as ReducedMotion from "@wardic/core/ReducedMotion"
10
+ import * as Layer from "effect/Layer"
11
+ import * as Predicate from "effect/Predicate"
12
+ import { dashCase } from "./svg.ts"
13
+
14
+ /** The box's position and size, as CSS properties. */
15
+ const BOX_KEYS: ReadonlySet<string> = new Set(["width", "height", "top", "left", "right", "bottom"])
16
+
17
+ /** A key of the box's position or size, written as a CSS name or in camelCase. */
18
+ const isBoxKey: Predicate.Predicate<string> = (key) => BOX_KEYS.has(dashCase(key))
19
+
20
+ /** Whether reduced motion lands `key` at once: a transform, or a key that places or sizes the box. */
21
+ export const isPositional: Predicate.Predicate<string> = Predicate.or(ReducedMotion.isTransform, isBoxKey)
22
+
23
+ /** The DOM Environment's `ReducedMotion.PositionalKeys`. */
24
+ export const PositionalKeysDomLive: Layer.Layer<ReducedMotion.PositionalKeys> = Layer.succeed(ReducedMotion.PositionalKeys, { isPositional })
@@ -0,0 +1,176 @@
1
+ /**
2
+ * `ProbeDomLive` — the browser realization of `Probe`: measurement
3
+ * (`getBoundingClientRect`), reactive viewport size, the `prefers-reduced-motion`
4
+ * preference, whether the page is shown (`document.visibilityState`, #443), and per-target
5
+ * resize/intersection observation streams.
6
+ *
7
+ * The writable Atoms (`viewport`, `prefersReducedMotion`, `pageVisible`) are pinned with
8
+ * `keepAlive` and written through the
9
+ * Environment's {@link AtomRegistry} captured at build time — the same registry
10
+ * a consumer reads `Probe.viewport`/`Probe.prefersReducedMotion` from — so the
11
+ * `make`/event path can update them without leaking an `AtomRegistry`
12
+ * requirement into the `Probe` interface. The window `resize`, the
13
+ * `MediaQueryList` `change` and the document's `visibilitychange` listeners are the only writers, forked into the
14
+ * layer's scope so they live exactly as long as the Layer.
15
+ *
16
+ * Per CONTEXT.md's **Shared source** contract, `observeResize` goes through core's
17
+ * `SharedSource.byTarget`: one `ResizeObserver` per target, ref-counted — the observer is
18
+ * constructed and `observe()`d on the first subscriber and `disconnect()`ed when the last
19
+ * unsubscribes. `observeIntersection` is an **Input feed** (#318) making one `IntersectionObserver`
20
+ * per subscriber, since each may ask its own root, margin and amount (#273, Motion's `viewport`).
21
+ *
22
+ * Observability (CONTEXT.md observability convention): core's `SharedSource` already
23
+ * carries a `wardic.sharedSource.*` span + a debug-log on subscribe; the hot
24
+ * `measure`/viewport-write paths stay quiet.
25
+ */
26
+ import type * as InputFeed from "@wardic/core/InputFeed"
27
+ import * as Probe from "@wardic/core/Probe"
28
+ import * as SharedSource from "@wardic/core/SharedSource"
29
+ import * as Arr from "effect/Array"
30
+ import * as Bool from "effect/Boolean"
31
+ import * as Effect from "effect/Effect"
32
+ import * as Layer from "effect/Layer"
33
+ import * as Option from "effect/Option"
34
+ import * as Predicate from "effect/Predicate"
35
+ import * as Queue from "effect/Queue"
36
+ import * as Stream from "effect/Stream"
37
+ import * as UndefinedOr from "effect/UndefinedOr"
38
+ import { Atom, AtomRegistry } from "effect/reactivity"
39
+ import { requireGlobal } from "./require-global.ts"
40
+ import { makeScroll } from "./scroll.ts"
41
+
42
+ /** Map a `DOMRect`-like value onto the engine's portable {@link Bounds}. */
43
+ /** An `IntersectionObserver`'s init from the Probe's options: each left out when not given. */
44
+ const observerInit = (options: Probe.IntersectionOptions): IntersectionObserverInit => ({
45
+ ...UndefinedOr.match(options.root as Element | Document | undefined, { onUndefined: () => ({}), onDefined: (root) => ({ root }) }),
46
+ ...UndefinedOr.match(options.margin, { onUndefined: () => ({}), onDefined: (rootMargin) => ({ rootMargin }) }),
47
+ ...UndefinedOr.match(options.amount, { onUndefined: () => ({}), onDefined: (threshold) => ({ threshold }) }),
48
+ })
49
+
50
+ const toBounds = (r: DOMRect): Probe.Bounds => ({
51
+ x: r.x,
52
+ y: r.y,
53
+ width: r.width,
54
+ height: r.height,
55
+ top: r.top,
56
+ right: r.right,
57
+ bottom: r.bottom,
58
+ left: r.left,
59
+ })
60
+
61
+ /**
62
+ * Captures the Environment's {@link AtomRegistry} and adds the window's `resize` listener and
63
+ * the media query's `change` listener in the layer scope; each is the sole writer of its Atom.
64
+ * The `resize` listener writes `viewport` inside the event (ADR-0018, *Amendment (#45)*).
65
+ * `Layer.effect` runs `make` in the layer's own scope (it `Exclude`s `Scope`),
66
+ * which the shared helpers' `RcMap`s and the forked listeners need.
67
+ */
68
+ const make = Effect.gen(function* () {
69
+ // Client-only (ADR-0021, decision 7): the viewport below reads `window`.
70
+ yield* requireGlobal("DomProbe.layer", "window")
71
+
72
+ // The shared Environment registry — the one consumers read these atoms from.
73
+ const registry = yield* AtomRegistry.AtomRegistry
74
+
75
+ // --- viewport -----------------------------------------------------------
76
+ // `keepAlive` pins the node so the value the resize listener writes survives
77
+ // between reads (an idle node with no subscriber would be GC'd and re-init).
78
+ const viewport = Atom.keepAlive(
79
+ Atom.make<Probe.Viewport>({ width: window.innerWidth, height: window.innerHeight }),
80
+ )
81
+ // The window's "resize" listener is the only writer of `viewport`, and it writes inside the
82
+ // event (ADR-0018, *Amendment (#45)*): a reader, the projection system's Resize block among
83
+ // them, sees the new size within the dispatch, not a scheduler hop later, before the frame's
84
+ // Measure pass. A forked Stream consumer would land a task after the event.
85
+ const writeViewport = (): void => registry.set(viewport, { width: window.innerWidth, height: window.innerHeight })
86
+ yield* Effect.acquireRelease(
87
+ Effect.sync(() => window.addEventListener("resize", writeViewport)),
88
+ () => Effect.sync(() => window.removeEventListener("resize", writeViewport)),
89
+ )
90
+
91
+ // --- prefersReducedMotion ----------------------------------------------
92
+ // Guard `matchMedia` (absent in some headless environments): default false.
93
+ const mql = Bool.match(Predicate.isFunction(window.matchMedia), {
94
+ onTrue: () => Option.some(window.matchMedia("(prefers-reduced-motion: reduce)")),
95
+ onFalse: () => Option.none<MediaQueryList>(),
96
+ })
97
+ const prefersReducedMotion = Atom.keepAlive(Atom.make(Option.exists(mql, (query) => query.matches)))
98
+ if (Option.isSome(mql)) {
99
+ // The MediaQueryList's `change` listener is the Atom's only writer, and it writes inside the
100
+ // event, as `viewport`'s and `pageVisible`'s do: a reader sees the new preference before the next
101
+ // frame (#618, M8: a fiber over `Stream.fromEventListener` set it a task later).
102
+ const query = mql.value
103
+ const writeReduced = (event: MediaQueryListEvent): void => registry.set(prefersReducedMotion, event.matches)
104
+ yield* Effect.acquireRelease(
105
+ Effect.sync(() => query.addEventListener("change", writeReduced)),
106
+ () => Effect.sync(() => query.removeEventListener("change", writeReduced)),
107
+ )
108
+ }
109
+
110
+ // --- pageVisible (#443) ---------------------------------------------------
111
+ // The document's `visibilitychange` listener is its only writer, and it writes inside the event,
112
+ // as `viewport`'s does: a reader sees the page hidden before the next frame.
113
+ const pageVisible = Atom.keepAlive(Atom.make(document.visibilityState !== "hidden"))
114
+ const writePageVisible = (): void => registry.set(pageVisible, document.visibilityState !== "hidden")
115
+ yield* Effect.acquireRelease(
116
+ Effect.sync(() => document.addEventListener("visibilitychange", writePageVisible)),
117
+ () => Effect.sync(() => document.removeEventListener("visibilitychange", writePageVisible)),
118
+ )
119
+
120
+ // --- observeResize ------------------------------------------------------
121
+ // One ResizeObserver per target, ref-counted via `SharedSource.byTarget`. The
122
+ // observer is constructed on the first subscriber; `Effect.addFinalizer`
123
+ // disconnects it when the last subscriber's scope closes.
124
+ const observeResize = yield* SharedSource.byTarget<Probe.Bounds>((target) =>
125
+ Effect.succeed(
126
+ Stream.callback<Probe.Bounds>((queue) =>
127
+ Effect.gen(function* () {
128
+ const el = target as Element
129
+ const ro = new ResizeObserver(() => {
130
+ Queue.offerUnsafe(queue, toBounds(el.getBoundingClientRect()))
131
+ })
132
+ ro.observe(el)
133
+ yield* Effect.addFinalizer(() => Effect.sync(() => ro.disconnect()))
134
+ })
135
+ )
136
+ )
137
+ )
138
+
139
+ // --- observeIntersection ------------------------------------------------
140
+ // An Input feed (#318) with one IntersectionObserver per subscriber, against its options (#273,
141
+ // Motion's `viewport`): the root (the viewport when none), the margin (`rootMargin`) and the amount
142
+ // (`threshold`). Each report carries the entry.
143
+ const observeIntersection =
144
+ (target: unknown, options: Probe.IntersectionOptions = {}): InputFeed.InputFeed<Probe.Intersection> => (emit) => {
145
+ const io = new IntersectionObserver(
146
+ (entries) => Arr.forEach(entries, (entry) => emit({ intersecting: entry.isIntersecting, raw: entry })),
147
+ observerInit(options),
148
+ )
149
+ io.observe(target as Element)
150
+ return () => io.disconnect()
151
+ }
152
+
153
+ return Probe.Probe.of({
154
+ // Hot path — intentionally unlogged/un-spanned.
155
+ measure: (target) => Effect.sync(() => toBounds((target as Element).getBoundingClientRect())),
156
+ viewport,
157
+ prefersReducedMotion,
158
+ pageVisible,
159
+ // A container's scroll, set within its events (#234): `scroll.ts`.
160
+ scroll: yield* makeScroll(registry),
161
+ observeResize,
162
+ observeIntersection,
163
+ })
164
+ })
165
+
166
+ /**
167
+ * Core's `Probe` on the DOM. It needs the Environment's `AtomRegistry`, which it writes the viewport's
168
+ * size, the reduced-motion preference and whether the page is shown through, and readers read them from;
169
+ * `DomLive` provides it. Building it on a server fails, with an error that says it's client-only.
170
+ */
171
+ // Built with `Layer.effect`, so `make` runs in the layer's scope: the shared helpers' `RcMap`s and the
172
+ // forked listeners live for the layer's lifetime; `testLayer` provides the registry the same way.
173
+ // ADR-0021, decision 7.
174
+ export const ProbeDomLive: Layer.Layer<Probe.Probe, never, AtomRegistry.AtomRegistry> = Layer.effect(
175
+ Probe.Probe,
176
+ )(make)
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Motion's default layout and drag transitions, apart from the Projection system: a binding's layout
3
+ * element code reads them on every page, and the engine, `projection.ts`, ships only where an app lays
4
+ * out.
5
+ */
6
+ // ADR-0009 *Amendment (#171)*, ADR-0016 *Amendment (#140)*; #548; ADR-0067, *Amendment (#548)*.
7
+ import type * as Machine from "@wardic/core/Machine"
8
+
9
+ /**
10
+ * A layout animation's transition: a spring, its fields and its `delay`; or a tween, its `duration`,
11
+ * `ease` and `delay`. A repeat means nothing for a flight that lands once. A tween with no `duration` or
12
+ * `ease` takes the tween's own defaults, 0.3 s and linear (a binding fills Motion's `easeOut` first).
13
+ */
14
+ // ADR-0009 *Amendment (#171)*; #360; ADR-0051.
15
+ export type LayoutTransition = ({ readonly type: "spring" } & Machine.SpringOptions) | ({ readonly type: "tween" } & Machine.TweenOptions)
16
+
17
+ /**
18
+ * Motion's default layout transition (motion-dom 13.3.0, `defaultLayoutTransition`): a 0.45 s tween,
19
+ * eased `cubic-bezier(0.4, 0, 0.1, 1)`, so a layout animation lands on time.
20
+ */
21
+ // ADR-0009 *Amendment (#171)*.
22
+ export const DEFAULT_LAYOUT_TRANSITION: LayoutTransition = { type: "tween", duration: 0.45, ease: [0.4, 0, 0.1, 1] }
23
+
24
+ /**
25
+ * A drag's release (Motion's `dragTransition`): the inertia a node's Release offset runs on, with both
26
+ * edges at its slot, so it springs back there on the bounce. Times in seconds. Its edges are the slot's,
27
+ * never its own.
28
+ */
29
+ // ADR-0016, *Amendment (#140)*.
30
+ export type DragTransition = Omit<Machine.InertiaOptions, "min" | "max">
31
+
32
+ /**
33
+ * Motion's drag transition (framer-motion 13.4.0, `VisualElementDragControls.startAnimation`), with its
34
+ * default `dragElastic`: a 0.75 s time constant, power 0.8, the bounce 200 / 40, at rest within 1 px and
35
+ * 10 px/s. A node's Release offset runs on it when it's given none.
36
+ */
37
+ // ADR-0016, *Amendment (#140)*.
38
+ export const DEFAULT_DRAG_TRANSITION: DragTransition = {
39
+ power: 0.8,
40
+ timeConstant: 0.75,
41
+ bounceStiffness: 200,
42
+ bounceDamping: 40,
43
+ restDelta: 1,
44
+ restSpeed: 10,
45
+ }