diff --git a/examples/app-module/src/custom-module.tsx b/examples/app-module/src/custom-module.tsx
index 20d79f07..9e2cac92 100644
--- a/examples/app-module/src/custom-module.tsx
+++ b/examples/app-module/src/custom-module.tsx
@@ -19,6 +19,7 @@ import {
import { primitiveComponentsDemoResource } from "./pages/primitives-demo";
import { dropdownComponentsDemoResource } from "./pages/dropdown-demo";
import { formComponentsDemoResource, zodRHFFormDemoResource } from "./pages/form-demo";
+import { dataTableDemoResource } from "./pages/data-table-demo";
export const customPageModule = defineModule({
path: "custom-page",
@@ -181,6 +182,17 @@ export const customPageModule = defineModule({
Zod + React Hook Form Demo
+
+
+ DataTable Demo (sortable columns, row actions, pagination)
+
+
);
@@ -205,5 +217,6 @@ export const customPageModule = defineModule({
dropdownComponentsDemoResource,
formComponentsDemoResource,
zodRHFFormDemoResource,
+ dataTableDemoResource,
],
});
diff --git a/examples/app-module/src/pages/data-table-demo.tsx b/examples/app-module/src/pages/data-table-demo.tsx
new file mode 100644
index 00000000..a06b6129
--- /dev/null
+++ b/examples/app-module/src/pages/data-table-demo.tsx
@@ -0,0 +1,259 @@
+import {
+ defineResource,
+ Badge,
+ DataTable,
+ Pagination,
+ useDataTable,
+ useCollectionVariables,
+ createColumnHelper,
+ Layout,
+ type CollectionResult,
+ type CollectionVariables,
+ type RowAction,
+} from "@tailor-platform/app-shell";
+import { useMemo } from "react";
+
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+type Product = {
+ id: string;
+ name: string;
+ category: string;
+ price: number;
+ stock: number;
+ status: "Active" | "Draft" | "Archived";
+};
+
+// ---------------------------------------------------------------------------
+// Mock metadata (normally generated by app-shell-sdk-plugins)
+// ---------------------------------------------------------------------------
+
+const productMetadata = {
+ name: "product",
+ pluralForm: "products",
+ fields: [
+ { name: "id", type: "uuid", required: true },
+ {
+ name: "name",
+ type: "string",
+ required: true,
+ description: "Product name",
+ },
+ { name: "category", type: "string", required: true },
+ { name: "price", type: "number", required: true },
+ { name: "stock", type: "number", required: false },
+ {
+ name: "status",
+ type: "enum",
+ required: true,
+ enumValues: ["Active", "Draft", "Archived"],
+ },
+ ],
+} as const;
+
+// ---------------------------------------------------------------------------
+// Columns (using createColumnHelper + inferColumns)
+// ---------------------------------------------------------------------------
+
+const { column, inferColumns } = createColumnHelper();
+const infer = inferColumns(productMetadata);
+
+const productColumns = [
+ column({
+ ...infer("name"),
+ render: (row) => {row.name},
+ }),
+ column(infer("category")),
+ column({
+ ...infer("price"),
+ render: (row) => `$${row.price.toFixed(2)}`,
+ }),
+ column({
+ ...infer("stock"),
+ render: (row) => row.stock.toLocaleString(),
+ }),
+ column({
+ ...infer("status"),
+ render: (row) => {
+ const variant =
+ row.status === "Active"
+ ? ("success" as const)
+ : row.status === "Draft"
+ ? ("outline-warning" as const)
+ : ("neutral" as const);
+ return {row.status};
+ },
+ }),
+];
+
+// ---------------------------------------------------------------------------
+// Mock data (CollectionResult format)
+// ---------------------------------------------------------------------------
+
+const mockProducts: CollectionResult = {
+ edges: [
+ {
+ node: {
+ id: "p-001",
+ name: "Ergonomic Chair",
+ category: "Furniture",
+ price: 499.99,
+ stock: 42,
+ status: "Active",
+ },
+ },
+ {
+ node: {
+ id: "p-002",
+ name: "Standing Desk",
+ category: "Furniture",
+ price: 899.0,
+ stock: 15,
+ status: "Active",
+ },
+ },
+ {
+ node: {
+ id: "p-003",
+ name: "Mechanical Keyboard",
+ category: "Electronics",
+ price: 159.99,
+ stock: 230,
+ status: "Active",
+ },
+ },
+ {
+ node: {
+ id: "p-004",
+ name: "USB-C Hub",
+ category: "Electronics",
+ price: 79.99,
+ stock: 0,
+ status: "Draft",
+ },
+ },
+ {
+ node: {
+ id: "p-005",
+ name: "Monitor Arm",
+ category: "Accessories",
+ price: 129.0,
+ stock: 57,
+ status: "Active",
+ },
+ },
+ {
+ node: {
+ id: "p-006",
+ name: "Webcam HD",
+ category: "Electronics",
+ price: 89.99,
+ stock: 120,
+ status: "Archived",
+ },
+ },
+ {
+ node: {
+ id: "p-007",
+ name: "Desk Lamp",
+ category: "Accessories",
+ price: 45.0,
+ stock: 88,
+ status: "Active",
+ },
+ },
+ {
+ node: {
+ id: "p-008",
+ name: "Cable Tray",
+ category: "Accessories",
+ price: 29.99,
+ stock: 200,
+ status: "Draft",
+ },
+ },
+ ],
+ pageInfo: {
+ hasNextPage: false,
+ endCursor: null,
+ hasPreviousPage: false,
+ startCursor: null,
+ },
+ total: 8,
+};
+
+// ---------------------------------------------------------------------------
+// Row actions
+// ---------------------------------------------------------------------------
+
+const productRowActions: RowAction[] = [
+ {
+ id: "edit",
+ label: "Edit",
+ onClick: (row) => alert(`Edit: ${row.name}`),
+ },
+ {
+ id: "delete",
+ label: "Delete",
+ variant: "destructive",
+ isDisabled: (row) => row.status === "Active",
+ onClick: (row) => alert(`Delete: ${row.name}`),
+ },
+];
+
+// ---------------------------------------------------------------------------
+// Mock query hook (simulates a real useQuery call)
+// ---------------------------------------------------------------------------
+
+function useProductsQuery(_variables: CollectionVariables) {
+ return useMemo(() => ({ data: mockProducts, loading: false }), []);
+}
+
+// ---------------------------------------------------------------------------
+// Page component
+// ---------------------------------------------------------------------------
+
+const DataTableDemoPage = () => {
+ const { variables, control } = useCollectionVariables({
+ params: { pageSize: 10 },
+ });
+
+ const { data, loading } = useProductsQuery(variables);
+
+ const table = useDataTable({
+ columns: productColumns,
+ data,
+ loading,
+ control,
+ rowActions: productRowActions,
+ onClickRow: (row) => alert(`Clicked: ${row.name}`),
+ });
+
+ return (
+
+
+
+
+ DataTable demo with sortable columns, row actions, and pagination.
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const dataTableDemoResource = defineResource({
+ path: "data-table-demo",
+ meta: {
+ title: "DataTable Demo",
+ },
+ component: DataTableDemoPage,
+});
diff --git a/packages/core/package.json b/packages/core/package.json
index 9044ee83..4862a17a 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -51,7 +51,6 @@
"@base-ui/react": "^1.3.0",
"@tailor-platform/app-shell-vite-plugin": "workspace:*",
"@tailor-platform/auth-public-client": "^0.5.0",
- "@tanstack/react-table": "^8.21.3",
"change-case": "^5.4.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
diff --git a/packages/core/src/components/data-table/API.md b/packages/core/src/components/data-table/API.md
new file mode 100644
index 00000000..a5c60cdf
--- /dev/null
+++ b/packages/core/src/components/data-table/API.md
@@ -0,0 +1,282 @@
+# DataTable API Reference (Source → app-shell)
+
+This document describes the source DataTable API as-is, with notes on what changes during migration.
+
+## Compound Component: `DataTable`
+
+```tsx
+export const DataTable = {
+ Provider, // Context provider wrapping useDataTable return value
+ Root, // Table container (renders Table.Root internally)
+ Headers, // Auto-generated header row from columns + sort controls
+ Body, // Auto-generated rows, or custom children for manual rendering
+ Row, // Thin wrapper around Table.Row (for custom Body children)
+ Cell, // Thin wrapper around Table.Cell (for custom Body children)
+} as const;
+```
+
+**Usage pattern:**
+
+```tsx
+const { variables, control } = useCollectionVariables({ params: { pageSize: 20 } });
+const [result] = useQuery({
+ query: GET_ORDERS,
+ variables: { ...variables.pagination, query: variables.query, order: variables.order },
+});
+const table = useDataTable({ columns, data: result.data?.orders, control });
+
+
+
+
+
+
+
+;
+```
+
+**Sub-component props:**
+
+| Sub-component | Props |
+| -------------------- | ------------------------------------------------------------- |
+| `DataTable.Provider` | `value: UseDataTableReturn`, `children` |
+| `DataTable.Root` | `className?`, `children` |
+| `DataTable.Headers` | `className?` (reads columns/sort/rowActions from context) |
+| `DataTable.Body` | `className?`, `children?` (if provided, skips auto-rendering) |
+| `DataTable.Row` | `ComponentProps<"tr">` (pass-through to `Table.Row`) |
+| `DataTable.Cell` | `ComponentProps<"td">` (pass-through to `Table.Cell`) |
+
+**Migration notes:**
+
+- `DataTable.Root` will hardcode `astw:border astw:rounded-md astw:bg-card` on wrapper
+- Internal `Table.Headers` → `Table.Header`, `Table.HeaderRow` → `Table.Row`, `Table.HeaderCell` → `Table.Head`
+- `RowActionsMenu` (internal): replace raw `