@ciridae/ui 0.7.1 → 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.
Files changed (52) hide show
  1. package/dist/components/blocks/comment-composer.d.ts +3 -1
  2. package/dist/components/blocks/comment-composer.js +13 -7
  3. package/dist/components/blocks/comment-thread.d.ts +2 -1
  4. package/dist/components/blocks/comment-thread.js +2 -2
  5. package/dist/components/blocks/feedback-center-comments.d.ts +3 -1
  6. package/dist/components/blocks/feedback-center-comments.js +9 -3
  7. package/dist/components/blocks/feedback-center-footer.d.ts +12 -0
  8. package/dist/components/blocks/feedback-center-footer.js +10 -0
  9. package/dist/components/blocks/feedback-center-sheet-parts.d.ts +14 -11
  10. package/dist/components/blocks/feedback-center-sheet-parts.js +17 -22
  11. package/dist/components/blocks/feedback-center-sheet.d.ts +8 -2
  12. package/dist/components/blocks/feedback-center-sheet.js +72 -15
  13. package/dist/components/blocks/feedback-composer-parts.d.ts +4 -4
  14. package/dist/components/blocks/feedback-composer-parts.js +4 -4
  15. package/dist/components/blocks/feedback-composer-types.d.ts +5 -4
  16. package/dist/components/blocks/feedback-composer.d.ts +7 -3
  17. package/dist/components/blocks/feedback-composer.js +49 -79
  18. package/dist/components/blocks/feedback-detail-sheet.d.ts +9 -3
  19. package/dist/components/blocks/feedback-detail-sheet.js +27 -9
  20. package/dist/components/blocks/feedback-emergency-fields.d.ts +17 -0
  21. package/dist/components/blocks/feedback-emergency-fields.js +32 -0
  22. package/dist/components/blocks/feedback-metrics.js +1 -1
  23. package/dist/components/blocks/feedback-sheet.d.ts +1 -1
  24. package/dist/components/blocks/feedback-sheet.js +17 -4
  25. package/dist/components/blocks/feedback-status-badge.js +3 -3
  26. package/dist/components/blocks/feedback-table-row.js +8 -18
  27. package/dist/components/blocks/feedback-table-sort.js +4 -4
  28. package/dist/components/blocks/feedback-table.js +2 -2
  29. package/dist/components/blocks/feedback-tracking-types.d.ts +20 -11
  30. package/dist/components/blocks/feedback-tracking-types.js +15 -7
  31. package/dist/components/blocks/feedback-type-badge.js +1 -1
  32. package/dist/components/blocks/feedback-user-detail.d.ts +2 -1
  33. package/dist/components/blocks/feedback-user-detail.js +13 -6
  34. package/dist/components/blocks/feedback-user-list.d.ts +3 -1
  35. package/dist/components/blocks/feedback-user-list.js +11 -3
  36. package/dist/components/blocks/submit-feedback-composer.d.ts +5 -0
  37. package/dist/components/blocks/submit-feedback-composer.js +40 -0
  38. package/dist/components/blocks/use-feedback-composer-state.d.ts +27 -0
  39. package/dist/components/blocks/use-feedback-composer-state.js +60 -0
  40. package/dist/components/ui/form-sheet.d.ts +15 -0
  41. package/dist/components/ui/form-sheet.js +106 -0
  42. package/dist/entries/feedback-center-footer.d.ts +1 -0
  43. package/dist/entries/feedback-center-footer.js +1 -0
  44. package/dist/entries/feedback-emergency-fields.d.ts +1 -0
  45. package/dist/entries/feedback-emergency-fields.js +1 -0
  46. package/dist/entries/form-sheet.d.ts +1 -0
  47. package/dist/entries/form-sheet.js +1 -0
  48. package/dist/entries/submit-feedback-composer.d.ts +1 -0
  49. package/dist/entries/submit-feedback-composer.js +1 -0
  50. package/dist/entries/use-feedback-composer-state.d.ts +1 -0
  51. package/dist/entries/use-feedback-composer-state.js +1 -0
  52. 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: "Reference", children: _jsx("span", { className: "font-mono text-xs tabular-nums", children: feedback.referenceId ?? feedback.id }) })] }));
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-2 p-(--panel-padding)", children: [_jsx("div", { className: "text-meta text-muted-foreground", children: activityLabel }), activitySlot] }));
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 };
@@ -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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciridae/ui",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "engines": {