@ciridae/ui 0.14.0 → 0.16.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 (57) hide show
  1. package/dist/components/blocks/feedback-center-comments.d.ts +2 -1
  2. package/dist/components/blocks/feedback-center-comments.js +6 -5
  3. package/dist/components/blocks/feedback-center-sheet-parts.d.ts +2 -1
  4. package/dist/components/blocks/feedback-center-sheet-parts.js +6 -6
  5. package/dist/components/blocks/feedback-center-sheet.d.ts +1 -0
  6. package/dist/components/blocks/feedback-center-sheet.js +3 -1
  7. package/dist/components/blocks/feedback-composer.d.ts +2 -0
  8. package/dist/components/blocks/feedback-composer.js +5 -5
  9. package/dist/components/blocks/generated-connect-link.js +1 -1
  10. package/dist/components/blocks/kanban-board.js +0 -1
  11. package/dist/components/blocks/mcp-access-types.d.ts +4 -3
  12. package/dist/components/blocks/timestamp-format.js +2 -3
  13. package/dist/components/ui/app-shell-page.js +6 -2
  14. package/dist/components/ui/data-table-context.js +0 -1
  15. package/dist/components/ui/data-table-tanstack.js +0 -2
  16. package/dist/components/ui/item.d.ts +1 -1
  17. package/dist/components/ui/sidebar/context.d.ts +6 -0
  18. package/dist/components/ui/sidebar/context.js +9 -1
  19. package/dist/components/ui/sidebar-account-menu.js +1 -1
  20. package/dist/components/ui/sidebar.d.ts +1 -1
  21. package/dist/components/ui/sidebar.js +1 -1
  22. package/dist/components/ui/tabs-variants.d.ts +1 -1
  23. package/dist/components/walkthrough/presentation/WalkthroughCoach.d.ts +14 -0
  24. package/dist/components/walkthrough/presentation/WalkthroughCoach.js +32 -0
  25. package/dist/components/walkthrough/presentation/WalkthroughHighlight.d.ts +5 -0
  26. package/dist/components/walkthrough/presentation/WalkthroughHighlight.js +9 -0
  27. package/dist/components/walkthrough/presentation/WalkthroughOverlay.d.ts +1 -0
  28. package/dist/components/walkthrough/presentation/WalkthroughOverlay.js +70 -0
  29. package/dist/components/walkthrough/presentation/positionWalkthroughCoach.d.ts +11 -0
  30. package/dist/components/walkthrough/presentation/positionWalkthroughCoach.js +24 -0
  31. package/dist/components/walkthrough/presentation/useWalkthroughCoachPosition.d.ts +4 -0
  32. package/dist/components/walkthrough/presentation/useWalkthroughCoachPosition.js +17 -0
  33. package/dist/components/walkthrough/runtime/WalkthroughContextDefinition.d.ts +20 -0
  34. package/dist/components/walkthrough/runtime/WalkthroughContextDefinition.js +2 -0
  35. package/dist/components/walkthrough/runtime/WalkthroughProvider.d.ts +12 -0
  36. package/dist/components/walkthrough/runtime/WalkthroughProvider.js +190 -0
  37. package/dist/components/walkthrough/runtime/useWalkthrough.d.ts +1 -0
  38. package/dist/components/walkthrough/runtime/useWalkthrough.js +8 -0
  39. package/dist/components/walkthrough/runtime/walkthroughEvents.d.ts +3 -0
  40. package/dist/components/walkthrough/runtime/walkthroughEvents.js +55 -0
  41. package/dist/components/walkthrough/runtime/walkthroughLocation.d.ts +2 -0
  42. package/dist/components/walkthrough/runtime/walkthroughLocation.js +27 -0
  43. package/dist/components/walkthrough/runtime/walkthroughStorage.d.ts +3 -0
  44. package/dist/components/walkthrough/runtime/walkthroughStorage.js +54 -0
  45. package/dist/components/walkthrough/runtime/walkthroughTypes.d.ts +32 -0
  46. package/dist/components/walkthrough/runtime/walkthroughTypes.js +1 -0
  47. package/dist/components/walkthrough/targeting/listenForOutsideWalkthroughInteraction.d.ts +2 -0
  48. package/dist/components/walkthrough/targeting/listenForOutsideWalkthroughInteraction.js +19 -0
  49. package/dist/components/walkthrough/targeting/useWalkthroughTargetRect.d.ts +15 -0
  50. package/dist/components/walkthrough/targeting/useWalkthroughTargetRect.js +54 -0
  51. package/dist/components/walkthrough/targeting/walkthroughTarget.d.ts +6 -0
  52. package/dist/components/walkthrough/targeting/walkthroughTarget.js +24 -0
  53. package/dist/components/walkthrough.d.ts +4 -0
  54. package/dist/components/walkthrough.js +2 -0
  55. package/dist/entries/walkthrough.d.ts +1 -0
  56. package/dist/entries/walkthrough.js +1 -0
  57. package/package.json +1 -2
@@ -10,6 +10,7 @@ type FeedbackCenterCommentThreadProps = {
10
10
  onRetry?: () => void;
11
11
  onSubmitComment?: FeedbackCommentSubmitHandler;
12
12
  statusEvents: UserFeedbackStatusEvent[];
13
+ submitError?: Error | null;
13
14
  };
14
- declare function FeedbackCenterCommentThread({ comments, error, feedback, isCommentSubmitting, isCommentsLoading, onRetry, onSubmitComment, statusEvents, }: FeedbackCenterCommentThreadProps): import("@types/react").JSX.Element;
15
+ declare function FeedbackCenterCommentThread({ comments, error, feedback, isCommentSubmitting, isCommentsLoading, onRetry, onSubmitComment, statusEvents, submitError, }: FeedbackCenterCommentThreadProps): import("@types/react").JSX.Element;
15
16
  export { FeedbackCenterCommentThread, type FeedbackCommentSubmitHandler };
@@ -8,15 +8,16 @@ import { useRevealThreadEnd } from "./use-reveal-thread-end.js";
8
8
  import { Alert, AlertDescription, AlertTitle } from "../ui/alert.js";
9
9
  import { Button } from "../ui/button.js";
10
10
  import { Skeleton } from "../ui/skeleton.js";
11
- function FeedbackCenterCommentThread({ comments, error, feedback, isCommentSubmitting, isCommentsLoading, onRetry, onSubmitComment, statusEvents, }) {
11
+ function FeedbackCenterCommentThread({ comments, error, feedback, isCommentSubmitting, isCommentsLoading, onRetry, onSubmitComment, statusEvents, submitError, }) {
12
12
  const activity = mergeFeedbackActivity(statusEvents, comments);
13
+ const initialStatusEventId = activity.find((entry) => entry.kind === "status")?.event.id;
13
14
  const { armReveal, endRef } = useRevealThreadEnd(activity.length);
14
15
  // A retry in flight shows the thread's loading state, not the stale error.
15
16
  if (error && !isCommentsLoading) {
16
17
  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] })] }));
17
18
  }
18
19
  const submitComment = getCommentSubmitHandler(feedback, onSubmitComment, armReveal);
19
- return (_jsxs("section", { "aria-label": "Activity", className: "relative grid gap-(--panel-gap)", children: [_jsxs("div", { className: "grid gap-1", children: [activity.length === 0 && !isCommentsLoading ? _jsx(FeedbackActivityEmpty, {}) : null, 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, _jsx("div", { "aria-hidden": true, className: "absolute bottom-0", ref: endRef })] }));
20
+ return (_jsxs("section", { "aria-label": "Activity", className: "relative grid gap-(--panel-gap)", children: [_jsxs("div", { className: "grid gap-1", children: [activity.length === 0 && !isCommentsLoading ? _jsx(FeedbackActivityEmpty, {}) : null, activity.map((entry) => entry.kind === "comment" ? (_jsx(CommentItem, { comment: entry.comment }, entry.comment.id)) : (_jsx(FeedbackStatusActivity, { event: entry.event, isInitial: entry.event.id === initialStatusEventId }, entry.event.id))), isCommentsLoading ? _jsx(FeedbackActivityLoading, {}) : null] }), submitComment ? (_jsx(CommentComposer, { isSubmitting: isCommentSubmitting, onSubmit: submitComment, placeholder: "Write a reply...", submitLabel: "Reply" })) : null, submitError && !isCommentSubmitting ? (_jsxs(Alert, { variant: "destructive", children: [_jsx(AlertCircleIcon, {}), _jsx(AlertTitle, { children: "Reply could not be sent" }), _jsx(AlertDescription, { children: submitError.message })] })) : null, _jsx("div", { "aria-hidden": true, className: "absolute bottom-0", ref: endRef })] }));
20
21
  }
21
22
  function FeedbackActivityEmpty() {
22
23
  return (_jsxs("div", { className: "flex items-start gap-2 py-2 text-sm text-muted-foreground", children: [_jsx(MessageSquareTextIcon, { className: "mt-0.5 size-4 shrink-0" }), _jsx("span", { children: "No activity yet." })] }));
@@ -43,11 +44,11 @@ function compareActivityEntries(left, right) {
43
44
  return timestampOrder;
44
45
  return left.kind === right.kind ? 0 : left.kind === "status" ? -1 : 1;
45
46
  }
46
- function FeedbackStatusActivity({ event }) {
47
- 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), title: formatFeedbackTimestampFull(event.createdAt), children: formatFeedbackTimestamp(event.createdAt) })] }));
47
+ function FeedbackStatusActivity({ event, isInitial, }) {
48
+ 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 bg-border" }), _jsx("span", { children: isInitial ? "Initial status" : "Status changed to" }), _jsx(FeedbackStatusBadge, { status: event.status }), _jsx("time", { className: "ml-auto shrink-0 font-mono tabular-nums", dateTime: getFeedbackTimestampDateTime(event.createdAt), title: formatFeedbackTimestampFull(event.createdAt), children: formatFeedbackTimestamp(event.createdAt) })] }));
48
49
  }
49
50
  function FeedbackActivityLoading() {
50
- 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" })] })] }));
51
+ return (_jsxs("div", { "aria-label": "Loading activity", className: "flex gap-3 px-1 py-2", role: "status", children: [_jsx(Skeleton, { className: "size-6 rounded-none" }), _jsxs("div", { className: "grid flex-1 gap-2", children: [_jsx(Skeleton, { className: "h-3 w-32" }), _jsx(Skeleton, { className: "h-3 w-4/5" })] })] }));
51
52
  }
52
53
  function getCommentSubmitHandler(feedback, onSubmitComment, armReveal) {
53
54
  if (!feedback || !onSubmitComment)
@@ -11,6 +11,7 @@ type FeedbackCenterTabsProps = {
11
11
  canComment?: (item: UserFeedbackItem) => boolean;
12
12
  commentsByFeedbackId: Record<string, CommentItemData[]>;
13
13
  commentsError?: Error | null;
14
+ commentSubmitError?: Error | null;
14
15
  composerProps: FeedbackCenterComposerProps;
15
16
  formId: string;
16
17
  onDirtyChange: (dirty: boolean) => void;
@@ -35,5 +36,5 @@ type FeedbackCenterTabsProps = {
35
36
  declare function FeedbackCenterSheetHeader({ unreadReplyCount }: {
36
37
  unreadReplyCount: number;
37
38
  }): 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;
39
+ declare function FeedbackCenterTabs({ canComment, composerProps, commentsByFeedbackId, commentsError, commentSubmitError, formId, onDirtyChange, onUrgencyChange, isCommentSubmitting, isCommentsLoading, isLoading, items, itemsError, onBack, onCancel, onSelectFeedback, onRetryComments, onRetryItems, onSubmittedChange, onSubmitComment, selectedFeedback, renderEvidence, statusEventsByFeedbackId, view, }: FeedbackCenterTabsProps): React.JSX.Element;
39
40
  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({ 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 })] }));
12
+ function FeedbackCenterTabs({ canComment, composerProps, commentsByFeedbackId, commentsError, commentSubmitError, 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, commentSubmitError: commentSubmitError, 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,14 +22,14 @@ function FeedbackCenterSubmitTab({ composerProps, formId, onCancel, onDirtyChang
22
22
  function FeedbackCenterMineContent(props) {
23
23
  return (_jsx(TabsContent, { className: "mt-0", value: "mine", children: _jsx(FeedbackCenterMineTab, { ...props }) }));
24
24
  }
25
- function FeedbackCenterMineTab({ canComment, isLoading, commentsByFeedbackId, commentsError, isCommentSubmitting, isCommentsLoading, items, itemsError, onBack, onSelectFeedback, onRetryComments, onRetryItems, onSubmitComment, selectedFeedback, renderEvidence, statusEventsByFeedbackId, view, }) {
25
+ function FeedbackCenterMineTab({ canComment, isLoading, commentsByFeedbackId, commentsError, commentSubmitError, isCommentSubmitting, isCommentsLoading, items, itemsError, onBack, onSelectFeedback, onRetryComments, onRetryItems, onSubmitComment, selectedFeedback, renderEvidence, statusEventsByFeedbackId, view, }) {
26
26
  if (view === "detail") {
27
- return (_jsx(FeedbackCenterDetailPane, { comments: getFeedbackScopedItems(commentsByFeedbackId, selectedFeedback), canComment: canComment, commentsError: commentsError, feedback: selectedFeedback, isCommentSubmitting: isCommentSubmitting, isCommentsLoading: isCommentsLoading, onBack: onBack, onRetryComments: onRetryComments, onSubmitComment: onSubmitComment, renderEvidence: renderEvidence, statusEvents: getFeedbackScopedItems(statusEventsByFeedbackId, selectedFeedback) }));
27
+ return (_jsx(FeedbackCenterDetailPane, { comments: getFeedbackScopedItems(commentsByFeedbackId, selectedFeedback), canComment: canComment, commentsError: commentsError, commentSubmitError: commentSubmitError, feedback: selectedFeedback, isCommentSubmitting: isCommentSubmitting, isCommentsLoading: isCommentsLoading, onBack: onBack, onRetryComments: onRetryComments, onSubmitComment: onSubmitComment, renderEvidence: renderEvidence, statusEvents: getFeedbackScopedItems(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({ 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 }));
31
+ function FeedbackCenterDetailPane({ canComment, comments, commentsError, commentSubmitError, 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, submitError: commentSubmitError }), evidenceSlot: feedback ? renderEvidence?.(feedback) : undefined, feedback: feedback, onBack: onBack }));
33
33
  }
34
34
  function getFeedbackScopedItems(itemsByFeedbackId, feedback) {
35
35
  if (!feedback)
@@ -8,6 +8,7 @@ type FeedbackCenterSheetProps = Omit<FeedbackComposerProps, "actions" | "formId"
8
8
  canComment?: (item: UserFeedbackItem) => boolean;
9
9
  commentsByFeedbackId?: Record<string, CommentItemData[]>;
10
10
  commentsError?: Error | null;
11
+ commentSubmitError?: Error | null;
11
12
  defaultTab?: FeedbackCenterTab;
12
13
  isCommentSubmitting?: boolean;
13
14
  isCommentsLoading?: boolean;
@@ -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, { 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 })] }) }));
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, commentSubmitError: props.commentSubmitError, 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";
@@ -85,6 +85,8 @@ function getFeedbackComposerProps(props) {
85
85
  isSubmitting: props.isSubmitting,
86
86
  maxAttachmentSizeMb: props.maxAttachmentSizeMb,
87
87
  maxAttachments: props.maxAttachments,
88
+ maxDescriptionLength: props.maxDescriptionLength,
89
+ maxSubjectLength: props.maxSubjectLength,
88
90
  onSubmit: props.onSubmit,
89
91
  urgencyEnabled: props.urgencyEnabled,
90
92
  };
@@ -10,6 +10,8 @@ type FeedbackComposerProps = {
10
10
  isSubmitting?: boolean;
11
11
  maxAttachmentSizeMb?: number;
12
12
  maxAttachments?: number;
13
+ maxDescriptionLength?: number;
14
+ maxSubjectLength?: number;
13
15
  onCancel?: () => void;
14
16
  onDirtyChange?: (dirty: boolean) => void;
15
17
  onSubmittedChange?: (submitted: boolean) => void;
@@ -31,15 +31,15 @@ function FeedbackComposer(props) {
31
31
  }
32
32
  function FeedbackComposerForm({ ids, props, state, }) {
33
33
  const isSubmitting = props.isSubmitting ?? false;
34
- return (_jsxs("form", { className: "grid gap-(--panel-gap)", id: props.formId, onSubmit: (event) => submitFeedbackComposer(event, props, state), children: [_jsxs(FieldGroup, { children: [state.urgency === "normal" ? (_jsx(FeedbackTypeField, { disabled: isSubmitting, onTypeChange: state.setType, type: state.type })) : null, _jsx(FeedbackSubjectField, { disabled: isSubmitting, error: state.error, errorId: ids.subjectErrorId, fieldId: ids.subjectId, onSubjectChange: state.setSubject, subject: state.subject }), _jsx(FeedbackDescriptionField, { description: state.description, disabled: isSubmitting, error: state.error, errorId: ids.descriptionErrorId, fieldId: ids.descriptionId, onDescriptionChange: state.setDescription }), _jsx(FeedbackAttachmentsField, { props: props, state: state }), _jsx(FeedbackComposerEmergencySection, { ids: ids, props: props, state: state }), state.submitError ? _jsx(FieldError, { children: state.submitError }) : null] }), props.actions === "external" ? null : (_jsx(FeedbackComposerActions, { isSubmitting: isSubmitting, onCancel: props.onCancel, urgency: state.urgency }))] }));
34
+ return (_jsxs("form", { className: "grid gap-(--panel-gap)", id: props.formId, onSubmit: (event) => submitFeedbackComposer(event, props, state), children: [_jsxs(FieldGroup, { children: [state.urgency === "normal" ? (_jsx(FeedbackTypeField, { disabled: isSubmitting, onTypeChange: state.setType, type: state.type })) : null, _jsx(FeedbackSubjectField, { disabled: isSubmitting, error: state.error, errorId: ids.subjectErrorId, fieldId: ids.subjectId, maxLength: props.maxSubjectLength ?? 255, onSubjectChange: state.setSubject, subject: state.subject }), _jsx(FeedbackDescriptionField, { description: state.description, disabled: isSubmitting, error: state.error, errorId: ids.descriptionErrorId, fieldId: ids.descriptionId, maxLength: props.maxDescriptionLength ?? 10000, onDescriptionChange: state.setDescription }), _jsx(FeedbackAttachmentsField, { props: props, state: state }), _jsx(FeedbackComposerEmergencySection, { ids: ids, props: props, state: state }), state.submitError ? _jsx(FieldError, { children: state.submitError }) : null] }), props.actions === "external" ? null : (_jsx(FeedbackComposerActions, { isSubmitting: isSubmitting, onCancel: props.onCancel, urgency: state.urgency }))] }));
35
35
  }
36
- function FeedbackSubjectField({ disabled, error, errorId, fieldId, onSubjectChange, subject, }) {
36
+ function FeedbackSubjectField({ disabled, error, errorId, fieldId, maxLength, onSubjectChange, subject, }) {
37
37
  const invalid = error === "subject";
38
- return (_jsxs(Field, { "data-invalid": invalid, children: [_jsx(FieldContent, { children: _jsx(FieldLabel, { htmlFor: fieldId, required: true, children: "Subject" }) }), _jsx(Input, { "aria-describedby": invalid ? errorId : undefined, "aria-invalid": invalid, "aria-required": "true", disabled: disabled, id: fieldId, maxLength: 255, onChange: (event) => onSubjectChange(event.target.value), placeholder: "Short subject for the team", value: subject }), invalid ? _jsx(FieldError, { id: errorId, children: "Subject is required." }) : null] }));
38
+ return (_jsxs(Field, { "data-invalid": invalid, children: [_jsx(FieldContent, { children: _jsx(FieldLabel, { htmlFor: fieldId, required: true, children: "Subject" }) }), _jsx(Input, { "aria-describedby": invalid ? errorId : undefined, "aria-invalid": invalid, "aria-required": "true", disabled: disabled, id: fieldId, maxLength: maxLength, onChange: (event) => onSubjectChange(event.target.value), placeholder: "Short subject for the team", value: subject }), invalid ? _jsx(FieldError, { id: errorId, children: "Subject is required." }) : null] }));
39
39
  }
40
- function FeedbackDescriptionField({ description, disabled, error, errorId, fieldId, onDescriptionChange, }) {
40
+ function FeedbackDescriptionField({ description, disabled, error, errorId, fieldId, maxLength, onDescriptionChange, }) {
41
41
  const invalid = error === "description";
42
- return (_jsxs(Field, { "data-invalid": invalid, children: [_jsx(FieldContent, { children: _jsx(FieldLabel, { htmlFor: fieldId, required: true, children: "Feedback" }) }), _jsx(Textarea, { "aria-describedby": invalid ? errorId : undefined, "aria-invalid": invalid, "aria-required": "true", className: "min-h-40", disabled: disabled, id: fieldId, maxLength: 10000, onChange: (event) => onDescriptionChange(event.target.value), placeholder: "What happened, what did you expect, or what would you like to see?", rows: 6, value: description }), invalid ? _jsx(FieldError, { id: errorId, children: "Feedback is required." }) : null] }));
42
+ return (_jsxs(Field, { "data-invalid": invalid, children: [_jsx(FieldContent, { children: _jsx(FieldLabel, { htmlFor: fieldId, required: true, children: "Feedback" }) }), _jsx(Textarea, { "aria-describedby": invalid ? errorId : undefined, "aria-invalid": invalid, "aria-required": "true", className: "min-h-40", disabled: disabled, id: fieldId, maxLength: maxLength, onChange: (event) => onDescriptionChange(event.target.value), placeholder: "What happened, what did you expect, or what would you like to see?", rows: 6, value: description }), invalid ? _jsx(FieldError, { id: errorId, children: "Feedback is required." }) : null] }));
43
43
  }
44
44
  function FeedbackAttachmentsField({ props, state, }) {
45
45
  if (props.attachmentsEnabled === false)
@@ -58,7 +58,7 @@ export function GeneratedConnectLink({ access, createConnectLink, currentTools,
58
58
  // well above this button, and this is the click that acts on it.
59
59
  // Keyed on whether the grant can write rather than on the mode, so
60
60
  // a Custom set holding an action tool does not read as read-only.
61
- variant: writeGranting ? "destructive" : "default", children: generateLabel(link !== undefined, access, writeGranting) }), createConnectLink.isPending ? (_jsx(Spinner, { "aria-label": "Generating connection code", className: "text-muted-foreground" })) : null, link === undefined ? null : (_jsxs("p", { className: "text-muted-foreground/70 text-xs", children: [_jsxs("span", { className: "font-medium", children: [ACCESS_LABEL[link.access], " access"] }), " \u00B7 single use \u00B7 expires ", expiryFormatter.format(new Date(link.expiresAt))] }))] }), staleLabel === null ? null : (_jsxs("p", { className: "text-destructive text-xs", children: ["This code was generated before you changed the access above. Generate a new one to share", " ", staleLabel, " access."] }))] }));
61
+ variant: writeGranting ? "destructive" : "default", children: generateLabel(link !== undefined, access, writeGranting) }), createConnectLink.isPending ? (_jsx(Spinner, { "aria-label": "Generating connection code", className: "text-muted-foreground" })) : null, link === undefined ? null : (_jsxs("p", { className: "text-muted-foreground/70 text-xs", children: [_jsxs("span", { className: "font-medium", children: [ACCESS_LABEL[link.access], " access"] }), " \u00B7 single use \u00B7 expires ", expiryFormatter.format(link.expiresAtEpochMilliseconds)] }))] }), staleLabel === null ? null : (_jsxs("p", { className: "text-destructive text-xs", children: ["This code was generated before you changed the access above. Generate a new one to share", " ", staleLabel, " access."] }))] }));
62
62
  }
63
63
  function sameSelection(left, right) {
64
64
  if (left === null || right === null)
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /* eslint-disable react-hooks/refs -- dnd-kit drag handlers need a mutable ref to suppress the click fired after a drag. */
3
2
  import * as React from "react";
4
3
  import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
5
4
  import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
@@ -29,13 +29,14 @@ export type McpAccessToolListing = {
29
29
  * code was stamped with, and when it stops working. The code itself is not
30
30
  * here — the app shows it through its own `CopyField`, and a type that never
31
31
  * carries the credential cannot leak it. Discriminated on `access`: only a
32
- * selected link carries tools, and it always does.
32
+ * selected link carries tools, and it always does. Expiry is integer Unix
33
+ * epoch milliseconds.
33
34
  */
34
35
  export type McpConnectLink = {
35
36
  access: Exclude<McpAccessMode, "selected">;
36
- expiresAt: string;
37
+ expiresAtEpochMilliseconds: number;
37
38
  } | {
38
39
  access: "selected";
39
40
  tools: McpAccessToolSelector[];
40
- expiresAt: string;
41
+ expiresAtEpochMilliseconds: number;
41
42
  };
@@ -1,4 +1,3 @@
1
- import { isValid, parseISO } from "date-fns";
2
1
  function formatTimestamp(value, options = {}) {
3
2
  const date = parseTimestamp(value);
4
3
  if (!date)
@@ -40,8 +39,8 @@ function toUtcTimestamp(value) {
40
39
  function parseTimestamp(value) {
41
40
  if (!value)
42
41
  return undefined;
43
- const date = parseISO(toUtcTimestamp(value));
44
- return isValid(date) ? date : undefined;
42
+ const date = new Date(toUtcTimestamp(value));
43
+ return Number.isNaN(date.getTime()) ? undefined : date;
45
44
  }
46
45
  function getTimestampFallback(value, fallback = "No update") {
47
46
  return value?.trim() || fallback;
@@ -1,12 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { cn } from "../../lib/utils.js";
3
3
  import { PageHeader, PageHeaderActions, PageHeaderContent, PageHeaderDescription, PageHeaderMain, PageHeaderTitle, } from "./page-header.js";
4
- import { SidebarTrigger } from "./sidebar.js";
4
+ import { SidebarTrigger, useOptionalSidebar } from "./sidebar.js";
5
5
  function AppShellPage({ className, ...props }) {
6
6
  return (_jsx("div", { "data-slot": "app-shell-page", className: cn("flex min-h-0 flex-1 flex-col bg-background text-foreground", className), ...props }));
7
7
  }
8
8
  function AppShellPageHeader({ actions, className, description, title, ...props }) {
9
- return (_jsx(PageHeader, { className: className, ...props, children: _jsxs(PageHeaderMain, { className: "relative pl-14 md:pl-6 lg:pl-8", children: [_jsx(SidebarTrigger, { className: "absolute top-1/2 left-4 -translate-y-1/2 md:hidden" }), _jsxs(PageHeaderContent, { children: [_jsx(PageHeaderTitle, { children: title }), description ? _jsx(PageHeaderDescription, { children: description }) : null] }), actions ? _jsx(PageHeaderActions, { children: actions }) : null] }) }));
9
+ // The mobile trigger is an affordance of the shell, not of the header. Pages
10
+ // rendered outside a SidebarProvider get the header without it — and without
11
+ // the gutter that only exists to clear it — rather than a useSidebar throw.
12
+ const hasSidebar = useOptionalSidebar() !== null;
13
+ return (_jsx(PageHeader, { className: className, ...props, children: _jsxs(PageHeaderMain, { className: cn(hasSidebar && "relative pl-14 md:pl-6 lg:pl-8"), children: [hasSidebar ? (_jsx(SidebarTrigger, { className: "absolute top-1/2 left-4 -translate-y-1/2 md:hidden" })) : null, _jsxs(PageHeaderContent, { children: [_jsx(PageHeaderTitle, { children: title }), description ? _jsx(PageHeaderDescription, { children: description }) : null] }), actions ? _jsx(PageHeaderActions, { children: actions }) : null] }) }));
10
14
  }
11
15
  function AppShellWelcome({ brand, children, className, description, title, ...props }) {
12
16
  return (_jsx("div", { "data-slot": "app-shell-welcome", className: cn("flex min-h-0 flex-1 items-center justify-center p-6", className), ...props, children: _jsxs("div", { className: "flex max-w-md flex-col items-center gap-4 text-center", children: [brand ? _jsx(AppShellWelcomeBrand, { brand: brand }) : null, _jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx("h2", { className: "font-heading text-2xl tracking-normal", children: title }), description ? (_jsx("div", { className: "text-sm text-muted-foreground [&>a]:font-medium [&>a]:text-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-foreground/72", children: description })) : null] }), children] }) }));
@@ -48,7 +48,6 @@ function useDataTable() {
48
48
  if (context === null) {
49
49
  throw new Error("This control must be rendered inside a <TanStackDataTable />.");
50
50
  }
51
- // eslint-disable-next-line no-restricted-syntax -- restores the row type erased by the context above; the provider only ever stores a TanStackTable.
52
51
  return context.table;
53
52
  }
54
53
  export { DataTableProvider, useDataTable };
@@ -70,7 +70,6 @@ function useClampedPageIndex(table, enabled, loading, onCorrected) {
70
70
  table.setPageIndex(lastPageIndex);
71
71
  }, [lastPageIndex, outOfRange, table]);
72
72
  }
73
- // eslint-disable-next-line complexity -- TanStack table setup is declarative option wiring.
74
73
  function useDataTableInstance(props) {
75
74
  const [sorting, setSorting] = React.useState(props.state?.sorting ?? []);
76
75
  const [columnFilters, setColumnFilters] = React.useState(props.state?.columnFilters ?? []);
@@ -79,7 +78,6 @@ function useDataTableInstance(props) {
79
78
  const [rowSelection, setRowSelection] = React.useState(props.state?.rowSelection ?? {});
80
79
  const [globalFilter, setGlobalFilter] = React.useState(props.state?.globalFilter ?? "");
81
80
  const [pagination, setPagination] = React.useState(props.state?.pagination ?? { pageIndex: 0, pageSize: props.defaultPageSize ?? 10 });
82
- // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Table intentionally returns imperative table helpers.
83
81
  return useReactTable({
84
82
  data: props.data,
85
83
  columns: props.columns,
@@ -12,7 +12,7 @@ declare function Item({ className, variant, size, asChild, ...props }: React.Com
12
12
  asChild?: boolean;
13
13
  }): React.JSX.Element;
14
14
  declare const itemMediaVariants: (props?: ({
15
- variant?: "default" | "icon" | "image" | null | undefined;
15
+ variant?: "image" | "default" | "icon" | null | undefined;
16
16
  } & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
17
17
  declare function ItemMedia({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>): React.JSX.Element;
18
18
  declare function ItemContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
@@ -18,4 +18,10 @@ export type SidebarProviderStyle = React.CSSProperties & Record<"--sidebar-width
18
18
  export type SidebarMobileStyle = React.CSSProperties & Record<"--sidebar-width", string>;
19
19
  export type SidebarSkeletonStyle = React.CSSProperties & Record<"--skeleton-width", string>;
20
20
  export declare const SidebarContext: React.Context<SidebarContextProps | null>;
21
+ /**
22
+ * The sidebar context, or `null` outside a `SidebarProvider`. For components
23
+ * whose sidebar affordance is optional; anything that needs the sidebar to
24
+ * function should keep using `useSidebar`.
25
+ */
26
+ export declare function useOptionalSidebar(): SidebarContextProps | null;
21
27
  export declare function useSidebar(): SidebarContextProps;
@@ -6,8 +6,16 @@ export const SIDEBAR_WIDTH_MOBILE = "18rem";
6
6
  export const SIDEBAR_WIDTH_ICON = "3rem";
7
7
  export const SIDEBAR_KEYBOARD_SHORTCUT = "b";
8
8
  export const SidebarContext = React.createContext(null);
9
+ /**
10
+ * The sidebar context, or `null` outside a `SidebarProvider`. For components
11
+ * whose sidebar affordance is optional; anything that needs the sidebar to
12
+ * function should keep using `useSidebar`.
13
+ */
14
+ export function useOptionalSidebar() {
15
+ return React.useContext(SidebarContext);
16
+ }
9
17
  export function useSidebar() {
10
- const context = React.useContext(SidebarContext);
18
+ const context = useOptionalSidebar();
11
19
  if (!context) {
12
20
  throw new Error("useSidebar must be used within a SidebarProvider.");
13
21
  }
@@ -46,6 +46,6 @@ function getAccountLabel(username, email) {
46
46
  return "Account";
47
47
  }
48
48
  function getAccountMark(label) {
49
- return label.match(/[A-Za-z0-9]/)?.[0]?.toUpperCase() ?? "U";
49
+ return label.match(/[a-z0-9]/i)?.[0]?.toUpperCase() ?? "U";
50
50
  }
51
51
  export { SidebarAccountMenu };
@@ -4,4 +4,4 @@ export { SidebarTrigger } from "./sidebar/trigger.js";
4
4
  export { SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, } from "./sidebar/group.js";
5
5
  export { SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, } from "./sidebar/menu.js";
6
6
  export { SidebarProvider } from "./sidebar/provider.js";
7
- export { useSidebar } from "./sidebar/context.js";
7
+ export { useOptionalSidebar, useSidebar } from "./sidebar/context.js";
@@ -4,4 +4,4 @@ export { SidebarTrigger } from "./sidebar/trigger.js";
4
4
  export { SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, } from "./sidebar/group.js";
5
5
  export { SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, } from "./sidebar/menu.js";
6
6
  export { SidebarProvider } from "./sidebar/provider.js";
7
- export { useSidebar } from "./sidebar/context.js";
7
+ export { useOptionalSidebar, useSidebar } from "./sidebar/context.js";
@@ -1,6 +1,6 @@
1
1
  import { type VariantProps } from "class-variance-authority";
2
2
  declare const tabsListVariants: (props?: ({
3
- variant?: "default" | "line" | null | undefined;
3
+ variant?: "line" | "default" | null | undefined;
4
4
  } & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
5
5
  type TabsListVariantProps = VariantProps<typeof tabsListVariants>;
6
6
  export { tabsListVariants, type TabsListVariantProps };
@@ -0,0 +1,14 @@
1
+ import type { WalkthroughAdvance, WalkthroughStep } from "../runtime/walkthroughTypes.js";
2
+ export type WalkthroughCoachState = "locating" | "missing" | "off-route" | "ready" | "restore";
3
+ export declare function WalkthroughCoach({ advanceType, coachState, flowTitle, onBack, onNext, onPause, onReturn, step, stepCount, stepIndex, }: {
4
+ advanceType: WalkthroughAdvance["type"];
5
+ coachState: WalkthroughCoachState;
6
+ flowTitle: string;
7
+ onBack: () => void;
8
+ onNext: () => void;
9
+ onPause: () => void;
10
+ onReturn: () => void;
11
+ step: WalkthroughStep;
12
+ stepCount: number;
13
+ stepIndex: number;
14
+ }): import("@types/react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Button } from "../../ui/button.js";
3
+ import { Card, CardContent } from "../../ui/card.js";
4
+ export function WalkthroughCoach({ advanceType, coachState, flowTitle, onBack, onNext, onPause, onReturn, step, stepCount, stepIndex, }) {
5
+ return (_jsx(Card, { className: "border-foreground/20 shadow-sm", children: _jsxs(CardContent, { className: "grid gap-4 py-4", children: [_jsxs("div", { className: "grid gap-1", children: [_jsxs("p", { className: "text-meta text-muted-foreground", children: [flowTitle, " \u00B7 Step ", stepIndex + 1, " of ", stepCount] }), _jsx("h2", { className: "font-heading text-lg text-foreground", children: step.title }), _jsx("p", { className: "text-sm leading-relaxed text-muted-foreground", children: step.body }), _jsx(CoachStatus, { advanceType: advanceType, state: coachState })] }), _jsxs("div", { className: "flex flex-wrap justify-end gap-2", children: [_jsx(Button, { onClick: onPause, size: "sm", type: "button", variant: "ghost", children: "Pause" }), _jsx(Button, { disabled: stepIndex === 0, onClick: onBack, size: "sm", type: "button", variant: "outline", children: "Back" }), _jsx(CoachPrimaryAction, { advanceType: advanceType, coachState: coachState, isLast: stepIndex === stepCount - 1, onNext: onNext, onReturn: onReturn })] })] }) }));
6
+ }
7
+ function CoachStatus({ advanceType, state, }) {
8
+ if (state === "off-route") {
9
+ return _jsx("p", { className: "text-xs text-muted-foreground", children: "This page is outside the walkthrough." });
10
+ }
11
+ if (state === "missing") {
12
+ return (_jsx("p", { className: "text-xs text-muted-foreground", children: "The required control is not available in the current app state." }));
13
+ }
14
+ if (state === "locating") {
15
+ return _jsx("p", { className: "text-xs text-muted-foreground", children: "Locating the next control\u2026" });
16
+ }
17
+ if (state === "restore") {
18
+ return (_jsx("p", { className: "text-xs text-muted-foreground", children: "Use the highlighted control to reopen this step." }));
19
+ }
20
+ if (advanceType !== "manual") {
21
+ return (_jsx("p", { className: "text-xs text-muted-foreground", children: "Use the highlighted control to continue \u00B7 click elsewhere to pause" }));
22
+ }
23
+ return (_jsx("p", { className: "text-xs text-muted-foreground", children: "Next to continue \u00B7 click elsewhere to pause" }));
24
+ }
25
+ function CoachPrimaryAction({ advanceType, coachState, isLast, onNext, onReturn, }) {
26
+ if (coachState === "off-route") {
27
+ return (_jsx(Button, { onClick: onReturn, size: "sm", type: "button", children: "Return to step" }));
28
+ }
29
+ if (advanceType !== "manual" || coachState === "restore")
30
+ return null;
31
+ return (_jsx(Button, { disabled: coachState !== "ready", onClick: onNext, size: "sm", type: "button", children: isLast ? "Finish" : "Next" }));
32
+ }
@@ -0,0 +1,5 @@
1
+ import type { WalkthroughTargetRect } from "../targeting/useWalkthroughTargetRect.js";
2
+ export declare function WalkthroughHighlight({ rect, stepNumber, }: {
3
+ rect: WalkthroughTargetRect;
4
+ stepNumber: number;
5
+ }): import("@types/react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function WalkthroughHighlight({ rect, stepNumber, }) {
3
+ const pad = 6;
4
+ const left = Math.min(window.innerWidth, Math.max(0, rect.left - pad));
5
+ const top = Math.min(window.innerHeight, Math.max(0, rect.top - pad));
6
+ const right = Math.max(left, Math.min(window.innerWidth, rect.left + rect.width + pad));
7
+ const bottom = Math.max(top, Math.min(window.innerHeight, rect.top + rect.height + pad));
8
+ return (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": true, className: "absolute inset-x-0 top-0 bg-foreground/40", style: { height: top } }), _jsx("div", { "aria-hidden": true, className: "absolute bottom-0 inset-x-0 bg-foreground/40", style: { top: bottom } }), _jsx("div", { "aria-hidden": true, className: "absolute bg-foreground/40", style: { bottom: window.innerHeight - bottom, left: 0, top, width: left } }), _jsx("div", { "aria-hidden": true, className: "absolute bg-foreground/40", style: { bottom: window.innerHeight - bottom, left: right, right: 0, top } }), _jsx("div", { "aria-hidden": true, className: "absolute rounded-md ring-2 ring-foreground", style: { height: bottom - top, left, top, width: right - left } }), _jsx("div", { "aria-hidden": true, className: "absolute z-10 grid size-7 place-items-center rounded-full bg-foreground font-mono text-xs text-background", style: { left: Math.max(8, left - 10), top: Math.max(8, top - 10) }, children: stepNumber })] }));
9
+ }
@@ -0,0 +1 @@
1
+ export declare function WalkthroughOverlay(): import("@types/react").JSX.Element | null;
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { listenForWalkthroughEvent, listenForWalkthroughTargetClick, } from "../runtime/walkthroughEvents.js";
5
+ import { walkthroughHrefMatches } from "../runtime/walkthroughLocation.js";
6
+ import { useWalkthrough } from "../runtime/useWalkthrough.js";
7
+ import { listenForOutsideWalkthroughInteraction } from "../targeting/listenForOutsideWalkthroughInteraction.js";
8
+ import { useWalkthroughTargetRect, } from "../targeting/useWalkthroughTargetRect.js";
9
+ import { WalkthroughCoach } from "./WalkthroughCoach.js";
10
+ import { WalkthroughHighlight } from "./WalkthroughHighlight.js";
11
+ import { useWalkthroughCoachPosition } from "./useWalkthroughCoachPosition.js";
12
+ export function WalkthroughOverlay() {
13
+ const walkthrough = useWalkthrough();
14
+ if (walkthrough.progress?.status !== "active" ||
15
+ !walkthrough.selectedWalkthrough ||
16
+ !walkthrough.currentStep) {
17
+ return null;
18
+ }
19
+ return (_jsx(WalkthroughStepOverlay, { advance: walkthrough.advance, currentHref: walkthrough.currentHref, flowTitle: walkthrough.selectedWalkthrough.title, goBack: walkthrough.goBack, navigateToCurrentStep: walkthrough.navigateToCurrentStep, pause: walkthrough.pauseWalkthrough, step: walkthrough.currentStep, stepCount: walkthrough.selectedWalkthrough.steps.length, stepIndex: walkthrough.progress.stepIndex }, `${walkthrough.selectedWalkthrough.id}:${walkthrough.currentStep.id}`));
20
+ }
21
+ function WalkthroughStepOverlay({ advance, currentHref, flowTitle, goBack, navigateToCurrentStep, pause, step, stepCount, stepIndex, }) {
22
+ const onStepRoute = walkthroughHrefMatches(currentHref, step.href);
23
+ const targetObservation = useWalkthroughTargetRect(onStepRoute ? step.target : null);
24
+ const restoreObservation = useWalkthroughTargetRect(onStepRoute && targetObservation.status === "missing" ? (step.restoreTarget ?? null) : null);
25
+ const advanceRef = useLatestValue(advance);
26
+ const onStepRouteRef = useLatestValue(onStepRoute);
27
+ const coachRef = useRef(null);
28
+ const coachState = resolveCoachState(onStepRoute, Boolean(step.restoreTarget), targetObservation, restoreObservation);
29
+ const observation = coachState === "restore" ? restoreObservation : targetObservation;
30
+ const coachPosition = useWalkthroughCoachPosition(observation, coachRef);
31
+ useEffect(() => listenForStepCompletion(step, () => onStepRouteRef.current, () => advanceRef.current()), [advanceRef, onStepRouteRef, step]);
32
+ useEffect(() => listenForOutsideInteractionOnStepRoute(onStepRoute, step, pause), [onStepRoute, pause, step]);
33
+ const rect = observation.status === "found" && onStepRoute ? observation.rect : null;
34
+ return createPortal(_jsxs("div", { "aria-live": "polite", className: "pointer-events-none fixed inset-0 z-[100]", children: [rect ? _jsx(WalkthroughHighlight, { rect: rect, stepNumber: stepIndex + 1 }) : null, _jsx("div", { className: "pointer-events-auto absolute w-[min(24rem,calc(100%-2rem))]", "data-walkthrough-coach": "", ref: coachRef, style: coachPosition && rect
35
+ ? { left: coachPosition.left, top: coachPosition.top }
36
+ : { bottom: 16, right: 16 }, children: _jsx(WalkthroughCoach, { advanceType: step.advance.type, coachState: coachState, flowTitle: flowTitle, onBack: goBack, onNext: advance, onPause: pause, onReturn: navigateToCurrentStep, step: step, stepCount: stepCount, stepIndex: stepIndex }) })] }), document.body);
37
+ }
38
+ function listenForOutsideInteractionOnStepRoute(onStepRoute, step, pause) {
39
+ return onStepRoute ? listenForOutsideWalkthroughInteraction(step, pause) : undefined;
40
+ }
41
+ function useLatestValue(value) {
42
+ const valueRef = useRef(value);
43
+ useLayoutEffect(() => {
44
+ valueRef.current = value;
45
+ }, [value]);
46
+ return valueRef;
47
+ }
48
+ function listenForStepCompletion(step, canComplete, advance) {
49
+ if (step.advance.type === "manual")
50
+ return () => undefined;
51
+ if (step.advance.type === "target-click") {
52
+ return listenForWalkthroughTargetClick(step.target, canComplete, advance);
53
+ }
54
+ return listenForWalkthroughEvent(step.advance.event, canComplete, advance);
55
+ }
56
+ function resolveCoachState(onStepRoute, canRestore, targetObservation, restoreObservation) {
57
+ if (!onStepRoute)
58
+ return "off-route";
59
+ if (targetObservation.status === "locating")
60
+ return "locating";
61
+ if (targetObservation.status === "found")
62
+ return "ready";
63
+ if (!canRestore)
64
+ return "missing";
65
+ if (restoreObservation.status === "found")
66
+ return "restore";
67
+ if (restoreObservation.status === "locating")
68
+ return "locating";
69
+ return "missing";
70
+ }
@@ -0,0 +1,11 @@
1
+ import type { WalkthroughTargetRect } from "../targeting/useWalkthroughTargetRect.js";
2
+ export type WalkthroughCoachPosition = {
3
+ left: number;
4
+ top: number;
5
+ };
6
+ type Size = {
7
+ height: number;
8
+ width: number;
9
+ };
10
+ export declare function positionWalkthroughCoach(target: WalkthroughTargetRect, coach: Size, viewport: Size): WalkthroughCoachPosition;
11
+ export {};
@@ -0,0 +1,24 @@
1
+ const GAP = 16;
2
+ const VIEWPORT_MARGIN = 16;
3
+ export function positionWalkthroughCoach(target, coach, viewport) {
4
+ const candidates = [
5
+ { left: target.left + target.width + GAP, top: target.top },
6
+ { left: target.left - coach.width - GAP, top: target.top },
7
+ { left: target.left, top: target.top + target.height + GAP },
8
+ { left: target.left, top: target.top - coach.height - GAP },
9
+ ];
10
+ const fitting = candidates.find((candidate) => fits(candidate, coach, viewport));
11
+ return clamp(fitting ?? candidates[2], coach, viewport);
12
+ }
13
+ function fits(position, coach, viewport) {
14
+ return (position.left >= VIEWPORT_MARGIN &&
15
+ position.top >= VIEWPORT_MARGIN &&
16
+ position.left + coach.width <= viewport.width - VIEWPORT_MARGIN &&
17
+ position.top + coach.height <= viewport.height - VIEWPORT_MARGIN);
18
+ }
19
+ function clamp(position, coach, viewport) {
20
+ return {
21
+ left: Math.max(VIEWPORT_MARGIN, Math.min(position.left, viewport.width - coach.width - VIEWPORT_MARGIN)),
22
+ top: Math.max(VIEWPORT_MARGIN, Math.min(position.top, viewport.height - coach.height - VIEWPORT_MARGIN)),
23
+ };
24
+ }
@@ -0,0 +1,4 @@
1
+ import { type RefObject } from "react";
2
+ import { type WalkthroughCoachPosition } from "./positionWalkthroughCoach.js";
3
+ import type { WalkthroughTargetObservation } from "../targeting/useWalkthroughTargetRect.js";
4
+ export declare function useWalkthroughCoachPosition(observation: WalkthroughTargetObservation, coachRef: RefObject<HTMLDivElement | null>): WalkthroughCoachPosition | null;
@@ -0,0 +1,17 @@
1
+ import { useLayoutEffect, useState } from "react";
2
+ import { positionWalkthroughCoach } from "./positionWalkthroughCoach.js";
3
+ const FALLBACK_COACH_SIZE = { height: 232, width: 384 };
4
+ export function useWalkthroughCoachPosition(observation, coachRef) {
5
+ const [position, setPosition] = useState(null);
6
+ useLayoutEffect(() => {
7
+ if (observation.status !== "found")
8
+ return;
9
+ const box = coachRef.current?.getBoundingClientRect();
10
+ const coach = box ? { height: box.height, width: box.width } : FALLBACK_COACH_SIZE;
11
+ setPosition(positionWalkthroughCoach(observation.rect, coach, {
12
+ height: window.innerHeight,
13
+ width: window.innerWidth,
14
+ }));
15
+ }, [coachRef, observation]);
16
+ return position;
17
+ }
@@ -0,0 +1,20 @@
1
+ import type { WalkthroughDefinition, WalkthroughProgress, WalkthroughStep } from "./walkthroughTypes.js";
2
+ export type WalkthroughContextValue = {
3
+ advance: () => void;
4
+ clearWalkthrough: () => void;
5
+ currentHref: string;
6
+ currentStep: WalkthroughStep | null;
7
+ emitEvent: (event: string) => void;
8
+ goBack: () => void;
9
+ navigateToCurrentStep: () => void;
10
+ pauseWalkthrough: () => void;
11
+ progress: WalkthroughProgress | null;
12
+ resumeWalkthrough: () => void;
13
+ selectedWalkthrough: WalkthroughDefinition | null;
14
+ startWalkthrough: (walkthroughId: string) => Promise<void>;
15
+ targetProps: (target: string) => {
16
+ "data-walkthrough-target": string;
17
+ };
18
+ walkthroughs: readonly WalkthroughDefinition[];
19
+ };
20
+ export declare const WalkthroughContext: import("@types/react").Context<WalkthroughContextValue | undefined>;
@@ -0,0 +1,2 @@
1
+ import { createContext } from "react";
2
+ export const WalkthroughContext = createContext(undefined);
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from "react";
2
+ import type { WalkthroughDefinition, WalkthroughFreshStartHandlers } from "./walkthroughTypes.js";
3
+ export type WalkthroughProviderProps = {
4
+ children: ReactNode;
5
+ currentHref: string;
6
+ freshStartHandlers: WalkthroughFreshStartHandlers;
7
+ navigate: (href: string) => unknown;
8
+ readCurrentHref: () => string;
9
+ storageKey: string;
10
+ walkthroughs: readonly WalkthroughDefinition[];
11
+ };
12
+ export declare function WalkthroughProvider({ children, currentHref, freshStartHandlers, navigate, readCurrentHref, storageKey, walkthroughs, }: WalkthroughProviderProps): import("@types/react").JSX.Element;
@@ -0,0 +1,190 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { WalkthroughContext } from "./WalkthroughContextDefinition.js";
4
+ import { emitWalkthroughEvent } from "./walkthroughEvents.js";
5
+ import { walkthroughHrefForNavigation, walkthroughHrefMatches } from "./walkthroughLocation.js";
6
+ import { readWalkthroughProgress, writeWalkthroughProgress } from "./walkthroughStorage.js";
7
+ import { WalkthroughOverlay } from "../presentation/WalkthroughOverlay.js";
8
+ import { walkthroughTargetProps } from "../targeting/walkthroughTarget.js";
9
+ export function WalkthroughProvider({ children, currentHref, freshStartHandlers, navigate, readCurrentHref, storageKey, walkthroughs, }) {
10
+ const startRequests = useMemo(() => createWalkthroughStartRequests(), []);
11
+ const [progress, setProgress] = useState(() => readInitialProgress(storageKey, walkthroughs));
12
+ useEffect(() => () => startRequests.cancel(), [startRequests]);
13
+ useEffect(() => {
14
+ writeWalkthroughProgress(storageKey, progress);
15
+ }, [progress, storageKey]);
16
+ const selectedWalkthrough = walkthroughById(walkthroughs, progress?.walkthroughId);
17
+ const currentStep = selectedWalkthrough
18
+ ? stepAt(selectedWalkthrough, progress?.stepIndex ?? -1)
19
+ : null;
20
+ const controller = useMemo(() => ({
21
+ freshStartHandlers,
22
+ navigate,
23
+ progress,
24
+ readCurrentHref,
25
+ selectedWalkthrough,
26
+ setProgress,
27
+ startRequests,
28
+ walkthroughs,
29
+ }), [
30
+ freshStartHandlers,
31
+ navigate,
32
+ progress,
33
+ readCurrentHref,
34
+ selectedWalkthrough,
35
+ startRequests,
36
+ walkthroughs,
37
+ ]);
38
+ const value = useMemo(() => buildContextValue(controller, currentHref, currentStep), [controller, currentHref, currentStep]);
39
+ return (_jsxs(WalkthroughContext.Provider, { value: value, children: [children, _jsx(WalkthroughOverlay, {})] }));
40
+ }
41
+ function buildContextValue(controller, currentHref, currentStep) {
42
+ return {
43
+ advance: () => {
44
+ advanceWalkthrough(controller);
45
+ },
46
+ clearWalkthrough: () => {
47
+ controller.startRequests.cancel();
48
+ controller.setProgress(null);
49
+ },
50
+ currentHref,
51
+ currentStep,
52
+ emitEvent: emitWalkthroughEvent,
53
+ goBack: () => {
54
+ goBack(controller);
55
+ },
56
+ navigateToCurrentStep: () => {
57
+ navigateToCurrentStep(controller);
58
+ },
59
+ pauseWalkthrough: () => {
60
+ pauseWalkthrough(controller);
61
+ },
62
+ progress: controller.progress,
63
+ resumeWalkthrough: () => {
64
+ resumeWalkthrough(controller);
65
+ },
66
+ selectedWalkthrough: controller.selectedWalkthrough,
67
+ startWalkthrough: (walkthroughId) => startWalkthrough(controller, walkthroughId),
68
+ targetProps: walkthroughTargetProps,
69
+ walkthroughs: controller.walkthroughs,
70
+ };
71
+ }
72
+ async function startWalkthrough(controller, walkthroughId) {
73
+ const walkthrough = walkthroughById(controller.walkthroughs, walkthroughId);
74
+ if (!walkthrough)
75
+ throw new Error(`Unknown walkthrough: ${walkthroughId}`);
76
+ if (walkthrough.steps.length === 0)
77
+ throw new Error(`Walkthrough has no steps: ${walkthroughId}`);
78
+ const freshStart = walkthrough.freshStart
79
+ ? controller.freshStartHandlers[walkthrough.freshStart]
80
+ : null;
81
+ if (walkthrough.freshStart && !freshStart) {
82
+ throw new Error(`Missing fresh-start handler: ${walkthrough.freshStart}`);
83
+ }
84
+ const startRequest = controller.startRequests.begin();
85
+ controller.setProgress(null);
86
+ if (freshStart) {
87
+ try {
88
+ await freshStart();
89
+ }
90
+ catch (error) {
91
+ controller.startRequests.finishIfLatest(startRequest);
92
+ throw error;
93
+ }
94
+ }
95
+ if (!controller.startRequests.finishIfLatest(startRequest))
96
+ return;
97
+ controller.setProgress({
98
+ status: "active",
99
+ stepIndex: 0,
100
+ version: walkthrough.version,
101
+ walkthroughId,
102
+ });
103
+ navigateToStep(controller, walkthrough, 0);
104
+ }
105
+ function createWalkthroughStartRequests() {
106
+ let latestRequest = null;
107
+ return {
108
+ begin: () => {
109
+ const request = {};
110
+ latestRequest = request;
111
+ return request;
112
+ },
113
+ cancel: () => {
114
+ latestRequest = null;
115
+ },
116
+ finishIfLatest: (request) => {
117
+ if (latestRequest !== request)
118
+ return false;
119
+ latestRequest = null;
120
+ return true;
121
+ },
122
+ };
123
+ }
124
+ function advanceWalkthrough(controller) {
125
+ const { progress, selectedWalkthrough } = controller;
126
+ if (!progress || progress.status !== "active" || !selectedWalkthrough)
127
+ return;
128
+ const nextStepIndex = progress.stepIndex + 1;
129
+ if (nextStepIndex >= selectedWalkthrough.steps.length) {
130
+ controller.setProgress({ ...progress, status: "completed" });
131
+ return;
132
+ }
133
+ controller.setProgress({ ...progress, stepIndex: nextStepIndex });
134
+ navigateToStep(controller, selectedWalkthrough, nextStepIndex);
135
+ }
136
+ function goBack(controller) {
137
+ const { progress, selectedWalkthrough } = controller;
138
+ if (!progress || progress.status !== "active" || !selectedWalkthrough)
139
+ return;
140
+ const previousStepIndex = progress.stepIndex - 1;
141
+ if (previousStepIndex < 0)
142
+ return;
143
+ controller.setProgress({ ...progress, stepIndex: previousStepIndex });
144
+ navigateToStep(controller, selectedWalkthrough, previousStepIndex);
145
+ }
146
+ function pauseWalkthrough(controller) {
147
+ const { progress } = controller;
148
+ if (progress?.status === "active") {
149
+ controller.setProgress({ ...progress, status: "paused" });
150
+ }
151
+ }
152
+ function resumeWalkthrough(controller) {
153
+ const { progress, selectedWalkthrough } = controller;
154
+ if (progress?.status !== "paused" || !selectedWalkthrough)
155
+ return;
156
+ controller.setProgress({ ...progress, status: "active" });
157
+ navigateToStep(controller, selectedWalkthrough, progress.stepIndex);
158
+ }
159
+ function navigateToCurrentStep(controller) {
160
+ if (!controller.progress || !controller.selectedWalkthrough)
161
+ return;
162
+ navigateToStep(controller, controller.selectedWalkthrough, controller.progress.stepIndex);
163
+ }
164
+ function navigateToStep(controller, walkthrough, stepIndex) {
165
+ const step = stepAt(walkthrough, stepIndex);
166
+ const currentHref = controller.readCurrentHref();
167
+ if (step && !walkthroughHrefMatches(currentHref, step.href)) {
168
+ controller.navigate(walkthroughHrefForNavigation(currentHref, step.href));
169
+ }
170
+ }
171
+ function stepAt(walkthrough, stepIndex) {
172
+ return walkthrough.steps.find((_step, index) => index === stepIndex) ?? null;
173
+ }
174
+ function readInitialProgress(storageKey, walkthroughs) {
175
+ const stored = readWalkthroughProgress(storageKey);
176
+ if (!stored)
177
+ return null;
178
+ const walkthrough = walkthroughById(walkthroughs, stored.walkthroughId);
179
+ if (!walkthrough ||
180
+ walkthrough.version !== stored.version ||
181
+ stored.stepIndex >= walkthrough.steps.length) {
182
+ return null;
183
+ }
184
+ return stored.status === "active" ? { ...stored, status: "paused" } : stored;
185
+ }
186
+ function walkthroughById(walkthroughs, walkthroughId) {
187
+ if (!walkthroughId)
188
+ return null;
189
+ return walkthroughs.find((walkthrough) => walkthrough.id === walkthroughId) ?? null;
190
+ }
@@ -0,0 +1 @@
1
+ export declare function useWalkthrough(): import("./WalkthroughContextDefinition.js").WalkthroughContextValue;
@@ -0,0 +1,8 @@
1
+ import { useContext } from "react";
2
+ import { WalkthroughContext } from "./WalkthroughContextDefinition.js";
3
+ export function useWalkthrough() {
4
+ const context = useContext(WalkthroughContext);
5
+ if (!context)
6
+ throw new Error("useWalkthrough must be used within WalkthroughProvider");
7
+ return context;
8
+ }
@@ -0,0 +1,3 @@
1
+ export declare function emitWalkthroughEvent(event: string): void;
2
+ export declare function listenForWalkthroughEvent(event: string, canComplete: () => boolean, onComplete: () => void): () => void;
3
+ export declare function listenForWalkthroughTargetClick(target: string, canComplete: () => boolean, onComplete: () => void): () => void;
@@ -0,0 +1,55 @@
1
+ import { isEnabledWalkthroughTarget, WALKTHROUGH_TARGET_ATTRIBUTE, } from "../targeting/walkthroughTarget.js";
2
+ const WALKTHROUGH_EVENT_PREFIX = "ciridae:walkthrough:";
3
+ export function emitWalkthroughEvent(event) {
4
+ window.dispatchEvent(new Event(walkthroughEventType(event)));
5
+ }
6
+ export function listenForWalkthroughEvent(event, canComplete, onComplete) {
7
+ const eventType = walkthroughEventType(event);
8
+ const eventListener = () => {
9
+ if (canComplete())
10
+ onComplete();
11
+ };
12
+ window.addEventListener(eventType, eventListener);
13
+ return () => {
14
+ window.removeEventListener(eventType, eventListener);
15
+ };
16
+ }
17
+ export function listenForWalkthroughTargetClick(target, canComplete, onComplete) {
18
+ let completionScheduled = false;
19
+ let completed = false;
20
+ const clickListener = (event) => {
21
+ if (completionScheduled || completed)
22
+ return;
23
+ if (!enabledTargetForEvent(event, target))
24
+ return;
25
+ if (!canComplete())
26
+ return;
27
+ completionScheduled = true;
28
+ queueMicrotask(() => {
29
+ completionScheduled = false;
30
+ if (completed)
31
+ return;
32
+ completed = true;
33
+ onComplete();
34
+ });
35
+ };
36
+ document.addEventListener("click", clickListener, true);
37
+ return () => {
38
+ document.removeEventListener("click", clickListener, true);
39
+ };
40
+ }
41
+ function enabledTargetForEvent(event, target) {
42
+ const element = event.target;
43
+ if (!(element instanceof Element))
44
+ return null;
45
+ const targetElement = element.closest(`[${WALKTHROUGH_TARGET_ATTRIBUTE}]`);
46
+ if (targetElement?.getAttribute(WALKTHROUGH_TARGET_ATTRIBUTE) !== target)
47
+ return null;
48
+ if (!isEnabledWalkthroughTarget(targetElement))
49
+ return null;
50
+ const disabledControl = element.closest(":disabled, [aria-disabled='true']");
51
+ return disabledControl && targetElement.contains(disabledControl) ? null : targetElement;
52
+ }
53
+ function walkthroughEventType(event) {
54
+ return `${WALKTHROUGH_EVENT_PREFIX}${event}`;
55
+ }
@@ -0,0 +1,2 @@
1
+ export declare function walkthroughHrefMatches(currentHref: string, stepHref: string): boolean;
2
+ export declare function walkthroughHrefForNavigation(currentHref: string, stepHref: string): string;
@@ -0,0 +1,27 @@
1
+ export function walkthroughHrefMatches(currentHref, stepHref) {
2
+ const current = new URL(currentHref, window.location.origin);
3
+ const step = new URL(stepHref, window.location.origin);
4
+ if (normalizePathname(current.pathname) !== normalizePathname(step.pathname))
5
+ return false;
6
+ if (step.hash && current.hash !== step.hash)
7
+ return false;
8
+ for (const [key, value] of step.searchParams) {
9
+ if (current.searchParams.get(key) !== value)
10
+ return false;
11
+ }
12
+ return true;
13
+ }
14
+ export function walkthroughHrefForNavigation(currentHref, stepHref) {
15
+ const current = new URL(currentHref, window.location.origin);
16
+ const step = new URL(stepHref, window.location.origin);
17
+ if (normalizePathname(current.pathname) !== normalizePathname(step.pathname))
18
+ return stepHref;
19
+ for (const [key, value] of step.searchParams)
20
+ current.searchParams.set(key, value);
21
+ if (step.hash)
22
+ current.hash = step.hash;
23
+ return `${current.pathname}${current.search}${current.hash}`;
24
+ }
25
+ function normalizePathname(pathname) {
26
+ return pathname.length > 1 ? pathname.replace(/\/$/, "") : pathname;
27
+ }
@@ -0,0 +1,3 @@
1
+ import type { WalkthroughProgress } from "./walkthroughTypes.js";
2
+ export declare function readWalkthroughProgress(storageKey: string): WalkthroughProgress | null;
3
+ export declare function writeWalkthroughProgress(storageKey: string, progress: WalkthroughProgress | null): void;
@@ -0,0 +1,54 @@
1
+ import { readStoredValue, removeStoredValue, writeStoredValue } from "../../../lib/browser-storage.js";
2
+ export function readWalkthroughProgress(storageKey) {
3
+ const raw = readStoredValue(storageKey);
4
+ if (!raw)
5
+ return null;
6
+ try {
7
+ const parsed = JSON.parse(raw);
8
+ return isWalkthroughProgress(parsed) ? parsed : null;
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export function writeWalkthroughProgress(storageKey, progress) {
15
+ if (!progress) {
16
+ removeStoredValue(storageKey);
17
+ return;
18
+ }
19
+ writeStoredValue(storageKey, JSON.stringify(progress));
20
+ }
21
+ function isWalkthroughProgress(value) {
22
+ if (!value || typeof value !== "object")
23
+ return false;
24
+ if (!hasWalkthroughProgressFields(value))
25
+ return false;
26
+ if (!isWalkthroughStatus(value.status))
27
+ return false;
28
+ if (!isNonnegativeInteger(value.stepIndex))
29
+ return false;
30
+ if (!isPositiveInteger(value.version))
31
+ return false;
32
+ return isNonemptyString(value.walkthroughId);
33
+ }
34
+ function isWalkthroughStatus(value) {
35
+ return value === "active" || value === "completed" || value === "paused";
36
+ }
37
+ function hasWalkthroughProgressFields(value) {
38
+ if (!("status" in value))
39
+ return false;
40
+ if (!("stepIndex" in value))
41
+ return false;
42
+ if (!("version" in value))
43
+ return false;
44
+ return "walkthroughId" in value;
45
+ }
46
+ function isNonnegativeInteger(value) {
47
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
48
+ }
49
+ function isPositiveInteger(value) {
50
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
51
+ }
52
+ function isNonemptyString(value) {
53
+ return typeof value === "string" && value.length > 0;
54
+ }
@@ -0,0 +1,32 @@
1
+ export type WalkthroughAdvance = {
2
+ type: "manual";
3
+ } | {
4
+ type: "target-click";
5
+ } | {
6
+ event: string;
7
+ type: "event";
8
+ };
9
+ export type WalkthroughStep = {
10
+ advance: WalkthroughAdvance;
11
+ body: string;
12
+ href: string;
13
+ id: string;
14
+ restoreTarget?: string;
15
+ target: string;
16
+ title: string;
17
+ };
18
+ export type WalkthroughDefinition = {
19
+ description: string;
20
+ freshStart?: string;
21
+ id: string;
22
+ steps: readonly WalkthroughStep[];
23
+ title: string;
24
+ version: number;
25
+ };
26
+ export type WalkthroughProgress = {
27
+ status: "active" | "completed" | "paused";
28
+ stepIndex: number;
29
+ version: number;
30
+ walkthroughId: string;
31
+ };
32
+ export type WalkthroughFreshStartHandlers = Readonly<Partial<Record<string, () => Promise<unknown>>>>;
@@ -0,0 +1,2 @@
1
+ import type { WalkthroughStep } from "../runtime/walkthroughTypes.js";
2
+ export declare function listenForOutsideWalkthroughInteraction(step: WalkthroughStep, pause: () => void): () => void;
@@ -0,0 +1,19 @@
1
+ import { WALKTHROUGH_TARGET_ATTRIBUTE } from "./walkthroughTarget.js";
2
+ export function listenForOutsideWalkthroughInteraction(step, pause) {
3
+ const listener = (event) => {
4
+ const element = event.target;
5
+ if (!(element instanceof Element))
6
+ return;
7
+ if (element.closest("[data-walkthrough-coach]"))
8
+ return;
9
+ const walkthroughTarget = element.closest(`[${WALKTHROUGH_TARGET_ATTRIBUTE}]`);
10
+ const targetName = walkthroughTarget?.getAttribute(WALKTHROUGH_TARGET_ATTRIBUTE);
11
+ if (walkthroughTarget && (targetName === step.target || targetName === step.restoreTarget))
12
+ return;
13
+ pause();
14
+ };
15
+ document.addEventListener("click", listener, true);
16
+ return () => {
17
+ document.removeEventListener("click", listener, true);
18
+ };
19
+ }
@@ -0,0 +1,15 @@
1
+ export type WalkthroughTargetRect = {
2
+ height: number;
3
+ left: number;
4
+ top: number;
5
+ width: number;
6
+ };
7
+ export type WalkthroughTargetObservation = {
8
+ status: "locating";
9
+ } | {
10
+ status: "missing";
11
+ } | {
12
+ rect: WalkthroughTargetRect;
13
+ status: "found";
14
+ };
15
+ export declare function useWalkthroughTargetRect(target: string | null): WalkthroughTargetObservation;
@@ -0,0 +1,54 @@
1
+ import { useEffect, useState } from "react";
2
+ import { findWalkthroughTarget } from "./walkthroughTarget.js";
3
+ const LOCATING_TARGET_OBSERVATION = { status: "locating" };
4
+ const MISSING_TARGET_OBSERVATION = { status: "missing" };
5
+ export function useWalkthroughTargetRect(target) {
6
+ const [observation, setObservation] = useState(LOCATING_TARGET_OBSERVATION);
7
+ useEffect(() => {
8
+ if (!target) {
9
+ setObservation(LOCATING_TARGET_OBSERVATION);
10
+ return;
11
+ }
12
+ let didScroll = false;
13
+ const measure = (forceUpdate = false) => {
14
+ const element = findWalkthroughTarget(target);
15
+ if (!element) {
16
+ didScroll = false;
17
+ setObservation(MISSING_TARGET_OBSERVATION);
18
+ return;
19
+ }
20
+ if (!didScroll) {
21
+ element.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
22
+ didScroll = true;
23
+ }
24
+ const rect = element.getBoundingClientRect();
25
+ const next = {
26
+ rect: { height: rect.height, left: rect.left, top: rect.top, width: rect.width },
27
+ status: "found",
28
+ };
29
+ setObservation((current) => (!forceUpdate && sameObservation(current, next) ? current : next));
30
+ };
31
+ const handleResize = () => measure(true);
32
+ const handleScroll = () => measure();
33
+ measure();
34
+ const intervalId = window.setInterval(() => measure(), 300);
35
+ window.addEventListener("resize", handleResize);
36
+ window.addEventListener("scroll", handleScroll, true);
37
+ return () => {
38
+ window.clearInterval(intervalId);
39
+ window.removeEventListener("resize", handleResize);
40
+ window.removeEventListener("scroll", handleScroll, true);
41
+ };
42
+ }, [target]);
43
+ return observation;
44
+ }
45
+ function sameObservation(current, next) {
46
+ if (current.status !== next.status)
47
+ return false;
48
+ if (current.status !== "found" || next.status !== "found")
49
+ return true;
50
+ return (current.rect.height === next.rect.height &&
51
+ current.rect.left === next.rect.left &&
52
+ current.rect.top === next.rect.top &&
53
+ current.rect.width === next.rect.width);
54
+ }
@@ -0,0 +1,6 @@
1
+ export declare const WALKTHROUGH_TARGET_ATTRIBUTE = "data-walkthrough-target";
2
+ export declare function walkthroughTargetProps(target: string): {
3
+ "data-walkthrough-target": string;
4
+ };
5
+ export declare function findWalkthroughTarget(target: string): HTMLElement | null;
6
+ export declare function isEnabledWalkthroughTarget(candidate: HTMLElement): boolean;
@@ -0,0 +1,24 @@
1
+ export const WALKTHROUGH_TARGET_ATTRIBUTE = "data-walkthrough-target";
2
+ export function walkthroughTargetProps(target) {
3
+ return { "data-walkthrough-target": target };
4
+ }
5
+ export function findWalkthroughTarget(target) {
6
+ const candidates = document.querySelectorAll(`[${WALKTHROUGH_TARGET_ATTRIBUTE}]`);
7
+ for (let index = candidates.length - 1; index >= 0; index -= 1) {
8
+ const candidate = candidates.item(index);
9
+ if (candidate.getAttribute(WALKTHROUGH_TARGET_ATTRIBUTE) === target &&
10
+ isVisibleWalkthroughTarget(candidate) &&
11
+ isEnabledWalkthroughTarget(candidate)) {
12
+ return candidate;
13
+ }
14
+ }
15
+ return null;
16
+ }
17
+ export function isEnabledWalkthroughTarget(candidate) {
18
+ return !candidate.matches(":disabled, [aria-disabled='true']");
19
+ }
20
+ function isVisibleWalkthroughTarget(candidate) {
21
+ const rect = candidate.getBoundingClientRect();
22
+ const style = window.getComputedStyle(candidate);
23
+ return (rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden");
24
+ }
@@ -0,0 +1,4 @@
1
+ export { WalkthroughProvider, type WalkthroughProviderProps, } from "./walkthrough/runtime/WalkthroughProvider.js";
2
+ export type { WalkthroughContextValue } from "./walkthrough/runtime/WalkthroughContextDefinition.js";
3
+ export { useWalkthrough } from "./walkthrough/runtime/useWalkthrough.js";
4
+ export type { WalkthroughAdvance, WalkthroughDefinition, WalkthroughFreshStartHandlers, WalkthroughProgress, WalkthroughStep, } from "./walkthrough/runtime/walkthroughTypes.js";
@@ -0,0 +1,2 @@
1
+ export { WalkthroughProvider, } from "./walkthrough/runtime/WalkthroughProvider.js";
2
+ export { useWalkthrough } from "./walkthrough/runtime/useWalkthrough.js";
@@ -0,0 +1 @@
1
+ export * from "../components/walkthrough.js";
@@ -0,0 +1 @@
1
+ export * from "../components/walkthrough.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciridae/ui",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "engines": {
@@ -87,7 +87,6 @@
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",
91
90
  "embla-carousel-react": "^8.6.0",
92
91
  "input-otp": "^1.4.2",
93
92
  "lucide-react": "^1.23.0",