@plannotator/ui 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/assets/diff-options.png +0 -0
- package/assets/icon-codex.png +0 -0
- package/assets/look-flat.png +0 -0
- package/assets/look-grid.png +0 -0
- package/assets/review-sections.png +0 -0
- package/assets/review-tree.png +0 -0
- package/assets/workspaces.webp +0 -0
- package/components/AISettingsTab.tsx +145 -0
- package/components/ActionMenu.tsx +101 -0
- package/components/AgentControls.tsx +216 -0
- package/components/AgentsTab.tsx +1294 -0
- package/components/AnnotationPanel.tsx +731 -0
- package/components/AnnotationSidebar.tsx +86 -0
- package/components/AnnotationToolbar.tsx +323 -0
- package/components/AnnotationToolstrip.tsx +355 -0
- package/components/ApproveDropdown.tsx +170 -0
- package/components/AttachmentsButton.tsx +410 -0
- package/components/BlockRenderer.tsx +163 -0
- package/components/BorderTrail.tsx +38 -0
- package/components/CodeFilePicker.tsx +64 -0
- package/components/CodeFilePopout.tsx +602 -0
- package/components/CodePathValidationContext.tsx +14 -0
- package/components/CommentPopover.tsx +512 -0
- package/components/CompletionOverlay.tsx +99 -0
- package/components/ConfirmDialog.tsx +128 -0
- package/components/DocBadges.tsx +231 -0
- package/components/EditorAnnotationCard.tsx +65 -0
- package/components/ExportModal.tsx +565 -0
- package/components/FloatingQuickLabelPicker.tsx +144 -0
- package/components/GitHubIcon.tsx +16 -0
- package/components/GitLabIcon.tsx +30 -0
- package/components/GraphvizBlock.tsx +511 -0
- package/components/ImageAnnotator/Canvas.tsx +118 -0
- package/components/ImageAnnotator/Toolbar.tsx +218 -0
- package/components/ImageAnnotator/index.tsx +272 -0
- package/components/ImageAnnotator/types.ts +39 -0
- package/components/ImageAnnotator/utils.ts +147 -0
- package/components/ImageThumbnail.tsx +126 -0
- package/components/ImportModal.tsx +146 -0
- package/components/InlineMarkdown.tsx +1094 -0
- package/components/KeyboardShortcuts.tsx +208 -0
- package/components/Landing.tsx +496 -0
- package/components/ListItemBody.tsx +62 -0
- package/components/ListMarker.tsx +73 -0
- package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
- package/components/MarkdownEditor.tsx +44 -0
- package/components/MenuVersionSection.tsx +91 -0
- package/components/MermaidBlock.tsx +579 -0
- package/components/ModeToggle.tsx +68 -0
- package/components/OpenInAppButton.tsx +299 -0
- package/components/OverlayScrollArea.tsx +82 -0
- package/components/PermissionModeSetup.tsx +92 -0
- package/components/PinpointOverlay.tsx +99 -0
- package/components/PlanAIAnnouncementDialog.tsx +187 -0
- package/components/PlanHeaderMenu.tsx +299 -0
- package/components/PopoutDialog.tsx +91 -0
- package/components/Popover.tsx +27 -0
- package/components/ProviderIcons.tsx +51 -0
- package/components/PullRequestIcon.tsx +12 -0
- package/components/QuickLabelDropdown.tsx +63 -0
- package/components/RenderedMarkdown.tsx +54 -0
- package/components/RepoIcon.tsx +12 -0
- package/components/ResizeHandle.tsx +86 -0
- package/components/ReviewAgentsIcon.tsx +18 -0
- package/components/SearchableSelect.tsx +157 -0
- package/components/Settings.tsx +2220 -0
- package/components/SparklesIcon.tsx +45 -0
- package/components/StickyHeaderLane.tsx +260 -0
- package/components/TableOfContents.tsx +157 -0
- package/components/TaterSpritePullup.tsx +34 -0
- package/components/TaterSpriteRunning.tsx +54 -0
- package/components/TaterSpriteSitting.tsx +37 -0
- package/components/TextShimmer.tsx +57 -0
- package/components/ThemeProvider.tsx +162 -0
- package/components/ThemeTab.tsx +139 -0
- package/components/ToolbarButtons.tsx +116 -0
- package/components/Tooltip.tsx +45 -0
- package/components/Viewer.tsx +977 -0
- package/components/ai/AIProviderBar.tsx +95 -0
- package/components/ai/DocumentAIChatPanel.tsx +313 -0
- package/components/blocks/AlertBlock.tsx +58 -0
- package/components/blocks/Callout.tsx +60 -0
- package/components/blocks/CodeBlock.tsx +75 -0
- package/components/blocks/HtmlBlock.tsx +122 -0
- package/components/blocks/MathBlock.tsx +36 -0
- package/components/blocks/TableBlock.tsx +146 -0
- package/components/blocks/TablePopout.tsx +279 -0
- package/components/blocks/TableToolbar.tsx +153 -0
- package/components/blocks/proseBody.tsx +102 -0
- package/components/core/button.tsx +44 -0
- package/components/core/textarea.tsx +26 -0
- package/components/diagramLanguages.ts +14 -0
- package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
- package/components/html-viewer/HtmlViewer.tsx +386 -0
- package/components/html-viewer/bridge-script.ts +505 -0
- package/components/html-viewer/index.ts +1 -0
- package/components/html-viewer/useHtmlAnnotation.ts +400 -0
- package/components/icons/AgentIcons.tsx +66 -0
- package/components/icons/AppIcon.tsx +56 -0
- package/components/icons/MessagesIcon.tsx +11 -0
- package/components/icons/ObsidianIcons.tsx +208 -0
- package/components/icons/app/android-studio.svg +369 -0
- package/components/icons/app/antigravity.svg +97 -0
- package/components/icons/app/cursor.svg +16 -0
- package/components/icons/app/file-explorer.svg +20 -0
- package/components/icons/app/finder.png +0 -0
- package/components/icons/app/ghostty.svg +13 -0
- package/components/icons/app/iterm2.svg +13 -0
- package/components/icons/app/powershell.svg +14 -0
- package/components/icons/app/sublime-text.svg +17 -0
- package/components/icons/app/terminal.png +0 -0
- package/components/icons/app/textmate.png +0 -0
- package/components/icons/app/vscode.svg +39 -0
- package/components/icons/app/warp.png +0 -0
- package/components/icons/app/xcode.png +0 -0
- package/components/icons/app/zed-dark.svg +15 -0
- package/components/icons/app/zed.svg +15 -0
- package/components/icons/themeIcons.tsx +47 -0
- package/components/mermaidSvg.ts +33 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
- package/components/plan-diff/PlanDiffBadge.tsx +48 -0
- package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
- package/components/plan-diff/PlanDiffViewer.tsx +214 -0
- package/components/plan-diff/PlanRawDiffView.tsx +102 -0
- package/components/plan-diff/VSCodeIcon.tsx +132 -0
- package/components/settings/HooksTab.tsx +208 -0
- package/components/sidebar/ArchiveBrowser.tsx +98 -0
- package/components/sidebar/CountBadge.tsx +12 -0
- package/components/sidebar/FileBrowser.tsx +498 -0
- package/components/sidebar/MessagesBrowser.tsx +109 -0
- package/components/sidebar/SidebarContainer.tsx +353 -0
- package/components/sidebar/SidebarTabs.tsx +155 -0
- package/components/sidebar/VersionBrowser.tsx +141 -0
- package/components/types.d.ts +7 -0
- package/components/ui/badge.tsx +44 -0
- package/components/ui/button.tsx +83 -0
- package/components/ui/card.tsx +58 -0
- package/components/ui/dialog.tsx +106 -0
- package/components/ui/dropdown-menu.tsx +240 -0
- package/components/ui/state-pill.tsx +45 -0
- package/components/ui/tabs.tsx +46 -0
- package/components/ui/textarea.tsx +25 -0
- package/config/configStore.ts +214 -0
- package/config/index.ts +4 -0
- package/config/reviewView.ts +42 -0
- package/config/settings.ts +310 -0
- package/config/useConfig.ts +20 -0
- package/configure.ts +69 -0
- package/globals.d.ts +14 -0
- package/hooks/pfm/useCodeFilePopout.ts +111 -0
- package/hooks/useAIChat.ts +532 -0
- package/hooks/useAIProviderConfig.ts +115 -0
- package/hooks/useActiveSection.ts +78 -0
- package/hooks/useAgentJobs.ts +306 -0
- package/hooks/useAgentSettings.ts +579 -0
- package/hooks/useAgents.ts +90 -0
- package/hooks/useAnnotationDraft.ts +523 -0
- package/hooks/useAnnotationHighlighter.ts +1170 -0
- package/hooks/useArchive.ts +171 -0
- package/hooks/useAutoClose.ts +104 -0
- package/hooks/useCodeAnnotationDraft.ts +194 -0
- package/hooks/useCodeFilePopout.ts +1 -0
- package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
- package/hooks/useDraggable.ts +108 -0
- package/hooks/useEditorAnnotations.ts +64 -0
- package/hooks/useExternalAnnotationHighlights.ts +105 -0
- package/hooks/useExternalAnnotations.ts +268 -0
- package/hooks/useFileBrowser.ts +402 -0
- package/hooks/useInputMethodSwitch.ts +89 -0
- package/hooks/useIsMobile.ts +17 -0
- package/hooks/useLinkedDoc.ts +494 -0
- package/hooks/useOverlayViewport.ts +36 -0
- package/hooks/usePinpoint.ts +182 -0
- package/hooks/usePlanDiff.ts +176 -0
- package/hooks/usePrintMode.ts +27 -0
- package/hooks/useResizablePanel.ts +173 -0
- package/hooks/useScrollViewport.ts +38 -0
- package/hooks/useSharing.ts +441 -0
- package/hooks/useSidebar.ts +55 -0
- package/hooks/useUpdateCheck.ts +132 -0
- package/hooks/useValidatedCodePaths.ts +94 -0
- package/icons/GitUser.tsx +17 -0
- package/lib/utils.ts +11 -0
- package/package.json +117 -0
- package/plannotator.webp +0 -0
- package/print.css +427 -0
- package/shortcuts/code-review/ai.shortcuts.ts +25 -0
- package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
- package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
- package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
- package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
- package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
- package/shortcuts/core.ts +399 -0
- package/shortcuts/index.ts +21 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
- package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
- package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
- package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
- package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
- package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
- package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
- package/shortcuts/runtime.ts +259 -0
- package/sprite_package_additional/index.html +34 -0
- package/sprite_package_additional/sprite.png +0 -0
- package/sprite_package_new/index.html +34 -0
- package/sprite_package_new/sprite.png +0 -0
- package/sprite_package_pulluphang/index.html +34 -0
- package/sprite_package_pulluphang/sprite.png +0 -0
- package/styles.css +1 -0
- package/theme.css +893 -0
- package/themes/adwaita.css +62 -0
- package/themes/andromeeda.css +66 -0
- package/themes/aurora-x.css +66 -0
- package/themes/ayu-dark.css +66 -0
- package/themes/caffeine.css +113 -0
- package/themes/catppuccin.css +62 -0
- package/themes/claude-plus.css +60 -0
- package/themes/cursor-hc.css +34 -0
- package/themes/cursor-midnight.css +34 -0
- package/themes/cursor.css +62 -0
- package/themes/dark-plus.css +66 -0
- package/themes/doom-64.css +109 -0
- package/themes/dracula.css +33 -0
- package/themes/everforest-hard.css +62 -0
- package/themes/everforest-soft.css +62 -0
- package/themes/everforest.css +62 -0
- package/themes/github.css +66 -0
- package/themes/gruvbox.css +62 -0
- package/themes/houston.css +66 -0
- package/themes/kanagawa-dragon.css +34 -0
- package/themes/kanagawa-lotus.css +34 -0
- package/themes/kanagawa-wave.css +34 -0
- package/themes/laserwave.css +66 -0
- package/themes/material.css +62 -0
- package/themes/min.css +66 -0
- package/themes/monokai-pro.css +34 -0
- package/themes/neutral.css +59 -0
- package/themes/night-owl.css +66 -0
- package/themes/nord.css +66 -0
- package/themes/one-dark-pro.css +66 -0
- package/themes/one-light.css +66 -0
- package/themes/paulmillr.css +34 -0
- package/themes/plannotator.css +60 -0
- package/themes/plastic.css +66 -0
- package/themes/poimandres.css +66 -0
- package/themes/quantum-rose.css +109 -0
- package/themes/red.css +66 -0
- package/themes/rose-pine.css +62 -0
- package/themes/simple.css +124 -0
- package/themes/slack.css +66 -0
- package/themes/snazzy-light.css +66 -0
- package/themes/soft-pop.css +60 -0
- package/themes/solar-dusk.css +109 -0
- package/themes/solarized.css +66 -0
- package/themes/synthwave-84.css +34 -0
- package/themes/terminal.css +62 -0
- package/themes/tinacious.css +57 -0
- package/themes/tokyo-night.css +62 -0
- package/themes/vesper.css +62 -0
- package/themes/vitesse-black.css +66 -0
- package/themes/vitesse.css +62 -0
- package/types.ts +264 -0
- package/utils/agentSwitch.ts +71 -0
- package/utils/aiChatFormat.ts +33 -0
- package/utils/aiPrompt.ts +29 -0
- package/utils/aiProvider.ts +217 -0
- package/utils/anchors.ts +11 -0
- package/utils/annotateAgentTerminal.ts +29 -0
- package/utils/annotationHelpers.ts +101 -0
- package/utils/bear.ts +59 -0
- package/utils/blockTargeting.ts +240 -0
- package/utils/callback.ts +99 -0
- package/utils/commentContent.ts +8 -0
- package/utils/defaultNotesApp.ts +20 -0
- package/utils/diffFonts.ts +33 -0
- package/utils/editorMode.ts +31 -0
- package/utils/fileBrowser.ts +40 -0
- package/utils/generateId.ts +7 -0
- package/utils/generateIdentity.ts +29 -0
- package/utils/identity.ts +112 -0
- package/utils/inlineTransforms.ts +41 -0
- package/utils/inputMethod.ts +17 -0
- package/utils/lookAndFeelAnnouncement.ts +18 -0
- package/utils/obsidian.ts +201 -0
- package/utils/octarine.ts +53 -0
- package/utils/parser.ts +991 -0
- package/utils/permissionMode.ts +77 -0
- package/utils/planAIAnnouncement.ts +17 -0
- package/utils/planAgentInstructions.ts +137 -0
- package/utils/planDiffEngine.ts +590 -0
- package/utils/planSave.ts +49 -0
- package/utils/platform.ts +11 -0
- package/utils/quickLabels.ts +77 -0
- package/utils/reviewAgentInstructions.ts +182 -0
- package/utils/sanitizeHtml.ts +45 -0
- package/utils/sharing.ts +350 -0
- package/utils/slugify.ts +37 -0
- package/utils/storage.ts +141 -0
- package/utils/themeRegistry.ts +567 -0
- package/utils/uiPreferences.ts +34 -0
- package/utils/upload.ts +56 -0
- package/utils/wideMode.ts +48 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import type { ImageAttachment } from '../types';
|
|
4
|
+
import { AttachmentsButton } from './AttachmentsButton';
|
|
5
|
+
import { submitHint } from '../utils/platform';
|
|
6
|
+
import { useDraggable } from '../hooks/useDraggable';
|
|
7
|
+
import { SparklesIcon } from './SparklesIcon';
|
|
8
|
+
import { hasUnsavedCommentContent } from '../utils/commentContent';
|
|
9
|
+
|
|
10
|
+
export interface CommentAskAIContext {
|
|
11
|
+
kind: 'general' | 'selection';
|
|
12
|
+
label?: string;
|
|
13
|
+
text?: string;
|
|
14
|
+
sourcePath?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type CommentAskAIHandler = (
|
|
18
|
+
question: string,
|
|
19
|
+
context: CommentAskAIContext,
|
|
20
|
+
) => boolean | void | Promise<boolean | void>;
|
|
21
|
+
|
|
22
|
+
interface CommentPopoverProps {
|
|
23
|
+
/** Element to anchor the popover near (re-reads position on scroll) */
|
|
24
|
+
anchorEl?: HTMLElement;
|
|
25
|
+
/** Static viewport rect to anchor near when no stable DOM element exists */
|
|
26
|
+
anchorRect?: DOMRect;
|
|
27
|
+
/** Truncated selected text shown in header, or empty for global */
|
|
28
|
+
contextText: string;
|
|
29
|
+
/** Whether this is a global comment */
|
|
30
|
+
isGlobal: boolean;
|
|
31
|
+
/** Pre-filled text (for type-to-comment) */
|
|
32
|
+
initialText?: string;
|
|
33
|
+
/** Called on submit with comment text and optional images */
|
|
34
|
+
onSubmit: (text: string, images?: ImageAttachment[]) => void;
|
|
35
|
+
/** Optional live draft observer for submit paths outside the popover. */
|
|
36
|
+
onDraftChange?: (text: string, images?: ImageAttachment[]) => void;
|
|
37
|
+
/** Called when popover is closed/cancelled */
|
|
38
|
+
onClose: () => void;
|
|
39
|
+
/** Opt-in: persist text + images across close/reopen, keyed by this string. Cleared on submit. */
|
|
40
|
+
draftKey?: string;
|
|
41
|
+
/** Whether image attachments are available in this comment surface. */
|
|
42
|
+
allowImages?: boolean;
|
|
43
|
+
/** Whether submitting empty text is allowed, for editors that support clearing. */
|
|
44
|
+
allowEmptySubmit?: boolean;
|
|
45
|
+
/** Optional Ask AI action. Absent by default so existing comment surfaces are unchanged. */
|
|
46
|
+
onAskAI?: CommentAskAIHandler;
|
|
47
|
+
askAIContext?: CommentAskAIContext;
|
|
48
|
+
askAIDisabled?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const MAX_POPOVER_WIDTH = 384;
|
|
52
|
+
const GAP = 8;
|
|
53
|
+
|
|
54
|
+
// Module-level draft store: survives popover unmount so reopening the same key restores in-progress text.
|
|
55
|
+
const draftStore = new Map<string, { text: string; images: ImageAttachment[] }>();
|
|
56
|
+
|
|
57
|
+
/** Mirrors the latest text + images into `draftStore[draftKey]` so they outlive popover unmount. No-op without a key. */
|
|
58
|
+
function useCommentDraftSync(draftKey: string | undefined, text: string, images: ImageAttachment[]) {
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!draftKey) return;
|
|
61
|
+
if (hasUnsavedCommentContent(text, images)) {
|
|
62
|
+
draftStore.set(draftKey, { text, images });
|
|
63
|
+
} else {
|
|
64
|
+
draftStore.delete(draftKey);
|
|
65
|
+
}
|
|
66
|
+
}, [draftKey, text, images]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function computePosition(anchorRect: DOMRect): { top: number; left: number; flipAbove: boolean; width: number } {
|
|
70
|
+
const spaceBelow = window.innerHeight - anchorRect.bottom;
|
|
71
|
+
const flipAbove = spaceBelow < 280;
|
|
72
|
+
const width = Math.min(MAX_POPOVER_WIDTH, window.innerWidth - 32);
|
|
73
|
+
|
|
74
|
+
const top = flipAbove
|
|
75
|
+
? anchorRect.top - GAP
|
|
76
|
+
: anchorRect.bottom + GAP;
|
|
77
|
+
|
|
78
|
+
let left = anchorRect.left + anchorRect.width / 2 - width / 2;
|
|
79
|
+
left = Math.max(16, Math.min(left, window.innerWidth - width - 16));
|
|
80
|
+
|
|
81
|
+
return { top, left, flipAbove, width };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const CommentPopover: React.FC<CommentPopoverProps> = ({
|
|
85
|
+
anchorEl,
|
|
86
|
+
anchorRect,
|
|
87
|
+
contextText,
|
|
88
|
+
isGlobal,
|
|
89
|
+
initialText = '',
|
|
90
|
+
onSubmit,
|
|
91
|
+
onDraftChange,
|
|
92
|
+
onClose,
|
|
93
|
+
draftKey,
|
|
94
|
+
allowImages = true,
|
|
95
|
+
allowEmptySubmit = false,
|
|
96
|
+
onAskAI,
|
|
97
|
+
askAIContext,
|
|
98
|
+
askAIDisabled = false,
|
|
99
|
+
}) => {
|
|
100
|
+
const [mode, setMode] = useState<'popover' | 'dialog'>('popover');
|
|
101
|
+
const initialDraft = draftKey ? draftStore.get(draftKey) : undefined;
|
|
102
|
+
const [text, setText] = useState(initialDraft?.text ?? initialText);
|
|
103
|
+
const [images, setImages] = useState<ImageAttachment[]>(allowImages ? initialDraft?.images ?? [] : []);
|
|
104
|
+
const [position, setPosition] = useState<{ top: number; left: number; flipAbove: boolean; width: number } | null>(null);
|
|
105
|
+
// Direction of an open popover that has scrolled out of view, or null when on-screen.
|
|
106
|
+
const [offscreen, setOffscreen] = useState<'above' | 'below' | null>(null);
|
|
107
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
108
|
+
const popoverRef = useRef<HTMLDivElement>(null);
|
|
109
|
+
const hasUnsavedContent = hasUnsavedCommentContent(text, allowImages ? images : []);
|
|
110
|
+
const hasUnsavedContentRef = useRef(hasUnsavedContent);
|
|
111
|
+
hasUnsavedContentRef.current = hasUnsavedContent;
|
|
112
|
+
const { dragPosition, dragHandleProps, wasDragged, reset: resetDrag } = useDraggable(popoverRef);
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const nextDraft = draftKey ? draftStore.get(draftKey) : undefined;
|
|
116
|
+
setText(nextDraft?.text ?? initialText);
|
|
117
|
+
setImages(allowImages ? nextDraft?.images ?? [] : []);
|
|
118
|
+
}, [draftKey, initialText, allowImages]);
|
|
119
|
+
|
|
120
|
+
useCommentDraftSync(draftKey, text, allowImages ? images : []);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
onDraftChange?.(text, allowImages ? images : undefined);
|
|
124
|
+
}, [allowImages, images, onDraftChange, text]);
|
|
125
|
+
|
|
126
|
+
// Reset drag when anchor changes (new annotation) or mode switches
|
|
127
|
+
useEffect(() => { resetDrag(); }, [anchorEl, anchorRect, resetDrag]);
|
|
128
|
+
useEffect(() => { if (mode === 'popover') resetDrag(); }, [mode, resetDrag]);
|
|
129
|
+
|
|
130
|
+
// Track anchor position on scroll/resize (popover mode only, not after user drag)
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (mode !== 'popover' || wasDragged) return;
|
|
133
|
+
|
|
134
|
+
const update = () => {
|
|
135
|
+
const rect = anchorEl?.getBoundingClientRect() ?? anchorRect;
|
|
136
|
+
if (rect) setPosition(computePosition(rect));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
update();
|
|
140
|
+
window.addEventListener('scroll', update, true);
|
|
141
|
+
window.addEventListener('resize', update);
|
|
142
|
+
return () => {
|
|
143
|
+
window.removeEventListener('scroll', update, true);
|
|
144
|
+
window.removeEventListener('resize', update);
|
|
145
|
+
};
|
|
146
|
+
}, [anchorEl, anchorRect, mode, wasDragged]);
|
|
147
|
+
|
|
148
|
+
// Surface a "jump back" arrow when an open popover scrolls out of view.
|
|
149
|
+
// Re-measures whenever the popover repositions (position updates every scroll
|
|
150
|
+
// step in tracked mode) so the indicator is accurate at rest, plus on resize.
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (mode !== 'popover') { setOffscreen(null); return; }
|
|
153
|
+
const measure = () => {
|
|
154
|
+
const el = popoverRef.current;
|
|
155
|
+
if (!el) return;
|
|
156
|
+
const rect = el.getBoundingClientRect();
|
|
157
|
+
if (rect.bottom < 8) setOffscreen('above');
|
|
158
|
+
else if (rect.top > window.innerHeight - 8) setOffscreen('below');
|
|
159
|
+
else setOffscreen(null);
|
|
160
|
+
};
|
|
161
|
+
measure();
|
|
162
|
+
window.addEventListener('resize', measure);
|
|
163
|
+
return () => window.removeEventListener('resize', measure);
|
|
164
|
+
}, [position, dragPosition, mode]);
|
|
165
|
+
|
|
166
|
+
const scrollToPopover = useCallback(() => {
|
|
167
|
+
anchorEl?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
168
|
+
}, [anchorEl]);
|
|
169
|
+
|
|
170
|
+
// Focus textarea on mount and mode changes
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const id = setTimeout(() => {
|
|
173
|
+
const el = textareaRef.current;
|
|
174
|
+
if (el) {
|
|
175
|
+
el.focus();
|
|
176
|
+
el.selectionStart = el.selectionEnd = el.value.length;
|
|
177
|
+
}
|
|
178
|
+
}, 0);
|
|
179
|
+
return () => clearTimeout(id);
|
|
180
|
+
}, [mode]);
|
|
181
|
+
|
|
182
|
+
// Click-outside for popover mode
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (mode !== 'popover') return;
|
|
185
|
+
|
|
186
|
+
const handlePointerDown = (e: PointerEvent) => {
|
|
187
|
+
const target = e.target as Node | null;
|
|
188
|
+
if (!target) return;
|
|
189
|
+
if (popoverRef.current?.contains(target)) return;
|
|
190
|
+
// Don't close if clicking inside a child portal (AttachmentsButton, ImageAnnotator, etc.)
|
|
191
|
+
const el = target as HTMLElement;
|
|
192
|
+
if (el.closest?.('[data-popover-layer]')) return;
|
|
193
|
+
if (hasUnsavedContentRef.current) return;
|
|
194
|
+
onClose();
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
document.addEventListener('pointerdown', handlePointerDown, true);
|
|
198
|
+
return () => document.removeEventListener('pointerdown', handlePointerDown, true);
|
|
199
|
+
}, [mode, onClose]);
|
|
200
|
+
|
|
201
|
+
const handleSubmit = useCallback(() => {
|
|
202
|
+
const canSubmitEmpty = allowEmptySubmit && initialText.trim().length > 0;
|
|
203
|
+
if (hasUnsavedContent || canSubmitEmpty) {
|
|
204
|
+
if (draftKey) draftStore.delete(draftKey);
|
|
205
|
+
onSubmit(text, allowImages && images.length > 0 ? images : undefined);
|
|
206
|
+
}
|
|
207
|
+
}, [text, images, onSubmit, draftKey, allowImages, allowEmptySubmit, initialText, hasUnsavedContent]);
|
|
208
|
+
|
|
209
|
+
const handleAskAI = useCallback(async () => {
|
|
210
|
+
const question = text.trim();
|
|
211
|
+
if (!question || !onAskAI) {
|
|
212
|
+
textareaRef.current?.focus();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
let accepted: boolean | void;
|
|
216
|
+
try {
|
|
217
|
+
accepted = await onAskAI(question, askAIContext ?? {
|
|
218
|
+
kind: isGlobal ? 'general' : 'selection',
|
|
219
|
+
text: contextText,
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error('Ask AI action failed:', error);
|
|
223
|
+
textareaRef.current?.focus();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (accepted === false) {
|
|
227
|
+
textareaRef.current?.focus();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (draftKey) draftStore.delete(draftKey);
|
|
231
|
+
onDraftChange?.('', allowImages ? [] : undefined);
|
|
232
|
+
onClose();
|
|
233
|
+
}, [allowImages, askAIContext, contextText, draftKey, isGlobal, onAskAI, onClose, onDraftChange, text]);
|
|
234
|
+
|
|
235
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
236
|
+
if (e.key === 'Escape') {
|
|
237
|
+
e.stopPropagation();
|
|
238
|
+
if (mode === 'dialog') {
|
|
239
|
+
setMode('popover');
|
|
240
|
+
} else {
|
|
241
|
+
onClose();
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
handleSubmit();
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const headerLabel = isGlobal
|
|
252
|
+
? 'Global Comment'
|
|
253
|
+
: contextText
|
|
254
|
+
? `"${contextText.length > 50 ? contextText.slice(0, 50) + '...' : contextText}"`
|
|
255
|
+
: 'Comment';
|
|
256
|
+
|
|
257
|
+
const canSubmit =
|
|
258
|
+
hasUnsavedContent ||
|
|
259
|
+
(allowEmptySubmit && initialText.trim().length > 0);
|
|
260
|
+
const canAskAI = !!onAskAI && !askAIDisabled && text.trim().length > 0;
|
|
261
|
+
|
|
262
|
+
if (mode === 'dialog') {
|
|
263
|
+
return createPortal(
|
|
264
|
+
<div data-comment-popover="true" className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
|
265
|
+
{/* Backdrop */}
|
|
266
|
+
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm" />
|
|
267
|
+
|
|
268
|
+
{/* Dialog card */}
|
|
269
|
+
<div
|
|
270
|
+
ref={popoverRef}
|
|
271
|
+
className="relative w-full max-w-xl bg-popover border border-border rounded-xl shadow-2xl flex flex-col"
|
|
272
|
+
style={{
|
|
273
|
+
animation: 'comment-dialog-in 0.15s ease-out',
|
|
274
|
+
}}
|
|
275
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
276
|
+
>
|
|
277
|
+
<style>{`
|
|
278
|
+
@keyframes comment-dialog-in {
|
|
279
|
+
from { opacity: 0; transform: scale(0.95); }
|
|
280
|
+
to { opacity: 1; transform: scale(1); }
|
|
281
|
+
}
|
|
282
|
+
`}</style>
|
|
283
|
+
|
|
284
|
+
{/* Header */}
|
|
285
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50">
|
|
286
|
+
<span className="text-xs text-muted-foreground truncate max-w-[400px]">
|
|
287
|
+
{headerLabel}
|
|
288
|
+
</span>
|
|
289
|
+
<div className="flex items-center gap-1">
|
|
290
|
+
<button
|
|
291
|
+
onClick={() => setMode('popover')}
|
|
292
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
293
|
+
title="Collapse"
|
|
294
|
+
>
|
|
295
|
+
<CollapseIcon />
|
|
296
|
+
</button>
|
|
297
|
+
<button
|
|
298
|
+
onClick={onClose}
|
|
299
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
300
|
+
title="Close"
|
|
301
|
+
>
|
|
302
|
+
<CloseIcon />
|
|
303
|
+
</button>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
{/* Textarea */}
|
|
308
|
+
<div className="px-4 py-3 flex-1">
|
|
309
|
+
<textarea
|
|
310
|
+
ref={textareaRef}
|
|
311
|
+
value={text}
|
|
312
|
+
onChange={(e) => setText(e.target.value)}
|
|
313
|
+
onKeyDown={handleKeyDown}
|
|
314
|
+
placeholder={isGlobal ? 'Add a global comment...' : 'Add a comment...'}
|
|
315
|
+
className="w-full bg-transparent text-sm placeholder:text-muted-foreground resize-none focus:outline-none min-h-48 max-h-96 px-1 py-0.5"
|
|
316
|
+
style={{ fieldSizing: 'content' } as React.CSSProperties}
|
|
317
|
+
/>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
{/* Footer */}
|
|
321
|
+
<div className="flex items-center justify-between px-4 py-3 border-t border-border/50">
|
|
322
|
+
<div className="flex items-center gap-2">
|
|
323
|
+
{allowImages && (
|
|
324
|
+
<AttachmentsButton
|
|
325
|
+
images={images}
|
|
326
|
+
onAdd={(img) => setImages((prev) => [...prev, img])}
|
|
327
|
+
onRemove={(path) => setImages((prev) => prev.filter((i) => i.path !== path))}
|
|
328
|
+
variant="inline"
|
|
329
|
+
/>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
<div className="flex items-center gap-3">
|
|
333
|
+
{onAskAI && (
|
|
334
|
+
<button
|
|
335
|
+
onClick={handleAskAI}
|
|
336
|
+
disabled={!canAskAI}
|
|
337
|
+
className="inline-flex items-center gap-1 px-2 py-1.5 text-xs font-medium rounded-md text-muted-foreground hover:text-primary hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
338
|
+
title={canAskAI ? 'Ask AI this question' : 'Type a question to ask AI'}
|
|
339
|
+
>
|
|
340
|
+
<SparklesIcon className="w-3 h-3" />
|
|
341
|
+
Ask AI
|
|
342
|
+
</button>
|
|
343
|
+
)}
|
|
344
|
+
<span className="text-[10px] text-muted-foreground">{submitHint}</span>
|
|
345
|
+
<button
|
|
346
|
+
onClick={handleSubmit}
|
|
347
|
+
disabled={!canSubmit}
|
|
348
|
+
className="px-3 py-1.5 text-xs font-medium rounded-md bg-primary text-primary-foreground hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
|
|
349
|
+
>
|
|
350
|
+
{isGlobal ? 'Add' : 'Save'}
|
|
351
|
+
</button>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
</div>,
|
|
356
|
+
document.body
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Popover mode
|
|
361
|
+
if (!position) return null;
|
|
362
|
+
|
|
363
|
+
return createPortal(
|
|
364
|
+
<>
|
|
365
|
+
{offscreen && (
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
data-popover-layer="true"
|
|
369
|
+
onClick={scrollToPopover}
|
|
370
|
+
title="Scroll back to your open comment"
|
|
371
|
+
className={`fixed left-1/2 -translate-x-1/2 z-[101] flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-popover border border-border shadow-lg text-xs text-muted-foreground hover:text-foreground transition-colors ${offscreen === 'above' ? 'top-3' : 'bottom-3'}`}
|
|
372
|
+
>
|
|
373
|
+
{offscreen === 'above' ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
|
374
|
+
<span>Open comment</span>
|
|
375
|
+
</button>
|
|
376
|
+
)}
|
|
377
|
+
<div
|
|
378
|
+
ref={popoverRef}
|
|
379
|
+
data-comment-popover="true"
|
|
380
|
+
className="fixed z-[100] bg-popover border border-border rounded-xl shadow-2xl flex flex-col"
|
|
381
|
+
style={dragPosition
|
|
382
|
+
? { top: dragPosition.top, left: dragPosition.left, width: position.width }
|
|
383
|
+
: {
|
|
384
|
+
top: position.top,
|
|
385
|
+
left: position.left,
|
|
386
|
+
width: position.width,
|
|
387
|
+
...(position.flipAbove ? { transform: 'translateY(-100%)' } : {}),
|
|
388
|
+
animation: position.flipAbove
|
|
389
|
+
? 'comment-popover-in-above 0.15s ease-out'
|
|
390
|
+
: 'comment-popover-in 0.15s ease-out',
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
394
|
+
>
|
|
395
|
+
<style>{`
|
|
396
|
+
@keyframes comment-popover-in {
|
|
397
|
+
from { opacity: 0; transform: translateY(-8px); }
|
|
398
|
+
to { opacity: 1; transform: translateY(0); }
|
|
399
|
+
}
|
|
400
|
+
@keyframes comment-popover-in-above {
|
|
401
|
+
from { opacity: 0; transform: translateY(-100%) translateY(8px); }
|
|
402
|
+
to { opacity: 1; transform: translateY(-100%); }
|
|
403
|
+
}
|
|
404
|
+
`}</style>
|
|
405
|
+
|
|
406
|
+
{/* Header (draggable) */}
|
|
407
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-border/50" {...dragHandleProps}>
|
|
408
|
+
<span className="text-xs text-muted-foreground truncate max-w-[260px]">
|
|
409
|
+
{headerLabel}
|
|
410
|
+
</span>
|
|
411
|
+
<div className="flex items-center gap-1">
|
|
412
|
+
<button
|
|
413
|
+
onClick={() => setMode('dialog')}
|
|
414
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
415
|
+
title="Expand"
|
|
416
|
+
>
|
|
417
|
+
<ExpandIcon />
|
|
418
|
+
</button>
|
|
419
|
+
<button
|
|
420
|
+
onClick={onClose}
|
|
421
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
422
|
+
title="Close"
|
|
423
|
+
>
|
|
424
|
+
<CloseIcon />
|
|
425
|
+
</button>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
{/* Textarea */}
|
|
430
|
+
<div className="px-3 py-2">
|
|
431
|
+
<textarea
|
|
432
|
+
ref={textareaRef}
|
|
433
|
+
value={text}
|
|
434
|
+
onChange={(e) => setText(e.target.value)}
|
|
435
|
+
onKeyDown={handleKeyDown}
|
|
436
|
+
placeholder={isGlobal ? 'Add a global comment...' : 'Add a comment...'}
|
|
437
|
+
className="w-full bg-transparent text-sm placeholder:text-muted-foreground resize-none focus:outline-none max-h-64 min-h-[4.5rem] px-1 py-0.5"
|
|
438
|
+
style={{ fieldSizing: 'content' } as React.CSSProperties}
|
|
439
|
+
/>
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
{/* Footer */}
|
|
443
|
+
<div className="flex items-center justify-between px-3 py-2 border-t border-border/50">
|
|
444
|
+
<div className="flex items-center gap-2">
|
|
445
|
+
{allowImages && (
|
|
446
|
+
<AttachmentsButton
|
|
447
|
+
images={images}
|
|
448
|
+
onAdd={(img) => setImages((prev) => [...prev, img])}
|
|
449
|
+
onRemove={(path) => setImages((prev) => prev.filter((i) => i.path !== path))}
|
|
450
|
+
variant="inline"
|
|
451
|
+
/>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
<div className="flex items-center gap-3">
|
|
455
|
+
{onAskAI && (
|
|
456
|
+
<button
|
|
457
|
+
onClick={handleAskAI}
|
|
458
|
+
disabled={!canAskAI}
|
|
459
|
+
className="inline-flex items-center gap-1 px-2 py-1.5 text-xs font-medium rounded-md text-muted-foreground hover:text-primary hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
460
|
+
title={canAskAI ? 'Ask AI this question' : 'Type a question to ask AI'}
|
|
461
|
+
>
|
|
462
|
+
<SparklesIcon className="w-3 h-3" />
|
|
463
|
+
Ask AI
|
|
464
|
+
</button>
|
|
465
|
+
)}
|
|
466
|
+
<span className="text-[10px] text-muted-foreground">{submitHint}</span>
|
|
467
|
+
<button
|
|
468
|
+
onClick={handleSubmit}
|
|
469
|
+
disabled={!canSubmit}
|
|
470
|
+
className="px-3 py-1.5 text-xs font-medium rounded-md bg-primary text-primary-foreground hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
|
|
471
|
+
>
|
|
472
|
+
{isGlobal ? 'Add' : 'Save'}
|
|
473
|
+
</button>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
</>,
|
|
478
|
+
document.body
|
|
479
|
+
);
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// Icons
|
|
483
|
+
|
|
484
|
+
const ChevronUpIcon = () => (
|
|
485
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
486
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
|
|
487
|
+
</svg>
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const ChevronDownIcon = () => (
|
|
491
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
492
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
493
|
+
</svg>
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const ExpandIcon = () => (
|
|
497
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
498
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
|
|
499
|
+
</svg>
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const CollapseIcon = () => (
|
|
503
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
504
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />
|
|
505
|
+
</svg>
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
const CloseIcon = () => (
|
|
509
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
510
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
511
|
+
</svg>
|
|
512
|
+
);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useAutoClose } from '../hooks/useAutoClose';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Icons
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
const CheckIcon = () => (
|
|
8
|
+
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
9
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
10
|
+
</svg>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const ChatBubbleIcon = () => (
|
|
14
|
+
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
15
|
+
<path
|
|
16
|
+
strokeLinecap="round"
|
|
17
|
+
strokeLinejoin="round"
|
|
18
|
+
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
|
19
|
+
/>
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Component
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
interface CompletionOverlayProps {
|
|
28
|
+
submitted: 'approved' | 'denied' | 'feedback' | 'exited' | null | false;
|
|
29
|
+
title: string;
|
|
30
|
+
subtitle: string;
|
|
31
|
+
agentLabel: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function CompletionOverlay({ submitted, title, subtitle, agentLabel }: CompletionOverlayProps) {
|
|
35
|
+
const { state, enableAndStart } = useAutoClose(!!submitted);
|
|
36
|
+
|
|
37
|
+
if (!submitted) return null;
|
|
38
|
+
|
|
39
|
+
const isApproved = submitted === 'approved';
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="fixed inset-0 z-[100] bg-background flex items-center justify-center">
|
|
43
|
+
<div className="text-center space-y-6 max-w-md px-8">
|
|
44
|
+
<div
|
|
45
|
+
className={`mx-auto w-16 h-16 rounded-full flex items-center justify-center ${
|
|
46
|
+
isApproved ? 'bg-success/20 text-success' : 'bg-accent/20 text-accent'
|
|
47
|
+
}`}
|
|
48
|
+
>
|
|
49
|
+
{isApproved ? <CheckIcon /> : <ChatBubbleIcon />}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div className="space-y-2">
|
|
53
|
+
<h2 className="text-xl font-semibold text-foreground">{title}</h2>
|
|
54
|
+
<p className="text-muted-foreground">{subtitle}</p>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div className="pt-4 border-t border-border space-y-2">
|
|
58
|
+
{state.phase === 'counting' ? (
|
|
59
|
+
<>
|
|
60
|
+
<p className="text-sm text-muted-foreground">
|
|
61
|
+
This tab will close in <span className="text-foreground font-medium">{state.remaining}</span> second
|
|
62
|
+
{state.remaining !== 1 ? 's' : ''}...
|
|
63
|
+
</p>
|
|
64
|
+
<p className="text-xs text-muted-foreground/60">You can change this in Settings.</p>
|
|
65
|
+
</>
|
|
66
|
+
) : state.phase === 'closeFailed' ? (
|
|
67
|
+
<>
|
|
68
|
+
<p className="text-sm text-muted-foreground">
|
|
69
|
+
Could not close this tab automatically. Please close it manually.
|
|
70
|
+
</p>
|
|
71
|
+
<p className="text-xs text-muted-foreground/60">
|
|
72
|
+
Auto-close works when the tab is opened by {agentLabel}.
|
|
73
|
+
</p>
|
|
74
|
+
</>
|
|
75
|
+
) : (
|
|
76
|
+
<>
|
|
77
|
+
<p className="text-sm text-muted-foreground">
|
|
78
|
+
You can close this tab and return to <span className="text-foreground font-medium">{agentLabel}</span>.
|
|
79
|
+
</p>
|
|
80
|
+
{state.phase === 'prompt' ? (
|
|
81
|
+
<>
|
|
82
|
+
<label className="flex items-center justify-center gap-2 cursor-pointer group">
|
|
83
|
+
<input type="checkbox" checked={false} onChange={enableAndStart} className="accent-primary" />
|
|
84
|
+
<span className="text-xs text-muted-foreground group-hover:text-foreground transition-colors">
|
|
85
|
+
Auto-close this tab after 3 seconds
|
|
86
|
+
</span>
|
|
87
|
+
</label>
|
|
88
|
+
<p className="text-xs text-muted-foreground/60">You can change the delay in Settings.</p>
|
|
89
|
+
</>
|
|
90
|
+
) : (
|
|
91
|
+
<p className="text-xs text-muted-foreground/60">Your response has been sent.</p>
|
|
92
|
+
)}
|
|
93
|
+
</>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|