@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,300 @@
1
+ /**
2
+ * SVG realization for the DOM renderer (ADR-0014). The core value pipeline +
3
+ * the pure `project` stay AGNOSTIC and flat: SVG numeric attrs (`cx`/`r`/…)
4
+ * already spring as numbers and `fill`/`stroke` already interpolate as colors.
5
+ * This module is the DOM-API ROUTING layer — the one place that decides whether
6
+ * a resolved value is written as a CSS `style` or an `setAttribute`, plus the
7
+ * SVG-specific shaping (`transform-box`, `pathLength`→dash). It is the seam a
8
+ * non-DOM SVG renderer (e.g. react-native-svg) would replace wholesale; nothing
9
+ * here is shared with `@wardic/core`.
10
+ *
11
+ * Model (Motion's `buildSVGAttrs`): for an svg-INNER element every key is an
12
+ * ATTRIBUTE except `transform`/`transformOrigin`/`transformBox` + the CSS
13
+ * motion-path props, which stay STYLE, and a custom property (`--…`), which is style
14
+ * under its own name on every element (#151). For an HTML element or the `<svg>` ROOT
15
+ * everything is style (the root has a CSS box; only `viewBox` would be an attr —
16
+ * deferred). So the HTML path is unchanged: its attribute map is always empty.
17
+ */
18
+ import type * as Renderer from "@wardic/core/Renderer"
19
+ import * as Bool from "effect/Boolean"
20
+ import * as Option from "effect/Option"
21
+ import * as Predicate from "effect/Predicate"
22
+
23
+ /**
24
+ * Keys that remain CSS `style` on an svg-inner element; everything else routes
25
+ * to an attribute. `transform` is applied as a CSS transform (with `transform-box`
26
+ * below); the motion-path props are CSS-only on SVG too (Motion parity).
27
+ */
28
+ const SVG_STYLE_KEYS: ReadonlySet<string> = new Set([
29
+ "transform",
30
+ "transformOrigin",
31
+ "transform-origin",
32
+ "transformBox",
33
+ "transform-box",
34
+ "offsetDistance",
35
+ "offsetPath",
36
+ "offsetRotate",
37
+ "offsetAnchor",
38
+ ])
39
+
40
+ /**
41
+ * SVG attribute names always written camelCase (never dash-cased) — Motion's
42
+ * `camelCaseAttributes`. Everything else is dash-cased (`strokeWidth` →
43
+ * `stroke-width`).
44
+ */
45
+ const CAMEL_CASE_ATTRS: ReadonlySet<string> = new Set([
46
+ "baseFrequency",
47
+ "diffuseConstant",
48
+ "kernelMatrix",
49
+ "kernelUnitLength",
50
+ "keySplines",
51
+ "keyTimes",
52
+ "limitingConeAngle",
53
+ "markerHeight",
54
+ "markerWidth",
55
+ "numOctaves",
56
+ "targetX",
57
+ "targetY",
58
+ "surfaceScale",
59
+ "specularConstant",
60
+ "specularExponent",
61
+ "stdDeviation",
62
+ "tableValues",
63
+ "viewBox",
64
+ "gradientTransform",
65
+ "pathLength",
66
+ "startOffset",
67
+ "textLength",
68
+ "lengthAdjust",
69
+ ])
70
+
71
+ /**
72
+ * Escape hatches: `x`/`y` are transform shorthands in Wardic (translateX/Y), so
73
+ * the SVG geometry ATTRIBUTE form is the explicit `attr*` key (Motion's
74
+ * `attrX`/`attrY`/`attrScale`). Maps to the bare attribute name.
75
+ */
76
+ const ATTR_ALIASES: Record<string, string> = { attrX: "x", attrY: "y", attrScale: "scale" }
77
+
78
+ /**
79
+ * Each key's dash-cased name, computed once (ADR-0034): the commit dash-cases every key of
80
+ * every patch, each frame. Keys come from animation configs, so the set stays small.
81
+ */
82
+ // Module state (#460, Q7): a cache keyed by what it caches: a bounded set of names.
83
+ const DASHED = new Map<string, string>()
84
+
85
+ /**
86
+ * Whether `key` is a CSS custom property (`--brandColor`): it keeps its name, which is
87
+ * case-sensitive, and is always a style property, on SVG too (ADR-0014, *Amendment (#151)*;
88
+ * Motion's `isCSSVariableName`).
89
+ */
90
+ const isCustomProperty = (key: string): boolean => key.startsWith("--")
91
+
92
+ /**
93
+ * camelCase → dash-case for a CSS/attribute name (`transformBox` → `transform-box`); a
94
+ * custom property keeps its name (`--brandColor`).
95
+ */
96
+ export const dashCase = (key: string): string => {
97
+ const cached = DASHED.get(key)
98
+ if (Predicate.isNotUndefined(cached)) return cached
99
+ const dashed = Bool.match(isCustomProperty(key), {
100
+ onTrue: () => key,
101
+ onFalse: () => key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`),
102
+ })
103
+ DASHED.set(key, dashed)
104
+ return dashed
105
+ }
106
+
107
+ /** The attribute name for an engine key: dash-cased unless it's a known camelCase attr. */
108
+ const attrName = (key: string): string => {
109
+ if (CAMEL_CASE_ATTRS.has(key)) return key
110
+ return dashCase(key)
111
+ }
112
+
113
+ /** Whether a key collapses into the stroke dash: `pathLength`, `pathSpacing` or `pathOffset`. */
114
+ export const isPathKey = (key: string): boolean => key === "pathLength" || key === "pathSpacing" || key === "pathOffset"
115
+
116
+ /**
117
+ * The attribute an svg-inner element's `key` is written as (the routing below), or
118
+ * `None` for a key that stays style ({@link SVG_STYLE_KEYS}, a custom property) or collapses into the
119
+ * stroke dash (`pathLength` / `pathSpacing` / `pathOffset`). Internal: the Own-value read
120
+ * reads the attribute a key is written as (ADR-0022).
121
+ */
122
+ export const svgAttrNameOf = (key: string): Option.Option<string> =>
123
+ Bool.match(isPathKey(key) || isCustomProperty(key), {
124
+ onTrue: () => Option.none(),
125
+ onFalse: () =>
126
+ Option.orElse(Option.fromUndefinedOr(ATTR_ALIASES[key]), () =>
127
+ Option.map(Option.liftPredicate(key, (k: string) => Bool.not(SVG_STYLE_KEYS.has(k))), attrName)
128
+ ),
129
+ })
130
+
131
+ /** Whether `el` is an `<svg>` root, which is styled as HTML is: it has a CSS box. */
132
+ export const isSvgRoot = (el: unknown): boolean => {
133
+ // No SVG classes on a server. Per frame (a flush's split): plain checks.
134
+ const SvgRoot = (globalThis as { readonly SVGSVGElement?: typeof SVGSVGElement }).SVGSVGElement
135
+ return Predicate.isNotUndefined(SvgRoot) && el instanceof SvgRoot
136
+ }
137
+
138
+ /** Whether `el` is an SVG element inside an `<svg>`: any SVG element but the root. */
139
+ export const isSvgInner = (el: unknown): boolean => {
140
+ const Svg = (globalThis as { readonly SVGElement?: typeof SVGElement }).SVGElement
141
+ return Predicate.isNotUndefined(Svg) && el instanceof Svg && !isSvgRoot(el)
142
+ }
143
+
144
+ /** A style patch, split: the keys written as CSS, and the ones written as attributes. */
145
+ export interface SplitPatch {
146
+ /** The keys written as CSS: the whole patch for HTML and an `<svg>` root, the very object given. */
147
+ readonly style: Readonly<Record<string, unknown>>
148
+ /** The keys written with `setAttribute`: none for HTML and an `<svg>` root. */
149
+ readonly attrs: Readonly<Record<string, string>>
150
+ }
151
+
152
+ /** An HTML element's attributes from a patch: none, one shared frozen record (#618: one a split). */
153
+ const NO_ATTRS: Readonly<Record<string, string>> = Object.freeze({})
154
+
155
+ /**
156
+ * A style patch split for `el`: an HTML element's, or an `<svg>` root's, is all style. An SVG element
157
+ * inside one takes every key as an attribute but the keys CSS styles and a custom property; `attrX`,
158
+ * `attrY` and `attrScale` as its `x`, `y` and `scale` attributes; a `transform` with `transform-box:
159
+ * fill-box` and `transform-origin: 50% 50%`, unless the values set them, so it turns about its own centre
160
+ * as an HTML element does; and `pathLength`, `pathSpacing` and `pathOffset` as a normalised
161
+ * `stroke-dasharray` and `stroke-dashoffset`, with `pathLength="1"`.
162
+ */
163
+ // The CSS keys: `SVG_STYLE_KEYS`.
164
+ export const splitPatch = (el: unknown, patch: Renderer.Patch): SplitPatch => {
165
+ if (isSvgInner(el)) return routeSvgInner(patch)
166
+ return { style: patch as Readonly<Record<string, unknown>>, attrs: NO_ATTRS }
167
+ }
168
+
169
+ /**
170
+ * SVG's element names that HTML doesn't share, lowercase (Motion's list,
171
+ * framer-motion/src/render/svg/lowercase-elements.ts @ 16c6122, without the `svg` root).
172
+ * A name with a capital letter (`clipPath`, `linearGradient`, `feGaussianBlur` …) is SVG
173
+ * too, as Motion's `isSVGComponent` decides; `a`, `script`, `style` and `title` count as
174
+ * HTML, since a server has no namespace to tell them apart.
175
+ */
176
+ const SVG_INNER_TAGS: ReadonlySet<string> = new Set([
177
+ "animate",
178
+ "circle",
179
+ "defs",
180
+ "desc",
181
+ "ellipse",
182
+ "g",
183
+ "image",
184
+ "line",
185
+ "filter",
186
+ "marker",
187
+ "mask",
188
+ "metadata",
189
+ "path",
190
+ "pattern",
191
+ "polygon",
192
+ "polyline",
193
+ "rect",
194
+ "stop",
195
+ "switch",
196
+ "symbol",
197
+ "text",
198
+ "tspan",
199
+ "use",
200
+ "view",
201
+ ])
202
+
203
+ /** Whether `tag` names an SVG element inside an `<svg>`, by its name alone. */
204
+ export const isSvgInnerTag = (tag: string): boolean => !tag.includes("-") && (SVG_INNER_TAGS.has(tag) || /[A-Z]/.test(tag))
205
+
206
+ /** `splitPatch` by tag name, for where there's no element yet: a server render, a starting style. `undefined` is an HTML element. */
207
+ // ADR-0021.
208
+ export const splitPatchForTag = (tag: string | undefined, patch: Renderer.Patch): SplitPatch =>
209
+ Bool.match(Predicate.isNotUndefined(tag) && isSvgInnerTag(tag), {
210
+ onTrue: () => routeSvgInner(patch),
211
+ onFalse: () => ({ style: patch as Readonly<Record<string, unknown>>, attrs: NO_ATTRS }),
212
+ })
213
+
214
+ /** The svg-inner routing {@link splitPatch} and {@link splitPatchForTag} share. */
215
+ const routeSvgInner = (patch: Renderer.Patch): SplitPatch => {
216
+ const style: Record<string, unknown> = {}
217
+ const attrs: Record<string, string> = {}
218
+ let hasTransform = false
219
+ let pathLength: unknown
220
+ let pathSpacing: unknown
221
+ let pathOffset: unknown
222
+
223
+ for (const key in patch) {
224
+ const value = patch[key]
225
+ if (Predicate.isUndefined(value)) continue
226
+ if (key === "pathLength") {
227
+ pathLength = value
228
+ continue
229
+ }
230
+ if (key === "pathSpacing") {
231
+ pathSpacing = value
232
+ continue
233
+ }
234
+ if (key === "pathOffset") {
235
+ pathOffset = value
236
+ continue
237
+ }
238
+ const alias: string | undefined = ATTR_ALIASES[key]
239
+ if (Predicate.isNotUndefined(alias)) {
240
+ attrs[alias] = String(value)
241
+ continue
242
+ }
243
+ if (SVG_STYLE_KEYS.has(key)) {
244
+ style[key] = value
245
+ if (key === "transform") hasTransform = true
246
+ continue
247
+ }
248
+ // A custom property is a style property (ADR-0014, *Amendment (#151)*).
249
+ if (isCustomProperty(key)) {
250
+ style[key] = value
251
+ continue
252
+ }
253
+ attrs[attrName(key)] = String(value)
254
+ }
255
+
256
+ // SVG's transform-origin defaults to the document origin (0 0), not the element
257
+ // centre — so a CSS transform on SVG reads differently from HTML. `fill-box` +
258
+ // a centred origin restore HTML-like semantics (Motion). User-set wins.
259
+ if (hasTransform) {
260
+ if (Predicate.isUndefined(style["transformBox"]) && Predicate.isUndefined(style["transform-box"])) {
261
+ style["transform-box"] = "fill-box"
262
+ }
263
+ if (Predicate.isUndefined(style["transformOrigin"]) && Predicate.isUndefined(style["transform-origin"])) {
264
+ style["transform-origin"] = "50% 50%"
265
+ }
266
+ }
267
+
268
+ // pathLength → normalised stroke dash (Motion's buildSVGPath). Unitless: Safari
269
+ // mis-scales px dash values when the page is zoomed.
270
+ if (Predicate.isNotUndefined(pathLength)) {
271
+ attrs["pathLength"] = "1"
272
+ attrs["stroke-dasharray"] = `${pathLength} ${spacingOf(pathSpacing)}`
273
+ attrs["stroke-dashoffset"] = String(dashOffsetOf(offsetOf(pathOffset)))
274
+ }
275
+
276
+ return { style, attrs }
277
+ }
278
+
279
+ // The stroke dash's parts, per frame: early returns.
280
+ /** `pathSpacing`, 1 when none is given. */
281
+ const spacingOf = (spacing: unknown): unknown => {
282
+ if (Predicate.isNullish(spacing)) return 1
283
+ return spacing
284
+ }
285
+ /** `pathOffset` as a number, 0 when none is given. */
286
+ const offsetOf = (offset: unknown): number => {
287
+ if (Predicate.isNumber(offset)) return offset
288
+ if (Predicate.isNullish(offset)) return 0
289
+ return Number(offset)
290
+ }
291
+ /** The dash offset an offset draws: its negation, 0 for one that isn't finite. */
292
+ const dashOffsetOf = (offset: number): number => {
293
+ if (Number.isFinite(offset)) return -offset
294
+ return 0
295
+ }
296
+
297
+ /** Writes each attribute onto `el` with `setAttribute`; nothing for an empty map. */
298
+ export const setSvgAttrs = (el: Element, attrs: Readonly<Record<string, string>>): void => {
299
+ for (const name in attrs) el.setAttribute(name, attrs[name])
300
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Variable values in the DOM (#55, ADR-0035 *Amendment (#55)*; CONTEXT.md): a value that is wholly a
3
+ * `var(--name)`, with or without a fallback, stands for that custom property's value on the element.
4
+ * `variableValues(element)` is a Key rule bound to one element, which a binding adds to each leg it
5
+ * builds (`InterpolatorRegistry.withKeyRules`). Motion's rule (`getVariableValue`,
6
+ * css-variables-conversion.ts:18-53, and `DOMKeyframesResolver.readKeyframes` at 16c6122):
7
+ *
8
+ * - Either end that is a Variable value is resolved against the element, once, when the leg is
9
+ * built: the custom property's computed value, else its fallback, following a fallback that is
10
+ * itself a Variable value up to 4 deep. A numeric result is a number.
11
+ * - The resolved pair goes on through `next`, so the rules after this one still apply (`"none"` →
12
+ * `var(--shadow)` grows from its Animatable none), then blends by value.
13
+ * - The leg's last frame writes the target as written, so at rest the element follows the custom
14
+ * property again; a change mid-leg is followed from there.
15
+ * - A Variable value that doesn't resolve leaves the pair as written: the rule declines, and it
16
+ * switches as a Discrete pair, as Motion's does. A `var()` inside a larger value is text (#158).
17
+ *
18
+ * Client-only: it reads the element's computed style, after mount (ADR-0021).
19
+ *
20
+ * @module
21
+ */
22
+ import type * as InterpolatorRegistry from "@wardic/core/InterpolatorRegistry"
23
+ import * as Arr from "effect/Array"
24
+ import * as Bool from "effect/Boolean"
25
+ import * as Effect from "effect/Effect"
26
+ import { pipe } from "effect/Function"
27
+ import * as Option from "effect/Option"
28
+ import * as Predicate from "effect/Predicate"
29
+ import * as Str from "effect/String"
30
+
31
+ /** A value a key blends: a number or a string. */
32
+ /** What a Variable value resolves to: a number or a string. */
33
+ type Value = number | string
34
+
35
+ /** A Variable value's custom property, and its fallback when it has one (Motion's `parseCSSVariable`). */
36
+ const PARTS_RE = /^var\(\s*(--[\w-]+)\s*(?:,\s*([\s\S]*?))?\s*\)$/
37
+
38
+ /** The cheap first check: a string that starts as a `var()` does. */
39
+ const VAR_START_RE = /^\s*var\(/
40
+
41
+ /** How a character moves the bracket depth. */
42
+ const depthStep = (ch: string): number => Bool.match(ch === "(", { onTrue: () => 1, onFalse: () => Bool.match(ch === ")", { onTrue: () => -1, onFalse: () => 0 }) })
43
+
44
+ /** Brackets that close as they open: a fallback that is one value, not the rest of a longer string. */
45
+ const isBalanced = (text: string): boolean => {
46
+ const end = Arr.reduce(Str.split(text, ""), 0, (depth, ch) => Bool.match(depth < 0, { onTrue: () => depth, onFalse: () => depth + depthStep(ch) }))
47
+ return end === 0
48
+ }
49
+
50
+ /** A Variable value's parts: its custom property's name and its fallback. */
51
+ interface Parts {
52
+ readonly name: string
53
+ readonly fallback: Option.Option<string>
54
+ }
55
+
56
+ /**
57
+ * A Variable value's parts, or `None` for any other value: `var(--name)` or `var(--name, fallback)`,
58
+ * the fallback's brackets balanced, at any depth of nested `var()`s (a pattern alone stops at one).
59
+ * So `var(--a, 1px) var(--b)` isn't one: two values side by side.
60
+ */
61
+ const partsOf = (value: string): Option.Option<Parts> => {
62
+ // Asked of a string end of every key, each leg (#460): one that doesn't start as one builds nothing.
63
+ if (!VAR_START_RE.test(value)) return Option.none()
64
+ return pipe(
65
+ Option.fromNullishOr(PARTS_RE.exec(Str.trim(value))),
66
+ Option.map(([, name, fallback]): Parts => ({ name, fallback: Option.map(Option.fromUndefinedOr(fallback), Str.trim) })),
67
+ Option.filter((parts) => Option.match(parts.fallback, { onNone: () => true, onSome: isBalanced })),
68
+ )
69
+ }
70
+
71
+ /** How deep a fallback that is itself a Variable value is followed (Motion's `maxDepth`). */
72
+ const MAX_DEPTH = 4
73
+
74
+ /** A numeric string, which a resolved value becomes a number from (Motion's `isNumericalString`). */
75
+ const NUMERIC_RE = /^-?\d*\.?\d+(?:e[-+]?\d+)?$/i
76
+
77
+ /** Whether `value` is a Variable value: a string that's wholly a `var()`. */
78
+ // CONTEXT.md's Variable value.
79
+ export const isVariableValue = (value: unknown): value is string => Predicate.isString(value) && Option.isSome(partsOf(value))
80
+
81
+ /** A resolved value as a key blends it: a number when it reads as one, else the string. */
82
+ const valueOf = (raw: string): Value => Bool.match(NUMERIC_RE.test(raw), { onTrue: () => Number.parseFloat(raw), onFalse: () => raw })
83
+
84
+ /**
85
+ * What a Variable value stands for on `element`: its custom property's computed value, else its
86
+ * fallback, a fallback that is itself a Variable value followed up to 4 deep. `None` when nothing
87
+ * resolves. Reads the element's computed style.
88
+ */
89
+ export const resolveVariable = (element: Element, value: string): Option.Option<Value> => resolveAt(element, value, 1)
90
+
91
+ /** {@link resolveVariable} at `depth`, following a fallback that's itself a Variable value. */
92
+ // #618 (F7): the depth counter was the public function's third parameter.
93
+ const resolveAt = (element: Element, value: string, depth: number): Option.Option<Value> => {
94
+ if (depth > MAX_DEPTH) return Option.none()
95
+ return Option.flatMap(partsOf(value), ({ name, fallback }) => {
96
+ const own = Str.trim(getComputedStyle(element).getPropertyValue(name))
97
+ if (Str.isNonEmpty(own)) return Option.some(valueOf(own))
98
+ return pipe(
99
+ fallback,
100
+ Option.filter(Str.isNonEmpty),
101
+ Option.flatMap((rest) =>
102
+ Bool.match(isVariableValue(rest), {
103
+ onTrue: () => resolveAt(element, rest, depth + 1),
104
+ onFalse: () => Option.some(valueOf(rest)),
105
+ })),
106
+ )
107
+ })
108
+ }
109
+
110
+ /** An end as the leg blends it: a Variable value resolved on `element`, any other value as it is. */
111
+ const endOf = (element: Element, value: Value): Option.Option<Value> =>
112
+ Bool.match(isVariableValue(value), {
113
+ onTrue: () => resolveVariable(element, value as string),
114
+ onFalse: () => Option.some(value),
115
+ })
116
+
117
+ /**
118
+ * A blend that writes its target as written on the run's last Frame: a Variable value rests as the
119
+ * `var()` itself, so the element follows its custom property again. Per frame: a statement `if`.
120
+ */
121
+ const landOn =
122
+ (blend: InterpolatorRegistry.KeyBlend, to: Value): InterpolatorRegistry.KeyBlend =>
123
+ (t, done) => {
124
+ if (done) return to
125
+ return blend(t, done)
126
+ }
127
+
128
+ /**
129
+ * The Key rule for `element`'s Variable values: a pair with one at either end blends what they resolve
130
+ * to on `element`, through the rules after it and then by value, and lands on its target as written. It
131
+ * declines a pair with none, and one that doesn't resolve.
132
+ *
133
+ * @example
134
+ * yield* buildLegs.pipe(InterpolatorRegistry.withKeyRules([Variables.values(element)]))
135
+ */
136
+ // #55.
137
+ export const variableValues = (element: Element): InterpolatorRegistry.KeyRule => ({
138
+ name: "variable values",
139
+ blend: (key, from, to, _resolve, next) => {
140
+ // Per key, each leg (#460): a statement.
141
+ if (!isVariableValue(from) && !isVariableValue(to)) return Option.none()
142
+ return Option.map(Option.all([endOf(element, from), endOf(element, to)]), ([a, b]) =>
143
+ Effect.map(next(key, a, b), (blend) => landOn(blend, to)))
144
+ },
145
+ })