@elabs-ai/components-data 4.2.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 (131) 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 +547 -8
  93. package/dist/index.js +102 -1512
  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/data-table/card-layout.test.tsx +26 -0
  103. package/src/data-table/card-layout.tsx +74 -0
  104. package/src/data-table/cell-scales.test.ts +118 -0
  105. package/src/data-table/cell-scales.ts +246 -0
  106. package/src/data-table/cells/bar-cell.test.tsx +158 -0
  107. package/src/data-table/cells/bar-cell.tsx +135 -0
  108. package/src/data-table/cells/category-legend.test.tsx +37 -0
  109. package/src/data-table/cells/category-legend.tsx +58 -0
  110. package/src/data-table/cells/columns-cell.test.tsx +27 -0
  111. package/src/data-table/cells/columns-cell.tsx +79 -0
  112. package/src/data-table/cells/heatmap-cell.test.tsx +31 -0
  113. package/src/data-table/cells/heatmap-cell.tsx +116 -0
  114. package/src/data-table/cells/markdown-cell.test.tsx +36 -0
  115. package/src/data-table/cells/markdown-cell.tsx +91 -0
  116. package/src/data-table/cells/sparkline-cell.test.tsx +43 -0
  117. package/src/data-table/cells/sparkline-cell.tsx +113 -0
  118. package/src/data-table/column-meta.test.ts +42 -0
  119. package/src/data-table/column-meta.ts +282 -0
  120. package/src/data-table/data-table.stories.tsx +733 -1
  121. package/src/data-table/data-table.test.tsx +486 -0
  122. package/src/data-table/data-table.tsx +965 -165
  123. package/src/data-table/ranks-column.test.tsx +65 -0
  124. package/src/data-table/ranks-column.tsx +91 -0
  125. package/src/data-table/sticky-rows.test.ts +16 -0
  126. package/src/data-table/sticky-rows.ts +77 -0
  127. package/src/data-table/use-table-breakpoint.test.ts +14 -0
  128. package/src/data-table/use-table-breakpoint.ts +67 -0
  129. package/src/filter-bar/filter-chip.stories.tsx +1 -0
  130. package/src/index.ts +64 -0
  131. package/src/templates-data-app.stories.tsx +7 -2
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { DataTableRankCell, DataTableRankHeader, computeRowRanks } from "./ranks-column";
4
+
5
+ describe("ranks-column", () => {
6
+ it("computeRowRanks numbers rows 1…n in data order, skipping excluded (sticky) rows", () => {
7
+ const ranks = computeRowRanks(["a", "avg", "b", "c"], new Set(["avg"]));
8
+ expect([...ranks.entries()]).toEqual([
9
+ ["a", 1],
10
+ ["b", 2],
11
+ ["c", 3],
12
+ ]);
13
+ });
14
+ it("renders a column header and a rank cell", () => {
15
+ render(
16
+ <table>
17
+ <thead>
18
+ <tr>
19
+ <DataTableRankHeader />
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ <tr>
24
+ <DataTableRankCell rank="3" />
25
+ </tr>
26
+ </tbody>
27
+ </table>,
28
+ );
29
+ expect(screen.getByRole("columnheader")).toHaveTextContent("#");
30
+ expect(screen.getByRole("cell")).toHaveTextContent("3");
31
+ });
32
+ /**
33
+ * b-5 — the printed number is the row's position in the DATA, which is why
34
+ * it reads 2, 1, 6, 4 beside a sorted column. A header whose accessible name
35
+ * is the single character "#" never says so, so the rule is unlearnable:
36
+ * the name has to BE the rule. Exact string, per the conventions' "never a
37
+ * name regex" note.
38
+ */
39
+ it("names the column by what the number means, not by the glyph", () => {
40
+ render(
41
+ <table>
42
+ <thead>
43
+ <tr>
44
+ <DataTableRankHeader />
45
+ </tr>
46
+ </thead>
47
+ </table>,
48
+ );
49
+ const header = screen.getByRole("columnheader");
50
+ expect(header).toHaveAccessibleName("Position in the data as supplied");
51
+ expect(header).toHaveAttribute("title", "Position in the data as supplied");
52
+ });
53
+ it("takes a caller's own name for the column", () => {
54
+ render(
55
+ <table>
56
+ <thead>
57
+ <tr>
58
+ <DataTableRankHeader label="Rank in the 2024 census" />
59
+ </tr>
60
+ </thead>
61
+ </table>,
62
+ );
63
+ expect(screen.getByRole("columnheader")).toHaveAccessibleName("Rank in the 2024 census");
64
+ });
65
+ });
@@ -0,0 +1,91 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type TdHTMLAttributes, type ThHTMLAttributes } from "react";
4
+ import { useLocale } from "@elabs-ai/components-ui";
5
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
6
+
7
+ /**
8
+ * ranks-column.tsx — `DataTable`'s `showRanks` column (RM-123).
9
+ *
10
+ * A rank belongs to the ROW: it is the row's 1-based position in the `data`
11
+ * you passed (sticky rows excluded), so re-sorting, filtering or paging moves
12
+ * the numbers with their rows and never renumbers them. It is rendered beside
13
+ * the TanStack columns, not as one, so the table model is unchanged.
14
+ */
15
+
16
+ /** Row id → rank (1…n in data order), skipping the ids in `exclude`. */
17
+ export function computeRowRanks(
18
+ rowIds: readonly string[],
19
+ exclude: ReadonlySet<string> = new Set(),
20
+ ): Map<string, number> {
21
+ const ranks = new Map<string, number>();
22
+ let rank = 0;
23
+ for (const id of rowIds) {
24
+ if (!exclude.has(id)) ranks.set(id, ++rank);
25
+ }
26
+ return ranks;
27
+ }
28
+
29
+ export interface DataTableRankHeaderProps extends ThHTMLAttributes<HTMLTableCellElement> {
30
+ /**
31
+ * The column's accessible NAME, and its `title`. Defaults to the locale
32
+ * seam's `data.table.rankHeader`.
33
+ *
34
+ * The visible glyph stays "#" — it has to, in a 40px column — so the glyph
35
+ * is decorative (`aria-hidden`) and this string is what the column is
36
+ * actually called. Without it the header's accessible name is the single
37
+ * character "#", which says nothing about WHICH position is printed: beside
38
+ * a sorted column the numbers legitimately read 2, 1, 6, 4 and a reader has
39
+ * no way to learn why. `DataTable` prints the same rule visibly, once, above
40
+ * the table (`data-slot="data-table-rank-key"`).
41
+ */
42
+ label?: string;
43
+ }
44
+
45
+ /**
46
+ * The ranks column's header cell: "#" as the visible glyph, `label` as the
47
+ * name AT reads.
48
+ */
49
+ export const DataTableRankHeader = forwardRef<HTMLTableCellElement, DataTableRankHeaderProps>(
50
+ function DataTableRankHeader({ className, children, label, ...props }, ref) {
51
+ const { t } = useLocale();
52
+ const name = label ?? t("data.table.rankHeader");
53
+ return (
54
+ <th
55
+ ref={ref}
56
+ scope="col"
57
+ title={name}
58
+ data-slot="data-table-rank-header"
59
+ className={cn("w-10 px-3 text-end align-middle tabular-nums", className)}
60
+ {...props}
61
+ >
62
+ {children ?? (
63
+ <>
64
+ <span aria-hidden="true">#</span>
65
+ <span className="sr-only">{name}</span>
66
+ </>
67
+ )}
68
+ </th>
69
+ );
70
+ },
71
+ );
72
+
73
+ /** One row's rank cell; `rank` undefined (a sticky row) renders empty. */
74
+ export const DataTableRankCell = forwardRef<
75
+ HTMLTableCellElement,
76
+ TdHTMLAttributes<HTMLTableCellElement> & { rank?: string }
77
+ >(function DataTableRankCell({ rank, className, ...props }, ref) {
78
+ return (
79
+ <td
80
+ ref={ref}
81
+ data-slot="data-table-rank-cell"
82
+ className={cn(
83
+ "w-10 px-3 text-end align-middle tabular-nums text-muted-foreground",
84
+ className,
85
+ )}
86
+ {...props}
87
+ >
88
+ {rank}
89
+ </td>
90
+ );
91
+ });
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { stickyRowPinning } from "./sticky-rows";
3
+
4
+ describe("stickyRowPinning", () => {
5
+ const data = [{ n: "a" }, { n: "avg" }, { n: "b" }, { n: "total" }];
6
+ it("maps records to TanStack row ids (index, or getRowId) in data order", () => {
7
+ const sticky = (r: { n: string }) =>
8
+ r.n === "avg" ? "top" : r.n === "total" ? "bottom" : undefined;
9
+ expect(stickyRowPinning(data, sticky)).toEqual({ top: ["1"], bottom: ["3"] });
10
+ expect(stickyRowPinning(data, sticky, (r) => r.n)).toEqual({ top: ["avg"], bottom: ["total"] });
11
+ });
12
+ it("is empty without a predicate or a match", () => {
13
+ expect(stickyRowPinning(data, undefined)).toEqual({ top: [], bottom: [] });
14
+ expect(stickyRowPinning(data, () => undefined)).toEqual({ top: [], bottom: [] });
15
+ });
16
+ });
@@ -0,0 +1,77 @@
1
+ import type { RowModel, RowPinningState, Table } from "@tanstack/react-table";
2
+
3
+ /**
4
+ * sticky-rows.ts — `DataTable`'s `stickyRows` (RM-123): rows such as an
5
+ * "average" or "total" that stay at the top or bottom of EVERY page and after
6
+ * every sort.
7
+ *
8
+ * Built on TanStack's own row pinning (`keepPinnedRows`), so the table model is
9
+ * unchanged: a sticky row keeps its id, its selection and its `data` index; it
10
+ * is only rendered outside the sorted / paged centre rows.
11
+ */
12
+
13
+ export type DataTableStickyRowPosition = "top" | "bottom";
14
+
15
+ /** Decides, per record, whether its row sticks to the top or the bottom (or neither). */
16
+ export type DataTableStickyRows<TData> = (
17
+ row: TData,
18
+ index: number,
19
+ ) => DataTableStickyRowPosition | undefined;
20
+
21
+ const EMPTY: RowPinningState = { top: [], bottom: [] };
22
+
23
+ /**
24
+ * The TanStack `rowPinning` state for `stickyRows` over `data`, in data order.
25
+ * Row ids follow TanStack's own rule: `getRowId(row, index)`, else the index.
26
+ */
27
+ export function stickyRowPinning<TData>(
28
+ data: readonly TData[],
29
+ stickyRows: DataTableStickyRows<TData> | undefined,
30
+ getRowId?: (row: TData, index: number) => string,
31
+ ): RowPinningState {
32
+ if (!stickyRows) return EMPTY;
33
+ const top: string[] = [];
34
+ const bottom: string[] = [];
35
+ data.forEach((row, index) => {
36
+ const position = stickyRows(row, index);
37
+ if (!position) return;
38
+ const id = getRowId ? getRowId(row, index) : String(index);
39
+ (position === "top" ? top : bottom).push(id);
40
+ });
41
+ return top.length === 0 && bottom.length === 0 ? EMPTY : { top, bottom };
42
+ }
43
+
44
+ /**
45
+ * Wraps a filtered-row-model factory so sticky (pinned) rows leave the flow the
46
+ * table sorts and pages: every page holds `pageSize` ordinary rows and the page
47
+ * count ignores the sticky ones, which `keepPinnedRows` still renders at the
48
+ * top / bottom of every page. Sticky rows are also exempt from search and
49
+ * filters — an "average" row describes the whole table, not the matches.
50
+ */
51
+ export function withoutStickyRows<TData>(
52
+ factory: (table: Table<TData>) => () => RowModel<TData>,
53
+ ): (table: Table<TData>) => () => RowModel<TData> {
54
+ return (table) => {
55
+ const compute = factory(table);
56
+ let lastModel: RowModel<TData> | undefined;
57
+ let lastPinning: RowPinningState | undefined;
58
+ let lastResult: RowModel<TData> | undefined;
59
+ return () => {
60
+ const model = compute();
61
+ const pinning = table.getState().rowPinning;
62
+ if (model === lastModel && pinning === lastPinning && lastResult) return lastResult;
63
+ lastModel = model;
64
+ lastPinning = pinning;
65
+ const sticky = new Set([...(pinning?.top ?? []), ...(pinning?.bottom ?? [])]);
66
+ if (sticky.size === 0) {
67
+ lastResult = model;
68
+ return model;
69
+ }
70
+ const rows = model.rows.filter((row) => !sticky.has(row.id));
71
+ const flatRows = model.flatRows.filter((row) => !sticky.has(row.id));
72
+ const rowsById = Object.fromEntries(flatRows.map((row) => [row.id, row]));
73
+ lastResult = { rows, flatRows, rowsById };
74
+ return lastResult;
75
+ };
76
+ };
77
+ }
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { DATA_TABLE_BREAKPOINT_THRESHOLDS, tableBreakpointForWidth } from "./use-table-breakpoint";
3
+
4
+ describe("tableBreakpointForWidth", () => {
5
+ it("is narrow below 450 px; the boundary and unmeasured widths are wide", () => {
6
+ expect(DATA_TABLE_BREAKPOINT_THRESHOLDS.narrow).toBe(450);
7
+ expect(tableBreakpointForWidth(348)).toBe("narrow");
8
+ expect(tableBreakpointForWidth(449.5)).toBe("narrow");
9
+ expect(tableBreakpointForWidth(450)).toBe("wide");
10
+ expect(tableBreakpointForWidth(640)).toBe("wide");
11
+ expect(tableBreakpointForWidth(0)).toBe("wide");
12
+ expect(tableBreakpointForWidth(Number.NaN)).toBe("wide");
13
+ });
14
+ });
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ /**
4
+ * use-table-breakpoint.ts — `DataTable`'s own container-measured breakpoint.
5
+ *
6
+ * A DELIBERATE COPY of `charts`' `useMeasuredChartBreakpoint`
7
+ * (`packages/charts/src/charts/chart-breakpoint.ts`, ADR 0039): `data` may not
8
+ * import `charts` (one-way dependency graph), and a table's threshold is its
9
+ * own. Keep the two in step when either changes; `.claude/rules/data.md`
10
+ * carries the same table.
11
+ *
12
+ * | Helper | Package | Tiers | Thresholds (container px) |
13
+ * | --------------------------- | ------- | ------------------------- | ------------------------- |
14
+ * | `useMeasuredChartBreakpoint` | charts | narrow / medium / wide | narrow < 480, medium < 768 |
15
+ * | `useTableBreakpoint` | data | narrow / wide | narrow < 450 |
16
+ *
17
+ * Measured on the table's OWN width, never the viewport. A width of `0` (not
18
+ * measured yet: server render, first pass, `display: none`) is `wide`, so a
19
+ * first paint matches the pre-breakpoint table. A boundary belongs to the
20
+ * wider tier (450 → wide).
21
+ */
22
+
23
+ import { useCallback, useLayoutEffect, useState } from "react";
24
+
25
+ /** A table container's width tier. */
26
+ export type DataTableBreakpoint = "narrow" | "wide";
27
+
28
+ /** A width (CSS px) below `narrow` is narrow. */
29
+ export const DATA_TABLE_BREAKPOINT_THRESHOLDS = { narrow: 450 } as const;
30
+
31
+ /** The tier for a measured container width. */
32
+ export function tableBreakpointForWidth(width: number): DataTableBreakpoint {
33
+ if (!Number.isFinite(width) || width <= 0) return "wide";
34
+ return width < DATA_TABLE_BREAKPOINT_THRESHOLDS.narrow ? "narrow" : "wide";
35
+ }
36
+
37
+ /**
38
+ * Measures the element the returned `ref` is attached to with a
39
+ * `ResizeObserver` and returns its tier. Re-renders only when the TIER changes.
40
+ * `enabled: false` skips the observer entirely (a table that uses no
41
+ * breakpoint behaviour pays nothing).
42
+ */
43
+ export function useTableBreakpoint<E extends Element = HTMLDivElement>(
44
+ enabled = true,
45
+ ): { ref: (node: E | null) => void; breakpoint: DataTableBreakpoint } {
46
+ const [node, setNode] = useState<E | null>(null);
47
+ const [breakpoint, setBreakpoint] = useState<DataTableBreakpoint>("wide");
48
+ const ref = useCallback((next: E | null) => setNode(next), []);
49
+
50
+ useLayoutEffect(() => {
51
+ if (!enabled || !node) return undefined;
52
+ const update = (width: number) => {
53
+ const next = tableBreakpointForWidth(width);
54
+ setBreakpoint((prev) => (prev === next ? prev : next));
55
+ };
56
+ update(node.getBoundingClientRect().width);
57
+ if (typeof ResizeObserver === "undefined") return undefined;
58
+ const observer = new ResizeObserver((entries) => {
59
+ const entry = entries[entries.length - 1];
60
+ if (entry) update(entry.contentRect.width);
61
+ });
62
+ observer.observe(node);
63
+ return () => observer.disconnect();
64
+ }, [enabled, node]);
65
+
66
+ return { ref, breakpoint: enabled ? breakpoint : "wide" };
67
+ }
@@ -137,6 +137,7 @@ export const LongLabelWithCount: Story = {
137
137
  };
138
138
 
139
139
  export const WithCountHighDecoration: Story = {
140
+ tags: ["!dev"],
140
141
  name: "With count — high decoration",
141
142
  globals: { decoration: "10" },
142
143
  args: { count: 1204, countLabel: "excluded" },
package/src/index.ts CHANGED
@@ -25,3 +25,67 @@ export type {
25
25
 
26
26
  // CSV helpers — pure, dependency-free serializer + browser download trigger.
27
27
  export * from "./to-csv";
28
+
29
+ // DataTable presentation layer — RM-123: typed column meta (visuals, format,
30
+ // colorBy, showAt, sizing, markdown), the in-cell visuals, the shared cell
31
+ // scales, the table breakpoint, card layout, sticky rows and ranks.
32
+ export {
33
+ columnSizeStyle,
34
+ formatCellValue,
35
+ resolveShowAt,
36
+ type DataTableBarVisual,
37
+ type DataTableCellVisual,
38
+ type DataTableColorBy,
39
+ type DataTableColumnsVisual,
40
+ type DataTableHeatmapScale,
41
+ type DataTableHeatmapVisual,
42
+ type DataTableMarkdownOptions,
43
+ type DataTableShowAt,
44
+ type DataTableSparklineVisual,
45
+ type DataTableValueFormatSpec,
46
+ } from "./data-table/column-meta";
47
+ export {
48
+ barDomain,
49
+ barGeometry,
50
+ computeColumnScales,
51
+ extentOf,
52
+ heatmapColorScaleSpec,
53
+ seriesValues,
54
+ type DataTableColumnScale,
55
+ type NumericExtent,
56
+ } from "./data-table/cell-scales";
57
+ export {
58
+ DATA_TABLE_BREAKPOINT_THRESHOLDS,
59
+ tableBreakpointForWidth,
60
+ useTableBreakpoint,
61
+ type DataTableBreakpoint,
62
+ } from "./data-table/use-table-breakpoint";
63
+ export {
64
+ stickyRowPinning,
65
+ type DataTableStickyRowPosition,
66
+ type DataTableStickyRows,
67
+ } from "./data-table/sticky-rows";
68
+ export { DataTableRankCell, DataTableRankHeader, computeRowRanks } from "./data-table/ranks-column";
69
+ export {
70
+ DataTableCard,
71
+ DataTableCardList,
72
+ type DataTableCardField,
73
+ type DataTableCardProps,
74
+ } from "./data-table/card-layout";
75
+ export { BarCell, type BarCellProps } from "./data-table/cells/bar-cell";
76
+ export { CategoryLegend, type CategoryLegendProps } from "./data-table/cells/category-legend";
77
+ export { SparklineCell, type SparklineCellProps } from "./data-table/cells/sparkline-cell";
78
+ export { ColumnsCell, type ColumnsCellProps } from "./data-table/cells/columns-cell";
79
+ export {
80
+ HeatmapCell,
81
+ HeatmapLegend,
82
+ heatmapCellStyle,
83
+ type HeatmapCellProps,
84
+ type HeatmapLegendProps,
85
+ } from "./data-table/cells/heatmap-cell";
86
+ export {
87
+ MarkdownCell,
88
+ isSafeCellUrl,
89
+ parseCellMarkdown,
90
+ type MarkdownCellProps,
91
+ } from "./data-table/cells/markdown-cell";
@@ -9,8 +9,10 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
9
9
  import { useState } from "react";
10
10
  import {
11
11
  Badge,
12
+ NavUser,
12
13
  Sidebar,
13
14
  SidebarContent,
15
+ SidebarFooter,
14
16
  SidebarGroup,
15
17
  SidebarGroupContent,
16
18
  SidebarHeader,
@@ -108,9 +110,12 @@ function DataAppTemplate({ loading = false }: { loading?: boolean }) {
108
110
  </SidebarGroupContent>
109
111
  </SidebarGroup>
110
112
  </SidebarContent>
113
+ <SidebarFooter>
114
+ <NavUser user={{ name: "Avery Rao", email: "avery@example.com" }} />
115
+ </SidebarFooter>
111
116
  </Sidebar>
112
117
  <SidebarInset>
113
- <header className="flex h-14 items-center gap-2 border-b px-4">
118
+ <header className="flex h-header items-center gap-2 border-b px-4">
114
119
  <SidebarTrigger />
115
120
  <h1 className="text-body font-medium capitalize">{active}</h1>
116
121
  </header>
@@ -146,7 +151,7 @@ function DataAppTemplate({ loading = false }: { loading?: boolean }) {
146
151
  }
147
152
 
148
153
  const meta = {
149
- title: "Patterns/Templates/Data App",
154
+ title: "Patterns/Templates/Starters/Data App",
150
155
  parameters: { layout: "fullscreen" },
151
156
  tags: ["autodocs"],
152
157
  } satisfies Meta;