;
+export default meta;
+
+const PriceCellRenderer = ({ value }: { value: number }) => (
+
+ {`$${value.toLocaleString(undefined, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ })}`}
+
+);
+
+const ConfidenceCellRenderer = ({ value }: { value: number }) => (
+ {`+/- ${value.toFixed(2)}%`}
+);
+
+const FeedCellRenderer = ({ value }: { value: string }) => (
+
+ }
+ description={value}
+ />
+
+);
+
+const FeedCellRendererLoading = () => (
+
+
+
+);
+
+const args = {
+ columnDefs: [
+ {
+ headerName: "ID",
+ field: "id",
+ },
+ {
+ headerName: "PRICE FEED",
+ field: "feed",
+ cellRenderer: FeedCellRenderer,
+ loadingCellRenderer: FeedCellRendererLoading,
+ flex: 2,
+ },
+ {
+ headerName: "PRICE",
+ field: "price",
+ flex: 3,
+ cellRenderer: PriceCellRenderer,
+ },
+ {
+ headerName: "CONFIDENCE",
+ field: "confidence",
+ cellRenderer: ConfidenceCellRenderer,
+ },
+ ],
+ rowHeight: 70,
+ rowData: dummyRowData,
+};
+
+export const TableGrid = {
+ args,
+} satisfies StoryObj;
+
+export const PriceFeedsCard = {
+ render: (props) => {
+ return ;
+ },
+ args: {
+ ...args,
+ pagination: true,
+ cardProps: {
+ icon: ,
+ title: (
+ <>
+ Price Feeds
+
+ {args.rowData.length}
+
+ >
+ ),
+ },
+ },
+} satisfies StoryObj;
diff --git a/packages/component-library/src/TableGrid/index.tsx b/packages/component-library/src/TableGrid/index.tsx
new file mode 100644
index 0000000000..b0287d97a7
--- /dev/null
+++ b/packages/component-library/src/TableGrid/index.tsx
@@ -0,0 +1,119 @@
+import {
+ AllCommunityModule,
+ ClientSideRowModelModule,
+ ModuleRegistry,
+ TextFilterModule,
+ themeQuartz,
+} from "ag-grid-community";
+import { AgGridReact } from "ag-grid-react";
+import type { ReactNode } from "react";
+import { useCallback, useMemo, useRef, useState } from "react";
+
+import { Card } from "../Card";
+import { Paginator } from "../Paginator";
+import { Skeleton } from "../Skeleton";
+import styles from "./index.module.scss";
+import type { TableGridProps } from "./table-grid-props";
+
+// Register all Community features
+ModuleRegistry.registerModules([
+ AllCommunityModule,
+ TextFilterModule,
+ ClientSideRowModelModule,
+]);
+
+const SkeletonCellRenderer = (props: { value?: ReactNode }) => {
+ if (!props.value) {
+ return (
+
+ );
+ }
+ return {props.value}
;
+};
+
+const DEFAULT_COL_DEF = {
+ cellRenderer: SkeletonCellRenderer,
+ flex: 1,
+};
+
+export const TableGrid = >({
+ rowData,
+ columnDefs,
+ loading,
+ cardProps,
+ pagination,
+ ...props
+}: TableGridProps) => {
+ const gridRef = useRef>(null);
+ const [pageSize, setPageSize] = useState(10);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [totalPages, setTotalPages] = useState(1);
+
+ const mappedColDefs = useMemo(() => {
+ return columnDefs.map((colDef) => {
+ return {
+ ...colDef,
+ // the types in ag-grid are `any` for the cellRenderers which is throwing an error here
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ cellRenderer: loading
+ ? (colDef.loadingCellRenderer ?? SkeletonCellRenderer)
+ : colDef.cellRenderer,
+ };
+ });
+ }, [columnDefs, loading]);
+
+ const onPaginationChanged = useCallback(() => {
+ const api = gridRef.current?.api;
+ if (!api) return;
+ setPageSize(api.paginationGetPageSize());
+ setCurrentPage(api.paginationGetCurrentPage() + 1);
+ setTotalPages(api.paginationGetTotalPages());
+ }, []);
+
+ const onPageChange = useCallback((newPage: number) => {
+ gridRef.current?.api.paginationGoToPage(newPage - 1);
+ }, []);
+
+ const tableGrid = (
+
+ className={styles.tableGrid}
+ // @ts-expect-error empty row data, which is throwing an error here btu required to display 1 row in the loading state
+ rowData={loading ? [[]] : rowData}
+ defaultColDef={DEFAULT_COL_DEF}
+ columnDefs={mappedColDefs}
+ theme={themeQuartz}
+ domLayout="autoHeight"
+ pagination={pagination ?? false}
+ paginationPageSize={pageSize}
+ suppressPaginationPanel
+ onPaginationChanged={onPaginationChanged}
+ ref={gridRef}
+ {...props}
+ />
+ );
+ if (!cardProps && !pagination) {
+ return tableGrid;
+ }
+ return (
+
+ )
+ }
+ {...cardProps}
+ >
+ {tableGrid}
+
+ );
+};
diff --git a/packages/component-library/src/TableGrid/table-grid-props.ts b/packages/component-library/src/TableGrid/table-grid-props.ts
new file mode 100644
index 0000000000..b2af133e49
--- /dev/null
+++ b/packages/component-library/src/TableGrid/table-grid-props.ts
@@ -0,0 +1,17 @@
+import type { ColDef } from "ag-grid-community";
+import type { AgGridReactProps } from "ag-grid-react";
+
+import type { Props as CardProps } from "../Card";
+
+type ExtendedColDef = ColDef & {
+ loadingCellRenderer?: ColDef["cellRenderer"];
+};
+
+export type TableGridProps> = {
+ rowData: TData[];
+ columnDefs: ExtendedColDef[];
+ cardProps?: Omit, "children" | "footer"> & {
+ nonInteractive?: true;
+ };
+ pagination?: boolean;
+} & Omit, "rowData" | "defaultColDef" | "columnDefs">;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6e89786681..66e2d13862 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -147,6 +147,12 @@ catalogs:
'@vercel/functions':
specifier: ^2.0.0
version: 2.0.0
+ ag-grid-community:
+ specifier: ^34.2.0
+ version: 34.2.0
+ ag-grid-react:
+ specifier: ^34.2.0
+ version: 34.2.0
async-cache-dedupe:
specifier: ^3.0.0
version: 3.0.0
@@ -2166,6 +2172,12 @@ importers:
'@react-hookz/web':
specifier: 'catalog:'
version: 25.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ ag-grid-community:
+ specifier: 'catalog:'
+ version: 34.2.0
+ ag-grid-react:
+ specifier: 'catalog:'
+ version: 34.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
bcp-47:
specifier: 'catalog:'
version: 2.1.0
@@ -12010,6 +12022,18 @@ packages:
aes-js@4.0.0-beta.5:
resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==}
+ ag-charts-types@12.2.0:
+ resolution: {integrity: sha512-d2qQrQirt9wP36YW5HPuOvXsiajyiFnr1CTsoCbs02bavPDz7Lk2jHp64+waM4YKgXb3GN7gafbBI9Qgk33BmQ==}
+
+ ag-grid-community@34.2.0:
+ resolution: {integrity: sha512-peS7THEMYwpIrwLQHmkRxw/TlOnddD/F5A88RqlBxf8j+WqVYRWMOOhU5TqymGcha7z2oZ8IoL9ROl3gvtdEjg==}
+
+ ag-grid-react@34.2.0:
+ resolution: {integrity: sha512-dLKFw6hz75S0HLuZvtcwjm+gyiI4gXVzHEu7lWNafWAX0mb8DhogEOP5wbzAlsN6iCfi7bK/cgZImZFjenlqwg==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
@@ -38279,6 +38303,19 @@ snapshots:
aes-js@4.0.0-beta.5: {}
+ ag-charts-types@12.2.0: {}
+
+ ag-grid-community@34.2.0:
+ dependencies:
+ ag-charts-types: 12.2.0
+
+ ag-grid-react@34.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ ag-grid-community: 34.2.0
+ prop-types: 15.8.1
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+
agent-base@6.0.2:
dependencies:
debug: 4.4.0(supports-color@8.1.1)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ad63702af2..583fe85248 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -51,6 +51,8 @@ catalog:
"@amplitude/analytics-browser": ^2.13.0
"@amplitude/plugin-autocapture-browser": ^1.0.0
"@axe-core/react": ^4.10.1
+ "ag-grid-community": ^34.2.0
+ "ag-grid-react": ^34.2.0
"@babel/cli": ^7.27.2
"@babel/core": ^7.27.1
"@babel/preset-typescript": ^7.27.1