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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user