Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions packages/editor/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useLayoutEffect, useMemo, useRef, useCallback } from 'react';
import { CopyIcon } from '@plannotator/ui/components/icons/copyIcons';
import { type Origin, getAgentName } from '@plannotator/shared/agents';
import { parseMarkdownToBlocks, exportAnnotations, exportLinkedDocAnnotations, exportEditorAnnotations, extractFrontmatter, wrapFeedbackForAgent, Frontmatter } from '@plannotator/ui/utils/parser';
import { Viewer, ViewerHandle } from '@plannotator/ui/components/Viewer';
Expand Down Expand Up @@ -1279,9 +1280,7 @@ const App: React.FC = () => {
title="Copy plan content"
>
<span className="hidden md:inline">Copy</span>
<svg className="w-4 h-4 md:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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" />
</svg>
<CopyIcon className="w-4 h-4 md:hidden" />
</button>
<button
onClick={archive.done}
Expand Down
72 changes: 3 additions & 69 deletions packages/review-editor/components/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,3 @@
import type React from 'react';
import { useState } from 'react';

interface CopyButtonProps {
text: string;
className?: string;
/** "overlay" (default): absolute-positioned, hover-reveal (parent needs group relative).
* "inline": normal flow, always visible, compact. */
variant?: 'overlay' | 'inline';
/** Optional label shown next to the icon (inline variant only). */
label?: string;
}

const CopyIcon = () => (
<svg aria-hidden="true" className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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" />
</svg>
);

const CheckIcon = () => (
<svg aria-hidden="true" className="w-3 h-3 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
);

/** Copy-to-clipboard button with "Copied" flash. */
export const CopyButton: React.FC<CopyButtonProps> = ({ text, className = '', variant = 'overlay', label }) => {
const [copied, setCopied] = useState(false);

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
// Clipboard API may not be available
}
};

if (variant === 'inline') {
return (
<button
type="button"
onClick={handleCopy}
className={`flex items-center gap-1 p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors ${className}`}
title={copied ? 'Copied!' : 'Copy'}
>
{copied ? <CheckIcon /> : <CopyIcon />}
{label && (
<span className="text-[10px]">
{copied ? 'Copied' : label}
</span>
)}
</button>
);
}

return (
<button
type="button"
onClick={handleCopy}
className={`absolute top-1.5 right-1.5 p-1 rounded-md opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground hover:bg-muted ${className}`}
title={copied ? 'Copied!' : 'Copy'}
>
{copied ? <CheckIcon /> : <CopyIcon />}
</button>
);
};
// Re-exported from @plannotator/ui for backwards compatibility.
// The canonical implementation now lives in packages/ui/components/CopyButton.tsx.
export { CopyButton } from '@plannotator/ui/components/CopyButton';
22 changes: 6 additions & 16 deletions packages/review-editor/components/FileHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import { CopyIcon, CheckIcon } from '@plannotator/ui/components/icons/copyIcons';
import { useCopyToClipboard } from '@plannotator/ui/hooks/useCopyToClipboard';

interface FileHeaderProps {
filePath: string;
Expand Down Expand Up @@ -43,7 +45,7 @@ export const FileHeader: React.FC<FileHeaderProps> = ({
stageError,
onFileComment,
}) => {
const [copied, setCopied] = useState(false);
const { copied, copy } = useCopyToClipboard();
const [headerWidth, setHeaderWidth] = useState<number>(0);
const headerRef = useRef<HTMLDivElement>(null);
const fileCommentRef = useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -168,30 +170,18 @@ export const FileHeader: React.FC<FileHeaderProps> = ({
</button>
)}
<button
onClick={async () => {
try {
await navigator.clipboard.writeText(patch);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
}}
onClick={() => copy(patch)}
className={`text-xs text-muted-foreground hover:text-foreground rounded hover:bg-muted transition-colors flex items-center ${copyLabel ? 'gap-1 px-2 py-1' : 'px-1.5 py-1'}`}
title="Copy this file's diff"
>
{copied ? (
<>
<svg className="w-3.5 h-3.5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
<CheckIcon className="w-3.5 h-3.5 text-success" />
{copyLabel && <span>{copyLabel}</span>}
</>
) : (
<>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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" />
</svg>
<CopyIcon className="w-3.5 h-3.5" />
{copyLabel && <span>{copyLabel}</span>}
</>
)}
Expand Down
9 changes: 3 additions & 6 deletions packages/review-editor/components/FileTree.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useEffect, useCallback, useState, useMemo } from 'react';
import { CopyIcon, CheckIcon } from '@plannotator/ui/components/icons/copyIcons';
import { CodeAnnotation } from '@plannotator/ui/types';
import type { DiffOption, WorktreeInfo } from '@plannotator/shared/types';
import { buildFileTree, getAncestorPaths, getAllFolderPaths } from '../utils/buildFileTree';
Expand Down Expand Up @@ -408,17 +409,13 @@ export const FileTree: React.FC<FileTreeProps> = ({
title="Copy all raw diffs to clipboard (Cmd/Ctrl+Shift+C)"
>
{copyRawDiffStatus === 'success' ? (
<svg className="w-3 h-3 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
<CheckIcon className="w-3 h-3 text-success" />
) : copyRawDiffStatus === 'error' ? (
<svg className="w-3 h-3 text-destructive" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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" />
</svg>
<CopyIcon />
)}
{copyRawDiffStatus === 'success' ? 'Copied' : copyRawDiffStatus === 'error' ? 'Failed' : 'Copy diffs'}
</button>
Expand Down
20 changes: 6 additions & 14 deletions packages/review-editor/components/ReviewSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, { useState, useEffect } from 'react';
import { CopyIcon, CheckIcon } from '@plannotator/ui/components/icons/copyIcons';
import { useCopyToClipboard } from '@plannotator/ui/hooks/useCopyToClipboard';
import { CodeAnnotation, type EditorAnnotation } from '@plannotator/ui/types';
import { isCurrentUser } from '@plannotator/ui/utils/identity';
import { EditorAnnotationCard } from '@plannotator/ui/components/EditorAnnotationCard';
Expand Down Expand Up @@ -148,7 +150,7 @@ export const ReviewSidebar: React.FC<ReviewSidebarProps> = /* React.memo */({
onOpenPRPanel,
}) => {
const totalCount = annotations.length + (editorAnnotations?.length ?? 0);
const [copied, setCopied] = useState(false);
const { copied, copy } = useCopyToClipboard();
const [activeTab, setActiveTab] = useState<ReviewSidebarTab>('annotations');
const hasAgents = REVIEW_AGENTS_ENABLED && !!agentCapabilities?.available;
const runningAgentCount = (agentJobs ?? []).filter(j => j.status === 'running' || j.status === 'starting').length;
Expand Down Expand Up @@ -176,13 +178,7 @@ export const ReviewSidebar: React.FC<ReviewSidebarProps> = /* React.memo */({

const handleQuickCopy = async () => {
if (!feedbackMarkdown) return;
try {
await navigator.clipboard.writeText(feedbackMarkdown);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (e) {
console.error('Failed to copy:', e);
}
await copy(feedbackMarkdown);
};

// Group annotations by file
Expand Down Expand Up @@ -451,16 +447,12 @@ export const ReviewSidebar: React.FC<ReviewSidebarProps> = /* React.memo */({
>
{copied ? (
<>
<svg className="w-3.5 h-3.5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
<CheckIcon className="w-3.5 h-3.5 text-success" />
Copied
</>
) : (
<>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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" />
</svg>
<CopyIcon className="w-3.5 h-3.5" />
Copy Feedback
</>
)}
Expand Down
11 changes: 4 additions & 7 deletions packages/ui/components/AnnotationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ImageThumbnail } from './ImageThumbnail';
import { EditorAnnotationCard } from './EditorAnnotationCard';
import { useIsMobile } from '../hooks/useIsMobile';
import { OverlayScrollArea } from './OverlayScrollArea';
import { CopyIcon, CheckIcon } from './icons/copyIcons';

interface PanelProps {
isOpen: boolean;
Expand Down Expand Up @@ -159,22 +160,18 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
onClick={async () => {
await onQuickCopy();
setCopiedText(true);
setTimeout(() => setCopiedText(false), 2000);
setTimeout(() => setCopiedText(false), 1500);
}}
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-xs font-medium transition-all text-muted-foreground hover:text-foreground hover:bg-muted/50"
>
{copiedText ? (
<>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
<CheckIcon className="w-3.5 h-3.5" />
Copied
</>
) : (
<>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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" />
</svg>
<CopyIcon className="w-3.5 h-3.5" />
Copy
</>
)}
Expand Down
26 changes: 7 additions & 19 deletions packages/ui/components/AnnotationToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { createPortal } from "react-dom";
import { useDismissOnOutsideAndEscape } from "../hooks/useDismissOnOutsideAndEscape";
import { type QuickLabel, getQuickLabels } from "../utils/quickLabels";
import { FloatingQuickLabelPicker } from "./FloatingQuickLabelPicker";
import { CopyIcon, CheckIcon } from "./icons/copyIcons";
import { useCopyToClipboard } from "../hooks/useCopyToClipboard";

type PositionMode = 'center-above' | 'top-right';

Expand Down Expand Up @@ -48,7 +50,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
onMouseLeave,
}) => {
const [position, setPosition] = useState<{ top: number; left?: number; right?: number } | null>(null);
const [copied, setCopied] = useState(false);
const { copied, copy, reset: resetCopied } = useCopyToClipboard();
const [showQuickLabels, setShowQuickLabels] = useState(false);
const toolbarRef = useRef<HTMLDivElement>(null);
const zapButtonRef = useRef<HTMLButtonElement>(null);
Expand All @@ -60,15 +62,13 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
const codeEl = element.querySelector('code');
textToCopy = codeEl?.textContent || element.textContent || '';
}
await navigator.clipboard.writeText(textToCopy);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
await copy(textToCopy);
};

// Reset copied state when element changes
useEffect(() => {
setCopied(false);
}, [element]);
resetCopied();
}, [element, resetCopied]);

// Update position on scroll/resize
useEffect(() => {
Expand Down Expand Up @@ -191,7 +191,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
<div className="flex items-center p-1 gap-0.5">
<ToolbarButton
onClick={handleCopy}
icon={copied ? <CheckIcon /> : <CopyIcon />}
icon={copied ? <CheckIcon className="w-4 h-4" /> : <CopyIcon className="w-4 h-4" />}
label={copied ? "Copied!" : "Copy"}
className={copied ? "text-success" : "text-muted-foreground hover:bg-muted hover:text-foreground"}
/>
Expand Down Expand Up @@ -243,18 +243,6 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
};

// Icons
const CopyIcon = () => (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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" />
</svg>
);

const CheckIcon = () => (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
);

const TrashIcon = () => (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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" />
Expand Down
52 changes: 52 additions & 0 deletions packages/ui/components/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import { CopyIcon, CheckIcon } from './icons/copyIcons';
import { useCopyToClipboard } from '../hooks/useCopyToClipboard';

interface CopyButtonProps {
text: string;
className?: string;
/** "overlay" (default): absolute-positioned, hover-reveal (parent needs group relative).
* "inline": normal flow, always visible, compact. */
variant?: 'overlay' | 'inline';
/** Optional label shown next to the icon (inline variant only). */
label?: string;
}

/** Copy-to-clipboard button with "Copied" flash. */
export const CopyButton: React.FC<CopyButtonProps> = ({ text, className = '', variant = 'overlay', label }) => {
const { copied, copy } = useCopyToClipboard();

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
await copy(text);
};

if (variant === 'inline') {
return (
<button
type="button"
onClick={handleCopy}
className={`flex items-center gap-1 p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors ${className}`}
title={copied ? 'Copied!' : 'Copy'}
>
{copied ? <CheckIcon className="w-3 h-3 text-success" /> : <CopyIcon />}
{label && (
<span className="text-[10px]">
{copied ? 'Copied' : label}
</span>
)}
</button>
);
}

return (
<button
type="button"
onClick={handleCopy}
className={`absolute top-1.5 right-1.5 p-1 rounded-md opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground hover:bg-muted ${className}`}
title={copied ? 'Copied!' : 'Copy'}
>
{copied ? <CheckIcon className="w-3 h-3 text-success" /> : <CopyIcon />}
</button>
);
};
Loading