+ {Array.from({ length: pageCount }, (_, index) => {
+ const pageTop = index * (pageHeight + DOCS_PAGE_GAP_PX)
+ return (
+
+ )
+ })}
+ >
+ )
+}
diff --git a/components/drive/richtext/docs-page-separators.tsx b/components/drive/richtext/docs-page-separators.tsx
new file mode 100644
index 0000000..a6a2a26
--- /dev/null
+++ b/components/drive/richtext/docs-page-separators.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import { DOCS_PAGE_GAP_PX } from "@/lib/drive/docs-page-layout-constants"
+
+/** Canvas-colored gap + side margin gutters between stacked pages. */
+export function DocsPageSeparators({
+ pageCount,
+ pageHeight,
+ pageWidth,
+ margins,
+ pageColor,
+ interPageSpacer,
+}: {
+ pageCount: number
+ pageHeight: number
+ pageWidth: number
+ margins: { top: number; right: number; bottom: number; left: number }
+ pageColor: string
+ interPageSpacer: number
+}) {
+ if (pageCount <= 1) return null
+
+ return (
+ <>
+ {Array.from({ length: pageCount - 1 }, (_, index) => {
+ const pageTop = index * (pageHeight + DOCS_PAGE_GAP_PX)
+ const gapTop = pageTop + pageHeight
+ const zoneTop = gapTop - margins.bottom
+
+ return (
+
+ )
+ })}
+ >
+ )
+}
diff --git a/components/drive/richtext/docs-page-view.tsx b/components/drive/richtext/docs-page-view.tsx
index de55793..afe8f77 100644
--- a/components/drive/richtext/docs-page-view.tsx
+++ b/components/drive/richtext/docs-page-view.tsx
@@ -9,6 +9,8 @@ import {
type DocsHeaderFooterRegion,
} from "@/components/drive/richtext/docs-header-footer-region"
import { DocsBodyMarginMasks } from "@/components/drive/richtext/docs-body-margin-masks"
+import { DocsPageSeparators } from "@/components/drive/richtext/docs-page-separators"
+import { DocsPageRims } from "@/components/drive/richtext/docs-page-rims"
import {
DOCS_REGION_EDIT_EVENT,
type DocsRegionEditDetail,
@@ -31,40 +33,12 @@ import { docsPageLengthToScreen, docsZoomToScale } from "@/lib/drive/docs-ruler-
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 {
+ applyPageFlowLayout,
+ measureFlowContentHeight,
+} 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,
@@ -227,24 +201,29 @@ function DocsPageViewInner({
let cancelled = false
const measurePageCount = () => {
- const prose = surface.querySelector(".ProseMirror") as HTMLElement | null
- if (!prose) return
- const contentHeight = measureProseContentHeight(prose)
+ if (editor.isDestroyed) return
+ const contentHeight = measureFlowContentHeight(editor.view)
const count = computePageCount(contentHeight, metrics)
setPageCount((prev) => (prev === count ? prev : count))
}
- const runLayoutPasses = (passesLeft: number) => {
+ const runLayoutPasses = () => {
if (cancelled || editor.isDestroyed) return
- requestAnimationFrame(() => {
+ let passesLeft = 12
+ const step = () => {
if (cancelled || editor.isDestroyed) return
- const changed = applyPageFlowLayout(editor)
- if (changed && passesLeft > 1) {
- runLayoutPasses(passesLeft - 1)
- return
- }
- measurePageCount()
- })
+ requestAnimationFrame(() => {
+ if (cancelled || editor.isDestroyed) return
+ const changed = applyPageFlowLayout(editor)
+ passesLeft -= 1
+ if (changed && passesLeft > 0) {
+ step()
+ return
+ }
+ measurePageCount()
+ })
+ }
+ step()
}
let flushPending = false
@@ -253,13 +232,13 @@ function DocsPageViewInner({
flushPending = true
requestAnimationFrame(() => {
flushPending = false
- runLayoutPasses(2)
+ runLayoutPasses()
})
}
if (debounceId) clearTimeout(debounceId)
debounceId = setTimeout(() => {
debounceId = null
- runLayoutPasses(2)
+ runLayoutPasses()
}, 32)
}
@@ -282,6 +261,15 @@ function DocsPageViewInner({
onPageCountChangeRef.current?.(pageCount)
}, [pageCount])
+ useEffect(() => {
+ if (!showLayout || pageCount <= 1) return
+ const id = requestAnimationFrame(() => {
+ if (editor.isDestroyed) return
+ applyPageFlowLayout(editor)
+ })
+ return () => cancelAnimationFrame(id)
+ }, [editor, pageCount, showLayout])
+
const stackHeight = computeStackHeight(pageCount, pageHeight)
const proseMinHeight = computeProseMinHeight(pageCount, metrics)
@@ -394,7 +382,7 @@ function DocsPageViewInner({
@@ -452,6 +430,26 @@ function DocsPageViewInner({
/>
) : null}
+ {showLayout ? (
+
+ ) : null}
+
+ {showLayout ? (
+
+ ) : null}
+
{showLayout
? Array.from({ length: pageCount }, (_, index) => {
const pageTop = index * (pageHeight + DOCS_PAGE_GAP_PX)
diff --git a/lib/drive/docs-page-flow.test.ts b/lib/drive/docs-page-flow.test.ts
index d1eb5d6..65a15e7 100644
--- a/lib/drive/docs-page-flow.test.ts
+++ b/lib/drive/docs-page-flow.test.ts
@@ -1,6 +1,11 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"
-import { countPageFlowSpacers } from "./extensions/docs-page-flow-decoration.ts"
+import {
+ computePageFlowPushes,
+ computeSimulatedLayoutHeight,
+ countPageFlowSpacers,
+} from "./extensions/docs-page-flow-decoration.ts"
+import { computePageCount } from "./docs-page-metrics.ts"
describe("docs-page-flow spacers", () => {
const bodyAreaH = 900
@@ -30,4 +35,69 @@ describe("docs-page-flow spacers", () => {
1
)
})
+
+ it("inserts spacers at each page boundary for many blocks", () => {
+ const blocks = Array.from({ length: 10 }, () => ({ height: 400 }))
+ const pushes = computePageFlowPushes(blocks, bodyAreaH, interPageSpacer)
+ assert.equal(pushes.length, 4)
+ assert.deepEqual(
+ pushes.map((p) => p.breakY),
+ [900, 2000, 3100, 4200]
+ )
+ })
+
+ it("pushes content that starts in the inter-page gap", () => {
+ const pushes = computePageFlowPushes(
+ [{ height: 910 }, { height: 100 }],
+ bodyAreaH,
+ interPageSpacer
+ )
+ assert.equal(pushes.length, 2)
+ assert.equal(pushes[0].blockIndex, 0)
+ assert.equal(pushes[1].blockIndex, 1)
+ assert.equal(pushes[1].pushPx, 190)
+ })
+
+ it("does not double-count nested blocks (list model)", () => {
+ const listHeight = 300
+ const topLevelOnly = [{ height: 850 }, { height: listHeight }, { height: 100 }]
+ const nestedOvercount = [
+ { height: 850 },
+ { height: listHeight },
+ ...Array.from({ length: 10 }, () => ({ height: listHeight })),
+ { height: 100 },
+ ]
+
+ const topLevelPushes = countPageFlowSpacers(topLevelOnly, bodyAreaH, interPageSpacer)
+ const nestedPushes = countPageFlowSpacers(nestedOvercount, bodyAreaH, interPageSpacer)
+ assert.equal(topLevelPushes, 1)
+ assert.ok(nestedPushes > topLevelPushes)
+ })
+
+ it("simulated height matches page count across five pages", () => {
+ const blocks = Array.from({ length: 10 }, () => ({ height: 400 }))
+ const simH = computeSimulatedLayoutHeight(blocks, bodyAreaH, interPageSpacer)
+ const pages = computePageCount(simH, {
+ bodyAreaHeight: bodyAreaH,
+ interPageSpacer,
+ pageWidth: 0,
+ pageHeight: 0,
+ margins: { top: 0, right: 0, bottom: 0, left: 0 },
+ headerMarginPx: 0,
+ footerMarginPx: 0,
+ })
+ assert.equal(simH, 5200)
+ assert.equal(pages, 5)
+ })
+
+ it("inserts multiple spacers for tall content split into lines", () => {
+ const lineHeight = 24
+ const totalH = 2500
+ const lines = Math.ceil(totalH / lineHeight)
+ const units = Array.from({ length: lines }, (_, i) => ({
+ height: i === lines - 1 ? totalH - lineHeight * (lines - 1) : lineHeight,
+ }))
+ const pushes = computePageFlowPushes(units, bodyAreaH, interPageSpacer)
+ assert.ok(pushes.length >= 2)
+ })
})
diff --git a/lib/drive/docs-page-layout-constants.ts b/lib/drive/docs-page-layout-constants.ts
index a71ffc7..60cf734 100644
--- a/lib/drive/docs-page-layout-constants.ts
+++ b/lib/drive/docs-page-layout-constants.ts
@@ -1,5 +1,5 @@
/** Gap between stacked pages in print layout (px, unscaled). */
-export const DOCS_PAGE_GAP_PX = 12
+export const DOCS_PAGE_GAP_PX = 24
export const DOCS_CANVAS_PADDING_Y_PX = 32
export const DOCS_CANVAS_PADDING_TOP_NARROW_PX = 0
diff --git a/lib/drive/docs-page-metrics.test.ts b/lib/drive/docs-page-metrics.test.ts
index 4612584..2843471 100644
--- a/lib/drive/docs-page-metrics.test.ts
+++ b/lib/drive/docs-page-metrics.test.ts
@@ -26,7 +26,7 @@ describe("docs-page-metrics", () => {
})
it("computes stack height with page gaps", () => {
- assert.equal(computeStackHeight(2, 1122), 1122 * 2 + 12)
+ assert.equal(computeStackHeight(2, 1122), 1122 * 2 + 24)
})
it("computes prose min height with inter-page spacers", () => {
diff --git a/lib/drive/extensions/docs-page-flow-decoration.ts b/lib/drive/extensions/docs-page-flow-decoration.ts
index cfe1953..534b73a 100644
--- a/lib/drive/extensions/docs-page-flow-decoration.ts
+++ b/lib/drive/extensions/docs-page-flow-decoration.ts
@@ -1,10 +1,15 @@
import { Extension } from "@tiptap/core"
import type { Editor } from "@tiptap/react"
+import type { Node as PMNode } from "@tiptap/pm/model"
import { Plugin, PluginKey } from "@tiptap/pm/state"
import { Decoration, DecorationSet, type EditorView } from "@tiptap/pm/view"
export const PAGE_FLOW_PLUGIN_KEY = new PluginKey("docsPageFlowDecoration")
+type FlowUnit = { height: number; offset: number }
+
+const LIST_TYPES = new Set(["bulletList", "orderedList", "taskList"])
+
function decorationSetsEqual(
current: DecorationSet | undefined,
next: DecorationSet
@@ -49,6 +54,13 @@ function blockDomAtOffset(view: EditorView, offset: number): HTMLElement | null
return null
}
+function nestedDomAt(view: EditorView, offset: number, selector: string): HTMLElement | null {
+ const dom = view.nodeDOM(offset)
+ if (!(dom instanceof HTMLElement)) return null
+ if (dom.matches(selector)) return dom
+ return dom.closest(selector)
+}
+
/** Block height in prose px (offsetHeight + bottom margin; immune to canvas scale). */
function measureBlockFlowHeight(dom: HTMLElement): number {
const style = getComputedStyle(dom)
@@ -56,6 +68,191 @@ function measureBlockFlowHeight(dom: HTMLElement): number {
return dom.offsetHeight + marginBottom
}
+function measureVisualLineHeights(dom: HTMLElement): Array<{ top: number; height: number }> {
+ const blockTop = dom.getBoundingClientRect().top
+ const range = document.createRange()
+ const lines: Array<{ top: number; height: number }> = []
+
+ const walker = document.createTreeWalker(dom, NodeFilter.SHOW_TEXT)
+ while (walker.nextNode()) {
+ const text = walker.currentNode as Text
+ if (!text.nodeValue?.length) continue
+ range.selectNodeContents(text)
+ for (const rect of range.getClientRects()) {
+ if (rect.width <= 0 || rect.height <= 0) continue
+ const relTop = rect.top - blockTop
+ const last = lines[lines.length - 1]
+ if (!last || Math.abs(last.top - relTop) > 2) {
+ lines.push({ top: relTop, height: rect.height })
+ } else {
+ last.height = Math.max(last.height, rect.height)
+ }
+ }
+ }
+
+ if (lines.length === 0) {
+ const h = dom.offsetHeight
+ if (h > 0) lines.push({ top: 0, height: h })
+ }
+
+ return lines
+}
+
+function findPosAtVisualLine(
+ view: EditorView,
+ dom: HTMLElement,
+ blockRect: DOMRect,
+ relTop: number
+): number | null {
+ const coords = view.posAtCoords({
+ left: blockRect.left + Math.min(Math.max(1, blockRect.width / 2), blockRect.width - 1),
+ top: blockRect.top + relTop + 1,
+ })
+ return coords?.pos ?? null
+}
+
+function estimatedLineUnits(
+ view: EditorView,
+ node: PMNode,
+ offset: number,
+ dom: HTMLElement,
+ totalH: number
+): FlowUnit[] {
+ const style = getComputedStyle(dom)
+ let lineHeight = parseFloat(style.lineHeight)
+ if (!Number.isFinite(lineHeight) || style.lineHeight === "normal") {
+ lineHeight = (parseFloat(style.fontSize) || 16) * 1.25
+ }
+ const lineCount = Math.max(1, Math.ceil(totalH / lineHeight))
+ const textLen = Math.max(1, node.content.size)
+ const units: FlowUnit[] = []
+
+ for (let i = 0; i < lineCount; i++) {
+ const h =
+ i === lineCount - 1
+ ? Math.max(1, totalH - lineHeight * (lineCount - 1))
+ : lineHeight
+ const pos =
+ i === 0
+ ? offset
+ : offset + Math.min(textLen - 1, Math.floor((textLen * i) / lineCount))
+ units.push({ height: h, offset: pos })
+ }
+
+ return units
+}
+
+function expandTallLeafUnits(
+ view: EditorView,
+ node: PMNode,
+ offset: number,
+ dom: HTMLElement,
+ totalH: number,
+ bodyAreaH: number
+): FlowUnit[] {
+ if (totalH <= bodyAreaH) {
+ return [{ height: totalH, offset }]
+ }
+
+ const lineSegments = measureVisualLineHeights(dom)
+ if (lineSegments.length <= 1) {
+ return estimatedLineUnits(view, node, offset, dom, totalH)
+ }
+
+ const blockRect = dom.getBoundingClientRect()
+ return lineSegments.map((segment, lineIndex) => ({
+ height: segment.height,
+ offset:
+ lineIndex === 0
+ ? offset
+ : (findPosAtVisualLine(view, dom, blockRect, segment.top) ?? offset),
+ }))
+}
+
+function expandLeafBlockUnits(
+ view: EditorView,
+ node: PMNode,
+ offset: number,
+ units: FlowUnit[],
+ bodyAreaH: number
+): void {
+ const dom = blockDomAtOffset(view, offset)
+ if (!dom) return
+ const totalH = measureBlockFlowHeight(dom)
+ if (totalH <= 0) return
+ units.push(...expandTallLeafUnits(view, node, offset, dom, totalH, bodyAreaH))
+}
+
+function expandNodeUnits(
+ view: EditorView,
+ node: PMNode,
+ offset: number,
+ units: FlowUnit[],
+ bodyAreaH: number
+): void {
+ if (!node.isBlock) return
+
+ if (LIST_TYPES.has(node.type.name)) {
+ let itemOffset = offset + 1
+ for (let j = 0; j < node.childCount; j++) {
+ const item = node.child(j)
+ const liDom = nestedDomAt(view, itemOffset, "li")
+ const h = liDom ? measureBlockFlowHeight(liDom) : 0
+ if (h > 0) {
+ units.push(...expandTallLeafUnits(view, item, itemOffset, liDom, h, bodyAreaH))
+ }
+ itemOffset += item.nodeSize
+ }
+ return
+ }
+
+ if (node.type.name === "table") {
+ let rowOffset = offset + 1
+ for (let j = 0; j < node.childCount; j++) {
+ const row = node.child(j)
+ if (row.type.name !== "tableRow") {
+ rowOffset += row.nodeSize
+ continue
+ }
+ const rowDom = nestedDomAt(view, rowOffset, "tr")
+ const h = rowDom ? measureBlockFlowHeight(rowDom) : 0
+ if (h > 0) {
+ units.push(...expandTallLeafUnits(view, row, rowOffset, rowDom, h, bodyAreaH))
+ }
+ rowOffset += row.nodeSize
+ }
+ return
+ }
+
+ if (node.type.name === "blockquote") {
+ let innerOffset = offset + 1
+ for (let j = 0; j < node.childCount; j++) {
+ const child = node.child(j)
+ if (child.isBlock) {
+ expandLeafBlockUnits(view, child, innerOffset, units, bodyAreaH)
+ }
+ innerOffset += child.nodeSize
+ }
+ return
+ }
+
+ expandLeafBlockUnits(view, node, offset, units, bodyAreaH)
+}
+
+function collectFlowUnits(view: EditorView, bodyAreaH: number): FlowUnit[] {
+ const units: FlowUnit[] = []
+ const doc = view.state.doc
+ let offset = 0
+
+ for (let i = 0; i < doc.childCount; i++) {
+ const node = doc.child(i)
+ expandNodeUnits(view, node, offset, units, bodyAreaH)
+ offset += node.nodeSize
+ }
+
+ return units
+}
+
function createSpacerElement(height: number): HTMLElement {
const el = document.createElement("div")
el.className = "docs-page-flow-spacer"
@@ -80,13 +277,24 @@ export function computePageFlowPushes(
if (blockH <= 0) return
while (simulatedY + blockH > nextBreakY) {
+ const nextBodyStart = nextBreakY + interPageSpacer
+
if (simulatedY < nextBreakY) {
const pushPx = nextBreakY - simulatedY + interPageSpacer
pushes.push({ blockIndex, pushPx, breakY: nextBreakY })
- simulatedY = nextBreakY + interPageSpacer
+ simulatedY = nextBodyStart
nextBreakY += pageStep
break
}
+
+ if (simulatedY < nextBodyStart) {
+ const pushPx = nextBodyStart - simulatedY
+ pushes.push({ blockIndex, pushPx, breakY: nextBreakY })
+ simulatedY = nextBodyStart
+ nextBreakY += pageStep
+ break
+ }
+
nextBreakY += pageStep
}
@@ -108,11 +316,20 @@ export function computeSimulatedLayoutHeight(
for (const { height: blockH } of blocks) {
if (blockH <= 0) continue
while (simulatedY + blockH > nextBreakY) {
+ const nextBodyStart = nextBreakY + interPageSpacer
+
if (simulatedY < nextBreakY) {
- simulatedY = nextBreakY + interPageSpacer
+ simulatedY = nextBodyStart
nextBreakY += pageStep
break
}
+
+ if (simulatedY < nextBodyStart) {
+ simulatedY = nextBodyStart
+ nextBreakY += pageStep
+ break
+ }
+
nextBreakY += pageStep
}
simulatedY += blockH
@@ -121,45 +338,55 @@ export function computeSimulatedLayoutHeight(
return simulatedY
}
-function collectFlowBlocks(view: EditorView): Array<{ height: number; offset: number }> {
- const blocks: Array<{ height: number; offset: number }> = []
- view.state.doc.forEach((node, offset) => {
- if (!node.isBlock || node.type.name === "doc") return
- const dom = blockDomAtOffset(view, offset)
- if (!dom) return
- const blockH = measureBlockFlowHeight(dom)
- if (blockH <= 0) return
- blocks.push({ height: blockH, offset })
- })
- return blocks
-}
-
/** Build page-flow spacer widgets for blocks that cross a page body boundary. */
export function buildPageFlowDecorations(view: EditorView): DecorationSet {
const metrics = readPageFlowMetrics(view.dom)
if (!metrics) return DecorationSet.empty
const { bodyAreaH, interPageSpacer } = metrics
- const blocks = collectFlowBlocks(view)
+ const units = collectFlowUnits(view, bodyAreaH)
const pushes = computePageFlowPushes(
- blocks.map((b) => ({ height: b.height })),
+ units.map((u) => ({ height: u.height })),
bodyAreaH,
interPageSpacer
)
const decorations = pushes.map(({ blockIndex, pushPx, breakY }) => {
- const block = blocks[blockIndex]
+ const unit = units[blockIndex]
return Decoration.widget(
- block.offset,
+ unit.offset,
() => createSpacerElement(pushPx),
- { side: -1, key: `page-flow-spacer-${block.offset}-${breakY}`, pushPx }
+ { side: -1, key: `page-flow-spacer-${unit.offset}-${breakY}`, pushPx }
)
})
return DecorationSet.create(view.state.doc, decorations)
}
+/** Measure paginated prose height using the same flow units as layout. */
+export function measureFlowContentHeight(view: EditorView): number {
+ const metrics = readPageFlowMetrics(view.dom)
+ if (!metrics) return 0
+
+ const units = collectFlowUnits(view, metrics.bodyAreaH)
+ if (units.length === 0) return 0
+
+ const simulated = computeSimulatedLayoutHeight(
+ units.map((u) => ({ height: u.height })),
+ metrics.bodyAreaH,
+ metrics.interPageSpacer
+ )
+
+ let maxBottom = 0
+ for (const child of view.dom.children) {
+ const el = child as HTMLElement
+ maxBottom = Math.max(maxBottom, el.offsetTop + el.offsetHeight)
+ }
+
+ return Math.max(simulated, maxBottom)
+}
+
/** Apply page-flow layout once. Returns true if decorations changed. */
export function applyPageFlowLayout(editor: Editor): boolean {
if (editor.isDestroyed || !editor.isInitialized) return false
diff --git a/next-env.d.ts b/next-env.d.ts
index c4b7818..9edff1c 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/dev/types/routes.d.ts";
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/styles/docs-print.css b/styles/docs-print.css
index bbc2d55..e3427e1 100644
--- a/styles/docs-print.css
+++ b/styles/docs-print.css
@@ -58,6 +58,12 @@
border-color: transparent !important;
}
+.docs-printing.docs-pdf-capture .docs-page-gap-band,
+.docs-printing.docs-pdf-capture .docs-page-inter-margin-gutter,
+.docs-printing.docs-pdf-capture .docs-page-rim {
+ display: none !important;
+}
+
.docs-printing.docs-pdf-capture .ultidrive-docs-editor-surface--dimmed .ProseMirror {
opacity: 1 !important;
}
diff --git a/styles/richtext-editor.css b/styles/richtext-editor.css
index e8b052e..b80a543 100644
--- a/styles/richtext-editor.css
+++ b/styles/richtext-editor.css
@@ -597,12 +597,36 @@ html.dark .ultidrive-richtext-region-editor table th {
.ultidrive-docs-editor-surface--paginated {
overflow: visible;
+ background: transparent;
}
.ultidrive-docs-editor-surface--paginated .ProseMirror {
min-height: var(--docs-prose-min-height);
- max-height: var(--docs-prose-min-height);
overflow: visible;
+ background: transparent;
+}
+
+.docs-page-gap-band {
+ background: #f9fbfd;
+}
+
+html.dark .docs-page-gap-band {
+ background: #202124;
+}
+
+.docs-page-rim {
+ border: 1px solid #dadce0;
+ box-shadow: 0 1px 3px 1px rgba(60, 64, 67, 0.15), 0 1px 2px 0 rgba(60, 64, 67, 0.3);
+ background: transparent;
+}
+
+html.dark .docs-page-rim:not(.docs-page-rim--imported) {
+ border-color: #5f6368;
+ box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.35), 0 1px 2px 0 rgba(0, 0, 0, 0.45);
+}
+
+.docs-page-rim--imported {
+ box-shadow: none;
}
.docs-hf-chrome__checkbox {
@@ -666,6 +690,7 @@ html.dark .docs-hf-chrome__options:hover {
flex-shrink: 0;
pointer-events: none;
user-select: none;
+ background: transparent;
}
.ultidrive-docs-editor-surface .ProseMirror .docs-page-flow-spacer {
@@ -1262,6 +1287,11 @@ html.dark .docs-menu-badge {
border: 1px solid #dadce0;
}
+.ultidrive-docs-page--sheet {
+ border: none;
+ box-shadow: none;
+}
+
.ultidrive-docs-page--imported-border {
border: none;
}