@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,731 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { Annotation, AnnotationType, Block, type CodeAnnotation, type EditorAnnotation } from '../types';
|
|
3
|
+
import { isCurrentUser } from '../utils/identity';
|
|
4
|
+
import { ImageThumbnail } from './ImageThumbnail';
|
|
5
|
+
import { EditorAnnotationCard } from './EditorAnnotationCard';
|
|
6
|
+
import { useIsMobile } from '../hooks/useIsMobile';
|
|
7
|
+
import { OverlayScrollArea } from './OverlayScrollArea';
|
|
8
|
+
import { Button } from './ui/button';
|
|
9
|
+
import { cn } from '../lib/utils';
|
|
10
|
+
|
|
11
|
+
// Card type-word colors. Deletion uses `destructive` (reliably red on every
|
|
12
|
+
// theme, matching the in-document .deletion highlight). Comment uses the
|
|
13
|
+
// `annotation-comment` token, which defaults to each theme's --accent (so it
|
|
14
|
+
// stays consistent with the .comment highlight) but is overridden to a legible
|
|
15
|
+
// blue in neutral themes whose accent is a low-contrast gray (e.g. "simple").
|
|
16
|
+
// Global has no in-document highlight, so it uses a fixed legible purple.
|
|
17
|
+
const TYPE_COLOR: Record<AnnotationType, string> = {
|
|
18
|
+
[AnnotationType.DELETION]: 'text-destructive',
|
|
19
|
+
[AnnotationType.COMMENT]: 'text-annotation-comment',
|
|
20
|
+
[AnnotationType.GLOBAL_COMMENT]: 'text-purple-500',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const TYPE_LABEL: Record<AnnotationType, string> = {
|
|
24
|
+
[AnnotationType.DELETION]: 'Deletion',
|
|
25
|
+
[AnnotationType.COMMENT]: 'Comment',
|
|
26
|
+
[AnnotationType.GLOBAL_COMMENT]: 'Global',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const PencilIcon = () => (
|
|
30
|
+
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
31
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const TrashCardIcon = () => (
|
|
36
|
+
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
37
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
interface DirectEditsPanelItem {
|
|
42
|
+
id: string;
|
|
43
|
+
title?: string;
|
|
44
|
+
label?: string;
|
|
45
|
+
added: number;
|
|
46
|
+
removed: number;
|
|
47
|
+
diffText: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
onDiscard?: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface PanelProps {
|
|
53
|
+
isOpen: boolean;
|
|
54
|
+
annotations: Annotation[];
|
|
55
|
+
blocks: Block[];
|
|
56
|
+
onSelect: (id: string) => void;
|
|
57
|
+
onDelete: (id: string) => void;
|
|
58
|
+
onEdit?: (id: string, updates: Partial<Annotation>) => void;
|
|
59
|
+
selectedId: string | null;
|
|
60
|
+
codeAnnotations?: CodeAnnotation[];
|
|
61
|
+
onSelectCodeAnnotation?: (id: string) => void;
|
|
62
|
+
onDeleteCodeAnnotation?: (id: string) => void;
|
|
63
|
+
onEditCodeAnnotation?: (id: string, updates: Partial<CodeAnnotation>) => void;
|
|
64
|
+
sharingEnabled?: boolean;
|
|
65
|
+
width?: number | string;
|
|
66
|
+
editorAnnotations?: EditorAnnotation[];
|
|
67
|
+
onDeleteEditorAnnotation?: (id: string) => void;
|
|
68
|
+
onClose?: () => void;
|
|
69
|
+
onQuickCopy?: () => Promise<void>;
|
|
70
|
+
onShare?: () => void;
|
|
71
|
+
otherFileAnnotations?: { count: number; files: number };
|
|
72
|
+
onOtherFileAnnotationsClick?: () => void;
|
|
73
|
+
/** Committed direct edits to one or more documents. Rendered as pinned cards
|
|
74
|
+
* above the annotation timeline with expandable unified diffs. */
|
|
75
|
+
directEdits?: DirectEditsPanelItem[] | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const AnnotationPanel: React.FC<PanelProps> = ({
|
|
79
|
+
isOpen,
|
|
80
|
+
annotations,
|
|
81
|
+
blocks,
|
|
82
|
+
onSelect,
|
|
83
|
+
onDelete,
|
|
84
|
+
onEdit,
|
|
85
|
+
selectedId,
|
|
86
|
+
codeAnnotations = [],
|
|
87
|
+
onSelectCodeAnnotation,
|
|
88
|
+
onDeleteCodeAnnotation,
|
|
89
|
+
onEditCodeAnnotation,
|
|
90
|
+
sharingEnabled = true,
|
|
91
|
+
width,
|
|
92
|
+
editorAnnotations,
|
|
93
|
+
onDeleteEditorAnnotation,
|
|
94
|
+
onClose,
|
|
95
|
+
onQuickCopy,
|
|
96
|
+
onShare,
|
|
97
|
+
otherFileAnnotations,
|
|
98
|
+
onOtherFileAnnotationsClick,
|
|
99
|
+
directEdits = null,
|
|
100
|
+
}) => {
|
|
101
|
+
const isMobile = useIsMobile();
|
|
102
|
+
const [copiedText, setCopiedText] = useState(false);
|
|
103
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
104
|
+
const sortedAnnotations = [...annotations].sort((a, b) => a.createdA - b.createdA);
|
|
105
|
+
const sortedCodeAnnotations = [...codeAnnotations].sort((a, b) => a.createdAt - b.createdAt);
|
|
106
|
+
const timelineEntries = [
|
|
107
|
+
...sortedAnnotations.map(annotation => ({ kind: 'plan' as const, ts: annotation.createdA, annotation })),
|
|
108
|
+
...sortedCodeAnnotations.map(annotation => ({ kind: 'code' as const, ts: annotation.createdAt, annotation })),
|
|
109
|
+
].sort((a, b) => a.ts - b.ts);
|
|
110
|
+
const totalCount = annotations.length + codeAnnotations.length + (editorAnnotations?.length ?? 0);
|
|
111
|
+
|
|
112
|
+
// Scroll selected annotation card into view
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!selectedId || !listRef.current) return;
|
|
115
|
+
const card = listRef.current.querySelector(`[data-annotation-id="${selectedId}"]`);
|
|
116
|
+
if (card) {
|
|
117
|
+
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
118
|
+
}
|
|
119
|
+
}, [selectedId]);
|
|
120
|
+
|
|
121
|
+
if (!isOpen) return null;
|
|
122
|
+
|
|
123
|
+
const panel = (
|
|
124
|
+
<aside
|
|
125
|
+
data-annotation-panel="true"
|
|
126
|
+
data-plan-sidebar="right"
|
|
127
|
+
className={`border-l border-border/50 bg-card flex flex-col flex-shrink-0 ${
|
|
128
|
+
isMobile ? 'fixed top-12 bottom-0 right-0 z-[60] w-full max-w-sm shadow-2xl bg-card' : ''
|
|
129
|
+
}`}
|
|
130
|
+
style={isMobile ? undefined : { width: width ?? 288 }}
|
|
131
|
+
>
|
|
132
|
+
{/* Header */}
|
|
133
|
+
<div className="border-b border-border/50">
|
|
134
|
+
<div className="flex h-10 items-center justify-between px-3">
|
|
135
|
+
<div className="flex items-center gap-2">
|
|
136
|
+
<h2 className="text-xs font-medium text-foreground">
|
|
137
|
+
Annotations
|
|
138
|
+
</h2>
|
|
139
|
+
{totalCount > 0 && (
|
|
140
|
+
<span className="flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-primary/10 px-1 font-mono text-[10px] font-medium tabular-nums text-primary">
|
|
141
|
+
{totalCount}
|
|
142
|
+
</span>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
{isMobile && onClose && (
|
|
146
|
+
<button
|
|
147
|
+
onClick={onClose}
|
|
148
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-foreground md:hidden"
|
|
149
|
+
title="Close panel"
|
|
150
|
+
aria-label="Close panel"
|
|
151
|
+
>
|
|
152
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
153
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
154
|
+
</svg>
|
|
155
|
+
</button>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
{otherFileAnnotations && otherFileAnnotations.count > 0 && (
|
|
159
|
+
<button
|
|
160
|
+
onClick={onOtherFileAnnotationsClick}
|
|
161
|
+
className="px-3 pb-2 text-[10px] text-primary/70 hover:text-primary transition-colors cursor-pointer"
|
|
162
|
+
title="Show annotated files in sidebar"
|
|
163
|
+
>
|
|
164
|
+
+{otherFileAnnotations.count} in {otherFileAnnotations.files} other file{otherFileAnnotations.files === 1 ? '' : 's'}
|
|
165
|
+
</button>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{/* List */}
|
|
170
|
+
<OverlayScrollArea className="flex-1 min-h-0">
|
|
171
|
+
<div ref={listRef} className="p-2 flex flex-col gap-1.5">
|
|
172
|
+
{directEdits?.map((item) => (
|
|
173
|
+
<DirectEditsCard key={item.id} {...item} />
|
|
174
|
+
))}
|
|
175
|
+
{totalCount === 0 ? (
|
|
176
|
+
(!directEdits || directEdits.length === 0) && (
|
|
177
|
+
<div className="flex flex-col items-center justify-center px-4 py-16 text-center">
|
|
178
|
+
<p className="text-xs text-muted-foreground/60">
|
|
179
|
+
No annotations yet
|
|
180
|
+
</p>
|
|
181
|
+
<p className="mt-1 text-[11px] text-muted-foreground/40">
|
|
182
|
+
Select text to annotate
|
|
183
|
+
</p>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
) : (
|
|
187
|
+
<>
|
|
188
|
+
{timelineEntries.map(entry => (
|
|
189
|
+
entry.kind === 'plan' ? (
|
|
190
|
+
<AnnotationCard
|
|
191
|
+
key={entry.annotation.id}
|
|
192
|
+
annotation={entry.annotation}
|
|
193
|
+
isSelected={selectedId === entry.annotation.id}
|
|
194
|
+
isMe={isCurrentUser(entry.annotation.author)}
|
|
195
|
+
onSelect={() => onSelect(entry.annotation.id)}
|
|
196
|
+
onDelete={() => onDelete(entry.annotation.id)}
|
|
197
|
+
onEdit={onEdit ? (updates: Partial<Annotation>) => onEdit(entry.annotation.id, updates) : undefined}
|
|
198
|
+
/>
|
|
199
|
+
) : (
|
|
200
|
+
<CodeAnnotationCard
|
|
201
|
+
key={entry.annotation.id}
|
|
202
|
+
annotation={entry.annotation}
|
|
203
|
+
isSelected={selectedId === entry.annotation.id}
|
|
204
|
+
isMe={isCurrentUser(entry.annotation.author)}
|
|
205
|
+
onSelect={() => onSelectCodeAnnotation?.(entry.annotation.id)}
|
|
206
|
+
onDelete={() => onDeleteCodeAnnotation?.(entry.annotation.id)}
|
|
207
|
+
onEdit={onEditCodeAnnotation ? (updates: Partial<CodeAnnotation>) => onEditCodeAnnotation(entry.annotation.id, updates) : undefined}
|
|
208
|
+
/>
|
|
209
|
+
)
|
|
210
|
+
))}
|
|
211
|
+
{editorAnnotations && editorAnnotations.length > 0 && (
|
|
212
|
+
<>
|
|
213
|
+
{timelineEntries.length > 0 && (
|
|
214
|
+
<div className="flex items-center gap-2 pt-2 pb-1">
|
|
215
|
+
<div className="flex-1 border-t border-border/30" />
|
|
216
|
+
<span className="text-[9px] font-semibold uppercase tracking-wider text-muted-foreground/60">Editor</span>
|
|
217
|
+
<div className="flex-1 border-t border-border/30" />
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
220
|
+
{editorAnnotations.map(ann => (
|
|
221
|
+
<EditorAnnotationCard
|
|
222
|
+
key={ann.id}
|
|
223
|
+
annotation={ann}
|
|
224
|
+
onDelete={() => onDeleteEditorAnnotation?.(ann.id)}
|
|
225
|
+
/>
|
|
226
|
+
))}
|
|
227
|
+
</>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
</>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
</OverlayScrollArea>
|
|
234
|
+
|
|
235
|
+
{/* Quick Actions Footer */}
|
|
236
|
+
{totalCount > 0 && (
|
|
237
|
+
<div className="border-t border-border/50 px-3 py-2 flex gap-1.5">
|
|
238
|
+
{onQuickCopy && (
|
|
239
|
+
<button
|
|
240
|
+
onClick={async () => {
|
|
241
|
+
await onQuickCopy();
|
|
242
|
+
setCopiedText(true);
|
|
243
|
+
setTimeout(() => setCopiedText(false), 2000);
|
|
244
|
+
}}
|
|
245
|
+
className={`flex-1 flex items-center justify-center gap-1.5 rounded-lg py-2 text-xs font-medium transition-colors ${
|
|
246
|
+
copiedText ? 'text-green-500' : 'text-muted-foreground hover:bg-surface-1 hover:text-foreground'
|
|
247
|
+
}`}
|
|
248
|
+
>
|
|
249
|
+
{copiedText ? (
|
|
250
|
+
<>
|
|
251
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
252
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
253
|
+
</svg>
|
|
254
|
+
Copied
|
|
255
|
+
</>
|
|
256
|
+
) : (
|
|
257
|
+
<>
|
|
258
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
259
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
260
|
+
</svg>
|
|
261
|
+
Copy
|
|
262
|
+
</>
|
|
263
|
+
)}
|
|
264
|
+
</button>
|
|
265
|
+
)}
|
|
266
|
+
{sharingEnabled && onShare && (
|
|
267
|
+
<button
|
|
268
|
+
onClick={onShare}
|
|
269
|
+
className="flex-1 flex items-center justify-center gap-1.5 rounded-lg py-2 text-xs font-medium transition-colors text-muted-foreground hover:bg-surface-1 hover:text-foreground"
|
|
270
|
+
>
|
|
271
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
272
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
|
273
|
+
</svg>
|
|
274
|
+
Share
|
|
275
|
+
</button>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
</aside>
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
if (isMobile) {
|
|
283
|
+
return (
|
|
284
|
+
<>
|
|
285
|
+
<div
|
|
286
|
+
className="fixed inset-0 z-[59] bg-background/60 backdrop-blur-sm"
|
|
287
|
+
onClick={onClose}
|
|
288
|
+
/>
|
|
289
|
+
{panel}
|
|
290
|
+
</>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return panel;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
function formatTimestamp(ts: number): string {
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
const diff = now - ts;
|
|
300
|
+
const seconds = Math.floor(diff / 1000);
|
|
301
|
+
const minutes = Math.floor(seconds / 60);
|
|
302
|
+
const hours = Math.floor(minutes / 60);
|
|
303
|
+
const days = Math.floor(hours / 24);
|
|
304
|
+
|
|
305
|
+
if (seconds < 60) return 'now';
|
|
306
|
+
if (minutes < 60) return `${minutes}m`;
|
|
307
|
+
if (hours < 24) return `${hours}h`;
|
|
308
|
+
if (days < 7) return `${days}d`;
|
|
309
|
+
|
|
310
|
+
return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Pinned card for committed direct edits: +N/−M summary, expandable unified
|
|
314
|
+
* diff, and a two-step discard. Not part of the annotation timeline — edits
|
|
315
|
+
* are document state, not a selection-anchored note. */
|
|
316
|
+
const DirectEditsCard: React.FC<{
|
|
317
|
+
title?: string;
|
|
318
|
+
label?: string;
|
|
319
|
+
added: number;
|
|
320
|
+
removed: number;
|
|
321
|
+
diffText: string;
|
|
322
|
+
description?: string;
|
|
323
|
+
onDiscard?: () => void;
|
|
324
|
+
}> = ({ title = 'Edits', label, added, removed, diffText, description, onDiscard }) => {
|
|
325
|
+
const [expanded, setExpanded] = useState(false);
|
|
326
|
+
const [confirmDiscard, setConfirmDiscard] = useState(false);
|
|
327
|
+
|
|
328
|
+
// Auto-cancel the discard confirmation after a beat.
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
if (!confirmDiscard) return;
|
|
331
|
+
const t = setTimeout(() => setConfirmDiscard(false), 3000);
|
|
332
|
+
return () => clearTimeout(t);
|
|
333
|
+
}, [confirmDiscard]);
|
|
334
|
+
|
|
335
|
+
// Show from the first hunk header; the ---/+++ preamble is noise here.
|
|
336
|
+
const diffLines = React.useMemo(() => {
|
|
337
|
+
const lines = diffText.split('\n');
|
|
338
|
+
const fileSeparators = lines.filter((l) => l.startsWith('===')).length;
|
|
339
|
+
if (fileSeparators > 1) return lines;
|
|
340
|
+
const firstHunk = lines.findIndex((l) => l.startsWith('@@'));
|
|
341
|
+
return firstHunk === -1 ? lines : lines.slice(firstHunk);
|
|
342
|
+
}, [diffText]);
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<div className="w-full rounded-lg px-3 py-2.5 bg-surface-1/40 ring-1 ring-border/40">
|
|
346
|
+
<div className="flex items-center gap-1.5">
|
|
347
|
+
<span className="text-[11px] font-medium text-primary">{title}</span>
|
|
348
|
+
{label && (
|
|
349
|
+
<span className="min-w-0 truncate text-[10px] text-muted-foreground" title={label}>
|
|
350
|
+
{label}
|
|
351
|
+
</span>
|
|
352
|
+
)}
|
|
353
|
+
<span className="font-mono text-[10px] tabular-nums">
|
|
354
|
+
<span className="text-success">+{added}</span>
|
|
355
|
+
<span className="text-muted-foreground/40">/</span>
|
|
356
|
+
<span className="text-destructive">-{removed}</span>
|
|
357
|
+
</span>
|
|
358
|
+
<div className="ml-auto flex items-center gap-1">
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
onClick={() => setExpanded((v) => !v)}
|
|
362
|
+
className="cursor-pointer rounded px-1.5 py-0.5 text-[10px] text-muted-foreground transition-colors hover:bg-surface-1 hover:text-foreground"
|
|
363
|
+
aria-expanded={expanded}
|
|
364
|
+
>
|
|
365
|
+
{expanded ? 'Hide diff' : 'Diff'}
|
|
366
|
+
</button>
|
|
367
|
+
{onDiscard && (
|
|
368
|
+
<button
|
|
369
|
+
type="button"
|
|
370
|
+
onClick={() => {
|
|
371
|
+
if (confirmDiscard) {
|
|
372
|
+
setConfirmDiscard(false);
|
|
373
|
+
onDiscard();
|
|
374
|
+
} else {
|
|
375
|
+
setConfirmDiscard(true);
|
|
376
|
+
}
|
|
377
|
+
}}
|
|
378
|
+
className={cn(
|
|
379
|
+
'cursor-pointer rounded px-1.5 py-0.5 text-[10px] transition-colors',
|
|
380
|
+
confirmDiscard
|
|
381
|
+
? 'bg-destructive/15 text-destructive hover:bg-destructive/25'
|
|
382
|
+
: 'text-muted-foreground hover:bg-surface-1 hover:text-destructive',
|
|
383
|
+
)}
|
|
384
|
+
>
|
|
385
|
+
{confirmDiscard ? 'Confirm?' : 'Discard'}
|
|
386
|
+
</button>
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
<p className="mt-1 text-[10px] leading-snug text-muted-foreground/60">
|
|
391
|
+
{description ?? 'Your text changes — sent with the feedback as a diff.'}
|
|
392
|
+
</p>
|
|
393
|
+
{expanded && (
|
|
394
|
+
<pre className="mt-2 max-h-56 overflow-auto rounded-md bg-muted/40 p-2 font-mono text-[10px] leading-relaxed">
|
|
395
|
+
{diffLines.map((line, i) => (
|
|
396
|
+
<div
|
|
397
|
+
key={i}
|
|
398
|
+
className={
|
|
399
|
+
line.startsWith('+') ? 'text-success'
|
|
400
|
+
: line.startsWith('-') ? 'text-destructive'
|
|
401
|
+
: line.startsWith('@@') ? 'text-primary/70'
|
|
402
|
+
: 'text-muted-foreground'
|
|
403
|
+
}
|
|
404
|
+
>
|
|
405
|
+
{line.length === 0 ? ' ' : line}
|
|
406
|
+
</div>
|
|
407
|
+
))}
|
|
408
|
+
</pre>
|
|
409
|
+
)}
|
|
410
|
+
</div>
|
|
411
|
+
);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const AnnotationCard: React.FC<{
|
|
415
|
+
annotation: Annotation;
|
|
416
|
+
isSelected: boolean;
|
|
417
|
+
isMe: boolean;
|
|
418
|
+
onSelect: () => void;
|
|
419
|
+
onDelete: () => void;
|
|
420
|
+
onEdit?: (updates: Partial<Annotation>) => void;
|
|
421
|
+
}> = ({ annotation, isSelected, isMe, onSelect, onDelete, onEdit }) => {
|
|
422
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
423
|
+
const [editText, setEditText] = useState(annotation.text || '');
|
|
424
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
425
|
+
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
if (isEditing && textareaRef.current) {
|
|
428
|
+
textareaRef.current.focus();
|
|
429
|
+
textareaRef.current.select();
|
|
430
|
+
}
|
|
431
|
+
}, [isEditing]);
|
|
432
|
+
|
|
433
|
+
// Update editText when annotation.text changes
|
|
434
|
+
useEffect(() => {
|
|
435
|
+
if (!isEditing) {
|
|
436
|
+
setEditText(annotation.text || '');
|
|
437
|
+
}
|
|
438
|
+
}, [annotation.text, isEditing]);
|
|
439
|
+
|
|
440
|
+
const handleStartEdit = (e: React.MouseEvent) => {
|
|
441
|
+
e.stopPropagation();
|
|
442
|
+
setEditText(annotation.text || '');
|
|
443
|
+
setIsEditing(true);
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const handleSaveEdit = () => {
|
|
447
|
+
if (onEdit) {
|
|
448
|
+
onEdit({ text: editText });
|
|
449
|
+
}
|
|
450
|
+
setIsEditing(false);
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const handleCancelEdit = () => {
|
|
454
|
+
setEditText(annotation.text || '');
|
|
455
|
+
setIsEditing(false);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
459
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
|
|
460
|
+
e.preventDefault();
|
|
461
|
+
handleSaveEdit();
|
|
462
|
+
} else if (e.key === 'Escape') {
|
|
463
|
+
e.preventDefault();
|
|
464
|
+
handleCancelEdit();
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const typeColor = TYPE_COLOR[annotation.type] ?? 'text-muted-foreground';
|
|
469
|
+
const typeLabel = TYPE_LABEL[annotation.type] ?? 'Note';
|
|
470
|
+
const isGlobal = annotation.type === AnnotationType.GLOBAL_COMMENT;
|
|
471
|
+
|
|
472
|
+
// Shared edit textarea — matches the prototype composer primitive
|
|
473
|
+
const editComposer = (
|
|
474
|
+
<div onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
|
475
|
+
<textarea
|
|
476
|
+
ref={textareaRef}
|
|
477
|
+
value={editText}
|
|
478
|
+
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setEditText(e.target.value)}
|
|
479
|
+
onKeyDown={handleKeyDown}
|
|
480
|
+
placeholder="Add your comment..."
|
|
481
|
+
aria-label="Annotation comment"
|
|
482
|
+
className="w-full resize-none rounded-lg border border-border/50 bg-card px-2.5 py-2 text-base leading-relaxed text-foreground outline-none transition-colors placeholder:text-muted-foreground/50 focus:border-primary/40 focus:ring-1 focus:ring-primary/20"
|
|
483
|
+
style={{ fieldSizing: 'content', minHeight: 44 } as React.CSSProperties}
|
|
484
|
+
/>
|
|
485
|
+
<div className="mt-1.5 flex justify-end gap-1.5">
|
|
486
|
+
<Button variant="ghost" size="xxs" onClick={handleCancelEdit}>Cancel</Button>
|
|
487
|
+
<Button size="xxs" disabled={!editText.trim()} onClick={handleSaveEdit}>Save</Button>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
<div
|
|
494
|
+
data-annotation-id={annotation.id}
|
|
495
|
+
role="button"
|
|
496
|
+
tabIndex={0}
|
|
497
|
+
onClick={onSelect}
|
|
498
|
+
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
499
|
+
if ((e.key === 'Enter' || e.key === ' ') && e.target === e.currentTarget) {
|
|
500
|
+
e.preventDefault();
|
|
501
|
+
onSelect();
|
|
502
|
+
}
|
|
503
|
+
}}
|
|
504
|
+
className={cn(
|
|
505
|
+
'group w-full cursor-pointer rounded-lg px-3 py-2.5 text-left transition-colors duration-150 outline-none focus-visible:ring-2 focus-visible:ring-ring/50',
|
|
506
|
+
isSelected ? 'bg-surface-1 ring-1 ring-border/50' : 'hover:bg-surface-1/50',
|
|
507
|
+
)}
|
|
508
|
+
>
|
|
509
|
+
{/* Header: type word + author · time + actions */}
|
|
510
|
+
<div className="mb-1.5 flex items-center gap-1.5">
|
|
511
|
+
<span className={cn('text-[11px] font-medium', typeColor)}>{typeLabel}</span>
|
|
512
|
+
{annotation.diffContext && (
|
|
513
|
+
<span className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-muted text-muted-foreground">
|
|
514
|
+
diff
|
|
515
|
+
</span>
|
|
516
|
+
)}
|
|
517
|
+
<span className="text-[10px] text-muted-foreground/50 truncate">
|
|
518
|
+
{annotation.author ? `${annotation.author}${isMe ? ' (me)' : ''} · ` : ''}{formatTimestamp(annotation.createdA)}
|
|
519
|
+
</span>
|
|
520
|
+
<div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100">
|
|
521
|
+
{onEdit && annotation.type !== AnnotationType.DELETION && !isEditing && (
|
|
522
|
+
<button
|
|
523
|
+
type="button"
|
|
524
|
+
onClick={handleStartEdit}
|
|
525
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-foreground"
|
|
526
|
+
title="Edit annotation"
|
|
527
|
+
>
|
|
528
|
+
<PencilIcon />
|
|
529
|
+
</button>
|
|
530
|
+
)}
|
|
531
|
+
<button
|
|
532
|
+
type="button"
|
|
533
|
+
onClick={(e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); onDelete(); }}
|
|
534
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
|
|
535
|
+
title="Delete annotation"
|
|
536
|
+
>
|
|
537
|
+
<TrashCardIcon />
|
|
538
|
+
</button>
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
|
|
542
|
+
{/* Global Comment - show text directly */}
|
|
543
|
+
{isGlobal ? (
|
|
544
|
+
isEditing ? (
|
|
545
|
+
editComposer
|
|
546
|
+
) : (
|
|
547
|
+
<p className="whitespace-pre-wrap text-[13px] leading-relaxed text-foreground/90">
|
|
548
|
+
{annotation.text}
|
|
549
|
+
</p>
|
|
550
|
+
)
|
|
551
|
+
) : (
|
|
552
|
+
<>
|
|
553
|
+
{/* Quote — the annotated text */}
|
|
554
|
+
<p className="mb-1.5 line-clamp-2 whitespace-pre-wrap font-mono text-[11px] leading-relaxed text-muted-foreground/80">
|
|
555
|
+
"{annotation.originalText}"
|
|
556
|
+
</p>
|
|
557
|
+
|
|
558
|
+
{/* Comment/Replacement Text */}
|
|
559
|
+
{annotation.type !== AnnotationType.DELETION && (
|
|
560
|
+
isEditing ? (
|
|
561
|
+
editComposer
|
|
562
|
+
) : (
|
|
563
|
+
annotation.text && (
|
|
564
|
+
<p className="whitespace-pre-wrap text-[13px] leading-relaxed text-foreground/90">
|
|
565
|
+
{annotation.text}
|
|
566
|
+
</p>
|
|
567
|
+
)
|
|
568
|
+
)
|
|
569
|
+
)}
|
|
570
|
+
</>
|
|
571
|
+
)}
|
|
572
|
+
|
|
573
|
+
{/* Attached Images */}
|
|
574
|
+
{annotation.images && annotation.images.length > 0 && (
|
|
575
|
+
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
576
|
+
{annotation.images.map((img, idx) => (
|
|
577
|
+
<div key={idx} className="text-center">
|
|
578
|
+
<ImageThumbnail
|
|
579
|
+
path={img.path}
|
|
580
|
+
size="sm"
|
|
581
|
+
showRemove={false}
|
|
582
|
+
/>
|
|
583
|
+
<div className="text-[9px] text-muted-foreground truncate max-w-[3rem]" title={img.name}>{img.name}</div>
|
|
584
|
+
</div>
|
|
585
|
+
))}
|
|
586
|
+
</div>
|
|
587
|
+
)}
|
|
588
|
+
</div>
|
|
589
|
+
);
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const CodeAnnotationCard: React.FC<{
|
|
593
|
+
annotation: CodeAnnotation;
|
|
594
|
+
isSelected: boolean;
|
|
595
|
+
isMe: boolean;
|
|
596
|
+
onSelect: () => void;
|
|
597
|
+
onDelete: () => void;
|
|
598
|
+
onEdit?: (updates: Partial<CodeAnnotation>) => void;
|
|
599
|
+
}> = ({ annotation, isSelected, isMe, onSelect, onDelete, onEdit }) => {
|
|
600
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
601
|
+
const [editText, setEditText] = useState(annotation.text || '');
|
|
602
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
603
|
+
|
|
604
|
+
useEffect(() => {
|
|
605
|
+
if (isEditing) {
|
|
606
|
+
textareaRef.current?.focus();
|
|
607
|
+
textareaRef.current?.select();
|
|
608
|
+
}
|
|
609
|
+
}, [isEditing]);
|
|
610
|
+
|
|
611
|
+
useEffect(() => {
|
|
612
|
+
if (!isEditing) setEditText(annotation.text || '');
|
|
613
|
+
}, [annotation.text, isEditing]);
|
|
614
|
+
|
|
615
|
+
const handleSaveEdit = () => {
|
|
616
|
+
onEdit?.({ text: editText });
|
|
617
|
+
setIsEditing(false);
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const lineRange = annotation.lineStart === annotation.lineEnd
|
|
621
|
+
? `line ${annotation.lineStart}`
|
|
622
|
+
: `lines ${annotation.lineStart}-${annotation.lineEnd}`;
|
|
623
|
+
const fileName = annotation.filePath.split('/').pop() || annotation.filePath;
|
|
624
|
+
|
|
625
|
+
const handleCancelEdit = () => {
|
|
626
|
+
setIsEditing(false);
|
|
627
|
+
setEditText(annotation.text || '');
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
return (
|
|
631
|
+
<div
|
|
632
|
+
data-annotation-id={annotation.id}
|
|
633
|
+
role="button"
|
|
634
|
+
tabIndex={0}
|
|
635
|
+
onClick={onSelect}
|
|
636
|
+
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
637
|
+
if ((e.key === 'Enter' || e.key === ' ') && e.target === e.currentTarget) {
|
|
638
|
+
e.preventDefault();
|
|
639
|
+
onSelect();
|
|
640
|
+
}
|
|
641
|
+
}}
|
|
642
|
+
className={cn(
|
|
643
|
+
'group w-full cursor-pointer rounded-lg px-3 py-2.5 text-left transition-colors duration-150 outline-none focus-visible:ring-2 focus-visible:ring-ring/50',
|
|
644
|
+
isSelected ? 'bg-surface-1 ring-1 ring-border/50' : 'hover:bg-surface-1/50',
|
|
645
|
+
)}
|
|
646
|
+
>
|
|
647
|
+
{/* Header: type word + author · time + actions */}
|
|
648
|
+
<div className="mb-1.5 flex items-center gap-1.5">
|
|
649
|
+
<span className="text-[11px] font-medium text-primary">Code</span>
|
|
650
|
+
<span className="text-[10px] text-muted-foreground/50 truncate">
|
|
651
|
+
{annotation.author ? `${annotation.author}${isMe ? ' (me)' : ''} · ` : ''}{formatTimestamp(annotation.createdAt)}
|
|
652
|
+
</span>
|
|
653
|
+
<div className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)]:opacity-100">
|
|
654
|
+
{onEdit && !isEditing && (
|
|
655
|
+
<button
|
|
656
|
+
type="button"
|
|
657
|
+
onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
|
|
658
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-foreground"
|
|
659
|
+
title="Edit annotation"
|
|
660
|
+
>
|
|
661
|
+
<PencilIcon />
|
|
662
|
+
</button>
|
|
663
|
+
)}
|
|
664
|
+
<button
|
|
665
|
+
type="button"
|
|
666
|
+
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
|
667
|
+
className="relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:text-destructive"
|
|
668
|
+
title="Delete annotation"
|
|
669
|
+
>
|
|
670
|
+
<TrashCardIcon />
|
|
671
|
+
</button>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
|
|
675
|
+
{/* File / line meta */}
|
|
676
|
+
<div className="rounded px-2 py-1 bg-surface-1 font-mono text-[11px] text-muted-foreground truncate" title={annotation.filePath}>
|
|
677
|
+
{fileName} · {lineRange}
|
|
678
|
+
</div>
|
|
679
|
+
|
|
680
|
+
{annotation.originalCode && (
|
|
681
|
+
<p className="mt-1.5 line-clamp-2 whitespace-pre-wrap font-mono text-[11px] leading-relaxed text-muted-foreground/80">
|
|
682
|
+
{annotation.originalCode}
|
|
683
|
+
</p>
|
|
684
|
+
)}
|
|
685
|
+
|
|
686
|
+
{isEditing ? (
|
|
687
|
+
<div className="mt-1.5" onClick={(e) => e.stopPropagation()}>
|
|
688
|
+
<textarea
|
|
689
|
+
ref={textareaRef}
|
|
690
|
+
value={editText}
|
|
691
|
+
onChange={(e) => setEditText(e.target.value)}
|
|
692
|
+
onKeyDown={(e) => {
|
|
693
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
|
|
694
|
+
e.preventDefault();
|
|
695
|
+
handleSaveEdit();
|
|
696
|
+
} else if (e.key === 'Escape') {
|
|
697
|
+
e.preventDefault();
|
|
698
|
+
handleCancelEdit();
|
|
699
|
+
}
|
|
700
|
+
}}
|
|
701
|
+
placeholder="Add your comment..."
|
|
702
|
+
aria-label="Annotation comment"
|
|
703
|
+
className="w-full resize-none rounded-lg border border-border/50 bg-card px-2.5 py-2 text-base leading-relaxed text-foreground outline-none transition-colors placeholder:text-muted-foreground/50 focus:border-primary/40 focus:ring-1 focus:ring-primary/20"
|
|
704
|
+
style={{ fieldSizing: 'content', minHeight: 44 } as React.CSSProperties}
|
|
705
|
+
/>
|
|
706
|
+
<div className="mt-1.5 flex justify-end gap-1.5">
|
|
707
|
+
<Button variant="ghost" size="xxs" onClick={handleCancelEdit}>Cancel</Button>
|
|
708
|
+
<Button size="xxs" disabled={!editText.trim()} onClick={handleSaveEdit}>Save</Button>
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
711
|
+
) : (
|
|
712
|
+
annotation.text && (
|
|
713
|
+
<p className="mt-1.5 whitespace-pre-wrap text-[13px] leading-relaxed text-foreground/90">
|
|
714
|
+
{annotation.text}
|
|
715
|
+
</p>
|
|
716
|
+
)
|
|
717
|
+
)}
|
|
718
|
+
|
|
719
|
+
{annotation.images && annotation.images.length > 0 && (
|
|
720
|
+
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
721
|
+
{annotation.images.map((img) => (
|
|
722
|
+
<div key={img.path} className="text-center">
|
|
723
|
+
<ImageThumbnail path={img.path} size="sm" showRemove={false} />
|
|
724
|
+
<div className="text-[9px] text-muted-foreground truncate max-w-[3rem]" title={img.name}>{img.name}</div>
|
|
725
|
+
</div>
|
|
726
|
+
))}
|
|
727
|
+
</div>
|
|
728
|
+
)}
|
|
729
|
+
</div>
|
|
730
|
+
);
|
|
731
|
+
};
|