@ciridae/ui 0.11.2 → 0.12.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.
@@ -8,6 +8,7 @@ type FeedbackCenterTab = "mine" | "submit";
8
8
  type FeedbackCenterView = "detail" | "list";
9
9
  type FeedbackCenterComposerProps = Omit<FeedbackComposerProps, "actions" | "formId" | "onCancel" | "onDirtyChange" | "onSubmittedChange" | "onUrgencyChange">;
10
10
  type FeedbackCenterTabsProps = {
11
+ canComment?: (item: UserFeedbackItem) => boolean;
11
12
  commentsByFeedbackId: Record<string, CommentItemData[]>;
12
13
  commentsError?: Error | null;
13
14
  composerProps: FeedbackCenterComposerProps;
@@ -33,5 +34,5 @@ type FeedbackCenterTabsProps = {
33
34
  declare function FeedbackCenterSheetHeader({ unreadReplyCount }: {
34
35
  unreadReplyCount: number;
35
36
  }): React.JSX.Element;
36
- declare function FeedbackCenterTabs({ composerProps, commentsByFeedbackId, commentsError, formId, onDirtyChange, onUrgencyChange, isCommentSubmitting, isCommentsLoading, isLoading, items, itemsError, onBack, onCancel, onSelectFeedback, onRetryComments, onRetryItems, onSubmittedChange, onSubmitComment, selectedFeedback, renderEvidence, view, }: FeedbackCenterTabsProps): React.JSX.Element;
37
+ declare function FeedbackCenterTabs({ canComment, composerProps, commentsByFeedbackId, commentsError, formId, onDirtyChange, onUrgencyChange, isCommentSubmitting, isCommentsLoading, isLoading, items, itemsError, onBack, onCancel, onSelectFeedback, onRetryComments, onRetryItems, onSubmittedChange, onSubmitComment, selectedFeedback, renderEvidence, view, }: FeedbackCenterTabsProps): React.JSX.Element;
37
38
  export { FeedbackCenterSheetHeader, FeedbackCenterTabs, type FeedbackCenterTab, type FeedbackCenterView, };
@@ -9,8 +9,8 @@ import { TabsContent, TabsList, TabsTrigger } from "../ui/tabs.js";
9
9
  function FeedbackCenterSheetHeader({ unreadReplyCount }) {
10
10
  return (_jsxs(SheetHeader, { className: "gap-3", children: [_jsxs("div", { className: "grid gap-1.5 pr-12", children: [_jsx(SheetTitle, { children: "Feedback" }), _jsx(SheetDescription, { children: "Send feedback and track your submissions in one place." })] }), _jsx(FeedbackCenterTabsList, { unreadReplyCount: unreadReplyCount })] }));
11
11
  }
12
- function FeedbackCenterTabs({ composerProps, commentsByFeedbackId, commentsError, formId, onDirtyChange, onUrgencyChange, isCommentSubmitting, isCommentsLoading, isLoading, items, itemsError, onBack, onCancel, onSelectFeedback, onRetryComments, onRetryItems, onSubmittedChange, onSubmitComment, selectedFeedback, renderEvidence, view, }) {
13
- return (_jsxs(_Fragment, { children: [_jsx(FeedbackCenterSubmitTab, { composerProps: composerProps, formId: formId, onCancel: onCancel, onDirtyChange: onDirtyChange, onSubmittedChange: onSubmittedChange, onUrgencyChange: onUrgencyChange }), _jsx(FeedbackCenterMineContent, { commentsByFeedbackId: commentsByFeedbackId, commentsError: commentsError, isCommentSubmitting: isCommentSubmitting, isCommentsLoading: isCommentsLoading, isLoading: isLoading, items: items, itemsError: itemsError, onBack: onBack, onSelectFeedback: onSelectFeedback, onRetryComments: onRetryComments, onRetryItems: onRetryItems, onSubmitComment: onSubmitComment, selectedFeedback: selectedFeedback, renderEvidence: renderEvidence, view: view })] }));
12
+ function FeedbackCenterTabs({ canComment, composerProps, commentsByFeedbackId, commentsError, formId, onDirtyChange, onUrgencyChange, isCommentSubmitting, isCommentsLoading, isLoading, items, itemsError, onBack, onCancel, onSelectFeedback, onRetryComments, onRetryItems, onSubmittedChange, onSubmitComment, selectedFeedback, renderEvidence, view, }) {
13
+ return (_jsxs(_Fragment, { children: [_jsx(FeedbackCenterSubmitTab, { composerProps: composerProps, formId: formId, onCancel: onCancel, onDirtyChange: onDirtyChange, onSubmittedChange: onSubmittedChange, onUrgencyChange: onUrgencyChange }), _jsx(FeedbackCenterMineContent, { commentsByFeedbackId: commentsByFeedbackId, canComment: canComment, commentsError: commentsError, isCommentSubmitting: isCommentSubmitting, isCommentsLoading: isCommentsLoading, isLoading: isLoading, items: items, itemsError: itemsError, onBack: onBack, onSelectFeedback: onSelectFeedback, onRetryComments: onRetryComments, onRetryItems: onRetryItems, onSubmitComment: onSubmitComment, selectedFeedback: selectedFeedback, renderEvidence: renderEvidence, view: view })] }));
14
14
  }
15
15
  function FeedbackCenterTabsList({ unreadReplyCount }) {
16
16
  return (_jsxs(TabsList, { className: "w-full", children: [_jsx(TabsTrigger, { value: "submit", children: "Send feedback" }), _jsxs(TabsTrigger, { value: "mine", children: ["My feedback", unreadReplyCount > 0 ? _jsx(Badge, { variant: "secondary", children: unreadReplyCount }) : null] })] }));
@@ -22,14 +22,14 @@ function FeedbackCenterSubmitTab({ composerProps, formId, onCancel, onDirtyChang
22
22
  function FeedbackCenterMineContent(props) {
23
23
  return (_jsx(TabsContent, { className: "mt-0", value: "mine", children: _jsx(FeedbackCenterMineTab, { ...props }) }));
24
24
  }
25
- function FeedbackCenterMineTab({ isLoading, commentsByFeedbackId, commentsError, isCommentSubmitting, isCommentsLoading, items, itemsError, onBack, onSelectFeedback, onRetryComments, onRetryItems, onSubmitComment, selectedFeedback, renderEvidence, view, }) {
25
+ function FeedbackCenterMineTab({ canComment, isLoading, commentsByFeedbackId, commentsError, isCommentSubmitting, isCommentsLoading, items, itemsError, onBack, onSelectFeedback, onRetryComments, onRetryItems, onSubmitComment, selectedFeedback, renderEvidence, view, }) {
26
26
  if (view === "detail") {
27
- return (_jsx(FeedbackCenterDetailPane, { comments: getFeedbackComments(commentsByFeedbackId, selectedFeedback), commentsError: commentsError, feedback: selectedFeedback, isCommentSubmitting: isCommentSubmitting, isCommentsLoading: isCommentsLoading, onBack: onBack, onRetryComments: onRetryComments, onSubmitComment: onSubmitComment, renderEvidence: renderEvidence }));
27
+ return (_jsx(FeedbackCenterDetailPane, { comments: getFeedbackComments(commentsByFeedbackId, selectedFeedback), canComment: canComment, commentsError: commentsError, feedback: selectedFeedback, isCommentSubmitting: isCommentSubmitting, isCommentsLoading: isCommentsLoading, onBack: onBack, onRetryComments: onRetryComments, onSubmitComment: onSubmitComment, renderEvidence: renderEvidence }));
28
28
  }
29
29
  return (_jsx(FeedbackUserList, { error: itemsError, isLoading: isLoading, items: items, onSelectFeedback: onSelectFeedback, onRetry: onRetryItems, selectedId: selectedFeedback?.id }));
30
30
  }
31
- function FeedbackCenterDetailPane({ comments, commentsError, feedback, isCommentSubmitting, isCommentsLoading, onBack, onRetryComments, onSubmitComment, renderEvidence, }) {
32
- return (_jsx(FeedbackUserDetail, { activityLabel: "Public conversation", activitySlot: _jsx(FeedbackCenterCommentThread, { comments: comments, error: commentsError, feedback: feedback, isCommentSubmitting: isCommentSubmitting, isCommentsLoading: isCommentsLoading, onSubmitComment: onSubmitComment, onRetry: onRetryComments }), evidenceSlot: feedback ? renderEvidence?.(feedback) : undefined, feedback: feedback, onBack: onBack }));
31
+ function FeedbackCenterDetailPane({ canComment, comments, commentsError, feedback, isCommentSubmitting, isCommentsLoading, onBack, onRetryComments, onSubmitComment, renderEvidence, }) {
32
+ return (_jsx(FeedbackUserDetail, { activityLabel: "Public conversation", activitySlot: _jsx(FeedbackCenterCommentThread, { comments: comments, error: commentsError, feedback: feedback, isCommentSubmitting: isCommentSubmitting, isCommentsLoading: isCommentsLoading, onSubmitComment: feedback && canComment?.(feedback) === false ? undefined : onSubmitComment, onRetry: onRetryComments }), evidenceSlot: feedback ? renderEvidence?.(feedback) : undefined, feedback: feedback, onBack: onBack }));
33
33
  }
34
34
  function getFeedbackComments(commentsByFeedbackId, feedback) {
35
35
  if (!feedback)
@@ -5,6 +5,7 @@ import { type FeedbackCenterTab } from "./feedback-center-sheet-parts.js";
5
5
  import type { FeedbackComposerProps } from "./feedback-composer.js";
6
6
  import type { UserFeedbackItem } from "./feedback-tracking-types.js";
7
7
  type FeedbackCenterSheetProps = Omit<FeedbackComposerProps, "actions" | "formId" | "onCancel" | "onDirtyChange" | "onSubmittedChange" | "onUrgencyChange"> & {
8
+ canComment?: (item: UserFeedbackItem) => boolean;
8
9
  commentsByFeedbackId?: Record<string, CommentItemData[]>;
9
10
  commentsError?: Error | null;
10
11
  defaultTab?: FeedbackCenterTab;
@@ -22,4 +23,4 @@ type FeedbackCenterSheetProps = Omit<FeedbackComposerProps, "actions" | "formId"
22
23
  renderEvidence?: (item: UserFeedbackItem) => React.ReactNode;
23
24
  };
24
25
  declare function FeedbackCenterSheet(props: FeedbackCenterSheetProps): React.JSX.Element;
25
- export { FeedbackCenterSheet, type FeedbackCenterSheetProps };
26
+ export { FeedbackCenterSheet, type FeedbackCenterSheetProps, type FeedbackCenterTab };
@@ -11,7 +11,7 @@ function FeedbackCenterSheet(props) {
11
11
  }
12
12
  function FeedbackCenterSheetContent({ props, state, }) {
13
13
  const { contentProps, requestClose } = useFormSheetActions();
14
- return (_jsx(SheetContent, { size: "md", ...contentProps, children: _jsxs(Tabs, { className: "min-h-0 flex-1", onValueChange: setFeedbackCenterTab(state.handleTabChange), value: state.activeTab, children: [_jsx(FeedbackCenterSheetHeader, { unreadReplyCount: state.unreadReplyCount }), _jsx(SheetBody, { className: "pb-(--panel-padding)", children: _jsx(FeedbackCenterTabs, { composerProps: getFeedbackComposerProps(props), commentsByFeedbackId: props.commentsByFeedbackId ?? {}, commentsError: props.commentsError, formId: state.formId, isCommentSubmitting: props.isCommentSubmitting ?? false, isCommentsLoading: props.isCommentsLoading ?? false, isLoading: props.isLoading ?? false, items: props.items, itemsError: props.itemsError, onBack: state.handleBack, onCancel: requestClose, onDirtyChange: state.setIsDirty, onRetryComments: props.onRetryComments, onRetryItems: props.onRetryItems, onSelectFeedback: state.handleSelectFeedback, onSubmitComment: props.onSubmitComment, onSubmittedChange: state.setIsSubmitted, onUrgencyChange: state.setUrgency, renderEvidence: props.renderEvidence, selectedFeedback: state.selectedFeedback, view: state.view }) }), _jsx(FeedbackCenterFooter, { activeTab: state.activeTab, formId: state.formId, isSubmitted: state.isSubmitted, isSubmitting: props.isSubmitting ?? false, onCancel: requestClose, urgency: state.urgency })] }) }));
14
+ return (_jsx(SheetContent, { size: "md", ...contentProps, children: _jsxs(Tabs, { className: "min-h-0 flex-1", onValueChange: setFeedbackCenterTab(state.handleTabChange), value: state.activeTab, children: [_jsx(FeedbackCenterSheetHeader, { unreadReplyCount: state.unreadReplyCount }), _jsx(SheetBody, { className: "pb-(--panel-padding)", children: _jsx(FeedbackCenterTabs, { canComment: props.canComment, composerProps: getFeedbackComposerProps(props), commentsByFeedbackId: props.commentsByFeedbackId ?? {}, commentsError: props.commentsError, formId: state.formId, isCommentSubmitting: props.isCommentSubmitting ?? false, isCommentsLoading: props.isCommentsLoading ?? false, isLoading: props.isLoading ?? false, items: props.items, itemsError: props.itemsError, onBack: state.handleBack, onCancel: requestClose, onDirtyChange: state.setIsDirty, onRetryComments: props.onRetryComments, onRetryItems: props.onRetryItems, onSelectFeedback: state.handleSelectFeedback, onSubmitComment: props.onSubmitComment, onSubmittedChange: state.setIsSubmitted, onUrgencyChange: state.setUrgency, renderEvidence: props.renderEvidence, selectedFeedback: state.selectedFeedback, view: state.view }) }), _jsx(FeedbackCenterFooter, { activeTab: state.activeTab, formId: state.formId, isSubmitted: state.isSubmitted, isSubmitting: props.isSubmitting ?? false, onCancel: requestClose, urgency: state.urgency })] }) }));
15
15
  }
16
16
  function useFeedbackCenterSheetState(props) {
17
17
  const defaultTab = props.defaultTab ?? "submit";
@@ -10,5 +10,6 @@ const feedbackStatusVariants = {
10
10
  in_progress: "info",
11
11
  no_action: "secondary",
12
12
  received: "outline",
13
+ withdrawn: "secondary",
13
14
  };
14
15
  export { FeedbackStatusBadge };
@@ -72,6 +72,7 @@ const statusSortOrder = {
72
72
  in_progress: 1,
73
73
  no_action: 4,
74
74
  received: 0,
75
+ withdrawn: 5,
75
76
  };
76
77
  const typeSortOrder = {
77
78
  bug: 0,
@@ -1,4 +1,4 @@
1
- declare const feedbackStatusValues: readonly ["received", "in_progress", "discussion_required", "completed", "no_action"];
1
+ declare const feedbackStatusValues: readonly ["received", "in_progress", "discussion_required", "completed", "no_action", "withdrawn"];
2
2
  type FeedbackStatus = (typeof feedbackStatusValues)[number];
3
3
  type StakeholderFeedbackType = "bug" | "feature" | "general";
4
4
  type FeedbackUrgency = "emergency" | "normal";
@@ -75,6 +75,9 @@ declare const feedbackStatusOptions: readonly [{
75
75
  }, {
76
76
  readonly label: string;
77
77
  readonly value: "no_action";
78
+ }, {
79
+ readonly label: string;
80
+ readonly value: "withdrawn";
78
81
  }];
79
82
  declare const feedbackTypeOptions: readonly [{
80
83
  readonly label: "All types";
@@ -5,6 +5,7 @@ const feedbackStatusValues = [
5
5
  "discussion_required",
6
6
  "completed",
7
7
  "no_action",
8
+ "withdrawn",
8
9
  ];
9
10
  const feedbackStatusLabels = {
10
11
  completed: "Completed",
@@ -12,6 +13,7 @@ const feedbackStatusLabels = {
12
13
  in_progress: "In progress",
13
14
  no_action: "No action",
14
15
  received: "Received",
16
+ withdrawn: "Withdrawn",
15
17
  };
16
18
  const feedbackTypeLabels = {
17
19
  bug: "Bug",
@@ -25,6 +27,7 @@ const feedbackStatusOptions = [
25
27
  { label: feedbackStatusLabels.discussion_required, value: "discussion_required" },
26
28
  { label: feedbackStatusLabels.completed, value: "completed" },
27
29
  { label: feedbackStatusLabels.no_action, value: "no_action" },
30
+ { label: feedbackStatusLabels.withdrawn, value: "withdrawn" },
28
31
  ];
29
32
  const feedbackTypeOptions = [
30
33
  { label: "All types", value: "all" },
@@ -1,5 +1,5 @@
1
1
  import type * as React from "react";
2
- import type { DragEndEvent, DragOverEvent, DragStartEvent } from "@dnd-kit/core";
2
+ import type { CollisionDetection, DragEndEvent, DragOverEvent, DragStartEvent } from "@dnd-kit/core";
3
3
  import type { KanbanCardData, KanbanColumnData, KanbanMoveEvent } from "./kanban-types.js";
4
4
  type KanbanBoardState = {
5
5
  cardById: Map<string, KanbanCardData>;
@@ -38,5 +38,21 @@ declare function getDragEndHandler({ activeMove, boardState, onCardMove, setters
38
38
  }): (event: DragEndEvent) => void;
39
39
  declare function clearDragState(setters: KanbanDragStateSetters): void;
40
40
  declare function getColumnDroppableId(columnId: string): string;
41
- export { clearDragState, getColumnDroppableId, getDragEndHandler, getDragOverHandler, getDragStartHandler, getKanbanBoardState, };
41
+ /**
42
+ * What a dragged card is over, preferring whatever the pointer is inside.
43
+ *
44
+ * Corner distance alone cannot reach an empty column. A column's drop target is
45
+ * the whole column, so its corners sit far from a pointer in the middle of it,
46
+ * while a card in the column alongside is small and close. The neighbour wins,
47
+ * and a column nobody has a card in becomes impossible to drop into.
48
+ *
49
+ * Cards are preferred over the column that contains them, because a pointer
50
+ * inside a card is always inside its column too, and answering "the column"
51
+ * there would send every drop to the end of the list instead of where it was
52
+ * aimed. Corner distance still decides when the pointer is outside every
53
+ * droppable, which keeps a card released off the edge of the board landing
54
+ * somewhere sensible.
55
+ */
56
+ declare const kanbanCollisionDetection: CollisionDetection;
57
+ export { clearDragState, getColumnDroppableId, getDragEndHandler, getDragOverHandler, getDragStartHandler, getKanbanBoardState, kanbanCollisionDetection, };
42
58
  export type { KanbanBoardState, KanbanDragStateSetters, KanbanOverlayRect };
@@ -1,3 +1,4 @@
1
+ import { closestCorners, pointerWithin } from "@dnd-kit/core";
1
2
  function getKanbanBoardState(columns, cards) {
2
3
  const cardsByColumnId = new Map();
3
4
  const cardById = new Map();
@@ -111,5 +112,29 @@ function areKanbanMovesEqual(currentMove, nextMove) {
111
112
  currentMove.fromIndex === nextMove.fromIndex &&
112
113
  currentMove.toIndex === nextMove.toIndex);
113
114
  }
115
+ /**
116
+ * What a dragged card is over, preferring whatever the pointer is inside.
117
+ *
118
+ * Corner distance alone cannot reach an empty column. A column's drop target is
119
+ * the whole column, so its corners sit far from a pointer in the middle of it,
120
+ * while a card in the column alongside is small and close. The neighbour wins,
121
+ * and a column nobody has a card in becomes impossible to drop into.
122
+ *
123
+ * Cards are preferred over the column that contains them, because a pointer
124
+ * inside a card is always inside its column too, and answering "the column"
125
+ * there would send every drop to the end of the list instead of where it was
126
+ * aimed. Corner distance still decides when the pointer is outside every
127
+ * droppable, which keeps a card released off the edge of the board landing
128
+ * somewhere sensible.
129
+ */
130
+ const kanbanCollisionDetection = (args) => {
131
+ const underPointer = pointerWithin(args);
132
+ const cards = underPointer.filter((collision) => !isColumnDroppableId(String(collision.id)));
133
+ if (cards.length > 0)
134
+ return cards;
135
+ if (underPointer.length > 0)
136
+ return underPointer;
137
+ return closestCorners(args);
138
+ };
114
139
  const columnDroppablePrefix = "kanban-column:";
115
- export { clearDragState, getColumnDroppableId, getDragEndHandler, getDragOverHandler, getDragStartHandler, getKanbanBoardState, };
140
+ export { clearDragState, getColumnDroppableId, getDragEndHandler, getDragOverHandler, getDragStartHandler, getKanbanBoardState, kanbanCollisionDetection, };
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /* eslint-disable react-hooks/refs -- dnd-kit drag handlers need a mutable ref to suppress the click fired after a drag. */
3
3
  import * as React from "react";
4
- import { closestCorners, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core";
4
+ import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
5
5
  import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
6
6
  import { KanbanBoardColumn, KanbanDragOverlay } from "./kanban-board-parts.js";
7
- import { clearDragState, getDragEndHandler, getDragOverHandler, getDragStartHandler, getKanbanBoardState, } from "./kanban-board-utils.js";
7
+ import { clearDragState, getDragEndHandler, getDragOverHandler, getDragStartHandler, getKanbanBoardState, kanbanCollisionDetection, } from "./kanban-board-utils.js";
8
8
  function KanbanBoard({ cards, columns, columnMaxHeight = "min(44rem, calc(100vh - 14rem))", empty, isLoading = false, onCardMove, onCardSelect, renderCard, renderCardActions, renderColumnActions, }) {
9
9
  const [activeCard, setActiveCard] = React.useState();
10
10
  const [activeCardRect, setActiveCardRect] = React.useState();
@@ -14,7 +14,7 @@ function KanbanBoard({ cards, columns, columnMaxHeight = "min(44rem, calc(100vh
14
14
  const boardState = React.useMemo(() => getKanbanBoardState(columns, cards), [cards, columns]);
15
15
  const dragStateSetters = React.useMemo(() => ({ setActiveCard, setActiveCardRect, setActiveMove, setOverColumnId }), []);
16
16
  const sensors = useKanbanSensors();
17
- return (_jsxs(DndContext, { collisionDetection: closestCorners, onDragCancel: () => clearDragState(dragStateSetters), onDragEnd: getDragEndHandler({
17
+ return (_jsxs(DndContext, { collisionDetection: kanbanCollisionDetection, onDragCancel: () => clearDragState(dragStateSetters), onDragEnd: getDragEndHandler({
18
18
  activeMove,
19
19
  boardState,
20
20
  onCardMove,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciridae/ui",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "engines": {