@elabs-ai/components-data 4.1.0 → 5.0.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.
Files changed (139) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-3OVN7MMO.js +2 -0
  3. package/dist/chunk-3OVN7MMO.js.map +1 -0
  4. package/dist/chunk-3UFWYVDS.js +2 -0
  5. package/dist/chunk-3UFWYVDS.js.map +1 -0
  6. package/dist/chunk-47ZBQZYS.js +47 -0
  7. package/dist/chunk-47ZBQZYS.js.map +1 -0
  8. package/dist/chunk-5K2IFOV2.js +2 -0
  9. package/dist/chunk-5K2IFOV2.js.map +1 -0
  10. package/dist/chunk-677ORWI6.js +17 -0
  11. package/dist/chunk-677ORWI6.js.map +1 -0
  12. package/dist/chunk-7ZYUY3UL.js +2 -0
  13. package/dist/chunk-7ZYUY3UL.js.map +1 -0
  14. package/dist/chunk-BEYHV3ZX.js +37 -0
  15. package/dist/chunk-BEYHV3ZX.js.map +1 -0
  16. package/dist/chunk-DBPJ7LVM.js +57 -0
  17. package/dist/chunk-DBPJ7LVM.js.map +1 -0
  18. package/dist/chunk-E3V3SYY7.js +87 -0
  19. package/dist/chunk-E3V3SYY7.js.map +1 -0
  20. package/dist/chunk-GBO56756.js +81 -0
  21. package/dist/chunk-GBO56756.js.map +1 -0
  22. package/dist/chunk-HG2YMSRM.js +95 -0
  23. package/dist/chunk-HG2YMSRM.js.map +1 -0
  24. package/dist/chunk-K2GHSRCP.js +1823 -0
  25. package/dist/chunk-K2GHSRCP.js.map +1 -0
  26. package/dist/chunk-KCB33PD3.js +43 -0
  27. package/dist/chunk-KCB33PD3.js.map +1 -0
  28. package/dist/chunk-MV2YVXEZ.js +67 -0
  29. package/dist/chunk-MV2YVXEZ.js.map +1 -0
  30. package/dist/chunk-NKVJUQYW.js +82 -0
  31. package/dist/chunk-NKVJUQYW.js.map +1 -0
  32. package/dist/chunk-OASM53LL.js +29 -0
  33. package/dist/chunk-OASM53LL.js.map +1 -0
  34. package/dist/chunk-RZ5MYZOA.js +2 -0
  35. package/dist/chunk-RZ5MYZOA.js.map +1 -0
  36. package/dist/chunk-SK6347WB.js +100 -0
  37. package/dist/chunk-SK6347WB.js.map +1 -0
  38. package/dist/chunk-TBB33PHQ.js +58 -0
  39. package/dist/chunk-TBB33PHQ.js.map +1 -0
  40. package/dist/chunk-VCLA45IC.js +150 -0
  41. package/dist/chunk-VCLA45IC.js.map +1 -0
  42. package/dist/chunk-XN6O6Z2N.js +58 -0
  43. package/dist/chunk-XN6O6Z2N.js.map +1 -0
  44. package/dist/chunk-ZFEW36RG.js +77 -0
  45. package/dist/chunk-ZFEW36RG.js.map +1 -0
  46. package/dist/chunk-ZQXJ34VJ.js +34 -0
  47. package/dist/chunk-ZQXJ34VJ.js.map +1 -0
  48. package/dist/chunk-ZUCGV2XD.js +42 -0
  49. package/dist/chunk-ZUCGV2XD.js.map +1 -0
  50. package/dist/column-picker/column-picker.js +8 -0
  51. package/dist/column-picker/column-picker.js.map +1 -0
  52. package/dist/column-picker/index.js +9 -0
  53. package/dist/column-picker/index.js.map +1 -0
  54. package/dist/data-table/card-layout.js +11 -0
  55. package/dist/data-table/card-layout.js.map +1 -0
  56. package/dist/data-table/cell-scales.js +26 -0
  57. package/dist/data-table/cell-scales.js.map +1 -0
  58. package/dist/data-table/cells/bar-cell.js +14 -0
  59. package/dist/data-table/cells/bar-cell.js.map +1 -0
  60. package/dist/data-table/cells/category-legend.js +9 -0
  61. package/dist/data-table/cells/category-legend.js.map +1 -0
  62. package/dist/data-table/cells/columns-cell.js +13 -0
  63. package/dist/data-table/cells/columns-cell.js.map +1 -0
  64. package/dist/data-table/cells/heatmap-cell.js +13 -0
  65. package/dist/data-table/cells/heatmap-cell.js.map +1 -0
  66. package/dist/data-table/cells/markdown-cell.js +13 -0
  67. package/dist/data-table/cells/markdown-cell.js.map +1 -0
  68. package/dist/data-table/cells/sparkline-cell.js +13 -0
  69. package/dist/data-table/cells/sparkline-cell.js.map +1 -0
  70. package/dist/data-table/column-meta.js +12 -0
  71. package/dist/data-table/column-meta.js.map +1 -0
  72. package/dist/data-table/data-table.js +23 -0
  73. package/dist/data-table/data-table.js.map +1 -0
  74. package/dist/data-table/index.js +23 -0
  75. package/dist/data-table/index.js.map +1 -0
  76. package/dist/data-table/ranks-column.js +13 -0
  77. package/dist/data-table/ranks-column.js.map +1 -0
  78. package/dist/data-table/sticky-rows.js +10 -0
  79. package/dist/data-table/sticky-rows.js.map +1 -0
  80. package/dist/data-table/use-table-breakpoint.js +13 -0
  81. package/dist/data-table/use-table-breakpoint.js.map +1 -0
  82. package/dist/facet-filter/facet-filter.js +8 -0
  83. package/dist/facet-filter/facet-filter.js.map +1 -0
  84. package/dist/facet-filter/index.js +9 -0
  85. package/dist/facet-filter/index.js.map +1 -0
  86. package/dist/filter-bar/filter-bar.js +8 -0
  87. package/dist/filter-bar/filter-bar.js.map +1 -0
  88. package/dist/filter-bar/filter-chip.js +9 -0
  89. package/dist/filter-bar/filter-chip.js.map +1 -0
  90. package/dist/filter-bar/index.js +13 -0
  91. package/dist/filter-bar/index.js.map +1 -0
  92. package/dist/index.d.ts +554 -10
  93. package/dist/index.js +102 -1494
  94. package/dist/index.js.map +1 -1
  95. package/dist/search-input/index.js +9 -0
  96. package/dist/search-input/index.js.map +1 -0
  97. package/dist/search-input/search-input.js +9 -0
  98. package/dist/search-input/search-input.js.map +1 -0
  99. package/dist/to-csv.js +10 -0
  100. package/dist/to-csv.js.map +1 -0
  101. package/package.json +7 -7
  102. package/src/__contract__/filter-chip.contract.test.tsx +49 -0
  103. package/src/column-picker/column-picker.tsx +3 -2
  104. package/src/data-table/card-layout.test.tsx +26 -0
  105. package/src/data-table/card-layout.tsx +74 -0
  106. package/src/data-table/cell-scales.test.ts +118 -0
  107. package/src/data-table/cell-scales.ts +246 -0
  108. package/src/data-table/cells/bar-cell.test.tsx +158 -0
  109. package/src/data-table/cells/bar-cell.tsx +135 -0
  110. package/src/data-table/cells/category-legend.test.tsx +37 -0
  111. package/src/data-table/cells/category-legend.tsx +58 -0
  112. package/src/data-table/cells/columns-cell.test.tsx +27 -0
  113. package/src/data-table/cells/columns-cell.tsx +79 -0
  114. package/src/data-table/cells/heatmap-cell.test.tsx +31 -0
  115. package/src/data-table/cells/heatmap-cell.tsx +116 -0
  116. package/src/data-table/cells/markdown-cell.test.tsx +36 -0
  117. package/src/data-table/cells/markdown-cell.tsx +91 -0
  118. package/src/data-table/cells/sparkline-cell.test.tsx +43 -0
  119. package/src/data-table/cells/sparkline-cell.tsx +113 -0
  120. package/src/data-table/column-meta.test.ts +42 -0
  121. package/src/data-table/column-meta.ts +282 -0
  122. package/src/data-table/data-table.stories.tsx +748 -1
  123. package/src/data-table/data-table.test.tsx +547 -11
  124. package/src/data-table/data-table.tsx +1014 -175
  125. package/src/data-table/ranks-column.test.tsx +65 -0
  126. package/src/data-table/ranks-column.tsx +91 -0
  127. package/src/data-table/sticky-rows.test.ts +16 -0
  128. package/src/data-table/sticky-rows.ts +77 -0
  129. package/src/data-table/use-table-breakpoint.test.ts +14 -0
  130. package/src/data-table/use-table-breakpoint.ts +67 -0
  131. package/src/facet-filter/facet-filter.stories.tsx +4 -1
  132. package/src/facet-filter/facet-filter.test.tsx +3 -3
  133. package/src/filter-bar/filter-chip.stories.tsx +1 -0
  134. package/src/index.ts +64 -0
  135. package/src/search-input/search-input.test.tsx +35 -0
  136. package/src/search-input/search-input.tsx +44 -19
  137. package/src/templates-data-app.stories.tsx +7 -2
  138. package/src/to-csv.test.ts +13 -0
  139. package/src/to-csv.ts +7 -30
@@ -0,0 +1,113 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type HTMLAttributes } from "react";
4
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
5
+ import type { NumericExtent } from "../cell-scales";
6
+ import { BAR_CELL_POSITIVE_COLOR } from "./bar-cell";
7
+
8
+ /** The line's stroke width (px, unscaled by the viewBox). */
9
+ export const SPARKLINE_CELL_STROKE_WIDTH = 1.5;
10
+ const VIEW_WIDTH = 100;
11
+ const PAD = 2;
12
+
13
+ export interface SparklineCellProps extends HTMLAttributes<HTMLDivElement> {
14
+ values: readonly (number | null)[];
15
+ /** The y domain: this row's own extent (`range: "cell"`) or the column's. */
16
+ domain: NumericExtent | null;
17
+ /** Every value as text, for screen readers, sorting and copy. */
18
+ label: string;
19
+ /** Printed first / last values (with `labels: "ends"`). */
20
+ ends?: readonly [first: string, last: string];
21
+ /**
22
+ * Room reserved for those two labels, in `ch` (`labelBoxCh` over the whole
23
+ * column). The drawing takes what they leave over, so one reservation per
24
+ * column keeps every row's line on the same x scale.
25
+ */
26
+ endsWidth?: readonly [first: number, last: number];
27
+ fill?: boolean;
28
+ /** Drawing height in px. Default 24. */
29
+ height?: number;
30
+ }
31
+
32
+ /** `M`/`L` path through the finite points; a `null` breaks the line. */
33
+ function sparkPath(values: readonly (number | null)[], domain: NumericExtent, height: number) {
34
+ const span = domain.max - domain.min;
35
+ const step = values.length > 1 ? VIEW_WIDTH / (values.length - 1) : 0;
36
+ const y = (v: number) =>
37
+ span > 0 ? PAD + (1 - (v - domain.min) / span) * Math.max(0, height - 2 * PAD) : height / 2;
38
+ let d = "";
39
+ let open = false;
40
+ values.forEach((v, i) => {
41
+ if (v === null) return void (open = false);
42
+ d += `${open ? "L" : "M"}${(i * step).toFixed(2)} ${y(v).toFixed(2)}`;
43
+ open = true;
44
+ });
45
+ return d;
46
+ }
47
+
48
+ /** An in-cell sparkline: a ≤ 40-line inline SVG on a shared or own y domain. */
49
+ export const SparklineCell = forwardRef<HTMLDivElement, SparklineCellProps>(function SparklineCell(
50
+ { values, domain, label, ends, endsWidth, fill = false, height = 24, className, ...props },
51
+ ref,
52
+ ) {
53
+ const h = Math.max(0, height);
54
+ const d = domain ? sparkPath(values, domain, h) : "";
55
+ const area = fill && d ? `${d}L${VIEW_WIDTH} ${h}L0 ${h}Z` : "";
56
+ return (
57
+ <div
58
+ ref={ref}
59
+ data-slot="sparkline-cell"
60
+ className={cn("flex min-w-0 items-center gap-1.5", className)}
61
+ {...props}
62
+ >
63
+ <span className="sr-only">{label}</span>
64
+ {/*
65
+ Both end labels are column-wide reservations (`endsWidth`), never this
66
+ row's own text width, so every row's drawing gets the same width and the
67
+ lines share one x scale as well as the y scale.
68
+ */}
69
+ {ends && (
70
+ <span
71
+ aria-hidden="true"
72
+ className="shrink-0 whitespace-nowrap text-end text-meta text-muted-foreground tabular-nums"
73
+ style={endsWidth?.[0] ? { width: `${endsWidth[0]}ch` } : undefined}
74
+ >
75
+ {ends[0]}
76
+ </span>
77
+ )}
78
+ <svg
79
+ aria-hidden="true"
80
+ data-slot="sparkline-cell-svg"
81
+ data-y-min={domain?.min}
82
+ data-y-max={domain?.max}
83
+ viewBox={`0 0 ${VIEW_WIDTH} ${h}`}
84
+ preserveAspectRatio="none"
85
+ className="block min-w-8 flex-1 overflow-visible"
86
+ style={{ height: h }}
87
+ >
88
+ {area && <path d={area} fill={BAR_CELL_POSITIVE_COLOR} fillOpacity={0.2} stroke="none" />}
89
+ <path
90
+ data-slot="sparkline-cell-line"
91
+ d={d}
92
+ fill="none"
93
+ // Same mark ink as the in-cell bar: a categorical token carries no
94
+ // contrast guarantee and read as 1.42:1 on `--card` in light (b-4).
95
+ stroke={BAR_CELL_POSITIVE_COLOR}
96
+ strokeWidth={SPARKLINE_CELL_STROKE_WIDTH}
97
+ strokeLinejoin="round"
98
+ strokeLinecap="round"
99
+ vectorEffect="non-scaling-stroke"
100
+ />
101
+ </svg>
102
+ {ends && (
103
+ <span
104
+ aria-hidden="true"
105
+ className="shrink-0 whitespace-nowrap text-start text-meta text-foreground tabular-nums"
106
+ style={endsWidth?.[1] ? { width: `${endsWidth[1]}ch` } : undefined}
107
+ >
108
+ {ends[1]}
109
+ </span>
110
+ )}
111
+ </div>
112
+ );
113
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { columnSizeStyle, formatCellValue, resolveShowAt } from "./column-meta";
3
+
4
+ const fmt = (n: number, opts?: Intl.NumberFormatOptions) =>
5
+ new Intl.NumberFormat("en-US", opts).format(n);
6
+
7
+ describe("column-meta", () => {
8
+ it("formatCellValue mirrors the charts valueFormat object shape", () => {
9
+ expect(formatCellValue(1234.5, undefined, fmt)).toBe("1,234.5");
10
+ expect(formatCellValue(1234.5, { abbreviate: false, decimals: 0 }, fmt)).toBe("1,235");
11
+ expect(formatCellValue(1500, {}, fmt)).toBe("1.5K");
12
+ expect(formatCellValue(0.423, { style: "percent" }, fmt)).toBe("42.3%");
13
+ expect(formatCellValue(12, { sign: "always", suffix: " pts" }, fmt)).toBe("+12 pts");
14
+ expect(formatCellValue(-12, { sign: "parens" }, fmt)).toBe("(12)");
15
+ expect(formatCellValue(12, { decimals: 1, optionalDecimals: false }, fmt)).toBe("12.0");
16
+ expect(formatCellValue(820, { style: "currency", currency: "EUR", decimals: 0 }, fmt)).toBe(
17
+ "€820",
18
+ );
19
+ expect(formatCellValue(12345, { abbreviate: false, grouping: false }, fmt)).toBe("12345");
20
+ expect(formatCellValue("text", { decimals: 2 }, fmt)).toBe("text");
21
+ expect(formatCellValue(null, undefined, fmt)).toBe("");
22
+ });
23
+
24
+ it("resolveShowAt: base is the wide value, narrow overrides below 450 px", () => {
25
+ expect(resolveShowAt(undefined, "narrow")).toBe(true);
26
+ expect(resolveShowAt(false, "wide")).toBe(false);
27
+ expect(resolveShowAt({ base: true, narrow: false }, "wide")).toBe(true);
28
+ expect(resolveShowAt({ base: true, narrow: false }, "narrow")).toBe(false);
29
+ expect(resolveShowAt({ base: false }, "narrow")).toBe(false);
30
+ });
31
+
32
+ it("columnSizeStyle: width %, min width px, style last; nothing for a plain column", () => {
33
+ expect(columnSizeStyle(undefined)).toBeUndefined();
34
+ expect(columnSizeStyle({ numeric: true })).toBeUndefined();
35
+ expect(columnSizeStyle({ width: 30, minWidth: 80, style: { fontStyle: "italic" } })).toEqual({
36
+ width: "30%",
37
+ minWidth: 80,
38
+ fontStyle: "italic",
39
+ });
40
+ expect(columnSizeStyle({ width: -4 })).toEqual({ width: "0%" });
41
+ });
42
+ });
@@ -0,0 +1,282 @@
1
+ import type { CSSProperties } from "react";
2
+ import type { RowData } from "@tanstack/react-table";
3
+ import type { ColorScaleDomain } from "@elabs-ai/components-ui";
4
+ import type { DataTableBreakpoint } from "./use-table-breakpoint";
5
+
6
+ /**
7
+ * column-meta.ts — the typed `columnDef.meta` contract `DataTable` reads.
8
+ *
9
+ * `numeric` / `align` (#69) style a column; everything else is the editorial
10
+ * presentation layer (RM-123): an in-cell visual, a number format, categorical
11
+ * colouring, per-breakpoint visibility and column sizing. Every key is optional
12
+ * and a column without them renders exactly as before.
13
+ *
14
+ * `data` never imports `@elabs-ai/components-charts` (one-way dependency
15
+ * graph), so the number-format shape below is a STRUCTURAL twin of charts'
16
+ * `ChartValueFormatSpec` (RM-109): a spec written for a chart type-checks here
17
+ * unchanged, and formats the same way.
18
+ */
19
+
20
+ /**
21
+ * A number format — the same object shape as charts' `ChartValueFormatSpec`.
22
+ *
23
+ * - `style`: `"number"` (default), `"currency"`, or `"percent"` (fraction in,
24
+ * `%` out).
25
+ * - `decimals`: maximum fraction digits. `optionalDecimals: false` makes it
26
+ * the minimum too.
27
+ * - `abbreviate`: `true` always compact (`1.5K`), `false` never, `"auto"`
28
+ * (default) from 1,000 up. Percent is never compacted.
29
+ * - `sign`: `"auto"` (minus only), `"always"` (`+`/`-`), `"parens"` (`(12)`).
30
+ * - `prefix` / `suffix`: literal text around the number.
31
+ * - `grouping: false` drops the thousands separator.
32
+ * - `currency`: ISO 4217 code for `style: "currency"` (default `"USD"`).
33
+ */
34
+ export interface DataTableValueFormatSpec {
35
+ style?: "number" | "currency" | "percent";
36
+ decimals?: number;
37
+ optionalDecimals?: boolean;
38
+ abbreviate?: boolean | "auto";
39
+ sign?: "auto" | "always" | "parens";
40
+ prefix?: string;
41
+ suffix?: string;
42
+ grouping?: boolean;
43
+ currency?: string;
44
+ }
45
+
46
+ /** An in-cell bar: the value as a horizontal bar from zero. */
47
+ export interface DataTableBarVisual {
48
+ kind: "bar";
49
+ /** `"regular"` (default) sits beside the value; `"slim"` is a thin rule under it. */
50
+ style?: "regular" | "slim";
51
+ /** Paint the grey remainder of the range behind the bar. */
52
+ track?: boolean;
53
+ /**
54
+ * The range a full-width bar spans. `"column"` (default): this column's own
55
+ * min / max; `"table"`: shared by every bar column with `range: "table"`;
56
+ * `[min, max]`: fixed. Zero is always inside the range.
57
+ */
58
+ range?: readonly [min: number, max: number] | "column" | "table";
59
+ /** Row key whose category picks the bar colour (`--chart-1…12`). */
60
+ colorBy?: string;
61
+ /**
62
+ * The `colorBy` key's title, or `false` to draw no key. Default `true`: a
63
+ * category carried by hue alone is unreadable without one (WCAG 1.4.1), so
64
+ * the key is opt-OUT — turn it off only when the table prints the category
65
+ * in a column of its own.
66
+ */
67
+ legend?: string | boolean;
68
+ /** Paint negative values in the negative token. Default `true`. */
69
+ negative?: boolean;
70
+ }
71
+
72
+ /** An in-cell sparkline across several numeric row keys (one point per key). */
73
+ export interface DataTableSparklineVisual {
74
+ kind: "sparkline";
75
+ /** Row keys, in x order. */
76
+ keys: readonly string[];
77
+ /** Fill the area under the line. */
78
+ fill?: boolean;
79
+ /** `"ends"` prints the first and last values beside the line. Default `"none"`. */
80
+ labels?: "ends" | "none";
81
+ /** Drawing height in px. Default 24. */
82
+ height?: number;
83
+ /** `"cell"` (default): this row's own min / max; `"column"`: shared by every row. */
84
+ range?: "cell" | "column";
85
+ }
86
+
87
+ /** In-cell mini columns across several numeric row keys. */
88
+ export interface DataTableColumnsVisual {
89
+ kind: "columns";
90
+ keys: readonly string[];
91
+ /** Drawing height in px. Default 24. */
92
+ height?: number;
93
+ /** `"cell"` (default): this row's own max; `"column"`: shared by every row. */
94
+ range?: "cell" | "column";
95
+ }
96
+
97
+ /**
98
+ * A heatmap colour scale. Resolved by `colorScaleFor` (`@elabs-ai/components-ui`),
99
+ * so the colours are `--chart-seq-*` / `--chart-div-*` ramp tokens.
100
+ */
101
+ export interface DataTableHeatmapScale {
102
+ /** `"stepped"` cuts countable classes; `"continuous"` snaps to the nearest ramp step. */
103
+ type: "stepped" | "continuous";
104
+ /** How many classes (stepped) or Jenks classes (continuous `jenks`). Default 5. */
105
+ steps?: number;
106
+ /**
107
+ * `"linear"` (default) equal widths; `"quantile"` equal counts; `"jenks"`
108
+ * natural breaks; `"custom"` the `breaks` you pass (always stepped).
109
+ */
110
+ method?: "linear" | "quantile" | "jenks" | "custom";
111
+ /** `method: "custom"` only: the inner thresholds, ascending. */
112
+ breaks?: readonly number[];
113
+ /** `[min, max]`, or `[min, center, max]` to pin the ramp's middle colour. */
114
+ domain?: ColorScaleDomain;
115
+ /** Default `"diverging"` when `domain` has a centre, else `"sequential"`. */
116
+ palette?: "sequential" | "diverging";
117
+ }
118
+
119
+ /** The cell's background is the value's ramp colour. */
120
+ export interface DataTableHeatmapVisual {
121
+ kind: "heatmap";
122
+ scale: DataTableHeatmapScale;
123
+ /** Hide the printed value visually; screen readers, sorting and copy still read it. */
124
+ hideValue?: boolean;
125
+ /**
126
+ * Print the scale's colour key above the table. `true` titles it with the
127
+ * column header; a string is the title (use it when one heatmap spans several
128
+ * columns — the key is printed once per shared scale).
129
+ */
130
+ legend?: boolean | string;
131
+ }
132
+
133
+ export type DataTableCellVisual =
134
+ | DataTableBarVisual
135
+ | DataTableSparklineVisual
136
+ | DataTableColumnsVisual
137
+ | DataTableHeatmapVisual;
138
+
139
+ /**
140
+ * Categorical conditional formatting: the category in row key `key` picks a
141
+ * `--chart-1…12` colour, painted as a background wash or as text ink, on this
142
+ * column's cell or on the whole row.
143
+ */
144
+ export interface DataTableColorBy {
145
+ key: string;
146
+ target: "background" | "text";
147
+ /** Default `"cell"`. */
148
+ scope?: "cell" | "row";
149
+ /**
150
+ * The colour key's title, or `false` to draw none. Default `true` — see
151
+ * `DataTableBarVisual.legend`.
152
+ */
153
+ legend?: string | boolean;
154
+ }
155
+
156
+ /**
157
+ * Show a column at a table breakpoint. `true` / `false` for every width, or
158
+ * `{ base, narrow }`: `base` is the wide value, `narrow` applies below 450 px.
159
+ */
160
+ export type DataTableShowAt = boolean | { base: boolean; narrow?: boolean };
161
+
162
+ /** `markdown: true` or options. Images are off unless `images: true`. */
163
+ export interface DataTableMarkdownOptions {
164
+ images?: boolean;
165
+ }
166
+
167
+ /**
168
+ * `DataTable`'s `columnDef.meta` contract, read by the header, body, card and
169
+ * skeleton cell renderers. Set `numeric: true` on a column to get
170
+ * `tabular-nums` + end-alignment on both the `<th>` and every `<td>`
171
+ * (including the loading skeleton) for free.
172
+ */
173
+ export interface DataTableColumnMeta {
174
+ /** Numeric column: tabular figures + end alignment on header and cells. */
175
+ numeric?: boolean;
176
+ /**
177
+ * Explicit alignment override for when `numeric` isn't the right cue (or
178
+ * to align a non-numeric column). Independent of `numeric` — `numeric`
179
+ * alone still drives `tabular-nums` even when `align` overrides the
180
+ * alignment away from `"end"`.
181
+ */
182
+ align?: "start" | "center" | "end";
183
+ /**
184
+ * An in-cell visual (bar, sparkline, mini columns, heatmap). The cell keeps
185
+ * its value as text — visually hidden only with `hideValue` — so sorting,
186
+ * copy and screen readers still read the number. Replaces the column's
187
+ * `cell` renderer.
188
+ */
189
+ visual?: DataTableCellVisual;
190
+ /** Number format for the printed value (the charts `valueFormat` object shape). */
191
+ format?: DataTableValueFormatSpec;
192
+ /** Categorical colouring driven by another row key. */
193
+ colorBy?: DataTableColorBy;
194
+ /** Per-breakpoint visibility; render-only, never written to `columnVisibility`. */
195
+ showAt?: DataTableShowAt;
196
+ /** Column width as a percentage of the table width. */
197
+ width?: number;
198
+ /** Minimum column width in px. */
199
+ minWidth?: number;
200
+ /** Extra inline style on this column's header and body cells. */
201
+ style?: CSSProperties;
202
+ /** Render a string value as safe inline markdown (bold, italic, links, `sup`, `code`). */
203
+ markdown?: boolean | DataTableMarkdownOptions;
204
+ }
205
+
206
+ declare module "@tanstack/react-table" {
207
+ // `TData`/`TValue` must stay in the signature to match the interface being
208
+ // augmented, even though `DataTableColumnMeta` (deliberately) doesn't use
209
+ // them; the empty extends-body is how TanStack's own module-augmentation
210
+ // pattern for `ColumnMeta` is documented.
211
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-object-type
212
+ interface ColumnMeta<TData extends RowData, TValue> extends DataTableColumnMeta {}
213
+ }
214
+
215
+ /** Charts' compaction threshold (`COMPACT_THRESHOLD`), mirrored. */
216
+ const COMPACT_THRESHOLD = 1000;
217
+
218
+ /**
219
+ * The printed text for a cell value. Numbers go through `format` (or the
220
+ * locale's plain number format); anything else is stringified; nullish is `""`.
221
+ */
222
+ export function formatCellValue(
223
+ value: unknown,
224
+ format: DataTableValueFormatSpec | undefined,
225
+ formatNumber: (n: number, opts?: Intl.NumberFormatOptions) => string,
226
+ ): string {
227
+ if (value === null || value === undefined) return "";
228
+ if (typeof value !== "number") return String(value);
229
+ if (!Number.isFinite(value)) return "";
230
+ if (!format) return formatNumber(value);
231
+ const style = format.style ?? "number";
232
+ const options: Intl.NumberFormatOptions = {};
233
+ if (style === "currency") {
234
+ options.style = "currency";
235
+ options.currency = format.currency ?? "USD";
236
+ } else if (style === "percent") {
237
+ options.style = "percent";
238
+ }
239
+ const abbreviate =
240
+ style !== "percent" &&
241
+ (format.abbreviate === true ||
242
+ (format.abbreviate !== false && Math.abs(value) >= COMPACT_THRESHOLD));
243
+ if (abbreviate) {
244
+ options.notation = "compact";
245
+ options.compactDisplay = "short";
246
+ }
247
+ const exact = style === "number" && format.abbreviate === false;
248
+ const digits = format.decimals ?? (exact ? undefined : 1);
249
+ if (digits !== undefined) {
250
+ options.maximumFractionDigits = digits;
251
+ if (format.optionalDecimals === false) options.minimumFractionDigits = digits;
252
+ }
253
+ if (format.grouping === false) options.useGrouping = false;
254
+ const parens = format.sign === "parens";
255
+ if (format.sign === "always") options.signDisplay = "always";
256
+ if (parens) options.signDisplay = "never";
257
+ const body = formatNumber(value, options);
258
+ const signed = parens && value < 0 ? `(${body})` : body;
259
+ return `${format.prefix ?? ""}${signed}${format.suffix ?? ""}`;
260
+ }
261
+
262
+ /** Whether a column is shown at `breakpoint` (no `showAt` → always). */
263
+ export function resolveShowAt(
264
+ showAt: DataTableShowAt | undefined,
265
+ breakpoint: DataTableBreakpoint,
266
+ ): boolean {
267
+ if (showAt === undefined) return true;
268
+ if (typeof showAt === "boolean") return showAt;
269
+ return breakpoint === "narrow" ? (showAt.narrow ?? showAt.base) : showAt.base;
270
+ }
271
+
272
+ /** The inline style for `meta.width` (%), `meta.minWidth` (px) and `meta.style`. */
273
+ export function columnSizeStyle(meta: DataTableColumnMeta | undefined): CSSProperties | undefined {
274
+ if (!meta || (meta.width === undefined && meta.minWidth === undefined && !meta.style)) {
275
+ return undefined;
276
+ }
277
+ return {
278
+ ...(meta.width !== undefined ? { width: `${Math.max(0, meta.width)}%` } : null),
279
+ ...(meta.minWidth !== undefined ? { minWidth: Math.max(0, meta.minWidth) } : null),
280
+ ...meta.style,
281
+ };
282
+ }