@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,98 @@
|
|
|
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_kanban_keyboard_coordinates = require('../utils/kanban-keyboard-coordinates.cjs');
|
|
7
|
+
const require_use_kanban_board_state = require('./use-kanban-board-state.cjs');
|
|
8
|
+
const require_use_kanban_card_drag = require('./use-kanban-card-drag.cjs');
|
|
9
|
+
const require_use_kanban_collision_detection = require('./use-kanban-collision-detection.cjs');
|
|
10
|
+
const require_use_kanban_column_reorder = require('./use-kanban-column-reorder.cjs');
|
|
11
|
+
let __dnd_kit_core = require("@dnd-kit/core");
|
|
12
|
+
__dnd_kit_core = require_rolldown_runtime.__toESM(__dnd_kit_core);
|
|
13
|
+
let react = require("react");
|
|
14
|
+
react = require_rolldown_runtime.__toESM(react);
|
|
15
|
+
|
|
16
|
+
//#region src/hooks/use-kanban-drag.ts
|
|
17
|
+
const POINTER_ACTIVATION_DISTANCE = 5;
|
|
18
|
+
/**
|
|
19
|
+
* Drag-and-drop state machine for the Kanban board, following dnd-kit's
|
|
20
|
+
* multiple-containers pattern. Owns the shared drag state and routes each
|
|
21
|
+
* event to either the card handlers or the column reorder handler.
|
|
22
|
+
*/
|
|
23
|
+
function useKanbanDrag({ externalItems, columns, onChange, onColumnChange }) {
|
|
24
|
+
const [activeId, setActiveId] = react.default.useState(null);
|
|
25
|
+
const isDragActiveRef = react.default.useRef(false);
|
|
26
|
+
const dragStartItemsRef = react.default.useRef(externalItems);
|
|
27
|
+
const lastOverIdRef = react.default.useRef(null);
|
|
28
|
+
const recentlyMovedToNewColumnRef = react.default.useRef(false);
|
|
29
|
+
const { items, itemsRef, applyItems, internalColumns, setInternalColumns } = require_use_kanban_board_state.useKanbanBoardState({
|
|
30
|
+
externalItems,
|
|
31
|
+
columns,
|
|
32
|
+
isDragActiveRef
|
|
33
|
+
});
|
|
34
|
+
react.default.useEffect(() => {
|
|
35
|
+
const frame = requestAnimationFrame(() => {
|
|
36
|
+
recentlyMovedToNewColumnRef.current = false;
|
|
37
|
+
});
|
|
38
|
+
return () => cancelAnimationFrame(frame);
|
|
39
|
+
}, [items]);
|
|
40
|
+
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.PointerSensor, { activationConstraint: { distance: POINTER_ACTIVATION_DISTANCE } }), (0, __dnd_kit_core.useSensor)(__dnd_kit_core.KeyboardSensor, { coordinateGetter: require_kanban_keyboard_coordinates.kanbanKeyboardCoordinates }));
|
|
42
|
+
const collisionDetection = require_use_kanban_collision_detection.useKanbanCollisionDetection({
|
|
43
|
+
columnIds,
|
|
44
|
+
itemsRef,
|
|
45
|
+
lastOverIdRef,
|
|
46
|
+
recentlyMovedToNewColumnRef
|
|
47
|
+
});
|
|
48
|
+
const { handleCardDragOver, handleCardDragEnd } = require_use_kanban_card_drag.useKanbanCardDrag({
|
|
49
|
+
columnIds,
|
|
50
|
+
itemsRef,
|
|
51
|
+
applyItems,
|
|
52
|
+
dragStartItemsRef,
|
|
53
|
+
recentlyMovedToNewColumnRef,
|
|
54
|
+
onChange
|
|
55
|
+
});
|
|
56
|
+
const handleColumnDragEnd = require_use_kanban_column_reorder.useKanbanColumnReorder({
|
|
57
|
+
internalColumns,
|
|
58
|
+
setInternalColumns,
|
|
59
|
+
onColumnChange
|
|
60
|
+
});
|
|
61
|
+
const handleDragStart = react.default.useCallback((event) => {
|
|
62
|
+
isDragActiveRef.current = true;
|
|
63
|
+
dragStartItemsRef.current = itemsRef.current;
|
|
64
|
+
lastOverIdRef.current = null;
|
|
65
|
+
setActiveId(event.active.id);
|
|
66
|
+
}, [itemsRef]);
|
|
67
|
+
const handleDragEnd = react.default.useCallback((event) => {
|
|
68
|
+
isDragActiveRef.current = false;
|
|
69
|
+
lastOverIdRef.current = null;
|
|
70
|
+
setActiveId(null);
|
|
71
|
+
if (require_column_sortable_id.isColumnSortableId(event.active.id)) {
|
|
72
|
+
handleColumnDragEnd(event);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
handleCardDragEnd(event);
|
|
76
|
+
}, [handleCardDragEnd, handleColumnDragEnd]);
|
|
77
|
+
const handleDragCancel = react.default.useCallback(() => {
|
|
78
|
+
isDragActiveRef.current = false;
|
|
79
|
+
lastOverIdRef.current = null;
|
|
80
|
+
setActiveId(null);
|
|
81
|
+
applyItems(dragStartItemsRef.current);
|
|
82
|
+
}, [applyItems]);
|
|
83
|
+
return {
|
|
84
|
+
items,
|
|
85
|
+
internalColumns,
|
|
86
|
+
activeId,
|
|
87
|
+
isDraggingColumn: activeId != null && require_column_sortable_id.isColumnSortableId(activeId),
|
|
88
|
+
sensors,
|
|
89
|
+
collisionDetection,
|
|
90
|
+
handleDragStart,
|
|
91
|
+
handleDragOver: handleCardDragOver,
|
|
92
|
+
handleDragEnd,
|
|
93
|
+
handleDragCancel
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
98
|
+
exports.useKanbanDrag = useKanbanDrag;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { isColumnSortableId } from "../utils/column-sortable-id.js";
|
|
5
|
+
import { kanbanKeyboardCoordinates } from "../utils/kanban-keyboard-coordinates.js";
|
|
6
|
+
import { useKanbanBoardState } from "./use-kanban-board-state.js";
|
|
7
|
+
import { useKanbanCardDrag } from "./use-kanban-card-drag.js";
|
|
8
|
+
import { useKanbanCollisionDetection } from "./use-kanban-collision-detection.js";
|
|
9
|
+
import { useKanbanColumnReorder } from "./use-kanban-column-reorder.js";
|
|
10
|
+
import { KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
11
|
+
import React from "react";
|
|
12
|
+
|
|
13
|
+
//#region src/hooks/use-kanban-drag.ts
|
|
14
|
+
const POINTER_ACTIVATION_DISTANCE = 5;
|
|
15
|
+
/**
|
|
16
|
+
* Drag-and-drop state machine for the Kanban board, following dnd-kit's
|
|
17
|
+
* multiple-containers pattern. Owns the shared drag state and routes each
|
|
18
|
+
* event to either the card handlers or the column reorder handler.
|
|
19
|
+
*/
|
|
20
|
+
function useKanbanDrag({ externalItems, columns, onChange, onColumnChange }) {
|
|
21
|
+
const [activeId, setActiveId] = React.useState(null);
|
|
22
|
+
const isDragActiveRef = React.useRef(false);
|
|
23
|
+
const dragStartItemsRef = React.useRef(externalItems);
|
|
24
|
+
const lastOverIdRef = React.useRef(null);
|
|
25
|
+
const recentlyMovedToNewColumnRef = React.useRef(false);
|
|
26
|
+
const { items, itemsRef, applyItems, internalColumns, setInternalColumns } = useKanbanBoardState({
|
|
27
|
+
externalItems,
|
|
28
|
+
columns,
|
|
29
|
+
isDragActiveRef
|
|
30
|
+
});
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
const frame = requestAnimationFrame(() => {
|
|
33
|
+
recentlyMovedToNewColumnRef.current = false;
|
|
34
|
+
});
|
|
35
|
+
return () => cancelAnimationFrame(frame);
|
|
36
|
+
}, [items]);
|
|
37
|
+
const columnIds = React.useMemo(() => new Set(internalColumns.map((column) => column.id)), [internalColumns]);
|
|
38
|
+
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: POINTER_ACTIVATION_DISTANCE } }), useSensor(KeyboardSensor, { coordinateGetter: kanbanKeyboardCoordinates }));
|
|
39
|
+
const collisionDetection = useKanbanCollisionDetection({
|
|
40
|
+
columnIds,
|
|
41
|
+
itemsRef,
|
|
42
|
+
lastOverIdRef,
|
|
43
|
+
recentlyMovedToNewColumnRef
|
|
44
|
+
});
|
|
45
|
+
const { handleCardDragOver, handleCardDragEnd } = useKanbanCardDrag({
|
|
46
|
+
columnIds,
|
|
47
|
+
itemsRef,
|
|
48
|
+
applyItems,
|
|
49
|
+
dragStartItemsRef,
|
|
50
|
+
recentlyMovedToNewColumnRef,
|
|
51
|
+
onChange
|
|
52
|
+
});
|
|
53
|
+
const handleColumnDragEnd = useKanbanColumnReorder({
|
|
54
|
+
internalColumns,
|
|
55
|
+
setInternalColumns,
|
|
56
|
+
onColumnChange
|
|
57
|
+
});
|
|
58
|
+
const handleDragStart = React.useCallback((event) => {
|
|
59
|
+
isDragActiveRef.current = true;
|
|
60
|
+
dragStartItemsRef.current = itemsRef.current;
|
|
61
|
+
lastOverIdRef.current = null;
|
|
62
|
+
setActiveId(event.active.id);
|
|
63
|
+
}, [itemsRef]);
|
|
64
|
+
const handleDragEnd = React.useCallback((event) => {
|
|
65
|
+
isDragActiveRef.current = false;
|
|
66
|
+
lastOverIdRef.current = null;
|
|
67
|
+
setActiveId(null);
|
|
68
|
+
if (isColumnSortableId(event.active.id)) {
|
|
69
|
+
handleColumnDragEnd(event);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
handleCardDragEnd(event);
|
|
73
|
+
}, [handleCardDragEnd, handleColumnDragEnd]);
|
|
74
|
+
const handleDragCancel = React.useCallback(() => {
|
|
75
|
+
isDragActiveRef.current = false;
|
|
76
|
+
lastOverIdRef.current = null;
|
|
77
|
+
setActiveId(null);
|
|
78
|
+
applyItems(dragStartItemsRef.current);
|
|
79
|
+
}, [applyItems]);
|
|
80
|
+
return {
|
|
81
|
+
items,
|
|
82
|
+
internalColumns,
|
|
83
|
+
activeId,
|
|
84
|
+
isDraggingColumn: activeId != null && isColumnSortableId(activeId),
|
|
85
|
+
sensors,
|
|
86
|
+
collisionDetection,
|
|
87
|
+
handleDragStart,
|
|
88
|
+
handleDragOver: handleCardDragOver,
|
|
89
|
+
handleDragEnd,
|
|
90
|
+
handleDragCancel
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//#endregion
|
|
95
|
+
export { useKanbanDrag };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
5
|
+
let react = require("react");
|
|
6
|
+
react = require_rolldown_runtime.__toESM(react);
|
|
7
|
+
|
|
8
|
+
//#region src/hooks/use-kanban-filters.ts
|
|
9
|
+
/** Tracks per-column active filters and notifies the parent about changes. */
|
|
10
|
+
function useKanbanFilters({ filters, onFilterChange }) {
|
|
11
|
+
const [activeFilters, setActiveFilters] = react.default.useState({});
|
|
12
|
+
/** Resolve the filters available for a given column. */
|
|
13
|
+
const resolveFilters = react.default.useCallback((column) => {
|
|
14
|
+
if (!filters) return [];
|
|
15
|
+
return (typeof filters === "function" ? filters(column) : filters) ?? [];
|
|
16
|
+
}, [filters]);
|
|
17
|
+
const emitFilterChange = react.default.useCallback((column, nextActiveIds) => {
|
|
18
|
+
if (!onFilterChange) return;
|
|
19
|
+
onFilterChange({
|
|
20
|
+
column,
|
|
21
|
+
activeFilterIds: nextActiveIds,
|
|
22
|
+
activeFilters: resolveFilters(column).filter((f) => nextActiveIds.includes(f.id))
|
|
23
|
+
});
|
|
24
|
+
}, [onFilterChange, resolveFilters]);
|
|
25
|
+
return {
|
|
26
|
+
activeFilters,
|
|
27
|
+
resolveFilters,
|
|
28
|
+
handleToggleFilter: react.default.useCallback((column, filterId) => {
|
|
29
|
+
const current = activeFilters[column.id] ?? [];
|
|
30
|
+
const next = current.includes(filterId) ? current.filter((id) => id !== filterId) : [...current, filterId];
|
|
31
|
+
setActiveFilters({
|
|
32
|
+
...activeFilters,
|
|
33
|
+
[column.id]: next
|
|
34
|
+
});
|
|
35
|
+
emitFilterChange(column, next);
|
|
36
|
+
}, [activeFilters, emitFilterChange]),
|
|
37
|
+
handleClearFilters: react.default.useCallback((column) => {
|
|
38
|
+
if ((activeFilters[column.id] ?? []).length === 0) return;
|
|
39
|
+
setActiveFilters({
|
|
40
|
+
...activeFilters,
|
|
41
|
+
[column.id]: []
|
|
42
|
+
});
|
|
43
|
+
emitFilterChange(column, []);
|
|
44
|
+
}, [activeFilters, emitFilterChange])
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
exports.useKanbanFilters = useKanbanFilters;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import React from "react";
|
|
5
|
+
|
|
6
|
+
//#region src/hooks/use-kanban-filters.ts
|
|
7
|
+
/** Tracks per-column active filters and notifies the parent about changes. */
|
|
8
|
+
function useKanbanFilters({ filters, onFilterChange }) {
|
|
9
|
+
const [activeFilters, setActiveFilters] = React.useState({});
|
|
10
|
+
/** Resolve the filters available for a given column. */
|
|
11
|
+
const resolveFilters = React.useCallback((column) => {
|
|
12
|
+
if (!filters) return [];
|
|
13
|
+
return (typeof filters === "function" ? filters(column) : filters) ?? [];
|
|
14
|
+
}, [filters]);
|
|
15
|
+
const emitFilterChange = React.useCallback((column, nextActiveIds) => {
|
|
16
|
+
if (!onFilterChange) return;
|
|
17
|
+
onFilterChange({
|
|
18
|
+
column,
|
|
19
|
+
activeFilterIds: nextActiveIds,
|
|
20
|
+
activeFilters: resolveFilters(column).filter((f) => nextActiveIds.includes(f.id))
|
|
21
|
+
});
|
|
22
|
+
}, [onFilterChange, resolveFilters]);
|
|
23
|
+
return {
|
|
24
|
+
activeFilters,
|
|
25
|
+
resolveFilters,
|
|
26
|
+
handleToggleFilter: React.useCallback((column, filterId) => {
|
|
27
|
+
const current = activeFilters[column.id] ?? [];
|
|
28
|
+
const next = current.includes(filterId) ? current.filter((id) => id !== filterId) : [...current, filterId];
|
|
29
|
+
setActiveFilters({
|
|
30
|
+
...activeFilters,
|
|
31
|
+
[column.id]: next
|
|
32
|
+
});
|
|
33
|
+
emitFilterChange(column, next);
|
|
34
|
+
}, [activeFilters, emitFilterChange]),
|
|
35
|
+
handleClearFilters: React.useCallback((column) => {
|
|
36
|
+
if ((activeFilters[column.id] ?? []).length === 0) return;
|
|
37
|
+
setActiveFilters({
|
|
38
|
+
...activeFilters,
|
|
39
|
+
[column.id]: []
|
|
40
|
+
});
|
|
41
|
+
emitFilterChange(column, []);
|
|
42
|
+
}, [activeFilters, emitFilterChange])
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { useKanbanFilters };
|
package/dist/index.cjs
ADDED
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { IntersectionOptions } from "./infinite-scroll/types.cjs";
|
|
2
|
+
import "./infinite-scroll/index.cjs";
|
|
3
|
+
import { ColumnOverflow, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanVirtualization } from "./types.cjs";
|
|
4
|
+
import { KanbanBoard } from "./KanbanBoard.cjs";
|
|
5
|
+
export { ColumnOverflow, type IntersectionOptions, KanbanBoard, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanVirtualization };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { IntersectionOptions } from "./infinite-scroll/types.js";
|
|
2
|
+
import "./infinite-scroll/index.js";
|
|
3
|
+
import { ColumnOverflow, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanVirtualization } from "./types.js";
|
|
4
|
+
import { KanbanBoard } from "./KanbanBoard.js";
|
|
5
|
+
export { ColumnOverflow, type IntersectionOptions, KanbanBoard, KanbanBoardProps, KanbanChangeEvent, KanbanColumn, KanbanFilter, KanbanFilterChangeEvent, KanbanFilters, KanbanInfiniteScroll, KanbanItem, KanbanVirtualization };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
5
|
+
const require_use_intersection_observer = require('./use-intersection-observer.cjs');
|
|
6
|
+
let __pixpilot_shadcn_ui = require("@pixpilot/shadcn-ui");
|
|
7
|
+
__pixpilot_shadcn_ui = require_rolldown_runtime.__toESM(__pixpilot_shadcn_ui);
|
|
8
|
+
let react = require("react");
|
|
9
|
+
react = require_rolldown_runtime.__toESM(react);
|
|
10
|
+
let lucide_react = require("lucide-react");
|
|
11
|
+
lucide_react = require_rolldown_runtime.__toESM(lucide_react);
|
|
12
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
13
|
+
react_jsx_runtime = require_rolldown_runtime.__toESM(react_jsx_runtime);
|
|
14
|
+
|
|
15
|
+
//#region src/infinite-scroll/InfiniteScrollSentinel.tsx
|
|
16
|
+
function DefaultLoading() {
|
|
17
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
18
|
+
className: "text-muted-foreground flex items-center gap-2 text-xs",
|
|
19
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "h-3.5 w-3.5 animate-spin" }), "Loading…"]
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* An invisible element placed after the last item of a scrollable list. When it
|
|
24
|
+
* scrolls into view it asks the parent for the next page.
|
|
25
|
+
*
|
|
26
|
+
* ```
|
|
27
|
+
* ┌─────────────────┐
|
|
28
|
+
* │ list item 1 │
|
|
29
|
+
* │ … │
|
|
30
|
+
* │ list item N │
|
|
31
|
+
* │ [sentinel] ◄───┼── observed
|
|
32
|
+
* └─────────────────┘
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* The parent stays in control of the data: this only reports *when* to load.
|
|
36
|
+
*/
|
|
37
|
+
function InfiniteScrollSentinel({ onLoadMore, hasMore = true, isLoading = false, loadingComponent, endMessage, className, distance, root, threshold, disabled = false }) {
|
|
38
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
39
|
+
ref: require_use_intersection_observer.useIntersectionObserver({
|
|
40
|
+
onIntersect: onLoadMore,
|
|
41
|
+
distance,
|
|
42
|
+
root,
|
|
43
|
+
threshold,
|
|
44
|
+
disabled: disabled || !hasMore || isLoading
|
|
45
|
+
}),
|
|
46
|
+
"data-slot": "infinite-scroll-sentinel",
|
|
47
|
+
"aria-hidden": !isLoading,
|
|
48
|
+
className: (0, __pixpilot_shadcn_ui.cn)("flex shrink-0 items-center justify-center py-2", className),
|
|
49
|
+
children: [isLoading ? loadingComponent ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultLoading, {}) : null, !isLoading && !hasMore ? endMessage : null]
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
exports.InfiniteScrollSentinel = InfiniteScrollSentinel;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { useIntersectionObserver } from "./use-intersection-observer.js";
|
|
5
|
+
import { cn } from "@pixpilot/shadcn-ui";
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Loader2 } from "lucide-react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
|
|
10
|
+
//#region src/infinite-scroll/InfiniteScrollSentinel.tsx
|
|
11
|
+
function DefaultLoading() {
|
|
12
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
13
|
+
className: "text-muted-foreground flex items-center gap-2 text-xs",
|
|
14
|
+
children: [/* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), "Loading…"]
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* An invisible element placed after the last item of a scrollable list. When it
|
|
19
|
+
* scrolls into view it asks the parent for the next page.
|
|
20
|
+
*
|
|
21
|
+
* ```
|
|
22
|
+
* ┌─────────────────┐
|
|
23
|
+
* │ list item 1 │
|
|
24
|
+
* │ … │
|
|
25
|
+
* │ list item N │
|
|
26
|
+
* │ [sentinel] ◄───┼── observed
|
|
27
|
+
* └─────────────────┘
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* The parent stays in control of the data: this only reports *when* to load.
|
|
31
|
+
*/
|
|
32
|
+
function InfiniteScrollSentinel({ onLoadMore, hasMore = true, isLoading = false, loadingComponent, endMessage, className, distance, root, threshold, disabled = false }) {
|
|
33
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
34
|
+
ref: useIntersectionObserver({
|
|
35
|
+
onIntersect: onLoadMore,
|
|
36
|
+
distance,
|
|
37
|
+
root,
|
|
38
|
+
threshold,
|
|
39
|
+
disabled: disabled || !hasMore || isLoading
|
|
40
|
+
}),
|
|
41
|
+
"data-slot": "infinite-scroll-sentinel",
|
|
42
|
+
"aria-hidden": !isLoading,
|
|
43
|
+
className: cn("flex shrink-0 items-center justify-center py-2", className),
|
|
44
|
+
children: [isLoading ? loadingComponent ?? /* @__PURE__ */ jsx(DefaultLoading, {}) : null, !isLoading && !hasMore ? endMessage : null]
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
export { InfiniteScrollSentinel };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/infinite-scroll/types.d.ts
|
|
4
|
+
/** Tuning knobs shared by the observer hook and the sentinel component. */
|
|
5
|
+
interface IntersectionOptions {
|
|
6
|
+
/**
|
|
7
|
+
* How many pixels before the sentinel actually reaches the scroll edge the
|
|
8
|
+
* callback should fire. Larger values load earlier and hide the wait.
|
|
9
|
+
*
|
|
10
|
+
* @default 200
|
|
11
|
+
*/
|
|
12
|
+
distance?: number;
|
|
13
|
+
/**
|
|
14
|
+
* The scrollable ancestor to measure against. Pass the element that owns the
|
|
15
|
+
* overflow; defaults to the browser viewport.
|
|
16
|
+
*/
|
|
17
|
+
root?: Element | null;
|
|
18
|
+
/**
|
|
19
|
+
* Fraction of the sentinel that must be visible before it counts as
|
|
20
|
+
* intersecting.
|
|
21
|
+
*
|
|
22
|
+
* @default 0
|
|
23
|
+
*/
|
|
24
|
+
threshold?: number;
|
|
25
|
+
/** Stops observing entirely while `true`. */
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { IntersectionOptions };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/infinite-scroll/types.d.ts
|
|
4
|
+
/** Tuning knobs shared by the observer hook and the sentinel component. */
|
|
5
|
+
interface IntersectionOptions {
|
|
6
|
+
/**
|
|
7
|
+
* How many pixels before the sentinel actually reaches the scroll edge the
|
|
8
|
+
* callback should fire. Larger values load earlier and hide the wait.
|
|
9
|
+
*
|
|
10
|
+
* @default 200
|
|
11
|
+
*/
|
|
12
|
+
distance?: number;
|
|
13
|
+
/**
|
|
14
|
+
* The scrollable ancestor to measure against. Pass the element that owns the
|
|
15
|
+
* overflow; defaults to the browser viewport.
|
|
16
|
+
*/
|
|
17
|
+
root?: Element | null;
|
|
18
|
+
/**
|
|
19
|
+
* Fraction of the sentinel that must be visible before it counts as
|
|
20
|
+
* intersecting.
|
|
21
|
+
*
|
|
22
|
+
* @default 0
|
|
23
|
+
*/
|
|
24
|
+
threshold?: number;
|
|
25
|
+
/** Stops observing entirely while `true`. */
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { IntersectionOptions };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
5
|
+
let react = require("react");
|
|
6
|
+
react = require_rolldown_runtime.__toESM(react);
|
|
7
|
+
|
|
8
|
+
//#region src/infinite-scroll/use-intersection-observer.ts
|
|
9
|
+
const DEFAULT_DISTANCE = 200;
|
|
10
|
+
/**
|
|
11
|
+
* Observes a single node and calls `onIntersect` whenever it comes into view.
|
|
12
|
+
*
|
|
13
|
+
* Returns a ref callback to attach to the node being watched. The observer is
|
|
14
|
+
* rebuilt whenever `disabled` or any of the geometry options change — which is
|
|
15
|
+
* what lets a caller resume paging: re-observing emits a fresh entry even when
|
|
16
|
+
* the node never actually left the viewport, so a page too short to push the
|
|
17
|
+
* node out of view does not silently stall the sequence.
|
|
18
|
+
*/
|
|
19
|
+
function useIntersectionObserver({ onIntersect, distance = DEFAULT_DISTANCE, root, threshold = 0, disabled = false }) {
|
|
20
|
+
const [node, setNode] = react.default.useState(null);
|
|
21
|
+
const onIntersectRef = react.default.useRef(onIntersect);
|
|
22
|
+
onIntersectRef.current = onIntersect;
|
|
23
|
+
react.default.useEffect(() => {
|
|
24
|
+
if (node == null || disabled) return void 0;
|
|
25
|
+
if (typeof IntersectionObserver === "undefined") return void 0;
|
|
26
|
+
const observer = new IntersectionObserver((entries) => {
|
|
27
|
+
if (entries.some((entry) => entry.isIntersecting)) onIntersectRef.current();
|
|
28
|
+
}, {
|
|
29
|
+
root: root ?? null,
|
|
30
|
+
rootMargin: `0px 0px ${distance}px 0px`,
|
|
31
|
+
threshold
|
|
32
|
+
});
|
|
33
|
+
observer.observe(node);
|
|
34
|
+
return () => observer.disconnect();
|
|
35
|
+
}, [
|
|
36
|
+
node,
|
|
37
|
+
disabled,
|
|
38
|
+
root,
|
|
39
|
+
distance,
|
|
40
|
+
threshold
|
|
41
|
+
]);
|
|
42
|
+
return setNode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
exports.useIntersectionObserver = useIntersectionObserver;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./types.cjs";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./types.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import React from "react";
|
|
5
|
+
|
|
6
|
+
//#region src/infinite-scroll/use-intersection-observer.ts
|
|
7
|
+
const DEFAULT_DISTANCE = 200;
|
|
8
|
+
/**
|
|
9
|
+
* Observes a single node and calls `onIntersect` whenever it comes into view.
|
|
10
|
+
*
|
|
11
|
+
* Returns a ref callback to attach to the node being watched. The observer is
|
|
12
|
+
* rebuilt whenever `disabled` or any of the geometry options change — which is
|
|
13
|
+
* what lets a caller resume paging: re-observing emits a fresh entry even when
|
|
14
|
+
* the node never actually left the viewport, so a page too short to push the
|
|
15
|
+
* node out of view does not silently stall the sequence.
|
|
16
|
+
*/
|
|
17
|
+
function useIntersectionObserver({ onIntersect, distance = DEFAULT_DISTANCE, root, threshold = 0, disabled = false }) {
|
|
18
|
+
const [node, setNode] = React.useState(null);
|
|
19
|
+
const onIntersectRef = React.useRef(onIntersect);
|
|
20
|
+
onIntersectRef.current = onIntersect;
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
if (node == null || disabled) return void 0;
|
|
23
|
+
if (typeof IntersectionObserver === "undefined") return void 0;
|
|
24
|
+
const observer = new IntersectionObserver((entries) => {
|
|
25
|
+
if (entries.some((entry) => entry.isIntersecting)) onIntersectRef.current();
|
|
26
|
+
}, {
|
|
27
|
+
root: root ?? null,
|
|
28
|
+
rootMargin: `0px 0px ${distance}px 0px`,
|
|
29
|
+
threshold
|
|
30
|
+
});
|
|
31
|
+
observer.observe(node);
|
|
32
|
+
return () => observer.disconnect();
|
|
33
|
+
}, [
|
|
34
|
+
node,
|
|
35
|
+
disabled,
|
|
36
|
+
root,
|
|
37
|
+
distance,
|
|
38
|
+
threshold
|
|
39
|
+
]);
|
|
40
|
+
return setNode;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { useIntersectionObserver };
|