@lad-sapienza/scms-core 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +73 -0
  3. package/components/BSNavbar.tsx +264 -0
  4. package/components/DataTb/DataTb.astro +33 -0
  5. package/components/DataTb/DataTb.tsx +340 -0
  6. package/components/DataTb/DataTbMdx.tsx +75 -0
  7. package/components/DataTb/README.md +275 -0
  8. package/components/DataTb/index.ts +36 -0
  9. package/components/DataTb/sources/ApiSource.tsx +89 -0
  10. package/components/DataTb/sources/CsvSource.tsx +69 -0
  11. package/components/DataTb/sources/DirectusSource.tsx +98 -0
  12. package/components/DataTb/sources/JsonSource.tsx +65 -0
  13. package/components/DataTb/sources/index.ts +17 -0
  14. package/components/DataTb/types.ts +215 -0
  15. package/components/DataTb/utils.ts +186 -0
  16. package/components/Gallery/Gallery.astro +65 -0
  17. package/components/Gallery/Gallery.tsx +136 -0
  18. package/components/Gallery/README.md +118 -0
  19. package/components/Gallery/__tests__/galleryUtils.test.ts +178 -0
  20. package/components/Gallery/galleryUtils.ts +186 -0
  21. package/components/Gallery/index.ts +7 -0
  22. package/components/Gallery/react-photoswipe-gallery.d.ts +26 -0
  23. package/components/Gallery/types.ts +46 -0
  24. package/components/Gallery/virtual-gallery.d.ts +15 -0
  25. package/components/Map/LayerControl.tsx +368 -0
  26. package/components/Map/Map.tsx +735 -0
  27. package/components/Map/MapMdx.tsx +37 -0
  28. package/components/Map/RasterLayerLibre.tsx +33 -0
  29. package/components/Map/Search/SearchOperators.ts +31 -0
  30. package/components/Map/Search/SearchUI.tsx +75 -0
  31. package/components/Map/Search/SearchUIAdvanced.tsx +289 -0
  32. package/components/Map/Search/SearchUISimple.tsx +115 -0
  33. package/components/Map/Search/index.ts +7 -0
  34. package/components/Map/defaultBasemaps.ts +88 -0
  35. package/components/Map/index.ts +6 -0
  36. package/components/Map/types.ts +179 -0
  37. package/components/Map/utils.ts +367 -0
  38. package/components/README.md +62 -0
  39. package/components/Record/Field.tsx +35 -0
  40. package/components/Record/Image.tsx +114 -0
  41. package/components/Record/RecordFetcher.tsx +127 -0
  42. package/components/Record/context.tsx +14 -0
  43. package/components/Record/index.ts +11 -0
  44. package/components/Record/useRecordFetcher.ts +104 -0
  45. package/components/Record/utils.ts +13 -0
  46. package/components/SEO/SEO.astro +132 -0
  47. package/components/TableOfContents/TableOfContents.astro +86 -0
  48. package/components/ZoteroGeoViewer/README.md +195 -0
  49. package/components/ZoteroGeoViewer/ZoteroGeoViewer.astro +26 -0
  50. package/components/ZoteroGeoViewer/ZoteroGeoViewer.tsx +483 -0
  51. package/components/ZoteroGeoViewer/ZoteroRecordsPreview.tsx +185 -0
  52. package/components/ZoteroGeoViewer/index.ts +16 -0
  53. package/components/ZoteroGeoViewer/types.ts +123 -0
  54. package/index.ts +26 -0
  55. package/integrations/__tests__/galleryIntegration.test.ts +70 -0
  56. package/integrations/contentAssetsIntegration.ts +132 -0
  57. package/integrations/directusLoader.ts +98 -0
  58. package/integrations/galleryIntegration.ts +78 -0
  59. package/package.json +56 -0
  60. package/scms.ts +95 -0
  61. package/utils/content.ts +88 -0
  62. package/utils/data-fetcher.ts +128 -0
  63. package/utils/directus-config.ts +160 -0
  64. package/utils/record-fetcher.ts +132 -0
@@ -0,0 +1,340 @@
1
+ /**
2
+ * DataTb Component
3
+ *
4
+ * Main data table component using TanStack Table
5
+ * Supports sorting, filtering, and pagination
6
+ */
7
+
8
+ import { useState, useEffect, useMemo, useRef } from 'react';
9
+ import {
10
+ useReactTable,
11
+ getCoreRowModel,
12
+ getSortedRowModel,
13
+ getFilteredRowModel,
14
+ getPaginationRowModel,
15
+ flexRender,
16
+ type SortingState,
17
+ type ColumnFiltersState,
18
+ } from '@tanstack/react-table';
19
+ import { fetchData } from '../../utils/data-fetcher';
20
+ import type { DataRow, DataTbProps } from './types';
21
+ import {
22
+ autoDetectColumns,
23
+ mergeColumns,
24
+ columnConfigToColumnDef,
25
+ globalFilterFn,
26
+ } from './utils';
27
+
28
+ export function DataTb({
29
+ columns: userColumns,
30
+ searchable = false,
31
+ pagination = true,
32
+ sortable = true,
33
+ initialSort,
34
+ className = '',
35
+ source,
36
+ loadingMessage = 'Loading data...',
37
+ emptyMessage = 'No data available',
38
+ errorMessage = 'Error loading data',
39
+ truncateContent = true,
40
+ truncateMaxWidth = '20rem',
41
+ contentSize = 'sm',
42
+ }: DataTbProps) {
43
+ const [data, setData] = useState<DataRow[]>([]);
44
+ const [loading, setLoading] = useState(true);
45
+ const [error, setError] = useState<Error | null>(null);
46
+ const [sorting, setSorting] = useState<SortingState>(
47
+ initialSort ? [{ id: initialSort.columnKey, desc: initialSort.direction === 'desc' }] : []
48
+ );
49
+ const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
50
+ const [globalFilter, setGlobalFilter] = useState('');
51
+
52
+ // Use a ref to track source changes more reliably
53
+ const sourceRef = useRef<string>('');
54
+ const currentSourceKey = JSON.stringify(source);
55
+
56
+ // Pagination config
57
+ const paginationConfig = typeof pagination === 'object' ? pagination : {};
58
+ const pageSize = paginationConfig.pageSize || 10;
59
+ const pageSizeOptions = paginationConfig.pageSizeOptions || [10, 20, 50, 100];
60
+ const contentSizeClass = contentSize === 'sm' ? 'small' : '';
61
+
62
+ // Fetch data based on source configuration
63
+ useEffect(() => {
64
+ // Check if source has actually changed
65
+ if (currentSourceKey === sourceRef.current) {
66
+ return;
67
+ }
68
+
69
+ sourceRef.current = currentSourceKey;
70
+
71
+ if (!source) {
72
+ setError(new Error('No data source provided'));
73
+ setLoading(false);
74
+ return;
75
+ }
76
+
77
+ // Guards against a stale response overwriting newer state if the source
78
+ // changes again before this fetch resolves.
79
+ let cancelled = false;
80
+
81
+ const loadData = async () => {
82
+ try {
83
+ setLoading(true);
84
+ setError(null);
85
+
86
+ const fetchedData = await fetchData(source);
87
+ if (cancelled) return;
88
+
89
+ setData(fetchedData);
90
+ setLoading(false);
91
+ } catch (err) {
92
+ if (cancelled) return;
93
+ setError(err instanceof Error ? err : new Error('Failed to load data'));
94
+ setLoading(false);
95
+ }
96
+ };
97
+
98
+ loadData();
99
+
100
+ return () => {
101
+ cancelled = true;
102
+ };
103
+ }, [currentSourceKey]); // Use stringified source as dependency
104
+
105
+ // Auto-detect or merge columns
106
+ const finalColumns = useMemo(() => {
107
+ if (data.length === 0) {
108
+ return [];
109
+ }
110
+
111
+ const autoColumns = autoDetectColumns(data);
112
+ const merged = mergeColumns(autoColumns, userColumns);
113
+ return merged;
114
+ }, [data, userColumns]);
115
+
116
+ // Convert to TanStack column definitions
117
+ const columnDefs = useMemo(
118
+ () => finalColumns.map(columnConfigToColumnDef),
119
+ [finalColumns]
120
+ );
121
+
122
+ // Initialize table
123
+ const table = useReactTable({
124
+ data,
125
+ columns: columnDefs,
126
+ getCoreRowModel: getCoreRowModel(),
127
+ getSortedRowModel: sortable ? getSortedRowModel() : undefined,
128
+ getFilteredRowModel: searchable ? getFilteredRowModel() : undefined,
129
+ getPaginationRowModel: pagination ? getPaginationRowModel() : undefined,
130
+ onSortingChange: setSorting,
131
+ onColumnFiltersChange: setColumnFilters,
132
+ onGlobalFilterChange: setGlobalFilter,
133
+ globalFilterFn: globalFilterFn,
134
+ state: {
135
+ sorting,
136
+ columnFilters,
137
+ globalFilter,
138
+ },
139
+ initialState: {
140
+ pagination: {
141
+ pageSize,
142
+ },
143
+ },
144
+ });
145
+
146
+ // Render loading state
147
+ if (loading) {
148
+ return (
149
+ <div className="datatb-container">
150
+ <div className="datatb-loading p-4 text-center text-secondary">
151
+ {loadingMessage}
152
+ </div>
153
+ </div>
154
+ );
155
+ }
156
+
157
+ // Render error state
158
+ if (error) {
159
+ return (
160
+ <div className="datatb-container">
161
+ <div className="datatb-error p-4 text-center text-danger">
162
+ {errorMessage}: {error.message}
163
+ </div>
164
+ </div>
165
+ );
166
+ }
167
+
168
+ // Render empty state
169
+ if (data.length === 0) {
170
+ return (
171
+ <div className="datatb-container">
172
+ <div className="datatb-empty p-4 text-center text-secondary">
173
+ {emptyMessage}
174
+ </div>
175
+ </div>
176
+ );
177
+ }
178
+
179
+ return (
180
+ <div className={`datatb-container ${className}`}>
181
+
182
+ {/* Search input */}
183
+ {searchable && (
184
+ <div className="datatb-search mb-3">
185
+ <input
186
+ type="text"
187
+ value={globalFilter ?? ''}
188
+ onChange={(e) => setGlobalFilter(e.target.value)}
189
+ placeholder="Search..."
190
+ className="form-control form-control-sm"
191
+ style={{ maxWidth: '24rem' }}
192
+ />
193
+ </div>
194
+ )}
195
+
196
+ {/* Table */}
197
+ <div className="datatb-table-wrapper table-responsive border rounded shadow-sm">
198
+ <table className="datatb-table table table-sm table-hover mb-0">
199
+ <thead className="table-light">
200
+ {table.getHeaderGroups().map((headerGroup) => (
201
+ <tr key={headerGroup.id}>
202
+ {headerGroup.headers.map((header, index) => (
203
+ <th
204
+ key={header.id}
205
+ className={`py-2 text-start small fw-semibold text-uppercase ${
206
+ index === 0 ? 'ps-3 pe-3' : 'px-3'
207
+ }`}
208
+ style={{ width: header.column.columnDef.meta?.width }}
209
+ >
210
+ {header.isPlaceholder ? null : (
211
+ <div
212
+ className={
213
+ header.column.getCanSort()
214
+ ? 'd-flex align-items-center gap-2 user-select-none'
215
+ : 'd-flex align-items-center gap-2'
216
+ }
217
+ onClick={header.column.getToggleSortingHandler()}
218
+ style={header.column.getCanSort() ? { cursor: 'pointer' } : undefined}
219
+ >
220
+ {flexRender(
221
+ header.column.columnDef.header,
222
+ header.getContext()
223
+ )}
224
+ {header.column.getCanSort() && (
225
+ <span className="text-secondary">
226
+ {{
227
+ asc: '↑',
228
+ desc: '↓',
229
+ }[header.column.getIsSorted() as string] ?? '↕'}
230
+ </span>
231
+ )}
232
+ </div>
233
+ )}
234
+ </th>
235
+ ))}
236
+ </tr>
237
+ ))}
238
+ </thead>
239
+ <tbody>
240
+ {table.getRowModel().rows.map((row) => (
241
+ <tr key={row.id}>
242
+ {row.getVisibleCells().map((cell, index) => (
243
+ <td
244
+ key={cell.id}
245
+ className={`py-2 ${contentSizeClass} ${
246
+ index === 0 ? 'ps-3 pe-3' : 'px-3'
247
+ }`}
248
+ >
249
+ {(() => {
250
+ const cellContent = flexRender(cell.column.columnDef.cell, cell.getContext());
251
+ const isSimpleText =
252
+ typeof cellContent === 'string' || typeof cellContent === 'number';
253
+
254
+ if (!truncateContent) {
255
+ return cellContent;
256
+ }
257
+
258
+ return (
259
+ <div
260
+ className="text-truncate"
261
+ style={{ maxWidth: truncateMaxWidth }}
262
+ title={isSimpleText ? String(cellContent) : undefined}
263
+ >
264
+ {cellContent}
265
+ </div>
266
+ );
267
+ })()}
268
+ </td>
269
+ ))}
270
+ </tr>
271
+ ))}
272
+ </tbody>
273
+ </table>
274
+ </div>
275
+
276
+ {/* Pagination */}
277
+ {pagination && (
278
+ <div className="datatb-pagination mt-3 d-flex flex-row align-items-center justify-content-between gap-2">
279
+ <div className="btn-group" role="group">
280
+ <button
281
+ onClick={() => table.setPageIndex(0)}
282
+ disabled={!table.getCanPreviousPage()}
283
+ className="btn btn-outline-secondary btn-sm"
284
+ >
285
+ {'<<'}
286
+ </button>
287
+ <button
288
+ onClick={() => table.previousPage()}
289
+ disabled={!table.getCanPreviousPage()}
290
+ className="btn btn-outline-secondary btn-sm"
291
+ >
292
+ {'<'}
293
+ </button>
294
+ <button
295
+ onClick={() => table.nextPage()}
296
+ disabled={!table.getCanNextPage()}
297
+ className="btn btn-outline-secondary btn-sm"
298
+ >
299
+ {'>'}
300
+ </button>
301
+ <button
302
+ onClick={() => table.setPageIndex(table.getPageCount() - 1)}
303
+ disabled={!table.getCanNextPage()}
304
+ className="btn btn-outline-secondary btn-sm"
305
+ >
306
+ {'>>'}
307
+ </button>
308
+ </div>
309
+
310
+ <div className="d-flex align-items-center gap-2 flex-nowrap">
311
+ <span className="small text-secondary text-nowrap">
312
+ Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
313
+ </span>
314
+
315
+ {paginationConfig.showPageSize !== false && (
316
+ <select
317
+ value={table.getState().pagination.pageSize}
318
+ onChange={(e) => {
319
+ table.setPageSize(Number(e.target.value));
320
+ }}
321
+ className="form-select form-select-sm"
322
+ style={{ width: 'auto' }}
323
+ >
324
+ {pageSizeOptions.map((size) => (
325
+ <option key={size} value={size}>
326
+ {size}
327
+ </option>
328
+ ))}
329
+ </select>
330
+ )}
331
+
332
+ <span className="small text-secondary text-nowrap">
333
+ ({data.length} total)
334
+ </span>
335
+ </div>
336
+ </div>
337
+ )}
338
+ </div>
339
+ );
340
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * MDX-safe wrapper for DataTb component
3
+ *
4
+ * This wrapper ensures that object props are properly memoized
5
+ * to prevent infinite re-renders in MDX context
6
+ */
7
+
8
+ import { useMemo } from 'react';
9
+ import { DataTb as DataTbCore } from './DataTb';
10
+ import type { DataTbProps, SourceConfig } from './types';
11
+ import { directusShorthandToConfig } from '../../utils/directus-config';
12
+
13
+ export function DataTb(props: DataTbProps) {
14
+ // Resolve source from simplified props or original source prop
15
+ const resolvedSource = useMemo<SourceConfig | undefined>(() => {
16
+ // If explicit source object is provided, use it (memoized later)
17
+ if (props.source) return props.source;
18
+
19
+ // Handle simplified CSV prop
20
+ if (props.csv) {
21
+ return { type: 'csv', url: props.csv } as const;
22
+ }
23
+
24
+ // Handle simplified JSON prop
25
+ if (props.json) {
26
+ if (Array.isArray(props.json)) {
27
+ return { type: 'json', data: props.json } as const;
28
+ }
29
+ return { type: 'json', url: props.json as string } as const;
30
+ }
31
+
32
+ // Handle simplified API prop
33
+ if (props.api) {
34
+ return { type: 'api', url: props.api } as const;
35
+ }
36
+
37
+ // Handle simplified Directus prop (same interface as Map component)
38
+ if (props.directus) {
39
+ const directusSource = directusShorthandToConfig(props.directus);
40
+ return directusSource ?? undefined;
41
+ }
42
+
43
+ return undefined;
44
+ }, [props.source, props.csv, props.json, props.api, JSON.stringify(props.directus)]);
45
+
46
+ // Memoize the source object to prevent re-creation on every render
47
+ // This is critical for MDX which might recreate objects on every pass
48
+ const stableSource = useMemo(() => resolvedSource, [JSON.stringify(resolvedSource)]);
49
+
50
+ // Memoize columns if provided
51
+ const stableColumns = useMemo(() => props.columns, [JSON.stringify(props.columns)]);
52
+
53
+ // Memoize pagination config if it's an object
54
+ const stablePagination = useMemo(() => {
55
+ if (typeof props.pagination === 'object') {
56
+ return props.pagination;
57
+ }
58
+ return props.pagination;
59
+ }, [JSON.stringify(props.pagination)]);
60
+
61
+ // Memoize initialSort if provided
62
+ const stableInitialSort = useMemo(() => props.initialSort, [JSON.stringify(props.initialSort)]);
63
+
64
+ return (
65
+ <DataTbCore
66
+ {...props}
67
+ source={stableSource}
68
+ columns={stableColumns}
69
+ pagination={stablePagination}
70
+ initialSort={stableInitialSort}
71
+ />
72
+ );
73
+ }
74
+
75
+ export default DataTb;
@@ -0,0 +1,275 @@
1
+ # DataTb Component
2
+
3
+ A powerful, flexible data table component for Astro with support for multiple data sources, built on TanStack Table.
4
+
5
+ ## Features
6
+
7
+ - 📊 **Multiple Data Sources**: CSV, JSON, Directus, and generic APIs
8
+ - 🔍 **Global Search**: Filter data across all columns
9
+ - ↕️ **Column Sorting**: Click headers to sort data
10
+ - 📄 **Pagination**: Built-in pagination with customizable page sizes
11
+ - 🎨 **Auto-detection**: Automatically detects columns from data
12
+ - ⚙️ **Customizable**: Override columns, formatting, and rendering
13
+ - 🎯 **TypeScript**: Full type safety
14
+ - 💅 **Bootstrap Styled**: Uses the same Bootstrap 5 classes as the rest of s:CMS, themeable via `usr/styles/global.css`
15
+
16
+ ## Basic Usage
17
+
18
+ ### CSV Source
19
+
20
+ ```jsx
21
+ import { DataTb } from '@lad-sapienza/scms-core/components/DataTb';
22
+
23
+ <DataTb
24
+ source={{ type: 'csv', url: '/data/products.csv' }}
25
+ searchable
26
+ pagination
27
+ client:idle
28
+ />
29
+ ```
30
+
31
+ ### JSON Source
32
+
33
+ ```jsx
34
+ <DataTb
35
+ source={{
36
+ type: 'json',
37
+ data: [
38
+ { name: 'Item 1', price: 10 },
39
+ { name: 'Item 2', price: 20 }
40
+ ]
41
+ }}
42
+ searchable
43
+ client:idle
44
+ />
45
+ ```
46
+
47
+ ### Directus Source
48
+
49
+ Reads `PUBLIC_DIRECTUS_URL` / `PUBLIC_DIRECTUS_TOKEN` from `.env` automatically — see [Directus Setup](#directus-setup) below.
50
+
51
+ ```jsx
52
+ <DataTb
53
+ directus={{
54
+ table: 'articles',
55
+ queryString: 'filter[status][_eq]=published&sort=-date_published',
56
+ }}
57
+ searchable
58
+ pagination
59
+ client:idle
60
+ />
61
+ ```
62
+
63
+ ### API Source
64
+
65
+ ```jsx
66
+ <DataTb
67
+ source={{
68
+ type: 'api',
69
+ url: 'https://api.example.com/data',
70
+ transformer: (data) => data.results
71
+ }}
72
+ searchable
73
+ pagination
74
+ client:idle
75
+ />
76
+ ```
77
+
78
+ ## Custom Columns
79
+
80
+ Override column display, formatting, and rendering:
81
+
82
+ ```jsx
83
+ <DataTb
84
+ source={{ type: 'csv', url: '/data/products.csv' }}
85
+ columns={[
86
+ { key: 'id', header: 'ID', width: '80px' },
87
+ { key: 'name', header: 'Product Name', sortable: true },
88
+ { key: 'price', header: 'Price', format: 'currency' },
89
+ { key: 'date', header: 'Date', format: 'date' },
90
+ {
91
+ key: 'status',
92
+ header: 'Status',
93
+ render: (val) => val === 'active' ? '✅' : '❌'
94
+ }
95
+ ]}
96
+ searchable
97
+ pagination={{ pageSize: 20 }}
98
+ initialSort={{ columnKey: 'name', direction: 'asc' }}
99
+ client:idle
100
+ />
101
+ ```
102
+
103
+ ## Props
104
+
105
+ ### DataTb Component
106
+
107
+ | Prop | Type | Default | Description |
108
+ |------|------|---------|-------------|
109
+ | `columns` | `ColumnConfig[]` | auto-detected | Column definitions |
110
+ | `searchable` | `boolean` | `false` | Enable global search |
111
+ | `pagination` | `boolean \| PaginationConfig` | `true` | Enable/configure pagination |
112
+ | `sortable` | `boolean` | `true` | Enable column sorting |
113
+ | `initialSort` | `{ columnKey, direction }` | - | Initial sort state |
114
+ | `className` | `string` | `''` | Custom CSS classes |
115
+ | `loadingMessage` | `string` | `'Loading data...'` | Loading message |
116
+ | `emptyMessage` | `string` | `'No data available'` | Empty state message |
117
+ | `errorMessage` | `string` | `'Error loading data'` | Error message |
118
+ | `truncateContent` | `boolean` | `true` | Truncate large cell content with ellipsis |
119
+ | `truncateMaxWidth` | `string` | `'20rem'` | Max width for truncated cell content |
120
+ | `contentSize` | `'sm' \| 'md'` | `'sm'` | Table body text size |
121
+
122
+ ### Column Config
123
+
124
+ ```typescript
125
+ {
126
+ key: string; // Data property key
127
+ header: string; // Display header text
128
+ sortable?: boolean; // Enable sorting (default: true)
129
+ format?: 'date' | 'number' | 'currency' | 'percent';
130
+ render?: (value, row) => ReactNode; // Custom cell renderer
131
+ width?: string; // Column width (CSS value)
132
+ }
133
+ ```
134
+
135
+ ### Source Components
136
+
137
+ #### CsvSource
138
+
139
+ | Prop | Type | Description |
140
+ |------|------|-------------|
141
+ | `url` | `string` | CSV file URL (local or remote) |
142
+ | `delimiter` | `string` | Delimiter character (default: `','`) |
143
+ | `skipRows` | `number` | Skip first N rows |
144
+
145
+ #### JsonSource
146
+
147
+ | Prop | Type | Description |
148
+ |------|------|-------------|
149
+ | `data` | `DataRow[]` | Inline JSON data |
150
+ | `url` | `string` | URL to fetch JSON from |
151
+
152
+ #### DirectusSource
153
+
154
+ | Prop | Type | Description |
155
+ |------|------|-------------|
156
+ | `collection` | `string` | Collection name |
157
+ | `config` | `{ url, token }` | Directus configuration |
158
+ | `filter` | `object` | Directus filter query |
159
+ | `fields` | `string[]` | Fields to select |
160
+ | `sort` | `string[]` | Sort order (e.g., `['-date']`) |
161
+ | `limit` | `number` | Limit results |
162
+
163
+ #### ApiSource
164
+
165
+ | Prop | Type | Description |
166
+ |------|------|-------------|
167
+ | `url` | `string` | API endpoint URL |
168
+ | `method` | `'GET' \| 'POST'` | HTTP method (default: `'GET'`) |
169
+ | `headers` | `Record<string, string>` | Request headers |
170
+ | `body` | `any` | Request body (for POST) |
171
+ | `transformer` | `(data) => DataRow[]` | Transform response data |
172
+
173
+ ## Formatting
174
+
175
+ Built-in formatters for common data types:
176
+
177
+ - `format: 'date'` - Formats dates (e.g., "Jan 10, 2026")
178
+ - `format: 'number'` - Formats numbers with thousands separators
179
+ - `format: 'currency'` - Formats as USD currency
180
+ - `format: 'percent'` - Formats as percentage
181
+
182
+ ## Custom Rendering
183
+
184
+ Use the `render` function for complete control:
185
+
186
+ ```jsx
187
+ columns={[
188
+ {
189
+ key: 'avatar',
190
+ header: 'Avatar',
191
+ render: (url) => <img src={url} alt="" className="w-10 h-10 rounded-full" />
192
+ },
193
+ {
194
+ key: 'status',
195
+ header: 'Status',
196
+ render: (status, row) => (
197
+ <span className={status === 'active' ? 'text-green-600' : 'text-red-600'}>
198
+ {status}
199
+ </span>
200
+ )
201
+ }
202
+ ]}
203
+ ```
204
+
205
+ ## Directus Setup
206
+
207
+ 1. Generate a token in the Directus admin panel:
208
+ - Settings → Access Tokens → Create Token
209
+ - Set appropriate permissions
210
+ - Copy the token
211
+
212
+ 2. Add it to `.env` in the project root:
213
+
214
+ ```env
215
+ PUBLIC_DIRECTUS_URL=https://your-directus-instance.com
216
+ PUBLIC_DIRECTUS_TOKEN=your-access-token
217
+ ```
218
+
219
+ 3. Use the simplified `directus` prop — `DataTb` reads the URL/token from those env vars automatically, no config object needed:
220
+
221
+ ```jsx
222
+ <DataTb
223
+ directus={{
224
+ table: 'your_collection',
225
+ queryString: 'filter[status][_eq]=published',
226
+ }}
227
+ client:idle
228
+ />
229
+ ```
230
+
231
+ The `source={{ type: 'directus', config: { url, token }, collection }}` form shown above is still supported for cases where you need to pass an explicit URL/token instead of the env vars (e.g. multiple Directus instances on one site).
232
+
233
+ ## Client Hydration
234
+
235
+ For optimal performance, use appropriate hydration strategies:
236
+
237
+ - `client:load` - For interactive tables (Directus, API, searchable)
238
+ - `client:idle` - For static data (CSV, JSON) - loads after initial page render
239
+ - `client:visible` - Load when table scrolls into view
240
+
241
+ ```jsx
242
+ {/* Dynamic data - load immediately */}
243
+ <DataTb
244
+ source={{
245
+ type: 'directus',
246
+ collection: 'articles',
247
+ config: directusConfig
248
+ }}
249
+ searchable
250
+ pagination
251
+ client:load
252
+ />
253
+
254
+ {/* Static data - load when idle */}
255
+ <DataTb
256
+ source={{ type: 'csv', url: '/data.csv' }}
257
+ client:idle
258
+ />
259
+ ```
260
+
261
+ ## Architecture
262
+
263
+ The DataTb component follows a simple, self-contained architecture:
264
+
265
+ - **DataTb.tsx** - Main React component using TanStack Table with built-in data fetching
266
+ - **DataTbMdx.tsx** - MDX-safe wrapper (memoizes props, resolves `csv`/`json`/`api`/`directus` shorthands) — this is what `@lad-sapienza/scms-core/components/DataTb`'s `DataTb` export actually is
267
+ - **types.ts** - TypeScript definitions for all source types
268
+ - **utils.ts** - Helper functions for column detection, formatting
269
+ - **sources/** - Standalone headless data-loaders (`CsvSource`, `JsonSource`, `DirectusSource`, `ApiSource`) for building a custom UI outside `DataTb` itself
270
+
271
+ Data fetching is handled internally based on the `source`/`csv`/`json`/`api`/`directus` prop, supporting CSV, JSON, Directus, and generic APIs.
272
+
273
+ ## Examples
274
+
275
+ See the [DataTb docs page](../../../usr/content/docs/components/datatb.mdx) (rendered at `/docs/components/datatb` on a running site) for live, interactive examples of every prop and source type.