diff --git a/src/lib/components/CLAUDE.md b/src/lib/components/CLAUDE.md
index 84a0a36..348a087 100644
--- a/src/lib/components/CLAUDE.md
+++ b/src/lib/components/CLAUDE.md
@@ -207,10 +207,15 @@ size alone makes a short word far too small and a long one overflow the page.
`App.svelte`'s `applyTemplate` — `resetDocumentState()` (the extracted body of New),
then the template's content, margins, styles and fold marks. `documentName` becomes the
localized template name and no file handle is bound, so the first Save asks where. The
-DIN letter's mm positions were verified headlessly (45mm address field, 125mm info
-column); its module documents the arithmetic. The letter-family templates share that
-geometry (and the JSON builders) via `templates/builders.ts`; `tests/unit/templates.test.ts`
-schema-checks every entry in both locales.
+norm's two DIN letter forms come out of one builder in `din5008.ts`, parameterized by
+where the address field opens; both were verified headlessly — Form B 44.97mm, Form A
+26.98mm, their info blocks 49.98/31.99mm, the info column 124.99mm. The letter-family
+templates share that geometry (and the JSON builders) via `templates/builders.ts`;
+`tests/unit/templates.test.ts` schema-checks every entry in both locales.
+
+A block's rect starts **above** its own first line — the spacing model puts space-before
+inside the box — so a probe measuring these positions must range-select the text, not
+read `getBoundingClientRect()` on the paragraph.
## Fold marks (`FoldMarkLayer.svelte`, `storage/foldMarks.ts`)
diff --git a/src/lib/components/TemplateGalleryDialog.svelte b/src/lib/components/TemplateGalleryDialog.svelte
index 21fd11a..09495a1 100644
--- a/src/lib/components/TemplateGalleryDialog.svelte
+++ b/src/lib/components/TemplateGalleryDialog.svelte
@@ -64,9 +64,11 @@
{:else}
-
- {#if entry.id === 'din5008b'}
-
+
+
+ {#if entry.id.startsWith('din5008')}
+
{:else}
{/if}
@@ -183,6 +185,7 @@
}
.addr { top: 26px; left: 12px; width: 28px; height: 16px; }
.info { top: 26px; left: 52px; width: 22px; height: 16px; }
+ .addr.high, .info.high { top: 15px; }
.date { top: 48px; right: 12px; width: 20px; height: 3px; }
.line { left: 12px; height: 3px; }
.l1 { top: 56px; width: 40px; background: #8a94a0; }
diff --git a/src/lib/i18n/locales/de.ts b/src/lib/i18n/locales/de.ts
index 90e0e07..1724c6c 100644
--- a/src/lib/i18n/locales/de.ts
+++ b/src/lib/i18n/locales/de.ts
@@ -689,8 +689,12 @@ const de: Messages = {
templates: {
title: 'Neu aus Vorlage',
din5008b: {
- name: 'Brief nach DIN 5008',
- description: 'Geschäftsbrief (Form B): Anschriftfeld fürs Fensterkuvert, Infoblock, Falzmarken.',
+ name: 'Brief nach DIN 5008 (Form B)',
+ description: 'Geschäftsbrief mit vollem Briefkopf: Anschriftfeld fürs Fensterkuvert ab 45 mm, Infoblock, Falzmarken.',
+ },
+ din5008a: {
+ name: 'Brief nach DIN 5008 (Form A)',
+ description: 'Geschäftsbrief mit einzeiligem Briefkopf: Anschriftfeld ab 27 mm, Infoblock, Falzmarken — 18 mm mehr Text auf Seite 1.',
},
privateLetter: {
name: 'Privatbrief',
diff --git a/src/lib/i18n/locales/en.ts b/src/lib/i18n/locales/en.ts
index 875a72f..29d94a0 100644
--- a/src/lib/i18n/locales/en.ts
+++ b/src/lib/i18n/locales/en.ts
@@ -695,8 +695,12 @@ const en = {
templates: {
title: 'New from template',
din5008b: {
- name: 'DIN 5008 letter',
- description: 'Business letter (Form B): address field for a window envelope, info block, fold marks.',
+ name: 'DIN 5008 letter (Form B)',
+ description: 'Business letter with a full letterhead: address field for a window envelope from 45 mm, info block, fold marks.',
+ },
+ din5008a: {
+ name: 'DIN 5008 letter (Form A)',
+ description: 'Business letter with a one-line letterhead: address field from 27 mm, info block, fold marks — 18 mm more text on page 1.',
},
privateLetter: {
name: 'Personal letter',
diff --git a/src/lib/templates/din5008b.ts b/src/lib/templates/din5008.ts
similarity index 72%
rename from src/lib/templates/din5008b.ts
rename to src/lib/templates/din5008.ts
index 19f2040..9d6af57 100644
--- a/src/lib/templates/din5008b.ts
+++ b/src/lib/templates/din5008.ts
@@ -2,17 +2,17 @@ import { t } from '../i18n/i18n.svelte';
import type { TemplateData, TemplateEntry } from './types';
import { BOLD, LINE_PT, MM_TO_PT, P, PLH, RETURN_SPACE_AFTER_PT, SMALL, T, dateField, type N } from './builders';
-// DIN 5008 Form B business letter. All vertical positions are paragraph rhythm from a
-// 20mm top margin at 12pt single spacing (one line ≈ 4.87mm); the mm targets below
-// are the norm's.
+// The two DIN 5008 business-letter forms, differing only in where the address field
+// opens: Form A at 27mm (a one-line letterhead), Form B at 45mm (a full one). Vertical
+// positions are paragraph rhythm from a 20mm top margin at 12pt (one line ≈ 4.87mm).
-// 45mm target − 20mm margin − one letterhead line.
-const RETURN_SPACE_BEFORE_PT = Math.round((45 - 20) * MM_TO_PT - LINE_PT);
// Info block at 125mm from the paper edge = 10cm from the 25mm text margin.
const INFO_TAB = '10l';
-function buildLetter(): TemplateData {
+function buildLetter(headMm: number): TemplateData {
const L = t().templates.letter;
+ // The address field's mm target − the 20mm margin − one letterhead line.
+ const returnSpaceBefore = Math.round((headMm - 20) * MM_TO_PT - LINE_PT);
// An info line with no address half indents to the column instead of leading with
// a tab (a tab with nothing before it stays on the default grid).
const addressRow = (attrs: N | null, addr: N[], info: N[]): N =>
@@ -26,9 +26,9 @@ function buildLetter(): TemplateData {
type: 'doc',
content: [
P(null, PLH(L.companyName, BOLD)),
- // The one-line return address opens the address field at 45mm from the top.
+ // The one-line return address opens the address field at its mm target.
// 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({ spaceBefore: returnSpaceBefore, spaceAfter: RETURN_SPACE_AFTER_PT, fontSize: '8pt' }, PLH(L.returnAddress, SMALL)),
// Two notation lines above the address (DIN's Zusatz- und Vermerkzone, filled
// bottom-up), sharing their rows with the top of the info block.
addressRow(null, [], [T(L.yourRef), PLH(L.reference)]),
@@ -59,5 +59,12 @@ export const din5008b: TemplateEntry = {
id: 'din5008b',
name: () => t().templates.din5008b.name,
description: () => t().templates.din5008b.description,
- build: buildLetter,
+ build: () => buildLetter(45),
+};
+
+export const din5008a: TemplateEntry = {
+ id: 'din5008a',
+ name: () => t().templates.din5008a.name,
+ description: () => t().templates.din5008a.description,
+ build: () => buildLetter(27),
};
diff --git a/src/lib/templates/registry.ts b/src/lib/templates/registry.ts
index 7f889a2..4c0173d 100644
--- a/src/lib/templates/registry.ts
+++ b/src/lib/templates/registry.ts
@@ -1,4 +1,4 @@
-import { din5008b } from './din5008b';
+import { din5008a, din5008b } from './din5008';
import { privateLetter } from './privateLetter';
import { coverLetter } from './coverLetter';
import { cv } from './cv';
@@ -7,7 +7,7 @@ import { minutes } from './minutes';
import type { TemplateEntry } from './types';
// What the gallery offers.
-export const TEMPLATES: TemplateEntry[] = [din5008b, privateLetter];
+export const TEMPLATES: TemplateEntry[] = [din5008b, din5008a, privateLetter];
// Drafts kept compiling and tested but not selectable; move up once settled.
export const SHELVED: TemplateEntry[] = [coverLetter, cv, invoice, minutes];