@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,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,4 @@
1
+ 'use client';
2
+
3
+ export { KanbanProvider, useKanbanContext } from './KanbanContext';
4
+ export type { KanbanContextValue, KanbanProviderProps } from './KanbanContext';
@@ -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';