@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.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/package.json +122 -0
  4. package/src/DataGrid/README.md +48 -0
  5. package/src/DataGrid/context/DataGridContext.tsx +442 -0
  6. package/src/DataGrid/context/index.ts +4 -0
  7. package/src/DataGrid/index.ts +34 -0
  8. package/src/DataGrid/lazy.tsx +23 -0
  9. package/src/DataGrid/parts/DataGrid.tsx +125 -0
  10. package/src/DataGrid/parts/DataGridCell.tsx +30 -0
  11. package/src/DataGrid/parts/DataGridHeader.tsx +111 -0
  12. package/src/DataGrid/parts/DataGridPagination.tsx +105 -0
  13. package/src/DataGrid/parts/DataGridRow.tsx +84 -0
  14. package/src/DataGrid/parts/index.ts +7 -0
  15. package/src/DataGrid/types.ts +95 -0
  16. package/src/DataTable/README.md +42 -0
  17. package/src/DataTable/context/DataTableContext.tsx +434 -0
  18. package/src/DataTable/context/index.ts +4 -0
  19. package/src/DataTable/index.ts +33 -0
  20. package/src/DataTable/lazy.tsx +23 -0
  21. package/src/DataTable/parts/DataTable.tsx +142 -0
  22. package/src/DataTable/parts/DataTableHeader.tsx +103 -0
  23. package/src/DataTable/parts/DataTablePagination.tsx +115 -0
  24. package/src/DataTable/parts/DataTableRow.tsx +96 -0
  25. package/src/DataTable/parts/index.ts +6 -0
  26. package/src/DataTable/types.ts +88 -0
  27. package/src/FileIcon/FileIcon.tsx +87 -0
  28. package/src/FileIcon/get-file-icon.ts +73 -0
  29. package/src/FileIcon/icons/icon-data.ts +399 -0
  30. package/src/FileIcon/index.ts +19 -0
  31. package/src/FileIcon/loader.ts +29 -0
  32. package/src/FileIcon/specialFolders.ts +110 -0
  33. package/src/FileIcon/treeAdapter.tsx +49 -0
  34. package/src/JsonTree/JsonViewer.tsx +720 -0
  35. package/src/JsonTree/README.md +142 -0
  36. package/src/JsonTree/index.tsx +11 -0
  37. package/src/JsonTree/lazy.tsx +22 -0
  38. package/src/JsonTree/types.ts +91 -0
  39. package/src/Kanban/context/KanbanContext.tsx +165 -0
  40. package/src/Kanban/context/index.ts +4 -0
  41. package/src/Kanban/index.ts +31 -0
  42. package/src/Kanban/lazy.tsx +23 -0
  43. package/src/Kanban/parts/Kanban.tsx +77 -0
  44. package/src/Kanban/parts/KanbanCard.tsx +76 -0
  45. package/src/Kanban/parts/KanbanColumn.tsx +79 -0
  46. package/src/Kanban/parts/KanbanDragOverlay.tsx +46 -0
  47. package/src/Kanban/parts/index.ts +6 -0
  48. package/src/Kanban/types.ts +45 -0
  49. package/src/Listbox/context/ListboxProvider.tsx +616 -0
  50. package/src/Listbox/context/index.ts +8 -0
  51. package/src/Listbox/index.ts +28 -0
  52. package/src/Listbox/lazy.tsx +31 -0
  53. package/src/Listbox/parts/ListboxGroup.tsx +71 -0
  54. package/src/Listbox/parts/ListboxItem.tsx +151 -0
  55. package/src/Listbox/parts/index.ts +12 -0
  56. package/src/Listbox/types.ts +98 -0
  57. package/src/Masonry/README.md +79 -0
  58. package/src/Masonry/context/MasonryProvider.tsx +1205 -0
  59. package/src/Masonry/context/index.ts +8 -0
  60. package/src/Masonry/index.ts +18 -0
  61. package/src/Masonry/lazy.tsx +20 -0
  62. package/src/Masonry/parts/index.ts +2 -0
  63. package/src/Masonry/types.ts +44 -0
  64. package/src/Timeline/context/TimelineProvider.tsx +474 -0
  65. package/src/Timeline/context/index.ts +16 -0
  66. package/src/Timeline/index.ts +29 -0
  67. package/src/Timeline/lazy.tsx +27 -0
  68. package/src/Timeline/parts/index.ts +2 -0
  69. package/src/Timeline/types.ts +25 -0
  70. package/src/Tree/FinderTree.tsx +42 -0
  71. package/src/Tree/README.md +542 -0
  72. package/src/Tree/TreeDndProvider.tsx +137 -0
  73. package/src/Tree/TreeRoot.tsx +290 -0
  74. package/src/Tree/components/TreeChevron.tsx +48 -0
  75. package/src/Tree/components/TreeContent.tsx +70 -0
  76. package/src/Tree/components/TreeDropIndicator.tsx +65 -0
  77. package/src/Tree/components/TreeEmpty.tsx +22 -0
  78. package/src/Tree/components/TreeEmptyArea.tsx +160 -0
  79. package/src/Tree/components/TreeError.tsx +24 -0
  80. package/src/Tree/components/TreeIcon.tsx +39 -0
  81. package/src/Tree/components/TreeIndentGuides.tsx +33 -0
  82. package/src/Tree/components/TreeLabel.tsx +39 -0
  83. package/src/Tree/components/TreeRenameInput.tsx +114 -0
  84. package/src/Tree/components/TreeRow.tsx +300 -0
  85. package/src/Tree/components/TreeSearchInput.tsx +50 -0
  86. package/src/Tree/components/TreeSkeleton.tsx +22 -0
  87. package/src/Tree/components/index.ts +28 -0
  88. package/src/Tree/context/TreeContext.tsx +409 -0
  89. package/src/Tree/context/TreeContextValue.ts +139 -0
  90. package/src/Tree/context/async-children/collect-ids.ts +27 -0
  91. package/src/Tree/context/async-children/index.ts +8 -0
  92. package/src/Tree/context/async-children/use-async-children.ts +157 -0
  93. package/src/Tree/context/clipboard/index.ts +4 -0
  94. package/src/Tree/context/clipboard/use-clipboard.ts +115 -0
  95. package/src/Tree/context/dnd/index.ts +8 -0
  96. package/src/Tree/context/dnd/use-dnd.ts +194 -0
  97. package/src/Tree/context/expansion/index.ts +4 -0
  98. package/src/Tree/context/expansion/use-expansion.ts +55 -0
  99. package/src/Tree/context/hooks.ts +177 -0
  100. package/src/Tree/context/index.ts +16 -0
  101. package/src/Tree/context/menu/builtin-actions.ts +358 -0
  102. package/src/Tree/context/menu/index.ts +11 -0
  103. package/src/Tree/context/menu/render.tsx +104 -0
  104. package/src/Tree/context/menu/use-resolved-menu.ts +141 -0
  105. package/src/Tree/context/persist/index.ts +4 -0
  106. package/src/Tree/context/persist/use-persist-sync.ts +74 -0
  107. package/src/Tree/context/rename/index.ts +4 -0
  108. package/src/Tree/context/rename/use-rename.ts +113 -0
  109. package/src/Tree/context/selection/index.ts +4 -0
  110. package/src/Tree/context/selection/use-selection.ts +146 -0
  111. package/src/Tree/context/state/index.ts +6 -0
  112. package/src/Tree/context/state/initial.ts +41 -0
  113. package/src/Tree/context/state/reducer.ts +76 -0
  114. package/src/Tree/context/state/types.ts +46 -0
  115. package/src/Tree/data/appearance.ts +237 -0
  116. package/src/Tree/data/childCache.ts +43 -0
  117. package/src/Tree/data/clipboard.ts +33 -0
  118. package/src/Tree/data/createDemoTree.ts +42 -0
  119. package/src/Tree/data/dnd.ts +123 -0
  120. package/src/Tree/data/finderShortcuts.ts +67 -0
  121. package/src/Tree/data/flatten.ts +66 -0
  122. package/src/Tree/data/index.ts +45 -0
  123. package/src/Tree/data/persist.ts +62 -0
  124. package/src/Tree/data/renameUtils.ts +51 -0
  125. package/src/Tree/data/selection.ts +161 -0
  126. package/src/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
  127. package/src/Tree/hooks/finder-hotkeys/index.ts +8 -0
  128. package/src/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
  129. package/src/Tree/hooks/index.ts +25 -0
  130. package/src/Tree/hooks/keyboard/activation.ts +27 -0
  131. package/src/Tree/hooks/keyboard/arrow-nav.ts +26 -0
  132. package/src/Tree/hooks/keyboard/expand-collapse.ts +54 -0
  133. package/src/Tree/hooks/keyboard/index.ts +10 -0
  134. package/src/Tree/hooks/keyboard/types.ts +39 -0
  135. package/src/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
  136. package/src/Tree/hooks/type-ahead/index.ts +5 -0
  137. package/src/Tree/hooks/type-ahead/match-prefix.ts +42 -0
  138. package/src/Tree/hooks/type-ahead/use-tree-type-ahead.ts +89 -0
  139. package/src/Tree/index.tsx +135 -0
  140. package/src/Tree/lazy.tsx +14 -0
  141. package/src/Tree/types/activation.ts +30 -0
  142. package/src/Tree/types/adapter.ts +70 -0
  143. package/src/Tree/types/index.ts +27 -0
  144. package/src/Tree/types/labels.ts +97 -0
  145. package/src/Tree/types/loader.ts +9 -0
  146. package/src/Tree/types/node.ts +38 -0
  147. package/src/Tree/types/root-props.ts +158 -0
  148. package/src/Tree/types/selection.ts +3 -0
  149. package/src/Tree/types/slots.ts +64 -0
  150. package/src/index.ts +21 -0
  151. package/src/stories/_fixtures/categories.ts +51 -0
  152. package/src/stories/_fixtures/finder-store.ts +196 -0
  153. package/src/stories/_fixtures/fs.ts +83 -0
  154. package/src/stories/_fixtures/index.ts +5 -0
  155. package/src/stories/_fixtures/org.ts +46 -0
  156. package/src/stories/_fixtures/status.ts +21 -0
  157. 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,7 @@
1
+ 'use client';
2
+
3
+ export { DataGrid } from './DataGrid';
4
+ export { DataGridHeader } from './DataGridHeader';
5
+ export { DataGridRow } from './DataGridRow';
6
+ export { DataGridCell } from './DataGridCell';
7
+ export { DataGridPagination } from './DataGridPagination';
@@ -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).