@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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/AddColumnButton.cjs +82 -0
  4. package/dist/AddColumnButton.js +77 -0
  5. package/dist/ColumnFilterButton.cjs +68 -0
  6. package/dist/ColumnFilterButton.js +63 -0
  7. package/dist/KanbanBoard.cjs +137 -0
  8. package/dist/KanbanBoard.d.cts +36 -0
  9. package/dist/KanbanBoard.d.ts +36 -0
  10. package/dist/KanbanBoard.js +131 -0
  11. package/dist/KanbanColumn.cjs +142 -0
  12. package/dist/KanbanColumn.js +134 -0
  13. package/dist/KanbanColumnCards.cjs +25 -0
  14. package/dist/KanbanColumnCards.js +23 -0
  15. package/dist/KanbanDragOverlay.cjs +42 -0
  16. package/dist/KanbanDragOverlay.js +37 -0
  17. package/dist/KanbanItem.cjs +58 -0
  18. package/dist/KanbanItem.js +52 -0
  19. package/dist/KanbanVirtualColumnCards.cjs +70 -0
  20. package/dist/KanbanVirtualColumnCards.js +66 -0
  21. package/dist/_virtual/rolldown_runtime.cjs +25 -0
  22. package/dist/hooks/use-kanban-board-state.cjs +43 -0
  23. package/dist/hooks/use-kanban-board-state.js +41 -0
  24. package/dist/hooks/use-kanban-card-drag.cjs +83 -0
  25. package/dist/hooks/use-kanban-card-drag.js +81 -0
  26. package/dist/hooks/use-kanban-collision-detection.cjs +70 -0
  27. package/dist/hooks/use-kanban-collision-detection.js +67 -0
  28. package/dist/hooks/use-kanban-column-reorder.cjs +31 -0
  29. package/dist/hooks/use-kanban-column-reorder.js +28 -0
  30. package/dist/hooks/use-kanban-drag.cjs +98 -0
  31. package/dist/hooks/use-kanban-drag.js +95 -0
  32. package/dist/hooks/use-kanban-filters.cjs +49 -0
  33. package/dist/hooks/use-kanban-filters.js +47 -0
  34. package/dist/index.cjs +3 -0
  35. package/dist/index.d.cts +5 -0
  36. package/dist/index.d.ts +5 -0
  37. package/dist/index.js +3 -0
  38. package/dist/infinite-scroll/InfiniteScrollSentinel.cjs +54 -0
  39. package/dist/infinite-scroll/InfiniteScrollSentinel.d.cts +2 -0
  40. package/dist/infinite-scroll/InfiniteScrollSentinel.d.ts +2 -0
  41. package/dist/infinite-scroll/InfiniteScrollSentinel.js +49 -0
  42. package/dist/infinite-scroll/index.d.cts +3 -0
  43. package/dist/infinite-scroll/index.d.ts +3 -0
  44. package/dist/infinite-scroll/types.d.cts +29 -0
  45. package/dist/infinite-scroll/types.d.ts +29 -0
  46. package/dist/infinite-scroll/use-intersection-observer.cjs +46 -0
  47. package/dist/infinite-scroll/use-intersection-observer.d.cts +1 -0
  48. package/dist/infinite-scroll/use-intersection-observer.d.ts +1 -0
  49. package/dist/infinite-scroll/use-intersection-observer.js +44 -0
  50. package/dist/types.d.cts +251 -0
  51. package/dist/types.d.ts +251 -0
  52. package/dist/utils/apply-kanban-drop.cjs +37 -0
  53. package/dist/utils/apply-kanban-drop.js +35 -0
  54. package/dist/utils/column-sortable-id.cjs +23 -0
  55. package/dist/utils/column-sortable-id.js +20 -0
  56. package/dist/utils/is-below-over-item.cjs +17 -0
  57. package/dist/utils/is-below-over-item.js +16 -0
  58. package/dist/utils/items-order-equal.cjs +13 -0
  59. package/dist/utils/items-order-equal.js +12 -0
  60. package/dist/utils/kanban-keyboard-coordinates.cjs +64 -0
  61. package/dist/utils/kanban-keyboard-coordinates.js +62 -0
  62. package/dist/utils/move-kanban-item.cjs +28 -0
  63. package/dist/utils/move-kanban-item.js +27 -0
  64. package/dist/utils/replace-state.cjs +15 -0
  65. package/dist/utils/replace-state.js +14 -0
  66. package/dist/utils/resolve-column-id.cjs +25 -0
  67. package/dist/utils/resolve-column-id.js +24 -0
  68. package/package.json +97 -0
@@ -0,0 +1,64 @@
1
+ const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
2
+ let __dnd_kit_core = require("@dnd-kit/core");
3
+ __dnd_kit_core = require_rolldown_runtime.__toESM(__dnd_kit_core);
4
+
5
+ //#region src/utils/kanban-keyboard-coordinates.ts
6
+ /** Which candidates an arrow key may move onto, relative to the dragged rect. */
7
+ const DIRECTION_FILTERS = {
8
+ [__dnd_kit_core.KeyboardCode.Down]: (collisionRect, rect) => collisionRect.top < rect.top,
9
+ [__dnd_kit_core.KeyboardCode.Up]: (collisionRect, rect) => collisionRect.top > rect.top,
10
+ [__dnd_kit_core.KeyboardCode.Left]: (collisionRect, rect) => collisionRect.left > rect.left,
11
+ [__dnd_kit_core.KeyboardCode.Right]: (collisionRect, rect) => collisionRect.left < rect.left
12
+ };
13
+ /**
14
+ * Keyboard coordinate getter for the Kanban board, adapted from dnd-kit's
15
+ * multiple-containers example.
16
+ *
17
+ * The stock `sortableKeyboardCoordinates` also targets the dragged card's own
18
+ * column drop zone, whose left edge sits just beside the card, so arrow keys
19
+ * could never leave the column. Here cards target other cards and empty
20
+ * columns only, and column drags target other columns.
21
+ */
22
+ const kanbanKeyboardCoordinates = (event, { context }) => {
23
+ const matchesDirection = DIRECTION_FILTERS[event.code];
24
+ if (!matchesDirection) return void 0;
25
+ event.preventDefault();
26
+ const { active, collisionRect, droppableRects, droppableContainers } = context;
27
+ if (!active || !collisionRect) return void 0;
28
+ const draggingColumn = active.data.current?.type === "column-sortable";
29
+ const enabled = droppableContainers.getEnabled();
30
+ const columnHasCards = (columnId) => enabled.some((entry) => {
31
+ const data = entry.data.current;
32
+ return data?.type === "item" && data.item?.columnId === columnId;
33
+ });
34
+ const isCandidate = (entry) => {
35
+ if (entry.id === active.id) return false;
36
+ const type = entry.data.current?.type;
37
+ if (draggingColumn) return type === "column-sortable";
38
+ if (type === "column-sortable") return false;
39
+ return !(type === "column" && columnHasCards(String(entry.id)));
40
+ };
41
+ const candidates = enabled.reduce((accumulator, container) => {
42
+ const rect = droppableRects.get(container.id);
43
+ if (rect && isCandidate(container) && matchesDirection(collisionRect, rect)) accumulator.push({
44
+ container,
45
+ rect
46
+ });
47
+ return accumulator;
48
+ }, []);
49
+ const closestId = (0, __dnd_kit_core.getFirstCollision)((0, __dnd_kit_core.closestCenter)({
50
+ active,
51
+ collisionRect,
52
+ droppableRects,
53
+ droppableContainers: candidates.map(({ container }) => container),
54
+ pointerCoordinates: null
55
+ }), "id");
56
+ const winner = candidates.find(({ container }) => container.id === closestId);
57
+ return winner ? {
58
+ x: winner.rect.left,
59
+ y: winner.rect.top
60
+ } : void 0;
61
+ };
62
+
63
+ //#endregion
64
+ exports.kanbanKeyboardCoordinates = kanbanKeyboardCoordinates;
@@ -0,0 +1,62 @@
1
+ import { KeyboardCode, closestCenter, getFirstCollision } from "@dnd-kit/core";
2
+
3
+ //#region src/utils/kanban-keyboard-coordinates.ts
4
+ /** Which candidates an arrow key may move onto, relative to the dragged rect. */
5
+ const DIRECTION_FILTERS = {
6
+ [KeyboardCode.Down]: (collisionRect, rect) => collisionRect.top < rect.top,
7
+ [KeyboardCode.Up]: (collisionRect, rect) => collisionRect.top > rect.top,
8
+ [KeyboardCode.Left]: (collisionRect, rect) => collisionRect.left > rect.left,
9
+ [KeyboardCode.Right]: (collisionRect, rect) => collisionRect.left < rect.left
10
+ };
11
+ /**
12
+ * Keyboard coordinate getter for the Kanban board, adapted from dnd-kit's
13
+ * multiple-containers example.
14
+ *
15
+ * The stock `sortableKeyboardCoordinates` also targets the dragged card's own
16
+ * column drop zone, whose left edge sits just beside the card, so arrow keys
17
+ * could never leave the column. Here cards target other cards and empty
18
+ * columns only, and column drags target other columns.
19
+ */
20
+ const kanbanKeyboardCoordinates = (event, { context }) => {
21
+ const matchesDirection = DIRECTION_FILTERS[event.code];
22
+ if (!matchesDirection) return void 0;
23
+ event.preventDefault();
24
+ const { active, collisionRect, droppableRects, droppableContainers } = context;
25
+ if (!active || !collisionRect) return void 0;
26
+ const draggingColumn = active.data.current?.type === "column-sortable";
27
+ const enabled = droppableContainers.getEnabled();
28
+ const columnHasCards = (columnId) => enabled.some((entry) => {
29
+ const data = entry.data.current;
30
+ return data?.type === "item" && data.item?.columnId === columnId;
31
+ });
32
+ const isCandidate = (entry) => {
33
+ if (entry.id === active.id) return false;
34
+ const type = entry.data.current?.type;
35
+ if (draggingColumn) return type === "column-sortable";
36
+ if (type === "column-sortable") return false;
37
+ return !(type === "column" && columnHasCards(String(entry.id)));
38
+ };
39
+ const candidates = enabled.reduce((accumulator, container) => {
40
+ const rect = droppableRects.get(container.id);
41
+ if (rect && isCandidate(container) && matchesDirection(collisionRect, rect)) accumulator.push({
42
+ container,
43
+ rect
44
+ });
45
+ return accumulator;
46
+ }, []);
47
+ const closestId = getFirstCollision(closestCenter({
48
+ active,
49
+ collisionRect,
50
+ droppableRects,
51
+ droppableContainers: candidates.map(({ container }) => container),
52
+ pointerCoordinates: null
53
+ }), "id");
54
+ const winner = candidates.find(({ container }) => container.id === closestId);
55
+ return winner ? {
56
+ x: winner.rect.left,
57
+ y: winner.rect.top
58
+ } : void 0;
59
+ };
60
+
61
+ //#endregion
62
+ export { kanbanKeyboardCoordinates };
@@ -0,0 +1,28 @@
1
+
2
+ //#region src/utils/move-kanban-item.ts
3
+ /**
4
+ * Returns a new item list with one card placed relative to the current drop target.
5
+ * The input is never mutated, so previews and final drops can use the same drag snapshot.
6
+ */
7
+ function moveKanbanItem({ items, activeId, overId, targetColumnId, insertAfter, overIsColumn }) {
8
+ const activeItem = items.find((item) => item.id === activeId);
9
+ if (!activeItem) return items;
10
+ const next = items.filter((item) => item.id !== activeId);
11
+ const movedItem = {
12
+ ...activeItem,
13
+ columnId: targetColumnId
14
+ };
15
+ if (!overIsColumn) {
16
+ const overIndex = next.findIndex((item) => item.id === overId);
17
+ if (overIndex !== -1) {
18
+ next.splice(overIndex + Number(insertAfter), 0, movedItem);
19
+ return next;
20
+ }
21
+ }
22
+ const lastIndexInColumn = next.reduce((last, item, index) => item.columnId === targetColumnId ? index : last, -1);
23
+ next.splice(lastIndexInColumn === -1 ? next.length : lastIndexInColumn + 1, 0, movedItem);
24
+ return next;
25
+ }
26
+
27
+ //#endregion
28
+ exports.moveKanbanItem = moveKanbanItem;
@@ -0,0 +1,27 @@
1
+ //#region src/utils/move-kanban-item.ts
2
+ /**
3
+ * Returns a new item list with one card placed relative to the current drop target.
4
+ * The input is never mutated, so previews and final drops can use the same drag snapshot.
5
+ */
6
+ function moveKanbanItem({ items, activeId, overId, targetColumnId, insertAfter, overIsColumn }) {
7
+ const activeItem = items.find((item) => item.id === activeId);
8
+ if (!activeItem) return items;
9
+ const next = items.filter((item) => item.id !== activeId);
10
+ const movedItem = {
11
+ ...activeItem,
12
+ columnId: targetColumnId
13
+ };
14
+ if (!overIsColumn) {
15
+ const overIndex = next.findIndex((item) => item.id === overId);
16
+ if (overIndex !== -1) {
17
+ next.splice(overIndex + Number(insertAfter), 0, movedItem);
18
+ return next;
19
+ }
20
+ }
21
+ const lastIndexInColumn = next.reduce((last, item, index) => item.columnId === targetColumnId ? index : last, -1);
22
+ next.splice(lastIndexInColumn === -1 ? next.length : lastIndexInColumn + 1, 0, movedItem);
23
+ return next;
24
+ }
25
+
26
+ //#endregion
27
+ export { moveKanbanItem };
@@ -0,0 +1,15 @@
1
+
2
+ //#region src/utils/replace-state.ts
3
+ /**
4
+ * Reducer for controlled values that are replaced wholesale.
5
+ *
6
+ * Used with `useReducer` instead of `useState` so prop mirroring happens via a
7
+ * dispatch, which React treats as an ordinary update even when triggered from
8
+ * an effect during a drag.
9
+ */
10
+ function replaceState(_current, next) {
11
+ return next;
12
+ }
13
+
14
+ //#endregion
15
+ exports.replaceState = replaceState;
@@ -0,0 +1,14 @@
1
+ //#region src/utils/replace-state.ts
2
+ /**
3
+ * Reducer for controlled values that are replaced wholesale.
4
+ *
5
+ * Used with `useReducer` instead of `useState` so prop mirroring happens via a
6
+ * dispatch, which React treats as an ordinary update even when triggered from
7
+ * an effect during a drag.
8
+ */
9
+ function replaceState(_current, next) {
10
+ return next;
11
+ }
12
+
13
+ //#endregion
14
+ export { replaceState };
@@ -0,0 +1,25 @@
1
+ const require_column_sortable_id = require('./column-sortable-id.cjs');
2
+
3
+ //#region src/utils/resolve-column-id.ts
4
+ /** Whether an id addresses a column as a whole rather than a single card. */
5
+ function isColumnDroppableId(id, columnIds) {
6
+ return columnIds.has(String(id)) || require_column_sortable_id.isColumnSortableId(id);
7
+ }
8
+ /**
9
+ * Resolves any droppable id — a card, a column drop zone, or a column-sortable
10
+ * handle — to the column it belongs to. Returns `undefined` for ids that match
11
+ * nothing on the board (e.g. a card that was removed mid-drag).
12
+ */
13
+ function resolveColumnId(id, columnIds, items) {
14
+ const key = String(id);
15
+ if (columnIds.has(key)) return key;
16
+ if (require_column_sortable_id.isColumnSortableId(id)) {
17
+ const columnId = require_column_sortable_id.extractColumnId(id);
18
+ return columnIds.has(columnId) ? columnId : void 0;
19
+ }
20
+ return items.find((item) => item.id === id)?.columnId;
21
+ }
22
+
23
+ //#endregion
24
+ exports.isColumnDroppableId = isColumnDroppableId;
25
+ exports.resolveColumnId = resolveColumnId;
@@ -0,0 +1,24 @@
1
+ import { extractColumnId, isColumnSortableId } from "./column-sortable-id.js";
2
+
3
+ //#region src/utils/resolve-column-id.ts
4
+ /** Whether an id addresses a column as a whole rather than a single card. */
5
+ function isColumnDroppableId(id, columnIds) {
6
+ return columnIds.has(String(id)) || isColumnSortableId(id);
7
+ }
8
+ /**
9
+ * Resolves any droppable id — a card, a column drop zone, or a column-sortable
10
+ * handle — to the column it belongs to. Returns `undefined` for ids that match
11
+ * nothing on the board (e.g. a card that was removed mid-drag).
12
+ */
13
+ function resolveColumnId(id, columnIds, items) {
14
+ const key = String(id);
15
+ if (columnIds.has(key)) return key;
16
+ if (isColumnSortableId(id)) {
17
+ const columnId = extractColumnId(id);
18
+ return columnIds.has(columnId) ? columnId : void 0;
19
+ }
20
+ return items.find((item) => item.id === id)?.columnId;
21
+ }
22
+
23
+ //#endregion
24
+ export { isColumnDroppableId, resolveColumnId };
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@pixpilot/shadcn-kanban",
3
+ "type": "module",
4
+ "version": "0.0.0",
5
+ "packageManager": "pnpm@10.33.0",
6
+ "description": "Accessible drag-and-drop Kanban board built with shadcn/ui and dnd-kit.",
7
+ "author": "m.doaie <m.doaie@hotmail.com>",
8
+ "license": "MIT",
9
+ "homepage": "https://github.com/pixpilot/shadcn-components",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/pixpilot/shadcn-components.git",
13
+ "directory": "packages/shadcn-kanban"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/pixpilot/shadcn-components/issues"
17
+ },
18
+ "keywords": [
19
+ "kanban",
20
+ "board",
21
+ "drag-and-drop",
22
+ "dnd-kit",
23
+ "shadcn",
24
+ "react"
25
+ ],
26
+ "sideEffects": false,
27
+ "exports": "./src/index.ts",
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js",
37
+ "require": "./dist/index.cjs"
38
+ }
39
+ },
40
+ "main": "./dist/index.cjs",
41
+ "module": "./dist/index.js",
42
+ "types": "./dist/index.d.ts"
43
+ },
44
+ "scripts": {
45
+ "prepublishOnly": "pnpm run clean && pnpm run build",
46
+ "clean": "git clean -xdf .cache .turbo dist",
47
+ "clean:all": "pnpm clean && git clean -xdf node_modules",
48
+ "build": "tsdown",
49
+ "build:watch": "tsdown --watch",
50
+ "test": "vitest --run --passWithNoTests",
51
+ "test:watch": "vitest --watch",
52
+ "test:ui": "vitest --ui --watch",
53
+ "test:ui:coverage": "vitest --ui --coverage --watch --hookTimeout=30000 --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.branches=0 --coverage.thresholds.statements=0",
54
+ "test:coverage": "vitest --coverage",
55
+ "typecheck": "tsc --noEmit",
56
+ "lint": "eslint",
57
+ "format": "prettier --check . --ignore-path ../../.gitignore --ignore-path ../../.prettierignore"
58
+ },
59
+ "peerDependencies": {
60
+ "react": "^18 || ^19",
61
+ "react-dom": "^18 || ^19"
62
+ },
63
+ "dependencies": {
64
+ "@dnd-kit/core": "^6.3.1",
65
+ "@dnd-kit/sortable": "^10.0.0",
66
+ "@dnd-kit/utilities": "^3.2.2",
67
+ "@pixpilot/shadcn-ui": "workspace:*",
68
+ "@tanstack/react-virtual": "^3.13.23",
69
+ "lucide-react": "catalog:prod"
70
+ },
71
+ "devDependencies": {
72
+ "@internal/eslint-config": "workspace:*",
73
+ "@internal/prettier-config": "workspace:*",
74
+ "@internal/tsconfig": "workspace:*",
75
+ "@internal/tsdown-config": "workspace:*",
76
+ "@internal/vitest-config": "workspace:*",
77
+ "@storybook/react": "catalog:dev",
78
+ "@testing-library/jest-dom": "catalog:dev",
79
+ "@testing-library/react": "catalog:dev",
80
+ "@types/node": "catalog:dev",
81
+ "@types/react": "catalog:dev",
82
+ "@types/react-dom": "catalog:dev",
83
+ "@vitest/coverage-v8": "catalog:dev",
84
+ "@vitest/ui": "catalog:dev",
85
+ "eslint": "catalog:dev",
86
+ "jsdom": "catalog:dev",
87
+ "postcss": "catalog:dev",
88
+ "react": "catalog:prod",
89
+ "react-dom": "catalog:prod",
90
+ "tailwindcss": "catalog:dev",
91
+ "tsdown": "catalog:dev",
92
+ "typescript": "catalog:dev",
93
+ "typescript-eslint": "catalog:dev",
94
+ "vitest": "catalog:dev"
95
+ },
96
+ "prettier": "@internal/prettier-config"
97
+ }