@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,142 @@
|
|
|
1
|
+
# JsonTree
|
|
2
|
+
|
|
3
|
+
Interactive JSON viewer with collapsible nodes, search, copy-path, and a
|
|
4
|
+
hover-revealed toolbar (ChatGPT-style).
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
import { JsonTree } from '@djangocfg/ui-tools/json-tree';
|
|
8
|
+
|
|
9
|
+
<JsonTree data={anyJson} />
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
That covers 95% of cases. The remaining props live in `types.ts`.
|
|
13
|
+
|
|
14
|
+
## Provenance
|
|
15
|
+
|
|
16
|
+
Ported from [jalco/ui's `JsonViewer`](https://ui.justinlevine.me/docs/components/json-viewer)
|
|
17
|
+
(MIT, Justin Levine — full source vendored at
|
|
18
|
+
`packages/@sources/jalcoui-main/`). Modifications:
|
|
19
|
+
|
|
20
|
+
- Removed the Shiki theme module (65 editor palettes) — we keep one
|
|
21
|
+
Tailwind-token palette that follows the host light/dark theme via
|
|
22
|
+
`useResolvedTheme`. No `colorTheme` prop.
|
|
23
|
+
- New `toolbar` prop (`'auto' | 'always' | 'never'`). `'auto'` fades the
|
|
24
|
+
toolbar in on hover or keyboard focus inside the container (the
|
|
25
|
+
ChatGPT pattern). `'always'` is for debug panels; `'never'` strips it
|
|
26
|
+
entirely.
|
|
27
|
+
- New `actions` prop selects which buttons to mount. Default omits the
|
|
28
|
+
download button — file-viewer hosts shouldn't offer "Download" for
|
|
29
|
+
JSON that already lives on disk.
|
|
30
|
+
- `bordered={false}` strips the card frame for embedded use; the second
|
|
31
|
+
source of borders (`JsonContent` wrapper) is gone too.
|
|
32
|
+
- Toolbar buttons use ui-core `<Tooltip>` / `<Button>` / `<Input>` so
|
|
33
|
+
they pick up shared focus/hover styling.
|
|
34
|
+
|
|
35
|
+
The original `react-json-tree`-based wrapper is gone; consumers that
|
|
36
|
+
called the legacy `<JsonTree mode="full" config={…}>` shape go through
|
|
37
|
+
`LegacyJsonTree` (default export) which adapts old props to the new
|
|
38
|
+
shape — see `legacyAdapter.tsx`.
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
interface JsonTreeProps {
|
|
44
|
+
data: unknown;
|
|
45
|
+
title?: string;
|
|
46
|
+
rootName?: string; // default 'root'
|
|
47
|
+
toolbar?: 'auto' | 'always' | 'never'; // default 'auto'
|
|
48
|
+
actions?: ('search' | 'expand' | 'copy' | 'download')[];
|
|
49
|
+
// default ['search','expand','copy']
|
|
50
|
+
defaultExpandedDepth?: number | true; // default 2; true = expand all
|
|
51
|
+
bordered?: boolean; // default true
|
|
52
|
+
className?: string;
|
|
53
|
+
downloadFilename?: string; // default 'data.json'
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### When to override which
|
|
58
|
+
|
|
59
|
+
| Goal | Set |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| Embed inside a pane that already draws border + bg | `bordered={false}` |
|
|
62
|
+
| Debug-panel feel (toolbar always visible) | `toolbar="always"` |
|
|
63
|
+
| Strip the toolbar entirely | `toolbar="never"` |
|
|
64
|
+
| Add a Download button (in-memory data) | `actions={['search','expand','copy','download']}` |
|
|
65
|
+
| Read-only display, no Search | `actions={['expand','copy']}` |
|
|
66
|
+
| Long config — open everything | `defaultExpandedDepth={true}` |
|
|
67
|
+
|
|
68
|
+
## Theme
|
|
69
|
+
|
|
70
|
+
The data-typed palette is hand-tuned per light/dark rather than wired to
|
|
71
|
+
semantic UI tokens — same convention as Chrome DevTools, Insomnia, and
|
|
72
|
+
Bruno. JSON inspectors carry their own contrast model; flattening keys
|
|
73
|
+
and values into theme pastels makes them illegible.
|
|
74
|
+
|
|
75
|
+
The component is **transparent by default** — no internal background,
|
|
76
|
+
so it inherits whatever surface the host provides. Pass `bordered`
|
|
77
|
+
explicitly when you want the card frame.
|
|
78
|
+
|
|
79
|
+
## Search
|
|
80
|
+
|
|
81
|
+
Hidden behind the magnifying-glass icon in the toolbar; toggling it
|
|
82
|
+
expands the whole tree so matches inside collapsed nodes become
|
|
83
|
+
visible. Matches highlight in both keys and string values; numeric and
|
|
84
|
+
boolean values match as text.
|
|
85
|
+
|
|
86
|
+
## Copy-path
|
|
87
|
+
|
|
88
|
+
Hover any row — a `⊕` icon appears on the right. Clicking it copies
|
|
89
|
+
the full JS access path (`root.dependencies["@tiptap/core"]`, including
|
|
90
|
+
bracket syntax for keys that aren't valid identifiers) to the clipboard.
|
|
91
|
+
|
|
92
|
+
## Legacy props (back-compat)
|
|
93
|
+
|
|
94
|
+
The default export — `LegacyJsonTree` — accepts the pre-refactor shape
|
|
95
|
+
so existing call sites compile unchanged:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
// Old code still works
|
|
99
|
+
import JsonTree from '@djangocfg/ui-tools/json-tree';
|
|
100
|
+
<JsonTree
|
|
101
|
+
data={obj}
|
|
102
|
+
mode="full"
|
|
103
|
+
config={{
|
|
104
|
+
maxAutoExpandDepth: 3,
|
|
105
|
+
showActionButtons: true,
|
|
106
|
+
autoHideToolbar: true,
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`legacyAdapter.ts` maps:
|
|
112
|
+
|
|
113
|
+
- `mode` → `bordered` + `defaultExpandedDepth` defaults
|
|
114
|
+
- `config.maxAutoExpandDepth` → `defaultExpandedDepth`
|
|
115
|
+
- `config.showExpandControls / showCopy / showDownload / showActionButtons`
|
|
116
|
+
→ `actions` array
|
|
117
|
+
- `config.autoHideToolbar` → `toolbar: 'auto'` (vs `'always'`)
|
|
118
|
+
- `jsonTreeProps` → silently dropped (react-json-tree-specific)
|
|
119
|
+
|
|
120
|
+
Migrate to the new API at your own pace — new code should target
|
|
121
|
+
`{ JsonTree }` from the named export.
|
|
122
|
+
|
|
123
|
+
## Lazy entries
|
|
124
|
+
|
|
125
|
+
`@djangocfg/ui-tools/json-tree` ships two lazy wrappers:
|
|
126
|
+
|
|
127
|
+
- `LazyJsonTree` — legacy shape, default export. Old code keeps working.
|
|
128
|
+
- `LazyJsonTreeNew` — new shape, for greenfield code.
|
|
129
|
+
|
|
130
|
+
Both wrap the renderer in `React.lazy` (~80KB chunk including the
|
|
131
|
+
toolbar UI and Tailwind utility surface).
|
|
132
|
+
|
|
133
|
+
## Related
|
|
134
|
+
|
|
135
|
+
- `@djangocfg/ui-tools/json-editor` — when you need to **edit** JSON
|
|
136
|
+
(drag-reorder, JSON Schema validation, undo/redo). Lazy-loads
|
|
137
|
+
`json-edit-react` (~19KB gz) on demand. Use for debug Store panels
|
|
138
|
+
and config admin UIs; for read-only display always reach for
|
|
139
|
+
`json-tree` first.
|
|
140
|
+
- `@djangocfg/ui-tools/pretty-code` — read-only syntax-highlighted code
|
|
141
|
+
blocks (any language). Use when you have a JSON **string** to display
|
|
142
|
+
unmodified.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/** Public surface for `@djangocfg/widget-data/json-tree`. */
|
|
4
|
+
|
|
5
|
+
export { JsonTree, JsonTree as default } from './JsonViewer';
|
|
6
|
+
export type { JsonTreeProps, JsonValue, JsonAction, JsonSize } from './types';
|
|
7
|
+
|
|
8
|
+
// In the retired ui-tools the `./json-tree` subpath resolved to `lazy.tsx` and this barrel
|
|
9
|
+
// was unreachable — the inverse of the other seven tools, whose subpath
|
|
10
|
+
// resolved here and left their `Lazy*` dead. One barrel now carries both.
|
|
11
|
+
export { LazyJsonTree } from './lazy';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `@djangocfg/widget-data/json-tree` subpath entrypoint.
|
|
5
|
+
*
|
|
6
|
+
* Single lazy-loaded export (`LazyJsonTree`) wrapping the renderer
|
|
7
|
+
* (~80KB chunk including the toolbar UI). Suspense fallback ships from
|
|
8
|
+
* the shared `LoadingFallback`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createLazyComponent, LoadingFallback } from '@djangocfg/widget-kit/lazy';
|
|
12
|
+
import type { JsonTreeProps } from './types';
|
|
13
|
+
|
|
14
|
+
export type { JsonTreeProps, JsonValue, JsonAction, JsonSize } from './types';
|
|
15
|
+
|
|
16
|
+
export const LazyJsonTree = createLazyComponent<JsonTreeProps>(
|
|
17
|
+
() => import('./JsonViewer').then((m) => ({ default: m.JsonTree })),
|
|
18
|
+
{
|
|
19
|
+
displayName: 'LazyJsonTree',
|
|
20
|
+
fallback: <LoadingFallback minHeight={64} text="Loading JSON viewer…" />,
|
|
21
|
+
},
|
|
22
|
+
);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the new JsonTree.
|
|
3
|
+
*
|
|
4
|
+
* The shape is intentionally narrow — five props cover ~95% of cases.
|
|
5
|
+
* Old shape (`mode`, `config`, `jsonTreeProps`, `showActionButtons`, …)
|
|
6
|
+
* is bridged in `legacyAdapter.ts` so existing consumers compile
|
|
7
|
+
* unchanged. New code should target this surface.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type JsonValue =
|
|
11
|
+
| string
|
|
12
|
+
| number
|
|
13
|
+
| boolean
|
|
14
|
+
| null
|
|
15
|
+
| JsonValue[]
|
|
16
|
+
| { [key: string]: JsonValue };
|
|
17
|
+
|
|
18
|
+
/** Which toolbar buttons to expose. Default `['search','expand','copy']`. */
|
|
19
|
+
export type JsonAction = 'search' | 'expand' | 'copy' | 'download';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Font size for the tree body. Toolbar metrics stay constant.
|
|
23
|
+
* - `'sm'` (default) — `text-xs`, dense for chat/debug
|
|
24
|
+
* - `'md'` — `text-sm`, reads like Monaco at normal zoom
|
|
25
|
+
* - `'lg'` — `text-base`, presentation / large screen
|
|
26
|
+
*/
|
|
27
|
+
export type JsonSize = 'sm' | 'md' | 'lg';
|
|
28
|
+
|
|
29
|
+
export interface JsonTreeProps {
|
|
30
|
+
/** Any JSON-serializable value. */
|
|
31
|
+
data: unknown;
|
|
32
|
+
|
|
33
|
+
/** Optional heading label rendered in the toolbar. */
|
|
34
|
+
title?: string;
|
|
35
|
+
|
|
36
|
+
/** Label for the root node in the tree. Default `'root'`. */
|
|
37
|
+
rootName?: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How the toolbar behaves:
|
|
41
|
+
* - `'auto'` — fades in on hover / keyboard focus (ChatGPT-style)
|
|
42
|
+
* - `'always'` — visible from the start (good for debug panels)
|
|
43
|
+
* - `'never'` — no toolbar at all
|
|
44
|
+
* Default `'auto'`.
|
|
45
|
+
*/
|
|
46
|
+
toolbar?: 'auto' | 'always' | 'never';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Which action buttons to render in the toolbar. Default
|
|
50
|
+
* `['search','expand','copy']`. Add `'download'` for in-memory data
|
|
51
|
+
* that should be exportable; omit for on-disk file viewers.
|
|
52
|
+
*/
|
|
53
|
+
actions?: readonly JsonAction[];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Auto-expand depth on mount. `true` = expand everything.
|
|
57
|
+
* Default `2` — root + first level open, deeper stays collapsed.
|
|
58
|
+
*/
|
|
59
|
+
defaultExpandedDepth?: number | true;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Font size for the tree body. Default `'sm'`. Use `'md'` for
|
|
63
|
+
* full-pane previews (text-viewer.tsx) so it reads at the same
|
|
64
|
+
* weight as Monaco; `'lg'` for talks / demos.
|
|
65
|
+
*/
|
|
66
|
+
size?: JsonSize;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Draw the card frame (border + rounded corners). Default `true`.
|
|
70
|
+
* Pass `false` to render flush inside a host pane that already owns
|
|
71
|
+
* the chrome (cmdop preview, OpenAPI response panel).
|
|
72
|
+
*/
|
|
73
|
+
bordered?: boolean;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Render the toolbar with reduced padding, smaller icons, and a hidden
|
|
77
|
+
* "X keys" badge. Use for embedded contexts where the JSON body is
|
|
78
|
+
* tiny (e.g. error responses with 2 keys) and the default toolbar
|
|
79
|
+
* dwarfs the content. Default `false`.
|
|
80
|
+
*/
|
|
81
|
+
compactHeader?: boolean;
|
|
82
|
+
|
|
83
|
+
/** Extra class on the root container. */
|
|
84
|
+
className?: string;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Filename used for the optional Download action. Default `'data.json'`.
|
|
88
|
+
* Ignored when `actions` doesn't include `'download'`.
|
|
89
|
+
*/
|
|
90
|
+
downloadFilename?: string;
|
|
91
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useCallback,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
KanbanCard,
|
|
13
|
+
KanbanCardId,
|
|
14
|
+
KanbanColumn,
|
|
15
|
+
KanbanColumnId,
|
|
16
|
+
KanbanDragState,
|
|
17
|
+
KanbanProps,
|
|
18
|
+
} from '../types';
|
|
19
|
+
|
|
20
|
+
// =====================================================================
|
|
21
|
+
// Context value
|
|
22
|
+
// =====================================================================
|
|
23
|
+
|
|
24
|
+
export interface KanbanContextValue {
|
|
25
|
+
columns: KanbanColumn[];
|
|
26
|
+
dragState: KanbanDragState;
|
|
27
|
+
setDragState: React.Dispatch<React.SetStateAction<KanbanDragState>>;
|
|
28
|
+
moveCard: (cardId: KanbanCardId, fromColumnId: KanbanColumnId, toColumnId: KanbanColumnId, toIndex: number) => void;
|
|
29
|
+
reorderCard: (columnId: KanbanColumnId, cardId: KanbanCardId, toIndex: number) => void;
|
|
30
|
+
renderCard?: (card: KanbanCard, columnId: KanbanColumnId) => React.ReactNode;
|
|
31
|
+
renderColumnHeader?: (column: KanbanColumn) => React.ReactNode;
|
|
32
|
+
loading: boolean;
|
|
33
|
+
emptyColumnMessage: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const KanbanContext = createContext<KanbanContextValue | null>(null);
|
|
37
|
+
|
|
38
|
+
export function useKanbanContext(): KanbanContextValue {
|
|
39
|
+
const ctx = React.useContext(KanbanContext);
|
|
40
|
+
if (!ctx) {
|
|
41
|
+
throw new Error('useKanbanContext must be used inside <KanbanProvider>');
|
|
42
|
+
}
|
|
43
|
+
return ctx;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// =====================================================================
|
|
47
|
+
// Provider
|
|
48
|
+
// =====================================================================
|
|
49
|
+
|
|
50
|
+
export interface KanbanProviderProps
|
|
51
|
+
extends Pick<
|
|
52
|
+
KanbanProps,
|
|
53
|
+
| 'columns'
|
|
54
|
+
| 'onMoveCard'
|
|
55
|
+
| 'onReorderCard'
|
|
56
|
+
| 'renderCard'
|
|
57
|
+
| 'renderColumnHeader'
|
|
58
|
+
| 'loading'
|
|
59
|
+
| 'emptyColumnMessage'
|
|
60
|
+
> {
|
|
61
|
+
children: React.ReactNode;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function KanbanProvider(props: KanbanProviderProps) {
|
|
65
|
+
const {
|
|
66
|
+
columns: initialColumns,
|
|
67
|
+
onMoveCard,
|
|
68
|
+
onReorderCard,
|
|
69
|
+
renderCard,
|
|
70
|
+
renderColumnHeader,
|
|
71
|
+
loading = false,
|
|
72
|
+
emptyColumnMessage = 'No cards',
|
|
73
|
+
children,
|
|
74
|
+
} = props;
|
|
75
|
+
|
|
76
|
+
const [columns, setColumns] = useState<KanbanColumn[]>(initialColumns);
|
|
77
|
+
const [dragState, setDragState] = useState<KanbanDragState>({
|
|
78
|
+
activeCardId: null,
|
|
79
|
+
activeColumnId: null,
|
|
80
|
+
overColumnId: null,
|
|
81
|
+
overCardId: null,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Sync with external columns prop
|
|
85
|
+
React.useEffect(() => {
|
|
86
|
+
setColumns(initialColumns);
|
|
87
|
+
}, [initialColumns]);
|
|
88
|
+
|
|
89
|
+
const moveCard = useCallback(
|
|
90
|
+
(cardId: KanbanCardId, fromColumnId: KanbanColumnId, toColumnId: KanbanColumnId, toIndex: number) => {
|
|
91
|
+
setColumns((prev) => {
|
|
92
|
+
const next = prev.map((col) => ({ ...col, cards: [...col.cards] }));
|
|
93
|
+
const fromCol = next.find((c) => c.id === fromColumnId);
|
|
94
|
+
const toCol = next.find((c) => c.id === toColumnId);
|
|
95
|
+
if (!fromCol || !toCol) return prev;
|
|
96
|
+
|
|
97
|
+
const cardIndex = fromCol.cards.findIndex((c) => c.id === cardId);
|
|
98
|
+
if (cardIndex === -1) return prev;
|
|
99
|
+
|
|
100
|
+
const [card] = fromCol.cards.splice(cardIndex, 1);
|
|
101
|
+
if (!card) return prev;
|
|
102
|
+
const insertIndex = Math.min(toIndex, toCol.cards.length);
|
|
103
|
+
toCol.cards.splice(insertIndex, 0, card);
|
|
104
|
+
|
|
105
|
+
return next;
|
|
106
|
+
});
|
|
107
|
+
onMoveCard?.(cardId, fromColumnId, toColumnId, toIndex);
|
|
108
|
+
},
|
|
109
|
+
[onMoveCard],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const reorderCard = useCallback(
|
|
113
|
+
(columnId: KanbanColumnId, cardId: KanbanCardId, toIndex: number) => {
|
|
114
|
+
setColumns((prev) => {
|
|
115
|
+
const next = prev.map((col) =>
|
|
116
|
+
col.id === columnId ? { ...col, cards: [...col.cards] } : col,
|
|
117
|
+
);
|
|
118
|
+
const col = next.find((c) => c.id === columnId);
|
|
119
|
+
if (!col) return prev;
|
|
120
|
+
|
|
121
|
+
const fromIndex = col.cards.findIndex((c) => c.id === cardId);
|
|
122
|
+
if (fromIndex === -1 || fromIndex === toIndex) return prev;
|
|
123
|
+
|
|
124
|
+
const [card] = col.cards.splice(fromIndex, 1);
|
|
125
|
+
if (!card) return prev;
|
|
126
|
+
const insertIndex = Math.min(toIndex, col.cards.length);
|
|
127
|
+
col.cards.splice(insertIndex, 0, card);
|
|
128
|
+
|
|
129
|
+
return next;
|
|
130
|
+
});
|
|
131
|
+
onReorderCard?.(columnId, cardId, toIndex);
|
|
132
|
+
},
|
|
133
|
+
[onReorderCard],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const value = useMemo<KanbanContextValue>(
|
|
137
|
+
() => ({
|
|
138
|
+
columns,
|
|
139
|
+
dragState,
|
|
140
|
+
setDragState,
|
|
141
|
+
moveCard,
|
|
142
|
+
reorderCard,
|
|
143
|
+
renderCard,
|
|
144
|
+
renderColumnHeader,
|
|
145
|
+
loading,
|
|
146
|
+
emptyColumnMessage,
|
|
147
|
+
}),
|
|
148
|
+
[
|
|
149
|
+
columns,
|
|
150
|
+
dragState,
|
|
151
|
+
moveCard,
|
|
152
|
+
reorderCard,
|
|
153
|
+
renderCard,
|
|
154
|
+
renderColumnHeader,
|
|
155
|
+
loading,
|
|
156
|
+
emptyColumnMessage,
|
|
157
|
+
],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<KanbanContext.Provider value={value}>
|
|
162
|
+
{children}
|
|
163
|
+
</KanbanContext.Provider>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
KanbanProvider,
|
|
5
|
+
useKanbanContext,
|
|
6
|
+
} from './context/KanbanContext';
|
|
7
|
+
export type {
|
|
8
|
+
KanbanContextValue,
|
|
9
|
+
KanbanProviderProps,
|
|
10
|
+
} from './context/KanbanContext';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
Kanban,
|
|
14
|
+
KanbanColumn,
|
|
15
|
+
KanbanCard,
|
|
16
|
+
KanbanDragOverlay,
|
|
17
|
+
} from './parts';
|
|
18
|
+
|
|
19
|
+
export type {
|
|
20
|
+
KanbanProps,
|
|
21
|
+
KanbanCard as KanbanCardType,
|
|
22
|
+
KanbanColumn as KanbanColumnType,
|
|
23
|
+
KanbanDragState,
|
|
24
|
+
KanbanCardId,
|
|
25
|
+
KanbanColumnId,
|
|
26
|
+
} from './types';
|
|
27
|
+
|
|
28
|
+
// The dynamic-import wrapper. Exported here because the subpath resolves to
|
|
29
|
+
// this barrel: in the retired ui-tools `lazy.tsx` was reachable from no declared export,
|
|
30
|
+
// so `LazyKanban` was dead code for every tool but `JsonTree`.
|
|
31
|
+
export { LazyKanban } from './lazy';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createLazyComponent, LoadingFallback } from '@djangocfg/widget-kit/lazy';
|
|
4
|
+
import type { KanbanProps } from './types';
|
|
5
|
+
|
|
6
|
+
export const LazyKanban = createLazyComponent<KanbanProps>(
|
|
7
|
+
() => import('./parts/Kanban').then((m) => ({ default: m.Kanban })),
|
|
8
|
+
{
|
|
9
|
+
displayName: 'LazyKanban',
|
|
10
|
+
fallback: <LoadingFallback minHeight={400} text="Loading kanban board…" />,
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// Re-export light surface synchronously
|
|
15
|
+
export * from './types';
|
|
16
|
+
export {
|
|
17
|
+
KanbanProvider,
|
|
18
|
+
useKanbanContext,
|
|
19
|
+
} from './context/KanbanContext';
|
|
20
|
+
export type {
|
|
21
|
+
KanbanContextValue,
|
|
22
|
+
KanbanProviderProps,
|
|
23
|
+
} from './context/KanbanContext';
|
|
@@ -0,0 +1,77 @@
|
|
|
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 { KanbanProps } from '../types';
|
|
8
|
+
import { KanbanProvider } from '../context/KanbanContext';
|
|
9
|
+
import { KanbanColumn } from './KanbanColumn';
|
|
10
|
+
|
|
11
|
+
export function Kanban(props: KanbanProps) {
|
|
12
|
+
const {
|
|
13
|
+
columns,
|
|
14
|
+
onMoveCard,
|
|
15
|
+
onReorderCard,
|
|
16
|
+
renderCard,
|
|
17
|
+
renderColumnHeader,
|
|
18
|
+
loading,
|
|
19
|
+
emptyColumnMessage,
|
|
20
|
+
className,
|
|
21
|
+
style,
|
|
22
|
+
} = props;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<KanbanProvider
|
|
26
|
+
columns={columns}
|
|
27
|
+
onMoveCard={onMoveCard}
|
|
28
|
+
onReorderCard={onReorderCard}
|
|
29
|
+
renderCard={renderCard}
|
|
30
|
+
renderColumnHeader={renderColumnHeader}
|
|
31
|
+
loading={loading}
|
|
32
|
+
emptyColumnMessage={emptyColumnMessage}
|
|
33
|
+
>
|
|
34
|
+
<KanbanShell className={className} style={style} />
|
|
35
|
+
</KanbanProvider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
Kanban.displayName = 'Kanban';
|
|
39
|
+
|
|
40
|
+
function KanbanShell({ className, style }: { className?: string; style?: React.CSSProperties }) {
|
|
41
|
+
const { columns, loading } = useKanbanContext();
|
|
42
|
+
|
|
43
|
+
if (loading) {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
data-slot="kanban"
|
|
47
|
+
className={cn(
|
|
48
|
+
'flex h-full items-center justify-center rounded-sm border border-border bg-background',
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
style={style}
|
|
52
|
+
>
|
|
53
|
+
<span className="text-muted-foreground">Loading…</span>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<ScrollArea className="w-full" orientation="horizontal">
|
|
60
|
+
<div
|
|
61
|
+
data-slot="kanban"
|
|
62
|
+
className={cn(
|
|
63
|
+
'flex h-full min-h-[400px] gap-4 rounded-sm border border-border bg-background p-4',
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
style={style}
|
|
67
|
+
>
|
|
68
|
+
{columns.map((column) => (
|
|
69
|
+
<KanbanColumn key={column.id} column={column} />
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
</ScrollArea>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Ambient import for JSX usage
|
|
77
|
+
import { useKanbanContext } from '../context/KanbanContext';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
import type { KanbanCard as KanbanCardType, KanbanColumnId } from '../types';
|
|
7
|
+
import { useKanbanContext } from '../context/KanbanContext';
|
|
8
|
+
|
|
9
|
+
interface KanbanCardProps {
|
|
10
|
+
card: KanbanCardType;
|
|
11
|
+
columnId: KanbanColumnId;
|
|
12
|
+
index: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function KanbanCard({ card, columnId, index }: KanbanCardProps) {
|
|
16
|
+
const { renderCard, dragState, setDragState, moveCard, reorderCard } = useKanbanContext();
|
|
17
|
+
const isActive = dragState.activeCardId === card.id;
|
|
18
|
+
const isOver = dragState.overCardId === card.id && dragState.activeCardId !== card.id;
|
|
19
|
+
|
|
20
|
+
const handlePointerDown = React.useCallback(
|
|
21
|
+
(e: React.PointerEvent) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setDragState({
|
|
24
|
+
activeCardId: card.id,
|
|
25
|
+
activeColumnId: columnId,
|
|
26
|
+
overColumnId: null,
|
|
27
|
+
overCardId: null,
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
[card.id, columnId, setDragState],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const handlePointerEnter = React.useCallback(() => {
|
|
34
|
+
if (!dragState.activeCardId || dragState.activeCardId === card.id) return;
|
|
35
|
+
|
|
36
|
+
setDragState((prev) => ({ ...prev, overCardId: card.id, overColumnId: columnId }));
|
|
37
|
+
|
|
38
|
+
if (dragState.activeColumnId === columnId) {
|
|
39
|
+
reorderCard(columnId, dragState.activeCardId, index);
|
|
40
|
+
} else if (dragState.activeColumnId) {
|
|
41
|
+
moveCard(dragState.activeCardId, dragState.activeColumnId, columnId, index);
|
|
42
|
+
setDragState((prev) => ({ ...prev, activeColumnId: columnId }));
|
|
43
|
+
}
|
|
44
|
+
}, [card.id, columnId, index, dragState, setDragState, reorderCard, moveCard]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
data-slot="kanban-card"
|
|
49
|
+
data-id={card.id}
|
|
50
|
+
data-column-id={columnId}
|
|
51
|
+
data-active={isActive ? '' : undefined}
|
|
52
|
+
data-over={isOver ? '' : undefined}
|
|
53
|
+
onPointerDown={handlePointerDown}
|
|
54
|
+
onPointerEnter={handlePointerEnter}
|
|
55
|
+
className={cn(
|
|
56
|
+
'cursor-grab select-none rounded-sm border border-border bg-background p-3 shadow-sm',
|
|
57
|
+
'transition-all',
|
|
58
|
+
'hover:border-primary/50',
|
|
59
|
+
isActive && 'z-50 scale-[1.02] opacity-80 shadow-lg',
|
|
60
|
+
isOver && 'border-dashed border-primary'
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{renderCard ? (
|
|
64
|
+
renderCard(card, columnId)
|
|
65
|
+
) : (
|
|
66
|
+
<>
|
|
67
|
+
<h4 className="text-sm font-medium text-foreground">{card.title}</h4>
|
|
68
|
+
{card.description && (
|
|
69
|
+
<p className="mt-1 text-xs text-muted-foreground">{card.description}</p>
|
|
70
|
+
)}
|
|
71
|
+
</>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
KanbanCard.displayName = 'KanbanCard';
|