Dokumentation

Positionierung

Das meiste Layout fließt von oben nach unten. Manchmal soll etwas ÜBER dem Flow sitzen: ein Badge auf einer Karte, ein Ribbon, ein Titel, der in eine Box hineinragt, ein Watermark. Genau das leisten relative-Frames und Positioned-Kinder. Die Idee ist die Positionierung aus CSS; Positioned ist die Komponente, die platziert.

Ein Badge auf einer Karte

Machen Sie eine Box mit relative: true zum Frame. Darin umschließen Sie beliebigen Inhalt mit Positioned und geben ihm Abstände von den Kanten des Frames. Ein Positioned-Kind ist OUT OF FLOW: Es reserviert keinen Platz und schiebt nichts weg, es zeichnet einfach dort, wo Sie es hinsetzen. Ein negativer Abstand lässt es über den Frame hinausragen - der „eingeschnittene“ Look.

import { writeFileSync } from "node:fs";
import { Document, Page, Box, Text, Positioned, renderToBytes } from "@jasy/pdf";

const tag = (bg: string, fg: string, t: string) =>
  Box({ bg, radius: 6, padding: { x: 11, y: 6 } }, [Text(t, { size: 11, bold: true, color: fg })]);

async function build() {
  const doc = Document([
    Page({ size: "A4", margin: 56 }, [
      Box({ relative: true, bg: "#eef3fb", border: "#1450aa", radius: 12, padding: 24 }, [
        Text("Project Aurora", { size: 18, bold: true, color: "#0a2348" }),
        Text("Badges anchor to the card's corners.", { size: 12, color: "gray" }),
        Positioned({ top: -12, left: -12 }, tag("#f3dc29", "#0a2348", "NEW")), // pokes out
        Positioned({ top: 14, right: 14 }, tag("#1450aa", "#ffffff", "v2")), // inside the corner
      ]),
    ]),
  ]);

  writeFileSync("badge.pdf", await renderToBytes(doc));
}

build();

Anchoring

Es gibt zwei Arten, ein Kind zu platzieren - nehmen Sie, was sich am besten liest, auch eine pro Achse.

An einer Kante festmachen. top / left / right / bottom sind Abstände von den Kanten des Frames. Setzen Sie eine pro Achse; das Kind passt sich seinem Inhalt an, und ein negativer Wert lässt es hinausragen.

Positioned({ top: 14, right: 14 }, badge); // inside the top-right corner
Positioned({ bottom: -10, left: -10 }, badge); // pokes out the bottom-left

Setzen Sie beide Kanten einer Achse, spannt sich das Kind dazwischen: left und right geben ihm die Breite dazwischen, top und bottom die Höhe.

Positioned platziert nur; das Kind behält seine eigene Größe. Mit einer Kante bleibt ein langer Text einzeilig und läuft über den Frame hinaus. Damit er umbricht, geben Sie ihm eine Breite - spannen Sie ihn, oder geben Sie dem Kind eine Größe:

Positioned({ top: 0, left: 20, right: 20 }, Text(note)); // wraps between the two edges
Positioned({ top: 0, left: 20 }, Box({ width: 160 }, [Text(note)])); // wraps at 160

Anchor setzen. h und v setzen das Kind an einen Punkt - start, center oder end auf jeder Achse -, und x / y verschieben es von dort aus. „Genau in der Mitte, zehn Punkt höher“ ist also { h: "center", v: "center", y: -10 }. Das ist, was Kanten nicht sauber können: zentrieren.

Positioned({ h: "center", v: "center" }, watermark); // dead center of the frame
Positioned({ h: "end", v: "end", x: -8, y: -8 }, stamp); // bottom-right, inset 8
Positioned({ h: "center", v: "start", y: 12 }, ribbon); // top-center, nudged 12 down

Eine Kante gewinnt auf derselben Achse gegen einen Anchor. Ohne relative-Vorfahren richtet sich ein Positioned an der Content-Box der Seite aus.

Stacking mit zIndex seit beta.5

Ohne zIndex liegt oben, was später geschrieben steht - ein Positioned, das vor dem Text steht, liegt darunter, eines danach darüber. zIndex legt die Reihenfolge stattdessen fest: Höher liegt über niedriger, ein positiver Wert über dem übrigen Inhalt des Frames, ein negativer darunter (aber über dem eigenen Hintergrund des Frames). Gleiche Werte behalten die geschriebene Reihenfolge; 0 ist dasselbe wie kein Wert.

Box({ relative: true, height: 130 }, [
  Positioned({ top: 30, left: 60, zIndex: 2 }, blueCard),
  Positioned({ top: 50, left: 120, zIndex: 3 }, yellowCard), // on top
  Positioned({ top: 10, left: 0, zIndex: 1 }, navyCard), // at the bottom, though written last
]);

Die Reihenfolge gilt im ganzen Frame: Ein Positioned in einem anderen wird mit allen übrigen sortiert, ein Badge tief in einer Karte kann also über einer Karte liegen, die danach geschrieben steht. Zwei Stellen haben ihre eigene Reihenfolge: eine Box mit overflow: "hidden" und ein Rotated. Ein Positioned darin wird dort sortiert und verlässt nie den Clip oder die Rotation, in der es geschrieben wurde.

Cropping mit overflow

Das overflow eines Frames entscheidet, was mit einem Kind geschieht, das über seine Kante läuft: "visible" (der Standard) lässt es überstehen, "hidden" schneidet es am Frame ab, samt abgerundeter Ecken. Hier ist dieselbe Karte zweimal nebeneinander, nur overflow ist anders.

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

// a blue box pinned inside the card, extending past its bottom edge
const card = (overflow: "visible" | "hidden") =>
  Box(
    {
      relative: true,
      overflow,
      bg: "#eef3fb",
      border: "#1450aa",
      radius: 10,
      height: 70,
      padding: 14,
    },
    [
      Text(`overflow: "${overflow}"`, { size: 12, bold: true, color: "#0a2348" }),
      Positioned(
        { top: 34, left: 14 },
        Box({ bg: "#1450aa", radius: 8, width: 170, height: 100, padding: 12 }, [
          Text("Pinned inside the card, it extends past the bottom edge.", {
            size: 10,
            color: "#ffffff",
          }),
        ]),
      ),
    ],
  );

async function build() {
  const doc = Document([
    Page({ size: "A4", margin: 56 }, [
      Row({ gap: 24 }, [Expanded(card("visible")), Expanded(card("hidden"))]),
    ]),
  ]);

  writeFileSync("overflow.pdf", await renderToBytes(doc));
}

build();

Links hängt die blaue Box über die Karte hinaus. Rechts wird die identische Box bündig an der Kante der Karte abgeschnitten. overflow: "hidden" schneidet auch ein Image auf eine abgerundete Box zu, ein sauberer Weg zu einem abgerundeten oder runden Foto.

Eine wiederverwendbare Komponente

Eine Komponente ist einfach eine Funktion, die einen Baum zurückgibt, „eine Karte mit eingeschnittenem Titel“ sind also ein paar Zeilen, die Sie überall wiederverwenden.

import { Box, Padding, Positioned, Text, type PDFElement } from "@jasy/pdf";

const TitledCard = (title: string, body: PDFElement) =>
  Box({ relative: true, overflow: "hidden", bg: "#eef3fb", radius: 10 }, [
    Padding(20, body),
    Positioned(
      { top: -10, left: -10 },
      Box({ bg: "#1450aa", radius: 6, padding: { x: 12, y: 6 } }, [
        Text(title, { color: "white", bold: true }),
      ]),
    ),
  ]);