@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,46 @@
1
+ /**
2
+ * DomLive is client-only, and says so (ADR-0021, decision 7). A server render builds no
3
+ * runtime, so building a DomLive Layer on a server is always a mistake: each Layer that
4
+ * needs a browser global checks for it as its build starts, and fails with one message
5
+ * naming the Layer, the global and the rule, instead of a `ReferenceError` from deep
6
+ * inside the build.
7
+ *
8
+ * Read when the Layer builds, never when `@wardic/dom` is imported. Once per build, so
9
+ * no log or span (the cold path's message is the error itself).
10
+ */
11
+ import * as Bool from "effect/Boolean"
12
+ import * as Effect from "effect/Effect"
13
+ import * as Predicate from "effect/Predicate"
14
+
15
+ /** The browser globals a DomLive Layer can need. */
16
+ export type BrowserGlobal = "requestAnimationFrame" | "document" | "window"
17
+
18
+ /** The DOM's Layers that need a browser global, by the names an app knows them by. */
19
+ export type ClientLayer = "DomClock.layer" | "DomInput.layer" | "DomProbe.layer" | "DomAnnouncer.layer"
20
+
21
+ /** Where an app gets each: three are `DomLive`'s, and a gesture builds the input. */
22
+ const FROM: Readonly<Record<ClientLayer, string>> = {
23
+ "DomClock.layer": "DomLive's",
24
+ "DomProbe.layer": "DomLive's",
25
+ "DomAnnouncer.layer": "DomLive's",
26
+ "DomInput.layer": "a gesture's",
27
+ }
28
+
29
+ /**
30
+ * Fail the build of `layer` with ADR-0021's client-only error when `global` doesn't exist.
31
+ * Make it the first step of the build: anything that reads the global must come after it.
32
+ */
33
+ // #618 (D7): the error named the Layers ADR-0041 renamed (`ClockDomLive`…), and an ADR; `layer` is a
34
+ // union of today's names, so a rename can't leave it behind. Short, as every app ships it.
35
+ export const requireGlobal = (layer: ClientLayer, global: BrowserGlobal): Effect.Effect<void> =>
36
+ Effect.suspend(() =>
37
+ Bool.match(Predicate.isUndefined((globalThis as Record<string, unknown>)[global]), {
38
+ onTrue: () =>
39
+ Effect.die(
40
+ new Error(
41
+ `${layer} (${FROM[layer]}) needs ${global}: Wardic's runtime is client-only, never built during a server render.`,
42
+ ),
43
+ ),
44
+ onFalse: () => Effect.void,
45
+ })
46
+ )
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Keep a **Keyboard reorder**'s item in view (ADR-0037, Q13): after a step, scroll its new place
3
+ * into view, as little as needed, in each scroller that moves it and then the page, as
4
+ * `scrollIntoView({ block: "nearest", inline: "nearest" })` would. Its new place, not where it's
5
+ * drawn: a step's layout animation starts it where it was, so the box is the element's painted one
6
+ * with its own transform (a translate and a scale about its origin) undone. Instant, so it never
7
+ * runs against the step's own animation, and with nothing for reduced motion to stop.
8
+ *
9
+ * @internal for the Reorder controller.
10
+ */
11
+ import { scrollersOf } from "./scrollers.ts"
12
+ import * as Arr from "effect/Array"
13
+
14
+ /** A box on screen, in client coordinates. */
15
+ interface Rect {
16
+ readonly left: number
17
+ readonly top: number
18
+ readonly right: number
19
+ readonly bottom: number
20
+ }
21
+
22
+ /**
23
+ * `el`'s layout box on screen: its painted box with its own transform undone. A translate and a
24
+ * scale (the projection's) about its `transform-origin`; a box drawn with no transform is as painted.
25
+ */
26
+ export const layoutRectOf = (el: Element): Rect => {
27
+ const painted = el.getBoundingClientRect()
28
+ const style = getComputedStyle(el)
29
+ const m = new DOMMatrixReadOnly(style.transform)
30
+ if (m.isIdentity) return painted
31
+ if (m.a === 0) return painted
32
+ if (m.d === 0) return painted
33
+ const [ox = 0, oy = 0] = style.transformOrigin.split(" ").map((v) => Number.parseFloat(v))
34
+ const left = painted.left - m.e - ox * (1 - m.a)
35
+ const top = painted.top - m.f - oy * (1 - m.d)
36
+ return { left, top, right: left + painted.width / m.a, bottom: top + painted.height / m.d }
37
+ }
38
+
39
+ /**
40
+ * How far to scroll a view from `viewMin` to `viewMax` so `min` to `max` shows: 0 when it does, else
41
+ * the least that brings it in, its start first when it can't all fit.
42
+ */
43
+ const scrollToShow = (min: number, max: number, viewMin: number, viewMax: number): number => {
44
+ if (min < viewMin) return min - viewMin
45
+ if (max > viewMax) return Math.min(max - viewMax, min - viewMin)
46
+ return 0
47
+ }
48
+
49
+ /** Scroll `el`'s new place into view (see the module doc). */
50
+ export const revealLayoutOf = (el: Element): void => {
51
+ const { ancestors, page } = scrollersOf(el)
52
+ // Each scroller in turn, nearest first: what it scrolls moves the element for the next.
53
+ const rect = Arr.reduce(ancestors, layoutRectOf(el), (rect, scroller) => {
54
+ const box = scroller.getBoundingClientRect()
55
+ const top = box.top + scroller.clientTop
56
+ const left = box.left + scroller.clientLeft
57
+ const dy = scrollToShow(rect.top, rect.bottom, top, top + scroller.clientHeight)
58
+ const dx = scrollToShow(rect.left, rect.right, left, left + scroller.clientWidth)
59
+ if (dx === 0 && dy === 0) return rect
60
+ scroller.scrollBy({ left: dx, top: dy, behavior: "instant" })
61
+ return { left: rect.left - dx, top: rect.top - dy, right: rect.right - dx, bottom: rect.bottom - dy }
62
+ })
63
+ const win = el.ownerDocument.defaultView
64
+ if (page && win !== null) {
65
+ const dy = scrollToShow(rect.top, rect.bottom, 0, win.innerHeight)
66
+ const dx = scrollToShow(rect.left, rect.right, 0, win.innerWidth)
67
+ if (dx !== 0 || dy !== 0) win.scrollBy({ left: dx, top: dy, behavior: "instant" })
68
+ }
69
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A Layer-built service's runner (#132, ADR-0041): how `FlushGate.make` and `DomProjectionSystem.make`
3
+ * run their Effects after they're built, synchronously and forked, on the context their Layer was
4
+ * built in, forking into its Scope.
5
+ *
6
+ * The context drops a sync scheduler, and keeps any other (ADR-0041, *Amendment (#208)*). A
7
+ * binding's first read builds the Layer inside a `ManagedRuntime.runSync`, whose fiber carries a sync
8
+ * `MixedScheduler`: kept, it would pin every later fork to that scheduler, which resumes a fork's
9
+ * yields on a microtask, where Effect's default resumes them on a macrotask. Without it, forks use
10
+ * the default, as a `ManagedRuntime`'s do. A host's own async scheduler, provided in the Layers it
11
+ * hands Wardic (Foldkit's microtask `MixedScheduler`), is the host's choice: forks keep it (ADR-0027,
12
+ * decision 6: a fork inherits the scheduler it's forked under). A host that provides a *sync* one is
13
+ * overridden too: a sync scheduler is for draining a `runSync`.
14
+ */
15
+ import * as Bool from "effect/Boolean"
16
+ import * as Context from "effect/Context"
17
+ import * as Effect from "effect/Effect"
18
+ import type * as Fiber from "effect/Fiber"
19
+ import * as FiberSet from "effect/FiberSet"
20
+ import * as Scheduler from "effect/Scheduler"
21
+ import * as Scope from "effect/Scope"
22
+
23
+ /** Runs Effects synchronously, and forked into the Scope it was made in. */
24
+ export interface Runner<R> {
25
+ readonly runSync: <A, E>(effect: Effect.Effect<A, E, R>) => A
26
+ readonly runFork: <A, E>(effect: Effect.Effect<A, E, R>) => Fiber.Fiber<A, E>
27
+ /**
28
+ * The Scope it was made in: what a service built with it keeps its own fibers in, so closing the
29
+ * Scope ends them, after the service's own cleanup, and awaits them.
30
+ */
31
+ // #618 (L1): each service opened a detached Scope of its own, closed by a fork or a throwing runSync.
32
+ readonly scope: Scope.Scope
33
+ }
34
+
35
+ /**
36
+ * A runner over the current context, without a sync scheduler, forking into a `FiberSet` in the
37
+ * current Scope. It forks with that context itself (`Effect.runForkWith`), not through
38
+ * `FiberSet.runtime`, which reads the context again in the current fiber, where the building call's
39
+ * scheduler still is: `Effect.provideContext` merges into a fiber's context, it doesn't replace it.
40
+ */
41
+ // Not `Effect.fnUntraced`, as effect-4's guide prefers: TypeScript can't carry `R` through it, and the
42
+ // runner's context would be `unknown` (#618, H1).
43
+ export const makeRunner = <R>(): Effect.Effect<Runner<R>, never, R | Scope.Scope> =>
44
+ Effect.gen(function* () {
45
+ const context = withoutSyncScheduler(yield* Effect.context<R | Scope.Scope>())
46
+ const fibers = yield* FiberSet.make()
47
+ const fork = Effect.runForkWith(context)
48
+ const runFork = <A, E>(effect: Effect.Effect<A, E, R>): Fiber.Fiber<A, E> => {
49
+ const fiber = fork(effect)
50
+ FiberSet.addUnsafe(fibers, fiber)
51
+ return fiber
52
+ }
53
+ return { runSync: Effect.runSyncWith(context), runFork, scope: Context.get(context, Scope.Scope) }
54
+ })
55
+
56
+ /**
57
+ * `context` without its scheduler when that one is sync (`runSync`'s), else as it is: a host's
58
+ * async scheduler stays, and with none stored the context has Effect's default already.
59
+ */
60
+ const withoutSyncScheduler = <S>(context: Context.Context<S>): Context.Context<S> =>
61
+ Bool.match(Context.get(context, Scheduler.Scheduler).executionMode === "sync", {
62
+ onTrue: () => Context.omit(Scheduler.Scheduler)(context),
63
+ onFalse: () => context,
64
+ })
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Scale-correction registry (ADR-0011) — the visual-correction half of the
3
+ * projection (ADR-0009). A scaling projection counter-scales an element's
4
+ * geometry via the transform, but non-geometric CSS — `border-radius`,
5
+ * `box-shadow` — is still distorted by that scale. Each {@link ScaleCorrector}
6
+ * reads the element's base value at measure time and, per frame, emits a CSS
7
+ * string that — once the transform scales it — renders as the AUTHORED value.
8
+ *
9
+ * The projection coordinator owns the pipeline (it has the element + the per-frame
10
+ * `delta`/`treeScale`); the corrected value is written into the node's gate entry
11
+ * as an ordinary style key, so the Renderer stays pure (ADR-0002/0006) — it just
12
+ * applies what the projection computed.
13
+ *
14
+ * Units: `getComputedStyle` resolves em/rem/etc. to px, leaving only **px**
15
+ * (corrected via `/scale`) and **`%`** (self-correcting — a percentage stays the
16
+ * same fraction of each scaled dimension, so it passes through). Reading resolved
17
+ * values therefore gives full-grammar parity.
18
+ */
19
+ import type * as Box from "@wardic/core/Box"
20
+ import type * as Point from "@wardic/core/Point"
21
+ import type * as Projection from "@wardic/core/Projection"
22
+ import * as Predicate from "effect/Predicate"
23
+
24
+ /**
25
+ * What an app's scale corrector is handed beside the value: the projection node's fields Motion's
26
+ * correctors read, as Motion names them. `target` is the box the node is drawn into this frame,
27
+ * `treeScale` its ancestors' scale, `projectionDelta` its delta.
28
+ */
29
+ // #443.
30
+ export interface CorrectorInfo {
31
+ /** The box the node is drawn into this frame. */
32
+ readonly target: Box.Box
33
+ /** Its ancestors' scale, multiplied together. */
34
+ readonly treeScale: Point.Point
35
+ /** Its delta, from its layout box to `target`. */
36
+ readonly projectionDelta: Projection.Delta
37
+ }
38
+
39
+ /**
40
+ * An app's scale corrector (Motion's `addScaleCorrector` entry), given to a provider by the value key it
41
+ * corrects (`borderRadius`, `--card-radius`): `correct` turns the element's value into the one to write
42
+ * while its projection scales it, written under `applyTo`'s keys when given, else its own.
43
+ */
44
+ // #443.
45
+ export interface ScaleCorrectorEntry {
46
+ /** The value to write while the projection scales the element, from its own value, `latest`, and the node's fields. */
47
+ readonly correct: (latest: string | number, info: CorrectorInfo) => string | number
48
+ /** The keys the corrected value is written under: its own key when left out. */
49
+ readonly applyTo?: ReadonlyArray<string> | undefined
50
+ }
51
+
52
+ /** A registered scale corrector — owns reading its base + emitting the corrected value. */
53
+ export interface ScaleCorrector {
54
+ /** The gate value-map / CSS key the corrected value is written under. */
55
+ readonly key: string
56
+ /**
57
+ * Read the element's base value(s) at measure time (read phase), or `undefined`
58
+ * if there is nothing to correct (e.g. radius all `0px`, shadow `none`).
59
+ */
60
+ readonly read: (el: HTMLElement) => unknown
61
+ /** Emit the corrected CSS string for the per-axis visual scale `(sx, sy)`. */
62
+ readonly correct: (base: unknown, sx: number, sy: number) => string
63
+ }
64
+
65
+ const EPS = 1e-3
66
+ /** Whether a per-axis scale is ~identity (no correction needed). */
67
+ export const isUnitScale = (sx: number, sy: number): boolean => Math.abs(sx - 1) < EPS && Math.abs(sy - 1) < EPS
68
+
69
+ // --- border-radius -----------------------------------------------------------
70
+
71
+ /** One corner's horizontal + vertical radius as authored strings (px or %). */
72
+ interface CornerRadius {
73
+ readonly h: string
74
+ readonly v: string
75
+ }
76
+ /** The four corners, in CSS order: top-left, top-right, bottom-right, bottom-left. */
77
+ type RadiusBase = readonly [CornerRadius, CornerRadius, CornerRadius, CornerRadius]
78
+
79
+ const CORNER_PROPS = [
80
+ "borderTopLeftRadius",
81
+ "borderTopRightRadius",
82
+ "borderBottomRightRadius",
83
+ "borderBottomLeftRadius",
84
+ ] as const
85
+
86
+ /** Parse a computed longhand (`"H V"` or `"H"`) into its H + V components. */
87
+ const parseCorner = (computed: string): CornerRadius => {
88
+ // `split` gives at least one part. A single value is circular (H === V).
89
+ const parts = computed.trim().split(/\s+/)
90
+ const h = parts[0]
91
+ if (parts.length < 2) return { h, v: h }
92
+ return { h, v: parts[1] }
93
+ }
94
+
95
+ /** Whether every corner is zero (nothing to correct). */
96
+ const isZeroRadius = (base: RadiusBase): boolean =>
97
+ base.every((c) => (Number.parseFloat(c.h) || 0) === 0 && (Number.parseFloat(c.v) || 0) === 0)
98
+
99
+ /**
100
+ * Correct one radius component for `scale`. A `%` is self-correcting (a fraction
101
+ * of each scaled dimension) and passes through; a px value is divided by the
102
+ * scale so the transform renders it back to the authored px.
103
+ */
104
+ const correctRadiusComponent = (value: string, scale: number): string => {
105
+ if (value.endsWith("%")) return value
106
+ const px = Number.parseFloat(value)
107
+ if (!Number.isFinite(px)) return value
108
+ return `${px / scale}px`
109
+ }
110
+
111
+ export const borderRadiusCorrector: ScaleCorrector = {
112
+ // Kebab-case: corrected values flow through the gate's style object, which the
113
+ // binding applies via `el.style.setProperty` — that requires hyphenated names
114
+ // (a camelCase key is silently dropped). `transform`/`opacity` work either way.
115
+ key: "border-radius",
116
+ read: (el) => {
117
+ const cs = getComputedStyle(el)
118
+ const base = CORNER_PROPS.map((p) => parseCorner((cs[p] as string) || "0px")) as unknown as RadiusBase
119
+ if (isZeroRadius(base)) return undefined
120
+ return base
121
+ },
122
+ correct: (base, sx, sy) => {
123
+ const corners = base as RadiusBase
124
+ const h = corners.map((c) => correctRadiusComponent(c.h, sx)).join(" ")
125
+ const v = corners.map((c) => correctRadiusComponent(c.v, sy)).join(" ")
126
+ // Elliptical shorthand: horizontal radii / vertical radii.
127
+ return `${h} / ${v}`
128
+ },
129
+ }
130
+
131
+ /** No corner rounded: what a twin with no radius blends from or to (ADR-0010 *Amendment (#166)*). */
132
+ const ZERO_CORNER: CornerRadius = { h: "0px", v: "0px" }
133
+ export const ZERO_RADIUS: unknown = [ZERO_CORNER, ZERO_CORNER, ZERO_CORNER, ZERO_CORNER] as RadiusBase
134
+
135
+ /** A radius, or {@link ZERO_RADIUS} for none. */
136
+ const orZero = (radius: unknown): RadiusBase => {
137
+ if (Predicate.isUndefined(radius)) return ZERO_RADIUS as RadiusBase
138
+ return radius as RadiusBase
139
+ }
140
+
141
+ /**
142
+ * One radius component between `from` and `to` at `progress` (ADR-0010 *Amendment (#166)*, Motion's
143
+ * `mixValues`): mixed when both are the same unit or either is 0, as a `%` when either is one, and
144
+ * never below 0; otherwise `to` as it is. Per frame.
145
+ */
146
+ const blendComponent = (from: string, to: string, progress: number): string => {
147
+ const f = Number.parseFloat(from) || 0
148
+ const t = Number.parseFloat(to) || 0
149
+ const fromPercent = from.endsWith("%")
150
+ const toPercent = to.endsWith("%")
151
+ if (f !== 0 && t !== 0 && fromPercent !== toPercent) return to
152
+ const mixed = Math.max(f + (t - f) * progress, 0)
153
+ if (fromPercent || toPercent) return `${mixed}%`
154
+ return `${mixed}px`
155
+ }
156
+
157
+ /**
158
+ * A border radius between a twin's and a Lead's at `progress` (ADR-0010 *Amendment (#166)*): corner by
159
+ * corner and axis by axis, `undefined` standing for no radius. What {@link borderRadiusCorrector}
160
+ * corrects while a handover blends. Per frame.
161
+ */
162
+ export const blendRadius = (from: unknown, to: unknown, progress: number): unknown => {
163
+ const b = orZero(to)
164
+ return orZero(from).map((corner, i) => blendCorner(corner, b[i], progress))
165
+ }
166
+
167
+ /** One corner between `from` and `to` at `progress`, axis by axis. Per frame. */
168
+ const blendCorner = (from: CornerRadius, to: CornerRadius | undefined, progress: number): CornerRadius => {
169
+ if (Predicate.isUndefined(to)) return from
170
+ return { h: blendComponent(from.h, to.h, progress), v: blendComponent(from.v, to.v, progress) }
171
+ }
172
+
173
+ /**
174
+ * `radius` as drawn under an own scale `(sx, sy)` (ADR-0010 *Amendment (#166)*): its px components
175
+ * scaled, its `%` ones as they are (a fraction of the scaled box already). Per frame.
176
+ */
177
+ export const scaleRadius = (radius: unknown, sx: number, sy: number): unknown => {
178
+ if (Predicate.isUndefined(radius)) return undefined
179
+ if (sx === 1 && sy === 1) return radius
180
+ return (radius as RadiusBase).map((corner) => ({ h: scaleComponent(corner.h, sx), v: scaleComponent(corner.v, sy) }))
181
+ }
182
+
183
+ /** One radius component under an own scale: px scaled, `%` as it is. */
184
+ const scaleComponent = (value: string, scale: number): string => {
185
+ if (value.endsWith("%")) return value
186
+ return `${(Number.parseFloat(value) || 0) * scale}px`
187
+ }
188
+
189
+ // --- box-shadow --------------------------------------------------------------
190
+
191
+ /** One parsed shadow layer: the verbatim color, the px lengths, and inset flag. */
192
+ interface ShadowLayer {
193
+ readonly color: string
194
+ readonly offsetX: number
195
+ readonly offsetY: number
196
+ readonly blur: number
197
+ readonly spread: number
198
+ readonly inset: boolean
199
+ /** How many length tokens were authored (2–4), to round-trip the shape faithfully. */
200
+ readonly lengthCount: number
201
+ }
202
+
203
+ /** Split a `box-shadow` value into layers on TOP-LEVEL commas (ignoring `rgb(…)` commas). */
204
+ const splitLayers = (value: string): Array<string> => {
205
+ const layers: Array<string> = []
206
+ let depth = 0
207
+ let start = 0
208
+ for (let i = 0; i < value.length; i++) {
209
+ const ch = value[i]
210
+ if (ch === "(") depth++
211
+ if (ch === ")") depth--
212
+ if (ch === "," && depth === 0) {
213
+ layers.push(value.slice(start, i))
214
+ start = i + 1
215
+ }
216
+ }
217
+ layers.push(value.slice(start))
218
+ return layers
219
+ }
220
+
221
+ /** Tokenise a layer, keeping parenthesised groups (e.g. `rgb(0, 0, 0)`) intact. */
222
+ const tokenizeLayer = (layer: string): Array<string> => {
223
+ const tokens: Array<string> = []
224
+ let depth = 0
225
+ let cur = ""
226
+ for (const ch of layer.trim()) {
227
+ if (ch === "(") depth++
228
+ if (ch === ")") depth--
229
+ if (/\s/.test(ch) && depth === 0) {
230
+ if (cur !== "") tokens.push(cur)
231
+ cur = ""
232
+ continue
233
+ }
234
+ cur += ch
235
+ }
236
+ if (cur !== "") tokens.push(cur)
237
+ return tokens
238
+ }
239
+
240
+ const isLength = (t: string): boolean => Number.isFinite(Number.parseFloat(t))
241
+
242
+ const parseLayer = (layer: string): ShadowLayer | undefined => {
243
+ const tokens = tokenizeLayer(layer)
244
+ if (tokens.length === 0) return undefined
245
+ let inset = false
246
+ const lengths: Array<number> = []
247
+ let color = ""
248
+ for (const t of tokens) {
249
+ if (t === "inset") {
250
+ inset = true
251
+ continue
252
+ }
253
+ if (isLength(t)) {
254
+ lengths.push(Number.parseFloat(t))
255
+ continue
256
+ }
257
+ color = t // a non-length, non-inset token is the color (verbatim)
258
+ }
259
+ if (lengths.length < 2) return undefined // offsetX + offsetY are required
260
+ return {
261
+ color,
262
+ offsetX: lengths[0],
263
+ offsetY: lengths[1],
264
+ blur: lengthAt(lengths, 2),
265
+ spread: lengthAt(lengths, 3),
266
+ inset,
267
+ lengthCount: lengths.length,
268
+ }
269
+ }
270
+
271
+ /** A shadow's `i`th length, 0 when it wasn't authored. */
272
+ const lengthAt = (lengths: ReadonlyArray<number>, i: number): number => {
273
+ if (i >= lengths.length) return 0
274
+ return lengths[i]
275
+ }
276
+
277
+ const formatLayer = (l: ShadowLayer, sx: number, sy: number): string => {
278
+ const mean = (sx + sy) / 2
279
+ // Per-axis offsets exact; blur + spread are scalar -> axis mean (no exact value
280
+ // under non-uniform scale).
281
+ const lens = [l.offsetX / sx, l.offsetY / sy, l.blur / mean, l.spread / mean]
282
+ .slice(0, l.lengthCount)
283
+ .map((n) => `${n}px`)
284
+ const parts: Array<string> = []
285
+ if (l.inset) parts.push("inset")
286
+ parts.push(...lens)
287
+ if (l.color !== "") parts.push(l.color)
288
+ return parts.join(" ")
289
+ }
290
+
291
+ export const boxShadowCorrector: ScaleCorrector = {
292
+ key: "box-shadow", // kebab-case (see borderRadiusCorrector.key)
293
+ read: (el) => {
294
+ const value = getComputedStyle(el).boxShadow
295
+ if (value === "" || value === "none") return undefined
296
+ const layers = splitLayers(value)
297
+ .map(parseLayer)
298
+ .filter(Predicate.isNotUndefined)
299
+ if (layers.length === 0) return undefined
300
+ return layers
301
+ },
302
+ correct: (base, sx, sy) => (base as Array<ShadowLayer>).map((l) => formatLayer(l, sx, sy)).join(", "),
303
+ }
304
+
305
+ /**
306
+ * The default scale-corrector registry (ADR-0011). Ship `borderRadius` +
307
+ * `boxShadow`; a public `addScaleCorrector` API is a deferred follow-up (adding
308
+ * an entry here is the extension point until then).
309
+ */
310
+ export const SCALE_CORRECTORS: ReadonlyArray<ScaleCorrector> = [borderRadiusCorrector, boxShadowCorrector]