From 16c2c1d0091f63393ea750593071268d2de7a444 Mon Sep 17 00:00:00 2001 From: Visal In Date: Fri, 20 Mar 2026 10:43:09 +0000 Subject: [PATCH 1/4] feat(table): add Table.SortIcon and TanStack Table sortable/resizable demo Add Table.SortIcon sub-component with direction prop (asc/desc/none), fix Table.ResizeHandle to prevent click propagation after drag, and add TanStack Table integration demo with sortable columns and column resizing. --- .changeset/table-sort-icon.md | 5 + packages/kumo-docs-astro/package.json | 1 + .../src/components/demos/TableDemo.tsx | 215 ++++++++++++++++++ .../src/pages/components/table.mdx | 197 ++++++++++++---- packages/kumo/ai/component-registry.json | 27 ++- packages/kumo/ai/component-registry.md | 118 +++++++++- packages/kumo/src/components/table/index.ts | 1 + packages/kumo/src/components/table/table.tsx | 92 +++++++- pnpm-lock.yaml | 34 ++- 9 files changed, 623 insertions(+), 67 deletions(-) create mode 100644 .changeset/table-sort-icon.md diff --git a/.changeset/table-sort-icon.md b/.changeset/table-sort-icon.md new file mode 100644 index 0000000000..0b81b3eb00 --- /dev/null +++ b/.changeset/table-sort-icon.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/kumo": minor +--- + +Add `Table.SortIcon` sub-component with `direction="asc" | "desc" | "none"` prop. diff --git a/packages/kumo-docs-astro/package.json b/packages/kumo-docs-astro/package.json index 43ce487fc2..f7c2701f8f 100644 --- a/packages/kumo-docs-astro/package.json +++ b/packages/kumo-docs-astro/package.json @@ -24,6 +24,7 @@ "@cloudflare/kumo": "workspace:*", "@phosphor-icons/react": "catalog:", "@tailwindcss/typography": "^0.5.19", + "@tanstack/react-table": "^8.21.3", "@types/turndown": "5.0.6", "astro": "^5.16.7", "clsx": "catalog:", diff --git a/packages/kumo-docs-astro/src/components/demos/TableDemo.tsx b/packages/kumo-docs-astro/src/components/demos/TableDemo.tsx index b4fd3eed78..552c379245 100644 --- a/packages/kumo-docs-astro/src/components/demos/TableDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/TableDemo.tsx @@ -13,6 +13,14 @@ import { PencilSimple, Trash, } from "@phosphor-icons/react"; +import { + type ColumnDef, + type SortingState, + flexRender, + getCoreRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; // Sample data for demos const emailData = [ @@ -264,6 +272,213 @@ export function TableFixedLayoutDemo() { ); } +// --------------------------------------------------------------------------- +// TanStack Table integration data + demo +// --------------------------------------------------------------------------- + +type Worker = { + id: string; + name: string; + requests: number; + errors: number; + cpuMs: number; + status: "Active" | "Inactive" | "Degraded"; +}; + +const workerData: Worker[] = [ + { + id: "1", + name: "api-gateway", + requests: 142_830, + errors: 12, + cpuMs: 4.2, + status: "Active", + }, + { + id: "2", + name: "auth-service", + requests: 98_210, + errors: 0, + cpuMs: 2.1, + status: "Active", + }, + { + id: "3", + name: "image-resizer", + requests: 34_560, + errors: 87, + cpuMs: 18.9, + status: "Degraded", + }, + { + id: "4", + name: "cache-purger", + requests: 6_120, + errors: 0, + cpuMs: 0.8, + status: "Active", + }, + { + id: "5", + name: "log-drain", + requests: 0, + errors: 0, + cpuMs: 0, + status: "Inactive", + }, + { + id: "6", + name: "edge-router", + requests: 215_400, + errors: 3, + cpuMs: 1.5, + status: "Active", + }, +]; + +const workerColumns: ColumnDef[] = [ + { + accessorKey: "name", + header: "Worker", + size: 200, + minSize: 120, + }, + { + accessorKey: "requests", + header: "Requests", + size: 130, + minSize: 90, + cell: ({ getValue }) => (getValue() as number).toLocaleString(), + }, + { + accessorKey: "errors", + header: "Errors", + size: 100, + minSize: 70, + }, + { + accessorKey: "cpuMs", + header: "CPU (ms)", + size: 110, + minSize: 80, + cell: ({ getValue }) => `${getValue() as number} ms`, + }, + { + accessorKey: "status", + header: "Status", + size: 110, + minSize: 80, + cell: ({ getValue }) => { + const status = getValue() as Worker["status"]; + const variantMap: Record< + Worker["status"], + "secondary" | "destructive" | "success" + > = { + Active: "success", + Degraded: "destructive", + Inactive: "secondary", + }; + return {status}; + }, + }, +]; + +/** TanStack Table with sortable columns and resizable column widths using Table.ResizeHandle */ +export function TableTanStackSortableResizableDemo() { + const [sorting, setSorting] = useState([]); + + const table = useReactTable({ + data: workerData, + columns: workerColumns, + state: { sorting }, + onSortingChange: setSorting, + columnResizeMode: "onChange", + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + }); + + return ( + + + + + {table.getAllColumns().map((col) => ( + + ))} + {/* Filler column — absorbs remaining space so fixed columns don't stretch */} + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const isSorted = header.column.getIsSorted(); + const canSort = header.column.getCanSort(); + return ( + +
+ {header.isPlaceholder ? null : ( + <> + {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + {canSort && ( + + )} + + )} +
+ +
+ ); + })} + {/* Filler header cell matching the filler col */} + +
+ ))} +
+ + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + {/* Filler cell matching the filler col */} + + + ))} + +
+
+
+ ); +} + export function TableFullDemo() { const [selectedIds, setSelectedIds] = useState>(new Set(["2"])); diff --git a/packages/kumo-docs-astro/src/pages/components/table.mdx b/packages/kumo-docs-astro/src/pages/components/table.mdx index 0fe78aba0f..8f171d8cec 100644 --- a/packages/kumo-docs-astro/src/pages/components/table.mdx +++ b/packages/kumo-docs-astro/src/pages/components/table.mdx @@ -17,6 +17,7 @@ import { TableSelectedRowDemo, TableFixedLayoutDemo, TableFullDemo, + TableTanStackSortableResizableDemo, } from "~/components/demos/TableDemo"; {/* Demo */} @@ -243,6 +244,131 @@ export default function Example() { > + + TanStack Table — Sortable & Resizable +

+ Combine @tanstack/react-table with + Kumo's Table for sortable columns + and draggable column resizing via Table.ResizeHandle. + Click any column header to sort; drag the resize handle on the right edge to change column width. +

+ [] = [ +{ accessorKey: "name", header: "Worker", size: 200 }, +{ accessorKey: "requests", header: "Requests", size: 130, +cell: ({ getValue }) => (getValue() as number).toLocaleString() }, +{ accessorKey: "errors", header: "Errors", size: 100 }, +{ accessorKey: "cpuMs", header: "CPU (ms)", size: 110, +cell: ({ getValue }) => \`\${getValue() as number} ms\` }, +{ accessorKey: "status", header: "Status", size: 110, +cell: ({ getValue }) => { +const status = getValue() as Worker["status"]; +const variant = { Active: "success", Degraded: "destructive", Inactive: "secondary" }[status]; +return {status}; +}}, +]; + +export function DataTable({ data }: { data: Worker[] }) { + const [sorting, setSorting] = useState([]); + +const table = useReactTable({ +data, columns, +state: { sorting }, +onSortingChange: setSorting, +columnResizeMode: "onChange", +getCoreRowModel: getCoreRowModel(), +getSortedRowModel: getSortedRowModel(), +}); + +return ( + + + + + + {table.getAllColumns().map((col) => ( + + ))} + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const isSorted = header.column.getIsSorted(); + return ( + +
+ +
+ +
+ ); + })} +
+ ))} +
+ + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
+
+
+); }`} + +> + + + +
{/* API Reference */} @@ -304,57 +430,38 @@ Body cell. Renders ``. class="text-kumo-link hover:underline" target="_blank" rel="noopener noreferrer">TanStack Table. The Table component is designed to work seamlessly with TanStack's - headless API. + headless API. See the live Sortable & Resizable example in the Examples section above for a complete working implementation.

+ Installation + + Key patterns -function DataTable({ data, columns }) { -const table = useReactTable({ -data, -columns, -getCoreRowModel: getCoreRowModel(), -columnResizeMode: "onChange", -}); +{/* 2. Drive colgroup widths from table state */} -return ( + + {table.getAllColumns().map((col) => ( + + ))} + - - - {table.getAllColumns().map((column) => ( - - ))} - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {flexRender(header.column.columnDef.header, header.getContext())} - - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ))} - -
-); }`} lang="tsx" /> +{/* 3. Wire sorting toggle to the header button */} + + + + + {/* 4. Attach resize handlers to Table.ResizeHandle */} + + +`} lang="tsx" /> {/* Accessibility */} diff --git a/packages/kumo/ai/component-registry.json b/packages/kumo/ai/component-registry.json index ecde5a51d5..9070dfa364 100644 --- a/packages/kumo/ai/component-registry.json +++ b/packages/kumo/ai/component-registry.json @@ -1938,7 +1938,13 @@ "description": "Additional CSS classes merged via `cn()`." } }, - "examples": [], + "examples": [ + "", + "", + "", + "", + "" + ], "colors": [ "bg-kumo-base", "border-kumo-fill", @@ -3381,9 +3387,9 @@ } }, "examples": [ - ",\n id: \"bold\",\n tooltip: \"Bold\",\n onClick: () => setActive(active === \"bold\" ? undefined : \"bold\"),\n },\n {\n icon: ,\n id: \"italic\",\n tooltip: \"Italic\",\n onClick: () => setActive(active === \"italic\" ? undefined : \"italic\"),\n },\n ]}\n />", - ",\n id: \"bold\",\n tooltip: \"Bold\",\n onClick: () => setActive(active === \"bold\" ? undefined : \"bold\"),\n },\n {\n icon: ,\n id: \"italic\",\n tooltip: \"Italic\",\n onClick: () => setActive(active === \"italic\" ? undefined : \"italic\"),\n },\n ]}\n />", - ",\n id: \"bold\",\n tooltip: \"Bold\",\n onClick: () => setActive(active === \"bold\" ? undefined : \"bold\"),\n },\n {\n icon: ,\n id: \"italic\",\n tooltip: \"Italic\",\n onClick: () => setActive(active === \"italic\" ? undefined : \"italic\"),\n },\n ]}\n />" + ",\n id: \"bold\",\n tooltip: \"Bold\",\n onClick: () => {},\n },\n {\n icon: ,\n id: \"italic\",\n tooltip: \"Italic\",\n onClick: () => {},\n },\n ]}\n />", + ",\n id: \"bold\",\n tooltip: \"Bold\",\n onClick: () => {},\n },\n {\n icon: ,\n id: \"italic\",\n tooltip: \"Italic\",\n onClick: () => {},\n },\n ]}\n />", + ",\n id: \"bold\",\n tooltip: \"Bold\",\n onClick: () => {},\n },\n {\n icon: ,\n id: \"italic\",\n tooltip: \"Italic\",\n onClick: () => {},\n },\n ]}\n />" ], "colors": [ "bg-kumo-base", @@ -3455,7 +3461,7 @@ "", "", "", - "" + "" ], "colors": [ "bg-kumo-fill", @@ -4356,7 +4362,7 @@ "Table": { "name": "Table", "type": "component", - "description": "Table — semantic HTML table with styled rows, cells, and selection support. Compound component: `Table` (Root), `.Header`, `.Head`, `.Body`, `.Row`, `.Cell`, `.Footer`, `.CheckCell`, `.CheckHead`, `.ResizeHandle`.", + "description": "Table — semantic HTML table with styled rows, cells, and selection support. Compound component: `Table` (Root), `.Header`, `.Head`, `.Body`, `.Row`, `.Cell`, `.Footer`, `.CheckCell`, `.CheckHead`, `.ResizeHandle`, `.SortIcon`.", "importPath": "@cloudflare/kumo", "category": "Other", "props": { @@ -4399,6 +4405,7 @@ "\n \n \n \n \n Subject\n From\n Date\n \n \n \n {emailData.slice(0, 3).map((row) => (\n \n {row.subject}\n {row.from}\n {row.date}\n \n ))}\n \n
\n
\n
", "\n \n \n \n \n 0 && selectedIds.size < rows.length\n }\n onValueChange={toggleAll}\n aria-label=\"Select all rows\"\n />\n Subject\n From\n Date\n \n \n \n {rows.map((row) => (\n \n toggleRow(row.id)}\n aria-label={`Select ${row.subject}`}\n />\n {row.subject}\n {row.from}\n {row.date}\n \n ))}\n \n
\n
\n
", "\n \n \n \n \n \n \n \n \n \n Subject\n From\n Date\n \n \n \n {emailData.map((row) => (\n \n {row.subject}\n {row.from}\n {row.date}\n \n ))}\n \n
\n
\n
", + "\n \n \n \n {table.getAllColumns().map((col) => (\n \n ))}\n {/* Filler column — absorbs remaining space so fixed columns don't stretch */}\n \n \n \n {table.getHeaderGroups().map((headerGroup) => (\n \n {headerGroup.headers.map((header) => {\n const isSorted = header.column.getIsSorted();\n const canSort = header.column.getCanSort();\n return (\n \n
\n {header.isPlaceholder ? null : (\n <>\n {flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n {canSort && (\n \n )}\n \n )}\n
\n \n \n );\n })}\n {/* Filler header cell matching the filler col */}\n \n
\n ))}\n
\n \n {table.getRowModel().rows.map((row) => (\n \n {row.getVisibleCells().map((cell) => (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n ))}\n {/* Filler cell matching the filler col */}\n \n \n ))}\n \n
\n
\n
", "\n \n \n \n {\" \"}\n {/* Checkbox column - width handled by Table.CheckHead/CheckCell */}\n \n \n \n \n \n \n \n 0 && selectedIds.size < emailData.length\n }\n onValueChange={toggleAll}\n aria-label=\"Select all rows\"\n />\n Subject\n From\n Date\n \n \n \n \n {emailData.map((row) => (\n \n toggleRow(row.id)}\n aria-label={`Select ${row.subject}`}\n />\n \n
\n \n {row.subject}\n {row.tags && (\n
\n {row.tags.map((tag) => (\n {tag}\n ))}\n
\n )}\n
\n
\n \n {row.from}\n \n \n {row.date}\n \n \n \n \n \n \n }\n />\n \n View\n \n Edit\n \n \n \n Delete\n \n \n \n \n \n ))}\n
\n
\n
\n
" ], "colors": [ @@ -4408,7 +4415,8 @@ "bg-kumo-tint", "border-kumo-fill", "text-kumo-default", - "text-kumo-strong" + "text-kumo-strong", + "text-kumo-subtle" ], "subComponents": { "Header": { @@ -4455,6 +4463,11 @@ "name": "ResizeHandle", "description": "ResizeHandle sub-component", "props": {} + }, + "SortIcon": { + "name": "SortIcon", + "description": "SortIcon sub-component", + "props": {} } } }, diff --git a/packages/kumo/ai/component-registry.md b/packages/kumo/ai/component-registry.md index 7ad20a8937..df3271ab69 100644 --- a/packages/kumo/ai/component-registry.md +++ b/packages/kumo/ai/component-registry.md @@ -1077,6 +1077,27 @@ Props: - `lang`: CodeLang +**Examples:** + +```tsx + +``` + +```tsx + +``` + + --- ### Collapsible @@ -3330,20 +3351,20 @@ MenuBar — horizontal icon-button toolbar with keyboard arrow-key navigation. ```tsx , id: "bold", tooltip: "Bold", - onClick: () => setActive(active === "bold" ? undefined : "bold"), + onClick: () => {}, }, { icon: , id: "italic", tooltip: "Italic", - onClick: () => setActive(active === "italic" ? undefined : "italic"), + onClick: () => {}, }, ]} /> @@ -3403,7 +3424,7 @@ Progress bar showing a measured value within a known range (e.g. quota usage). ``` @@ -4630,7 +4651,7 @@ Props: ### Table -Table — semantic HTML table with styled rows, cells, and selection support. Compound component: `Table` (Root), `.Header`, `.Head`, `.Body`, `.Row`, `.Cell`, `.Footer`, `.CheckCell`, `.CheckHead`, `.ResizeHandle`. +Table — semantic HTML table with styled rows, cells, and selection support. Compound component: `Table` (Root), `.Header`, `.Head`, `.Body`, `.Row`, `.Cell`, `.Footer`, `.CheckCell`, `.CheckHead`, `.ResizeHandle`, `.SortIcon`. **Type:** component @@ -4653,7 +4674,7 @@ Table — semantic HTML table with styled rows, cells, and selection support. C **Colors (kumo tokens used):** -`bg-kumo-base`, `bg-kumo-elevated`, `bg-kumo-ring`, `bg-kumo-tint`, `border-kumo-fill`, `text-kumo-default`, `text-kumo-strong` +`bg-kumo-base`, `bg-kumo-elevated`, `bg-kumo-ring`, `bg-kumo-tint`, `border-kumo-fill`, `text-kumo-default`, `text-kumo-strong`, `text-kumo-subtle` **Sub-Components:** @@ -4695,6 +4716,10 @@ Footer sub-component ResizeHandle sub-component +#### Table.SortIcon + +SortIcon sub-component + **Examples:** @@ -4857,6 +4882,87 @@ ResizeHandle sub-component ``` +```tsx + + + + + {table.getAllColumns().map((col) => ( + + ))} + {/* Filler column — absorbs remaining space so fixed columns don't stretch */} + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const isSorted = header.column.getIsSorted(); + const canSort = header.column.getCanSort(); + return ( + +
+ {header.isPlaceholder ? null : ( + <> + {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + {canSort && ( + + )} + + )} +
+ +
+ ); + })} + {/* Filler header cell matching the filler col */} + +
+ ))} +
+ + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + {/* Filler cell matching the filler col */} + + + ))} + +
+
+
+``` + ```tsx diff --git a/packages/kumo/src/components/table/index.ts b/packages/kumo/src/components/table/index.ts index cfe97d3d73..de72e7ddc2 100644 --- a/packages/kumo/src/components/table/index.ts +++ b/packages/kumo/src/components/table/index.ts @@ -4,4 +4,5 @@ export { KUMO_TABLE_VARIANTS, type KumoTableLayout, type KumoTableRowVariant, + type KumoTableSortDirection, } from "./table"; diff --git a/packages/kumo/src/components/table/table.tsx b/packages/kumo/src/components/table/table.tsx index 58590c9254..77708ac01c 100644 --- a/packages/kumo/src/components/table/table.tsx +++ b/packages/kumo/src/components/table/table.tsx @@ -1,4 +1,5 @@ -import { forwardRef } from "react"; +import { forwardRef, useRef } from "react"; +import { ArrowUpIcon } from "@phosphor-icons/react"; import { cn } from "../../utils"; import { Checkbox } from "../checkbox"; @@ -35,6 +36,55 @@ export const KUMO_TABLE_DEFAULT_VARIANTS = { export type KumoTableRowVariant = keyof typeof KUMO_TABLE_VARIANTS.variant; export type KumoTableLayout = keyof typeof KUMO_TABLE_VARIANTS.layout; +/** Sort direction for `Table.SortIcon`. */ +export type KumoTableSortDirection = "asc" | "desc" | "none"; + +/** + * Sort direction icon for use inside `Table.Head`. + * + * Maps directly to TanStack Table's `column.getIsSorted()` return value + * (`"asc" | "desc" | false`) — pass `false` as `"none"` to render nothing. + * + * @example + * ```tsx + * // With TanStack Table + * const isSorted = header.column.getIsSorted(); // "asc" | "desc" | false + * + * + * // Standalone + * + * ``` + */ +const TableSortIcon = ({ + direction, + className, +}: { + /** + * - `"asc"` — ascending arrow (`↑`) + * - `"desc"` — descending arrow (`↓`) + * - `"none"` — renders nothing; column is sortable but not currently sorted + */ + direction: KumoTableSortDirection; + className?: string; +}) => { + if (direction === "none") return null; + + return ( + + + + ); +}; + +TableSortIcon.displayName = "Table.SortIcon"; + /** * Table root — applies layout, borders, padding, and header styles. * @@ -143,7 +193,9 @@ const TableFooter = forwardRef< const TableResizeHandle = forwardRef< HTMLButtonElement, React.HTMLAttributes ->((props, ref) => { +>(({ onMouseDown, onTouchStart, ...props }, ref) => { + const didDrag = useRef(false); + return ( @@ -253,7 +338,7 @@ TableCheckHead.displayName = "Table.CheckHead"; * Table — semantic HTML table with styled rows, cells, and selection support. * * Compound component: `Table` (Root), `.Header`, `.Head`, `.Body`, `.Row`, - * `.Cell`, `.Footer`, `.CheckCell`, `.CheckHead`, `.ResizeHandle`. + * `.Cell`, `.Footer`, `.CheckCell`, `.CheckHead`, `.ResizeHandle`, `.SortIcon`. * * @example * ```tsx @@ -285,4 +370,5 @@ export const Table = Object.assign(TableRoot, { CheckHead: TableCheckHead, Footer: TableFooter, ResizeHandle: TableResizeHandle, + SortIcon: TableSortIcon, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cba6af326f..fdef4f6782 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -270,6 +270,9 @@ importers: '@tailwindcss/typography': specifier: ^0.5.19 version: 0.5.19(tailwindcss@4.1.17) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@types/turndown': specifier: 5.0.6 version: 5.0.6 @@ -695,37 +698,37 @@ packages: optional: true '@cloudflare/workerd-darwin-64@1.20260111.0': - resolution: {integrity: sha512-UGAjrGLev2/CMLZy7b+v1NIXA4Hupc/QJBFlJwMqldywMcJ/iEqvuUYYuVI2wZXuXeWkgmgFP87oFDQsg78YTQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260111.0.tgz} + resolution: {integrity: sha512-UGAjrGLev2/CMLZy7b+v1NIXA4Hupc/QJBFlJwMqldywMcJ/iEqvuUYYuVI2wZXuXeWkgmgFP87oFDQsg78YTQ==} engines: {node: '>=16'} cpu: [x64] os: [darwin] '@cloudflare/workerd-darwin-arm64@1.20260111.0': - resolution: {integrity: sha512-YFAZwidLCQVa6rKCCaiWrhA+eh87a7MUhyd9lat3KSbLBAGpYM+ORpyTXpi2Gjm3j6Mp1e/wtzcFTSeMIy2UqA==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260111.0.tgz} + resolution: {integrity: sha512-YFAZwidLCQVa6rKCCaiWrhA+eh87a7MUhyd9lat3KSbLBAGpYM+ORpyTXpi2Gjm3j6Mp1e/wtzcFTSeMIy2UqA==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] '@cloudflare/workerd-linux-64@1.20260111.0': - resolution: {integrity: sha512-zx1GW6FwfOBjCV7QUCRzGRkViUtn3Is/zaaVPmm57xyy9sjtInx6/SdeBr2Y45tx9AnOP1CnaOFFdmH1P7VIEg==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260111.0.tgz} + resolution: {integrity: sha512-zx1GW6FwfOBjCV7QUCRzGRkViUtn3Is/zaaVPmm57xyy9sjtInx6/SdeBr2Y45tx9AnOP1CnaOFFdmH1P7VIEg==} engines: {node: '>=16'} cpu: [x64] os: [linux] '@cloudflare/workerd-linux-arm64@1.20260111.0': - resolution: {integrity: sha512-wFVKxNvCyjRaAcgiSnJNJAmIos3p3Vv6Uhf4pFUZ9JIxr69GNlLWlm9SdCPvtwNFAjzSoDaKzDwjj5xqpuCS6Q==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260111.0.tgz} + resolution: {integrity: sha512-wFVKxNvCyjRaAcgiSnJNJAmIos3p3Vv6Uhf4pFUZ9JIxr69GNlLWlm9SdCPvtwNFAjzSoDaKzDwjj5xqpuCS6Q==} engines: {node: '>=16'} cpu: [arm64] os: [linux] '@cloudflare/workerd-windows-64@1.20260111.0': - resolution: {integrity: sha512-zWgd77L7OI1BxgBbG+2gybDahIMgPX5iNo6e3LqcEz1Xm3KfiqgnDyMBcxeQ7xDrj7fHUGAlc//QnKvDchuUoQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260111.0.tgz} + resolution: {integrity: sha512-zWgd77L7OI1BxgBbG+2gybDahIMgPX5iNo6e3LqcEz1Xm3KfiqgnDyMBcxeQ7xDrj7fHUGAlc//QnKvDchuUoQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] '@cloudflare/workers-types@4.20260317.1': - resolution: {integrity: sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ==, tarball: https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260317.1.tgz} + resolution: {integrity: sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -2107,6 +2110,17 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -7697,6 +7711,14 @@ snapshots: tailwindcss: 4.1.17 vite: 7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.2) + '@tanstack/react-table@8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@tanstack/table-core@8.21.3': {} + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 From 3f39d826675c7aadaa6b3171b5477ec8bf8269f7 Mon Sep 17 00:00:00 2001 From: Visal In Date: Fri, 20 Mar 2026 11:03:27 +0000 Subject: [PATCH 2/4] feat(table): use false | "asc" | "desc" for Table.SortIcon direction to match TanStack Table --- .changeset/table-sort-icon.md | 2 +- .../src/components/demos/TableDemo.tsx | 4 +--- packages/kumo/ai/component-registry.json | 5 ++--- packages/kumo/ai/component-registry.md | 6 ++---- packages/kumo/src/components/table/table.tsx | 21 ++++++++++++------- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.changeset/table-sort-icon.md b/.changeset/table-sort-icon.md index 0b81b3eb00..69c033620b 100644 --- a/.changeset/table-sort-icon.md +++ b/.changeset/table-sort-icon.md @@ -2,4 +2,4 @@ "@cloudflare/kumo": minor --- -Add `Table.SortIcon` sub-component with `direction="asc" | "desc" | "none"` prop. +Add `Table.SortIcon` sub-component with `direction: false | "asc" | "desc"` prop diff --git a/packages/kumo-docs-astro/src/components/demos/TableDemo.tsx b/packages/kumo-docs-astro/src/components/demos/TableDemo.tsx index 552c379245..5f14cbb652 100644 --- a/packages/kumo-docs-astro/src/components/demos/TableDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/TableDemo.tsx @@ -442,9 +442,7 @@ export function TableTanStackSortableResizableDemo() { header.column.columnDef.header, header.getContext(), )} - {canSort && ( - - )} + {canSort && } )} diff --git a/packages/kumo/ai/component-registry.json b/packages/kumo/ai/component-registry.json index 9070dfa364..48476ce1cb 100644 --- a/packages/kumo/ai/component-registry.json +++ b/packages/kumo/ai/component-registry.json @@ -4405,7 +4405,7 @@ "\n \n \n \n \n Subject\n From\n Date\n \n \n \n {emailData.slice(0, 3).map((row) => (\n \n {row.subject}\n {row.from}\n {row.date}\n \n ))}\n \n
\n
\n
", "\n \n \n \n \n 0 && selectedIds.size < rows.length\n }\n onValueChange={toggleAll}\n aria-label=\"Select all rows\"\n />\n Subject\n From\n Date\n \n \n \n {rows.map((row) => (\n \n toggleRow(row.id)}\n aria-label={`Select ${row.subject}`}\n />\n {row.subject}\n {row.from}\n {row.date}\n \n ))}\n \n
\n
\n
", "\n \n \n \n \n \n \n \n \n \n Subject\n From\n Date\n \n \n \n {emailData.map((row) => (\n \n {row.subject}\n {row.from}\n {row.date}\n \n ))}\n \n
\n
\n
", - "\n \n \n \n {table.getAllColumns().map((col) => (\n \n ))}\n {/* Filler column — absorbs remaining space so fixed columns don't stretch */}\n \n \n \n {table.getHeaderGroups().map((headerGroup) => (\n \n {headerGroup.headers.map((header) => {\n const isSorted = header.column.getIsSorted();\n const canSort = header.column.getCanSort();\n return (\n \n
\n {header.isPlaceholder ? null : (\n <>\n {flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n {canSort && (\n \n )}\n \n )}\n
\n \n \n );\n })}\n {/* Filler header cell matching the filler col */}\n \n
\n ))}\n
\n \n {table.getRowModel().rows.map((row) => (\n \n {row.getVisibleCells().map((cell) => (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n ))}\n {/* Filler cell matching the filler col */}\n \n \n ))}\n \n
\n
\n
", + "\n \n \n \n {table.getAllColumns().map((col) => (\n \n ))}\n {/* Filler column — absorbs remaining space so fixed columns don't stretch */}\n \n \n \n {table.getHeaderGroups().map((headerGroup) => (\n \n {headerGroup.headers.map((header) => {\n const isSorted = header.column.getIsSorted();\n const canSort = header.column.getCanSort();\n return (\n \n
\n {header.isPlaceholder ? null : (\n <>\n {flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n {canSort && }\n \n )}\n
\n \n \n );\n })}\n {/* Filler header cell matching the filler col */}\n \n
\n ))}\n
\n \n {table.getRowModel().rows.map((row) => (\n \n {row.getVisibleCells().map((cell) => (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n ))}\n {/* Filler cell matching the filler col */}\n \n \n ))}\n \n
\n
\n
", "\n \n \n \n {\" \"}\n {/* Checkbox column - width handled by Table.CheckHead/CheckCell */}\n \n \n \n \n \n \n \n 0 && selectedIds.size < emailData.length\n }\n onValueChange={toggleAll}\n aria-label=\"Select all rows\"\n />\n Subject\n From\n Date\n \n \n \n \n {emailData.map((row) => (\n \n toggleRow(row.id)}\n aria-label={`Select ${row.subject}`}\n />\n \n
\n \n {row.subject}\n {row.tags && (\n
\n {row.tags.map((tag) => (\n {tag}\n ))}\n
\n )}\n
\n
\n \n {row.from}\n \n \n {row.date}\n \n \n \n \n \n \n }\n />\n \n View\n \n Edit\n \n \n \n Delete\n \n \n \n \n \n ))}\n
\n
\n
\n
" ], "colors": [ @@ -4415,8 +4415,7 @@ "bg-kumo-tint", "border-kumo-fill", "text-kumo-default", - "text-kumo-strong", - "text-kumo-subtle" + "text-kumo-strong" ], "subComponents": { "Header": { diff --git a/packages/kumo/ai/component-registry.md b/packages/kumo/ai/component-registry.md index df3271ab69..2ea9427ed7 100644 --- a/packages/kumo/ai/component-registry.md +++ b/packages/kumo/ai/component-registry.md @@ -4674,7 +4674,7 @@ Table — semantic HTML table with styled rows, cells, and selection support. C **Colors (kumo tokens used):** -`bg-kumo-base`, `bg-kumo-elevated`, `bg-kumo-ring`, `bg-kumo-tint`, `border-kumo-fill`, `text-kumo-default`, `text-kumo-strong`, `text-kumo-subtle` +`bg-kumo-base`, `bg-kumo-elevated`, `bg-kumo-ring`, `bg-kumo-tint`, `border-kumo-fill`, `text-kumo-default`, `text-kumo-strong` **Sub-Components:** @@ -4927,9 +4927,7 @@ SortIcon sub-component header.column.columnDef.header, header.getContext(), )} - {canSort && ( - - )} + {canSort && } )} diff --git a/packages/kumo/src/components/table/table.tsx b/packages/kumo/src/components/table/table.tsx index 77708ac01c..19a471e3ca 100644 --- a/packages/kumo/src/components/table/table.tsx +++ b/packages/kumo/src/components/table/table.tsx @@ -36,20 +36,25 @@ export const KUMO_TABLE_DEFAULT_VARIANTS = { export type KumoTableRowVariant = keyof typeof KUMO_TABLE_VARIANTS.variant; export type KumoTableLayout = keyof typeof KUMO_TABLE_VARIANTS.layout; -/** Sort direction for `Table.SortIcon`. */ -export type KumoTableSortDirection = "asc" | "desc" | "none"; +/** + * Sort direction for `Table.SortIcon`. + * + * Matches TanStack Table's `column.getIsSorted()` return type exactly: + * `false` when unsorted, `"asc"` or `"desc"` when sorted. + */ +export type KumoTableSortDirection = false | "asc" | "desc"; /** * Sort direction icon for use inside `Table.Head`. * - * Maps directly to TanStack Table's `column.getIsSorted()` return value - * (`"asc" | "desc" | false`) — pass `false` as `"none"` to render nothing. + * Accepts the same type as TanStack Table's `column.getIsSorted()` — + * pass the value directly without any conversion. * * @example * ```tsx * // With TanStack Table - * const isSorted = header.column.getIsSorted(); // "asc" | "desc" | false - * + * const isSorted = header.column.getIsSorted(); // false | "asc" | "desc" + * * * // Standalone * @@ -62,12 +67,12 @@ const TableSortIcon = ({ /** * - `"asc"` — ascending arrow (`↑`) * - `"desc"` — descending arrow (`↓`) - * - `"none"` — renders nothing; column is sortable but not currently sorted + * - `false` — renders nothing; column is sortable but not currently sorted */ direction: KumoTableSortDirection; className?: string; }) => { - if (direction === "none") return null; + if (!direction) return null; return ( Date: Fri, 20 Mar 2026 13:05:45 +0000 Subject: [PATCH 3/4] docs(table): update TanStack example to match current demo Replace outdated manual sort icon pattern (ArrowUp/ArrowDown/ArrowsDownUp) with Table.SortIcon, add canSort guard, filler col/row pattern, proper indentation, and escape template literals in the code snippet. --- .../src/pages/components/table.mdx | 277 ++++++++++-------- 1 file changed, 162 insertions(+), 115 deletions(-) diff --git a/packages/kumo-docs-astro/src/pages/components/table.mdx b/packages/kumo-docs-astro/src/pages/components/table.mdx index 8f171d8cec..fd861f75a0 100644 --- a/packages/kumo-docs-astro/src/pages/components/table.mdx +++ b/packages/kumo-docs-astro/src/pages/components/table.mdx @@ -253,7 +253,8 @@ export default function Example() { Click any column header to sort; drag the resize handle on the right edge to change column width.

= { + Active: "success", + Degraded: "destructive", + Inactive: "secondary", }; const columns: ColumnDef[] = [ -{ accessorKey: "name", header: "Worker", size: 200 }, -{ accessorKey: "requests", header: "Requests", size: 130, -cell: ({ getValue }) => (getValue() as number).toLocaleString() }, -{ accessorKey: "errors", header: "Errors", size: 100 }, -{ accessorKey: "cpuMs", header: "CPU (ms)", size: 110, -cell: ({ getValue }) => \`\${getValue() as number} ms\` }, -{ accessorKey: "status", header: "Status", size: 110, -cell: ({ getValue }) => { -const status = getValue() as Worker["status"]; -const variant = { Active: "success", Degraded: "destructive", Inactive: "secondary" }[status]; -return {status}; -}}, + { accessorKey: "name", header: "Worker", size: 200, minSize: 120 }, + { + accessorKey: "requests", + header: "Requests", + size: 130, + minSize: 90, + cell: ({ getValue }) => (getValue() as number).toLocaleString(), + }, + { accessorKey: "errors", header: "Errors", size: 100, minSize: 70 }, + { + accessorKey: "cpuMs", + header: "CPU (ms)", + size: 110, + minSize: 80, + cell: ({ getValue }) => \`\${getValue() as number} ms\`, + }, + { + accessorKey: "status", + header: "Status", + size: 110, + minSize: 80, + cell: ({ getValue }) => { + const status = getValue() as Worker["status"]; + return {status}; + }, + }, ]; -export function DataTable({ data }: { data: Worker[] }) { +export function DataTable() { const [sorting, setSorting] = useState([]); -const table = useReactTable({ -data, columns, -state: { sorting }, -onSortingChange: setSorting, -columnResizeMode: "onChange", -getCoreRowModel: getCoreRowModel(), -getSortedRowModel: getSortedRowModel(), -}); - -return ( - - - - - - {table.getAllColumns().map((col) => ( - - ))} - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - const isSorted = header.column.getIsSorted(); - return ( - -
-
+ + {table.getAllColumns().map((col) => ( + + ))} + {/* Filler column — absorbs remaining space so fixed columns don't stretch */} + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const isSorted = header.column.getIsSorted(); + const canSort = header.column.getCanSort(); + return ( + - {flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - {isSorted === "asc" ? ( - - ) : isSorted === "desc" ? ( - - ) : ( - +
+ {header.isPlaceholder ? null : ( + <> + {flexRender(header.column.columnDef.header, header.getContext())} + {canSort && } + )} - - -
- -
- ); - })} -
- ))} -
- - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - + + + + ); + })} + {/* Filler header cell matching the filler col */} + + ))} - - ))} - -
-
-
-); }`} - -> - + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + {/* Filler cell matching the filler col */} + + + ))} + + +
+
+ ); +}`} + > - @@ -439,28 +469,45 @@ Body cell. Renders ``. code={`// 1. Use layout="fixed" so column widths from TanStack are respected -{/* 2. Drive colgroup widths from table state */} - -- {table.getAllColumns().map((col) => ( - - ))} - - -{/* 3. Wire sorting toggle to the header button */} - - - + {/* 2. Drive colgroup widths from table state */} + + {table.getAllColumns().map((col) => ( + + ))} + {/* Filler column — absorbs remaining space so fixed columns don't stretch */} + + - {/* 4. Attach resize handlers to Table.ResizeHandle */} - + + + {headerGroup.headers.map((header) => { + const isSorted = header.column.getIsSorted(); + const canSort = header.column.getCanSort(); + return ( + {/* 3. Wire sorting toggle directly on Table.Head */} + +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {/* 4. Use Table.SortIcon — pass isSorted directly, no conversion needed */} + {canSort && } +
+ {/* 5. Attach resize handlers to Table.ResizeHandle */} + +
+ ); + })} + {/* Filler header cell matching the filler col */} + +
+
-
`} lang="tsx" /> From a7a5c86b720a25d6c8b75089b5a04ab5fbb828e3 Mon Sep 17 00:00:00 2001 From: Visal In Date: Fri, 20 Mar 2026 16:01:04 +0000 Subject: [PATCH 4/4] refactor(table): move drag-tracking out of ResizeHandle into consumer --- .../src/components/demos/TableDemo.tsx | 21 +- .../src/pages/components/table.mdx | 251 +++++++----------- packages/kumo/src/components/table/table.tsx | 36 +-- 3 files changed, 118 insertions(+), 190 deletions(-) diff --git a/packages/kumo-docs-astro/src/components/demos/TableDemo.tsx b/packages/kumo-docs-astro/src/components/demos/TableDemo.tsx index 5f14cbb652..b35ac84568 100644 --- a/packages/kumo-docs-astro/src/components/demos/TableDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/TableDemo.tsx @@ -423,17 +423,17 @@ export function TableTanStackSortableResizableDemo() { return (
{header.isPlaceholder ? null : ( @@ -447,8 +447,15 @@ export function TableTanStackSortableResizableDemo() { )}
{ + e.stopPropagation(); + }} + onMouseDown={(e) => { + header.getResizeHandler()(e); + }} + onTouchStart={(e) => { + header.getResizeHandler()(e); + }} />
); diff --git a/packages/kumo-docs-astro/src/pages/components/table.mdx b/packages/kumo-docs-astro/src/pages/components/table.mdx index fd861f75a0..cb564e0db5 100644 --- a/packages/kumo-docs-astro/src/pages/components/table.mdx +++ b/packages/kumo-docs-astro/src/pages/components/table.mdx @@ -264,30 +264,6 @@ import { } from "@tanstack/react-table"; import { Badge, LayerCard, Table } from "@cloudflare/kumo"; -type Worker = { - id: string; - name: string; - requests: number; - errors: number; - cpuMs: number; - status: "Active" | "Inactive" | "Degraded"; -}; - -const workerData: Worker[] = [ - { id: "1", name: "api-gateway", requests: 142_830, errors: 12, cpuMs: 4.2, status: "Active" }, - { id: "2", name: "auth-service", requests: 98_210, errors: 0, cpuMs: 2.1, status: "Active" }, - { id: "3", name: "image-resizer", requests: 34_560, errors: 87, cpuMs: 18.9, status: "Degraded" }, - { id: "4", name: "cache-purger", requests: 6_120, errors: 0, cpuMs: 0.8, status: "Active" }, - { id: "5", name: "log-drain", requests: 0, errors: 0, cpuMs: 0, status: "Inactive" }, - { id: "6", name: "edge-router", requests: 215_400, errors: 3, cpuMs: 1.5, status: "Active" }, -]; - -const variantMap: Record = { - Active: "success", - Degraded: "destructive", - Inactive: "secondary", -}; - const columns: ColumnDef[] = [ { accessorKey: "name", header: "Worker", size: 200, minSize: 120 }, { @@ -331,71 +307,112 @@ export function DataTable() { }); return ( - - - - - {table.getAllColumns().map((col) => ( - - ))} - {/* Filler column — absorbs remaining space so fixed columns don't stretch */} - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - const isSorted = header.column.getIsSorted(); - const canSort = header.column.getCanSort(); - return ( - -
- {header.isPlaceholder ? null : ( - <> - {flexRender(header.column.columnDef.header, header.getContext())} - {canSort && } - +
+ + {table.getAllColumns().map((col) => ( + + ))} + {/* Filler column — absorbs remaining space so fixed columns don't stretch */} + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const isSorted = header.column.getIsSorted(); + const canSort = header.column.getCanSort(); + return ( + +
+ {header.isPlaceholder ? null : ( + <> + {flexRender( + header.column.columnDef.header, + header.getContext(), )} -
- -
- ); - })} - {/* Filler header cell matching the filler col */} - -
- ))} -
- - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - {/* Filler cell matching the filler col */} - - + {canSort && } + + )} + + { + e.stopPropagation(); + }} + onMouseDown={(e) => { + header.getResizeHandler()(e); + }} + onTouchStart={(e) => { + header.getResizeHandler()(e); + }} + /> + + ); + })} + {/* Filler header cell matching the filler col */} + + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} - -
-
-
+ {/* Filler cell matching the filler col */} + + + ))} + + ); -}`} +} + +type Worker = { + id: string; + name: string; + requests: number; + errors: number; + cpuMs: number; + status: "Active" | "Inactive" | "Degraded"; +}; + +const workerData: Worker[] = [ + { id: "1", name: "api-gateway", requests: 142_830, errors: 12, cpuMs: 4.2, status: "Active" }, + { id: "2", name: "auth-service", requests: 98_210, errors: 0, cpuMs: 2.1, status: "Active" }, + { id: "3", name: "image-resizer", requests: 34_560, errors: 87, cpuMs: 18.9, status: "Degraded" }, + { id: "4", name: "cache-purger", requests: 6_120, errors: 0, cpuMs: 0.8, status: "Active" }, + { id: "5", name: "log-drain", requests: 0, errors: 0, cpuMs: 0, status: "Inactive" }, + { id: "6", name: "edge-router", requests: 215_400, errors: 3, cpuMs: 1.5, status: "Active" }, +]; + +const variantMap: Record = { + Active: "success", + Degraded: "destructive", + Inactive: "secondary", +}; +`} > @@ -449,68 +466,6 @@ Body cell. Renders ``.

-{/* TanStack Table Integration */} - - - TanStack Table Integration -

- For advanced features like sorting, filtering, and resizable columns, - integrate with TanStack Table. The Table component is designed to work seamlessly with TanStack's - headless API. See the live Sortable & Resizable example in the Examples section above for a complete working implementation. -

- Installation - - Key patterns - - - {/* 2. Drive colgroup widths from table state */} - - {table.getAllColumns().map((col) => ( - - ))} - {/* Filler column — absorbs remaining space so fixed columns don't stretch */} - - - - - - {headerGroup.headers.map((header) => { - const isSorted = header.column.getIsSorted(); - const canSort = header.column.getCanSort(); - return ( - {/* 3. Wire sorting toggle directly on Table.Head */} - -
- {flexRender(header.column.columnDef.header, header.getContext())} - {/* 4. Use Table.SortIcon — pass isSorted directly, no conversion needed */} - {canSort && } -
- {/* 5. Attach resize handlers to Table.ResizeHandle */} - -
- ); - })} - {/* Filler header cell matching the filler col */} - -
-
- -`} lang="tsx" /> -
- {/* Accessibility */} diff --git a/packages/kumo/src/components/table/table.tsx b/packages/kumo/src/components/table/table.tsx index 19a471e3ca..6813d9875d 100644 --- a/packages/kumo/src/components/table/table.tsx +++ b/packages/kumo/src/components/table/table.tsx @@ -198,9 +198,7 @@ const TableFooter = forwardRef< const TableResizeHandle = forwardRef< HTMLButtonElement, React.HTMLAttributes ->(({ onMouseDown, onTouchStart, ...props }, ref) => { - const didDrag = useRef(false); - +>((props, ref) => { return (