@ciridae/ui 0.11.3 → 0.12.1
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/components/blocks/comment-thread.js +1 -1
- package/dist/components/blocks/feedback-center-comments.d.ts +3 -2
- package/dist/components/blocks/feedback-center-comments.js +35 -3
- package/dist/components/blocks/feedback-center-sheet-parts.d.ts +4 -2
- package/dist/components/blocks/feedback-center-sheet-parts.js +11 -6
- package/dist/components/blocks/feedback-center-sheet.d.ts +4 -2
- package/dist/components/blocks/feedback-center-sheet.js +1 -1
- package/dist/components/blocks/feedback-status-badge.js +1 -0
- package/dist/components/blocks/feedback-table-sort.js +1 -0
- package/dist/components/blocks/feedback-tracking-types.d.ts +10 -2
- package/dist/components/blocks/feedback-tracking-types.js +3 -0
- package/dist/components/blocks/feedback-user-detail.js +7 -8
- package/dist/components/blocks/feedback-user-list.js +4 -4
- package/dist/components/blocks/timestamp-format.js +5 -42
- package/package.json +2 -1
|
@@ -55,6 +55,6 @@ function CommentSkeletonRow() {
|
|
|
55
55
|
return (_jsxs("div", { className: "flex gap-3 px-1 py-2", children: [_jsx(Skeleton, { className: "size-6 rounded-full" }), _jsxs("div", { className: "grid flex-1 gap-2", children: [_jsx(Skeleton, { className: "h-3 w-40" }), _jsx(Skeleton, { className: "h-3 w-full" }), _jsx(Skeleton, { className: "h-3 w-4/5" })] })] }));
|
|
56
56
|
}
|
|
57
57
|
function CommentThreadEmpty({ description, title }) {
|
|
58
|
-
return (_jsxs("div", { className: "flex min-w-0 items-start gap-2
|
|
58
|
+
return (_jsxs("div", { className: "flex min-w-0 items-start gap-2 py-2", children: [_jsx(MessageSquareTextIcon, { className: "mt-0.5 size-4 shrink-0 text-muted-foreground" }), _jsxs("div", { className: "grid min-w-0 gap-0.5", children: [_jsx("div", { className: "text-sm font-medium text-foreground", children: title }), _jsx("p", { className: "text-sm text-muted-foreground", children: description })] })] }));
|
|
59
59
|
}
|
|
60
60
|
export { CommentThread };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CommentItemData } from "./comment-types.js";
|
|
2
|
-
import type { UserFeedbackItem } from "./feedback-tracking-types.js";
|
|
2
|
+
import type { FeedbackStatusEvent, UserFeedbackItem } from "./feedback-tracking-types.js";
|
|
3
3
|
type FeedbackCommentSubmitHandler = (feedback: UserFeedbackItem, body: string) => void | Promise<void>;
|
|
4
4
|
type FeedbackCenterCommentThreadProps = {
|
|
5
5
|
comments: CommentItemData[];
|
|
@@ -9,6 +9,7 @@ type FeedbackCenterCommentThreadProps = {
|
|
|
9
9
|
isCommentsLoading: boolean;
|
|
10
10
|
onRetry?: () => void;
|
|
11
11
|
onSubmitComment?: FeedbackCommentSubmitHandler;
|
|
12
|
+
statusEvents: FeedbackStatusEvent[];
|
|
12
13
|
};
|
|
13
|
-
declare function FeedbackCenterCommentThread({ comments, error, feedback, isCommentSubmitting, isCommentsLoading, onRetry, onSubmitComment, }: FeedbackCenterCommentThreadProps): import("@types/react").JSX.Element;
|
|
14
|
+
declare function FeedbackCenterCommentThread({ comments, error, feedback, isCommentSubmitting, isCommentsLoading, onRetry, onSubmitComment, statusEvents, }: FeedbackCenterCommentThreadProps): import("@types/react").JSX.Element;
|
|
14
15
|
export { FeedbackCenterCommentThread, type FeedbackCommentSubmitHandler };
|
|
@@ -1,14 +1,46 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { AlertCircleIcon } from "lucide-react";
|
|
3
|
-
import {
|
|
3
|
+
import { CommentComposer } from "./comment-composer.js";
|
|
4
|
+
import { CommentItem } from "./comment-item.js";
|
|
5
|
+
import { formatFeedbackTimestamp, getFeedbackTimestampDateTime } from "./feedback-date-format.js";
|
|
6
|
+
import { FeedbackStatusBadge } from "./feedback-status-badge.js";
|
|
4
7
|
import { Alert, AlertDescription, AlertTitle } from "../ui/alert.js";
|
|
5
8
|
import { Button } from "../ui/button.js";
|
|
6
|
-
|
|
9
|
+
import { Skeleton } from "../ui/skeleton.js";
|
|
10
|
+
function FeedbackCenterCommentThread({ comments, error, feedback, isCommentSubmitting, isCommentsLoading, onRetry, onSubmitComment, statusEvents, }) {
|
|
7
11
|
// A retry in flight shows the thread's loading state, not the stale error.
|
|
8
12
|
if (error && !isCommentsLoading) {
|
|
9
13
|
return (_jsxs(Alert, { variant: "destructive", children: [_jsx(AlertCircleIcon, {}), _jsx(AlertTitle, { children: "Conversation could not be loaded" }), _jsxs(AlertDescription, { className: "grid gap-3", children: [_jsx("span", { children: error.message }), onRetry ? (_jsx(Button, { className: "justify-self-start", onClick: onRetry, size: "sm", type: "button", variant: "outline", children: "Try again" })) : null] })] }));
|
|
10
14
|
}
|
|
11
|
-
|
|
15
|
+
const submitComment = getCommentSubmitHandler(feedback, onSubmitComment);
|
|
16
|
+
const activity = mergeFeedbackActivity(statusEvents, comments);
|
|
17
|
+
return (_jsxs("section", { "aria-label": "Activity", className: "grid gap-(--panel-gap)", children: [_jsxs("div", { className: "grid gap-1", children: [activity.map((entry) => entry.kind === "comment" ? (_jsx(CommentItem, { comment: entry.comment }, entry.comment.id)) : (_jsx(FeedbackStatusActivity, { event: entry.event }, entry.event.id))), isCommentsLoading ? _jsx(FeedbackActivityLoading, {}) : null] }), submitComment ? (_jsx(CommentComposer, { isSubmitting: isCommentSubmitting, onSubmit: submitComment, placeholder: "Write a reply...", submitLabel: "Reply" })) : null] }));
|
|
18
|
+
}
|
|
19
|
+
function mergeFeedbackActivity(statusEvents, comments) {
|
|
20
|
+
return [
|
|
21
|
+
...statusEvents.map((event) => ({
|
|
22
|
+
createdAt: event.createdAt,
|
|
23
|
+
event,
|
|
24
|
+
kind: "status",
|
|
25
|
+
})),
|
|
26
|
+
...comments.map((comment) => ({
|
|
27
|
+
comment,
|
|
28
|
+
createdAt: comment.createdAt,
|
|
29
|
+
kind: "comment",
|
|
30
|
+
})),
|
|
31
|
+
].sort(compareActivityEntries);
|
|
32
|
+
}
|
|
33
|
+
function compareActivityEntries(left, right) {
|
|
34
|
+
const timestampOrder = left.createdAt.localeCompare(right.createdAt);
|
|
35
|
+
if (timestampOrder !== 0)
|
|
36
|
+
return timestampOrder;
|
|
37
|
+
return left.kind === right.kind ? 0 : left.kind === "status" ? -1 : 1;
|
|
38
|
+
}
|
|
39
|
+
function FeedbackStatusActivity({ event }) {
|
|
40
|
+
return (_jsxs("div", { className: "flex min-w-0 items-center gap-2 py-2 text-xs text-muted-foreground", children: [_jsx("span", { "aria-hidden": true, className: "size-1.5 shrink-0 rounded-full bg-border" }), _jsx("span", { children: "Status changed to" }), _jsx(FeedbackStatusBadge, { status: event.status }), _jsx("time", { className: "ml-auto shrink-0 font-mono tabular-nums", dateTime: getFeedbackTimestampDateTime(event.createdAt), children: formatFeedbackTimestamp(event.createdAt) })] }));
|
|
41
|
+
}
|
|
42
|
+
function FeedbackActivityLoading() {
|
|
43
|
+
return (_jsxs("div", { "aria-label": "Loading activity", className: "flex gap-3 px-1 py-2", role: "status", children: [_jsx(Skeleton, { className: "size-6 rounded-full" }), _jsxs("div", { className: "grid flex-1 gap-2", children: [_jsx(Skeleton, { className: "h-3 w-32" }), _jsx(Skeleton, { className: "h-3 w-4/5" })] })] }));
|
|
12
44
|
}
|
|
13
45
|
function getCommentSubmitHandler(feedback, onSubmitComment) {
|
|
14
46
|
if (!feedback || !onSubmitComment)
|
|
@@ -3,11 +3,12 @@ import type { CommentItemData } from "./comment-types.js";
|
|
|
3
3
|
import { type FeedbackCommentSubmitHandler } from "./feedback-center-comments.js";
|
|
4
4
|
import { type FeedbackComposerProps } from "./feedback-composer.js";
|
|
5
5
|
import type { FeedbackUrgency } from "./feedback-composer-types.js";
|
|
6
|
-
import type { UserFeedbackItem } from "./feedback-tracking-types.js";
|
|
6
|
+
import type { FeedbackStatusEvent, UserFeedbackItem } from "./feedback-tracking-types.js";
|
|
7
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;
|
|
@@ -28,10 +29,11 @@ type FeedbackCenterTabsProps = {
|
|
|
28
29
|
onSubmitComment?: FeedbackCommentSubmitHandler;
|
|
29
30
|
selectedFeedback?: UserFeedbackItem;
|
|
30
31
|
renderEvidence?: (item: UserFeedbackItem) => React.ReactNode;
|
|
32
|
+
statusEventsByFeedbackId: Record<string, FeedbackStatusEvent[]>;
|
|
31
33
|
view: FeedbackCenterView;
|
|
32
34
|
};
|
|
33
35
|
declare function FeedbackCenterSheetHeader({ unreadReplyCount }: {
|
|
34
36
|
unreadReplyCount: number;
|
|
35
37
|
}): 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;
|
|
38
|
+
declare function FeedbackCenterTabs({ canComment, composerProps, commentsByFeedbackId, commentsError, formId, onDirtyChange, onUrgencyChange, isCommentSubmitting, isCommentsLoading, isLoading, items, itemsError, onBack, onCancel, onSelectFeedback, onRetryComments, onRetryItems, onSubmittedChange, onSubmitComment, selectedFeedback, renderEvidence, statusEventsByFeedbackId, view, }: FeedbackCenterTabsProps): React.JSX.Element;
|
|
37
39
|
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, statusEventsByFeedbackId, 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, statusEventsByFeedbackId: statusEventsByFeedbackId, 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,18 +22,23 @@ 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, statusEventsByFeedbackId, 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, statusEvents: getFeedbackStatusEvents(statusEventsByFeedbackId, selectedFeedback) }));
|
|
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: "
|
|
31
|
+
function FeedbackCenterDetailPane({ canComment, comments, commentsError, feedback, isCommentSubmitting, isCommentsLoading, onBack, onRetryComments, onSubmitComment, renderEvidence, statusEvents, }) {
|
|
32
|
+
return (_jsx(FeedbackUserDetail, { activityLabel: "Activity", activitySlot: _jsx(FeedbackCenterCommentThread, { comments: comments, error: commentsError, feedback: feedback, isCommentSubmitting: isCommentSubmitting, isCommentsLoading: isCommentsLoading, onSubmitComment: feedback && canComment?.(feedback) === false ? undefined : onSubmitComment, onRetry: onRetryComments, statusEvents: statusEvents }), evidenceSlot: feedback ? renderEvidence?.(feedback) : undefined, feedback: feedback, onBack: onBack }));
|
|
33
33
|
}
|
|
34
34
|
function getFeedbackComments(commentsByFeedbackId, feedback) {
|
|
35
35
|
if (!feedback)
|
|
36
36
|
return [];
|
|
37
37
|
return commentsByFeedbackId[feedback.id] ?? [];
|
|
38
38
|
}
|
|
39
|
+
function getFeedbackStatusEvents(statusEventsByFeedbackId, feedback) {
|
|
40
|
+
if (!feedback)
|
|
41
|
+
return [];
|
|
42
|
+
return statusEventsByFeedbackId[feedback.id] ?? [];
|
|
43
|
+
}
|
|
39
44
|
export { FeedbackCenterSheetHeader, FeedbackCenterTabs, };
|
|
@@ -3,8 +3,9 @@ import type { CommentItemData } from "./comment-types.js";
|
|
|
3
3
|
import type { FeedbackCommentSubmitHandler } from "./feedback-center-comments.js";
|
|
4
4
|
import { type FeedbackCenterTab } from "./feedback-center-sheet-parts.js";
|
|
5
5
|
import type { FeedbackComposerProps } from "./feedback-composer.js";
|
|
6
|
-
import type { UserFeedbackItem } from "./feedback-tracking-types.js";
|
|
6
|
+
import type { FeedbackStatusEvent, 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;
|
|
@@ -20,6 +21,7 @@ type FeedbackCenterSheetProps = Omit<FeedbackComposerProps, "actions" | "formId"
|
|
|
20
21
|
onOpenChange: (open: boolean) => void;
|
|
21
22
|
open: boolean;
|
|
22
23
|
renderEvidence?: (item: UserFeedbackItem) => React.ReactNode;
|
|
24
|
+
statusEventsByFeedbackId?: Record<string, FeedbackStatusEvent[]>;
|
|
23
25
|
};
|
|
24
26
|
declare function FeedbackCenterSheet(props: FeedbackCenterSheetProps): React.JSX.Element;
|
|
25
|
-
export { FeedbackCenterSheet, type FeedbackCenterSheetProps };
|
|
27
|
+
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, statusEventsByFeedbackId: props.statusEventsByFeedbackId ?? {}, 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";
|
|
@@ -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";
|
|
@@ -42,6 +42,11 @@ type UserFeedbackItem = {
|
|
|
42
42
|
updatedAt?: string;
|
|
43
43
|
urgency?: FeedbackUrgency;
|
|
44
44
|
};
|
|
45
|
+
type FeedbackStatusEvent = {
|
|
46
|
+
createdAt: string;
|
|
47
|
+
id: string;
|
|
48
|
+
status: FeedbackStatus;
|
|
49
|
+
};
|
|
45
50
|
type FeedbackMetrics = {
|
|
46
51
|
completed: number;
|
|
47
52
|
discussionRequired: number;
|
|
@@ -75,6 +80,9 @@ declare const feedbackStatusOptions: readonly [{
|
|
|
75
80
|
}, {
|
|
76
81
|
readonly label: string;
|
|
77
82
|
readonly value: "no_action";
|
|
83
|
+
}, {
|
|
84
|
+
readonly label: string;
|
|
85
|
+
readonly value: "withdrawn";
|
|
78
86
|
}];
|
|
79
87
|
declare const feedbackTypeOptions: readonly [{
|
|
80
88
|
readonly label: "All types";
|
|
@@ -90,4 +98,4 @@ declare const feedbackTypeOptions: readonly [{
|
|
|
90
98
|
readonly value: "general";
|
|
91
99
|
}];
|
|
92
100
|
declare function getFeedbackInitials(name: string, email: string | undefined): string;
|
|
93
|
-
export { feedbackStatusLabels, feedbackStatusOptions, feedbackStatusValues, feedbackTypeLabels, feedbackTypeOptions, getFeedbackInitials, type FeedbackAffectedScope, type FeedbackAttachment, type FeedbackMetrics, type FeedbackStatus, type FeedbackSubmittedBy, type FeedbackTimelineEvent, type FeedbackUrgency, type StakeholderFeedbackItem, type StakeholderFeedbackType, type UserFeedbackItem, };
|
|
101
|
+
export { feedbackStatusLabels, feedbackStatusOptions, feedbackStatusValues, feedbackTypeLabels, feedbackTypeOptions, getFeedbackInitials, type FeedbackAffectedScope, type FeedbackAttachment, type FeedbackMetrics, type FeedbackStatus, type FeedbackStatusEvent, type FeedbackSubmittedBy, type FeedbackTimelineEvent, type FeedbackUrgency, type StakeholderFeedbackItem, type StakeholderFeedbackType, type UserFeedbackItem, };
|
|
@@ -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: "Canceled",
|
|
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,11 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { AlertTriangleIcon, ArrowLeftIcon,
|
|
3
|
-
import { formatFeedbackTimestampFull, getFeedbackTimestampDateTime } from "./feedback-date-format.js";
|
|
2
|
+
import { AlertTriangleIcon, ArrowLeftIcon, MessageSquareTextIcon, PaperclipIcon, } from "lucide-react";
|
|
3
|
+
import { formatFeedbackTimestamp, formatFeedbackTimestampFull, getFeedbackTimestampDateTime, } from "./feedback-date-format.js";
|
|
4
4
|
import { FeedbackStatusBadge } from "./feedback-status-badge.js";
|
|
5
5
|
import { FeedbackTypeBadge } from "./feedback-type-badge.js";
|
|
6
6
|
import { Badge } from "../ui/badge.js";
|
|
7
7
|
import { Button } from "../ui/button.js";
|
|
8
|
-
import { Card } from "../ui/card.js";
|
|
9
8
|
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty.js";
|
|
10
9
|
import { Item, ItemContent, ItemGroup, ItemGroupItem, ItemMedia, ItemTitle } from "../ui/item.js";
|
|
11
10
|
import { PropertyList, PropertyListItem, PropertyListLabel, PropertyListValue, } from "../ui/property-list.js";
|
|
@@ -24,16 +23,16 @@ function FeedbackUserDetailBackButton({ onBack }) {
|
|
|
24
23
|
return (_jsx(Button, { "aria-label": "Back to my feedback", className: "-ml-2 shrink-0", onClick: onBack, size: "icon", type: "button", variant: "ghost", children: _jsx(ArrowLeftIcon, {}) }));
|
|
25
24
|
}
|
|
26
25
|
function FeedbackUserDetailMeta({ feedback }) {
|
|
27
|
-
return (_jsxs(PropertyList, { children: [_jsx(FeedbackProperty, { label: "Submitted", children: _jsx(FeedbackUserDateValue, { value: feedback.submittedAt }) }),
|
|
26
|
+
return (_jsxs(PropertyList, { children: [_jsx(FeedbackProperty, { label: "Submitted", children: _jsx(FeedbackUserDateValue, { value: feedback.submittedAt }) }), feedback.urgency === "emergency" && feedback.affectedScope ? (_jsx(FeedbackProperty, { label: "Affected scope", children: _jsx("span", { className: "text-xs", children: affectedScopeLabels[feedback.affectedScope] }) })) : null] }));
|
|
28
27
|
}
|
|
29
28
|
function FeedbackUserDetailBody({ feedback }) {
|
|
30
|
-
return (_jsxs(
|
|
29
|
+
return (_jsxs("section", { className: "grid gap-2 border-t pt-4", children: [_jsx("div", { className: "text-meta text-muted-foreground", children: "Feedback" }), _jsx("p", { className: "text-sm whitespace-pre-wrap text-foreground", children: feedback.description })] }));
|
|
31
30
|
}
|
|
32
31
|
function FeedbackProperty({ children, label }) {
|
|
33
32
|
return (_jsxs(PropertyListItem, { children: [_jsx(PropertyListLabel, { children: label }), _jsx(PropertyListValue, { children: children })] }));
|
|
34
33
|
}
|
|
35
34
|
function FeedbackUserDateValue({ value }) {
|
|
36
|
-
return (_jsx("time", { className: "font-mono text-xs tabular-nums", dateTime: getFeedbackTimestampDateTime(value),
|
|
35
|
+
return (_jsx("time", { className: "font-mono text-xs tabular-nums", dateTime: getFeedbackTimestampDateTime(value), title: formatFeedbackTimestampFull(value), children: formatFeedbackTimestamp(value) }));
|
|
37
36
|
}
|
|
38
37
|
function FeedbackUserAttachments({ feedback }) {
|
|
39
38
|
const attachments = feedback.attachments ?? [];
|
|
@@ -50,7 +49,7 @@ function getAttachmentTitle(attachment) {
|
|
|
50
49
|
return (_jsx("a", { className: "underline-offset-4 hover:underline", href: attachment.url, children: attachment.name }));
|
|
51
50
|
}
|
|
52
51
|
function FeedbackUserActivity({ activityLabel, activitySlot, }) {
|
|
53
|
-
return (_jsxs(
|
|
52
|
+
return (_jsxs("section", { className: "grid gap-3 border-t pt-4", children: [_jsxs("div", { className: "grid gap-0.5", children: [_jsx("div", { className: "text-sm font-medium text-foreground", children: activityLabel }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Messages here are visible to you and the support team." })] }), activitySlot] }));
|
|
54
53
|
}
|
|
55
54
|
const affectedScopeLabels = {
|
|
56
55
|
everyone: "Everyone",
|
|
@@ -59,6 +58,6 @@ const affectedScopeLabels = {
|
|
|
59
58
|
unknown: "Not sure",
|
|
60
59
|
};
|
|
61
60
|
function FeedbackUserDetailEmpty() {
|
|
62
|
-
return (_jsx(Empty, { className: "min-h-72
|
|
61
|
+
return (_jsx(Empty, { className: "min-h-72", children: _jsxs(EmptyHeader, { children: [_jsx(EmptyMedia, { variant: "icon", children: _jsx(MessageSquareTextIcon, {}) }), _jsx(EmptyTitle, { children: "Select feedback" }), _jsx(EmptyDescription, { children: "Choose a submission to see its progress and details." })] }) }));
|
|
63
62
|
}
|
|
64
63
|
export { FeedbackUserDetail };
|
|
@@ -17,13 +17,13 @@ function FeedbackUserList({ error, isLoading = false, items, onRetry, onSelectFe
|
|
|
17
17
|
return _jsx(FeedbackUserListError, { error: error, onRetry: onRetry });
|
|
18
18
|
if (items.length === 0)
|
|
19
19
|
return _jsx(FeedbackUserListEmpty, {});
|
|
20
|
-
return (_jsx(ItemGroup, { className: "
|
|
20
|
+
return (_jsx(ItemGroup, { className: "divide-y divide-border", children: items.map((item) => (_jsx(FeedbackUserListItem, { item: item, onSelectFeedback: onSelectFeedback, selected: item.id === selectedId }, item.id))) }));
|
|
21
21
|
}
|
|
22
22
|
function FeedbackUserListError({ error, onRetry }) {
|
|
23
23
|
return (_jsxs(Alert, { variant: "destructive", children: [_jsx(AlertCircleIcon, {}), _jsx(AlertTitle, { children: "Feedback could not be loaded" }), _jsxs(AlertDescription, { className: "grid gap-3", children: [_jsx("span", { children: error.message }), onRetry ? (_jsx(Button, { className: "justify-self-start", onClick: onRetry, size: "sm", type: "button", variant: "outline", children: "Try again" })) : null] })] }));
|
|
24
24
|
}
|
|
25
25
|
function FeedbackUserListItem({ item, onSelectFeedback, selected, }) {
|
|
26
|
-
return (_jsx(ItemGroupItem, { children: _jsx(Item, { "aria-current": selected ? "true" : undefined, asChild: true, className: cn("w-full cursor-pointer text-left", selected && "
|
|
26
|
+
return (_jsx(ItemGroupItem, { children: _jsx(Item, { "aria-current": selected ? "true" : undefined, asChild: true, className: cn("w-full cursor-pointer rounded-none px-0 text-left", selected && "bg-muted/50"), size: "sm", variant: "default", children: _jsx("button", { onClick: () => onSelectFeedback?.(item), type: "button", children: _jsx(FeedbackUserListItemContent, { item: item }) }) }) }));
|
|
27
27
|
}
|
|
28
28
|
function FeedbackUserListItemContent({ item }) {
|
|
29
29
|
return (_jsxs(_Fragment, { children: [_jsxs(ItemContent, { className: "min-w-0", children: [_jsx(ItemTitle, { className: "max-w-full truncate", children: item.subject }), _jsx(ItemDescription, { className: "line-clamp-1", children: item.description })] }), _jsxs(ItemActions, { className: "shrink-0", children: [item.hasUnreadReply ? _jsx(Badge, { variant: "secondary", children: "New activity" }) : null, item.urgency === "emergency" ? (_jsxs(Badge, { variant: "destructive", children: [_jsx(AlertTriangleIcon, {}), "Emergency"] })) : null, _jsx(FeedbackTypeBadge, { type: item.type }), _jsx(FeedbackStatusBadge, { status: item.status })] }), _jsxs(ItemFooter, { children: [_jsx(FeedbackListDate, { label: "Submitted", value: item.submittedAt }), item.updatedAt ? _jsx(FeedbackListDate, { label: "Updated", value: item.updatedAt }) : null] })] }));
|
|
@@ -32,10 +32,10 @@ function FeedbackListDate({ label, value }) {
|
|
|
32
32
|
return (_jsxs("span", { className: "font-mono text-xs text-muted-foreground tabular-nums", children: [label, " ", formatFeedbackTimestamp(value)] }));
|
|
33
33
|
}
|
|
34
34
|
function FeedbackUserListLoading() {
|
|
35
|
-
return (_jsx("div", { "aria-label": "Loading feedback", className: "grid gap-2", role: "status", children: loadingItems.map((item) => (_jsxs("div", { className: "
|
|
35
|
+
return (_jsx("div", { "aria-label": "Loading feedback", className: "grid gap-2", role: "status", children: loadingItems.map((item) => (_jsxs("div", { className: "py-(--panel-padding)", children: [_jsx(Skeleton, { className: "mb-2 h-4 w-2/3" }), _jsx(Skeleton, { className: "mb-4 h-3 w-full" }), _jsx(Skeleton, { className: "h-3 w-1/2" })] }, item))) }));
|
|
36
36
|
}
|
|
37
37
|
function FeedbackUserListEmpty() {
|
|
38
|
-
return (_jsx(Empty, { className: "min-h-72
|
|
38
|
+
return (_jsx(Empty, { className: "min-h-72", children: _jsxs(EmptyHeader, { children: [_jsx(EmptyMedia, { variant: "icon", children: _jsx(InboxIcon, {}) }), _jsx(EmptyTitle, { children: "No feedback yet" }), _jsx(EmptyDescription, { children: "Feedback you submit will appear here with its current progress." })] }) }));
|
|
39
39
|
}
|
|
40
40
|
const loadingItems = ["first", "second", "third"];
|
|
41
41
|
export { FeedbackUserList };
|
|
@@ -1,26 +1,15 @@
|
|
|
1
|
+
import { format, formatDistanceToNowStrict, isValid, parseISO } from "date-fns";
|
|
1
2
|
function formatTimestamp(value, options = {}) {
|
|
2
3
|
const date = parseTimestamp(value);
|
|
3
4
|
if (!date)
|
|
4
5
|
return getTimestampFallback(value, options.fallback);
|
|
5
|
-
|
|
6
|
-
const dayDifference = getLocalCalendarDayDifference(date, new Date());
|
|
7
|
-
if (dayDifference === 0)
|
|
8
|
-
return `Today, ${time}`;
|
|
9
|
-
if (dayDifference === -1)
|
|
10
|
-
return `Yesterday, ${time}`;
|
|
11
|
-
if (dayDifference < 0 && dayDifference > -7) {
|
|
12
|
-
return `${weekdayFormatter.format(date)}, ${time}`;
|
|
13
|
-
}
|
|
14
|
-
if (date.getFullYear() === new Date().getFullYear()) {
|
|
15
|
-
return `${monthDayFormatter.format(date)}, ${time}`;
|
|
16
|
-
}
|
|
17
|
-
return dateTimeFormatter.format(date);
|
|
6
|
+
return formatDistanceToNowStrict(date, { addSuffix: true });
|
|
18
7
|
}
|
|
19
8
|
function formatTimestampFull(value, options = {}) {
|
|
20
9
|
const date = parseTimestamp(value);
|
|
21
10
|
if (!date)
|
|
22
11
|
return getTimestampFallback(value, options.fallback);
|
|
23
|
-
return
|
|
12
|
+
return format(date, "PPp");
|
|
24
13
|
}
|
|
25
14
|
function getTimestampDateTime(value) {
|
|
26
15
|
const date = parseTimestamp(value);
|
|
@@ -39,36 +28,10 @@ function toUtcTimestamp(value) {
|
|
|
39
28
|
function parseTimestamp(value) {
|
|
40
29
|
if (!value)
|
|
41
30
|
return undefined;
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
return undefined;
|
|
45
|
-
return new Date(timestamp);
|
|
31
|
+
const date = parseISO(value);
|
|
32
|
+
return isValid(date) ? date : undefined;
|
|
46
33
|
}
|
|
47
34
|
function getTimestampFallback(value, fallback = "No update") {
|
|
48
35
|
return value?.trim() || fallback;
|
|
49
36
|
}
|
|
50
|
-
function getLocalCalendarDayDifference(date, comparisonDate) {
|
|
51
|
-
const dateStart = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
52
|
-
const comparisonStart = new Date(comparisonDate.getFullYear(), comparisonDate.getMonth(), comparisonDate.getDate()).getTime();
|
|
53
|
-
return Math.round((dateStart - comparisonStart) / millisecondsPerDay);
|
|
54
|
-
}
|
|
55
|
-
const millisecondsPerDay = 24 * 60 * 60 * 1000;
|
|
56
|
-
const timeFormatter = new Intl.DateTimeFormat(undefined, {
|
|
57
|
-
hour: "numeric",
|
|
58
|
-
minute: "2-digit",
|
|
59
|
-
});
|
|
60
|
-
const weekdayFormatter = new Intl.DateTimeFormat(undefined, {
|
|
61
|
-
weekday: "short",
|
|
62
|
-
});
|
|
63
|
-
const monthDayFormatter = new Intl.DateTimeFormat(undefined, {
|
|
64
|
-
day: "numeric",
|
|
65
|
-
month: "short",
|
|
66
|
-
});
|
|
67
|
-
const dateTimeFormatter = new Intl.DateTimeFormat(undefined, {
|
|
68
|
-
day: "numeric",
|
|
69
|
-
hour: "numeric",
|
|
70
|
-
minute: "2-digit",
|
|
71
|
-
month: "short",
|
|
72
|
-
year: "numeric",
|
|
73
|
-
});
|
|
74
37
|
export { formatTimestamp, formatTimestampFull, getTimestampDateTime, toUtcTimestamp };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ciridae/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"engines": {
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"class-variance-authority": "^0.7.1",
|
|
88
88
|
"clsx": "^2.1.1",
|
|
89
89
|
"cmdk": "^1.1.1",
|
|
90
|
+
"date-fns": "^4.4.0",
|
|
90
91
|
"embla-carousel-react": "^8.6.0",
|
|
91
92
|
"input-otp": "^1.4.2",
|
|
92
93
|
"lucide-react": "^1.23.0",
|