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