From dd380416c8880546c08ff46cc72b69a18bf73793 Mon Sep 17 00:00:00 2001 From: John Betancur <1385932+jbetancur@users.noreply.github.com> Date: Fri, 22 May 2026 21:54:15 -0400 Subject: [PATCH 1/2] feat: add sitemap integration and update site configuration - Integrated `@astrojs/sitemap` for automatic sitemap generation. - Updated site URL in `astro.config.mjs`. - Added `robots.txt` to allow search engines to crawl the site. - Updated package dependencies to include `@astrojs/sitemap`. - Enhanced various documentation pages with descriptions for better SEO. - Improved layout and styling in the main page and other components. - Refactored the recipes page to use a more concise format. - Added social proof statistics for weekly downloads and GitHub stars. --- README.md | 4 +- apps/docs/astro.config.mjs | 4 +- apps/docs/package-lock.json | 70 ++++ apps/docs/package.json | 1 + apps/docs/public/robots.txt | 4 + apps/docs/src/components/CodeBlock.astro | 6 +- apps/docs/src/layouts/Layout.astro | 2 +- apps/docs/src/pages/docs/accessibility.md | 1 + apps/docs/src/pages/docs/api.md | 1 + apps/docs/src/pages/docs/changelog.astro | 2 +- .../docs/src/pages/docs/getting-started.astro | 4 +- apps/docs/src/pages/docs/installation.astro | 2 +- apps/docs/src/pages/docs/localization.astro | 2 +- apps/docs/src/pages/docs/migration.md | 1 + apps/docs/src/pages/docs/performance.astro | 2 +- apps/docs/src/pages/docs/recipes.astro | 393 +++--------------- apps/docs/src/pages/docs/ssr.astro | 2 +- apps/docs/src/pages/docs/typescript.astro | 2 +- apps/docs/src/pages/docs/whats-new.astro | 2 +- apps/docs/src/pages/index.astro | 89 +++- apps/docs/src/pages/support.astro | 4 +- package.json | 13 +- 22 files changed, 234 insertions(+), 377 deletions(-) create mode 100644 apps/docs/public/robots.txt diff --git a/README.md b/README.md index 64890ea5..308eb44d 100644 --- a/README.md +++ b/README.md @@ -42,11 +42,11 @@ The documentation contains information about installation, usage and contributio # Supporting React Data Table Component -React Data Table Component is maintained by one person and downloaded ~200k times a week. If your team ships products with it, your support keeps it maintained, bug-free, and moving forward. +React Data Table Component has been actively maintained since 2018 and is downloaded ~215k times a week. If your team ships products with it, your support keeps it maintained, bug-free, and moving forward. ## Sponsor the project -Sponsoring puts your company logo in front of ~200k developers a week: in the README, the docs site, and every release. It's the right move if your team depends on this library and you want it to keep improving. +Sponsoring puts your company logo in front of ~215k developers a week: in the README, the docs site, and every release. It's the right move if your team depends on this library and you want it to keep improving. | Tier | Price/month | Perk | | --- | --- | --- | diff --git a/apps/docs/astro.config.mjs b/apps/docs/astro.config.mjs index 36abf2c6..a5d32c16 100644 --- a/apps/docs/astro.config.mjs +++ b/apps/docs/astro.config.mjs @@ -1,12 +1,14 @@ import { defineConfig } from 'astro/config'; import react from '@astrojs/react'; import tailwind from '@astrojs/tailwind'; +import sitemap from '@astrojs/sitemap'; export default defineConfig({ + site: 'https://reactdatatable.com', markdown: { shikiConfig: { theme: 'catppuccin-macchiato' }, }, - integrations: [react(), tailwind({ applyBaseStyles: false })], + integrations: [react(), tailwind({ applyBaseStyles: false }), sitemap()], vite: { resolve: { alias: { diff --git a/apps/docs/package-lock.json b/apps/docs/package-lock.json index de6b984b..34fba527 100644 --- a/apps/docs/package-lock.json +++ b/apps/docs/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@astrojs/react": "^4.2.0", + "@astrojs/sitemap": "^3.7.2", "@astrojs/tailwind": "^5.1.4", "@shikijs/langs": "^4.1.0", "astro": "^5.7.0", @@ -109,6 +110,26 @@ "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.2.tgz", + "integrity": "sha512-PqkzkcZTb5ICiyIR8VoKbIAP/laNRXi5tw616N1Ckk+40oNB8Can1AzVV56lrbC5GKSZFCyJYUVYqVivMisvpA==", + "license": "MIT", + "dependencies": { + "sitemap": "^9.0.0", + "stream-replace-string": "^2.0.0", + "zod": "^4.3.6" + } + }, + "node_modules/@astrojs/sitemap/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@astrojs/tailwind": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/@astrojs/tailwind/-/tailwind-5.1.5.tgz", @@ -2128,6 +2149,15 @@ "@types/unist": "*" } }, + "node_modules/@types/node": { + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -2153,6 +2183,15 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -6175,6 +6214,25 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, + "node_modules/sitemap": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz", + "integrity": "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^24.9.2", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" + }, + "bin": { + "sitemap": "dist/esm/cli.js" + }, + "engines": { + "node": ">=20.19.5", + "npm": ">=10.8.2" + } + }, "node_modules/smol-toml": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", @@ -6206,6 +6264,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stream-replace-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", + "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", + "license": "MIT" + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -6588,6 +6652,12 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", diff --git a/apps/docs/package.json b/apps/docs/package.json index 4217ad81..ebfd2ad3 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@astrojs/react": "^4.2.0", + "@astrojs/sitemap": "^3.7.2", "@astrojs/tailwind": "^5.1.4", "@shikijs/langs": "^4.1.0", "astro": "^5.7.0", diff --git a/apps/docs/public/robots.txt b/apps/docs/public/robots.txt new file mode 100644 index 00000000..4257a012 --- /dev/null +++ b/apps/docs/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://reactdatatable.com/sitemap-index.xml diff --git a/apps/docs/src/components/CodeBlock.astro b/apps/docs/src/components/CodeBlock.astro index dffaf75f..38a93dff 100644 --- a/apps/docs/src/components/CodeBlock.astro +++ b/apps/docs/src/components/CodeBlock.astro @@ -13,7 +13,7 @@ const { code, lang = 'tsx' } = Astro.props;
@@ -35,14 +34,11 @@ const { code, lang = 'tsx' } = Astro.props; await navigator.clipboard.writeText(code); const iconCopy = btn.querySelector('.icon-copy'); const iconCheck = btn.querySelector('.icon-check'); - const label = btn.querySelector('.label'); iconCopy?.classList.add('hidden'); iconCheck?.classList.remove('hidden'); - if (label) label.textContent = 'Copied!'; setTimeout(() => { iconCopy?.classList.remove('hidden'); iconCheck?.classList.add('hidden'); - if (label) label.textContent = 'Copy'; }, 2000); }); }); diff --git a/apps/docs/src/layouts/Layout.astro b/apps/docs/src/layouts/Layout.astro index ef71a870..507d1800 100644 --- a/apps/docs/src/layouts/Layout.astro +++ b/apps/docs/src/layouts/Layout.astro @@ -28,6 +28,7 @@ const { + {title} @@ -151,7 +152,6 @@ const {

Apache 2.0 licensed · GitHub - · Built with Astro

diff --git a/apps/docs/src/pages/docs/accessibility.md b/apps/docs/src/pages/docs/accessibility.md index 6b285dd4..2d31ffbb 100644 --- a/apps/docs/src/pages/docs/accessibility.md +++ b/apps/docs/src/pages/docs/accessibility.md @@ -1,6 +1,7 @@ --- layout: '../../layouts/DocsLayout.astro' title: 'Accessibility | react-data-table-component' +description: 'ARIA roles, keyboard navigation, and screen reader support in react-data-table-component.' --- diff --git a/apps/docs/src/pages/docs/api.md b/apps/docs/src/pages/docs/api.md index eb2ad88f..9733d6af 100644 --- a/apps/docs/src/pages/docs/api.md +++ b/apps/docs/src/pages/docs/api.md @@ -1,6 +1,7 @@ --- layout: '../../layouts/DocsLayout.astro' title: 'API reference | react-data-table-component' +description: 'Complete prop, type, and export reference for react-data-table-component v8.' --- # API reference diff --git a/apps/docs/src/pages/docs/changelog.astro b/apps/docs/src/pages/docs/changelog.astro index 4f1a286c..d582e3ae 100644 --- a/apps/docs/src/pages/docs/changelog.astro +++ b/apps/docs/src/pages/docs/changelog.astro @@ -3,7 +3,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro'; import CodeBlock from '../../components/CodeBlock.astro'; --- - +

Changelog

diff --git a/apps/docs/src/pages/docs/getting-started.astro b/apps/docs/src/pages/docs/getting-started.astro index a513e697..38672070 100644 --- a/apps/docs/src/pages/docs/getting-started.astro +++ b/apps/docs/src/pages/docs/getting-started.astro @@ -3,7 +3,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro'; import CodeBlock from '../../components/CodeBlock.astro'; --- - +

Getting started

@@ -55,7 +55,7 @@ export default function App() { paginationbooleanEnable built-in pagination selectableRowsbooleanEnable row checkboxes expandableRowsbooleanEnable expandable row panels - themestringNamed theme (e.g. "slate-dark") + themestringNamed theme (e.g. "material", "catppuccin", "crisp") animateRowsbooleanStaggered row entrance + sort animation columnSeparatorboolean | "full"Body column separators (headers always shown) diff --git a/apps/docs/src/pages/docs/installation.astro b/apps/docs/src/pages/docs/installation.astro index 637e0b03..c1ed58ec 100644 --- a/apps/docs/src/pages/docs/installation.astro +++ b/apps/docs/src/pages/docs/installation.astro @@ -3,7 +3,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro'; import CodeBlock from '../../components/CodeBlock.astro'; --- - +

Installation

npm

diff --git a/apps/docs/src/pages/docs/localization.astro b/apps/docs/src/pages/docs/localization.astro index cfdcb1df..304c078a 100644 --- a/apps/docs/src/pages/docs/localization.astro +++ b/apps/docs/src/pages/docs/localization.astro @@ -3,7 +3,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro'; import CodeBlock from '../../components/CodeBlock.astro'; --- - +

Localization

diff --git a/apps/docs/src/pages/docs/migration.md b/apps/docs/src/pages/docs/migration.md index 95af14ba..4e1c36ca 100644 --- a/apps/docs/src/pages/docs/migration.md +++ b/apps/docs/src/pages/docs/migration.md @@ -1,6 +1,7 @@ --- layout: '../../layouts/DocsLayout.astro' title: 'Migration guide | react-data-table-component' +description: 'Step-by-step upgrade instructions and breaking changes for each major version of react-data-table-component.' --- # Migration guide diff --git a/apps/docs/src/pages/docs/performance.astro b/apps/docs/src/pages/docs/performance.astro index 7542634b..6b7788a3 100644 --- a/apps/docs/src/pages/docs/performance.astro +++ b/apps/docs/src/pages/docs/performance.astro @@ -3,7 +3,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro'; import CodeBlock from '../../components/CodeBlock.astro'; --- - +

Performance

diff --git a/apps/docs/src/pages/docs/recipes.astro b/apps/docs/src/pages/docs/recipes.astro index 35deb0dd..71f004bb 100644 --- a/apps/docs/src/pages/docs/recipes.astro +++ b/apps/docs/src/pages/docs/recipes.astro @@ -1,355 +1,64 @@ --- import DocsLayout from '../../layouts/DocsLayout.astro'; -import CodeBlock from '../../components/CodeBlock.astro'; + +const recipes = [ + { + href: '/docs/recipes/bulk-action-toolbar', + title: 'Approval workflow', + desc: 'Bulk-approve or reject requests. Toolbar buttons adapt to the selected rows\' status — already-resolved rows are excluded automatically.', + }, + { + href: '/docs/recipes/server-side', + title: 'Server-side sort, page & filter', + desc: 'Wire sorting, pagination, and column filters to your own API in a single useEffect.', + }, + { + href: '/docs/recipes/editable-grid', + title: 'URL-synced table state', + desc: 'Sync sort, filters, and pagination to the URL — shareable links, reload-safe, back button works.', + }, + { + href: '/docs/recipes/master-detail', + title: 'Dashboard with drill-down', + desc: 'Department overview with expandable rows, health-status row tinting, and inline budget-spend bars.', + }, + { + href: '/docs/recipes/sticky-footer', + title: 'Audit log viewer', + desc: 'Fixed header, severity row tinting, and multi-field live search — all wired together.', + }, + { + href: '/docs/recipes/persist-column-widths', + title: 'Persist column widths', + desc: 'Save and restore resizable column widths to localStorage or a back-end API.', + }, + { + href: '/docs/recipes/row-grouping', + title: 'Inline row actions', + desc: 'Per-row ⋮ menu with edit, duplicate, and delete — confirmation modals included.', + }, +]; --- - +

Recipes

- Working patterns for common product needs. Each recipe is self-contained. Copy into your + Working patterns for common product needs. Each recipe is self-contained — copy into your project and adjust.

-

Persist column widths

- -

- Pass onColumnResize to receive the settled width after every drag, and - initialColumnWidths to hydrate those widths on mount. The callback receives - the resized column's id, its new width in px, and the full widths map. Write it - to localStorage, a database, or anywhere else. -

- -

localStorage

- - { - try { return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}'); } - catch { return {}; } -} - -const columns: TableColumn[] = [ - { id: 'name', name: 'Name', selector: r => r.name }, - { id: 'salary', name: 'Salary', selector: r => r.salary, right: true }, -]; - -export default function App() { - const [initialWidths] = useState(loadWidths); - - return ( - - localStorage.setItem(STORAGE_KEY, JSON.stringify(all)) - } - /> - ); -}`} /> - -

Database / back-end

- -

- Swap the callback body to call your API instead. Wrap with a debounce if you want to - batch rapid resizes into a single request. -

- - >({}); - - useEffect(() => { - api.getColumnPrefs(userId).then(setInitialWidths); - }, [userId]); - - const handleResize = useCallback( - debounce((_id: string | number, _w: number, all: Record) => { - api.saveColumnPrefs(userId, all); - }, 500), - [userId], - ); - - return ( - - ); -}`} /> - -

Server-side sort, page, and filter

- -

- Enable sortServer, paginationServer, and pass controlled - filterValues. Refetch from a single effect that depends on every state knob: -

- - ([]); - const [total, setTotal] = useState(0); - const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(20); - const [sort, setSort] = useState<{ id?: string; dir: SortOrder }>({ dir: SortOrder.ASC }); - const [filters, setFilters] = useState>({}); - const [loading, setLoading] = useState(false); - - useEffect(() => { - let cancelled = false; - setLoading(true); - fetchEmployees({ page, perPage, sort, filters }).then(res => { - if (cancelled) return; - setRows(res.rows); - setTotal(res.total); - setLoading(false); - }); - return () => { cancelled = true; }; - }, [page, perPage, sort, filters]); - - const columns: TableColumn[] = [ - { id: 'name', name: 'Name', selector: r => r.name, sortable: true, filterable: true }, - { id: 'salary', name: 'Salary', selector: r => r.salary, sortable: true, filterable: true, - filterType: 'number', right: true }, - ]; - - return ( - setSort({ id: col.id as string, dir })} - - filterValues={filters} - onFilterChange={(columnId, next) => - setFilters(prev => ({ ...prev, [columnId]: next })) - } - /> - ); -}`} /> - -

Bulk-action toolbar

- -

- v8 removed the built-in contextMessage and contextActions props. - The recommended replacement is a toolbar rendered outside the table, driven by - onSelectedRowsChange. This gives you full control over layout, styling, - and what actions appear. -

- -

- Show the toolbar only when rows are selected, display a count and names, and use the - imperative ref to clear selection after the action completes. -

- - [] = [ - { name: 'Name', selector: r => r.name }, - { name: 'Department', selector: r => r.department }, -]; - -export default function App() { - const ref = useRef(null); - const [selected, setSelected] = useState([]); - - async function handleArchive() { - await api.archive(selected.map(r => r.id)); - ref.current?.clearSelectedRows(); - } - - async function handleExport() { - await api.export(selected.map(r => r.id)); - ref.current?.clearSelectedRows(); - } - - return ( -
- {/* Toolbar — only visible when rows are selected */} - {selected.length > 0 && ( -
- - {selected.length} selected: {selected.map(r => r.name).join(', ')} - -
- - - -
+
+ {recipes.map(({ href, title, desc }) => ( + +
+ {title} →
- )} - - setSelected(selectedRows)} - /> -
- ); -}`} /> - -

Master/detail with a nested table

- -

- Render a second DataTable inside expandableRowsComponent: -

- - [] = [ - { name: 'SKU', selector: i => i.sku }, - { name: 'Qty', selector: i => i.qty, center: true, width: '80px' }, - { name: 'Price', selector: i => i.price, right: true, width: '110px', - format: i => \`$\${i.price.toFixed(2)}\` }, -]; - -function OrderDetail({ data }: ExpanderComponentProps) { - return ( -
- -
- ); -} - -`} /> - -

Sticky footer with totals

- -

- Use the built-in footer — add a footer field on any - column and the table renders an aligned totals row automatically. If you'd rather render - your own summary outside the table, the pattern below still works: -

- - ({ salary: acc.salary + r.salary, headcount: acc.headcount + 1 }), - { salary: 0, headcount: 0 }, -); - -<> - -
- {totals.headcount} employees - Total: \${totals.salary.toLocaleString()} +

{desc}

+ + ))}
-`} /> - -

Excel-style editable grid

- -

- Combine editable: true, onCellEdit, and an immer-style - reducer for a spreadsheet feel. See the inline editing - docs for the full pattern. -

- - [] = [ - { id: 'name', name: 'Name', selector: r => r.name, editable: true, onCellEdit }, - { id: 'quantity', name: 'Quantity', selector: r => r.quantity, editable: true, onCellEdit, - right: true, width: '110px' }, - { id: 'price', name: 'Price', selector: r => r.price, editable: true, onCellEdit, - right: true, width: '110px', - format: r => \`$\${r.price.toFixed(2)}\` }, - { id: 'total', name: 'Total', selector: r => r.quantity * r.price, - right: true, width: '110px', - format: r => \`$\${(r.quantity * r.price).toFixed(2)}\` }, -];`} /> - -

Row grouping (manual)

- -

- The library doesn't include row grouping, but you can fake it by pre-grouping your data - and using conditionalRowStyles to highlight group headers: -

- - e.dept === 'Engineering'), - { __group: 'Product' }, - ...employees.filter(e => e.dept === 'Product'), -]; - - '__group' in row - ? {row.__group} - : row.name, - }, - // ... other columns ... - ]} - conditionalRowStyles={[ - { when: row => '__group' in row, - style: { background: '#f3f4f6', fontWeight: 600 } }, - ]} -/>`} /> - -

CSV export

- -

- No built-in exporter. For typical sizes, a 20-line client-side function is fine: -

- - (rows: T[], columns: TableColumn[], filename = 'export.csv') { - const header = columns.map(c => JSON.stringify(c.name)).join(','); - const lines = rows.map(row => - columns - .map(c => JSON.stringify(c.selector ? String(c.selector(row)) : '')) - .join(','), - ); - const csv = [header, ...lines].join('\\n'); - const blob = new Blob([csv], { type: 'text/csv' }); - const url = URL.createObjectURL(blob); - const a = Object.assign(document.createElement('a'), { href: url, download: filename }); - a.click(); - URL.revokeObjectURL(url); -} - -`} /> diff --git a/apps/docs/src/pages/docs/ssr.astro b/apps/docs/src/pages/docs/ssr.astro index 72630cac..41437f0c 100644 --- a/apps/docs/src/pages/docs/ssr.astro +++ b/apps/docs/src/pages/docs/ssr.astro @@ -3,7 +3,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro'; import CodeBlock from '../../components/CodeBlock.astro'; --- - +

Server-Side Rendering

diff --git a/apps/docs/src/pages/docs/typescript.astro b/apps/docs/src/pages/docs/typescript.astro index cdd44989..6644b1f1 100644 --- a/apps/docs/src/pages/docs/typescript.astro +++ b/apps/docs/src/pages/docs/typescript.astro @@ -3,7 +3,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro'; import CodeBlock from '../../components/CodeBlock.astro'; --- - +

TypeScript

diff --git a/apps/docs/src/pages/docs/whats-new.astro b/apps/docs/src/pages/docs/whats-new.astro index a0709447..8b81c3cc 100644 --- a/apps/docs/src/pages/docs/whats-new.astro +++ b/apps/docs/src/pages/docs/whats-new.astro @@ -3,7 +3,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro'; import CodeBlock from '../../components/CodeBlock.astro'; --- - +

What's new in v8

diff --git a/apps/docs/src/pages/index.astro b/apps/docs/src/pages/index.astro index 4f8c7eb5..3e49d356 100644 --- a/apps/docs/src/pages/index.astro +++ b/apps/docs/src/pages/index.astro @@ -2,9 +2,29 @@ import { Code } from 'astro:components'; import Layout from '../layouts/Layout.astro'; import LiveDemo from '../components/LiveDemo.tsx'; + +const [npmRes, ghRes] = await Promise.allSettled([ + fetch('https://api.npmjs.org/downloads/point/last-week/react-data-table-component'), + fetch('https://api.github.com/repos/jbetancur/react-data-table-component'), +]); + +const weeklyDownloads: number = + npmRes.status === 'fulfilled' && npmRes.value.ok + ? (await npmRes.value.json()).downloads + : 215000; + +const stars: number = + ghRes.status === 'fulfilled' && ghRes.value.ok + ? (await ghRes.value.json()).stargazers_count + : 2000; + +function fmt(n: number): string { + if (n >= 1000) return `${Math.floor(n / 1000)}k+`; + return String(n); +} --- - +

@@ -49,16 +69,57 @@ import LiveDemo from '../components/LiveDemo.tsx';
-
- $ npm install react-data-table-component +
+ $ npm install react-data-table-component + +
+ + + +
+
+
{fmt(weeklyDownloads)}
+
weekly downloads
+
+ + + +
+
2018
+
actively maintained since
+
-
-

Try it live: switch themes, toggle features

-
@@ -84,7 +145,7 @@ export default function App() { lang="tsx" theme="catppuccin-macchiato" defaultColor={false} - class="rounded-xl text-sm leading-relaxed overflow-x-auto" + class="rounded-xl text-sm leading-relaxed overflow-x-auto p-6" />
@@ -121,9 +182,9 @@ export default function App() { desc: 'Render any component inside an expandable detail panel. Animate open/close with a single prop.', }, { - icon: '⚡', - title: 'Animate rows', - desc: 'Staggered row entrance and sort-pulse animations. Respects prefers-reduced-motion.', + icon: '⌕', + title: 'Column filtering', + desc: 'Per-column filter inputs for text, number, and select. Client-side or hook into your own server search.', }, { icon: '⊞', @@ -136,9 +197,9 @@ export default function App() { desc: 'Drag column edges to resize. Widths preserved in state, serializable for persistence.', }, { - icon: '○', - title: 'Zero peer deps', - desc: 'styled-components removed in v8. CSS custom properties handle all theming at zero runtime cost.', + icon: '▲', + title: 'SSR & Next.js ready', + desc: 'Works in Next.js App Router, Remix, and Astro. No useLayoutEffect warnings. Ships "use client" automatically.', }, ].map(({ icon, title, desc }) => (
@@ -197,7 +258,7 @@ export default function App() {

Sponsors

- This library is maintained by one person. If your team ships products with it, consider sponsoring. It funds bug + Actively maintained since 2018. If your team ships products with it, consider sponsoring — it funds bug fixes and keeps pace with the React ecosystem.

diff --git a/apps/docs/src/pages/support.astro b/apps/docs/src/pages/support.astro index fdf56b53..849cac95 100644 --- a/apps/docs/src/pages/support.astro +++ b/apps/docs/src/pages/support.astro @@ -13,8 +13,8 @@ const OC_BASE = 'https://opencollective.com/react-data-table-component';

Support & Sponsorship

- This library is maintained by one person. Your support funds bug fixes, new features, and keeps - pace with the React ecosystem. + Actively maintained since 2018 and downloaded ~215k times a week. Your sponsorship funds bug fixes, + new features, and keeps pace with the React ecosystem.

diff --git a/package.json b/package.json index b2e61b89..cd615b20 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,19 @@ "data", "table", "react-table", - "react-data-table-component" + "react-data-table-component", + "pagination", + "sorting", + "expandable-rows", + "row-selection", + "typescript", + "accessible", + "themeable", + "nextjs", + "dashboard", + "admin" ], + "homepage": "https://reactdatatable.com", "repository": "https://github.com/jbetancur/react-data-table-component", "author": "jbetancur", "license": "Apache-2.0", From 9cb5053a6def006fc97404b9a5b558443de551b2 Mon Sep 17 00:00:00 2001 From: John Betancur <1385932+jbetancur@users.noreply.github.com> Date: Fri, 22 May 2026 22:01:22 -0400 Subject: [PATCH 2/2] feat: add new recipes and demos for data table functionalities - Implemented PersistColumnWidthsDemo to demonstrate persistent column widths using localStorage. - Created ServerSideRecipeDemo for server-side sorting, pagination, and filtering. - Developed UrlSyncDemo to sync table state with URL parameters. - Updated DocsLayout to include new recipe links for better navigation. - Added bulk action toolbar recipe with multi-step approval workflow. - Introduced editable grid recipe with URL-synced state. - Created master-detail recipe showcasing dashboard drill-down functionality. - Added persist column widths recipe demonstrating localStorage and API integration. - Implemented inline row actions recipe with confirmation modals and optimistic updates. - Developed server-side recipe for sorting, pagination, and filtering. - Created sticky footer recipe for an audit log viewer with fixed headers and multi-field search. --- apps/docs/src/components/LiveDemo.tsx | 69 +++--- .../components/demos/ApprovalWorkflowDemo.tsx | 158 +++++++++++++ .../src/components/demos/AuditLogDemo.tsx | 121 ++++++++++ .../src/components/demos/BulkActionDemo.tsx | 97 ++++++++ .../demos/DashboardDrilldownDemo.tsx | 164 ++++++++++++++ .../src/components/demos/EditableGridDemo.tsx | 91 ++++++++ .../components/demos/InlineRowActionsDemo.tsx | 208 ++++++++++++++++++ .../demos/PersistColumnWidthsDemo.tsx | 80 +++++++ .../components/demos/ServerSideRecipeDemo.tsx | 120 ++++++++++ .../docs/src/components/demos/UrlSyncDemo.tsx | 128 +++++++++++ apps/docs/src/layouts/DocsLayout.astro | 49 +++-- .../docs/recipes/bulk-action-toolbar.astro | 77 +++++++ .../pages/docs/recipes/editable-grid.astro | 99 +++++++++ .../pages/docs/recipes/master-detail.astro | 80 +++++++ .../docs/recipes/persist-column-widths.astro | 89 ++++++++ .../src/pages/docs/recipes/row-grouping.astro | 90 ++++++++ .../src/pages/docs/recipes/server-side.astro | 72 ++++++ .../pages/docs/recipes/sticky-footer.astro | 62 ++++++ 18 files changed, 1810 insertions(+), 44 deletions(-) create mode 100644 apps/docs/src/components/demos/ApprovalWorkflowDemo.tsx create mode 100644 apps/docs/src/components/demos/AuditLogDemo.tsx create mode 100644 apps/docs/src/components/demos/BulkActionDemo.tsx create mode 100644 apps/docs/src/components/demos/DashboardDrilldownDemo.tsx create mode 100644 apps/docs/src/components/demos/EditableGridDemo.tsx create mode 100644 apps/docs/src/components/demos/InlineRowActionsDemo.tsx create mode 100644 apps/docs/src/components/demos/PersistColumnWidthsDemo.tsx create mode 100644 apps/docs/src/components/demos/ServerSideRecipeDemo.tsx create mode 100644 apps/docs/src/components/demos/UrlSyncDemo.tsx create mode 100644 apps/docs/src/pages/docs/recipes/bulk-action-toolbar.astro create mode 100644 apps/docs/src/pages/docs/recipes/editable-grid.astro create mode 100644 apps/docs/src/pages/docs/recipes/master-detail.astro create mode 100644 apps/docs/src/pages/docs/recipes/persist-column-widths.astro create mode 100644 apps/docs/src/pages/docs/recipes/row-grouping.astro create mode 100644 apps/docs/src/pages/docs/recipes/server-side.astro create mode 100644 apps/docs/src/pages/docs/recipes/sticky-footer.astro diff --git a/apps/docs/src/components/LiveDemo.tsx b/apps/docs/src/components/LiveDemo.tsx index 7e8036ef..4e4c1f57 100644 --- a/apps/docs/src/components/LiveDemo.tsx +++ b/apps/docs/src/components/LiveDemo.tsx @@ -90,9 +90,21 @@ const columns = [ }, ]; +function ExpandedRow({ data }: { data: Row }) { + return ( +
+
Department: {data.department}
+
Status: {data.status}
+
Salary: ${data.salary.toLocaleString()}
+
Role: {data.role}
+
+ ); +} + export default function LiveDemo() { const [theme, setTheme] = useState('default'); const [selectable, setSelectable] = useState(true); + const [expandable, setExpandable] = useState(false); const [striped, setStriped] = useState(false); const [animateRows, setAnimateRows] = useState(true); const [selectedCount, setSelectedCount] = useState(0); @@ -108,10 +120,16 @@ export default function LiveDemo() { : 'bg-white text-gray-600 border-gray-200 hover:border-gray-300' }`; + const toggleClass = (active: boolean) => + `relative inline-flex h-4 w-7 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors ${ + active ? 'bg-brand-600' : 'bg-gray-200' + }`; + return (
{/* Toolbar */} -
+
+ {/* Theme row */}
Theme {THEMES.map(t => ( @@ -121,34 +139,29 @@ export default function LiveDemo() { ))}
-
- - - - - + {/* Toggles row */} +
+ {([ + ['Selectable', selectable, setSelectable], + ['Expandable', expandable, setExpandable], + ['Striped', striped, setStriped], + ['Animate', animateRows, setAnimateRows], + ] as [string, boolean, (v: boolean) => void][]).map(([label, value, setter]) => ( + + ))} {selectable && selectedCount > 0 && ( - {selectedCount} selected + {selectedCount} selected )}
@@ -163,6 +176,8 @@ export default function LiveDemo() { highlightOnHover selectableRows={selectable} onSelectedRowsChange={handleSelectedChange} + expandableRows={expandable} + expandableRowsComponent={ExpandedRow} animateRows={animateRows} resizable pagination diff --git a/apps/docs/src/components/demos/ApprovalWorkflowDemo.tsx b/apps/docs/src/components/demos/ApprovalWorkflowDemo.tsx new file mode 100644 index 00000000..f7783497 --- /dev/null +++ b/apps/docs/src/components/demos/ApprovalWorkflowDemo.tsx @@ -0,0 +1,158 @@ +import React, { useRef, useState } from 'react'; +import DataTable, { type DataTableHandle, type TableColumn } from 'react-data-table-component'; + +type Status = 'pending' | 'approved' | 'rejected' | 'needs-info'; + +interface Request { + id: number; + title: string; + requester: string; + department: string; + amount: number; + status: Status; + submittedAt: string; +} + +const STATUS_LABEL: Record = { + 'pending': 'Pending', + 'approved': 'Approved', + 'rejected': 'Rejected', + 'needs-info': 'Needs info', +}; + +const STATUS_CLASS: Record = { + 'pending': 'bg-yellow-50 text-yellow-700 border border-yellow-200', + 'approved': 'bg-green-50 text-green-700 border border-green-200', + 'rejected': 'bg-red-50 text-red-700 border border-red-200', + 'needs-info': 'bg-blue-50 text-blue-700 border border-blue-200', +}; + +const initialData: Request[] = [ + { id: 1, title: 'New MacBook Pro', requester: 'Sam Rivera', department: 'Engineering', amount: 2499, status: 'pending', submittedAt: '2024-05-14' }, + { id: 2, title: 'Figma Teams plan', requester: 'Priya Kapoor', department: 'Design', amount: 720, status: 'pending', submittedAt: '2024-05-14' }, + { id: 3, title: 'AWS reserved instance', requester: 'Aria Chen', department: 'Engineering', amount: 8400, status: 'needs-info', submittedAt: '2024-05-13' }, + { id: 4, title: 'Office chairs (x4)', requester: 'Marcus Webb', department: 'Product', amount: 1200, status: 'pending', submittedAt: '2024-05-12' }, + { id: 5, title: 'Tableau license', requester: 'Jordan Ellis', department: 'Analytics', amount: 1800, status: 'approved', submittedAt: '2024-05-10' }, + { id: 6, title: 'Conference travel', requester: 'Taylor Brooks', department: 'Sales', amount: 950, status: 'rejected', submittedAt: '2024-05-09' }, + { id: 7, title: 'Slack Enterprise', requester: 'Casey Morgan', department: 'Engineering', amount: 3600, status: 'pending', submittedAt: '2024-05-08' }, +]; + +const ACTIONABLE: Status[] = ['pending', 'needs-info']; + +export default function ApprovalWorkflowDemo() { + const ref = useRef(null); + const [data, setData] = useState(initialData); + const [selected, setSelected] = useState([]); + const [toast, setToast] = useState(''); + + function showToast(msg: string) { + setToast(msg); + setTimeout(() => setToast(''), 2500); + } + + function applyStatus(ids: number[], next: Status) { + setData(prev => prev.map(r => ids.includes(r.id) ? { ...r, status: next } : r)); + ref.current?.clearSelectedRows(); + showToast(`${ids.length} request${ids.length !== 1 ? 's' : ''} marked as "${STATUS_LABEL[next]}"`); + } + + const actionable = selected.filter(r => ACTIONABLE.includes(r.status)); + const selectedIds = actionable.map(r => r.id); + + const columns: TableColumn[] = [ + { id: 'title', name: 'Request', selector: r => r.title, grow: 2 }, + { id: 'requester', name: 'Requester', selector: r => r.requester, sortable: true }, + { id: 'department', name: 'Dept', selector: r => r.department, sortable: true, width: '120px' }, + { + id: 'amount', + name: 'Amount', + selector: r => r.amount, + sortable: true, + right: true, + width: '110px', + format: r => `$${r.amount.toLocaleString()}`, + }, + { + id: 'status', + name: 'Status', + selector: r => r.status, + sortable: true, + width: '130px', + cell: r => ( + + {STATUS_LABEL[r.status]} + + ), + }, + { id: 'submittedAt', name: 'Submitted', selector: r => r.submittedAt, sortable: true, width: '115px' }, + ]; + + return ( +
+ {/* Bulk toolbar — actions change based on what's selected */} + {selected.length > 0 && ( +
+ + {selected.length} selected + {actionable.length < selected.length && ( + + ({selected.length - actionable.length} already resolved, excluded) + + )} + +
+ {actionable.length > 0 && ( + <> + + + + + )} + +
+
+ )} + +
+ !ACTIONABLE.includes(r.status)} + onSelectedRowsChange={({ selectedRows }) => setSelected(selectedRows)} + highlightOnHover + defaultSortFieldId="submittedAt" + defaultSortAsc={false} + /> +
+ + {toast && ( +
+ {toast} +
+ )} +
+ ); +} diff --git a/apps/docs/src/components/demos/AuditLogDemo.tsx b/apps/docs/src/components/demos/AuditLogDemo.tsx new file mode 100644 index 00000000..e5a2ed61 --- /dev/null +++ b/apps/docs/src/components/demos/AuditLogDemo.tsx @@ -0,0 +1,121 @@ +import React, { useState } from 'react'; +import DataTable, { type ConditionalStyles, type TableColumn } from 'react-data-table-component'; + +type Severity = 'info' | 'warning' | 'error' | 'critical'; + +interface LogEntry { + id: number; + timestamp: string; + severity: Severity; + user: string; + action: string; + resource: string; + ip: string; +} + +const SEVERITY_STYLE: Record = { + info: 'bg-blue-50 text-blue-700 border border-blue-100', + warning: 'bg-yellow-50 text-yellow-700 border border-yellow-100', + error: 'bg-red-50 text-red-700 border border-red-100', + critical: 'bg-red-100 text-red-900 border border-red-300 font-bold', +}; + +const ALL_LOGS: LogEntry[] = [ + { id: 1, timestamp: '2024-05-16 09:01:12', severity: 'info', user: 'aria.chen', action: 'LOGIN', resource: '/dashboard', ip: '10.0.1.42' }, + { id: 2, timestamp: '2024-05-16 09:03:44', severity: 'info', user: 'marcus.webb', action: 'VIEW', resource: '/reports/q1', ip: '10.0.1.18' }, + { id: 3, timestamp: '2024-05-16 09:12:05', severity: 'warning', user: 'aria.chen', action: 'EXPORT', resource: '/users/all', ip: '10.0.1.42' }, + { id: 4, timestamp: '2024-05-16 09:15:30', severity: 'error', user: 'unknown', action: 'LOGIN_FAILED', resource: '/auth', ip: '203.0.113.7' }, + { id: 5, timestamp: '2024-05-16 09:15:31', severity: 'error', user: 'unknown', action: 'LOGIN_FAILED', resource: '/auth', ip: '203.0.113.7' }, + { id: 6, timestamp: '2024-05-16 09:15:32', severity: 'critical', user: 'unknown', action: 'BRUTE_FORCE', resource: '/auth', ip: '203.0.113.7' }, + { id: 7, timestamp: '2024-05-16 09:22:18', severity: 'info', user: 'priya.kapoor', action: 'UPDATE', resource: '/settings/theme', ip: '10.0.1.55' }, + { id: 8, timestamp: '2024-05-16 09:31:00', severity: 'warning', user: 'jordan.ellis', action: 'DELETE', resource: '/reports/draft-4', ip: '10.0.2.11' }, + { id: 9, timestamp: '2024-05-16 09:45:09', severity: 'info', user: 'sam.rivera', action: 'DEPLOY', resource: '/infra/staging', ip: '10.0.1.99' }, + { id: 10, timestamp: '2024-05-16 10:02:44', severity: 'critical', user: 'system', action: 'DB_CONN_LOST', resource: '/db/primary', ip: '10.0.0.1' }, + { id: 11, timestamp: '2024-05-16 10:03:01', severity: 'critical', user: 'system', action: 'FAILOVER', resource: '/db/replica', ip: '10.0.0.2' }, + { id: 12, timestamp: '2024-05-16 10:15:22', severity: 'info', user: 'aria.chen', action: 'LOGOUT', resource: '/auth', ip: '10.0.1.42' }, + { id: 13, timestamp: '2024-05-16 10:28:33', severity: 'warning', user: 'taylor.brooks',action: 'PERMISSION_DENY', resource: '/admin/users', ip: '10.0.1.77' }, + { id: 14, timestamp: '2024-05-16 10:55:50', severity: 'error', user: 'system', action: 'DISK_FULL', resource: '/storage/logs', ip: '10.0.0.5' }, + { id: 15, timestamp: '2024-05-16 11:10:04', severity: 'info', user: 'marcus.webb', action: 'LOGIN', resource: '/dashboard', ip: '10.0.1.18' }, +]; + +const SEVERITIES: ('all' | Severity)[] = ['all', 'info', 'warning', 'error', 'critical']; + +const conditionalRowStyles: ConditionalStyles[] = [ + { when: r => r.severity === 'critical', style: { backgroundColor: '#fef2f2' } }, + { when: r => r.severity === 'error', style: { backgroundColor: '#fff7f7' } }, +]; + +const columns: TableColumn[] = [ + { id: 'timestamp', name: 'Timestamp', selector: r => r.timestamp, sortable: true, width: '175px' }, + { + id: 'severity', + name: 'Severity', + selector: r => r.severity, + sortable: true, + width: '110px', + cell: r => ( + + {r.severity} + + ), + }, + { id: 'user', name: 'User', selector: r => r.user, sortable: true, width: '145px' }, + { id: 'action', name: 'Action', selector: r => r.action, sortable: true, width: '145px', style: { fontFamily: 'monospace', fontSize: 12 } }, + { id: 'resource', name: 'Resource', selector: r => r.resource, grow: 1, style: { fontFamily: 'monospace', fontSize: 12 } }, + { id: 'ip', name: 'IP', selector: r => r.ip, width: '130px', style: { fontFamily: 'monospace', fontSize: 12 } }, +]; + +export default function AuditLogDemo() { + const [severity, setSeverity] = useState<'all' | Severity>('all'); + const [search, setSearch] = useState(''); + + const filtered = ALL_LOGS.filter(r => { + if (severity !== 'all' && r.severity !== severity) return false; + if (search) { + const q = search.toLowerCase(); + return r.user.includes(q) || r.action.includes(q) || r.resource.includes(q) || r.ip.includes(q); + } + return true; + }); + + return ( +
+
+ setSearch(e.target.value)} + placeholder="Search user, action, resource, IP…" + className="px-3 py-1.5 text-xs border border-gray-200 rounded-md w-56 focus:outline-none focus:border-gray-400" + /> +
+ {SEVERITIES.map(s => ( + + ))} +
+
+
+ No matching log entries
} + /> +
+
+ ); +} diff --git a/apps/docs/src/components/demos/BulkActionDemo.tsx b/apps/docs/src/components/demos/BulkActionDemo.tsx new file mode 100644 index 00000000..0e7aa394 --- /dev/null +++ b/apps/docs/src/components/demos/BulkActionDemo.tsx @@ -0,0 +1,97 @@ +import React, { useRef, useState } from 'react'; +import DataTable, { type DataTableHandle, type TableColumn } from 'react-data-table-component'; + +interface Employee { + id: number; + name: string; + department: string; + role: string; +} + +const data: Employee[] = [ + { id: 1, name: 'Aria Chen', department: 'Engineering', role: 'Engineering Lead' }, + { id: 2, name: 'Marcus Webb', department: 'Product', role: 'Product Manager' }, + { id: 3, name: 'Priya Kapoor', department: 'Design', role: 'Senior Designer' }, + { id: 4, name: 'Jordan Ellis', department: 'Analytics', role: 'Data Scientist' }, + { id: 5, name: 'Sam Rivera', department: 'Engineering', role: 'DevOps Engineer' }, + { id: 6, name: 'Taylor Brooks', department: 'Sales', role: 'Account Manager' }, +]; + +const columns: TableColumn[] = [ + { name: 'Name', selector: r => r.name, sortable: true }, + { name: 'Department', selector: r => r.department, sortable: true }, + { name: 'Role', selector: r => r.role }, +]; + +export default function BulkActionDemo() { + const ref = useRef(null); + const [selected, setSelected] = useState([]); + const [toast, setToast] = useState(''); + + function showToast(msg: string) { + setToast(msg); + setTimeout(() => setToast(''), 2500); + } + + function handleExport() { + showToast(`Exported ${selected.length} row${selected.length !== 1 ? 's' : ''}`); + ref.current?.clearSelectedRows(); + } + + function handleArchive() { + showToast(`Archived: ${selected.map(r => r.name).join(', ')}`); + ref.current?.clearSelectedRows(); + } + + return ( +
+ {/* Toolbar */} + {selected.length > 0 && ( +
+ {selected.length} selected +
+ + + +
+
+ )} + +
+ setSelected(selectedRows)} + pagination + paginationPerPage={5} + /> +
+ + {/* Toast */} + {toast && ( +
+ {toast} +
+ )} +
+ ); +} diff --git a/apps/docs/src/components/demos/DashboardDrilldownDemo.tsx b/apps/docs/src/components/demos/DashboardDrilldownDemo.tsx new file mode 100644 index 00000000..18bbb160 --- /dev/null +++ b/apps/docs/src/components/demos/DashboardDrilldownDemo.tsx @@ -0,0 +1,164 @@ +import React from 'react'; +import DataTable, { type ConditionalStyles, type ExpanderComponentProps, type TableColumn } from 'react-data-table-component'; + +interface TeamMember { + name: string; + role: string; + tickets: number; + utilization: number; +} + +interface Department { + id: number; + name: string; + headcount: number; + budget: number; + spent: number; + openTickets: number; + health: 'good' | 'at-risk' | 'critical'; + members: TeamMember[]; +} + +const departments: Department[] = [ + { + id: 1, name: 'Engineering', headcount: 12, budget: 1800000, spent: 1240000, openTickets: 8, health: 'good', + members: [ + { name: 'Aria Chen', role: 'Engineering Lead', tickets: 3, utilization: 82 }, + { name: 'Sam Rivera', role: 'DevOps Engineer', tickets: 2, utilization: 91 }, + { name: 'Taylor Kim', role: 'Frontend Eng', tickets: 2, utilization: 74 }, + { name: 'Casey Morgan', role: 'Backend Eng', tickets: 1, utilization: 68 }, + ], + }, + { + id: 2, name: 'Product', headcount: 5, budget: 750000, spent: 680000, openTickets: 14, health: 'at-risk', + members: [ + { name: 'Marcus Webb', role: 'Product Manager', tickets: 7, utilization: 98 }, + { name: 'Dana Park', role: 'Product Designer', tickets: 7, utilization: 95 }, + ], + }, + { + id: 3, name: 'Design', headcount: 4, budget: 480000, spent: 195000, openTickets: 3, health: 'good', + members: [ + { name: 'Priya Kapoor', role: 'Senior Designer', tickets: 2, utilization: 65 }, + { name: 'Alex Kim', role: 'UX Researcher', tickets: 1, utilization: 55 }, + ], + }, + { + id: 4, name: 'Analytics', headcount: 3, budget: 420000, spent: 410000, openTickets: 21, health: 'critical', + members: [ + { name: 'Jordan Ellis', role: 'Data Scientist', tickets: 12, utilization: 100 }, + { name: 'Quinn Adams', role: 'Data Engineer', tickets: 9, utilization: 100 }, + ], + }, + { + id: 5, name: 'Sales', headcount: 6, budget: 600000, spent: 320000, openTickets: 5, health: 'good', + members: [ + { name: 'Taylor Brooks', role: 'Account Manager', tickets: 3, utilization: 72 }, + { name: 'Riley Stone', role: 'Sales Rep', tickets: 2, utilization: 60 }, + ], + }, +]; + +const HEALTH_CLASS = { + good: 'bg-green-50 text-green-700 border border-green-200', + 'at-risk':'bg-yellow-50 text-yellow-700 border border-yellow-200', + critical: 'bg-red-50 text-red-700 border border-red-200', +}; + +function UtilBar({ pct }: { pct: number }) { + const color = pct >= 95 ? '#ef4444' : pct >= 80 ? '#f59e0b' : '#22c55e'; + return ( +
+
+
+
+ {pct}% +
+ ); +} + +function SpendBar({ budget, spent }: { budget: number; spent: number }) { + const pct = Math.min(100, Math.round((spent / budget) * 100)); + const color = pct >= 95 ? '#ef4444' : pct >= 80 ? '#f59e0b' : '#6366f1'; + return ( +
+
+
+
+ {pct}% +
+ ); +} + +const memberColumns: TableColumn[] = [ + { name: 'Name', selector: m => m.name, grow: 1 }, + { name: 'Role', selector: m => m.role, grow: 1 }, + { name: 'Open tickets',selector: m => m.tickets, width: '110px', right: true }, + { name: 'Utilization', selector: m => m.utilization, width: '160px', + cell: m => , + }, +]; + +function DepartmentDetail({ data: dept }: ExpanderComponentProps) { + return ( +
+

Team members

+ +
+ ); +} + +const conditionalRowStyles: ConditionalStyles[] = [ + { when: r => r.health === 'critical', style: { backgroundColor: '#fff7f7' } }, + { when: r => r.health === 'at-risk', style: { backgroundColor: '#fffdf0' } }, +]; + +const columns: TableColumn[] = [ + { + id: 'name', name: 'Department', selector: r => r.name, sortable: true, grow: 1, + }, + { + id: 'headcount', name: 'Headcount', selector: r => r.headcount, sortable: true, right: true, width: '110px', + }, + { + id: 'spend', name: 'Budget spend', selector: r => r.spent, sortable: true, width: '200px', + cell: r => ( +
+
+ ${r.spent.toLocaleString()} / ${r.budget.toLocaleString()} +
+ +
+ ), + }, + { + id: 'openTickets', name: 'Open tickets', selector: r => r.openTickets, sortable: true, right: true, width: '120px', + }, + { + id: 'health', name: 'Health', selector: r => r.health, sortable: true, width: '110px', + cell: r => ( + + {r.health} + + ), + }, +]; + +export default function DashboardDrilldownDemo() { + return ( +
+

Expand any row to see team member utilization.

+
+ +
+
+ ); +} diff --git a/apps/docs/src/components/demos/EditableGridDemo.tsx b/apps/docs/src/components/demos/EditableGridDemo.tsx new file mode 100644 index 00000000..3b846e00 --- /dev/null +++ b/apps/docs/src/components/demos/EditableGridDemo.tsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import DataTable, { type TableColumn } from 'react-data-table-component'; + +interface LineItem { + id: number; + product: string; + quantity: number; + price: number; +} + +const initialData: LineItem[] = [ + { id: 1, product: 'Widget A', quantity: 4, price: 12.5 }, + { id: 2, product: 'Widget B', quantity: 2, price: 34.0 }, + { id: 3, product: 'Gadget Pro', quantity: 1, price: 89.99 }, + { id: 4, product: 'Connector X', quantity: 10, price: 5.25 }, + { id: 5, product: 'Cable Pack', quantity: 3, price: 18.0 }, +]; + +export default function EditableGridDemo() { + const [data, setData] = useState(initialData); + + const handleCellEdit = (row: LineItem, value: string, column: TableColumn) => { + const field = column.id as keyof LineItem; + setData(prev => + prev.map(r => { + if (r.id !== row.id) return r; + if (field === 'quantity') return { ...r, quantity: Math.max(0, parseInt(value) || 0) }; + if (field === 'price') return { ...r, price: Math.max(0, parseFloat(value) || 0) }; + return { ...r, [field]: value }; + }), + ); + }; + + const columns: TableColumn[] = [ + { + id: 'product', + name: 'Product', + selector: r => r.product, + editable: true, + onCellEdit: handleCellEdit, + width: '160px', + }, + { + id: 'quantity', + name: 'Qty', + selector: r => r.quantity, + editable: true, + editor: { type: 'number' }, + onCellEdit: handleCellEdit, + right: true, + width: '100px', + }, + { + id: 'price', + name: 'Unit price', + selector: r => r.price, + editable: true, + editor: { type: 'number' }, + onCellEdit: handleCellEdit, + format: r => `$${r.price.toFixed(2)}`, + right: true, + width: '120px', + }, + { + id: 'total', + name: 'Total', + selector: r => r.quantity * r.price, + format: r => `$${(r.quantity * r.price).toFixed(2)}`, + right: true, + width: '120px', + style: { fontWeight: 600, color: '#1d4ed8' }, + }, + ]; + + const grandTotal = data.reduce((sum, r) => sum + r.quantity * r.price, 0); + + return ( +
+ +
+ {data.length} items + Grand total: ${grandTotal.toFixed(2)} +
+
+ ); +} diff --git a/apps/docs/src/components/demos/InlineRowActionsDemo.tsx b/apps/docs/src/components/demos/InlineRowActionsDemo.tsx new file mode 100644 index 00000000..1d721adf --- /dev/null +++ b/apps/docs/src/components/demos/InlineRowActionsDemo.tsx @@ -0,0 +1,208 @@ +import React, { useEffect, useRef, useState } from 'react'; +import DataTable, { type TableColumn } from 'react-data-table-component'; + +interface Employee { + id: number; + name: string; + department: string; + role: string; + email: string; +} + +const seed: Employee[] = [ + { id: 1, name: 'Aria Chen', department: 'Engineering', role: 'Engineering Lead', email: 'aria@example.com' }, + { id: 2, name: 'Marcus Webb', department: 'Product', role: 'Product Manager', email: 'marcus@example.com' }, + { id: 3, name: 'Priya Kapoor', department: 'Design', role: 'Senior Designer', email: 'priya@example.com' }, + { id: 4, name: 'Jordan Ellis', department: 'Analytics', role: 'Data Scientist', email: 'jordan@example.com' }, + { id: 5, name: 'Sam Rivera', department: 'Engineering', role: 'DevOps Engineer', email: 'sam@example.com' }, + { id: 6, name: 'Taylor Brooks', department: 'Sales', role: 'Account Manager', email: 'taylor@example.com' }, +]; + +type ModalState = + | { type: 'none' } + | { type: 'edit'; row: Employee } + | { type: 'delete'; row: Employee } + | { type: 'duplicate'; row: Employee }; + +function DropdownMenu({ row, onEdit, onDuplicate, onDelete }: { + row: Employee; + onEdit: (r: Employee) => void; + onDuplicate: (r: Employee) => void; + onDelete: (r: Employee) => void; +}) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!open) return; + function handle(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + } + document.addEventListener('mousedown', handle); + return () => document.removeEventListener('mousedown', handle); + }, [open]); + + return ( +
+ + {open && ( +
+ + +
+ +
+ )} +
+ ); +} + +export default function InlineRowActionsDemo() { + const [data, setData] = useState(seed); + const [modal, setModal] = useState({ type: 'none' }); + const [editDraft, setEditDraft] = useState(null); + const [toast, setToast] = useState(''); + const nextId = useRef(seed.length + 1); + + function showToast(msg: string) { + setToast(msg); + setTimeout(() => setToast(''), 2000); + } + + function openEdit(row: Employee) { + setEditDraft({ ...row }); + setModal({ type: 'edit', row }); + } + + function commitEdit() { + if (!editDraft) return; + setData(prev => prev.map(r => r.id === editDraft.id ? editDraft : r)); + setModal({ type: 'none' }); + showToast(`Saved ${editDraft.name}`); + } + + function commitDuplicate(row: Employee) { + const copy = { ...row, id: nextId.current++, name: `${row.name} (copy)` }; + setData(prev => [...prev, copy]); + setModal({ type: 'none' }); + showToast(`Duplicated ${row.name}`); + } + + function commitDelete(row: Employee) { + setData(prev => prev.filter(r => r.id !== row.id)); + setModal({ type: 'none' }); + showToast(`Deleted ${row.name}`); + } + + const columns: TableColumn[] = [ + { name: 'Name', selector: r => r.name, sortable: true, grow: 1 }, + { name: 'Department', selector: r => r.department, sortable: true }, + { name: 'Role', selector: r => r.role, grow: 1 }, + { + name: '', + button: true, + width: '48px', + cell: row => ( + setModal({ type: 'duplicate', row: r })} + onDelete={r => setModal({ type: 'delete', row: r })} + /> + ), + }, + ]; + + return ( +
+
+ No employees
} + /> +
+ + {/* Edit modal */} + {modal.type === 'edit' && editDraft && ( +
setModal({ type: 'none' })}> +
e.stopPropagation()}> +

Edit employee

+ {(['name', 'role', 'email'] as const).map(field => ( +
+ + setEditDraft(d => d ? { ...d, [field]: e.target.value } : d)} + className="w-full px-3 py-1.5 text-sm border border-gray-200 rounded-md focus:outline-none focus:border-brand-400" + /> +
+ ))} +
+ + +
+
+
+ )} + + {/* Duplicate confirmation */} + {modal.type === 'duplicate' && ( +
setModal({ type: 'none' })}> +
e.stopPropagation()}> +

Duplicate row?

+

A copy of {modal.row.name} will be added to the list.

+
+ + +
+
+
+ )} + + {/* Delete confirmation */} + {modal.type === 'delete' && ( +
setModal({ type: 'none' })}> +
e.stopPropagation()}> +

Delete employee?

+

This will permanently remove {modal.row.name}.

+
+ + +
+
+
+ )} + + {toast && ( +
+ {toast} +
+ )} +
+ ); +} diff --git a/apps/docs/src/components/demos/PersistColumnWidthsDemo.tsx b/apps/docs/src/components/demos/PersistColumnWidthsDemo.tsx new file mode 100644 index 00000000..c0ac1f5f --- /dev/null +++ b/apps/docs/src/components/demos/PersistColumnWidthsDemo.tsx @@ -0,0 +1,80 @@ +import React, { useState } from 'react'; +import DataTable, { type TableColumn } from 'react-data-table-component'; + +interface Employee { + id: number; + name: string; + department: string; + salary: number; + role: string; +} + +const data: Employee[] = [ + { id: 1, name: 'Aria Chen', department: 'Engineering', salary: 155000, role: 'Engineering Lead' }, + { id: 2, name: 'Marcus Webb', department: 'Product', salary: 132000, role: 'Product Manager' }, + { id: 3, name: 'Priya Kapoor', department: 'Design', salary: 118000, role: 'Senior Designer' }, + { id: 4, name: 'Jordan Ellis', department: 'Analytics', salary: 143000, role: 'Data Scientist' }, + { id: 5, name: 'Sam Rivera', department: 'Engineering', salary: 128000, role: 'DevOps Engineer' }, +]; + +const STORAGE_KEY = 'recipe-demo-column-widths'; + +function loadWidths(): Record { + try { return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}'); } + catch { return {}; } +} + +const columnDefs: TableColumn[] = [ + { id: 'name', name: 'Name', selector: r => r.name, sortable: true }, + { id: 'department', name: 'Department', selector: r => r.department, sortable: true }, + { id: 'role', name: 'Role', selector: r => r.role }, + { id: 'salary', name: 'Salary', selector: r => r.salary, right: true, format: r => `$${r.salary.toLocaleString()}` }, +]; + +export default function PersistColumnWidthsDemo() { + const [initialWidths, setInitialWidths] = useState | null>(null); + const [saved, setSaved] = useState(false); + + React.useEffect(() => { + setInitialWidths(loadWidths()); + }, []); + + function handleResize(_id: string | number, _w: number, all: Record) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(all)); + setSaved(true); + setTimeout(() => setSaved(false), 1500); + } + + function handleReset() { + localStorage.removeItem(STORAGE_KEY); + setInitialWidths({}); + } + + return ( +
+
+ Drag column edges to resize — widths persist across sessions. + + {saved && Saved!} +
+
+ {initialWidths !== null && ( + + )} +
+
+ ); +} diff --git a/apps/docs/src/components/demos/ServerSideRecipeDemo.tsx b/apps/docs/src/components/demos/ServerSideRecipeDemo.tsx new file mode 100644 index 00000000..beb86b51 --- /dev/null +++ b/apps/docs/src/components/demos/ServerSideRecipeDemo.tsx @@ -0,0 +1,120 @@ +import React, { useEffect, useState } from 'react'; +import DataTable, { type FilterState, SortOrder, type TableColumn } from 'react-data-table-component'; + +interface Employee { + id: number; + name: string; + department: string; + salary: number; +} + +const ALL_DATA: Employee[] = [ + { id: 1, name: 'Aria Chen', department: 'Engineering', salary: 155000 }, + { id: 2, name: 'Marcus Webb', department: 'Product', salary: 132000 }, + { id: 3, name: 'Priya Kapoor', department: 'Design', salary: 118000 }, + { id: 4, name: 'Jordan Ellis', department: 'Analytics', salary: 143000 }, + { id: 5, name: 'Sam Rivera', department: 'Engineering', salary: 128000 }, + { id: 6, name: 'Taylor Brooks', department: 'Sales', salary: 97000 }, + { id: 7, name: 'Casey Morgan', department: 'Engineering', salary: 138000 }, + { id: 8, name: 'Alex Kim', department: 'Design', salary: 112000 }, + { id: 9, name: 'Dana Park', department: 'Product', salary: 125000 }, + { id: 10, name: 'Riley Stone', department: 'Sales', salary: 104000 }, + { id: 11, name: 'Quinn Adams', department: 'Analytics', salary: 137000 }, + { id: 12, name: 'Morgan Lee', department: 'Engineering', salary: 149000 }, +]; + +function simulateFetch({ + page, perPage, sortId, sortDir, filters, +}: { + page: number; + perPage: number; + sortId: string; + sortDir: SortOrder; + filters: Record; +}): Promise<{ rows: Employee[]; total: number }> { + return new Promise(resolve => { + setTimeout(() => { + let rows = [...ALL_DATA]; + + // filter + const nameFilter = filters['name']; + if (nameFilter?.value) { + rows = rows.filter(r => r.name.toLowerCase().includes(String(nameFilter.value).toLowerCase())); + } + const deptFilter = filters['department']; + if (deptFilter?.value) { + rows = rows.filter(r => r.department.toLowerCase().includes(String(deptFilter.value).toLowerCase())); + } + + // sort + if (sortId) { + rows.sort((a, b) => { + const av = a[sortId as keyof Employee]; + const bv = b[sortId as keyof Employee]; + return sortDir === SortOrder.ASC + ? av < bv ? -1 : av > bv ? 1 : 0 + : av > bv ? -1 : av < bv ? 1 : 0; + }); + } + + const total = rows.length; + rows = rows.slice((page - 1) * perPage, page * perPage); + resolve({ rows, total }); + }, 400); + }); +} + +export default function ServerSideRecipeDemo() { + const [rows, setRows] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(5); + const [sortId, setSortId] = useState(''); + const [sortDir, setSortDir] = useState(SortOrder.ASC); + const [filters, setFilters] = useState>({}); + const [loading, setLoading] = useState(false); + + useEffect(() => { + let cancelled = false; + setLoading(true); + simulateFetch({ page, perPage, sortId, sortDir, filters }).then(res => { + if (cancelled) return; + setRows(res.rows); + setTotal(res.total); + setLoading(false); + }); + return () => { cancelled = true; }; + }, [page, perPage, sortId, sortDir, filters]); + + const columns: TableColumn[] = [ + { id: 'name', name: 'Name', selector: r => r.name, sortable: true, filterable: true }, + { id: 'department', name: 'Department', selector: r => r.department, sortable: true, filterable: true }, + { + id: 'salary', name: 'Salary', selector: r => r.salary, sortable: true, + right: true, format: r => `$${r.salary.toLocaleString()}`, + }, + ]; + + return ( +
+ setPage(p)} + onChangeRowsPerPage={(pp, p) => { setPerPage(pp); setPage(p); }} + sortServer + onSort={(col, dir) => { setSortId(col.id as string); setSortDir(dir); setPage(1); }} + filterValues={filters} + onFilterChange={(columnId, next) => + setFilters(prev => ({ ...prev, [columnId]: next })) + } + highlightOnHover + /> +
+ ); +} diff --git a/apps/docs/src/components/demos/UrlSyncDemo.tsx b/apps/docs/src/components/demos/UrlSyncDemo.tsx new file mode 100644 index 00000000..b13f4b6a --- /dev/null +++ b/apps/docs/src/components/demos/UrlSyncDemo.tsx @@ -0,0 +1,128 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import DataTable, { type FilterState, SortOrder, type TableColumn } from 'react-data-table-component'; + +interface Employee { + id: number; + name: string; + department: string; + role: string; + salary: number; +} + +const ALL_DATA: Employee[] = [ + { id: 1, name: 'Aria Chen', department: 'Engineering', role: 'Engineering Lead', salary: 155000 }, + { id: 2, name: 'Marcus Webb', department: 'Product', role: 'Product Manager', salary: 132000 }, + { id: 3, name: 'Priya Kapoor', department: 'Design', role: 'Senior Designer', salary: 118000 }, + { id: 4, name: 'Jordan Ellis', department: 'Analytics', role: 'Data Scientist', salary: 143000 }, + { id: 5, name: 'Sam Rivera', department: 'Engineering', role: 'DevOps Engineer', salary: 128000 }, + { id: 6, name: 'Taylor Brooks', department: 'Sales', role: 'Account Manager', salary: 97000 }, + { id: 7, name: 'Casey Morgan', department: 'Engineering', role: 'Software Engineer', salary: 138000 }, + { id: 8, name: 'Alex Kim', department: 'Design', role: 'UX Researcher', salary: 112000 }, + { id: 9, name: 'Dana Park', department: 'Product', role: 'Product Designer', salary: 125000 }, + { id: 10, name: 'Riley Stone', department: 'Sales', role: 'Sales Rep', salary: 104000 }, + { id: 11, name: 'Quinn Adams', department: 'Analytics', role: 'Data Engineer', salary: 137000 }, + { id: 12, name: 'Morgan Lee', department: 'Engineering', role: 'Backend Engineer', salary: 149000 }, +]; + +// --- URL <-> state helpers --- + +function readParams(): { sortId: string; sortDir: SortOrder; page: number; perPage: number; filters: Record } { + const p = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : ''); + return { + sortId: p.get('sort') ?? '', + sortDir: p.get('dir') === 'desc' ? SortOrder.DESC : SortOrder.ASC, + page: parseInt(p.get('page') ?? '1', 10), + perPage: parseInt(p.get('per') ?? '5', 10), + filters: Object.fromEntries( + [...p.entries()] + .filter(([k]) => k.startsWith('f_')) + .map(([k, v]) => [k.slice(2), v]), + ), + }; +} + +function writeParams(state: { sortId: string; sortDir: SortOrder; page: number; perPage: number; filters: Record }) { + const p = new URLSearchParams(); + if (state.sortId) { p.set('sort', state.sortId); p.set('dir', state.sortDir === SortOrder.DESC ? 'desc' : 'asc'); } + if (state.page > 1) p.set('page', String(state.page)); + if (state.perPage !== 5) p.set('per', String(state.perPage)); + Object.entries(state.filters).forEach(([k, v]) => { if (v) p.set(`f_${k}`, v); }); + const qs = p.toString(); + history.replaceState(null, '', qs ? `?${qs}` : window.location.pathname); +} + +// --- Component --- + +export default function UrlSyncDemo() { + const init = useMemo(() => readParams(), []); + + const [sortId, setSortId] = useState(init.sortId); + const [sortDir, setSortDir] = useState(init.sortDir); + const [page, setPage] = useState(init.page); + const [perPage, setPerPage] = useState(init.perPage); + const [filters, setFilters] = useState>(() => + Object.fromEntries( + Object.entries(init.filters).map(([k, v]) => [k, { value: v, operator: 'contains' as const, condition2: null, join: 'and' as const }]), + ), + ); + + // Sync every state change to the URL + useEffect(() => { + writeParams({ + sortId, sortDir, page, perPage, + filters: Object.fromEntries( + Object.entries(filters).map(([k, f]) => [k, String(f.value ?? '')]).filter(([, v]) => v), + ), + }); + }, [sortId, sortDir, page, perPage, filters]); + + const handleSort = useCallback((col: TableColumn, dir: SortOrder) => { + setSortId(col.id as string); + setSortDir(dir); + setPage(1); + }, []); + + const handleFilterChange = useCallback((columnId: string | number, next: FilterState) => { + setFilters(prev => ({ ...prev, [columnId]: next })); + setPage(1); + }, []); + + const columns: TableColumn[] = [ + { id: 'name', name: 'Name', selector: r => r.name, sortable: true, filterable: true, grow: 1 }, + { id: 'department', name: 'Department', selector: r => r.department, sortable: true, filterable: true }, + { id: 'role', name: 'Role', selector: r => r.role, sortable: true, grow: 1 }, + { id: 'salary', name: 'Salary', selector: r => r.salary, sortable: true, right: true, width: '110px', + format: r => `$${r.salary.toLocaleString()}` }, + ]; + + // Show current URL params so the demo is self-explanatory + const [urlDisplay, setUrlDisplay] = useState(''); + useEffect(() => { + setUrlDisplay(window.location.search || '(no params — default state)'); + }, [sortId, sortDir, page, perPage, filters]); + + return ( +
+
+ URL params: + {urlDisplay} +
+ setPage(p)} + onChangeRowsPerPage={(pp, p) => { setPerPage(pp); setPage(p); }} + highlightOnHover + /> +
+ ); +} diff --git a/apps/docs/src/layouts/DocsLayout.astro b/apps/docs/src/layouts/DocsLayout.astro index 57e88215..00404c1c 100644 --- a/apps/docs/src/layouts/DocsLayout.astro +++ b/apps/docs/src/layouts/DocsLayout.astro @@ -84,7 +84,18 @@ const nav = [ { label: 'TypeScript', href: '/docs/typescript' }, { label: 'SSR', href: '/docs/ssr' }, { label: 'Performance', href: '/docs/performance' }, - { label: 'Recipes', href: '/docs/recipes' }, + ], + }, + { + group: 'Recipes', + links: [ + { label: 'Approval workflow', href: '/docs/recipes/bulk-action-toolbar' }, + { label: 'Server-side sort & filter', href: '/docs/recipes/server-side' }, + { label: 'URL-synced table state', href: '/docs/recipes/editable-grid' }, + { label: 'Dashboard drill-down', href: '/docs/recipes/master-detail' }, + { label: 'Audit log viewer', href: '/docs/recipes/sticky-footer' }, + { label: 'Persist column widths', href: '/docs/recipes/persist-column-widths' }, + { label: 'Inline row actions', href: '/docs/recipes/row-grouping' }, ], }, { @@ -109,7 +120,7 @@ const nextLink = currentIndex < allLinks.length - 1 ? allLinks[currentIndex + 1]
-