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];