@svelterm/core 0.24.0 → 0.26.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 +46 -0
- package/dist/src/css/compute.d.ts +5 -1
- package/dist/src/css/compute.js +26 -4
- 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/layout/engine.js +73 -14
- package/docs/layout.md +7 -3
- package/docs/reference.md +10 -6
- 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,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.26.0 — 2026-07-05
|
|
4
|
+
|
|
5
|
+
Sibling border collapse becomes explicit.
|
|
6
|
+
|
|
7
|
+
### Changed (breaking)
|
|
8
|
+
|
|
9
|
+
- **Sibling border collapse is now opt-in.** Adjacent bordered siblings
|
|
10
|
+
(stacked blocks, flex items, grid items) previously always shared a
|
|
11
|
+
border line with junction glyphs (`├` `┬` `┼`). Now that happens only
|
|
12
|
+
under `border-collapse: collapse` — an extension of the CSS table
|
|
13
|
+
property to all boxes. It inherits per spec, so opt in once on the
|
|
14
|
+
container or app-wide:
|
|
15
|
+
|
|
16
|
+
```css
|
|
17
|
+
:root { border-collapse: collapse; }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Without it, sibling frames render separately, matching browsers.
|
|
21
|
+
`border-collapse: separate` on a child opts it back out.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `border-collapse` now inherits (CSS 2.2 §17.6) — previously it was
|
|
26
|
+
only read from the table element itself.
|
|
27
|
+
|
|
28
|
+
## 0.25.0 — 2026-07-05
|
|
29
|
+
|
|
30
|
+
Grid and generated-content completeness.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`grid-auto-flow: column`** — auto-placed items fill down each
|
|
35
|
+
column, wrapping to a new (implicit) column after the explicit row
|
|
36
|
+
count; implicit columns take the last explicit column's width.
|
|
37
|
+
- **`minmax()` redistribution** — a `fr` track clamped to its
|
|
38
|
+
`minmax()` minimum leaves the distribution pool and the freed space
|
|
39
|
+
re-splits among the remaining `fr` tracks (previously other tracks
|
|
40
|
+
kept their naive share, overflowing the container).
|
|
41
|
+
- **`counter()` in `content:`** — `counter-reset` and
|
|
42
|
+
`counter-increment` (with optional amounts) resolve in document
|
|
43
|
+
order; flat namespace, no `counters()` nesting.
|
|
44
|
+
- **Pseudo-elements in table-internal boxes** — `::before`/`::after`
|
|
45
|
+
now render on rows, row groups, and table boxes per CSS
|
|
46
|
+
anonymous-box rules (a row pseudo becomes a leading anonymous cell);
|
|
47
|
+
previously they were silently dropped.
|
|
48
|
+
|
|
3
49
|
## 0.24.0 — 2026-07-05
|
|
4
50
|
|
|
5
51
|
Motion timing completeness: transitions and keyframes behave per spec.
|
|
@@ -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;
|
|
@@ -95,7 +99,7 @@ export interface ResolvedStyle {
|
|
|
95
99
|
export declare function defaultStyle(tag?: string): ResolvedStyle;
|
|
96
100
|
export declare function resolveStyles(root: TermNode, stylesheet: CSSStyleSheet, media?: MediaContext, availWidth?: number, availHeight?: number): Map<number, ResolvedStyle>;
|
|
97
101
|
export declare function filterByMedia(stylesheet: CSSStyleSheet, context: MediaContext): CSSStyleSheet;
|
|
98
|
-
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;
|
|
99
103
|
export declare function applyDeclaration(style: ResolvedStyle, property: string, value: string, scheme?: 'dark' | 'light'): void;
|
|
100
104
|
/**
|
|
101
105
|
* Pair the transition lists per spec: durations/timings repeat
|
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,7 +52,9 @@ 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,
|
|
@@ -206,7 +209,7 @@ function evaluateSupports(condition) {
|
|
|
206
209
|
const property = condition.substring(0, colonIdx).trim();
|
|
207
210
|
return SUPPORTED_PROPERTIES.has(property);
|
|
208
211
|
}
|
|
209
|
-
export function resolveNode(node, stylesheet, styles, variables, scheme = 'dark') {
|
|
212
|
+
export function resolveNode(node, stylesheet, styles, variables, scheme = 'dark', counters = new CounterContext()) {
|
|
210
213
|
if (node.nodeType === 'element') {
|
|
211
214
|
const vars = variables.get(node.id) ?? new Map();
|
|
212
215
|
const parentStyle = node.parent ? styles.get(node.parent.id) : undefined;
|
|
@@ -214,14 +217,20 @@ export function resolveNode(node, stylesheet, styles, variables, scheme = 'dark'
|
|
|
214
217
|
styles.set(node.id, resolved);
|
|
215
218
|
node.cache.resolvedStyle = resolved;
|
|
216
219
|
node.cache.classAttr = node.attributes.get('class') ?? '';
|
|
217
|
-
|
|
220
|
+
// Counter effects apply in document order, before pseudo content
|
|
221
|
+
counters.enter(resolved);
|
|
222
|
+
resolvePseudoElements(node, stylesheet, styles, vars, scheme, counters);
|
|
218
223
|
}
|
|
219
224
|
for (const child of node.children) {
|
|
220
|
-
resolveNode(child, stylesheet, styles, variables, scheme);
|
|
225
|
+
resolveNode(child, stylesheet, styles, variables, scheme, counters);
|
|
221
226
|
}
|
|
222
227
|
}
|
|
223
228
|
function computeStyle(node, stylesheet, vars, parentStyle, scheme = 'dark') {
|
|
224
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;
|
|
225
234
|
// Collect all matching declarations with specificity
|
|
226
235
|
const scored = [];
|
|
227
236
|
let order = 0;
|
|
@@ -292,6 +301,7 @@ function parseInlineStyle(text) {
|
|
|
292
301
|
return result;
|
|
293
302
|
}
|
|
294
303
|
const INHERITABLE_PROPERTIES = new Set([
|
|
304
|
+
'border-collapse',
|
|
295
305
|
'color', 'font-weight', 'font-style', 'text-decoration',
|
|
296
306
|
'white-space', 'word-break', 'text-align', 'visibility', 'opacity',
|
|
297
307
|
]);
|
|
@@ -317,6 +327,9 @@ function applyInherit(style, property, parentStyle) {
|
|
|
317
327
|
case 'white-space':
|
|
318
328
|
style.whiteSpace = parentStyle.whiteSpace;
|
|
319
329
|
break;
|
|
330
|
+
case 'border-collapse':
|
|
331
|
+
style.borderCollapse = parentStyle.borderCollapse;
|
|
332
|
+
break;
|
|
320
333
|
case 'word-break':
|
|
321
334
|
style.wordBreak = parentStyle.wordBreak;
|
|
322
335
|
break;
|
|
@@ -557,6 +570,15 @@ export function applyDeclaration(style, property, value, scheme = 'dark') {
|
|
|
557
570
|
case 'grid-area':
|
|
558
571
|
parseGridArea(style, value);
|
|
559
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;
|
|
560
582
|
case 'animation':
|
|
561
583
|
parseAnimationShorthand(style, value);
|
|
562
584
|
break;
|
|
@@ -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] ?? '';
|
|
@@ -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;
|
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/reference.md
CHANGED
|
@@ -153,10 +153,14 @@ All standard matching semantics. Reference: [MDN selectors](https://developer.mo
|
|
|
153
153
|
`:nth-of-type()`, `:nth-last-of-type()` (full An+B), `:not()`, `:is()`,
|
|
154
154
|
`:where()`, `:checked`, `:disabled`, `:enabled`
|
|
155
155
|
- Pseudo-elements: `::before`, `::after` (single-colon legacy accepted)
|
|
156
|
-
with `content:` strings, `attr(x)`,
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
with `content:` strings, `attr(x)`, `counter(name)` (with
|
|
157
|
+
`counter-reset` / `counter-increment`, including explicit amounts),
|
|
158
|
+
space-separated concatenation, and `none`/`""`. Counters use a flat
|
|
159
|
+
namespace — no per-scope nesting or `counters()` joining — and update
|
|
160
|
+
on full style resolution, so an incremental restyle can serve stale
|
|
161
|
+
numbers until the next full pass. Pseudo boxes are inline and invisible
|
|
162
|
+
to `:empty`/`:nth-*`. In table-internal boxes they render per §17.2.1:
|
|
163
|
+
a pseudo on a row or table box becomes an anonymous cell/row.
|
|
160
164
|
- Specificity, source order, and inline-`style` precedence follow the
|
|
161
165
|
[cascade](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Cascade).
|
|
162
166
|
|
|
@@ -171,10 +175,10 @@ adaptations. Lengths are cells (`cell`/`ch`, `%`, or `calc()`).
|
|
|
171
175
|
| [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
176
|
| [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
177
|
| [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
|
|
178
|
+
| [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
179
|
| [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
180
|
| [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) |
|
|
181
|
+
| 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
182
|
| [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
183
|
| [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
184
|
| [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` |
|
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
|