feat(client): add Pagination component and integrate with Table and pages
- add Pagination component with prev/next, ellipsis page range, and optional page-size selector - add Pagination and Table pagination stories (WithPagination, WithPaginationAndSizeSelector) - extend Table with optional pageSize and pageSizeOptions props for built-in client-side pagination - add Timestamp column to Table story fixtures - enable pagination on all four pages with pageSize=10 and size selector - add pagination i18n keys to en.json
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Pagination } from '../../src/ui/Pagination/Pagination';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Pagination> = {
|
||||||
|
title: 'UI/Pagination',
|
||||||
|
component: Pagination,
|
||||||
|
parameters: { layout: 'padded' },
|
||||||
|
tags: ['autodocs'],
|
||||||
|
};
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Pagination>;
|
||||||
|
|
||||||
|
function Controlled(args: React.ComponentProps<typeof Pagination>) {
|
||||||
|
const [page, setPage] = useState(args.page);
|
||||||
|
const [pageSize, setPageSize] = useState(args.pageSize);
|
||||||
|
return (
|
||||||
|
<Pagination
|
||||||
|
{...args}
|
||||||
|
page={page}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={args.onPageSizeChange ? setPageSize : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FewPages: Story = {
|
||||||
|
render: (args) => <Controlled {...args} />,
|
||||||
|
args: { page: 1, pageSize: 10, total: 30 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ManyPages: Story = {
|
||||||
|
render: (args) => <Controlled {...args} />,
|
||||||
|
args: { page: 5, pageSize: 10, total: 200 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LastPage: Story = {
|
||||||
|
render: (args) => <Controlled {...args} />,
|
||||||
|
args: { page: 20, pageSize: 10, total: 200 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SinglePage: Story = {
|
||||||
|
render: (args) => <Controlled {...args} />,
|
||||||
|
args: { page: 1, pageSize: 10, total: 7 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithPageSizeSelector: Story = {
|
||||||
|
render: (args) => <Controlled {...args} />,
|
||||||
|
args: {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 150,
|
||||||
|
onPageSizeChange: () => {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
import { Table } from '../../src/ui/Table/Table';
|
import { Table } from '../../src/ui/Table/Table';
|
||||||
import { Badge } from '../../src/ui/Badge/Badge';
|
import { Badge } from '../../src/ui/Badge/Badge';
|
||||||
|
import { Timestamp } from '../../src/ui/Timestamp/Timestamp';
|
||||||
|
|
||||||
const meta: Meta<typeof Table> = {
|
const meta: Meta<typeof Table> = {
|
||||||
title: 'UI/Table',
|
title: 'UI/Table',
|
||||||
@@ -15,53 +16,74 @@ interface User {
|
|||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
status: 'active' | 'inactive';
|
status: 'active' | 'inactive';
|
||||||
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const daysAgo = (n: number) => new Date(Date.now() - n * 86_400_000).toISOString();
|
||||||
|
|
||||||
const users: User[] = [
|
const users: User[] = [
|
||||||
{ id: 1, name: 'Alice Müller', email: 'alice@example.com', status: 'active' },
|
{ id: 1, name: 'Alice Müller', email: 'alice@example.com', status: 'active', updatedAt: daysAgo(1) },
|
||||||
{ id: 2, name: 'Bob Nguyen', email: 'bob@example.com', status: 'inactive' },
|
{ id: 2, name: 'Bob Nguyen', email: 'bob@example.com', status: 'inactive', updatedAt: daysAgo(5) },
|
||||||
{ id: 3, name: 'Carol Santos', email: 'carol@example.com', status: 'active' },
|
{ id: 3, name: 'Carol Santos', email: 'carol@example.com', status: 'active', updatedAt: daysAgo(30) },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const manyUsers: User[] = Array.from({ length: 47 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: `User ${i + 1}`,
|
||||||
|
email: `user${i + 1}@example.com`,
|
||||||
|
status: i % 3 === 0 ? 'inactive' : 'active',
|
||||||
|
updatedAt: daysAgo(i),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const columns: StoryObj<typeof Table<User>>['args'] = {
|
||||||
|
rowKey: (u: User) => u.id,
|
||||||
|
emptyMessage: 'No users found.',
|
||||||
|
columns: [
|
||||||
|
{ key: 'id', header: 'ID', render: (u: User) => u.id, width: 60 },
|
||||||
|
{ key: 'name', header: 'Name', render: (u: User) => u.name },
|
||||||
|
{ key: 'email', header: 'Email', render: (u: User) => u.email },
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
width: 110,
|
||||||
|
render: (u: User) => (
|
||||||
|
<Badge variant={u.status === 'active' ? 'success' : 'neutral'}>{u.status}</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'updated',
|
||||||
|
header: 'Updated',
|
||||||
|
width: 140,
|
||||||
|
render: (u: User) => <Timestamp value={u.updatedAt} />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export const Default: StoryObj<typeof Table<User>> = {
|
export const Default: StoryObj<typeof Table<User>> = {
|
||||||
args: {
|
args: { ...columns, data: users },
|
||||||
data: users,
|
|
||||||
rowKey: (u) => u.id,
|
|
||||||
emptyMessage: 'No users found.',
|
|
||||||
columns: [
|
|
||||||
{ key: 'id', header: 'ID', render: (u) => u.id, width: 60 },
|
|
||||||
{ key: 'name', header: 'Name', render: (u) => u.name },
|
|
||||||
{ key: 'email', header: 'Email', render: (u) => u.email },
|
|
||||||
{
|
|
||||||
key: 'status',
|
|
||||||
header: 'Status',
|
|
||||||
width: 110,
|
|
||||||
render: (u) => (
|
|
||||||
<Badge variant={u.status === 'active' ? 'success' : 'neutral'}>{u.status}</Badge>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Loading: StoryObj<typeof Table<User>> = {
|
export const Loading: StoryObj<typeof Table<User>> = {
|
||||||
args: {
|
args: { ...columns, data: users, loading: true },
|
||||||
...Default.args,
|
|
||||||
loading: true,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Empty: StoryObj<typeof Table<User>> = {
|
export const Empty: StoryObj<typeof Table<User>> = {
|
||||||
args: {
|
args: { ...columns, data: [] },
|
||||||
...Default.args,
|
|
||||||
data: [],
|
|
||||||
emptyMessage: 'No users found.',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleRow: StoryObj<typeof Table<User>> = {
|
export const SingleRow: StoryObj<typeof Table<User>> = {
|
||||||
|
args: { ...columns, data: [users[0]] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithPagination: StoryObj<typeof Table<User>> = {
|
||||||
|
args: { ...columns, data: manyUsers, pageSize: 10 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithPaginationAndSizeSelector: StoryObj<typeof Table<User>> = {
|
||||||
args: {
|
args: {
|
||||||
...Default.args,
|
...columns,
|
||||||
data: [users[0]],
|
data: manyUsers,
|
||||||
|
pageSize: 10,
|
||||||
|
pageSizeOptions: [10, 25, 50],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,5 +41,12 @@
|
|||||||
"switch_to_dark": "Switch to dark theme",
|
"switch_to_dark": "Switch to dark theme",
|
||||||
"light_mode": "Light mode",
|
"light_mode": "Light mode",
|
||||||
"dark_mode": "Dark mode"
|
"dark_mode": "Dark mode"
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"prev": "Previous page",
|
||||||
|
"next": "Next page",
|
||||||
|
"range": "{{from}}–{{to}} of {{total}}",
|
||||||
|
"per_page": "{{count}} per page",
|
||||||
|
"page_size": "Items per page"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export function EnvironmentsPage() {
|
|||||||
rowKey={(e) => e.id}
|
rowKey={(e) => e.id}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
emptyMessage={t('environments.empty')}
|
emptyMessage={t('environments.empty')}
|
||||||
|
pageSize={10}
|
||||||
|
pageSizeOptions={[10, 25, 50]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export function KeysPage() {
|
|||||||
rowKey={(k) => k.name}
|
rowKey={(k) => k.name}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
emptyMessage={t('keys.empty')}
|
emptyMessage={t('keys.empty')}
|
||||||
|
pageSize={10}
|
||||||
|
pageSizeOptions={[10, 25, 50]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ export function ScenariosPage() {
|
|||||||
rowKey={(s) => s.id}
|
rowKey={(s) => s.id}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
emptyMessage={t('scenarios.empty')}
|
emptyMessage={t('scenarios.empty')}
|
||||||
|
pageSize={10}
|
||||||
|
pageSizeOptions={[10, 25, 50]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export function SessionsPage() {
|
|||||||
rowKey={(s) => s.id}
|
rowKey={(s) => s.id}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
emptyMessage={t('sessions.empty')}
|
emptyMessage={t('sessions.empty')}
|
||||||
|
pageSize={10}
|
||||||
|
pageSizeOptions={[10, 25, 50]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
border-top: var(--border-width) solid var(--color-border);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pages {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageBtn,
|
||||||
|
.arrow {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 var(--space-1);
|
||||||
|
border: var(--border-width) solid transparent;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition), border-color var(--transition), color var(--transition);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageBtn:hover:not(:disabled),
|
||||||
|
.arrow:hover:not(:disabled) {
|
||||||
|
background: var(--color-bg);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageBtn.active {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-primary-fg);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 24px;
|
||||||
|
height: 28px;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sizeSelect {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 var(--space-2);
|
||||||
|
border: var(--border-width) solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--color-bg-subtle);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sizeSelect:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import styles from './Pagination.module.css';
|
||||||
|
|
||||||
|
export interface PaginationProps {
|
||||||
|
/** 1-indexed current page */
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
onPageSizeChange?: (size: number) => void;
|
||||||
|
pageSizeOptions?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPages(current: number, last: number): (number | '…')[] {
|
||||||
|
if (last <= 7) return Array.from({ length: last }, (_, i) => i + 1);
|
||||||
|
|
||||||
|
const pages: (number | '…')[] = [1];
|
||||||
|
|
||||||
|
if (current > 3) pages.push('…');
|
||||||
|
|
||||||
|
const start = Math.max(2, current - 1);
|
||||||
|
const end = Math.min(last - 1, current + 1);
|
||||||
|
for (let i = start; i <= end; i++) pages.push(i);
|
||||||
|
|
||||||
|
if (current < last - 2) pages.push('…');
|
||||||
|
|
||||||
|
pages.push(last);
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Pagination({
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
onPageChange,
|
||||||
|
onPageSizeChange,
|
||||||
|
pageSizeOptions = [10, 25, 50],
|
||||||
|
}: PaginationProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const lastPage = Math.max(1, Math.ceil(total / pageSize));
|
||||||
|
const from = Math.min(total, (page - 1) * pageSize + 1);
|
||||||
|
const to = Math.min(total, page * pageSize);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<span className={styles.info}>{t('pagination.range', { from, to, total })}</span>
|
||||||
|
|
||||||
|
<div className={styles.pages}>
|
||||||
|
<button
|
||||||
|
className={styles.arrow}
|
||||||
|
onClick={() => onPageChange(page - 1)}
|
||||||
|
disabled={page <= 1}
|
||||||
|
aria-label={t('pagination.prev')}
|
||||||
|
>
|
||||||
|
<ChevronLeft size={14} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{buildPages(page, lastPage).map((p, i) =>
|
||||||
|
p === '…' ? (
|
||||||
|
<span key={`ellipsis-${i}`} className={styles.ellipsis}>
|
||||||
|
…
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
key={p}
|
||||||
|
className={`${styles.pageBtn} ${p === page ? styles.active : ''}`}
|
||||||
|
onClick={() => onPageChange(p)}
|
||||||
|
aria-current={p === page ? 'page' : undefined}
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={styles.arrow}
|
||||||
|
onClick={() => onPageChange(page + 1)}
|
||||||
|
disabled={page >= lastPage}
|
||||||
|
aria-label={t('pagination.next')}
|
||||||
|
>
|
||||||
|
<ChevronRight size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{onPageSizeChange && (
|
||||||
|
<select
|
||||||
|
className={styles.sizeSelect}
|
||||||
|
value={pageSize}
|
||||||
|
onChange={(e) => {
|
||||||
|
onPageSizeChange(Number(e.target.value));
|
||||||
|
onPageChange(1);
|
||||||
|
}}
|
||||||
|
aria-label={t('pagination.page_size')}
|
||||||
|
>
|
||||||
|
{pageSizeOptions.map((s) => (
|
||||||
|
<option key={s} value={s}>
|
||||||
|
{t('pagination.per_page', { count: s })}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ReactNode } from 'react';
|
import { useState, type ReactNode } from 'react';
|
||||||
|
import { Pagination } from '../Pagination/Pagination';
|
||||||
import styles from './Table.module.css';
|
import styles from './Table.module.css';
|
||||||
|
|
||||||
export interface TableColumn<T> {
|
export interface TableColumn<T> {
|
||||||
@@ -16,6 +17,10 @@ export interface TableProps<T> {
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** Enable built-in client-side pagination with this default page size */
|
||||||
|
pageSize?: number;
|
||||||
|
/** Offer a page-size selector when pagination is enabled */
|
||||||
|
pageSizeOptions?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Table<T>({
|
export function Table<T>({
|
||||||
@@ -25,7 +30,19 @@ export function Table<T>({
|
|||||||
loading = false,
|
loading = false,
|
||||||
emptyMessage = 'No data.',
|
emptyMessage = 'No data.',
|
||||||
className,
|
className,
|
||||||
|
pageSize: defaultPageSize,
|
||||||
|
pageSizeOptions,
|
||||||
}: TableProps<T>) {
|
}: TableProps<T>) {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(defaultPageSize ?? 0);
|
||||||
|
|
||||||
|
const paginated = defaultPageSize !== undefined && !loading;
|
||||||
|
const visibleData = paginated ? data.slice((page - 1) * pageSize, page * pageSize) : data;
|
||||||
|
|
||||||
|
// Reset to page 1 when data changes and current page would be empty
|
||||||
|
const lastPage = pageSize > 0 ? Math.ceil(data.length / pageSize) : 1;
|
||||||
|
const safePage = Math.min(page, Math.max(1, lastPage));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.wrapper} ${className ?? ''}`}>
|
<div className={`${styles.wrapper} ${className ?? ''}`}>
|
||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
@@ -52,14 +69,14 @@ export function Table<T>({
|
|||||||
Loading…
|
Loading…
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : data.length === 0 ? (
|
) : visibleData.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={columns.length} className={styles.empty}>
|
<td colSpan={columns.length} className={styles.empty}>
|
||||||
{emptyMessage}
|
{emptyMessage}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
data.map((row) => (
|
visibleData.map((row) => (
|
||||||
<tr key={rowKey(row)} className={styles.tr}>
|
<tr key={rowKey(row)} className={styles.tr}>
|
||||||
{columns.map((col) => (
|
{columns.map((col) => (
|
||||||
<td
|
<td
|
||||||
@@ -75,6 +92,23 @@ export function Table<T>({
|
|||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{paginated && data.length > 0 && (
|
||||||
|
<Pagination
|
||||||
|
page={safePage}
|
||||||
|
pageSize={pageSize}
|
||||||
|
total={data.length}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={
|
||||||
|
pageSizeOptions
|
||||||
|
? (size) => {
|
||||||
|
setPageSize(size);
|
||||||
|
setPage(1);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
pageSizeOptions={pageSizeOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,5 +24,8 @@ export { ThemeSwitcher } from './ThemeSwitcher/ThemeSwitcher';
|
|||||||
export { Timestamp } from './Timestamp/Timestamp';
|
export { Timestamp } from './Timestamp/Timestamp';
|
||||||
export type { TimestampProps } from './Timestamp/Timestamp';
|
export type { TimestampProps } from './Timestamp/Timestamp';
|
||||||
|
|
||||||
|
export { Pagination } from './Pagination/Pagination';
|
||||||
|
export type { PaginationProps } from './Pagination/Pagination';
|
||||||
|
|
||||||
export { Table } from './Table/Table';
|
export { Table } from './Table/Table';
|
||||||
export type { TableProps, TableColumn } from './Table/Table';
|
export type { TableProps, TableColumn } from './Table/Table';
|
||||||
|
|||||||
Reference in New Issue
Block a user