@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,86 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Annotation, AnnotationType, Block } from '../types';
|
|
3
|
+
|
|
4
|
+
interface SidebarProps {
|
|
5
|
+
annotations: Annotation[];
|
|
6
|
+
blocks: Block[];
|
|
7
|
+
onSelect: (id: string) => void;
|
|
8
|
+
onDelete: (id: string) => void;
|
|
9
|
+
selectedId: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const AnnotationSidebar: React.FC<SidebarProps> = ({
|
|
13
|
+
annotations,
|
|
14
|
+
blocks,
|
|
15
|
+
onSelect,
|
|
16
|
+
onDelete,
|
|
17
|
+
selectedId
|
|
18
|
+
}) => {
|
|
19
|
+
const sortedAnnotations = [...annotations].sort((a, b) => {
|
|
20
|
+
const blockA = blocks.findIndex(blk => blk.id === a.blockId);
|
|
21
|
+
const blockB = blocks.findIndex(blk => blk.id === b.blockId);
|
|
22
|
+
if (blockA !== blockB) return blockA - blockB;
|
|
23
|
+
return a.startOffset - b.startOffset;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="w-[min(22rem,32vw)] min-w-[16rem] border-l border-border/50 bg-card/50 backdrop-blur-sm h-full flex flex-col transition-colors">
|
|
28
|
+
<div className="p-4 border-b border-border/50 flex items-center justify-between">
|
|
29
|
+
<h2 className="font-semibold text-foreground">Review Changes</h2>
|
|
30
|
+
<span className="text-xs bg-muted text-muted-foreground px-2 py-1 rounded-full">
|
|
31
|
+
{annotations.length}
|
|
32
|
+
</span>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
36
|
+
{sortedAnnotations.length === 0 ? (
|
|
37
|
+
<div className="text-center text-muted-foreground mt-10 text-sm">
|
|
38
|
+
<p>No annotations yet.</p>
|
|
39
|
+
<p className="mt-2 text-xs">Select text in the document to add comments or suggest changes.</p>
|
|
40
|
+
</div>
|
|
41
|
+
) : (
|
|
42
|
+
sortedAnnotations.map(ann => (
|
|
43
|
+
<div
|
|
44
|
+
key={ann.id}
|
|
45
|
+
onClick={() => onSelect(ann.id)}
|
|
46
|
+
className={`
|
|
47
|
+
group relative p-3 rounded-lg border transition-all cursor-pointer
|
|
48
|
+
${selectedId === ann.id
|
|
49
|
+
? 'bg-accent/20 border-accent shadow-sm'
|
|
50
|
+
: 'bg-card border-border hover:border-accent hover:shadow-sm'
|
|
51
|
+
}
|
|
52
|
+
`}
|
|
53
|
+
>
|
|
54
|
+
<div className="flex justify-between items-start mb-1">
|
|
55
|
+
<span className={`
|
|
56
|
+
text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded
|
|
57
|
+
${ann.type === AnnotationType.DELETION ? 'bg-destructive/20 text-destructive' :
|
|
58
|
+
'bg-accent/20 text-accent'}
|
|
59
|
+
`}>
|
|
60
|
+
{ann.type}
|
|
61
|
+
</span>
|
|
62
|
+
<button
|
|
63
|
+
onClick={(e) => { e.stopPropagation(); onDelete(ann.id); }}
|
|
64
|
+
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-destructive transition-all p-1 rounded hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
65
|
+
title="Remove Annotation"
|
|
66
|
+
>
|
|
67
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className="text-sm text-muted-foreground mb-2 font-mono bg-muted p-2 rounded-lg text-xs truncate">
|
|
72
|
+
"{ann.originalText}"
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{(ann.text && ann.type !== AnnotationType.DELETION) && (
|
|
76
|
+
<div className="text-sm text-foreground font-medium pl-3 border-l-2 border-primary">
|
|
77
|
+
{ann.text}
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
))
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useMemo, useCallback } from "react";
|
|
2
|
+
import { AnnotationType } from "../types";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { useDismissOnOutsideAndEscape } from "../hooks/useDismissOnOutsideAndEscape";
|
|
5
|
+
import { type QuickLabel, getQuickLabels } from "../utils/quickLabels";
|
|
6
|
+
import { FloatingQuickLabelPicker } from "./FloatingQuickLabelPicker";
|
|
7
|
+
|
|
8
|
+
type PositionMode = 'center-above' | 'top-right';
|
|
9
|
+
|
|
10
|
+
const THUMBS_UP_LABEL: QuickLabel = {
|
|
11
|
+
id: 'thumbs-up',
|
|
12
|
+
emoji: '👍',
|
|
13
|
+
text: 'Looks good',
|
|
14
|
+
color: 'green',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const isEditableElement = (node: EventTarget | Element | null): boolean => {
|
|
18
|
+
if (!(node instanceof Element)) return false;
|
|
19
|
+
if (node.matches('input, textarea, select, [role="textbox"]')) return true;
|
|
20
|
+
if (node.closest('[contenteditable]:not([contenteditable="false"])')) return true;
|
|
21
|
+
return (node as HTMLElement).isContentEditable;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
interface AnnotationToolbarProps {
|
|
25
|
+
element: HTMLElement;
|
|
26
|
+
positionMode: PositionMode;
|
|
27
|
+
onAnnotate: (type: AnnotationType) => void;
|
|
28
|
+
onClose: () => void;
|
|
29
|
+
/** Called when user wants to write a comment (opens CommentPopover in parent) */
|
|
30
|
+
onRequestComment?: (initialChar?: string) => void;
|
|
31
|
+
/** Called when a quick label chip is selected */
|
|
32
|
+
onQuickLabel?: (label: QuickLabel) => void;
|
|
33
|
+
/** Text to copy when the button is clicked */
|
|
34
|
+
copyText?: string;
|
|
35
|
+
/** Hide the copy button (set when a keyboard copy handler exists) */
|
|
36
|
+
hideCopyButton?: boolean;
|
|
37
|
+
/** Close toolbar when element scrolls out of viewport */
|
|
38
|
+
closeOnScrollOut?: boolean;
|
|
39
|
+
/** Exit animation state */
|
|
40
|
+
isExiting?: boolean;
|
|
41
|
+
/** Hover callbacks for code block behavior */
|
|
42
|
+
onMouseEnter?: () => void;
|
|
43
|
+
onMouseLeave?: () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
|
|
47
|
+
element,
|
|
48
|
+
positionMode,
|
|
49
|
+
onAnnotate,
|
|
50
|
+
onClose,
|
|
51
|
+
onRequestComment,
|
|
52
|
+
onQuickLabel,
|
|
53
|
+
copyText,
|
|
54
|
+
hideCopyButton = false,
|
|
55
|
+
closeOnScrollOut = false,
|
|
56
|
+
isExiting = false,
|
|
57
|
+
onMouseEnter,
|
|
58
|
+
onMouseLeave,
|
|
59
|
+
}) => {
|
|
60
|
+
const [position, setPosition] = useState<{ top: number; left?: number; right?: number } | null>(null);
|
|
61
|
+
const [copied, setCopied] = useState(false);
|
|
62
|
+
const [showQuickLabels, setShowQuickLabels] = useState(false);
|
|
63
|
+
const toolbarRef = useRef<HTMLDivElement>(null);
|
|
64
|
+
const zapButtonRef = useRef<HTMLButtonElement>(null);
|
|
65
|
+
const quickLabels = useMemo(() => getQuickLabels(), []);
|
|
66
|
+
|
|
67
|
+
useEffect(() => { setCopied(false); }, [element]);
|
|
68
|
+
|
|
69
|
+
const handleCopy = async () => {
|
|
70
|
+
let textToCopy = copyText;
|
|
71
|
+
if (!textToCopy) {
|
|
72
|
+
const codeEl = element.querySelector('code');
|
|
73
|
+
textToCopy = codeEl?.textContent || element.textContent || '';
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
await navigator.clipboard.writeText(textToCopy);
|
|
77
|
+
} catch {
|
|
78
|
+
const textarea = document.createElement('textarea');
|
|
79
|
+
textarea.value = textToCopy;
|
|
80
|
+
textarea.style.cssText = 'position:fixed;opacity:0';
|
|
81
|
+
document.body.appendChild(textarea);
|
|
82
|
+
textarea.select();
|
|
83
|
+
document.execCommand('copy');
|
|
84
|
+
textarea.remove();
|
|
85
|
+
}
|
|
86
|
+
setCopied(true);
|
|
87
|
+
setTimeout(() => setCopied(false), 1500);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Update position on scroll/resize
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const updatePosition = () => {
|
|
93
|
+
const rect = element.getBoundingClientRect();
|
|
94
|
+
|
|
95
|
+
if (closeOnScrollOut && (rect.bottom < 0 || rect.top > window.innerHeight)) {
|
|
96
|
+
onClose();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (positionMode === 'center-above') {
|
|
101
|
+
setPosition({
|
|
102
|
+
top: rect.top - 48,
|
|
103
|
+
left: rect.left + rect.width / 2,
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
setPosition({
|
|
107
|
+
top: rect.top - 40,
|
|
108
|
+
right: window.innerWidth - rect.right,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
updatePosition();
|
|
114
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
115
|
+
window.addEventListener("resize", updatePosition);
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
119
|
+
window.removeEventListener("resize", updatePosition);
|
|
120
|
+
};
|
|
121
|
+
}, [element, positionMode, closeOnScrollOut, onClose]);
|
|
122
|
+
|
|
123
|
+
// Type-to-comment + Alt+N / bare digit quick label shortcuts
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
126
|
+
if (e.isComposing) return;
|
|
127
|
+
if (isEditableElement(e.target) || isEditableElement(document.activeElement)) return;
|
|
128
|
+
|
|
129
|
+
// When picker is open, let FloatingQuickLabelPicker own all keyboard input
|
|
130
|
+
if (showQuickLabels) return;
|
|
131
|
+
|
|
132
|
+
if (e.key === "Escape") {
|
|
133
|
+
onClose();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Alt+N applies quick label (picker closed)
|
|
138
|
+
const isDigit = (e.code >= 'Digit1' && e.code <= 'Digit9') || e.code === 'Digit0';
|
|
139
|
+
if (isDigit && !e.ctrlKey && !e.metaKey && e.altKey) {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
const digit = parseInt(e.code.slice(5), 10);
|
|
142
|
+
const index = digit === 0 ? 9 : digit - 1;
|
|
143
|
+
if (index < quickLabels.length) {
|
|
144
|
+
onQuickLabel?.(quickLabels[index]);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
150
|
+
if (e.key === "Tab" || e.key === "Enter") return;
|
|
151
|
+
if (e.key.length !== 1) return;
|
|
152
|
+
|
|
153
|
+
onRequestComment?.(e.key);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
157
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
158
|
+
}, [onClose, onRequestComment, onQuickLabel, quickLabels, showQuickLabels]);
|
|
159
|
+
|
|
160
|
+
useDismissOnOutsideAndEscape({
|
|
161
|
+
enabled: !showQuickLabels,
|
|
162
|
+
ref: toolbarRef,
|
|
163
|
+
onDismiss: onClose,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!position) return null;
|
|
167
|
+
|
|
168
|
+
const handleTypeSelect = (type: AnnotationType) => {
|
|
169
|
+
if (type === AnnotationType.DELETION) {
|
|
170
|
+
onAnnotate(type);
|
|
171
|
+
} else {
|
|
172
|
+
onRequestComment?.();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const isCentered = position.left !== undefined;
|
|
177
|
+
const translateX = isCentered ? ' translateX(-50%)' : '';
|
|
178
|
+
|
|
179
|
+
const style: React.CSSProperties = {
|
|
180
|
+
top: position.top,
|
|
181
|
+
...(isCentered
|
|
182
|
+
? { left: position.left, transform: 'translateX(-50%)' }
|
|
183
|
+
: { right: position.right }),
|
|
184
|
+
animation: isExiting
|
|
185
|
+
? 'annotation-toolbar-out 0.15s ease-in forwards'
|
|
186
|
+
: 'annotation-toolbar-in 0.15s ease-out',
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
return createPortal(
|
|
190
|
+
<div
|
|
191
|
+
ref={toolbarRef}
|
|
192
|
+
className="annotation-toolbar fixed z-[100] bg-popover border border-border rounded-lg shadow-2xl"
|
|
193
|
+
style={style}
|
|
194
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
195
|
+
onMouseEnter={onMouseEnter}
|
|
196
|
+
onMouseLeave={onMouseLeave}
|
|
197
|
+
>
|
|
198
|
+
<style>{`
|
|
199
|
+
@keyframes annotation-toolbar-in {
|
|
200
|
+
from { opacity: 0; transform: translateY(12px)${translateX}; }
|
|
201
|
+
to { opacity: 1; transform: translateY(0)${translateX}; }
|
|
202
|
+
}
|
|
203
|
+
@keyframes annotation-toolbar-out {
|
|
204
|
+
from { opacity: 1; transform: translateY(0)${translateX}; }
|
|
205
|
+
to { opacity: 0; transform: translateY(8px)${translateX}; }
|
|
206
|
+
}
|
|
207
|
+
`}</style>
|
|
208
|
+
<div className="flex items-center p-1 gap-0.5">
|
|
209
|
+
{!hideCopyButton && (
|
|
210
|
+
<>
|
|
211
|
+
<ToolbarButton
|
|
212
|
+
onClick={handleCopy}
|
|
213
|
+
icon={copied ? <CheckIcon /> : <CopyIcon />}
|
|
214
|
+
label={copied ? "Copied!" : "Copy"}
|
|
215
|
+
className={copied ? "text-success" : "text-muted-foreground hover:bg-muted hover:text-foreground"}
|
|
216
|
+
/>
|
|
217
|
+
<div className="w-px h-5 bg-border mx-0.5" />
|
|
218
|
+
</>
|
|
219
|
+
)}
|
|
220
|
+
<ToolbarButton
|
|
221
|
+
onClick={() => handleTypeSelect(AnnotationType.DELETION)}
|
|
222
|
+
icon={<TrashIcon />}
|
|
223
|
+
label="Delete"
|
|
224
|
+
className="text-destructive hover:bg-destructive/10"
|
|
225
|
+
/>
|
|
226
|
+
<ToolbarButton
|
|
227
|
+
onClick={() => handleTypeSelect(AnnotationType.COMMENT)}
|
|
228
|
+
icon={<CommentIcon />}
|
|
229
|
+
label="Comment"
|
|
230
|
+
className="text-annotation-comment hover:bg-annotation-comment/10"
|
|
231
|
+
/>
|
|
232
|
+
{onQuickLabel && (
|
|
233
|
+
<>
|
|
234
|
+
<ToolbarButton
|
|
235
|
+
ref={zapButtonRef}
|
|
236
|
+
onClick={() => setShowQuickLabels(prev => !prev)}
|
|
237
|
+
icon={<ZapIcon />}
|
|
238
|
+
label="Quick label"
|
|
239
|
+
className={showQuickLabels ? "text-amber-500 bg-amber-500/10" : "text-amber-500 hover:bg-amber-500/10"}
|
|
240
|
+
/>
|
|
241
|
+
<ToolbarButton
|
|
242
|
+
onClick={() => onQuickLabel(THUMBS_UP_LABEL)}
|
|
243
|
+
icon={<span className="block w-4 h-4 text-sm leading-4 text-center">👍</span>}
|
|
244
|
+
label="Looks good"
|
|
245
|
+
className="hover:bg-green-500/10"
|
|
246
|
+
/>
|
|
247
|
+
{showQuickLabels && zapButtonRef.current && (
|
|
248
|
+
<FloatingQuickLabelPicker
|
|
249
|
+
anchorEl={zapButtonRef.current}
|
|
250
|
+
onSelect={(label) => {
|
|
251
|
+
setShowQuickLabels(false);
|
|
252
|
+
onQuickLabel(label);
|
|
253
|
+
}}
|
|
254
|
+
onDismiss={() => setShowQuickLabels(false)}
|
|
255
|
+
/>
|
|
256
|
+
)}
|
|
257
|
+
</>
|
|
258
|
+
)}
|
|
259
|
+
<div className="w-px h-5 bg-border mx-0.5" />
|
|
260
|
+
<ToolbarButton
|
|
261
|
+
onClick={onClose}
|
|
262
|
+
icon={<CloseIcon />}
|
|
263
|
+
label="Cancel"
|
|
264
|
+
className="text-muted-foreground hover:bg-muted"
|
|
265
|
+
/>
|
|
266
|
+
</div>
|
|
267
|
+
</div>,
|
|
268
|
+
document.body
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Icons
|
|
273
|
+
const CopyIcon = () => (
|
|
274
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
275
|
+
<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" />
|
|
276
|
+
</svg>
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const CheckIcon = () => (
|
|
280
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
281
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
282
|
+
</svg>
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const TrashIcon = () => (
|
|
286
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
287
|
+
<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" />
|
|
288
|
+
</svg>
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const CommentIcon = () => (
|
|
292
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
293
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
|
|
294
|
+
</svg>
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const ZapIcon = () => (
|
|
298
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
299
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
300
|
+
</svg>
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const CloseIcon = () => (
|
|
304
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
305
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
306
|
+
</svg>
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const ToolbarButton = React.forwardRef<HTMLButtonElement, {
|
|
310
|
+
onClick: () => void;
|
|
311
|
+
icon: React.ReactNode;
|
|
312
|
+
label: string;
|
|
313
|
+
className: string;
|
|
314
|
+
}>(({ onClick, icon, label, className }, ref) => (
|
|
315
|
+
<button
|
|
316
|
+
ref={ref}
|
|
317
|
+
onClick={onClick}
|
|
318
|
+
title={label}
|
|
319
|
+
className={`p-1.5 rounded-md transition-colors ${className}`}
|
|
320
|
+
>
|
|
321
|
+
{icon}
|
|
322
|
+
</button>
|
|
323
|
+
));
|