shadimage
extend
uidocx-editorsource: ok · hit

DOCX Editor

An experimental DOCX editor with a Word-style toolbar, page thumbnails, import, export, and live document mutation.

npx shadcn add "https://ui.extend.ai/r/docx-editor.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@extend-ai/react-docx@^0.9.2

Registry dependencies

@extend/document-viewer-sidebar@extend/file-thumbnailutilsbuttondropdown-menuinputscroll-areaselectseparatortoggle-grouptooltip@extend/document-color-picker@extend/document-preview-cardbadgecard

Files2 · source cached 10/14/2026, 12:27:47 PM

components/extend/docx-editor.tsx
"use client"

import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"
import {
  DocxEditorViewer,
  paragraphLetterheadFloatSideAtNodeIndex,
  useDocxBorders,
  useDocxComments,
  useDocxDocumentTheme,
  useDocxEditor,
  useDocxLineSpacing,
  useDocxPageLayout,
  useDocxParagraphStyles,
  useDocxTrackChanges,
  useDocxViewerThumbnails,
  type DocxBorderContext,
  type DocxBorderPreset,
  type DocxDocumentTheme,
  type DocxEditorController,
  type ParagraphStyleDefinition,
} from "@extend-ai/react-docx"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
  ScrollArea as InlineScrollArea,
  ScrollBar,
} from "@/components/ui/scroll-area"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"
import { ColorPicker } from "@/components/extend/document-color-picker"
import {
  PreviewCard,
  PreviewCardContent,
  PreviewCardTrigger,
} from "@/components/extend/document-preview-card"
import {
  DocumentViewerThumbnailSidebar,
  useElementWidth,
  useInlineThumbnailSidebar,
} from "@/components/extend/document-viewer-sidebar"
import {
  createDocxCommentCardRenderer,
  createDocxTrackedChangeCardRenderer,
} from "@/components/extend/docx-annotation-card"
import { FileThumbnail } from "@/components/extend/file-thumbnail"
import { IconPlaceholder } from "@/components/icon-placeholder"

function ArrowExpandDiagonal01Glyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="ArrowDownRight"
      tabler="IconArrowDownRight"
      hugeicons="ArrowExpandDiagonal01Icon"
      phosphor="ArrowDownRightIcon"
      remixicon="RiArrowRightDownLine"
      {...props}
    />
  )
}
function ArrowExpandDiagonal02Glyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="ArrowUpRight"
      tabler="IconArrowUpRight"
      hugeicons="ArrowExpandDiagonal02Icon"
      phosphor="ArrowUpRightIcon"
      remixicon="RiArrowRightUpLine"
      {...props}
    />
  )
}
function BorderAll01Glyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="Grid2x2"
      tabler="IconBorderAll"
      hugeicons="BorderAll01Icon"
      phosphor="GridFourIcon"
      remixicon="RiGridLine"
      {...props}
    />
  )
}
function BorderBottom01Glyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="PanelBottom"
      tabler="IconBorderBottom"
      hugeicons="BorderBottom01Icon"
      phosphor="SquareHalfBottomIcon"
      remixicon="RiLayoutBottomLine"
      {...props}
    />
  )
}
function BorderHorizontalGlyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="SeparatorHorizontal"
      tabler="IconBorderHorizontal"
      hugeicons="BorderHorizontalIcon"
      phosphor="MinusIcon"
      remixicon="RiSeparator"
      {...props}
    />
  )
}
function BorderInnerGlyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="SquarePlus"
      tabler="IconBorderInner"
      hugeicons="BorderInnerIcon"
      phosphor="PlusSquareIcon"
      remixicon="RiAddBoxLine"
      {...props}
    />
  )
}
function BorderLeft01Glyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="PanelLeft"
      tabler="IconBorderLeft"
      hugeicons="BorderLeft01Icon"
      phosphor="SquareHalfIcon"
      remixicon="RiLayoutLeftLine"
      {...props}
    />
  )
}
function BorderNone01Glyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="SquareDashed"
      tabler="IconBorderNone"
      hugeicons="BorderNone01Icon"
      phosphor="SelectionIcon"
      remixicon="RiCheckboxBlankLine"
      {...props}
    />
  )
}
function BorderRight01Glyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="PanelRight"
      tabler="IconBorderRight"
      hugeicons="BorderRight01Icon"
      phosphor="SquareHalfIcon"
      remixicon="RiLayoutRightLine"
      {...props}
    />
  )
}
function BorderTop01Glyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="PanelTop"
      tabler="IconBorderTop"
      hugeicons="BorderTop01Icon"
      phosphor="AlignTopSimpleIcon"
      remixicon="RiLayoutTopLine"
      {...props}
    />
  )
}
function BorderVerticalGlyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="SeparatorVertical"
      tabler="IconBorderVertical"
      hugeicons="BorderVerticalIcon"
      phosphor="LineVerticalIcon"
      remixicon="RiLayoutColumnLine"
      {...props}
    />
  )
}
function LineGlyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="Minus"
      tabler="IconMinus"
      hugeicons="LineIcon"
      phosphor="MinusIcon"
      remixicon="RiSubtractLine"
      {...props}
    />
  )
}
function Moon02Glyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="Moon"
      tabler="IconMoon"
      hugeicons="Moon02Icon"
      phosphor="MoonIcon"
      remixicon="RiMoonLine"
      {...props}
    />
  )
}
function Sun03Glyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="Sun"
      tabler="IconSun"
      hugeicons="Sun03Icon"
      phosphor="SunIcon"
      remixicon="RiSunLine"
      {...props}
    />
  )
}
function TextColorGlyph(props: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="Baseline"
      tabler="IconTextColor"
      hugeicons="TextColorIcon"
      phosphor="TextAaIcon"
      remixicon="RiFontColor"
      {...props}
    />
  )
}
const DOCX_MIME_TYPE =
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
const DOCX_LOADING_INDICATOR_DELAY_MS = 300
const DOCX_THUMBNAIL_WIDTH = 92
const DOCX_EDITOR_DEFAULT_ZOOM_SCALE = 100
const ZOOM_OPTIONS = [50, 75, 90, 100, 110, 125, 150, 175, 200] as const
const FONT_FAMILIES = [
  "Calibri",
  "Arial",
  "Times New Roman",
  "Georgia",
  "Helvetica",
  "Courier New",
] as const
const FONT_SIZE_OPTIONS = [
  8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48,
] as const
const LINE_SPACING_OPTIONS = [1, 1.15, 1.2, 1.5, 2, 2.5, 3] as const
const FALLBACK_PARAGRAPH_STYLE_OPTIONS = [
  { id: "Normal", name: "Body" },
  { id: "Heading1", name: "Heading 1" },
  { id: "Heading2", name: "Heading 2" },
  { id: "Heading3", name: "Heading 3" },
] as const
const HIGHLIGHT_COLORS = [
  { label: "Yellow", value: "yellow", color: "#fff59d" },
  { label: "Green", value: "green", color: "#bbf7d0" },
  { label: "Cyan", value: "cyan", color: "#a5f3fc" },
  { label: "Magenta", value: "magenta", color: "#f5d0fe" },
  { label: "Red", value: "red", color: "#fecaca" },
  { label: "Blue", value: "blue", color: "#bfdbfe" },
] as const
const HIGHLIGHT_PREVIEW_COLORS: Record<string, string> = {
  black: "#111827",
  white: "#ffffff",
  ...Object.fromEntries(
    HIGHLIGHT_COLORS.map((option) => [option.value, option.color])
  ),
}
const DOCX_PADDING_WARNING_TEXT = "a style property during rerender"
const DEFAULT_HEADING_PREVIEW_RUN_STYLES: Record<
  1 | 2 | 3 | 4 | 5 | 6,
  NonNullable<ParagraphStyleDefinition["runStyle"]>
> = {
  1: {
    fontFamily: "Calibri Light",
    fontSizePt: 16,
    bold: true,
    color: "#2f5496",
  },
  2: {
    fontFamily: "Calibri Light",
    fontSizePt: 13,
    bold: true,
    color: "#2f5496",
  },
  3: {
    fontFamily: "Calibri",
    fontSizePt: 12,
    bold: true,
    color: "#1f3763",
  },
  4: {
    fontFamily: "Calibri",
    fontSizePt: 11,
    bold: true,
    color: "#1f3763",
  },
  5: {
    fontFamily: "Calibri",
    fontSizePt: 11,
    bold: true,
    color: "#1f3763",
  },
  6: {
    fontFamily: "Calibri",
    fontSizePt: 11,
    bold: true,
    color: "#1f3763",
  },
}
type UploadedDocxFile = {
  file: File
  identity: string
}
type BorderControlOption = {
  id: DocxBorderPreset
  label: string
  contexts?: DocxBorderContext[]
  separatorBefore?: boolean
}
type DocxFontToggleValue =
  | "bold"
  | "italic"
  | "underline"
  | "strike"
  | "superscript"
  | "subscript"
const BORDER_CONTROL_OPTIONS: BorderControlOption[] = [
  { id: "bottom", label: "Bottom Border" },
  { id: "top", label: "Top Border" },
  { id: "left", label: "Left Border" },
  { id: "right", label: "Right Border" },
  { id: "none", label: "No Border", separatorBefore: true },
  { id: "all", label: "All Borders" },
  { id: "outside", label: "Outside Borders" },
  { id: "inside", label: "Inside Borders", contexts: ["table"] },
  {
    id: "inside-horizontal",
    label: "Inside Horizontal Border",
    contexts: ["table"],
  },
  {
    id: "inside-vertical",
    label: "Inside Vertical Border",
    contexts: ["table"],
  },
  {
    id: "diagonal-down",
    label: "Diagonal Down Border",
    contexts: ["table"],
    separatorBefore: true,
  },
  { id: "diagonal-up", label: "Diagonal Up Border", contexts: ["table"] },
  { id: "horizontal-line", label: "Horizontal Line", separatorBefore: true },
]
function borderControlOptionIcon(optionId: DocxBorderPreset) {
  switch (optionId) {
    case "bottom":
      return BorderBottom01Glyph
    case "top":
      return BorderTop01Glyph
    case "left":
      return BorderLeft01Glyph
    case "right":
      return BorderRight01Glyph
    case "none":
      return BorderNone01Glyph
    case "inside":
      return BorderInnerGlyph
    case "inside-horizontal":
      return BorderHorizontalGlyph
    case "inside-vertical":
      return BorderVerticalGlyph
    case "diagonal-down":
      return ArrowExpandDiagonal01Glyph
    case "diagonal-up":
      return ArrowExpandDiagonal02Glyph
    case "horizontal-line":
      return LineGlyph
    default:
      return BorderAll01Glyph
  }
}
async function loadDocxFile(
  url: string,
  displayFileName: string
): Promise<File> {
  const response = await fetch(url)
  if (!response.ok) {
    throw new Error(`Failed to fetch DOCX (${response.status})`)
  }
  const blob = await response.blob()
  return new File([blob], displayFileName, {
    type: blob.type || DOCX_MIME_TYPE,
  })
}
function formatDocumentName(fileName: string | undefined, url: string) {
  if (fileName?.trim()) return fileName
  const pathname = url.split("?")[0] ?? ""
  const rawName = pathname.split("/").pop() ?? "document.docx"
  try {
    return decodeURIComponent(rawName)
  } catch {
    return rawName
  }
}
function getNextZoomScale(currentZoomScale: number, direction: 1 | -1) {
  const currentIndex = ZOOM_OPTIONS.indexOf(
    currentZoomScale as (typeof ZOOM_OPTIONS)[number]
  )
  let fallbackIndex = -1
  if (direction > 0) {
    fallbackIndex = ZOOM_OPTIONS.findIndex((value) => value > currentZoomScale)
  } else {
    for (let index = ZOOM_OPTIONS.length - 1; index >= 0; index -= 1) {
      if (ZOOM_OPTIONS[index] < currentZoomScale) {
        fallbackIndex = index
        break
      }
    }
  }
  const resolvedIndex = currentIndex >= 0 ? currentIndex : fallbackIndex
  if (resolvedIndex < 0) return currentZoomScale
  const nextIndex = Math.min(
    Math.max(resolvedIndex + direction, 0),
    ZOOM_OPTIONS.length - 1
  )
  return ZOOM_OPTIONS[nextIndex] ?? currentZoomScale
}
function normalizeDocxZoomScale(value: number | undefined): number {
  return typeof value === "number" &&
    ZOOM_OPTIONS.includes(value as (typeof ZOOM_OPTIONS)[number])
    ? value
    : DOCX_EDITOR_DEFAULT_ZOOM_SCALE
}
function useDelayedLoadingIndicator(isLoading: boolean, delayMs: number) {
  const [showSpinner, setShowSpinner] = React.useState(false)
  const [previousIsLoading, setPreviousIsLoading] = React.useState(isLoading)
  if (previousIsLoading !== isLoading) {
    setPreviousIsLoading(isLoading)
    setShowSpinner(false)
  }
  React.useEffect(() => {
    if (!isLoading) return
    const timeoutId = window.setTimeout(() => {
      setShowSpinner(true)
    }, delayMs)
    return () => window.clearTimeout(timeoutId)
  }, [delayMs, isLoading])
  return isLoading && showSpinner
}
function isDocxPaddingWarning(args: unknown[]) {
  return (
    typeof args[0] === "string" &&
    args[0].includes(DOCX_PADDING_WARNING_TEXT) &&
    args.some((arg) => String(arg).includes("padding"))
  )
}
function useSuppressDocxPaddingWarning(enabled: boolean) {
  React.useEffect(() => {
    if (!enabled) return
    const originalConsoleError = console.error
    console.error = (...args: unknown[]) => {
      if (isDocxPaddingWarning(args)) return
      originalConsoleError(...args)
    }
    return () => {
      console.error = originalConsoleError
    }
  }, [enabled])
}
function normalizeHexColor(value?: string, fallback = "#111827") {
  if (!value) return fallback
  const trimmed = value.trim()
  const threeDigit = /^#([0-9a-f]{3})$/i.exec(trimmed)
  if (threeDigit?.[1]) {
    const [red, green, blue] = threeDigit[1].split("")
    return `#${red}${red}${green}${green}${blue}${blue}`.toLowerCase()
  }
  const sixDigit = /^#([0-9a-f]{6})$/i.exec(trimmed)
  if (sixDigit?.[1]) {
    return `#${sixDigit[1].toLowerCase()}`
  }
  return fallback
}
function inferParagraphStyleHeadingLevel(value?: string) {
  if (!value) return undefined
  const match = value.match(/(?:^|[\s_-])(?:heading|h)\s*([1-6])(?:$|[\s_-])/i)
  const level = match?.[1] ? Number(match[1]) : NaN
  return level >= 1 && level <= 6 ? (level as 1 | 2 | 3 | 4 | 5 | 6) : undefined
}
function resolveParagraphStyleHeadingLevel(option?: ParagraphStyleDefinition) {
  if (
    Number.isFinite(option?.headingLevel) &&
    option?.headingLevel &&
    option.headingLevel >= 1 &&
    option.headingLevel <= 6
  ) {
    return option.headingLevel as 1 | 2 | 3 | 4 | 5 | 6
  }
  return (
    inferParagraphStyleHeadingLevel(option?.id) ??
    inferParagraphStyleHeadingLevel(option?.name)
  )
}
function resolveParagraphStyleRunPreview(option?: ParagraphStyleDefinition) {
  if (!option) return undefined
  const headingLevel = resolveParagraphStyleHeadingLevel(option)
  const headingRunStyle = headingLevel
    ? DEFAULT_HEADING_PREVIEW_RUN_STYLES[headingLevel]
    : undefined
  return headingRunStyle
    ? {
        ...headingRunStyle,
        ...(option.runStyle ?? {}),
      }
    : option.runStyle
}
function resolveHighlightPreview(value?: string) {
  if (!value) return undefined
  const normalized = value.trim().toLowerCase()
  if (!normalized) return undefined
  const hex = normalizeHexColor(normalized, "")
  if (hex) return hex
  return HIGHLIGHT_PREVIEW_COLORS[normalized]
}
function themedPreviewColor(
  color: string | undefined,
  documentTheme: DocxDocumentTheme
) {
  if (documentTheme !== "dark") return color
  if (!color) return "#f3f4f6"
  const normalized = color.trim().toLowerCase()
  if (
    normalized === "#000" ||
    normalized === "#000000" ||
    normalized === "#111111" ||
    normalized === "#111827" ||
    normalized === "black" ||
    normalized === "rgb(0,0,0)" ||
    normalized === "rgb(0, 0, 0)"
  ) {
    return "#f3f4f6"
  }
  return color
}
function paragraphStylePreviewStyle(
  option: ParagraphStyleDefinition,
  documentTheme: DocxDocumentTheme
): React.CSSProperties {
  const runStyle = resolveParagraphStyleRunPreview(option)
  const textDecoration = [
    runStyle?.underline ? "underline" : "",
    runStyle?.strike ? "line-through" : "",
  ]
    .filter(Boolean)
    .join(" ")
  return {
    textAlign: option.align ?? "left",
    fontFamily: runStyle?.fontFamily,
    fontSize: runStyle?.fontSizePt ? `${runStyle.fontSizePt}pt` : "11pt",
    fontWeight:
      runStyle?.bold !== undefined ? (runStyle.bold ? 700 : 400) : undefined,
    fontStyle: runStyle?.italic ? "italic" : undefined,
    textDecoration: textDecoration || undefined,
    color:
      runStyle?.color !== undefined
        ? themedPreviewColor(normalizeHexColor(runStyle.color), documentTheme)
        : undefined,
    backgroundColor: resolveHighlightPreview(runStyle?.highlight),
    lineHeight: 1,
    whiteSpace: "pre-wrap",
  }
}
function parseToolbarSectionColumns(sectionPropertiesXml?: string):
  | {
      count: number
      gapPx: number
    }
  | undefined {
  if (!sectionPropertiesXml) return undefined
  const columnsTag = sectionPropertiesXml.match(/<w:cols\b[^>]*\/?>/i)?.[0]
  if (!columnsTag) return undefined
  const countRaw = columnsTag.match(/\bw:num="(\d+)"/i)?.[1]
  const count = countRaw ? Number(countRaw) : 1
  if (!Number.isFinite(count) || count <= 1) return undefined
  const gapRaw = columnsTag.match(/\bw:space="(\d+)"/i)?.[1]
  const gapTwips = gapRaw ? Number(gapRaw) : 720
  const gapPx = Math.max(0, Math.round((gapTwips * 96) / 1440))
  return {
    count: Math.max(2, Math.round(count)),
    gapPx,
  }
}
function ToolbarTooltip({
  label,
  children,
}: {
  label: string
  children: React.ReactNode
}) {
  return (
    <Tooltip>
      <TooltipTrigger
        render={<span className="inline-flex">{children}</span>}
      ></TooltipTrigger>
      <TooltipContent side="bottom">{label}</TooltipContent>
    </Tooltip>
  )
}
function EditorLoadingSurface({
  showSpinner = true,
}: {
  showSpinner?: boolean
}) {
  return (
    <div className="grid h-full min-h-52 place-items-center bg-transparent">
      {showSpinner ? <InlineSpinner className="size-4" /> : null}
    </div>
  )
}
function ToolbarIconButton({
  active,
  children,
  label,
  ...props
}: React.ComponentProps<typeof Button> & {
  active?: boolean
  label: string
}) {
  return (
    <ToolbarTooltip label={label}>
      <Button
        type="button"
        variant={active ? "secondary" : "ghost"}
        size="icon-sm"
        aria-label={label}
        data-pressed={active ? "" : undefined}
        {...props}
      >
        {children}
      </Button>
    </ToolbarTooltip>
  )
}
function ToolbarSeparator() {
  return <Separator orientation="vertical" className="mx-1 h-4 self-center" />
}
function DocxSidebarThumbnail({
  canvasRef,
  displayFileName,
  hasError,
  isActive,
  isLoading,
  pageNumber,
  pixelHeightPx,
  pixelWidthPx,
  previewAspectRatio,
}: {
  canvasRef: React.RefCallback<HTMLCanvasElement>
  displayFileName: string
  hasError: boolean
  isActive: boolean
  isLoading: boolean
  pageNumber: number
  pixelHeightPx: number
  pixelWidthPx: number
  previewAspectRatio: number
}) {
  return (
    <FileThumbnail
      file={{
        name: `${displayFileName} page ${pageNumber}`,
        type: DOCX_MIME_TYPE,
      }}
      previewAspectRatio={previewAspectRatio}
      previewClassName="rounded-md bg-white"
      previewContent={
        <canvas
          ref={canvasRef}
          width={pixelWidthPx}
          height={pixelHeightPx}
          className="!size-full bg-white object-cover object-top"
        />
      }
      isLoading={isLoading}
      hasError={hasError}
      className={cn(
        "w-[92px] rounded-md border-0 shadow-xs ring-0 transition-shadow duration-150",
        isActive && "shadow-sm"
      )}
    />
  )
}
function ParagraphStylePreviewCard({
  documentTheme,
  option,
}: {
  documentTheme: DocxDocumentTheme
  option: ParagraphStyleDefinition
}) {
  const surfaceStyle: React.CSSProperties =
    documentTheme === "dark"
      ? {
          backgroundColor: "#111827",
          borderColor: "#374151",
          color: "#f3f4f6",
        }
      : {
          backgroundColor: "#ffffff",
          borderColor: "#d4d4d8",
          color: "#111827",
        }
  const secondaryTextColor = documentTheme === "dark" ? "#9ca3af" : "#6b7280"
  return (
    <div className="w-[260px] p-3">
      <p className="text-[10px] font-medium tracking-wide text-muted-foreground uppercase">
        Style Preview
      </p>
      <div className="mt-2 rounded-sm border p-2.5" style={surfaceStyle}>
        <p style={paragraphStylePreviewStyle(option, documentTheme)}>
          {option.name}
        </p>
        <p
          className="mt-1 text-[11px]"
          style={{
            color: secondaryTextColor,
            textAlign: option.align ?? "left",
          }}
        >
          The quick brown fox jumps over the lazy dog.
        </p>
      </div>
    </div>
  )
}
function DocxEditorToolbar({
  activePage,
  controlsDisabled,
  editor,
  isReadOnly,
  onImageUploadClick,
  onIsDarkChange,
  onIsReadOnlyChange,
  onOpenLinkEditor,
  onToggleSidebar,
  onUploadClick,
  pageCount,
  setZoomScale,
  showNightRenderToggle,
  zoomScale,
}: {
  activePage: number
  controlsDisabled: boolean
  editor: DocxEditorController
  isReadOnly: boolean
  onImageUploadClick: () => void
  onIsDarkChange: (checked: boolean) => void
  onIsReadOnlyChange: (checked: boolean) => void
  onOpenLinkEditor: () => void
  onToggleSidebar: () => void
  onUploadClick: () => void
  pageCount: number
  setZoomScale: React.Dispatch<React.SetStateAction<number>>
  showNightRenderToggle: boolean
  zoomScale: number
}) {
  const { documentTheme, setDocumentTheme } = useDocxDocumentTheme(editor)
  const { layout: pageLayout } = useDocxPageLayout(editor)
  const { paragraphStyles, selectedParagraphStyleId, setParagraphStyle } =
    useDocxParagraphStyles(editor)
  const { lineSpacing, setLineSpacing } = useDocxLineSpacing(editor)
  const { borderContext, activeBorderPresets, applyBorderPreset } =
    useDocxBorders(editor)
  const { showTrackedChanges, setShowTrackedChanges } =
    useDocxTrackChanges(editor)
  const { showComments, setShowComments } = useDocxComments(editor)
  const selectedRunStyle = editor.selectedRunStyle
  const selectedParagraph = editor.selectedParagraph
  const paragraphStyleOptions: ParagraphStyleDefinition[] =
    paragraphStyles.length > 0
      ? paragraphStyles
      : [...FALLBACK_PARAGRAPH_STYLE_OPTIONS]
  const selectedParagraphStyleValue =
    selectedParagraphStyleId ??
    paragraphStyleOptions.find((option) => "isDefault" in option)?.id ??
    paragraphStyleOptions[0]?.id ??
    "Normal"
  const [paragraphStylePreviewId, setParagraphStylePreviewId] = React.useState<
    string | null
  >(null)
  const selectedLineSpacingValue = React.useMemo(() => {
    const current = Number.isFinite(lineSpacing.multiple)
      ? lineSpacing.multiple
      : 1
    const nearest = LINE_SPACING_OPTIONS.reduce((closest, candidate) => {
      return Math.abs(candidate - current) < Math.abs(closest - current)
        ? candidate
        : closest
    }, LINE_SPACING_OPTIONS[0])
    return String(nearest)
  }, [lineSpacing.multiple])
  const textColorValue = normalizeHexColor(selectedRunStyle?.color)
  const highlightColor =
    HIGHLIGHT_COLORS.find(
      (option) => option.value === selectedRunStyle?.highlight
    )?.color ?? "#fff59d"
  const activeNodeIndex =
    editor.selection.kind === "paragraph"
      ? editor.selection.nodeIndex
      : editor.selection.tableIndex
  const letterheadColumns =
    editor.selection.kind === "paragraph" &&
    paragraphLetterheadFloatSideAtNodeIndex(
      editor.model.nodes,
      editor.selection.nodeIndex
    )
      ? { count: 2, gapPx: 28 }
      : undefined
  const sections = editor.model.metadata.sections ?? []
  const activeSection = sections
    .filter((section) => section.startNodeIndex <= activeNodeIndex)
    .at(-1)
  const activeColumns =
    letterheadColumns ??
    parseToolbarSectionColumns(activeSection?.sectionPropertiesXml) ??
    parseToolbarSectionColumns(editor.model.metadata.sectionPropertiesXml) ??
    pageLayout.columns
  const activeBorderControlOptions = BORDER_CONTROL_OPTIONS.filter(
    (option) => activeBorderPresets[option.id]
  )
  const borderTriggerLabel =
    activeBorderControlOptions.length === 1
      ? activeBorderControlOptions[0].label
      : "Borders"
  const borderTriggerIcon = React.createElement(
    borderControlOptionIcon(
      activeBorderControlOptions.length === 1
        ? activeBorderControlOptions[0].id
        : "all"
    ),
    { className: "size-4" }
  )
  const canEdit = !controlsDisabled && !isReadOnly
  const canZoomIn = zoomScale < ZOOM_OPTIONS[ZOOM_OPTIONS.length - 1]
  const canZoomOut = zoomScale > ZOOM_OPTIONS[0]
  const fontToggleValues = React.useMemo<DocxFontToggleValue[]>(() => {
    const values: DocxFontToggleValue[] = []
    if (selectedRunStyle?.bold) values.push("bold")
    if (selectedRunStyle?.italic) values.push("italic")
    if (selectedRunStyle?.underline) values.push("underline")
    if (selectedRunStyle?.strike) values.push("strike")
    if (selectedRunStyle?.verticalAlign === "superscript") {
      values.push("superscript")
    }
    if (selectedRunStyle?.verticalAlign === "subscript") {
      values.push("subscript")
    }
    return values
  }, [selectedRunStyle])
  const preserveTextSelection = React.useCallback(
    (event: React.MouseEvent<HTMLElement> | React.PointerEvent<HTMLElement>) =>
      event.preventDefault(),
    []
  )
  const applyFontToggleValues = React.useCallback(
    (nextValues: DocxFontToggleValue[]) => {
      const changedValue =
        nextValues.find((value) => !fontToggleValues.includes(value)) ??
        fontToggleValues.find((value) => !nextValues.includes(value))
      if (changedValue === "bold") {
        editor.toggleBold()
      } else if (changedValue === "italic") {
        editor.toggleItalic()
      } else if (changedValue === "underline") {
        editor.toggleUnderline()
      } else if (changedValue === "strike") {
        editor.toggleStrike()
      } else if (changedValue === "superscript") {
        editor.toggleSuperscript()
      } else if (changedValue === "subscript") {
        editor.toggleSubscript()
      }
    },
    [editor, fontToggleValues]
  )
  return (
    <div className="bg-background">
      <TooltipProvider>
        <div className="flex min-h-11 items-center gap-2 overflow-x-auto overflow-y-hidden border-b px-3">
          <ToolbarIconButton
            label="Toggle pages"
            disabled={controlsDisabled}
            onClick={onToggleSidebar}
          >
            <IconPlaceholder
              lucide="PanelLeft"
              tabler="IconLayoutSidebar"
              hugeicons="SidebarLeftIcon"
              phosphor="SidebarIcon"
              remixicon="RiLayoutLeftLine"
              className="size-4"
            />
          </ToolbarIconButton>
          <div className="min-w-28 text-sm whitespace-nowrap text-primary">
            Page {pageCount ? activePage : 1} of {pageCount || "-"}
          </div>
          <div className="min-w-0 flex-1" />
          {showNightRenderToggle ? (
            <>
              <ToolbarIconButton
                label={
                  documentTheme === "dark"
                    ? "Use light document"
                    : "Use dark document"
                }
                disabled={controlsDisabled}
                onClick={() => {
                  const nextIsDark = documentTheme !== "dark"
                  setDocumentTheme(nextIsDark ? "dark" : "light")
                  onIsDarkChange(nextIsDark)
                }}
              >
                {documentTheme === "dark" ? (
                  <Sun03Glyph className="size-4" />
                ) : (
                  <Moon02Glyph className="size-4" />
                )}
              </ToolbarIconButton>
              <Separator
                orientation="vertical"
                className="mx-1 h-4 self-center"
              />
            </>
          ) : null}
          <ToolbarIconButton
            label="Show tracked changes"
            active={showTrackedChanges}
            disabled={controlsDisabled}
            onClick={() => setShowTrackedChanges(!showTrackedChanges)}
          >
            <IconPlaceholder
              lucide="FileDiff"
              tabler="IconFileDiff"
              hugeicons="FileDiffIcon"
              phosphor="GitDiffIcon"
              remixicon="RiFileHistoryLine"
              className="size-4"
            />
          </ToolbarIconButton>
          <ToolbarIconButton
            label="Show comments"
            active={showComments}
            disabled={controlsDisabled}
            onClick={() => setShowComments(!showComments)}
          >
            <IconPlaceholder
              lucide="MessageSquare"
              tabler="IconMessage"
              hugeicons="Comment01Icon"
              phosphor="ChatIcon"
              remixicon="RiChat1Line"
              className="size-4"
            />
          </ToolbarIconButton>
          <ToolbarIconButton
            label={isReadOnly ? "Switch to edit mode" : "Switch to read-only"}
            active={isReadOnly}
            disabled={controlsDisabled}
            onClick={() => onIsReadOnlyChange(!isReadOnly)}
          >
            <IconPlaceholder
              lucide="PencilOff"
              tabler="IconPencilOff"
              hugeicons="EditOffIcon"
              phosphor="PencilSlashIcon"
              remixicon="RiLockLine"
              className="size-4"
            />
          </ToolbarIconButton>
          <Separator orientation="vertical" className="mx-1 h-4 self-center" />
          <ToolbarIconButton
            label="Import DOCX"
            disabled={editor.isImporting}
            onClick={onUploadClick}
          >
            {editor.isImporting ? (
              <InlineSpinner className="size-4" />
            ) : (
              <IconPlaceholder
                lucide="Upload"
                tabler="IconUpload"
                hugeicons="Upload01Icon"
                phosphor="UploadSimpleIcon"
                remixicon="RiUpload2Line"
                className="size-4"
              />
            )}
          </ToolbarIconButton>
          <ToolbarIconButton
            label="Download DOCX"
            disabled={controlsDisabled || editor.isImporting}
            onClick={editor.exportDocx}
          >
            <IconPlaceholder
              lucide="Download"
              tabler="IconDownload"
              hugeicons="Download01Icon"
              phosphor="DownloadSimpleIcon"
              remixicon="RiDownload2Line"
              className="size-4"
            />
          </ToolbarIconButton>
        </div>

        <div className="flex min-h-12 flex-wrap items-center gap-2 border-b px-3 py-2">
          <div className="flex shrink-0 items-center gap-1">
            <ToolbarIconButton
              label="Undo"
              disabled={!editor.canUndo || isReadOnly}
              onClick={editor.undo}
            >
              <IconPlaceholder
                lucide="Undo2"
                tabler="IconArrowBackUp"
                hugeicons="Undo02Icon"
                phosphor="ArrowUUpLeftIcon"
                remixicon="RiArrowGoBackLine"
                className="size-4"
              />
            </ToolbarIconButton>
            <ToolbarIconButton
              label="Redo"
              disabled={!editor.canRedo || isReadOnly}
              onClick={editor.redo}
            >
              <IconPlaceholder
                lucide="Redo2"
                tabler="IconArrowForwardUp"
                hugeicons="Redo02Icon"
                phosphor="ArrowUUpRightIcon"
                remixicon="RiArrowGoForwardLine"
                className="size-4"
              />
            </ToolbarIconButton>
          </div>

          <ToolbarSeparator />

          <Select
            value={selectedParagraphStyleValue}
            onOpenChange={() => setParagraphStylePreviewId(null)}
            onValueChange={(value) => {
              if (value) {
                setParagraphStyle(value)
              }
            }}
            disabled={!canEdit}
            modal={false}
          >
            <SelectTrigger
              size="sm"
              className="w-[136px] min-w-[136px]"
              aria-label="Paragraph style"
            >
              <SelectValue placeholder="Style" />
            </SelectTrigger>
            <SelectContent
              align="start"
              alignItemWithTrigger={false}
              className="z-40 min-w-[210px]"
            >
              {paragraphStyleOptions.map((option) => (
                <PreviewCard
                  key={option.id}
                  open={paragraphStylePreviewId === option.id}
                  onOpenChange={(open) => {
                    setParagraphStylePreviewId((current) =>
                      open ? option.id : current === option.id ? null : current
                    )
                  }}
                  openDelay={0}
                  closeDelay={120}
                >
                  <SelectItem
                    value={option.id}
                    label={option.name}
                    className="relative min-w-[190px]"
                    onFocus={() => setParagraphStylePreviewId(option.id)}
                    onPointerEnter={() => setParagraphStylePreviewId(option.id)}
                  >
                    <PreviewCardTrigger
                      render={
                        <span className="block w-full truncate">
                          {option.name}
                        </span>
                      }
                    ></PreviewCardTrigger>
                  </SelectItem>
                  <PreviewCardContent
                    side="right"
                    align="start"
                    sideOffset={10}
                    alignOffset={-4}
                    data-slot="paragraph-style-preview-card"
                    className="origin-(--transform-origin) rounded-lg border bg-popover text-popover-foreground shadow-lg/5 transition-opacity duration-100 outline-none not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]"
                  >
                    <ParagraphStylePreviewCard
                      documentTheme={documentTheme}
                      option={option}
                    />
                  </PreviewCardContent>
                </PreviewCard>
              ))}
            </SelectContent>
          </Select>

          <Select
            value={selectedRunStyle?.fontFamily ?? "Calibri"}
            onValueChange={(value) => {
              if (value) {
                editor.setFontFamily(value)
              }
            }}
            disabled={!canEdit}
            modal={false}
          >
            <SelectTrigger
              size="sm"
              className="w-[156px] min-w-[156px]"
              aria-label="Font family"
            >
              <SelectValue placeholder="Font" />
            </SelectTrigger>
            <SelectContent
              align="start"
              alignItemWithTrigger={false}
              className="z-40"
            >
              {FONT_FAMILIES.map((fontFamily) => (
                <SelectItem key={fontFamily} value={fontFamily}>
                  <span style={{ fontFamily }}>{fontFamily}</span>
                </SelectItem>
              ))}
            </SelectContent>
          </Select>

          <Select
            value={String(Math.round(selectedRunStyle?.fontSizePt ?? 12))}
            onValueChange={(value) => {
              const nextSize = Number(value)
              if (Number.isFinite(nextSize)) {
                editor.setFontSize(nextSize)
              }
            }}
            disabled={!canEdit}
            modal={false}
          >
            <SelectTrigger
              size="sm"
              className="w-[86px] min-w-[86px]"
              aria-label="Font size"
            >
              <SelectValue placeholder="Size" />
            </SelectTrigger>
            <SelectContent
              align="start"
              alignItemWithTrigger={false}
              className="z-40"
            >
              {FONT_SIZE_OPTIONS.map((size) => (
                <SelectItem key={size} value={String(size)}>
                  {size} pt
                </SelectItem>
              ))}
            </SelectContent>
          </Select>

          <Select
            value={selectedLineSpacingValue}
            onValueChange={(value) => {
              const nextSpacing = Number(value)
              if (Number.isFinite(nextSpacing)) {
                setLineSpacing(nextSpacing)
              }
            }}
            disabled={!canEdit}
            modal={false}
          >
            <SelectTrigger
              size="sm"
              className="w-[94px] min-w-[94px]"
              aria-label="Line spacing"
            >
              <SelectValue placeholder="Spacing" />
            </SelectTrigger>
            <SelectContent
              align="start"
              alignItemWithTrigger={false}
              className="z-40"
            >
              {LINE_SPACING_OPTIONS.map((spacing) => (
                <SelectItem key={spacing} value={String(spacing)}>
                  {spacing}x
                </SelectItem>
              ))}
            </SelectContent>
          </Select>

          <ToolbarSeparator />

          <ToggleGroup
            className="shrink-0"
            disabled={!canEdit}
            multiple
            value={fontToggleValues}
            onMouseDown={preserveTextSelection}
            onPointerDown={preserveTextSelection}
            onValueChange={(value) =>
              applyFontToggleValues(value as DocxFontToggleValue[])
            }
            spacing={1}
          >
            <ToolbarTooltip label="Bold">
              <ToggleGroupItem aria-label="Bold" size="sm" value="bold">
                <IconPlaceholder
                  lucide="Bold"
                  tabler="IconBold"
                  hugeicons="TextBoldIcon"
                  phosphor="TextBIcon"
                  remixicon="RiBold"
                  className="size-4"
                />
              </ToggleGroupItem>
            </ToolbarTooltip>
            <ToolbarTooltip label="Italic">
              <ToggleGroupItem aria-label="Italic" size="sm" value="italic">
                <IconPlaceholder
                  lucide="Italic"
                  tabler="IconItalic"
                  hugeicons="TextItalicIcon"
                  phosphor="TextItalicIcon"
                  remixicon="RiItalic"
                  className="size-4"
                />
              </ToggleGroupItem>
            </ToolbarTooltip>
            <ToolbarTooltip label="Underline">
              <ToggleGroupItem
                aria-label="Underline"
                size="sm"
                value="underline"
              >
                <IconPlaceholder
                  lucide="Underline"
                  tabler="IconUnderline"
                  hugeicons="TextUnderlineIcon"
                  phosphor="TextUnderlineIcon"
                  remixicon="RiUnderline"
                  className="size-4"
                />
              </ToggleGroupItem>
            </ToolbarTooltip>
            <ToolbarTooltip label="Strikethrough">
              <ToggleGroupItem
                aria-label="Strikethrough"
                size="sm"
                value="strike"
              >
                <IconPlaceholder
                  lucide="Strikethrough"
                  tabler="IconStrikethrough"
                  hugeicons="TextStrikethroughIcon"
                  phosphor="TextStrikethroughIcon"
                  remixicon="RiStrikethrough"
                  className="size-4"
                />
              </ToggleGroupItem>
            </ToolbarTooltip>
            <ToolbarTooltip label="Superscript">
              <ToggleGroupItem
                aria-label="Superscript"
                size="sm"
                value="superscript"
              >
                <IconPlaceholder
                  lucide="Superscript"
                  tabler="IconSuperscript"
                  hugeicons="TextSuperscriptIcon"
                  phosphor="TextSuperscriptIcon"
                  remixicon="RiSuperscript"
                  className="size-4"
                />
              </ToggleGroupItem>
            </ToolbarTooltip>
            <ToolbarTooltip label="Subscript">
              <ToggleGroupItem
                aria-label="Subscript"
                size="sm"
                value="subscript"
              >
                <IconPlaceholder
                  lucide="Subscript"
                  tabler="IconSubscript"
                  hugeicons="TextSubscriptIcon"
                  phosphor="TextSubscriptIcon"
                  remixicon="RiSubscript"
                  className="size-4"
                />
              </ToggleGroupItem>
            </ToolbarTooltip>
          </ToggleGroup>

          <ToolbarSeparator />

          <div className="flex shrink-0 items-center gap-1">
            <ColorPicker
              label="Text color"
              icon={TextColorGlyph}
              color={textColorValue}
              disabled={!canEdit}
              onTriggerMouseDown={preserveTextSelection}
              onTriggerPointerDown={preserveTextSelection}
              onChange={(color) =>
                editor.setTextColor(normalizeHexColor(color))
              }
            />
            <DropdownMenu>
              <DropdownMenuTrigger
                render={
                  <Button
                    type="button"
                    variant="ghost"
                    size="icon-sm"
                    disabled={!canEdit}
                    aria-label="Highlight color"
                    className="relative"
                  >
                    <IconPlaceholder
                      lucide="Highlighter"
                      tabler="IconHighlight"
                      hugeicons="HighlighterIcon"
                      phosphor="HighlighterIcon"
                      remixicon="RiMarkPenLine"
                      className="size-4"
                    />
                    <span
                      className="absolute right-1 bottom-1 h-1 w-4 rounded-full border border-background"
                      style={{ backgroundColor: highlightColor }}
                    />
                  </Button>
                }
              ></DropdownMenuTrigger>
              <DropdownMenuContent align="start" className="z-40 w-44">
                {HIGHLIGHT_COLORS.map((option) => (
                  <DropdownMenuCheckboxItem
                    key={option.value}
                    checked={selectedRunStyle?.highlight === option.value}
                    onCheckedChange={() => editor.setHighlight(option.value)}
                  >
                    <span
                      className="size-3 rounded-full border"
                      style={{ backgroundColor: option.color }}
                    />
                    {option.label}
                  </DropdownMenuCheckboxItem>
                ))}
                <DropdownMenuSeparator />
                <DropdownMenuCheckboxItem
                  checked={!selectedRunStyle?.highlight}
                  onCheckedChange={() => editor.setHighlight(undefined)}
                >
                  No highlight
                </DropdownMenuCheckboxItem>
              </DropdownMenuContent>
            </DropdownMenu>
            <ToolbarIconButton
              label="Link"
              disabled={!canEdit}
              onMouseDown={preserveTextSelection}
              onPointerDown={preserveTextSelection}
              onClick={onOpenLinkEditor}
            >
              <IconPlaceholder
                lucide="Link"
                tabler="IconLink"
                hugeicons="Link02Icon"
                phosphor="LinkIcon"
                remixicon="RiLink"
                className="size-4"
              />
            </ToolbarIconButton>
          </div>

          <ToolbarSeparator />

          <div className="flex shrink-0 items-center gap-1">
            <ToolbarIconButton
              label="Align left"
              active={
                selectedParagraph?.style?.align === "left" ||
                !selectedParagraph?.style?.align
              }
              disabled={!canEdit}
              onMouseDown={preserveTextSelection}
              onPointerDown={preserveTextSelection}
              onClick={() => editor.setAlignment("left")}
            >
              <IconPlaceholder
                lucide="AlignLeft"
                tabler="IconAlignLeft"
                hugeicons="TextAlignLeft01Icon"
                phosphor="TextAlignLeftIcon"
                remixicon="RiAlignLeft"
                className="size-4"
              />
            </ToolbarIconButton>
            <ToolbarIconButton
              label="Align center"
              active={selectedParagraph?.style?.align === "center"}
              disabled={!canEdit}
              onMouseDown={preserveTextSelection}
              onPointerDown={preserveTextSelection}
              onClick={() => editor.setAlignment("center")}
            >
              <IconPlaceholder
                lucide="AlignCenter"
                tabler="IconAlignCenter"
                hugeicons="TextAlignCenterIcon"
                phosphor="TextAlignCenterIcon"
                remixicon="RiAlignCenter"
                className="size-4"
              />
            </ToolbarIconButton>
            <ToolbarIconButton
              label="Align right"
              active={selectedParagraph?.style?.align === "right"}
              disabled={!canEdit}
              onMouseDown={preserveTextSelection}
              onPointerDown={preserveTextSelection}
              onClick={() => editor.setAlignment("right")}
            >
              <IconPlaceholder
                lucide="AlignRight"
                tabler="IconAlignRight"
                hugeicons="TextAlignRight01Icon"
                phosphor="TextAlignRightIcon"
                remixicon="RiAlignRight"
                className="size-4"
              />
            </ToolbarIconButton>
            <ToolbarIconButton
              label="Justify"
              active={selectedParagraph?.style?.align === "justify"}
              disabled={!canEdit}
              onMouseDown={preserveTextSelection}
              onPointerDown={preserveTextSelection}
              onClick={() => editor.setAlignment("justify")}
            >
              <IconPlaceholder
                lucide="AlignJustify"
                tabler="IconAlignJustified"
                hugeicons="TextAlignJustifyLeftIcon"
                phosphor="TextAlignJustifyIcon"
                remixicon="RiAlignJustify"
                className="size-4"
              />
            </ToolbarIconButton>
          </div>

          <ToolbarSeparator />

          <div className="flex shrink-0 items-center gap-1">
            <ToolbarIconButton
              label="Bulleted list"
              active={editor.hasUnorderedList}
              disabled={!canEdit}
              onMouseDown={preserveTextSelection}
              onPointerDown={preserveTextSelection}
              onClick={() => editor.toggleList("unordered")}
            >
              <IconPlaceholder
                lucide="Columns3Icon"
                tabler="IconLayoutColumns"
                hugeicons="LeftToRightListBulletIcon"
                phosphor="ColumnsIcon"
                remixicon="RiLayoutColumnLine"
                className="size-4"
              />
            </ToolbarIconButton>
            <ToolbarIconButton
              label="Numbered list"
              active={editor.hasOrderedList}
              disabled={!canEdit}
              onMouseDown={preserveTextSelection}
              onPointerDown={preserveTextSelection}
              onClick={() => editor.toggleList("ordered")}
            >
              <IconPlaceholder
                lucide="ListOrdered"
                tabler="IconListNumbers"
                hugeicons="LeftToRightListNumberIcon"
                phosphor="ListNumbersIcon"
                remixicon="RiListOrdered"
                className="size-4"
              />
            </ToolbarIconButton>
          </div>

          <ToolbarSeparator />

          <DropdownMenu>
            <DropdownMenuTrigger
              render={
                <Button
                  type="button"
                  variant="ghost"
                  size="sm"
                  disabled={!canEdit}
                  className="h-7 gap-1 px-2 shadow-none"
                >
                  {borderTriggerIcon}
                  {borderTriggerLabel}
                </Button>
              }
            ></DropdownMenuTrigger>
            <DropdownMenuContent align="start" className="z-40 w-56">
              {BORDER_CONTROL_OPTIONS.map((option) => {
                const enabledForContext =
                  !option.contexts || option.contexts.includes(borderContext)
                const BorderOptionIcon = borderControlOptionIcon(option.id)
                return (
                  <React.Fragment key={option.id}>
                    {option.separatorBefore ? <DropdownMenuSeparator /> : null}
                    <DropdownMenuCheckboxItem
                      checked={activeBorderPresets[option.id]}
                      disabled={!enabledForContext}
                      onCheckedChange={() => {
                        if (enabledForContext) {
                          applyBorderPreset(option.id)
                        }
                      }}
                    >
                      <BorderOptionIcon className="size-4 text-muted-foreground" />
                      {option.label}
                    </DropdownMenuCheckboxItem>
                  </React.Fragment>
                )
              })}
            </DropdownMenuContent>
          </DropdownMenu>

          <ToolbarSeparator />

          <div className="flex shrink-0 items-center gap-1">
            <ToolbarIconButton
              label="Insert image"
              disabled={!canEdit}
              onClick={onImageUploadClick}
            >
              <IconPlaceholder
                lucide="ImagePlus"
                tabler="IconPhotoPlus"
                hugeicons="ImageAdd01Icon"
                phosphor="ImageIcon"
                remixicon="RiImageAddLine"
                className="size-4"
              />
            </ToolbarIconButton>
            <ToolbarIconButton
              label="Insert table"
              disabled={!canEdit}
              onClick={editor.insertTable}
            >
              <IconPlaceholder
                lucide="TableIcon"
                tabler="IconTable"
                hugeicons="TableIcon"
                phosphor="TableIcon"
                remixicon="RiTableLine"
                className="size-4"
              />
            </ToolbarIconButton>
            <ToolbarTooltip label="Section columns">
              <div className="flex h-7 shrink-0 items-center gap-1 rounded-lg px-2 text-xs text-muted-foreground">
                <IconPlaceholder
                  lucide="Columns3Cog"
                  tabler="IconColumns3"
                  hugeicons="ColumnsThreeCogIcon"
                  phosphor="ColumnsIcon"
                  remixicon="RiLayoutColumnLine"
                  className="size-4"
                />
                {activeColumns ? `${activeColumns.count} cols` : "1 col"}
              </div>
            </ToolbarTooltip>
          </div>

          <ToolbarSeparator />

          <div className="ml-auto flex flex-none items-center gap-1">
            <ToolbarIconButton
              label="Zoom out"
              disabled={controlsDisabled || !canZoomOut}
              onClick={() =>
                setZoomScale((currentZoomScale) =>
                  getNextZoomScale(currentZoomScale, -1)
                )
              }
            >
              <IconPlaceholder
                lucide="CircleMinus"
                tabler="IconCircleMinus"
                hugeicons="MinusSignCircleIcon"
                phosphor="MinusCircleIcon"
                remixicon="RiIndeterminateCircleLine"
                className="size-4"
              />
            </ToolbarIconButton>
            <Select
              value={zoomScale.toString()}
              onValueChange={(value) => setZoomScale(Number(value))}
              disabled={controlsDisabled}
              modal={false}
            >
              <SelectTrigger
                size="sm"
                className="w-[84px] min-w-[84px]"
                aria-label="Zoom level"
              >
                <SelectValue>{Math.round(zoomScale)}%</SelectValue>
              </SelectTrigger>
              <SelectContent
                align="end"
                alignItemWithTrigger={false}
                className="z-40"
              >
                {ZOOM_OPTIONS.map((value) => (
                  <SelectItem key={value} value={value.toString()}>
                    {value}%
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
            <ToolbarIconButton
              label="Zoom in"
              disabled={controlsDisabled || !canZoomIn}
              onClick={() =>
                setZoomScale((currentZoomScale) =>
                  getNextZoomScale(currentZoomScale, 1)
                )
              }
            >
              <IconPlaceholder
                lucide="CirclePlusIcon"
                tabler="IconCirclePlusFilled"
                hugeicons="PlusSignCircleIcon"
                phosphor="PlusCircleIcon"
                remixicon="RiAddCircleFill"
                className="size-4"
              />
            </ToolbarIconButton>
          </div>
        </div>
      </TooltipProvider>
    </div>
  )
}
export function DocxEditorPreview({
  className,
  defaultZoomScale = DOCX_EDITOR_DEFAULT_ZOOM_SCALE,
  fileName,
  isDark,
  onIsDarkChange,
  src,
}: {
  className?: string
  defaultZoomScale?: number
  fileName?: string
  isDark: boolean
  onIsDarkChange: (isDark: boolean) => void
  src?: string
}) {
  return (
    <DocxEditorContent
      key={src}
      className={className}
      defaultZoomScale={defaultZoomScale}
      effectiveIsDark={isDark}
      fileName={fileName}
      setIsDark={onIsDarkChange}
      shouldRenderNightMode
      url={src}
    />
  )
}
function DocxEditorContent({
  className,
  defaultZoomScale,
  effectiveIsDark,
  fileName,
  setIsDark,
  shouldRenderNightMode,
  url,
}: {
  className?: string
  defaultZoomScale?: number
  effectiveIsDark: boolean
  fileName?: string
  setIsDark: (checked: boolean) => void
  shouldRenderNightMode: boolean
  url?: string
}) {
  const fileInputRef = React.useRef<HTMLInputElement>(null)
  const imageInputRef = React.useRef<HTMLInputElement>(null)
  const viewportRef = React.useRef<HTMLDivElement | null>(null)
  const [viewportElement, setViewportElement] =
    React.useState<HTMLDivElement | null>(null)
  const [viewerShellRef, viewerShellWidth] = useElementWidth<HTMLDivElement>()
  const [uploadedDocxFile, setUploadedDocxFile] =
    React.useState<UploadedDocxFile | null>(null)
  const [activePage, setActivePage] = React.useState(1)
  const [sidebarOpen, setSidebarOpen] = React.useState(false)
  const resolvedDefaultZoomScale = normalizeDocxZoomScale(defaultZoomScale)
  const [zoomScale, setZoomScale] = React.useState<number>(
    resolvedDefaultZoomScale
  )
  const [loadError, setLoadError] = React.useState<string>()
  const [isLoadingDocument, setIsLoadingDocument] = React.useState(Boolean(url))
  const [isReadOnly, setIsReadOnly] = React.useState(false)
  const [linkEditorOpen, setLinkEditorOpen] = React.useState(false)
  const [linkDraft, setLinkDraft] = React.useState("")
  const viewerBackgroundColor =
    "color-mix(in oklab, var(--muted) 40%, transparent)"
  const displayFileName = React.useMemo(
    () =>
      uploadedDocxFile?.file.name ??
      (url ? formatDocumentName(fileName, url) : (fileName ?? "document.docx")),
    [fileName, uploadedDocxFile?.file.name, url]
  )
  const [initialDocumentTheme] = React.useState<DocxDocumentTheme>(() =>
    effectiveIsDark ? "dark" : "light"
  )
  const editorOptions = React.useMemo(
    () => ({
      initialDocumentTheme,
      initialFileName: displayFileName,
    }),
    [displayFileName, initialDocumentTheme]
  )
  const editor = useDocxEditor(editorOptions)
  const { layout: pageLayout } = useDocxPageLayout(editor)
  const { importDocxFile, setDocumentTheme, status } = editor
  const [reportedPageCount, setReportedPageCount] = React.useState(0)
  const thumbnailEditor = React.useMemo<DocxEditorController>(
    () => ({
      ...editor,
      totalPages: Math.max(editor.totalPages, reportedPageCount),
    }),
    [editor, reportedPageCount]
  )
  const thumbnailOptions = React.useMemo(
    () => ({
      pixelRatio: 2,
      resolution: {
        maxHeight: DOCX_THUMBNAIL_WIDTH * 1.35,
        maxWidth: DOCX_THUMBNAIL_WIDTH,
      },
    }),
    []
  )
  const { thumbnails } = useDocxViewerThumbnails(
    thumbnailEditor,
    thumbnailOptions
  )
  const shouldShowDocumentSpinner = useDelayedLoadingIndicator(
    isLoadingDocument,
    DOCX_LOADING_INDICATOR_DELAY_MS
  )
  const loadingState = (
    <EditorLoadingSurface showSpinner={shouldShowDocumentSpinner} />
  )
  const renderTrackedChangeCard = React.useMemo(
    () => createDocxTrackedChangeCardRenderer(editor.documentTheme),
    [editor.documentTheme]
  )
  const renderCommentCard = React.useMemo(
    () => createDocxCommentCardRenderer(editor.documentTheme),
    [editor.documentTheme]
  )
  const hasDocument = Boolean(url || uploadedDocxFile)
  const sidebarInline = useInlineThumbnailSidebar(viewerShellWidth)
  const pageCount =
    hasDocument && !isLoadingDocument && !loadError
      ? Math.max(1, reportedPageCount || editor.totalPages)
      : 0
  const controlsDisabled =
    !hasDocument || isLoadingDocument || Boolean(loadError)
  const thumbnailSidebarOpen = Boolean(
    sidebarOpen && (pageCount || isLoadingDocument)
  )
  const handlePageCountChange = React.useCallback((nextPageCount: number) => {
    setReportedPageCount(Math.max(1, Math.round(nextPageCount || 1)))
  }, [])
  const setViewportRef = React.useCallback((element: HTMLDivElement | null) => {
    viewportRef.current = element
    setViewportElement(element)
  }, [])
  const pageVirtualization = React.useMemo(
    () => ({
      enabled: true,
      overscan: 1,
      scrollElement: viewportElement,
      zoomScale: zoomScale / 100,
    }),
    [viewportElement, zoomScale]
  )
  useSuppressDocxPaddingWarning(!isLoadingDocument && !loadError)
  const [previousDefaultZoom, setPreviousDefaultZoom] = React.useState(
    resolvedDefaultZoomScale
  )
  if (previousDefaultZoom !== resolvedDefaultZoomScale) {
    setPreviousDefaultZoom(resolvedDefaultZoomScale)
    setZoomScale(resolvedDefaultZoomScale)
    setActivePage(1)
  }
  React.useEffect(() => {
    viewportRef.current?.scrollTo({ top: 0, left: 0 })
  }, [resolvedDefaultZoomScale, url])
  React.useEffect(() => {
    setDocumentTheme(effectiveIsDark ? "dark" : "light")
  }, [effectiveIsDark, setDocumentTheme])
  const [previousStatus, setPreviousStatus] = React.useState(status)
  if (previousStatus !== status) {
    setPreviousStatus(status)
    if (
      status.startsWith("Failed to load file") ||
      status === "Only .docx files are supported"
    ) {
      setLoadError(status)
      setIsLoadingDocument(false)
    }
  }
  React.useEffect(() => {
    let isCurrent = true
    async function load() {
      if (!uploadedDocxFile && !url) return
      try {
        const docxFile =
          uploadedDocxFile?.file ??
          (url ? await loadDocxFile(url, displayFileName) : null)
        if (!docxFile) return
        await importDocxFile(docxFile)
        if (isCurrent) {
          setIsLoadingDocument(false)
          setActivePage(1)
          viewportRef.current?.scrollTo({ top: 0, left: 0 })
        }
      } catch (error) {
        if (isCurrent) {
          setLoadError(
            error instanceof Error ? error.message : "Unknown DOCX load error"
          )
          setIsLoadingDocument(false)
        }
      }
    }
    void load()
    return () => {
      isCurrent = false
    }
  }, [displayFileName, importDocxFile, uploadedDocxFile, url])
  const updateActivePageFromViewport = React.useCallback(() => {
    const viewport = viewportRef.current
    if (!viewport || !pageCount) return
    const viewportRect = viewport.getBoundingClientRect()
    const viewportCenter = viewportRect.top + viewportRect.height / 2
    let closestPage = 1
    let closestDistance = Number.POSITIVE_INFINITY
    viewport
      .querySelectorAll<HTMLElement>(
        '[data-docx-page-wrapper="true"][data-index]'
      )
      .forEach((page) => {
        const pageIndex = Number(page.dataset.index)
        if (!Number.isFinite(pageIndex)) return
        const pageRect = page.getBoundingClientRect()
        const pageCenter = pageRect.top + pageRect.height / 2
        const distance = Math.abs(pageCenter - viewportCenter)
        if (distance < closestDistance) {
          closestDistance = distance
          closestPage = pageIndex + 1
        }
      })
    setActivePage((currentPage) =>
      currentPage === closestPage ? currentPage : closestPage
    )
  }, [pageCount])
  React.useEffect(() => {
    const viewport = viewportRef.current
    if (!viewport || !pageCount) return
    let frameId = 0
    const handleScroll = () => {
      window.cancelAnimationFrame(frameId)
      frameId = window.requestAnimationFrame(updateActivePageFromViewport)
    }
    frameId = window.requestAnimationFrame(updateActivePageFromViewport)
    viewport.addEventListener("scroll", handleScroll, { passive: true })
    return () => {
      window.cancelAnimationFrame(frameId)
      viewport.removeEventListener("scroll", handleScroll)
    }
  }, [pageCount, updateActivePageFromViewport])
  const scrollToPage = React.useCallback(
    (pageNumber: number) => {
      const viewport = viewportRef.current
      const targetPageIndex = pageNumber - 1
      const page = viewport?.querySelector<HTMLElement>(
        `[data-docx-page-wrapper="true"][data-index="${targetPageIndex}"]`
      )
      setActivePage(pageNumber)
      if (!viewport) return
      if (!page) {
        const pageStridePx =
          (pageLayout.pageHeightPx + pageLayout.viewportDefaults.pageGapPx) *
          (zoomScale / 100)
        viewport.scrollTo({
          top: Math.max(0, targetPageIndex * pageStridePx - 24),
          behavior: "auto",
        })
        return
      }
      viewport.scrollTo({
        top:
          page.getBoundingClientRect().top -
          viewport.getBoundingClientRect().top +
          viewport.scrollTop -
          24,
        behavior: "auto",
      })
    },
    [pageLayout.pageHeightPx, pageLayout.viewportDefaults.pageGapPx, zoomScale]
  )
  async function handleUpload(event: React.ChangeEvent<HTMLInputElement>) {
    const file = event.target.files?.[0]
    event.target.value = ""
    if (!file) return
    setZoomScale(resolvedDefaultZoomScale)
    setActivePage(1)
    setReportedPageCount(0)
    setIsLoadingDocument(true)
    setLoadError(undefined)
    setUploadedDocxFile({
      file,
      identity: `${file.name}-${file.size}-${file.lastModified}`,
    })
  }
  async function handleImageUpload(event: React.ChangeEvent<HTMLInputElement>) {
    const file = event.target.files?.[0]
    event.target.value = ""
    if (!file) return
    await editor.insertImageFile(file)
  }
  function openLinkEditor() {
    setLinkDraft(editor.selectedLink ?? "")
    setLinkEditorOpen(true)
  }
  function applyLink() {
    editor.setLink(linkDraft.trim() || undefined)
    setLinkEditorOpen(false)
  }
  return (
    <div
      className={cn(
        "flex h-[720px] min-h-0 flex-col overflow-hidden bg-background",
        className
      )}
    >
      <input
        ref={fileInputRef}
        type="file"
        accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
        className="hidden"
        onChange={handleUpload}
      />
      <input
        ref={imageInputRef}
        type="file"
        accept="image/*"
        className="hidden"
        onChange={handleImageUpload}
      />
      <DocxEditorToolbar
        activePage={activePage}
        controlsDisabled={controlsDisabled}
        editor={editor}
        isReadOnly={isReadOnly}
        onImageUploadClick={() => imageInputRef.current?.click()}
        onIsDarkChange={setIsDark}
        onIsReadOnlyChange={setIsReadOnly}
        onOpenLinkEditor={openLinkEditor}
        onToggleSidebar={() => setSidebarOpen((open) => !open)}
        onUploadClick={() => fileInputRef.current?.click()}
        pageCount={pageCount}
        setZoomScale={setZoomScale}
        showNightRenderToggle={shouldRenderNightMode}
        zoomScale={zoomScale}
      />
      <div
        ref={viewerShellRef}
        className="relative flex min-h-0 flex-1 overflow-hidden bg-muted/30"
      >
        {linkEditorOpen ? (
          <div className="absolute top-3 left-1/2 z-50 flex w-[min(420px,calc(100%-2rem))] -translate-x-1/2 items-center gap-2 rounded-lg border bg-background p-2 shadow-lg">
            <Input
              value={linkDraft}
              onChange={(event) => setLinkDraft(event.target.value)}
              onKeyDown={(event) => {
                if (event.key === "Enter") {
                  event.preventDefault()
                  applyLink()
                }
                if (event.key === "Escape") {
                  setLinkEditorOpen(false)
                }
              }}
              placeholder="https://example.com"
              aria-label="Link URL"
            />
            <Button type="button" size="sm" onClick={applyLink}>
              Apply
            </Button>
            <Button
              type="button"
              size="sm"
              variant="ghost"
              onClick={() => {
                editor.setLink(undefined)
                setLinkEditorOpen(false)
              }}
            >
              Clear
            </Button>
          </div>
        ) : null}
        <DocumentViewerThumbnailSidebar
          inline={sidebarInline}
          open={thumbnailSidebarOpen}
        >
          <InlineScrollArea2
            className="h-full"
            scrollFade
            viewportClassName="overscroll-contain"
          >
            <div className="p-4">
              {isLoadingDocument ? (
                <>
                  <div className="mx-auto h-28 w-20 overflow-hidden rounded-md bg-background shadow-xs">
                    <div className="h-full animate-pulse bg-muted" />
                  </div>
                  <div className="mx-auto mt-3 h-3 w-10 rounded-full bg-muted" />
                </>
              ) : (
                <div className="flex flex-col items-center gap-3">
                  {thumbnails.slice(0, pageCount || 0).map((thumbnail) => (
                    <Button
                      key={thumbnail.pageIndex}
                      type="button"
                      variant="ghost"
                      size="sm"
                      className={cn(
                        "!h-auto w-full flex-col items-center gap-2 p-2 text-xs shadow-none hover:bg-sidebar-accent",
                        thumbnail.pageNumber === activePage &&
                          "bg-sidebar-accent text-foreground",
                        thumbnail.pageNumber !== activePage &&
                          "text-muted-foreground"
                      )}
                      onFocus={(event) => event.currentTarget.blur()}
                      onClick={() => scrollToPage(thumbnail.pageNumber)}
                    >
                      <DocxSidebarThumbnail
                        canvasRef={thumbnail.canvasRef}
                        displayFileName={displayFileName}
                        hasError={thumbnail.status === "error"}
                        isActive={thumbnail.pageNumber === activePage}
                        isLoading={
                          !thumbnail.isMounted &&
                          thumbnail.status !== "ready" &&
                          thumbnail.status !== "error"
                        }
                        pageNumber={thumbnail.pageNumber}
                        pixelHeightPx={thumbnail.pixelHeightPx}
                        pixelWidthPx={thumbnail.pixelWidthPx}
                        previewAspectRatio={thumbnail.aspectRatio}
                      />
                      {thumbnail.pageNumber}
                    </Button>
                  ))}
                </div>
              )}
            </div>
          </InlineScrollArea2>
        </DocumentViewerThumbnailSidebar>
        <InlineScrollArea2
          className="min-h-0 flex-1"
          style={{ backgroundColor: viewerBackgroundColor }}
          viewportClassName="overscroll-contain p-4"
          viewportRef={setViewportRef}
        >
          {!url && !uploadedDocxFile ? (
            <div className="grid h-full min-h-96 place-items-center p-6 text-center">
              <div className="max-w-md rounded-lg border bg-background p-4 text-sm shadow-xs">
                <div className="font-medium">Upload a DOCX to edit</div>
                <div className="mt-1 text-muted-foreground">
                  Pass a DOCX URL with the <code>src</code> prop or upload a
                  file.
                </div>
                <Button
                  type="button"
                  variant="outline"
                  size="sm"
                  className="mt-4"
                  onClick={() => fileInputRef.current?.click()}
                >
                  <IconPlaceholder
                    lucide="Upload"
                    tabler="IconUpload"
                    hugeicons="Upload01Icon"
                    phosphor="UploadSimpleIcon"
                    remixicon="RiUpload2Line"
                    className="size-4"
                  />
                  Upload DOCX
                </Button>
              </div>
            </div>
          ) : loadError ? (
            <div className="grid h-full min-h-96 place-items-center p-6 text-center">
              <div className="max-w-md rounded-lg border bg-background p-4 text-sm text-destructive shadow-xs">
                <div className="font-medium">Unable to edit DOCX</div>
                <div className="mt-1 text-muted-foreground">{loadError}</div>
              </div>
            </div>
          ) : isLoadingDocument ? (
            loadingState
          ) : (
            <div className="mx-auto flex min-h-full justify-center">
              <div
                className={cn(
                  "origin-top",
                  editor.documentTheme === "dark" && "docx-night-reader-shell"
                )}
                style={{ zoom: zoomScale / 100 }}
              >
                <DocxEditorViewer
                  editor={editor}
                  mode={isReadOnly ? "read-only" : "edit"}
                  showTrackedChanges={editor.showTrackedChanges}
                  renderTrackedChangeCard={renderTrackedChangeCard}
                  showComments={editor.showComments}
                  renderCommentCard={renderCommentCard}
                  loadingState={loadingState}
                  pageBackgroundColor={
                    editor.documentTheme === "dark" ? "#0a0a0a" : undefined
                  }
                  pageGapBackgroundColor={viewerBackgroundColor}
                  pageVirtualization={pageVirtualization}
                  deferInitialPaginationPaint={false}
                  onPageCountChange={handlePageCountChange}
                />
              </div>
            </div>
          )}
        </InlineScrollArea2>
      </div>
    </div>
  )
}
type InlineRegistryIconProps = Omit<
  React.ComponentProps<"svg">,
  "children" | "strokeWidth"
> & { strokeWidth?: number }
function InlineScrollArea2({
  className,
  children,
  orientation = "both",
  scrollFade = false,
  scrollbarGutter = false,
  scrollbarOverflowOnly = false,
  viewportClassName,
  viewportProps,
  viewportRef,
  ...props
}: InlineScrollAreaProps) {
  const {
    className: viewportPropsClassName,
    ref: viewportPropsRef,
    ...resolvedViewportProps
  } = viewportProps ?? {}
  const composedViewportRef = React.useMemo(
    () => InlineComposeRefs(viewportPropsRef, viewportRef),
    [viewportPropsRef, viewportRef]
  )

  if (
    !viewportProps &&
    !viewportRef &&
    !viewportClassName &&
    !scrollFade &&
    !scrollbarGutter &&
    !scrollbarOverflowOnly
  ) {
    return (
      <InlineScrollArea
        {...props}
        className={cn(
          "size-full min-h-0",
          orientation === "horizontal" &&
            "[&>[data-orientation=vertical]]:hidden",
          className
        )}
      >
        {children}
        {orientation !== "vertical" ? (
          <ScrollBar orientation="horizontal" />
        ) : null}
      </InlineScrollArea>
    )
  }

  return (
    <ScrollAreaPrimitive.Root
      className={cn(
        "size-full min-h-0",
        scrollbarOverflowOnly &&
          "[&:not(:has([data-slot=scroll-area-viewport][data-has-overflow-x]))_[data-orientation=horizontal]]:hidden [&:not(:has([data-slot=scroll-area-viewport][data-has-overflow-y]))_[data-orientation=vertical]]:hidden",
        className
      )}
      {...props}
    >
      <ScrollAreaPrimitive.Viewport
        {...resolvedViewportProps}
        ref={composedViewportRef}
        className={cn(
          "h-full rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-ring",
          scrollFade &&
            "mask-t-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-start)))] mask-r-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-end)))] mask-b-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-end)))] mask-l-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-start)))] [--fade-size:1.5rem]",
          scrollbarGutter && orientation !== "vertical" && "pb-3.5",
          scrollbarGutter && orientation !== "horizontal" && "pe-3.5",
          viewportPropsClassName,
          viewportClassName
        )}
        data-slot="scroll-area-viewport"
      >
        {children}
      </ScrollAreaPrimitive.Viewport>
      {orientation !== "horizontal" ? (
        <ScrollBar orientation="vertical" />
      ) : null}
      {orientation !== "vertical" ? (
        <ScrollBar orientation="horizontal" />
      ) : null}
      {orientation === "both" ? <ScrollAreaPrimitive.Corner /> : null}
    </ScrollAreaPrimitive.Root>
  )
}
type InlineScrollAreaProps = ScrollAreaPrimitive.Root.Props & {
  orientation?: "vertical" | "horizontal" | "both"
  scrollFade?: boolean
  scrollbarGutter?: boolean
  scrollbarOverflowOnly?: boolean
  viewportClassName?: string
  viewportProps?: ScrollAreaPrimitive.Viewport.Props
  viewportRef?: React.Ref<HTMLDivElement>
}
function InlineComposeRefs<T>(...refs: Array<React.Ref<T> | undefined>) {
  return (node: T | null) => {
    for (const ref of refs) {
      if (!ref) continue
      if (typeof ref === "function") ref(node)
      else ref.current = node
    }
  }
}
function InlineSpinner({ className, ...props }: InlineRegistryIconProps) {
  return (
    <IconPlaceholder
      lucide="LoaderCircle"
      tabler="IconLoader2"
      hugeicons="Loading03Icon"
      phosphor="CircleNotchIcon"
      remixicon="RiLoader4Line"
      role="status"
      aria-label="Loading"
      className={cn("size-4 animate-spin", className)}
      {...props}
    />
  )
}
components/extend/docx-annotation-card.tsx
"use client"

import * as React from "react"
import type {
  DocxCommentCardRenderProps,
  DocxDocumentTheme,
  DocxTrackedChangeCardRenderProps,
} from "@extend-ai/react-docx"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Card } from "@/components/ui/card"

type AnnotationBadgeVariant =
  | "outline"
  | "secondary"
  | "success"
  | "error"
  | "warning"
  | "info"
function trackedChangeBadgeVariant(
  kind: DocxTrackedChangeCardRenderProps["change"]["kind"]
): AnnotationBadgeVariant {
  switch (kind) {
    case "insertion":
    case "move-to":
      return "success"
    case "deletion":
    case "move-from":
      return "error"
    default:
      return "warning"
  }
}
function trackedChangeBadgeLabel({
  change,
  kindLabel,
}: Pick<DocxTrackedChangeCardRenderProps, "change" | "kindLabel">) {
  switch (change.kind) {
    case "insertion":
      return "Inserted"
    case "deletion":
      return "Removed"
    case "move-from":
      return "Moved from"
    case "move-to":
      return "Moved to"
    default:
      return kindLabel
  }
}
function DocxAnnotationCard({
  anchorText,
  badge,
  badgeVariant = "outline",
  date,
  meta,
  documentTheme,
  snippet,
  style,
}: {
  anchorText?: string
  badge: string
  badgeVariant?: AnnotationBadgeVariant
  date?: string
  documentTheme: DocxDocumentTheme
  meta: string
  snippet: string
  style: React.CSSProperties
}) {
  const isDarkDocument = documentTheme === "dark"
  const cardStyle: React.CSSProperties = {
    ...style,
    backgroundColor: isDarkDocument
      ? "rgb(24 24 27 / 0.95)"
      : "rgb(255 255 255 / 0.95)",
    color: isDarkDocument ? "#f4f4f5" : "#18181b",
  }
  const mutedTextColor = isDarkDocument ? "#a1a1aa" : "#71717a"
  const anchorStyle: React.CSSProperties = {
    backgroundColor: isDarkDocument
      ? "rgb(63 63 70 / 0.55)"
      : "rgb(244 244 245 / 0.75)",
    color: mutedTextColor,
  }
  return (
    <Card
      style={cardStyle}
      className="pointer-events-auto box-border gap-2 rounded-lg p-2 shadow-sm before:rounded-[7px]"
    >
      <div className="flex min-w-0 items-start justify-between gap-2">
        <div
          className="min-w-0 text-[11px] leading-tight font-medium"
          style={{ color: mutedTextColor }}
        >
          <div className="truncate">{meta}</div>
          {date ? <div className="mt-0.5 truncate">{date}</div> : null}
        </div>
        <Badge
          variant={badgeVariant === "outline" ? "outline" : "secondary"}
          className={cn(
            "h-4 px-1 text-[10px]",
            cn(
              "max-w-[92px] truncate",
              badgeVariant === "success" &&
                "bg-green-500/10 text-green-700 dark:text-green-300",
              badgeVariant === "error" && "bg-destructive/10 text-destructive",
              badgeVariant === "warning" &&
                "bg-amber-500/10 text-amber-700 dark:text-amber-300",
              badgeVariant === "info" &&
                "bg-blue-500/10 text-blue-700 dark:text-blue-300"
            )
          )}
        >
          {badge}
        </Badge>
      </div>
      {anchorText ? (
        <div
          className="rounded-md px-2 py-1 text-[11px] leading-snug italic"
          style={anchorStyle}
        >
          {anchorText}
        </div>
      ) : null}
      <div className="text-xs leading-snug break-words">{snippet}</div>
    </Card>
  )
}
export function createDocxTrackedChangeCardRenderer(
  documentTheme: DocxDocumentTheme
) {
  return function renderDocxTrackedChangeCard({
    change,
    formattedDate,
    kindLabel,
    snippet,
    style,
  }: DocxTrackedChangeCardRenderProps) {
    return (
      <DocxAnnotationCard
        badge={trackedChangeBadgeLabel({ change, kindLabel })}
        badgeVariant={trackedChangeBadgeVariant(change.kind)}
        date={formattedDate}
        documentTheme={documentTheme}
        meta={change.author?.trim() || "Unknown author"}
        snippet={snippet}
        style={style}
      />
    )
  }
}
export function createDocxCommentCardRenderer(
  documentTheme: DocxDocumentTheme
) {
  return function renderDocxCommentCard({
    comment,
    formattedDate,
    snippet,
    style,
  }: DocxCommentCardRenderProps) {
    const badge = comment.resolved
      ? "Resolved"
      : comment.parentId !== undefined
        ? "Reply"
        : "Comment"
    return (
      <DocxAnnotationCard
        anchorText={comment.anchorText}
        badge={badge}
        badgeVariant={comment.resolved ? "secondary" : "info"}
        date={formattedDate}
        documentTheme={documentTheme}
        meta={comment.author?.trim() || "Unknown author"}
        snippet={snippet}
        style={style}
      />
    )
  }
}

More from extend

Browse all components in this library →