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