@varialkit/table 0.1.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.
package/examples.tsx ADDED
@@ -0,0 +1,1177 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@solara/button';
4
+ import { Checkbox } from '@solara/checkbox';
5
+
6
+ import React, { useEffect, useMemo, useState } from 'react';
7
+ import type { ReactElement } from 'react';
8
+ import { createColumnHelper, flexRender, getCoreRowModel, getExpandedRowModel, getPaginationRowModel, Row, Table as TanStackTable, ColumnDef } from '@tanstack/react-table';
9
+ import { Table } from './src/Table';
10
+ import type { TableDensity, TableGridType, TableLayoutMode } from './src/Table.types';
11
+
12
+ type PersonRow = {
13
+ avatar: string;
14
+ name: string;
15
+ role: string;
16
+ status: string;
17
+ lastActive: string;
18
+ subRows?: PersonRow[];
19
+ };
20
+
21
+ const columnHelper = createColumnHelper<PersonRow>();
22
+
23
+ const data: PersonRow[] = [
24
+ { avatar: 'https://i.pravatar.cc/600?img=11', name: 'Avery Miles', role: 'Product Design', status: 'Active', lastActive: '2 hours ago' },
25
+ { avatar: 'https://i.pravatar.cc/600?img=22', name: 'Ravi Patel', role: 'Engineering', status: 'Active', lastActive: '1 day ago' },
26
+ { avatar: 'https://i.pravatar.cc/600?img=33', name: 'Jordan Lee', role: 'Operations', status: 'Away', lastActive: '3 days ago' },
27
+ { avatar: 'https://i.pravatar.cc/600?img=44', name: 'Maya Chen', role: 'Research', status: 'Active', lastActive: 'Just now' },
28
+ { avatar: 'https://i.pravatar.cc/600?img=55', name: 'Logan Kim', role: 'Sales', status: 'Inactive', lastActive: '2 weeks ago' },
29
+ ];
30
+
31
+ const columns = [
32
+ columnHelper.accessor('avatar', {
33
+ header: 'Photo',
34
+ cell: (info) => (
35
+ <img
36
+ src={info.getValue()}
37
+ alt=""
38
+ style={{
39
+ width: '48px',
40
+ height: '48px',
41
+ borderRadius: '999px',
42
+ objectFit: 'cover',
43
+ }}
44
+ />
45
+ ),
46
+ }),
47
+ columnHelper.accessor('name', {
48
+ header: 'Name',
49
+ cell: (info) => info.getValue(),
50
+ }),
51
+ columnHelper.accessor('role', {
52
+ header: 'Role',
53
+ cell: (info) => info.getValue(),
54
+ }),
55
+ columnHelper.accessor('status', {
56
+ header: 'Status',
57
+ cell: (info) => info.getValue(),
58
+ }),
59
+ columnHelper.accessor('lastActive', {
60
+ header: 'Last Active',
61
+ cell: (info) => info.getValue(),
62
+ }),
63
+ ];
64
+
65
+ const makeLargeData = (count: number): PersonRow[] =>
66
+ Array.from({ length: count }, (_, index) => ({
67
+ avatar: `https://i.pravatar.cc/600?img=${(index % 70) + 1}`,
68
+ name: `Person ${index + 1}`,
69
+ role: ['Engineering', 'Design', 'Operations', 'Sales'][index % 4],
70
+ status: ['Active', 'Away', 'Inactive'][index % 3],
71
+ lastActive: `${(index % 24) + 1} hours ago`,
72
+ }));
73
+
74
+ function DensityMenuExample() {
75
+ const [density, setDensity] = useState<TableDensity>('standard');
76
+
77
+ return (
78
+ <Table
79
+ data={data}
80
+ columns={columns as any}
81
+ // Density lives in state so the column controls menu can update it.
82
+ density={density}
83
+ enableColumnControls
84
+ onDensityChange={setDensity}
85
+ />
86
+ );
87
+ }
88
+
89
+ function GridDefaultExample() {
90
+ const [layoutMode, setLayoutMode] = useState<TableLayoutMode>('grid');
91
+
92
+ return (
93
+ <Table
94
+ data={data}
95
+ columns={columns as any}
96
+ // Start in grid mode and allow switching to list from the built-in action menu.
97
+ layoutMode={layoutMode}
98
+ onLayoutModeChange={setLayoutMode}
99
+ enableLayoutToggle
100
+ enableColumnControls
101
+ gridType="grid"
102
+ // Grid can treat image/media as a full-bleed top area while details remain toggled fields.
103
+ gridColumnConfig={{
104
+ avatar: { variant: 'media', unpadded: true, showLabel: false },
105
+ name: { hiddenByDefault: true },
106
+ role: { hiddenByDefault: true },
107
+ status: { hiddenByDefault: true },
108
+ lastActive: { hiddenByDefault: true },
109
+ }}
110
+ // Grid renderer map lets each column control its own visual output.
111
+ gridCellRenderers={{
112
+ avatar: (cell, row) => (
113
+ <img
114
+ src={String(cell.getValue())}
115
+ alt={`${String(row.original.name)} profile`}
116
+ style={{ width: '100%', height: '100%', minHeight: '180px', objectFit: 'cover' }}
117
+ />
118
+ ),
119
+ status: (cell) => {
120
+ const value = String(cell.getValue());
121
+ const tone =
122
+ value === 'Active'
123
+ ? '#16a34a'
124
+ : value === 'Away'
125
+ ? '#f59e0b'
126
+ : '#64748b';
127
+ return (
128
+ <span
129
+ style={{
130
+ display: 'inline-flex',
131
+ alignItems: 'center',
132
+ borderRadius: '999px',
133
+ padding: '2px 10px',
134
+ backgroundColor: `${tone}1A`,
135
+ color: tone,
136
+ fontWeight: 600,
137
+ }}
138
+ >
139
+ {value}
140
+ </span>
141
+ );
142
+ },
143
+ lastActive: (cell) => (
144
+ <span style={{ color: 'var(--color-text-secondary)' }}>
145
+ {String(cell.getValue())}
146
+ </span>
147
+ ),
148
+ }}
149
+ />
150
+ );
151
+ }
152
+
153
+ function GridMasonryExample() {
154
+ const [layoutMode, setLayoutMode] = useState<TableLayoutMode>('grid');
155
+ const [gridType, setGridType] = useState<TableGridType>('masonry');
156
+ const masonryImageSizes = [
157
+ { width: 720, height: 420 },
158
+ { width: 640, height: 860 },
159
+ { width: 800, height: 500 },
160
+ { width: 600, height: 920 },
161
+ { width: 760, height: 460 },
162
+ ];
163
+
164
+ return (
165
+ <Table
166
+ data={data}
167
+ columns={columns as any}
168
+ layoutMode={layoutMode}
169
+ onLayoutModeChange={setLayoutMode}
170
+ enableLayoutToggle
171
+ enableColumnControls
172
+ // View menu supports List, Standard grid, and Masonry grid.
173
+ gridType={gridType}
174
+ onGridTypeChange={setGridType}
175
+ gridColumnConfig={{
176
+ avatar: { variant: 'media', unpadded: true, showLabel: false },
177
+ name: { hiddenByDefault: true },
178
+ role: { hiddenByDefault: true },
179
+ status: { hiddenByDefault: true },
180
+ lastActive: { hiddenByDefault: true },
181
+ }}
182
+ gridCellRenderers={{
183
+ avatar: (_cell, row) => {
184
+ // Use intentionally different source dimensions so masonry is obvious in docs/storybook.
185
+ const size = masonryImageSizes[row.index % masonryImageSizes.length];
186
+ // Seed by row id + name for stable-but-varied demo images across reloads.
187
+ const seed = `${row.id}-${String(row.original.name).toLowerCase().replace(/\s+/g, '-')}`;
188
+ const src = `https://picsum.photos/seed/${seed}/${size.width}/${size.height}`;
189
+
190
+ return (
191
+ <img
192
+ src={src}
193
+ alt={`${String(row.original.name)} profile`}
194
+ // In standard grid, fill the media slot; in masonry, preserve natural aspect ratio.
195
+ style={
196
+ gridType === 'masonry'
197
+ ? { width: '100%', height: 'auto', display: 'block' }
198
+ : { width: '100%', height: '100%', display: 'block', objectFit: 'cover' }
199
+ }
200
+ />
201
+ );
202
+ },
203
+ }}
204
+ />
205
+ );
206
+ }
207
+
208
+ function ListOnlyLayoutExample() {
209
+ return (
210
+ <Table
211
+ data={data}
212
+ columns={columns as any}
213
+ layoutMode="list"
214
+ // List-only surfaces keep the same controls without exposing grid as a mode.
215
+ layoutModeOptions={['list']}
216
+ enableLayoutToggle
217
+ enableColumnControls
218
+ />
219
+ );
220
+ }
221
+
222
+ function SelectableRowsExample() {
223
+ const [rowSelection, setRowSelection] = React.useState({});
224
+
225
+ const columnsWithSelect = useMemo(
226
+ () => [
227
+ {
228
+ id: 'select',
229
+ header: function Header({ table }: { table: TanStackTable<PersonRow> }) {
230
+ // Solara Checkbox supports indeterminate directly, so we can mirror TanStack state without refs.
231
+ const isAllRowsSelected = table.getIsAllRowsSelected();
232
+ const isSomeRowsSelected = table.getIsSomeRowsSelected();
233
+
234
+ return (
235
+ <Checkbox
236
+ aria-label="Select all rows"
237
+ size="small"
238
+ checked={isAllRowsSelected}
239
+ indeterminate={!isAllRowsSelected && isSomeRowsSelected}
240
+ onChange={table.getToggleAllRowsSelectedHandler()}
241
+ />
242
+ );
243
+ },
244
+ cell: ({ row }: { row: Row<PersonRow> }) => (
245
+ <Checkbox
246
+ aria-label={`Select row ${row.id}`}
247
+ size="small"
248
+ checked={row.getIsSelected()}
249
+ disabled={!row.getCanSelect()}
250
+ onChange={row.getToggleSelectedHandler()}
251
+ />
252
+ ),
253
+ },
254
+ ...columns,
255
+ ],
256
+ []
257
+ );
258
+
259
+ return (
260
+ <Table
261
+ data={data}
262
+ columns={columnsWithSelect as any}
263
+ tableOptions={{
264
+ getCoreRowModel: getCoreRowModel(),
265
+ enableRowSelection: true,
266
+ state: { rowSelection },
267
+ onRowSelectionChange: setRowSelection,
268
+ }}
269
+ />
270
+ );
271
+ }
272
+
273
+ const dataWithSubRows: PersonRow[] = [
274
+ {
275
+ name: 'Avery Miles',
276
+ avatar: 'https://i.pravatar.cc/600?img=11',
277
+ role: 'Product Design',
278
+ status: 'Active',
279
+ lastActive: '2 hours ago',
280
+ subRows: [
281
+ { avatar: 'https://i.pravatar.cc/600?img=12', name: 'Sub Avery 1', role: 'Sub Role 1', status: 'Active', lastActive: '2 hours ago' },
282
+ { avatar: 'https://i.pravatar.cc/600?img=13', name: 'Sub Avery 2', role: 'Sub Role 2', status: 'Away', lastActive: '1 day ago' },
283
+ ],
284
+ },
285
+ { avatar: 'https://i.pravatar.cc/600?img=22', name: 'Ravi Patel', role: 'Engineering', status: 'Active', lastActive: '1 day ago', subRows: [] },
286
+ {
287
+ avatar: 'https://i.pravatar.cc/600?img=33',
288
+ name: 'Jordan Lee',
289
+ role: 'Operations',
290
+ status: 'Away',
291
+ lastActive: '3 days ago',
292
+ subRows: [
293
+ { avatar: 'https://i.pravatar.cc/600?img=34', name: 'Sub Jordan 1', role: 'Sub Role 3', status: 'Inactive', lastActive: '2 weeks ago' },
294
+ ],
295
+ },
296
+ { avatar: 'https://i.pravatar.cc/600?img=44', name: 'Maya Chen', role: 'Research', status: 'Active', lastActive: 'Just now', subRows: [] },
297
+ { avatar: 'https://i.pravatar.cc/600?img=55', name: 'Logan Kim', role: 'Sales', status: 'Inactive', lastActive: '2 weeks ago', subRows: [] },
298
+ ];
299
+
300
+ function ExpandableRowsExample() {
301
+ const [expanded, setExpanded] = React.useState({});
302
+
303
+ const columnsWithToggle = useMemo(
304
+ () => [
305
+ {
306
+ id: 'expand',
307
+ header: () => null,
308
+ cell: ({ row }: { row: Row<PersonRow> }) => {
309
+ return row.getCanExpand() ? (
310
+ <Button
311
+ aria-label={row.getIsExpanded() ? 'Collapse row' : 'Expand row'}
312
+ variant="ghost"
313
+ iconOnly={row.getIsExpanded() ? 'arrow_chevron_down_16' : 'arrow_chevron_right_16'}
314
+ onClick={row.getToggleExpandedHandler()}
315
+ />
316
+ ) : null;
317
+ },
318
+ },
319
+ ...columns,
320
+ ],
321
+ []
322
+ );
323
+
324
+ return (
325
+ <Table
326
+ data={dataWithSubRows}
327
+ columns={columnsWithToggle as any}
328
+ tableOptions={{
329
+ state: { expanded },
330
+ onExpandedChange: setExpanded,
331
+ getSubRows: (row) => row.subRows,
332
+ getCoreRowModel: getCoreRowModel(),
333
+ getExpandedRowModel: getExpandedRowModel(),
334
+ }}
335
+ />
336
+ );
337
+ }
338
+
339
+ function PinnedColumnsExample() {
340
+ const [columnPinning, setColumnPinning] = useState({
341
+ left: ['name'],
342
+ right: [],
343
+ });
344
+ const [density, setDensity] = useState<TableDensity>('standard');
345
+
346
+ const pinnedColumns = useMemo(
347
+ () => [
348
+ columnHelper.accessor('name', {
349
+ header: 'Name',
350
+ cell: (info) => info.getValue(),
351
+ }),
352
+ columnHelper.accessor('role', {
353
+ header: 'Role',
354
+ cell: (info) => info.getValue(),
355
+ }),
356
+ columnHelper.accessor('status', {
357
+ header: 'Status',
358
+ cell: (info) => info.getValue(),
359
+ }),
360
+ columnHelper.accessor('lastActive', {
361
+ header: 'Last Active',
362
+ cell: (info) => info.getValue(),
363
+ }),
364
+ ],
365
+ []
366
+ );
367
+
368
+ return (
369
+ <div style={{ maxWidth: '520px' }}>
370
+ <Table
371
+ data={[...data, ...data, ...data, ...data]}
372
+ columns={pinnedColumns as any}
373
+ transparentSticky
374
+ transparentBackground
375
+ wrapperProps={{ style: { maxHeight: '220px', overflow: 'auto' } }}
376
+ tableProps={{ style: { minWidth: '640px' } }}
377
+ // Built-in menu handles visibility and order; pinning is still controlled via TanStack state.
378
+ enableColumnControls
379
+ density={density}
380
+ onDensityChange={setDensity}
381
+ tableOptions={{
382
+ getCoreRowModel: getCoreRowModel(),
383
+ enableColumnPinning: true,
384
+ state: { columnPinning },
385
+ onColumnPinningChange: setColumnPinning as any,
386
+ }}
387
+ getHeaderProps={(header) => {
388
+ const pinState = header.column.getIsPinned();
389
+ if (!pinState) return {};
390
+ const left = header.column.getStart('left');
391
+ return {
392
+ style: { left },
393
+ className: 'solara-table__header-cell--pinned',
394
+ };
395
+ }}
396
+ getCellProps={(cell) => {
397
+ const pinState = cell.column.getIsPinned();
398
+ if (!pinState) return {};
399
+ const left = cell.column.getStart('left');
400
+ return {
401
+ style: { left },
402
+ className: 'solara-table__cell--pinned',
403
+ };
404
+ }}
405
+ />
406
+ </div>
407
+ );
408
+ }
409
+
410
+ function PaginationExample() {
411
+ const [pagination, setPagination] = useState({
412
+ pageIndex: 0,
413
+ pageSize: 5,
414
+ });
415
+
416
+ return (
417
+ <Table
418
+ data={data}
419
+ columns={columns as any}
420
+ tableOptions={{
421
+ // Keep pagination state in TanStack so UI stays decoupled.
422
+ state: { pagination },
423
+ onPaginationChange: setPagination,
424
+ getPaginationRowModel: getPaginationRowModel(),
425
+ getCoreRowModel: getCoreRowModel(),
426
+ }}
427
+ />
428
+ );
429
+ }
430
+
431
+ function PaginationControlsExample() {
432
+ const [pagination, setPagination] = useState({
433
+ pageIndex: 0,
434
+ pageSize: 5,
435
+ });
436
+
437
+ const largeData = useMemo(() => makeLargeData(2000), []);
438
+
439
+ return (
440
+ <div>
441
+ <Table
442
+ data={largeData}
443
+ columns={columns as any}
444
+ showPagination
445
+ tableOptions={{
446
+ // Pagination is controlled here; footer UI reads from the same state.
447
+ state: { pagination },
448
+ onPaginationChange: setPagination,
449
+ getPaginationRowModel: getPaginationRowModel(),
450
+ getCoreRowModel: getCoreRowModel(),
451
+ }}
452
+ />
453
+ </div>
454
+ );
455
+ }
456
+
457
+ function VirtualizedRowsExample() {
458
+ const [scrollTop, setScrollTop] = useState(0);
459
+ const [viewportHeight, setViewportHeight] = useState(320);
460
+ const rowHeight = 40;
461
+ const overscan = 6;
462
+ const largeData = useMemo(() => makeLargeData(2000), []);
463
+
464
+ return (
465
+ <Table
466
+ data={largeData}
467
+ columns={columns as any}
468
+ transparentSticky
469
+ transparentBackground
470
+ stickyHeader
471
+ wrapperProps={{
472
+ style: { maxHeight: '320px', overflow: 'auto' },
473
+ onScroll: (event) => {
474
+ const target = event.currentTarget;
475
+ setScrollTop(target.scrollTop);
476
+ setViewportHeight(target.clientHeight);
477
+ },
478
+ }}
479
+ renderBody={(table, rows) => {
480
+ // Virtualize the row model we intend to display (post-sorting/pagination).
481
+ const totalSize = rows.length * rowHeight;
482
+ const startIndex = Math.max(
483
+ 0,
484
+ Math.floor(scrollTop / rowHeight) - overscan
485
+ );
486
+ const endIndex = Math.min(
487
+ rows.length,
488
+ Math.ceil((scrollTop + viewportHeight) / rowHeight) + overscan
489
+ );
490
+ const visibleRows = rows.slice(startIndex, endIndex);
491
+ const paddingTop = startIndex * rowHeight;
492
+ const paddingBottom = totalSize - endIndex * rowHeight;
493
+ const colSpan = table.getVisibleLeafColumns().length;
494
+
495
+ return (
496
+ <tbody className="solara-table__body">
497
+ {paddingTop > 0 ? (
498
+ <tr aria-hidden="true">
499
+ <td colSpan={colSpan} style={{ height: `${paddingTop}px`, padding: 0 }} />
500
+ </tr>
501
+ ) : null}
502
+ {visibleRows.map((row) => (
503
+ <tr key={row.id} className="solara-table__row" style={{ height: `${rowHeight}px` }}>
504
+ {row.getVisibleCells().map((cell) => (
505
+ <td key={cell.id} className="solara-table__cell">
506
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
507
+ </td>
508
+ ))}
509
+ </tr>
510
+ ))}
511
+ {paddingBottom > 0 ? (
512
+ <tr aria-hidden="true">
513
+ <td colSpan={colSpan} style={{ height: `${paddingBottom}px`, padding: 0 }} />
514
+ </tr>
515
+ ) : null}
516
+ </tbody>
517
+ );
518
+ }}
519
+ />
520
+ );
521
+ }
522
+
523
+ function LargeDataPerformanceExample() {
524
+ const [pagination, setPagination] = useState({
525
+ pageIndex: 0,
526
+ pageSize: 25,
527
+ });
528
+ const [isLoading, setIsLoading] = useState(false);
529
+ const largeData = useMemo(() => makeLargeData(5000), []);
530
+ const totalRows = isLoading ? 0 : largeData.length;
531
+ const pageCount = Math.max(1, Math.ceil(totalRows / pagination.pageSize));
532
+ const safePageIndex = Math.min(pagination.pageIndex, pageCount - 1);
533
+ const safePagination = {
534
+ pageIndex: safePageIndex,
535
+ pageSize: pagination.pageSize,
536
+ };
537
+
538
+ useEffect(() => {
539
+ // Clamp page index when row count changes (e.g., loading -> empty).
540
+ if (safePageIndex !== pagination.pageIndex) {
541
+ setPagination((prev) => ({ ...prev, pageIndex: safePageIndex }));
542
+ }
543
+ }, [pagination.pageIndex, safePageIndex]);
544
+
545
+ const toggleLoading = () => {
546
+ // Reset to page 0 so we never point past the end during loading.
547
+ setIsLoading((prev) => !prev);
548
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }));
549
+ };
550
+
551
+ return (
552
+ <div>
553
+ <div style={{ marginBottom: '12px', display: 'flex', gap: '8px' }}>
554
+ <button type="button" onClick={toggleLoading}>
555
+ Toggle loading
556
+ </button>
557
+ <span>{isLoading ? 'Loading rows...' : 'Loaded rows'}</span>
558
+ </div>
559
+ <Table
560
+ data={isLoading ? [] : largeData}
561
+ columns={columns as any}
562
+ emptyState={isLoading ? 'Loading...' : 'No results.'}
563
+ tableOptions={{
564
+ state: { pagination: safePagination },
565
+ onPaginationChange: setPagination,
566
+ getPaginationRowModel: getPaginationRowModel(),
567
+ getCoreRowModel: getCoreRowModel(),
568
+ }}
569
+ />
570
+ </div>
571
+ );
572
+ }
573
+
574
+ type StoryControlOption = {
575
+ label: string;
576
+ value: string;
577
+ };
578
+
579
+ type StoryControl = {
580
+ name: string;
581
+ label?: string;
582
+ type: 'select' | 'text' | 'boolean' | 'number';
583
+ options?: Array<string | StoryControlOption>;
584
+ min?: number;
585
+ max?: number;
586
+ step?: number;
587
+ };
588
+
589
+ type StoryDefinition = {
590
+ title: string;
591
+ description?: string;
592
+ render: (props: Record<string, unknown>) => ReactElement;
593
+ controls?: StoryControl[];
594
+ initialProps?: Record<string, unknown>;
595
+ showProps?: boolean;
596
+ applyPropsToPreview?: boolean;
597
+ code?: string;
598
+ };
599
+
600
+ export const stories: Record<string, StoryDefinition> = {
601
+ playground: {
602
+ title: 'Playground',
603
+ description: 'Adjust the density and behavior of the Table wrapper.',
604
+ render: (props) => (
605
+ <Table
606
+ data={data}
607
+ columns={columns as any}
608
+ transparentSticky
609
+ transparentBackground
610
+ density={props.density as TableDensity}
611
+ showHeader={props.showHeader as boolean}
612
+ layoutMode={props.layoutMode as TableLayoutMode}
613
+ stickyHeader={props.stickyHeader as boolean}
614
+ enableSorting={props.enableSorting as boolean}
615
+ // Column controls menu lives in the header action column.
616
+ enableColumnControls={props.enableColumnControls as boolean}
617
+ enableLayoutToggle={props.enableLayoutToggle as boolean}
618
+ showPagination={props.showPagination as boolean}
619
+ />
620
+ ),
621
+ controls: [
622
+ {
623
+ name: 'density',
624
+ label: 'Density',
625
+ type: 'select',
626
+ options: ['compact', 'standard', 'spacious'],
627
+ },
628
+ {
629
+ name: 'layoutMode',
630
+ label: 'Layout Mode',
631
+ type: 'select',
632
+ options: ['list', 'grid'],
633
+ },
634
+ {
635
+ name: 'showHeader',
636
+ label: 'Show Header',
637
+ type: 'boolean',
638
+ },
639
+ {
640
+ name: 'stickyHeader',
641
+ label: 'Sticky Header',
642
+ type: 'boolean',
643
+ },
644
+ {
645
+ name: 'enableSorting',
646
+ label: 'Enable Sorting',
647
+ type: 'boolean',
648
+ },
649
+ {
650
+ name: 'enableColumnControls',
651
+ label: 'Column Controls',
652
+ type: 'boolean',
653
+ },
654
+ {
655
+ name: 'enableLayoutToggle',
656
+ label: 'Layout Toggle',
657
+ type: 'boolean',
658
+ },
659
+ {
660
+ name: 'showPagination',
661
+ label: 'Show Pagination',
662
+ type: 'boolean',
663
+ },
664
+ ],
665
+ initialProps: {
666
+ density: 'standard',
667
+ layoutMode: 'list',
668
+ showHeader: true,
669
+ stickyHeader: false,
670
+ enableSorting: true,
671
+ enableColumnControls: true,
672
+ enableLayoutToggle: true,
673
+ showPagination: false,
674
+ },
675
+ },
676
+ gridDefault: {
677
+ title: 'Grid Default',
678
+ showProps: true,
679
+ render: () => <GridDefaultExample />,
680
+ code: `import { Table } from "@solara/table";
681
+ import type { TableLayoutMode } from "@solara/table";
682
+
683
+ export function Example() {
684
+ const [layoutMode, setLayoutMode] = useState<TableLayoutMode>("grid");
685
+
686
+ return (
687
+ <Table
688
+ data={data}
689
+ columns={columns as any}
690
+ layoutMode={layoutMode}
691
+ onLayoutModeChange={setLayoutMode}
692
+ enableLayoutToggle
693
+ enableColumnControls
694
+ gridColumnConfig={{
695
+ avatar: { variant: "media", unpadded: true, showLabel: false },
696
+ name: { hiddenByDefault: true },
697
+ role: { hiddenByDefault: true },
698
+ status: { hiddenByDefault: true },
699
+ lastActive: { hiddenByDefault: true },
700
+ }}
701
+ gridCellRenderers={{
702
+ avatar: (cell, row) => (
703
+ <img
704
+ src={String(cell.getValue())}
705
+ alt={String(row.original.name) + " profile"}
706
+ style={{ width: "100%", height: "100%", minHeight: "180px", objectFit: "cover" }}
707
+ />
708
+ ),
709
+ status: (cell) => <StatusPill value={String(cell.getValue())} />,
710
+ }}
711
+ />
712
+ );
713
+ }
714
+ `,
715
+ },
716
+ gridMasonry: {
717
+ title: 'Grid Masonry',
718
+ showProps: true,
719
+ render: () => <GridMasonryExample />,
720
+ code: `import { Table } from "@solara/table";
721
+ import type { TableGridType, TableLayoutMode } from "@solara/table";
722
+
723
+ export function Example() {
724
+ const [layoutMode, setLayoutMode] = useState<TableLayoutMode>("grid");
725
+ const [gridType, setGridType] = useState<TableGridType>("masonry");
726
+ const masonryImageSizes = [
727
+ { width: 720, height: 420 },
728
+ { width: 640, height: 860 },
729
+ { width: 800, height: 500 },
730
+ { width: 600, height: 920 },
731
+ { width: 760, height: 460 },
732
+ ];
733
+
734
+ return (
735
+ <Table
736
+ data={data}
737
+ columns={columns as any}
738
+ layoutMode={layoutMode}
739
+ onLayoutModeChange={setLayoutMode}
740
+ enableLayoutToggle
741
+ enableColumnControls
742
+ gridType={gridType}
743
+ onGridTypeChange={setGridType}
744
+ gridColumnConfig={{
745
+ avatar: { variant: "media", unpadded: true, showLabel: false },
746
+ name: { hiddenByDefault: true },
747
+ role: { hiddenByDefault: true },
748
+ status: { hiddenByDefault: true },
749
+ lastActive: { hiddenByDefault: true },
750
+ }}
751
+ gridCellRenderers={{
752
+ avatar: (_cell, row) => {
753
+ const size = masonryImageSizes[row.index % masonryImageSizes.length];
754
+ const seed = \`\${row.id}-\${String(row.original.name).toLowerCase().replace(/\\s+/g, "-")}\`;
755
+ const src = \`https://picsum.photos/seed/\${seed}/\${size.width}/\${size.height}\`;
756
+ return (
757
+ <img
758
+ src={src}
759
+ alt={String(row.original.name) + " profile"}
760
+ style={
761
+ gridType === "masonry"
762
+ ? { width: "100%", height: "auto", display: "block" }
763
+ : { width: "100%", height: "100%", display: "block", objectFit: "cover" }
764
+ }
765
+ />
766
+ );
767
+ },
768
+ }}
769
+ />
770
+ );
771
+ }
772
+ `,
773
+ },
774
+ listOnlyLayout: {
775
+ title: 'List Only Layout',
776
+ showProps: true,
777
+ render: () => <ListOnlyLayoutExample />,
778
+ code: `import { Table } from "@solara/table";
779
+
780
+ export function Example() {
781
+ return (
782
+ <Table
783
+ data={data}
784
+ columns={columns}
785
+ layoutMode="list"
786
+ layoutModeOptions={["list"]}
787
+ enableLayoutToggle
788
+ enableColumnControls
789
+ />
790
+ );
791
+ }
792
+ `,
793
+ },
794
+ overview: {
795
+ title: 'Overview',
796
+ showProps: true,
797
+ render: () => <DensityMenuExample />,
798
+ code: `import { Table } from "@solara/table";
799
+ import { createColumnHelper } from "@tanstack/react-table";
800
+
801
+ const columnHelper = createColumnHelper();
802
+
803
+ const columns = [
804
+ columnHelper.accessor("name", { header: "Name" }),
805
+ columnHelper.accessor("role", { header: "Role" }),
806
+ columnHelper.accessor("status", { header: "Status" }),
807
+ columnHelper.accessor("lastActive", { header: "Last Active" }),
808
+ ];
809
+
810
+ export function Example() {
811
+ const [density, setDensity] = useState("standard");
812
+
813
+ return (
814
+ <Table
815
+ data={data}
816
+ columns={columns}
817
+ density={density}
818
+ enableSorting
819
+ enableColumnControls
820
+ onDensityChange={setDensity}
821
+ />
822
+ );
823
+ }
824
+ `,
825
+ },
826
+ stickyHeader: {
827
+ title: 'Sticky Header',
828
+ showProps: true,
829
+ render: () => (
830
+ <Table
831
+ data={[...data, ...data, ...data]}
832
+ columns={columns as any}
833
+ transparentSticky
834
+ transparentBackground
835
+ stickyHeader
836
+ // Scroll container must be on the wrapper for sticky headers to work.
837
+ wrapperProps={{ style: { maxHeight: '220px', overflow: 'auto' } }}
838
+ enableColumnControls
839
+ />
840
+ ),
841
+ code: `import { Table } from "@solara/table";
842
+
843
+ export function Example() {
844
+ return (
845
+ <Table
846
+ data={data}
847
+ columns={columns}
848
+ stickyHeader
849
+ wrapperProps={{ style: { maxHeight: 220, overflow: "auto" } }}
850
+ />
851
+ );
852
+ }
853
+ `,
854
+ },
855
+ pinnedColumns: {
856
+ title: 'Pinned Columns',
857
+ showProps: true,
858
+ render: () => <PinnedColumnsExample />,
859
+ code: `import { Table } from "@solara/table";
860
+ import { createColumnHelper } from "@tanstack/react-table";
861
+
862
+ const columnHelper = createColumnHelper();
863
+
864
+ export function Example() {
865
+ const [columnPinning, setColumnPinning] = useState({
866
+ left: ["name"],
867
+ right: [],
868
+ });
869
+
870
+ const columns = [
871
+ columnHelper.accessor("name", { header: "Name" }),
872
+ columnHelper.accessor("role", { header: "Role" }),
873
+ columnHelper.accessor("status", { header: "Status" }),
874
+ columnHelper.accessor("lastActive", { header: "Last Active" }),
875
+ ];
876
+
877
+ return (
878
+ <Table
879
+ data={data}
880
+ columns={columns as any}
881
+ tableProps={{ style: { minWidth: 640 } }}
882
+ enableColumnControls
883
+ tableOptions={{
884
+ enableColumnPinning: true,
885
+ state: { columnPinning },
886
+ onColumnPinningChange: setColumnPinning,
887
+ }}
888
+ getHeaderProps={(header) => {
889
+ if (!header.column.getIsPinned()) return {};
890
+ return {
891
+ style: { left: header.column.getStart("left") },
892
+ className: "solara-table__header-cell--pinned",
893
+ };
894
+ }}
895
+ getCellProps={(cell) => {
896
+ if (!cell.column.getIsPinned()) return {};
897
+ return {
898
+ style: { left: cell.column.getStart("left") },
899
+ className: "solara-table__cell--pinned",
900
+ };
901
+ }}
902
+ />
903
+ );
904
+ }
905
+ `,
906
+ },
907
+ pagination: {
908
+ title: 'Pagination',
909
+ showProps: true,
910
+ render: () => <PaginationExample />,
911
+ code: `import { Table } from "@solara/table";
912
+ import { getPaginationRowModel } from "@tanstack/react-table";
913
+
914
+ export function Example() {
915
+ const [pagination, setPagination] = useState({
916
+ pageIndex: 0,
917
+ pageSize: 5,
918
+ });
919
+
920
+ return (
921
+ <Table
922
+ data={data}
923
+ columns={columns as any}
924
+ tableOptions={{
925
+ state: { pagination },
926
+ onPaginationChange: setPagination,
927
+ getPaginationRowModel: getPaginationRowModel(),
928
+ }}
929
+ />
930
+ );
931
+ }
932
+ `,
933
+ },
934
+ paginationControls: {
935
+ title: 'Pagination Controls',
936
+ showProps: true,
937
+ render: () => <PaginationControlsExample />,
938
+ code: `import { Table } from "@solara/table";
939
+ import { getPaginationRowModel } from "@tanstack/react-table";
940
+
941
+ export function Example() {
942
+ const [pagination, setPagination] = useState({
943
+ pageIndex: 0,
944
+ pageSize: 5,
945
+ });
946
+ const largeData = useMemo(() => makeLargeData(2000), []);
947
+
948
+ return (
949
+ <div>
950
+ <Table
951
+ data={largeData}
952
+ columns={columns as any}
953
+ showPagination
954
+ tableOptions={{
955
+ state: { pagination },
956
+ onPaginationChange: setPagination,
957
+ getPaginationRowModel: getPaginationRowModel(),
958
+ }}
959
+ />
960
+ </div>
961
+ );
962
+ }
963
+ `,
964
+ },
965
+ virtualizedRows: {
966
+ title: 'Virtualized Rows',
967
+ showProps: true,
968
+ render: () => <VirtualizedRowsExample />,
969
+ code: `import { Table } from "@solara/table";
970
+ import { flexRender } from "@tanstack/react-table";
971
+
972
+ export function Example() {
973
+ const [scrollTop, setScrollTop] = useState(0);
974
+ const [viewportHeight, setViewportHeight] = useState(320);
975
+ const rowHeight = 40;
976
+ const overscan = 6;
977
+
978
+ return (
979
+ <Table
980
+ data={largeData}
981
+ columns={columns as any}
982
+ stickyHeader
983
+ wrapperProps={{
984
+ style: { maxHeight: 320, overflow: "auto" },
985
+ onScroll: (event) => {
986
+ const target = event.currentTarget;
987
+ setScrollTop(target.scrollTop);
988
+ setViewportHeight(target.clientHeight);
989
+ },
990
+ }}
991
+ renderBody={(table, rows) => {
992
+ const totalSize = rows.length * rowHeight;
993
+ const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
994
+ const endIndex = Math.min(
995
+ rows.length,
996
+ Math.ceil((scrollTop + viewportHeight) / rowHeight) + overscan
997
+ );
998
+ const visibleRows = rows.slice(startIndex, endIndex);
999
+ const paddingTop = startIndex * rowHeight;
1000
+ const paddingBottom = totalSize - endIndex * rowHeight;
1001
+ const colSpan = table.getVisibleLeafColumns().length;
1002
+
1003
+ return (
1004
+ <tbody className="solara-table__body">
1005
+ {paddingTop > 0 ? (
1006
+ <tr aria-hidden="true">
1007
+ <td colSpan={colSpan} style={{ height: paddingTop, padding: 0 }} />
1008
+ </tr>
1009
+ ) : null}
1010
+ {visibleRows.map((row) => (
1011
+ <tr key={row.id} className="solara-table__row">
1012
+ {row.getVisibleCells().map((cell) => (
1013
+ <td key={cell.id} className="solara-table__cell">
1014
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
1015
+ </td>
1016
+ ))}
1017
+ </tr>
1018
+ ))}
1019
+ {paddingBottom > 0 ? (
1020
+ <tr aria-hidden="true">
1021
+ <td colSpan={colSpan} style={{ height: paddingBottom, padding: 0 }} />
1022
+ </tr>
1023
+ ) : null}
1024
+ </tbody>
1025
+ );
1026
+ }}
1027
+ />
1028
+ );
1029
+ }
1030
+ `,
1031
+ },
1032
+ expandableRows: {
1033
+ title: 'Expandable Rows',
1034
+ showProps: true,
1035
+ render: () => <ExpandableRowsExample />,
1036
+ code: `import { Table } from "@solara/table";
1037
+ import { createColumnHelper, getCoreRowModel, getExpandedRowModel } from "@tanstack/react-table";
1038
+
1039
+ const columnHelper = createColumnHelper();
1040
+
1041
+ export function Example() {
1042
+ const [expanded, setExpanded] = React.useState({});
1043
+
1044
+ const columnsWithToggle = React.useMemo(
1045
+ () => [
1046
+ {
1047
+ id: 'expand',
1048
+ header: () => null,
1049
+ cell: ({ row }) => {
1050
+ return row.getCanExpand() ? (
1051
+ <button
1052
+ aria-label={row.getIsExpanded() ? 'Collapse row' : 'Expand row'}
1053
+ onClick={row.getToggleExpandedHandler()}
1054
+ >
1055
+ {row.getIsExpanded() ? '👇' : '👉'}
1056
+ </button>
1057
+ ) : null;
1058
+ },
1059
+ },
1060
+ ...columns,
1061
+ ],
1062
+ []
1063
+ );
1064
+
1065
+ return (
1066
+ <Table
1067
+ data={dataWithSubRows}
1068
+ columns={columnsWithToggle as any}
1069
+ tableOptions={{
1070
+ state: { expanded },
1071
+ onExpandedChange: setExpanded,
1072
+ getSubRows: (row) => row.subRows,
1073
+ getCoreRowModel: getCoreRowModel(),
1074
+ getExpandedRowModel: getExpandedRowModel(),
1075
+ }}
1076
+ />
1077
+ );
1078
+ }
1079
+ `,
1080
+ },
1081
+ selectableRows: {
1082
+ title: 'Selectable Rows',
1083
+ showProps: true,
1084
+ render: () => <SelectableRowsExample />,
1085
+ code: `import { Table } from "@solara/table";
1086
+ import { Checkbox } from "@solara/checkbox";
1087
+ import { createColumnHelper } from "@tanstack/react-table";
1088
+
1089
+ const columnHelper = createColumnHelper();
1090
+
1091
+ export function Example() {
1092
+ const [rowSelection, setRowSelection] = React.useState({});
1093
+
1094
+ const columnsWithSelect = React.useMemo(
1095
+ () => [
1096
+ {
1097
+ id: "select",
1098
+ header: ({ table }) => {
1099
+ const isAllRowsSelected = table.getIsAllRowsSelected();
1100
+ const isSomeRowsSelected = table.getIsSomeRowsSelected();
1101
+ return (
1102
+ <Checkbox
1103
+ aria-label="Select all rows"
1104
+ size="small"
1105
+ checked={isAllRowsSelected}
1106
+ indeterminate={!isAllRowsSelected && isSomeRowsSelected}
1107
+ onChange={table.getToggleAllRowsSelectedHandler()}
1108
+ />
1109
+ );
1110
+ },
1111
+ cell: ({ row }) => (
1112
+ <Checkbox
1113
+ aria-label={"Select row " + row.id}
1114
+ size="small"
1115
+ checked={row.getIsSelected()}
1116
+ disabled={!row.getCanSelect()}
1117
+ onChange={row.getToggleSelectedHandler()}
1118
+ />
1119
+ ),
1120
+ },
1121
+ ...columns,
1122
+ ],
1123
+ []
1124
+ );
1125
+
1126
+ return (
1127
+ <Table
1128
+ data={data}
1129
+ columns={columnsWithSelect as any}
1130
+ tableOptions={{
1131
+ enableRowSelection: true,
1132
+ state: { rowSelection },
1133
+ onRowSelectionChange: setRowSelection,
1134
+ getCoreRowModel: getCoreRowModel(),
1135
+ }}
1136
+ />
1137
+ );
1138
+ }
1139
+ `,
1140
+ },
1141
+ largeDataPerformance: {
1142
+ title: 'Large Data Performance',
1143
+ showProps: true,
1144
+ render: () => <LargeDataPerformanceExample />,
1145
+ code: `import { Table } from "@solara/table";
1146
+ import { getPaginationRowModel } from "@tanstack/react-table";
1147
+
1148
+ export function Example() {
1149
+ const [pagination, setPagination] = useState({
1150
+ pageIndex: 0,
1151
+ pageSize: 25,
1152
+ });
1153
+ const [isLoading, setIsLoading] = useState(false);
1154
+ const largeData = useMemo(() => makeLargeData(5000), []);
1155
+
1156
+ return (
1157
+ <div>
1158
+ <button type="button" onClick={() => setIsLoading((prev) => !prev)}>
1159
+ Toggle loading
1160
+ </button>
1161
+ <Table
1162
+ data={isLoading ? [] : largeData}
1163
+ columns={columns as any}
1164
+ emptyState={isLoading ? "Loading..." : "No results."}
1165
+ tableOptions={{
1166
+ state: { pagination },
1167
+ onPaginationChange: setPagination,
1168
+ getPaginationRowModel: getPaginationRowModel(),
1169
+ getCoreRowModel: getCoreRowModel(),
1170
+ }}
1171
+ />
1172
+ </div>
1173
+ );
1174
+ }
1175
+ `,
1176
+ },
1177
+ };