619 lines
22 KiB
TypeScript
619 lines
22 KiB
TypeScript
"use client"
|
||
|
||
import { memo, useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react"
|
||
import { EditorContent, type Editor } from "@tiptap/react"
|
||
import type { DocPageLayout, DocPageSetup } from "@/lib/drive/doc-page-setup"
|
||
import {
|
||
DocsHeaderFooterBand,
|
||
type DocsHeaderFooterEditTarget,
|
||
type DocsHeaderFooterRegion,
|
||
} from "@/components/drive/richtext/docs-header-footer-region"
|
||
import { DocsBodyMarginMasks } from "@/components/drive/richtext/docs-body-margin-masks"
|
||
import {
|
||
DOCS_REGION_EDIT_EVENT,
|
||
type DocsRegionEditDetail,
|
||
} from "@/lib/drive/docs-page-elements-bridge"
|
||
import {
|
||
DOCS_CANVAS_PADDING_TOP_NARROW_PX,
|
||
DOCS_CANVAS_PADDING_Y_PX,
|
||
DOCS_PAGE_GAP_PX,
|
||
} from "@/lib/drive/docs-page-layout-constants"
|
||
import {
|
||
effectiveMarginsPx,
|
||
} from "@/lib/drive/docs-header-footer-layout"
|
||
import {
|
||
computePageCount,
|
||
computePageMetrics,
|
||
computeProseMinHeight,
|
||
computeStackHeight,
|
||
} from "@/lib/drive/docs-page-metrics"
|
||
import { docsPageLengthToScreen, docsZoomToScale } from "@/lib/drive/docs-ruler-scale"
|
||
import { cn } from "@/lib/utils"
|
||
import { DocsGraphicSnapGuides } from "@/components/drive/richtext/docs-graphic-snap-guides"
|
||
import { DocsTableContextMenu } from "@/components/drive/richtext/docs-table-context-menu"
|
||
import { applyPageFlowLayout, computeSimulatedLayoutHeight, readPageFlowMetrics } from "@/lib/drive/extensions/docs-page-flow-decoration"
|
||
import { focusEditorAtPointer } from "@/lib/drive/focus-editor-at-pointer"
|
||
|
||
/** Total layout height inside ProseMirror (blocks + flow spacers). */
|
||
function measureProseContentHeight(prose: HTMLElement): number {
|
||
const metrics = readPageFlowMetrics(prose)
|
||
if (!metrics) return 0
|
||
|
||
const blocks: Array<{ height: number }> = []
|
||
for (const child of prose.children) {
|
||
const el = child as HTMLElement
|
||
if (el.classList.contains("docs-page-flow-spacer")) continue
|
||
const style = getComputedStyle(el)
|
||
const marginBottom = parseFloat(style.marginBottom) || 0
|
||
blocks.push({ height: el.offsetHeight + marginBottom })
|
||
}
|
||
|
||
if (blocks.length === 0) return 0
|
||
|
||
const simulated = computeSimulatedLayoutHeight(
|
||
blocks,
|
||
metrics.bodyAreaH,
|
||
metrics.interPageSpacer
|
||
)
|
||
|
||
let maxBottom = 0
|
||
for (const child of prose.children) {
|
||
const el = child as HTMLElement
|
||
maxBottom = Math.max(maxBottom, el.offsetTop + el.offsetHeight)
|
||
}
|
||
|
||
return Math.max(simulated, maxBottom)
|
||
}
|
||
|
||
function DocsPageViewInner({
|
||
editor,
|
||
pageLayout,
|
||
zoom,
|
||
editable,
|
||
showLayout,
|
||
showNonPrintableChars,
|
||
editorMode,
|
||
canvasRef: canvasRefProp,
|
||
onPageCountChange,
|
||
onNarrowViewportChange,
|
||
onCanvasHeightChange,
|
||
onRegionContentChange,
|
||
onPageSetupChange,
|
||
onRegionEditorChange,
|
||
}: {
|
||
editor: Editor
|
||
pageLayout: DocPageLayout
|
||
zoom: number
|
||
editable: boolean
|
||
showLayout: boolean
|
||
showRuler: boolean
|
||
showNonPrintableChars: boolean
|
||
editorMode: "edit" | "suggest" | "view"
|
||
canvasRef?: RefObject<HTMLDivElement | null>
|
||
onPageCountChange?: (count: number) => void
|
||
onNarrowViewportChange?: (narrow: boolean) => void
|
||
onCanvasHeightChange?: (height: number) => void
|
||
onRegionContentChange?: (
|
||
region: DocsHeaderFooterRegion,
|
||
content: Record<string, unknown>,
|
||
meta: { pageIndex: number; contentHeightPx: number }
|
||
) => void
|
||
onPageSetupChange?: (patch: Partial<DocPageSetup>) => void
|
||
onRegionEditorChange?: (editor: Editor | null) => void
|
||
}) {
|
||
const pageWidth = pageLayout.widthPx
|
||
const pageHeight = pageLayout.heightPx
|
||
const margins = pageLayout.marginsPx
|
||
|
||
const [pageCount, setPageCount] = useState(1)
|
||
const [narrowViewport, setNarrowViewport] = useState(false)
|
||
const [editingTarget, setEditingTarget] = useState<DocsHeaderFooterEditTarget>(null)
|
||
const [pageRegionHeights, setPageRegionHeights] = useState<
|
||
Record<string, number>
|
||
>({})
|
||
|
||
const handleRegionHeightMeasure = useCallback(
|
||
(payload: {
|
||
region: DocsHeaderFooterRegion
|
||
pageIndex: number
|
||
heightPx: number
|
||
}) => {
|
||
const key = `${payload.region}-${payload.pageIndex}`
|
||
setPageRegionHeights((prev) => {
|
||
if (prev[key] === payload.heightPx) return prev
|
||
return { ...prev, [key]: payload.heightPx }
|
||
})
|
||
},
|
||
[]
|
||
)
|
||
|
||
const measuredRegionHeights = useMemo(() => {
|
||
let header = 0
|
||
let footer = 0
|
||
for (const [key, heightPx] of Object.entries(pageRegionHeights)) {
|
||
if (key.startsWith("header-")) header = Math.max(header, heightPx)
|
||
if (key.startsWith("footer-")) footer = Math.max(footer, heightPx)
|
||
}
|
||
return {
|
||
...(header > 0 ? { header } : {}),
|
||
...(footer > 0 ? { footer } : {}),
|
||
}
|
||
}, [pageRegionHeights])
|
||
|
||
const effectiveMargins = useMemo(
|
||
() =>
|
||
effectiveMarginsPx(
|
||
pageLayout,
|
||
null,
|
||
editingTarget ? undefined : measuredRegionHeights
|
||
),
|
||
[pageLayout, editingTarget, measuredRegionHeights]
|
||
)
|
||
const metrics = useMemo(
|
||
() => computePageMetrics({ ...pageLayout, effectiveMarginsPx: effectiveMargins }),
|
||
[pageLayout, effectiveMargins]
|
||
)
|
||
|
||
const localCanvasRef = useRef<HTMLDivElement>(null)
|
||
const canvasRef = canvasRefProp ?? localCanvasRef
|
||
const contentRef = useRef<HTMLDivElement>(null)
|
||
const onPageCountChangeRef = useRef(onPageCountChange)
|
||
onPageCountChangeRef.current = onPageCountChange
|
||
|
||
const scale = docsZoomToScale(zoom)
|
||
const scaledWidth = docsPageLengthToScreen(pageWidth, scale)
|
||
|
||
const stopRegionEdit = useCallback(() => {
|
||
setEditingTarget(null)
|
||
onRegionEditorChange?.(null)
|
||
}, [onRegionEditorChange])
|
||
|
||
const startRegionEdit = useCallback(
|
||
(region: DocsHeaderFooterRegion, pageIndex: number) => {
|
||
setEditingTarget({ region, pageIndex })
|
||
},
|
||
[]
|
||
)
|
||
|
||
const handleRegionEditorReady = useCallback(
|
||
(editor: Editor | null) => {
|
||
onRegionEditorChange?.(editor)
|
||
},
|
||
[onRegionEditorChange]
|
||
)
|
||
|
||
useEffect(() => {
|
||
const onKey = (event: KeyboardEvent) => {
|
||
if (event.key === "Escape" && editingTarget) {
|
||
event.preventDefault()
|
||
stopRegionEdit()
|
||
}
|
||
}
|
||
window.addEventListener("keydown", onKey)
|
||
return () => window.removeEventListener("keydown", onKey)
|
||
}, [editingTarget, stopRegionEdit])
|
||
|
||
useEffect(() => {
|
||
const onRegionEditRequest = (event: Event) => {
|
||
const detail = (event as CustomEvent<DocsRegionEditDetail>).detail
|
||
if (!detail?.region) return
|
||
startRegionEdit(detail.region, detail.pageIndex ?? 0)
|
||
}
|
||
window.addEventListener(DOCS_REGION_EDIT_EVENT, onRegionEditRequest)
|
||
return () => window.removeEventListener(DOCS_REGION_EDIT_EVENT, onRegionEditRequest)
|
||
}, [startRegionEdit])
|
||
|
||
useEffect(() => {
|
||
const canvas = canvasRef.current
|
||
if (!canvas) return
|
||
const syncViewport = () => {
|
||
const narrow = canvas.clientWidth < scaledWidth
|
||
setNarrowViewport(narrow)
|
||
onNarrowViewportChange?.(narrow)
|
||
onCanvasHeightChange?.(canvas.clientHeight)
|
||
}
|
||
syncViewport()
|
||
const ro = new ResizeObserver(syncViewport)
|
||
ro.observe(canvas)
|
||
return () => ro.disconnect()
|
||
}, [onCanvasHeightChange, onNarrowViewportChange, scaledWidth, canvasRef])
|
||
|
||
useEffect(() => {
|
||
if (!showLayout) return
|
||
const surface = contentRef.current
|
||
if (!surface) return
|
||
|
||
let debounceId: ReturnType<typeof setTimeout> | null = null
|
||
let cancelled = false
|
||
|
||
const measurePageCount = () => {
|
||
const prose = surface.querySelector(".ProseMirror") as HTMLElement | null
|
||
if (!prose) return
|
||
const contentHeight = measureProseContentHeight(prose)
|
||
const count = computePageCount(contentHeight, metrics)
|
||
setPageCount((prev) => (prev === count ? prev : count))
|
||
}
|
||
|
||
const runLayoutPasses = (passesLeft: number) => {
|
||
if (cancelled || editor.isDestroyed) return
|
||
requestAnimationFrame(() => {
|
||
if (cancelled || editor.isDestroyed) return
|
||
const changed = applyPageFlowLayout(editor)
|
||
if (changed && passesLeft > 1) {
|
||
runLayoutPasses(passesLeft - 1)
|
||
return
|
||
}
|
||
measurePageCount()
|
||
})
|
||
}
|
||
|
||
let flushPending = false
|
||
const scheduleLayout = () => {
|
||
if (!flushPending) {
|
||
flushPending = true
|
||
requestAnimationFrame(() => {
|
||
flushPending = false
|
||
runLayoutPasses(2)
|
||
})
|
||
}
|
||
if (debounceId) clearTimeout(debounceId)
|
||
debounceId = setTimeout(() => {
|
||
debounceId = null
|
||
runLayoutPasses(2)
|
||
}, 32)
|
||
}
|
||
|
||
scheduleLayout()
|
||
|
||
const onTransaction = ({ transaction }: { transaction: { docChanged: boolean } }) => {
|
||
if (!transaction.docChanged) return
|
||
scheduleLayout()
|
||
}
|
||
editor.on("transaction", onTransaction)
|
||
|
||
return () => {
|
||
cancelled = true
|
||
if (debounceId) clearTimeout(debounceId)
|
||
editor.off("transaction", onTransaction)
|
||
}
|
||
}, [editor, metrics, pageRegionHeights, showLayout])
|
||
|
||
useEffect(() => {
|
||
onPageCountChangeRef.current?.(pageCount)
|
||
}, [pageCount])
|
||
|
||
const stackHeight = computeStackHeight(pageCount, pageHeight)
|
||
const proseMinHeight = computeProseMinHeight(pageCount, metrics)
|
||
|
||
const scaledHeight = docsPageLengthToScreen(stackHeight, scale)
|
||
const verticalPaddingTop = narrowViewport
|
||
? DOCS_CANVAS_PADDING_TOP_NARROW_PX
|
||
: DOCS_CANVAS_PADDING_Y_PX
|
||
const verticalPaddingBottom = DOCS_CANVAS_PADDING_Y_PX
|
||
|
||
const textAreaBorderCss = pageLayout.textAreaBorderCss
|
||
const sheetBorderCss = pageLayout.sheetBorderCss
|
||
const pageBackground = pageLayout.pageColor
|
||
const backgroundLayers = pageLayout.pageBackgroundLayers
|
||
|
||
const renderPageBackground = (index: number) => (
|
||
<>
|
||
{backgroundLayers?.gradientCss ? (
|
||
<div
|
||
key={`bg-gradient-${index}`}
|
||
className="pointer-events-none absolute inset-0 z-[1]"
|
||
style={{ background: backgroundLayers.gradientCss }}
|
||
aria-hidden
|
||
/>
|
||
) : null}
|
||
{backgroundLayers?.fillImageStyle ? (
|
||
<div
|
||
key={`bg-image-${index}`}
|
||
className="pointer-events-none absolute inset-0 z-[1]"
|
||
style={backgroundLayers.fillImageStyle}
|
||
aria-hidden
|
||
/>
|
||
) : null}
|
||
{backgroundLayers?.watermarkStyle ? (
|
||
<div
|
||
key={`bg-watermark-${index}`}
|
||
className="pointer-events-none absolute inset-0 z-[2] flex items-center justify-center overflow-hidden"
|
||
aria-hidden
|
||
>
|
||
{backgroundLayers.watermarkStyle.imageSrc ? (
|
||
<img
|
||
src={backgroundLayers.watermarkStyle.imageSrc}
|
||
alt=""
|
||
className="max-h-[70%] max-w-[70%] select-none object-contain"
|
||
style={{
|
||
opacity: backgroundLayers.watermarkStyle.opacity,
|
||
transform: `rotate(${backgroundLayers.watermarkStyle.rotationDeg}deg)`,
|
||
}}
|
||
/>
|
||
) : (
|
||
<span
|
||
className="select-none whitespace-nowrap text-[72px] font-light leading-none"
|
||
style={{
|
||
color: backgroundLayers.watermarkStyle.color,
|
||
opacity: backgroundLayers.watermarkStyle.opacity,
|
||
transform: `rotate(${backgroundLayers.watermarkStyle.rotationDeg}deg)`,
|
||
}}
|
||
>
|
||
{backgroundLayers.watermarkStyle.text}
|
||
</span>
|
||
)}
|
||
</div>
|
||
) : null}
|
||
</>
|
||
)
|
||
|
||
const bodyDimmed = editingTarget != null
|
||
|
||
return (
|
||
<div ref={canvasRef} className={cn(
|
||
"ultidrive-docs-canvas h-full min-h-0 overflow-auto",
|
||
showLayout ? "bg-[#f9fbfd] dark:bg-[#202124]" : "bg-white dark:bg-background",
|
||
showNonPrintableChars && "docs-show-non-printable",
|
||
editorMode === "suggest" && "docs-editor-mode-suggest",
|
||
editorMode === "view" && "docs-editor-mode-view"
|
||
)}>
|
||
<div
|
||
className="mx-auto"
|
||
style={{
|
||
width: scaledWidth,
|
||
paddingTop: verticalPaddingTop,
|
||
paddingBottom: verticalPaddingBottom,
|
||
minHeight: (showLayout ? scaledHeight : proseMinHeight + margins.top + margins.bottom) + verticalPaddingTop + verticalPaddingBottom,
|
||
}}
|
||
>
|
||
<div
|
||
className="relative mx-auto overflow-visible"
|
||
style={{ width: scaledWidth, height: showLayout ? scaledHeight : undefined }}
|
||
>
|
||
<div
|
||
data-docs-page-stack
|
||
data-docs-page-height={pageHeight}
|
||
data-docs-page-width={pageWidth}
|
||
data-docs-page-scale={scale}
|
||
data-docs-page-margin-top={effectiveMargins.top}
|
||
data-docs-page-margin-right={effectiveMargins.right}
|
||
data-docs-page-margin-bottom={effectiveMargins.bottom}
|
||
data-docs-page-margin-left={effectiveMargins.left}
|
||
className="absolute left-1/2 top-0 -translate-x-1/2 overflow-visible"
|
||
style={{
|
||
width: pageWidth,
|
||
height: stackHeight,
|
||
transform: `scale(${scale})`,
|
||
transformOrigin: "top center",
|
||
}}
|
||
>
|
||
{showLayout
|
||
? Array.from({ length: pageCount }, (_, index) => {
|
||
const pageTop = index * (pageHeight + DOCS_PAGE_GAP_PX)
|
||
return (
|
||
<div
|
||
key={index}
|
||
className={cn(
|
||
"ultidrive-docs-page absolute left-0 overflow-hidden dark:bg-white",
|
||
sheetBorderCss && "ultidrive-docs-page--imported-border"
|
||
)}
|
||
style={{
|
||
top: pageTop,
|
||
width: pageWidth,
|
||
height: pageHeight,
|
||
backgroundColor: pageBackground,
|
||
boxShadow:
|
||
"0 1px 3px 1px rgba(60,64,67,.15), 0 1px 2px 0 rgba(60,64,67,.3)",
|
||
...(sheetBorderCss
|
||
? {
|
||
borderTop: sheetBorderCss.top ?? "none",
|
||
borderRight: sheetBorderCss.right ?? "none",
|
||
borderBottom: sheetBorderCss.bottom ?? "none",
|
||
borderLeft: sheetBorderCss.left ?? "none",
|
||
}
|
||
: {}),
|
||
}}
|
||
aria-hidden
|
||
>
|
||
{renderPageBackground(index)}
|
||
</div>
|
||
)
|
||
})
|
||
: null}
|
||
|
||
{showLayout && textAreaBorderCss
|
||
? Array.from({ length: pageCount }, (_, index) => (
|
||
<div
|
||
key={`text-border-${index}`}
|
||
className="pointer-events-none absolute z-[5] box-border"
|
||
style={{
|
||
top: index * (pageHeight + DOCS_PAGE_GAP_PX) + effectiveMargins.top,
|
||
left: effectiveMargins.left,
|
||
width: pageWidth - effectiveMargins.left - effectiveMargins.right,
|
||
height: pageHeight - effectiveMargins.top - effectiveMargins.bottom,
|
||
borderTop: textAreaBorderCss.top ?? "none",
|
||
borderRight: textAreaBorderCss.right ?? "none",
|
||
borderBottom: textAreaBorderCss.bottom ?? "none",
|
||
borderLeft: textAreaBorderCss.left ?? "none",
|
||
}}
|
||
aria-hidden
|
||
/>
|
||
))
|
||
: null}
|
||
|
||
{showLayout ? (
|
||
<DocsBodyMarginMasks
|
||
pageCount={pageCount}
|
||
pageLayout={pageLayout}
|
||
pageWidth={pageWidth}
|
||
pageHeight={pageHeight}
|
||
pageColor={pageBackground}
|
||
pageRegionHeights={pageRegionHeights}
|
||
/>
|
||
) : null}
|
||
|
||
{showLayout
|
||
? Array.from({ length: pageCount }, (_, index) => {
|
||
const pageTop = index * (pageHeight + DOCS_PAGE_GAP_PX)
|
||
return (
|
||
<div key={`hf-${index}`}>
|
||
<DocsHeaderFooterBand
|
||
region="header"
|
||
pageLayout={pageLayout}
|
||
pageIndex={index}
|
||
pageTop={pageTop}
|
||
pageWidth={pageWidth}
|
||
pageHeight={pageHeight}
|
||
margins={margins}
|
||
editable={editable}
|
||
editingTarget={editingTarget}
|
||
onStartEdit={startRegionEdit}
|
||
onStopEdit={stopRegionEdit}
|
||
onContentChange={(region, content, meta) =>
|
||
onRegionContentChange?.(region, content, meta)
|
||
}
|
||
onPageSetupChange={(patch) => onPageSetupChange?.(patch)}
|
||
onRegionHeightMeasure={handleRegionHeightMeasure}
|
||
onRegionEditorReady={
|
||
editingTarget?.region === "header" &&
|
||
editingTarget.pageIndex === index
|
||
? handleRegionEditorReady
|
||
: undefined
|
||
}
|
||
/>
|
||
<DocsHeaderFooterBand
|
||
region="footer"
|
||
pageLayout={pageLayout}
|
||
pageIndex={index}
|
||
pageTop={pageTop}
|
||
pageWidth={pageWidth}
|
||
pageHeight={pageHeight}
|
||
margins={margins}
|
||
editable={editable}
|
||
editingTarget={editingTarget}
|
||
onStartEdit={startRegionEdit}
|
||
onStopEdit={stopRegionEdit}
|
||
onContentChange={(region, content, meta) =>
|
||
onRegionContentChange?.(region, content, meta)
|
||
}
|
||
onPageSetupChange={(patch) => onPageSetupChange?.(patch)}
|
||
onRegionHeightMeasure={handleRegionHeightMeasure}
|
||
onRegionEditorReady={
|
||
editingTarget?.region === "footer" &&
|
||
editingTarget.pageIndex === index
|
||
? handleRegionEditorReady
|
||
: undefined
|
||
}
|
||
/>
|
||
</div>
|
||
)
|
||
})
|
||
: null}
|
||
|
||
{bodyDimmed ? (
|
||
<div
|
||
className="pointer-events-none absolute inset-0 z-[12] bg-[#e8eaed]/40"
|
||
style={{ height: stackHeight }}
|
||
aria-hidden
|
||
/>
|
||
) : null}
|
||
|
||
<div
|
||
id="docs-page-graphic-layer-behind"
|
||
className="pointer-events-none absolute left-0 top-0 z-[12]"
|
||
style={{ width: pageWidth, height: stackHeight }}
|
||
/>
|
||
|
||
<div
|
||
id="docs-page-graphic-layer-front"
|
||
className="pointer-events-none absolute left-0 top-0 z-[18]"
|
||
style={{ width: pageWidth, height: stackHeight }}
|
||
/>
|
||
|
||
<DocsGraphicSnapGuides pageWidth={pageWidth} pageHeight={pageHeight} />
|
||
|
||
<div
|
||
ref={contentRef}
|
||
className={cn(
|
||
"ultidrive-docs-editor-surface relative",
|
||
showLayout && "ultidrive-docs-editor-surface--paginated",
|
||
!showLayout && "ultidrive-docs-editor-surface--compact",
|
||
bodyDimmed && "ultidrive-docs-editor-surface--dimmed"
|
||
)}
|
||
style={{
|
||
padding: `${effectiveMargins.top}px ${effectiveMargins.right}px ${effectiveMargins.bottom}px ${effectiveMargins.left}px`,
|
||
height: showLayout ? stackHeight : undefined,
|
||
minHeight: showLayout
|
||
? undefined
|
||
: proseMinHeight + effectiveMargins.top + effectiveMargins.bottom,
|
||
["--docs-stack-height" as string]: `${stackHeight}px`,
|
||
["--docs-prose-min-height" as string]: `${proseMinHeight}px`,
|
||
["--docs-body-area-h" as string]: `${metrics.bodyAreaHeight}px`,
|
||
["--docs-inter-page-spacer" as string]: `${metrics.interPageSpacer}px`,
|
||
}}
|
||
onMouseDown={(event) => {
|
||
if (bodyDimmed) {
|
||
const target = event.target as HTMLElement
|
||
if (
|
||
!target.closest(".docs-hf-band") &&
|
||
!target.closest(".docs-hf-chrome")
|
||
) {
|
||
stopRegionEdit()
|
||
}
|
||
return
|
||
}
|
||
if (!editable || editingTarget) return
|
||
const target = event.target as HTMLElement
|
||
if (target.closest(".ProseMirror")) return
|
||
event.preventDefault()
|
||
focusEditorAtPointer(editor, event.clientX, event.clientY)
|
||
}}
|
||
>
|
||
<DocsTableContextMenu editor={editor} disabled={!editable || bodyDimmed}>
|
||
<div className="min-h-0 min-w-0">
|
||
<EditorContent
|
||
editor={editor}
|
||
className={cn(!editable && "pointer-events-none select-text")}
|
||
/>
|
||
</div>
|
||
</DocsTableContextMenu>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export const DocsPageView = memo(DocsPageViewInner)
|
||
|
||
export function DocsStatusBar({
|
||
pageLayout,
|
||
pageCount,
|
||
currentPage = 1,
|
||
className,
|
||
}: {
|
||
pageLayout: DocPageLayout
|
||
pageCount: number
|
||
currentPage?: number
|
||
className?: string
|
||
}) {
|
||
const pageLabel = Math.min(Math.max(1, currentPage), Math.max(1, pageCount))
|
||
|
||
return (
|
||
<div
|
||
className={cn(
|
||
"docs-status-bar flex shrink-0 items-center justify-between border-t border-[#dadce0] bg-[#edf2fa] px-4 py-1 text-xs text-[#5f6368] dark:border-border dark:bg-muted/40 dark:text-muted-foreground",
|
||
className
|
||
)}
|
||
>
|
||
<span>
|
||
Page {pageLabel} sur {pageCount}
|
||
</span>
|
||
<span>
|
||
{pageLayout.format.label} ({pageLayout.format.widthMm} × {pageLayout.format.heightMm} mm)
|
||
</span>
|
||
</div>
|
||
)
|
||
}
|