@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,396 @@
1
+ /**
2
+ * The Starting style (ADR-0003, ADR-0021): an element's style before its first Frame,
3
+ * `project(resolveInitial(config))`, computed purely with no runtime. It is the server's
4
+ * markup, the style the hydration render keeps, and the animation's `from`. And the style
5
+ * after the first Frame, the fallback: the user's, without the keys Wardic holds (ADR-0034).
6
+ *
7
+ * Framework-free, so every binding renders it the same way on a server and on the client
8
+ * (the thin-bindings rule): a binding reads its config, calls these, and casts the style to
9
+ * its own JSX type. `toValues`, `resolveInitial` and the merge `over` does moved here from
10
+ * @wardic/solid with their behaviour unchanged; `over` and `without` are `dual` (#132's PR 3,
11
+ * ADR-0041 decision 5). Tier 2, for binding authors.
12
+ *
13
+ * Observability: pure and per element, so quiet (no span, no log).
14
+ */
15
+ import type * as Values from "@wardic/core/Values"
16
+ import * as Arr from "effect/Array"
17
+ import * as Bool from "effect/Boolean"
18
+ import { dual } from "effect/Function"
19
+ import * as Match from "effect/Match"
20
+ import * as Option from "effect/Option"
21
+ import * as Predicate from "effect/Predicate"
22
+ import * as Rec from "effect/Record"
23
+ import * as UndefinedOr from "effect/UndefinedOr"
24
+ import { project } from "./renderer.ts"
25
+ import { dashCase, type SplitPatch, splitPatchForTag } from "./svg.ts"
26
+
27
+ /**
28
+ * A values map with only the keys an animation can take: numbers and strings (custom
29
+ * properties included). `undefined` and anything else drop out, so the engine never sees
30
+ * them.
31
+ */
32
+ export const toValues = (v: object | undefined): Values.Values =>
33
+ UndefinedOr.match(v, {
34
+ onUndefined: (): Values.Values => ({}),
35
+ onDefined: (values) => Rec.filter(values as Readonly<Record<string, unknown>>, isAnimatable),
36
+ })
37
+
38
+ /** Whether a value is one an animation can take: a number or a string. */
39
+ const isAnimatable = Predicate.or(Predicate.isNumber, Predicate.isString)
40
+
41
+ /** A keyframes value's stops, as Motion's: each a value, or `null`, for where the key is (the first) or the stop before (a later one). */
42
+ // **Keyframes** (#429).
43
+ export type Stops = ReadonlyArray<number | string | null>
44
+
45
+ /** Where a keyframes value is read: `initial`'s at its first stop, a target's at its last. */
46
+ // **Keyframes** (#429).
47
+ export type StopAt = "first" | "last"
48
+
49
+ /** Whether a stop is one an animation can take, or `null`. */
50
+ const isStop = Predicate.or(isAnimatable, Predicate.isNull)
51
+
52
+ /**
53
+ * A Keyframes value's stop (#429): its first, as Motion's `initial` reads it (`use-visual-state.ts`),
54
+ * or its last that isn't `null`, where it rests (Motion's final keyframe, `fill-wildcards.ts`'s).
55
+ * `null` or `undefined` when it has none.
56
+ */
57
+ const stopOf = (stops: ReadonlyArray<unknown>, at: StopAt): unknown =>
58
+ Bool.match(at === "first", { onTrue: () => stops[0], onFalse: () => Option.getOrUndefined(Arr.findLast(stops, Predicate.isNotNull)) })
59
+
60
+ /** Whether a value is Keyframes an animation can take (#429): an array of stops, one of them not `null`. */
61
+ const isStops = (value: unknown): value is Stops => {
62
+ if (Bool.not(Arr.isArray(value))) return false
63
+ const stops = value as ReadonlyArray<unknown>
64
+ return Bool.and(Arr.every(stops, isStop), Arr.some(stops, Predicate.isNotNull))
65
+ }
66
+
67
+ /**
68
+ * The Keyframes {@link keepAnimatable} set aside while it filtered a values map (#452): written by it,
69
+ * read and cleared right after, never across a call. A target with none leaves it `undefined`.
70
+ */
71
+ // Module state (#460, Q7): scratch within one synchronous call, never held across one.
72
+ let setAside: Record<string, Stops> | undefined
73
+
74
+ /**
75
+ * A values map's filter (#452): a value an animation can take stays, and Keyframes go aside, into
76
+ * {@link setAside}, so one pass over the keys finds both. Built once, at module load.
77
+ */
78
+ const keepAnimatable = (value: unknown, key: string): boolean => {
79
+ if (Bool.not(Arr.isArray(value))) return isAnimatable(value)
80
+ if (isStops(value)) {
81
+ if (Predicate.isUndefined(setAside)) setAside = {}
82
+ setAside[key] = value
83
+ }
84
+ return false
85
+ }
86
+
87
+ /** A values map's numbers and strings, its Keyframes set aside (#452, {@link keepAnimatable}): {@link toValues}' one pass. */
88
+ const filtered = (v: object | undefined): Values.Values => {
89
+ // Per element at mount (#540): statements and a loop.
90
+ setAside = undefined
91
+ const out: Record<string, number | string> = {}
92
+ if (Predicate.isUndefined(v)) return out
93
+ const values = v as Readonly<Record<string, unknown>>
94
+ for (const key of Object.keys(values)) {
95
+ const value = values[key]
96
+ // `keepAnimatable` keeps numbers and strings alone, as `isAnimatable` does.
97
+ if (keepAnimatable(value, key)) out[key] = value as number | string
98
+ }
99
+ return out
100
+ }
101
+
102
+ /** The Keyframes the last {@link filtered} set aside, taken: `undefined` with none. */
103
+ const takeSetAside = (): Readonly<Record<string, Stops>> | undefined => {
104
+ const taken = setAside
105
+ setAside = undefined
106
+ return taken
107
+ }
108
+
109
+ /** `values` with each of `keyframes` at one of its stops (#429), one with no such stop left out. */
110
+ const withStops = (values: Values.Values, keyframes: Readonly<Record<string, Stops>>, at: StopAt): Values.Values => ({
111
+ ...values,
112
+ ...toValues(Rec.map(keyframes, (stops) => stopOf(stops, at))),
113
+ })
114
+
115
+ /** A values map's own values and its keyframes: `splitValues`' result. */
116
+ // #452.
117
+ export interface SplitValues {
118
+ /** Its numbers and strings, each Keyframes value at one of its stops. */
119
+ readonly values: Values.Values
120
+ /** Its Keyframes, if it has any. */
121
+ // An `Option` (#618, A1): none is Effect's one `None`, so a target with no Keyframes allocates nothing.
122
+ readonly keyframes: Option.Option<Readonly<Record<string, Stops>>>
123
+ }
124
+
125
+ /**
126
+ * A values map's values, each keyframes value read at one of its stops, and its keyframes, in one pass
127
+ * over its keys: a target's resolution reads both, and a target with no keyframes makes nothing
128
+ * `toValues` doesn't.
129
+ */
130
+ // **Keyframes** (#429); one pass (#452).
131
+ export const splitValues = (v: object | undefined, at: StopAt): SplitValues => {
132
+ const values = filtered(v)
133
+ const keyframes = takeSetAside()
134
+ if (Predicate.isUndefined(keyframes)) return { values, keyframes: Option.none() }
135
+ return { values: withStops(values, keyframes, at), keyframes: Option.some(keyframes) }
136
+ }
137
+
138
+ /**
139
+ * A values map with only the keys an animation can take, each keyframes value read at one of its stops:
140
+ * `initial`'s first, a target's last. A keyframes value with no such stop drops out, as does an array
141
+ * `keyframesOf` doesn't take, and anything that isn't a number or a string. One pass over its keys, as
142
+ * `toValues`'.
143
+ *
144
+ * @example
145
+ * Style.valuesAt({ opacity: [0, 0.5, 1], x: 10 }, "first") // { x: 10, opacity: 0 }
146
+ */
147
+ // **Keyframes** (#429); one pass (#452).
148
+ export const valuesAt = (v: object | undefined, at: StopAt): Values.Values => {
149
+ const values = filtered(v)
150
+ const keyframes = takeSetAside()
151
+ if (Predicate.isUndefined(keyframes)) return values
152
+ return withStops(values, keyframes, at)
153
+ }
154
+
155
+ /**
156
+ * A values map's keyframes values: each array of stops an animation can take, one with a stop that isn't
157
+ * `null`. None when it has none, so a target without any makes nothing.
158
+ *
159
+ * @example
160
+ * Style.keyframesOf({ opacity: [0, 0.5, 1], x: 10 }) // { opacity: [0, 0.5, 1] }
161
+ */
162
+ // **Keyframes** (#429).
163
+ export const keyframesOf = (v: object | undefined): Option.Option<Readonly<Record<string, Stops>>> => splitValues(v, "last").keyframes
164
+
165
+ /**
166
+ * What of an element's config its starting style is read from. Its values are typed `object`, so a
167
+ * binding's own value types fit as they are.
168
+ */
169
+ export interface InitialConfig {
170
+ /**
171
+ * Its start: values used as they are, the name of a `variants` entry, a list of names merged in order,
172
+ * a later one's winning, or `false`, to start at `animate`, as Motion's `initial={false}`.
173
+ */
174
+ // #297 (lists); #134 (`false`).
175
+ readonly initial?: object | string | ReadonlyArray<string> | false | undefined
176
+ /** What `initial: false` starts at: a values map, the name of a `variants` entry, or a list of names. */
177
+ readonly animate?: object | string | ReadonlyArray<string> | undefined
178
+ /** Its variants: the values each name stands for. */
179
+ readonly variants?: Readonly<Record<string, object | undefined>> | undefined
180
+ }
181
+
182
+ /** One values entry, the variants a name in it would name, and the stop its Keyframes values are read at (#429). */
183
+ interface Entry {
184
+ readonly entry: object | string | ReadonlyArray<string> | undefined
185
+ readonly variants: InitialConfig["variants"]
186
+ readonly at: StopAt
187
+ }
188
+
189
+ /** Whether an entry is a list of names (#297): an array, which a values map never is. */
190
+ const isList = (entry: unknown): entry is ReadonlyArray<string> => Arr.isArray(entry)
191
+
192
+ /**
193
+ * A values map, or the `variants` entry a name names (an unknown name is empty), or nothing.
194
+ * Built once, at module load: every element reads its own at setup (#69).
195
+ */
196
+ const entryOf: (entry: Entry) => Values.Values = Match.type<Entry>().pipe(
197
+ Match.when({ entry: Match.undefined }, (): Values.Values => ({})),
198
+ Match.when({ entry: Match.string }, ({ entry, variants, at }) => valuesAt(UndefinedOr.map(variants, (byName) => byName[entry]), at)),
199
+ // A list's names merged in order, a later one's winning a key both set (#297, Motion's).
200
+ Match.when({ entry: isList }, ({ entry, variants, at }) =>
201
+ Arr.reduce(entry, {} as Values.Values, (merged, name) => ({ ...merged, ...valuesAt(UndefinedOr.map(variants, (byName) => byName[name]), at) }))),
202
+ Match.orElse(({ entry, at }) => valuesAt(entry as object, at)),
203
+ )
204
+
205
+ /**
206
+ * The values an element starts from: its `initial` values, or the `variants` entries its `initial` names,
207
+ * an unknown name empty, or nothing. With `initial: false`, its `animate`, resolved the same way, so it
208
+ * starts at its target and nothing animates at mount, as Motion's. A keyframes value starts at its first
209
+ * stop, or, with `initial: false`, at `animate`'s last, where the animation it skips would end. Read once,
210
+ * at setup: the starting style is where it starts, never a live target.
211
+ *
212
+ * @example
213
+ * Style.resolveInitial({ initial: false, animate: { opacity: 1 } }) // { opacity: 1 }
214
+ */
215
+ // #134, Motion's `initial={false}`; **Keyframes** (#429, Motion's `use-visual-state.ts`).
216
+ export const resolveInitial = (config: InitialConfig): Values.Values => {
217
+ // Per element at mount (#540): statements.
218
+ if (config.initial === false) return entryOf({ entry: config.animate, variants: config.variants, at: "last" })
219
+ return entryOf({ entry: config.initial as object | string | ReadonlyArray<string> | undefined, variants: config.variants, at: "first" })
220
+ }
221
+
222
+ /**
223
+ * A CSS declaration list (`"color: red; background: url(a;b)"`) as `{ property: value }`.
224
+ * A `;` inside parentheses or quotes doesn't end a declaration. Internal (not in the
225
+ * package index): `own-values.ts`'s style lookup shares it.
226
+ */
227
+ export const parseCssText = (text: string): Record<string, string> => {
228
+ const out: Record<string, string> = {}
229
+ // A counted scan over UTF-16 units, as `slice` counts: a `;` outside parentheses and quotes ends a
230
+ // declaration, and a backslash in a quote escapes the next unit (#618, F6: a reduce built a scan
231
+ // state and a matcher each character, 15-25 times as slow).
232
+ let depth = 0
233
+ let quote = 0
234
+ let escaped = false
235
+ let start = 0
236
+ for (let i = 0; i < text.length; i++) {
237
+ const c = text.charCodeAt(i)
238
+ if (escaped) {
239
+ escaped = false
240
+ continue
241
+ }
242
+ if (quote !== 0) {
243
+ if (c === BACKSLASH) escaped = true
244
+ if (c === quote) quote = 0
245
+ continue
246
+ }
247
+ if (c === DOUBLE_QUOTE || c === SINGLE_QUOTE) quote = c
248
+ if (c === OPEN) depth++
249
+ if (c === CLOSE) depth = Math.max(0, depth - 1)
250
+ if (c === SEMICOLON && depth === 0) {
251
+ takeDeclaration(text, start, i, out)
252
+ start = i + 1
253
+ }
254
+ }
255
+ takeDeclaration(text, start, text.length, out)
256
+ return out
257
+ }
258
+
259
+ const BACKSLASH = 92
260
+ const DOUBLE_QUOTE = 34
261
+ const SINGLE_QUOTE = 39
262
+ const OPEN = 40
263
+ const CLOSE = 41
264
+ const SEMICOLON = 59
265
+
266
+ /** The declaration in `text` from `start` to `end` into `out`: its property and value, trimmed, when it has both. */
267
+ const takeDeclaration = (text: string, start: number, end: number, out: Record<string, string>): void => {
268
+ const declaration = text.slice(start, end)
269
+ const colon = declaration.indexOf(":")
270
+ if (colon <= 0) return
271
+ const key = declaration.slice(0, colon).trim()
272
+ const value = declaration.slice(colon + 1).trim()
273
+ if (key !== "" && value !== "") out[key] = value
274
+ }
275
+
276
+ /** Where {@link parseCssText} is: the parentheses open, the string's quote, a backslash just read in it, the declaration's start. */
277
+ /**
278
+ * A style as a binding hands it over: a record of CSS properties, CSS text (`"color: red"`, which
279
+ * JSX allows), or none.
280
+ */
281
+ export type StyleInput = object | string | undefined
282
+
283
+ /** A style record: CSS properties to their values. */
284
+ export type StyleRecord = Readonly<Record<string, unknown>>
285
+
286
+ /** A style given as CSS text, parsed; a record as it is. */
287
+ const recordOf = (input: object | string): StyleRecord => {
288
+ // Per element at mount (#540): statements.
289
+ if (Predicate.isString(input)) return parseCssText(input)
290
+ return input as StyleRecord
291
+ }
292
+
293
+ /** A style given as CSS text, parsed; a record as it is; none, an empty record. */
294
+ const recordOfInput = (input: StyleInput): StyleRecord => {
295
+ // Per element at mount (#540): statements.
296
+ if (Predicate.isUndefined(input)) return {}
297
+ return recordOf(input)
298
+ }
299
+
300
+ const setsKey =
301
+ (key: string): Predicate.Predicate<StyleRecord> =>
302
+ (style) =>
303
+ Predicate.isNotUndefined(style[key])
304
+
305
+ /**
306
+ * Whether a style sets `user-select` and not `-webkit-user-select`, and the reverse. Chromium has
307
+ * one property under both names, so a value set under one is the value for both.
308
+ */
309
+ const setsOnlyUserSelect = Predicate.and(setsKey("user-select"), Predicate.not(setsKey("-webkit-user-select")))
310
+ const setsOnlyWebkitUserSelect = Predicate.and(setsKey("-webkit-user-select"), Predicate.not(setsKey("user-select")))
311
+
312
+ /** `self` laid over `that`: both present. */
313
+ const layered = (self: StyleRecord, that: StyleRecord): StyleRecord => {
314
+ const merged: Record<string, unknown> = { ...that, ...self }
315
+ // `self`'s value for either name is the value for both, or `that`'s left on the other wins.
316
+ if (setsOnlyUserSelect(self)) merged["-webkit-user-select"] = self["user-select"]
317
+ if (setsOnlyWebkitUserSelect(self)) merged["user-select"] = self["-webkit-user-select"]
318
+ return merged
319
+ }
320
+
321
+ /**
322
+ * `self` laid over `that`: `self`'s keys win, and `that`'s others stay. A chain reads from the top layer
323
+ * down: an element's style is its drag defaults, under its base, under the app's, under what Wardic
324
+ * animates. A side given as CSS text is parsed, so a framework never writes `cssText` over the keys
325
+ * Wardic holds, and a side that's `undefined` leaves the other as it is, the same object. Whatever `self`
326
+ * sets for `user-select` or `-webkit-user-select` it sets for both: one property in Chromium.
327
+ *
328
+ * @example
329
+ * // Before the first commit: the starting style over the app's, over a base and a drag element's defaults.
330
+ * pipe(starting, Style.over(user), Style.over(base), Style.over(Style.dragDefaults("x")))
331
+ */
332
+ // `dual`, `self` first (ADR-0041, decision 5). The layering: ADR-0019 *Amendment (Reorder)*.
333
+ export const over: {
334
+ (that: StyleInput): (self: StyleInput) => StyleRecord
335
+ (self: StyleInput, that: StyleInput): StyleRecord
336
+ } = dual(2, (self: StyleInput, that: StyleInput): StyleRecord => {
337
+ // Per element at mount (#540): statements.
338
+ if (Predicate.isUndefined(that)) return recordOfInput(self)
339
+ if (Predicate.isUndefined(self)) return recordOf(that)
340
+ return layered(recordOf(self), recordOf(that))
341
+ })
342
+
343
+ /** Whether `key`, a CSS property, is one of the gate's held patch keys: by its name, or its name dash-cased. */
344
+ const isHeld = (held: ReadonlyArray<string>, key: string): boolean => {
345
+ // Per element at mount (#540): a loop.
346
+ for (const patchKey of held) {
347
+ if (patchKey === key || dashCase(patchKey) === key) return true
348
+ }
349
+ return false
350
+ }
351
+
352
+ /**
353
+ * `self` without the keys the flush gate holds on the element (`held`, by patch key, matched by CSS
354
+ * property): the style once its first frame is committed. Wardic writes those while it drives them, and
355
+ * the app's `style` is their fallback, so a key the gate lets go of comes back.
356
+ *
357
+ * @example
358
+ * pipe(user, Style.over(base), Style.without(held))
359
+ */
360
+ // ADR-0034.
361
+ export const without: {
362
+ (held: ReadonlyArray<string>): (self: StyleInput) => StyleRecord
363
+ (self: StyleInput, held: ReadonlyArray<string>): StyleRecord
364
+ } = dual(2, (self: StyleInput, held: ReadonlyArray<string>): StyleRecord => {
365
+ // Per element at mount (#540): a loop.
366
+ const record = recordOfInput(self)
367
+ const out: Record<string, unknown> = {}
368
+ for (const key of Object.keys(record)) {
369
+ if (!isHeld(held, key)) out[key] = record[key]
370
+ }
371
+ return out
372
+ })
373
+
374
+ /**
375
+ * Whether the keys the flush gate holds, changing from `prev` to `next`, change `self`'s `without`:
376
+ * whether a key taken or let go of is one `self` sets, by its name or its name dash-cased. Most changes
377
+ * touch none, and a binding then leaves its `style` alone.
378
+ */
379
+ // ADR-0034.
380
+ export const heldTouches: {
381
+ (prev: ReadonlyArray<string>, next: ReadonlyArray<string>): (self: StyleRecord) => boolean
382
+ (self: StyleRecord, prev: ReadonlyArray<string>, next: ReadonlyArray<string>): boolean
383
+ } = dual(3, (self: StyleRecord, prev: ReadonlyArray<string>, next: ReadonlyArray<string>): boolean =>
384
+ Arr.some([...Arr.difference(prev, next), ...Arr.difference(next, prev)], (key) =>
385
+ Arr.some([key, dashCase(key)], (name) => Object.hasOwn(self, name)),
386
+ ),
387
+ )
388
+
389
+ /**
390
+ * The starting style of a `tag` element starting from `values`: projected as the renderer projects a
391
+ * Frame, then split by tag, an SVG element's keys inside an `<svg>` as attributes, and the rest as style.
392
+ * `undefined` is an HTML element, as for a spread that doesn't know its tag.
393
+ */
394
+ // The Starting style; routed by tag (ADR-0014). The spread: a `createWard` spread.
395
+ export const startingStyle = (tag: string | undefined, values: Readonly<Record<string, unknown>>): SplitPatch =>
396
+ splitPatchForTag(tag, project(values))