@svelterm/core 0.23.0 → 0.25.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 CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.25.0 — 2026-07-05
4
+
5
+ Grid and generated-content completeness.
6
+
7
+ ### Added
8
+
9
+ - **`grid-auto-flow: column`** — auto-placed items fill down each
10
+ column, wrapping to a new (implicit) column after the explicit row
11
+ count; implicit columns take the last explicit column's width.
12
+ - **`minmax()` redistribution** — a `fr` track clamped to its
13
+ `minmax()` minimum leaves the distribution pool and the freed space
14
+ re-splits among the remaining `fr` tracks (previously other tracks
15
+ kept their naive share, overflowing the container).
16
+ - **`counter()` in `content:`** — `counter-reset` and
17
+ `counter-increment` (with optional amounts) resolve in document
18
+ order; flat namespace, no `counters()` nesting.
19
+ - **Pseudo-elements in table-internal boxes** — `::before`/`::after`
20
+ now render on rows, row groups, and table boxes per CSS
21
+ anonymous-box rules (a row pseudo becomes a leading anonymous cell);
22
+ previously they were silently dropped.
23
+
24
+ ## 0.24.0 — 2026-07-05
25
+
26
+ Motion timing completeness: transitions and keyframes behave per spec.
27
+
28
+ ### Added
29
+
30
+ - **Per-property transitions** — `transition: color 150ms linear, width
31
+ 400ms ease` runs each property on its own duration and timing;
32
+ longhand lists pair cyclically per spec. (Previously the first
33
+ duration and one timing function applied to everything.)
34
+ - **Interrupted transitions continue** — a retargeted transition starts
35
+ from its current blended value instead of jumping back to the previous
36
+ target (reversals no longer flash).
37
+ - **Per-keyframe `animation-timing-function`** — a timing function
38
+ declared inside a keyframe applies from that stop to the next.
39
+ - **Keyframe `var()`/`light-dark()` re-resolution** — scheme flips and
40
+ custom-property changes retarget a running animation in place, without
41
+ restarting it.
42
+
3
43
  ## 0.23.0 — 2026-07-05
4
44
 
5
45
  Positioning: `relative` offsets apply, and `sticky` arrives.
@@ -1,6 +1,6 @@
1
1
  import type { KeyframeStop } from './parser.js';
2
2
  import { type ResolvedStyle } from './compute.js';
3
- import type { Easing } from './easing.js';
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
- const localT = this.easing(segment.localT);
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
+ }>;
@@ -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, gridArea: 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,10 +217,12 @@ 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
- resolvePseudoElements(node, stylesheet, styles, vars, scheme);
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') {
@@ -272,6 +279,8 @@ function computeStyle(node, stylesheet, vars, parentStyle, scheme = 'dark') {
272
279
  applyDeclaration(style, decl.property, resolveVar(decl.value, vars), scheme);
273
280
  }
274
281
  }
282
+ // Pair transition longhand lists now the cascade is complete
283
+ style.transitions = pairTransitions(style);
275
284
  return style;
276
285
  }
277
286
  function parseInlineStyle(text) {
@@ -553,6 +562,15 @@ export function applyDeclaration(style, property, value, scheme = 'dark') {
553
562
  case 'grid-area':
554
563
  parseGridArea(style, value);
555
564
  break;
565
+ case 'grid-auto-flow':
566
+ style.gridAutoFlow = value.startsWith('column') ? 'column' : 'row';
567
+ break;
568
+ case 'counter-reset':
569
+ style.counterReset = value === 'none' ? null : value;
570
+ break;
571
+ case 'counter-increment':
572
+ style.counterIncrement = value === 'none' ? null : value;
573
+ break;
556
574
  case 'animation':
557
575
  parseAnimationShorthand(style, value);
558
576
  break;
@@ -563,10 +581,12 @@ export function applyDeclaration(style, property, value, scheme = 'dark') {
563
581
  style.transitionProperty = value === 'none' ? null : value;
564
582
  break;
565
583
  case 'transition-duration':
566
- style.transitionDuration = parseDuration(value);
584
+ style.transitionDurations = value.split(',').map(v => parseDuration(v.trim()));
585
+ style.transitionDuration = style.transitionDurations[0] ?? 0;
567
586
  break;
568
587
  case 'transition-timing-function':
569
- style.transitionTimingFunction = value;
588
+ style.transitionTimings = splitTimingList(value);
589
+ style.transitionTimingFunction = style.transitionTimings[0] ?? 'ease';
570
590
  break;
571
591
  case 'animation-name':
572
592
  style.animationName = value === 'none' ? null : value;
@@ -806,35 +826,85 @@ function parseAnimationShorthand(style, value) {
806
826
  }
807
827
  }
808
828
  }
809
- /** Parse `transition: <property> <duration> [...]`, comma-separated groups. */
829
+ /** Split a timing-function list on commas, keeping function args intact. */
830
+ function splitTimingList(value) {
831
+ const out = [];
832
+ let depth = 0;
833
+ let current = '';
834
+ for (const ch of value) {
835
+ if (ch === '(')
836
+ depth++;
837
+ if (ch === ')')
838
+ depth--;
839
+ if (ch === ',' && depth === 0) {
840
+ out.push(current.trim());
841
+ current = '';
842
+ }
843
+ else
844
+ current += ch;
845
+ }
846
+ if (current.trim())
847
+ out.push(current.trim());
848
+ return out;
849
+ }
850
+ /**
851
+ * Parse `transition: <property> <duration> <timing> [...]` — each
852
+ * comma-separated group configures one property (or `all`).
853
+ */
810
854
  function parseTransitionShorthand(style, value) {
811
855
  const trimmed = value.trim();
812
856
  if (trimmed === 'none') {
813
857
  style.transitionProperty = null;
814
858
  style.transitionDuration = 0;
859
+ style.transitionDurations = [];
860
+ style.transitionTimings = [];
815
861
  return;
816
862
  }
817
- const { easing, rest } = extractEasingFunction(trimmed);
818
- if (easing)
819
- style.transitionTimingFunction = easing;
820
863
  const properties = [];
821
- let duration = 0;
822
- for (const group of rest.split(',')) {
823
- for (const token of group.trim().split(/\s+/)) {
864
+ const durations = [];
865
+ const timings = [];
866
+ for (const groupRaw of splitTimingList(trimmed)) {
867
+ const { easing, rest } = extractEasingFunction(groupRaw);
868
+ let property = 'all';
869
+ let duration = 0;
870
+ let timing = easing ?? 'ease';
871
+ for (const token of rest.trim().split(/\s+/)) {
824
872
  if (/^\d*\.?\d+(ms|s)$/.test(token)) {
825
873
  if (duration === 0)
826
874
  duration = parseDuration(token);
827
875
  }
828
876
  else if (TIMING_KEYWORDS.has(token)) {
829
- style.transitionTimingFunction = token;
877
+ timing = token;
830
878
  }
831
879
  else if (token) {
832
- properties.push(token);
880
+ property = token;
833
881
  }
834
882
  }
883
+ properties.push(property);
884
+ durations.push(duration);
885
+ timings.push(timing);
835
886
  }
836
887
  style.transitionProperty = properties.length > 0 ? properties.join(',') : 'all';
837
- style.transitionDuration = duration;
888
+ style.transitionDurations = durations;
889
+ style.transitionTimings = timings;
890
+ style.transitionDuration = durations[0] ?? 0;
891
+ style.transitionTimingFunction = timings[0] ?? 'ease';
892
+ }
893
+ /**
894
+ * Pair the transition lists per spec: durations/timings repeat
895
+ * cyclically to cover every listed property.
896
+ */
897
+ export function pairTransitions(style) {
898
+ if (!style.transitionProperty)
899
+ return [];
900
+ const properties = style.transitionProperty.split(',').map(p => p.trim()).filter(Boolean);
901
+ const durations = style.transitionDurations.length > 0 ? style.transitionDurations : [style.transitionDuration];
902
+ const timings = style.transitionTimings.length > 0 ? style.transitionTimings : [style.transitionTimingFunction];
903
+ return properties.map((property, i) => ({
904
+ property,
905
+ duration: durations[i % durations.length] ?? 0,
906
+ timing: timings[i % timings.length] ?? 'ease',
907
+ }));
838
908
  }
839
909
  function parseDuration(value) {
840
910
  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
+ }
@@ -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: a space-separated sequence of quoted strings and attr() lookups. */
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[3] !== undefined)
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] ?? '';
@@ -526,7 +526,7 @@ function isRowLevelDisplay(display) {
526
526
  || display === 'table-column-group';
527
527
  }
528
528
  function cellsOfRow(trNode, styles) {
529
- return trNode.children.filter(c => isCellContent(c, styles));
529
+ return childrenWithPseudos(trNode).filter(c => isCellContent(c, styles));
530
530
  }
531
531
  /**
532
532
  * Group the children of a table or row-group into rows. Explicit table-rows
@@ -574,15 +574,15 @@ function collectTableRows(children, styles) {
574
574
  const display = child.nodeType === 'element' ? styles.get(child.id)?.display : undefined;
575
575
  if (display === 'table-header-group') {
576
576
  flushStray();
577
- headerRows.push(...groupIntoRows(child.children, styles));
577
+ headerRows.push(...groupIntoRows(childrenWithPseudos(child), styles));
578
578
  }
579
579
  else if (display === 'table-footer-group') {
580
580
  flushStray();
581
- footerRows.push(...groupIntoRows(child.children, styles));
581
+ footerRows.push(...groupIntoRows(childrenWithPseudos(child), styles));
582
582
  }
583
583
  else if (display === 'table-row-group') {
584
584
  flushStray();
585
- bodyRows.push(...groupIntoRows(child.children, styles));
585
+ bodyRows.push(...groupIntoRows(childrenWithPseudos(child), styles));
586
586
  }
587
587
  else {
588
588
  // table-rows and stray cell content accumulate; captions/columns
@@ -774,7 +774,7 @@ function placeRows(rows, grid, styles, boxes, x, startY, colWidths, tableWidth,
774
774
  return rowY - startY - (rows.length > 0 ? gaps.row : 0);
775
775
  }
776
776
  function layoutTable(node, styles, boxes, x, y, availW, availH) {
777
- return layoutTableChildren(node.children, styles.get(node.id), styles, boxes, x, y, availW, availH);
777
+ return layoutTableChildren(childrenWithPseudos(node), styles.get(node.id), styles, boxes, x, y, availW, availH);
778
778
  }
779
779
  /**
780
780
  * Table layout over a list of children. Called with a table element's
@@ -829,6 +829,10 @@ function layoutGrid(node, styles, boxes, x, y, availW, availH, style) {
829
829
  }
830
830
  const rowHeights = parseGridTemplate(style.gridTemplateRows ?? '', availH);
831
831
  const numCols = colWidths.length || 1;
832
+ const columnFlow = style.gridAutoFlow === 'column';
833
+ // Column flow wraps at the explicit row count; implicit columns take
834
+ // the last explicit column's width.
835
+ const numRows = rowHeights.length || 1;
832
836
  const gap = style.gap ?? 0;
833
837
  // Pre-compute border-adjusted gaps for grid children
834
838
  let hGap = gap;
@@ -850,7 +854,12 @@ function layoutGrid(node, styles, boxes, x, y, availW, availH, style) {
850
854
  for (const child of children) {
851
855
  const childStyle = styles.get(child.id);
852
856
  const area = childStyle?.gridArea ? areas?.byName.get(childStyle.gridArea) : undefined;
853
- const placed = resolveGridPlacement(childStyle, area, cursor, numCols);
857
+ const placed = columnFlow
858
+ ? resolveGridPlacementColumn(childStyle, area, cursor, numRows)
859
+ : resolveGridPlacement(childStyle, area, cursor, numCols);
860
+ while (columnFlow && placed.col >= colWidths.length && colWidths.length > 0) {
861
+ colWidths.push(colWidths[colWidths.length - 1]);
862
+ }
854
863
  const colW = trackSpanSize(colWidths, placed.col, placed.span, hGap);
855
864
  // Measure content height with unconstrained available height
856
865
  const size = layoutNode(child, styles, boxes, 0, 0, colW, availH);
@@ -884,6 +893,30 @@ function layoutGrid(node, styles, boxes, x, y, availW, availH, style) {
884
893
  const totalHeight = totalRows === 0 ? 0 : trackOffset(trackHeights, totalRows, vGap) - vGap;
885
894
  return { width: maxWidth, height: totalHeight };
886
895
  }
896
+ /**
897
+ * grid-auto-flow: column — auto-placed items fill down each column,
898
+ * wrapping to a new (implicit) column after the explicit row count.
899
+ * Explicit line placements behave as in row flow.
900
+ */
901
+ function resolveGridPlacementColumn(childStyle, area, cursor, numRows) {
902
+ if (area) {
903
+ return {
904
+ col: area.colStart, span: area.colEnd - area.colStart,
905
+ row: area.rowStart, rowSpan: area.rowEnd - area.rowStart,
906
+ };
907
+ }
908
+ if (cursor.row >= numRows) {
909
+ cursor.row = 0;
910
+ cursor.col++;
911
+ }
912
+ const col = childStyle?.gridColumnStart != null ? childStyle.gridColumnStart - 1 : cursor.col;
913
+ const row = childStyle?.gridRowStart != null ? childStyle.gridRowStart - 1 : cursor.row;
914
+ const span = childStyle?.gridColumnSpan ?? 1;
915
+ const rowSpan = childStyle?.gridRowSpan ?? 1;
916
+ cursor.col = col;
917
+ cursor.row = row + rowSpan;
918
+ return { col, span, row, rowSpan };
919
+ }
887
920
  /**
888
921
  * Where one grid item lands: a named area wins outright; otherwise
889
922
  * explicit lines/spans combine with the auto-flow cursor, which only
@@ -1053,12 +1086,34 @@ function resolveTrackSizes(parts, availSize) {
1053
1086
  sizes.push(0);
1054
1087
  }
1055
1088
  }
1056
- // Distribute remaining space to fr units, honouring minmax() minimums
1089
+ // Distribute remaining space to fr units. minmax() minimums are
1090
+ // honoured with redistribution: a track clamped to its minimum leaves
1091
+ // the pool, and the freed space re-splits among the rest (iterate
1092
+ // until no new clamps).
1057
1093
  if (frParts.length > 0) {
1058
- const totalFr = frParts.reduce((sum, p) => sum + p.fr, 0);
1059
- const remaining = Math.max(0, availSize - fixedTotal);
1060
- for (const { index, fr, min } of frParts) {
1061
- sizes[index] = Math.max(min, Math.floor(remaining * fr / totalFr));
1094
+ let pool = Math.max(0, availSize - fixedTotal);
1095
+ let flexible = [...frParts];
1096
+ const clamped = new Set();
1097
+ while (true) {
1098
+ const totalFr = flexible.reduce((sum, p) => sum + p.fr, 0);
1099
+ let reclamped = false;
1100
+ for (const part of flexible) {
1101
+ const share = totalFr > 0 ? Math.floor(pool * part.fr / totalFr) : 0;
1102
+ if (share < part.min) {
1103
+ sizes[part.index] = part.min;
1104
+ pool -= part.min;
1105
+ clamped.add(part.index);
1106
+ reclamped = true;
1107
+ }
1108
+ }
1109
+ flexible = flexible.filter(p => !clamped.has(p.index));
1110
+ if (!reclamped) {
1111
+ const finalFr = flexible.reduce((sum, p) => sum + p.fr, 0);
1112
+ for (const part of flexible) {
1113
+ sizes[part.index] = finalFr > 0 ? Math.max(part.min, Math.floor(Math.max(0, pool) * part.fr / finalFr)) : part.min;
1114
+ }
1115
+ break;
1116
+ }
1062
1117
  }
1063
1118
  }
1064
1119
  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
- /** Snapshot the node's target values; start a transition on any change. */
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
- return (this.active.get(node.id)?.runner.touchesLayout
75
- || this.transitions.get(node.id)?.runner.touchesLayout) ?? false;
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.delete(id);
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 [id, anim] of entries) {
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(id);
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?.transitionProperty && style.transitionDuration > 0) {
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
- /** Snapshot the node's target values; start a transition on any change. */
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 included = transitionedProperties(style.transitionProperty);
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
- if (!included.all && !included.names.has(prop.css))
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 !== undefined && before !== target) {
195
- fromDecls.push({ property, value: before });
196
- toDecls.push({ property, value: target });
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
- if (fromDecls.length === 0)
200
- return;
201
- const stops = [
202
- { offset: 0, declarations: fromDecls },
203
- { offset: 1, declarations: toDecls },
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/docs/layout.md CHANGED
@@ -74,9 +74,13 @@ support:
74
74
  .nav { grid-area: nav; }
75
75
  ```
76
76
 
77
- Deviations: auto-flow is row-based (`grid-auto-flow: column` is not
78
- implemented); `minmax()` minimums on `fr` tracks are enforced without
79
- redistribution; spanning content doesn't stretch individual tracks.
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
- - Keyframe `var()`/`light-dark()` resolution happens once when the
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
@@ -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)`, space-separated concatenation, and
157
- `none`/`""`. `counter()` is not supported. Pseudo boxes are inline and
158
- invisible to `:empty`/`:nth-*`. Known gap: pseudo-elements don't render
159
- inside table-internal boxes.
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,12 +175,12 @@ 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`. Auto-flow is row-based; `grid-auto-flow: column` is not implemented. Fractional `minmax()` minimums are enforced without redistribution |
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
181
  | Borders | `border`/`border-style`/`border-color`/`border-corner` + per-side toggles (terminal values above) |
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
- | [Transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions) | `transition` shorthand, `transition-property` (list or `all`), `transition-duration`, `transition-timing-function` |
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` |
181
185
 
182
186
  ### Animation & transition semantics on the grid
@@ -185,12 +189,14 @@ Colours interpolate in RGB at ~30fps; single cell/ch lengths interpolate
185
189
  to whole cells (movement steps cell by cell); every other supported
186
190
  property applies discretely, switching at the segment midpoint (the CSS
187
191
  rule for non-interpolable values). Layout-affecting animations re-flow
188
- each frame. Easing applies per keyframe segment; non-interpolable values
189
- switch when eased progress crosses the midpoint. Deviations: one duration
190
- and one timing function for all listed transition properties, interrupted
191
- transitions restart from the previous target value, keyframe
192
- `var()`/`light-dark()` resolves once at animation start, and per-keyframe
193
- `animation-timing-function` overrides are ignored.
192
+ each frame. Easing applies per keyframe segment (a timing function
193
+ declared inside a keyframe overrides the element's for that segment);
194
+ non-interpolable values switch when eased progress crosses the midpoint.
195
+ Transitions run per property with their own duration/timing;
196
+ interruptions continue from the current blended value. Keyframe
197
+ `var()`/`light-dark()` re-resolves on scheme/custom-property changes
198
+ without restarting the animation. Deviation: no `transition-delay` /
199
+ `animation-delay`.
194
200
 
195
201
  ## Values, functions and at-rules
196
202
 
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 box). `counter()` is
68
- not supported. Pseudo boxes are invisible to `:empty` and `:nth-*`, and
69
- style like inline elements (they inherit the host's visual attributes
70
- unless the pseudo rule overrides them).
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
- Known gap: pseudo-elements don't render inside table-internal layout
73
- boxes.
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelterm/core",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "Svelte 5 components rendered to the terminal with real CSS",
5
5
  "type": "module",
6
6
  "exports": {