@svelterm/core 0.23.0 → 0.27.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 +86 -0
- package/dist/src/components/text-buffer.d.ts +4 -0
- package/dist/src/components/text-buffer.js +20 -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 +23 -1
- package/dist/src/css/compute.js +94 -16
- package/dist/src/css/counters.d.ts +13 -0
- package/dist/src/css/counters.js +42 -0
- package/dist/src/css/incremental.js +1 -1
- package/dist/src/css/pseudo-elements.d.ts +2 -1
- package/dist/src/css/pseudo-elements.js +13 -11
- package/dist/src/index.js +12 -2
- package/dist/src/input/edit-constraints.d.ts +7 -0
- package/dist/src/input/edit-constraints.js +14 -0
- package/dist/src/layout/engine.js +73 -14
- package/dist/src/render/animation-clock.d.ts +9 -1
- package/dist/src/render/animation-clock.js +67 -42
- package/dist/src/render/paint.js +5 -1
- package/docs/elements.md +3 -2
- package/docs/layout.md +7 -3
- package/docs/motion.md +10 -9
- package/docs/reference.md +21 -14
- package/docs/selectors.md +21 -6
- package/docs/terminal-css.md +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,91 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.27.0 — 2026-07-05
|
|
4
|
+
|
|
5
|
+
Form control parity: password, maxlength, readonly.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`input type="password"`** — the value paints as `•` bullets; layout,
|
|
10
|
+
scrolling, and cursor behave as for text inputs and the real value
|
|
11
|
+
stays in `value`.
|
|
12
|
+
- **`maxlength`** on `input`/`textarea` — caps typing and paste at the
|
|
13
|
+
limit (counted in code units, as in HTML). An over-long initial value
|
|
14
|
+
is kept and can be edited down, matching browsers.
|
|
15
|
+
- **`readonly`** on `input`/`textarea` — blocks all edits (typing,
|
|
16
|
+
deletion, Ctrl+U/K, paste) while the caret still moves; the control
|
|
17
|
+
remains focusable.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `input` events now fire only when the value changes — caret-movement
|
|
22
|
+
keys (arrows, Home/End) no longer dispatch spurious `input` events.
|
|
23
|
+
|
|
24
|
+
## 0.26.0 — 2026-07-05
|
|
25
|
+
|
|
26
|
+
Sibling border collapse becomes explicit.
|
|
27
|
+
|
|
28
|
+
### Changed (breaking)
|
|
29
|
+
|
|
30
|
+
- **Sibling border collapse is now opt-in.** Adjacent bordered siblings
|
|
31
|
+
(stacked blocks, flex items, grid items) previously always shared a
|
|
32
|
+
border line with junction glyphs (`├` `┬` `┼`). Now that happens only
|
|
33
|
+
under `border-collapse: collapse` — an extension of the CSS table
|
|
34
|
+
property to all boxes. It inherits per spec, so opt in once on the
|
|
35
|
+
container or app-wide:
|
|
36
|
+
|
|
37
|
+
```css
|
|
38
|
+
:root { border-collapse: collapse; }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Without it, sibling frames render separately, matching browsers.
|
|
42
|
+
`border-collapse: separate` on a child opts it back out.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- `border-collapse` now inherits (CSS 2.2 §17.6) — previously it was
|
|
47
|
+
only read from the table element itself.
|
|
48
|
+
|
|
49
|
+
## 0.25.0 — 2026-07-05
|
|
50
|
+
|
|
51
|
+
Grid and generated-content completeness.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **`grid-auto-flow: column`** — auto-placed items fill down each
|
|
56
|
+
column, wrapping to a new (implicit) column after the explicit row
|
|
57
|
+
count; implicit columns take the last explicit column's width.
|
|
58
|
+
- **`minmax()` redistribution** — a `fr` track clamped to its
|
|
59
|
+
`minmax()` minimum leaves the distribution pool and the freed space
|
|
60
|
+
re-splits among the remaining `fr` tracks (previously other tracks
|
|
61
|
+
kept their naive share, overflowing the container).
|
|
62
|
+
- **`counter()` in `content:`** — `counter-reset` and
|
|
63
|
+
`counter-increment` (with optional amounts) resolve in document
|
|
64
|
+
order; flat namespace, no `counters()` nesting.
|
|
65
|
+
- **Pseudo-elements in table-internal boxes** — `::before`/`::after`
|
|
66
|
+
now render on rows, row groups, and table boxes per CSS
|
|
67
|
+
anonymous-box rules (a row pseudo becomes a leading anonymous cell);
|
|
68
|
+
previously they were silently dropped.
|
|
69
|
+
|
|
70
|
+
## 0.24.0 — 2026-07-05
|
|
71
|
+
|
|
72
|
+
Motion timing completeness: transitions and keyframes behave per spec.
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- **Per-property transitions** — `transition: color 150ms linear, width
|
|
77
|
+
400ms ease` runs each property on its own duration and timing;
|
|
78
|
+
longhand lists pair cyclically per spec. (Previously the first
|
|
79
|
+
duration and one timing function applied to everything.)
|
|
80
|
+
- **Interrupted transitions continue** — a retargeted transition starts
|
|
81
|
+
from its current blended value instead of jumping back to the previous
|
|
82
|
+
target (reversals no longer flash).
|
|
83
|
+
- **Per-keyframe `animation-timing-function`** — a timing function
|
|
84
|
+
declared inside a keyframe applies from that stop to the next.
|
|
85
|
+
- **Keyframe `var()`/`light-dark()` re-resolution** — scheme flips and
|
|
86
|
+
custom-property changes retarget a running animation in place, without
|
|
87
|
+
restarting it.
|
|
88
|
+
|
|
3
89
|
## 0.23.0 — 2026-07-05
|
|
4
90
|
|
|
5
91
|
Positioning: `relative` offsets apply, and `sticky` arrives.
|
|
@@ -2,6 +2,10 @@ import type { KeyEvent } from '../input/keyboard.js';
|
|
|
2
2
|
export declare class TextBuffer {
|
|
3
3
|
private _text;
|
|
4
4
|
private _cursor;
|
|
5
|
+
/** Insertion cap in code units, as HTML maxlength counts them. */
|
|
6
|
+
maxLength: number | null;
|
|
7
|
+
/** Blocks all mutation while leaving caret movement live. */
|
|
8
|
+
readOnly: boolean;
|
|
5
9
|
constructor(initial?: string);
|
|
6
10
|
get text(): string;
|
|
7
11
|
set text(value: string);
|
|
@@ -2,6 +2,10 @@ import { nextGraphemeBoundary, prevGraphemeBoundary } from '../layout/unicode.js
|
|
|
2
2
|
export class TextBuffer {
|
|
3
3
|
_text;
|
|
4
4
|
_cursor;
|
|
5
|
+
/** Insertion cap in code units, as HTML maxlength counts them. */
|
|
6
|
+
maxLength = null;
|
|
7
|
+
/** Blocks all mutation while leaving caret movement live. */
|
|
8
|
+
readOnly = false;
|
|
5
9
|
constructor(initial = '') {
|
|
6
10
|
this._text = initial;
|
|
7
11
|
this._cursor = initial.length;
|
|
@@ -13,16 +17,28 @@ export class TextBuffer {
|
|
|
13
17
|
this._cursor = Math.max(0, Math.min(value, this._text.length));
|
|
14
18
|
}
|
|
15
19
|
insert(chars) {
|
|
20
|
+
if (this.readOnly)
|
|
21
|
+
return;
|
|
22
|
+
if (this.maxLength !== null) {
|
|
23
|
+
const room = Math.max(0, this.maxLength - this._text.length);
|
|
24
|
+
chars = chars.slice(0, room);
|
|
25
|
+
}
|
|
26
|
+
if (chars.length === 0)
|
|
27
|
+
return;
|
|
16
28
|
this._text = this._text.substring(0, this._cursor) + chars + this._text.substring(this._cursor);
|
|
17
29
|
this._cursor += chars.length;
|
|
18
30
|
}
|
|
19
31
|
delete() {
|
|
32
|
+
if (this.readOnly)
|
|
33
|
+
return;
|
|
20
34
|
if (this._cursor >= this._text.length)
|
|
21
35
|
return;
|
|
22
36
|
const end = nextGraphemeBoundary(this._text, this._cursor);
|
|
23
37
|
this._text = this._text.substring(0, this._cursor) + this._text.substring(end);
|
|
24
38
|
}
|
|
25
39
|
backspace() {
|
|
40
|
+
if (this.readOnly)
|
|
41
|
+
return;
|
|
26
42
|
if (this._cursor <= 0)
|
|
27
43
|
return;
|
|
28
44
|
const start = prevGraphemeBoundary(this._text, this._cursor);
|
|
@@ -34,10 +50,14 @@ export class TextBuffer {
|
|
|
34
50
|
home() { this._cursor = 0; }
|
|
35
51
|
end() { this._cursor = this._text.length; }
|
|
36
52
|
clearToStart() {
|
|
53
|
+
if (this.readOnly)
|
|
54
|
+
return;
|
|
37
55
|
this._text = this._text.substring(this._cursor);
|
|
38
56
|
this._cursor = 0;
|
|
39
57
|
}
|
|
40
58
|
clearToEnd() {
|
|
59
|
+
if (this.readOnly)
|
|
60
|
+
return;
|
|
41
61
|
this._text = this._text.substring(0, this._cursor);
|
|
42
62
|
}
|
|
43
63
|
handleKey(key) {
|
|
@@ -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) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TermNode } from '../renderer/node.js';
|
|
2
2
|
import { NodeMap } from '../utils/node-map.js';
|
|
3
|
+
import { CounterContext } from './counters.js';
|
|
3
4
|
export type StyleMap = NodeMap<ResolvedStyle>;
|
|
4
5
|
import { CSSStyleSheet } from './parser.js';
|
|
5
6
|
import { type MediaContext } from './media.js';
|
|
@@ -45,6 +46,9 @@ export interface ResolvedStyle {
|
|
|
45
46
|
gridRowEnd: number | null;
|
|
46
47
|
gridRowSpan: number | null;
|
|
47
48
|
gridTemplateAreas: string | null;
|
|
49
|
+
gridAutoFlow: 'row' | 'column';
|
|
50
|
+
counterReset: string | null;
|
|
51
|
+
counterIncrement: string | null;
|
|
48
52
|
gridArea: string | null;
|
|
49
53
|
animationName: string | null;
|
|
50
54
|
animationDuration: number;
|
|
@@ -53,6 +57,15 @@ export interface ResolvedStyle {
|
|
|
53
57
|
transitionProperty: string | null;
|
|
54
58
|
transitionDuration: number;
|
|
55
59
|
transitionTimingFunction: string;
|
|
60
|
+
/** Longhand lists, paired cyclically per spec into `transitions`. */
|
|
61
|
+
transitionDurations: number[];
|
|
62
|
+
transitionTimings: string[];
|
|
63
|
+
/** Per-property transition config, resolved after cascade. */
|
|
64
|
+
transitions: Array<{
|
|
65
|
+
property: string;
|
|
66
|
+
duration: number;
|
|
67
|
+
timing: string;
|
|
68
|
+
}>;
|
|
56
69
|
borderStyle: 'none' | 'single' | 'double' | 'rounded' | 'heavy' | 'ascii' | 'eighth-cell-inner' | 'eighth-cell-outer' | 'half-cell-inner' | 'half-cell-outer' | 'full-cell';
|
|
57
70
|
borderCorner: 'none' | 'h' | 'v';
|
|
58
71
|
borderColor: string;
|
|
@@ -86,5 +99,14 @@ export interface ResolvedStyle {
|
|
|
86
99
|
export declare function defaultStyle(tag?: string): ResolvedStyle;
|
|
87
100
|
export declare function resolveStyles(root: TermNode, stylesheet: CSSStyleSheet, media?: MediaContext, availWidth?: number, availHeight?: number): Map<number, ResolvedStyle>;
|
|
88
101
|
export declare function filterByMedia(stylesheet: CSSStyleSheet, context: MediaContext): CSSStyleSheet;
|
|
89
|
-
export declare function resolveNode(node: TermNode, stylesheet: CSSStyleSheet, styles: Map<number, ResolvedStyle>, variables: Map<number, Map<string, string>>, scheme?: 'dark' | 'light'): void;
|
|
102
|
+
export declare function resolveNode(node: TermNode, stylesheet: CSSStyleSheet, styles: Map<number, ResolvedStyle>, variables: Map<number, Map<string, string>>, scheme?: 'dark' | 'light', counters?: CounterContext): void;
|
|
90
103
|
export declare function applyDeclaration(style: ResolvedStyle, property: string, value: string, scheme?: 'dark' | 'light'): void;
|
|
104
|
+
/**
|
|
105
|
+
* Pair the transition lists per spec: durations/timings repeat
|
|
106
|
+
* cyclically to cover every listed property.
|
|
107
|
+
*/
|
|
108
|
+
export declare function pairTransitions(style: ResolvedStyle): Array<{
|
|
109
|
+
property: string;
|
|
110
|
+
duration: number;
|
|
111
|
+
timing: string;
|
|
112
|
+
}>;
|
package/dist/src/css/compute.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NodeMap } from '../utils/node-map.js';
|
|
2
2
|
import { resolvePseudoElements } from './pseudo-elements.js';
|
|
3
|
+
import { CounterContext } from './counters.js';
|
|
3
4
|
import { matchesSelector } from './selector.js';
|
|
4
5
|
import { resolveColor, expandLightDark } from './color.js';
|
|
5
6
|
import { parseCellValue, parseSizeValue, parseJustify, parseAlign } from './values.js';
|
|
@@ -51,11 +52,15 @@ export function defaultStyle(tag) {
|
|
|
51
52
|
gridTemplateColumns: null, gridTemplateRows: null,
|
|
52
53
|
gridColumnStart: null, gridColumnEnd: null, gridColumnSpan: null,
|
|
53
54
|
gridRowStart: null, gridRowEnd: null, gridRowSpan: null,
|
|
54
|
-
gridTemplateAreas: null,
|
|
55
|
+
gridTemplateAreas: null,
|
|
56
|
+
gridAutoFlow: 'row',
|
|
57
|
+
counterReset: null, counterIncrement: null, gridArea: null,
|
|
55
58
|
animationName: null, animationDuration: 0, animationIterationCount: 1,
|
|
56
59
|
animationTimingFunction: 'ease',
|
|
57
60
|
transitionProperty: null, transitionDuration: 0,
|
|
58
61
|
transitionTimingFunction: 'ease',
|
|
62
|
+
transitionDurations: [], transitionTimings: [],
|
|
63
|
+
transitions: [],
|
|
59
64
|
borderStyle: 'none', borderColor: 'default', borderCorner: 'none',
|
|
60
65
|
borderTop: true, borderRight: true, borderBottom: true, borderLeft: true,
|
|
61
66
|
boxSizing: 'border-box',
|
|
@@ -204,7 +209,7 @@ function evaluateSupports(condition) {
|
|
|
204
209
|
const property = condition.substring(0, colonIdx).trim();
|
|
205
210
|
return SUPPORTED_PROPERTIES.has(property);
|
|
206
211
|
}
|
|
207
|
-
export function resolveNode(node, stylesheet, styles, variables, scheme = 'dark') {
|
|
212
|
+
export function resolveNode(node, stylesheet, styles, variables, scheme = 'dark', counters = new CounterContext()) {
|
|
208
213
|
if (node.nodeType === 'element') {
|
|
209
214
|
const vars = variables.get(node.id) ?? new Map();
|
|
210
215
|
const parentStyle = node.parent ? styles.get(node.parent.id) : undefined;
|
|
@@ -212,14 +217,20 @@ export function resolveNode(node, stylesheet, styles, variables, scheme = 'dark'
|
|
|
212
217
|
styles.set(node.id, resolved);
|
|
213
218
|
node.cache.resolvedStyle = resolved;
|
|
214
219
|
node.cache.classAttr = node.attributes.get('class') ?? '';
|
|
215
|
-
|
|
220
|
+
// Counter effects apply in document order, before pseudo content
|
|
221
|
+
counters.enter(resolved);
|
|
222
|
+
resolvePseudoElements(node, stylesheet, styles, vars, scheme, counters);
|
|
216
223
|
}
|
|
217
224
|
for (const child of node.children) {
|
|
218
|
-
resolveNode(child, stylesheet, styles, variables, scheme);
|
|
225
|
+
resolveNode(child, stylesheet, styles, variables, scheme, counters);
|
|
219
226
|
}
|
|
220
227
|
}
|
|
221
228
|
function computeStyle(node, stylesheet, vars, parentStyle, scheme = 'dark') {
|
|
222
229
|
const style = defaultStyle(node.tag);
|
|
230
|
+
// border-collapse is an inherited property (CSS 2.2 §17.6), so opting a
|
|
231
|
+
// container in flows down to the siblings the collapse applies between.
|
|
232
|
+
if (parentStyle)
|
|
233
|
+
style.borderCollapse = parentStyle.borderCollapse;
|
|
223
234
|
// Collect all matching declarations with specificity
|
|
224
235
|
const scored = [];
|
|
225
236
|
let order = 0;
|
|
@@ -272,6 +283,8 @@ function computeStyle(node, stylesheet, vars, parentStyle, scheme = 'dark') {
|
|
|
272
283
|
applyDeclaration(style, decl.property, resolveVar(decl.value, vars), scheme);
|
|
273
284
|
}
|
|
274
285
|
}
|
|
286
|
+
// Pair transition longhand lists now the cascade is complete
|
|
287
|
+
style.transitions = pairTransitions(style);
|
|
275
288
|
return style;
|
|
276
289
|
}
|
|
277
290
|
function parseInlineStyle(text) {
|
|
@@ -288,6 +301,7 @@ function parseInlineStyle(text) {
|
|
|
288
301
|
return result;
|
|
289
302
|
}
|
|
290
303
|
const INHERITABLE_PROPERTIES = new Set([
|
|
304
|
+
'border-collapse',
|
|
291
305
|
'color', 'font-weight', 'font-style', 'text-decoration',
|
|
292
306
|
'white-space', 'word-break', 'text-align', 'visibility', 'opacity',
|
|
293
307
|
]);
|
|
@@ -313,6 +327,9 @@ function applyInherit(style, property, parentStyle) {
|
|
|
313
327
|
case 'white-space':
|
|
314
328
|
style.whiteSpace = parentStyle.whiteSpace;
|
|
315
329
|
break;
|
|
330
|
+
case 'border-collapse':
|
|
331
|
+
style.borderCollapse = parentStyle.borderCollapse;
|
|
332
|
+
break;
|
|
316
333
|
case 'word-break':
|
|
317
334
|
style.wordBreak = parentStyle.wordBreak;
|
|
318
335
|
break;
|
|
@@ -553,6 +570,15 @@ export function applyDeclaration(style, property, value, scheme = 'dark') {
|
|
|
553
570
|
case 'grid-area':
|
|
554
571
|
parseGridArea(style, value);
|
|
555
572
|
break;
|
|
573
|
+
case 'grid-auto-flow':
|
|
574
|
+
style.gridAutoFlow = value.startsWith('column') ? 'column' : 'row';
|
|
575
|
+
break;
|
|
576
|
+
case 'counter-reset':
|
|
577
|
+
style.counterReset = value === 'none' ? null : value;
|
|
578
|
+
break;
|
|
579
|
+
case 'counter-increment':
|
|
580
|
+
style.counterIncrement = value === 'none' ? null : value;
|
|
581
|
+
break;
|
|
556
582
|
case 'animation':
|
|
557
583
|
parseAnimationShorthand(style, value);
|
|
558
584
|
break;
|
|
@@ -563,10 +589,12 @@ export function applyDeclaration(style, property, value, scheme = 'dark') {
|
|
|
563
589
|
style.transitionProperty = value === 'none' ? null : value;
|
|
564
590
|
break;
|
|
565
591
|
case 'transition-duration':
|
|
566
|
-
style.
|
|
592
|
+
style.transitionDurations = value.split(',').map(v => parseDuration(v.trim()));
|
|
593
|
+
style.transitionDuration = style.transitionDurations[0] ?? 0;
|
|
567
594
|
break;
|
|
568
595
|
case 'transition-timing-function':
|
|
569
|
-
style.
|
|
596
|
+
style.transitionTimings = splitTimingList(value);
|
|
597
|
+
style.transitionTimingFunction = style.transitionTimings[0] ?? 'ease';
|
|
570
598
|
break;
|
|
571
599
|
case 'animation-name':
|
|
572
600
|
style.animationName = value === 'none' ? null : value;
|
|
@@ -806,35 +834,85 @@ function parseAnimationShorthand(style, value) {
|
|
|
806
834
|
}
|
|
807
835
|
}
|
|
808
836
|
}
|
|
809
|
-
/**
|
|
837
|
+
/** Split a timing-function list on commas, keeping function args intact. */
|
|
838
|
+
function splitTimingList(value) {
|
|
839
|
+
const out = [];
|
|
840
|
+
let depth = 0;
|
|
841
|
+
let current = '';
|
|
842
|
+
for (const ch of value) {
|
|
843
|
+
if (ch === '(')
|
|
844
|
+
depth++;
|
|
845
|
+
if (ch === ')')
|
|
846
|
+
depth--;
|
|
847
|
+
if (ch === ',' && depth === 0) {
|
|
848
|
+
out.push(current.trim());
|
|
849
|
+
current = '';
|
|
850
|
+
}
|
|
851
|
+
else
|
|
852
|
+
current += ch;
|
|
853
|
+
}
|
|
854
|
+
if (current.trim())
|
|
855
|
+
out.push(current.trim());
|
|
856
|
+
return out;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Parse `transition: <property> <duration> <timing> [...]` — each
|
|
860
|
+
* comma-separated group configures one property (or `all`).
|
|
861
|
+
*/
|
|
810
862
|
function parseTransitionShorthand(style, value) {
|
|
811
863
|
const trimmed = value.trim();
|
|
812
864
|
if (trimmed === 'none') {
|
|
813
865
|
style.transitionProperty = null;
|
|
814
866
|
style.transitionDuration = 0;
|
|
867
|
+
style.transitionDurations = [];
|
|
868
|
+
style.transitionTimings = [];
|
|
815
869
|
return;
|
|
816
870
|
}
|
|
817
|
-
const { easing, rest } = extractEasingFunction(trimmed);
|
|
818
|
-
if (easing)
|
|
819
|
-
style.transitionTimingFunction = easing;
|
|
820
871
|
const properties = [];
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
872
|
+
const durations = [];
|
|
873
|
+
const timings = [];
|
|
874
|
+
for (const groupRaw of splitTimingList(trimmed)) {
|
|
875
|
+
const { easing, rest } = extractEasingFunction(groupRaw);
|
|
876
|
+
let property = 'all';
|
|
877
|
+
let duration = 0;
|
|
878
|
+
let timing = easing ?? 'ease';
|
|
879
|
+
for (const token of rest.trim().split(/\s+/)) {
|
|
824
880
|
if (/^\d*\.?\d+(ms|s)$/.test(token)) {
|
|
825
881
|
if (duration === 0)
|
|
826
882
|
duration = parseDuration(token);
|
|
827
883
|
}
|
|
828
884
|
else if (TIMING_KEYWORDS.has(token)) {
|
|
829
|
-
|
|
885
|
+
timing = token;
|
|
830
886
|
}
|
|
831
887
|
else if (token) {
|
|
832
|
-
|
|
888
|
+
property = token;
|
|
833
889
|
}
|
|
834
890
|
}
|
|
891
|
+
properties.push(property);
|
|
892
|
+
durations.push(duration);
|
|
893
|
+
timings.push(timing);
|
|
835
894
|
}
|
|
836
895
|
style.transitionProperty = properties.length > 0 ? properties.join(',') : 'all';
|
|
837
|
-
style.
|
|
896
|
+
style.transitionDurations = durations;
|
|
897
|
+
style.transitionTimings = timings;
|
|
898
|
+
style.transitionDuration = durations[0] ?? 0;
|
|
899
|
+
style.transitionTimingFunction = timings[0] ?? 'ease';
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Pair the transition lists per spec: durations/timings repeat
|
|
903
|
+
* cyclically to cover every listed property.
|
|
904
|
+
*/
|
|
905
|
+
export function pairTransitions(style) {
|
|
906
|
+
if (!style.transitionProperty)
|
|
907
|
+
return [];
|
|
908
|
+
const properties = style.transitionProperty.split(',').map(p => p.trim()).filter(Boolean);
|
|
909
|
+
const durations = style.transitionDurations.length > 0 ? style.transitionDurations : [style.transitionDuration];
|
|
910
|
+
const timings = style.transitionTimings.length > 0 ? style.transitionTimings : [style.transitionTimingFunction];
|
|
911
|
+
return properties.map((property, i) => ({
|
|
912
|
+
property,
|
|
913
|
+
duration: durations[i % durations.length] ?? 0,
|
|
914
|
+
timing: timings[i % timings.length] ?? 'ease',
|
|
915
|
+
}));
|
|
838
916
|
}
|
|
839
917
|
function parseDuration(value) {
|
|
840
918
|
if (value.endsWith('ms'))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS counters for `content: counter(name)` — threaded through the
|
|
3
|
+
* style-resolution walk, which visits elements in document order.
|
|
4
|
+
* Flat namespace (no per-scope nesting or `counters()` joining yet);
|
|
5
|
+
* incremental restyles reuse the values from the last full resolve.
|
|
6
|
+
*/
|
|
7
|
+
import type { ResolvedStyle } from './compute.js';
|
|
8
|
+
export declare class CounterContext {
|
|
9
|
+
private values;
|
|
10
|
+
/** Apply an element's counter-reset then counter-increment. */
|
|
11
|
+
enter(style: ResolvedStyle): void;
|
|
12
|
+
value(name: string): number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS counters for `content: counter(name)` — threaded through the
|
|
3
|
+
* style-resolution walk, which visits elements in document order.
|
|
4
|
+
* Flat namespace (no per-scope nesting or `counters()` joining yet);
|
|
5
|
+
* incremental restyles reuse the values from the last full resolve.
|
|
6
|
+
*/
|
|
7
|
+
export class CounterContext {
|
|
8
|
+
values = new Map();
|
|
9
|
+
/** Apply an element's counter-reset then counter-increment. */
|
|
10
|
+
enter(style) {
|
|
11
|
+
if (style.counterReset) {
|
|
12
|
+
for (const { name, amount } of parseCounterList(style.counterReset, 0)) {
|
|
13
|
+
this.values.set(name, amount);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (style.counterIncrement) {
|
|
17
|
+
for (const { name, amount } of parseCounterList(style.counterIncrement, 1)) {
|
|
18
|
+
this.values.set(name, (this.values.get(name) ?? 0) + amount);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
value(name) {
|
|
23
|
+
return this.values.get(name) ?? 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Parse `name [amount] name [amount] …` with a per-property default. */
|
|
27
|
+
function parseCounterList(value, defaultAmount) {
|
|
28
|
+
const out = [];
|
|
29
|
+
const tokens = value.trim().split(/\s+/);
|
|
30
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
31
|
+
const name = tokens[i];
|
|
32
|
+
const next = tokens[i + 1];
|
|
33
|
+
if (next !== undefined && /^-?\d+$/.test(next)) {
|
|
34
|
+
out.push({ name, amount: parseInt(next, 10) });
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
out.push({ name, amount: defaultAmount });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
@@ -8,7 +8,7 @@ const LAYOUT_PROPERTIES = [
|
|
|
8
8
|
'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
|
|
9
9
|
'borderStyle', 'borderTop', 'borderRight', 'borderBottom', 'borderLeft',
|
|
10
10
|
'position', 'top', 'right', 'bottom', 'left',
|
|
11
|
-
'overflow', 'whiteSpace',
|
|
11
|
+
'overflow', 'whiteSpace', 'borderCollapse',
|
|
12
12
|
];
|
|
13
13
|
/**
|
|
14
14
|
* Incremental style resolution — re-resolves dirty nodes and their
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { TermNode } from '../renderer/node.js';
|
|
2
2
|
import { CSSStyleSheet } from './parser.js';
|
|
3
3
|
import { type ResolvedStyle } from './compute.js';
|
|
4
|
+
import type { CounterContext } from './counters.js';
|
|
4
5
|
/**
|
|
5
6
|
* Resolve ::before/::after for one element: build the pseudo's style from
|
|
6
7
|
* matching rules, materialise (or drop) its synthetic box on the node, and
|
|
7
8
|
* record the style under the synthetic node's id.
|
|
8
9
|
*/
|
|
9
|
-
export declare function resolvePseudoElements(node: TermNode, stylesheet: CSSStyleSheet, styles: Map<number, ResolvedStyle>, vars: Map<string, string>, scheme: 'dark' | 'light'): void;
|
|
10
|
+
export declare function resolvePseudoElements(node: TermNode, stylesheet: CSSStyleSheet, styles: Map<number, ResolvedStyle>, vars: Map<string, string>, scheme: 'dark' | 'light', counters?: CounterContext): void;
|
|
@@ -8,13 +8,13 @@ import { defaultStyle, applyDeclaration } from './compute.js';
|
|
|
8
8
|
* matching rules, materialise (or drop) its synthetic box on the node, and
|
|
9
9
|
* record the style under the synthetic node's id.
|
|
10
10
|
*/
|
|
11
|
-
export function resolvePseudoElements(node, stylesheet, styles, vars, scheme) {
|
|
12
|
-
node.pseudoBefore = syncPseudo(node, node.pseudoBefore, 'before', stylesheet, styles, vars, scheme);
|
|
13
|
-
node.pseudoAfter = syncPseudo(node, node.pseudoAfter, 'after', stylesheet, styles, vars, scheme);
|
|
11
|
+
export function resolvePseudoElements(node, stylesheet, styles, vars, scheme, counters) {
|
|
12
|
+
node.pseudoBefore = syncPseudo(node, node.pseudoBefore, 'before', stylesheet, styles, vars, scheme, counters);
|
|
13
|
+
node.pseudoAfter = syncPseudo(node, node.pseudoAfter, 'after', stylesheet, styles, vars, scheme, counters);
|
|
14
14
|
}
|
|
15
|
-
function syncPseudo(host, existing, which, stylesheet, styles, vars, scheme) {
|
|
15
|
+
function syncPseudo(host, existing, which, stylesheet, styles, vars, scheme, counters) {
|
|
16
16
|
const declarations = collectPseudoDeclarations(host, which, stylesheet, vars);
|
|
17
|
-
const content = resolveContent(declarations, host);
|
|
17
|
+
const content = resolveContent(declarations, host, counters);
|
|
18
18
|
if (content === null) {
|
|
19
19
|
if (existing)
|
|
20
20
|
styles.delete(existing.id);
|
|
@@ -65,22 +65,24 @@ function collectPseudoDeclarations(host, which, stylesheet, vars) {
|
|
|
65
65
|
* The winning `content` value rendered to text, or null when the pseudo
|
|
66
66
|
* generates no box (no content declaration, `none`/`normal`, or empty).
|
|
67
67
|
*/
|
|
68
|
-
function resolveContent(declarations, host) {
|
|
68
|
+
function resolveContent(declarations, host, counters) {
|
|
69
69
|
const winner = declarations.filter(d => d.property === 'content').pop();
|
|
70
70
|
if (!winner)
|
|
71
71
|
return null;
|
|
72
|
-
const text = parseContentValue(winner.value, host);
|
|
72
|
+
const text = parseContentValue(winner.value, host, counters);
|
|
73
73
|
return text === '' ? null : text;
|
|
74
74
|
}
|
|
75
|
-
const CONTENT_TOKEN = /"([^"]*)"|'([^']*)'|attr\(\s*([^)\s]+)\s*\)/g;
|
|
76
|
-
/** content:
|
|
77
|
-
function parseContentValue(value, host) {
|
|
75
|
+
const CONTENT_TOKEN = /"([^"]*)"|'([^']*)'|attr\(\s*([^)\s]+)\s*\)|counter\(\s*([a-zA-Z0-9_-]+)\s*(?:,[^)]*)?\)/g;
|
|
76
|
+
/** content: quoted strings, attr() lookups, and counter() values. */
|
|
77
|
+
function parseContentValue(value, host, counters) {
|
|
78
78
|
const trimmed = value.trim();
|
|
79
79
|
if (trimmed === 'none' || trimmed === 'normal')
|
|
80
80
|
return '';
|
|
81
81
|
let text = '';
|
|
82
82
|
for (const match of trimmed.matchAll(CONTENT_TOKEN)) {
|
|
83
|
-
if (match[
|
|
83
|
+
if (match[4] !== undefined)
|
|
84
|
+
text += String(counters?.value(match[4]) ?? 0);
|
|
85
|
+
else if (match[3] !== undefined)
|
|
84
86
|
text += host.attributes.get(match[3]) ?? '';
|
|
85
87
|
else
|
|
86
88
|
text += match[1] ?? match[2] ?? '';
|
package/dist/src/index.js
CHANGED
|
@@ -27,6 +27,7 @@ import { toggleDetails } from './input/details.js';
|
|
|
27
27
|
import { cycleSelect } from './input/select.js';
|
|
28
28
|
import { labelledControl } from './input/label.js';
|
|
29
29
|
import { TextBuffer } from './components/text-buffer.js';
|
|
30
|
+
import { syncEditConstraints } from './input/edit-constraints.js';
|
|
30
31
|
import { StdinRouter, matchOSC11, parseOSC11Scheme } from './terminal/stdin-router.js';
|
|
31
32
|
import { detectCapabilities, matchCPR, parseCPRRow } from './terminal/capabilities.js';
|
|
32
33
|
import { copyToClipboard } from './terminal/clipboard.js';
|
|
@@ -490,6 +491,8 @@ export function run(AppComponent, options) {
|
|
|
490
491
|
if (focused && (focused.tag === 'input' || focused.tag === 'textarea') && !isCheckableInput(focused)) {
|
|
491
492
|
if (!focused.textBuffer)
|
|
492
493
|
focused.textBuffer = new TextBuffer(focused.attributes.get('value') ?? '');
|
|
494
|
+
syncEditConstraints(focused);
|
|
495
|
+
const oldValue = focused.textBuffer.text;
|
|
493
496
|
if (focused.textBuffer.handleKey(key)) {
|
|
494
497
|
const newValue = focused.textBuffer.text;
|
|
495
498
|
focused.attributes.set('value', newValue);
|
|
@@ -498,7 +501,10 @@ export function run(AppComponent, options) {
|
|
|
498
501
|
ctx.onSetText(textChild, newValue);
|
|
499
502
|
// Enqueue the input element itself for repaint (cursor may have moved)
|
|
500
503
|
ctx.queue.enqueuePaintOnly(focused);
|
|
501
|
-
|
|
504
|
+
// input fires on value change, not caret movement (per spec)
|
|
505
|
+
if (newValue !== oldValue) {
|
|
506
|
+
dispatchEvent(focused, 'input', { value: newValue, cursor: focused.textBuffer.cursor });
|
|
507
|
+
}
|
|
502
508
|
scheduleRender();
|
|
503
509
|
return;
|
|
504
510
|
}
|
|
@@ -528,13 +534,17 @@ export function run(AppComponent, options) {
|
|
|
528
534
|
if (focused && (focused.tag === 'input' || focused.tag === 'textarea')) {
|
|
529
535
|
if (!focused.textBuffer)
|
|
530
536
|
focused.textBuffer = new TextBuffer(focused.attributes.get('value') ?? '');
|
|
537
|
+
syncEditConstraints(focused);
|
|
538
|
+
const oldValue = focused.textBuffer.text;
|
|
531
539
|
focused.textBuffer.insert(text);
|
|
532
540
|
const newValue = focused.textBuffer.text;
|
|
533
541
|
focused.attributes.set('value', newValue);
|
|
534
542
|
const textChild = focused.children.find(c => c.nodeType === 'text');
|
|
535
543
|
if (textChild)
|
|
536
544
|
ctx.onSetText(textChild, newValue);
|
|
537
|
-
|
|
545
|
+
if (newValue !== oldValue) {
|
|
546
|
+
dispatchEvent(focused, 'input', { value: newValue, cursor: focused.textBuffer.cursor });
|
|
547
|
+
}
|
|
538
548
|
scheduleRender();
|
|
539
549
|
}
|
|
540
550
|
else {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { TermNode } from '../renderer/node.js';
|
|
2
|
+
/**
|
|
3
|
+
* Mirror the element's maxlength/readonly attributes onto its TextBuffer
|
|
4
|
+
* before each edit, so attribute changes made after focus apply
|
|
5
|
+
* immediately (Svelte removes boolean attributes when false).
|
|
6
|
+
*/
|
|
7
|
+
export declare function syncEditConstraints(node: TermNode): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirror the element's maxlength/readonly attributes onto its TextBuffer
|
|
3
|
+
* before each edit, so attribute changes made after focus apply
|
|
4
|
+
* immediately (Svelte removes boolean attributes when false).
|
|
5
|
+
*/
|
|
6
|
+
export function syncEditConstraints(node) {
|
|
7
|
+
if (!node.textBuffer)
|
|
8
|
+
return;
|
|
9
|
+
const max = node.attributes.get('maxlength');
|
|
10
|
+
const parsed = max !== undefined ? parseInt(max, 10) : NaN;
|
|
11
|
+
node.textBuffer.maxLength = Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
12
|
+
const readonly = node.attributes.get('readonly');
|
|
13
|
+
node.textBuffer.readOnly = readonly !== undefined && readonly !== 'false';
|
|
14
|
+
}
|
|
@@ -7,13 +7,17 @@ import { imageIntrinsicSize } from '../render/image.js';
|
|
|
7
7
|
import { resolveSize, constrain } from './size.js';
|
|
8
8
|
import { parseCellLength } from '../css/values.js';
|
|
9
9
|
/**
|
|
10
|
-
* Check if two adjacent siblings
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Check if two adjacent siblings should share a single border line on
|
|
11
|
+
* their common edge (overlapping by 1 cell so the strokes merge into
|
|
12
|
+
* junction glyphs). Opt-in: both siblings must be under border-collapse:
|
|
13
|
+
* collapse — an inherited property, so setting it on the container (or
|
|
14
|
+
* :root) is enough — and both must have a border on the shared edge.
|
|
13
15
|
*/
|
|
14
16
|
function shouldAdjustBorderGap(prevStyle, nextStyle, direction) {
|
|
15
17
|
if (!prevStyle || !nextStyle)
|
|
16
18
|
return false;
|
|
19
|
+
if (prevStyle.borderCollapse !== 'collapse' || nextStyle.borderCollapse !== 'collapse')
|
|
20
|
+
return false;
|
|
17
21
|
if (prevStyle.borderStyle === 'none' || nextStyle.borderStyle === 'none')
|
|
18
22
|
return false;
|
|
19
23
|
if (direction === 'vertical') {
|
|
@@ -526,7 +530,7 @@ function isRowLevelDisplay(display) {
|
|
|
526
530
|
|| display === 'table-column-group';
|
|
527
531
|
}
|
|
528
532
|
function cellsOfRow(trNode, styles) {
|
|
529
|
-
return trNode.
|
|
533
|
+
return childrenWithPseudos(trNode).filter(c => isCellContent(c, styles));
|
|
530
534
|
}
|
|
531
535
|
/**
|
|
532
536
|
* Group the children of a table or row-group into rows. Explicit table-rows
|
|
@@ -574,15 +578,15 @@ function collectTableRows(children, styles) {
|
|
|
574
578
|
const display = child.nodeType === 'element' ? styles.get(child.id)?.display : undefined;
|
|
575
579
|
if (display === 'table-header-group') {
|
|
576
580
|
flushStray();
|
|
577
|
-
headerRows.push(...groupIntoRows(child
|
|
581
|
+
headerRows.push(...groupIntoRows(childrenWithPseudos(child), styles));
|
|
578
582
|
}
|
|
579
583
|
else if (display === 'table-footer-group') {
|
|
580
584
|
flushStray();
|
|
581
|
-
footerRows.push(...groupIntoRows(child
|
|
585
|
+
footerRows.push(...groupIntoRows(childrenWithPseudos(child), styles));
|
|
582
586
|
}
|
|
583
587
|
else if (display === 'table-row-group') {
|
|
584
588
|
flushStray();
|
|
585
|
-
bodyRows.push(...groupIntoRows(child
|
|
589
|
+
bodyRows.push(...groupIntoRows(childrenWithPseudos(child), styles));
|
|
586
590
|
}
|
|
587
591
|
else {
|
|
588
592
|
// table-rows and stray cell content accumulate; captions/columns
|
|
@@ -774,7 +778,7 @@ function placeRows(rows, grid, styles, boxes, x, startY, colWidths, tableWidth,
|
|
|
774
778
|
return rowY - startY - (rows.length > 0 ? gaps.row : 0);
|
|
775
779
|
}
|
|
776
780
|
function layoutTable(node, styles, boxes, x, y, availW, availH) {
|
|
777
|
-
return layoutTableChildren(node
|
|
781
|
+
return layoutTableChildren(childrenWithPseudos(node), styles.get(node.id), styles, boxes, x, y, availW, availH);
|
|
778
782
|
}
|
|
779
783
|
/**
|
|
780
784
|
* Table layout over a list of children. Called with a table element's
|
|
@@ -829,6 +833,10 @@ function layoutGrid(node, styles, boxes, x, y, availW, availH, style) {
|
|
|
829
833
|
}
|
|
830
834
|
const rowHeights = parseGridTemplate(style.gridTemplateRows ?? '', availH);
|
|
831
835
|
const numCols = colWidths.length || 1;
|
|
836
|
+
const columnFlow = style.gridAutoFlow === 'column';
|
|
837
|
+
// Column flow wraps at the explicit row count; implicit columns take
|
|
838
|
+
// the last explicit column's width.
|
|
839
|
+
const numRows = rowHeights.length || 1;
|
|
832
840
|
const gap = style.gap ?? 0;
|
|
833
841
|
// Pre-compute border-adjusted gaps for grid children
|
|
834
842
|
let hGap = gap;
|
|
@@ -850,7 +858,12 @@ function layoutGrid(node, styles, boxes, x, y, availW, availH, style) {
|
|
|
850
858
|
for (const child of children) {
|
|
851
859
|
const childStyle = styles.get(child.id);
|
|
852
860
|
const area = childStyle?.gridArea ? areas?.byName.get(childStyle.gridArea) : undefined;
|
|
853
|
-
const placed =
|
|
861
|
+
const placed = columnFlow
|
|
862
|
+
? resolveGridPlacementColumn(childStyle, area, cursor, numRows)
|
|
863
|
+
: resolveGridPlacement(childStyle, area, cursor, numCols);
|
|
864
|
+
while (columnFlow && placed.col >= colWidths.length && colWidths.length > 0) {
|
|
865
|
+
colWidths.push(colWidths[colWidths.length - 1]);
|
|
866
|
+
}
|
|
854
867
|
const colW = trackSpanSize(colWidths, placed.col, placed.span, hGap);
|
|
855
868
|
// Measure content height with unconstrained available height
|
|
856
869
|
const size = layoutNode(child, styles, boxes, 0, 0, colW, availH);
|
|
@@ -884,6 +897,30 @@ function layoutGrid(node, styles, boxes, x, y, availW, availH, style) {
|
|
|
884
897
|
const totalHeight = totalRows === 0 ? 0 : trackOffset(trackHeights, totalRows, vGap) - vGap;
|
|
885
898
|
return { width: maxWidth, height: totalHeight };
|
|
886
899
|
}
|
|
900
|
+
/**
|
|
901
|
+
* grid-auto-flow: column — auto-placed items fill down each column,
|
|
902
|
+
* wrapping to a new (implicit) column after the explicit row count.
|
|
903
|
+
* Explicit line placements behave as in row flow.
|
|
904
|
+
*/
|
|
905
|
+
function resolveGridPlacementColumn(childStyle, area, cursor, numRows) {
|
|
906
|
+
if (area) {
|
|
907
|
+
return {
|
|
908
|
+
col: area.colStart, span: area.colEnd - area.colStart,
|
|
909
|
+
row: area.rowStart, rowSpan: area.rowEnd - area.rowStart,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
if (cursor.row >= numRows) {
|
|
913
|
+
cursor.row = 0;
|
|
914
|
+
cursor.col++;
|
|
915
|
+
}
|
|
916
|
+
const col = childStyle?.gridColumnStart != null ? childStyle.gridColumnStart - 1 : cursor.col;
|
|
917
|
+
const row = childStyle?.gridRowStart != null ? childStyle.gridRowStart - 1 : cursor.row;
|
|
918
|
+
const span = childStyle?.gridColumnSpan ?? 1;
|
|
919
|
+
const rowSpan = childStyle?.gridRowSpan ?? 1;
|
|
920
|
+
cursor.col = col;
|
|
921
|
+
cursor.row = row + rowSpan;
|
|
922
|
+
return { col, span, row, rowSpan };
|
|
923
|
+
}
|
|
887
924
|
/**
|
|
888
925
|
* Where one grid item lands: a named area wins outright; otherwise
|
|
889
926
|
* explicit lines/spans combine with the auto-flow cursor, which only
|
|
@@ -1053,12 +1090,34 @@ function resolveTrackSizes(parts, availSize) {
|
|
|
1053
1090
|
sizes.push(0);
|
|
1054
1091
|
}
|
|
1055
1092
|
}
|
|
1056
|
-
// Distribute remaining space to fr units
|
|
1093
|
+
// Distribute remaining space to fr units. minmax() minimums are
|
|
1094
|
+
// honoured with redistribution: a track clamped to its minimum leaves
|
|
1095
|
+
// the pool, and the freed space re-splits among the rest (iterate
|
|
1096
|
+
// until no new clamps).
|
|
1057
1097
|
if (frParts.length > 0) {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1098
|
+
let pool = Math.max(0, availSize - fixedTotal);
|
|
1099
|
+
let flexible = [...frParts];
|
|
1100
|
+
const clamped = new Set();
|
|
1101
|
+
while (true) {
|
|
1102
|
+
const totalFr = flexible.reduce((sum, p) => sum + p.fr, 0);
|
|
1103
|
+
let reclamped = false;
|
|
1104
|
+
for (const part of flexible) {
|
|
1105
|
+
const share = totalFr > 0 ? Math.floor(pool * part.fr / totalFr) : 0;
|
|
1106
|
+
if (share < part.min) {
|
|
1107
|
+
sizes[part.index] = part.min;
|
|
1108
|
+
pool -= part.min;
|
|
1109
|
+
clamped.add(part.index);
|
|
1110
|
+
reclamped = true;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
flexible = flexible.filter(p => !clamped.has(p.index));
|
|
1114
|
+
if (!reclamped) {
|
|
1115
|
+
const finalFr = flexible.reduce((sum, p) => sum + p.fr, 0);
|
|
1116
|
+
for (const part of flexible) {
|
|
1117
|
+
sizes[part.index] = finalFr > 0 ? Math.max(part.min, Math.floor(Math.max(0, pool) * part.fr / finalFr)) : part.min;
|
|
1118
|
+
}
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1062
1121
|
}
|
|
1063
1122
|
}
|
|
1064
1123
|
return sizes;
|
|
@@ -12,6 +12,7 @@ export type { KeyframeResolution } from '../css/animation.js';
|
|
|
12
12
|
*/
|
|
13
13
|
export declare class AnimationClock {
|
|
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;
|
|
@@ -51,7 +52,14 @@ export declare class AnimationClock {
|
|
|
51
52
|
stop(): void;
|
|
52
53
|
private discover;
|
|
53
54
|
private discoverTransitions;
|
|
54
|
-
/**
|
|
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
|
+
*/
|
|
55
61
|
private trackTransitionTargets;
|
|
62
|
+
/** Evaluate an in-flight transition's value for one property, now. */
|
|
63
|
+
private currentValue;
|
|
56
64
|
private updateTimer;
|
|
57
65
|
}
|
|
@@ -6,17 +6,6 @@ import { systemClock, clockFromNow } from './clock.js';
|
|
|
6
6
|
function easingFor(value) {
|
|
7
7
|
return parseEasing(value) ?? (t => t);
|
|
8
8
|
}
|
|
9
|
-
/** Parse a transition-property value into the tracked-property filter. */
|
|
10
|
-
function transitionedProperties(value) {
|
|
11
|
-
const names = new Set();
|
|
12
|
-
for (const raw of value.split(',')) {
|
|
13
|
-
const name = raw.trim();
|
|
14
|
-
if (name === 'all')
|
|
15
|
-
return { all: true, names };
|
|
16
|
-
names.add(name === 'background' ? 'background-color' : name);
|
|
17
|
-
}
|
|
18
|
-
return { all: false, names };
|
|
19
|
-
}
|
|
20
9
|
function cellValue(value) {
|
|
21
10
|
return typeof value === 'number' && value >= 0 ? `${value}cell` : null;
|
|
22
11
|
}
|
|
@@ -50,6 +39,7 @@ const FRAME_INTERVAL_MS = 33;
|
|
|
50
39
|
*/
|
|
51
40
|
export class AnimationClock {
|
|
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();
|
|
@@ -71,8 +61,13 @@ export class AnimationClock {
|
|
|
71
61
|
}
|
|
72
62
|
/** Whether this node's animation needs re-layout each frame (vs repaint only). */
|
|
73
63
|
touchesLayout(node) {
|
|
74
|
-
|
|
75
|
-
|
|
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;
|
|
76
71
|
}
|
|
77
72
|
/**
|
|
78
73
|
* Track transitioned elements and start a one-shot transition when a
|
|
@@ -87,7 +82,10 @@ export class AnimationClock {
|
|
|
87
82
|
for (const id of this.transitionTargets.keys()) {
|
|
88
83
|
if (!seen.has(id)) {
|
|
89
84
|
this.transitionTargets.delete(id);
|
|
90
|
-
this.transitions.
|
|
85
|
+
for (const key of this.transitions.keys()) {
|
|
86
|
+
if (key.startsWith(`${id}:`))
|
|
87
|
+
this.transitions.delete(key);
|
|
88
|
+
}
|
|
91
89
|
}
|
|
92
90
|
}
|
|
93
91
|
this.updateTimer();
|
|
@@ -116,15 +114,15 @@ export class AnimationClock {
|
|
|
116
114
|
return dirty;
|
|
117
115
|
}
|
|
118
116
|
applyEntries(entries, styles, dirty) {
|
|
119
|
-
for (const [
|
|
120
|
-
const style = styles.get(id);
|
|
117
|
+
for (const [key, anim] of entries) {
|
|
118
|
+
const style = styles.get(anim.node.id);
|
|
121
119
|
if (!style)
|
|
122
120
|
continue;
|
|
123
121
|
const elapsed = this.now() - anim.start;
|
|
124
122
|
anim.runner.apply(style, elapsed);
|
|
125
123
|
dirty.push({ node: anim.node, touchesLayout: anim.runner.touchesLayout });
|
|
126
124
|
if (anim.runner.isFinished(elapsed))
|
|
127
|
-
entries.delete(
|
|
125
|
+
entries.delete(key);
|
|
128
126
|
}
|
|
129
127
|
}
|
|
130
128
|
stop() {
|
|
@@ -140,18 +138,27 @@ export class AnimationClock {
|
|
|
140
138
|
const stops = name ? keyframes.get(name) : undefined;
|
|
141
139
|
if (style && name && stops && style.animationDuration > 0) {
|
|
142
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);
|
|
143
145
|
if (!existing || existing.name !== name || existing.duration !== style.animationDuration) {
|
|
144
|
-
const resolved = resolution
|
|
145
|
-
? resolveKeyframeStops(stops, resolution, node.id)
|
|
146
|
-
: stops;
|
|
147
146
|
this.active.set(node.id, {
|
|
148
147
|
node,
|
|
149
148
|
runner: new AnimationRunner(resolved, style.animationDuration, style.animationIterationCount, easingFor(style.animationTimingFunction)),
|
|
150
149
|
name,
|
|
151
150
|
duration: style.animationDuration,
|
|
152
151
|
start: this.now(),
|
|
152
|
+
resolvedKey,
|
|
153
153
|
});
|
|
154
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
|
+
}
|
|
155
162
|
seen.add(node.id);
|
|
156
163
|
}
|
|
157
164
|
}
|
|
@@ -162,7 +169,7 @@ export class AnimationClock {
|
|
|
162
169
|
const subtreeResolved = parentResolved || (resolvedIds?.has(node.id) ?? false);
|
|
163
170
|
if (node.nodeType === 'element') {
|
|
164
171
|
const style = styles.get(node.id);
|
|
165
|
-
if (style
|
|
172
|
+
if (style && style.transitions.some(t => t.duration > 0)) {
|
|
166
173
|
seen.add(node.id);
|
|
167
174
|
if (subtreeResolved)
|
|
168
175
|
this.trackTransitionTargets(node, style);
|
|
@@ -172,12 +179,20 @@ export class AnimationClock {
|
|
|
172
179
|
this.discoverTransitions(child, styles, subtreeResolved, resolvedIds, seen);
|
|
173
180
|
}
|
|
174
181
|
}
|
|
175
|
-
/**
|
|
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
|
+
*/
|
|
176
188
|
trackTransitionTargets(node, style) {
|
|
177
|
-
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');
|
|
178
192
|
const targets = {};
|
|
179
193
|
for (const prop of TRANSITIONABLE) {
|
|
180
|
-
|
|
194
|
+
const config = configFor(prop.css);
|
|
195
|
+
if (!config || config.duration <= 0)
|
|
181
196
|
continue;
|
|
182
197
|
const value = prop.read(style);
|
|
183
198
|
if (value !== null)
|
|
@@ -187,28 +202,38 @@ export class AnimationClock {
|
|
|
187
202
|
this.transitionTargets.set(node.id, targets);
|
|
188
203
|
if (!previous)
|
|
189
204
|
return; // first sight — the initial style never transitions
|
|
190
|
-
const fromDecls = [];
|
|
191
|
-
const toDecls = [];
|
|
192
205
|
for (const [property, target] of Object.entries(targets)) {
|
|
193
206
|
const before = previous[property];
|
|
194
|
-
if (before
|
|
195
|
-
|
|
196
|
-
|
|
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;
|
|
197
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
|
+
});
|
|
198
230
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this.transitions.set(node.id, {
|
|
206
|
-
node,
|
|
207
|
-
runner: new AnimationRunner(stops, style.transitionDuration, 1, easingFor(style.transitionTimingFunction)),
|
|
208
|
-
name: '',
|
|
209
|
-
duration: style.transitionDuration,
|
|
210
|
-
start: this.now(),
|
|
211
|
-
});
|
|
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;
|
|
212
237
|
}
|
|
213
238
|
updateTimer() {
|
|
214
239
|
if (this.activeCount > 0 && this.timer === null) {
|
package/dist/src/render/paint.js
CHANGED
|
@@ -350,7 +350,11 @@ function paintCheckable(node, buffer, box, visuals, clip) {
|
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
function paintInput(node, buffer, box, visuals, clip) {
|
|
353
|
-
const
|
|
353
|
+
const rawValue = node.attributes.get('value') ?? '';
|
|
354
|
+
// Password values paint as bullets; layout and scrolling use the
|
|
355
|
+
// masked string, which has the same length as the real value.
|
|
356
|
+
const value = node.attributes.get('type') === 'password'
|
|
357
|
+
? '•'.repeat(rawValue.length) : rawValue;
|
|
354
358
|
const isFocused = node.attributes.has('data-focused');
|
|
355
359
|
const cursor = node.textBuffer?.cursor ?? value.length;
|
|
356
360
|
const style = node.cache.resolvedStyle;
|
package/docs/elements.md
CHANGED
|
@@ -37,8 +37,9 @@ unprevented, the `href` opens in the local browser.
|
|
|
37
37
|
|
|
38
38
|
| Control | Rendering | Interaction |
|
|
39
39
|
|---|---|---|
|
|
40
|
-
| `input` (text) | one-row editor on the grid | readline-style editing with a real cursor; `input` events carry `{ value, cursor }` |
|
|
41
|
-
| `
|
|
40
|
+
| `input` (text) | one-row editor on the grid | readline-style editing with a real cursor; `input` events carry `{ value, cursor }`; `maxlength` caps typing and paste; `readonly` blocks edits but keeps caret movement |
|
|
41
|
+
| `input type="password"` | as text, value painted as `•` bullets | editing as text inputs; the real value stays in `value` |
|
|
42
|
+
| `textarea` | multi-line editor | as text inputs, including `maxlength`/`readonly` |
|
|
42
43
|
| `input type="checkbox"` | `[x]` / `[ ]` (3×1) | `Space` or click toggles; `change`/`input` carry `{ checked, value }` |
|
|
43
44
|
| `input type="radio"` | `(•)` / `( )` | selecting unchecks same-`name` radios across the tree; never untoggles itself |
|
|
44
45
|
| `select` + `option`/`optgroup` | selected label + `▾`, sized to the longest option | popup-less cycling: `ArrowUp`/`ArrowDown` move with wraparound, `Space`/`Enter`/click advance; `change` carries `{ value }` |
|
package/docs/layout.md
CHANGED
|
@@ -74,9 +74,13 @@ support:
|
|
|
74
74
|
.nav { grid-area: nav; }
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
`grid-auto-flow: column` fills down each column, wrapping to a new
|
|
78
|
+
(implicit) column after the explicit row count; implicit columns take
|
|
79
|
+
the last explicit column's width. `minmax()` minimums on `fr` tracks
|
|
80
|
+
redistribute — a track clamped to its minimum leaves the pool and the
|
|
81
|
+
freed space re-splits among the remaining `fr` tracks.
|
|
82
|
+
|
|
83
|
+
Deviations: spanning content doesn't stretch individual tracks.
|
|
80
84
|
|
|
81
85
|
## Tables
|
|
82
86
|
|
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
|
@@ -130,7 +130,8 @@ block/inline box per its display default.
|
|
|
130
130
|
| `strong`/`b`, `em`/`i`, `u`, `s`/`del`, `mark`, `kbd`, `abbr`, `samp`, `var` | text attributes (bold/italic/underline/strikethrough/colour) | — |
|
|
131
131
|
| `a` | underlined, focusable; Enter/click opens `href` in the local browser | [`<a>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a) |
|
|
132
132
|
| `table` and friends | full table layout: colspan/rowspan, header/footer groups, caption, `colgroup`/`col` width hints, collapse/separate borders, `empty-cells` | [`<table>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table) |
|
|
133
|
-
| `input` (text) | single-line editor with cursor, `value`, `input` events | [`<input>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) |
|
|
133
|
+
| `input` (text) | single-line editor with cursor, `value`, `input` events; `maxlength`, `readonly` | [`<input>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) |
|
|
134
|
+
| `input type="password"` | value masked as `•` bullets; editing as text | [password](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/password) |
|
|
134
135
|
| `input type="checkbox" / "radio"` | glyph toggles; `checked` attribute/property; `change`+`input` events | [checkbox](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox), [radio](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio) |
|
|
135
136
|
| `textarea` | multi-line editing | [`<textarea>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) |
|
|
136
137
|
| `button` | focusable, centred text, `click` on Enter/click | [`<button>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button) |
|
|
@@ -153,10 +154,14 @@ All standard matching semantics. Reference: [MDN selectors](https://developer.mo
|
|
|
153
154
|
`:nth-of-type()`, `:nth-last-of-type()` (full An+B), `:not()`, `:is()`,
|
|
154
155
|
`:where()`, `:checked`, `:disabled`, `:enabled`
|
|
155
156
|
- Pseudo-elements: `::before`, `::after` (single-colon legacy accepted)
|
|
156
|
-
with `content:` strings, `attr(x)`,
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
with `content:` strings, `attr(x)`, `counter(name)` (with
|
|
158
|
+
`counter-reset` / `counter-increment`, including explicit amounts),
|
|
159
|
+
space-separated concatenation, and `none`/`""`. Counters use a flat
|
|
160
|
+
namespace — no per-scope nesting or `counters()` joining — and update
|
|
161
|
+
on full style resolution, so an incremental restyle can serve stale
|
|
162
|
+
numbers until the next full pass. Pseudo boxes are inline and invisible
|
|
163
|
+
to `:empty`/`:nth-*`. In table-internal boxes they render per §17.2.1:
|
|
164
|
+
a pseudo on a row or table box becomes an anonymous cell/row.
|
|
160
165
|
- Specificity, source order, and inline-`style` precedence follow the
|
|
161
166
|
[cascade](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Cascade).
|
|
162
167
|
|
|
@@ -171,12 +176,12 @@ adaptations. Lengths are cells (`cell`/`ch`, `%`, or `calc()`).
|
|
|
171
176
|
| [Box model](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_box_model) | `width`, `height`, `min/max-width`, `min/max-height`, `padding(-*)`, `margin(-*)` (incl. `auto` centring and margin collapse), `box-sizing`, `overflow` (`hidden`/`scroll`/`auto` with real scrolling + fading scrollbars) |
|
|
172
177
|
| [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 |
|
|
173
178
|
| [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` |
|
|
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
|
|
179
|
+
| [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`, `grid-auto-flow: row \| column` (column flow wraps at the explicit row count; implicit columns take the last explicit column's width). Fractional `minmax()` minimums redistribute: a track clamped to its minimum leaves the pool and the freed space re-splits among the rest |
|
|
175
180
|
| [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) |
|
|
176
181
|
| [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`) |
|
|
177
|
-
| Borders | `border`/`border-style`/`border-color`/`border-corner` + per-side toggles (terminal values above) |
|
|
182
|
+
| Borders | `border`/`border-style`/`border-color`/`border-corner` + per-side toggles (terminal values above). `border-collapse: collapse` on a container (inherited — `:root` works) makes adjacent bordered siblings in block flow, flex, and grid share a single border line with junction glyphs (`├` `┬` `┼`) — a cell-grid extension of the table property |
|
|
178
183
|
| [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()`) |
|
|
179
|
-
| [Transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions) | `transition` shorthand
|
|
184
|
+
| [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 |
|
|
180
185
|
| [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` |
|
|
181
186
|
|
|
182
187
|
### Animation & transition semantics on the grid
|
|
@@ -185,12 +190,14 @@ Colours interpolate in RGB at ~30fps; single cell/ch lengths interpolate
|
|
|
185
190
|
to whole cells (movement steps cell by cell); every other supported
|
|
186
191
|
property applies discretely, switching at the segment midpoint (the CSS
|
|
187
192
|
rule for non-interpolable values). Layout-affecting animations re-flow
|
|
188
|
-
each frame. Easing applies per keyframe segment
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
`
|
|
193
|
+
each frame. Easing applies per keyframe segment (a timing function
|
|
194
|
+
declared inside a keyframe overrides the element's for that segment);
|
|
195
|
+
non-interpolable values switch when eased progress crosses the midpoint.
|
|
196
|
+
Transitions run per property with their own duration/timing;
|
|
197
|
+
interruptions continue from the current blended value. Keyframe
|
|
198
|
+
`var()`/`light-dark()` re-resolves on scheme/custom-property changes
|
|
199
|
+
without restarting the animation. Deviation: no `transition-delay` /
|
|
200
|
+
`animation-delay`.
|
|
194
201
|
|
|
195
202
|
## Values, functions and at-rules
|
|
196
203
|
|
package/docs/selectors.md
CHANGED
|
@@ -64,13 +64,28 @@ a[href$=".pdf"]::after { content: " [pdf]"; color: red; }
|
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
`content:` supports quoted strings, `attr(x)` against the host element,
|
|
67
|
-
space-separated concatenation, and `none`/`""` (no
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
`counter(name)`, space-separated concatenation, and `none`/`""` (no
|
|
68
|
+
box). Pseudo boxes are invisible to `:empty` and `:nth-*`, and style
|
|
69
|
+
like inline elements (they inherit the host's visual attributes unless
|
|
70
|
+
the pseudo rule overrides them). In table-internal boxes they follow
|
|
71
|
+
CSS anonymous-box rules: a `::before` on a row renders as a leading
|
|
72
|
+
anonymous cell.
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
### Counters
|
|
75
|
+
|
|
76
|
+
`counter-reset` and `counter-increment` work with optional amounts:
|
|
77
|
+
|
|
78
|
+
```css
|
|
79
|
+
.doc { counter-reset: sec; }
|
|
80
|
+
.section { counter-increment: sec; }
|
|
81
|
+
.section::before { content: counter(sec) ". "; }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Counters resolve in document order with a flat namespace — nested
|
|
85
|
+
elements share the same counter rather than creating a scoped one, and
|
|
86
|
+
`counters()` (the nested-join form) is not supported. Values update on
|
|
87
|
+
full style resolution, so an incremental restyle can briefly show stale
|
|
88
|
+
numbers.
|
|
74
89
|
|
|
75
90
|
## What re-resolves when
|
|
76
91
|
|
package/docs/terminal-css.md
CHANGED
|
@@ -61,10 +61,17 @@ Browser values (`solid`, `dashed`…) are ignored.
|
|
|
61
61
|
- `border-corner: h | v | none` biases which line wins at corners.
|
|
62
62
|
- `border-color` is standard, including `currentColor`.
|
|
63
63
|
- Tables support `border-collapse: collapse` with shared grid lines.
|
|
64
|
+
- `border-collapse: collapse` extends beyond tables: on any container
|
|
65
|
+
(it inherits, so `:root` opts the whole app in), adjacent bordered
|
|
66
|
+
siblings — stacked blocks, flex items, grid items — share a single
|
|
67
|
+
border line, merging into junction glyphs (`├` `┬` `┼`) in the border's
|
|
68
|
+
family. Without it, sibling frames stay separate as in browsers.
|
|
69
|
+
`border-collapse: separate` on a child opts it back out.
|
|
64
70
|
|
|
65
71
|
```css
|
|
66
72
|
.panel { border: rounded; border-color: cyan; }
|
|
67
73
|
.rule { border-top: true; border-style: single; } /* horizontal rule */
|
|
74
|
+
.list { border-collapse: collapse; } /* children share dividers */
|
|
68
75
|
```
|
|
69
76
|
|
|
70
77
|
## Colour
|