Nodes
Shape
A drawn rectangle with the document's own words on top of it.
Diese Seite ist noch nicht übersetzt und wird deshalb auf Englisch angezeigt.
Word draws a real rectangle and the paragraphs sit on its fill rather than beside it. What separates it from a paragraph with a background is the size: a shape is a box you decided the dimensions of and does not grow with its text, which is exactly what a banner needs — one that changed depth with its wording would be a different notice on every letter. When the box *should* grow with what is in it, a `Table` with one cell is the thing to reach for.
- Helper
- Shape
- Node-Art
- shape
- Kategorie
- Medien
- Wird aufgelöst
- Both
- Kinder
- Paragraphs, usually. Text goes straight in for the one-line case.
| Option | Typ | Was sie bewirkt |
|---|---|---|
id | string | Stable address for the node. Engines target it and build artifacts diff on it, so treat a rename as a breaking change. Optional: a node without one takes an id from where it sits, which is what keeps branches and loops from forcing you to invent names. Two nodes claiming one id is an error rather than a race. |
width | number | How wide the box is drawn, in points. The full text column unless it is said, which is the width a banner wants and the only one that needs no arithmetic from the document. |
height | number | How deep the box is drawn, in points. A shape does not grow to fit its words, so this is a decision the document or its theme makes — the block style's heightPt is taken when the node says nothing. |
variant | string | A block style the theme looks up — "band", "badge", "panel". Names what the node is, never what it looks like: the appearance lives in the style's blocks, so a document restyles without a node changing. A name the theme has not heard of draws as an ordinary block rather than failing. |
derivers | DeriverInvocation[] | Values the engine computes before the node resolves, written to derived.* and readable from a template token. Built with derive(). These survive publishing and run per document — use them for anything computed from request data. useDeriver runs one during the build instead. |
Writing one
A shape needs two things from you: how big it is, and what goes on it.
import { Paragraph, Shape, useFormat, useState } from "docxcelerate/template";
export const RenewalBanner: Shape = () => {
const { date } = useFormat("en-GB");
const [state] = useState((data: MemberData) => ({
plan: data.plan,
renewsOn: data.renewsOn,
}));
return (
<Shape id="renewal-banner" variant="banner" height={44}>
<Paragraph id="renewal-banner-line">
{state.plan} renews on {date(state.renewsOn)}
</Paragraph>
</Shape>
);
};
width and height are in points. A shape with no width fills the text
column, which is what a banner wants; saying one makes a block that sits in the
column rather than spanning it. For the one-line case the text can go straight
in — <Shape id="paid">Paid in full</Shape> reads as one.
Why it is not a paragraph with a background
Because a shape is a box you decided the size of, and that is usually the whole reason you wanted one.
A banner that grew with its wording would be a different depth on every document it went out on. The one saying “Paid” and the one saying “Overdue — 14 days past the due date” would be two different blocks, and a reader flicking through a stack reads the shorter one as a different kind of notice. A shape holds its dimensions whatever is written on it.
The trade runs the other way too, and it is worth knowing which side you are
on. When the box should grow with what is in it — a description, a quoted
clause, anything whose length you do not control — a Table
with one cell in it is the thing to reach for. It draws the same fill, rule and
padding, and it grows. Neither is a worse version of the other.
What it looks like is the theme’s
variant names what the banner is — banner, stamp, bannerOverdue —
and never what colour it should be. The fill, the rule, the room inside and how
the words sit on it all come from the style’s blocks, so restyling a document
restyles its shapes without a node changing. A variant the theme has not heard
of draws in the palette’s accent, which is a banner without an opinion rather
than a broken one.
The block properties a shape reads are the ones a table cell reads: fill,
border, borderWidthPt, paddingPt, align, and heightPt — which a shape
takes as its depth when the node has not given one, because a block declaring a
height is already saying exactly that.
How it is packed, and why it matters
A shape is written as VML — a w:pict holding a v:rect — rather than as the
DrawingML shape Word writes itself.
Word reads both identically: it opens either as a real Rectangle with the
width, height, fill and text the file gives it. The difference is on the other
side. docx-preview renders VML and has no reading of DrawingML shapes at all,
so the modern form would draw correctly in Word and show nothing in your
preview. Packing the form both engines read is what keeps the preview showing
the document rather than an opinion about it.
The same reasoning is why rounded corners are not here yet. v:roundrect is
not something the preview can draw, and a radius that draws in Word and vanishes
on screen is worse than no radius at all. It goes in when both engines can.
A banner across the column
src/nodes/shape/basic.node.tsx A shape with no width, filling the text column at a stated depth.
import { Paragraph, type Shape as ShapeComponent, Shape, useFormat, useState } from "docxcelerate/template";
import type { SampleData } from "../sample-data.ts";
/**
* A drawn rectangle with the document's own words on it.
*
* The height is the point. A banner that grew with its wording would be a
* different depth on every letter it went out on, and a reader flicking
* through a stack would read the shorter one as a different kind of notice.
*/
export const RenewalBanner: ShapeComponent = () => {
const { date } = useFormat("en-GB");
const [state] = useState((data: SampleData) => ({
plan: data.plan,
renewsOn: data.renewsOn,
}));
return (
<Shape id="renewal-banner" variant="bannerAttention" height={44}>
<Paragraph id="renewal-banner-line">
{state.plan} renews on {date(state.renewsOn)}
</Paragraph>
</Shape>
);
}; Wozu es aufgelöst wird
Der Node, wie er im DocumentModel erscheint: das JSON, das ein Renderer bekommt. Kein Styling, kein Layout.
{
"id": "renewal-banner",
"kind": "shape",
"height": 44,
"children": [
{
"id": "renewal-banner-line",
"kind": "paragraph",
"mode": "static",
"text": "Peak Anytime renews on 1 October 2026"
}
],
"variant": "bannerAttention"
} A block of its own size
src/nodes/shape/sized.node.tsx The same element given a width, so it sits in the column rather than spanning it.
import { Paragraph, type Shape as ShapeComponent, Shape } from "docxcelerate/template";
/**
* The same element given a width as well as a height.
*
* A shape with no width fills the text column, which is what a banner wants.
* Saying one makes a block that sits in the column rather than spanning it —
* a stamp, a callout, a badge beside prose. The tone is the theme's, named
* for the state it reports rather than the colour it happens to be.
*/
export const PaidStamp: ShapeComponent = () => (
<Shape id="paid-stamp" variant="bannerPositive" width={160} height={56}>
<Paragraph id="paid-stamp-line">Paid in full</Paragraph>
</Shape>
); Wozu es aufgelöst wird
Der Node, wie er im DocumentModel erscheint: das JSON, das ein Renderer bekommt. Kein Styling, kein Layout.
{
"id": "paid-stamp",
"kind": "shape",
"width": 160,
"height": 56,
"children": [
{
"id": "paid-stamp-line",
"kind": "paragraph",
"mode": "static",
"text": "Paid in full"
}
],
"variant": "bannerPositive"
} Diese Seite auf GitHub bearbeiten ↗ Diese Seite als Markdown lesen