entasis 0.9.3 → 0.10.1

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.
Files changed (82) hide show
  1. package/dist/components/ButtonGroup/ButtonGroup.svelte +10 -4
  2. package/dist/components/ButtonGroup/buttonGroup.mcp.d.ts +1 -1
  3. package/dist/components/ButtonGroup/buttonGroup.mcp.js +23 -3
  4. package/dist/components/ButtonGroup/buttonGroup.props.d.ts +19 -5
  5. package/dist/components/Chart/Chart.svelte +1 -1
  6. package/dist/components/Chart/chart.cartesian.js +3 -2
  7. package/dist/components/Chart/chart.fit.d.ts +53 -0
  8. package/dist/components/Chart/chart.fit.js +81 -0
  9. package/dist/components/Chart/chart.mcp.d.ts +1 -1
  10. package/dist/components/Chart/chart.mcp.js +2 -1
  11. package/dist/components/Chart/chart.polar.js +137 -18
  12. package/dist/components/Chart/chart.polar.props.d.ts +5 -0
  13. package/dist/components/Chart/chart.proportion.js +12 -6
  14. package/dist/components/Chart/chart.relation.network.js +32 -4
  15. package/dist/components/Chart/chart.relation.sankey.js +53 -6
  16. package/dist/components/Chart/chart.relation.tree.js +84 -24
  17. package/dist/components/Chart/chart.series.props.d.ts +6 -0
  18. package/dist/components/Chart/chart.state.svelte.d.ts +1 -0
  19. package/dist/components/Chart/chart.state.svelte.js +9 -6
  20. package/dist/components/Chart/chart.viewport.svelte.d.ts +1 -0
  21. package/dist/components/Chart/chart.viewport.svelte.js +25 -7
  22. package/dist/components/DataTable/DataTable.svelte +25 -8
  23. package/dist/components/DataTable/DataTableRow.svelte +33 -10
  24. package/dist/components/DataTable/dataTable.mcp.d.ts +1 -1
  25. package/dist/components/DataTable/dataTable.mcp.js +12 -1
  26. package/dist/components/DataTable/dataTable.model.svelte.js +4 -3
  27. package/dist/components/DataTable/dataTable.props.d.ts +17 -0
  28. package/dist/components/DataTable/dataTable.theme.d.ts +12 -0
  29. package/dist/components/DataTable/dataTable.theme.js +3 -2
  30. package/dist/components/DataTable/index.d.ts +1 -1
  31. package/dist/components/Dialog/Dialog.svelte +12 -7
  32. package/dist/components/Dialog/dialog.state.svelte.d.ts +2 -1
  33. package/dist/components/Dialog/dialog.state.svelte.js +7 -7
  34. package/dist/components/Dialog/dialog.theme.js +1 -1
  35. package/dist/components/FloatingWindow/floatingWindow.theme.js +1 -1
  36. package/dist/components/Form/Select/Select.svelte +3 -1
  37. package/dist/components/Form/Select/select.align.d.ts +3 -2
  38. package/dist/components/Form/Select/select.align.js +11 -4
  39. package/dist/components/PageShell/pageShell.state.svelte.d.ts +1 -1
  40. package/dist/components/PageShell/pageShell.state.svelte.js +6 -13
  41. package/dist/components/Popover/Popover.svelte +5 -5
  42. package/dist/components/Popover/index.d.ts +1 -1
  43. package/dist/components/Popover/popover.mcp.d.ts +1 -1
  44. package/dist/components/Popover/popover.mcp.js +33 -6
  45. package/dist/components/Popover/popover.state.svelte.d.ts +9 -1
  46. package/dist/components/Popover/popover.state.svelte.js +61 -8
  47. package/dist/components/Popover/popover.theme.js +1 -1
  48. package/dist/components/Sidebar/SidebarDesktopShell.svelte +26 -6
  49. package/dist/components/Sidebar/sidebar.theme.js +1 -1
  50. package/dist/components/Theme/index.d.ts +1 -1
  51. package/dist/components/Theme/index.js +1 -1
  52. package/dist/components/Theme/theme.mcp.d.ts +1 -1
  53. package/dist/components/Theme/theme.mcp.js +3 -0
  54. package/dist/components/Theme/theme.state.svelte.d.ts +4 -0
  55. package/dist/components/Theme/theme.state.svelte.js +4 -0
  56. package/dist/components/Tooltip/Tooltip.svelte +2 -4
  57. package/dist/components/Tooltip/tooltip.attachment.svelte.js +5 -0
  58. package/dist/components/Tooltip/tooltip.mcp.d.ts +1 -1
  59. package/dist/components/Tooltip/tooltip.mcp.js +4 -2
  60. package/dist/generated/componentAliases.d.ts +1 -0
  61. package/dist/generated/componentAliases.js +1 -0
  62. package/dist/generated/componentContract.d.ts +13 -3
  63. package/dist/generated/componentContract.js +15 -1
  64. package/dist/generated/componentMcpRegistry.d.ts +7 -6
  65. package/dist/generated/componentMcpRegistry.js +2 -0
  66. package/dist/i18n/ar.js +1 -1
  67. package/dist/i18n/de.js +1 -1
  68. package/dist/i18n/en.js +1 -1
  69. package/dist/i18n/es.js +1 -1
  70. package/dist/i18n/fr.js +1 -1
  71. package/dist/i18n/pt.js +1 -1
  72. package/dist/i18n/zh.js +1 -1
  73. package/dist/tailwind/colors.d.ts +10 -3
  74. package/dist/tailwind/colors.js +6 -0
  75. package/dist/tailwind/palette.d.ts +5 -0
  76. package/dist/tailwind/palette.js +4 -0
  77. package/dist/tailwind/palette.mcp.d.ts +1 -0
  78. package/dist/tailwind/palette.mcp.js +34 -0
  79. package/dist/utils/layers.svelte.js +9 -8
  80. package/dist/utils/registry.svelte.d.ts +21 -0
  81. package/dist/utils/registry.svelte.js +49 -0
  82. package/package.json +8 -1
@@ -11,7 +11,9 @@ export function compileProportion({ data, mark, path, category, value }) {
11
11
  : compileArcs(summaries, variant, id);
12
12
  }
13
13
  export function isProportionDatum(value) {
14
- return (typeof value === 'object' && value !== null && Reflect.get(value, '__entasisProportion') === true);
14
+ return (typeof value === 'object' &&
15
+ value !== null &&
16
+ Reflect.get(value, '__entasisProportion') === true);
15
17
  }
16
18
  function normalizeProportionVariant(mark, path) {
17
19
  const input = mark.variant;
@@ -119,8 +121,11 @@ function compileArcs(summaries, variant, id) {
119
121
  })
120
122
  ],
121
123
  scales: { angle: null, radius: null },
122
- inset: 16,
123
- radiusRatio: 0.92
124
+ // A pie has no guides or outside labels to make room for (the legend lives outside the
125
+ // plot), so it fills the plot like any other mark. The inset only keeps the outer half of
126
+ // the slice separator stroke inside the plot.
127
+ inset: arcOptions.strokeWidth,
128
+ radiusRatio: 1
124
129
  });
125
130
  return [base];
126
131
  }
@@ -162,9 +167,10 @@ function waffleMark(summaries, cells, variant, id) {
162
167
  seriesFromColor: true,
163
168
  render({ chart, color: resolveColor }) {
164
169
  const rows = Math.ceil(variant.cells / variant.columns);
165
- const side = Math.min(chart.width, chart.height) * 0.86;
166
- const gap = Math.min(variant.gap, side / (Math.max(variant.columns, rows) * 2));
167
- const size = Math.max(0, Math.min((side - gap * (variant.columns - 1)) / variant.columns, (side - gap * (rows - 1)) / rows));
170
+ // Square cells as large as the plot allows: the grid fills the plot's width or its
171
+ // height, whichever runs out first, and centres on the other.
172
+ const gap = Math.min(variant.gap, Math.min(chart.width / variant.columns, chart.height / rows) / 2);
173
+ const size = Math.max(0, Math.min((chart.width - gap * (variant.columns - 1)) / variant.columns, (chart.height - gap * (rows - 1)) / rows));
168
174
  const width = size * variant.columns + gap * (variant.columns - 1);
169
175
  const height = size * rows + gap * (rows - 1);
170
176
  const originX = chart.x + (chart.width - width) / 2;
@@ -1,5 +1,6 @@
1
1
  import { forceCenter, forceCollide, forceLink, forceManyBody, forceSimulation, forceX, forceY } from 'd3-force';
2
2
  import { compileRelationEdges } from './chart.relation.data.js';
3
+ import { axisFraction, estimateLabelWidth, fitAxis, LABEL_LINE_BOX } from './chart.fit.js';
3
4
  import { compileRelationPointMarks } from './chart.relation.marks.js';
4
5
  export function compileNetworkRelation(data, mark, compiled, path, width, height) {
5
6
  const relations = compileRelationEdges(data, compiled, mark, path);
@@ -23,10 +24,37 @@ export function compileNetworkRelation(data, mark, compiled, path, width, height
23
24
  .force('y', forceY(height / 2).strength(0.035))
24
25
  .stop();
25
26
  simulation.tick(300);
26
- const padding = radius + compiled.labels.fontSize + 16;
27
- const positionedNodes = nodes.map((node) => {
28
- const x = clamp(coordinate(node.x, node.id, 'x'), padding, Math.max(padding, width - padding));
29
- const y = clamp(coordinate(node.y, node.id, 'y'), padding, Math.max(padding, height - padding));
27
+ // The simulation settles a shape around the centre at whatever size its forces give it; fit
28
+ // that shape to the plot on each axis, leaving room for the labels drawn above the nodes.
29
+ const labels = compiled.labels;
30
+ const settled = nodes.map((node) => ({
31
+ node,
32
+ x: coordinate(node.x, node.id, 'x'),
33
+ y: coordinate(node.y, node.id, 'y'),
34
+ labelWidth: labels.enabled
35
+ ? estimateLabelWidth(node.label, labels.fontSize, labels.fontWeight)
36
+ : 0
37
+ }));
38
+ const xs = settled.map((node) => node.x);
39
+ const ys = settled.map((node) => node.y);
40
+ const [xMin, xMax, yMin, yMax] = [
41
+ Math.min(...xs),
42
+ Math.max(...xs),
43
+ Math.min(...ys),
44
+ Math.max(...ys)
45
+ ];
46
+ const xFit = fitAxis(settled.map((node) => {
47
+ const extent = Math.max(radius, node.labelWidth / 2);
48
+ return { fraction: axisFraction(node.x, xMin, xMax), before: extent, after: extent };
49
+ }), width);
50
+ const yFit = fitAxis(settled.map((node) => ({
51
+ fraction: axisFraction(node.y, yMin, yMax),
52
+ before: labels.enabled ? radius + 10 + labels.fontSize * LABEL_LINE_BOX.middle.above : radius,
53
+ after: radius
54
+ })), height);
55
+ const positionedNodes = settled.map(({ node, x: settledX, y: settledY }) => {
56
+ const x = xFit.start + axisFraction(settledX, xMin, xMax) * xFit.span;
57
+ const y = yFit.start + axisFraction(settledY, yMin, yMax) * yFit.span;
30
58
  return {
31
59
  ...node,
32
60
  kind: 'relation-node',
@@ -3,23 +3,22 @@ import { sankey, sankeyCenter, sankeyJustify, sankeyLeft, sankeyRight } from 'd3
3
3
  import { curveBumpX } from 'd3-shape';
4
4
  import { compileColor } from './chart.channels.js';
5
5
  import { compileRelationEdges } from './chart.relation.data.js';
6
+ import { axisFraction, CHART_FIT_MARGIN, estimateLabelWidth, fitAxis, LABEL_LINE_BOX } from './chart.fit.js';
6
7
  import { withoutTooltipPoints } from './chart.mark.js';
7
8
  export function compileSankeyRelation(data, mark, compiled, path, width, height) {
8
9
  const relations = compileRelationEdges(data, compiled, mark, path);
9
- const labelSpace = compiled.labels.enabled ? clamp(width * 0.12, 64, 112) : 24;
10
- const top = clamp(height * 0.06, 20, 36);
11
- const bottom = top;
10
+ const labels = compiled.labels;
12
11
  const nodeWidth = mark.nodeWidth ?? clamp(width * 0.022, 10, 22);
13
12
  const nodeGap = mark.nodeGap ?? clamp(height * 0.055, 10, 30);
14
- const graph = sankey()
13
+ const layout = (top, bottom) => sankey()
15
14
  .nodeId((node) => node.identity)
16
15
  .nodeAlign(resolveAlignment(mark.align))
17
16
  .nodeSort((left, right) => left.index - right.index)
18
17
  .nodeWidth(nodeWidth)
19
18
  .nodePadding(nodeGap)
20
19
  .extent([
21
- [labelSpace, top],
22
- [Math.max(labelSpace + 1, width - labelSpace), Math.max(top + 1, height - bottom)]
20
+ [0, top],
21
+ [width, Math.max(top + 1, height - bottom)]
23
22
  ])
24
23
  .iterations(32)({
25
24
  nodes: compiled.nodes.map((node) => ({ ...node })),
@@ -30,10 +29,58 @@ export function compileSankeyRelation(data, mark, compiled, path, width, height)
30
29
  relation
31
30
  }))
32
31
  });
32
+ let graph = layout(CHART_FIT_MARGIN, CHART_FIT_MARGIN);
33
+ // Labels are centred on their node, so only a node shorter than a line pushes its label past
34
+ // the plot edge. Lay out again with exactly that overhang reserved.
35
+ const overhang = labelOverhang(graph.nodes, labels, height);
36
+ if (overhang.top > 0.5 || overhang.bottom > 0.5) {
37
+ graph = layout(CHART_FIT_MARGIN + overhang.top, CHART_FIT_MARGIN + overhang.bottom);
38
+ }
39
+ fitColumns(graph.nodes, nodeWidth, labels, width);
33
40
  const nodes = graph.nodes.map((node) => compileNode(node, width));
34
41
  const links = graph.links.map(compileLink);
35
42
  return compileSankeyMarks(nodes, links, mark, compiled.labels, path);
36
43
  }
44
+ function labelOverhang(nodes, labels, height) {
45
+ if (!labels.enabled)
46
+ return { top: 0, bottom: 0 };
47
+ const above = labels.fontSize * LABEL_LINE_BOX.middle.above;
48
+ const below = labels.fontSize * LABEL_LINE_BOX.middle.below;
49
+ let top = 0;
50
+ let bottom = 0;
51
+ for (const node of nodes) {
52
+ const { y0, y1 } = resolveNodeBounds(node);
53
+ const middle = (y0 + y1) / 2;
54
+ top = Math.max(top, CHART_FIT_MARGIN - (middle - above));
55
+ bottom = Math.max(bottom, middle + below - (height - CHART_FIT_MARGIN));
56
+ }
57
+ return { top, bottom };
58
+ }
59
+ // Columns are placed evenly across the plot, which leaves the outer labels to overflow or, with
60
+ // guessed margins, a gap. Re-place them so the widest outer labels reach the plot edges: a
61
+ // source's label sits left of its node, every other label to the right.
62
+ function fitColumns(nodes, nodeWidth, labels, width) {
63
+ const columns = nodes.map((node) => resolveNodeBounds(node).x0);
64
+ const first = Math.min(...columns);
65
+ const last = Math.max(...columns);
66
+ const placed = nodes.map((node, index) => {
67
+ const label = labels.enabled
68
+ ? 7 + estimateLabelWidth(node.label, labels.fontSize, labels.fontWeight)
69
+ : 0;
70
+ const labelOnLeft = (node.targetLinks?.length ?? 0) === 0;
71
+ return {
72
+ node,
73
+ fraction: axisFraction(columns[index] ?? first, first, last),
74
+ before: labelOnLeft ? label : 0,
75
+ after: nodeWidth + (labelOnLeft ? 0 : label)
76
+ };
77
+ });
78
+ const fit = fitAxis(placed, width);
79
+ for (const { node, fraction } of placed) {
80
+ node.x0 = fit.start + fraction * fit.span;
81
+ node.x1 = node.x0 + nodeWidth;
82
+ }
83
+ }
37
84
  function compileSankeyMarks(nodes, links, mark, labels, path) {
38
85
  const linkMark = link(links, {
39
86
  id: `${mark.id ?? path}:links`,
@@ -1,6 +1,7 @@
1
1
  import { stratify, tree } from 'd3-hierarchy';
2
2
  import { compileChannel } from './chart.channels.js';
3
3
  import { relationKeyIdentity } from './chart.relation.data.js';
4
+ import { axisFraction, estimateLabelWidth, fitAxis, LABEL_LINE_BOX } from './chart.fit.js';
4
5
  import { compileRelationPointMarks } from './chart.relation.marks.js';
5
6
  export function compileTreeRelation(data, mark, compiled, path, width, height) {
6
7
  if (compiled.nodes.length === 0) {
@@ -26,16 +27,74 @@ export function compileTreeRelation(data, mark, compiled, path, width, height) {
26
27
  .parentId((node) => node.parentIdentity)(sourceNodes);
27
28
  const orientation = mark.orientation ?? 'horizontal';
28
29
  const radius = mark.nodeRadius ?? 5;
29
- const padding = clamp(Math.min(width, height) * 0.06, 24, 48);
30
- const labelSpace = compiled.labels.enabled ? clamp(width * 0.13, 64, 120) : padding;
31
30
  const horizontal = orientation === 'horizontal';
32
- const breadth = Math.max(1, (horizontal ? height : width) - padding * 2);
33
- const depth = Math.max(1, horizontal ? width - labelSpace * 2 : height - padding * 2);
34
- const layoutRoot = tree().size([breadth, depth])(root);
35
- const nodes = layoutRoot.descendants().map((node) => {
36
- const x = horizontal ? labelSpace + node.y : padding + node.x;
37
- const y = horizontal ? padding + node.x : padding + node.y;
31
+ const labels = compiled.labels;
32
+ // d3 spaces the tree on a unit grid; the fit below maps it onto the plot. Only the relative
33
+ // positions matter here, and d3's half-separation gaps at the edges are dropped.
34
+ const layoutRoot = tree().size([1, 1])(root);
35
+ const layoutNodes = layoutRoot.descendants();
36
+ const breadthValues = layoutNodes.map((node) => node.x);
37
+ const breadthMin = Math.min(...breadthValues);
38
+ const breadthMax = Math.max(...breadthValues);
39
+ const maxDepth = layoutRoot.height;
40
+ const placed = layoutNodes.map((node) => {
38
41
  const internal = node.children !== undefined;
42
+ const labelWidth = labels.enabled
43
+ ? estimateLabelWidth(node.data.label, labels.fontSize, labels.fontWeight)
44
+ : 0;
45
+ return {
46
+ node,
47
+ internal,
48
+ labelWidth,
49
+ breadth: axisFraction(node.x, breadthMin, breadthMax),
50
+ depth: maxDepth > 0 ? node.depth / maxDepth : 0.5
51
+ };
52
+ });
53
+ // Horizontal: internal labels sit left of their node and leaf labels right, both centred on
54
+ // it vertically. Vertical: labels are centred on the node, above internal nodes and below
55
+ // leaves. Each axis is fitted to the extents those labels add around the nodes.
56
+ const lineAbove = labels.enabled ? labels.fontSize * LABEL_LINE_BOX.middle.above : 0;
57
+ const lineBelow = labels.enabled ? labels.fontSize * LABEL_LINE_BOX.middle.below : 0;
58
+ const depthFit = fitAxis(placed.map(({ depth, internal, labelWidth }) => {
59
+ if (!labels.enabled)
60
+ return { fraction: depth, before: radius, after: radius };
61
+ if (horizontal) {
62
+ return {
63
+ fraction: depth,
64
+ before: internal ? radius + 7 + labelWidth : radius,
65
+ after: internal ? radius : radius + 7 + labelWidth
66
+ };
67
+ }
68
+ return {
69
+ fraction: depth,
70
+ before: internal ? radius + 8 + lineAbove : radius,
71
+ after: internal ? radius : radius + 10 + lineBelow
72
+ };
73
+ }), horizontal ? width : height);
74
+ const breadthFit = fitAxis(placed.map(({ breadth, labelWidth }) => {
75
+ if (horizontal) {
76
+ return {
77
+ fraction: breadth,
78
+ before: Math.max(radius, lineAbove),
79
+ after: Math.max(radius, lineBelow)
80
+ };
81
+ }
82
+ const extent = Math.max(radius, labelWidth / 2);
83
+ return { fraction: breadth, before: extent, after: extent };
84
+ }), horizontal ? height : width);
85
+ const position = new Map(placed.map(({ node, breadth, depth }) => {
86
+ const along = depthFit.start + depth * depthFit.span;
87
+ const across = breadthFit.start + breadth * breadthFit.span;
88
+ return [node, horizontal ? { x: along, y: across } : { x: across, y: along }];
89
+ }));
90
+ const at = (node) => {
91
+ const point = position.get(node);
92
+ if (!point)
93
+ throw new Error('[Chart] Tree layout lost a positioned node.');
94
+ return point;
95
+ };
96
+ const nodes = placed.map(({ node, internal }) => {
97
+ const { x, y } = at(node);
39
98
  return {
40
99
  ...node.data,
41
100
  kind: 'relation-node',
@@ -47,21 +106,22 @@ export function compileTreeRelation(data, mark, compiled, path, width, height) {
47
106
  labelAnchor: horizontal ? (internal ? 'end' : 'start') : 'middle'
48
107
  };
49
108
  });
50
- const links = layoutRoot.links().map(({ source, target }) => ({
51
- kind: 'relation-link',
52
- identity: `${source.data.identity}:${target.data.identity}`,
53
- source: source.data.id,
54
- target: target.data.id,
55
- sourceLabel: source.data.label,
56
- targetLabel: target.data.label,
57
- group: target.data.group,
58
- x1: horizontal ? labelSpace + source.y : padding + source.x,
59
- y1: horizontal ? padding + source.x : padding + source.y,
60
- x2: horizontal ? labelSpace + target.y : padding + target.x,
61
- y2: horizontal ? padding + target.x : padding + target.y
62
- }));
109
+ const links = layoutRoot.links().map(({ source, target }) => {
110
+ const from = at(source);
111
+ const to = at(target);
112
+ return {
113
+ kind: 'relation-link',
114
+ identity: `${source.data.identity}:${target.data.identity}`,
115
+ source: source.data.id,
116
+ target: target.data.id,
117
+ sourceLabel: source.data.label,
118
+ targetLabel: target.data.label,
119
+ group: target.data.group,
120
+ x1: from.x,
121
+ y1: from.y,
122
+ x2: to.x,
123
+ y2: to.y
124
+ };
125
+ });
63
126
  return compileRelationPointMarks(nodes, links, mark, compiled.labels, path);
64
127
  }
65
- function clamp(value, minimum, maximum) {
66
- return Math.min(maximum, Math.max(minimum, value));
67
- }
@@ -95,6 +95,12 @@ export type ChartSeriesMark<TRow> = ChartDataMarkProps<TRow> & ChartFillStyle<TR
95
95
  line?: boolean | ChartLineOptions<TRow>;
96
96
  points?: boolean | ChartPointOptions<TRow>;
97
97
  curve?: ChartCurve;
98
+ /**
99
+ * How an area is painted: `gradient` shades the series colour along the value axis, `solid`
100
+ * is one flat fill at `fillOpacity` (20% by default). Gradients apply to palette colours.
101
+ * @default 'gradient'
102
+ */
103
+ areaFill?: 'gradient' | 'solid';
98
104
  analysis?: readonly [ChartSeriesAnalysis, ...ChartSeriesAnalysis[]];
99
105
  };
100
106
  type ChartBarCoordinates<TRow> = {
@@ -78,6 +78,7 @@ export declare class ChartState<TRow extends object> extends BoundChartOptions<C
78
78
  readonly plotClass: string;
79
79
  readonly size: import("./chart.validation.js").ChartSize | undefined;
80
80
  readonly rootStyle: string;
81
+ readonly plotStyle: string | undefined;
81
82
  readonly initialMarkup: string;
82
83
  constructor(options: ChartStateOptions<TRow>);
83
84
  /** Row key of the pinned datum, controlled by `tooltip.value` when it is provided. */
@@ -24,13 +24,16 @@ export class ChartState extends BoundChartOptions {
24
24
  rootClass = $derived(this.classes.root({ className: this.className }));
25
25
  plotClass = $derived(this.classes.plot());
26
26
  size = $derived(resolveChartSize(this.height, this.aspectRatio));
27
- // `height` and `aspectRatio` own the box when they are set, so they win over the root
28
- // class; with neither, the root class keeps its own height and the chart observes it.
29
- rootStyle = $derived(this.size?.height !== undefined
30
- ? `position:relative;width:100%;height:${this.size.height}px`
27
+ // `height` and `aspectRatio` size the plot, the box the engine draws into, so they go on the
28
+ // plot box and the legend row adds to the chart's height. Sizing the root instead left the
29
+ // plot shorter than the scene the engine laid out, which then shrank to fit. With neither,
30
+ // the root class keeps its own height and the engine measures what the legend leaves.
31
+ rootStyle = $derived(this.size ? 'position:relative;width:100%;height:auto' : 'position:relative;width:100%');
32
+ plotStyle = $derived(this.size?.height !== undefined
33
+ ? `flex:none;height:${this.size.height}px`
31
34
  : this.size?.aspectRatio !== undefined
32
- ? `position:relative;width:100%;height:auto;aspect-ratio:${this.size.aspectRatio}`
33
- : 'position:relative;width:100%');
35
+ ? `flex:none;aspect-ratio:${this.size.aspectRatio}`
36
+ : undefined);
34
37
  initialMarkup;
35
38
  #adapter;
36
39
  #interaction;
@@ -20,6 +20,7 @@ export declare class ChartViewportState<TRow extends object> {
20
20
  readonly controls: readonly ChartControl<any, any>[] | undefined;
21
21
  readonly status: string;
22
22
  constructor(chart: ChartState<TRow>);
23
+ private clear;
23
24
  syncScene(scene: ChartScene<TRow>): void;
24
25
  reset: () => void;
25
26
  private createControls;
@@ -1,3 +1,4 @@
1
+ import { untrack } from 'svelte';
1
2
  import { brushX } from '@tanstack/charts/interaction/brush';
2
3
  import { controlledSignal } from '@tanstack/charts/interaction/signal';
3
4
  import { resolveChartViewport, sameChartViewportDomain } from './chart.viewport.js';
@@ -20,16 +21,32 @@ export class ChartViewportState {
20
21
  animation = $derived(this.configuration?.animation);
21
22
  controls = $derived(this.createControls());
22
23
  status = $derived(this.isZoomed ? 'Chart zoomed on the x axis.' : '');
24
+ /** Whether the x scale brushes categories or a continuous range. */
25
+ #xKind = $derived(this.chart.x?.scale.type === 'band' || this.chart.x?.scale.type === 'point'
26
+ ? 'categorical'
27
+ : 'continuous');
23
28
  constructor(chart) {
24
29
  this.#chart = chart;
25
30
  $effect(() => {
26
31
  if (this.configuration)
27
32
  return;
28
- this.xDomain = undefined;
29
- this.fullDomain = [];
30
- this.range = undefined;
31
- this.isBrushing = false;
33
+ this.clear();
32
34
  });
35
+ // A zoom window kept across a switch between categorical and continuous x would hold
36
+ // bounds of the wrong kind; start over from the next scene's domain instead.
37
+ let kind = untrack(() => this.#xKind);
38
+ $effect(() => {
39
+ if (this.#xKind === kind)
40
+ return;
41
+ kind = this.#xKind;
42
+ untrack(() => this.clear());
43
+ });
44
+ }
45
+ clear() {
46
+ this.xDomain = undefined;
47
+ this.fullDomain = [];
48
+ this.range = undefined;
49
+ this.isBrushing = false;
33
50
  }
34
51
  syncScene(scene) {
35
52
  if (!this.configuration || this.isZoomed)
@@ -81,9 +98,10 @@ export class ChartViewportState {
81
98
  ];
82
99
  }
83
100
  const { start, end } = this.range;
84
- if (typeof start === 'string' || typeof end === 'string') {
85
- throw new TypeError('[Chart] A continuous brush requires number or Date bounds.');
86
- }
101
+ // Category bounds left from before the x scale turned continuous: the reset above clears
102
+ // them, and the next scene publishes the continuous domain.
103
+ if (typeof start === 'string' || typeof end === 'string')
104
+ return undefined;
87
105
  return [
88
106
  brushX({
89
107
  ...options,
@@ -64,6 +64,8 @@
64
64
  toolbarSuffix,
65
65
  bulkActions,
66
66
  rowActions,
67
+ rowActionsWidth,
68
+ onRowActivate,
67
69
  expandedContent,
68
70
  loading = false,
69
71
  error,
@@ -120,6 +122,8 @@
120
122
  toolbarSuffix,
121
123
  bulkActions,
122
124
  rowActions,
125
+ rowActionsWidth,
126
+ onRowActivate,
123
127
  expandedContent,
124
128
  loading,
125
129
  error,
@@ -164,7 +168,8 @@
164
168
  rowCount,
165
169
  selectionMode,
166
170
  pagination,
167
- rowActions
171
+ rowActions,
172
+ rowActionsWidth
168
173
  ];
169
174
  model.reconcileProcessingMode();
170
175
  model.reconcileColumns();
@@ -439,6 +444,18 @@
439
444
  if (pagination === false) return null;
440
445
  return pagination;
441
446
  });
447
+ // The current size is always offered, so a `pageSize` missing from `pageSizes` still shows in
448
+ // the select instead of leaving it on its placeholder.
449
+ const pageSizeItems = $derived.by(() => {
450
+ const sizes = paginationConfig?.pageSizes ?? [25, 50, 100];
451
+ const current = tableState!.pagination.pageSize;
452
+ const offered = sizes.includes(current)
453
+ ? sizes
454
+ : [...sizes, current].sort((left, right) => left - right);
455
+ return offered.map((value) => ({ value: String(value), label: String(value) }));
456
+ });
457
+ const uid = $props.id();
458
+ const rowsPerPageId = `${uid}-rows-per-page`;
442
459
  const tableApi: DataTableApi<TData> = {
443
460
  get state() {
444
461
  return model.state;
@@ -633,15 +650,15 @@
633
650
  {#if paginationConfig && paginationConfig.showControls !== false}
634
651
  <div class={classes.footer()}>
635
652
  <div class={classes.toolbarGroup()}>
636
- <span class={classes.summary()}>{pageStart}–{pageEnd} of {totalItems}</span>
637
- <span class={classes.summary()}>{t.dataTableRowsPerPage}</span>
653
+ <span class={classes.summary()}>
654
+ {t.paginationSummary(pageStart, pageEnd, totalItems)}
655
+ </span>
656
+ <!-- The visible text names the select; a field label would print it a second time. -->
657
+ <span id={rowsPerPageId} class={classes.summary()}>{t.dataTableRowsPerPage}</span>
638
658
  <Select
639
659
  size="small"
640
- label={t.dataTableRowsPerPage}
641
- items={(paginationConfig.pageSizes ?? [25, 50, 100]).map((value) => ({
642
- value: String(value),
643
- label: String(value)
644
- }))}
660
+ triggerAttrs={{ 'aria-labelledby': rowsPerPageId }}
661
+ items={pageSizeItems}
645
662
  value={String(tableState!.pagination.pageSize)}
646
663
  {disabled}
647
664
  onValueChange={(value) => model.setPageSize(Number(value))}
@@ -151,21 +151,44 @@
151
151
  const control = (event.currentTarget as HTMLElement).querySelector<HTMLElement>(
152
152
  'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
153
153
  );
154
- control?.focus();
154
+ if (control) control.focus();
155
+ else if (event.key === 'Enter' && activatesFrom(column.id)) activate(event);
155
156
  };
156
157
 
157
- const handleCellClick = (event: MouseEvent, rowIndex: number, columnIndex: number) => {
158
- if (interactionMode !== 'grid') return;
159
- model.moveFocusedCell(rowIndex, columnIndex);
160
-
158
+ const handleCellClick = (
159
+ event: MouseEvent,
160
+ rowIndex: number,
161
+ columnIndex: number,
162
+ columnId: string
163
+ ) => {
161
164
  const target = event.target;
162
165
  const interactive =
163
166
  target instanceof Element
164
- ? target.closest('button, input, select, textarea, [tabindex]:not([tabindex="-1"])')
167
+ ? target.closest(
168
+ 'a[href], button, input, select, textarea, label, [contenteditable], [tabindex]:not([tabindex="-1"])'
169
+ )
165
170
  : null;
166
- if (interactive && interactive !== event.currentTarget) return;
167
- (event.currentTarget as HTMLElement).focus();
171
+ const onControl = !!interactive && interactive !== event.currentTarget;
172
+ if (interactionMode === 'grid') {
173
+ model.moveFocusedCell(rowIndex, columnIndex);
174
+ if (!onControl) (event.currentTarget as HTMLElement).focus();
175
+ }
176
+ // A click that ends a text selection is the reader copying a value, not opening the row.
177
+ const selecting = !(window.getSelection()?.isCollapsed ?? true);
178
+ if (!onControl && !selecting && activatesFrom(columnId)) activate(event);
168
179
  };
180
+
181
+ // The selection and actions columns own their clicks; group rows expand rather than open.
182
+ const activatable = $derived(
183
+ !!model.props.onRowActivate && !model.props.disabled && !row.getIsGrouped()
184
+ );
185
+ const activatesFrom = (columnId: string) =>
186
+ activatable &&
187
+ columnId !== DATA_TABLE_SELECTION_COLUMN &&
188
+ columnId !== DATA_TABLE_ACTIONS_COLUMN &&
189
+ model.editing?.row.id !== row.id;
190
+ const activate = (event: MouseEvent | KeyboardEvent) =>
191
+ model.props.onRowActivate?.({ ...rowPayload, event });
169
192
  </script>
170
193
 
171
194
  <tr
@@ -177,7 +200,7 @@
177
200
  data-row-id={row.id}
178
201
  data-grid-row={rowIndex}
179
202
  data-selected={rowSelected}
180
- class={classes.row({ density, grouped: row.getIsGrouped() })}
203
+ class={classes.row({ density, grouped: row.getIsGrouped(), activatable })}
181
204
  style:grid-template-columns={gridTemplate}
182
205
  {@attach measureAttachment}
183
206
  >
@@ -225,7 +248,7 @@
225
248
  onmousedown={(event) => {
226
249
  if (event.detail > 1 && config?.editor) event.preventDefault();
227
250
  }}
228
- onclick={(event) => handleCellClick(event, rowIndex, allIndex)}
251
+ onclick={(event) => handleCellClick(event, rowIndex, allIndex, column.id)}
229
252
  onkeydown={(event) => handleCellKeydown(event, column, allIndex)}
230
253
  ondblclick={(event) => {
231
254
  if (!config?.editor) return;
@@ -1 +1 @@
1
- export declare const dataTableDescription = "\n# DataTable\n\nDataTable is the typed, interactive, virtualized table for application data. Its public API is\nEntasis-native; TanStack Table Core remains private, but it must be installed as an optional peer\ndependency. Use Table for static tabular content.\n\n## Requires\n\nDataTable builds its row model with TanStack Table Core, which ships with entasis.\n\n## Basic usage\n\n```svelte\n<script lang=\"ts\">\n import {\n createDataTableColumnHelper,\n DataTable\n } from './index.ts';\n\n type Person = { id: string; name: string; role: string; active: boolean };\n const column = createDataTableColumnHelper<Person>();\n const columns = [\n column.accessor('name', {\n id: 'name',\n header: 'Name',\n sortable: true,\n filter: { type: 'text' }\n }),\n column.accessor('role', { id: 'role', header: 'Role' }),\n column.accessor('active', {\n id: 'active',\n header: 'Active',\n filter: { type: 'boolean' }\n })\n ];\n</script>\n\n<DataTable\n {items}\n {columns}\n getRowId={(person) => person.id}\n height={420}\n search\n pagination={{ pageSize: 25, pageSizes: [25, 50, 100] }}\n/>\n```\n\n`items`, `columns`, and `getRowId` are required. Without `height`, DataTable fills a\nparent with a definite height so the virtual viewport remains bounded. Pass `virtualize={false}`\ninstead to drop the table into normal document flow at its natural height:\n\n```svelte\n<DataTable {items} {columns} {getRowId} virtualize={false} />\n```\n\n## Core props\n\n- **items**: `readonly TData[]`.\n- **columns**: `readonly DataTableColumn<TData>[]`.\n- **getRowId**: stable row identity used by selection, expansion, virtualization, and focus.\n- **height**: optional `string | number`; otherwise fills a definite-height parent. Only read\n while virtualizing.\n- **virtualize**: default true. False renders every row in normal flow, so the table needs no\n height and no definite-height parent.\n- **density**: `compact | normal | comfortable`, default `normal`.\n- **interactionMode**: `table | grid`, default `table`.\n- **processingMode**: `client | manual`, default `client`.\n- **selectionMode**: `none | single | multiple`, default `none`.\n- **pagination**: `false | DataTablePaginationConfig`. False disables pagination processing.\n `showControls: false` keeps processing active while hiding the built-in footer.\n- **search**: false, true, or placeholder/debounce configuration.\n- **showColumnVisibilityControl**: default false.\n- **stickyHeader**, **overscan**, **estimatedRowHeight**, and **animateRows** control rendering.\n- **disabled** blocks sorting controls, selection paths, editing, and column manipulation.\n- **loading** and **error** drive table states and the NetworkIndicator.\n- **api**: bindable narrow external-control instance handle (`DataTableApi<TData>`).\n- **cell** and **header**: table-level renderer snippets with `renderDefault` delegates.\n- **class**, **theme**, **ref**, and Svelte attachments follow Entasis conventions.\n\n## Column definitions\n\n`DataTableColumn<TData, TValue>` supports `id`, `accessor`, required `header`, optional\n`cell` and `aggregatedCell`, sorting, filtering, grouping, aggregation, editing, visibility,\nresizing, reordering, pinning, alignment, `width` / `minWidth` / `maxWidth`, and column\nclasses.\n\nUse `createDataTableColumnHelper<TData>()` to infer accessor values inside cell and editor\nconfiguration. Raw DataTableColumn definitions remain available as an escape hatch.\n\n## Rendering\n\nColumn-level renderers are part of the default rendering chain. A table-level `cell` renderer\nwraps every public ordinary, grouped, aggregated, and placeholder cell. It does not wrap selection\ncells, action cells, detail rows, or active editors. A table-level `header` renderer wraps only\nconfigured public header content; sorting, menus, dragging, resizing, focus, and ARIA remain owned\nby DataTable.\n\n`DataTableCellRenderPayload<TData>` contains the normal cell payload plus `column`,\n`placeholder`, and `renderDefault`. `DataTableHeaderRenderPayload<TData>` contains the normal\nheader payload plus `renderDefault`.\n\n```svelte\n{#snippet cell({ columnId, value, renderDefault })}\n {#if columnId === 'status'}\n <Chip>{value}</Chip>\n {:else}\n {@render renderDefault()}\n {/if}\n{/snippet}\n```\n\nPrecedence is structural rendering or an active editor, then the table-level renderer, then\n`renderDefault`, which resolves the column renderer before the built-in fallback.\n\n## Filters\n\nBuilt-in filters are text, number range, select, multi-select, date range, and boolean.\nA custom filter is `{ type: 'custom', render, predicate }`. Its renderer receives the column,\ncurrent value, active state, `setValue(value)`, and `clear()`; `predicate(row, value,\ncolumnValue)` owns client matching. Filtering, searching, sorting, and grouping reset page one.\n\n## State and external composition\n\nUse `createDataTableState(columns, initialState?, pageSize?)` for controlled state. The bindable\nstate contains sorting, globalFilter, columnFilters, one-based pagination, rowSelection,\ncolumnVisibility, columnOrder, columnPinning, columnSizing, grouping, and expanded.\n\nDataTable updates replace slices immutably and call `onStateChange(nextState)`. Parent-originated\nmutations through `bind:state` are observed without calling that callback again.\n\n`bind:api` exposes a stable `DataTableApi<TData>` with reactive state, totals, visible\nrows, selected loaded rows, save status, and commands for global/column filters, selection clearing,\npage, and page size. Commands use the same guards and reset rules as built-in controls.\n\n```svelte\n<DataTable\n bind:state\n bind:api\n pagination={{ pageSize: 25, showControls: false }}\n {...props}\n/>\n```\n\n## Client and manual processing\n\nClient mode runs filtering, sorting, grouping, aggregation, expansion, and pagination locally.\n\nManual mode delegates filtering, sorting, and pagination together. It requires `rowCount`, and\n`items` must contain the already processed current page. Grouping and aggregation are client-only\nin this release: grouping commands are hidden and unsupported manual grouping state is normalized\naway. Abort stale requests when state changes. In manual mode, `dataTable.selectedRows` includes\nonly loaded row objects; all selected IDs remain in `state.rowSelection`.\n\n## Editing\n\nBuilt-in editors are text, number, select, date, and switch. A custom editor snippet receives row\nmetadata, draft, pending/error state, `setDraft`, `commit`, and `cancel`. DataTable never\nmutates rows; changed values call `onCellCommit` and render optimistically while the\nNetworkIndicator tracks the request. Unchanged drafts close without a commit. Rejection rolls back,\nrestores the editor and draft, and exposes the error. Only one asynchronous transaction can be\nactive or recoverable. Columns with editors do not enter edit mode without `onCellCommit` and\nissue a development warning.\n\nEnter and outside clicks commit, Escape cancels, and Tab commits without leaving the edited cell.\nCalendar selection synchronizes its draft before commit. Arrow keys remain owned by editor controls.\n\n## Slots\n\n- **caption**, **toolbarPrefix**, **toolbarSuffix**, **bulkActions**, **rowActions**,\n **expandedContent**, **loadingContent**, **empty**, **noResults**, and **errorContent**.\n- Toolbar slots receive state, selected loaded rows, visible rows, clearFilters, and clearSelection.\n- Row slots receive row identity, selection/expansion state, depth, and guarded toggle actions.\n\n## Accessibility and virtualization\n\nTable mode preserves native table semantics. Grid mode uses one roving cell tab stop with arrows,\nTab/Shift+Tab, Home/End, Ctrl+Home/Ctrl+End, and PageUp/PageDown. Focus is reconciled by row and\ncolumn identity after data, pagination, filtering, grouping, and visibility changes. State rows use\ncomplete column spans and keep recovery controls keyboard reachable. Logical row indices derive\nfrom the final grouped/expanded row model.\n\nRows are virtualized by default; `virtualize={false}` renders all of them in normal flow at\ntheir natural height, keeping the same selection, keyboard navigation, and editing paths. Use it for\nsmall datasets that belong in document flow, not for large ones. Semantic table mode keeps all\ncolumns mounted; grid mode additionally virtualizes center columns while pinned columns stay\nmounted. Resize handles support pointer and\nkeyboard resizing. Reorder handles support left/right keyboard movement and pointer movement within\ntheir current pin region. Logical inset positioning preserves RTL pinning.\n";
1
+ export declare const dataTableDescription = "\n# DataTable\n\nDataTable is the typed, interactive, virtualized table for application data. Its public API is\nEntasis-native; TanStack Table Core remains private, but it must be installed as an optional peer\ndependency. Use Table for static tabular content.\n\n## Requires\n\nDataTable builds its row model with TanStack Table Core, which ships with entasis.\n\n## Basic usage\n\n```svelte\n<script lang=\"ts\">\n import {\n createDataTableColumnHelper,\n DataTable\n } from './index.ts';\n\n type Person = { id: string; name: string; role: string; active: boolean };\n const column = createDataTableColumnHelper<Person>();\n const columns = [\n column.accessor('name', {\n id: 'name',\n header: 'Name',\n sortable: true,\n filter: { type: 'text' }\n }),\n column.accessor('role', { id: 'role', header: 'Role' }),\n column.accessor('active', {\n id: 'active',\n header: 'Active',\n filter: { type: 'boolean' }\n })\n ];\n</script>\n\n<DataTable\n {items}\n {columns}\n getRowId={(person) => person.id}\n height={420}\n search\n pagination={{ pageSize: 25, pageSizes: [25, 50, 100] }}\n/>\n```\n\n`items`, `columns`, and `getRowId` are required. Without `height`, DataTable fills a\nparent with a definite height so the virtual viewport remains bounded. Pass `virtualize={false}`\ninstead to drop the table into normal document flow at its natural height:\n\n```svelte\n<DataTable {items} {columns} {getRowId} virtualize={false} />\n```\n\n## Core props\n\n- **items**: `readonly TData[]`.\n- **columns**: `readonly DataTableColumn<TData>[]`.\n- **getRowId**: stable row identity used by selection, expansion, virtualization, and focus.\n- **height**: optional `string | number`; otherwise fills a definite-height parent. Only read\n while virtualizing.\n- **virtualize**: default true. False renders every row in normal flow, so the table needs no\n height and no definite-height parent.\n- **density**: `compact | normal | comfortable`, default `normal`.\n- **interactionMode**: `table | grid`, default `table`.\n- **processingMode**: `client | manual`, default `client`.\n- **selectionMode**: `none | single | multiple`, default `none`.\n- **pagination**: `false | DataTablePaginationConfig`. False disables pagination processing.\n `showControls: false` keeps processing active while hiding the built-in footer. The size select\n always offers the current `pageSize`, even when `pageSizes` leaves it out.\n- **search**: false, true, or placeholder/debounce configuration.\n- **showColumnVisibilityControl**: default false.\n- **stickyHeader**, **overscan**, **estimatedRowHeight**, and **animateRows** control rendering.\n- **disabled** blocks sorting controls, selection paths, editing, and column manipulation.\n- **loading** and **error** drive table states and the NetworkIndicator.\n- **api**: bindable narrow external-control instance handle (`DataTableApi<TData>`).\n- **cell** and **header**: table-level renderer snippets with `renderDefault` delegates.\n- **class**, **theme**, **ref**, and Svelte attachments follow Entasis conventions.\n\n## Column definitions\n\n`DataTableColumn<TData, TValue>` supports `id`, `accessor`, required `header`, optional\n`cell` and `aggregatedCell`, sorting, filtering, grouping, aggregation, editing, visibility,\nresizing, reordering, pinning, alignment, `width` / `minWidth` / `maxWidth`, and column\nclasses.\n\nUse `createDataTableColumnHelper<TData>()` to infer accessor values inside cell and editor\nconfiguration. Raw DataTableColumn definitions remain available as an escape hatch.\n\n## Rendering\n\nColumn-level renderers are part of the default rendering chain. A table-level `cell` renderer\nwraps every public ordinary, grouped, aggregated, and placeholder cell. It does not wrap selection\ncells, action cells, detail rows, or active editors. A table-level `header` renderer wraps only\nconfigured public header content; sorting, menus, dragging, resizing, focus, and ARIA remain owned\nby DataTable.\n\n`DataTableCellRenderPayload<TData>` contains the normal cell payload plus `column`,\n`placeholder`, and `renderDefault`. `DataTableHeaderRenderPayload<TData>` contains the normal\nheader payload plus `renderDefault`.\n\n```svelte\n{#snippet cell({ columnId, value, renderDefault })}\n {#if columnId === 'status'}\n <Chip>{value}</Chip>\n {:else}\n {@render renderDefault()}\n {/if}\n{/snippet}\n```\n\nPrecedence is structural rendering or an active editor, then the table-level renderer, then\n`renderDefault`, which resolves the column renderer before the built-in fallback.\n\n## Filters\n\nBuilt-in filters are text, number range, select, multi-select, date range, and boolean.\nA custom filter is `{ type: 'custom', render, predicate }`. Its renderer receives the column,\ncurrent value, active state, `setValue(value)`, and `clear()`; `predicate(row, value,\ncolumnValue)` owns client matching. Filtering, searching, sorting, and grouping reset page one.\n\n## State and external composition\n\nUse `createDataTableState(columns, initialState?, pageSize?)` for controlled state. The bindable\nstate contains sorting, globalFilter, columnFilters, one-based pagination, rowSelection,\ncolumnVisibility, columnOrder, columnPinning, columnSizing, grouping, and expanded.\n\nDataTable updates replace slices immutably and call `onStateChange(nextState)`. Parent-originated\nmutations through `bind:state` are observed without calling that callback again.\n\n`bind:api` exposes a stable `DataTableApi<TData>` with reactive state, totals, visible\nrows, selected loaded rows, save status, and commands for global/column filters, selection clearing,\npage, and page size. Commands use the same guards and reset rules as built-in controls.\n\n```svelte\n<DataTable\n bind:state\n bind:api\n pagination={{ pageSize: 25, showControls: false }}\n {...props}\n/>\n```\n\n## Client and manual processing\n\nClient mode runs filtering, sorting, grouping, aggregation, expansion, and pagination locally.\n\nManual mode delegates filtering, sorting, and pagination together. It requires `rowCount`, and\n`items` must contain the already processed current page. Grouping and aggregation are client-only\nin this release: grouping commands are hidden and unsupported manual grouping state is normalized\naway. Abort stale requests when state changes. In manual mode, `dataTable.selectedRows` includes\nonly loaded row objects; all selected IDs remain in `state.rowSelection`.\n\n## Editing\n\nBuilt-in editors are text, number, select, date, and switch. A custom editor snippet receives row\nmetadata, draft, pending/error state, `setDraft`, `commit`, and `cancel`. DataTable never\nmutates rows; changed values call `onCellCommit` and render optimistically while the\nNetworkIndicator tracks the request. Unchanged drafts close without a commit. Rejection rolls back,\nrestores the editor and draft, and exposes the error. Only one asynchronous transaction can be\nactive or recoverable. Columns with editors do not enter edit mode without `onCellCommit` and\nissue a development warning.\n\nEnter and outside clicks commit, Escape cancels, and Tab commits without leaving the edited cell.\nCalendar selection synchronizes its draft before commit. Arrow keys remain owned by editor controls.\n\n## Slots\n\n- **caption**, **toolbarPrefix**, **toolbarSuffix**, **bulkActions**, **rowActions**,\n **expandedContent**, **loadingContent**, **empty**, **noResults**, and **errorContent**.\n- Toolbar slots receive state, selected loaded rows, visible rows, clearFilters, and clearSelection.\n- Row slots receive row identity, selection/expansion state, depth, and guarded toggle actions.\n- The row actions column is 36px, one icon button; set `rowActionsWidth` (pixels) for text\n buttons or several actions.\n\n## Row activation\n\n`onRowActivate(payload)` receives the row payload plus the native `event`, for modifier keys.\nIt fires on a click anywhere in the row except on a control (link, button, input, label, focusable\nelement), the selection or actions column, a group row, or a click that ends a text selection.\nIn grid mode Enter activates a cell that has no editor and no control. Rows show a pointer while it\nis set. Table-mode rows are not focusable, so keep a link or row action for keyboard users.\n\n## Accessibility and virtualization\n\nTable mode preserves native table semantics. Grid mode uses one roving cell tab stop with arrows,\nTab/Shift+Tab, Home/End, Ctrl+Home/Ctrl+End, and PageUp/PageDown. Focus is reconciled by row and\ncolumn identity after data, pagination, filtering, grouping, and visibility changes. State rows use\ncomplete column spans and keep recovery controls keyboard reachable. Logical row indices derive\nfrom the final grouped/expanded row model.\n\nRows are virtualized by default; `virtualize={false}` renders all of them in normal flow at\ntheir natural height, keeping the same selection, keyboard navigation, and editing paths. Use it for\nsmall datasets that belong in document flow, not for large ones. Semantic table mode keeps all\ncolumns mounted; grid mode additionally virtualizes center columns while pinned columns stay\nmounted. Resize handles support pointer and\nkeyboard resizing. Reorder handles support left/right keyboard movement and pointer movement within\ntheir current pin region. Logical inset positioning preserves RTL pinning.\n";
@@ -68,7 +68,8 @@ instead to drop the table into normal document flow at its natural height:
68
68
  - **processingMode**: \`client | manual\`, default \`client\`.
69
69
  - **selectionMode**: \`none | single | multiple\`, default \`none\`.
70
70
  - **pagination**: \`false | DataTablePaginationConfig\`. False disables pagination processing.
71
- \`showControls: false\` keeps processing active while hiding the built-in footer.
71
+ \`showControls: false\` keeps processing active while hiding the built-in footer. The size select
72
+ always offers the current \`pageSize\`, even when \`pageSizes\` leaves it out.
72
73
  - **search**: false, true, or placeholder/debounce configuration.
73
74
  - **showColumnVisibilityControl**: default false.
74
75
  - **stickyHeader**, **overscan**, **estimatedRowHeight**, and **animateRows** control rendering.
@@ -171,6 +172,16 @@ Calendar selection synchronizes its draft before commit. Arrow keys remain owned
171
172
  **expandedContent**, **loadingContent**, **empty**, **noResults**, and **errorContent**.
172
173
  - Toolbar slots receive state, selected loaded rows, visible rows, clearFilters, and clearSelection.
173
174
  - Row slots receive row identity, selection/expansion state, depth, and guarded toggle actions.
175
+ - The row actions column is 36px, one icon button; set \`rowActionsWidth\` (pixels) for text
176
+ buttons or several actions.
177
+
178
+ ## Row activation
179
+
180
+ \`onRowActivate(payload)\` receives the row payload plus the native \`event\`, for modifier keys.
181
+ It fires on a click anywhere in the row except on a control (link, button, input, label, focusable
182
+ element), the selection or actions column, a group row, or a click that ends a text selection.
183
+ In grid mode Enter activates a cell that has no editor and no control. Rows show a pointer while it
184
+ is set. Table-mode rows are not focusable, so keep a link or row action for keyboard users.
174
185
 
175
186
  ## Accessibility and virtualization
176
187