@ciridae/ui 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/blocks/comment-composer.d.ts +3 -1
- package/dist/components/blocks/comment-composer.js +13 -7
- package/dist/components/blocks/comment-thread.d.ts +2 -1
- package/dist/components/blocks/comment-thread.js +2 -2
- package/dist/components/blocks/feedback-center-comments.d.ts +3 -1
- package/dist/components/blocks/feedback-center-comments.js +9 -3
- package/dist/components/blocks/feedback-center-footer.d.ts +12 -0
- package/dist/components/blocks/feedback-center-footer.js +10 -0
- package/dist/components/blocks/feedback-center-sheet-parts.d.ts +14 -11
- package/dist/components/blocks/feedback-center-sheet-parts.js +17 -22
- package/dist/components/blocks/feedback-center-sheet.d.ts +8 -2
- package/dist/components/blocks/feedback-center-sheet.js +72 -15
- package/dist/components/blocks/feedback-composer-parts.d.ts +4 -4
- package/dist/components/blocks/feedback-composer-parts.js +4 -4
- package/dist/components/blocks/feedback-composer-types.d.ts +5 -4
- package/dist/components/blocks/feedback-composer.d.ts +7 -3
- package/dist/components/blocks/feedback-composer.js +49 -79
- package/dist/components/blocks/feedback-detail-sheet.d.ts +9 -3
- package/dist/components/blocks/feedback-detail-sheet.js +27 -9
- package/dist/components/blocks/feedback-emergency-fields.d.ts +17 -0
- package/dist/components/blocks/feedback-emergency-fields.js +32 -0
- package/dist/components/blocks/feedback-metrics.js +1 -1
- package/dist/components/blocks/feedback-sheet.d.ts +1 -1
- package/dist/components/blocks/feedback-sheet.js +17 -4
- package/dist/components/blocks/feedback-status-badge.js +3 -3
- package/dist/components/blocks/feedback-table-row.js +8 -18
- package/dist/components/blocks/feedback-table-sort.js +4 -4
- package/dist/components/blocks/feedback-table.js +2 -2
- package/dist/components/blocks/feedback-tracking-types.d.ts +20 -11
- package/dist/components/blocks/feedback-tracking-types.js +15 -7
- package/dist/components/blocks/feedback-type-badge.js +1 -1
- package/dist/components/blocks/feedback-user-detail.d.ts +2 -1
- package/dist/components/blocks/feedback-user-detail.js +13 -6
- package/dist/components/blocks/feedback-user-list.d.ts +3 -1
- package/dist/components/blocks/feedback-user-list.js +11 -3
- package/dist/components/blocks/submit-feedback-composer.d.ts +5 -0
- package/dist/components/blocks/submit-feedback-composer.js +40 -0
- package/dist/components/blocks/use-feedback-composer-state.d.ts +27 -0
- package/dist/components/blocks/use-feedback-composer-state.js +60 -0
- package/dist/components/ui/color-theme.js +6 -9
- package/dist/components/ui/form-sheet.d.ts +15 -0
- package/dist/components/ui/form-sheet.js +106 -0
- package/dist/entries/feedback-center-footer.d.ts +1 -0
- package/dist/entries/feedback-center-footer.js +1 -0
- package/dist/entries/feedback-emergency-fields.d.ts +1 -0
- package/dist/entries/feedback-emergency-fields.js +1 -0
- package/dist/entries/form-sheet.d.ts +1 -0
- package/dist/entries/form-sheet.js +1 -0
- package/dist/entries/submit-feedback-composer.d.ts +1 -0
- package/dist/entries/submit-feedback-composer.js +1 -0
- package/dist/entries/use-feedback-composer-state.d.ts +1 -0
- package/dist/entries/use-feedback-composer-state.js +1 -0
- package/dist/lib/browser-storage.d.ts +13 -0
- package/dist/lib/browser-storage.js +43 -0
- package/package.json +1 -1
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ArrowLeftIcon, MessageSquareTextIcon, PaperclipIcon } from "lucide-react";
|
|
2
|
+
import { AlertTriangleIcon, ArrowLeftIcon, EyeIcon, MessageSquareTextIcon, PaperclipIcon, } from "lucide-react";
|
|
3
3
|
import { formatFeedbackTimestampFull, getFeedbackTimestampDateTime } from "./feedback-date-format";
|
|
4
4
|
import { FeedbackStatusBadge } from "./feedback-status-badge";
|
|
5
5
|
import { FeedbackTypeBadge } from "./feedback-type-badge";
|
|
6
|
+
import { Badge } from "../ui/badge";
|
|
6
7
|
import { Button } from "../ui/button";
|
|
7
8
|
import { Card } from "../ui/card";
|
|
8
9
|
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";
|
|
9
10
|
import { Item, ItemContent, ItemGroup, ItemGroupItem, ItemMedia, ItemTitle } from "../ui/item";
|
|
10
11
|
import { PropertyList, PropertyListItem, PropertyListLabel, PropertyListValue, } from "../ui/property-list";
|
|
11
|
-
function FeedbackUserDetail({ activityLabel = "Activity", activitySlot, feedback, onBack, }) {
|
|
12
|
+
function FeedbackUserDetail({ activityLabel = "Activity", activitySlot, evidenceSlot, feedback, onBack, }) {
|
|
12
13
|
if (!feedback)
|
|
13
14
|
return _jsx(FeedbackUserDetailEmpty, {});
|
|
14
|
-
return (_jsxs("div", { className: "grid content-start gap-(--panel-gap)", children: [_jsx(FeedbackUserDetailHeader, { feedback: feedback, onBack: onBack }), _jsx(FeedbackUserDetailMeta, { feedback: feedback }), _jsx(FeedbackUserDetailBody, { feedback: feedback }), _jsx(FeedbackUserAttachments, { feedback: feedback }), activitySlot ? (_jsx(FeedbackUserActivity, { activityLabel: activityLabel, activitySlot: activitySlot })) : null] }));
|
|
15
|
+
return (_jsxs("div", { className: "grid content-start gap-(--panel-gap)", children: [_jsx(FeedbackUserDetailHeader, { feedback: feedback, onBack: onBack }), _jsx(FeedbackUserDetailMeta, { feedback: feedback }), _jsx(FeedbackUserDetailBody, { feedback: feedback }), evidenceSlot ?? _jsx(FeedbackUserAttachments, { feedback: feedback }), activitySlot ? (_jsx(FeedbackUserActivity, { activityLabel: activityLabel, activitySlot: activitySlot })) : null] }));
|
|
15
16
|
}
|
|
16
17
|
function FeedbackUserDetailHeader({ feedback, onBack, }) {
|
|
17
18
|
return (_jsx("header", { className: "grid gap-3", children: _jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [onBack ? _jsx(FeedbackUserDetailBackButton, { onBack: onBack }) : null, _jsx("div", { className: "min-w-0 flex-1", children: _jsxs("div", { className: "flex min-w-0 items-center justify-between gap-3", children: [_jsx("h3", { className: "truncate text-sm font-medium text-foreground", children: feedback.subject }), _jsx(FeedbackUserDetailBadges, { feedback: feedback })] }) })] }) }));
|
|
18
19
|
}
|
|
19
20
|
function FeedbackUserDetailBadges({ feedback }) {
|
|
20
|
-
return (_jsxs("div", { className: "flex shrink-0 flex-wrap justify-end gap-2", children: [_jsx(FeedbackTypeBadge, { type: feedback.type }), _jsx(FeedbackStatusBadge, { status: feedback.status })] }));
|
|
21
|
+
return (_jsxs("div", { className: "flex shrink-0 flex-wrap justify-end gap-2", children: [feedback.hasUnreadReply ? _jsx(Badge, { variant: "secondary", children: "New reply" }) : null, feedback.urgency === "emergency" ? (_jsxs(Badge, { variant: "destructive", children: [_jsx(AlertTriangleIcon, {}), "Emergency"] })) : null, _jsx(FeedbackTypeBadge, { type: feedback.type }), _jsx(FeedbackStatusBadge, { status: feedback.status })] }));
|
|
21
22
|
}
|
|
22
23
|
function FeedbackUserDetailBackButton({ onBack }) {
|
|
23
24
|
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, {}) }));
|
|
24
25
|
}
|
|
25
26
|
function FeedbackUserDetailMeta({ feedback }) {
|
|
26
|
-
return (_jsxs(PropertyList, { children: [_jsx(FeedbackProperty, { label: "Submitted", children: _jsx(FeedbackUserDateValue, { value: feedback.submittedAt }) }), _jsx(FeedbackProperty, { label: "Updated", children: _jsx(FeedbackUserDateValue, { value: feedback.updatedAt }) }), _jsx(FeedbackProperty, { label: "
|
|
27
|
+
return (_jsxs(PropertyList, { children: [_jsx(FeedbackProperty, { label: "Submitted", children: _jsx(FeedbackUserDateValue, { value: feedback.submittedAt }) }), _jsx(FeedbackProperty, { label: "Updated", children: _jsx(FeedbackUserDateValue, { value: feedback.updatedAt }) }), feedback.urgency === "emergency" && feedback.affectedScope ? (_jsx(FeedbackProperty, { label: "Affected scope", children: _jsx("span", { className: "text-xs", children: affectedScopeLabels[feedback.affectedScope] }) })) : null] }));
|
|
27
28
|
}
|
|
28
29
|
function FeedbackUserDetailBody({ feedback }) {
|
|
29
30
|
return (_jsxs(Card, { className: "grid gap-2 p-(--panel-padding)", children: [_jsx("div", { className: "text-meta text-muted-foreground", children: "Feedback" }), _jsx("p", { className: "text-sm whitespace-pre-wrap text-foreground", children: feedback.description })] }));
|
|
@@ -49,8 +50,14 @@ function getAttachmentTitle(attachment) {
|
|
|
49
50
|
return (_jsx("a", { className: "underline-offset-4 hover:underline", href: attachment.url, children: attachment.name }));
|
|
50
51
|
}
|
|
51
52
|
function FeedbackUserActivity({ activityLabel, activitySlot, }) {
|
|
52
|
-
return (_jsxs(Card, { className: "grid gap-
|
|
53
|
+
return (_jsxs(Card, { className: "grid gap-3 p-(--panel-padding)", children: [_jsxs("div", { className: "flex items-start gap-2 border-b pb-3", children: [_jsx(EyeIcon, { className: "mt-0.5 size-4 shrink-0 text-muted-foreground" }), _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] }));
|
|
53
54
|
}
|
|
55
|
+
const affectedScopeLabels = {
|
|
56
|
+
everyone: "Everyone",
|
|
57
|
+
just_me: "Just me",
|
|
58
|
+
my_team: "My team",
|
|
59
|
+
unknown: "Not sure",
|
|
60
|
+
};
|
|
54
61
|
function FeedbackUserDetailEmpty() {
|
|
55
62
|
return (_jsx(Empty, { className: "min-h-72 border border-dashed", 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." })] }) }));
|
|
56
63
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { UserFeedbackItem } from "./feedback-tracking-types";
|
|
2
2
|
type FeedbackUserListProps = {
|
|
3
|
+
error?: Error | null;
|
|
3
4
|
isLoading?: boolean;
|
|
4
5
|
items: UserFeedbackItem[];
|
|
6
|
+
onRetry?: () => void;
|
|
5
7
|
onSelectFeedback?: (item: UserFeedbackItem) => void;
|
|
6
8
|
selectedId?: string;
|
|
7
9
|
};
|
|
8
|
-
declare function FeedbackUserList({ isLoading, items, onSelectFeedback, selectedId, }: FeedbackUserListProps): import("@types/react").JSX.Element;
|
|
10
|
+
declare function FeedbackUserList({ error, isLoading, items, onRetry, onSelectFeedback, selectedId, }: FeedbackUserListProps): import("@types/react").JSX.Element;
|
|
9
11
|
export { FeedbackUserList, type FeedbackUserListProps };
|
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { InboxIcon } from "lucide-react";
|
|
2
|
+
import { AlertCircleIcon, AlertTriangleIcon, InboxIcon } from "lucide-react";
|
|
3
3
|
import { formatFeedbackTimestamp } from "./feedback-date-format";
|
|
4
4
|
import { FeedbackStatusBadge } from "./feedback-status-badge";
|
|
5
5
|
import { FeedbackTypeBadge } from "./feedback-type-badge";
|
|
6
|
+
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
|
7
|
+
import { Badge } from "../ui/badge";
|
|
8
|
+
import { Button } from "../ui/button";
|
|
6
9
|
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";
|
|
7
10
|
import { Item, ItemActions, ItemContent, ItemDescription, ItemFooter, ItemGroup, ItemGroupItem, ItemTitle, } from "../ui/item";
|
|
8
11
|
import { Skeleton } from "../ui/skeleton";
|
|
9
12
|
import { cn } from "../../lib/utils";
|
|
10
|
-
function FeedbackUserList({ isLoading = false, items, onSelectFeedback, selectedId, }) {
|
|
13
|
+
function FeedbackUserList({ error, isLoading = false, items, onRetry, onSelectFeedback, selectedId, }) {
|
|
11
14
|
if (isLoading)
|
|
12
15
|
return _jsx(FeedbackUserListLoading, {});
|
|
16
|
+
if (error)
|
|
17
|
+
return _jsx(FeedbackUserListError, { error: error, onRetry: onRetry });
|
|
13
18
|
if (items.length === 0)
|
|
14
19
|
return _jsx(FeedbackUserListEmpty, {});
|
|
15
20
|
return (_jsx(ItemGroup, { className: "gap-2", children: items.map((item) => (_jsx(FeedbackUserListItem, { item: item, onSelectFeedback: onSelectFeedback, selected: item.id === selectedId }, item.id))) }));
|
|
16
21
|
}
|
|
22
|
+
function FeedbackUserListError({ error, onRetry }) {
|
|
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
|
+
}
|
|
17
25
|
function FeedbackUserListItem({ item, onSelectFeedback, selected, }) {
|
|
18
26
|
return (_jsx(ItemGroupItem, { children: _jsx(Item, { "aria-current": selected ? "true" : undefined, asChild: true, className: cn("w-full cursor-pointer text-left", selected && "border-foreground/30 bg-muted/50"), size: "sm", variant: "outline", children: _jsx("button", { onClick: () => onSelectFeedback?.(item), type: "button", children: _jsx(FeedbackUserListItemContent, { item: item }) }) }) }));
|
|
19
27
|
}
|
|
20
28
|
function FeedbackUserListItemContent({ item }) {
|
|
21
|
-
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: [_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] })] }));
|
|
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 reply" }) : 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] })] }));
|
|
22
30
|
}
|
|
23
31
|
function FeedbackListDate({ label, value }) {
|
|
24
32
|
return (_jsxs("span", { className: "font-mono text-xs text-muted-foreground tabular-nums", children: [label, " ", formatFeedbackTimestamp(value)] }));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import type { FeedbackComposerProps } from "./feedback-composer";
|
|
3
|
+
import type { useFeedbackComposerState } from "./use-feedback-composer-state";
|
|
4
|
+
declare function submitFeedbackComposer(event: React.FormEvent<HTMLFormElement>, props: FeedbackComposerProps, state: ReturnType<typeof useFeedbackComposerState>): Promise<void>;
|
|
5
|
+
export { submitFeedbackComposer };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
async function submitFeedbackComposer(event, props, state) {
|
|
2
|
+
event.preventDefault();
|
|
3
|
+
const trimmedDescription = state.description.trim();
|
|
4
|
+
const trimmedSubject = state.subject.trim();
|
|
5
|
+
const validationError = getValidationError({
|
|
6
|
+
description: trimmedDescription,
|
|
7
|
+
emergencyConfirmed: state.emergencyConfirmed,
|
|
8
|
+
subject: trimmedSubject,
|
|
9
|
+
urgency: state.urgency,
|
|
10
|
+
});
|
|
11
|
+
state.setError(validationError);
|
|
12
|
+
state.setSubmitError(undefined);
|
|
13
|
+
if (validationError)
|
|
14
|
+
return;
|
|
15
|
+
try {
|
|
16
|
+
await props.onSubmit({
|
|
17
|
+
affectedScope: state.affectedScope,
|
|
18
|
+
attachments: state.attachments,
|
|
19
|
+
description: trimmedDescription,
|
|
20
|
+
subject: trimmedSubject,
|
|
21
|
+
type: state.type,
|
|
22
|
+
urgency: state.urgency,
|
|
23
|
+
});
|
|
24
|
+
state.setIsSubmitted(true);
|
|
25
|
+
props.onSubmittedChange?.(true);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
state.setSubmitError("Unable to submit feedback. Please try again.");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function getValidationError({ description, emergencyConfirmed, subject, urgency, }) {
|
|
32
|
+
if (!subject)
|
|
33
|
+
return "subject";
|
|
34
|
+
if (!description)
|
|
35
|
+
return "description";
|
|
36
|
+
if (urgency === "emergency" && !emergencyConfirmed)
|
|
37
|
+
return "emergency_confirmation";
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
export { submitFeedbackComposer };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { FeedbackAffectedScope, FeedbackType, FeedbackUrgency, FeedbackValidationError } from "./feedback-composer-types";
|
|
3
|
+
declare function useFeedbackComposerState(defaultType: FeedbackType, defaultUrgency: FeedbackUrgency): {
|
|
4
|
+
affectedScope: FeedbackAffectedScope;
|
|
5
|
+
attachments: File[];
|
|
6
|
+
description: string;
|
|
7
|
+
emergencyConfirmed: boolean;
|
|
8
|
+
error: FeedbackValidationError;
|
|
9
|
+
isDirty: boolean;
|
|
10
|
+
isSubmitted: boolean;
|
|
11
|
+
resetForm: () => void;
|
|
12
|
+
setAffectedScope: React.Dispatch<React.SetStateAction<FeedbackAffectedScope>>;
|
|
13
|
+
setAttachments: React.Dispatch<React.SetStateAction<File[]>>;
|
|
14
|
+
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
|
15
|
+
setEmergencyConfirmed: React.Dispatch<React.SetStateAction<boolean>>;
|
|
16
|
+
setError: React.Dispatch<React.SetStateAction<FeedbackValidationError>>;
|
|
17
|
+
setIsSubmitted: React.Dispatch<React.SetStateAction<boolean>>;
|
|
18
|
+
setSubmitError: React.Dispatch<React.SetStateAction<string | undefined>>;
|
|
19
|
+
setSubject: React.Dispatch<React.SetStateAction<string>>;
|
|
20
|
+
setType: React.Dispatch<React.SetStateAction<FeedbackType>>;
|
|
21
|
+
setUrgency: React.Dispatch<React.SetStateAction<FeedbackUrgency>>;
|
|
22
|
+
submitError: string | undefined;
|
|
23
|
+
subject: string;
|
|
24
|
+
type: FeedbackType;
|
|
25
|
+
urgency: FeedbackUrgency;
|
|
26
|
+
};
|
|
27
|
+
export { useFeedbackComposerState };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
function useFeedbackComposerState(defaultType, defaultUrgency) {
|
|
3
|
+
const [affectedScope, setAffectedScope] = React.useState("unknown");
|
|
4
|
+
const [attachments, setAttachments] = React.useState([]);
|
|
5
|
+
const [description, setDescription] = React.useState("");
|
|
6
|
+
const [emergencyConfirmed, setEmergencyConfirmed] = React.useState(false);
|
|
7
|
+
const [error, setError] = React.useState();
|
|
8
|
+
const [submitError, setSubmitError] = React.useState();
|
|
9
|
+
const [isSubmitted, setIsSubmitted] = React.useState(false);
|
|
10
|
+
const [subject, setSubject] = React.useState("");
|
|
11
|
+
const [type, setType] = React.useState(defaultType);
|
|
12
|
+
const [urgency, setUrgency] = React.useState(defaultUrgency);
|
|
13
|
+
React.useEffect(() => setType(defaultType), [defaultType]);
|
|
14
|
+
React.useEffect(() => setUrgency(defaultUrgency), [defaultUrgency]);
|
|
15
|
+
const isDirty = !isSubmitted &&
|
|
16
|
+
(attachments.length > 0 ||
|
|
17
|
+
description.length > 0 ||
|
|
18
|
+
subject.length > 0 ||
|
|
19
|
+
type !== defaultType ||
|
|
20
|
+
urgency !== defaultUrgency ||
|
|
21
|
+
affectedScope !== "unknown" ||
|
|
22
|
+
emergencyConfirmed);
|
|
23
|
+
const resetForm = () => {
|
|
24
|
+
setAffectedScope("unknown");
|
|
25
|
+
setAttachments([]);
|
|
26
|
+
setDescription("");
|
|
27
|
+
setEmergencyConfirmed(false);
|
|
28
|
+
setError(undefined);
|
|
29
|
+
setSubmitError(undefined);
|
|
30
|
+
setIsSubmitted(false);
|
|
31
|
+
setSubject("");
|
|
32
|
+
setType(defaultType);
|
|
33
|
+
setUrgency(defaultUrgency);
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
affectedScope,
|
|
37
|
+
attachments,
|
|
38
|
+
description,
|
|
39
|
+
emergencyConfirmed,
|
|
40
|
+
error,
|
|
41
|
+
isDirty,
|
|
42
|
+
isSubmitted,
|
|
43
|
+
resetForm,
|
|
44
|
+
setAffectedScope,
|
|
45
|
+
setAttachments,
|
|
46
|
+
setDescription,
|
|
47
|
+
setEmergencyConfirmed,
|
|
48
|
+
setError,
|
|
49
|
+
setIsSubmitted,
|
|
50
|
+
setSubmitError,
|
|
51
|
+
setSubject,
|
|
52
|
+
setType,
|
|
53
|
+
setUrgency,
|
|
54
|
+
submitError,
|
|
55
|
+
subject,
|
|
56
|
+
type,
|
|
57
|
+
urgency,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export { useFeedbackComposerState };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { PALETTE_TOKEN_NAMES, canonicalPaletteSelection, derivePaletteTokens, paletteSelectionKey, resolvePaletteSelection, } from "../../lib/palette";
|
|
4
|
+
import { readStoredValue, removeStoredValue, writeStoredValue } from "../../lib/browser-storage";
|
|
4
5
|
import { ThemeContext, } from "./color-theme-context";
|
|
5
6
|
const defaultStorageKey = "ciridae-ui-theme";
|
|
6
7
|
const defaultPaletteStorageKey = "ciridae-ui-palette";
|
|
@@ -41,18 +42,18 @@ function ThemeProvider({ children, defaultMode = "system", defaultPalette, stora
|
|
|
41
42
|
}, []);
|
|
42
43
|
const setMode = React.useCallback((nextMode) => {
|
|
43
44
|
setModeState(nextMode);
|
|
44
|
-
|
|
45
|
+
writeStoredValue(storageKey, nextMode);
|
|
45
46
|
}, [storageKey]);
|
|
46
47
|
const toggleTheme = React.useCallback(() => {
|
|
47
48
|
setMode(resolvedTheme === "dark" ? "light" : "dark");
|
|
48
49
|
}, [resolvedTheme, setMode]);
|
|
49
50
|
const setPalette = React.useCallback((nextPalette) => {
|
|
50
51
|
setStoredPalette(nextPalette);
|
|
51
|
-
|
|
52
|
+
writeStoredValue(paletteStorageKey, JSON.stringify(nextPalette));
|
|
52
53
|
}, [paletteStorageKey]);
|
|
53
54
|
const resetPalette = React.useCallback(() => {
|
|
54
55
|
setStoredPalette(null);
|
|
55
|
-
|
|
56
|
+
removeStoredValue(paletteStorageKey);
|
|
56
57
|
}, [paletteStorageKey]);
|
|
57
58
|
const contextValue = React.useMemo(() => ({
|
|
58
59
|
mode,
|
|
@@ -104,9 +105,7 @@ function usePaletteTokens(palette, mode) {
|
|
|
104
105
|
}, [palette, mode]);
|
|
105
106
|
}
|
|
106
107
|
function readStoredMode(storageKey, defaultMode) {
|
|
107
|
-
|
|
108
|
-
return defaultMode;
|
|
109
|
-
const storedMode = localStorage.getItem(storageKey);
|
|
108
|
+
const storedMode = readStoredValue(storageKey);
|
|
110
109
|
if (storedMode === "light" || storedMode === "dark" || storedMode === "system") {
|
|
111
110
|
return storedMode;
|
|
112
111
|
}
|
|
@@ -117,9 +116,7 @@ function readStoredMode(storageKey, defaultMode) {
|
|
|
117
116
|
* deploys, so it outlives renamed presets and older shapes of the type.
|
|
118
117
|
*/
|
|
119
118
|
function readStoredPalette(storageKey) {
|
|
120
|
-
|
|
121
|
-
return null;
|
|
122
|
-
const raw = localStorage.getItem(storageKey);
|
|
119
|
+
const raw = readStoredValue(storageKey);
|
|
123
120
|
if (!raw)
|
|
124
121
|
return null;
|
|
125
122
|
try {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
type FormSheetActions = Readonly<{
|
|
3
|
+
requestClose: () => void;
|
|
4
|
+
}>;
|
|
5
|
+
type FormSheetProps = {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
closeDisabled?: boolean;
|
|
8
|
+
isDirty: boolean;
|
|
9
|
+
onOpenChange: (open: boolean) => void;
|
|
10
|
+
open: boolean;
|
|
11
|
+
trigger?: React.ReactElement;
|
|
12
|
+
};
|
|
13
|
+
declare function useFormSheetActions(): FormSheetActions;
|
|
14
|
+
declare function FormSheet({ children, closeDisabled, isDirty, onOpenChange, open, trigger, }: FormSheetProps): React.JSX.Element;
|
|
15
|
+
export { FormSheet, useFormSheetActions, type FormSheetActions, type FormSheetProps };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "./alert-dialog";
|
|
4
|
+
import { Sheet, SheetTrigger } from "./sheet";
|
|
5
|
+
const FormSheetContext = React.createContext(null);
|
|
6
|
+
function useFormSheetActions() {
|
|
7
|
+
const actions = React.useContext(FormSheetContext);
|
|
8
|
+
if (!actions)
|
|
9
|
+
throw new Error("useFormSheetActions must be used within a FormSheet");
|
|
10
|
+
return actions;
|
|
11
|
+
}
|
|
12
|
+
function FormSheet({ children, closeDisabled = false, isDirty, onOpenChange, open, trigger, }) {
|
|
13
|
+
const [discardPromptOpen, setDiscardPromptOpen] = React.useState(false);
|
|
14
|
+
const discardReturnFocusRef = React.useRef(null);
|
|
15
|
+
const discardingRef = React.useRef(false);
|
|
16
|
+
const returnFocus = useFormSheetReturnFocus(open);
|
|
17
|
+
const closeSheet = React.useCallback(() => {
|
|
18
|
+
returnFocus.prepareForClose();
|
|
19
|
+
onOpenChange(false);
|
|
20
|
+
}, [onOpenChange, returnFocus]);
|
|
21
|
+
const requestClose = React.useCallback(() => {
|
|
22
|
+
if (closeDisabled)
|
|
23
|
+
return;
|
|
24
|
+
if (isDirty) {
|
|
25
|
+
discardReturnFocusRef.current = getActiveElement();
|
|
26
|
+
setDiscardPromptOpen(true);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
closeSheet();
|
|
30
|
+
}, [closeDisabled, closeSheet, isDirty]);
|
|
31
|
+
const actions = React.useMemo(() => ({ requestClose }), [requestClose]);
|
|
32
|
+
function handleOpenChange(nextOpen) {
|
|
33
|
+
if (nextOpen) {
|
|
34
|
+
returnFocus.capture();
|
|
35
|
+
onOpenChange(true);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
requestClose();
|
|
39
|
+
}
|
|
40
|
+
function discardChanges() {
|
|
41
|
+
discardingRef.current = true;
|
|
42
|
+
setDiscardPromptOpen(false);
|
|
43
|
+
closeSheet();
|
|
44
|
+
}
|
|
45
|
+
function handleDiscardPromptOpenChange(nextOpen) {
|
|
46
|
+
setDiscardPromptOpen(nextOpen);
|
|
47
|
+
if (!nextOpen && !discardingRef.current) {
|
|
48
|
+
requestAnimationFrame(() => discardReturnFocusRef.current?.focus());
|
|
49
|
+
}
|
|
50
|
+
discardingRef.current = false;
|
|
51
|
+
}
|
|
52
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Sheet, { onOpenChange: handleOpenChange, open: open, children: [trigger ? _jsx(SheetTrigger, { asChild: true, children: trigger }) : null, _jsx(FormSheetContext.Provider, { value: actions, children: children })] }), _jsx(DiscardChangesPrompt, { onDiscard: discardChanges, onOpenChange: handleDiscardPromptOpenChange, open: discardPromptOpen })] }));
|
|
53
|
+
}
|
|
54
|
+
function useFormSheetReturnFocus(open) {
|
|
55
|
+
const activeElementDuringRender = getActiveElement();
|
|
56
|
+
const focusFrameRef = React.useRef(null);
|
|
57
|
+
const returnFocusRef = React.useRef(activeElementDuringRender);
|
|
58
|
+
const wasOpenRef = React.useRef(open);
|
|
59
|
+
const schedule = React.useCallback(() => {
|
|
60
|
+
const returnFocus = returnFocusRef.current;
|
|
61
|
+
if (!returnFocus)
|
|
62
|
+
return;
|
|
63
|
+
if (focusFrameRef.current !== null)
|
|
64
|
+
cancelAnimationFrame(focusFrameRef.current);
|
|
65
|
+
focusFrameRef.current = requestAnimationFrame(() => {
|
|
66
|
+
focusFrameRef.current = null;
|
|
67
|
+
if (returnFocus.isConnected)
|
|
68
|
+
returnFocus.focus();
|
|
69
|
+
});
|
|
70
|
+
}, []);
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
if (open) {
|
|
73
|
+
if (!wasOpenRef.current)
|
|
74
|
+
returnFocusRef.current = activeElementDuringRender;
|
|
75
|
+
wasOpenRef.current = true;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!wasOpenRef.current)
|
|
79
|
+
return;
|
|
80
|
+
wasOpenRef.current = false;
|
|
81
|
+
schedule();
|
|
82
|
+
}, [activeElementDuringRender, open, schedule]);
|
|
83
|
+
React.useEffect(() => () => {
|
|
84
|
+
if (wasOpenRef.current)
|
|
85
|
+
schedule();
|
|
86
|
+
}, [schedule]);
|
|
87
|
+
const capture = React.useCallback(() => {
|
|
88
|
+
returnFocusRef.current = getActiveElement();
|
|
89
|
+
wasOpenRef.current = true;
|
|
90
|
+
}, []);
|
|
91
|
+
const prepareForClose = React.useCallback(() => {
|
|
92
|
+
schedule();
|
|
93
|
+
wasOpenRef.current = false;
|
|
94
|
+
}, [schedule]);
|
|
95
|
+
return React.useMemo(() => ({ capture, prepareForClose }), [capture, prepareForClose]);
|
|
96
|
+
}
|
|
97
|
+
function DiscardChangesPrompt({ onDiscard, onOpenChange, open, }) {
|
|
98
|
+
return (_jsx(AlertDialog, { onOpenChange: onOpenChange, open: open, children: _jsxs(AlertDialogContent, { size: "sm", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Discard unsaved changes?" }), _jsx(AlertDialogDescription, { children: "Your changes have not been saved. Keep editing or discard them and close this panel." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Keep editing" }), _jsx(AlertDialogAction, { onClick: onDiscard, variant: "destructive", children: "Discard changes" })] })] }) }));
|
|
99
|
+
}
|
|
100
|
+
function getActiveElement() {
|
|
101
|
+
// Called during render, so it must be safe on the server too.
|
|
102
|
+
if (typeof document === "undefined")
|
|
103
|
+
return null;
|
|
104
|
+
return document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
105
|
+
}
|
|
106
|
+
export { FormSheet, useFormSheetActions };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/feedback-center-footer";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/feedback-center-footer";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/feedback-emergency-fields";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/feedback-emergency-fields";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/ui/form-sheet";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/ui/form-sheet";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/submit-feedback-composer";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/submit-feedback-composer";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/use-feedback-composer-state";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/use-feedback-composer-state";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* localStorage that cannot throw at its callers.
|
|
3
|
+
*
|
|
4
|
+
* Every access can raise: the API is absent during SSR, `getItem` throws when
|
|
5
|
+
* storage is disabled by policy or by Safari's private mode, and `setItem`
|
|
6
|
+
* throws on quota. A preference is worth remembering and never worth failing
|
|
7
|
+
* over, so each call degrades to "no stored value" and the caller keeps its
|
|
8
|
+
* in-memory state.
|
|
9
|
+
*/
|
|
10
|
+
declare function readStoredValue(key: string): string | null;
|
|
11
|
+
declare function writeStoredValue(key: string, value: string): void;
|
|
12
|
+
declare function removeStoredValue(key: string): void;
|
|
13
|
+
export { readStoredValue, removeStoredValue, writeStoredValue };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* localStorage that cannot throw at its callers.
|
|
3
|
+
*
|
|
4
|
+
* Every access can raise: the API is absent during SSR, `getItem` throws when
|
|
5
|
+
* storage is disabled by policy or by Safari's private mode, and `setItem`
|
|
6
|
+
* throws on quota. A preference is worth remembering and never worth failing
|
|
7
|
+
* over, so each call degrades to "no stored value" and the caller keeps its
|
|
8
|
+
* in-memory state.
|
|
9
|
+
*/
|
|
10
|
+
function storage() {
|
|
11
|
+
try {
|
|
12
|
+
return typeof window === "undefined" ? null : window.localStorage;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// Accessing the property itself throws when storage is blocked by policy.
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function readStoredValue(key) {
|
|
20
|
+
try {
|
|
21
|
+
return storage()?.getItem(key) ?? null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function writeStoredValue(key, value) {
|
|
28
|
+
try {
|
|
29
|
+
storage()?.setItem(key, value);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Disabled or over quota: the preference lives for this session only.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function removeStoredValue(key) {
|
|
36
|
+
try {
|
|
37
|
+
storage()?.removeItem(key);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Nothing to recover from -- the value either was not written or stays.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export { readStoredValue, removeStoredValue, writeStoredValue };
|