@djangocfg/widget-data 0.1.1 → 0.1.2
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/package.json +6 -5
- package/src/DataGrid/context/DataGridContext.tsx +4 -4
- package/src/DataGrid/parts/DataGridHeader.tsx +0 -1
- package/src/DataTable/context/DataTableContext.tsx +4 -4
- package/src/DataTable/parts/DataTable.tsx +0 -4
- package/src/FileIcon/treeAdapter.tsx +4 -1
- package/src/JsonTree/JsonViewer.tsx +6 -0
- package/src/Kanban/parts/KanbanColumn.tsx +1 -1
- package/src/Listbox/parts/ListboxGroup.tsx +0 -1
- package/src/Listbox/parts/ListboxItem.tsx +13 -0
- package/src/Masonry/context/MasonryProvider.tsx +0 -8
- package/src/Tree/context/TreeContext.tsx +6 -1
- package/src/Tree/context/async-children/use-async-children.ts +5 -0
- package/src/Tree/context/rename/use-rename.ts +0 -1
- package/src/Tree/data/dnd.ts +1 -1
- package/src/Tree/hooks/keyboard/activation.ts +2 -2
- package/src/Tree/hooks/keyboard/expand-collapse.ts +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/widget-data",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Data surfaces: file tree, JSON viewer, data grid and table, kanban, listbox, masonry and timeline",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tree",
|
|
@@ -85,25 +85,26 @@
|
|
|
85
85
|
"scripts": {
|
|
86
86
|
"check": "tsc --noEmit",
|
|
87
87
|
"check-types": "tsc --noEmit",
|
|
88
|
+
"lint": "eslint .",
|
|
88
89
|
"test": "vitest run",
|
|
89
90
|
"test:watch": "vitest"
|
|
90
91
|
},
|
|
91
92
|
"dependencies": {
|
|
92
|
-
"@djangocfg/widget-kit": "^0.1.
|
|
93
|
+
"@djangocfg/widget-kit": "^0.1.2",
|
|
93
94
|
"@dnd-kit/core": "^6.3.1",
|
|
94
95
|
"@radix-ui/react-direction": "^1.1.2",
|
|
95
96
|
"@radix-ui/react-slot": "^1.3.0",
|
|
96
97
|
"class-variance-authority": "^0.7.1"
|
|
97
98
|
},
|
|
98
99
|
"peerDependencies": {
|
|
99
|
-
"@djangocfg/ui-core": "^2.1.
|
|
100
|
+
"@djangocfg/ui-core": "^2.1.543",
|
|
100
101
|
"lucide-react": "^0.545.0",
|
|
101
102
|
"react": "^19.0.0",
|
|
102
103
|
"react-dom": "^19.0.0"
|
|
103
104
|
},
|
|
104
105
|
"devDependencies": {
|
|
105
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
106
|
-
"@djangocfg/ui-core": "^2.1.
|
|
106
|
+
"@djangocfg/typescript-config": "^2.1.543",
|
|
107
|
+
"@djangocfg/ui-core": "^2.1.543",
|
|
107
108
|
"@storybook/react-vite": "^10.5.0",
|
|
108
109
|
"@types/node": "^25.9.5",
|
|
109
110
|
"@types/react": "19.2.15",
|
|
@@ -25,14 +25,14 @@ import type {
|
|
|
25
25
|
// Reducer
|
|
26
26
|
// =====================================================================
|
|
27
27
|
|
|
28
|
-
interface State
|
|
28
|
+
interface State {
|
|
29
29
|
sort: DataGridSortState;
|
|
30
30
|
filters: DataGridFilterState;
|
|
31
31
|
pagination: DataGridPaginationState;
|
|
32
32
|
selected: Set<DataGridRowId>;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
type Action
|
|
35
|
+
type Action =
|
|
36
36
|
| { type: 'set-sort'; sort: DataGridSortState }
|
|
37
37
|
| { type: 'set-filter'; key: string; value: string }
|
|
38
38
|
| { type: 'clear-filters' }
|
|
@@ -44,7 +44,7 @@ type Action<T> =
|
|
|
44
44
|
| { type: 'select-all'; ids: DataGridRowId[] }
|
|
45
45
|
| { type: 'reset-pagination' };
|
|
46
46
|
|
|
47
|
-
const reducer =
|
|
47
|
+
const reducer = (state: State, action: Action): State => {
|
|
48
48
|
switch (action.type) {
|
|
49
49
|
case 'set-sort':
|
|
50
50
|
return { ...state, sort: action.sort };
|
|
@@ -203,7 +203,7 @@ export function DataGridProvider<T>(props: DataGridProviderProps<T>) {
|
|
|
203
203
|
const isControlledFilters = controlledFilters !== undefined;
|
|
204
204
|
const isControlledPagination = controlledPagination !== undefined;
|
|
205
205
|
|
|
206
|
-
const [state, dispatch] = useReducer(reducer
|
|
206
|
+
const [state, dispatch] = useReducer(reducer, undefined, () => ({
|
|
207
207
|
sort: initialSort ?? defaultSort,
|
|
208
208
|
filters: initialFilters ?? {},
|
|
209
209
|
pagination: initialPagination ?? defaultPagination,
|
|
@@ -5,7 +5,6 @@ import { cn } from '@djangocfg/ui-core/lib';
|
|
|
5
5
|
import { ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react';
|
|
6
6
|
|
|
7
7
|
import { useDataGridContext } from '../context/DataGridContext';
|
|
8
|
-
import { DataGridCell } from './DataGridCell';
|
|
9
8
|
|
|
10
9
|
export function DataGridHeader() {
|
|
11
10
|
const { columns, selectionMode, selectAllVisible, isSelected, rows, getRowId, stickyHeader } =
|
|
@@ -25,14 +25,14 @@ import type {
|
|
|
25
25
|
// Reducer
|
|
26
26
|
// =====================================================================
|
|
27
27
|
|
|
28
|
-
interface State
|
|
28
|
+
interface State {
|
|
29
29
|
sort: DataTableSortState;
|
|
30
30
|
filters: DataTableFilterState;
|
|
31
31
|
pagination: DataTablePaginationState;
|
|
32
32
|
selected: Set<DataTableRowId>;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
type Action
|
|
35
|
+
type Action =
|
|
36
36
|
| { type: 'set-sort'; sort: DataTableSortState }
|
|
37
37
|
| { type: 'set-filter'; key: string; value: string }
|
|
38
38
|
| { type: 'clear-filters' }
|
|
@@ -43,7 +43,7 @@ type Action<T> =
|
|
|
43
43
|
| { type: 'clear-selection' }
|
|
44
44
|
| { type: 'select-all'; ids: DataTableRowId[] };
|
|
45
45
|
|
|
46
|
-
const reducer =
|
|
46
|
+
const reducer = (state: State, action: Action): State => {
|
|
47
47
|
switch (action.type) {
|
|
48
48
|
case 'set-sort':
|
|
49
49
|
return { ...state, sort: action.sort };
|
|
@@ -197,7 +197,7 @@ export function DataTableProvider<T>(props: DataTableProviderProps<T>) {
|
|
|
197
197
|
const isControlledFilters = controlledFilters !== undefined;
|
|
198
198
|
const isControlledPagination = controlledPagination !== undefined;
|
|
199
199
|
|
|
200
|
-
const [state, dispatch] = useReducer(reducer
|
|
200
|
+
const [state, dispatch] = useReducer(reducer, undefined, () => ({
|
|
201
201
|
sort: initialSort ?? defaultSort,
|
|
202
202
|
filters: initialFilters ?? {},
|
|
203
203
|
pagination: initialPagination ?? defaultPagination,
|
|
@@ -4,13 +4,9 @@ import * as React from 'react';
|
|
|
4
4
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
5
|
import {
|
|
6
6
|
Table,
|
|
7
|
-
TableHeader,
|
|
8
7
|
TableBody,
|
|
9
|
-
TableFooter,
|
|
10
8
|
TableRow,
|
|
11
|
-
TableHead,
|
|
12
9
|
TableCell,
|
|
13
|
-
TableCaption,
|
|
14
10
|
} from '@djangocfg/ui-core/components';
|
|
15
11
|
|
|
16
12
|
import type { DataTableProps } from '../types';
|
|
@@ -37,7 +37,9 @@ export function createFileIconSlot<T>({
|
|
|
37
37
|
size = 16,
|
|
38
38
|
folderOverrides,
|
|
39
39
|
}: CreateFileIconSlotOptions<T>): TreeRowSlot<T> {
|
|
40
|
-
|
|
40
|
+
// Named so the slot shows as `FileIconSlot` in React devtools rather than
|
|
41
|
+
// as an anonymous function.
|
|
42
|
+
const FileIconSlot: TreeRowSlot<T> = ({ node, isFolder, isExpanded }) => (
|
|
41
43
|
<FileIcon
|
|
42
44
|
name={getName(node)}
|
|
43
45
|
isFolder={isFolder}
|
|
@@ -46,4 +48,5 @@ export function createFileIconSlot<T>({
|
|
|
46
48
|
folderOverrides={folderOverrides}
|
|
47
49
|
/>
|
|
48
50
|
);
|
|
51
|
+
return FileIconSlot;
|
|
49
52
|
}
|
|
@@ -99,11 +99,17 @@ function hasSearchMatch(value: JsonValue, key: string | number, query: string):
|
|
|
99
99
|
* brick-red strings, forest-green numbers) — pastels read as washed-out
|
|
100
100
|
* on a white background. Dark mirrors the One Dark family.
|
|
101
101
|
*/
|
|
102
|
+
// Syntax-highlighting hues, not status colours: `violet` names "this is a key",
|
|
103
|
+
// not "info", and `emerald` names "this is a string", not "success". A semantic
|
|
104
|
+
// token here would assert a meaning the value does not have, and re-theming
|
|
105
|
+
// would silently recolour the grammar.
|
|
102
106
|
const TOKEN_CLASS: Record<string, string> = {
|
|
107
|
+
/* eslint-disable no-restricted-syntax */
|
|
103
108
|
key: 'text-[#001080] dark:text-violet-400',
|
|
104
109
|
string: 'text-[#a31515] dark:text-emerald-400',
|
|
105
110
|
number: 'text-[#098658] dark:text-sky-400',
|
|
106
111
|
boolean: 'text-[#0550ae] dark:text-amber-400',
|
|
112
|
+
/* eslint-enable no-restricted-syntax */
|
|
107
113
|
null: 'text-muted-foreground/70 dark:text-muted-foreground/60',
|
|
108
114
|
punctuation: 'text-muted-foreground dark:text-muted-foreground',
|
|
109
115
|
};
|
|
@@ -13,7 +13,7 @@ interface KanbanColumnProps {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function KanbanColumn({ column }: KanbanColumnProps) {
|
|
16
|
-
const { renderColumnHeader, emptyColumnMessage, dragState, setDragState, moveCard
|
|
16
|
+
const { renderColumnHeader, emptyColumnMessage, dragState, setDragState, moveCard } =
|
|
17
17
|
useKanbanContext();
|
|
18
18
|
|
|
19
19
|
const handlePointerEnter = React.useCallback(() => {
|
|
@@ -47,8 +47,16 @@ const ListboxItem = React.forwardRef<HTMLDivElement, ListboxItemProps>(
|
|
|
47
47
|
);
|
|
48
48
|
}, [value, isDisabled, context.onItemRegister, groupContext?.id, onSelect]);
|
|
49
49
|
|
|
50
|
+
// The handlers below depend on individual context MEMBERS, not on
|
|
51
|
+
// `context`. Every one is a `useCallback` in ListboxProvider and the
|
|
52
|
+
// context is a `useMemo` over exactly them, so a member is stable whenever
|
|
53
|
+
// it matters. Depending on the whole object instead would rebuild all six
|
|
54
|
+
// handlers — and re-register the item — whenever any unrelated context
|
|
55
|
+
// field moved. The linter cannot see through the member access, hence the
|
|
56
|
+
// per-hook disables.
|
|
50
57
|
const onBlur = React.useCallback(() => {
|
|
51
58
|
context.onItemBlur();
|
|
59
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
60
|
}, [context.onItemBlur]);
|
|
53
61
|
|
|
54
62
|
const onClick = React.useCallback(
|
|
@@ -59,6 +67,7 @@ const ListboxItem = React.forwardRef<HTMLDivElement, ListboxItemProps>(
|
|
|
59
67
|
context.onItemSelect(value, isMultipleSelectionKey);
|
|
60
68
|
}
|
|
61
69
|
},
|
|
70
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
62
71
|
[context.onItemSelect, value, isDisabled, context.multiple],
|
|
63
72
|
);
|
|
64
73
|
|
|
@@ -67,6 +76,7 @@ const ListboxItem = React.forwardRef<HTMLDivElement, ListboxItemProps>(
|
|
|
67
76
|
context.onItemFocus(value);
|
|
68
77
|
context.onItemHighlight(value);
|
|
69
78
|
}
|
|
79
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
70
80
|
}, [context.onItemFocus, context.onItemHighlight, isDisabled, value]);
|
|
71
81
|
|
|
72
82
|
const onKeyDown = React.useCallback(
|
|
@@ -78,17 +88,20 @@ const ListboxItem = React.forwardRef<HTMLDivElement, ListboxItemProps>(
|
|
|
78
88
|
return;
|
|
79
89
|
}
|
|
80
90
|
},
|
|
91
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
81
92
|
[context.onItemBlur],
|
|
82
93
|
);
|
|
83
94
|
|
|
84
95
|
const onPointerLeave = React.useCallback(() => {
|
|
85
96
|
context.onItemHighlight(null);
|
|
97
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
98
|
}, [context.onItemHighlight]);
|
|
87
99
|
|
|
88
100
|
const onPointerMove = React.useCallback(() => {
|
|
89
101
|
if (!isDisabled) {
|
|
90
102
|
context.onItemHighlight(value);
|
|
91
103
|
}
|
|
104
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
92
105
|
}, [context.onItemHighlight, isDisabled, value]);
|
|
93
106
|
|
|
94
107
|
const ItemPrimitive = asChild ? Slot : "div";
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Slot } from "@radix-ui/react-slot";
|
|
5
5
|
import { useComposedRefs } from "@djangocfg/ui-core/lib";
|
|
6
|
-
import { cn } from "@djangocfg/ui-core/lib";
|
|
7
6
|
import type { MasonryProps, MasonryItemProps, Positioner, PositionerItem } from "../types";
|
|
8
7
|
|
|
9
8
|
// ============================================================================
|
|
@@ -615,13 +614,6 @@ function useDebouncedWindowSize(options: DebouncedWindowSizeOptions) {
|
|
|
615
614
|
return size;
|
|
616
615
|
}
|
|
617
616
|
|
|
618
|
-
function useLazyRef<T>(init: () => T): React.RefObject<T> {
|
|
619
|
-
const ref = React.useRef<T | null>(null);
|
|
620
|
-
if (ref.current === null) {
|
|
621
|
-
ref.current = init();
|
|
622
|
-
}
|
|
623
|
-
return ref as React.RefObject<T>;
|
|
624
|
-
}
|
|
625
617
|
|
|
626
618
|
type OnRafScheduleReturn<T extends unknown[]> = { (...args: T): void; cancel: () => void };
|
|
627
619
|
|
|
@@ -24,7 +24,7 @@ import { useSelection } from './selection';
|
|
|
24
24
|
import { useRename } from './rename';
|
|
25
25
|
import { useClipboard } from './clipboard';
|
|
26
26
|
import { useResolvedMenu, renderItemsAsContextMenu, tidyMenuItems } from './menu';
|
|
27
|
-
import { useDnd
|
|
27
|
+
import { useDnd } from './dnd';
|
|
28
28
|
import { usePersistSync } from './persist';
|
|
29
29
|
import type { TreeContextValue } from './TreeContextValue';
|
|
30
30
|
|
|
@@ -189,6 +189,11 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
|
|
|
189
189
|
cache,
|
|
190
190
|
filterNode,
|
|
191
191
|
}),
|
|
192
|
+
// `state.cacheTick` reads as unnecessary because `cache` is a stable
|
|
193
|
+
// ref-backed Map whose identity never changes when it mutates. It is the
|
|
194
|
+
// only invalidation signal: drop it and rows for async-loaded children are
|
|
195
|
+
// never flattened, so an expanded folder stays visually empty.
|
|
196
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
192
197
|
[data, state.expanded, state.cacheTick, cache, filterNode],
|
|
193
198
|
);
|
|
194
199
|
|
|
@@ -104,6 +104,11 @@ export function useAsyncChildren<T>({
|
|
|
104
104
|
};
|
|
105
105
|
walk(data);
|
|
106
106
|
return map;
|
|
107
|
+
// `cacheTick` reads as unnecessary because the walk reaches the cache
|
|
108
|
+
// through `cacheRef.current`, which the linter cannot track. It is the only
|
|
109
|
+
// invalidation signal the cache has: drop it and `nodeById` keeps the map
|
|
110
|
+
// built before the async load, so loaded children stay invisible.
|
|
111
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
112
|
}, [data, cacheTick]);
|
|
108
113
|
|
|
109
114
|
// Auto-fetch every expanded folder that isn't already in cache. Cheap
|
|
@@ -50,7 +50,6 @@ export function useRename<T>({
|
|
|
50
50
|
if (!enableInlineRename) return;
|
|
51
51
|
if (!adapter?.rename) {
|
|
52
52
|
if (process.env.NODE_ENV !== 'production') {
|
|
53
|
-
// eslint-disable-next-line no-console
|
|
54
53
|
console.warn(
|
|
55
54
|
'[Tree] startRename called but adapter.rename is not defined.',
|
|
56
55
|
);
|
package/src/Tree/data/dnd.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { TreeItemId, TreeMovePosition, TreeNode } from '../types';
|
|
4
4
|
|
|
5
5
|
// =====================================================================
|
|
6
6
|
// Drag-and-drop primitives — pure, framework-agnostic.
|
|
@@ -9,12 +9,12 @@ import type { FlatRow } from '../../types';
|
|
|
9
9
|
*
|
|
10
10
|
* Always selects (single-select semantics for keyboard input).
|
|
11
11
|
*/
|
|
12
|
-
export type ActivateOutcome
|
|
12
|
+
export type ActivateOutcome =
|
|
13
13
|
| { kind: 'toggle-folder'; id: string; willExpand: boolean }
|
|
14
14
|
| { kind: 'activate-leaf'; id: string }
|
|
15
15
|
| { kind: 'noop' };
|
|
16
16
|
|
|
17
|
-
export function resolveActivate<T>(current: FlatRow<T> | null): ActivateOutcome
|
|
17
|
+
export function resolveActivate<T>(current: FlatRow<T> | null): ActivateOutcome {
|
|
18
18
|
if (!current) return { kind: 'noop' };
|
|
19
19
|
if (current.isFolder) {
|
|
20
20
|
return {
|
|
@@ -9,7 +9,7 @@ import type { FlatRow } from '../../types';
|
|
|
9
9
|
* - expanded folder → jump to first visible child
|
|
10
10
|
* - leaf → no-op
|
|
11
11
|
*/
|
|
12
|
-
export type RightArrowOutcome
|
|
12
|
+
export type RightArrowOutcome =
|
|
13
13
|
| { kind: 'expand'; id: string }
|
|
14
14
|
| { kind: 'focus'; id: string }
|
|
15
15
|
| { kind: 'noop' };
|
|
@@ -18,7 +18,7 @@ export function resolveRightArrow<T>(
|
|
|
18
18
|
current: FlatRow<T> | null,
|
|
19
19
|
rows: readonly FlatRow<T>[],
|
|
20
20
|
idx: number,
|
|
21
|
-
): RightArrowOutcome
|
|
21
|
+
): RightArrowOutcome {
|
|
22
22
|
if (!current) return { kind: 'noop' };
|
|
23
23
|
if (current.isFolder && !current.isExpanded) {
|
|
24
24
|
return { kind: 'expand', id: current.node.id };
|
|
@@ -37,12 +37,12 @@ export function resolveRightArrow<T>(
|
|
|
37
37
|
* - leaf / collapsed w/ parent → focus parent
|
|
38
38
|
* - root leaf → no-op
|
|
39
39
|
*/
|
|
40
|
-
export type LeftArrowOutcome
|
|
40
|
+
export type LeftArrowOutcome =
|
|
41
41
|
| { kind: 'collapse'; id: string }
|
|
42
42
|
| { kind: 'focus'; id: string }
|
|
43
43
|
| { kind: 'noop' };
|
|
44
44
|
|
|
45
|
-
export function resolveLeftArrow<T>(current: FlatRow<T> | null): LeftArrowOutcome
|
|
45
|
+
export function resolveLeftArrow<T>(current: FlatRow<T> | null): LeftArrowOutcome {
|
|
46
46
|
if (!current) return { kind: 'noop' };
|
|
47
47
|
if (current.isFolder && current.isExpanded) {
|
|
48
48
|
return { kind: 'collapse', id: current.node.id };
|