feat: add a personal-letter template

Same window-envelope geometry as the Form B letter (45mm address
field, fold marks), but no info block: a three-line sender block on
top and a right-aligned place/date line instead. The gallery thumb
swaps the info column for a date line on this card. Positions
verified headlessly: return address 44.8mm, recipient 49.8mm, date
flush with the right margin.
This commit is contained in:
Steffen Becker committed 2026-08-24 19:51:51 +00:00
1 parent 70888409da
commit 46c59ffb11
5 files changed
+85 -3

No files matched your search

@@ -42,10 +42,14 @@
<div class="grid">
{#each TEMPLATES as entry (entry.id)}
<button class="tpl" onclick={() => pick(entry)}>
<!-- A stylized letter page: address block, info column, body lines. -->
<!-- A stylized letter page: address block, info column or date line, body lines. -->
<span class="thumb" aria-hidden="true">
<span class="addr"></span>
<span class="info"></span>
{#if entry.id === 'din5008b'}
<span class="info"></span>
{:else}
<span class="date"></span>
{/if}
<span class="line l1"></span>
<span class="line l2"></span>
<span class="line l3"></span>
@@ -155,6 +159,7 @@
}
.addr { top: 26px; left: 12px; width: 28px; height: 16px; }
.info { top: 26px; left: 52px; width: 22px; height: 16px; }
.date { top: 48px; right: 12px; width: 20px; height: 3px; }
.line { left: 12px; height: 3px; }
.l1 { top: 56px; width: 40px; background: #8a94a0; }
.l2 { top: 66px; width: 60px; }
+5
View File
@@ -680,6 +680,10 @@ const de: Messages = {
name: 'Brief nach DIN 5008',
description: 'Geschäftsbrief (Form B): Anschriftfeld fürs Fensterkuvert, Infoblock, Falzmarken.',
},
privateLetter: {
name: 'Privatbrief',
description: 'Privater Brief auf derselben Geometrie: Anschriftfeld fürs Fensterkuvert, Ort/Datum-Zeile, Falzmarken.',
},
letter: {
companyName: 'Firmenname',
returnAddress: 'Absender · Straße · PLZ Ort',
@@ -697,6 +701,7 @@ const de: Messages = {
date: 'Datum',
phoneNumber: 'Nummer',
emailAddress: 'Adresse',
place: 'Ort',
subject: 'Betreff',
salutation: 'Anrede',
bodyText: 'Brieftext',
+5
View File
@@ -686,6 +686,10 @@ const en = {
name: 'DIN 5008 letter',
description: 'Business letter (Form B): address field for a window envelope, info block, fold marks.',
},
privateLetter: {
name: 'Personal letter',
description: 'Private letter on the same geometry: window envelope address field, place and date line, fold marks.',
},
letter: {
companyName: 'Company name',
returnAddress: 'Sender · Street · Postal code City',
@@ -703,6 +707,7 @@ const en = {
date: 'Date',
phoneNumber: 'Number',
emailAddress: 'Address',
place: 'Place',
subject: 'Subject',
salutation: 'Salutation',
bodyText: 'Letter text',
+66
View File
@@ -0,0 +1,66 @@
import { t } from '../i18n/i18n.svelte';
import { toDateValue, DEFAULT_DATE_FORMAT } from '../utils/dateTime';
import type { TemplateData, TemplateEntry } from './types';
// Personal letter on the DIN 5008 window-envelope geometry: same 45mm address field
// and fold marks as Form B, but no info block — just a right-aligned place/date line.
// Sender block height in pt; the space that lands the return-address line at 45mm.
const LINE_PT = 12 * 1.15;
const MM_TO_PT = 72 / 25.4;
// 45mm target − 20mm margin − the three sender-address lines.
const RETURN_SPACE_BEFORE_PT = Math.round((45 - 20) * MM_TO_PT - 3 * LINE_PT);
// The return-address zone is 5mm tall; its 8pt line fills ~3.25mm of it.
const RETURN_SPACE_AFTER_PT = Math.round((5 * MM_TO_PT - 8 * 1.15) * 10) / 10;
type N = Record<string, unknown>;
const T = (text: string, ...marks: N[]): N => ({ type: 'text', text, ...(marks.length ? { marks } : {}) });
const PLH = (text: string, ...marks: N[]): N => ({ type: 'placeholderField', attrs: { text }, ...(marks.length ? { marks } : {}) });
const P = (attrs: N | null, ...content: N[]): N => ({ type: 'paragraph', ...(attrs ? { attrs } : {}), ...(content.length ? { content } : {}) });
const SMALL = { type: 'textStyle', attrs: { fontSize: '8pt' } };
const BOLD = { type: 'bold' };
function buildLetter(): TemplateData {
const L = t().templates.letter;
const date: N = { type: 'dateTimeField', attrs: { kind: 'date', format: DEFAULT_DATE_FORMAT, fixed: true, value: toDateValue(new Date()) } };
return {
margins: { top: 2, bottom: 2, left: 2.5, right: 2 },
foldMarks: true,
content: {
type: 'doc',
content: [
P(null, PLH(L.recipientName)),
P(null, PLH(L.recipientStreet)),
P(null, PLH(L.recipientCity)),
// The one-line return address opens the address field at 45mm from the top.
// fontSize also shrinks the paragraph mark, so the line box is 8pt tall.
P({ spaceBefore: RETURN_SPACE_BEFORE_PT, spaceAfter: RETURN_SPACE_AFTER_PT, fontSize: '8pt' }, PLH(L.returnAddress, SMALL)),
P(null, PLH(L.recipientName)),
P(null, PLH(L.recipientStreet)),
P(null, PLH(L.recipientCity)),
P(null),
P({ textAlign: 'right' }, PLH(L.place), T(', '), date),
P(null),
P(null),
P(null, PLH(L.subject, BOLD)),
P(null),
P(null, PLH(L.salutation)),
P(null),
P(null, PLH(L.bodyText)),
P(null),
P(null, T(L.closing)),
P(null),
P(null),
P(null),
P(null, PLH(L.signature)),
],
},
};
}
export const privateLetter: TemplateEntry = {
id: 'privateLetter',
name: () => t().templates.privateLetter.name,
description: () => t().templates.privateLetter.description,
build: buildLetter,
};
+2 -1
View File
@@ -1,4 +1,5 @@
import { din5008b } from './din5008b';
import { privateLetter } from './privateLetter';
import type { TemplateEntry } from './types';
export const TEMPLATES: TemplateEntry[] = [din5008b];
export const TEMPLATES: TemplateEntry[] = [din5008b, privateLetter];