@carlonicora/nextjs-jsonapi 0.0.1 → 1.0.4
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/dist/BlockNoteEditor-VFWG6LXI.js.map +1 -1
- package/dist/JsonApiRequest-ZZLSP26T.js.map +1 -1
- package/dist/atoms/index.js.map +1 -1
- package/dist/chunk-2K3Q24UF.js.map +1 -1
- package/dist/chunk-3FBCC4G3.js.map +1 -1
- package/dist/chunk-4HCRAOS5.js.map +1 -1
- package/dist/chunk-6GKHCVF6.js.map +1 -1
- package/dist/chunk-7QVYU63E.js.map +1 -1
- package/dist/chunk-A5DDIABK.js.map +1 -1
- package/dist/chunk-AWONBQQP.js.map +1 -1
- package/dist/chunk-CXQOWQSY.js.map +1 -1
- package/dist/chunk-DO2HLAZO.js.map +1 -1
- package/dist/chunk-EFJEWLRL.js.map +1 -1
- package/dist/chunk-FY4SXJGU.js.map +1 -1
- package/dist/chunk-H6FMOA6B.js.map +1 -1
- package/dist/chunk-I2REI7OA.js.map +1 -1
- package/dist/chunk-IBS6NI7D.js.map +1 -1
- package/dist/chunk-J4Q36PMP.js.map +1 -1
- package/dist/chunk-JC3WJK65.js.map +1 -1
- package/dist/chunk-LXKSUWAV.js.map +1 -1
- package/dist/chunk-RAF7PNLG.js.map +1 -1
- package/dist/chunk-RUR22SVM.js.map +1 -1
- package/dist/chunk-TEGF6ZWG.js.map +1 -1
- package/dist/chunk-TMVHSY3Y.js.map +1 -1
- package/dist/chunk-V2JJPI7N.js.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/contexts/index.js.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/features/index.js.map +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/index.js.map +1 -1
- package/dist/permissions/index.js.map +1 -1
- package/dist/request-QFS7NEIE.js.map +1 -1
- package/dist/request-ZYY6RI5X.js.map +1 -1
- package/dist/roles/index.js.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/shadcnui/index.js.map +1 -1
- package/dist/token-MJMC26ON.js.map +1 -1
- package/dist/token-UYE7CV6X.js.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +6 -1
- package/src/atoms/index.ts +1 -0
- package/src/atoms/recentPagesAtom.ts +10 -0
- package/src/client/context/JsonApiContext.ts +61 -0
- package/src/client/context/JsonApiProvider.tsx +27 -0
- package/src/client/context/index.ts +2 -0
- package/src/client/hooks/index.ts +3 -0
- package/src/client/hooks/useJsonApiGet.ts +188 -0
- package/src/client/hooks/useJsonApiMutation.ts +193 -0
- package/src/client/hooks/useRehydration.ts +47 -0
- package/src/client/index.ts +11 -0
- package/src/client/request.ts +97 -0
- package/src/client/token.ts +10 -0
- package/src/components/containers/PageContainer.tsx +15 -0
- package/src/components/containers/ReactMarkdownContainer.tsx +119 -0
- package/src/components/containers/TabsContainer.tsx +93 -0
- package/src/components/containers/index.ts +3 -0
- package/src/components/contents/AttributeElement.tsx +20 -0
- package/src/components/contents/index.ts +1 -0
- package/src/components/details/AllowedUsersDetails.tsx +23 -0
- package/src/components/details/index.ts +1 -0
- package/src/components/editors/BlockNoteDiffInlineContent.tsx +152 -0
- package/src/components/editors/BlockNoteEditor.tsx +404 -0
- package/src/components/editors/BlockNoteEditorContainer.tsx +13 -0
- package/src/components/editors/BlockNoteEditorFormattingToolbar.tsx +38 -0
- package/src/components/editors/index.ts +1 -0
- package/src/components/errors/ErrorDetails.tsx +41 -0
- package/src/components/errors/errorToast.ts +9 -0
- package/src/components/errors/index.ts +2 -0
- package/src/components/forms/CommonAssociationForm.tsx +162 -0
- package/src/components/forms/CommonDeleter.tsx +94 -0
- package/src/components/forms/CommonEditorButtons.tsx +30 -0
- package/src/components/forms/CommonEditorHeader.tsx +35 -0
- package/src/components/forms/CommonEditorTrigger.tsx +26 -0
- package/src/components/forms/DatePickerPopover.tsx +219 -0
- package/src/components/forms/DateRangeSelector.tsx +110 -0
- package/src/components/forms/FileUploader.tsx +324 -0
- package/src/components/forms/FormCheckbox.tsx +66 -0
- package/src/components/forms/FormContainerGeneric.tsx +39 -0
- package/src/components/forms/FormDate.tsx +247 -0
- package/src/components/forms/FormDateTime.tsx +231 -0
- package/src/components/forms/FormInput.tsx +110 -0
- package/src/components/forms/FormPassword.tsx +54 -0
- package/src/components/forms/FormPlaceAutocomplete.tsx +286 -0
- package/src/components/forms/FormSelect.tsx +72 -0
- package/src/components/forms/FormSlider.tsx +51 -0
- package/src/components/forms/FormSwitch.tsx +25 -0
- package/src/components/forms/FormTextarea.tsx +44 -0
- package/src/components/forms/MultiFileUploader.tsx +107 -0
- package/src/components/forms/PasswordInput.tsx +47 -0
- package/src/components/forms/index.ts +21 -0
- package/src/components/index.ts +11 -0
- package/src/components/navigations/Breadcrumb.tsx +83 -0
- package/src/components/navigations/ContentTitle.tsx +39 -0
- package/src/components/navigations/Header.tsx +27 -0
- package/src/components/navigations/ModeToggleSwitch.tsx +25 -0
- package/src/components/navigations/PageSection.tsx +64 -0
- package/src/components/navigations/RecentPagesNavigator.tsx +52 -0
- package/src/components/navigations/index.ts +6 -0
- package/src/components/pages/PageContainerContentDetails.tsx +76 -0
- package/src/components/pages/PageContentContainer.tsx +31 -0
- package/src/components/pages/index.ts +2 -0
- package/src/components/tables/ContentListTable.tsx +165 -0
- package/src/components/tables/ContentTableSearch.tsx +105 -0
- package/src/components/tables/cells/cell.component.tsx +18 -0
- package/src/components/tables/cells/cell.date.tsx +16 -0
- package/src/components/tables/cells/cell.id.tsx +27 -0
- package/src/components/tables/cells/cell.link.tsx +18 -0
- package/src/components/tables/cells/cell.text.tsx +12 -0
- package/src/components/tables/cells/cell.url.tsx +13 -0
- package/src/components/tables/cells/index.ts +5 -0
- package/src/components/tables/index.ts +3 -0
- package/src/contexts/SharedContext.tsx +35 -0
- package/src/contexts/index.ts +2 -0
- package/src/core/abstracts/AbstractApiData.ts +138 -0
- package/src/core/abstracts/AbstractService.ts +263 -0
- package/src/core/abstracts/index.ts +2 -0
- package/src/core/endpoint/EndpointCreator.ts +97 -0
- package/src/core/endpoint/index.ts +1 -0
- package/src/core/factories/JsonApiDataFactory.ts +12 -0
- package/src/core/factories/RehydrationFactory.ts +30 -0
- package/src/core/factories/index.ts +2 -0
- package/src/core/fields/FieldSelector.ts +15 -0
- package/src/core/fields/index.ts +1 -0
- package/src/core/index.ts +20 -0
- package/src/core/interfaces/ApiData.ts +8 -0
- package/src/core/interfaces/ApiDataInterface.ts +15 -0
- package/src/core/interfaces/ApiRequestDataTypeInterface.ts +14 -0
- package/src/core/interfaces/ApiResponseInterface.ts +17 -0
- package/src/core/interfaces/JsonApiHydratedDataInterface.ts +5 -0
- package/src/core/interfaces/index.ts +5 -0
- package/src/core/registry/DataClassRegistry.ts +51 -0
- package/src/core/registry/ModuleRegistrar.ts +43 -0
- package/src/core/registry/ModuleRegistry.ts +64 -0
- package/src/core/registry/index.ts +3 -0
- package/src/core/utils/index.ts +2 -0
- package/src/core/utils/rehydrate.ts +24 -0
- package/src/core/utils/translateResponse.ts +125 -0
- package/src/features/auth/auth.module.ts +9 -0
- package/src/features/auth/config.ts +57 -0
- package/src/features/auth/data/auth.interface.ts +31 -0
- package/src/features/auth/data/auth.service.ts +159 -0
- package/src/features/auth/data/auth.ts +54 -0
- package/src/features/auth/data/index.ts +3 -0
- package/src/features/auth/index.ts +3 -0
- package/src/features/company/company.module.ts +10 -0
- package/src/features/company/data/company.fields.ts +6 -0
- package/src/features/company/data/company.interface.ts +28 -0
- package/src/features/company/data/company.service.ts +73 -0
- package/src/features/company/data/company.ts +104 -0
- package/src/features/company/data/index.ts +4 -0
- package/src/features/company/index.ts +2 -0
- package/src/features/content/content.module.ts +20 -0
- package/src/features/content/data/content.fields.ts +13 -0
- package/src/features/content/data/content.interface.ts +23 -0
- package/src/features/content/data/content.service.ts +75 -0
- package/src/features/content/data/content.ts +85 -0
- package/src/features/content/data/index.ts +4 -0
- package/src/features/content/index.ts +2 -0
- package/src/features/feature/components/forms/FormFeatures.tsx +149 -0
- package/src/features/feature/components/index.ts +1 -0
- package/src/features/feature/data/feature.interface.ts +9 -0
- package/src/features/feature/data/feature.service.ts +19 -0
- package/src/features/feature/data/feature.ts +33 -0
- package/src/features/feature/data/index.ts +3 -0
- package/src/features/feature/feature.module.ts +10 -0
- package/src/features/feature/index.ts +3 -0
- package/src/features/index.ts +12 -0
- package/src/features/module/data/index.ts +2 -0
- package/src/features/module/data/module.interface.ts +12 -0
- package/src/features/module/data/module.ts +42 -0
- package/src/features/module/index.ts +2 -0
- package/src/features/module/module.module.ts +10 -0
- package/src/features/notification/data/index.ts +4 -0
- package/src/features/notification/data/notification.fields.ts +8 -0
- package/src/features/notification/data/notification.interface.ts +14 -0
- package/src/features/notification/data/notification.service.ts +34 -0
- package/src/features/notification/data/notification.ts +51 -0
- package/src/features/notification/index.ts +2 -0
- package/src/features/notification/notification.module.ts +10 -0
- package/src/features/push/data/index.ts +3 -0
- package/src/features/push/data/push.interface.ts +8 -0
- package/src/features/push/data/push.service.ts +17 -0
- package/src/features/push/data/push.ts +18 -0
- package/src/features/push/index.ts +2 -0
- package/src/features/push/push.module.ts +10 -0
- package/src/features/role/data/index.ts +4 -0
- package/src/features/role/data/role.fields.ts +8 -0
- package/src/features/role/data/role.interface.ts +16 -0
- package/src/features/role/data/role.service.ts +117 -0
- package/src/features/role/data/role.ts +62 -0
- package/src/features/role/index.ts +2 -0
- package/src/features/role/role.module.ts +10 -0
- package/src/features/s3/data/index.ts +3 -0
- package/src/features/s3/data/s3.interface.ts +11 -0
- package/src/features/s3/data/s3.service.ts +30 -0
- package/src/features/s3/data/s3.ts +60 -0
- package/src/features/s3/index.ts +2 -0
- package/src/features/s3/s3.module.ts +10 -0
- package/src/features/search/index.ts +1 -0
- package/src/features/search/interfaces/index.ts +1 -0
- package/src/features/search/interfaces/search.result.interface.ts +3 -0
- package/src/features/user/author.module.ts +10 -0
- package/src/features/user/components/index.ts +2 -0
- package/src/features/user/components/lists/ContributorsList.tsx +41 -0
- package/src/features/user/components/lists/index.ts +1 -0
- package/src/features/user/components/widgets/UserAvatar.tsx +86 -0
- package/src/features/user/components/widgets/index.ts +1 -0
- package/src/features/user/contexts/CurrentUserContext.tsx +156 -0
- package/src/features/user/contexts/index.ts +1 -0
- package/src/features/user/data/index.ts +4 -0
- package/src/features/user/data/user.fields.ts +8 -0
- package/src/features/user/data/user.interface.ts +41 -0
- package/src/features/user/data/user.service.ts +246 -0
- package/src/features/user/data/user.ts +162 -0
- package/src/features/user/index.ts +4 -0
- package/src/features/user/user.module.ts +21 -0
- package/src/hooks/TableGeneratorRegistry.ts +53 -0
- package/src/hooks/index.ts +33 -0
- package/src/hooks/types.ts +35 -0
- package/src/hooks/url.rewriter.ts +22 -0
- package/src/hooks/useCustomD3Graph.tsx +705 -0
- package/src/hooks/useDataListRetriever.ts +349 -0
- package/src/hooks/useDebounce.ts +33 -0
- package/src/hooks/usePageUrlGenerator.ts +50 -0
- package/src/hooks/useTableGenerator.ts +16 -0
- package/src/i18n/config.ts +73 -0
- package/src/i18n/index.ts +18 -0
- package/src/index.ts +16 -0
- package/src/interfaces/breadcrumb.item.data.interface.ts +4 -0
- package/src/interfaces/d3.link.interface.ts +7 -0
- package/src/interfaces/d3.node.interface.ts +12 -0
- package/src/interfaces/index.ts +3 -0
- package/src/permissions/check.ts +127 -0
- package/src/permissions/index.ts +2 -0
- package/src/permissions/types.ts +109 -0
- package/src/roles/config.ts +46 -0
- package/src/roles/index.ts +1 -0
- package/src/server/cache.ts +28 -0
- package/src/server/index.ts +3 -0
- package/src/server/request.ts +113 -0
- package/src/server/token.ts +10 -0
- package/src/shadcnui/custom/kanban.tsx +1001 -0
- package/src/shadcnui/custom/link.tsx +18 -0
- package/src/shadcnui/custom/multi-select.tsx +382 -0
- package/src/shadcnui/index.ts +49 -0
- package/src/shadcnui/ui/accordion.tsx +52 -0
- package/src/shadcnui/ui/alert-dialog.tsx +141 -0
- package/src/shadcnui/ui/alert.tsx +43 -0
- package/src/shadcnui/ui/avatar.tsx +50 -0
- package/src/shadcnui/ui/badge.tsx +40 -0
- package/src/shadcnui/ui/breadcrumb.tsx +115 -0
- package/src/shadcnui/ui/button.tsx +51 -0
- package/src/shadcnui/ui/calendar.tsx +73 -0
- package/src/shadcnui/ui/card.tsx +43 -0
- package/src/shadcnui/ui/carousel.tsx +225 -0
- package/src/shadcnui/ui/chart.tsx +320 -0
- package/src/shadcnui/ui/checkbox.tsx +29 -0
- package/src/shadcnui/ui/collapsible.tsx +11 -0
- package/src/shadcnui/ui/command.tsx +155 -0
- package/src/shadcnui/ui/context-menu.tsx +179 -0
- package/src/shadcnui/ui/dialog.tsx +96 -0
- package/src/shadcnui/ui/drawer.tsx +89 -0
- package/src/shadcnui/ui/dropdown-menu.tsx +205 -0
- package/src/shadcnui/ui/form.tsx +138 -0
- package/src/shadcnui/ui/hover-card.tsx +29 -0
- package/src/shadcnui/ui/input.tsx +21 -0
- package/src/shadcnui/ui/label.tsx +26 -0
- package/src/shadcnui/ui/navigation-menu.tsx +168 -0
- package/src/shadcnui/ui/popover.tsx +33 -0
- package/src/shadcnui/ui/progress.tsx +25 -0
- package/src/shadcnui/ui/radio-group.tsx +37 -0
- package/src/shadcnui/ui/resizable.tsx +47 -0
- package/src/shadcnui/ui/scroll-area.tsx +40 -0
- package/src/shadcnui/ui/select.tsx +164 -0
- package/src/shadcnui/ui/separator.tsx +28 -0
- package/src/shadcnui/ui/sheet.tsx +139 -0
- package/src/shadcnui/ui/sidebar.tsx +677 -0
- package/src/shadcnui/ui/skeleton.tsx +13 -0
- package/src/shadcnui/ui/slider.tsx +25 -0
- package/src/shadcnui/ui/sonner.tsx +25 -0
- package/src/shadcnui/ui/switch.tsx +31 -0
- package/src/shadcnui/ui/table.tsx +120 -0
- package/src/shadcnui/ui/tabs.tsx +55 -0
- package/src/shadcnui/ui/textarea.tsx +24 -0
- package/src/shadcnui/ui/toggle.tsx +39 -0
- package/src/shadcnui/ui/tooltip.tsx +61 -0
- package/src/unified/JsonApiRequest.ts +325 -0
- package/src/unified/index.ts +1 -0
- package/src/utils/blocknote-diff.util.ts +815 -0
- package/src/utils/blocknote-word-diff-renderer.util.ts +413 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/compose-refs.ts +61 -0
- package/src/utils/date-formatter.ts +53 -0
- package/src/utils/exists.ts +7 -0
- package/src/utils/index.ts +15 -0
- package/src/utils/schemas/entity.object.schema.ts +8 -0
- package/src/utils/schemas/index.ts +2 -0
- package/src/utils/schemas/user.object.schema.ts +9 -0
- package/src/utils/table-options.ts +67 -0
- package/src/utils/use-mobile.tsx +21 -0
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type Announcements,
|
|
5
|
+
type CollisionDetection,
|
|
6
|
+
DndContext,
|
|
7
|
+
type DndContextProps,
|
|
8
|
+
type DragCancelEvent,
|
|
9
|
+
type DragEndEvent,
|
|
10
|
+
type DragOverEvent,
|
|
11
|
+
DragOverlay,
|
|
12
|
+
type DragStartEvent,
|
|
13
|
+
type DraggableAttributes,
|
|
14
|
+
type DraggableSyntheticListeners,
|
|
15
|
+
type DropAnimation,
|
|
16
|
+
type DroppableContainer,
|
|
17
|
+
KeyboardCode,
|
|
18
|
+
type KeyboardCoordinateGetter,
|
|
19
|
+
KeyboardSensor,
|
|
20
|
+
MeasuringStrategy,
|
|
21
|
+
MouseSensor,
|
|
22
|
+
TouchSensor,
|
|
23
|
+
type UniqueIdentifier,
|
|
24
|
+
closestCenter,
|
|
25
|
+
closestCorners,
|
|
26
|
+
defaultDropAnimationSideEffects,
|
|
27
|
+
getFirstCollision,
|
|
28
|
+
pointerWithin,
|
|
29
|
+
rectIntersection,
|
|
30
|
+
useSensor,
|
|
31
|
+
useSensors,
|
|
32
|
+
} from "@dnd-kit/core";
|
|
33
|
+
import {
|
|
34
|
+
type AnimateLayoutChanges,
|
|
35
|
+
SortableContext,
|
|
36
|
+
type SortableContextProps,
|
|
37
|
+
arrayMove,
|
|
38
|
+
defaultAnimateLayoutChanges,
|
|
39
|
+
horizontalListSortingStrategy,
|
|
40
|
+
useSortable,
|
|
41
|
+
verticalListSortingStrategy,
|
|
42
|
+
} from "@dnd-kit/sortable";
|
|
43
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
44
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
45
|
+
import * as React from "react";
|
|
46
|
+
import * as ReactDOM from "react-dom";
|
|
47
|
+
|
|
48
|
+
import { useComposedRefs } from "../../utils/compose-refs";
|
|
49
|
+
import { cn } from "../../utils/cn";
|
|
50
|
+
|
|
51
|
+
const directions: string[] = [KeyboardCode.Down, KeyboardCode.Right, KeyboardCode.Up, KeyboardCode.Left];
|
|
52
|
+
|
|
53
|
+
const coordinateGetter: KeyboardCoordinateGetter = (event, { context }) => {
|
|
54
|
+
const { active, droppableRects, droppableContainers, collisionRect } = context;
|
|
55
|
+
|
|
56
|
+
if (directions.includes(event.code)) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
|
|
59
|
+
if (!active || !collisionRect) return;
|
|
60
|
+
|
|
61
|
+
const filteredContainers: DroppableContainer[] = [];
|
|
62
|
+
|
|
63
|
+
for (const entry of droppableContainers.getEnabled()) {
|
|
64
|
+
if (!entry || entry?.disabled) return;
|
|
65
|
+
|
|
66
|
+
const rect = droppableRects.get(entry.id);
|
|
67
|
+
|
|
68
|
+
if (!rect) return;
|
|
69
|
+
|
|
70
|
+
const data = entry.data.current;
|
|
71
|
+
|
|
72
|
+
if (data) {
|
|
73
|
+
const { type, children } = data;
|
|
74
|
+
|
|
75
|
+
if (type === "container" && children?.length > 0) {
|
|
76
|
+
if (active.data.current?.type !== "container") {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
switch (event.code) {
|
|
83
|
+
case KeyboardCode.Down:
|
|
84
|
+
if (collisionRect.top < rect.top) {
|
|
85
|
+
filteredContainers.push(entry);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case KeyboardCode.Up:
|
|
89
|
+
if (collisionRect.top > rect.top) {
|
|
90
|
+
filteredContainers.push(entry);
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
case KeyboardCode.Left:
|
|
94
|
+
if (collisionRect.left >= rect.left + rect.width) {
|
|
95
|
+
filteredContainers.push(entry);
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
case KeyboardCode.Right:
|
|
99
|
+
if (collisionRect.left + collisionRect.width <= rect.left) {
|
|
100
|
+
filteredContainers.push(entry);
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const collisions = closestCorners({
|
|
107
|
+
active,
|
|
108
|
+
collisionRect: collisionRect,
|
|
109
|
+
droppableRects,
|
|
110
|
+
droppableContainers: filteredContainers,
|
|
111
|
+
pointerCoordinates: null,
|
|
112
|
+
});
|
|
113
|
+
const closestId = getFirstCollision(collisions, "id");
|
|
114
|
+
|
|
115
|
+
if (closestId != null) {
|
|
116
|
+
const newDroppable = droppableContainers.get(closestId);
|
|
117
|
+
const newNode = newDroppable?.node.current;
|
|
118
|
+
const newRect = newDroppable?.rect.current;
|
|
119
|
+
|
|
120
|
+
if (newNode && newRect) {
|
|
121
|
+
if (newDroppable.id === "placeholder") {
|
|
122
|
+
return {
|
|
123
|
+
x: newRect.left + (newRect.width - collisionRect.width) / 2,
|
|
124
|
+
y: newRect.top + (newRect.height - collisionRect.height) / 2,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (newDroppable.data.current?.type === "container") {
|
|
129
|
+
return {
|
|
130
|
+
x: newRect.left + 20,
|
|
131
|
+
y: newRect.top + 74,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
x: newRect.left,
|
|
137
|
+
y: newRect.top,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return undefined;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const ROOT_NAME = "Kanban";
|
|
147
|
+
const BOARD_NAME = "KanbanBoard";
|
|
148
|
+
const COLUMN_NAME = "KanbanColumn";
|
|
149
|
+
const COLUMN_HANDLE_NAME = "KanbanColumnHandle";
|
|
150
|
+
const ITEM_NAME = "KanbanItem";
|
|
151
|
+
const ITEM_HANDLE_NAME = "KanbanItemHandle";
|
|
152
|
+
const OVERLAY_NAME = "KanbanOverlay";
|
|
153
|
+
|
|
154
|
+
interface KanbanContextValue<T> {
|
|
155
|
+
id: string;
|
|
156
|
+
items: Record<UniqueIdentifier, T[]>;
|
|
157
|
+
modifiers: DndContextProps["modifiers"];
|
|
158
|
+
strategy: SortableContextProps["strategy"];
|
|
159
|
+
orientation: "horizontal" | "vertical";
|
|
160
|
+
activeId: UniqueIdentifier | null;
|
|
161
|
+
setActiveId: (id: UniqueIdentifier | null) => void;
|
|
162
|
+
getItemValue: (item: T) => UniqueIdentifier;
|
|
163
|
+
flatCursor: boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const KanbanContext = React.createContext<KanbanContextValue<unknown> | null>(null);
|
|
167
|
+
KanbanContext.displayName = ROOT_NAME;
|
|
168
|
+
|
|
169
|
+
function useKanbanContext(consumerName: string) {
|
|
170
|
+
const context = React.useContext(KanbanContext);
|
|
171
|
+
if (!context) {
|
|
172
|
+
throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
|
|
173
|
+
}
|
|
174
|
+
return context;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface GetItemValue<T> {
|
|
178
|
+
/**
|
|
179
|
+
* Callback that returns a unique identifier for each kanban item. Required for array of objects.
|
|
180
|
+
* @example getItemValue={(item) => item.id}
|
|
181
|
+
*/
|
|
182
|
+
getItemValue: (item: T) => UniqueIdentifier;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
type KanbanRootProps<T> = Omit<DndContextProps, "collisionDetection"> &
|
|
186
|
+
GetItemValue<T> & {
|
|
187
|
+
value: Record<UniqueIdentifier, T[]>;
|
|
188
|
+
onValueChange?: (columns: Record<UniqueIdentifier, T[]>) => void;
|
|
189
|
+
onMove?: (event: DragEndEvent & { activeIndex: number; overIndex: number }) => void;
|
|
190
|
+
strategy?: SortableContextProps["strategy"];
|
|
191
|
+
orientation?: "horizontal" | "vertical";
|
|
192
|
+
flatCursor?: boolean;
|
|
193
|
+
} & (T extends object ? GetItemValue<T> : Partial<GetItemValue<T>>);
|
|
194
|
+
|
|
195
|
+
function KanbanRoot<T>(props: KanbanRootProps<T>) {
|
|
196
|
+
const {
|
|
197
|
+
value,
|
|
198
|
+
onValueChange,
|
|
199
|
+
modifiers,
|
|
200
|
+
strategy = verticalListSortingStrategy,
|
|
201
|
+
orientation = "horizontal",
|
|
202
|
+
onMove,
|
|
203
|
+
getItemValue: getItemValueProp,
|
|
204
|
+
accessibility,
|
|
205
|
+
flatCursor = false,
|
|
206
|
+
...kanbanProps
|
|
207
|
+
} = props;
|
|
208
|
+
|
|
209
|
+
const id = React.useId();
|
|
210
|
+
const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null);
|
|
211
|
+
const lastOverIdRef = React.useRef<UniqueIdentifier | null>(null);
|
|
212
|
+
const hasMovedRef = React.useRef(false);
|
|
213
|
+
const sensors = useSensors(
|
|
214
|
+
useSensor(MouseSensor),
|
|
215
|
+
useSensor(TouchSensor),
|
|
216
|
+
useSensor(KeyboardSensor, {
|
|
217
|
+
coordinateGetter,
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const getItemValue = React.useCallback(
|
|
222
|
+
(item: T): UniqueIdentifier => {
|
|
223
|
+
if (typeof item === "object" && !getItemValueProp) {
|
|
224
|
+
throw new Error("getItemValue is required when using array of objects");
|
|
225
|
+
}
|
|
226
|
+
return getItemValueProp ? getItemValueProp(item) : (item as UniqueIdentifier);
|
|
227
|
+
},
|
|
228
|
+
[getItemValueProp],
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const getColumn = React.useCallback(
|
|
232
|
+
(id: UniqueIdentifier) => {
|
|
233
|
+
if (id in value) return id;
|
|
234
|
+
|
|
235
|
+
for (const [columnId, items] of Object.entries(value)) {
|
|
236
|
+
if (items.some((item) => getItemValue(item) === id)) {
|
|
237
|
+
return columnId;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
},
|
|
243
|
+
[value, getItemValue],
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const collisionDetection: CollisionDetection = React.useCallback(
|
|
247
|
+
(args) => {
|
|
248
|
+
if (activeId && activeId in value) {
|
|
249
|
+
return closestCenter({
|
|
250
|
+
...args,
|
|
251
|
+
droppableContainers: args.droppableContainers.filter((container) => container.id in value),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const pointerIntersections = pointerWithin(args);
|
|
256
|
+
const intersections = pointerIntersections.length > 0 ? pointerIntersections : rectIntersection(args);
|
|
257
|
+
let overId = getFirstCollision(intersections, "id");
|
|
258
|
+
|
|
259
|
+
if (!overId) {
|
|
260
|
+
if (hasMovedRef.current) {
|
|
261
|
+
lastOverIdRef.current = activeId;
|
|
262
|
+
}
|
|
263
|
+
return lastOverIdRef.current ? [{ id: lastOverIdRef.current }] : [];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (overId in value) {
|
|
267
|
+
const containerItems = value[overId];
|
|
268
|
+
if (containerItems && containerItems.length > 0) {
|
|
269
|
+
const closestItem = closestCenter({
|
|
270
|
+
...args,
|
|
271
|
+
droppableContainers: args.droppableContainers.filter(
|
|
272
|
+
(container) =>
|
|
273
|
+
container.id !== overId && containerItems.some((item) => getItemValue(item) === container.id),
|
|
274
|
+
),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (closestItem.length > 0) {
|
|
278
|
+
overId = closestItem[0]?.id ?? overId;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
lastOverIdRef.current = overId;
|
|
284
|
+
return [{ id: overId }];
|
|
285
|
+
},
|
|
286
|
+
[activeId, value, getItemValue],
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const onDragStart = React.useCallback(
|
|
290
|
+
(event: DragStartEvent) => {
|
|
291
|
+
kanbanProps.onDragStart?.(event);
|
|
292
|
+
|
|
293
|
+
if (event.activatorEvent.defaultPrevented) return;
|
|
294
|
+
setActiveId(event.active.id);
|
|
295
|
+
},
|
|
296
|
+
[kanbanProps.onDragStart],
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const onDragOver = React.useCallback(
|
|
300
|
+
(event: DragOverEvent) => {
|
|
301
|
+
kanbanProps.onDragOver?.(event);
|
|
302
|
+
|
|
303
|
+
if (event.activatorEvent.defaultPrevented) return;
|
|
304
|
+
|
|
305
|
+
const { active, over } = event;
|
|
306
|
+
if (!over) return;
|
|
307
|
+
|
|
308
|
+
const activeColumn = getColumn(active.id);
|
|
309
|
+
const overColumn = getColumn(over.id);
|
|
310
|
+
|
|
311
|
+
if (!activeColumn || !overColumn) return;
|
|
312
|
+
|
|
313
|
+
if (activeColumn === overColumn) {
|
|
314
|
+
const items = value[activeColumn];
|
|
315
|
+
if (!items) return;
|
|
316
|
+
|
|
317
|
+
const activeIndex = items.findIndex((item) => getItemValue(item) === active.id);
|
|
318
|
+
const overIndex = items.findIndex((item) => getItemValue(item) === over.id);
|
|
319
|
+
|
|
320
|
+
if (activeIndex !== overIndex) {
|
|
321
|
+
const newColumns = { ...value };
|
|
322
|
+
newColumns[activeColumn] = arrayMove(items, activeIndex, overIndex);
|
|
323
|
+
onValueChange?.(newColumns);
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
const activeItems = value[activeColumn];
|
|
327
|
+
const overItems = value[overColumn];
|
|
328
|
+
|
|
329
|
+
if (!activeItems || !overItems) return;
|
|
330
|
+
|
|
331
|
+
const activeIndex = activeItems.findIndex((item) => getItemValue(item) === active.id);
|
|
332
|
+
|
|
333
|
+
if (activeIndex === -1) return;
|
|
334
|
+
|
|
335
|
+
const activeItem = activeItems[activeIndex];
|
|
336
|
+
if (!activeItem) return;
|
|
337
|
+
|
|
338
|
+
const updatedItems = {
|
|
339
|
+
...value,
|
|
340
|
+
[activeColumn]: activeItems.filter((item) => getItemValue(item) !== active.id),
|
|
341
|
+
[overColumn]: [...overItems, activeItem],
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
onValueChange?.(updatedItems);
|
|
345
|
+
hasMovedRef.current = true;
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
[value, getColumn, getItemValue, onValueChange, kanbanProps.onDragOver],
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const onDragEnd = React.useCallback(
|
|
352
|
+
(event: DragEndEvent) => {
|
|
353
|
+
kanbanProps.onDragEnd?.(event);
|
|
354
|
+
|
|
355
|
+
if (event.activatorEvent.defaultPrevented) return;
|
|
356
|
+
|
|
357
|
+
const { active, over } = event;
|
|
358
|
+
|
|
359
|
+
if (!over) {
|
|
360
|
+
setActiveId(null);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (active.id in value && over.id in value) {
|
|
365
|
+
const activeIndex = Object.keys(value).indexOf(active.id as string);
|
|
366
|
+
const overIndex = Object.keys(value).indexOf(over.id as string);
|
|
367
|
+
|
|
368
|
+
if (activeIndex !== overIndex) {
|
|
369
|
+
const orderedColumns = Object.keys(value);
|
|
370
|
+
const newOrder = arrayMove(orderedColumns, activeIndex, overIndex);
|
|
371
|
+
|
|
372
|
+
const newColumns: Record<UniqueIdentifier, T[]> = {};
|
|
373
|
+
for (const key of newOrder) {
|
|
374
|
+
const items = value[key];
|
|
375
|
+
if (items) {
|
|
376
|
+
newColumns[key] = items;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (onMove) {
|
|
381
|
+
onMove({ ...event, activeIndex, overIndex });
|
|
382
|
+
} else {
|
|
383
|
+
onValueChange?.(newColumns);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
const activeColumn = getColumn(active.id);
|
|
388
|
+
const overColumn = getColumn(over.id);
|
|
389
|
+
|
|
390
|
+
if (!activeColumn || !overColumn) {
|
|
391
|
+
setActiveId(null);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (activeColumn === overColumn) {
|
|
396
|
+
const items = value[activeColumn];
|
|
397
|
+
if (!items) {
|
|
398
|
+
setActiveId(null);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const activeIndex = items.findIndex((item) => getItemValue(item) === active.id);
|
|
403
|
+
const overIndex = items.findIndex((item) => getItemValue(item) === over.id);
|
|
404
|
+
|
|
405
|
+
if (activeIndex !== overIndex) {
|
|
406
|
+
const newColumns = { ...value };
|
|
407
|
+
newColumns[activeColumn] = arrayMove(items, activeIndex, overIndex);
|
|
408
|
+
if (onMove) {
|
|
409
|
+
onMove({
|
|
410
|
+
...event,
|
|
411
|
+
activeIndex,
|
|
412
|
+
overIndex,
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
onValueChange?.(newColumns);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
setActiveId(null);
|
|
422
|
+
hasMovedRef.current = false;
|
|
423
|
+
},
|
|
424
|
+
[value, getColumn, getItemValue, onValueChange, onMove, kanbanProps.onDragEnd],
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
const onDragCancel = React.useCallback(
|
|
428
|
+
(event: DragCancelEvent) => {
|
|
429
|
+
kanbanProps.onDragCancel?.(event);
|
|
430
|
+
|
|
431
|
+
if (event.activatorEvent.defaultPrevented) return;
|
|
432
|
+
|
|
433
|
+
setActiveId(null);
|
|
434
|
+
hasMovedRef.current = false;
|
|
435
|
+
},
|
|
436
|
+
[kanbanProps.onDragCancel],
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const announcements: Announcements = React.useMemo(
|
|
440
|
+
() => ({
|
|
441
|
+
onDragStart({ active }) {
|
|
442
|
+
const isColumn = active.id in value;
|
|
443
|
+
const itemType = isColumn ? "column" : "item";
|
|
444
|
+
const position = isColumn
|
|
445
|
+
? Object.keys(value).indexOf(active.id as string) + 1
|
|
446
|
+
: (() => {
|
|
447
|
+
const column = getColumn(active.id);
|
|
448
|
+
if (!column || !value[column]) return 1;
|
|
449
|
+
return value[column].findIndex((item) => getItemValue(item) === active.id) + 1;
|
|
450
|
+
})();
|
|
451
|
+
const total = isColumn
|
|
452
|
+
? Object.keys(value).length
|
|
453
|
+
: (() => {
|
|
454
|
+
const column = getColumn(active.id);
|
|
455
|
+
return column ? (value[column]?.length ?? 0) : 0;
|
|
456
|
+
})();
|
|
457
|
+
|
|
458
|
+
return `Picked up ${itemType} at position ${position} of ${total}`;
|
|
459
|
+
},
|
|
460
|
+
onDragOver({ active, over }) {
|
|
461
|
+
if (!over) return;
|
|
462
|
+
|
|
463
|
+
const isColumn = active.id in value;
|
|
464
|
+
const itemType = isColumn ? "column" : "item";
|
|
465
|
+
const position = isColumn
|
|
466
|
+
? Object.keys(value).indexOf(over.id as string) + 1
|
|
467
|
+
: (() => {
|
|
468
|
+
const column = getColumn(over.id);
|
|
469
|
+
if (!column || !value[column]) return 1;
|
|
470
|
+
return value[column].findIndex((item) => getItemValue(item) === over.id) + 1;
|
|
471
|
+
})();
|
|
472
|
+
const total = isColumn
|
|
473
|
+
? Object.keys(value).length
|
|
474
|
+
: (() => {
|
|
475
|
+
const column = getColumn(over.id);
|
|
476
|
+
return column ? (value[column]?.length ?? 0) : 0;
|
|
477
|
+
})();
|
|
478
|
+
|
|
479
|
+
const overColumn = getColumn(over.id);
|
|
480
|
+
const activeColumn = getColumn(active.id);
|
|
481
|
+
|
|
482
|
+
if (isColumn) {
|
|
483
|
+
return `${itemType} is now at position ${position} of ${total}`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (activeColumn !== overColumn) {
|
|
487
|
+
return `${itemType} is now at position ${position} of ${total} in ${overColumn}`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return `${itemType} is now at position ${position} of ${total}`;
|
|
491
|
+
},
|
|
492
|
+
onDragEnd({ active, over }) {
|
|
493
|
+
if (!over) return;
|
|
494
|
+
|
|
495
|
+
const isColumn = active.id in value;
|
|
496
|
+
const itemType = isColumn ? "column" : "item";
|
|
497
|
+
const position = isColumn
|
|
498
|
+
? Object.keys(value).indexOf(over.id as string) + 1
|
|
499
|
+
: (() => {
|
|
500
|
+
const column = getColumn(over.id);
|
|
501
|
+
if (!column || !value[column]) return 1;
|
|
502
|
+
return value[column].findIndex((item) => getItemValue(item) === over.id) + 1;
|
|
503
|
+
})();
|
|
504
|
+
const total = isColumn
|
|
505
|
+
? Object.keys(value).length
|
|
506
|
+
: (() => {
|
|
507
|
+
const column = getColumn(over.id);
|
|
508
|
+
return column ? (value[column]?.length ?? 0) : 0;
|
|
509
|
+
})();
|
|
510
|
+
|
|
511
|
+
const overColumn = getColumn(over.id);
|
|
512
|
+
const activeColumn = getColumn(active.id);
|
|
513
|
+
|
|
514
|
+
if (isColumn) {
|
|
515
|
+
return `${itemType} was dropped at position ${position} of ${total}`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (activeColumn !== overColumn) {
|
|
519
|
+
return `${itemType} was dropped at position ${position} of ${total} in ${overColumn}`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return `${itemType} was dropped at position ${position} of ${total}`;
|
|
523
|
+
},
|
|
524
|
+
onDragCancel({ active }) {
|
|
525
|
+
const isColumn = active.id in value;
|
|
526
|
+
const itemType = isColumn ? "column" : "item";
|
|
527
|
+
return `Dragging was cancelled. ${itemType} was dropped.`;
|
|
528
|
+
},
|
|
529
|
+
}),
|
|
530
|
+
[value, getColumn, getItemValue],
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const contextValue = React.useMemo<KanbanContextValue<T>>(
|
|
534
|
+
() => ({
|
|
535
|
+
id,
|
|
536
|
+
items: value,
|
|
537
|
+
modifiers,
|
|
538
|
+
strategy,
|
|
539
|
+
orientation,
|
|
540
|
+
activeId,
|
|
541
|
+
setActiveId,
|
|
542
|
+
getItemValue,
|
|
543
|
+
flatCursor,
|
|
544
|
+
}),
|
|
545
|
+
[id, value, activeId, modifiers, strategy, orientation, getItemValue, flatCursor],
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
return (
|
|
549
|
+
<KanbanContext.Provider value={contextValue as KanbanContextValue<unknown>}>
|
|
550
|
+
<DndContext
|
|
551
|
+
collisionDetection={collisionDetection}
|
|
552
|
+
modifiers={modifiers}
|
|
553
|
+
sensors={sensors}
|
|
554
|
+
{...kanbanProps}
|
|
555
|
+
id={id}
|
|
556
|
+
measuring={{
|
|
557
|
+
droppable: {
|
|
558
|
+
strategy: MeasuringStrategy.Always,
|
|
559
|
+
},
|
|
560
|
+
}}
|
|
561
|
+
onDragStart={onDragStart}
|
|
562
|
+
onDragOver={onDragOver}
|
|
563
|
+
onDragEnd={onDragEnd}
|
|
564
|
+
onDragCancel={onDragCancel}
|
|
565
|
+
accessibility={{
|
|
566
|
+
announcements,
|
|
567
|
+
screenReaderInstructions: {
|
|
568
|
+
draggable: `
|
|
569
|
+
To pick up a kanban item or column, press space or enter.
|
|
570
|
+
While dragging, use the arrow keys to move the item.
|
|
571
|
+
Press space or enter again to drop the item in its new position, or press escape to cancel.
|
|
572
|
+
`,
|
|
573
|
+
},
|
|
574
|
+
...accessibility,
|
|
575
|
+
}}
|
|
576
|
+
/>
|
|
577
|
+
</KanbanContext.Provider>
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const KanbanBoardContext = React.createContext<boolean>(false);
|
|
582
|
+
KanbanBoardContext.displayName = BOARD_NAME;
|
|
583
|
+
|
|
584
|
+
interface KanbanBoardProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
585
|
+
children: React.ReactNode;
|
|
586
|
+
asChild?: boolean;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const KanbanBoard = React.forwardRef<HTMLDivElement, KanbanBoardProps>((props, forwardedRef) => {
|
|
590
|
+
const { asChild, className, ...boardProps } = props;
|
|
591
|
+
|
|
592
|
+
const context = useKanbanContext(BOARD_NAME);
|
|
593
|
+
|
|
594
|
+
const columns = React.useMemo(() => {
|
|
595
|
+
return Object.keys(context.items);
|
|
596
|
+
}, [context.items]);
|
|
597
|
+
|
|
598
|
+
const BoardPrimitive = asChild ? Slot : "div";
|
|
599
|
+
|
|
600
|
+
return (
|
|
601
|
+
<KanbanBoardContext.Provider value={true}>
|
|
602
|
+
<SortableContext
|
|
603
|
+
items={columns}
|
|
604
|
+
strategy={context.orientation === "horizontal" ? horizontalListSortingStrategy : verticalListSortingStrategy}
|
|
605
|
+
>
|
|
606
|
+
<BoardPrimitive
|
|
607
|
+
aria-orientation={context.orientation}
|
|
608
|
+
data-orientation={context.orientation}
|
|
609
|
+
data-slot="kanban-board"
|
|
610
|
+
{...boardProps}
|
|
611
|
+
ref={forwardedRef}
|
|
612
|
+
className={cn(
|
|
613
|
+
"flex size-full gap-4",
|
|
614
|
+
context.orientation === "horizontal" ? "flex-row" : "flex-col",
|
|
615
|
+
className,
|
|
616
|
+
)}
|
|
617
|
+
/>
|
|
618
|
+
</SortableContext>
|
|
619
|
+
</KanbanBoardContext.Provider>
|
|
620
|
+
);
|
|
621
|
+
});
|
|
622
|
+
KanbanBoard.displayName = BOARD_NAME;
|
|
623
|
+
|
|
624
|
+
interface KanbanColumnContextValue {
|
|
625
|
+
id: string;
|
|
626
|
+
attributes: DraggableAttributes;
|
|
627
|
+
listeners: DraggableSyntheticListeners | undefined;
|
|
628
|
+
setActivatorNodeRef: (node: HTMLElement | null) => void;
|
|
629
|
+
isDragging?: boolean;
|
|
630
|
+
disabled?: boolean;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const KanbanColumnContext = React.createContext<KanbanColumnContextValue | null>(null);
|
|
634
|
+
KanbanColumnContext.displayName = COLUMN_NAME;
|
|
635
|
+
|
|
636
|
+
function useKanbanColumnContext(consumerName: string) {
|
|
637
|
+
const context = React.useContext(KanbanColumnContext);
|
|
638
|
+
if (!context) {
|
|
639
|
+
throw new Error(`\`${consumerName}\` must be used within \`${COLUMN_NAME}\``);
|
|
640
|
+
}
|
|
641
|
+
return context;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const animateLayoutChanges: AnimateLayoutChanges = (args) =>
|
|
645
|
+
defaultAnimateLayoutChanges({ ...args, wasDragging: true });
|
|
646
|
+
|
|
647
|
+
interface KanbanColumnProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
648
|
+
value: UniqueIdentifier;
|
|
649
|
+
children: React.ReactNode;
|
|
650
|
+
asChild?: boolean;
|
|
651
|
+
asHandle?: boolean;
|
|
652
|
+
disabled?: boolean;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const KanbanColumn = React.forwardRef<HTMLDivElement, KanbanColumnProps>((props, forwardedRef) => {
|
|
656
|
+
const { value, asChild, asHandle, disabled, className, style, ...columnProps } = props;
|
|
657
|
+
|
|
658
|
+
const id = React.useId();
|
|
659
|
+
const context = useKanbanContext(COLUMN_NAME);
|
|
660
|
+
const inBoard = React.useContext(KanbanBoardContext);
|
|
661
|
+
const inOverlay = React.useContext(KanbanOverlayContext);
|
|
662
|
+
|
|
663
|
+
if (!inBoard && !inOverlay) {
|
|
664
|
+
throw new Error(`\`${COLUMN_NAME}\` must be used within \`${BOARD_NAME}\` or \`${OVERLAY_NAME}\``);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (value === "") {
|
|
668
|
+
throw new Error(`\`${COLUMN_NAME}\` value cannot be an empty string`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging } = useSortable({
|
|
672
|
+
id: value,
|
|
673
|
+
disabled,
|
|
674
|
+
animateLayoutChanges,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const composedRef = useComposedRefs(forwardedRef, (node) => {
|
|
678
|
+
if (disabled) return;
|
|
679
|
+
setNodeRef(node);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const composedStyle = React.useMemo<React.CSSProperties>(() => {
|
|
683
|
+
return {
|
|
684
|
+
transform: CSS.Transform.toString(transform),
|
|
685
|
+
transition,
|
|
686
|
+
...style,
|
|
687
|
+
};
|
|
688
|
+
}, [transform, transition, style]);
|
|
689
|
+
|
|
690
|
+
const items = React.useMemo(() => {
|
|
691
|
+
const items = context.items[value] ?? [];
|
|
692
|
+
return items.map((item) => context.getItemValue(item));
|
|
693
|
+
}, [context.items, value, context.getItemValue]);
|
|
694
|
+
|
|
695
|
+
const columnContext = React.useMemo<KanbanColumnContextValue>(
|
|
696
|
+
() => ({
|
|
697
|
+
id,
|
|
698
|
+
attributes,
|
|
699
|
+
listeners,
|
|
700
|
+
setActivatorNodeRef,
|
|
701
|
+
isDragging,
|
|
702
|
+
disabled,
|
|
703
|
+
}),
|
|
704
|
+
[id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
const ColumnPrimitive = asChild ? Slot : "div";
|
|
708
|
+
|
|
709
|
+
return (
|
|
710
|
+
<KanbanColumnContext.Provider value={columnContext}>
|
|
711
|
+
<SortableContext
|
|
712
|
+
items={items}
|
|
713
|
+
strategy={context.orientation === "horizontal" ? horizontalListSortingStrategy : verticalListSortingStrategy}
|
|
714
|
+
>
|
|
715
|
+
<ColumnPrimitive
|
|
716
|
+
id={id}
|
|
717
|
+
data-disabled={disabled}
|
|
718
|
+
data-dragging={isDragging ? "" : undefined}
|
|
719
|
+
data-slot="kanban-column"
|
|
720
|
+
{...columnProps}
|
|
721
|
+
{...(asHandle && !disabled ? attributes : {})}
|
|
722
|
+
{...(asHandle && !disabled ? listeners : {})}
|
|
723
|
+
ref={composedRef}
|
|
724
|
+
style={composedStyle}
|
|
725
|
+
className={cn(
|
|
726
|
+
"flex size-full w-full flex-col gap-2 rounded-lg border bg-zinc-100 p-2.5 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:bg-zinc-900",
|
|
727
|
+
{
|
|
728
|
+
"touch-none select-none": asHandle,
|
|
729
|
+
"cursor-default": context.flatCursor,
|
|
730
|
+
"data-dragging:cursor-grabbing": !context.flatCursor,
|
|
731
|
+
"cursor-grab": !isDragging && asHandle && !context.flatCursor,
|
|
732
|
+
"opacity-50": isDragging,
|
|
733
|
+
"pointer-events-none opacity-50": disabled,
|
|
734
|
+
},
|
|
735
|
+
className,
|
|
736
|
+
)}
|
|
737
|
+
/>
|
|
738
|
+
</SortableContext>
|
|
739
|
+
</KanbanColumnContext.Provider>
|
|
740
|
+
);
|
|
741
|
+
});
|
|
742
|
+
KanbanColumn.displayName = COLUMN_NAME;
|
|
743
|
+
|
|
744
|
+
interface KanbanColumnHandleProps extends React.ComponentPropsWithoutRef<"button"> {
|
|
745
|
+
asChild?: boolean;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const KanbanColumnHandle = React.forwardRef<HTMLButtonElement, KanbanColumnHandleProps>((props, forwardedRef) => {
|
|
749
|
+
const { asChild, disabled, className, ...columnHandleProps } = props;
|
|
750
|
+
|
|
751
|
+
const context = useKanbanContext(COLUMN_NAME);
|
|
752
|
+
const columnContext = useKanbanColumnContext(COLUMN_HANDLE_NAME);
|
|
753
|
+
|
|
754
|
+
const isDisabled = disabled ?? columnContext.disabled;
|
|
755
|
+
|
|
756
|
+
const composedRef = useComposedRefs(forwardedRef, (node) => {
|
|
757
|
+
if (isDisabled) return;
|
|
758
|
+
columnContext.setActivatorNodeRef(node);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const HandlePrimitive = asChild ? Slot : "button";
|
|
762
|
+
|
|
763
|
+
return (
|
|
764
|
+
<HandlePrimitive
|
|
765
|
+
type="button"
|
|
766
|
+
aria-controls={columnContext.id}
|
|
767
|
+
data-disabled={isDisabled}
|
|
768
|
+
data-dragging={columnContext.isDragging ? "" : undefined}
|
|
769
|
+
data-slot="kanban-column-handle"
|
|
770
|
+
{...columnHandleProps}
|
|
771
|
+
{...(isDisabled ? {} : columnContext.attributes)}
|
|
772
|
+
{...(isDisabled ? {} : columnContext.listeners)}
|
|
773
|
+
ref={composedRef}
|
|
774
|
+
className={cn(
|
|
775
|
+
"select-none disabled:pointer-events-none disabled:opacity-50",
|
|
776
|
+
context.flatCursor ? "cursor-default" : "cursor-grab data-dragging:cursor-grabbing",
|
|
777
|
+
className,
|
|
778
|
+
)}
|
|
779
|
+
disabled={isDisabled}
|
|
780
|
+
/>
|
|
781
|
+
);
|
|
782
|
+
});
|
|
783
|
+
KanbanColumnHandle.displayName = COLUMN_HANDLE_NAME;
|
|
784
|
+
|
|
785
|
+
interface KanbanItemContextValue {
|
|
786
|
+
id: string;
|
|
787
|
+
attributes: DraggableAttributes;
|
|
788
|
+
listeners: DraggableSyntheticListeners | undefined;
|
|
789
|
+
setActivatorNodeRef: (node: HTMLElement | null) => void;
|
|
790
|
+
isDragging?: boolean;
|
|
791
|
+
disabled?: boolean;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const KanbanItemContext = React.createContext<KanbanItemContextValue | null>(null);
|
|
795
|
+
KanbanItemContext.displayName = ITEM_NAME;
|
|
796
|
+
|
|
797
|
+
function useKanbanItemContext(consumerName: string) {
|
|
798
|
+
const context = React.useContext(KanbanItemContext);
|
|
799
|
+
if (!context) {
|
|
800
|
+
throw new Error(`\`${consumerName}\` must be used within \`${ITEM_NAME}\``);
|
|
801
|
+
}
|
|
802
|
+
return context;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
interface KanbanItemProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
806
|
+
value: UniqueIdentifier;
|
|
807
|
+
asHandle?: boolean;
|
|
808
|
+
asChild?: boolean;
|
|
809
|
+
disabled?: boolean;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const KanbanItem = React.forwardRef<HTMLDivElement, KanbanItemProps>((props, forwardedRef) => {
|
|
813
|
+
const { value, style, asHandle, asChild, disabled, className, ...itemProps } = props;
|
|
814
|
+
|
|
815
|
+
const id = React.useId();
|
|
816
|
+
const context = useKanbanContext(ITEM_NAME);
|
|
817
|
+
const inBoard = React.useContext(KanbanBoardContext);
|
|
818
|
+
const inOverlay = React.useContext(KanbanOverlayContext);
|
|
819
|
+
|
|
820
|
+
if (!inBoard && !inOverlay) {
|
|
821
|
+
throw new Error(`\`${ITEM_NAME}\` must be used within \`${BOARD_NAME}\``);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging } = useSortable({
|
|
825
|
+
id: value,
|
|
826
|
+
disabled,
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
if (value === "") {
|
|
830
|
+
throw new Error(`\`${ITEM_NAME}\` value cannot be an empty string`);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const composedRef = useComposedRefs(forwardedRef, (node) => {
|
|
834
|
+
if (disabled) return;
|
|
835
|
+
setNodeRef(node);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
const composedStyle = React.useMemo<React.CSSProperties>(() => {
|
|
839
|
+
return {
|
|
840
|
+
transform: CSS.Transform.toString(transform),
|
|
841
|
+
transition,
|
|
842
|
+
...style,
|
|
843
|
+
};
|
|
844
|
+
}, [transform, transition, style]);
|
|
845
|
+
|
|
846
|
+
const itemContext = React.useMemo<KanbanItemContextValue>(
|
|
847
|
+
() => ({
|
|
848
|
+
id,
|
|
849
|
+
attributes,
|
|
850
|
+
listeners,
|
|
851
|
+
setActivatorNodeRef,
|
|
852
|
+
isDragging,
|
|
853
|
+
disabled,
|
|
854
|
+
}),
|
|
855
|
+
[id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
const ItemPrimitive = asChild ? Slot : "div";
|
|
859
|
+
|
|
860
|
+
return (
|
|
861
|
+
<KanbanItemContext.Provider value={itemContext}>
|
|
862
|
+
<ItemPrimitive
|
|
863
|
+
id={id}
|
|
864
|
+
data-disabled={disabled}
|
|
865
|
+
data-dragging={isDragging ? "" : undefined}
|
|
866
|
+
data-slot="kanban-item"
|
|
867
|
+
{...itemProps}
|
|
868
|
+
{...(asHandle && !disabled ? attributes : {})}
|
|
869
|
+
{...(asHandle && !disabled ? listeners : {})}
|
|
870
|
+
ref={composedRef}
|
|
871
|
+
style={composedStyle}
|
|
872
|
+
className={cn(
|
|
873
|
+
"focus-visible:ring-ring focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden",
|
|
874
|
+
{
|
|
875
|
+
"touch-none select-none": asHandle,
|
|
876
|
+
"cursor-default": context.flatCursor,
|
|
877
|
+
"data-dragging:cursor-grabbing": !context.flatCursor,
|
|
878
|
+
"cursor-grab": !isDragging && asHandle && !context.flatCursor,
|
|
879
|
+
"opacity-50": isDragging,
|
|
880
|
+
"pointer-events-none opacity-50": disabled,
|
|
881
|
+
},
|
|
882
|
+
className,
|
|
883
|
+
)}
|
|
884
|
+
/>
|
|
885
|
+
</KanbanItemContext.Provider>
|
|
886
|
+
);
|
|
887
|
+
});
|
|
888
|
+
KanbanItem.displayName = ITEM_NAME;
|
|
889
|
+
|
|
890
|
+
interface KanbanItemHandleProps extends React.ComponentPropsWithoutRef<"button"> {
|
|
891
|
+
asChild?: boolean;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const KanbanItemHandle = React.forwardRef<HTMLButtonElement, KanbanItemHandleProps>((props, forwardedRef) => {
|
|
895
|
+
const { asChild, disabled, className, ...itemHandleProps } = props;
|
|
896
|
+
|
|
897
|
+
const context = useKanbanContext(ITEM_HANDLE_NAME);
|
|
898
|
+
const itemContext = useKanbanItemContext(ITEM_HANDLE_NAME);
|
|
899
|
+
|
|
900
|
+
const isDisabled = disabled ?? itemContext.disabled;
|
|
901
|
+
|
|
902
|
+
const composedRef = useComposedRefs(forwardedRef, (node) => {
|
|
903
|
+
if (isDisabled) return;
|
|
904
|
+
itemContext.setActivatorNodeRef(node);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
const HandlePrimitive = asChild ? Slot : "button";
|
|
908
|
+
|
|
909
|
+
return (
|
|
910
|
+
<HandlePrimitive
|
|
911
|
+
type="button"
|
|
912
|
+
aria-controls={itemContext.id}
|
|
913
|
+
data-disabled={isDisabled}
|
|
914
|
+
data-dragging={itemContext.isDragging ? "" : undefined}
|
|
915
|
+
data-slot="kanban-item-handle"
|
|
916
|
+
{...itemHandleProps}
|
|
917
|
+
{...(isDisabled ? {} : itemContext.attributes)}
|
|
918
|
+
{...(isDisabled ? {} : itemContext.listeners)}
|
|
919
|
+
ref={composedRef}
|
|
920
|
+
className={cn(
|
|
921
|
+
"select-none disabled:pointer-events-none disabled:opacity-50",
|
|
922
|
+
context.flatCursor ? "cursor-default" : "cursor-grab data-dragging:cursor-grabbing",
|
|
923
|
+
className,
|
|
924
|
+
)}
|
|
925
|
+
disabled={isDisabled}
|
|
926
|
+
/>
|
|
927
|
+
);
|
|
928
|
+
});
|
|
929
|
+
KanbanItemHandle.displayName = ITEM_HANDLE_NAME;
|
|
930
|
+
|
|
931
|
+
const KanbanOverlayContext = React.createContext(false);
|
|
932
|
+
KanbanOverlayContext.displayName = OVERLAY_NAME;
|
|
933
|
+
|
|
934
|
+
const dropAnimation: DropAnimation = {
|
|
935
|
+
sideEffects: defaultDropAnimationSideEffects({
|
|
936
|
+
styles: {
|
|
937
|
+
active: {
|
|
938
|
+
opacity: "0.4",
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
}),
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
interface KanbanOverlayProps extends Omit<React.ComponentPropsWithoutRef<typeof DragOverlay>, "children"> {
|
|
945
|
+
container?: Element | DocumentFragment | null;
|
|
946
|
+
children?: ((params: { value: UniqueIdentifier; variant: "column" | "item" }) => React.ReactNode) | React.ReactNode;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function KanbanOverlay(props: KanbanOverlayProps) {
|
|
950
|
+
const { container: containerProp, children, ...overlayProps } = props;
|
|
951
|
+
|
|
952
|
+
const context = useKanbanContext(OVERLAY_NAME);
|
|
953
|
+
|
|
954
|
+
const [mounted, setMounted] = React.useState(false);
|
|
955
|
+
React.useLayoutEffect(() => setMounted(true), []);
|
|
956
|
+
|
|
957
|
+
const container = containerProp ?? (mounted ? globalThis.document?.body : null);
|
|
958
|
+
|
|
959
|
+
if (!container) return null;
|
|
960
|
+
|
|
961
|
+
const variant = context.activeId && context.activeId in context.items ? "column" : "item";
|
|
962
|
+
|
|
963
|
+
return ReactDOM.createPortal(
|
|
964
|
+
<DragOverlay
|
|
965
|
+
dropAnimation={dropAnimation}
|
|
966
|
+
modifiers={context.modifiers}
|
|
967
|
+
className={cn(!context.flatCursor && "cursor-grabbing")}
|
|
968
|
+
{...overlayProps}
|
|
969
|
+
>
|
|
970
|
+
<KanbanOverlayContext.Provider value={true}>
|
|
971
|
+
{context.activeId && children
|
|
972
|
+
? typeof children === "function"
|
|
973
|
+
? children({
|
|
974
|
+
value: context.activeId,
|
|
975
|
+
variant,
|
|
976
|
+
})
|
|
977
|
+
: children
|
|
978
|
+
: null}
|
|
979
|
+
</KanbanOverlayContext.Provider>
|
|
980
|
+
</DragOverlay>,
|
|
981
|
+
container,
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export {
|
|
986
|
+
KanbanBoard as Board,
|
|
987
|
+
KanbanColumn as Column,
|
|
988
|
+
KanbanColumnHandle as ColumnHandle,
|
|
989
|
+
KanbanItem as Item,
|
|
990
|
+
KanbanItemHandle as ItemHandle,
|
|
991
|
+
KanbanRoot as Kanban,
|
|
992
|
+
KanbanBoard,
|
|
993
|
+
KanbanColumn,
|
|
994
|
+
KanbanColumnHandle,
|
|
995
|
+
KanbanItem,
|
|
996
|
+
KanbanItemHandle,
|
|
997
|
+
KanbanOverlay,
|
|
998
|
+
KanbanOverlay as Overlay,
|
|
999
|
+
//
|
|
1000
|
+
KanbanRoot as Root,
|
|
1001
|
+
};
|