@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,103 @@
|
|
|
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
|
+
import {
|
|
7
|
+
TableHeader,
|
|
8
|
+
TableRow,
|
|
9
|
+
TableHead,
|
|
10
|
+
} from '@djangocfg/ui-core/components';
|
|
11
|
+
|
|
12
|
+
import { useDataTableContext } from '../context/DataTableContext';
|
|
13
|
+
|
|
14
|
+
export function DataTableHeader() {
|
|
15
|
+
const { columns, selectionMode, selectAllVisible, isSelected, rows, getRowId } =
|
|
16
|
+
useDataTableContext();
|
|
17
|
+
|
|
18
|
+
const allVisibleSelected = rows.length > 0 && rows.every((r) => isSelected(getRowId(r)));
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<TableHeader>
|
|
22
|
+
<TableRow>
|
|
23
|
+
{selectionMode === 'multiple' && (
|
|
24
|
+
<TableHead className="w-10">
|
|
25
|
+
<input
|
|
26
|
+
type="checkbox"
|
|
27
|
+
checked={allVisibleSelected}
|
|
28
|
+
onChange={selectAllVisible}
|
|
29
|
+
className="h-4 w-4 rounded border-border accent-primary"
|
|
30
|
+
aria-label="Select all visible rows"
|
|
31
|
+
/>
|
|
32
|
+
</TableHead>
|
|
33
|
+
)}
|
|
34
|
+
{selectionMode === 'single' && <TableHead className="w-10" />}
|
|
35
|
+
{columns.map((col) => (
|
|
36
|
+
<DataTableHeaderCell key={col.key} column={col} />
|
|
37
|
+
))}
|
|
38
|
+
</TableRow>
|
|
39
|
+
<DataTableFilterRow />
|
|
40
|
+
</TableHeader>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
DataTableHeader.displayName = 'DataTableHeader';
|
|
44
|
+
|
|
45
|
+
function DataTableHeaderCell({ column }: { column: { key: string; header: React.ReactNode; sortable?: boolean; align?: 'left' | 'center' | 'right' } }) {
|
|
46
|
+
const { sort, setSort } = useDataTableContext();
|
|
47
|
+
const isSorted = sort.key === column.key;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<TableHead
|
|
51
|
+
className={cn(
|
|
52
|
+
column.align === 'center' && 'text-center',
|
|
53
|
+
column.align === 'right' && 'text-right',
|
|
54
|
+
column.sortable && 'cursor-pointer select-none'
|
|
55
|
+
)}
|
|
56
|
+
onClick={() => column.sortable && setSort(column.key)}
|
|
57
|
+
data-sort={isSorted ? sort.direction : undefined}
|
|
58
|
+
>
|
|
59
|
+
<span className="inline-flex items-center gap-1">
|
|
60
|
+
{column.header}
|
|
61
|
+
{column.sortable && (
|
|
62
|
+
<span className="inline-flex text-muted-foreground/60">
|
|
63
|
+
{isSorted && sort.direction === 'asc' && <ArrowUp className="h-3.5 w-3.5" />}
|
|
64
|
+
{isSorted && sort.direction === 'desc' && <ArrowDown className="h-3.5 w-3.5" />}
|
|
65
|
+
{!isSorted && <ArrowUpDown className="h-3.5 w-3.5" />}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
</span>
|
|
69
|
+
</TableHead>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function DataTableFilterRow() {
|
|
74
|
+
const { columns, filters, setFilter, selectionMode } = useDataTableContext();
|
|
75
|
+
const hasFilterable = columns.some((c) => c.filterable);
|
|
76
|
+
|
|
77
|
+
if (!hasFilterable) return null;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<TableRow>
|
|
81
|
+
{selectionMode !== 'none' && <TableHead />}
|
|
82
|
+
{columns.map((col) => (
|
|
83
|
+
<TableHead key={col.key}>
|
|
84
|
+
{col.filterable ? (
|
|
85
|
+
<input
|
|
86
|
+
type="text"
|
|
87
|
+
value={filters[col.key] ?? ''}
|
|
88
|
+
onChange={(e) => setFilter(col.key, e.target.value)}
|
|
89
|
+
placeholder="Filter…"
|
|
90
|
+
className={cn(
|
|
91
|
+
'w-full rounded-sm border border-input bg-background px-2 py-1 text-xs',
|
|
92
|
+
'placeholder:text-muted-foreground',
|
|
93
|
+
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'
|
|
94
|
+
)}
|
|
95
|
+
/>
|
|
96
|
+
) : (
|
|
97
|
+
<span />
|
|
98
|
+
)}
|
|
99
|
+
</TableHead>
|
|
100
|
+
))}
|
|
101
|
+
</TableRow>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from '@djangocfg/ui-core/components';
|
|
12
|
+
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
import { useDataTableContext } from '../context/DataTableContext';
|
|
15
|
+
|
|
16
|
+
export function DataTablePagination() {
|
|
17
|
+
const { pagination, setPage, setPageSize, totalRows, pageCount, pageSizeOptions } =
|
|
18
|
+
useDataTableContext();
|
|
19
|
+
|
|
20
|
+
const start = totalRows === 0 ? 0 : (pagination.page - 1) * pagination.pageSize + 1;
|
|
21
|
+
const end = Math.min(pagination.page * pagination.pageSize, totalRows);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
data-slot="data-table-pagination"
|
|
26
|
+
className="flex flex-col items-center justify-between gap-2 border-t border-border px-4 py-2 sm:flex-row"
|
|
27
|
+
>
|
|
28
|
+
<span className="text-sm text-muted-foreground">
|
|
29
|
+
{start}-{end} of {totalRows}
|
|
30
|
+
</span>
|
|
31
|
+
|
|
32
|
+
<div className="flex items-center gap-2">
|
|
33
|
+
<Select
|
|
34
|
+
value={String(pagination.pageSize)}
|
|
35
|
+
onValueChange={(value) => setPageSize(Number(value))}
|
|
36
|
+
>
|
|
37
|
+
{/* `--radius` is the shared control corner used by ui-core <Button>,
|
|
38
|
+
* so the page-size trigger and the nav buttons round identically. */}
|
|
39
|
+
<SelectTrigger
|
|
40
|
+
className="h-8 w-auto gap-1 rounded-[var(--radius)] px-2 shadow-none"
|
|
41
|
+
aria-label="Rows per page"
|
|
42
|
+
>
|
|
43
|
+
<SelectValue />
|
|
44
|
+
</SelectTrigger>
|
|
45
|
+
<SelectContent>
|
|
46
|
+
{pageSizeOptions.map((size) => (
|
|
47
|
+
<SelectItem key={size} value={String(size)}>
|
|
48
|
+
{size} / page
|
|
49
|
+
</SelectItem>
|
|
50
|
+
))}
|
|
51
|
+
</SelectContent>
|
|
52
|
+
</Select>
|
|
53
|
+
|
|
54
|
+
{/* Nav controls reuse the library <Button> (outline/icon) rather than a
|
|
55
|
+
* bespoke button, so hover, disabled, focus-ring and radius all track
|
|
56
|
+
* the rest of the app automatically. `h-8 w-8` shrinks the default
|
|
57
|
+
* icon size (h-9 w-9) to match the compact pagination row.
|
|
58
|
+
* `elevated={false}` drops the variant's shadow so the buttons sit
|
|
59
|
+
* flat against the panel, matching the shadow-less page-size select. */}
|
|
60
|
+
<div className="flex items-center gap-1">
|
|
61
|
+
<Button
|
|
62
|
+
variant="outline"
|
|
63
|
+
size="icon"
|
|
64
|
+
elevated={false}
|
|
65
|
+
className="h-8 w-8"
|
|
66
|
+
onClick={() => setPage(1)}
|
|
67
|
+
disabled={pagination.page <= 1}
|
|
68
|
+
aria-label="First page"
|
|
69
|
+
>
|
|
70
|
+
<ChevronsLeft className="h-4 w-4" />
|
|
71
|
+
</Button>
|
|
72
|
+
<Button
|
|
73
|
+
variant="outline"
|
|
74
|
+
size="icon"
|
|
75
|
+
elevated={false}
|
|
76
|
+
className="h-8 w-8"
|
|
77
|
+
onClick={() => setPage(pagination.page - 1)}
|
|
78
|
+
disabled={pagination.page <= 1}
|
|
79
|
+
aria-label="Previous page"
|
|
80
|
+
>
|
|
81
|
+
<ChevronLeft className="h-4 w-4" />
|
|
82
|
+
</Button>
|
|
83
|
+
|
|
84
|
+
<span className="px-2 text-sm tabular-nums">
|
|
85
|
+
{pagination.page} / {pageCount}
|
|
86
|
+
</span>
|
|
87
|
+
|
|
88
|
+
<Button
|
|
89
|
+
variant="outline"
|
|
90
|
+
size="icon"
|
|
91
|
+
elevated={false}
|
|
92
|
+
className="h-8 w-8"
|
|
93
|
+
onClick={() => setPage(pagination.page + 1)}
|
|
94
|
+
disabled={pagination.page >= pageCount}
|
|
95
|
+
aria-label="Next page"
|
|
96
|
+
>
|
|
97
|
+
<ChevronRight className="h-4 w-4" />
|
|
98
|
+
</Button>
|
|
99
|
+
<Button
|
|
100
|
+
variant="outline"
|
|
101
|
+
size="icon"
|
|
102
|
+
elevated={false}
|
|
103
|
+
className="h-8 w-8"
|
|
104
|
+
onClick={() => setPage(pageCount)}
|
|
105
|
+
disabled={pagination.page >= pageCount}
|
|
106
|
+
aria-label="Last page"
|
|
107
|
+
>
|
|
108
|
+
<ChevronsRight className="h-4 w-4" />
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
DataTablePagination.displayName = 'DataTablePagination';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
import { TableRow, TableCell } from '@djangocfg/ui-core/components';
|
|
6
|
+
|
|
7
|
+
import { useDataTableContext } from '../context/DataTableContext';
|
|
8
|
+
|
|
9
|
+
interface DataTableRowProps<T> {
|
|
10
|
+
row: T;
|
|
11
|
+
index: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DataTableRow<T>({ row, index }: DataTableRowProps<T>) {
|
|
15
|
+
const {
|
|
16
|
+
columns,
|
|
17
|
+
getRowId,
|
|
18
|
+
selectionMode,
|
|
19
|
+
select,
|
|
20
|
+
isSelected,
|
|
21
|
+
getRowClassName,
|
|
22
|
+
onRowClick,
|
|
23
|
+
onRowDoubleClick,
|
|
24
|
+
} = useDataTableContext<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
|
+
<TableRow
|
|
42
|
+
data-slot="data-table-row"
|
|
43
|
+
data-id={String(id)}
|
|
44
|
+
data-selected={selected ? '' : undefined}
|
|
45
|
+
data-state={selected ? 'selected' : undefined}
|
|
46
|
+
className={cn(getRowClassName?.(row, index))}
|
|
47
|
+
onClick={handleClick}
|
|
48
|
+
onDoubleClick={handleDoubleClick}
|
|
49
|
+
>
|
|
50
|
+
{selectionMode === 'multiple' && (
|
|
51
|
+
<TableCell>
|
|
52
|
+
<input
|
|
53
|
+
type="checkbox"
|
|
54
|
+
checked={selected}
|
|
55
|
+
onChange={() => select(id)}
|
|
56
|
+
className="h-4 w-4 rounded border-border accent-primary"
|
|
57
|
+
aria-label={`Select row ${String(id)}`}
|
|
58
|
+
onClick={(e) => e.stopPropagation()}
|
|
59
|
+
/>
|
|
60
|
+
</TableCell>
|
|
61
|
+
)}
|
|
62
|
+
{selectionMode === 'single' && (
|
|
63
|
+
<TableCell>
|
|
64
|
+
<input
|
|
65
|
+
type="radio"
|
|
66
|
+
checked={selected}
|
|
67
|
+
onChange={() => select(id)}
|
|
68
|
+
className="h-4 w-4 border-border accent-primary"
|
|
69
|
+
aria-label={`Select row ${String(id)}`}
|
|
70
|
+
onClick={(e) => e.stopPropagation()}
|
|
71
|
+
/>
|
|
72
|
+
</TableCell>
|
|
73
|
+
)}
|
|
74
|
+
{columns.map((col) => (
|
|
75
|
+
<DataTableCell key={col.key} column={col} row={row} />
|
|
76
|
+
))}
|
|
77
|
+
</TableRow>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
DataTableRow.displayName = 'DataTableRow';
|
|
81
|
+
|
|
82
|
+
function DataTableCell<T>({ column, row }: { column: { key: string; cell?: (row: T) => React.ReactNode; align?: 'left' | 'center' | 'right' }; row: T }) {
|
|
83
|
+
const value = (row as Record<string, unknown>)[column.key];
|
|
84
|
+
const content = column.cell ? column.cell(row) : value != null ? String(value) : null;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<TableCell
|
|
88
|
+
className={cn(
|
|
89
|
+
column.align === 'center' && 'text-center',
|
|
90
|
+
column.align === 'right' && 'text-right'
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
{content}
|
|
94
|
+
</TableCell>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
export type DataTableRowId = string | number;
|
|
6
|
+
|
|
7
|
+
export type DataTableSortDirection = 'asc' | 'desc' | null;
|
|
8
|
+
|
|
9
|
+
export interface DataTableColumn<T = unknown> {
|
|
10
|
+
key: string;
|
|
11
|
+
header: ReactNode;
|
|
12
|
+
sortable?: boolean;
|
|
13
|
+
filterable?: boolean;
|
|
14
|
+
align?: 'left' | 'center' | 'right';
|
|
15
|
+
/** Custom cell renderer. Receives the row data. */
|
|
16
|
+
cell?: (row: T) => ReactNode;
|
|
17
|
+
/** Custom filter predicate for this column. */
|
|
18
|
+
filterFn?: (row: T, filterValue: string) => boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DataTableSortState {
|
|
22
|
+
key: string | null;
|
|
23
|
+
direction: DataTableSortDirection;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DataTableFilterState {
|
|
27
|
+
[columnKey: string]: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DataTablePaginationState {
|
|
31
|
+
page: number;
|
|
32
|
+
pageSize: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type DataTableSelectionMode = 'none' | 'single' | 'multiple';
|
|
36
|
+
|
|
37
|
+
export interface DataTableProps<T = unknown> {
|
|
38
|
+
/** Row data */
|
|
39
|
+
data: T[];
|
|
40
|
+
/** Column definitions */
|
|
41
|
+
columns: DataTableColumn<T>[];
|
|
42
|
+
/** Unique row id getter */
|
|
43
|
+
getRowId: (row: T) => DataTableRowId;
|
|
44
|
+
|
|
45
|
+
/** Selection behaviour. Default: 'none' */
|
|
46
|
+
selectionMode?: DataTableSelectionMode;
|
|
47
|
+
/** Initially selected row ids */
|
|
48
|
+
initialSelectedIds?: DataTableRowId[];
|
|
49
|
+
/** Triggered when selection changes */
|
|
50
|
+
onSelectionChange?: (selectedIds: DataTableRowId[]) => void;
|
|
51
|
+
|
|
52
|
+
/** Controlled sort state */
|
|
53
|
+
sort?: DataTableSortState;
|
|
54
|
+
/** Called when sort changes */
|
|
55
|
+
onSortChange?: (sort: DataTableSortState) => void;
|
|
56
|
+
/** Default sort */
|
|
57
|
+
initialSort?: DataTableSortState;
|
|
58
|
+
|
|
59
|
+
/** Controlled filter state */
|
|
60
|
+
filters?: DataTableFilterState;
|
|
61
|
+
/** Called when filters change */
|
|
62
|
+
onFilterChange?: (filters: DataTableFilterState) => void;
|
|
63
|
+
/** Default filters */
|
|
64
|
+
initialFilters?: DataTableFilterState;
|
|
65
|
+
|
|
66
|
+
/** Controlled pagination */
|
|
67
|
+
pagination?: DataTablePaginationState;
|
|
68
|
+
/** Called when pagination changes */
|
|
69
|
+
onPaginationChange?: (pagination: DataTablePaginationState) => void;
|
|
70
|
+
/** Default pagination */
|
|
71
|
+
initialPagination?: DataTablePaginationState;
|
|
72
|
+
/** Available page sizes. Default: [10, 25, 50, 100] */
|
|
73
|
+
pageSizeOptions?: number[];
|
|
74
|
+
|
|
75
|
+
/** Loading state */
|
|
76
|
+
loading?: boolean;
|
|
77
|
+
/** Empty state message */
|
|
78
|
+
emptyMessage?: ReactNode;
|
|
79
|
+
/** Row className getter */
|
|
80
|
+
getRowClassName?: (row: T, index: number) => string;
|
|
81
|
+
/** Called when a row is clicked */
|
|
82
|
+
onRowClick?: (row: T) => void;
|
|
83
|
+
/** Called when a row is double-clicked */
|
|
84
|
+
onRowDoubleClick?: (row: T) => void;
|
|
85
|
+
|
|
86
|
+
className?: string;
|
|
87
|
+
style?: React.CSSProperties;
|
|
88
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
4
|
+
|
|
5
|
+
import { getFileIcon } from './get-file-icon';
|
|
6
|
+
import { resolveFolderIcon, type FolderIconOverrides } from './specialFolders';
|
|
7
|
+
|
|
8
|
+
export type FileIconSize = 12 | 14 | 16 | 18 | 20 | 24 | 28 | 32 | 40 | 48 | 64;
|
|
9
|
+
|
|
10
|
+
export interface FileIconProps {
|
|
11
|
+
/** File name (with extension) or folder name. */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Render a folder icon instead of a file icon. */
|
|
14
|
+
isFolder?: boolean;
|
|
15
|
+
/** Folder open / closed state. Ignored when `isFolder` is false. */
|
|
16
|
+
isExpanded?: boolean;
|
|
17
|
+
/** Pixel size of the icon. */
|
|
18
|
+
size?: FileIconSize;
|
|
19
|
+
/**
|
|
20
|
+
* Override or extend the built-in special-folder mapping. Keys are
|
|
21
|
+
* matched case-insensitively. Only used when `isFolder` is true.
|
|
22
|
+
*/
|
|
23
|
+
folderOverrides?: FolderIconOverrides;
|
|
24
|
+
/**
|
|
25
|
+
* Accessible label. When omitted the icon is decorative (`aria-hidden`) —
|
|
26
|
+
* correct when it sits next to a visible file name. Pass a string to
|
|
27
|
+
* expose the icon to assistive technology.
|
|
28
|
+
*/
|
|
29
|
+
label?: string;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* VSCode-style file icon.
|
|
35
|
+
*
|
|
36
|
+
* - Folders render Lucide `Folder` / `FolderOpen` (warning-token tint).
|
|
37
|
+
* - Files render rich, colourful SVGs vendored from `material-file-icons`
|
|
38
|
+
* (MIT). Resolution is synchronous — no runtime dependency, no async load.
|
|
39
|
+
*
|
|
40
|
+
* The icon SVG data is a static module, so it bundles into the `FileIcon`
|
|
41
|
+
* chunk and is only fetched by consumers that actually mount this component.
|
|
42
|
+
*/
|
|
43
|
+
export function FileIcon({
|
|
44
|
+
name,
|
|
45
|
+
isFolder = false,
|
|
46
|
+
isExpanded = false,
|
|
47
|
+
size = 16,
|
|
48
|
+
folderOverrides,
|
|
49
|
+
label,
|
|
50
|
+
className,
|
|
51
|
+
}: FileIconProps) {
|
|
52
|
+
// Either a labelled image, or fully hidden from assistive tech (the default,
|
|
53
|
+
// since the icon usually sits beside a visible file name).
|
|
54
|
+
const a11y =
|
|
55
|
+
label === undefined
|
|
56
|
+
? ({ 'aria-hidden': true } as const)
|
|
57
|
+
: ({ role: 'img', 'aria-label': label } as const);
|
|
58
|
+
|
|
59
|
+
if (isFolder) {
|
|
60
|
+
const Icon = resolveFolderIcon({
|
|
61
|
+
name,
|
|
62
|
+
isExpanded,
|
|
63
|
+
overrides: folderOverrides,
|
|
64
|
+
});
|
|
65
|
+
return (
|
|
66
|
+
<Icon
|
|
67
|
+
{...a11y}
|
|
68
|
+
className={cn('shrink-0 text-warning', className)}
|
|
69
|
+
style={{ width: size, height: size }}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Vendored static data — `getFileIcon` always returns a definition.
|
|
75
|
+
const svg = getFileIcon(name).svg;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<span
|
|
79
|
+
{...a11y}
|
|
80
|
+
className={cn('inline-block shrink-0', className)}
|
|
81
|
+
style={{ width: size, height: size }}
|
|
82
|
+
dangerouslySetInnerHTML={{ __html: svg }}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default FileIcon;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolver for VSCode-style file icons. Replaces the former optional
|
|
5
|
+
* `material-file-icons` runtime dependency — the icon SVGs are now vendored
|
|
6
|
+
* as a static data module (`./icons/icon-data`), so the bundler resolves them
|
|
7
|
+
* deterministically with no dynamic `import()` and no `@vite-ignore`.
|
|
8
|
+
*
|
|
9
|
+
* Lookup matches the original `material-file-icons` algorithm:
|
|
10
|
+
* 1. exact full-name match (`files[]`, lowercased)
|
|
11
|
+
* 2. longest matching extension (`name.endsWith('.' + ext)`)
|
|
12
|
+
* 3. fall back to the default file icon
|
|
13
|
+
*
|
|
14
|
+
* The `files` / `extensions` lookup maps are built lazily on first call.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_FILE_ICON,
|
|
19
|
+
FILE_ICONS,
|
|
20
|
+
type FileIconDef,
|
|
21
|
+
} from './icons/icon-data';
|
|
22
|
+
|
|
23
|
+
export type { FileIconDef };
|
|
24
|
+
export { DEFAULT_FILE_ICON };
|
|
25
|
+
|
|
26
|
+
/** Full-name → icon (e.g. `dockerfile`, `.gitignore`). */
|
|
27
|
+
let fileMap: Map<string, FileIconDef> | null = null;
|
|
28
|
+
/** Extension → icon. When several icons share an extension, last wins. */
|
|
29
|
+
let extMap: Map<string, FileIconDef> | null = null;
|
|
30
|
+
|
|
31
|
+
function buildIndex(): void {
|
|
32
|
+
fileMap = new Map();
|
|
33
|
+
extMap = new Map();
|
|
34
|
+
for (const icon of FILE_ICONS) {
|
|
35
|
+
if (icon.files) {
|
|
36
|
+
for (const f of icon.files) fileMap.set(f, icon);
|
|
37
|
+
}
|
|
38
|
+
if (icon.extensions) {
|
|
39
|
+
for (const ext of icon.extensions) extMap.set(ext, icon);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the icon definition for a file name. Always returns a definition —
|
|
46
|
+
* unknown types yield {@link DEFAULT_FILE_ICON}.
|
|
47
|
+
*/
|
|
48
|
+
export function getFileIcon(filename: string): FileIconDef {
|
|
49
|
+
if (typeof filename !== 'string' || filename.length === 0) {
|
|
50
|
+
return DEFAULT_FILE_ICON;
|
|
51
|
+
}
|
|
52
|
+
if (fileMap === null || extMap === null) buildIndex();
|
|
53
|
+
|
|
54
|
+
const lc = filename.toLowerCase();
|
|
55
|
+
|
|
56
|
+
// 1. Exact full-name match.
|
|
57
|
+
const byName = fileMap!.get(lc);
|
|
58
|
+
if (byName) return byName;
|
|
59
|
+
|
|
60
|
+
// 2. Longest matching extension.
|
|
61
|
+
let match: FileIconDef | null = null;
|
|
62
|
+
let matchLength = 0;
|
|
63
|
+
for (const [ext, icon] of extMap!) {
|
|
64
|
+
if (ext.length > matchLength && lc.endsWith(`.${ext}`)) {
|
|
65
|
+
match = icon;
|
|
66
|
+
matchLength = ext.length;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (match) return match;
|
|
70
|
+
|
|
71
|
+
// 3. Fallback.
|
|
72
|
+
return DEFAULT_FILE_ICON;
|
|
73
|
+
}
|