@ciridae/ui 0.8.0 → 0.8.2

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.
@@ -4,7 +4,8 @@ import { CommentThread } from "./comment-thread";
4
4
  import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
5
5
  import { Button } from "../ui/button";
6
6
  function FeedbackCenterCommentThread({ comments, error, feedback, isCommentSubmitting, isCommentsLoading, onRetry, onSubmitComment, }) {
7
- if (error) {
7
+ // A retry in flight shows the thread's loading state, not the stale error.
8
+ if (error && !isCommentsLoading) {
8
9
  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] })] }));
9
10
  }
10
11
  return (_jsx(CommentThread, { comments: comments, composerPlaceholder: "Write a reply...", composerSubmitLabel: "Reply", isLoading: isCommentsLoading, isSubmitting: isCommentSubmitting, onSubmit: getCommentSubmitHandler(feedback, onSubmitComment), readOnly: !onSubmitComment, showHeader: false }));
@@ -10,8 +10,8 @@ function FeedbackCenterSheet(props) {
10
10
  return (_jsx(FormSheet, { closeDisabled: props.isSubmitting ?? false, isDirty: state.isDirty, onOpenChange: props.onOpenChange, open: props.open, children: _jsx(FeedbackCenterSheetContent, { props: props, state: state }) }));
11
11
  }
12
12
  function FeedbackCenterSheetContent({ props, state, }) {
13
- const { requestClose } = useFormSheetActions();
14
- return (_jsx(SheetContent, { size: "md", 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 })] }) }));
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 })] }) }));
15
15
  }
16
16
  function useFeedbackCenterSheetState(props) {
17
17
  const defaultTab = props.defaultTab ?? "submit";
@@ -14,7 +14,7 @@ function FeedbackSheet({ onOpenChange, open, ...composerProps }) {
14
14
  return (_jsx(FormSheet, { closeDisabled: composerProps.isSubmitting ?? false, isDirty: isDirty, onOpenChange: onOpenChange, open: open, children: _jsx(FeedbackSheetContent, { composerProps: composerProps, onDirtyChange: setIsDirty, open: open }) }));
15
15
  }
16
16
  function FeedbackSheetContent({ composerProps, onDirtyChange, open, }) {
17
- const { requestClose } = useFormSheetActions();
17
+ const { contentProps, requestClose } = useFormSheetActions();
18
18
  const formId = React.useId();
19
19
  const [isSubmitted, setIsSubmitted] = React.useState(false);
20
20
  const [urgency, setUrgency] = React.useState("normal");
@@ -24,6 +24,6 @@ function FeedbackSheetContent({ composerProps, onDirtyChange, open, }) {
24
24
  setUrgency("normal");
25
25
  }
26
26
  }, [open]);
27
- return (_jsxs(SheetContent, { size: "md", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: "Send feedback" }), _jsx(SheetDescription, { children: "Share a bug, feature request, or general note." })] }), _jsx(SheetBody, { children: _jsx(FeedbackComposer, { ...composerProps, actions: "external", formId: formId, onCancel: requestClose, onDirtyChange: onDirtyChange, onSubmittedChange: setIsSubmitted, onUrgencyChange: setUrgency }) }), isSubmitted ? null : (_jsxs(SheetFooter, { children: [_jsx(Button, { disabled: composerProps.isSubmitting, onClick: requestClose, type: "button", variant: "outline", children: "Cancel" }), _jsxs(Button, { form: formId, loading: composerProps.isSubmitting, type: "submit", variant: urgency === "emergency" ? "destructive" : "default", children: [urgency === "emergency" ? _jsx(AlertTriangleIcon, {}) : _jsx(SendIcon, {}), urgency === "emergency" ? "Send emergency" : "Submit feedback"] })] }))] }));
27
+ return (_jsxs(SheetContent, { size: "md", ...contentProps, children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: "Send feedback" }), _jsx(SheetDescription, { children: "Share a bug, feature request, or general note." })] }), _jsx(SheetBody, { children: _jsx(FeedbackComposer, { ...composerProps, actions: "external", formId: formId, onCancel: requestClose, onDirtyChange: onDirtyChange, onSubmittedChange: setIsSubmitted, onUrgencyChange: setUrgency }) }), isSubmitted ? null : (_jsxs(SheetFooter, { children: [_jsx(Button, { disabled: composerProps.isSubmitting, onClick: requestClose, type: "button", variant: "outline", children: "Cancel" }), _jsxs(Button, { form: formId, loading: composerProps.isSubmitting, type: "submit", variant: urgency === "emergency" ? "destructive" : "default", children: [urgency === "emergency" ? _jsx(AlertTriangleIcon, {}) : _jsx(SendIcon, {}), urgency === "emergency" ? "Send emergency" : "Submit feedback"] })] }))] }));
28
28
  }
29
29
  export { FeedbackSheet };
@@ -22,5 +22,5 @@ declare function KanbanBoardColumn({ activeCardId, activeCardRect, activeMove, b
22
22
  declare function KanbanDragOverlay({ activeCard, activeCardRect, }: {
23
23
  activeCard: KanbanCardData | undefined;
24
24
  activeCardRect: KanbanOverlayRect | undefined;
25
- }): React.JSX.Element;
25
+ }): React.ReactPortal;
26
26
  export { KanbanBoardColumn, KanbanDragOverlay };
@@ -45,8 +45,6 @@ function KanbanDropPlaceholder({ height }) {
45
45
  }
46
46
  function KanbanDragOverlay({ activeCard, activeCardRect, }) {
47
47
  const overlay = (_jsx(DragOverlay, { adjustScale: false, children: activeCard ? (_jsx("div", { style: { width: activeCardRect?.width }, children: _jsx(KanbanCard, { card: activeCard, isDragging: true }) })) : null }));
48
- if (typeof document === "undefined")
49
- return overlay;
50
48
  return createPortal(overlay, document.body);
51
49
  }
52
50
  function getSortableCardClickHandler({ card, onCardSelect, suppressSelectRef, }) {
@@ -139,8 +139,6 @@ function parseStoredPalette(value) {
139
139
  return null;
140
140
  }
141
141
  function getSystemTheme() {
142
- if (typeof window === "undefined")
143
- return "light";
144
142
  return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
145
143
  }
146
144
  export { ThemeProvider };
@@ -1,5 +1,12 @@
1
1
  import * as React from "react";
2
2
  type FormSheetActions = Readonly<{
3
+ /**
4
+ * Spread onto the `SheetContent` this form sheet wraps. Carries the
5
+ * close-focus handler; without it, closing drops focus on `<body>`.
6
+ */
7
+ contentProps: Readonly<{
8
+ onCloseAutoFocus: (event: Event) => void;
9
+ }>;
3
10
  requestClose: () => void;
4
11
  }>;
5
12
  type FormSheetProps = {
@@ -11,13 +11,14 @@ function useFormSheetActions() {
11
11
  }
12
12
  function FormSheet({ children, closeDisabled = false, isDirty, onOpenChange, open, trigger, }) {
13
13
  const [discardPromptOpen, setDiscardPromptOpen] = React.useState(false);
14
+ const sheetReturnFocusRef = React.useRef(null);
14
15
  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]);
16
+ // Layout effect: parent layout effects run before the sheet's focus scope
17
+ // captures on mount, so this still sees the element focused before opening.
18
+ React.useLayoutEffect(() => {
19
+ if (open)
20
+ sheetReturnFocusRef.current = getActiveElement();
21
+ }, [open]);
21
22
  const requestClose = React.useCallback(() => {
22
23
  if (closeDisabled)
23
24
  return;
@@ -26,81 +27,48 @@ function FormSheet({ children, closeDisabled = false, isDirty, onOpenChange, ope
26
27
  setDiscardPromptOpen(true);
27
28
  return;
28
29
  }
29
- closeSheet();
30
- }, [closeDisabled, closeSheet, isDirty]);
31
- const actions = React.useMemo(() => ({ requestClose }), [requestClose]);
30
+ onOpenChange(false);
31
+ }, [closeDisabled, isDirty, onOpenChange]);
32
+ const handleSheetCloseAutoFocus = React.useCallback((event) => {
33
+ returnFocusTo(event, sheetReturnFocusRef.current);
34
+ }, []);
35
+ const handleDiscardCloseAutoFocus = React.useCallback((event) => {
36
+ returnFocusTo(event, discardReturnFocusRef.current);
37
+ }, []);
38
+ const actions = React.useMemo(() => ({
39
+ contentProps: { onCloseAutoFocus: handleSheetCloseAutoFocus },
40
+ requestClose,
41
+ }), [handleSheetCloseAutoFocus, requestClose]);
32
42
  function handleOpenChange(nextOpen) {
33
43
  if (nextOpen) {
34
- returnFocus.capture();
35
44
  onOpenChange(true);
36
45
  return;
37
46
  }
38
47
  requestClose();
39
48
  }
40
49
  function discardChanges() {
41
- discardingRef.current = true;
42
50
  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
+ onOpenChange(false);
51
52
  }
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
+ 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, { onCloseAutoFocus: handleDiscardCloseAutoFocus, onDiscard: discardChanges, onOpenChange: setDiscardPromptOpen, open: discardPromptOpen })] }));
53
54
  }
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]);
55
+ /**
56
+ * Radix aims its close-focus at the overlay's trigger. A controlled sheet has
57
+ * no trigger, so Radix suppresses the focus scope's own fallback and then
58
+ * focuses nothing, leaving `<body>` active. Restoring here rather than on a
59
+ * timer is what keeps the timing right: Radix fires this after the content
60
+ * unmounts, which is the moment the element is safe to focus again.
61
+ */
62
+ function returnFocusTo(event, returnFocus) {
63
+ if (!returnFocus?.isConnected)
64
+ return; // discarded: the sheet owns focus from here
65
+ event.preventDefault();
66
+ returnFocus.focus();
96
67
  }
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" })] })] }) }));
68
+ function DiscardChangesPrompt({ onCloseAutoFocus, onDiscard, onOpenChange, open, }) {
69
+ return (_jsx(AlertDialog, { onOpenChange: onOpenChange, open: open, children: _jsxs(AlertDialogContent, { onCloseAutoFocus: onCloseAutoFocus, 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
70
  }
100
71
  function getActiveElement() {
101
- // Called during render, so it must be safe on the server too.
102
- if (typeof document === "undefined")
103
- return null;
104
72
  return document.activeElement instanceof HTMLElement ? document.activeElement : null;
105
73
  }
106
74
  export { FormSheet, useFormSheetActions };
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * localStorage that cannot throw at its callers.
3
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.
4
+ * Every access can raise: reading the property throws when storage is disabled
5
+ * by policy or by Safari's private mode, and `setItem` throws on quota. A
6
+ * preference is worth remembering and never worth failing over, so each call
7
+ * degrades to "no stored value" and the caller keeps its in-memory state.
9
8
  */
10
9
  declare function readStoredValue(key: string): string | null;
11
10
  declare function writeStoredValue(key: string, value: string): void;
@@ -1,15 +1,14 @@
1
1
  /**
2
2
  * localStorage that cannot throw at its callers.
3
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.
4
+ * Every access can raise: reading the property throws when storage is disabled
5
+ * by policy or by Safari's private mode, and `setItem` throws on quota. A
6
+ * preference is worth remembering and never worth failing over, so each call
7
+ * degrades to "no stored value" and the caller keeps its in-memory state.
9
8
  */
10
9
  function storage() {
11
10
  try {
12
- return typeof window === "undefined" ? null : window.localStorage;
11
+ return window.localStorage;
13
12
  }
14
13
  catch {
15
14
  // Accessing the property itself throws when storage is blocked by policy.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciridae/ui",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "engines": {