@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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { AddColumnButton } from "./AddColumnButton.js";
|
|
5
|
+
import { toColumnSortableId } from "./utils/column-sortable-id.js";
|
|
6
|
+
import { useKanbanDrag } from "./hooks/use-kanban-drag.js";
|
|
7
|
+
import { useKanbanFilters } from "./hooks/use-kanban-filters.js";
|
|
8
|
+
import { KanbanColumn } from "./KanbanColumn.js";
|
|
9
|
+
import { KanbanDragOverlay } from "./KanbanDragOverlay.js";
|
|
10
|
+
import { DndContext, MeasuringStrategy } from "@dnd-kit/core";
|
|
11
|
+
import { SortableContext, horizontalListSortingStrategy } from "@dnd-kit/sortable";
|
|
12
|
+
import { cn } from "@pixpilot/shadcn-ui";
|
|
13
|
+
import React from "react";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
|
|
16
|
+
//#region src/KanbanBoard.tsx
|
|
17
|
+
const MEASURING = { droppable: { strategy: MeasuringStrategy.Always } };
|
|
18
|
+
/**
|
|
19
|
+
* Warns once, in development only, when a scroll-only feature is requested for
|
|
20
|
+
* a column mode that cannot support it. Deliberately non-throwing: a mis-set
|
|
21
|
+
* prop should not take a board down in production.
|
|
22
|
+
*/
|
|
23
|
+
function useScrollOnlyFeatureWarning(feature, consequence, requested, columnOverflow) {
|
|
24
|
+
const warned = React.useRef(false);
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
if (process.env.NODE_ENV === "production") return;
|
|
27
|
+
if (!requested || columnOverflow === "scroll" || warned.current) return;
|
|
28
|
+
warned.current = true;
|
|
29
|
+
console.error(`[KanbanBoard] \`${feature}\` is not implemented for columnOverflow="${columnOverflow}" — only "scroll" is supported. ${consequence}`);
|
|
30
|
+
}, [
|
|
31
|
+
feature,
|
|
32
|
+
consequence,
|
|
33
|
+
requested,
|
|
34
|
+
columnOverflow
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A generic, reusable Kanban board with drag-and-drop powered by `@dnd-kit`.
|
|
39
|
+
*
|
|
40
|
+
* Accepts an array of {@link KanbanItem} items and {@link KanbanColumn}
|
|
41
|
+
* column definitions and calls `onChange` whenever an item is moved
|
|
42
|
+
* between (or within) columns.
|
|
43
|
+
*/
|
|
44
|
+
function KanbanBoard({ items: externalItems, columns, onChange, renderItem, renderColumnHeader, className, style, columnClassName, getColumnProps, hideColumnHeaders = false, itemClassName, columnOverflow = "scroll", allowAddColumn, onAddColumn, onColumnChange, filters, onFilterChange, infiniteScroll, virtualization, dragDisabled = false }) {
|
|
45
|
+
useScrollOnlyFeatureWarning("infiniteScroll", "The sentinel will not be rendered.", Boolean(infiniteScroll), columnOverflow);
|
|
46
|
+
useScrollOnlyFeatureWarning("virtualization", "Every card will be rendered.", Boolean(virtualization), columnOverflow);
|
|
47
|
+
const activeInfiniteScroll = columnOverflow === "scroll" ? infiniteScroll : void 0;
|
|
48
|
+
const activeVirtualization = columnOverflow === "scroll" && virtualization?.disabled !== true ? virtualization : void 0;
|
|
49
|
+
const { items, internalColumns, activeId, isDraggingColumn, sensors, collisionDetection, handleDragStart, handleDragOver, handleDragEnd, handleDragCancel } = useKanbanDrag({
|
|
50
|
+
externalItems,
|
|
51
|
+
columns,
|
|
52
|
+
onChange,
|
|
53
|
+
onColumnChange
|
|
54
|
+
});
|
|
55
|
+
const { activeFilters, resolveFilters, handleToggleFilter, handleClearFilters } = useKanbanFilters({
|
|
56
|
+
filters,
|
|
57
|
+
onFilterChange
|
|
58
|
+
});
|
|
59
|
+
const activeItem = activeId !== null && !isDraggingColumn ? items.find((i) => i.id === activeId) ?? null : null;
|
|
60
|
+
const activeItemColumn = activeItem ? internalColumns.find((c) => c.id === activeItem.columnId) : void 0;
|
|
61
|
+
const activeColumn = activeId !== null && isDraggingColumn ? internalColumns.find((c) => toColumnSortableId(c.id) === String(activeId)) : void 0;
|
|
62
|
+
const columnItemsMap = React.useMemo(() => internalColumns.map((col) => {
|
|
63
|
+
const colFilters = resolveFilters(col);
|
|
64
|
+
const activeIds = activeFilters[col.id] ?? [];
|
|
65
|
+
const activePredicates = colFilters.filter((f) => activeIds.includes(f.id) && typeof f.predicate === "function");
|
|
66
|
+
let colItems = items.filter((i) => i.columnId === col.id);
|
|
67
|
+
if (activePredicates.length > 0) colItems = colItems.filter((item) => activePredicates.every((f) => f.predicate(item, col)));
|
|
68
|
+
return {
|
|
69
|
+
column: col,
|
|
70
|
+
colItems,
|
|
71
|
+
colFilters,
|
|
72
|
+
activeIds
|
|
73
|
+
};
|
|
74
|
+
}), [
|
|
75
|
+
internalColumns,
|
|
76
|
+
items,
|
|
77
|
+
resolveFilters,
|
|
78
|
+
activeFilters
|
|
79
|
+
]);
|
|
80
|
+
const columnSortableIds = React.useMemo(() => internalColumns.map((c) => toColumnSortableId(c.id)), [internalColumns]);
|
|
81
|
+
const isColumnSortable = Boolean(onColumnChange) && !dragDisabled;
|
|
82
|
+
return /* @__PURE__ */ jsxs(DndContext, {
|
|
83
|
+
sensors,
|
|
84
|
+
collisionDetection,
|
|
85
|
+
measuring: MEASURING,
|
|
86
|
+
onDragStart: handleDragStart,
|
|
87
|
+
onDragOver: handleDragOver,
|
|
88
|
+
onDragEnd: handleDragEnd,
|
|
89
|
+
onDragCancel: handleDragCancel,
|
|
90
|
+
children: [/* @__PURE__ */ jsx(SortableContext, {
|
|
91
|
+
items: columnSortableIds,
|
|
92
|
+
strategy: horizontalListSortingStrategy,
|
|
93
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
94
|
+
"data-testid": "kanban-board",
|
|
95
|
+
className: cn("relative flex gap-4 overflow-x-auto overscroll-x-contain p-2", columnOverflow === "scroll" ? "h-full min-h-0" : "min-h-full", className),
|
|
96
|
+
style,
|
|
97
|
+
children: [columnItemsMap.map(({ column, colItems, colFilters, activeIds }) => /* @__PURE__ */ jsx(KanbanColumn, {
|
|
98
|
+
column,
|
|
99
|
+
items: colItems,
|
|
100
|
+
renderItem,
|
|
101
|
+
renderColumnHeader,
|
|
102
|
+
columnClassName,
|
|
103
|
+
containerProps: getColumnProps?.(column),
|
|
104
|
+
hideHeader: hideColumnHeaders,
|
|
105
|
+
itemClassName,
|
|
106
|
+
sortable: isColumnSortable,
|
|
107
|
+
filters: colFilters,
|
|
108
|
+
activeFilterIds: activeIds,
|
|
109
|
+
columnOverflow,
|
|
110
|
+
infiniteScroll: activeInfiniteScroll,
|
|
111
|
+
virtualization: activeVirtualization,
|
|
112
|
+
dragDisabled,
|
|
113
|
+
activeItemId: activeItem?.id ?? null,
|
|
114
|
+
onToggleFilter: (filterId) => handleToggleFilter(column, filterId),
|
|
115
|
+
onClearFilters: () => handleClearFilters(column)
|
|
116
|
+
}, column.id)), allowAddColumn && onAddColumn ? /* @__PURE__ */ jsx(AddColumnButton, { onAdd: onAddColumn }) : null]
|
|
117
|
+
})
|
|
118
|
+
}), /* @__PURE__ */ jsx(KanbanDragOverlay, {
|
|
119
|
+
activeItem,
|
|
120
|
+
activeItemColumn,
|
|
121
|
+
activeColumn,
|
|
122
|
+
activeColumnItemCount: activeColumn ? items.filter((i) => i.columnId === activeColumn.id).length : 0,
|
|
123
|
+
renderItem,
|
|
124
|
+
itemClassName,
|
|
125
|
+
columnClassName
|
|
126
|
+
})]
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
131
|
+
export { KanbanBoard };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
5
|
+
const require_column_sortable_id = require('./utils/column-sortable-id.cjs');
|
|
6
|
+
const require_ColumnFilterButton = require('./ColumnFilterButton.cjs');
|
|
7
|
+
const require_InfiniteScrollSentinel = require('./infinite-scroll/InfiniteScrollSentinel.cjs');
|
|
8
|
+
const require_KanbanColumnCards = require('./KanbanColumnCards.cjs');
|
|
9
|
+
const require_KanbanVirtualColumnCards = require('./KanbanVirtualColumnCards.cjs');
|
|
10
|
+
let __dnd_kit_core = require("@dnd-kit/core");
|
|
11
|
+
__dnd_kit_core = require_rolldown_runtime.__toESM(__dnd_kit_core);
|
|
12
|
+
let __dnd_kit_sortable = require("@dnd-kit/sortable");
|
|
13
|
+
__dnd_kit_sortable = require_rolldown_runtime.__toESM(__dnd_kit_sortable);
|
|
14
|
+
let __pixpilot_shadcn_ui = require("@pixpilot/shadcn-ui");
|
|
15
|
+
__pixpilot_shadcn_ui = require_rolldown_runtime.__toESM(__pixpilot_shadcn_ui);
|
|
16
|
+
let react = require("react");
|
|
17
|
+
react = require_rolldown_runtime.__toESM(react);
|
|
18
|
+
let lucide_react = require("lucide-react");
|
|
19
|
+
lucide_react = require_rolldown_runtime.__toESM(lucide_react);
|
|
20
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
21
|
+
react_jsx_runtime = require_rolldown_runtime.__toESM(react_jsx_runtime);
|
|
22
|
+
let __dnd_kit_utilities = require("@dnd-kit/utilities");
|
|
23
|
+
__dnd_kit_utilities = require_rolldown_runtime.__toESM(__dnd_kit_utilities);
|
|
24
|
+
|
|
25
|
+
//#region src/KanbanColumn.tsx
|
|
26
|
+
function KanbanColumn({ column, items, renderItem, renderColumnHeader, columnClassName, itemClassName, containerProps, hideHeader = false, sortable = false, filters, activeFilterIds, columnOverflow, infiniteScroll, virtualization, dragDisabled = false, activeItemId = null, onToggleFilter, onClearFilters }) {
|
|
27
|
+
const { className: containerClassName, style: containerStyle,...containerAttributes } = containerProps ?? {};
|
|
28
|
+
const filterButton = filters && filters.length > 0 && onToggleFilter && onClearFilters ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ColumnFilterButton.ColumnFilterButton, {
|
|
29
|
+
filters,
|
|
30
|
+
activeFilterIds: activeFilterIds ?? [],
|
|
31
|
+
onToggle: onToggleFilter,
|
|
32
|
+
onClear: onClearFilters
|
|
33
|
+
}) : null;
|
|
34
|
+
const { setNodeRef: setDroppableRef, isOver } = (0, __dnd_kit_core.useDroppable)({
|
|
35
|
+
id: column.id,
|
|
36
|
+
data: {
|
|
37
|
+
type: "column",
|
|
38
|
+
column
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const { attributes, listeners, setNodeRef: setSortableRef, transform, transition, isDragging } = (0, __dnd_kit_sortable.useSortable)({
|
|
42
|
+
id: require_column_sortable_id.toColumnSortableId(column.id),
|
|
43
|
+
data: {
|
|
44
|
+
type: "column-sortable",
|
|
45
|
+
column
|
|
46
|
+
},
|
|
47
|
+
disabled: !sortable
|
|
48
|
+
});
|
|
49
|
+
const style = sortable ? {
|
|
50
|
+
transform: __dnd_kit_utilities.CSS.Transform.toString(transform),
|
|
51
|
+
transition: transition ?? void 0
|
|
52
|
+
} : {};
|
|
53
|
+
const itemIds = react.default.useMemo(() => items.map((i) => i.id), [items]);
|
|
54
|
+
const [scroller, setScroller] = react.default.useState(null);
|
|
55
|
+
const setScrollerRef = react.default.useCallback((node) => {
|
|
56
|
+
setDroppableRef(node);
|
|
57
|
+
setScroller(node);
|
|
58
|
+
}, [setDroppableRef]);
|
|
59
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
60
|
+
ref: setSortableRef,
|
|
61
|
+
style: {
|
|
62
|
+
...containerStyle,
|
|
63
|
+
...style
|
|
64
|
+
},
|
|
65
|
+
className: (0, __pixpilot_shadcn_ui.cn)("bg-muted/40 relative flex min-w-[250px] flex-1 flex-col rounded-lg border", columnOverflow === "scroll" ? "h-full min-h-0 overflow-hidden" : "min-h-full overflow-visible", isOver && "ring-primary/30 ring-2", isDragging && "opacity-50", columnClassName, containerClassName),
|
|
66
|
+
...sortable ? attributes : {},
|
|
67
|
+
...containerAttributes,
|
|
68
|
+
children: [!hideHeader && (renderColumnHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
69
|
+
className: "flex items-center gap-1 px-3 pt-3 pb-3",
|
|
70
|
+
children: [
|
|
71
|
+
sortable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
72
|
+
type: "button",
|
|
73
|
+
className: "text-muted-foreground hover:text-foreground cursor-grab touch-none active:cursor-grabbing",
|
|
74
|
+
...listeners,
|
|
75
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.GripVertical, { className: "h-4 w-4" })
|
|
76
|
+
}) : null,
|
|
77
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
78
|
+
className: "flex-1",
|
|
79
|
+
children: renderColumnHeader(column, items.length)
|
|
80
|
+
}),
|
|
81
|
+
filterButton
|
|
82
|
+
]
|
|
83
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
84
|
+
className: "flex items-center justify-between px-3 pt-3 pb-3",
|
|
85
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
86
|
+
className: "flex items-center gap-1",
|
|
87
|
+
children: [sortable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
88
|
+
type: "button",
|
|
89
|
+
className: "text-muted-foreground hover:text-foreground cursor-grab touch-none active:cursor-grabbing",
|
|
90
|
+
...listeners,
|
|
91
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.GripVertical, { className: "h-4 w-4" })
|
|
92
|
+
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
93
|
+
className: "text-sm font-semibold",
|
|
94
|
+
children: column.title
|
|
95
|
+
})]
|
|
96
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
97
|
+
className: "flex items-center gap-1",
|
|
98
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
99
|
+
className: "bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs",
|
|
100
|
+
children: items.length
|
|
101
|
+
}), filterButton]
|
|
102
|
+
})]
|
|
103
|
+
})), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__dnd_kit_sortable.SortableContext, {
|
|
104
|
+
items: itemIds,
|
|
105
|
+
strategy: __dnd_kit_sortable.verticalListSortingStrategy,
|
|
106
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
107
|
+
ref: setScrollerRef,
|
|
108
|
+
"data-testid": `kanban-column-scroller-${column.id}`,
|
|
109
|
+
className: (0, __pixpilot_shadcn_ui.cn)("relative flex flex-1 flex-col gap-2 px-3 pb-3", columnOverflow === "scroll" ? "min-h-0 overflow-y-auto pr-2" : "min-h-[60px] overflow-visible"),
|
|
110
|
+
children: [virtualization ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_KanbanVirtualColumnCards.KanbanVirtualColumnCards, {
|
|
111
|
+
column,
|
|
112
|
+
items,
|
|
113
|
+
renderItem,
|
|
114
|
+
itemClassName,
|
|
115
|
+
scroller,
|
|
116
|
+
activeItemId,
|
|
117
|
+
options: virtualization,
|
|
118
|
+
dragDisabled
|
|
119
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_KanbanColumnCards.KanbanColumnCards, {
|
|
120
|
+
column,
|
|
121
|
+
items,
|
|
122
|
+
renderItem,
|
|
123
|
+
itemClassName,
|
|
124
|
+
dragDisabled
|
|
125
|
+
}), infiniteScroll ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_InfiniteScrollSentinel.InfiniteScrollSentinel, {
|
|
126
|
+
root: scroller,
|
|
127
|
+
distance: infiniteScroll.distance,
|
|
128
|
+
threshold: infiniteScroll.threshold,
|
|
129
|
+
disabled: infiniteScroll.disabled,
|
|
130
|
+
hasMore: infiniteScroll.hasMore(column),
|
|
131
|
+
isLoading: infiniteScroll.isLoading?.(column) ?? false,
|
|
132
|
+
loadingComponent: infiniteScroll.loadingComponent,
|
|
133
|
+
endMessage: infiniteScroll.endMessage,
|
|
134
|
+
onLoadMore: () => infiniteScroll.onLoadMore(column)
|
|
135
|
+
}) : null]
|
|
136
|
+
})
|
|
137
|
+
})]
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
exports.KanbanColumn = KanbanColumn;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { toColumnSortableId } from "./utils/column-sortable-id.js";
|
|
5
|
+
import { ColumnFilterButton } from "./ColumnFilterButton.js";
|
|
6
|
+
import { InfiniteScrollSentinel } from "./infinite-scroll/InfiniteScrollSentinel.js";
|
|
7
|
+
import { KanbanColumnCards } from "./KanbanColumnCards.js";
|
|
8
|
+
import { KanbanVirtualColumnCards } from "./KanbanVirtualColumnCards.js";
|
|
9
|
+
import { useDroppable } from "@dnd-kit/core";
|
|
10
|
+
import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
11
|
+
import { cn } from "@pixpilot/shadcn-ui";
|
|
12
|
+
import React from "react";
|
|
13
|
+
import { GripVertical } from "lucide-react";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
16
|
+
|
|
17
|
+
//#region src/KanbanColumn.tsx
|
|
18
|
+
function KanbanColumn({ column, items, renderItem, renderColumnHeader, columnClassName, itemClassName, containerProps, hideHeader = false, sortable = false, filters, activeFilterIds, columnOverflow, infiniteScroll, virtualization, dragDisabled = false, activeItemId = null, onToggleFilter, onClearFilters }) {
|
|
19
|
+
const { className: containerClassName, style: containerStyle,...containerAttributes } = containerProps ?? {};
|
|
20
|
+
const filterButton = filters && filters.length > 0 && onToggleFilter && onClearFilters ? /* @__PURE__ */ jsx(ColumnFilterButton, {
|
|
21
|
+
filters,
|
|
22
|
+
activeFilterIds: activeFilterIds ?? [],
|
|
23
|
+
onToggle: onToggleFilter,
|
|
24
|
+
onClear: onClearFilters
|
|
25
|
+
}) : null;
|
|
26
|
+
const { setNodeRef: setDroppableRef, isOver } = useDroppable({
|
|
27
|
+
id: column.id,
|
|
28
|
+
data: {
|
|
29
|
+
type: "column",
|
|
30
|
+
column
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const { attributes, listeners, setNodeRef: setSortableRef, transform, transition, isDragging } = useSortable({
|
|
34
|
+
id: toColumnSortableId(column.id),
|
|
35
|
+
data: {
|
|
36
|
+
type: "column-sortable",
|
|
37
|
+
column
|
|
38
|
+
},
|
|
39
|
+
disabled: !sortable
|
|
40
|
+
});
|
|
41
|
+
const style = sortable ? {
|
|
42
|
+
transform: CSS.Transform.toString(transform),
|
|
43
|
+
transition: transition ?? void 0
|
|
44
|
+
} : {};
|
|
45
|
+
const itemIds = React.useMemo(() => items.map((i) => i.id), [items]);
|
|
46
|
+
const [scroller, setScroller] = React.useState(null);
|
|
47
|
+
const setScrollerRef = React.useCallback((node) => {
|
|
48
|
+
setDroppableRef(node);
|
|
49
|
+
setScroller(node);
|
|
50
|
+
}, [setDroppableRef]);
|
|
51
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
52
|
+
ref: setSortableRef,
|
|
53
|
+
style: {
|
|
54
|
+
...containerStyle,
|
|
55
|
+
...style
|
|
56
|
+
},
|
|
57
|
+
className: cn("bg-muted/40 relative flex min-w-[250px] flex-1 flex-col rounded-lg border", columnOverflow === "scroll" ? "h-full min-h-0 overflow-hidden" : "min-h-full overflow-visible", isOver && "ring-primary/30 ring-2", isDragging && "opacity-50", columnClassName, containerClassName),
|
|
58
|
+
...sortable ? attributes : {},
|
|
59
|
+
...containerAttributes,
|
|
60
|
+
children: [!hideHeader && (renderColumnHeader ? /* @__PURE__ */ jsxs("div", {
|
|
61
|
+
className: "flex items-center gap-1 px-3 pt-3 pb-3",
|
|
62
|
+
children: [
|
|
63
|
+
sortable ? /* @__PURE__ */ jsx("button", {
|
|
64
|
+
type: "button",
|
|
65
|
+
className: "text-muted-foreground hover:text-foreground cursor-grab touch-none active:cursor-grabbing",
|
|
66
|
+
...listeners,
|
|
67
|
+
children: /* @__PURE__ */ jsx(GripVertical, { className: "h-4 w-4" })
|
|
68
|
+
}) : null,
|
|
69
|
+
/* @__PURE__ */ jsx("div", {
|
|
70
|
+
className: "flex-1",
|
|
71
|
+
children: renderColumnHeader(column, items.length)
|
|
72
|
+
}),
|
|
73
|
+
filterButton
|
|
74
|
+
]
|
|
75
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
76
|
+
className: "flex items-center justify-between px-3 pt-3 pb-3",
|
|
77
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
78
|
+
className: "flex items-center gap-1",
|
|
79
|
+
children: [sortable ? /* @__PURE__ */ jsx("button", {
|
|
80
|
+
type: "button",
|
|
81
|
+
className: "text-muted-foreground hover:text-foreground cursor-grab touch-none active:cursor-grabbing",
|
|
82
|
+
...listeners,
|
|
83
|
+
children: /* @__PURE__ */ jsx(GripVertical, { className: "h-4 w-4" })
|
|
84
|
+
}) : null, /* @__PURE__ */ jsx("h3", {
|
|
85
|
+
className: "text-sm font-semibold",
|
|
86
|
+
children: column.title
|
|
87
|
+
})]
|
|
88
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
89
|
+
className: "flex items-center gap-1",
|
|
90
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
91
|
+
className: "bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs",
|
|
92
|
+
children: items.length
|
|
93
|
+
}), filterButton]
|
|
94
|
+
})]
|
|
95
|
+
})), /* @__PURE__ */ jsx(SortableContext, {
|
|
96
|
+
items: itemIds,
|
|
97
|
+
strategy: verticalListSortingStrategy,
|
|
98
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
99
|
+
ref: setScrollerRef,
|
|
100
|
+
"data-testid": `kanban-column-scroller-${column.id}`,
|
|
101
|
+
className: cn("relative flex flex-1 flex-col gap-2 px-3 pb-3", columnOverflow === "scroll" ? "min-h-0 overflow-y-auto pr-2" : "min-h-[60px] overflow-visible"),
|
|
102
|
+
children: [virtualization ? /* @__PURE__ */ jsx(KanbanVirtualColumnCards, {
|
|
103
|
+
column,
|
|
104
|
+
items,
|
|
105
|
+
renderItem,
|
|
106
|
+
itemClassName,
|
|
107
|
+
scroller,
|
|
108
|
+
activeItemId,
|
|
109
|
+
options: virtualization,
|
|
110
|
+
dragDisabled
|
|
111
|
+
}) : /* @__PURE__ */ jsx(KanbanColumnCards, {
|
|
112
|
+
column,
|
|
113
|
+
items,
|
|
114
|
+
renderItem,
|
|
115
|
+
itemClassName,
|
|
116
|
+
dragDisabled
|
|
117
|
+
}), infiniteScroll ? /* @__PURE__ */ jsx(InfiniteScrollSentinel, {
|
|
118
|
+
root: scroller,
|
|
119
|
+
distance: infiniteScroll.distance,
|
|
120
|
+
threshold: infiniteScroll.threshold,
|
|
121
|
+
disabled: infiniteScroll.disabled,
|
|
122
|
+
hasMore: infiniteScroll.hasMore(column),
|
|
123
|
+
isLoading: infiniteScroll.isLoading?.(column) ?? false,
|
|
124
|
+
loadingComponent: infiniteScroll.loadingComponent,
|
|
125
|
+
endMessage: infiniteScroll.endMessage,
|
|
126
|
+
onLoadMore: () => infiniteScroll.onLoadMore(column)
|
|
127
|
+
}) : null]
|
|
128
|
+
})
|
|
129
|
+
})]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
export { KanbanColumn };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
5
|
+
const require_KanbanItem = require('./KanbanItem.cjs');
|
|
6
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
7
|
+
react_jsx_runtime = require_rolldown_runtime.__toESM(react_jsx_runtime);
|
|
8
|
+
|
|
9
|
+
//#region src/KanbanColumnCards.tsx
|
|
10
|
+
/**
|
|
11
|
+
* The card list of a column, rendered in full. Each card is a flex child of
|
|
12
|
+
* the column's scroller, so spacing comes from the scroller's `gap`.
|
|
13
|
+
*/
|
|
14
|
+
function KanbanColumnCards({ column, items, renderItem, itemClassName, dragDisabled = false }) {
|
|
15
|
+
return items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_KanbanItem.KanbanItem, {
|
|
16
|
+
item,
|
|
17
|
+
column,
|
|
18
|
+
renderItem,
|
|
19
|
+
className: itemClassName,
|
|
20
|
+
dragDisabled
|
|
21
|
+
}, item.id));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
exports.KanbanColumnCards = KanbanColumnCards;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { KanbanItem } from "./KanbanItem.js";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
|
|
7
|
+
//#region src/KanbanColumnCards.tsx
|
|
8
|
+
/**
|
|
9
|
+
* The card list of a column, rendered in full. Each card is a flex child of
|
|
10
|
+
* the column's scroller, so spacing comes from the scroller's `gap`.
|
|
11
|
+
*/
|
|
12
|
+
function KanbanColumnCards({ column, items, renderItem, itemClassName, dragDisabled = false }) {
|
|
13
|
+
return items.map((item) => /* @__PURE__ */ jsx(KanbanItem, {
|
|
14
|
+
item,
|
|
15
|
+
column,
|
|
16
|
+
renderItem,
|
|
17
|
+
className: itemClassName,
|
|
18
|
+
dragDisabled
|
|
19
|
+
}, item.id));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { KanbanColumnCards };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
5
|
+
let __dnd_kit_core = require("@dnd-kit/core");
|
|
6
|
+
__dnd_kit_core = require_rolldown_runtime.__toESM(__dnd_kit_core);
|
|
7
|
+
let __pixpilot_shadcn_ui = require("@pixpilot/shadcn-ui");
|
|
8
|
+
__pixpilot_shadcn_ui = require_rolldown_runtime.__toESM(__pixpilot_shadcn_ui);
|
|
9
|
+
let react = require("react");
|
|
10
|
+
react = require_rolldown_runtime.__toESM(react);
|
|
11
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
12
|
+
react_jsx_runtime = require_rolldown_runtime.__toESM(react_jsx_runtime);
|
|
13
|
+
|
|
14
|
+
//#region src/KanbanDragOverlay.tsx
|
|
15
|
+
/** Floating card/column preview that follows the cursor while dragging. */
|
|
16
|
+
function KanbanDragOverlay({ activeItem, activeItemColumn, activeColumn, activeColumnItemCount, renderItem, itemClassName, columnClassName }) {
|
|
17
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__dnd_kit_core.DragOverlay, { children: [activeItem && activeItemColumn ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
18
|
+
className: (0, __pixpilot_shadcn_ui.cn)("bg-background cursor-grabbing rounded-md border p-3 shadow-lg", itemClassName),
|
|
19
|
+
children: renderItem ? renderItem(activeItem, activeItemColumn) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
20
|
+
className: "text-sm",
|
|
21
|
+
children: activeItem.name
|
|
22
|
+
})
|
|
23
|
+
}) : null, activeColumn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24
|
+
className: (0, __pixpilot_shadcn_ui.cn)("bg-muted/40 flex min-w-[250px] flex-col rounded-lg border p-3 opacity-90 shadow-lg", columnClassName),
|
|
25
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
26
|
+
className: "mb-3 flex items-center justify-between px-1",
|
|
27
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
28
|
+
className: "text-sm font-semibold",
|
|
29
|
+
children: activeColumn.title
|
|
30
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31
|
+
className: "bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs",
|
|
32
|
+
children: activeColumnItemCount
|
|
33
|
+
})]
|
|
34
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35
|
+
className: "text-muted-foreground flex min-h-[60px] items-center justify-center text-xs",
|
|
36
|
+
children: "Dragging column…"
|
|
37
|
+
})]
|
|
38
|
+
}) : null] });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
exports.KanbanDragOverlay = KanbanDragOverlay;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { DragOverlay } from "@dnd-kit/core";
|
|
5
|
+
import { cn } from "@pixpilot/shadcn-ui";
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
|
|
9
|
+
//#region src/KanbanDragOverlay.tsx
|
|
10
|
+
/** Floating card/column preview that follows the cursor while dragging. */
|
|
11
|
+
function KanbanDragOverlay({ activeItem, activeItemColumn, activeColumn, activeColumnItemCount, renderItem, itemClassName, columnClassName }) {
|
|
12
|
+
return /* @__PURE__ */ jsxs(DragOverlay, { children: [activeItem && activeItemColumn ? /* @__PURE__ */ jsx("div", {
|
|
13
|
+
className: cn("bg-background cursor-grabbing rounded-md border p-3 shadow-lg", itemClassName),
|
|
14
|
+
children: renderItem ? renderItem(activeItem, activeItemColumn) : /* @__PURE__ */ jsx("span", {
|
|
15
|
+
className: "text-sm",
|
|
16
|
+
children: activeItem.name
|
|
17
|
+
})
|
|
18
|
+
}) : null, activeColumn ? /* @__PURE__ */ jsxs("div", {
|
|
19
|
+
className: cn("bg-muted/40 flex min-w-[250px] flex-col rounded-lg border p-3 opacity-90 shadow-lg", columnClassName),
|
|
20
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
21
|
+
className: "mb-3 flex items-center justify-between px-1",
|
|
22
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
23
|
+
className: "text-sm font-semibold",
|
|
24
|
+
children: activeColumn.title
|
|
25
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
26
|
+
className: "bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs",
|
|
27
|
+
children: activeColumnItemCount
|
|
28
|
+
})]
|
|
29
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
30
|
+
className: "text-muted-foreground flex min-h-[60px] items-center justify-center text-xs",
|
|
31
|
+
children: "Dragging column…"
|
|
32
|
+
})]
|
|
33
|
+
}) : null] });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { KanbanDragOverlay };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
5
|
+
let __dnd_kit_sortable = require("@dnd-kit/sortable");
|
|
6
|
+
__dnd_kit_sortable = require_rolldown_runtime.__toESM(__dnd_kit_sortable);
|
|
7
|
+
let __pixpilot_shadcn_ui = require("@pixpilot/shadcn-ui");
|
|
8
|
+
__pixpilot_shadcn_ui = require_rolldown_runtime.__toESM(__pixpilot_shadcn_ui);
|
|
9
|
+
let react = require("react");
|
|
10
|
+
react = require_rolldown_runtime.__toESM(react);
|
|
11
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
12
|
+
react_jsx_runtime = require_rolldown_runtime.__toESM(react_jsx_runtime);
|
|
13
|
+
let __dnd_kit_utilities = require("@dnd-kit/utilities");
|
|
14
|
+
__dnd_kit_utilities = require_rolldown_runtime.__toESM(__dnd_kit_utilities);
|
|
15
|
+
|
|
16
|
+
//#region src/KanbanItem.tsx
|
|
17
|
+
function KanbanItemContent({ item, column, renderItem }) {
|
|
18
|
+
return renderItem ? renderItem(item, column) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
19
|
+
className: "text-sm",
|
|
20
|
+
children: item.name
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const MemoizedKanbanItemContent = react.default.memo(KanbanItemContent);
|
|
24
|
+
/**
|
|
25
|
+
* A single draggable item inside a Kanban column.
|
|
26
|
+
* Wraps `useSortable` from `@dnd-kit/sortable`.
|
|
27
|
+
*/
|
|
28
|
+
function KanbanItem({ item, column, renderItem, className, dragDisabled = false }) {
|
|
29
|
+
const sortableData = react.default.useMemo(() => ({
|
|
30
|
+
type: "item",
|
|
31
|
+
item
|
|
32
|
+
}), [item]);
|
|
33
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = (0, __dnd_kit_sortable.useSortable)({
|
|
34
|
+
id: item.id,
|
|
35
|
+
data: sortableData,
|
|
36
|
+
disabled: dragDisabled
|
|
37
|
+
});
|
|
38
|
+
const style = {
|
|
39
|
+
transform: __dnd_kit_utilities.CSS.Transform.toString(transform),
|
|
40
|
+
transition: transition ?? void 0
|
|
41
|
+
};
|
|
42
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
43
|
+
ref: setNodeRef,
|
|
44
|
+
"data-testid": `kanban-item-${item.id}`,
|
|
45
|
+
style,
|
|
46
|
+
className: (0, __pixpilot_shadcn_ui.cn)("bg-background touch-none rounded-md border p-3 shadow-sm transition-shadow", "hover:shadow-md", dragDisabled ? "cursor-default" : "cursor-grab active:cursor-grabbing", isDragging && "z-50 opacity-50 shadow-lg", className),
|
|
47
|
+
...attributes,
|
|
48
|
+
...listeners,
|
|
49
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoizedKanbanItemContent, {
|
|
50
|
+
item,
|
|
51
|
+
column,
|
|
52
|
+
renderItem
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
exports.KanbanItem = KanbanItem;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { useSortable } from "@dnd-kit/sortable";
|
|
5
|
+
import { cn } from "@pixpilot/shadcn-ui";
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
9
|
+
|
|
10
|
+
//#region src/KanbanItem.tsx
|
|
11
|
+
function KanbanItemContent({ item, column, renderItem }) {
|
|
12
|
+
return renderItem ? renderItem(item, column) : /* @__PURE__ */ jsx("span", {
|
|
13
|
+
className: "text-sm",
|
|
14
|
+
children: item.name
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
const MemoizedKanbanItemContent = React.memo(KanbanItemContent);
|
|
18
|
+
/**
|
|
19
|
+
* A single draggable item inside a Kanban column.
|
|
20
|
+
* Wraps `useSortable` from `@dnd-kit/sortable`.
|
|
21
|
+
*/
|
|
22
|
+
function KanbanItem({ item, column, renderItem, className, dragDisabled = false }) {
|
|
23
|
+
const sortableData = React.useMemo(() => ({
|
|
24
|
+
type: "item",
|
|
25
|
+
item
|
|
26
|
+
}), [item]);
|
|
27
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
28
|
+
id: item.id,
|
|
29
|
+
data: sortableData,
|
|
30
|
+
disabled: dragDisabled
|
|
31
|
+
});
|
|
32
|
+
const style = {
|
|
33
|
+
transform: CSS.Transform.toString(transform),
|
|
34
|
+
transition: transition ?? void 0
|
|
35
|
+
};
|
|
36
|
+
return /* @__PURE__ */ jsx("div", {
|
|
37
|
+
ref: setNodeRef,
|
|
38
|
+
"data-testid": `kanban-item-${item.id}`,
|
|
39
|
+
style,
|
|
40
|
+
className: cn("bg-background touch-none rounded-md border p-3 shadow-sm transition-shadow", "hover:shadow-md", dragDisabled ? "cursor-default" : "cursor-grab active:cursor-grabbing", isDragging && "z-50 opacity-50 shadow-lg", className),
|
|
41
|
+
...attributes,
|
|
42
|
+
...listeners,
|
|
43
|
+
children: /* @__PURE__ */ jsx(MemoizedKanbanItemContent, {
|
|
44
|
+
item,
|
|
45
|
+
column,
|
|
46
|
+
renderItem
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { KanbanItem };
|