@svelterm/core 0.24.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,26 @@
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
+
3
24
  ## 0.24.0 — 2026-07-05
4
25
 
5
26
  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
@@ -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, 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,
@@ -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,10 +217,12 @@ 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
- 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);
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') {
@@ -557,6 +562,15 @@ export function applyDeclaration(style, property, value, scheme = 'dark') {
557
562
  case 'grid-area':
558
563
  parseGridArea(style, value);
559
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;
560
574
  case 'animation':
561
575
  parseAnimationShorthand(style, value);
562
576
  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
+ }
@@ -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;
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/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,7 +175,7 @@ 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) |
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.24.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": {