@svelterm/core 0.21.0 → 0.24.0
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/CHANGELOG.md +59 -0
- package/dist/src/css/animation-runner.d.ts +1 -1
- package/dist/src/css/animation-runner.js +9 -1
- package/dist/src/css/compute.d.ts +19 -1
- package/dist/src/css/compute.js +69 -13
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/layout/engine.js +8 -0
- package/dist/src/render/animation-clock.d.ts +17 -3
- package/dist/src/render/animation-clock.js +80 -47
- package/dist/src/render/clock.d.ts +35 -0
- package/dist/src/render/clock.js +67 -0
- package/dist/src/render/paint.js +32 -3
- package/docs/elements.md +3 -3
- package/docs/layout.md +8 -2
- package/docs/motion.md +10 -9
- package/docs/reference.md +12 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.24.0 — 2026-07-05
|
|
4
|
+
|
|
5
|
+
Motion timing completeness: transitions and keyframes behave per spec.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Per-property transitions** — `transition: color 150ms linear, width
|
|
10
|
+
400ms ease` runs each property on its own duration and timing;
|
|
11
|
+
longhand lists pair cyclically per spec. (Previously the first
|
|
12
|
+
duration and one timing function applied to everything.)
|
|
13
|
+
- **Interrupted transitions continue** — a retargeted transition starts
|
|
14
|
+
from its current blended value instead of jumping back to the previous
|
|
15
|
+
target (reversals no longer flash).
|
|
16
|
+
- **Per-keyframe `animation-timing-function`** — a timing function
|
|
17
|
+
declared inside a keyframe applies from that stop to the next.
|
|
18
|
+
- **Keyframe `var()`/`light-dark()` re-resolution** — scheme flips and
|
|
19
|
+
custom-property changes retarget a running animation in place, without
|
|
20
|
+
restarting it.
|
|
21
|
+
|
|
22
|
+
## 0.23.0 — 2026-07-05
|
|
23
|
+
|
|
24
|
+
Positioning: `relative` offsets apply, and `sticky` arrives.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`position: relative`** — `top/right/bottom/left` now shift the box
|
|
29
|
+
and its descendants visually while flow position and size behave as if
|
|
30
|
+
unshifted, per spec (previously parsed and dropped).
|
|
31
|
+
- **`position: sticky`** (top edge) — inside a scroll container the
|
|
32
|
+
element pins to the container top + `top` once scrolled past, painting
|
|
33
|
+
above in-flow content; descendants move with it. Deviations
|
|
34
|
+
(documented): top-only, no push-out at the containing-block end, and
|
|
35
|
+
hit-testing targets the flow position.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **Opaque backgrounds now cover what's beneath** — an element's
|
|
40
|
+
background fill previously kept stale glyphs from earlier paints, so
|
|
41
|
+
overlapping paints (sticky, absolute) showed old content through the
|
|
42
|
+
background. Fills now clear covered cells.
|
|
43
|
+
|
|
44
|
+
## 0.22.0 — 2026-07-05
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- **`Clock` seam for deterministic animation tests** — the animation
|
|
49
|
+
engine's time source *and* its frame scheduler now go through a
|
|
50
|
+
`Clock` interface. `TestClock` (exported) lets tests set time and
|
|
51
|
+
advance it, firing the frame timer at each tick — so the animation and
|
|
52
|
+
transition frame lifecycle is exact, not `setInterval`/`Date.now`
|
|
53
|
+
dependent. `run()` uses the real `systemClock` by default.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **Docs: `<img>` rendering** — `reference.md` still listed `img` as
|
|
58
|
+
"not rendered", and `elements.md`'s intro contradicted its own Images
|
|
59
|
+
section. Both now describe the half-block + kitty-graphics rendering
|
|
60
|
+
shipped in 0.15.0 / 0.19.0.
|
|
61
|
+
|
|
3
62
|
## 0.21.0 — 2026-07-05
|
|
4
63
|
|
|
5
64
|
DevTools polish.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { KeyframeStop } from './parser.js';
|
|
2
2
|
import { type ResolvedStyle } from './compute.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type Easing } from './easing.js';
|
|
4
4
|
/**
|
|
5
5
|
* Runs a CSS animation by applying keyframe properties at the current
|
|
6
6
|
* time. Colours interpolate in RGB space between stops; properties that
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { applyDeclaration } from './compute.js';
|
|
2
2
|
import { resolveColor } from './color.js';
|
|
3
|
+
import { parseEasing } from './easing.js';
|
|
3
4
|
import { lerpColor, lerpNumber } from './interpolate.js';
|
|
4
5
|
import { parseCellLength } from './values.js';
|
|
5
6
|
/** Properties whose animation only needs repaint; anything else re-layouts. */
|
|
@@ -37,7 +38,11 @@ export class AnimationRunner {
|
|
|
37
38
|
const progress = this.getProgress(elapsedMs);
|
|
38
39
|
const segment = this.segmentAt(progress);
|
|
39
40
|
const { from, to } = segment;
|
|
40
|
-
|
|
41
|
+
// A timing function declared inside a keyframe applies from that
|
|
42
|
+
// stop to the next, overriding the element-level easing (per CSS).
|
|
43
|
+
const override = from.declarations.find(d => d.property === 'animation-timing-function');
|
|
44
|
+
const easing = override ? (parseEasing(override.value) ?? this.easing) : this.easing;
|
|
45
|
+
const localT = easing(segment.localT);
|
|
41
46
|
// Hold the earlier stop's values, then interpolate toward the next
|
|
42
47
|
for (const decl of from.declarations) {
|
|
43
48
|
applyAnimatedProperty(style, decl);
|
|
@@ -87,6 +92,9 @@ export class AnimationRunner {
|
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
function applyAnimatedProperty(style, decl) {
|
|
95
|
+
// Keyframe-level timing functions steer the runner, not the style
|
|
96
|
+
if (decl.property === 'animation-timing-function')
|
|
97
|
+
return;
|
|
90
98
|
applyDeclaration(style, decl.property, decl.value);
|
|
91
99
|
}
|
|
92
100
|
function applyInterpolatedProperty(style, from, to, t) {
|
|
@@ -53,6 +53,15 @@ export interface ResolvedStyle {
|
|
|
53
53
|
transitionProperty: string | null;
|
|
54
54
|
transitionDuration: number;
|
|
55
55
|
transitionTimingFunction: string;
|
|
56
|
+
/** Longhand lists, paired cyclically per spec into `transitions`. */
|
|
57
|
+
transitionDurations: number[];
|
|
58
|
+
transitionTimings: string[];
|
|
59
|
+
/** Per-property transition config, resolved after cascade. */
|
|
60
|
+
transitions: Array<{
|
|
61
|
+
property: string;
|
|
62
|
+
duration: number;
|
|
63
|
+
timing: string;
|
|
64
|
+
}>;
|
|
56
65
|
borderStyle: 'none' | 'single' | 'double' | 'rounded' | 'heavy' | 'ascii' | 'eighth-cell-inner' | 'eighth-cell-outer' | 'half-cell-inner' | 'half-cell-outer' | 'full-cell';
|
|
57
66
|
borderCorner: 'none' | 'h' | 'v';
|
|
58
67
|
borderColor: string;
|
|
@@ -68,7 +77,7 @@ export interface ResolvedStyle {
|
|
|
68
77
|
opacity: number;
|
|
69
78
|
textAlign: 'left' | 'center' | 'right';
|
|
70
79
|
textTransform: 'none' | 'uppercase' | 'lowercase' | 'capitalize';
|
|
71
|
-
position: 'static' | 'relative' | 'absolute' | 'fixed';
|
|
80
|
+
position: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
|
|
72
81
|
top: number | null;
|
|
73
82
|
right: number | null;
|
|
74
83
|
bottom: number | null;
|
|
@@ -88,3 +97,12 @@ export declare function resolveStyles(root: TermNode, stylesheet: CSSStyleSheet,
|
|
|
88
97
|
export declare function filterByMedia(stylesheet: CSSStyleSheet, context: MediaContext): CSSStyleSheet;
|
|
89
98
|
export declare function resolveNode(node: TermNode, stylesheet: CSSStyleSheet, styles: Map<number, ResolvedStyle>, variables: Map<number, Map<string, string>>, scheme?: 'dark' | 'light'): void;
|
|
90
99
|
export declare function applyDeclaration(style: ResolvedStyle, property: string, value: string, scheme?: 'dark' | 'light'): void;
|
|
100
|
+
/**
|
|
101
|
+
* Pair the transition lists per spec: durations/timings repeat
|
|
102
|
+
* cyclically to cover every listed property.
|
|
103
|
+
*/
|
|
104
|
+
export declare function pairTransitions(style: ResolvedStyle): Array<{
|
|
105
|
+
property: string;
|
|
106
|
+
duration: number;
|
|
107
|
+
timing: string;
|
|
108
|
+
}>;
|
package/dist/src/css/compute.js
CHANGED
|
@@ -56,6 +56,8 @@ export function defaultStyle(tag) {
|
|
|
56
56
|
animationTimingFunction: 'ease',
|
|
57
57
|
transitionProperty: null, transitionDuration: 0,
|
|
58
58
|
transitionTimingFunction: 'ease',
|
|
59
|
+
transitionDurations: [], transitionTimings: [],
|
|
60
|
+
transitions: [],
|
|
59
61
|
borderStyle: 'none', borderColor: 'default', borderCorner: 'none',
|
|
60
62
|
borderTop: true, borderRight: true, borderBottom: true, borderLeft: true,
|
|
61
63
|
boxSizing: 'border-box',
|
|
@@ -272,6 +274,8 @@ function computeStyle(node, stylesheet, vars, parentStyle, scheme = 'dark') {
|
|
|
272
274
|
applyDeclaration(style, decl.property, resolveVar(decl.value, vars), scheme);
|
|
273
275
|
}
|
|
274
276
|
}
|
|
277
|
+
// Pair transition longhand lists now the cascade is complete
|
|
278
|
+
style.transitions = pairTransitions(style);
|
|
275
279
|
return style;
|
|
276
280
|
}
|
|
277
281
|
function parseInlineStyle(text) {
|
|
@@ -563,10 +567,12 @@ export function applyDeclaration(style, property, value, scheme = 'dark') {
|
|
|
563
567
|
style.transitionProperty = value === 'none' ? null : value;
|
|
564
568
|
break;
|
|
565
569
|
case 'transition-duration':
|
|
566
|
-
style.
|
|
570
|
+
style.transitionDurations = value.split(',').map(v => parseDuration(v.trim()));
|
|
571
|
+
style.transitionDuration = style.transitionDurations[0] ?? 0;
|
|
567
572
|
break;
|
|
568
573
|
case 'transition-timing-function':
|
|
569
|
-
style.
|
|
574
|
+
style.transitionTimings = splitTimingList(value);
|
|
575
|
+
style.transitionTimingFunction = style.transitionTimings[0] ?? 'ease';
|
|
570
576
|
break;
|
|
571
577
|
case 'animation-name':
|
|
572
578
|
style.animationName = value === 'none' ? null : value;
|
|
@@ -639,7 +645,7 @@ export function applyDeclaration(style, property, value, scheme = 'dark') {
|
|
|
639
645
|
style.textTransform = 'none';
|
|
640
646
|
break;
|
|
641
647
|
case 'position':
|
|
642
|
-
if (value === 'relative' || value === 'absolute' || value === 'fixed')
|
|
648
|
+
if (value === 'relative' || value === 'absolute' || value === 'fixed' || value === 'sticky')
|
|
643
649
|
style.position = value;
|
|
644
650
|
else
|
|
645
651
|
style.position = 'static';
|
|
@@ -806,35 +812,85 @@ function parseAnimationShorthand(style, value) {
|
|
|
806
812
|
}
|
|
807
813
|
}
|
|
808
814
|
}
|
|
809
|
-
/**
|
|
815
|
+
/** Split a timing-function list on commas, keeping function args intact. */
|
|
816
|
+
function splitTimingList(value) {
|
|
817
|
+
const out = [];
|
|
818
|
+
let depth = 0;
|
|
819
|
+
let current = '';
|
|
820
|
+
for (const ch of value) {
|
|
821
|
+
if (ch === '(')
|
|
822
|
+
depth++;
|
|
823
|
+
if (ch === ')')
|
|
824
|
+
depth--;
|
|
825
|
+
if (ch === ',' && depth === 0) {
|
|
826
|
+
out.push(current.trim());
|
|
827
|
+
current = '';
|
|
828
|
+
}
|
|
829
|
+
else
|
|
830
|
+
current += ch;
|
|
831
|
+
}
|
|
832
|
+
if (current.trim())
|
|
833
|
+
out.push(current.trim());
|
|
834
|
+
return out;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Parse `transition: <property> <duration> <timing> [...]` — each
|
|
838
|
+
* comma-separated group configures one property (or `all`).
|
|
839
|
+
*/
|
|
810
840
|
function parseTransitionShorthand(style, value) {
|
|
811
841
|
const trimmed = value.trim();
|
|
812
842
|
if (trimmed === 'none') {
|
|
813
843
|
style.transitionProperty = null;
|
|
814
844
|
style.transitionDuration = 0;
|
|
845
|
+
style.transitionDurations = [];
|
|
846
|
+
style.transitionTimings = [];
|
|
815
847
|
return;
|
|
816
848
|
}
|
|
817
|
-
const { easing, rest } = extractEasingFunction(trimmed);
|
|
818
|
-
if (easing)
|
|
819
|
-
style.transitionTimingFunction = easing;
|
|
820
849
|
const properties = [];
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
850
|
+
const durations = [];
|
|
851
|
+
const timings = [];
|
|
852
|
+
for (const groupRaw of splitTimingList(trimmed)) {
|
|
853
|
+
const { easing, rest } = extractEasingFunction(groupRaw);
|
|
854
|
+
let property = 'all';
|
|
855
|
+
let duration = 0;
|
|
856
|
+
let timing = easing ?? 'ease';
|
|
857
|
+
for (const token of rest.trim().split(/\s+/)) {
|
|
824
858
|
if (/^\d*\.?\d+(ms|s)$/.test(token)) {
|
|
825
859
|
if (duration === 0)
|
|
826
860
|
duration = parseDuration(token);
|
|
827
861
|
}
|
|
828
862
|
else if (TIMING_KEYWORDS.has(token)) {
|
|
829
|
-
|
|
863
|
+
timing = token;
|
|
830
864
|
}
|
|
831
865
|
else if (token) {
|
|
832
|
-
|
|
866
|
+
property = token;
|
|
833
867
|
}
|
|
834
868
|
}
|
|
869
|
+
properties.push(property);
|
|
870
|
+
durations.push(duration);
|
|
871
|
+
timings.push(timing);
|
|
835
872
|
}
|
|
836
873
|
style.transitionProperty = properties.length > 0 ? properties.join(',') : 'all';
|
|
837
|
-
style.
|
|
874
|
+
style.transitionDurations = durations;
|
|
875
|
+
style.transitionTimings = timings;
|
|
876
|
+
style.transitionDuration = durations[0] ?? 0;
|
|
877
|
+
style.transitionTimingFunction = timings[0] ?? 'ease';
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Pair the transition lists per spec: durations/timings repeat
|
|
881
|
+
* cyclically to cover every listed property.
|
|
882
|
+
*/
|
|
883
|
+
export function pairTransitions(style) {
|
|
884
|
+
if (!style.transitionProperty)
|
|
885
|
+
return [];
|
|
886
|
+
const properties = style.transitionProperty.split(',').map(p => p.trim()).filter(Boolean);
|
|
887
|
+
const durations = style.transitionDurations.length > 0 ? style.transitionDurations : [style.transitionDuration];
|
|
888
|
+
const timings = style.transitionTimings.length > 0 ? style.transitionTimings : [style.transitionTimingFunction];
|
|
889
|
+
return properties.map((property, i) => ({
|
|
890
|
+
property,
|
|
891
|
+
duration: durations[i % durations.length] ?? 0,
|
|
892
|
+
timing: timings[i % timings.length] ?? 'ease',
|
|
893
|
+
}));
|
|
838
894
|
}
|
|
839
895
|
function parseDuration(value) {
|
|
840
896
|
if (value.endsWith('ms'))
|
package/dist/src/index.d.ts
CHANGED
|
@@ -78,3 +78,4 @@ export { StdinRouter } from './terminal/stdin-router.js';
|
|
|
78
78
|
export { type TerminalIO, ProcessIO, InProcessIO } from './terminal/io.js';
|
|
79
79
|
export { copyToClipboard, osc52Copy } from './terminal/clipboard.js';
|
|
80
80
|
export { FrameLog, createFrameLog } from './framelog.js';
|
|
81
|
+
export { type Clock, TestClock, systemClock } from './render/clock.js';
|
package/dist/src/index.js
CHANGED
|
@@ -1011,3 +1011,4 @@ export { StdinRouter } from './terminal/stdin-router.js';
|
|
|
1011
1011
|
export { ProcessIO, InProcessIO } from './terminal/io.js';
|
|
1012
1012
|
export { copyToClipboard, osc52Copy } from './terminal/clipboard.js';
|
|
1013
1013
|
export { FrameLog, createFrameLog } from './framelog.js';
|
|
1014
|
+
export { TestClock, systemClock } from './render/clock.js';
|
|
@@ -159,6 +159,14 @@ function layoutElement(node, styles, boxes, x, y, availWidth, availHeight) {
|
|
|
159
159
|
const absY = y + (style.top ?? 0);
|
|
160
160
|
return layoutAbsolute(node, styles, boxes, absX, absY, availWidth, availHeight, style);
|
|
161
161
|
}
|
|
162
|
+
// position: relative — a visual shift of the box and its descendants;
|
|
163
|
+
// flow position and size behave as if unshifted (the return value,
|
|
164
|
+
// which advances the flow, is untouched). left beats right, top beats
|
|
165
|
+
// bottom, per LTR CSS.
|
|
166
|
+
if (style?.position === 'relative') {
|
|
167
|
+
x += style.left ?? -(style.right ?? 0);
|
|
168
|
+
y += style.top ?? -(style.bottom ?? 0);
|
|
169
|
+
}
|
|
162
170
|
let margin = {
|
|
163
171
|
top: resolvePadding(style?.marginTop, availWidth),
|
|
164
172
|
right: resolvePadding(style?.marginRight, availWidth),
|
|
@@ -2,6 +2,7 @@ import { TermNode } from '../renderer/node.js';
|
|
|
2
2
|
import { type KeyframeResolution } from '../css/animation.js';
|
|
3
3
|
import type { KeyframeStop } from '../css/parser.js';
|
|
4
4
|
import type { ResolvedStyle } from '../css/compute.js';
|
|
5
|
+
import { type Clock } from './clock.js';
|
|
5
6
|
export type { KeyframeResolution } from '../css/animation.js';
|
|
6
7
|
/**
|
|
7
8
|
* Drives CSS animations: discovers animated elements after style
|
|
@@ -10,14 +11,20 @@ export type { KeyframeResolution } from '../css/animation.js';
|
|
|
10
11
|
* wires `onFrame` to re-apply and repaint.
|
|
11
12
|
*/
|
|
12
13
|
export declare class AnimationClock {
|
|
13
|
-
private now;
|
|
14
14
|
private active;
|
|
15
|
+
/** Per-property transition runners, keyed `nodeId:property`. */
|
|
15
16
|
private transitions;
|
|
16
17
|
/** Last-seen target values per transitioned node, as CSS property → value. */
|
|
17
18
|
private transitionTargets;
|
|
18
19
|
private timer;
|
|
20
|
+
private clock;
|
|
19
21
|
onFrame?: () => void;
|
|
20
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Accepts a Clock (time source + scheduler) or, for convenience and
|
|
24
|
+
* backward compatibility, a bare `() => number` time function.
|
|
25
|
+
*/
|
|
26
|
+
constructor(clock?: Clock | (() => number));
|
|
27
|
+
private now;
|
|
21
28
|
get activeCount(): number;
|
|
22
29
|
/** Whether this node's animation needs re-layout each frame (vs repaint only). */
|
|
23
30
|
touchesLayout(node: TermNode): boolean;
|
|
@@ -45,7 +52,14 @@ export declare class AnimationClock {
|
|
|
45
52
|
stop(): void;
|
|
46
53
|
private discover;
|
|
47
54
|
private discoverTransitions;
|
|
48
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Snapshot the node's target values; start a per-property transition
|
|
57
|
+
* runner on any change, each with its own duration and timing. An
|
|
58
|
+
* interrupted transition continues from its current blended value
|
|
59
|
+
* rather than restarting from the previous target.
|
|
60
|
+
*/
|
|
49
61
|
private trackTransitionTargets;
|
|
62
|
+
/** Evaluate an in-flight transition's value for one property, now. */
|
|
63
|
+
private currentValue;
|
|
50
64
|
private updateTimer;
|
|
51
65
|
}
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import { AnimationRunner } from '../css/animation-runner.js';
|
|
2
2
|
import { resolveKeyframeStops } from '../css/animation.js';
|
|
3
3
|
import { parseEasing } from '../css/easing.js';
|
|
4
|
+
import { systemClock, clockFromNow } from './clock.js';
|
|
4
5
|
/** The runner's easing for a CSS timing-function value; invalid → linear. */
|
|
5
6
|
function easingFor(value) {
|
|
6
7
|
return parseEasing(value) ?? (t => t);
|
|
7
8
|
}
|
|
8
|
-
/** Parse a transition-property value into the tracked-property filter. */
|
|
9
|
-
function transitionedProperties(value) {
|
|
10
|
-
const names = new Set();
|
|
11
|
-
for (const raw of value.split(',')) {
|
|
12
|
-
const name = raw.trim();
|
|
13
|
-
if (name === 'all')
|
|
14
|
-
return { all: true, names };
|
|
15
|
-
names.add(name === 'background' ? 'background-color' : name);
|
|
16
|
-
}
|
|
17
|
-
return { all: false, names };
|
|
18
|
-
}
|
|
19
9
|
function cellValue(value) {
|
|
20
10
|
return typeof value === 'number' && value >= 0 ? `${value}cell` : null;
|
|
21
11
|
}
|
|
@@ -48,23 +38,36 @@ const FRAME_INTERVAL_MS = 33;
|
|
|
48
38
|
* wires `onFrame` to re-apply and repaint.
|
|
49
39
|
*/
|
|
50
40
|
export class AnimationClock {
|
|
51
|
-
now;
|
|
52
41
|
active = new Map();
|
|
42
|
+
/** Per-property transition runners, keyed `nodeId:property`. */
|
|
53
43
|
transitions = new Map();
|
|
54
44
|
/** Last-seen target values per transitioned node, as CSS property → value. */
|
|
55
45
|
transitionTargets = new Map();
|
|
56
46
|
timer = null;
|
|
47
|
+
clock;
|
|
57
48
|
onFrame;
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Accepts a Clock (time source + scheduler) or, for convenience and
|
|
51
|
+
* backward compatibility, a bare `() => number` time function.
|
|
52
|
+
*/
|
|
53
|
+
constructor(clock = systemClock) {
|
|
54
|
+
this.clock = typeof clock === 'function' ? clockFromNow(clock) : clock;
|
|
55
|
+
}
|
|
56
|
+
now() {
|
|
57
|
+
return this.clock.now();
|
|
60
58
|
}
|
|
61
59
|
get activeCount() {
|
|
62
60
|
return this.active.size + this.transitions.size;
|
|
63
61
|
}
|
|
64
62
|
/** Whether this node's animation needs re-layout each frame (vs repaint only). */
|
|
65
63
|
touchesLayout(node) {
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
if (this.active.get(node.id)?.runner.touchesLayout)
|
|
65
|
+
return true;
|
|
66
|
+
for (const anim of this.transitions.values()) {
|
|
67
|
+
if (anim.node.id === node.id && anim.runner.touchesLayout)
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
68
71
|
}
|
|
69
72
|
/**
|
|
70
73
|
* Track transitioned elements and start a one-shot transition when a
|
|
@@ -79,7 +82,10 @@ export class AnimationClock {
|
|
|
79
82
|
for (const id of this.transitionTargets.keys()) {
|
|
80
83
|
if (!seen.has(id)) {
|
|
81
84
|
this.transitionTargets.delete(id);
|
|
82
|
-
this.transitions.
|
|
85
|
+
for (const key of this.transitions.keys()) {
|
|
86
|
+
if (key.startsWith(`${id}:`))
|
|
87
|
+
this.transitions.delete(key);
|
|
88
|
+
}
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
91
|
this.updateTimer();
|
|
@@ -108,20 +114,20 @@ export class AnimationClock {
|
|
|
108
114
|
return dirty;
|
|
109
115
|
}
|
|
110
116
|
applyEntries(entries, styles, dirty) {
|
|
111
|
-
for (const [
|
|
112
|
-
const style = styles.get(id);
|
|
117
|
+
for (const [key, anim] of entries) {
|
|
118
|
+
const style = styles.get(anim.node.id);
|
|
113
119
|
if (!style)
|
|
114
120
|
continue;
|
|
115
121
|
const elapsed = this.now() - anim.start;
|
|
116
122
|
anim.runner.apply(style, elapsed);
|
|
117
123
|
dirty.push({ node: anim.node, touchesLayout: anim.runner.touchesLayout });
|
|
118
124
|
if (anim.runner.isFinished(elapsed))
|
|
119
|
-
entries.delete(
|
|
125
|
+
entries.delete(key);
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
128
|
stop() {
|
|
123
129
|
if (this.timer !== null) {
|
|
124
|
-
clearInterval(this.timer);
|
|
130
|
+
this.clock.clearInterval(this.timer);
|
|
125
131
|
this.timer = null;
|
|
126
132
|
}
|
|
127
133
|
}
|
|
@@ -132,18 +138,27 @@ export class AnimationClock {
|
|
|
132
138
|
const stops = name ? keyframes.get(name) : undefined;
|
|
133
139
|
if (style && name && stops && style.animationDuration > 0) {
|
|
134
140
|
const existing = this.active.get(node.id);
|
|
141
|
+
const resolved = resolution
|
|
142
|
+
? resolveKeyframeStops(stops, resolution, node.id)
|
|
143
|
+
: stops;
|
|
144
|
+
const resolvedKey = JSON.stringify(resolved);
|
|
135
145
|
if (!existing || existing.name !== name || existing.duration !== style.animationDuration) {
|
|
136
|
-
const resolved = resolution
|
|
137
|
-
? resolveKeyframeStops(stops, resolution, node.id)
|
|
138
|
-
: stops;
|
|
139
146
|
this.active.set(node.id, {
|
|
140
147
|
node,
|
|
141
148
|
runner: new AnimationRunner(resolved, style.animationDuration, style.animationIterationCount, easingFor(style.animationTimingFunction)),
|
|
142
149
|
name,
|
|
143
150
|
duration: style.animationDuration,
|
|
144
151
|
start: this.now(),
|
|
152
|
+
resolvedKey,
|
|
145
153
|
});
|
|
146
154
|
}
|
|
155
|
+
else if (existing.resolvedKey !== undefined && existing.resolvedKey !== resolvedKey) {
|
|
156
|
+
// var()/light-dark() re-resolved to new values (scheme
|
|
157
|
+
// flip, custom property change): retarget the runner
|
|
158
|
+
// without restarting — the original start time holds.
|
|
159
|
+
existing.runner = new AnimationRunner(resolved, style.animationDuration, style.animationIterationCount, easingFor(style.animationTimingFunction));
|
|
160
|
+
existing.resolvedKey = resolvedKey;
|
|
161
|
+
}
|
|
147
162
|
seen.add(node.id);
|
|
148
163
|
}
|
|
149
164
|
}
|
|
@@ -154,7 +169,7 @@ export class AnimationClock {
|
|
|
154
169
|
const subtreeResolved = parentResolved || (resolvedIds?.has(node.id) ?? false);
|
|
155
170
|
if (node.nodeType === 'element') {
|
|
156
171
|
const style = styles.get(node.id);
|
|
157
|
-
if (style
|
|
172
|
+
if (style && style.transitions.some(t => t.duration > 0)) {
|
|
158
173
|
seen.add(node.id);
|
|
159
174
|
if (subtreeResolved)
|
|
160
175
|
this.trackTransitionTargets(node, style);
|
|
@@ -164,12 +179,20 @@ export class AnimationClock {
|
|
|
164
179
|
this.discoverTransitions(child, styles, subtreeResolved, resolvedIds, seen);
|
|
165
180
|
}
|
|
166
181
|
}
|
|
167
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* Snapshot the node's target values; start a per-property transition
|
|
184
|
+
* runner on any change, each with its own duration and timing. An
|
|
185
|
+
* interrupted transition continues from its current blended value
|
|
186
|
+
* rather than restarting from the previous target.
|
|
187
|
+
*/
|
|
168
188
|
trackTransitionTargets(node, style) {
|
|
169
|
-
const
|
|
189
|
+
const configFor = (css) => style.transitions.find(t => t.property === css
|
|
190
|
+
|| (t.property === 'background' && css === 'background-color'))
|
|
191
|
+
?? style.transitions.find(t => t.property === 'all');
|
|
170
192
|
const targets = {};
|
|
171
193
|
for (const prop of TRANSITIONABLE) {
|
|
172
|
-
|
|
194
|
+
const config = configFor(prop.css);
|
|
195
|
+
if (!config || config.duration <= 0)
|
|
173
196
|
continue;
|
|
174
197
|
const value = prop.read(style);
|
|
175
198
|
if (value !== null)
|
|
@@ -179,32 +202,42 @@ export class AnimationClock {
|
|
|
179
202
|
this.transitionTargets.set(node.id, targets);
|
|
180
203
|
if (!previous)
|
|
181
204
|
return; // first sight — the initial style never transitions
|
|
182
|
-
const fromDecls = [];
|
|
183
|
-
const toDecls = [];
|
|
184
205
|
for (const [property, target] of Object.entries(targets)) {
|
|
185
206
|
const before = previous[property];
|
|
186
|
-
if (before
|
|
187
|
-
|
|
188
|
-
|
|
207
|
+
if (before === undefined || before === target)
|
|
208
|
+
continue;
|
|
209
|
+
const config = configFor(property);
|
|
210
|
+
const key = `${node.id}:${property}`;
|
|
211
|
+
// Interrupted mid-flight? Continue from the current value.
|
|
212
|
+
let from = before;
|
|
213
|
+
const inFlight = this.transitions.get(key);
|
|
214
|
+
if (inFlight) {
|
|
215
|
+
const current = this.currentValue(inFlight, style, property);
|
|
216
|
+
if (current !== null)
|
|
217
|
+
from = current;
|
|
189
218
|
}
|
|
219
|
+
const stops = [
|
|
220
|
+
{ offset: 0, declarations: [{ property, value: from }] },
|
|
221
|
+
{ offset: 1, declarations: [{ property, value: target }] },
|
|
222
|
+
];
|
|
223
|
+
this.transitions.set(key, {
|
|
224
|
+
node,
|
|
225
|
+
runner: new AnimationRunner(stops, config.duration, 1, easingFor(config.timing)),
|
|
226
|
+
name: property,
|
|
227
|
+
duration: config.duration,
|
|
228
|
+
start: this.now(),
|
|
229
|
+
});
|
|
190
230
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
this.transitions.set(node.id, {
|
|
198
|
-
node,
|
|
199
|
-
runner: new AnimationRunner(stops, style.transitionDuration, 1, easingFor(style.transitionTimingFunction)),
|
|
200
|
-
name: '',
|
|
201
|
-
duration: style.transitionDuration,
|
|
202
|
-
start: this.now(),
|
|
203
|
-
});
|
|
231
|
+
}
|
|
232
|
+
/** Evaluate an in-flight transition's value for one property, now. */
|
|
233
|
+
currentValue(anim, base, property) {
|
|
234
|
+
const scratch = { ...base };
|
|
235
|
+
anim.runner.apply(scratch, this.now() - anim.start);
|
|
236
|
+
return TRANSITIONABLE.find(p => p.css === property)?.read(scratch) ?? null;
|
|
204
237
|
}
|
|
205
238
|
updateTimer() {
|
|
206
239
|
if (this.activeCount > 0 && this.timer === null) {
|
|
207
|
-
this.timer = setInterval(() => this.onFrame?.(), FRAME_INTERVAL_MS);
|
|
240
|
+
this.timer = this.clock.setInterval(() => this.onFrame?.(), FRAME_INTERVAL_MS);
|
|
208
241
|
}
|
|
209
242
|
else if (this.activeCount === 0) {
|
|
210
243
|
this.stop();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clock seam for time-driven rendering (animations, transitions, and the
|
|
3
|
+
* scrollbar fade). Abstracts both the time source and the frame
|
|
4
|
+
* scheduler so tests can advance time and fire ticks deterministically
|
|
5
|
+
* instead of depending on `Date.now()` and real `setInterval`.
|
|
6
|
+
*/
|
|
7
|
+
export type ClockTimer = unknown;
|
|
8
|
+
export interface Clock {
|
|
9
|
+
now(): number;
|
|
10
|
+
setInterval(fn: () => void, ms: number): ClockTimer;
|
|
11
|
+
clearInterval(timer: ClockTimer): void;
|
|
12
|
+
}
|
|
13
|
+
/** The real clock: wall time and the platform timer. */
|
|
14
|
+
export declare const systemClock: Clock;
|
|
15
|
+
/** Wrap a bare time function in a Clock backed by the real scheduler. */
|
|
16
|
+
export declare function clockFromNow(now: () => number): Clock;
|
|
17
|
+
/**
|
|
18
|
+
* A controllable clock for tests: set the time directly, or `advance`
|
|
19
|
+
* it, which fires any registered intervals at each tick they cross.
|
|
20
|
+
*/
|
|
21
|
+
export declare class TestClock implements Clock {
|
|
22
|
+
private time;
|
|
23
|
+
private timers;
|
|
24
|
+
constructor(start?: number);
|
|
25
|
+
now(): number;
|
|
26
|
+
setInterval(fn: () => void, ms: number): ClockTimer;
|
|
27
|
+
clearInterval(timer: ClockTimer): void;
|
|
28
|
+
/** Jump to an absolute time without firing intervals. */
|
|
29
|
+
setTime(time: number): void;
|
|
30
|
+
/** Advance by `ms`, firing each interval at every tick it crosses. */
|
|
31
|
+
advance(ms: number): void;
|
|
32
|
+
/** Timers registered right now (for assertions). */
|
|
33
|
+
get activeTimers(): number;
|
|
34
|
+
private nextTimerBefore;
|
|
35
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clock seam for time-driven rendering (animations, transitions, and the
|
|
3
|
+
* scrollbar fade). Abstracts both the time source and the frame
|
|
4
|
+
* scheduler so tests can advance time and fire ticks deterministically
|
|
5
|
+
* instead of depending on `Date.now()` and real `setInterval`.
|
|
6
|
+
*/
|
|
7
|
+
/** The real clock: wall time and the platform timer. */
|
|
8
|
+
export const systemClock = {
|
|
9
|
+
now: () => Date.now(),
|
|
10
|
+
setInterval: (fn, ms) => setInterval(fn, ms),
|
|
11
|
+
clearInterval: (timer) => clearInterval(timer),
|
|
12
|
+
};
|
|
13
|
+
/** Wrap a bare time function in a Clock backed by the real scheduler. */
|
|
14
|
+
export function clockFromNow(now) {
|
|
15
|
+
return { now, setInterval: systemClock.setInterval, clearInterval: systemClock.clearInterval };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A controllable clock for tests: set the time directly, or `advance`
|
|
19
|
+
* it, which fires any registered intervals at each tick they cross.
|
|
20
|
+
*/
|
|
21
|
+
export class TestClock {
|
|
22
|
+
time;
|
|
23
|
+
timers = new Set();
|
|
24
|
+
constructor(start = 0) {
|
|
25
|
+
this.time = start;
|
|
26
|
+
}
|
|
27
|
+
now() {
|
|
28
|
+
return this.time;
|
|
29
|
+
}
|
|
30
|
+
setInterval(fn, ms) {
|
|
31
|
+
const timer = { fn, ms, next: this.time + ms };
|
|
32
|
+
this.timers.add(timer);
|
|
33
|
+
return timer;
|
|
34
|
+
}
|
|
35
|
+
clearInterval(timer) {
|
|
36
|
+
this.timers.delete(timer);
|
|
37
|
+
}
|
|
38
|
+
/** Jump to an absolute time without firing intervals. */
|
|
39
|
+
setTime(time) {
|
|
40
|
+
this.time = time;
|
|
41
|
+
}
|
|
42
|
+
/** Advance by `ms`, firing each interval at every tick it crosses. */
|
|
43
|
+
advance(ms) {
|
|
44
|
+
const target = this.time + ms;
|
|
45
|
+
while (true) {
|
|
46
|
+
const due = this.nextTimerBefore(target);
|
|
47
|
+
if (!due)
|
|
48
|
+
break;
|
|
49
|
+
this.time = due.next;
|
|
50
|
+
due.next += due.ms;
|
|
51
|
+
due.fn();
|
|
52
|
+
}
|
|
53
|
+
this.time = target;
|
|
54
|
+
}
|
|
55
|
+
/** Timers registered right now (for assertions). */
|
|
56
|
+
get activeTimers() {
|
|
57
|
+
return this.timers.size;
|
|
58
|
+
}
|
|
59
|
+
nextTimerBefore(target) {
|
|
60
|
+
let earliest = null;
|
|
61
|
+
for (const timer of this.timers) {
|
|
62
|
+
if (timer.next <= target && (!earliest || timer.next < earliest.next))
|
|
63
|
+
earliest = timer;
|
|
64
|
+
}
|
|
65
|
+
return earliest;
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/src/render/paint.js
CHANGED
|
@@ -25,7 +25,21 @@ function paintNode(node, buffer, styles, layout, inherited, clip, scroll, damage
|
|
|
25
25
|
return;
|
|
26
26
|
const visuals = resolveVisuals(node, styles, inherited);
|
|
27
27
|
const rawBox = layout?.get(node.id);
|
|
28
|
-
|
|
28
|
+
let box = rawBox ? applyScroll(rawBox, scroll) : undefined;
|
|
29
|
+
// position: sticky (top): when scrolled past inside a clipping
|
|
30
|
+
// container, pin to the container top + offset. Descendants follow
|
|
31
|
+
// via an adjusted child scroll; applied before culling so a stuck
|
|
32
|
+
// element scrolled far past its flow position still paints.
|
|
33
|
+
let stickyDelta = 0;
|
|
34
|
+
if (box && clip && node.nodeType === 'element') {
|
|
35
|
+
const stickyStyle = styles?.get(node.id);
|
|
36
|
+
if (stickyStyle?.position === 'sticky' && stickyStyle.top !== null) {
|
|
37
|
+
const stuckY = Math.max(box.y, clip.y + (stickyStyle.top ?? 0));
|
|
38
|
+
stickyDelta = stuckY - box.y;
|
|
39
|
+
if (stickyDelta > 0)
|
|
40
|
+
box = { ...box, y: stuckY };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
29
43
|
// Skip nodes entirely outside the damage region
|
|
30
44
|
if (damageClip && box && !boxesOverlap(box, damageClip))
|
|
31
45
|
return;
|
|
@@ -118,7 +132,18 @@ function paintNode(node, buffer, styles, layout, inherited, clip, scroll, damage
|
|
|
118
132
|
childScroll = { x: scroll.x + node.scrollLeft, y: scroll.y + node.scrollTop };
|
|
119
133
|
}
|
|
120
134
|
}
|
|
121
|
-
|
|
135
|
+
// Descendants of a stuck element move with it
|
|
136
|
+
if (stickyDelta > 0)
|
|
137
|
+
childScroll = { x: childScroll.x, y: childScroll.y - stickyDelta };
|
|
138
|
+
// Sticky children paint after in-flow siblings so scrolled content
|
|
139
|
+
// doesn't overpaint a stuck header (positioned elements stack above).
|
|
140
|
+
const kids = childrenWithPseudos(node);
|
|
141
|
+
const hasSticky = kids.some(c => c.nodeType === 'element' && styles?.get(c.id)?.position === 'sticky');
|
|
142
|
+
const ordered = hasSticky
|
|
143
|
+
? [...kids.filter(c => styles?.get(c.id)?.position !== 'sticky'),
|
|
144
|
+
...kids.filter(c => styles?.get(c.id)?.position === 'sticky')]
|
|
145
|
+
: kids;
|
|
146
|
+
for (const child of ordered) {
|
|
122
147
|
paintNode(child, buffer, styles, layout, visuals, childClip, childScroll, damageClip);
|
|
123
148
|
}
|
|
124
149
|
// Render scrollbar overlays for scrollable containers
|
|
@@ -233,7 +258,11 @@ function fillBackground(buffer, box, visuals, clip, style) {
|
|
|
233
258
|
const bg = bgHasAlpha
|
|
234
259
|
? blendColor(buffer.getCell(col, row)?.bg ?? 'default', visuals.bg)
|
|
235
260
|
: visuals.bg;
|
|
236
|
-
|
|
261
|
+
// An opaque fill covers what's beneath — clear stale glyphs
|
|
262
|
+
// too, so overlapping paints (sticky, absolute) don't show
|
|
263
|
+
// earlier content through the background. The element's own
|
|
264
|
+
// text repaints after this fill.
|
|
265
|
+
buffer.setCell(col, row, { bg, char: ' ' });
|
|
237
266
|
}
|
|
238
267
|
}
|
|
239
268
|
}
|
package/docs/elements.md
CHANGED
|
@@ -9,9 +9,9 @@ its display default.
|
|
|
9
9
|
Headings, paragraphs, lists (`ul`/`ol` with markers), `blockquote`,
|
|
10
10
|
`pre`, `hr` (a `─` rule), `figure`, `dl`, and the text-level elements
|
|
11
11
|
(`strong`/`b`, `em`/`i`, `u`, `s`/`del`, `mark`, `code`, `kbd`, `abbr`,
|
|
12
|
-
`samp`, `var`) carry a browser-like UA stylesheet in cells. `img
|
|
13
|
-
`video`, `canvas`, and `iframe`
|
|
14
|
-
|
|
12
|
+
`samp`, `var`) carry a browser-like UA stylesheet in cells. `img`
|
|
13
|
+
renders (see [Images](#images) below); `video`, `canvas`, and `iframe`
|
|
14
|
+
are not rendered.
|
|
15
15
|
|
|
16
16
|
## Images
|
|
17
17
|
|
package/docs/layout.md
CHANGED
|
@@ -92,8 +92,14 @@ cells has no baseline distinct from its top).
|
|
|
92
92
|
|
|
93
93
|
`position: absolute` and `fixed` take elements out of flow and place them
|
|
94
94
|
by `top`/`right`/`bottom`/`left` with `z-index` stacking. `position:
|
|
95
|
-
relative`
|
|
96
|
-
|
|
95
|
+
relative` shifts the element (and its descendants) visually while the
|
|
96
|
+
flow behaves as if it hadn't moved, per spec. `position: sticky` pins an
|
|
97
|
+
element to the top of its scroll container once scrolled past
|
|
98
|
+
(`top`-edge only; it doesn't yet push out at the end of its containing
|
|
99
|
+
block, and hit-testing targets the flow position rather than the stuck
|
|
100
|
+
one). A sticky element with a transparent background shows scrolled
|
|
101
|
+
content through it — give it a background. Sub-cell geometry
|
|
102
|
+
(`transform`, floats) is out of scope; see
|
|
97
103
|
[compatibility](./compatibility.md).
|
|
98
104
|
|
|
99
105
|
## Sizing behaviours worth knowing
|
package/docs/motion.md
CHANGED
|
@@ -66,19 +66,20 @@ inline style updates, a `:checked` rule starts applying:
|
|
|
66
66
|
`ms`/`s`. The same interpolation rules as animations apply. The initial
|
|
67
67
|
style never transitions.
|
|
68
68
|
|
|
69
|
+
Transitions are configured per property, per spec: comma-separated
|
|
70
|
+
shorthand groups (`transition: color 150ms linear, width 400ms ease`)
|
|
71
|
+
or longhand lists paired cyclically. An interrupted transition
|
|
72
|
+
continues from its current blended value. A timing function declared
|
|
73
|
+
*inside a keyframe* applies from that stop to the next, overriding the
|
|
74
|
+
element's. Keyframe `var()`/`light-dark()` values re-resolve when the
|
|
75
|
+
scheme or custom properties change mid-animation, retargeting without
|
|
76
|
+
restarting.
|
|
77
|
+
|
|
69
78
|
## Deviations from browsers
|
|
70
79
|
|
|
71
|
-
- One duration and one timing function apply to all listed transition
|
|
72
|
-
properties (per-property lists aren't split).
|
|
73
|
-
- An interrupted transition restarts from its previous target value, not
|
|
74
|
-
the current blended value.
|
|
75
80
|
- `opacity` doesn't interpolate (it applies discretely mid-animation) —
|
|
76
81
|
animate colour toward the background for a smooth fade.
|
|
77
|
-
-
|
|
78
|
-
animation starts; changing a custom property doesn't retarget a
|
|
79
|
-
running animation.
|
|
80
|
-
- Per-keyframe `animation-timing-function` overrides are ignored — the
|
|
81
|
-
element's timing function applies to every segment.
|
|
82
|
+
- `transition-delay` / `animation-delay` are not implemented.
|
|
82
83
|
|
|
83
84
|
## Reduced motion
|
|
84
85
|
|
package/docs/reference.md
CHANGED
|
@@ -137,7 +137,8 @@ block/inline box per its display default.
|
|
|
137
137
|
| `select`/`option`/`optgroup` | cycling control (see above); `change`+`input` with the option value | [`<select>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select) |
|
|
138
138
|
| `progress`, `meter` | block-glyph bars; `value`/`max` (+`min` for meter); no-value progress renders track only | [`<progress>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress), [`<meter>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meter) |
|
|
139
139
|
| `details`/`summary` | ▶/▼ disclosure, `open` attribute, `toggle` event, focusable summary | [`<details>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details) |
|
|
140
|
-
| `img
|
|
140
|
+
| `img` | half-block pixels (▀) from PNG file paths / `data:image/png` URIs; real pixels via the kitty graphics protocol where supported; sized by CSS `width`/`height` | [`<img>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) |
|
|
141
|
+
| `video`, `canvas`, `iframe` | not rendered | — |
|
|
141
142
|
|
|
142
143
|
## CSS selectors
|
|
143
144
|
|
|
@@ -171,11 +172,11 @@ adaptations. Lengths are cells (`cell`/`ch`, `%`, or `calc()`).
|
|
|
171
172
|
| [Display & flow](https://developer.mozilla.org/en-US/docs/Web/CSS/display) | `display: block, inline, inline-block, flex, grid, none, contents`, all table display types |
|
|
172
173
|
| [Flexbox](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout) | `flex-direction` (all four), `flex-wrap`, `flex`/`flex-grow`/`flex-shrink`/`flex-basis`, `gap`, `justify-content` (incl. `space-*`), `align-items`, `align-self`, `order` |
|
|
173
174
|
| [Grid](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout) | `grid-template-columns/rows` (`cell`/`ch`/`%`/`fr`, `repeat()`, `minmax()`), `grid-template-areas` + `grid-area` (named and numeric), `grid-column`, `grid-row` (start / start‑end / `span n`), `gap`. Auto-flow is row-based; `grid-auto-flow: column` is not implemented. Fractional `minmax()` minimums are enforced without redistribution |
|
|
174
|
-
| [Positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/position) | `position: static/absolute/fixed` with `top/right/bottom/left`, `z-index`.
|
|
175
|
+
| [Positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/position) | `position: static/relative/absolute/fixed/sticky` with `top/right/bottom/left`, `z-index`. Relative offsets shift visually without moving flow; sticky is top-edge only inside scroll containers (no push-out at the containing block end) |
|
|
175
176
|
| [Tables](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_table) | `border-collapse`, `border-spacing`, `caption-side`, `table-layout`, `empty-cells`, `vertical-align` (`baseline` ≈ `top`) |
|
|
176
177
|
| Borders | `border`/`border-style`/`border-color`/`border-corner` + per-side toggles (terminal values above) |
|
|
177
178
|
| [Animation](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animations) | `animation` shorthand, `animation-name/-duration/-iteration-count` (incl. `infinite`)/`-timing-function`, `@keyframes` (from/to/percentages, values resolve `var()`/`light-dark()`) |
|
|
178
|
-
| [Transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions) | `transition` shorthand
|
|
179
|
+
| [Transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions) | `transition` shorthand with per-property comma groups; `transition-property`/`-duration`/`-timing-function` longhand lists paired per spec; interruptions continue from the current value |
|
|
179
180
|
| [Easing](https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function) | `linear`, `ease` (default), `ease-in`, `ease-out`, `ease-in-out`, `cubic-bezier()`, `steps()`, `step-start`, `step-end` |
|
|
180
181
|
|
|
181
182
|
### Animation & transition semantics on the grid
|
|
@@ -184,12 +185,14 @@ Colours interpolate in RGB at ~30fps; single cell/ch lengths interpolate
|
|
|
184
185
|
to whole cells (movement steps cell by cell); every other supported
|
|
185
186
|
property applies discretely, switching at the segment midpoint (the CSS
|
|
186
187
|
rule for non-interpolable values). Layout-affecting animations re-flow
|
|
187
|
-
each frame. Easing applies per keyframe segment
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
`
|
|
188
|
+
each frame. Easing applies per keyframe segment (a timing function
|
|
189
|
+
declared inside a keyframe overrides the element's for that segment);
|
|
190
|
+
non-interpolable values switch when eased progress crosses the midpoint.
|
|
191
|
+
Transitions run per property with their own duration/timing;
|
|
192
|
+
interruptions continue from the current blended value. Keyframe
|
|
193
|
+
`var()`/`light-dark()` re-resolves on scheme/custom-property changes
|
|
194
|
+
without restarting the animation. Deviation: no `transition-delay` /
|
|
195
|
+
`animation-delay`.
|
|
193
196
|
|
|
194
197
|
## Values, functions and at-rules
|
|
195
198
|
|