GitHub

Segmented Document Viewer

A legend + sidebar + page-timeline abstraction for page-segmented document results (partition, split).

Partition (keyed chunks) and split (named subdocuments) are the same shape: a set of labels, each owning some pages. This component reduces both to one Segment[] model and renders three reusable surfaces over it, all driven by one shared hover/selection state:

  • SegmentSidebar — a selectable list of segments with page ranges and confidence.
  • SegmentLegend — a compact color key.
  • PageTimeline — a colored strip of pages, plus the source PDF with per-segment color overlays.

Hovering a segment anywhere dims the others everywhere; clicking one scrolls the document to its first page.

Installation

pnpm dlx shadcn@latest add @retab/segmented-document-viewer

Usage

Reduce a partition or split result to Segment[] with toSegments, then render:

import { toSegments } from "@/lib/segments"
import { SegmentedDocumentViewer } from "@/components/ui/segmented-document-viewer"
 
// partition: [{ key, pages }]   split: [{ name, pages }]
const segments = toSegments(result.output)
 
export function Example() {
  return (
    <SegmentedDocumentViewer
      segments={segments}
      src="/document.pdf"
      unitLabel="subdocument"
    />
  )
}

Legend variants

SegmentLegend owns how it attaches to the document surface, so the classify, split, and partition viewers all share one legend instead of each re-building its own chrome. The variant prop is the placement; orientation + side decide which edge it lives on.

import { SegmentLegend } from "@/components/ui/segment-legend"
 
// flush bordered bar (default)            floating overlay card
<SegmentLegend segments={segments} />
<SegmentLegend segments={segments} variant="floating" />
 
// gapped rounded panel                    vertical rail on the left
<SegmentLegend segments={segments} variant="inset" />
<SegmentLegend segments={segments} orientation="vertical" side="left" />
PropTypeDescription
variant"bar" | "floating" | "inset" | "plain"How it attaches to the surface. floating/inset need a relative parent. Defaults to bar.
orientation"horizontal" | "vertical"Wrap/grid of entries, or a vertical rail.
side"top" | "bottom" | "left" | "right"Edge it docks to (border for bar, anchor for floating).
density"comfortable" | "compact"Swatch + label scale.
captionReactNodeMuted line under the entries (e.g. a classification's reasoning).
accessoryReactNodeExtra content inside the same chrome (e.g. a page ribbon).

Composing the surfaces yourself

The three surfaces are independent primitives — wire them to your own shared activeId state for a custom layout:

import { SegmentLegend } from "@/components/ui/segment-legend"
import { SegmentSidebar } from "@/components/ui/segment-sidebar"
import { PageTimeline } from "@/components/ui/page-timeline"
 
const [activeId, setActiveId] = useState<string | null>(null)
 
<SegmentLegend segments={segments} activeId={activeId} onActivate={setActiveId} />
<SegmentSidebar segments={segments} activeId={activeId} onActivate={setActiveId} />
<PageTimeline segments={segments} activeId={activeId} onActivate={setActiveId} />

Props

PropTypeDescription
segmentsSegment[]Labels + pages + colors (use toSegments).
srcstringOptional source PDF, previewed with per-segment color overlays.
pageCountnumberTotal pages; defaults to the max page across segments.
unitLabelstringRow noun, e.g. "chunk" or "subdocument".
titleReactNodeHeader title.