Skip to content

Commit eccbc6f

Browse files
backnotpropclaude
andcommitted
UX improvements for annotations and feedback flow
- Global comment input now multiline (textarea with auto-expand) - Enter submits, Shift+Enter for newlines (all annotation inputs) - Buttons stay at top when textarea expands (items-start) - Rename "Provide Feedback" to "Deny with Feedback" - Dim approve button when Claude Code + annotations exist - Add CSS tooltip warning about annotations not sent on approve - Add image attachments to code block toolbar - Lock code block toolbar open when in input mode (fixes dialog closing) - Rename "Images" to "Attach" with descriptive tooltip 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1f8c36b commit eccbc6f

4 files changed

Lines changed: 93 additions & 47 deletions

File tree

packages/editor/App.tsx

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -558,26 +558,37 @@ const App: React.FC = () => {
558558
? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
559559
: 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/30'
560560
}`}
561-
title="Provide Feedback"
561+
title="Deny with Feedback"
562562
>
563563
<svg className="w-4 h-4 md:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
564564
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
565565
</svg>
566-
<span className="hidden md:inline">{isSubmitting ? 'Sending...' : 'Provide Feedback'}</span>
566+
<span className="hidden md:inline">{isSubmitting ? 'Sending...' : 'Deny with Feedback'}</span>
567567
</button>
568568

569-
<button
570-
onClick={handleApprove}
571-
disabled={isSubmitting}
572-
className={`px-2 py-1 md:px-2.5 rounded-md text-xs font-medium transition-all ${
573-
isSubmitting
574-
? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
575-
: 'bg-green-600 text-white hover:bg-green-500'
576-
}`}
577-
>
578-
<span className="md:hidden">{isSubmitting ? '...' : 'OK'}</span>
579-
<span className="hidden md:inline">{isSubmitting ? 'Approving...' : 'Approve'}</span>
580-
</button>
569+
<div className="relative group/approve">
570+
<button
571+
onClick={handleApprove}
572+
disabled={isSubmitting}
573+
className={`px-2 py-1 md:px-2.5 rounded-md text-xs font-medium transition-all ${
574+
isSubmitting
575+
? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
576+
: origin === 'claude-code' && annotations.length > 0
577+
? 'bg-green-600/50 text-white/70 hover:bg-green-600 hover:text-white'
578+
: 'bg-green-600 text-white hover:bg-green-500'
579+
}`}
580+
>
581+
<span className="md:hidden">{isSubmitting ? '...' : 'OK'}</span>
582+
<span className="hidden md:inline">{isSubmitting ? 'Approving...' : 'Approve'}</span>
583+
</button>
584+
{origin === 'claude-code' && annotations.length > 0 && (
585+
<div className="absolute top-full right-0 mt-2 px-3 py-2 bg-popover border border-border rounded-lg shadow-xl text-xs text-foreground w-56 text-center opacity-0 invisible group-hover/approve:opacity-100 group-hover/approve:visible transition-all pointer-events-none z-50">
586+
<div className="absolute bottom-full right-4 border-4 border-transparent border-b-border" />
587+
<div className="absolute bottom-full right-4 mt-px border-4 border-transparent border-b-popover" />
588+
Claude Code doesn't support feedback on approval. Your annotations won't be seen.
589+
</div>
590+
)}
591+
</div>
581592

582593
<div className="w-px h-5 bg-border/50 mx-1 hidden md:block" />
583594
</>

packages/ui/components/AttachmentsButton.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,9 @@ export const AttachmentsButton: React.FC<AttachmentsButtonProps> = ({
213213
</span>
214214
)}
215215
</div>
216+
<p className="text-[11px] text-muted-foreground -mt-1">
217+
Add images to include with your feedback
218+
</p>
216219

217220
{/* Drop zone / file picker */}
218221
<div
@@ -300,13 +303,16 @@ export const AttachmentsButton: React.FC<AttachmentsButtonProps> = ({
300303
document.body
301304
)}
302305

303-
{/* Image Annotator Dialog */}
304-
<ImageAnnotator
305-
isOpen={!!annotatorImage || !!editingPath}
306-
imageSrc={annotatorImage?.blobUrl ?? (editingPath ? getImageSrc(editingPath) : '')}
307-
onAccept={handleAnnotatorAccept}
308-
onClose={handleAnnotatorClose}
309-
/>
306+
{/* Image Annotator Dialog - portaled to body for correct positioning */}
307+
{(!!annotatorImage || !!editingPath) && createPortal(
308+
<ImageAnnotator
309+
isOpen={!!annotatorImage || !!editingPath}
310+
imageSrc={annotatorImage?.blobUrl ?? (editingPath ? getImageSrc(editingPath) : '')}
311+
onAccept={handleAnnotatorAccept}
312+
onClose={handleAnnotatorClose}
313+
/>,
314+
document.body
315+
)}
310316
</>
311317
);
312318
};

packages/ui/components/Toolbar.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({
8787

8888
const handleSubmit = (e: React.FormEvent) => {
8989
e.preventDefault();
90-
if (activeType && inputValue.trim()) {
91-
onAnnotate(activeType, inputValue, imagePaths.length > 0 ? imagePaths : undefined);
90+
if (activeType && (inputValue.trim() || imagePaths.length > 0)) {
91+
onAnnotate(activeType, inputValue || undefined, imagePaths.length > 0 ? imagePaths : undefined);
9292
}
9393
};
9494

@@ -177,14 +177,12 @@ export const Toolbar: React.FC<ToolbarProps> = ({
177177
onChange={(e) => setInputValue(e.target.value)}
178178
onKeyDown={(e) => {
179179
if (e.key === "Escape") setStep("menu");
180-
// Cmd/Ctrl+Enter to submit
181-
if (
182-
e.key === "Enter" &&
183-
(e.metaKey || e.ctrlKey) &&
184-
inputValue.trim()
185-
) {
180+
// Enter to submit, Shift+Enter for newline
181+
if (e.key === "Enter" && !e.shiftKey) {
186182
e.preventDefault();
187-
onAnnotate(activeType!, inputValue, imagePaths.length > 0 ? imagePaths : undefined);
183+
if (inputValue.trim() || imagePaths.length > 0) {
184+
onAnnotate(activeType!, inputValue || undefined, imagePaths.length > 0 ? imagePaths : undefined);
185+
}
188186
}
189187
}}
190188
/>
@@ -196,7 +194,7 @@ export const Toolbar: React.FC<ToolbarProps> = ({
196194
/>
197195
<button
198196
type="submit"
199-
disabled={!inputValue.trim()}
197+
disabled={!inputValue.trim() && imagePaths.length === 0}
200198
className="px-[15px] py-1 text-xs font-medium rounded bg-primary text-primary-foreground hover:opacity-90 disabled:opacity-50 transition-opacity self-stretch"
201199
>
202200
Save

packages/ui/components/Viewer.tsx

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
4545
const [copied, setCopied] = useState(false);
4646
const [showGlobalCommentInput, setShowGlobalCommentInput] = useState(false);
4747
const [globalCommentValue, setGlobalCommentValue] = useState('');
48-
const globalCommentInputRef = useRef<HTMLInputElement>(null);
48+
const globalCommentInputRef = useRef<HTMLTextAreaElement>(null);
4949

5050
const handleCopyPlan = async () => {
5151
try {
@@ -90,6 +90,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
9090
const [toolbarState, setToolbarState] = useState<{ element: HTMLElement; source: any } | null>(null);
9191
const [hoveredCodeBlock, setHoveredCodeBlock] = useState<{ block: Block; element: HTMLElement } | null>(null);
9292
const [isCodeBlockToolbarExiting, setIsCodeBlockToolbarExiting] = useState(false);
93+
const [isCodeBlockToolbarLocked, setIsCodeBlockToolbarLocked] = useState(false);
9394
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
9495

9596
// Keep refs in sync with props
@@ -451,7 +452,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
451452
window.getSelection()?.removeAllRanges();
452453
};
453454

454-
const handleCodeBlockAnnotate = (type: AnnotationType, text?: string) => {
455+
const handleCodeBlockAnnotate = (type: AnnotationType, text?: string, imagePaths?: string[]) => {
455456
const highlighter = highlighterRef.current;
456457
if (!hoveredCodeBlock || !highlighter) return;
457458

@@ -499,17 +500,20 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
499500
originalText: codeText,
500501
createdA: Date.now(),
501502
author: getIdentity(),
503+
imagePaths,
502504
};
503505

504506
onAddAnnotationRef.current(newAnnotation);
505507

506508
// Clear selection
507509
selection?.removeAllRanges();
508510
setHoveredCodeBlock(null);
511+
setIsCodeBlockToolbarLocked(false);
509512
};
510513

511514
const handleCodeBlockToolbarClose = () => {
512515
setHoveredCodeBlock(null);
516+
setIsCodeBlockToolbarLocked(false);
513517
};
514518

515519
return (
@@ -520,7 +524,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
520524
className="w-full max-w-3xl bg-card border border-border/50 rounded-xl shadow-xl p-5 md:p-10 lg:p-14 relative"
521525
>
522526
{/* Header buttons */}
523-
<div className="absolute top-3 right-3 md:top-5 md:right-5 flex items-center gap-2">
527+
<div className="absolute top-3 right-3 md:top-5 md:right-5 flex items-start gap-2">
524528
{/* Attachments button */}
525529
{onAddGlobalAttachment && onRemoveGlobalAttachment && (
526530
<AttachmentsButton
@@ -538,12 +542,13 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
538542
e.preventDefault();
539543
handleAddGlobalComment();
540544
}}
541-
className="flex items-center gap-1.5 bg-muted/80 rounded-md p-1"
545+
className="flex items-start gap-1.5 bg-muted/80 rounded-md p-1"
542546
>
543-
<input
547+
<textarea
544548
ref={globalCommentInputRef}
545-
type="text"
546-
className="bg-transparent border-none outline-none text-xs w-40 md:w-56 px-2 placeholder:text-muted-foreground"
549+
rows={1}
550+
className="bg-transparent text-xs min-w-40 md:min-w-56 max-w-80 max-h-32 placeholder:text-muted-foreground resize-none px-2 py-1.5 focus:outline-none"
551+
style={{ fieldSizing: 'content' } as React.CSSProperties}
547552
placeholder="Add a global comment..."
548553
value={globalCommentValue}
549554
onChange={(e) => setGlobalCommentValue(e.target.value)}
@@ -552,12 +557,19 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
552557
setShowGlobalCommentInput(false);
553558
setGlobalCommentValue('');
554559
}
560+
// Enter to submit, Shift+Enter for newline
561+
if (e.key === 'Enter' && !e.shiftKey) {
562+
e.preventDefault();
563+
if (globalCommentValue.trim()) {
564+
handleAddGlobalComment();
565+
}
566+
}
555567
}}
556568
/>
557569
<button
558570
type="submit"
559571
disabled={!globalCommentValue.trim()}
560-
className="px-2 py-1 text-xs font-medium rounded bg-purple-600 text-white hover:bg-purple-500 disabled:opacity-50 transition-all"
572+
className="self-start px-2 py-1.5 text-xs font-medium rounded bg-purple-600 text-white hover:bg-purple-500 disabled:opacity-50 transition-all"
561573
>
562574
Add
563575
</button>
@@ -567,7 +579,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
567579
setShowGlobalCommentInput(false);
568580
setGlobalCommentValue('');
569581
}}
570-
className="p-1 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
582+
className="self-start p-1 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
571583
>
572584
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
573585
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
@@ -667,6 +679,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
667679
setIsCodeBlockToolbarExiting(false);
668680
}}
669681
onMouseLeave={() => {
682+
// Don't close if toolbar is locked (in input mode)
683+
if (isCodeBlockToolbarLocked) return;
670684
hoverTimeoutRef.current = setTimeout(() => {
671685
setIsCodeBlockToolbarExiting(true);
672686
setTimeout(() => {
@@ -675,6 +689,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
675689
}, 150);
676690
}, 100);
677691
}}
692+
onLockChange={setIsCodeBlockToolbarLocked}
678693
/>
679694
)}
680695
</article>
@@ -957,21 +972,28 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ block, onHover, onLeave, isHovere
957972

958973
const CodeBlockToolbar: React.FC<{
959974
element: HTMLElement;
960-
onAnnotate: (type: AnnotationType, text?: string) => void;
975+
onAnnotate: (type: AnnotationType, text?: string, imagePaths?: string[]) => void;
961976
onClose: () => void;
962977
isExiting: boolean;
963978
onMouseEnter: () => void;
964979
onMouseLeave: () => void;
965-
}> = ({ element, onAnnotate, onClose, isExiting, onMouseEnter, onMouseLeave }) => {
980+
onLockChange?: (locked: boolean) => void;
981+
}> = ({ element, onAnnotate, onClose, isExiting, onMouseEnter, onMouseLeave, onLockChange }) => {
966982
const [step, setStep] = useState<'menu' | 'input'>('menu');
967983
const [inputValue, setInputValue] = useState('');
984+
const [imagePaths, setImagePaths] = useState<string[]>([]);
968985
const [position, setPosition] = useState<{ top: number; right: number }>({ top: 0, right: 0 });
969986
const inputRef = useRef<HTMLTextAreaElement>(null);
970987

971988
useEffect(() => {
972989
if (step === 'input') inputRef.current?.focus();
973990
}, [step]);
974991

992+
// Notify parent when locked (in input mode)
993+
useEffect(() => {
994+
onLockChange?.(step === 'input');
995+
}, [step, onLockChange]);
996+
975997
// Update position on scroll/resize
976998
useEffect(() => {
977999
const updatePosition = () => {
@@ -996,8 +1018,8 @@ const CodeBlockToolbar: React.FC<{
9961018

9971019
const handleSubmit = (e: React.FormEvent) => {
9981020
e.preventDefault();
999-
if (inputValue.trim()) {
1000-
onAnnotate(AnnotationType.COMMENT, inputValue);
1021+
if (inputValue.trim() || imagePaths.length > 0) {
1022+
onAnnotate(AnnotationType.COMMENT, inputValue || undefined, imagePaths.length > 0 ? imagePaths : undefined);
10011023
}
10021024
};
10031025

@@ -1078,15 +1100,24 @@ const CodeBlockToolbar: React.FC<{
10781100
onChange={e => setInputValue(e.target.value)}
10791101
onKeyDown={e => {
10801102
if (e.key === 'Escape') setStep('menu');
1081-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && inputValue.trim()) {
1103+
// Enter to submit, Shift+Enter for newline
1104+
if (e.key === 'Enter' && !e.shiftKey) {
10821105
e.preventDefault();
1083-
onAnnotate(AnnotationType.COMMENT, inputValue);
1106+
if (inputValue.trim() || imagePaths.length > 0) {
1107+
onAnnotate(AnnotationType.COMMENT, inputValue || undefined, imagePaths.length > 0 ? imagePaths : undefined);
1108+
}
10841109
}
10851110
}}
10861111
/>
1112+
<AttachmentsButton
1113+
paths={imagePaths}
1114+
onAdd={(path) => setImagePaths(prev => [...prev, path])}
1115+
onRemove={(path) => setImagePaths(prev => prev.filter(p => p !== path))}
1116+
variant="inline"
1117+
/>
10871118
<button
10881119
type="submit"
1089-
disabled={!inputValue.trim()}
1120+
disabled={!inputValue.trim() && imagePaths.length === 0}
10901121
className="px-[15px] py-1 text-xs font-medium rounded bg-primary text-primary-foreground hover:opacity-90 disabled:opacity-50 transition-opacity self-stretch"
10911122
>
10921123
Save

0 commit comments

Comments
 (0)