Add typography roles, text primitives, fontkit measurer and layout lint

Txt always sets an absolute line height; RenderTxt never does. Row pins
every cell and stacks when the fill column would be too narrow. IdText and
FitText shrink then chunk or truncate. The fontkit measurer matches react-pdf
text widths exactly. A TypeScript-AST lint test enforces the layout rules
(no lineHeight outside Txt, no non-literal fixed/wrap/minPresenceAhead,
flexGrow needs flexBasis, no stray absolute positioning).
This commit is contained in:
2026-10-04 04:48:19 +05:30
parent 92ba3d9590
commit 40aa3e05d9
22 changed files with 1375 additions and 34 deletions
+103
View File
@@ -0,0 +1,103 @@
import { View } from "@react-pdf/renderer";
import { Children, type ReactNode } from "react";
import { colsDetailed, FILL, pinned, type ColSpec } from "../engine/columns";
import { AvailableWidthProvider, useAvailableWidth } from "./AvailableWidth";
import { pdfFlags, type PdfFlagsInput } from "./pdfProps";
export interface RowProps extends PdfFlagsInput {
/** One spec per child, in order. */
widths: ColSpec[];
gap?: number;
/** Stack the children as a column when the row is narrower than this. */
stackBelow?: number;
/** Cross-axis alignment of the cells; never flex-end around multi-line text. */
align?: "flex-start" | "center" | "stretch";
/** Overrides the width from AvailableWidth. */
width?: number;
style?: Record<string, unknown>;
children?: ReactNode;
}
/**
* A flex row whose children each sit in a cell of explicit width: fixed and fractional cells are pinned
* (width = minWidth = maxWidth) and the fill cell uses {flexGrow: 1, flexBasis: 0}. Descendants read the
* cell's content width through useAvailableWidth(). When a column falls under its `min` (or the row is
* narrower than `stackBelow`), the children are stacked at full width instead.
*/
export function Row({ widths, gap = 0, stackBelow, align = "flex-start", width, style, children, fixed, wrap, minPresenceAhead }: RowProps) {
const inherited = useAvailableWidth();
const total = width ?? inherited;
const kids = Children.toArray(children);
if (kids.length !== widths.length) throw new Error(`Row: ${kids.length} children for ${widths.length} column specs`);
const { widths: w, ok } = colsDetailed(total, widths, gap);
const stacked = !ok || (stackBelow !== undefined && total < stackBelow);
const flags = pdfFlags({ fixed, wrap, minPresenceAhead });
if (stacked) {
return (
<View style={{ flexDirection: "column", alignItems: "flex-start", ...style } as never} {...flags}>
{kids.map((child, i) => (
<View key={i} style={{ ...pinned(total), alignItems: "flex-start", ...(gap && i > 0 ? { marginTop: gap } : {}) } as never}>
<AvailableWidthProvider width={total}>{child}</AvailableWidthProvider>
</View>
))}
</View>
);
}
return (
<View style={{ flexDirection: "row", alignItems: align, ...style } as never} {...flags}>
{kids.map((child, i) => {
const isFill = "fill" in widths[i];
const box = isFill ? FILL : pinned(w[i]);
return (
<View
key={i}
style={{ ...box, alignItems: "flex-start", ...(i > 0 && gap ? { marginLeft: gap } : {}) } as never}
>
<AvailableWidthProvider width={w[i]}>{child}</AvailableWidthProvider>
</View>
);
})}
</View>
);
}
export interface CellProps {
/** Padding in pt: one number for all sides, or per side. */
padding?: number | { top?: number; right?: number; bottom?: number; left?: number };
borderLeft?: number;
borderRight?: number;
style?: Record<string, unknown>;
children?: ReactNode;
}
/**
* A padded/bordered box inside a Row column. It fills the column's width and republishes the content
* width (column width minus its own padding and border) to its descendants.
*/
export function Cell({ padding = 0, borderLeft = 0, borderRight = 0, style, children }: CellProps) {
const full = useAvailableWidth();
const p = typeof padding === "number" ? { top: padding, right: padding, bottom: padding, left: padding } : padding;
const inner = Math.max(0, full - (p.left ?? 0) - (p.right ?? 0) - borderLeft - borderRight);
return (
<View
style={
{
...pinned(full),
alignItems: "flex-start",
paddingTop: p.top ?? 0,
paddingRight: p.right ?? 0,
paddingBottom: p.bottom ?? 0,
paddingLeft: p.left ?? 0,
...(borderLeft ? { borderLeftWidth: borderLeft } : {}),
...(borderRight ? { borderRightWidth: borderRight } : {}),
...style,
} as never
}
>
<AvailableWidthProvider width={inner}>{children}</AvailableWidthProvider>
</View>
);
}