Dokumentation

Layout

Jedes Layout entsteht aus einer Handvoll Primitives. Column und Row stapeln ihre Kinder, Spacer und Expanded verteilen den Platz dazwischen, und Box und Padding rahmen Inhalt ein und rücken ihn ein.

Column und Row

Column stapelt seine Kinder von oben nach unten, Row von links nach rechts. Beide nehmen dieselben Optionen.

Column({ gap: 12 }, [Text("First"), Text("Second")]);

Row({ gap: 8, justify: "between", align: "center" }, [
  Text("Total", { size: 16, bold: true }),
  Text("100.00 EUR"),
]);
PropTypWas es tut
gapnumberAbstand zwischen den Kindern, in Punkt
justify"start" | "center" | "end" | "between" | "around"Verteilung entlang der Main-Axis (wie CSS justify-content)
align"start" | "center" | "end" | "stretch"Position auf der Cross-Axis (wie CSS align-items)
wrapbooleanKinder in weitere Zeilen umbrechen, wenn sie nicht passen
alignContent"start" | "center" | "end" | "between" | "around" | "stretch"wie die UMGEBROCHENEN Zeilen als Block sitzen
reversebooleandie Kinder in umgekehrter Reihenfolge anordnen
width heightnumber | stringeine Abmessung festlegen, in Punkt oder als "50%" des Angebotenen

justify: "between" schiebt das erste und das letzte Kind an die Ränder. align: "center" richtet die Kinder auf der Cross-Axis mittig aus. Beide stehen standardmäßig auf start.

Wrapping und Steuerung pro Kind

Mit wrap beginnen Kinder, die nicht passen, eine neue Zeile, und alignContent setzt diese Zeilen als Block. Einzelne Kinder haben drei eigene Props, an jedem Element:

Row({ wrap: true, gap: 8, alignContent: "between" }, [
  Box({ width: "33%" }, [Text("one")]),
  Box({ width: "33%" }, [Text("two")]),
  Box({ width: "33%", order: -1 }, [Text("drawn first")]),
  Box({ flexShrink: 0 }, [Text("keeps its width, and overflows")]),
  Expanded({ flex: 1, flexBasis: 120 }, Box([Text("starts at 120pt, then grows")])),
  Box({ alignSelf: "end" }, [Text("overrides the row's align")]),
]);

flexShrink ist 1, wie in CSS: Läuft eine Zeile über, geben ihre Kinder im Verhältnis zu ihrer eigenen Breite Platz ab, bis die Zeile passt. Mit 0 nimmt sich ein Kind heraus, behält seine Breite und läuft über den Rand. Ein Expanded mit flexBasis macht ebenfalls mit, gewichtet nach seiner Basis - Expanded({ flexShrink: 0 }, ...) behält die Basis in jedem Fall. Ein Spacer hat keine Basis und gibt nichts ab. seit beta.4

Nichts wird enger gedrückt, als es tatsächlich fassen kann. Die Untergrenze eines Kindes ist seine min-content-Ausdehnung - bei einem Text sein längstes Wort -, dieselbe Regel, die ein Browser über das automatische min-width eines Flex-Items anwendet. Sitzt ein Kind auf seiner Untergrenze, gibt es nichts mehr ab, und der Anteil, den es nicht aufnehmen konnte, geht an die, die noch können.

Eine Prozentbreite in einer Flex-Zeile bezieht sich auf die Zeile abzüglich ihrer Abstände, also passen N Spalten mit (100/N)% genau - in CSS müssen Sie die Abstände selbst abziehen.

Das align eines Layouts verwendet die Begriffe der Cross-Axis, start / center / end (es ist richtungsunabhängig - bei einer Row ist die Cross-Axis senkrecht, left/right ergäbe dort keinen Sinn). Das unterscheidet sich vom eigenen align eines Text, das left / center / right lautet.

Spacer

Spacer ist dehnbarer leerer Raum. Zwischen zwei Elemente gesetzt, schiebt er sie auseinander.

Row([Text("Subtotal"), Spacer(), Text("100.00 EUR")]);

Mit einer Zahl gewichten Sie mehrere Spacer gegeneinander: Spacer(2) nimmt doppelt so viel Platz wie Spacer(1).

Expanded

Expanded lässt ein Kind wachsen, bis es den restlichen Platz füllt. Geben Sie jedem ein flex-Gewicht, um den Platz anteilig zu teilen.

Row({ gap: 12 }, [
  Expanded({ flex: 1 }, Box({ border: "steelblue", padding: 12, radius: 6 }, [Text("flex 1")])),
  Expanded(
    { flex: 2 },
    Box({ bg: "#1450aa11", padding: 12, radius: 6 }, [Text("flex 2, twice as wide")]),
  ),
]);
PropTypWas es tut
flexnumberder Anteil des Kindes am restlichen Platz (Standard 1)

Expanded macht sein Kind zum Flex-Item, wie CSS flex: 1 am Kind selbst geändert in beta.5:

  • die eigene width des Kindes weicht seinem Anteil;
  • es wird nie schmaler, als sein Inhalt braucht - ein langes Wort hält es offen;
  • ein maxWidth am Kind stoppt sein Wachstum, und der frei bleibende Platz geht an die anderen;
  • das Kind reiht sich wie jedes Element in die Zeile ein: das align der Row, das eigene alignSelf des Kindes und order;
  • ein %-Padding am Kind bezieht sich auf die Row.

Vor beta.5 behielt das Kind innerhalb seines Anteils seine eigene Breite.

Expanded nimmt ein einzelnes Kind, kein Array. In einer Row füllt es die Breite, in einer Column die Höhe.

Box

Box ist der gestaltete Container: Hintergrund, Border, abgerundete Ecken, Padding und auf Wunsch eine feste Größe. Standardmäßig füllt sie die angebotene Breite (wie ein Block-Element) und passt ihre Höhe dem Inhalt an. Mit width oder height legen Sie eine Abmessung fest. (Als festes Kind einer Row passt sie auch ihre Breite dem Inhalt an, denn dort wird ihr keine Breite zum Füllen angeboten.)

Box({ bg: "#1450aa11", border: "steelblue", borderWidth: 1, radius: 8, padding: 16 }, [
  Text("A framed, padded, rounded box."),
]);
PropTypWas es tut
bgColorInputFüllfarbe
borderColorInputBorder-Farbe (alle Seiten)
borderTop borderRight borderBottom borderLeftColorInputeine einzelne Seite einfärben
borderWidthnumberBorder-Stärke (Standard 1, sobald eine Border gesetzt ist)
radiusnumber | CornersEckenradius - eine Zahl, oder pro Ecke
paddingInsetsPadding (siehe unten)
width heightnumber | stringeine feste Größe in Punkt, oder "50%" des angebotenen Platzes
minWidth maxWidth minHeight maxHeightnumber | stringeine Abmessung begrenzen
aspectRationumberdie nicht festgelegte Achse ableiten (16 / 9)
overflow"hidden"die Kinder auf die Box zuschneiden, samt abgerundeter Ecken
relativebooleandiese Box zum Frame für Positioned-Kinder machen

Größen, in der Reihenfolge, in der sie entstehen

Die Reihenfolge ist der Vertrag, und sie folgt CSS: relative Größen → die Aspect Ratio füllt die offen gebliebene Achse → min/max begrenzt. Eine ausdrückliche Grenze schlägt also die Aspect Ratio, so wie min-height im Browser aspect-ratio schlägt.

Box({ width: "50%", maxWidth: 100 }, []); // 50% of a 400pt region is 200, clamped to 100
Box({ width: 240, aspectRatio: 16 / 9 }, []); // height derived: 135

Prozentwerte bei padding und margin beziehen sich auf allen vier Seiten auf die Breite, oben und unten eingeschlossen. Das ist die Eigenheit von CSS, absichtlich beibehalten, damit sich ein aus dem Web übernommenes Layout genauso verhält.

Das Padding einer Box bezieht sich auf die Breite, die der Box angeboten wird - die Content-Breite ihres Elternelements, so wie CSS den Containing Block nimmt. geändert in beta.4 Vor beta.4 nahm es die eigene Breite der Box, Box({ width: 200, padding: "10%" }) in einer 400pt breiten Spalte hat also jetzt 40pt Padding statt 20pt. Innerhalb einer Row bezieht es sich auf die Breite der Row geändert in beta.5 - vor beta.5 nahm es dort die eigene.

Gradients

bg nimmt einen Gradient genauso wie eine Farbe. Er wird relativ zur Box geschrieben - ein Winkel und Color-Stops - und beim Zeichnen gegen die Box aufgelöst:

import { linearGradient, radialGradient } from "@jasy/pdf";

Box({ bg: linearGradient({ angle: 135, stops: ["#1450aa", "#7aa5e8"] }), padding: 20 }, [...]);
Box({ bg: radialGradient({ stops: ["#ffffff", "#e2483d"] }), width: 80, height: 80 }, []);

Winkel folgen CSS: 0 zeigt nach oben, gedreht wird im Uhrzeigersinn.

Ecken, einzeln

radius nimmt auch ein Objekt pro Ecke. Zwei Radien, die sich eine Kante teilen und überlappen würden, werden gemeinsam verkleinert, damit die Form glatt bleibt - react-pdf begrenzt jede Ecke für sich, und das gibt Knicke.

Box({ radius: { topLeft: 16, bottomRight: 16 }, bg: "#eef1f5", padding: 16 }, [...]);

Padding

Padding rückt ein einzelnes Kind ein, ohne etwas zu zeichnen. Es ist der Verwandte von Box, der nur Abstand macht.

Padding({ top: 16, bottom: 8 }, Text("Breathing room above and below."));

Überall, wo ein Abstandswert verlangt wird (padding an einer Box, das Element Padding), können Sie ihn auf vier Arten schreiben:

16                       // all four sides
{ x: 16, y: 8 }          // horizontal / vertical
{ top: 16, left: 8 }     // per side, any subset
[16, 8, 16, 8]           // [top, right, bottom, left]

Divider

Divider zieht eine dünne waagerechte Linie über die volle Breite seines Elternelements - eine saubere Trennung zwischen Abschnitten, Summen oder Listeneinträgen.

Column({ gap: 4 }, [Text("Subtotal"), Divider(), Text("Total", { bold: true })]);
PropTypWas es tut
colorColorInputLinienfarbe (Standard ein helles Grau)
thicknessnumberLinienstärke in Punkt (Standard 1)
marginInsetsAbstand über / unter der Linie (Standard ein kleiner Abstand)
Divider({ color: "#1450aa", thickness: 2, margin: { y: 12 } });

Ein Element, zwei Stellen seit beta.5

Ein Element hat eine Position, ein zweites Platzieren desselben Elements wirft deshalb ElementReusedError. Platzieren Sie eine Kopie: clone() kopiert ein Element mit allem, was darin steckt, so wie es gebaut wurde.

const badge = Box({ bg: "#1450aa", padding: 4 }, [Text("PAID", { color: "white" })]);

Page([badge, Text("Thank you for your order."), badge.clone()]);

Eine Funktion, die das Element baut (const badge = () => Box(...)), funktioniert genauso.

Farben

Überall, wo eine Farbe verlangt wird (color, bg, border), nimmt jasy mehrere Formen an. rgb und rgba sind kleine Helfer, die Sie aus dem Paket importieren.

import { rgb, rgba } from "@jasy/pdf";

// every entry below is a valid color value:
const colors = [
  "steelblue", // any CSS color name
  "#1450aa", // hex
  "#1450aacc", // hex with alpha
  0xff1450aa, // ARGB number, alpha first
  rgb(20, 80, 170), // channels 0 to 255
  rgba(20, 80, 170, 0.5), // ... with an alpha of 0 to 1
  "rgb(20, 80, 170)", // the CSS function, as a string
  "rgb(20 80 170 / 50%)", // the modern space form, with slash-alpha
  "hsl(214, 79%, 37%)", // and hsl(), the same sRGB family
];

Eine Funktion für erweiterten Farbraum wie color(display-p3 …) oder light-dark() wird benannt und abgelehnt, statt sie in sRGB zu quetschen - das Ergebnis wäre eine Farbe, die niemand gewählt hat.

Alles in einer Datei

Hier ist jedes Primitive dieser Seite in einem einzigen Dokument. Ein Document kann mehr als eine Page enthalten, also zeigt jede Seite unten eine Idee. Kopieren Sie es, führen Sie es einmal aus, und blättern Sie durch das Ergebnis.

import { writeFileSync } from "node:fs";
import {
  Document,
  Page,
  Column,
  Row,
  Box,
  Padding,
  Text,
  Spacer,
  Expanded,
  Divider,
  renderToBytes,
} from "@jasy/pdf";

const heading = (t: string) => Text(t, { size: 22, bold: true, color: "#1450aa" });
const sub = (t: string) => Text(t, { size: 12, color: "gray" });

async function build() {
  const doc = Document([
    Page({ size: "A4", margin: 56 }, [
      Column({ gap: 16 }, [
        heading("Column and Row"),
        sub("Column stacks top to bottom, Row stacks left to right."),
        Divider({ color: "steelblue" }),
        Row({ justify: "between", align: "center" }, [
          Text("Left", { bold: true }),
          Text("Right", { bold: true }),
        ]),
      ]),
    ]),
    Page({ size: "A4", margin: 56 }, [
      Column({ gap: 16 }, [
        heading("Spacer"),
        sub("Flexible empty space that pushes items apart."),
        Divider({ color: "steelblue" }),
        Row([Text("Subtotal"), Spacer(), Text("100.00 EUR")]),
      ]),
    ]),
    Page({ size: "A4", margin: 56 }, [
      Column({ gap: 16 }, [
        heading("Expanded"),
        sub("Children grow to share the remaining space by their flex weight."),
        Divider({ color: "steelblue" }),
        Row({ gap: 12 }, [
          Expanded(
            { flex: 1 },
            Box({ border: "steelblue", padding: 14, radius: 6 }, [Text("flex 1")]),
          ),
          Expanded({ flex: 2 }, Box({ bg: "#1450aa11", padding: 14, radius: 6 }, [Text("flex 2")])),
        ]),
      ]),
    ]),
    Page({ size: "A4", margin: 56 }, [
      Column({ gap: 16 }, [
        heading("Box"),
        sub("A background, a border, rounded corners and padding."),
        Divider({ color: "steelblue" }),
        Box({ bg: "#1450aa11", border: "steelblue", borderWidth: 1, radius: 8, padding: 18 }, [
          Text("A framed, padded, rounded box."),
        ]),
      ]),
    ]),
    Page({ size: "A4", margin: 56 }, [
      Column({ gap: 16 }, [
        heading("Padding"),
        sub("Insets a single child without drawing anything."),
        Divider({ color: "steelblue" }),
        Box({ border: "steelblue", radius: 6 }, [
          Padding(
            { x: 20, y: 14 },
            Text("This text sits inside 20pt horizontal, 14pt vertical padding."),
          ),
        ]),
      ]),
    ]),
  ]);

  writeFileSync("layout-tour.pdf", await renderToBytes(doc));
}

build();

Führen Sie es aus, öffnen Sie layout-tour.pdf, und blättern Sie durch Column und Row, Spacer, Expanded, Box und Padding.