import type { Editor } from "@tiptap/react" import type { Mark, ResolvedPos } from "@tiptap/pm/model" export const DOCS_FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 72] as const export const DOCS_DEFAULT_FONT_SIZE = 11 /** Matches .ultidrive-richtext-editor h1–h4 in richtext-editor.css (rem × 16). */ const HEADING_FONT_SIZE_PX: Record = { 1: 28, 2: 22, 3: 18, 4: 16, } let lastToolbarFontSize = DOCS_DEFAULT_FONT_SIZE export type FontSizeToolbarState = | { kind: "single"; size: number } | { kind: "unset" } function parseFontSizePx(raw: string | null | undefined): number | null { if (!raw) return null const parsed = Number.parseInt(String(raw).replace("px", ""), 10) return Number.isFinite(parsed) ? parsed : null } function nearestFontSizeIndex(current: number): number { let best = 0 let bestDistance = Number.POSITIVE_INFINITY DOCS_FONT_SIZES.forEach((size, index) => { const distance = Math.abs(size - current) if (distance < bestDistance) { bestDistance = distance best = index } }) return best } function textStyleFontSizePx( marks: ReadonlyArray<{ type: { name: string }; attrs: Record }> ): number | null { const mark = marks.find((m) => m.type.name === "textStyle") return parseFontSizePx(mark?.attrs.fontSize as string | undefined) } function headingLevelAt($pos: ResolvedPos): number | null { for (let depth = $pos.depth; depth > 0; depth--) { const node = $pos.node(depth) if (node.type.name === "heading") { return node.attrs.level as number } } return null } function headingFontSizePx(level: number): number { return HEADING_FONT_SIZE_PX[level] ?? DOCS_DEFAULT_FONT_SIZE } /** Effective size of existing text at a position (no storedMarks). */ function effectiveFontSizeAtPos($pos: ResolvedPos, marks: readonly Mark[]): number { const marked = textStyleFontSizePx(marks) if (marked != null) return marked const level = headingLevelAt($pos) if (level != null) return headingFontSizePx(level) return DOCS_DEFAULT_FONT_SIZE } function resolveCursorFontSizePx(editor: Editor): number { const { state } = editor const { $from } = state.selection if (state.storedMarks) { const stored = textStyleFontSizePx(state.storedMarks) if (stored != null) return stored } const atCursor = textStyleFontSizePx($from.marks()) if (atCursor != null) return atCursor const level = headingLevelAt($from) if (level != null) return headingFontSizePx(level) // Empty block: show last toolbar pick (typing size). Existing body text → default. if ($from.parent.content.size === 0) return lastToolbarFontSize return DOCS_DEFAULT_FONT_SIZE } function collectEffectiveSizesInRange(editor: Editor, from: number, to: number): Set { const sizes = new Set() const { state } = editor state.doc.nodesBetween(from, to, (node, pos) => { if (!node.isText || !node.text) return const nodeStart = pos const nodeEnd = pos + node.text.length const start = Math.max(from, nodeStart) const end = Math.min(to, nodeEnd) if (start >= end) return sizes.add(effectiveFontSizeAtPos(state.doc.resolve(start), node.marks)) }) return sizes } export function readFontSizeToolbarState(editor: Editor): FontSizeToolbarState { const { from, to, empty } = editor.state.selection if (empty) { return { kind: "single", size: resolveCursorFontSizePx(editor) } } const sizes = collectEffectiveSizesInRange(editor, from, to) if (sizes.size === 0) { return { kind: "single", size: resolveCursorFontSizePx(editor) } } if (sizes.size === 1) { return { kind: "single", size: [...sizes][0]! } } return { kind: "unset" } } export function readFontSizePxForStep(editor: Editor): number { const state = readFontSizeToolbarState(editor) return state.kind === "single" ? state.size : DOCS_DEFAULT_FONT_SIZE } export function applyFontSizePx(editor: Editor, size: number) { lastToolbarFontSize = size editor.chain().focus().setFontSize(`${size}px`).run() } export function stepFontSizePx(editor: Editor, direction: -1 | 1) { const idx = nearestFontSizeIndex(readFontSizePxForStep(editor)) const nextIdx = Math.min( DOCS_FONT_SIZES.length - 1, Math.max(0, idx + direction) ) applyFontSizePx(editor, DOCS_FONT_SIZES[nextIdx]!) } export function canStepFontSizePx(editor: Editor, direction: -1 | 1): boolean { const idx = nearestFontSizeIndex(readFontSizePxForStep(editor)) if (direction < 0) return idx > 0 return idx < DOCS_FONT_SIZES.length - 1 } /** @deprecated Use readFontSizeToolbarState */ export function readFontSizePx(editor: Editor): number { const state = readFontSizeToolbarState(editor) return state.kind === "single" ? state.size : DOCS_DEFAULT_FONT_SIZE }