@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,434 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import {
5
+ createContext,
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useReducer,
10
+ useRef,
11
+ } from 'react';
12
+
13
+ import type {
14
+ DataTableColumn,
15
+ DataTableFilterState,
16
+ DataTablePaginationState,
17
+ DataTableProps,
18
+ DataTableRowId,
19
+ DataTableSelectionMode,
20
+ DataTableSortState,
21
+ DataTableSortDirection,
22
+ } from '../types';
23
+
24
+ // =====================================================================
25
+ // Reducer
26
+ // =====================================================================
27
+
28
+ interface State<T> {
29
+ sort: DataTableSortState;
30
+ filters: DataTableFilterState;
31
+ pagination: DataTablePaginationState;
32
+ selected: Set<DataTableRowId>;
33
+ }
34
+
35
+ type Action<T> =
36
+ | { type: 'set-sort'; sort: DataTableSortState }
37
+ | { type: 'set-filter'; key: string; value: string }
38
+ | { type: 'clear-filters' }
39
+ | { type: 'set-page'; page: number }
40
+ | { type: 'set-page-size'; pageSize: number }
41
+ | { type: 'select'; id: DataTableRowId; mode: DataTableSelectionMode }
42
+ | { type: 'select-many'; ids: DataTableRowId[] }
43
+ | { type: 'clear-selection' }
44
+ | { type: 'select-all'; ids: DataTableRowId[] };
45
+
46
+ const reducer = <T,>(state: State<T>, action: Action<T>): State<T> => {
47
+ switch (action.type) {
48
+ case 'set-sort':
49
+ return { ...state, sort: action.sort };
50
+ case 'set-filter': {
51
+ const next = { ...state.filters, [action.key]: action.value };
52
+ if (action.value === '') delete next[action.key];
53
+ return { ...state, filters: next, pagination: { ...state.pagination, page: 1 } };
54
+ }
55
+ case 'clear-filters':
56
+ return { ...state, filters: {}, pagination: { ...state.pagination, page: 1 } };
57
+ case 'set-page':
58
+ return { ...state, pagination: { ...state.pagination, page: action.page } };
59
+ case 'set-page-size':
60
+ return {
61
+ ...state,
62
+ pagination: { page: 1, pageSize: action.pageSize },
63
+ };
64
+ case 'select': {
65
+ if (action.mode === 'none') return state;
66
+ if (action.mode === 'single') {
67
+ return { ...state, selected: new Set([action.id]) };
68
+ }
69
+ const next = new Set(state.selected);
70
+ if (next.has(action.id)) next.delete(action.id);
71
+ else next.add(action.id);
72
+ return { ...state, selected: next };
73
+ }
74
+ case 'select-many':
75
+ return { ...state, selected: new Set(action.ids) };
76
+ case 'clear-selection':
77
+ return { ...state, selected: new Set() };
78
+ case 'select-all':
79
+ return { ...state, selected: new Set(action.ids) };
80
+ default:
81
+ return state;
82
+ }
83
+ };
84
+
85
+ // =====================================================================
86
+ // Context value
87
+ // =====================================================================
88
+
89
+ export interface DataTableContextValue<T = unknown> {
90
+ // State
91
+ sort: DataTableSortState;
92
+ filters: DataTableFilterState;
93
+ pagination: DataTablePaginationState;
94
+ selected: ReadonlySet<DataTableRowId>;
95
+
96
+ // Processed data
97
+ rows: T[];
98
+ totalRows: number;
99
+ pageCount: number;
100
+
101
+ // Actions
102
+ setSort: (key: string) => void;
103
+ setFilter: (key: string, value: string) => void;
104
+ clearFilters: () => void;
105
+ setPage: (page: number) => void;
106
+ setPageSize: (pageSize: number) => void;
107
+ select: (id: DataTableRowId) => void;
108
+ selectMany: (ids: DataTableRowId[]) => void;
109
+ clearSelection: () => void;
110
+ selectAllVisible: () => void;
111
+ isSelected: (id: DataTableRowId) => boolean;
112
+
113
+ // Config
114
+ columns: DataTableColumn<T>[];
115
+ getRowId: (row: T) => DataTableRowId;
116
+ selectionMode: DataTableSelectionMode;
117
+ pageSizeOptions: number[];
118
+ loading: boolean;
119
+ emptyMessage: React.ReactNode;
120
+ getRowClassName?: (row: T, index: number) => string;
121
+ onRowClick?: (row: T) => void;
122
+ onRowDoubleClick?: (row: T) => void;
123
+ }
124
+
125
+ const DataTableContext = createContext<DataTableContextValue<unknown> | null>(null);
126
+
127
+ export function useDataTableContext<T>(): DataTableContextValue<T> {
128
+ const ctx = React.useContext(DataTableContext);
129
+ if (!ctx) {
130
+ throw new Error('useDataTableContext must be used inside <DataTableProvider>');
131
+ }
132
+ return ctx as DataTableContextValue<T>;
133
+ }
134
+
135
+ // =====================================================================
136
+ // Provider
137
+ // =====================================================================
138
+
139
+ export interface DataTableProviderProps<T>
140
+ extends Pick<
141
+ DataTableProps<T>,
142
+ | 'data'
143
+ | 'columns'
144
+ | 'getRowId'
145
+ | 'selectionMode'
146
+ | 'initialSelectedIds'
147
+ | 'onSelectionChange'
148
+ | 'sort'
149
+ | 'onSortChange'
150
+ | 'initialSort'
151
+ | 'filters'
152
+ | 'onFilterChange'
153
+ | 'initialFilters'
154
+ | 'pagination'
155
+ | 'onPaginationChange'
156
+ | 'initialPagination'
157
+ | 'pageSizeOptions'
158
+ | 'loading'
159
+ | 'emptyMessage'
160
+ | 'getRowClassName'
161
+ | 'onRowClick'
162
+ | 'onRowDoubleClick'
163
+ > {
164
+ children: React.ReactNode;
165
+ }
166
+
167
+ const defaultSort: DataTableSortState = { key: null, direction: null };
168
+ const defaultPagination: DataTablePaginationState = { page: 1, pageSize: 25 };
169
+
170
+ export function DataTableProvider<T>(props: DataTableProviderProps<T>) {
171
+ const {
172
+ data,
173
+ columns,
174
+ getRowId,
175
+ selectionMode = 'none',
176
+ initialSelectedIds,
177
+ onSelectionChange,
178
+ sort: controlledSort,
179
+ onSortChange,
180
+ initialSort,
181
+ filters: controlledFilters,
182
+ onFilterChange,
183
+ initialFilters,
184
+ pagination: controlledPagination,
185
+ onPaginationChange,
186
+ initialPagination,
187
+ pageSizeOptions = [10, 25, 50, 100],
188
+ loading = false,
189
+ emptyMessage = 'No data',
190
+ getRowClassName,
191
+ onRowClick,
192
+ onRowDoubleClick,
193
+ children,
194
+ } = props;
195
+
196
+ const isControlledSort = controlledSort !== undefined;
197
+ const isControlledFilters = controlledFilters !== undefined;
198
+ const isControlledPagination = controlledPagination !== undefined;
199
+
200
+ const [state, dispatch] = useReducer(reducer<T>, undefined, () => ({
201
+ sort: initialSort ?? defaultSort,
202
+ filters: initialFilters ?? {},
203
+ pagination: initialPagination ?? defaultPagination,
204
+ selected: new Set(initialSelectedIds ?? []),
205
+ }));
206
+
207
+ const effectiveSort = isControlledSort ? controlledSort : state.sort;
208
+ const effectiveFilters = isControlledFilters ? controlledFilters : state.filters;
209
+ const effectivePagination = isControlledPagination
210
+ ? controlledPagination
211
+ : state.pagination;
212
+
213
+ // Sorting
214
+ const sortedData = useMemo(() => {
215
+ if (!effectiveSort.key || !effectiveSort.direction) return data;
216
+ const col = columns.find((c) => c.key === effectiveSort.key);
217
+ if (!col || !col.sortable) return data;
218
+
219
+ const dir = effectiveSort.direction === 'asc' ? 1 : -1;
220
+ return [...data].sort((a, b) => {
221
+ const aVal = (a as Record<string, unknown>)[effectiveSort.key!];
222
+ const bVal = (b as Record<string, unknown>)[effectiveSort.key!];
223
+ if (aVal == null && bVal == null) return 0;
224
+ if (aVal == null) return 1 * dir;
225
+ if (bVal == null) return -1 * dir;
226
+ if (typeof aVal === 'string' && typeof bVal === 'string') {
227
+ return aVal.localeCompare(bVal) * dir;
228
+ }
229
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
230
+ return (aVal - bVal) * dir;
231
+ }
232
+ return String(aVal).localeCompare(String(bVal)) * dir;
233
+ });
234
+ }, [data, effectiveSort, columns]);
235
+
236
+ // Filtering
237
+ const filteredData = useMemo(() => {
238
+ const entries = Object.entries(effectiveFilters);
239
+ if (entries.length === 0) return sortedData;
240
+ return sortedData.filter((row) => {
241
+ for (const [key, value] of entries) {
242
+ if (!value) continue;
243
+ const col = columns.find((c) => c.key === key);
244
+ if (col?.filterFn) {
245
+ if (!col.filterFn(row, value)) return false;
246
+ continue;
247
+ }
248
+ const cellValue = (row as Record<string, unknown>)[key];
249
+ if (cellValue == null) return false;
250
+ if (!String(cellValue).toLowerCase().includes(value.toLowerCase())) return false;
251
+ }
252
+ return true;
253
+ });
254
+ }, [sortedData, effectiveFilters, columns]);
255
+
256
+ // Pagination
257
+ const totalRows = filteredData.length;
258
+ const pageCount = Math.max(1, Math.ceil(totalRows / effectivePagination.pageSize));
259
+ const safePage = Math.min(effectivePagination.page, pageCount);
260
+
261
+ const paginatedData = useMemo(() => {
262
+ const start = (safePage - 1) * effectivePagination.pageSize;
263
+ return filteredData.slice(start, start + effectivePagination.pageSize);
264
+ }, [filteredData, safePage, effectivePagination.pageSize]);
265
+
266
+ // External callbacks via stable refs
267
+ const onSelectionChangeRef = useRef(onSelectionChange);
268
+ const onSortChangeRef = useRef(onSortChange);
269
+ const onFilterChangeRef = useRef(onFilterChange);
270
+ const onPaginationChangeRef = useRef(onPaginationChange);
271
+ onSelectionChangeRef.current = onSelectionChange;
272
+ onSortChangeRef.current = onSortChange;
273
+ onFilterChangeRef.current = onFilterChange;
274
+ onPaginationChangeRef.current = onPaginationChange;
275
+
276
+ useEffect(() => {
277
+ onSelectionChangeRef.current?.([...state.selected]);
278
+ }, [state.selected]);
279
+
280
+ // Actions
281
+ const setSort = useCallback(
282
+ (key: string) => {
283
+ let nextSort: DataTableSortState;
284
+ if (effectiveSort.key === key) {
285
+ const cycle: DataTableSortDirection[] = ['asc', 'desc', null];
286
+ const idx = cycle.indexOf(effectiveSort.direction);
287
+ const nextDirection = cycle[(idx + 1) % cycle.length] ?? 'asc';
288
+ nextSort = { key, direction: nextDirection };
289
+ } else {
290
+ nextSort = { key, direction: 'asc' };
291
+ }
292
+ if (isControlledSort) {
293
+ onSortChangeRef.current?.(nextSort);
294
+ } else {
295
+ dispatch({ type: 'set-sort', sort: nextSort });
296
+ }
297
+ },
298
+ [effectiveSort, isControlledSort],
299
+ );
300
+
301
+ const setFilter = useCallback(
302
+ (key: string, value: string) => {
303
+ if (isControlledFilters) {
304
+ const next = { ...effectiveFilters, [key]: value };
305
+ if (value === '') delete next[key];
306
+ onFilterChangeRef.current?.(next);
307
+ } else {
308
+ dispatch({ type: 'set-filter', key, value });
309
+ }
310
+ },
311
+ [isControlledFilters, effectiveFilters],
312
+ );
313
+
314
+ const clearFilters = useCallback(() => {
315
+ if (isControlledFilters) {
316
+ onFilterChangeRef.current?.({});
317
+ } else {
318
+ dispatch({ type: 'clear-filters' });
319
+ }
320
+ }, [isControlledFilters]);
321
+
322
+ const setPage = useCallback(
323
+ (page: number) => {
324
+ const safe = Math.max(1, Math.min(page, pageCount));
325
+ if (isControlledPagination) {
326
+ onPaginationChangeRef.current?.({ ...effectivePagination, page: safe });
327
+ } else {
328
+ dispatch({ type: 'set-page', page: safe });
329
+ }
330
+ },
331
+ [isControlledPagination, effectivePagination, pageCount],
332
+ );
333
+
334
+ const setPageSize = useCallback(
335
+ (pageSize: number) => {
336
+ if (isControlledPagination) {
337
+ onPaginationChangeRef.current?.({ page: 1, pageSize });
338
+ } else {
339
+ dispatch({ type: 'set-page-size', pageSize });
340
+ }
341
+ },
342
+ [isControlledPagination],
343
+ );
344
+
345
+ const select = useCallback(
346
+ (id: DataTableRowId) => dispatch({ type: 'select', id, mode: selectionMode }),
347
+ [selectionMode],
348
+ );
349
+
350
+ const selectMany = useCallback((ids: DataTableRowId[]) => {
351
+ dispatch({ type: 'select-many', ids });
352
+ }, []);
353
+
354
+ const clearSelection = useCallback(() => {
355
+ dispatch({ type: 'clear-selection' });
356
+ }, []);
357
+
358
+ const selectAllVisible = useCallback(() => {
359
+ if (selectionMode !== 'multiple') return;
360
+ const ids = paginatedData.map(getRowId);
361
+ dispatch({ type: 'select-all', ids });
362
+ }, [paginatedData, getRowId, selectionMode]);
363
+
364
+ const isSelected = useCallback(
365
+ (id: DataTableRowId) => state.selected.has(id),
366
+ [state.selected],
367
+ );
368
+
369
+ const value = useMemo<DataTableContextValue<T>>(
370
+ () => ({
371
+ sort: effectiveSort,
372
+ filters: effectiveFilters,
373
+ pagination: { ...effectivePagination, page: safePage },
374
+ selected: state.selected,
375
+ rows: paginatedData,
376
+ totalRows,
377
+ pageCount,
378
+ setSort,
379
+ setFilter,
380
+ clearFilters,
381
+ setPage,
382
+ setPageSize,
383
+ select,
384
+ selectMany,
385
+ clearSelection,
386
+ selectAllVisible,
387
+ isSelected,
388
+ columns,
389
+ getRowId,
390
+ selectionMode,
391
+ pageSizeOptions,
392
+ loading,
393
+ emptyMessage,
394
+ getRowClassName,
395
+ onRowClick,
396
+ onRowDoubleClick,
397
+ }),
398
+ [
399
+ effectiveSort,
400
+ effectiveFilters,
401
+ effectivePagination,
402
+ safePage,
403
+ state.selected,
404
+ paginatedData,
405
+ totalRows,
406
+ pageCount,
407
+ setSort,
408
+ setFilter,
409
+ clearFilters,
410
+ setPage,
411
+ setPageSize,
412
+ select,
413
+ selectMany,
414
+ clearSelection,
415
+ selectAllVisible,
416
+ isSelected,
417
+ columns,
418
+ getRowId,
419
+ selectionMode,
420
+ pageSizeOptions,
421
+ loading,
422
+ emptyMessage,
423
+ getRowClassName,
424
+ onRowClick,
425
+ onRowDoubleClick,
426
+ ],
427
+ );
428
+
429
+ return (
430
+ <DataTableContext.Provider value={value as DataTableContextValue<unknown>}>
431
+ {children}
432
+ </DataTableContext.Provider>
433
+ );
434
+ }
@@ -0,0 +1,4 @@
1
+ 'use client';
2
+
3
+ export { DataTableProvider, useDataTableContext } from './DataTableContext';
4
+ export type { DataTableContextValue, DataTableProviderProps } from './DataTableContext';
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+
3
+ export {
4
+ DataTableProvider,
5
+ useDataTableContext,
6
+ } from './context/DataTableContext';
7
+ export type {
8
+ DataTableContextValue,
9
+ DataTableProviderProps,
10
+ } from './context/DataTableContext';
11
+
12
+ export {
13
+ DataTable,
14
+ DataTableHeader,
15
+ DataTableRow,
16
+ DataTablePagination,
17
+ } from './parts';
18
+
19
+ export type {
20
+ DataTableProps,
21
+ DataTableColumn,
22
+ DataTableSortState,
23
+ DataTableFilterState,
24
+ DataTablePaginationState,
25
+ DataTableSelectionMode,
26
+ DataTableRowId,
27
+ DataTableSortDirection,
28
+ } from './types';
29
+
30
+ // The dynamic-import wrapper. Exported here because the subpath resolves to
31
+ // this barrel: in the retired ui-tools `lazy.tsx` was reachable from no declared export,
32
+ // so `LazyDataTable` was dead code for every tool but `JsonTree`.
33
+ export { LazyDataTable } from './lazy';
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+
3
+ import { createLazyComponent, LoadingFallback } from '@djangocfg/widget-kit/lazy';
4
+ import type { DataTableProps } from './types';
5
+
6
+ export const LazyDataTable = createLazyComponent<DataTableProps<unknown>>(
7
+ () => import('./parts/DataTable').then((m) => ({ default: m.DataTable })),
8
+ {
9
+ displayName: 'LazyDataTable',
10
+ fallback: <LoadingFallback minHeight={300} text="Loading data table…" />,
11
+ },
12
+ );
13
+
14
+ // Re-export light surface synchronously
15
+ export * from './types';
16
+ export {
17
+ DataTableProvider,
18
+ useDataTableContext,
19
+ } from './context/DataTableContext';
20
+ export type {
21
+ DataTableContextValue,
22
+ DataTableProviderProps,
23
+ } from './context/DataTableContext';
@@ -0,0 +1,142 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { cn } from '@djangocfg/ui-core/lib';
5
+ import {
6
+ Table,
7
+ TableHeader,
8
+ TableBody,
9
+ TableFooter,
10
+ TableRow,
11
+ TableHead,
12
+ TableCell,
13
+ TableCaption,
14
+ } from '@djangocfg/ui-core/components';
15
+
16
+ import type { DataTableProps } from '../types';
17
+ import { DataTableProvider } from '../context/DataTableContext';
18
+ import { DataTableHeader } from './DataTableHeader';
19
+ import { DataTableRow } from './DataTableRow';
20
+ import { DataTablePagination } from './DataTablePagination';
21
+
22
+ export function DataTable<T>(props: DataTableProps<T>) {
23
+ const {
24
+ data,
25
+ columns,
26
+ getRowId,
27
+ selectionMode,
28
+ initialSelectedIds,
29
+ onSelectionChange,
30
+ sort,
31
+ onSortChange,
32
+ initialSort,
33
+ filters,
34
+ onFilterChange,
35
+ initialFilters,
36
+ pagination,
37
+ onPaginationChange,
38
+ initialPagination,
39
+ pageSizeOptions,
40
+ loading,
41
+ emptyMessage,
42
+ getRowClassName,
43
+ onRowClick,
44
+ onRowDoubleClick,
45
+ className,
46
+ style,
47
+ } = props;
48
+
49
+ return (
50
+ <DataTableProvider<T>
51
+ data={data}
52
+ columns={columns}
53
+ getRowId={getRowId}
54
+ selectionMode={selectionMode}
55
+ initialSelectedIds={initialSelectedIds}
56
+ onSelectionChange={onSelectionChange}
57
+ sort={sort}
58
+ onSortChange={onSortChange}
59
+ initialSort={initialSort}
60
+ filters={filters}
61
+ onFilterChange={onFilterChange}
62
+ initialFilters={initialFilters}
63
+ pagination={pagination}
64
+ onPaginationChange={onPaginationChange}
65
+ initialPagination={initialPagination}
66
+ pageSizeOptions={pageSizeOptions}
67
+ loading={loading}
68
+ emptyMessage={emptyMessage}
69
+ getRowClassName={getRowClassName}
70
+ onRowClick={onRowClick}
71
+ onRowDoubleClick={onRowDoubleClick}
72
+ >
73
+ <DataTableShell className={className} style={style} />
74
+ </DataTableProvider>
75
+ );
76
+ }
77
+ DataTable.displayName = 'DataTable';
78
+
79
+ function DataTableShell({ className, style }: { className?: string; style?: React.CSSProperties }) {
80
+ return (
81
+ <div
82
+ data-slot="data-table"
83
+ // Single card frame owned by the shell, matching ui-core <Card>:
84
+ // `--radius-popover` corner + `--shadow-card` so a data panel reads the
85
+ // same as every other panel in the app (and gains the soft light-theme
86
+ // lift). The inner <Table> renders `frame={false}` so it doesn't draw a
87
+ // second border/radius inside this one (that produced the visible double
88
+ // border + mismatched corners). `overflow-hidden` clips the header band
89
+ // and the pagination strip to the rounded corners; the pagination's own
90
+ // top border is the only divider between body and footer — no `gap`
91
+ // gutter that reads as a second line.
92
+ className={cn(
93
+ 'flex flex-col overflow-hidden rounded-[var(--radius-popover)] border border-border bg-card text-card-foreground [box-shadow:var(--shadow-card)]',
94
+ className
95
+ )}
96
+ style={style}
97
+ >
98
+ <Table frame={false}>
99
+ <DataTableHeader />
100
+ <TableBody>
101
+ <DataTableRows />
102
+ </TableBody>
103
+ </Table>
104
+ <DataTablePagination />
105
+ </div>
106
+ );
107
+ }
108
+
109
+ function DataTableRows() {
110
+ const { rows, getRowId, loading, emptyMessage } = useDataTableContext();
111
+
112
+ if (loading) {
113
+ return (
114
+ <TableRow>
115
+ <TableCell colSpan={999} className="p-8 text-center text-muted-foreground">
116
+ Loading…
117
+ </TableCell>
118
+ </TableRow>
119
+ );
120
+ }
121
+
122
+ if (rows.length === 0) {
123
+ return (
124
+ <TableRow>
125
+ <TableCell colSpan={999} className="p-8 text-center text-muted-foreground">
126
+ {emptyMessage}
127
+ </TableCell>
128
+ </TableRow>
129
+ );
130
+ }
131
+
132
+ return (
133
+ <>
134
+ {rows.map((row, index) => (
135
+ <DataTableRow key={String(getRowId(row))} row={row} index={index} />
136
+ ))}
137
+ </>
138
+ );
139
+ }
140
+
141
+ // Ambient import for JSX usage
142
+ import { useDataTableContext } from '../context/DataTableContext';