feat(fp): WP-25 — server-rendered letter HTML preview

Adds LetterHtml.Render, a pure composer mirroring the FE letter canvas'
class vocabulary, behind two ExcludeFromDescription()'d endpoints
(GET /brief/preview, GET /admin/org-template/{subOrgId}/preview).
Auto-resolvable placeholders pull from seed/case data; unresolved
manual ones render as "[NOG IN TE VULLEN: label]". A sent brief
archives its composed HTML (BriefEntity.ArchivedHtml) so a later
org-template republish never changes it. FE gets a hand-written fetch
adapter (text/html, not JSON) and a "Voorbeeld" button that opens the
preview in a new tab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 12:56:36 +02:00
parent c07a33ee3e
commit 1bb9383344
17 changed files with 1020 additions and 8 deletions

View File

@@ -0,0 +1,215 @@
<!doctype html><html lang="nl"><head><meta charset="utf-8"><title>golden-brief-1</title><style>/* letter.css — the FE⇄BE letter-rendering CONTRACT (WP-24/WP-25).
*
* One stylesheet, two consumers: the FE letter canvas loads it via <link>
* (index.html + Storybook preview-head), the backend HTML renderer (WP-25)
* inlines this same file. Its class-parity test is the fence against drift.
*
* Class vocabulary: .letter, .letter__letterhead, .letter__body,
* .letter__signature, .letter__footer, .letter__page-break.
* Margins arrive as --letter-margin-* custom props (mm, from the OrgTemplate);
* the defaults below match the seed template.
*
* Deliberately self-contained: no --rhc- or --bs- tokens — the backend renderer
* has no token bridge. Geometry follows the sample voorbeeldbrief-inschrijving
* (A4, return address above the envelope window, reference block, footer rule).
*/
.letter {
--letter-margin-top: 25mm;
--letter-margin-right: 25mm;
--letter-margin-bottom: 25mm;
--letter-margin-left: 25mm;
position: relative;
box-sizing: border-box;
width: 210mm; /* A4 */
max-width: 100%;
min-height: 297mm;
margin-inline: auto;
padding: var(--letter-margin-top) var(--letter-margin-right) var(--letter-margin-bottom)
var(--letter-margin-left);
background: #fff;
color: #1a1a1a;
/* Letter typography is the letter's, not the portal UI's. Licensed RO/Rijks
fonts are not shipped; system stack mirrors the app-wide decision (ADR-0003). */
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
font-size: 10.5pt;
line-height: 1.5;
display: flex;
flex-direction: column;
}
.letter p {
margin: 0 0 0.75em;
}
.letter ul,
.letter ol {
margin: 0 0 0.75em;
padding-inline-start: 1.4em;
}
/* --- Letterhead: wordmark, return address above the envelope window, reference block --- */
.letter__letterhead {
margin-block-end: 12mm;
}
.letter__letterhead .org-wordmark {
font-size: 13pt;
font-weight: 700;
margin: 0 0 10mm;
}
.letter__letterhead .return-address {
font-style: normal;
font-size: 7.5pt;
white-space: pre-line;
color: #555;
margin-block-end: 2mm;
}
/* Envelope-window position (~C5 venstercouvert): recipient block. */
.letter__letterhead .address-window {
min-height: 22mm;
font-style: normal;
white-space: pre-line;
margin-block-end: 8mm;
}
.letter__letterhead .reference {
display: flex;
gap: 10mm;
font-size: 8.5pt;
color: #333;
}
.letter__letterhead .reference dt {
font-weight: 700;
margin: 0;
}
.letter__letterhead .reference dd {
margin: 0;
}
/* --- Body: the case-type template's sections --- */
.letter__body {
flex: 1;
/* Above the absolutely-positioned page-break marks: the dashed line stays visible
in the gaps but never draws THROUGH opaque content (editors, pickers). */
position: relative;
z-index: 1;
}
.letter__body h3 {
font-size: inherit;
font-weight: 700;
margin: 0 0 0.25em;
}
.letter__body section {
margin-block-end: 1.5em;
}
/* --- Signature --- */
.letter__signature {
margin-block-start: 10mm;
}
.letter__signature p {
margin: 0;
}
.letter__signature .signature-name {
margin-block-start: 3em; /* room for the (not-shipped) handwritten signature */
font-weight: 700;
}
/* --- Footer: contact + legal, above a rule --- */
.letter__footer {
margin-block-start: 10mm;
padding-block-start: 3mm;
border-block-start: 0.5pt solid #999;
font-size: 7.5pt;
color: #555;
display: flex;
justify-content: space-between;
gap: 10mm;
}
.letter__footer .footer-contact {
white-space: pre-line;
}
.letter__footer .footer-legal {
text-align: end;
}
/* --- Approximate page-break indicator (screen only; PRD §2b honesty rule) ---
Positioned per A4-interval by the canvas; the print preview is authoritative. */
.letter__page-break {
position: absolute;
inset-inline: 0;
border-block-start: 1px dashed #b36200;
pointer-events: none;
}
.letter__page-break > span {
position: absolute;
inset-inline-end: 2mm;
inset-block-start: 0;
transform: translateY(-50%);
/* Above .letter__body: the honesty caption stays legible even over content. */
z-index: 2;
font-size: 7pt;
color: #b36200;
background: #fff;
padding-inline: 1mm;
white-space: nowrap;
}
/* --- Print: real pages, no indicator chrome --- */
@page {
size: A4;
margin: 0;
}
@media print {
.letter {
width: auto;
min-height: 0;
}
.letter__page-break {
display: none;
}
}
.preview-watermark {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 72pt;
font-weight: 700;
color: rgb(200 30 30 / 0.18);
transform: rotate(-30deg);
pointer-events: none;
z-index: 3;
}
.org-logo {
display: block;
max-height: 18mm;
margin-block-end: 4mm;
}</style></head><body><div class="letter" style="--letter-margin-top:25mm;--letter-margin-right:20mm;--letter-margin-bottom:20mm;--letter-margin-left:25mm;"><div class="letter__letterhead"><p class="org-wordmark">BIG-register</p><address class="return-address">Retouradres: Postbus 00000, 2500 AA Den Haag</address><address class="address-window">Adres van de geadresseerde<br>(wordt ingevuld bij verzending)</address><dl class="reference"><div><dt>Ons kenmerk</dt><dd>golden-brief-1</dd></div><div><dt>Datum</dt><dd>5 juli 2026</dd></div></dl></div><div class="letter__body"><section><h3>Aanhef</h3><p>Geachte heer/mevrouw Dr. A. (Anna) de Vries,</p></section><section><h3>Kern van het besluit</h3><p>Op </p><ul><li>Eerste punt: [NOG IN TE VULLEN: Reden besluit]</li><li>Tweede punt</li></ul></section><section><h3>Slot</h3><p>Met vriendelijke groet,</p></section></div><div class="letter__signature"><p>Met vriendelijke groet,</p><p class="signature-name">A. de Vries</p><p>Hoofd Registratie, BIG-register</p></div><div class="letter__footer"><div class="footer-contact">BIG-register &#183; Postbus 00000, 2500 AA Den Haag &#183; 070 000 00 00 &#183; info@voorbeeld.example</div><div class="footer-legal">Dit is een gegenereerd voorbeelddocument uit de register-reference PoC. Alle gegevens zijn fictief.</div></div><div class="preview-watermark" aria-hidden="true">VOORBEELD</div></div></body></html>