` | border-bottom, hover bg, selected bg |
+| `TableHead` | `| ` | header height (density), title font, text-default, left-align |
+| `TableCell` | ` | ` | cell padding (density), body font, text-default |
+| `TableCaption` | `` | mt-4, body small, text-placeholder |
+
+## Token Mapping
+
+| Element | State | ToolJet class |
+|---|---|---|
+| Table container | default | `tw-relative tw-w-full tw-overflow-auto` |
+| Table | default | `tw-w-full tw-caption-bottom tw-border-separate tw-border-spacing-0 tw-font-body-default tw-text-text-default` |
+| TableHeader | default | `tw-border-solid tw-border-0 tw-border-b tw-border-border-weak` |
+| TableRow | default | `tw-transition-colors` |
+| TableRow | hover | `hover:tw-bg-interactive-hover` |
+| TableRow | selected | `data-[state=selected]:tw-bg-interactive-selected` |
+| TableHead | default | `tw-h-10 tw-px-3.5 tw-py-0 tw-text-left tw-align-middle tw-font-title-default tw-text-text-default` |
+| TableHead | compact | `tw-h-8 tw-px-3 tw-py-0` |
+| TableCell | default | `tw-h-[52px] tw-p-3.5 tw-align-middle tw-text-text-default first:tw-rounded-l-[10px] last:tw-rounded-r-[10px]` |
+| TableCell | compact | `tw-h-9 tw-px-3 tw-py-2 first:tw-rounded-l-[10px] last:tw-rounded-r-[10px]` |
+| TableFooter | default | `tw-border-solid tw-border-0 tw-border-t tw-border-border-weak tw-bg-background-surface-layer-02 tw-font-title-default` |
+| TableCaption | default | `tw-mt-4 tw-font-body-small tw-text-text-placeholder` |
+
+## CVA Shape
+
+Shape C — sizes only (`density`). Plus a `TableDensityContext` so child components (`TableHead`, `TableCell`) read density from the root `Table`.
+
+## Slots
+
+Standard HTML table slots — consumer composes via sub-components.
+
+## Notes
+
+- Wraps the shadcn `table` primitive (installed at `Rocket/shadcn/table.jsx`).
+- Replaces all shadcn semantic classes (`tw-bg-muted`, `tw-text-foreground`) with ToolJet tokens via className override.
+- `density` prop controls row/cell heights via context — font stays `12px Medium` (`tw-font-title-default`) on header and `12px Regular` (`tw-font-body-default`) on body across both densities.
+- **Borderless rows** — rows do NOT have bottom borders. Visual separation comes from the hover/selected background which uses `rounded-[10px]` corners on the first/last cells, creating a "pill" highlight.
+- The `` itself uses `tw-border-separate tw-border-spacing-0` so the rounded cell corners render correctly (the default `border-collapse` collapses cell borders and breaks the rounded corners).
+- Header has a single `border-bottom` (`tw-border-border-weak`) on `TableHeader` — separates header from body.
+- Hover and selected states use `tw-bg-interactive-hover` and `tw-bg-interactive-selected`.
+- `TableFooter` uses surface-layer-02 bg + medium font weight (slightly emphasized).
+- Higher-level features (sorting indicators, sticky header, loading skeleton, empty state, pagination) live in the `DataTable` block (layer 2), not here.
+- Typography uses `tw-font-*` plugin utilities — never manual font combos.
diff --git a/frontend/src/components/ui/Rocket/Table/Table.stories.jsx b/frontend/src/components/ui/Rocket/Table/Table.stories.jsx
new file mode 100644
index 0000000000..a07b0c173c
--- /dev/null
+++ b/frontend/src/components/ui/Rocket/Table/Table.stories.jsx
@@ -0,0 +1,157 @@
+import React from 'react';
+import { Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption } from './Table';
+
+export default {
+ title: 'Rocket/Table',
+ tags: ['autodocs'],
+ parameters: { layout: 'padded' },
+};
+
+const sampleRows = [
+ { name: 'My first app', editedBy: 'Alice', editedAt: 'Edited 2m ago', type: 'App' },
+ { name: 'Customer dashboard', editedBy: 'Bob', editedAt: 'Edited 1h ago', type: 'App' },
+ { name: 'Sales pipeline', editedBy: 'Carol', editedAt: 'Edited yesterday', type: 'Workflow' },
+ { name: 'HR onboarding', editedBy: 'Dan', editedAt: 'Edited 3 days ago', type: 'Workflow' },
+];
+
+// ── Default ─────────────────────────────────────────────────────────────────
+
+export const Default = {
+ render: () => (
+
+
+
+ Name
+ Type
+ Edited by
+ Edited at
+
+
+
+ {sampleRows.map((row) => (
+
+ {row.name}
+ {row.type}
+ {row.editedBy}
+ {row.editedAt}
+
+ ))}
+
+
+ ),
+};
+
+// ── Compact density ─────────────────────────────────────────────────────────
+
+export const Compact = {
+ render: () => (
+
+
+
+ Name
+ Type
+ Edited by
+ Edited at
+
+
+
+ {sampleRows.map((row) => (
+
+ {row.name}
+ {row.type}
+ {row.editedBy}
+ {row.editedAt}
+
+ ))}
+
+
+ ),
+};
+
+// ── Selected row ────────────────────────────────────────────────────────────
+
+export const SelectedRow = {
+ render: () => (
+
+
+
+ Name
+ Type
+ Edited by
+ Edited at
+
+
+
+ {sampleRows.map((row, i) => (
+
+ {row.name}
+ {row.type}
+ {row.editedBy}
+ {row.editedAt}
+
+ ))}
+
+
+ ),
+};
+
+// ── With Footer ─────────────────────────────────────────────────────────────
+
+export const WithFooter = {
+ render: () => (
+
+
+
+ Item
+ Quantity
+ Price
+
+
+
+
+ Apples
+ 3
+ $3.00
+
+
+ Oranges
+ 2
+ $4.00
+
+
+
+
+ Total
+ 5
+ $7.00
+
+
+
+ ),
+};
+
+// ── With Caption ────────────────────────────────────────────────────────────
+
+export const WithCaption = {
+ render: () => (
+
+ A list of recent apps from your workspace.
+
+
+ Name
+ Edited by
+ Edited at
+
+
+
+ {sampleRows.slice(0, 3).map((row) => (
+
+ {row.name}
+ {row.editedBy}
+ {row.editedAt}
+
+ ))}
+
+
+ ),
+};
diff --git a/frontend/src/components/ui/Rocket/Textarea/Textarea.jsx b/frontend/src/components/ui/Rocket/Textarea/Textarea.jsx
new file mode 100644
index 0000000000..8ed95b4b71
--- /dev/null
+++ b/frontend/src/components/ui/Rocket/Textarea/Textarea.jsx
@@ -0,0 +1,47 @@
+import React, { forwardRef } from 'react';
+import PropTypes from 'prop-types';
+import { cva } from 'class-variance-authority';
+import { cn } from '@/lib/utils';
+import { Textarea as ShadcnTextarea } from '@/components/ui/Rocket/shadcn/textarea';
+
+const textareaVariants = cva(
+ [
+ // Resets not covered by shadcn base (preflight is off)
+ 'tw-appearance-none tw-border-solid tw-outline-none',
+ // Base tokens
+ 'tw-bg-background-surface-layer-01 tw-border-border-default tw-text-text-default tw-shadow-elevation-none',
+ 'tw-text-base',
+ 'placeholder:tw-text-text-placeholder',
+ // Override shadcn focus ring with ToolJet token
+ 'focus-visible:tw-ring-2 focus-visible:tw-ring-interactive-focus-outline !focus-visible:tw-outline-none',
+ // Hover
+ 'hover:tw-border-border-strong',
+ // Error (via aria-invalid)
+ 'aria-[invalid=true]:tw-border-border-danger-strong aria-[invalid=true]:tw-bg-background-error-weak',
+ // Disabled
+ 'disabled:tw-bg-background-surface-layer-02 disabled:tw-text-text-disabled disabled:tw-border-border-disabled',
+ ],
+ {
+ variants: {
+ size: {
+ large: 'tw-px-3 tw-py-2.5 tw-text-lg',
+ default: 'tw-px-3 tw-py-2 tw-text-base',
+ small: 'tw-px-3 tw-py-1.5 tw-text-base',
+ },
+ },
+ defaultVariants: { size: 'default' },
+ }
+);
+
+const Textarea = forwardRef(function Textarea({ className, size, ...props }, ref) {
+ return ;
+});
+
+Textarea.displayName = 'Textarea';
+
+Textarea.propTypes = {
+ size: PropTypes.oneOf(['large', 'default', 'small']),
+ className: PropTypes.string,
+};
+
+export { Textarea, textareaVariants };
diff --git a/frontend/src/components/ui/Rocket/Textarea/Textarea.stories.jsx b/frontend/src/components/ui/Rocket/Textarea/Textarea.stories.jsx
new file mode 100644
index 0000000000..a7d8c4d86a
--- /dev/null
+++ b/frontend/src/components/ui/Rocket/Textarea/Textarea.stories.jsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import { Textarea } from './Textarea';
+import { Field, FieldLabel, FieldDescription, FieldError } from '../Field/Field';
+
+export default {
+ title: 'Rocket/Textarea',
+ component: Textarea,
+ tags: ['autodocs'],
+ parameters: { layout: 'centered' },
+ argTypes: {
+ size: {
+ control: 'select',
+ options: ['large', 'default', 'small'],
+ },
+ disabled: { control: 'boolean' },
+ },
+};
+
+// ── Default ───────────────────────────────────────────────────────────────
+export const Default = {
+ args: { placeholder: 'Enter text...' },
+};
+
+// ── Sizes ─────────────────────────────────────────────────────────────────
+export const Sizes = {
+ render: () => (
+
+
+
+
+
+ ),
+ parameters: { layout: 'padded' },
+};
+
+// ── States ────────────────────────────────────────────────────────────────
+export const States = {
+ render: () => (
+
+
+
+
+
+
+ ),
+ parameters: { layout: 'padded' },
+};
+
+// ── With Field ────────────────────────────────────────────────────────────
+export const WithField = {
+ render: () => (
+
+
+ Description
+
+ Brief summary of the item.
+
+
+
+ Notes
+
+ This field is required.
+
+
+ ),
+ parameters: { layout: 'padded' },
+};
+
+// ── Resizable ─────────────────────────────────────────────────────────────
+export const Resizable = {
+ render: () => (
+
+
+
+
+
+ ),
+ parameters: { layout: 'padded' },
+};
diff --git a/frontend/src/components/ui/Rocket/index.js b/frontend/src/components/ui/Rocket/index.js
index b426355d97..76313b24aa 100644
--- a/frontend/src/components/ui/Rocket/index.js
+++ b/frontend/src/components/ui/Rocket/index.js
@@ -148,3 +148,56 @@ export {
tabsContentClasses,
} from './Tabs/Tabs';
export { Switch, switchClasses } from './Switch/Switch';
+export {
+ AlertDialog,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ alertDialogContentVariants,
+ AlertDialogOverlay,
+ AlertDialogMedia,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogAction,
+ AlertDialogCancel,
+} from './AlertDialog/AlertDialog';
+export {
+ Collapsible,
+ collapsibleVariants,
+ CollapsibleTrigger,
+ collapsibleTriggerVariants,
+ CollapsibleIcon,
+ CollapsibleContent,
+ collapsibleContentVariants,
+} from './Collapsible/Collapsible';
+export { Toaster, toast } from './Sonner/Sonner';
+export {
+ Sheet,
+ SheetTrigger,
+ SheetClose,
+ SheetPortal,
+ SheetOverlay,
+ SheetContent,
+ sheetContentVariants,
+ SheetHeader,
+ SheetBody,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+} from './Sheet/Sheet';
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableRow,
+ TableHead,
+ TableCell,
+ TableCaption,
+} from './Table/Table';
+export { Skeleton, skeletonClasses } from './Skeleton/Skeleton';
+export { Checkbox, checkboxVariants } from './Checkbox/Checkbox';
+export { RadioGroup, RadioGroupItem, radioGroupItemVariants } from './RadioGroup/RadioGroup';
+export { Textarea, textareaVariants } from './Textarea/Textarea';
+export { Spinner, spinnerVariants } from './Spinner/Spinner';
diff --git a/frontend/src/components/ui/Rocket/shadcn/alert-dialog.jsx b/frontend/src/components/ui/Rocket/shadcn/alert-dialog.jsx
new file mode 100644
index 0000000000..28377389df
--- /dev/null
+++ b/frontend/src/components/ui/Rocket/shadcn/alert-dialog.jsx
@@ -0,0 +1,143 @@
+import * as React from 'react';
+import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/Rocket/shadcn/button';
+
+function AlertDialog({ ...props }) {
+ return ;
+}
+
+function AlertDialogTrigger({ ...props }) {
+ return ;
+}
+
+function AlertDialogPortal({ ...props }) {
+ return ;
+}
+
+function AlertDialogOverlay({ className, ...props }) {
+ return (
+
+ );
+}
+
+function AlertDialogContent({ className, size = 'default', ...props }) {
+ return (
+
+
+
+
+ );
+}
+
+function AlertDialogHeader({ className, ...props }) {
+ return (
+
+ );
+}
+
+function AlertDialogFooter({ className, ...props }) {
+ return (
+
+ );
+}
+
+function AlertDialogMedia({ className, ...props }) {
+ return (
+
+ );
+}
+
+function AlertDialogTitle({ className, ...props }) {
+ return (
+
+ );
+}
+
+function AlertDialogDescription({ className, ...props }) {
+ return (
+
+ );
+}
+
+function AlertDialogAction({ className, variant = 'default', size = 'default', ...props }) {
+ return (
+
+ );
+}
+
+function AlertDialogCancel({ className, variant = 'outline', size = 'default', ...props }) {
+ return (
+
+ );
+}
+
+export {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogMedia,
+ AlertDialogOverlay,
+ AlertDialogPortal,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+};
diff --git a/frontend/src/components/ui/Rocket/shadcn/checkbox.jsx b/frontend/src/components/ui/Rocket/shadcn/checkbox.jsx
new file mode 100644
index 0000000000..dc10d8a5e1
--- /dev/null
+++ b/frontend/src/components/ui/Rocket/shadcn/checkbox.jsx
@@ -0,0 +1,27 @@
+import * as React from 'react';
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
+
+import { cn } from '@/lib/utils';
+import { CheckIcon } from 'lucide-react';
+
+function Checkbox({ className, ...props }) {
+ return (
+
+
+
+
+
+ );
+}
+
+export { Checkbox };
diff --git a/frontend/src/components/ui/Rocket/shadcn/collapsible.jsx b/frontend/src/components/ui/Rocket/shadcn/collapsible.jsx
new file mode 100644
index 0000000000..cd881c9c4f
--- /dev/null
+++ b/frontend/src/components/ui/Rocket/shadcn/collapsible.jsx
@@ -0,0 +1,16 @@
+import * as React from 'react';
+import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
+
+function Collapsible({ ...props }) {
+ return ;
+}
+
+function CollapsibleTrigger({ ...props }) {
+ return ;
+}
+
+function CollapsibleContent({ ...props }) {
+ return ;
+}
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent };
diff --git a/frontend/src/components/ui/Rocket/shadcn/radio-group.jsx b/frontend/src/components/ui/Rocket/shadcn/radio-group.jsx
new file mode 100644
index 0000000000..255ba0074a
--- /dev/null
+++ b/frontend/src/components/ui/Rocket/shadcn/radio-group.jsx
@@ -0,0 +1,36 @@
+import * as React from 'react';
+import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
+
+import { cn } from '@/lib/utils';
+
+function RadioGroup({ className, ...props }) {
+ return (
+
+ );
+}
+
+function RadioGroupItem({ className, ...props }) {
+ return (
+
+
+
+
+
+ );
+}
+
+export { RadioGroup, RadioGroupItem };
diff --git a/frontend/src/components/ui/Rocket/shadcn/sheet.jsx b/frontend/src/components/ui/Rocket/shadcn/sheet.jsx
new file mode 100644
index 0000000000..a31b504257
--- /dev/null
+++ b/frontend/src/components/ui/Rocket/shadcn/sheet.jsx
@@ -0,0 +1,109 @@
+import * as React from 'react';
+import * as SheetPrimitive from '@radix-ui/react-dialog';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/Rocket/shadcn/button';
+import { XIcon } from 'lucide-react';
+
+function Sheet({ ...props }) {
+ return ;
+}
+
+function SheetTrigger({ ...props }) {
+ return ;
+}
+
+function SheetClose({ ...props }) {
+ return ;
+}
+
+function SheetPortal({ ...props }) {
+ return ;
+}
+
+function SheetOverlay({ className, ...props }) {
+ return (
+
+ );
+}
+
+function SheetContent({ className, children, side = 'right', showCloseButton = true, ...props }) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+
+ )}
+
+
+ );
+}
+
+function SheetHeader({ className, ...props }) {
+ return ;
+}
+
+function SheetFooter({ className, ...props }) {
+ return (
+
+ );
+}
+
+function SheetTitle({ className, ...props }) {
+ return (
+
+ );
+}
+
+function SheetDescription({ className, ...props }) {
+ return (
+
+ );
+}
+
+export {
+ Sheet,
+ SheetTrigger,
+ SheetClose,
+ SheetPortal,
+ SheetOverlay,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+};
diff --git a/frontend/src/components/ui/Rocket/shadcn/skeleton.jsx b/frontend/src/components/ui/Rocket/shadcn/skeleton.jsx
new file mode 100644
index 0000000000..0c794212b8
--- /dev/null
+++ b/frontend/src/components/ui/Rocket/shadcn/skeleton.jsx
@@ -0,0 +1,10 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+function Skeleton({ className, ...props }) {
+ return (
+
+ );
+}
+
+export { Skeleton };
diff --git a/frontend/src/components/ui/Rocket/shadcn/sonner.jsx b/frontend/src/components/ui/Rocket/shadcn/sonner.jsx
new file mode 100644
index 0000000000..a0ce4d22cf
--- /dev/null
+++ b/frontend/src/components/ui/Rocket/shadcn/sonner.jsx
@@ -0,0 +1,32 @@
+import * as React from 'react';
+import { Toaster as Sonner } from 'sonner';
+import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from 'lucide-react';
+
+const Toaster = ({ ...props }) => {
+ return (
+ ,
+ info: ,
+ warning: ,
+ error: ,
+ loading: ,
+ }}
+ style={{
+ '--normal-bg': 'var(--popover)',
+ '--normal-text': 'var(--popover-foreground)',
+ '--normal-border': 'var(--border)',
+ '--border-radius': 'var(--radius)',
+ }}
+ toastOptions={{
+ classNames: {
+ toast: 'cn-toast',
+ },
+ }}
+ {...props}
+ />
+ );
+};
+
+export { Toaster };
diff --git a/frontend/src/components/ui/Rocket/shadcn/table.jsx b/frontend/src/components/ui/Rocket/shadcn/table.jsx
new file mode 100644
index 0000000000..a9b2125522
--- /dev/null
+++ b/frontend/src/components/ui/Rocket/shadcn/table.jsx
@@ -0,0 +1,77 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+function Table({ className, ...props }) {
+ return (
+
+ );
+}
+
+function TableHeader({ className, ...props }) {
+ return ;
+}
+
+function TableBody({ className, ...props }) {
+ return ;
+}
+
+function TableFooter({ className, ...props }) {
+ return (
+ tr]:last:tw-border-b-0', className)}
+ {...props}
+ />
+ );
+}
+
+function TableRow({ className, ...props }) {
+ return (
+
+ );
+}
+
+function TableHead({ className, ...props }) {
+ return (
+ |
+ );
+}
+
+function TableCell({ className, ...props }) {
+ return (
+ |
+ );
+}
+
+function TableCaption({ className, ...props }) {
+ return (
+
+ );
+}
+
+export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
diff --git a/frontend/src/components/ui/blocks/DataTable/DataTable.jsx b/frontend/src/components/ui/blocks/DataTable/DataTable.jsx
new file mode 100644
index 0000000000..a424f26da4
--- /dev/null
+++ b/frontend/src/components/ui/blocks/DataTable/DataTable.jsx
@@ -0,0 +1,78 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { flexRender } from '@tanstack/react-table';
+
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/Rocket/Table/Table';
+import { TableSkeleton } from '@/components/ui/blocks/TableSkeleton';
+
+/**
+ * DataTable — TanStack-driven table block.
+ *
+ * Pass a TanStack `table` instance (from `useReactTable`). This block renders
+ * the header, body (or skeleton during loading), and a "no results" empty state.
+ *
+ * Higher-level features (sorting indicators, pagination controls, toolbar) are
+ * the responsibility of the consuming feature, not this block.
+ */
+function DataTableInternal({ table, isLoading = false, skeleton, density = 'default', emptyMessage = 'No results.' }) {
+ const columnCount = table?.getAllColumns()?.length || 4;
+
+ return (
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
+
+ ))}
+
+ ))}
+
+
+ {isLoading ? (
+ skeleton ||
+ ) : (
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ ))}
+
+ ))
+ ) : (
+
+
+ {emptyMessage}
+
+
+ )}
+
+ )}
+
+
+ );
+}
+DataTableInternal.displayName = 'DataTable';
+DataTableInternal.propTypes = {
+ // TanStack `table` instance — required, but PropTypes can't validate object shape easily
+ // eslint-disable-next-line react/forbid-prop-types
+ table: PropTypes.object.isRequired,
+ isLoading: PropTypes.bool,
+ skeleton: PropTypes.node,
+ density: PropTypes.oneOf(['default', 'compact']),
+ emptyMessage: PropTypes.string,
+};
+
+export const DataTable = React.memo(DataTableInternal);
diff --git a/frontend/src/components/ui/blocks/DataTable/DataTable.stories.jsx b/frontend/src/components/ui/blocks/DataTable/DataTable.stories.jsx
new file mode 100644
index 0000000000..2562814c47
--- /dev/null
+++ b/frontend/src/components/ui/blocks/DataTable/DataTable.stories.jsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { useReactTable, getCoreRowModel, createColumnHelper } from '@tanstack/react-table';
+import { DataTable } from './DataTable';
+
+export default {
+ title: 'Blocks/DataTable',
+ tags: ['autodocs'],
+ parameters: { layout: 'padded' },
+};
+
+const sampleData = [
+ { id: 1, name: 'My first app', type: 'App', editedBy: 'Alice', editedAt: 'Edited 2m ago' },
+ { id: 2, name: 'Customer dashboard', type: 'App', editedBy: 'Bob', editedAt: 'Edited 1h ago' },
+ { id: 3, name: 'Sales pipeline', type: 'Workflow', editedBy: 'Carol', editedAt: 'Edited yesterday' },
+ { id: 4, name: 'HR onboarding', type: 'Workflow', editedBy: 'Dan', editedAt: 'Edited 3 days ago' },
+ { id: 5, name: 'Inventory tracker', type: 'App', editedBy: 'Eve', editedAt: 'Edited last week' },
+];
+
+const columnHelper = createColumnHelper();
+const columns = [
+ columnHelper.accessor('name', { header: 'Name', cell: (info) => info.getValue() }),
+ columnHelper.accessor('type', { header: 'Type', cell: (info) => info.getValue() }),
+ columnHelper.accessor('editedBy', { header: 'Edited by', cell: (info) => info.getValue() }),
+ columnHelper.accessor('editedAt', { header: 'Edited at', cell: (info) => info.getValue() }),
+];
+
+function useTable(data) {
+ return useReactTable({
+ data,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ });
+}
+
+// ── Default ─────────────────────────────────────────────────────────────────
+
+export const Default = {
+ render: () => {
+ const table = useTable(sampleData);
+ return ;
+ },
+};
+
+// ── Compact ─────────────────────────────────────────────────────────────────
+
+export const Compact = {
+ render: () => {
+ const table = useTable(sampleData);
+ return ;
+ },
+};
+
+// ── Loading ─────────────────────────────────────────────────────────────────
+
+export const Loading = {
+ render: () => {
+ const table = useTable([]);
+ return ;
+ },
+};
+
+// ── Empty ───────────────────────────────────────────────────────────────────
+
+export const Empty = {
+ render: () => {
+ const table = useTable([]);
+ return ;
+ },
+};
diff --git a/frontend/src/components/ui/blocks/DataTable/index.js b/frontend/src/components/ui/blocks/DataTable/index.js
new file mode 100644
index 0000000000..beee5f02af
--- /dev/null
+++ b/frontend/src/components/ui/blocks/DataTable/index.js
@@ -0,0 +1 @@
+export { DataTable } from './DataTable';
diff --git a/frontend/src/components/ui/blocks/TableSkeleton/TableSkeleton.jsx b/frontend/src/components/ui/blocks/TableSkeleton/TableSkeleton.jsx
new file mode 100644
index 0000000000..62f313d306
--- /dev/null
+++ b/frontend/src/components/ui/blocks/TableSkeleton/TableSkeleton.jsx
@@ -0,0 +1,37 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { TableBody, TableCell, TableRow } from '@/components/ui/Rocket/Table/Table';
+import { Skeleton } from '@/components/ui/Rocket/Skeleton/Skeleton';
+
+/**
+ * Generic table skeleton — renders pulsing placeholder rows.
+ * Designed to be used inside a `` while data is loading,
+ * replacing the real ``.
+ */
+function TableSkeleton({ rowCount = 5, columnCount = 4 }) {
+ const rows = React.useMemo(
+ () => Array.from({ length: rowCount }, (_, i) => ({ id: `table-skeleton-${i}` })),
+ [rowCount]
+ );
+
+ return (
+
+ {rows.map((row) => (
+
+ {Array.from({ length: columnCount }, (_, colIndex) => (
+
+
+
+ ))}
+
+ ))}
+
+ );
+}
+TableSkeleton.displayName = 'TableSkeleton';
+TableSkeleton.propTypes = {
+ rowCount: PropTypes.number,
+ columnCount: PropTypes.number,
+};
+
+export { TableSkeleton };
diff --git a/frontend/src/components/ui/blocks/TableSkeleton/index.js b/frontend/src/components/ui/blocks/TableSkeleton/index.js
new file mode 100644
index 0000000000..acc0989301
--- /dev/null
+++ b/frontend/src/components/ui/blocks/TableSkeleton/index.js
@@ -0,0 +1 @@
+export { TableSkeleton } from './TableSkeleton';
|