| ` | Individual cell with context menu support |
| `SeizenTable.Paginator` | ` ` | Pagination controls |
+| `SeizenTable.Loader` | ` ` | Loading overlay for Remote Mode |
### SeizenTablePlugins
@@ -138,6 +139,7 @@ SeizenTable.Root (providers + context)
├─ SeizenTable.Content (main content area)
│ ├─ SeizenTablePlugins.Header (plugin-provided toolbars)
│ ├─ SeizenTable.Table
+│ │ ├─ SeizenTable.Loader (optional, via `before` prop)
│ │ ├─ SeizenTable.Header
│ │ └─ SeizenTable.Body
│ │ ├─ SeizenTable.Row
@@ -148,6 +150,34 @@ SeizenTable.Root (providers + context)
└─ SeizenTablePlugins.SidePanel (position: "right")
```
+## Loading Overlay
+
+For [Remote Mode](/seizen-table/guides/4-remote-data/), use `SeizenTable.Loader` to show a loading state while fetching data:
+
+```tsx
+const [isLoading, setIsLoading] = useState(false);
+
+
+
+
+ );
+}
+
+/**
+ * Loading overlay component for Composable UI.
+ * Use with `SeizenTable.Table`'s `before` prop to show loading state.
+ *
+ * @example
+ * ```tsx
+ *
+ {children ??
+ );
}
/**
@@ -86,6 +161,8 @@ export function SeizenTable
+ {before}
);
diff --git a/packages/core/src/table/components/index.ts b/packages/core/src/table/components/index.ts
index 4ae9d7b..a203f90 100644
--- a/packages/core/src/table/components/index.ts
+++ b/packages/core/src/table/components/index.ts
@@ -1,6 +1,6 @@
export { TableRoot, useSeizenTableContext, type TableRootProps } from "./Root";
export { TableContent } from "./Content";
-export { TableTable } from "./Table";
+export { TableTable, type TableTableProps } from "./Table";
export { TableHeader } from "./Header";
export { TableBody } from "./Body";
export { TableRow, type TableRowProps } from "./Row";
diff --git a/packages/core/src/table/index.ts b/packages/core/src/table/index.ts
index 04a9789..0d55070 100644
--- a/packages/core/src/table/index.ts
+++ b/packages/core/src/table/index.ts
@@ -3,7 +3,11 @@ export { Paginator } from "./components/Paginator";
export { useSeizenTable } from "./useSeizenTable";
export { useSeizenTableEvent } from "./useSeizenTableEvent";
export { useSeizenTableContext } from "./components/Root";
-export type { SeizenTableProps, PaginateOptions } from "./SeizenTable";
+export type {
+ SeizenTableProps,
+ PaginateOptions,
+ LoaderProps,
+} from "./SeizenTable";
export type { PaginatorProps } from "./components/Paginator";
// Compound component types
@@ -11,6 +15,7 @@ export type {
TableRootProps,
TableRowProps,
TableCellProps,
+ TableTableProps,
} from "./components";
// Hooks - re-export from plugin for convenience
@@ -21,6 +26,7 @@ export type {
SeizenTableColumn,
SeizenTableInstance,
UseSeizenTableOptions,
+ RemoteOptions,
} from "./useSeizenTable";
// Re-export PluginArgsRegistry for module augmentation
diff --git a/packages/core/src/table/styles.css.ts b/packages/core/src/table/styles.css.ts
index b9757d8..afa39b9 100644
--- a/packages/core/src/table/styles.css.ts
+++ b/packages/core/src/table/styles.css.ts
@@ -1,4 +1,4 @@
-import { style, globalStyle } from "@vanilla-extract/css";
+import { style, globalStyle, keyframes } from "@vanilla-extract/css";
/**
* CSS Variables for theming
@@ -54,6 +54,7 @@ export const mainContent = style({
// Wrapper for the main table
export const tableWrapper = style({
+ position: "relative",
flex: 1,
overflow: "auto",
});
@@ -226,3 +227,36 @@ export const paginatorPageNumber = style({
whiteSpace: "nowrap",
padding: "0 8px",
});
+// =============================================================================
+// Loading Overlay Styles
+// =============================================================================
+
+export const loadingOverlay = style({
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: `var(--szui-loading-overlay-bg, rgba(255, 255, 255, 0.8))`,
+ zIndex: 10,
+ backdropFilter: "blur(1px)",
+});
+
+const spinAnimation = keyframes({
+ from: { transform: "rotate(0deg)" },
+ to: { transform: "rotate(360deg)" },
+});
+
+export const spinner = style({
+ width: `var(--szui-spinner-size, 32px)`,
+ height: `var(--szui-spinner-size, 32px)`,
+ color: `var(--szui-spinner-color, #3b82f6)`,
+});
+
+// Global styles for spinner SVG animation
+globalStyle(`${spinner} svg`, {
+ animation: `${spinAnimation} 1s linear infinite`,
+});
diff --git a/packages/core/src/table/useSeizenTable.test.ts b/packages/core/src/table/useSeizenTable.test.ts
index 3c17088..9f7c538 100644
--- a/packages/core/src/table/useSeizenTable.test.ts
+++ b/packages/core/src/table/useSeizenTable.test.ts
@@ -168,22 +168,6 @@ describe("useSeizenTable", () => {
expect(selectedRows).toHaveLength(1);
expect(selectedRows).toContainEqual(data[0]);
});
-
- it("should call onSelectionChange callback when selection changes", () => {
- const data = createTestData();
- const columns = createTestColumns();
- const onSelectionChange = vi.fn();
-
- const { result } = renderHook(() =>
- useSeizenTable({ data, columns, onSelectionChange })
- );
-
- act(() => {
- result.current.setSelectedRows([data[0]]);
- });
-
- expect(onSelectionChange).toHaveBeenCalled();
- });
});
// ===========================================================================
@@ -699,4 +683,202 @@ describe("useSeizenTable", () => {
expect(result.current.getData()).toEqual(newData);
});
});
+
+ // ===========================================================================
+ // Remote Mode Tests
+ // ===========================================================================
+
+ describe("Remote Mode", () => {
+ it("should set remote: false by default", () => {
+ const data = createTestData();
+ const columns = createTestColumns();
+
+ const { result } = renderHook(() => useSeizenTable({ data, columns }));
+
+ expect(result.current.remote).toBe(false);
+ });
+
+ it("should set remote: true when remote option is true", () => {
+ const data = createTestData();
+ const columns = createTestColumns();
+
+ const { result } = renderHook(() =>
+ useSeizenTable({ data, columns, remote: true })
+ );
+
+ expect(result.current.remote).toBe(true);
+ });
+
+ it("should set remote: true when remote option has totalRowCount", () => {
+ const data = createTestData();
+ const columns = createTestColumns();
+
+ const { result } = renderHook(() =>
+ useSeizenTable({ data, columns, remote: { totalRowCount: 100 } })
+ );
+
+ expect(result.current.remote).toBe(true);
+ });
+
+ it("should calculate pageCount from totalRowCount in Remote Mode", () => {
+ const data = createTestData();
+ const columns = createTestColumns();
+
+ const { result } = renderHook(() =>
+ useSeizenTable({ data, columns, remote: { totalRowCount: 55 } })
+ );
+
+ // Default pageSize is 10, so 55 rows = 6 pages
+ const tanstackTable = result.current._tanstackTable;
+ expect(tanstackTable.getPageCount()).toBe(6);
+ });
+
+ it("should update pageCount when pageSize changes", () => {
+ const data = createTestData();
+ const columns = createTestColumns();
+
+ const { result } = renderHook(() =>
+ useSeizenTable({ data, columns, remote: { totalRowCount: 100 } })
+ );
+
+ // Default pageSize is 10, so 100 rows = 10 pages
+ expect(result.current._tanstackTable.getPageCount()).toBe(10);
+
+ // Change pageSize to 20
+ act(() => {
+ result.current.setPageSize(20);
+ });
+
+ // Now 100 rows = 5 pages
+ expect(result.current._tanstackTable.getPageCount()).toBe(5);
+ });
+
+ it("should not filter data internally when remote is enabled", () => {
+ // Create data where only 2 rows would match a filter
+ const data: TestRow[] = [
+ { id: 1, name: "Alice", age: 30, status: "active" },
+ { id: 2, name: "Bob", age: 25, status: "inactive" },
+ { id: 3, name: "Charlie", age: 35, status: "active" },
+ ];
+ const columns = createTestColumns();
+
+ const { result } = renderHook(() =>
+ useSeizenTable({ data, columns, remote: true })
+ );
+
+ // Apply a filter
+ act(() => {
+ result.current.setFilter([
+ { id: "status", value: { operator: "equals", value: "active" } },
+ ]);
+ });
+
+ // In Remote Mode, internal filtering is disabled (manualFiltering: true)
+ // So all rows should still be displayed
+ const tanstackTable = result.current._tanstackTable;
+ expect(tanstackTable.getRowModel().rows).toHaveLength(3);
+ });
+
+ it("should not sort data internally when remote is enabled", () => {
+ const data: TestRow[] = [
+ { id: 3, name: "Charlie", age: 35, status: "active" },
+ { id: 1, name: "Alice", age: 30, status: "active" },
+ { id: 2, name: "Bob", age: 25, status: "inactive" },
+ ];
+ const columns = createTestColumns();
+
+ const { result } = renderHook(() =>
+ useSeizenTable({ data, columns, remote: true })
+ );
+
+ // Apply sorting
+ act(() => {
+ result.current.setSorting([{ id: "id", desc: false }]);
+ });
+
+ // In Remote Mode, internal sorting is disabled (manualSorting: true)
+ // So the order should remain as provided
+ const rows = result.current._tanstackTable.getRowModel().rows;
+ expect(rows[0].original.id).toBe(3);
+ expect(rows[1].original.id).toBe(1);
+ expect(rows[2].original.id).toBe(2);
+ });
+
+ it("should not paginate data internally when remote is enabled", () => {
+ // Create more than one page of data (default pageSize is 10)
+ const data: TestRow[] = Array.from({ length: 15 }, (_, i) => ({
+ id: i + 1,
+ name: `User ${i + 1}`,
+ age: 20 + i,
+ status: "active",
+ }));
+ const columns = createTestColumns();
+
+ const { result } = renderHook(() =>
+ useSeizenTable({ data, columns, remote: { totalRowCount: 15 } })
+ );
+
+ // In Remote Mode, internal pagination is disabled (manualPagination: true)
+ // So all 15 rows should be in the row model (data is pre-paginated externally)
+ const tanstackTable = result.current._tanstackTable;
+ expect(tanstackTable.getRowModel().rows).toHaveLength(15);
+ });
+
+ it("should still update internal filter state for UI sync", () => {
+ const data = createTestData();
+ const columns = createTestColumns();
+
+ const { result } = renderHook(() =>
+ useSeizenTable({ data, columns, remote: true })
+ );
+
+ // Apply a filter
+ act(() => {
+ result.current.setFilter([
+ { id: "status", value: { operator: "equals", value: "active" } },
+ ]);
+ });
+
+ // Internal state should be updated for plugin UI sync
+ expect(result.current.getFilterState()).toEqual([
+ { id: "status", value: { operator: "equals", value: "active" } },
+ ]);
+ });
+
+ it("should still update internal sorting state for UI sync", () => {
+ const data = createTestData();
+ const columns = createTestColumns();
+
+ const { result } = renderHook(() =>
+ useSeizenTable({ data, columns, remote: true })
+ );
+
+ // Apply sorting
+ act(() => {
+ result.current.setSorting([{ id: "name", desc: true }]);
+ });
+
+ // Internal state should be updated for plugin UI sync
+ expect(result.current.getSortingState()).toEqual([
+ { id: "name", desc: true },
+ ]);
+ });
+
+ it("should still update internal pagination state for UI sync", () => {
+ const data = createTestData();
+ const columns = createTestColumns();
+
+ const { result } = renderHook(() =>
+ useSeizenTable({ data, columns, remote: { totalRowCount: 100 } })
+ );
+
+ // Change page
+ act(() => {
+ result.current.setPageIndex(3);
+ });
+
+ // Internal state should be updated for plugin UI sync
+ expect(result.current.getPaginationState().pageIndex).toBe(3);
+ });
+ });
});
diff --git a/packages/core/src/table/useSeizenTable.ts b/packages/core/src/table/useSeizenTable.ts
index fa922d0..de08cf5 100644
--- a/packages/core/src/table/useSeizenTable.ts
+++ b/packages/core/src/table/useSeizenTable.ts
@@ -32,6 +32,18 @@ export type SeizenTableColumn |