@pixpilot/shadcn-kanban 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/AddColumnButton.cjs +82 -0
  4. package/dist/AddColumnButton.js +77 -0
  5. package/dist/ColumnFilterButton.cjs +68 -0
  6. package/dist/ColumnFilterButton.js +63 -0
  7. package/dist/KanbanBoard.cjs +137 -0
  8. package/dist/KanbanBoard.d.cts +36 -0
  9. package/dist/KanbanBoard.d.ts +36 -0
  10. package/dist/KanbanBoard.js +131 -0
  11. package/dist/KanbanColumn.cjs +142 -0
  12. package/dist/KanbanColumn.js +134 -0
  13. package/dist/KanbanColumnCards.cjs +25 -0
  14. package/dist/KanbanColumnCards.js +23 -0
  15. package/dist/KanbanDragOverlay.cjs +42 -0
  16. package/dist/KanbanDragOverlay.js +37 -0
  17. package/dist/KanbanItem.cjs +58 -0
  18. package/dist/KanbanItem.js +52 -0
  19. package/dist/KanbanVirtualColumnCards.cjs +70 -0
  20. package/dist/KanbanVirtualColumnCards.js +66 -0
  21. package/dist/_virtual/rolldown_runtime.cjs +25 -0
  22. package/dist/hooks/use-kanban-board-state.cjs +43 -0
  23. package/dist/hooks/use-kanban-board-state.js +41 -0
  24. package/dist/hooks/use-kanban-card-drag.cjs +83 -0
  25. package/dist/hooks/use-kanban-card-drag.js +81 -0
  26. package/dist/hooks/use-kanban-collision-detection.cjs +70 -0
  27. package/dist/hooks/use-kanban-collision-detection.js +67 -0
  28. package/dist/hooks/use-kanban-column-reorder.cjs +31 -0
  29. package/dist/hooks/use-kanban-column-reorder.js +28 -0
  30. package/dist/hooks/use-kanban-drag.cjs +98 -0
  31. package/dist/hooks/use-kanban-drag.js +95 -0
  32. package/dist/hooks/use-kanban-filters.cjs +49 -0
  33. package/dist/hooks/use-kanban-filters.js +47 -0
  34. package/dist/index.cjs +3 -0
  35. package/dist/index.d.cts +5 -0
  36. package/dist/index.d.ts +5 -0
  37. package/dist/index.js +3 -0
  38. package/dist/infinite-scroll/InfiniteScrollSentinel.cjs +54 -0
  39. package/dist/infinite-scroll/InfiniteScrollSentinel.d.cts +2 -0
  40. package/dist/infinite-scroll/InfiniteScrollSentinel.d.ts +2 -0
  41. package/dist/infinite-scroll/InfiniteScrollSentinel.js +49 -0
  42. package/dist/infinite-scroll/index.d.cts +3 -0
  43. package/dist/infinite-scroll/index.d.ts +3 -0
  44. package/dist/infinite-scroll/types.d.cts +29 -0
  45. package/dist/infinite-scroll/types.d.ts +29 -0
  46. package/dist/infinite-scroll/use-intersection-observer.cjs +46 -0
  47. package/dist/infinite-scroll/use-intersection-observer.d.cts +1 -0
  48. package/dist/infinite-scroll/use-intersection-observer.d.ts +1 -0
  49. package/dist/infinite-scroll/use-intersection-observer.js +44 -0
  50. package/dist/types.d.cts +251 -0
  51. package/dist/types.d.ts +251 -0
  52. package/dist/utils/apply-kanban-drop.cjs +37 -0
  53. package/dist/utils/apply-kanban-drop.js +35 -0
  54. package/dist/utils/column-sortable-id.cjs +23 -0
  55. package/dist/utils/column-sortable-id.js +20 -0
  56. package/dist/utils/is-below-over-item.cjs +17 -0
  57. package/dist/utils/is-below-over-item.js +16 -0
  58. package/dist/utils/items-order-equal.cjs +13 -0
  59. package/dist/utils/items-order-equal.js +12 -0
  60. package/dist/utils/kanban-keyboard-coordinates.cjs +64 -0
  61. package/dist/utils/kanban-keyboard-coordinates.js +62 -0
  62. package/dist/utils/move-kanban-item.cjs +28 -0
  63. package/dist/utils/move-kanban-item.js +27 -0
  64. package/dist/utils/replace-state.cjs +15 -0
  65. package/dist/utils/replace-state.js +14 -0
  66. package/dist/utils/resolve-column-id.cjs +25 -0
  67. package/dist/utils/resolve-column-id.js +24 -0
  68. package/package.json +97 -0
@@ -0,0 +1,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
@@ -0,0 +1,3 @@
1
+ const require_KanbanBoard = require('./KanbanBoard.cjs');
2
+
3
+ exports.KanbanBoard = require_KanbanBoard.KanbanBoard;
@@ -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 };
@@ -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,3 @@
1
+ import { KanbanBoard } from "./KanbanBoard.js";
2
+
3
+ export { KanbanBoard };
@@ -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,2 @@
1
+ import "./types.cjs";
2
+ import "react/jsx-runtime";
@@ -0,0 +1,2 @@
1
+ import "./types.js";
2
+ import "react/jsx-runtime";
@@ -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,3 @@
1
+ import { IntersectionOptions } from "./types.cjs";
2
+ import "./InfiniteScrollSentinel.cjs";
3
+ import "./use-intersection-observer.cjs";
@@ -0,0 +1,3 @@
1
+ import { IntersectionOptions } from "./types.js";
2
+ import "./InfiniteScrollSentinel.js";
3
+ import "./use-intersection-observer.js";
@@ -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 };