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,64 @@
|
||||
import { View } from "@react-pdf/renderer";
|
||||
import { getMeasurer, measureToken } from "../engine/measure";
|
||||
import { resolveFace, roleToken, type RoleName, type TypeToken } from "../fonts/roles";
|
||||
import { chunkIdentifier } from "../model/text";
|
||||
import { useAvailableWidth } from "./AvailableWidth";
|
||||
import { fitSize, type FitInfo } from "./FitText";
|
||||
import { Txt, type TxtProps } from "./Txt";
|
||||
|
||||
export interface IdTextInfo extends FitInfo {
|
||||
mode: "single" | "chunked";
|
||||
chunks: string[];
|
||||
}
|
||||
|
||||
export interface IdTextProps extends Omit<TxtProps, "children" | "role" | "maxLines" | "width" | "grow"> {
|
||||
role: RoleName | TypeToken;
|
||||
children: string;
|
||||
/** Width to fit; defaults to the available width of the enclosing cell. */
|
||||
width?: number;
|
||||
minSize?: number;
|
||||
/** For tests and audits only. */
|
||||
onFit?: (info: IdTextInfo) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text for identifiers (e-mail, URL, GSTIN, phone). textkit never breaks inside a word, so a long
|
||||
* value would overflow silently. This shrinks it to the role's minimum size; if it is still too wide
|
||||
* it is split at "@ . / - _" into a flex-wrap row of adjacent Text chunks (no gaps, so extraction
|
||||
* joins them back into the original string).
|
||||
*/
|
||||
export function IdText({ role, children, width, minSize, onFit, ...rest }: IdTextProps) {
|
||||
const avail = useAvailableWidth();
|
||||
const target = width ?? avail;
|
||||
const token = roleToken(role);
|
||||
const fit = fitSize(token, children, target, minSize, { weight: rest.weight, italic: rest.italic });
|
||||
|
||||
if (fit.fits) {
|
||||
onFit?.({ ...fit, mode: "single", chunks: [children] });
|
||||
return <Txt {...rest} role={{ ...token, size: fit.size }}>{children}</Txt>;
|
||||
}
|
||||
|
||||
const sized: TypeToken = { ...token, size: fit.size };
|
||||
const m = getMeasurer();
|
||||
const family = resolveFace(token.face, rest.faceOverrides);
|
||||
const widthOf = (s: string) =>
|
||||
measureToken(m, sized, s, { weight: rest.weight, italic: rest.italic, family });
|
||||
let chunks = chunkIdentifier(children, 1);
|
||||
for (let max = children.length; max >= 1; max--) {
|
||||
const candidate = chunkIdentifier(children, max);
|
||||
if (candidate.every((c) => widthOf(c) <= target + 1e-6)) {
|
||||
chunks = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
onFit?.({ ...fit, mode: "chunked", chunks });
|
||||
return (
|
||||
<View style={{ flexDirection: "row", flexWrap: "wrap", width: target, minWidth: target, maxWidth: target } as never}>
|
||||
{chunks.map((c, i) => (
|
||||
<Txt key={i} {...rest} align={undefined} role={sized}>
|
||||
{c}
|
||||
</Txt>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user