@pixpilot/shadcn-kanban 1.0.0 → 1.1.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/README.md +25 -8
- package/dist/KanbanBoard.cjs +17 -6
- package/dist/KanbanBoard.d.cts +3 -1
- package/dist/KanbanBoard.d.ts +3 -1
- package/dist/KanbanBoard.js +17 -6
- package/dist/KanbanColumn.cjs +13 -5
- package/dist/KanbanColumn.js +13 -5
- package/dist/KanbanColumnCards.cjs +3 -2
- package/dist/KanbanColumnCards.js +3 -2
- package/dist/KanbanItem.cjs +24 -2
- package/dist/KanbanItem.js +24 -2
- package/dist/KanbanVirtualColumnCards.cjs +3 -2
- package/dist/KanbanVirtualColumnCards.js +3 -2
- package/dist/hooks/use-kanban-drag.cjs +19 -3
- package/dist/hooks/use-kanban-drag.js +20 -4
- package/dist/hooks/use-kanban-press-feedback.cjs +68 -0
- package/dist/hooks/use-kanban-press-feedback.js +66 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/types.d.cts +77 -1
- package/dist/types.d.ts +77 -1
- package/dist/utils/kanban-touch-defaults.cjs +29 -0
- package/dist/utils/kanban-touch-defaults.js +25 -0
- package/dist/utils/resolve-column-snap.cjs +44 -0
- package/dist/utils/resolve-column-snap.js +43 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,14 +32,31 @@ assign `event.items` back to your state and the board settles.
|
|
|
32
32
|
|
|
33
33
|
## Documentation
|
|
34
34
|
|
|
35
|
-
| Document
|
|
36
|
-
|
|
|
37
|
-
| [docs/README.md](./docs/README.md)
|
|
38
|
-
| [docs/api.md](./docs/api.md)
|
|
39
|
-
| [docs/drag-and-drop.md](./docs/drag-and-drop.md)
|
|
40
|
-
| [docs/filters.md](./docs/filters.md)
|
|
41
|
-
| [docs/infinite-scroll.md](./docs/infinite-scroll.md)
|
|
42
|
-
| [docs/virtualization.md](./docs/virtualization.md)
|
|
35
|
+
| Document | Covers |
|
|
36
|
+
| ------------------------------------------------------ | -------------------------------------------------------------- |
|
|
37
|
+
| [docs/README.md](./docs/README.md) | Overview, layout contract and generics |
|
|
38
|
+
| [docs/api.md](./docs/api.md) | Every prop, type and callback |
|
|
39
|
+
| [docs/drag-and-drop.md](./docs/drag-and-drop.md) | How the drag state machine works, and why it is built this way |
|
|
40
|
+
| [docs/filters.md](./docs/filters.md) | Per-column filters, board-side and parent-side |
|
|
41
|
+
| [docs/infinite-scroll.md](./docs/infinite-scroll.md) | Loading a column's items a page at a time |
|
|
42
|
+
| [docs/virtualization.md](./docs/virtualization.md) | Mounting only the cards near a column's scroll window |
|
|
43
|
+
| [docs/touch-and-mobile.md](./docs/touch-and-mobile.md) | Hold-to-drag on touch, and columns as a swipeable slider |
|
|
44
|
+
|
|
45
|
+
## On a phone
|
|
46
|
+
|
|
47
|
+
Out of the box, and without any prop:
|
|
48
|
+
|
|
49
|
+
- **Swiping wins over dragging.** A card only becomes draggable after the finger
|
|
50
|
+
has rested on it for 250 ms, and rings while it is held. Anything shorter
|
|
51
|
+
stays a scroll, so reaching for the next column no longer drags a card along
|
|
52
|
+
with it.
|
|
53
|
+
- **Columns behave like a slider.** Below the `sm` breakpoint each column is a
|
|
54
|
+
CSS scroll-snap child roughly a screen wide, so a swipe moves exactly one
|
|
55
|
+
column and settles with the platform's own momentum.
|
|
56
|
+
|
|
57
|
+
Both are tunable — `touch` and `columnSnap` — and both are off the table for
|
|
58
|
+
mouse and keyboard, which are unchanged. See
|
|
59
|
+
[docs/touch-and-mobile.md](./docs/touch-and-mobile.md).
|
|
43
60
|
|
|
44
61
|
## License
|
|
45
62
|
|
package/dist/KanbanBoard.cjs
CHANGED
|
@@ -8,6 +8,7 @@ const require_use_kanban_drag = require('./hooks/use-kanban-drag.cjs');
|
|
|
8
8
|
const require_use_kanban_filters = require('./hooks/use-kanban-filters.cjs');
|
|
9
9
|
const require_KanbanColumn = require('./KanbanColumn.cjs');
|
|
10
10
|
const require_KanbanDragOverlay = require('./KanbanDragOverlay.cjs');
|
|
11
|
+
const require_resolve_column_snap = require('./utils/resolve-column-snap.cjs');
|
|
11
12
|
let __dnd_kit_core = require("@dnd-kit/core");
|
|
12
13
|
__dnd_kit_core = require_rolldown_runtime.__toESM(__dnd_kit_core);
|
|
13
14
|
let __dnd_kit_sortable = require("@dnd-kit/sortable");
|
|
@@ -47,7 +48,7 @@ function useScrollOnlyFeatureWarning(feature, consequence, requested, columnOver
|
|
|
47
48
|
* column definitions and calls `onChange` whenever an item is moved
|
|
48
49
|
* between (or within) columns.
|
|
49
50
|
*/
|
|
50
|
-
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 }) {
|
|
51
|
+
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, touch, columnSnap = true }) {
|
|
51
52
|
useScrollOnlyFeatureWarning("infiniteScroll", "The sentinel will not be rendered.", Boolean(infiniteScroll), columnOverflow);
|
|
52
53
|
useScrollOnlyFeatureWarning("virtualization", "Every card will be rendered.", Boolean(virtualization), columnOverflow);
|
|
53
54
|
const activeInfiniteScroll = columnOverflow === "scroll" ? infiniteScroll : void 0;
|
|
@@ -56,8 +57,10 @@ function KanbanBoard({ items: externalItems, columns, onChange, renderItem, rend
|
|
|
56
57
|
externalItems,
|
|
57
58
|
columns,
|
|
58
59
|
onChange,
|
|
59
|
-
onColumnChange
|
|
60
|
+
onColumnChange,
|
|
61
|
+
touch
|
|
60
62
|
});
|
|
63
|
+
const snap = require_resolve_column_snap.resolveColumnSnap(columnSnap, activeId !== null);
|
|
61
64
|
const { activeFilters, resolveFilters, handleToggleFilter, handleClearFilters } = require_use_kanban_filters.useKanbanFilters({
|
|
62
65
|
filters,
|
|
63
66
|
onFilterChange
|
|
@@ -98,14 +101,18 @@ function KanbanBoard({ items: externalItems, columns, onChange, renderItem, rend
|
|
|
98
101
|
strategy: __dnd_kit_sortable.horizontalListSortingStrategy,
|
|
99
102
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
100
103
|
"data-testid": "kanban-board",
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
"data-snapping": snap.enabled ? "" : void 0,
|
|
105
|
+
className: (0, __pixpilot_shadcn_ui.cn)("relative flex gap-4 overflow-x-auto overscroll-x-contain p-2", columnOverflow === "scroll" ? "h-full min-h-0" : "min-h-full", snap.boardClassName, className),
|
|
106
|
+
style: {
|
|
107
|
+
...snap.style,
|
|
108
|
+
...style
|
|
109
|
+
},
|
|
103
110
|
children: [columnItemsMap.map(({ column, colItems, colFilters, activeIds }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_KanbanColumn.KanbanColumn, {
|
|
104
111
|
column,
|
|
105
112
|
items: colItems,
|
|
106
113
|
renderItem,
|
|
107
114
|
renderColumnHeader,
|
|
108
|
-
columnClassName,
|
|
115
|
+
columnClassName: (0, __pixpilot_shadcn_ui.cn)(snap.columnClassName, columnClassName),
|
|
109
116
|
containerProps: getColumnProps?.(column),
|
|
110
117
|
hideHeader: hideColumnHeaders,
|
|
111
118
|
itemClassName,
|
|
@@ -116,10 +123,14 @@ function KanbanBoard({ items: externalItems, columns, onChange, renderItem, rend
|
|
|
116
123
|
infiniteScroll: activeInfiniteScroll,
|
|
117
124
|
virtualization: activeVirtualization,
|
|
118
125
|
dragDisabled,
|
|
126
|
+
touch,
|
|
119
127
|
activeItemId: activeItem?.id ?? null,
|
|
120
128
|
onToggleFilter: (filterId) => handleToggleFilter(column, filterId),
|
|
121
129
|
onClearFilters: () => handleClearFilters(column)
|
|
122
|
-
}, column.id)), allowAddColumn && onAddColumn ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_AddColumnButton.AddColumnButton, {
|
|
130
|
+
}, column.id)), allowAddColumn && onAddColumn ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_AddColumnButton.AddColumnButton, {
|
|
131
|
+
onAdd: onAddColumn,
|
|
132
|
+
className: snap.columnClassName
|
|
133
|
+
}) : null]
|
|
123
134
|
})
|
|
124
135
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_KanbanDragOverlay.KanbanDragOverlay, {
|
|
125
136
|
activeItem,
|
package/dist/KanbanBoard.d.cts
CHANGED
|
@@ -30,7 +30,9 @@ declare function KanbanBoard<T = Record<string, unknown>>({
|
|
|
30
30
|
onFilterChange,
|
|
31
31
|
infiniteScroll,
|
|
32
32
|
virtualization,
|
|
33
|
-
dragDisabled
|
|
33
|
+
dragDisabled,
|
|
34
|
+
touch,
|
|
35
|
+
columnSnap
|
|
34
36
|
}: KanbanBoardProps<T>): react_jsx_runtime0.JSX.Element;
|
|
35
37
|
//#endregion
|
|
36
38
|
export { KanbanBoard };
|
package/dist/KanbanBoard.d.ts
CHANGED
|
@@ -30,7 +30,9 @@ declare function KanbanBoard<T = Record<string, unknown>>({
|
|
|
30
30
|
onFilterChange,
|
|
31
31
|
infiniteScroll,
|
|
32
32
|
virtualization,
|
|
33
|
-
dragDisabled
|
|
33
|
+
dragDisabled,
|
|
34
|
+
touch,
|
|
35
|
+
columnSnap
|
|
34
36
|
}: KanbanBoardProps<T>): react_jsx_runtime0.JSX.Element;
|
|
35
37
|
//#endregion
|
|
36
38
|
export { KanbanBoard };
|
package/dist/KanbanBoard.js
CHANGED
|
@@ -7,6 +7,7 @@ import { useKanbanDrag } from "./hooks/use-kanban-drag.js";
|
|
|
7
7
|
import { useKanbanFilters } from "./hooks/use-kanban-filters.js";
|
|
8
8
|
import { KanbanColumn } from "./KanbanColumn.js";
|
|
9
9
|
import { KanbanDragOverlay } from "./KanbanDragOverlay.js";
|
|
10
|
+
import { resolveColumnSnap } from "./utils/resolve-column-snap.js";
|
|
10
11
|
import { DndContext, MeasuringStrategy } from "@dnd-kit/core";
|
|
11
12
|
import { SortableContext, horizontalListSortingStrategy } from "@dnd-kit/sortable";
|
|
12
13
|
import { cn } from "@pixpilot/shadcn-ui";
|
|
@@ -41,7 +42,7 @@ function useScrollOnlyFeatureWarning(feature, consequence, requested, columnOver
|
|
|
41
42
|
* column definitions and calls `onChange` whenever an item is moved
|
|
42
43
|
* between (or within) columns.
|
|
43
44
|
*/
|
|
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
|
+
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, touch, columnSnap = true }) {
|
|
45
46
|
useScrollOnlyFeatureWarning("infiniteScroll", "The sentinel will not be rendered.", Boolean(infiniteScroll), columnOverflow);
|
|
46
47
|
useScrollOnlyFeatureWarning("virtualization", "Every card will be rendered.", Boolean(virtualization), columnOverflow);
|
|
47
48
|
const activeInfiniteScroll = columnOverflow === "scroll" ? infiniteScroll : void 0;
|
|
@@ -50,8 +51,10 @@ function KanbanBoard({ items: externalItems, columns, onChange, renderItem, rend
|
|
|
50
51
|
externalItems,
|
|
51
52
|
columns,
|
|
52
53
|
onChange,
|
|
53
|
-
onColumnChange
|
|
54
|
+
onColumnChange,
|
|
55
|
+
touch
|
|
54
56
|
});
|
|
57
|
+
const snap = resolveColumnSnap(columnSnap, activeId !== null);
|
|
55
58
|
const { activeFilters, resolveFilters, handleToggleFilter, handleClearFilters } = useKanbanFilters({
|
|
56
59
|
filters,
|
|
57
60
|
onFilterChange
|
|
@@ -92,14 +95,18 @@ function KanbanBoard({ items: externalItems, columns, onChange, renderItem, rend
|
|
|
92
95
|
strategy: horizontalListSortingStrategy,
|
|
93
96
|
children: /* @__PURE__ */ jsxs("div", {
|
|
94
97
|
"data-testid": "kanban-board",
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
"data-snapping": snap.enabled ? "" : void 0,
|
|
99
|
+
className: cn("relative flex gap-4 overflow-x-auto overscroll-x-contain p-2", columnOverflow === "scroll" ? "h-full min-h-0" : "min-h-full", snap.boardClassName, className),
|
|
100
|
+
style: {
|
|
101
|
+
...snap.style,
|
|
102
|
+
...style
|
|
103
|
+
},
|
|
97
104
|
children: [columnItemsMap.map(({ column, colItems, colFilters, activeIds }) => /* @__PURE__ */ jsx(KanbanColumn, {
|
|
98
105
|
column,
|
|
99
106
|
items: colItems,
|
|
100
107
|
renderItem,
|
|
101
108
|
renderColumnHeader,
|
|
102
|
-
columnClassName,
|
|
109
|
+
columnClassName: cn(snap.columnClassName, columnClassName),
|
|
103
110
|
containerProps: getColumnProps?.(column),
|
|
104
111
|
hideHeader: hideColumnHeaders,
|
|
105
112
|
itemClassName,
|
|
@@ -110,10 +117,14 @@ function KanbanBoard({ items: externalItems, columns, onChange, renderItem, rend
|
|
|
110
117
|
infiniteScroll: activeInfiniteScroll,
|
|
111
118
|
virtualization: activeVirtualization,
|
|
112
119
|
dragDisabled,
|
|
120
|
+
touch,
|
|
113
121
|
activeItemId: activeItem?.id ?? null,
|
|
114
122
|
onToggleFilter: (filterId) => handleToggleFilter(column, filterId),
|
|
115
123
|
onClearFilters: () => handleClearFilters(column)
|
|
116
|
-
}, column.id)), allowAddColumn && onAddColumn ? /* @__PURE__ */ jsx(AddColumnButton, {
|
|
124
|
+
}, column.id)), allowAddColumn && onAddColumn ? /* @__PURE__ */ jsx(AddColumnButton, {
|
|
125
|
+
onAdd: onAddColumn,
|
|
126
|
+
className: snap.columnClassName
|
|
127
|
+
}) : null]
|
|
117
128
|
})
|
|
118
129
|
}), /* @__PURE__ */ jsx(KanbanDragOverlay, {
|
|
119
130
|
activeItem,
|
package/dist/KanbanColumn.cjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
5
5
|
const require_column_sortable_id = require('./utils/column-sortable-id.cjs');
|
|
6
|
+
const require_kanban_touch_defaults = require('./utils/kanban-touch-defaults.cjs');
|
|
6
7
|
const require_ColumnFilterButton = require('./ColumnFilterButton.cjs');
|
|
7
8
|
const require_InfiniteScrollSentinel = require('./infinite-scroll/InfiniteScrollSentinel.cjs');
|
|
8
9
|
const require_KanbanColumnCards = require('./KanbanColumnCards.cjs');
|
|
@@ -23,7 +24,7 @@ let __dnd_kit_utilities = require("@dnd-kit/utilities");
|
|
|
23
24
|
__dnd_kit_utilities = require_rolldown_runtime.__toESM(__dnd_kit_utilities);
|
|
24
25
|
|
|
25
26
|
//#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
|
+
function KanbanColumn({ column, items, renderItem, renderColumnHeader, columnClassName, itemClassName, containerProps, hideHeader = false, sortable = false, filters, activeFilterIds, columnOverflow, infiniteScroll, virtualization, dragDisabled = false, touch, activeItemId = null, onToggleFilter, onClearFilters }) {
|
|
27
28
|
const { className: containerClassName, style: containerStyle,...containerAttributes } = containerProps ?? {};
|
|
28
29
|
const filterButton = filters && filters.length > 0 && onToggleFilter && onClearFilters ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ColumnFilterButton.ColumnFilterButton, {
|
|
29
30
|
filters,
|
|
@@ -58,6 +59,7 @@ function KanbanColumn({ column, items, renderItem, renderColumnHeader, columnCla
|
|
|
58
59
|
}, [setDroppableRef]);
|
|
59
60
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
60
61
|
ref: setSortableRef,
|
|
62
|
+
"data-testid": `kanban-column-${column.id}`,
|
|
61
63
|
style: {
|
|
62
64
|
...containerStyle,
|
|
63
65
|
...style
|
|
@@ -70,7 +72,9 @@ function KanbanColumn({ column, items, renderItem, renderColumnHeader, columnCla
|
|
|
70
72
|
children: [
|
|
71
73
|
sortable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
72
74
|
type: "button",
|
|
73
|
-
|
|
75
|
+
"aria-label": `Reorder ${column.title} column`,
|
|
76
|
+
className: "text-muted-foreground hover:text-foreground -m-1 cursor-grab touch-none p-1 active:cursor-grabbing",
|
|
77
|
+
...require_kanban_touch_defaults.dragHandleProps,
|
|
74
78
|
...listeners,
|
|
75
79
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.GripVertical, { className: "h-4 w-4" })
|
|
76
80
|
}) : null,
|
|
@@ -86,7 +90,9 @@ function KanbanColumn({ column, items, renderItem, renderColumnHeader, columnCla
|
|
|
86
90
|
className: "flex items-center gap-1",
|
|
87
91
|
children: [sortable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
88
92
|
type: "button",
|
|
89
|
-
|
|
93
|
+
"aria-label": `Reorder ${column.title} column`,
|
|
94
|
+
className: "text-muted-foreground hover:text-foreground -m-1 cursor-grab touch-none p-1 active:cursor-grabbing",
|
|
95
|
+
...require_kanban_touch_defaults.dragHandleProps,
|
|
90
96
|
...listeners,
|
|
91
97
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.GripVertical, { className: "h-4 w-4" })
|
|
92
98
|
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
@@ -115,13 +121,15 @@ function KanbanColumn({ column, items, renderItem, renderColumnHeader, columnCla
|
|
|
115
121
|
scroller,
|
|
116
122
|
activeItemId,
|
|
117
123
|
options: virtualization,
|
|
118
|
-
dragDisabled
|
|
124
|
+
dragDisabled,
|
|
125
|
+
touch
|
|
119
126
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_KanbanColumnCards.KanbanColumnCards, {
|
|
120
127
|
column,
|
|
121
128
|
items,
|
|
122
129
|
renderItem,
|
|
123
130
|
itemClassName,
|
|
124
|
-
dragDisabled
|
|
131
|
+
dragDisabled,
|
|
132
|
+
touch
|
|
125
133
|
}), infiniteScroll ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_InfiniteScrollSentinel.InfiniteScrollSentinel, {
|
|
126
134
|
root: scroller,
|
|
127
135
|
distance: infiniteScroll.distance,
|
package/dist/KanbanColumn.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
import { toColumnSortableId } from "./utils/column-sortable-id.js";
|
|
5
|
+
import { dragHandleProps } from "./utils/kanban-touch-defaults.js";
|
|
5
6
|
import { ColumnFilterButton } from "./ColumnFilterButton.js";
|
|
6
7
|
import { InfiniteScrollSentinel } from "./infinite-scroll/InfiniteScrollSentinel.js";
|
|
7
8
|
import { KanbanColumnCards } from "./KanbanColumnCards.js";
|
|
@@ -15,7 +16,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
15
16
|
import { CSS } from "@dnd-kit/utilities";
|
|
16
17
|
|
|
17
18
|
//#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
|
+
function KanbanColumn({ column, items, renderItem, renderColumnHeader, columnClassName, itemClassName, containerProps, hideHeader = false, sortable = false, filters, activeFilterIds, columnOverflow, infiniteScroll, virtualization, dragDisabled = false, touch, activeItemId = null, onToggleFilter, onClearFilters }) {
|
|
19
20
|
const { className: containerClassName, style: containerStyle,...containerAttributes } = containerProps ?? {};
|
|
20
21
|
const filterButton = filters && filters.length > 0 && onToggleFilter && onClearFilters ? /* @__PURE__ */ jsx(ColumnFilterButton, {
|
|
21
22
|
filters,
|
|
@@ -50,6 +51,7 @@ function KanbanColumn({ column, items, renderItem, renderColumnHeader, columnCla
|
|
|
50
51
|
}, [setDroppableRef]);
|
|
51
52
|
return /* @__PURE__ */ jsxs("div", {
|
|
52
53
|
ref: setSortableRef,
|
|
54
|
+
"data-testid": `kanban-column-${column.id}`,
|
|
53
55
|
style: {
|
|
54
56
|
...containerStyle,
|
|
55
57
|
...style
|
|
@@ -62,7 +64,9 @@ function KanbanColumn({ column, items, renderItem, renderColumnHeader, columnCla
|
|
|
62
64
|
children: [
|
|
63
65
|
sortable ? /* @__PURE__ */ jsx("button", {
|
|
64
66
|
type: "button",
|
|
65
|
-
|
|
67
|
+
"aria-label": `Reorder ${column.title} column`,
|
|
68
|
+
className: "text-muted-foreground hover:text-foreground -m-1 cursor-grab touch-none p-1 active:cursor-grabbing",
|
|
69
|
+
...dragHandleProps,
|
|
66
70
|
...listeners,
|
|
67
71
|
children: /* @__PURE__ */ jsx(GripVertical, { className: "h-4 w-4" })
|
|
68
72
|
}) : null,
|
|
@@ -78,7 +82,9 @@ function KanbanColumn({ column, items, renderItem, renderColumnHeader, columnCla
|
|
|
78
82
|
className: "flex items-center gap-1",
|
|
79
83
|
children: [sortable ? /* @__PURE__ */ jsx("button", {
|
|
80
84
|
type: "button",
|
|
81
|
-
|
|
85
|
+
"aria-label": `Reorder ${column.title} column`,
|
|
86
|
+
className: "text-muted-foreground hover:text-foreground -m-1 cursor-grab touch-none p-1 active:cursor-grabbing",
|
|
87
|
+
...dragHandleProps,
|
|
82
88
|
...listeners,
|
|
83
89
|
children: /* @__PURE__ */ jsx(GripVertical, { className: "h-4 w-4" })
|
|
84
90
|
}) : null, /* @__PURE__ */ jsx("h3", {
|
|
@@ -107,13 +113,15 @@ function KanbanColumn({ column, items, renderItem, renderColumnHeader, columnCla
|
|
|
107
113
|
scroller,
|
|
108
114
|
activeItemId,
|
|
109
115
|
options: virtualization,
|
|
110
|
-
dragDisabled
|
|
116
|
+
dragDisabled,
|
|
117
|
+
touch
|
|
111
118
|
}) : /* @__PURE__ */ jsx(KanbanColumnCards, {
|
|
112
119
|
column,
|
|
113
120
|
items,
|
|
114
121
|
renderItem,
|
|
115
122
|
itemClassName,
|
|
116
|
-
dragDisabled
|
|
123
|
+
dragDisabled,
|
|
124
|
+
touch
|
|
117
125
|
}), infiniteScroll ? /* @__PURE__ */ jsx(InfiniteScrollSentinel, {
|
|
118
126
|
root: scroller,
|
|
119
127
|
distance: infiniteScroll.distance,
|
|
@@ -11,13 +11,14 @@ react_jsx_runtime = require_rolldown_runtime.__toESM(react_jsx_runtime);
|
|
|
11
11
|
* The card list of a column, rendered in full. Each card is a flex child of
|
|
12
12
|
* the column's scroller, so spacing comes from the scroller's `gap`.
|
|
13
13
|
*/
|
|
14
|
-
function KanbanColumnCards({ column, items, renderItem, itemClassName, dragDisabled = false }) {
|
|
14
|
+
function KanbanColumnCards({ column, items, renderItem, itemClassName, dragDisabled = false, touch }) {
|
|
15
15
|
return items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_KanbanItem.KanbanItem, {
|
|
16
16
|
item,
|
|
17
17
|
column,
|
|
18
18
|
renderItem,
|
|
19
19
|
className: itemClassName,
|
|
20
|
-
dragDisabled
|
|
20
|
+
dragDisabled,
|
|
21
|
+
touch
|
|
21
22
|
}, item.id));
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -9,13 +9,14 @@ import { jsx } from "react/jsx-runtime";
|
|
|
9
9
|
* The card list of a column, rendered in full. Each card is a flex child of
|
|
10
10
|
* the column's scroller, so spacing comes from the scroller's `gap`.
|
|
11
11
|
*/
|
|
12
|
-
function KanbanColumnCards({ column, items, renderItem, itemClassName, dragDisabled = false }) {
|
|
12
|
+
function KanbanColumnCards({ column, items, renderItem, itemClassName, dragDisabled = false, touch }) {
|
|
13
13
|
return items.map((item) => /* @__PURE__ */ jsx(KanbanItem, {
|
|
14
14
|
item,
|
|
15
15
|
column,
|
|
16
16
|
renderItem,
|
|
17
17
|
className: itemClassName,
|
|
18
|
-
dragDisabled
|
|
18
|
+
dragDisabled,
|
|
19
|
+
touch
|
|
19
20
|
}, item.id));
|
|
20
21
|
}
|
|
21
22
|
|
package/dist/KanbanItem.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
5
|
+
const require_use_kanban_press_feedback = require('./hooks/use-kanban-press-feedback.cjs');
|
|
5
6
|
let __dnd_kit_sortable = require("@dnd-kit/sortable");
|
|
6
7
|
__dnd_kit_sortable = require_rolldown_runtime.__toESM(__dnd_kit_sortable);
|
|
7
8
|
let __pixpilot_shadcn_ui = require("@pixpilot/shadcn-ui");
|
|
@@ -21,11 +22,24 @@ function KanbanItemContent({ item, column, renderItem }) {
|
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
const MemoizedKanbanItemContent = react.default.memo(KanbanItemContent);
|
|
25
|
+
/** Runs the sortable's own touch listener first, then the feedback one. */
|
|
26
|
+
function chainTouchHandlers(listeners, pressHandlers) {
|
|
27
|
+
const chained = {};
|
|
28
|
+
for (const key of Object.keys(pressHandlers)) {
|
|
29
|
+
const existing = listeners?.[key];
|
|
30
|
+
const added = pressHandlers[key];
|
|
31
|
+
chained[key] = (event) => {
|
|
32
|
+
existing?.(event);
|
|
33
|
+
added(event);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return chained;
|
|
37
|
+
}
|
|
24
38
|
/**
|
|
25
39
|
* A single draggable item inside a Kanban column.
|
|
26
40
|
* Wraps `useSortable` from `@dnd-kit/sortable`.
|
|
27
41
|
*/
|
|
28
|
-
function KanbanItem({ item, column, renderItem, className, dragDisabled = false }) {
|
|
42
|
+
function KanbanItem({ item, column, renderItem, className, dragDisabled = false, touch }) {
|
|
29
43
|
const sortableData = react.default.useMemo(() => ({
|
|
30
44
|
type: "item",
|
|
31
45
|
item
|
|
@@ -35,6 +49,12 @@ function KanbanItem({ item, column, renderItem, className, dragDisabled = false
|
|
|
35
49
|
data: sortableData,
|
|
36
50
|
disabled: dragDisabled
|
|
37
51
|
});
|
|
52
|
+
const { isPressing, handlers: pressHandlers } = require_use_kanban_press_feedback.useKanbanPressFeedback({
|
|
53
|
+
delay: touch?.dragActivationDelay,
|
|
54
|
+
tolerance: touch?.dragActivationTolerance,
|
|
55
|
+
disabled: dragDisabled || touch?.pressFeedback === false
|
|
56
|
+
});
|
|
57
|
+
const touchHandlers = react.default.useMemo(() => chainTouchHandlers(listeners, pressHandlers), [listeners, pressHandlers]);
|
|
38
58
|
const style = {
|
|
39
59
|
transform: __dnd_kit_utilities.CSS.Transform.toString(transform),
|
|
40
60
|
transition: transition ?? void 0
|
|
@@ -42,10 +62,12 @@ function KanbanItem({ item, column, renderItem, className, dragDisabled = false
|
|
|
42
62
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
43
63
|
ref: setNodeRef,
|
|
44
64
|
"data-testid": `kanban-item-${item.id}`,
|
|
65
|
+
"data-pressing": isPressing ? "" : void 0,
|
|
45
66
|
style,
|
|
46
|
-
className: (0, __pixpilot_shadcn_ui.cn)("bg-background touch-
|
|
67
|
+
className: (0, __pixpilot_shadcn_ui.cn)("bg-background touch-manipulation rounded-md border p-3 shadow-sm select-none", "[-webkit-touch-callout:none]", "transition-[box-shadow,scale] hover:shadow-md", dragDisabled ? "cursor-default" : "cursor-grab active:cursor-grabbing", isPressing && "ring-primary/40 scale-[1.02] shadow-md ring-2", isDragging && "z-50 opacity-50 shadow-lg", className),
|
|
47
68
|
...attributes,
|
|
48
69
|
...listeners,
|
|
70
|
+
...touchHandlers,
|
|
49
71
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoizedKanbanItemContent, {
|
|
50
72
|
item,
|
|
51
73
|
column,
|
package/dist/KanbanItem.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
import { useKanbanPressFeedback } from "./hooks/use-kanban-press-feedback.js";
|
|
4
5
|
import { useSortable } from "@dnd-kit/sortable";
|
|
5
6
|
import { cn } from "@pixpilot/shadcn-ui";
|
|
6
7
|
import React from "react";
|
|
@@ -15,11 +16,24 @@ function KanbanItemContent({ item, column, renderItem }) {
|
|
|
15
16
|
});
|
|
16
17
|
}
|
|
17
18
|
const MemoizedKanbanItemContent = React.memo(KanbanItemContent);
|
|
19
|
+
/** Runs the sortable's own touch listener first, then the feedback one. */
|
|
20
|
+
function chainTouchHandlers(listeners, pressHandlers) {
|
|
21
|
+
const chained = {};
|
|
22
|
+
for (const key of Object.keys(pressHandlers)) {
|
|
23
|
+
const existing = listeners?.[key];
|
|
24
|
+
const added = pressHandlers[key];
|
|
25
|
+
chained[key] = (event) => {
|
|
26
|
+
existing?.(event);
|
|
27
|
+
added(event);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return chained;
|
|
31
|
+
}
|
|
18
32
|
/**
|
|
19
33
|
* A single draggable item inside a Kanban column.
|
|
20
34
|
* Wraps `useSortable` from `@dnd-kit/sortable`.
|
|
21
35
|
*/
|
|
22
|
-
function KanbanItem({ item, column, renderItem, className, dragDisabled = false }) {
|
|
36
|
+
function KanbanItem({ item, column, renderItem, className, dragDisabled = false, touch }) {
|
|
23
37
|
const sortableData = React.useMemo(() => ({
|
|
24
38
|
type: "item",
|
|
25
39
|
item
|
|
@@ -29,6 +43,12 @@ function KanbanItem({ item, column, renderItem, className, dragDisabled = false
|
|
|
29
43
|
data: sortableData,
|
|
30
44
|
disabled: dragDisabled
|
|
31
45
|
});
|
|
46
|
+
const { isPressing, handlers: pressHandlers } = useKanbanPressFeedback({
|
|
47
|
+
delay: touch?.dragActivationDelay,
|
|
48
|
+
tolerance: touch?.dragActivationTolerance,
|
|
49
|
+
disabled: dragDisabled || touch?.pressFeedback === false
|
|
50
|
+
});
|
|
51
|
+
const touchHandlers = React.useMemo(() => chainTouchHandlers(listeners, pressHandlers), [listeners, pressHandlers]);
|
|
32
52
|
const style = {
|
|
33
53
|
transform: CSS.Transform.toString(transform),
|
|
34
54
|
transition: transition ?? void 0
|
|
@@ -36,10 +56,12 @@ function KanbanItem({ item, column, renderItem, className, dragDisabled = false
|
|
|
36
56
|
return /* @__PURE__ */ jsx("div", {
|
|
37
57
|
ref: setNodeRef,
|
|
38
58
|
"data-testid": `kanban-item-${item.id}`,
|
|
59
|
+
"data-pressing": isPressing ? "" : void 0,
|
|
39
60
|
style,
|
|
40
|
-
className: cn("bg-background touch-
|
|
61
|
+
className: cn("bg-background touch-manipulation rounded-md border p-3 shadow-sm select-none", "[-webkit-touch-callout:none]", "transition-[box-shadow,scale] hover:shadow-md", dragDisabled ? "cursor-default" : "cursor-grab active:cursor-grabbing", isPressing && "ring-primary/40 scale-[1.02] shadow-md ring-2", isDragging && "z-50 opacity-50 shadow-lg", className),
|
|
41
62
|
...attributes,
|
|
42
63
|
...listeners,
|
|
64
|
+
...touchHandlers,
|
|
43
65
|
children: /* @__PURE__ */ jsx(MemoizedKanbanItemContent, {
|
|
44
66
|
item,
|
|
45
67
|
column,
|
|
@@ -24,7 +24,7 @@ const DEFAULT_GAP = 8;
|
|
|
24
24
|
* wrapper carries the virtual offset, the card inside carries whatever
|
|
25
25
|
* dnd-kit's sortable is animating.
|
|
26
26
|
*/
|
|
27
|
-
function KanbanVirtualColumnCards({ column, items, renderItem, itemClassName, scroller, activeItemId, options, dragDisabled = false }) {
|
|
27
|
+
function KanbanVirtualColumnCards({ column, items, renderItem, itemClassName, scroller, activeItemId, options, dragDisabled = false, touch }) {
|
|
28
28
|
const { estimateItemHeight = DEFAULT_ESTIMATED_ITEM_HEIGHT, overscan = DEFAULT_OVERSCAN, gap = DEFAULT_GAP } = options;
|
|
29
29
|
const activeIndex = react.default.useMemo(() => activeItemId == null ? -1 : items.findIndex((item) => item.id === String(activeItemId)), [items, activeItemId]);
|
|
30
30
|
const rangeExtractor = react.default.useCallback((range) => {
|
|
@@ -59,7 +59,8 @@ function KanbanVirtualColumnCards({ column, items, renderItem, itemClassName, sc
|
|
|
59
59
|
column,
|
|
60
60
|
renderItem,
|
|
61
61
|
className: itemClassName,
|
|
62
|
-
dragDisabled
|
|
62
|
+
dragDisabled,
|
|
63
|
+
touch
|
|
63
64
|
})
|
|
64
65
|
}, virtualItem.key);
|
|
65
66
|
})
|
|
@@ -20,7 +20,7 @@ const DEFAULT_GAP = 8;
|
|
|
20
20
|
* wrapper carries the virtual offset, the card inside carries whatever
|
|
21
21
|
* dnd-kit's sortable is animating.
|
|
22
22
|
*/
|
|
23
|
-
function KanbanVirtualColumnCards({ column, items, renderItem, itemClassName, scroller, activeItemId, options, dragDisabled = false }) {
|
|
23
|
+
function KanbanVirtualColumnCards({ column, items, renderItem, itemClassName, scroller, activeItemId, options, dragDisabled = false, touch }) {
|
|
24
24
|
const { estimateItemHeight = DEFAULT_ESTIMATED_ITEM_HEIGHT, overscan = DEFAULT_OVERSCAN, gap = DEFAULT_GAP } = options;
|
|
25
25
|
const activeIndex = React.useMemo(() => activeItemId == null ? -1 : items.findIndex((item) => item.id === String(activeItemId)), [items, activeItemId]);
|
|
26
26
|
const rangeExtractor = React.useCallback((range) => {
|
|
@@ -55,7 +55,8 @@ function KanbanVirtualColumnCards({ column, items, renderItem, itemClassName, sc
|
|
|
55
55
|
column,
|
|
56
56
|
renderItem,
|
|
57
57
|
className: itemClassName,
|
|
58
|
-
dragDisabled
|
|
58
|
+
dragDisabled,
|
|
59
|
+
touch
|
|
59
60
|
})
|
|
60
61
|
}, virtualItem.key);
|
|
61
62
|
})
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
5
5
|
const require_column_sortable_id = require('../utils/column-sortable-id.cjs');
|
|
6
6
|
const require_kanban_keyboard_coordinates = require('../utils/kanban-keyboard-coordinates.cjs');
|
|
7
|
+
const require_kanban_touch_defaults = require('../utils/kanban-touch-defaults.cjs');
|
|
7
8
|
const require_use_kanban_board_state = require('./use-kanban-board-state.cjs');
|
|
8
9
|
const require_use_kanban_card_drag = require('./use-kanban-card-drag.cjs');
|
|
9
10
|
const require_use_kanban_collision_detection = require('./use-kanban-collision-detection.cjs');
|
|
@@ -14,13 +15,22 @@ let react = require("react");
|
|
|
14
15
|
react = require_rolldown_runtime.__toESM(react);
|
|
15
16
|
|
|
16
17
|
//#region src/hooks/use-kanban-drag.ts
|
|
17
|
-
const
|
|
18
|
+
const MOUSE_ACTIVATION_DISTANCE = 5;
|
|
19
|
+
/**
|
|
20
|
+
* An explicit drag handle is already an unambiguous gesture, so it skips the
|
|
21
|
+
* hold and picks up on contact. Only cards — which share their hit area with
|
|
22
|
+
* the board's own scrolling — have to earn the drag.
|
|
23
|
+
*/
|
|
24
|
+
function isOnDragHandle(event) {
|
|
25
|
+
const { target } = event;
|
|
26
|
+
return target instanceof Element && target.closest(require_kanban_touch_defaults.DRAG_HANDLE_SELECTOR) !== null;
|
|
27
|
+
}
|
|
18
28
|
/**
|
|
19
29
|
* Drag-and-drop state machine for the Kanban board, following dnd-kit's
|
|
20
30
|
* multiple-containers pattern. Owns the shared drag state and routes each
|
|
21
31
|
* event to either the card handlers or the column reorder handler.
|
|
22
32
|
*/
|
|
23
|
-
function useKanbanDrag({ externalItems, columns, onChange, onColumnChange }) {
|
|
33
|
+
function useKanbanDrag({ externalItems, columns, onChange, onColumnChange, touch }) {
|
|
24
34
|
const [activeId, setActiveId] = react.default.useState(null);
|
|
25
35
|
const isDragActiveRef = react.default.useRef(false);
|
|
26
36
|
const dragStartItemsRef = react.default.useRef(externalItems);
|
|
@@ -38,7 +48,13 @@ function useKanbanDrag({ externalItems, columns, onChange, onColumnChange }) {
|
|
|
38
48
|
return () => cancelAnimationFrame(frame);
|
|
39
49
|
}, [items]);
|
|
40
50
|
const columnIds = react.default.useMemo(() => new Set(internalColumns.map((column) => column.id)), [internalColumns]);
|
|
41
|
-
const sensors = (0, __dnd_kit_core.useSensors)((0, __dnd_kit_core.useSensor)(__dnd_kit_core.
|
|
51
|
+
const sensors = (0, __dnd_kit_core.useSensors)((0, __dnd_kit_core.useSensor)(__dnd_kit_core.MouseSensor, { activationConstraint: { distance: MOUSE_ACTIVATION_DISTANCE } }), (0, __dnd_kit_core.useSensor)(__dnd_kit_core.TouchSensor, {
|
|
52
|
+
activationConstraint: {
|
|
53
|
+
delay: touch?.dragActivationDelay ?? require_kanban_touch_defaults.TOUCH_ACTIVATION_DELAY,
|
|
54
|
+
tolerance: touch?.dragActivationTolerance ?? require_kanban_touch_defaults.TOUCH_ACTIVATION_TOLERANCE
|
|
55
|
+
},
|
|
56
|
+
bypassActivationConstraint: ({ event }) => isOnDragHandle(event)
|
|
57
|
+
}), (0, __dnd_kit_core.useSensor)(__dnd_kit_core.KeyboardSensor, { coordinateGetter: require_kanban_keyboard_coordinates.kanbanKeyboardCoordinates }));
|
|
42
58
|
const collisionDetection = require_use_kanban_collision_detection.useKanbanCollisionDetection({
|
|
43
59
|
columnIds,
|
|
44
60
|
itemsRef,
|
|
@@ -3,21 +3,31 @@
|
|
|
3
3
|
|
|
4
4
|
import { isColumnSortableId } from "../utils/column-sortable-id.js";
|
|
5
5
|
import { kanbanKeyboardCoordinates } from "../utils/kanban-keyboard-coordinates.js";
|
|
6
|
+
import { DRAG_HANDLE_SELECTOR, TOUCH_ACTIVATION_DELAY, TOUCH_ACTIVATION_TOLERANCE } from "../utils/kanban-touch-defaults.js";
|
|
6
7
|
import { useKanbanBoardState } from "./use-kanban-board-state.js";
|
|
7
8
|
import { useKanbanCardDrag } from "./use-kanban-card-drag.js";
|
|
8
9
|
import { useKanbanCollisionDetection } from "./use-kanban-collision-detection.js";
|
|
9
10
|
import { useKanbanColumnReorder } from "./use-kanban-column-reorder.js";
|
|
10
|
-
import { KeyboardSensor,
|
|
11
|
+
import { KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
11
12
|
import React from "react";
|
|
12
13
|
|
|
13
14
|
//#region src/hooks/use-kanban-drag.ts
|
|
14
|
-
const
|
|
15
|
+
const MOUSE_ACTIVATION_DISTANCE = 5;
|
|
16
|
+
/**
|
|
17
|
+
* An explicit drag handle is already an unambiguous gesture, so it skips the
|
|
18
|
+
* hold and picks up on contact. Only cards — which share their hit area with
|
|
19
|
+
* the board's own scrolling — have to earn the drag.
|
|
20
|
+
*/
|
|
21
|
+
function isOnDragHandle(event) {
|
|
22
|
+
const { target } = event;
|
|
23
|
+
return target instanceof Element && target.closest(DRAG_HANDLE_SELECTOR) !== null;
|
|
24
|
+
}
|
|
15
25
|
/**
|
|
16
26
|
* Drag-and-drop state machine for the Kanban board, following dnd-kit's
|
|
17
27
|
* multiple-containers pattern. Owns the shared drag state and routes each
|
|
18
28
|
* event to either the card handlers or the column reorder handler.
|
|
19
29
|
*/
|
|
20
|
-
function useKanbanDrag({ externalItems, columns, onChange, onColumnChange }) {
|
|
30
|
+
function useKanbanDrag({ externalItems, columns, onChange, onColumnChange, touch }) {
|
|
21
31
|
const [activeId, setActiveId] = React.useState(null);
|
|
22
32
|
const isDragActiveRef = React.useRef(false);
|
|
23
33
|
const dragStartItemsRef = React.useRef(externalItems);
|
|
@@ -35,7 +45,13 @@ function useKanbanDrag({ externalItems, columns, onChange, onColumnChange }) {
|
|
|
35
45
|
return () => cancelAnimationFrame(frame);
|
|
36
46
|
}, [items]);
|
|
37
47
|
const columnIds = React.useMemo(() => new Set(internalColumns.map((column) => column.id)), [internalColumns]);
|
|
38
|
-
const sensors = useSensors(useSensor(
|
|
48
|
+
const sensors = useSensors(useSensor(MouseSensor, { activationConstraint: { distance: MOUSE_ACTIVATION_DISTANCE } }), useSensor(TouchSensor, {
|
|
49
|
+
activationConstraint: {
|
|
50
|
+
delay: touch?.dragActivationDelay ?? TOUCH_ACTIVATION_DELAY,
|
|
51
|
+
tolerance: touch?.dragActivationTolerance ?? TOUCH_ACTIVATION_TOLERANCE
|
|
52
|
+
},
|
|
53
|
+
bypassActivationConstraint: ({ event }) => isOnDragHandle(event)
|
|
54
|
+
}), useSensor(KeyboardSensor, { coordinateGetter: kanbanKeyboardCoordinates }));
|
|
39
55
|
const collisionDetection = useKanbanCollisionDetection({
|
|
40
56
|
columnIds,
|
|
41
57
|
itemsRef,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
5
|
+
const require_kanban_touch_defaults = require('../utils/kanban-touch-defaults.cjs');
|
|
6
|
+
let react = require("react");
|
|
7
|
+
react = require_rolldown_runtime.__toESM(react);
|
|
8
|
+
|
|
9
|
+
//#region src/hooks/use-kanban-press-feedback.ts
|
|
10
|
+
/**
|
|
11
|
+
* Mirrors the touch sensor's activation window so a held card can show that it
|
|
12
|
+
* is being held.
|
|
13
|
+
*
|
|
14
|
+
* dnd-kit only reports a drag once it has already started, which on touch is
|
|
15
|
+
* one delay too late: without feedback the hold feels like nothing is
|
|
16
|
+
* happening, and the user lets go before the drag ever arms. This watches the
|
|
17
|
+
* same touch stream under the same constraints and reports the in-between
|
|
18
|
+
* state, so the card can acknowledge the press while it is still a press.
|
|
19
|
+
*
|
|
20
|
+
* It never drives the drag itself — dnd-kit remains the only thing that decides
|
|
21
|
+
* whether a gesture becomes one. The worst a desync can do is show or hide a
|
|
22
|
+
* ring.
|
|
23
|
+
*/
|
|
24
|
+
function useKanbanPressFeedback({ delay = require_kanban_touch_defaults.TOUCH_ACTIVATION_DELAY, tolerance = require_kanban_touch_defaults.TOUCH_ACTIVATION_TOLERANCE, disabled = false } = {}) {
|
|
25
|
+
const [isPressing, setIsPressing] = react.default.useState(false);
|
|
26
|
+
const originRef = react.default.useRef(null);
|
|
27
|
+
const timeoutRef = react.default.useRef(null);
|
|
28
|
+
const clear = react.default.useCallback(() => {
|
|
29
|
+
if (timeoutRef.current !== null) {
|
|
30
|
+
clearTimeout(timeoutRef.current);
|
|
31
|
+
timeoutRef.current = null;
|
|
32
|
+
}
|
|
33
|
+
originRef.current = null;
|
|
34
|
+
setIsPressing(false);
|
|
35
|
+
}, []);
|
|
36
|
+
react.default.useEffect(() => clear, [clear]);
|
|
37
|
+
return {
|
|
38
|
+
isPressing,
|
|
39
|
+
handlers: react.default.useMemo(() => ({
|
|
40
|
+
onTouchStart: (event) => {
|
|
41
|
+
const touch = event.touches[0];
|
|
42
|
+
if (disabled || !touch || event.touches.length > 1) return;
|
|
43
|
+
originRef.current = {
|
|
44
|
+
x: touch.clientX,
|
|
45
|
+
y: touch.clientY
|
|
46
|
+
};
|
|
47
|
+
setIsPressing(true);
|
|
48
|
+
timeoutRef.current = setTimeout(() => setIsPressing(false), delay);
|
|
49
|
+
},
|
|
50
|
+
onTouchMove: (event) => {
|
|
51
|
+
const origin = originRef.current;
|
|
52
|
+
const touch = event.touches[0];
|
|
53
|
+
if (!origin || !touch) return;
|
|
54
|
+
if (Math.abs(touch.clientX - origin.x) > tolerance || Math.abs(touch.clientY - origin.y) > tolerance) clear();
|
|
55
|
+
},
|
|
56
|
+
onTouchEnd: clear,
|
|
57
|
+
onTouchCancel: clear
|
|
58
|
+
}), [
|
|
59
|
+
clear,
|
|
60
|
+
delay,
|
|
61
|
+
tolerance,
|
|
62
|
+
disabled
|
|
63
|
+
])
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
exports.useKanbanPressFeedback = useKanbanPressFeedback;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { TOUCH_ACTIVATION_DELAY, TOUCH_ACTIVATION_TOLERANCE } from "../utils/kanban-touch-defaults.js";
|
|
5
|
+
import React from "react";
|
|
6
|
+
|
|
7
|
+
//#region src/hooks/use-kanban-press-feedback.ts
|
|
8
|
+
/**
|
|
9
|
+
* Mirrors the touch sensor's activation window so a held card can show that it
|
|
10
|
+
* is being held.
|
|
11
|
+
*
|
|
12
|
+
* dnd-kit only reports a drag once it has already started, which on touch is
|
|
13
|
+
* one delay too late: without feedback the hold feels like nothing is
|
|
14
|
+
* happening, and the user lets go before the drag ever arms. This watches the
|
|
15
|
+
* same touch stream under the same constraints and reports the in-between
|
|
16
|
+
* state, so the card can acknowledge the press while it is still a press.
|
|
17
|
+
*
|
|
18
|
+
* It never drives the drag itself — dnd-kit remains the only thing that decides
|
|
19
|
+
* whether a gesture becomes one. The worst a desync can do is show or hide a
|
|
20
|
+
* ring.
|
|
21
|
+
*/
|
|
22
|
+
function useKanbanPressFeedback({ delay = TOUCH_ACTIVATION_DELAY, tolerance = TOUCH_ACTIVATION_TOLERANCE, disabled = false } = {}) {
|
|
23
|
+
const [isPressing, setIsPressing] = React.useState(false);
|
|
24
|
+
const originRef = React.useRef(null);
|
|
25
|
+
const timeoutRef = React.useRef(null);
|
|
26
|
+
const clear = React.useCallback(() => {
|
|
27
|
+
if (timeoutRef.current !== null) {
|
|
28
|
+
clearTimeout(timeoutRef.current);
|
|
29
|
+
timeoutRef.current = null;
|
|
30
|
+
}
|
|
31
|
+
originRef.current = null;
|
|
32
|
+
setIsPressing(false);
|
|
33
|
+
}, []);
|
|
34
|
+
React.useEffect(() => clear, [clear]);
|
|
35
|
+
return {
|
|
36
|
+
isPressing,
|
|
37
|
+
handlers: React.useMemo(() => ({
|
|
38
|
+
onTouchStart: (event) => {
|
|
39
|
+
const touch = event.touches[0];
|
|
40
|
+
if (disabled || !touch || event.touches.length > 1) return;
|
|
41
|
+
originRef.current = {
|
|
42
|
+
x: touch.clientX,
|
|
43
|
+
y: touch.clientY
|
|
44
|
+
};
|
|
45
|
+
setIsPressing(true);
|
|
46
|
+
timeoutRef.current = setTimeout(() => setIsPressing(false), delay);
|
|
47
|
+
},
|
|
48
|
+
onTouchMove: (event) => {
|
|
49
|
+
const origin = originRef.current;
|
|
50
|
+
const touch = event.touches[0];
|
|
51
|
+
if (!origin || !touch) return;
|
|
52
|
+
if (Math.abs(touch.clientX - origin.x) > tolerance || Math.abs(touch.clientY - origin.y) > tolerance) clear();
|
|
53
|
+
},
|
|
54
|
+
onTouchEnd: clear,
|
|
55
|
+
onTouchCancel: clear
|
|
56
|
+
}), [
|
|
57
|
+
clear,
|
|
58
|
+
delay,
|
|
59
|
+
tolerance,
|
|
60
|
+
disabled
|
|
61
|
+
])
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
export { useKanbanPressFeedback };
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IntersectionOptions } from "./infinite-scroll/types.cjs";
|
|
2
2
|
import "./infinite-scroll/index.cjs";
|
|
3
|
-
import { ColumnOverflow, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanVirtualization } from "./types.cjs";
|
|
3
|
+
import { ColumnOverflow, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanColumnSnapAlign, KanbanColumnSnapOptions, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanTouchOptions, KanbanVirtualization } from "./types.cjs";
|
|
4
4
|
import { KanbanBoard } from "./KanbanBoard.cjs";
|
|
5
|
-
export { ColumnOverflow, type IntersectionOptions, KanbanBoard, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanVirtualization };
|
|
5
|
+
export { ColumnOverflow, type IntersectionOptions, KanbanBoard, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanColumnSnapAlign, KanbanColumnSnapOptions, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanTouchOptions, KanbanVirtualization };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IntersectionOptions } from "./infinite-scroll/types.js";
|
|
2
2
|
import "./infinite-scroll/index.js";
|
|
3
|
-
import { ColumnOverflow, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanVirtualization } from "./types.js";
|
|
3
|
+
import { ColumnOverflow, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanColumnSnapAlign, KanbanColumnSnapOptions, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanTouchOptions, KanbanVirtualization } from "./types.js";
|
|
4
4
|
import { KanbanBoard } from "./KanbanBoard.js";
|
|
5
|
-
export { ColumnOverflow, type IntersectionOptions, KanbanBoard, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanVirtualization };
|
|
5
|
+
export { ColumnOverflow, type IntersectionOptions, KanbanBoard, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanColumnSnapAlign, KanbanColumnSnapOptions, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanTouchOptions, KanbanVirtualization };
|
package/dist/types.d.cts
CHANGED
|
@@ -53,6 +53,68 @@ interface KanbanFilter<T = Record<string, unknown>> {
|
|
|
53
53
|
type KanbanFilters<T = Record<string, unknown>> = KanbanFilter<T>[] | ((column: KanbanColumn) => KanbanFilter<T>[] | undefined);
|
|
54
54
|
/** Controls how columns behave when their cards exceed the board height. */
|
|
55
55
|
type ColumnOverflow = 'scroll' | 'expand';
|
|
56
|
+
/**
|
|
57
|
+
* Touch-input behaviour.
|
|
58
|
+
*
|
|
59
|
+
* On a touch screen a finger that lands on a card is ambiguous: it could be the
|
|
60
|
+
* start of a drag, or of a swipe to reach the next column. The board resolves
|
|
61
|
+
* that the way native mobile UIs do — a card only becomes draggable after the
|
|
62
|
+
* finger has been held still on it for {@link dragActivationDelay}. Anything
|
|
63
|
+
* shorter, or any movement past {@link dragActivationTolerance} before the
|
|
64
|
+
* delay elapses, stays a scroll.
|
|
65
|
+
*
|
|
66
|
+
* Mouse and keyboard input is unaffected: a mouse drag still starts as soon as
|
|
67
|
+
* the pointer has travelled a few pixels.
|
|
68
|
+
*/
|
|
69
|
+
interface KanbanTouchOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Milliseconds a finger must rest on a card before the drag arms.
|
|
72
|
+
*
|
|
73
|
+
* @default 250
|
|
74
|
+
*/
|
|
75
|
+
dragActivationDelay?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Pixels of movement tolerated during the hold. Moving further cancels the
|
|
78
|
+
* press and hands the gesture back to the browser as a scroll.
|
|
79
|
+
*
|
|
80
|
+
* @default 8
|
|
81
|
+
*/
|
|
82
|
+
dragActivationTolerance?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Renders a ring on a card while it is being held but has not armed yet, so
|
|
85
|
+
* the press is visible before the drag begins.
|
|
86
|
+
*
|
|
87
|
+
* @default true
|
|
88
|
+
*/
|
|
89
|
+
pressFeedback?: boolean;
|
|
90
|
+
}
|
|
91
|
+
/** Where a snapped column comes to rest inside the board's viewport. */
|
|
92
|
+
type KanbanColumnSnapAlign = 'start' | 'center';
|
|
93
|
+
/**
|
|
94
|
+
* Turns the board into a one-column-at-a-time slider on small screens, using
|
|
95
|
+
* CSS scroll snapping — so the swipe keeps the platform's own momentum and
|
|
96
|
+
* settling animation rather than a re-implementation of it.
|
|
97
|
+
*
|
|
98
|
+
* Only applies below the `sm` breakpoint (40rem); above it the board keeps its
|
|
99
|
+
* regular multi-column layout. Snapping is suspended for the duration of a drag
|
|
100
|
+
* so dnd-kit's auto-scroll is not fought by the snap points.
|
|
101
|
+
*/
|
|
102
|
+
interface KanbanColumnSnapOptions {
|
|
103
|
+
/**
|
|
104
|
+
* Where each column lands when the swipe settles.
|
|
105
|
+
*
|
|
106
|
+
* @default 'start'
|
|
107
|
+
*/
|
|
108
|
+
align?: KanbanColumnSnapAlign;
|
|
109
|
+
/**
|
|
110
|
+
* Any CSS width for a column while snapping is active. Leaving a margin
|
|
111
|
+
* (the default 85%) keeps the next column peeking in, which is what tells the
|
|
112
|
+
* user there is more to swipe to.
|
|
113
|
+
*
|
|
114
|
+
* @default '85%'
|
|
115
|
+
*/
|
|
116
|
+
columnWidth?: string;
|
|
117
|
+
}
|
|
56
118
|
/**
|
|
57
119
|
* Payload delivered by {@link KanbanBoardProps.onFilterChange} whenever the
|
|
58
120
|
* set of active filters for a column changes.
|
|
@@ -246,6 +308,20 @@ interface KanbanBoardProps<T = Record<string, unknown>> {
|
|
|
246
308
|
* board. Cards stay mounted and fully interactive; only the drag is off.
|
|
247
309
|
*/
|
|
248
310
|
dragDisabled?: boolean;
|
|
311
|
+
/**
|
|
312
|
+
* Tunes how a touch drag arms. See {@link KanbanTouchOptions}; the defaults
|
|
313
|
+
* are what most boards want, so this is mainly an escape hatch for a board
|
|
314
|
+
* whose cards are unusually small or tall.
|
|
315
|
+
*/
|
|
316
|
+
touch?: KanbanTouchOptions;
|
|
317
|
+
/**
|
|
318
|
+
* Makes the board swipe one column at a time on small screens. Pass `false`
|
|
319
|
+
* to keep the plain horizontal scroller at every width, or an options object
|
|
320
|
+
* to change the resting alignment or the column width.
|
|
321
|
+
*
|
|
322
|
+
* @default true
|
|
323
|
+
*/
|
|
324
|
+
columnSnap?: boolean | KanbanColumnSnapOptions;
|
|
249
325
|
}
|
|
250
326
|
//#endregion
|
|
251
|
-
export { ColumnOverflow, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanVirtualization };
|
|
327
|
+
export { ColumnOverflow, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanColumnSnapAlign, KanbanColumnSnapOptions, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanTouchOptions, KanbanVirtualization };
|
package/dist/types.d.ts
CHANGED
|
@@ -53,6 +53,68 @@ interface KanbanFilter<T = Record<string, unknown>> {
|
|
|
53
53
|
type KanbanFilters<T = Record<string, unknown>> = KanbanFilter<T>[] | ((column: KanbanColumn) => KanbanFilter<T>[] | undefined);
|
|
54
54
|
/** Controls how columns behave when their cards exceed the board height. */
|
|
55
55
|
type ColumnOverflow = 'scroll' | 'expand';
|
|
56
|
+
/**
|
|
57
|
+
* Touch-input behaviour.
|
|
58
|
+
*
|
|
59
|
+
* On a touch screen a finger that lands on a card is ambiguous: it could be the
|
|
60
|
+
* start of a drag, or of a swipe to reach the next column. The board resolves
|
|
61
|
+
* that the way native mobile UIs do — a card only becomes draggable after the
|
|
62
|
+
* finger has been held still on it for {@link dragActivationDelay}. Anything
|
|
63
|
+
* shorter, or any movement past {@link dragActivationTolerance} before the
|
|
64
|
+
* delay elapses, stays a scroll.
|
|
65
|
+
*
|
|
66
|
+
* Mouse and keyboard input is unaffected: a mouse drag still starts as soon as
|
|
67
|
+
* the pointer has travelled a few pixels.
|
|
68
|
+
*/
|
|
69
|
+
interface KanbanTouchOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Milliseconds a finger must rest on a card before the drag arms.
|
|
72
|
+
*
|
|
73
|
+
* @default 250
|
|
74
|
+
*/
|
|
75
|
+
dragActivationDelay?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Pixels of movement tolerated during the hold. Moving further cancels the
|
|
78
|
+
* press and hands the gesture back to the browser as a scroll.
|
|
79
|
+
*
|
|
80
|
+
* @default 8
|
|
81
|
+
*/
|
|
82
|
+
dragActivationTolerance?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Renders a ring on a card while it is being held but has not armed yet, so
|
|
85
|
+
* the press is visible before the drag begins.
|
|
86
|
+
*
|
|
87
|
+
* @default true
|
|
88
|
+
*/
|
|
89
|
+
pressFeedback?: boolean;
|
|
90
|
+
}
|
|
91
|
+
/** Where a snapped column comes to rest inside the board's viewport. */
|
|
92
|
+
type KanbanColumnSnapAlign = 'start' | 'center';
|
|
93
|
+
/**
|
|
94
|
+
* Turns the board into a one-column-at-a-time slider on small screens, using
|
|
95
|
+
* CSS scroll snapping — so the swipe keeps the platform's own momentum and
|
|
96
|
+
* settling animation rather than a re-implementation of it.
|
|
97
|
+
*
|
|
98
|
+
* Only applies below the `sm` breakpoint (40rem); above it the board keeps its
|
|
99
|
+
* regular multi-column layout. Snapping is suspended for the duration of a drag
|
|
100
|
+
* so dnd-kit's auto-scroll is not fought by the snap points.
|
|
101
|
+
*/
|
|
102
|
+
interface KanbanColumnSnapOptions {
|
|
103
|
+
/**
|
|
104
|
+
* Where each column lands when the swipe settles.
|
|
105
|
+
*
|
|
106
|
+
* @default 'start'
|
|
107
|
+
*/
|
|
108
|
+
align?: KanbanColumnSnapAlign;
|
|
109
|
+
/**
|
|
110
|
+
* Any CSS width for a column while snapping is active. Leaving a margin
|
|
111
|
+
* (the default 85%) keeps the next column peeking in, which is what tells the
|
|
112
|
+
* user there is more to swipe to.
|
|
113
|
+
*
|
|
114
|
+
* @default '85%'
|
|
115
|
+
*/
|
|
116
|
+
columnWidth?: string;
|
|
117
|
+
}
|
|
56
118
|
/**
|
|
57
119
|
* Payload delivered by {@link KanbanBoardProps.onFilterChange} whenever the
|
|
58
120
|
* set of active filters for a column changes.
|
|
@@ -246,6 +308,20 @@ interface KanbanBoardProps<T = Record<string, unknown>> {
|
|
|
246
308
|
* board. Cards stay mounted and fully interactive; only the drag is off.
|
|
247
309
|
*/
|
|
248
310
|
dragDisabled?: boolean;
|
|
311
|
+
/**
|
|
312
|
+
* Tunes how a touch drag arms. See {@link KanbanTouchOptions}; the defaults
|
|
313
|
+
* are what most boards want, so this is mainly an escape hatch for a board
|
|
314
|
+
* whose cards are unusually small or tall.
|
|
315
|
+
*/
|
|
316
|
+
touch?: KanbanTouchOptions;
|
|
317
|
+
/**
|
|
318
|
+
* Makes the board swipe one column at a time on small screens. Pass `false`
|
|
319
|
+
* to keep the plain horizontal scroller at every width, or an options object
|
|
320
|
+
* to change the resting alignment or the column width.
|
|
321
|
+
*
|
|
322
|
+
* @default true
|
|
323
|
+
*/
|
|
324
|
+
columnSnap?: boolean | KanbanColumnSnapOptions;
|
|
249
325
|
}
|
|
250
326
|
//#endregion
|
|
251
|
-
export { ColumnOverflow, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanVirtualization };
|
|
327
|
+
export { ColumnOverflow, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanColumnSnapAlign, KanbanColumnSnapOptions, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanTouchOptions, KanbanVirtualization };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/utils/kanban-touch-defaults.ts
|
|
3
|
+
/**
|
|
4
|
+
* Shared touch constants.
|
|
5
|
+
*
|
|
6
|
+
* The sensor and the press-feedback hook have to agree on the hold duration and
|
|
7
|
+
* the movement tolerance, or the ring would appear at a moment the drag does
|
|
8
|
+
* not actually arm. Keeping both numbers here is what makes that guarantee
|
|
9
|
+
* mechanical rather than a comment.
|
|
10
|
+
*/
|
|
11
|
+
/** Milliseconds a finger must rest on a card before the drag arms. */
|
|
12
|
+
const TOUCH_ACTIVATION_DELAY = 250;
|
|
13
|
+
/** Pixels of movement tolerated during the hold before the press is cancelled. */
|
|
14
|
+
const TOUCH_ACTIVATION_TOLERANCE = 8;
|
|
15
|
+
/**
|
|
16
|
+
* Marks an element as an explicit drag handle, which picks up on contact
|
|
17
|
+
* instead of waiting out the hold.
|
|
18
|
+
*/
|
|
19
|
+
const DRAG_HANDLE_ATTRIBUTE = "data-kanban-drag-handle";
|
|
20
|
+
/** Matches {@link DRAG_HANDLE_ATTRIBUTE} and anything inside it (e.g. an icon). */
|
|
21
|
+
const DRAG_HANDLE_SELECTOR = `[${DRAG_HANDLE_ATTRIBUTE}]`;
|
|
22
|
+
/** Spread onto an element to turn it into a drag handle. */
|
|
23
|
+
const dragHandleProps = { [DRAG_HANDLE_ATTRIBUTE]: "" };
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
exports.DRAG_HANDLE_SELECTOR = DRAG_HANDLE_SELECTOR;
|
|
27
|
+
exports.TOUCH_ACTIVATION_DELAY = TOUCH_ACTIVATION_DELAY;
|
|
28
|
+
exports.TOUCH_ACTIVATION_TOLERANCE = TOUCH_ACTIVATION_TOLERANCE;
|
|
29
|
+
exports.dragHandleProps = dragHandleProps;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/utils/kanban-touch-defaults.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared touch constants.
|
|
4
|
+
*
|
|
5
|
+
* The sensor and the press-feedback hook have to agree on the hold duration and
|
|
6
|
+
* the movement tolerance, or the ring would appear at a moment the drag does
|
|
7
|
+
* not actually arm. Keeping both numbers here is what makes that guarantee
|
|
8
|
+
* mechanical rather than a comment.
|
|
9
|
+
*/
|
|
10
|
+
/** Milliseconds a finger must rest on a card before the drag arms. */
|
|
11
|
+
const TOUCH_ACTIVATION_DELAY = 250;
|
|
12
|
+
/** Pixels of movement tolerated during the hold before the press is cancelled. */
|
|
13
|
+
const TOUCH_ACTIVATION_TOLERANCE = 8;
|
|
14
|
+
/**
|
|
15
|
+
* Marks an element as an explicit drag handle, which picks up on contact
|
|
16
|
+
* instead of waiting out the hold.
|
|
17
|
+
*/
|
|
18
|
+
const DRAG_HANDLE_ATTRIBUTE = "data-kanban-drag-handle";
|
|
19
|
+
/** Matches {@link DRAG_HANDLE_ATTRIBUTE} and anything inside it (e.g. an icon). */
|
|
20
|
+
const DRAG_HANDLE_SELECTOR = `[${DRAG_HANDLE_ATTRIBUTE}]`;
|
|
21
|
+
/** Spread onto an element to turn it into a drag handle. */
|
|
22
|
+
const dragHandleProps = { [DRAG_HANDLE_ATTRIBUTE]: "" };
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
export { DRAG_HANDLE_SELECTOR, TOUCH_ACTIVATION_DELAY, TOUCH_ACTIVATION_TOLERANCE, dragHandleProps };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/utils/resolve-column-snap.ts
|
|
3
|
+
/** Custom property the column reads its snapped width from. */
|
|
4
|
+
const COLUMN_SNAP_WIDTH_VAR = "--kanban-column-snap-width";
|
|
5
|
+
const DEFAULT_COLUMN_WIDTH = "85%";
|
|
6
|
+
const DEFAULT_ALIGN = "start";
|
|
7
|
+
const BOARD_SNAP_CLASS = "max-sm:snap-x max-sm:snap-mandatory max-sm:scroll-px-2 scroll-smooth";
|
|
8
|
+
const COLUMN_SNAP_CLASS = "max-sm:w-[var(--kanban-column-snap-width)] max-sm:min-w-0 max-sm:flex-none max-sm:snap-always";
|
|
9
|
+
const ALIGN_CLASS = {
|
|
10
|
+
start: "max-sm:snap-start",
|
|
11
|
+
center: "max-sm:snap-center"
|
|
12
|
+
};
|
|
13
|
+
const DISABLED = {
|
|
14
|
+
enabled: false,
|
|
15
|
+
boardClassName: "",
|
|
16
|
+
columnClassName: ""
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Turns the `columnSnap` prop into the classes that make the board swipe one
|
|
20
|
+
* column at a time below the `sm` breakpoint.
|
|
21
|
+
*
|
|
22
|
+
* Everything here is CSS scroll snapping, which buys the platform's own
|
|
23
|
+
* momentum, rubber-banding and settle animation for free — and, unlike a JS
|
|
24
|
+
* carousel, keeps working while the main thread is busy re-measuring droppables
|
|
25
|
+
* mid-drag.
|
|
26
|
+
*
|
|
27
|
+
* @param columnSnap - The board's `columnSnap` prop.
|
|
28
|
+
* @param isDragging - Suspends snapping for the length of a drag: dnd-kit
|
|
29
|
+
* auto-scrolls the board by writing `scrollLeft`, and mandatory snap points
|
|
30
|
+
* would pull every one of those writes back to the nearest column.
|
|
31
|
+
*/
|
|
32
|
+
function resolveColumnSnap(columnSnap, isDragging) {
|
|
33
|
+
if (columnSnap === false) return DISABLED;
|
|
34
|
+
const { align = DEFAULT_ALIGN, columnWidth = DEFAULT_COLUMN_WIDTH } = columnSnap === true ? {} : columnSnap ?? {};
|
|
35
|
+
return {
|
|
36
|
+
enabled: true,
|
|
37
|
+
boardClassName: isDragging ? "snap-none" : BOARD_SNAP_CLASS,
|
|
38
|
+
columnClassName: `${COLUMN_SNAP_CLASS} ${ALIGN_CLASS[align]}`,
|
|
39
|
+
style: { [COLUMN_SNAP_WIDTH_VAR]: columnWidth }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
exports.resolveColumnSnap = resolveColumnSnap;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/utils/resolve-column-snap.ts
|
|
2
|
+
/** Custom property the column reads its snapped width from. */
|
|
3
|
+
const COLUMN_SNAP_WIDTH_VAR = "--kanban-column-snap-width";
|
|
4
|
+
const DEFAULT_COLUMN_WIDTH = "85%";
|
|
5
|
+
const DEFAULT_ALIGN = "start";
|
|
6
|
+
const BOARD_SNAP_CLASS = "max-sm:snap-x max-sm:snap-mandatory max-sm:scroll-px-2 scroll-smooth";
|
|
7
|
+
const COLUMN_SNAP_CLASS = "max-sm:w-[var(--kanban-column-snap-width)] max-sm:min-w-0 max-sm:flex-none max-sm:snap-always";
|
|
8
|
+
const ALIGN_CLASS = {
|
|
9
|
+
start: "max-sm:snap-start",
|
|
10
|
+
center: "max-sm:snap-center"
|
|
11
|
+
};
|
|
12
|
+
const DISABLED = {
|
|
13
|
+
enabled: false,
|
|
14
|
+
boardClassName: "",
|
|
15
|
+
columnClassName: ""
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Turns the `columnSnap` prop into the classes that make the board swipe one
|
|
19
|
+
* column at a time below the `sm` breakpoint.
|
|
20
|
+
*
|
|
21
|
+
* Everything here is CSS scroll snapping, which buys the platform's own
|
|
22
|
+
* momentum, rubber-banding and settle animation for free — and, unlike a JS
|
|
23
|
+
* carousel, keeps working while the main thread is busy re-measuring droppables
|
|
24
|
+
* mid-drag.
|
|
25
|
+
*
|
|
26
|
+
* @param columnSnap - The board's `columnSnap` prop.
|
|
27
|
+
* @param isDragging - Suspends snapping for the length of a drag: dnd-kit
|
|
28
|
+
* auto-scrolls the board by writing `scrollLeft`, and mandatory snap points
|
|
29
|
+
* would pull every one of those writes back to the nearest column.
|
|
30
|
+
*/
|
|
31
|
+
function resolveColumnSnap(columnSnap, isDragging) {
|
|
32
|
+
if (columnSnap === false) return DISABLED;
|
|
33
|
+
const { align = DEFAULT_ALIGN, columnWidth = DEFAULT_COLUMN_WIDTH } = columnSnap === true ? {} : columnSnap ?? {};
|
|
34
|
+
return {
|
|
35
|
+
enabled: true,
|
|
36
|
+
boardClassName: isDragging ? "snap-none" : BOARD_SNAP_CLASS,
|
|
37
|
+
columnClassName: `${COLUMN_SNAP_CLASS} ${ALIGN_CLASS[align]}`,
|
|
38
|
+
style: { [COLUMN_SNAP_WIDTH_VAR]: columnWidth }
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { resolveColumnSnap };
|
package/package.json
CHANGED