Files
Michael Ramos e20fd97be7 Consumer enablement for @plannotator/ui (0.24.0) (#1017)
* feat(ui): AnnotationPanel renderCardFooter + readOnly host props

Per-card footer slot for host reply/resolve UI (clicks inside don't
select the card); readOnly hides delete/edit on all card kinds.
Both optional, both no-op by default — Plannotator unchanged.

* feat(ui): bless 6 exports into the strict-consumer surface

TableOfContents, ResizeHandle, useResizablePanel, useActiveSection,
useScrollViewport, utils/annotationHelpers — verified strict-clean and
backend-free (useResizablePanel persists via the storageBackend seam);
added to the gate and the HANDOFF supported-imports table.

* feat(ui): Viewer allowImages + readOnly host props

allowImages threads to both CommentPopover sites (popover already gated
its attach affordance; Viewer never exposed the knob). readOnly
suppresses every composer entry point — selection toolbar (highlighter
'enabled'), pinpoint, global comment, attachments, checkbox toggles —
while existing annotations still render and select.
Both default to today's behavior.

* fix(ui): lazy-import Viewer/CommentPopover in the consumer test (DOM-less bun test crashed on web-highlighter)

* feat(ui): strict-consumer gate gains verbatimModuleSyntax + noUnusedLocals/Parameters

Fixed the 24 violations the flags surfaced across the supported-import
graph: type-only imports (verbatimModuleSyntax) and dead imports/locals.
Consumers with stricter tsconfigs no longer have to relax them.

* feat(ui): opt-in content-verifying annotation restore

verifyRestoredContent on useAnnotationHighlighter: after a meta-based
fromStore restore, the painted text is checked against originalText
(whitespace-normalized). Mismatch -> highlight removed, text-search
fallback re-anchors; if that fails too, onRestoreMismatch(annotation,
restoredText) fires and nothing is painted. Default off — today's
trust-the-positions behavior. Workspaces hit this live after document
drift; correctness upgrade for every consumer.

* chore(ui): 0.24.0 — HANDOFF consumer-enablement notes + version bump
2026-07-07 19:09:52 -07:00

69 lines
2.9 KiB
TypeScript

import React from 'react';
import type { EditorAnnotation } from '../types';
import { cn } from '../lib/utils';
// EditorAnnotationCard is SHARED across surfaces:
// - the plan/annotate AnnotationPanel (flat surface-1 cards), and
// - the code-review ReviewSidebar (bordered cards, sitting next to
// renderAnnotationCard's `p-2.5 rounded border border-transparent
// hover:bg-muted/30` code cards).
// The `variant` prop keeps each surface visually cohesive:
// - 'plan' → flat surface-1 hover (matches the plan AnnotationCard restyle)
// - 'code-review' → bordered + muted hover (matches code-review's code cards)
type EditorAnnotationVariant = 'plan' | 'code-review';
interface EditorAnnotationCardProps {
annotation: EditorAnnotation;
/** Omit to render the card read-only (no delete affordance). */
onDelete?: () => void;
variant?: EditorAnnotationVariant;
}
export const EditorAnnotationCard: React.FC<EditorAnnotationCardProps> = ({ annotation, onDelete, variant = 'plan' }) => {
const lineRange = annotation.lineStart === annotation.lineEnd
? `L${annotation.lineStart}`
: `L${annotation.lineStart}-${annotation.lineEnd}`;
return (
<div
className={cn(
'group w-full text-left transition-colors duration-150',
variant === 'code-review'
? 'relative p-2.5 rounded border border-transparent hover:bg-muted/30'
: 'rounded-lg px-3 py-2.5 hover:bg-surface-1/50',
)}
>
{/* Header: type word + file:line + delete */}
<div className="mb-1.5 flex items-center gap-1.5">
<span className="text-[11px] font-medium text-amber-600 dark:text-amber-400">Editor</span>
<span className="text-[10px] font-mono text-muted-foreground/50 truncate" title={annotation.filePath}>
{annotation.filePath}:{lineRange}
</span>
{onDelete && (
<button
onClick={(e) => { e.stopPropagation(); onDelete(); }}
className="ml-auto relative rounded-md p-1.5 text-muted-foreground transition-colors before:absolute before:-inset-1.5 before:content-[''] opacity-0 group-hover:opacity-100 [@media(hover:none)]:opacity-100 hover:text-destructive"
title="Delete annotation"
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
{/* Selected text */}
<p className="mb-1.5 line-clamp-2 whitespace-pre-wrap font-mono text-[11px] leading-relaxed text-muted-foreground/80">
{annotation.selectedText}
</p>
{/* Comment */}
{annotation.comment && (
<p className="whitespace-pre-wrap text-[13px] leading-relaxed text-foreground/90">
{annotation.comment}
</p>
)}
</div>
);
};