/** * DocBadges — repo / branch / plan-diff / demo / archive / linked-doc badge cluster. * * Extracted from Viewer.tsx so the same markup can render in two places: * - layout="column": original location at the top-left of the plan card (absolute) * - layout="row": compact context inside the legacy sticky header lane * - layout="header": complete context inside Viewer's in-flow shared header * * In row layout, the demo badge and linked-doc breadcrumb are dropped — * everything else is decorative top-of-doc context except the plan-diff * badge, which is NOT suppressed by linkedDocInfo: `planDiffStats`/ * `hasPreviousVersion` already arrive pre-scoped to whichever document is * currently active (root, or a folder/linked doc with its own diff * baseline), so it renders in linked-doc mode exactly when that document * actually has a previous version to diff against. */ import React from 'react'; import { PlanDiffBadge } from './plan-diff/PlanDiffBadge'; import type { PlanDiffStats } from '../utils/planDiffEngine'; import { hostnameOrFallback } from '@plannotator/core/project'; import { OpenInAppButton } from './OpenInAppButton'; export interface LinkedDocBadgeInfo { filepath: string; /** Leave the document. Optional for `folder-file`: absent shows no Close * (a file of a review of several files, which the switcher moves between). */ onBack?: () => void; label?: string; backLabel?: string; variant?: 'breadcrumb' | 'folder-file'; } export interface DocBadgesProps { layout: 'column' | 'row' | 'header'; repoInfo?: { display: string; branch?: string } | null; planDiffStats?: PlanDiffStats | null; isPlanDiffActive?: boolean; hasPreviousVersion?: boolean; onPlanDiffToggle?: () => void; /** Baseline suffix + tooltip for the plan-diff badge — annotate/folder * sessions pass "since last review" so the badge's numbers read against * the right baseline. See PlanDiffBadge. Plan review passes nothing. */ planDiffBaselineLabel?: string; planDiffBaselineTooltip?: string; showDemoBadge?: boolean; archiveInfo?: { status: 'approved' | 'denied' | 'unknown'; timestamp: string; title: string } | null; linkedDocInfo?: LinkedDocBadgeInfo | null; /** Source attribution for HTML/URL annotations (e.g. "https://..." or "index.html") */ sourceInfo?: string; /** * Absolute on-disk path of the annotated source file, used by the * Open-in-app control. Omitted (or an https:// URL) -> no control rendered. */ openInAppPath?: string | null; } export const DocBadges: React.FC = ({ layout, repoInfo, planDiffStats, isPlanDiffActive, hasPreviousVersion, onPlanDiffToggle, planDiffBaselineLabel, planDiffBaselineTooltip, showDemoBadge, archiveInfo, linkedDocInfo, sourceInfo, openInAppPath, }) => { const isRow = layout === 'row'; const isHorizontal = layout !== 'column'; const canOpenInApp = !!openInAppPath && !/^https?:\/\//i.test(openInAppPath); const openInButton = canOpenInApp ? ( ) : null; // In row layout, only PlanDiffBadge (when it has stats to show) and // archiveInfo (outside linked-doc mode) actually render — everything else // is hidden. Check what will truly produce visible output to avoid an // empty wrapper div. PlanDiffBadge is not linkedDocInfo-gated — see the // header comment. const anything = isRow ? (hasPreviousVersion && planDiffStats) || (archiveInfo && !linkedDocInfo) : repoInfo || hasPreviousVersion || showDemoBadge || linkedDocInfo || archiveInfo || sourceInfo || canOpenInApp; if (!anything) return null; // Row layout: single horizontal line. Column layout: stacked rows. const outerClass = isHorizontal ? `flex flex-row items-center gap-1.5 text-[9px] text-muted-foreground/70 font-mono ${layout === 'header' ? 'flex-wrap' : ''}` : 'flex flex-col items-start gap-1 text-[9px] text-muted-foreground/50 font-mono'; return (
{/* Legacy row layout omits repo/branch to keep the ghost bar compact — they'd otherwise push the container wide enough to visually extend under the action buttons. The Viewer-owned header measures and wraps the complete badge set, so its `header` layout keeps this context. */} {repoInfo && !linkedDocInfo && !isRow && (
{repoInfo.display} {repoInfo.branch && ( {repoInfo.branch} )} {/* Markdown/text annotate sessions have no separate source badge, so keep the open-in control attached to the repository context. */} {canOpenInApp && !sourceInfo && openInButton}
)} {/* Non-repository files still need an open-in entry when there is no source or linked-document row to attach it to. */} {!isRow && canOpenInApp && !repoInfo && !sourceInfo && !linkedDocInfo && openInButton} {sourceInfo && !linkedDocInfo && !isRow && (
{/^https?:\/\//i.test(sourceInfo) ? hostnameOrFallback(sourceInfo) : sourceInfo} {openInButton}
)} {onPlanDiffToggle && ( )} {/* Demo badge: only in column (top-of-doc) layout */} {!isRow && showDemoBadge && !linkedDocInfo && ( Demo )} {archiveInfo && !linkedDocInfo && (
{archiveInfo.status === 'approved' ? 'Approved' : archiveInfo.status === 'denied' ? 'Denied' : 'Unknown'} {archiveInfo.timestamp && ( {new Date(archiveInfo.timestamp).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric', })}{' '} {new Date(archiveInfo.timestamp).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', })} )}
)} {/* Linked-doc breadcrumb: only in column layout (sticky lane is hidden in linked-doc mode) */} {!isRow && linkedDocInfo && ( linkedDocInfo.variant === 'folder-file' ? (
{linkedDocInfo.onBack && ( )} {linkedDocInfo.filepath.split('/').pop()} {openInButton}
) : (
{linkedDocInfo.label || 'Linked File'} {linkedDocInfo.filepath.split('/').pop()} {openInButton}
) )}
); };