@djangocfg/widget-data 0.1.1
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/LICENSE +21 -0
- package/README.md +55 -0
- package/package.json +122 -0
- package/src/DataGrid/README.md +48 -0
- package/src/DataGrid/context/DataGridContext.tsx +442 -0
- package/src/DataGrid/context/index.ts +4 -0
- package/src/DataGrid/index.ts +34 -0
- package/src/DataGrid/lazy.tsx +23 -0
- package/src/DataGrid/parts/DataGrid.tsx +125 -0
- package/src/DataGrid/parts/DataGridCell.tsx +30 -0
- package/src/DataGrid/parts/DataGridHeader.tsx +111 -0
- package/src/DataGrid/parts/DataGridPagination.tsx +105 -0
- package/src/DataGrid/parts/DataGridRow.tsx +84 -0
- package/src/DataGrid/parts/index.ts +7 -0
- package/src/DataGrid/types.ts +95 -0
- package/src/DataTable/README.md +42 -0
- package/src/DataTable/context/DataTableContext.tsx +434 -0
- package/src/DataTable/context/index.ts +4 -0
- package/src/DataTable/index.ts +33 -0
- package/src/DataTable/lazy.tsx +23 -0
- package/src/DataTable/parts/DataTable.tsx +142 -0
- package/src/DataTable/parts/DataTableHeader.tsx +103 -0
- package/src/DataTable/parts/DataTablePagination.tsx +115 -0
- package/src/DataTable/parts/DataTableRow.tsx +96 -0
- package/src/DataTable/parts/index.ts +6 -0
- package/src/DataTable/types.ts +88 -0
- package/src/FileIcon/FileIcon.tsx +87 -0
- package/src/FileIcon/get-file-icon.ts +73 -0
- package/src/FileIcon/icons/icon-data.ts +399 -0
- package/src/FileIcon/index.ts +19 -0
- package/src/FileIcon/loader.ts +29 -0
- package/src/FileIcon/specialFolders.ts +110 -0
- package/src/FileIcon/treeAdapter.tsx +49 -0
- package/src/JsonTree/JsonViewer.tsx +720 -0
- package/src/JsonTree/README.md +142 -0
- package/src/JsonTree/index.tsx +11 -0
- package/src/JsonTree/lazy.tsx +22 -0
- package/src/JsonTree/types.ts +91 -0
- package/src/Kanban/context/KanbanContext.tsx +165 -0
- package/src/Kanban/context/index.ts +4 -0
- package/src/Kanban/index.ts +31 -0
- package/src/Kanban/lazy.tsx +23 -0
- package/src/Kanban/parts/Kanban.tsx +77 -0
- package/src/Kanban/parts/KanbanCard.tsx +76 -0
- package/src/Kanban/parts/KanbanColumn.tsx +79 -0
- package/src/Kanban/parts/KanbanDragOverlay.tsx +46 -0
- package/src/Kanban/parts/index.ts +6 -0
- package/src/Kanban/types.ts +45 -0
- package/src/Listbox/context/ListboxProvider.tsx +616 -0
- package/src/Listbox/context/index.ts +8 -0
- package/src/Listbox/index.ts +28 -0
- package/src/Listbox/lazy.tsx +31 -0
- package/src/Listbox/parts/ListboxGroup.tsx +71 -0
- package/src/Listbox/parts/ListboxItem.tsx +151 -0
- package/src/Listbox/parts/index.ts +12 -0
- package/src/Listbox/types.ts +98 -0
- package/src/Masonry/README.md +79 -0
- package/src/Masonry/context/MasonryProvider.tsx +1205 -0
- package/src/Masonry/context/index.ts +8 -0
- package/src/Masonry/index.ts +18 -0
- package/src/Masonry/lazy.tsx +20 -0
- package/src/Masonry/parts/index.ts +2 -0
- package/src/Masonry/types.ts +44 -0
- package/src/Timeline/context/TimelineProvider.tsx +474 -0
- package/src/Timeline/context/index.ts +16 -0
- package/src/Timeline/index.ts +29 -0
- package/src/Timeline/lazy.tsx +27 -0
- package/src/Timeline/parts/index.ts +2 -0
- package/src/Timeline/types.ts +25 -0
- package/src/Tree/FinderTree.tsx +42 -0
- package/src/Tree/README.md +542 -0
- package/src/Tree/TreeDndProvider.tsx +137 -0
- package/src/Tree/TreeRoot.tsx +290 -0
- package/src/Tree/components/TreeChevron.tsx +48 -0
- package/src/Tree/components/TreeContent.tsx +70 -0
- package/src/Tree/components/TreeDropIndicator.tsx +65 -0
- package/src/Tree/components/TreeEmpty.tsx +22 -0
- package/src/Tree/components/TreeEmptyArea.tsx +160 -0
- package/src/Tree/components/TreeError.tsx +24 -0
- package/src/Tree/components/TreeIcon.tsx +39 -0
- package/src/Tree/components/TreeIndentGuides.tsx +33 -0
- package/src/Tree/components/TreeLabel.tsx +39 -0
- package/src/Tree/components/TreeRenameInput.tsx +114 -0
- package/src/Tree/components/TreeRow.tsx +300 -0
- package/src/Tree/components/TreeSearchInput.tsx +50 -0
- package/src/Tree/components/TreeSkeleton.tsx +22 -0
- package/src/Tree/components/index.ts +28 -0
- package/src/Tree/context/TreeContext.tsx +409 -0
- package/src/Tree/context/TreeContextValue.ts +139 -0
- package/src/Tree/context/async-children/collect-ids.ts +27 -0
- package/src/Tree/context/async-children/index.ts +8 -0
- package/src/Tree/context/async-children/use-async-children.ts +157 -0
- package/src/Tree/context/clipboard/index.ts +4 -0
- package/src/Tree/context/clipboard/use-clipboard.ts +115 -0
- package/src/Tree/context/dnd/index.ts +8 -0
- package/src/Tree/context/dnd/use-dnd.ts +194 -0
- package/src/Tree/context/expansion/index.ts +4 -0
- package/src/Tree/context/expansion/use-expansion.ts +55 -0
- package/src/Tree/context/hooks.ts +177 -0
- package/src/Tree/context/index.ts +16 -0
- package/src/Tree/context/menu/builtin-actions.ts +358 -0
- package/src/Tree/context/menu/index.ts +11 -0
- package/src/Tree/context/menu/render.tsx +104 -0
- package/src/Tree/context/menu/use-resolved-menu.ts +141 -0
- package/src/Tree/context/persist/index.ts +4 -0
- package/src/Tree/context/persist/use-persist-sync.ts +74 -0
- package/src/Tree/context/rename/index.ts +4 -0
- package/src/Tree/context/rename/use-rename.ts +113 -0
- package/src/Tree/context/selection/index.ts +4 -0
- package/src/Tree/context/selection/use-selection.ts +146 -0
- package/src/Tree/context/state/index.ts +6 -0
- package/src/Tree/context/state/initial.ts +41 -0
- package/src/Tree/context/state/reducer.ts +76 -0
- package/src/Tree/context/state/types.ts +46 -0
- package/src/Tree/data/appearance.ts +237 -0
- package/src/Tree/data/childCache.ts +43 -0
- package/src/Tree/data/clipboard.ts +33 -0
- package/src/Tree/data/createDemoTree.ts +42 -0
- package/src/Tree/data/dnd.ts +123 -0
- package/src/Tree/data/finderShortcuts.ts +67 -0
- package/src/Tree/data/flatten.ts +66 -0
- package/src/Tree/data/index.ts +45 -0
- package/src/Tree/data/persist.ts +62 -0
- package/src/Tree/data/renameUtils.ts +51 -0
- package/src/Tree/data/selection.ts +161 -0
- package/src/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
- package/src/Tree/hooks/finder-hotkeys/index.ts +8 -0
- package/src/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
- package/src/Tree/hooks/index.ts +25 -0
- package/src/Tree/hooks/keyboard/activation.ts +27 -0
- package/src/Tree/hooks/keyboard/arrow-nav.ts +26 -0
- package/src/Tree/hooks/keyboard/expand-collapse.ts +54 -0
- package/src/Tree/hooks/keyboard/index.ts +10 -0
- package/src/Tree/hooks/keyboard/types.ts +39 -0
- package/src/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
- package/src/Tree/hooks/type-ahead/index.ts +5 -0
- package/src/Tree/hooks/type-ahead/match-prefix.ts +42 -0
- package/src/Tree/hooks/type-ahead/use-tree-type-ahead.ts +89 -0
- package/src/Tree/index.tsx +135 -0
- package/src/Tree/lazy.tsx +14 -0
- package/src/Tree/types/activation.ts +30 -0
- package/src/Tree/types/adapter.ts +70 -0
- package/src/Tree/types/index.ts +27 -0
- package/src/Tree/types/labels.ts +97 -0
- package/src/Tree/types/loader.ts +9 -0
- package/src/Tree/types/node.ts +38 -0
- package/src/Tree/types/root-props.ts +158 -0
- package/src/Tree/types/selection.ts +3 -0
- package/src/Tree/types/slots.ts +64 -0
- package/src/index.ts +21 -0
- package/src/stories/_fixtures/categories.ts +51 -0
- package/src/stories/_fixtures/finder-store.ts +196 -0
- package/src/stories/_fixtures/fs.ts +83 -0
- package/src/stories/_fixtures/index.ts +5 -0
- package/src/stories/_fixtures/org.ts +46 -0
- package/src/stories/_fixtures/status.ts +21 -0
- package/src/styles/index.css +10 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
DataGridProvider,
|
|
5
|
+
useDataGridContext,
|
|
6
|
+
} from './context/DataGridContext';
|
|
7
|
+
export type {
|
|
8
|
+
DataGridContextValue,
|
|
9
|
+
DataGridProviderProps,
|
|
10
|
+
} from './context/DataGridContext';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
DataGrid,
|
|
14
|
+
DataGridHeader,
|
|
15
|
+
DataGridRow,
|
|
16
|
+
DataGridCell,
|
|
17
|
+
DataGridPagination,
|
|
18
|
+
} from './parts';
|
|
19
|
+
|
|
20
|
+
export type {
|
|
21
|
+
DataGridProps,
|
|
22
|
+
DataGridColumn,
|
|
23
|
+
DataGridSortState,
|
|
24
|
+
DataGridFilterState,
|
|
25
|
+
DataGridPaginationState,
|
|
26
|
+
DataGridSelectionMode,
|
|
27
|
+
DataGridRowId,
|
|
28
|
+
SortDirection,
|
|
29
|
+
} from './types';
|
|
30
|
+
|
|
31
|
+
// The dynamic-import wrapper. Exported here because the subpath resolves to
|
|
32
|
+
// this barrel: in the retired ui-tools `lazy.tsx` was reachable from no declared export,
|
|
33
|
+
// so `LazyDataGrid` was dead code for every tool but `JsonTree`.
|
|
34
|
+
export { LazyDataGrid } from './lazy';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createLazyComponent, LoadingFallback } from '@djangocfg/widget-kit/lazy';
|
|
4
|
+
import type { DataGridProps } from './types';
|
|
5
|
+
|
|
6
|
+
export const LazyDataGrid = createLazyComponent<DataGridProps<unknown>>(
|
|
7
|
+
() => import('./parts/DataGrid').then((m) => ({ default: m.DataGrid })),
|
|
8
|
+
{
|
|
9
|
+
displayName: 'LazyDataGrid',
|
|
10
|
+
fallback: <LoadingFallback minHeight={300} text="Loading data grid…" />,
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// Re-export light surface synchronously
|
|
15
|
+
export * from './types';
|
|
16
|
+
export {
|
|
17
|
+
DataGridProvider,
|
|
18
|
+
useDataGridContext,
|
|
19
|
+
} from './context/DataGridContext';
|
|
20
|
+
export type {
|
|
21
|
+
DataGridContextValue,
|
|
22
|
+
DataGridProviderProps,
|
|
23
|
+
} from './context/DataGridContext';
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
import { ScrollArea } from '@djangocfg/ui-core/components';
|
|
6
|
+
|
|
7
|
+
import type { DataGridProps } from '../types';
|
|
8
|
+
import { DataGridProvider } from '../context/DataGridContext';
|
|
9
|
+
import { DataGridHeader } from './DataGridHeader';
|
|
10
|
+
import { DataGridRow } from './DataGridRow';
|
|
11
|
+
import { DataGridPagination } from './DataGridPagination';
|
|
12
|
+
|
|
13
|
+
export function DataGrid<T>(props: DataGridProps<T>) {
|
|
14
|
+
const {
|
|
15
|
+
data,
|
|
16
|
+
columns,
|
|
17
|
+
getRowId,
|
|
18
|
+
selectionMode,
|
|
19
|
+
initialSelectedIds,
|
|
20
|
+
onSelectionChange,
|
|
21
|
+
sort,
|
|
22
|
+
onSortChange,
|
|
23
|
+
initialSort,
|
|
24
|
+
filters,
|
|
25
|
+
onFilterChange,
|
|
26
|
+
initialFilters,
|
|
27
|
+
pagination,
|
|
28
|
+
onPaginationChange,
|
|
29
|
+
initialPagination,
|
|
30
|
+
pageSizeOptions,
|
|
31
|
+
loading,
|
|
32
|
+
emptyMessage,
|
|
33
|
+
getRowClassName,
|
|
34
|
+
onRowClick,
|
|
35
|
+
onRowDoubleClick,
|
|
36
|
+
stickyHeader = true,
|
|
37
|
+
className,
|
|
38
|
+
style,
|
|
39
|
+
} = props;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<DataGridProvider<T>
|
|
43
|
+
data={data}
|
|
44
|
+
columns={columns}
|
|
45
|
+
getRowId={getRowId}
|
|
46
|
+
selectionMode={selectionMode}
|
|
47
|
+
initialSelectedIds={initialSelectedIds}
|
|
48
|
+
onSelectionChange={onSelectionChange}
|
|
49
|
+
sort={sort}
|
|
50
|
+
onSortChange={onSortChange}
|
|
51
|
+
initialSort={initialSort}
|
|
52
|
+
filters={filters}
|
|
53
|
+
onFilterChange={onFilterChange}
|
|
54
|
+
initialFilters={initialFilters}
|
|
55
|
+
pagination={pagination}
|
|
56
|
+
onPaginationChange={onPaginationChange}
|
|
57
|
+
initialPagination={initialPagination}
|
|
58
|
+
pageSizeOptions={pageSizeOptions}
|
|
59
|
+
loading={loading}
|
|
60
|
+
emptyMessage={emptyMessage}
|
|
61
|
+
getRowClassName={getRowClassName}
|
|
62
|
+
onRowClick={onRowClick}
|
|
63
|
+
onRowDoubleClick={onRowDoubleClick}
|
|
64
|
+
stickyHeader={stickyHeader}
|
|
65
|
+
>
|
|
66
|
+
<DataGridShell className={className} style={style} />
|
|
67
|
+
</DataGridProvider>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
DataGrid.displayName = 'DataGrid';
|
|
71
|
+
|
|
72
|
+
function DataGridShell({ className, style }: { className?: string; style?: React.CSSProperties }) {
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
data-slot="data-grid"
|
|
76
|
+
className={cn('flex flex-col gap-2 rounded-sm border border-border bg-background', className)}
|
|
77
|
+
style={style}
|
|
78
|
+
>
|
|
79
|
+
<ScrollArea className="w-full" viewportClassName="max-h-[60vh]">
|
|
80
|
+
<table className="w-full caption-bottom text-sm">
|
|
81
|
+
<DataGridHeader />
|
|
82
|
+
<tbody className="[&_tr:last-child]:border-0">
|
|
83
|
+
<DataGridRows />
|
|
84
|
+
</tbody>
|
|
85
|
+
</table>
|
|
86
|
+
</ScrollArea>
|
|
87
|
+
<DataGridPagination />
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function DataGridRows() {
|
|
93
|
+
const { rows, getRowId, loading, emptyMessage } = useDataGridContext();
|
|
94
|
+
|
|
95
|
+
if (loading) {
|
|
96
|
+
return (
|
|
97
|
+
<tr>
|
|
98
|
+
<td colSpan={999} className="p-8 text-center text-muted-foreground">
|
|
99
|
+
Loading…
|
|
100
|
+
</td>
|
|
101
|
+
</tr>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (rows.length === 0) {
|
|
106
|
+
return (
|
|
107
|
+
<tr>
|
|
108
|
+
<td colSpan={999} className="p-8 text-center text-muted-foreground">
|
|
109
|
+
{emptyMessage}
|
|
110
|
+
</td>
|
|
111
|
+
</tr>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
{rows.map((row, index) => (
|
|
118
|
+
<DataGridRow key={String(getRowId(row))} row={row} index={index} />
|
|
119
|
+
))}
|
|
120
|
+
</>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Ambient import for JSX usage
|
|
125
|
+
import { useDataGridContext } from '../context/DataGridContext';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
import type { DataGridColumn } from '../types';
|
|
7
|
+
|
|
8
|
+
interface DataGridCellProps<T> {
|
|
9
|
+
column: DataGridColumn<T>;
|
|
10
|
+
row: T;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DataGridCell<T>({ column, row }: DataGridCellProps<T>) {
|
|
14
|
+
const value = (row as Record<string, unknown>)[column.key];
|
|
15
|
+
|
|
16
|
+
const content = column.cell ? column.cell(row) : value != null ? String(value) : null;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<td
|
|
20
|
+
className={cn(
|
|
21
|
+
'p-2 align-middle',
|
|
22
|
+
column.align === 'center' && 'text-center',
|
|
23
|
+
column.align === 'right' && 'text-right'
|
|
24
|
+
)}
|
|
25
|
+
>
|
|
26
|
+
{content}
|
|
27
|
+
</td>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
DataGridCell.displayName = 'DataGridCell';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
import { ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
import { useDataGridContext } from '../context/DataGridContext';
|
|
8
|
+
import { DataGridCell } from './DataGridCell';
|
|
9
|
+
|
|
10
|
+
export function DataGridHeader() {
|
|
11
|
+
const { columns, selectionMode, selectAllVisible, isSelected, rows, getRowId, stickyHeader } =
|
|
12
|
+
useDataGridContext();
|
|
13
|
+
|
|
14
|
+
const allVisibleSelected = rows.length > 0 && rows.every((r) => isSelected(getRowId(r)));
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<thead
|
|
18
|
+
className={cn(
|
|
19
|
+
'[&_tr]:border-b',
|
|
20
|
+
stickyHeader && 'sticky top-0 z-10 bg-background'
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
<tr className="border-b transition-colors hover:bg-muted/50">
|
|
24
|
+
{selectionMode === 'multiple' && (
|
|
25
|
+
<th className="h-10 px-2 text-left align-middle font-medium text-muted-foreground w-10">
|
|
26
|
+
<input
|
|
27
|
+
type="checkbox"
|
|
28
|
+
checked={allVisibleSelected}
|
|
29
|
+
onChange={selectAllVisible}
|
|
30
|
+
className="h-4 w-4 rounded border-border accent-primary"
|
|
31
|
+
aria-label="Select all visible rows"
|
|
32
|
+
/>
|
|
33
|
+
</th>
|
|
34
|
+
)}
|
|
35
|
+
{selectionMode === 'single' && <th className="w-10" />}
|
|
36
|
+
{columns.map((col) => (
|
|
37
|
+
<DataGridHeaderCell key={col.key} column={col} />
|
|
38
|
+
))}
|
|
39
|
+
</tr>
|
|
40
|
+
<DataGridFilterRow />
|
|
41
|
+
</thead>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
DataGridHeader.displayName = 'DataGridHeader';
|
|
45
|
+
|
|
46
|
+
function DataGridHeaderCell({ column }: { column: { key: string; header: React.ReactNode; sortable?: boolean; align?: 'left' | 'center' | 'right'; width?: string | number; minWidth?: string | number; maxWidth?: string | number } }) {
|
|
47
|
+
const { sort, setSort } = useDataGridContext();
|
|
48
|
+
const isSorted = sort.key === column.key;
|
|
49
|
+
|
|
50
|
+
const style: React.CSSProperties = {};
|
|
51
|
+
if (column.width) style.width = column.width;
|
|
52
|
+
if (column.minWidth) style.minWidth = column.minWidth;
|
|
53
|
+
if (column.maxWidth) style.maxWidth = column.maxWidth;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<th
|
|
57
|
+
className={cn(
|
|
58
|
+
'h-10 px-2 text-left align-middle font-medium text-muted-foreground',
|
|
59
|
+
column.align === 'center' && 'text-center',
|
|
60
|
+
column.align === 'right' && 'text-right',
|
|
61
|
+
column.sortable && 'cursor-pointer select-none'
|
|
62
|
+
)}
|
|
63
|
+
style={style}
|
|
64
|
+
onClick={() => column.sortable && setSort(column.key)}
|
|
65
|
+
data-sort={isSorted ? sort.direction : undefined}
|
|
66
|
+
>
|
|
67
|
+
<span className="inline-flex items-center gap-1">
|
|
68
|
+
{column.header}
|
|
69
|
+
{column.sortable && (
|
|
70
|
+
<span className="inline-flex text-muted-foreground/60">
|
|
71
|
+
{isSorted && sort.direction === 'asc' && <ArrowUp className="h-3.5 w-3.5" />}
|
|
72
|
+
{isSorted && sort.direction === 'desc' && <ArrowDown className="h-3.5 w-3.5" />}
|
|
73
|
+
{!isSorted && <ArrowUpDown className="h-3.5 w-3.5" />}
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
76
|
+
</span>
|
|
77
|
+
</th>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function DataGridFilterRow() {
|
|
82
|
+
const { columns, filters, setFilter, selectionMode } = useDataGridContext();
|
|
83
|
+
const hasFilterable = columns.some((c) => c.filterable);
|
|
84
|
+
|
|
85
|
+
if (!hasFilterable) return null;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<tr className="border-b transition-colors">
|
|
89
|
+
{selectionMode !== 'none' && <td className="p-2" />}
|
|
90
|
+
{columns.map((col) => (
|
|
91
|
+
<td key={col.key} className="p-2">
|
|
92
|
+
{col.filterable ? (
|
|
93
|
+
<input
|
|
94
|
+
type="text"
|
|
95
|
+
value={filters[col.key] ?? ''}
|
|
96
|
+
onChange={(e) => setFilter(col.key, e.target.value)}
|
|
97
|
+
placeholder="Filter…"
|
|
98
|
+
className={cn(
|
|
99
|
+
'w-full rounded-sm border border-input bg-background px-2 py-1 text-xs',
|
|
100
|
+
'placeholder:text-muted-foreground',
|
|
101
|
+
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'
|
|
102
|
+
)}
|
|
103
|
+
/>
|
|
104
|
+
) : (
|
|
105
|
+
<span />
|
|
106
|
+
)}
|
|
107
|
+
</td>
|
|
108
|
+
))}
|
|
109
|
+
</tr>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
import { useDataGridContext } from '../context/DataGridContext';
|
|
8
|
+
|
|
9
|
+
export function DataGridPagination() {
|
|
10
|
+
const { pagination, setPage, setPageSize, totalRows, pageCount, pageSizeOptions } =
|
|
11
|
+
useDataGridContext();
|
|
12
|
+
|
|
13
|
+
const start = totalRows === 0 ? 0 : (pagination.page - 1) * pagination.pageSize + 1;
|
|
14
|
+
const end = Math.min(pagination.page * pagination.pageSize, totalRows);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
data-slot="data-grid-pagination"
|
|
19
|
+
className="flex flex-col items-center justify-between gap-2 px-4 py-2 sm:flex-row"
|
|
20
|
+
>
|
|
21
|
+
<span className="text-sm text-muted-foreground">
|
|
22
|
+
{start}-{end} of {totalRows}
|
|
23
|
+
</span>
|
|
24
|
+
|
|
25
|
+
<div className="flex items-center gap-2">
|
|
26
|
+
<select
|
|
27
|
+
value={pagination.pageSize}
|
|
28
|
+
onChange={(e) => setPageSize(Number(e.target.value))}
|
|
29
|
+
className={cn(
|
|
30
|
+
'h-8 rounded-sm border border-input bg-background px-2 text-sm',
|
|
31
|
+
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'
|
|
32
|
+
)}
|
|
33
|
+
aria-label="Rows per page"
|
|
34
|
+
>
|
|
35
|
+
{pageSizeOptions.map((size) => (
|
|
36
|
+
<option key={size} value={size}>
|
|
37
|
+
{size} / page
|
|
38
|
+
</option>
|
|
39
|
+
))}
|
|
40
|
+
</select>
|
|
41
|
+
|
|
42
|
+
<div className="flex items-center gap-1">
|
|
43
|
+
<PaginationButton
|
|
44
|
+
onClick={() => setPage(1)}
|
|
45
|
+
disabled={pagination.page <= 1}
|
|
46
|
+
aria-label="First page"
|
|
47
|
+
>
|
|
48
|
+
<ChevronsLeft className="h-4 w-4" />
|
|
49
|
+
</PaginationButton>
|
|
50
|
+
<PaginationButton
|
|
51
|
+
onClick={() => setPage(pagination.page - 1)}
|
|
52
|
+
disabled={pagination.page <= 1}
|
|
53
|
+
aria-label="Previous page"
|
|
54
|
+
>
|
|
55
|
+
<ChevronLeft className="h-4 w-4" />
|
|
56
|
+
</PaginationButton>
|
|
57
|
+
|
|
58
|
+
<span className="px-2 text-sm tabular-nums">
|
|
59
|
+
{pagination.page} / {pageCount}
|
|
60
|
+
</span>
|
|
61
|
+
|
|
62
|
+
<PaginationButton
|
|
63
|
+
onClick={() => setPage(pagination.page + 1)}
|
|
64
|
+
disabled={pagination.page >= pageCount}
|
|
65
|
+
aria-label="Next page"
|
|
66
|
+
>
|
|
67
|
+
<ChevronRight className="h-4 w-4" />
|
|
68
|
+
</PaginationButton>
|
|
69
|
+
<PaginationButton
|
|
70
|
+
onClick={() => setPage(pageCount)}
|
|
71
|
+
disabled={pagination.page >= pageCount}
|
|
72
|
+
aria-label="Last page"
|
|
73
|
+
>
|
|
74
|
+
<ChevronsRight className="h-4 w-4" />
|
|
75
|
+
</PaginationButton>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
DataGridPagination.displayName = 'DataGridPagination';
|
|
82
|
+
|
|
83
|
+
function PaginationButton({
|
|
84
|
+
children,
|
|
85
|
+
onClick,
|
|
86
|
+
disabled,
|
|
87
|
+
...props
|
|
88
|
+
}: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
89
|
+
return (
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
onClick={onClick}
|
|
93
|
+
disabled={disabled}
|
|
94
|
+
className={cn(
|
|
95
|
+
'inline-flex h-8 w-8 items-center justify-center rounded-sm border border-input bg-background text-sm',
|
|
96
|
+
'hover:bg-accent hover:text-accent-foreground',
|
|
97
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
98
|
+
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'
|
|
99
|
+
)}
|
|
100
|
+
{...props}
|
|
101
|
+
>
|
|
102
|
+
{children}
|
|
103
|
+
</button>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
import { useDataGridContext } from '../context/DataGridContext';
|
|
7
|
+
import { DataGridCell } from './DataGridCell';
|
|
8
|
+
|
|
9
|
+
interface DataGridRowProps<T> {
|
|
10
|
+
row: T;
|
|
11
|
+
index: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DataGridRow<T>({ row, index }: DataGridRowProps<T>) {
|
|
15
|
+
const {
|
|
16
|
+
columns,
|
|
17
|
+
getRowId,
|
|
18
|
+
selectionMode,
|
|
19
|
+
select,
|
|
20
|
+
isSelected,
|
|
21
|
+
getRowClassName,
|
|
22
|
+
onRowClick,
|
|
23
|
+
onRowDoubleClick,
|
|
24
|
+
} = useDataGridContext<T>();
|
|
25
|
+
|
|
26
|
+
const id = getRowId(row);
|
|
27
|
+
const selected = isSelected(id);
|
|
28
|
+
|
|
29
|
+
const handleClick = React.useCallback(() => {
|
|
30
|
+
if (selectionMode !== 'none') {
|
|
31
|
+
select(id);
|
|
32
|
+
}
|
|
33
|
+
onRowClick?.(row);
|
|
34
|
+
}, [id, selectionMode, select, onRowClick, row]);
|
|
35
|
+
|
|
36
|
+
const handleDoubleClick = React.useCallback(() => {
|
|
37
|
+
onRowDoubleClick?.(row);
|
|
38
|
+
}, [onRowDoubleClick, row]);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<tr
|
|
42
|
+
data-slot="data-grid-row"
|
|
43
|
+
data-id={String(id)}
|
|
44
|
+
data-selected={selected ? '' : undefined}
|
|
45
|
+
data-state={selected ? 'selected' : undefined}
|
|
46
|
+
className={cn(
|
|
47
|
+
'border-b transition-colors hover:bg-muted/50',
|
|
48
|
+
selected && 'bg-muted',
|
|
49
|
+
getRowClassName?.(row, index)
|
|
50
|
+
)}
|
|
51
|
+
onClick={handleClick}
|
|
52
|
+
onDoubleClick={handleDoubleClick}
|
|
53
|
+
>
|
|
54
|
+
{selectionMode === 'multiple' && (
|
|
55
|
+
<td className="p-2 align-middle">
|
|
56
|
+
<input
|
|
57
|
+
type="checkbox"
|
|
58
|
+
checked={selected}
|
|
59
|
+
onChange={() => select(id)}
|
|
60
|
+
className="h-4 w-4 rounded border-border accent-primary"
|
|
61
|
+
aria-label={`Select row ${String(id)}`}
|
|
62
|
+
onClick={(e) => e.stopPropagation()}
|
|
63
|
+
/>
|
|
64
|
+
</td>
|
|
65
|
+
)}
|
|
66
|
+
{selectionMode === 'single' && (
|
|
67
|
+
<td className="p-2 align-middle">
|
|
68
|
+
<input
|
|
69
|
+
type="radio"
|
|
70
|
+
checked={selected}
|
|
71
|
+
onChange={() => select(id)}
|
|
72
|
+
className="h-4 w-4 border-border accent-primary"
|
|
73
|
+
aria-label={`Select row ${String(id)}`}
|
|
74
|
+
onClick={(e) => e.stopPropagation()}
|
|
75
|
+
/>
|
|
76
|
+
</td>
|
|
77
|
+
)}
|
|
78
|
+
{columns.map((col) => (
|
|
79
|
+
<DataGridCell key={col.key} column={col} row={row} />
|
|
80
|
+
))}
|
|
81
|
+
</tr>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
DataGridRow.displayName = 'DataGridRow';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
export type DataGridRowId = string | number;
|
|
6
|
+
|
|
7
|
+
export type SortDirection = 'asc' | 'desc' | null;
|
|
8
|
+
|
|
9
|
+
export interface DataGridColumn<T = unknown> {
|
|
10
|
+
key: string;
|
|
11
|
+
header: ReactNode;
|
|
12
|
+
width?: string | number;
|
|
13
|
+
minWidth?: string | number;
|
|
14
|
+
maxWidth?: string | number;
|
|
15
|
+
sortable?: boolean;
|
|
16
|
+
filterable?: boolean;
|
|
17
|
+
align?: 'left' | 'center' | 'right';
|
|
18
|
+
/** Custom cell renderer. Receives the row data. */
|
|
19
|
+
cell?: (row: T) => ReactNode;
|
|
20
|
+
/** Custom filter predicate for this column. */
|
|
21
|
+
filterFn?: (row: T, filterValue: string) => boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DataGridSortState {
|
|
25
|
+
key: string | null;
|
|
26
|
+
direction: SortDirection;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DataGridFilterState {
|
|
30
|
+
[columnKey: string]: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DataGridPaginationState {
|
|
34
|
+
page: number;
|
|
35
|
+
pageSize: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type DataGridSelectionMode = 'none' | 'single' | 'multiple';
|
|
39
|
+
|
|
40
|
+
export interface DataGridProps<T = unknown> {
|
|
41
|
+
/** Row data */
|
|
42
|
+
data: T[];
|
|
43
|
+
/** Column definitions */
|
|
44
|
+
columns: DataGridColumn<T>[];
|
|
45
|
+
/** Unique row id getter */
|
|
46
|
+
getRowId: (row: T) => DataGridRowId;
|
|
47
|
+
|
|
48
|
+
/** Selection behaviour. Default: 'none' */
|
|
49
|
+
selectionMode?: DataGridSelectionMode;
|
|
50
|
+
/** Initially selected row ids */
|
|
51
|
+
initialSelectedIds?: DataGridRowId[];
|
|
52
|
+
/** Triggered when selection changes */
|
|
53
|
+
onSelectionChange?: (selectedIds: DataGridRowId[]) => void;
|
|
54
|
+
|
|
55
|
+
/** Controlled sort state */
|
|
56
|
+
sort?: DataGridSortState;
|
|
57
|
+
/** Called when sort changes */
|
|
58
|
+
onSortChange?: (sort: DataGridSortState) => void;
|
|
59
|
+
/** Default sort */
|
|
60
|
+
initialSort?: DataGridSortState;
|
|
61
|
+
|
|
62
|
+
/** Controlled filter state */
|
|
63
|
+
filters?: DataGridFilterState;
|
|
64
|
+
/** Called when filters change */
|
|
65
|
+
onFilterChange?: (filters: DataGridFilterState) => void;
|
|
66
|
+
/** Default filters */
|
|
67
|
+
initialFilters?: DataGridFilterState;
|
|
68
|
+
|
|
69
|
+
/** Controlled pagination */
|
|
70
|
+
pagination?: DataGridPaginationState;
|
|
71
|
+
/** Called when pagination changes */
|
|
72
|
+
onPaginationChange?: (pagination: DataGridPaginationState) => void;
|
|
73
|
+
/** Default pagination */
|
|
74
|
+
initialPagination?: DataGridPaginationState;
|
|
75
|
+
/** Available page sizes. Default: [10, 25, 50, 100] */
|
|
76
|
+
pageSizeOptions?: number[];
|
|
77
|
+
|
|
78
|
+
/** Enable column resizing. Default: false */
|
|
79
|
+
resizable?: boolean;
|
|
80
|
+
/** Sticky header. Default: true */
|
|
81
|
+
stickyHeader?: boolean;
|
|
82
|
+
/** Loading state */
|
|
83
|
+
loading?: boolean;
|
|
84
|
+
/** Empty state message */
|
|
85
|
+
emptyMessage?: ReactNode;
|
|
86
|
+
/** Row className getter */
|
|
87
|
+
getRowClassName?: (row: T, index: number) => string;
|
|
88
|
+
/** Called when a row is clicked */
|
|
89
|
+
onRowClick?: (row: T) => void;
|
|
90
|
+
/** Called when a row is double-clicked */
|
|
91
|
+
onRowDoubleClick?: (row: T) => void;
|
|
92
|
+
|
|
93
|
+
className?: string;
|
|
94
|
+
style?: React.CSSProperties;
|
|
95
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# DataTable
|
|
2
|
+
|
|
3
|
+
Lighter sibling of `DataGrid`. Same sort / filter / pagination / selection model, but no column resizing, no sticky header, no `width`/`minWidth`/`maxWidth` per column — use when you want a plain semantic `<table>` for read-heavy lists. For resizable / sticky-header grids reach for `DataGrid` instead.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { DataTable } from '@djangocfg/ui-tools/data-table';
|
|
7
|
+
|
|
8
|
+
<DataTable
|
|
9
|
+
data={invoices}
|
|
10
|
+
columns={[
|
|
11
|
+
{ key: 'number', header: '#', sortable: true },
|
|
12
|
+
{ key: 'client', header: 'Client', filterable: true },
|
|
13
|
+
{ key: 'total', header: 'Total', align: 'right' },
|
|
14
|
+
]}
|
|
15
|
+
getRowId={(inv) => inv.id}
|
|
16
|
+
onRowClick={openInvoice}
|
|
17
|
+
/>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Prop | Type | Default | Description |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `data` | `T[]` | — | Row data. |
|
|
25
|
+
| `columns` | `DataTableColumn<T>[]` | — | Column defs (`key`, `header`, `cell?`, `sortable?`, `filterable?`, `align?`, `filterFn?`). |
|
|
26
|
+
| `getRowId` | `(row: T) => string \| number` | — | Stable row id. |
|
|
27
|
+
| `selectionMode` | `'none' \| 'single' \| 'multiple'` | `'none'` | Selection behaviour. |
|
|
28
|
+
| `initialSelectedIds` / `onSelectionChange` | `RowId[]` / `(ids) => void` | — | Uncontrolled selection. |
|
|
29
|
+
| `sort` / `onSortChange` / `initialSort` | `DataTableSortState` | — | Controlled or uncontrolled sort. |
|
|
30
|
+
| `filters` / `onFilterChange` / `initialFilters` | `DataTableFilterState` | — | Per-column text filters. |
|
|
31
|
+
| `pagination` / `onPaginationChange` / `initialPagination` | `DataTablePaginationState` | — | Controlled or uncontrolled pagination. |
|
|
32
|
+
| `pageSizeOptions` | `number[]` | `[10, 25, 50, 100]` | Page-size choices. |
|
|
33
|
+
| `loading` | `boolean` | `false` | Loading state. |
|
|
34
|
+
| `emptyMessage` | `ReactNode` | — | Rendered when `data` is empty. |
|
|
35
|
+
| `getRowClassName` | `(row, i) => string` | — | Per-row class hook. |
|
|
36
|
+
| `onRowClick` / `onRowDoubleClick` | `(row) => void` | — | Row event handlers. |
|
|
37
|
+
|
|
38
|
+
Composable parts (`DataTableProvider`, `DataTableHeader`, `DataTableRow`, `DataTablePagination`) are exported for custom compositions.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
Adapted from jalcoui (MIT).
|