@pixpilot/shadcn-kanban 0.0.0
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 +46 -0
- package/dist/AddColumnButton.cjs +82 -0
- package/dist/AddColumnButton.js +77 -0
- package/dist/ColumnFilterButton.cjs +68 -0
- package/dist/ColumnFilterButton.js +63 -0
- package/dist/KanbanBoard.cjs +137 -0
- package/dist/KanbanBoard.d.cts +36 -0
- package/dist/KanbanBoard.d.ts +36 -0
- package/dist/KanbanBoard.js +131 -0
- package/dist/KanbanColumn.cjs +142 -0
- package/dist/KanbanColumn.js +134 -0
- package/dist/KanbanColumnCards.cjs +25 -0
- package/dist/KanbanColumnCards.js +23 -0
- package/dist/KanbanDragOverlay.cjs +42 -0
- package/dist/KanbanDragOverlay.js +37 -0
- package/dist/KanbanItem.cjs +58 -0
- package/dist/KanbanItem.js +52 -0
- package/dist/KanbanVirtualColumnCards.cjs +70 -0
- package/dist/KanbanVirtualColumnCards.js +66 -0
- package/dist/_virtual/rolldown_runtime.cjs +25 -0
- package/dist/hooks/use-kanban-board-state.cjs +43 -0
- package/dist/hooks/use-kanban-board-state.js +41 -0
- package/dist/hooks/use-kanban-card-drag.cjs +83 -0
- package/dist/hooks/use-kanban-card-drag.js +81 -0
- package/dist/hooks/use-kanban-collision-detection.cjs +70 -0
- package/dist/hooks/use-kanban-collision-detection.js +67 -0
- package/dist/hooks/use-kanban-column-reorder.cjs +31 -0
- package/dist/hooks/use-kanban-column-reorder.js +28 -0
- package/dist/hooks/use-kanban-drag.cjs +98 -0
- package/dist/hooks/use-kanban-drag.js +95 -0
- package/dist/hooks/use-kanban-filters.cjs +49 -0
- package/dist/hooks/use-kanban-filters.js +47 -0
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/infinite-scroll/InfiniteScrollSentinel.cjs +54 -0
- package/dist/infinite-scroll/InfiniteScrollSentinel.d.cts +2 -0
- package/dist/infinite-scroll/InfiniteScrollSentinel.d.ts +2 -0
- package/dist/infinite-scroll/InfiniteScrollSentinel.js +49 -0
- package/dist/infinite-scroll/index.d.cts +3 -0
- package/dist/infinite-scroll/index.d.ts +3 -0
- package/dist/infinite-scroll/types.d.cts +29 -0
- package/dist/infinite-scroll/types.d.ts +29 -0
- package/dist/infinite-scroll/use-intersection-observer.cjs +46 -0
- package/dist/infinite-scroll/use-intersection-observer.d.cts +1 -0
- package/dist/infinite-scroll/use-intersection-observer.d.ts +1 -0
- package/dist/infinite-scroll/use-intersection-observer.js +44 -0
- package/dist/types.d.cts +251 -0
- package/dist/types.d.ts +251 -0
- package/dist/utils/apply-kanban-drop.cjs +37 -0
- package/dist/utils/apply-kanban-drop.js +35 -0
- package/dist/utils/column-sortable-id.cjs +23 -0
- package/dist/utils/column-sortable-id.js +20 -0
- package/dist/utils/is-below-over-item.cjs +17 -0
- package/dist/utils/is-below-over-item.js +16 -0
- package/dist/utils/items-order-equal.cjs +13 -0
- package/dist/utils/items-order-equal.js +12 -0
- package/dist/utils/kanban-keyboard-coordinates.cjs +64 -0
- package/dist/utils/kanban-keyboard-coordinates.js +62 -0
- package/dist/utils/move-kanban-item.cjs +28 -0
- package/dist/utils/move-kanban-item.js +27 -0
- package/dist/utils/replace-state.cjs +15 -0
- package/dist/utils/replace-state.js +14 -0
- package/dist/utils/resolve-column-id.cjs +25 -0
- package/dist/utils/resolve-column-id.js +24 -0
- package/package.json +97 -0
package/dist/types.d.cts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { IntersectionOptions } from "./infinite-scroll/types.cjs";
|
|
2
|
+
import "./infinite-scroll/index.cjs";
|
|
3
|
+
import { CSSProperties, HTMLAttributes, ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/types.d.ts
|
|
6
|
+
/** A single item that lives inside a Kanban column. */
|
|
7
|
+
interface KanbanItem<T = Record<string, unknown>> {
|
|
8
|
+
/** Unique identifier for the item. */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Display name (used by the default renderer). */
|
|
11
|
+
name: string;
|
|
12
|
+
/** The column id this item currently belongs to. */
|
|
13
|
+
columnId: string;
|
|
14
|
+
/** Arbitrary extra data attached to the item. */
|
|
15
|
+
data?: T;
|
|
16
|
+
}
|
|
17
|
+
/** Definition of a single Kanban column. */
|
|
18
|
+
interface KanbanColumn {
|
|
19
|
+
/** Unique identifier for the column. */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Display title rendered at the top of the column. */
|
|
22
|
+
title: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A filter that can be applied to the items of a column.
|
|
26
|
+
*
|
|
27
|
+
* Filters are passed to the board as props and surfaced behind a filter
|
|
28
|
+
* button in each column header. They are intentionally generic: provide a
|
|
29
|
+
* {@link KanbanFilter.predicate} to let the board hide non-matching cards
|
|
30
|
+
* itself, or omit it and react to {@link KanbanBoardProps.onFilterChange}
|
|
31
|
+
* to perform the filtering yourself (e.g. server-side).
|
|
32
|
+
*/
|
|
33
|
+
interface KanbanFilter<T = Record<string, unknown>> {
|
|
34
|
+
/** Unique identifier for the filter (unique within a column's filter set). */
|
|
35
|
+
id: string;
|
|
36
|
+
/** Human-readable label shown in the filter menu. */
|
|
37
|
+
label: string;
|
|
38
|
+
/**
|
|
39
|
+
* Optional predicate deciding whether an item stays visible while this
|
|
40
|
+
* filter is active. Return `true` to keep the item.
|
|
41
|
+
*
|
|
42
|
+
* When provided, the board filters the column's cards in place. When
|
|
43
|
+
* omitted, the board only tracks the active state and fires
|
|
44
|
+
* {@link KanbanBoardProps.onFilterChange} so the parent can filter itself.
|
|
45
|
+
*/
|
|
46
|
+
predicate?: (item: KanbanItem<T>, column: KanbanColumn) => boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Either a flat list of filters shared by every column, or a resolver that
|
|
50
|
+
* returns the filters for a given column (return `undefined`/empty for
|
|
51
|
+
* columns that should not show a filter button).
|
|
52
|
+
*/
|
|
53
|
+
type KanbanFilters<T = Record<string, unknown>> = KanbanFilter<T>[] | ((column: KanbanColumn) => KanbanFilter<T>[] | undefined);
|
|
54
|
+
/** Controls how columns behave when their cards exceed the board height. */
|
|
55
|
+
type ColumnOverflow = 'scroll' | 'expand';
|
|
56
|
+
/**
|
|
57
|
+
* Payload delivered by {@link KanbanBoardProps.onFilterChange} whenever the
|
|
58
|
+
* set of active filters for a column changes.
|
|
59
|
+
*/
|
|
60
|
+
interface KanbanFilterChangeEvent<T = Record<string, unknown>> {
|
|
61
|
+
/** The column whose active filters changed. */
|
|
62
|
+
column: KanbanColumn;
|
|
63
|
+
/** Ids of the filters currently active on the column. */
|
|
64
|
+
activeFilterIds: string[];
|
|
65
|
+
/** The filter objects currently active on the column. */
|
|
66
|
+
activeFilters: KanbanFilter<T>[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Per-column infinite scrolling. A sentinel is rendered after the last card of
|
|
70
|
+
* every column; when it scrolls into view the board calls {@link onLoadMore}
|
|
71
|
+
* for that column.
|
|
72
|
+
*
|
|
73
|
+
* `hasMore` and `isLoading` are resolved per column rather than passed as flat
|
|
74
|
+
* booleans so a consumer that pages each column from its own request can drive
|
|
75
|
+
* them independently.
|
|
76
|
+
*
|
|
77
|
+
* Only supported when {@link KanbanBoardProps.columnOverflow} is `'scroll'` —
|
|
78
|
+
* an expanding column has no scroll edge to observe.
|
|
79
|
+
*/
|
|
80
|
+
interface KanbanInfiniteScroll extends IntersectionOptions {
|
|
81
|
+
/** Load the next page of items for `column`. */
|
|
82
|
+
onLoadMore: (column: KanbanColumn) => void;
|
|
83
|
+
/** Whether `column` still has items left to load. */
|
|
84
|
+
hasMore: (column: KanbanColumn) => boolean;
|
|
85
|
+
/** Whether a load is currently in flight for `column`. */
|
|
86
|
+
isLoading?: (column: KanbanColumn) => boolean;
|
|
87
|
+
/** Rendered inside a column while it is loading. */
|
|
88
|
+
loadingComponent?: ReactNode;
|
|
89
|
+
/** Rendered at the bottom of a column once it is fully loaded. */
|
|
90
|
+
endMessage?: ReactNode;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Per-column virtualization, powered by `@tanstack/react-virtual`.
|
|
94
|
+
*
|
|
95
|
+
* Only the cards near a column's visible window stay mounted, which is what
|
|
96
|
+
* keeps a thousand-card column usable: dnd-kit re-measures every mounted
|
|
97
|
+
* droppable on each drag frame, so the mounted count — not the item count — is
|
|
98
|
+
* what a drag actually costs.
|
|
99
|
+
*
|
|
100
|
+
* The card being dragged is always kept mounted, even once it scrolls out of
|
|
101
|
+
* the window. Unmounting it mid-gesture would tear down the node dnd-kit holds
|
|
102
|
+
* and strand the drag.
|
|
103
|
+
*
|
|
104
|
+
* Only supported when {@link KanbanBoardProps.columnOverflow} is `'scroll'` —
|
|
105
|
+
* an expanding column has no window to virtualize against.
|
|
106
|
+
*/
|
|
107
|
+
interface KanbanVirtualization {
|
|
108
|
+
/**
|
|
109
|
+
* Height in px assumed for a card that has not been measured yet. Cards are
|
|
110
|
+
* measured as soon as they mount, so this only has to be in the right
|
|
111
|
+
* ballpark; a poor guess just makes the scrollbar settle as you scroll.
|
|
112
|
+
*
|
|
113
|
+
* @default 96
|
|
114
|
+
*/
|
|
115
|
+
estimateItemHeight?: number;
|
|
116
|
+
/**
|
|
117
|
+
* How many extra cards to mount above and below the visible window. Raise it
|
|
118
|
+
* if fast scrolling shows blank gaps — but every extra card is one more
|
|
119
|
+
* droppable dnd-kit measures per drag frame.
|
|
120
|
+
*
|
|
121
|
+
* @default 6
|
|
122
|
+
*/
|
|
123
|
+
overscan?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Vertical gap in px between cards. Virtualized cards are positioned
|
|
126
|
+
* absolutely and cannot inherit the list's flex `gap`, so the virtualizer
|
|
127
|
+
* adds this to every offset instead. Keep it in sync with any custom card
|
|
128
|
+
* spacing.
|
|
129
|
+
*
|
|
130
|
+
* @default 8
|
|
131
|
+
*/
|
|
132
|
+
gap?: number;
|
|
133
|
+
/** Renders every card again while `true`. */
|
|
134
|
+
disabled?: boolean;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Payload delivered by {@link KanbanBoardProps.onChange} after
|
|
138
|
+
* a drag-and-drop operation completes.
|
|
139
|
+
*/
|
|
140
|
+
interface KanbanChangeEvent<T = Record<string, unknown>> {
|
|
141
|
+
/** The item that was moved. */
|
|
142
|
+
item: KanbanItem<T>;
|
|
143
|
+
/** Column id the item was in before the move. */
|
|
144
|
+
previousColumnId: string;
|
|
145
|
+
/** Column id the item is in after the move. */
|
|
146
|
+
nextColumnId: string;
|
|
147
|
+
/**
|
|
148
|
+
* The full items array after the move, with correct ordering.
|
|
149
|
+
* Use this to update your external state so positions are preserved.
|
|
150
|
+
*/
|
|
151
|
+
items: KanbanItem<T>[];
|
|
152
|
+
}
|
|
153
|
+
/** Props for the main `<KanbanBoard />` component. */
|
|
154
|
+
interface KanbanBoardProps<T = Record<string, unknown>> {
|
|
155
|
+
/** All items across every column. */
|
|
156
|
+
items: KanbanItem<T>[];
|
|
157
|
+
/** Column definitions (order determines visual order). */
|
|
158
|
+
columns: KanbanColumn[];
|
|
159
|
+
/**
|
|
160
|
+
* Called after a successful drag-and-drop.
|
|
161
|
+
* Receives the moved item together with its previous and next column ids.
|
|
162
|
+
*/
|
|
163
|
+
onChange?: (event: KanbanChangeEvent<T>) => void;
|
|
164
|
+
/**
|
|
165
|
+
* Optional custom renderer for each item card.
|
|
166
|
+
* Receives the item and the column it belongs to.
|
|
167
|
+
* When omitted, a default card showing `item.name` is used.
|
|
168
|
+
*/
|
|
169
|
+
renderItem?: (item: KanbanItem<T>, column: KanbanColumn) => ReactNode;
|
|
170
|
+
/**
|
|
171
|
+
* Optional custom renderer for column headers.
|
|
172
|
+
* Receives the column definition and the count of items inside it.
|
|
173
|
+
*/
|
|
174
|
+
renderColumnHeader?: (column: KanbanColumn, itemCount: number) => ReactNode;
|
|
175
|
+
/** Extra className applied to the root wrapper. */
|
|
176
|
+
className?: string;
|
|
177
|
+
/** Inline styles applied to the root wrapper. */
|
|
178
|
+
style?: CSSProperties;
|
|
179
|
+
/** Extra className applied to every column wrapper. */
|
|
180
|
+
columnClassName?: string;
|
|
181
|
+
/**
|
|
182
|
+
* Additional DOM props for a column wrapper. This supports alternative
|
|
183
|
+
* layouts (such as CSS grids) while retaining Kanban's drag-and-drop logic.
|
|
184
|
+
*/
|
|
185
|
+
getColumnProps?: (column: KanbanColumn) => HTMLAttributes<HTMLDivElement>;
|
|
186
|
+
/** Hides the built-in column header while keeping its drop zone active. */
|
|
187
|
+
hideColumnHeaders?: boolean;
|
|
188
|
+
/** Extra className applied to every item card. */
|
|
189
|
+
itemClassName?: string;
|
|
190
|
+
/**
|
|
191
|
+
* Controls whether overflowing column cards scroll inside the column or grow
|
|
192
|
+
* the column beyond the board height.
|
|
193
|
+
*
|
|
194
|
+
* @default 'scroll'
|
|
195
|
+
*/
|
|
196
|
+
columnOverflow?: ColumnOverflow;
|
|
197
|
+
/**
|
|
198
|
+
* When `true`, renders an "Add column" button after the last column.
|
|
199
|
+
* Requires {@link onAddColumn} to handle the creation.
|
|
200
|
+
*/
|
|
201
|
+
allowAddColumn?: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Called when the user submits a new column name via the add-column popover.
|
|
204
|
+
* Only relevant when {@link allowAddColumn} is `true`.
|
|
205
|
+
*/
|
|
206
|
+
onAddColumn?: (title: string) => void;
|
|
207
|
+
/**
|
|
208
|
+
* Called after a column is reordered via drag-and-drop.
|
|
209
|
+
* Receives the new columns array in the updated order.
|
|
210
|
+
* When omitted, columns are not draggable.
|
|
211
|
+
*/
|
|
212
|
+
onColumnChange?: (columns: KanbanColumn[]) => void;
|
|
213
|
+
/**
|
|
214
|
+
* Optional filters surfaced behind a filter button in each column header.
|
|
215
|
+
*
|
|
216
|
+
* Pass a flat array to share the same filters across every column, or a
|
|
217
|
+
* function to resolve filters per column. A column only shows a filter
|
|
218
|
+
* button when it resolves to at least one filter.
|
|
219
|
+
*
|
|
220
|
+
* Filters with a {@link KanbanFilter.predicate} are applied in place by the
|
|
221
|
+
* board; filters without one are tracked and reported via
|
|
222
|
+
* {@link onFilterChange} so the parent can do the filtering.
|
|
223
|
+
*/
|
|
224
|
+
filters?: KanbanFilters<T>;
|
|
225
|
+
/**
|
|
226
|
+
* Called whenever the active filters for a column change (a filter is
|
|
227
|
+
* toggled or cleared). Use this to drive external/server-side filtering.
|
|
228
|
+
*/
|
|
229
|
+
onFilterChange?: (event: KanbanFilterChangeEvent<T>) => void;
|
|
230
|
+
/**
|
|
231
|
+
* Opt into per-column infinite scrolling. Requires
|
|
232
|
+
* {@link columnOverflow} to be `'scroll'`; the board logs a development-only
|
|
233
|
+
* error and stays inert otherwise.
|
|
234
|
+
*/
|
|
235
|
+
infiniteScroll?: KanbanInfiniteScroll;
|
|
236
|
+
/**
|
|
237
|
+
* Opt into per-column virtualization, so only the cards near each column's
|
|
238
|
+
* visible window are mounted. Requires {@link columnOverflow} to be
|
|
239
|
+
* `'scroll'`; the board logs a development-only error and renders every card
|
|
240
|
+
* otherwise.
|
|
241
|
+
*/
|
|
242
|
+
virtualization?: KanbanVirtualization;
|
|
243
|
+
/**
|
|
244
|
+
* Freezes card and column dragging while `true` — for views where a drop
|
|
245
|
+
* position would be meaningless, such as a filtered or externally re-sorted
|
|
246
|
+
* board. Cards stay mounted and fully interactive; only the drag is off.
|
|
247
|
+
*/
|
|
248
|
+
dragDisabled?: boolean;
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
251
|
+
export { ColumnOverflow, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanVirtualization };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { IntersectionOptions } from "./infinite-scroll/types.js";
|
|
2
|
+
import "./infinite-scroll/index.js";
|
|
3
|
+
import { CSSProperties, HTMLAttributes, ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/types.d.ts
|
|
6
|
+
/** A single item that lives inside a Kanban column. */
|
|
7
|
+
interface KanbanItem<T = Record<string, unknown>> {
|
|
8
|
+
/** Unique identifier for the item. */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Display name (used by the default renderer). */
|
|
11
|
+
name: string;
|
|
12
|
+
/** The column id this item currently belongs to. */
|
|
13
|
+
columnId: string;
|
|
14
|
+
/** Arbitrary extra data attached to the item. */
|
|
15
|
+
data?: T;
|
|
16
|
+
}
|
|
17
|
+
/** Definition of a single Kanban column. */
|
|
18
|
+
interface KanbanColumn {
|
|
19
|
+
/** Unique identifier for the column. */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Display title rendered at the top of the column. */
|
|
22
|
+
title: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A filter that can be applied to the items of a column.
|
|
26
|
+
*
|
|
27
|
+
* Filters are passed to the board as props and surfaced behind a filter
|
|
28
|
+
* button in each column header. They are intentionally generic: provide a
|
|
29
|
+
* {@link KanbanFilter.predicate} to let the board hide non-matching cards
|
|
30
|
+
* itself, or omit it and react to {@link KanbanBoardProps.onFilterChange}
|
|
31
|
+
* to perform the filtering yourself (e.g. server-side).
|
|
32
|
+
*/
|
|
33
|
+
interface KanbanFilter<T = Record<string, unknown>> {
|
|
34
|
+
/** Unique identifier for the filter (unique within a column's filter set). */
|
|
35
|
+
id: string;
|
|
36
|
+
/** Human-readable label shown in the filter menu. */
|
|
37
|
+
label: string;
|
|
38
|
+
/**
|
|
39
|
+
* Optional predicate deciding whether an item stays visible while this
|
|
40
|
+
* filter is active. Return `true` to keep the item.
|
|
41
|
+
*
|
|
42
|
+
* When provided, the board filters the column's cards in place. When
|
|
43
|
+
* omitted, the board only tracks the active state and fires
|
|
44
|
+
* {@link KanbanBoardProps.onFilterChange} so the parent can filter itself.
|
|
45
|
+
*/
|
|
46
|
+
predicate?: (item: KanbanItem<T>, column: KanbanColumn) => boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Either a flat list of filters shared by every column, or a resolver that
|
|
50
|
+
* returns the filters for a given column (return `undefined`/empty for
|
|
51
|
+
* columns that should not show a filter button).
|
|
52
|
+
*/
|
|
53
|
+
type KanbanFilters<T = Record<string, unknown>> = KanbanFilter<T>[] | ((column: KanbanColumn) => KanbanFilter<T>[] | undefined);
|
|
54
|
+
/** Controls how columns behave when their cards exceed the board height. */
|
|
55
|
+
type ColumnOverflow = 'scroll' | 'expand';
|
|
56
|
+
/**
|
|
57
|
+
* Payload delivered by {@link KanbanBoardProps.onFilterChange} whenever the
|
|
58
|
+
* set of active filters for a column changes.
|
|
59
|
+
*/
|
|
60
|
+
interface KanbanFilterChangeEvent<T = Record<string, unknown>> {
|
|
61
|
+
/** The column whose active filters changed. */
|
|
62
|
+
column: KanbanColumn;
|
|
63
|
+
/** Ids of the filters currently active on the column. */
|
|
64
|
+
activeFilterIds: string[];
|
|
65
|
+
/** The filter objects currently active on the column. */
|
|
66
|
+
activeFilters: KanbanFilter<T>[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Per-column infinite scrolling. A sentinel is rendered after the last card of
|
|
70
|
+
* every column; when it scrolls into view the board calls {@link onLoadMore}
|
|
71
|
+
* for that column.
|
|
72
|
+
*
|
|
73
|
+
* `hasMore` and `isLoading` are resolved per column rather than passed as flat
|
|
74
|
+
* booleans so a consumer that pages each column from its own request can drive
|
|
75
|
+
* them independently.
|
|
76
|
+
*
|
|
77
|
+
* Only supported when {@link KanbanBoardProps.columnOverflow} is `'scroll'` —
|
|
78
|
+
* an expanding column has no scroll edge to observe.
|
|
79
|
+
*/
|
|
80
|
+
interface KanbanInfiniteScroll extends IntersectionOptions {
|
|
81
|
+
/** Load the next page of items for `column`. */
|
|
82
|
+
onLoadMore: (column: KanbanColumn) => void;
|
|
83
|
+
/** Whether `column` still has items left to load. */
|
|
84
|
+
hasMore: (column: KanbanColumn) => boolean;
|
|
85
|
+
/** Whether a load is currently in flight for `column`. */
|
|
86
|
+
isLoading?: (column: KanbanColumn) => boolean;
|
|
87
|
+
/** Rendered inside a column while it is loading. */
|
|
88
|
+
loadingComponent?: ReactNode;
|
|
89
|
+
/** Rendered at the bottom of a column once it is fully loaded. */
|
|
90
|
+
endMessage?: ReactNode;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Per-column virtualization, powered by `@tanstack/react-virtual`.
|
|
94
|
+
*
|
|
95
|
+
* Only the cards near a column's visible window stay mounted, which is what
|
|
96
|
+
* keeps a thousand-card column usable: dnd-kit re-measures every mounted
|
|
97
|
+
* droppable on each drag frame, so the mounted count — not the item count — is
|
|
98
|
+
* what a drag actually costs.
|
|
99
|
+
*
|
|
100
|
+
* The card being dragged is always kept mounted, even once it scrolls out of
|
|
101
|
+
* the window. Unmounting it mid-gesture would tear down the node dnd-kit holds
|
|
102
|
+
* and strand the drag.
|
|
103
|
+
*
|
|
104
|
+
* Only supported when {@link KanbanBoardProps.columnOverflow} is `'scroll'` —
|
|
105
|
+
* an expanding column has no window to virtualize against.
|
|
106
|
+
*/
|
|
107
|
+
interface KanbanVirtualization {
|
|
108
|
+
/**
|
|
109
|
+
* Height in px assumed for a card that has not been measured yet. Cards are
|
|
110
|
+
* measured as soon as they mount, so this only has to be in the right
|
|
111
|
+
* ballpark; a poor guess just makes the scrollbar settle as you scroll.
|
|
112
|
+
*
|
|
113
|
+
* @default 96
|
|
114
|
+
*/
|
|
115
|
+
estimateItemHeight?: number;
|
|
116
|
+
/**
|
|
117
|
+
* How many extra cards to mount above and below the visible window. Raise it
|
|
118
|
+
* if fast scrolling shows blank gaps — but every extra card is one more
|
|
119
|
+
* droppable dnd-kit measures per drag frame.
|
|
120
|
+
*
|
|
121
|
+
* @default 6
|
|
122
|
+
*/
|
|
123
|
+
overscan?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Vertical gap in px between cards. Virtualized cards are positioned
|
|
126
|
+
* absolutely and cannot inherit the list's flex `gap`, so the virtualizer
|
|
127
|
+
* adds this to every offset instead. Keep it in sync with any custom card
|
|
128
|
+
* spacing.
|
|
129
|
+
*
|
|
130
|
+
* @default 8
|
|
131
|
+
*/
|
|
132
|
+
gap?: number;
|
|
133
|
+
/** Renders every card again while `true`. */
|
|
134
|
+
disabled?: boolean;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Payload delivered by {@link KanbanBoardProps.onChange} after
|
|
138
|
+
* a drag-and-drop operation completes.
|
|
139
|
+
*/
|
|
140
|
+
interface KanbanChangeEvent<T = Record<string, unknown>> {
|
|
141
|
+
/** The item that was moved. */
|
|
142
|
+
item: KanbanItem<T>;
|
|
143
|
+
/** Column id the item was in before the move. */
|
|
144
|
+
previousColumnId: string;
|
|
145
|
+
/** Column id the item is in after the move. */
|
|
146
|
+
nextColumnId: string;
|
|
147
|
+
/**
|
|
148
|
+
* The full items array after the move, with correct ordering.
|
|
149
|
+
* Use this to update your external state so positions are preserved.
|
|
150
|
+
*/
|
|
151
|
+
items: KanbanItem<T>[];
|
|
152
|
+
}
|
|
153
|
+
/** Props for the main `<KanbanBoard />` component. */
|
|
154
|
+
interface KanbanBoardProps<T = Record<string, unknown>> {
|
|
155
|
+
/** All items across every column. */
|
|
156
|
+
items: KanbanItem<T>[];
|
|
157
|
+
/** Column definitions (order determines visual order). */
|
|
158
|
+
columns: KanbanColumn[];
|
|
159
|
+
/**
|
|
160
|
+
* Called after a successful drag-and-drop.
|
|
161
|
+
* Receives the moved item together with its previous and next column ids.
|
|
162
|
+
*/
|
|
163
|
+
onChange?: (event: KanbanChangeEvent<T>) => void;
|
|
164
|
+
/**
|
|
165
|
+
* Optional custom renderer for each item card.
|
|
166
|
+
* Receives the item and the column it belongs to.
|
|
167
|
+
* When omitted, a default card showing `item.name` is used.
|
|
168
|
+
*/
|
|
169
|
+
renderItem?: (item: KanbanItem<T>, column: KanbanColumn) => ReactNode;
|
|
170
|
+
/**
|
|
171
|
+
* Optional custom renderer for column headers.
|
|
172
|
+
* Receives the column definition and the count of items inside it.
|
|
173
|
+
*/
|
|
174
|
+
renderColumnHeader?: (column: KanbanColumn, itemCount: number) => ReactNode;
|
|
175
|
+
/** Extra className applied to the root wrapper. */
|
|
176
|
+
className?: string;
|
|
177
|
+
/** Inline styles applied to the root wrapper. */
|
|
178
|
+
style?: CSSProperties;
|
|
179
|
+
/** Extra className applied to every column wrapper. */
|
|
180
|
+
columnClassName?: string;
|
|
181
|
+
/**
|
|
182
|
+
* Additional DOM props for a column wrapper. This supports alternative
|
|
183
|
+
* layouts (such as CSS grids) while retaining Kanban's drag-and-drop logic.
|
|
184
|
+
*/
|
|
185
|
+
getColumnProps?: (column: KanbanColumn) => HTMLAttributes<HTMLDivElement>;
|
|
186
|
+
/** Hides the built-in column header while keeping its drop zone active. */
|
|
187
|
+
hideColumnHeaders?: boolean;
|
|
188
|
+
/** Extra className applied to every item card. */
|
|
189
|
+
itemClassName?: string;
|
|
190
|
+
/**
|
|
191
|
+
* Controls whether overflowing column cards scroll inside the column or grow
|
|
192
|
+
* the column beyond the board height.
|
|
193
|
+
*
|
|
194
|
+
* @default 'scroll'
|
|
195
|
+
*/
|
|
196
|
+
columnOverflow?: ColumnOverflow;
|
|
197
|
+
/**
|
|
198
|
+
* When `true`, renders an "Add column" button after the last column.
|
|
199
|
+
* Requires {@link onAddColumn} to handle the creation.
|
|
200
|
+
*/
|
|
201
|
+
allowAddColumn?: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Called when the user submits a new column name via the add-column popover.
|
|
204
|
+
* Only relevant when {@link allowAddColumn} is `true`.
|
|
205
|
+
*/
|
|
206
|
+
onAddColumn?: (title: string) => void;
|
|
207
|
+
/**
|
|
208
|
+
* Called after a column is reordered via drag-and-drop.
|
|
209
|
+
* Receives the new columns array in the updated order.
|
|
210
|
+
* When omitted, columns are not draggable.
|
|
211
|
+
*/
|
|
212
|
+
onColumnChange?: (columns: KanbanColumn[]) => void;
|
|
213
|
+
/**
|
|
214
|
+
* Optional filters surfaced behind a filter button in each column header.
|
|
215
|
+
*
|
|
216
|
+
* Pass a flat array to share the same filters across every column, or a
|
|
217
|
+
* function to resolve filters per column. A column only shows a filter
|
|
218
|
+
* button when it resolves to at least one filter.
|
|
219
|
+
*
|
|
220
|
+
* Filters with a {@link KanbanFilter.predicate} are applied in place by the
|
|
221
|
+
* board; filters without one are tracked and reported via
|
|
222
|
+
* {@link onFilterChange} so the parent can do the filtering.
|
|
223
|
+
*/
|
|
224
|
+
filters?: KanbanFilters<T>;
|
|
225
|
+
/**
|
|
226
|
+
* Called whenever the active filters for a column change (a filter is
|
|
227
|
+
* toggled or cleared). Use this to drive external/server-side filtering.
|
|
228
|
+
*/
|
|
229
|
+
onFilterChange?: (event: KanbanFilterChangeEvent<T>) => void;
|
|
230
|
+
/**
|
|
231
|
+
* Opt into per-column infinite scrolling. Requires
|
|
232
|
+
* {@link columnOverflow} to be `'scroll'`; the board logs a development-only
|
|
233
|
+
* error and stays inert otherwise.
|
|
234
|
+
*/
|
|
235
|
+
infiniteScroll?: KanbanInfiniteScroll;
|
|
236
|
+
/**
|
|
237
|
+
* Opt into per-column virtualization, so only the cards near each column's
|
|
238
|
+
* visible window are mounted. Requires {@link columnOverflow} to be
|
|
239
|
+
* `'scroll'`; the board logs a development-only error and renders every card
|
|
240
|
+
* otherwise.
|
|
241
|
+
*/
|
|
242
|
+
virtualization?: KanbanVirtualization;
|
|
243
|
+
/**
|
|
244
|
+
* Freezes card and column dragging while `true` — for views where a drop
|
|
245
|
+
* position would be meaningless, such as a filtered or externally re-sorted
|
|
246
|
+
* board. Cards stay mounted and fully interactive; only the drag is off.
|
|
247
|
+
*/
|
|
248
|
+
dragDisabled?: boolean;
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
251
|
+
export { ColumnOverflow, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanVirtualization };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
const require_move_kanban_item = require('./move-kanban-item.cjs');
|
|
3
|
+
let __dnd_kit_sortable = require("@dnd-kit/sortable");
|
|
4
|
+
__dnd_kit_sortable = require_rolldown_runtime.__toESM(__dnd_kit_sortable);
|
|
5
|
+
|
|
6
|
+
//#region src/utils/apply-kanban-drop.ts
|
|
7
|
+
/**
|
|
8
|
+
* Produces the final item list for a completed drop.
|
|
9
|
+
*
|
|
10
|
+
* Drops inside the card's current column commit the reorder that the sortable
|
|
11
|
+
* transforms have been previewing. Drops onto another column are applied here
|
|
12
|
+
* because `onDragOver` never previewed them — keyboard drags and drops onto an
|
|
13
|
+
* empty column both land in this branch. Anything else leaves the list as-is.
|
|
14
|
+
*/
|
|
15
|
+
function applyKanbanDrop({ items, activeId, overId, overColumnId, insertAfter, overIsColumn }) {
|
|
16
|
+
const activeItem = items.find((item) => item.id === activeId);
|
|
17
|
+
if (!activeItem) return items;
|
|
18
|
+
const activeColumnId = activeItem.columnId;
|
|
19
|
+
const overItem = items.find((item) => item.id === overId);
|
|
20
|
+
if (overItem !== void 0 && overItem.columnId === activeColumnId) {
|
|
21
|
+
const activeIndex = items.indexOf(activeItem);
|
|
22
|
+
const overIndex = items.indexOf(overItem);
|
|
23
|
+
return activeIndex === overIndex ? items : (0, __dnd_kit_sortable.arrayMove)(items, activeIndex, overIndex);
|
|
24
|
+
}
|
|
25
|
+
if (overColumnId !== void 0 && overColumnId !== activeColumnId) return require_move_kanban_item.moveKanbanItem({
|
|
26
|
+
items,
|
|
27
|
+
activeId,
|
|
28
|
+
overId,
|
|
29
|
+
targetColumnId: overColumnId,
|
|
30
|
+
insertAfter,
|
|
31
|
+
overIsColumn
|
|
32
|
+
});
|
|
33
|
+
return items;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
exports.applyKanbanDrop = applyKanbanDrop;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { moveKanbanItem } from "./move-kanban-item.js";
|
|
2
|
+
import { arrayMove } from "@dnd-kit/sortable";
|
|
3
|
+
|
|
4
|
+
//#region src/utils/apply-kanban-drop.ts
|
|
5
|
+
/**
|
|
6
|
+
* Produces the final item list for a completed drop.
|
|
7
|
+
*
|
|
8
|
+
* Drops inside the card's current column commit the reorder that the sortable
|
|
9
|
+
* transforms have been previewing. Drops onto another column are applied here
|
|
10
|
+
* because `onDragOver` never previewed them — keyboard drags and drops onto an
|
|
11
|
+
* empty column both land in this branch. Anything else leaves the list as-is.
|
|
12
|
+
*/
|
|
13
|
+
function applyKanbanDrop({ items, activeId, overId, overColumnId, insertAfter, overIsColumn }) {
|
|
14
|
+
const activeItem = items.find((item) => item.id === activeId);
|
|
15
|
+
if (!activeItem) return items;
|
|
16
|
+
const activeColumnId = activeItem.columnId;
|
|
17
|
+
const overItem = items.find((item) => item.id === overId);
|
|
18
|
+
if (overItem !== void 0 && overItem.columnId === activeColumnId) {
|
|
19
|
+
const activeIndex = items.indexOf(activeItem);
|
|
20
|
+
const overIndex = items.indexOf(overItem);
|
|
21
|
+
return activeIndex === overIndex ? items : arrayMove(items, activeIndex, overIndex);
|
|
22
|
+
}
|
|
23
|
+
if (overColumnId !== void 0 && overColumnId !== activeColumnId) return moveKanbanItem({
|
|
24
|
+
items,
|
|
25
|
+
activeId,
|
|
26
|
+
overId,
|
|
27
|
+
targetColumnId: overColumnId,
|
|
28
|
+
insertAfter,
|
|
29
|
+
overIsColumn
|
|
30
|
+
});
|
|
31
|
+
return items;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
export { applyKanbanDrop };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/utils/column-sortable-id.ts
|
|
3
|
+
/**
|
|
4
|
+
* Columns participate in two dnd-kit registries at once: a droppable using the
|
|
5
|
+
* raw column id (items dropped into the column) and a sortable using a
|
|
6
|
+
* prefixed id (columns reordered against each other). These helpers convert
|
|
7
|
+
* between the two id spaces.
|
|
8
|
+
*/
|
|
9
|
+
const COLUMN_SORTABLE_PREFIX = "column-";
|
|
10
|
+
function toColumnSortableId(columnId) {
|
|
11
|
+
return `${COLUMN_SORTABLE_PREFIX}${columnId}`;
|
|
12
|
+
}
|
|
13
|
+
function isColumnSortableId(id) {
|
|
14
|
+
return String(id).startsWith(COLUMN_SORTABLE_PREFIX);
|
|
15
|
+
}
|
|
16
|
+
function extractColumnId(sortableId) {
|
|
17
|
+
return String(sortableId).slice(7);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
exports.extractColumnId = extractColumnId;
|
|
22
|
+
exports.isColumnSortableId = isColumnSortableId;
|
|
23
|
+
exports.toColumnSortableId = toColumnSortableId;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/utils/column-sortable-id.ts
|
|
2
|
+
/**
|
|
3
|
+
* Columns participate in two dnd-kit registries at once: a droppable using the
|
|
4
|
+
* raw column id (items dropped into the column) and a sortable using a
|
|
5
|
+
* prefixed id (columns reordered against each other). These helpers convert
|
|
6
|
+
* between the two id spaces.
|
|
7
|
+
*/
|
|
8
|
+
const COLUMN_SORTABLE_PREFIX = "column-";
|
|
9
|
+
function toColumnSortableId(columnId) {
|
|
10
|
+
return `${COLUMN_SORTABLE_PREFIX}${columnId}`;
|
|
11
|
+
}
|
|
12
|
+
function isColumnSortableId(id) {
|
|
13
|
+
return String(id).startsWith(COLUMN_SORTABLE_PREFIX);
|
|
14
|
+
}
|
|
15
|
+
function extractColumnId(sortableId) {
|
|
16
|
+
return String(sortableId).slice(7);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { extractColumnId, isColumnSortableId, toColumnSortableId };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/utils/is-below-over-item.ts
|
|
3
|
+
/**
|
|
4
|
+
* Whether the dragged card should be inserted *after* the card it is over.
|
|
5
|
+
*
|
|
6
|
+
* The card only counts as "below" once its top edge clears the hovered card's
|
|
7
|
+
* bottom edge, so a card resting on top of another still lands before it.
|
|
8
|
+
*/
|
|
9
|
+
function isBelowOverItem(event) {
|
|
10
|
+
const activeRect = event.active.rect?.current?.translated;
|
|
11
|
+
const overRect = event.over?.rect;
|
|
12
|
+
if (activeRect == null || overRect == null) return false;
|
|
13
|
+
return activeRect.top > overRect.top + overRect.height;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
exports.isBelowOverItem = isBelowOverItem;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/utils/is-below-over-item.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether the dragged card should be inserted *after* the card it is over.
|
|
4
|
+
*
|
|
5
|
+
* The card only counts as "below" once its top edge clears the hovered card's
|
|
6
|
+
* bottom edge, so a card resting on top of another still lands before it.
|
|
7
|
+
*/
|
|
8
|
+
function isBelowOverItem(event) {
|
|
9
|
+
const activeRect = event.active.rect?.current?.translated;
|
|
10
|
+
const overRect = event.over?.rect;
|
|
11
|
+
if (activeRect == null || overRect == null) return false;
|
|
12
|
+
return activeRect.top > overRect.top + overRect.height;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { isBelowOverItem };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/utils/items-order-equal.ts
|
|
3
|
+
/** Serialises the only things a drag can change: column membership and order. */
|
|
4
|
+
function orderKey(items) {
|
|
5
|
+
return JSON.stringify(items.map((item) => [item.id, item.columnId]));
|
|
6
|
+
}
|
|
7
|
+
/** Whether two item lists hold the same cards, in the same columns and order. */
|
|
8
|
+
function itemsOrderEqual(a, b) {
|
|
9
|
+
return orderKey(a) === orderKey(b);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
exports.itemsOrderEqual = itemsOrderEqual;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/utils/items-order-equal.ts
|
|
2
|
+
/** Serialises the only things a drag can change: column membership and order. */
|
|
3
|
+
function orderKey(items) {
|
|
4
|
+
return JSON.stringify(items.map((item) => [item.id, item.columnId]));
|
|
5
|
+
}
|
|
6
|
+
/** Whether two item lists hold the same cards, in the same columns and order. */
|
|
7
|
+
function itemsOrderEqual(a, b) {
|
|
8
|
+
return orderKey(a) === orderKey(b);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { itemsOrderEqual };
|