Operand

do no harem.

gram: docs

> ./packages/core/src/utils/stylePreview.ts

/**
* Paragraph-style preview + option resolution — shared between the React and
* Vue toolbars so the style-picker dropdown looks and behaves identically.
*
* Pure logic only: no i18n and no framework CSS types. The returned preview is
* a plain `{ fontSize, lineHeight, fontWeight?, fontStyle?, color? }` object,
* which is structurally assignable to both React's `CSSProperties` and Vue's
* inline-style record, so neither adapter needs a cast. Name localization stays
* in the adapters (they own the i18n `t()` boundary).
* @packageDocumentation
* @public
*/
import type { Style } from '../types/document';
/**
* Inline preview style for a paragraph-style dropdown item.
* @public
*/
export interface StylePreviewProps {
fontSize: string;
lineHeight: string;
fontWeight?: 'bold';
fontStyle?: 'italic';
color?: string;
}
/**
* A resolved paragraph-style option (document order, with extracted visual
* fields). Adapters add their own localized label on top of `name`.
* @public
*/
export interface ResolvedStyleOption {
styleId: string;
name: string;
priority: number;
fontSize?: number;
bold?: boolean;
italic?: boolean;
color?: string;
}
/** Dropdown preview sizes (px) for well-known styles, matching Google Docs. @public */
export const STYLE_PREVIEW_SIZES: Record<string, number> = {
Title: 26,
Subtitle: 18,
Heading1: 24,
Heading2: 18,
Heading3: 16,
Heading4: 14,
Heading5: 13,
Heading6: 13,
Normal: 14,
};
/** Styles rendered bold in the dropdown preview by default. @public */
export const STYLE_DEFAULT_BOLD: ReadonlySet<string> = new Set([
'Title',
'Heading1',
'Heading2',
'Heading3',
]);
/** Default preview text color (hex, no `#`) for styles that don't set one. @public */
export const STYLE_DEFAULT_COLOR: Record<string, string> = { Subtitle: '666666' };
/** Google-Docs heading preview color. @public */
export const HEADING_COLOR = '#4a6c8c';
/**
* Build the inline preview style for a style option. `fontSize` is in
* half-points (OOXML); known styles use {@link STYLE_PREVIEW_SIZES} instead.
* @public
*/
export function getStylePreviewProps(input: {
styleId: string;
fontSize?: number;
bold?: boolean;
italic?: boolean;
color?: string;
}): StylePreviewProps {
const known = STYLE_PREVIEW_SIZES[input.styleId];
const fontSize = known
? `${known}px`
: `${Math.min(Math.max(input.fontSize ? input.fontSize / 2 : 11, 11), 20)}px`;
const props: StylePreviewProps = { fontSize, lineHeight: '1.3' };
if (input.bold ?? STYLE_DEFAULT_BOLD.has(input.styleId)) props.fontWeight = 'bold';
if (input.italic) props.fontStyle = 'italic';
const color = input.color ?? STYLE_DEFAULT_COLOR[input.styleId];
if (color) props.color = `#${color}`;
else if (input.styleId.startsWith('Heading')) props.color = HEADING_COLOR;
return props;
}
/**
* Filter the document's styles to the visible paragraph styles, extract their
* visual fields, and sort by UI priority — the list a toolbar style picker
* shows. Returns `[]` when there are no styles (adapters fall back to presets).
* @public
*/
export function resolveParagraphStyleOptions(styles: Style[] | undefined): ResolvedStyleOption[] {
if (!styles || styles.length === 0) return [];
return styles
.filter((s) => s.type === 'paragraph')
.filter((s) => (s.qFormat ? true : !(s.hidden || s.semiHidden)))
.map((s) => ({
styleId: s.styleId,
name: s.name || s.styleId,
priority: s.uiPriority ?? 99,
fontSize: s.rPr?.fontSize,
bold: s.rPr?.bold,
italic: s.rPr?.italic,
color: s.rPr?.color?.rgb,
}))
.sort((a, b) => a.priority - b.priority);
}