diff --git a/apps/ssp/src/app/brief/ui/letter-canvas/letter-canvas.component.ts b/apps/ssp/src/app/brief/ui/letter-canvas/letter-canvas.component.ts index bebded2..279a404 100644 --- a/apps/ssp/src/app/brief/ui/letter-canvas/letter-canvas.component.ts +++ b/apps/ssp/src/app/brief/ui/letter-canvas/letter-canvas.component.ts @@ -1,4 +1,6 @@ -/* eslint-disable max-lines */ // 77 lines are CSS, the rest is one letter — RD-26 rewrites this reason, keeps the disable +/* eslint-disable max-lines */ // 77 lines of CSS + one letter's markup; splitting it into +// letterhead/body/signature/footer makes "what does the letter look like" a five-file +// question for no behavioural seam. Deliberate, not deferred. import { Component, DestroyRef, @@ -12,9 +14,7 @@ import { signal, viewChild, } from '@angular/core'; -import { NgTemplateOutlet } from '@angular/common'; import { ButtonComponent } from '@shared/ui/button/button.component'; -import { PlaceholderChipComponent } from '@shared/ui/placeholder-chip/placeholder-chip.component'; import { formatDatumNl } from '@shared/kernel/datum'; import { Paragraph } from '@shared/kernel/rich-text'; import { Brief, LetterBlock } from '@brief/domain/brief'; @@ -22,6 +22,7 @@ import { OrgTemplate } from '@brief/domain/org-template'; import { OrgTemplateTextField } from '@brief/domain/org-template.machine'; import { Diagnostic } from '@brief/domain/placeholders'; import { BlockDiffKind } from '@brief/domain/brief-diff'; +import { LetterLineComponent } from './letter-line.component'; /** A run of consecutive lines to render together: a list (bullet/number) or a single plain line. */ type PreviewSegment = { @@ -40,12 +41,6 @@ function groupParagraphs(paras: readonly Paragraph[]): PreviewSegment[] { return out; } -// Illustrative values for the "Voorbeeld" toggle — what send resolves server-side. -const SAMPLE_VALUES: Record = { - naam_zorgverlener: 'J. Jansen', - big_nummer: '12345678901', -}; - /** A4 height in CSS px (1in = 96px = 25.4mm) — for the approximate page-break marks. */ const A4_HEIGHT_PX = (297 * 96) / 25.4; @@ -55,10 +50,12 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4; renders everything read-only (approver/locked, absorbs the old letter-preview), `'template'` reserves the org-identity regions for the admin editor. Letter typography/geometry come from the shared `public/letter.css` contract — - the same file the backend preview renderer inlines. */ + the same file the backend preview renderer inlines. Each rendered line is its own + `app-letter-line` (RD-26), replacing the outlet-template indirection that stood + in for it. */ @Component({ selector: 'app-letter-canvas', - imports: [NgTemplateOutlet, ButtonComponent, PlaceholderChipComponent], + imports: [ButtonComponent, LetterLineComponent], styles: [ ` :host { @@ -137,37 +134,19 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4; `, ], template: ` - - @for (node of nodes; track $index) { - @switch (node.type) { - @case ('text') { - {{ node.text }} - } - @case ('lineBreak') { -
- } - @case ('placeholder') { - @if (showSample() && autoFor(node.key)) { - {{ sampleFor(node.key) }} - } @else { - - } - } - } - } -
- @if (editableRegions() !== 'template') {
-
+
@@ -175,13 +154,14 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4; + - {{ - zoomResetLabel() - }} + 100%
@if (editableRegions() === 'none') { - {{ showSample() ? hideSampleLabel() : showSampleLabel() }} + @if (showSample()) { + Testwaarden verbergen + } @else { + Voorbeeld met testwaarden + } }
@@ -201,20 +187,27 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4; (robijn footer background) — the letter surface must stay letter.css-only. -->
@if (logoUrl()) { - + } @if (editing()) { } @else { @@ -224,11 +217,11 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4;
{{ recipientText() }}
-
{{ referenceLabel() }}
+
Ons kenmerk
{{ brief().briefId }}
-
{{ dateLabel() }}
+
Datum
{{ letterDate }}
@@ -242,18 +235,27 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4; @let diffKind = showDiff() ? blockDiffs().get(block.blockId) : undefined;
@if (diffKind) { - {{ - diffLabel(diffKind) - }} + + @if (diffKind === 'added') { + nieuw + } @else { + gewijzigd sinds afwijzing + } + } @for (seg of segmentsOf(block); track $index) { @if (seg.list === 'bullet') {
    @for (para of seg.items; track $index) {
  • -
  • } @@ -262,18 +264,24 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4;
      @for (para of seg.items; track $index) {
    1. -
    2. }
    } @else {

    -

    } @@ -289,19 +297,22 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4; } @else { @@ -317,13 +328,15 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4; class="tmpl-textarea footer-contact" rows="2" [value]="orgTemplate().footerContact" - [attr.aria-label]="footerContactLabel()" + aria-label="Contactgegevens (voettekst)" + i18n-aria-label="@@brief.canvas.footerContact" (input)="emitEdit('footerContact', $event)" > } @else { @@ -334,7 +347,7 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4; @for (top of pageBreaks(); track $index) { }
@@ -360,30 +373,13 @@ export class LetterCanvasComponent { /** An in-place edit to an org-identity field (only in `editableRegions='template'`). */ templateEdit = output<{ field: OrgTemplateTextField; value: string }>(); - showSampleLabel = input($localize`:@@brief.preview.showSample:Voorbeeld met testwaarden`); - hideSampleLabel = input($localize`:@@brief.preview.hideSample:Testwaarden verbergen`); - pageBreakCaption = input( - $localize`:@@brief.canvas.pageBreak:±pagina-einde — afdrukvoorbeeld is leidend`, - ); + /** The one label kept as an `input()` rather than inlined `i18n` (RD-26, decision 2): + its message embeds a literal `\n`. As template text that `\n` becomes a source + line break, a different string to Angular's extractor — so inlining it would + change the extracted source text, unlike the other 19 labels this ticket inlines. */ recipientText = input( $localize`:@@brief.canvas.recipient:Adres van de geadresseerde\n(wordt ingevuld bij verzending)`, ); - referenceLabel = input($localize`:@@brief.canvas.reference:Ons kenmerk`); - dateLabel = input($localize`:@@brief.canvas.date:Datum`); - logoAlt = input($localize`:@@brief.canvas.logoAlt:Logo van de organisatie`); - orgNameLabel = input($localize`:@@brief.canvas.orgName:Organisatienaam`); - returnAddressLabel = input($localize`:@@brief.canvas.returnAddress:Retouradres`); - signatureClosingLabel = input($localize`:@@brief.canvas.signatureClosing:Afsluiting`); - signatureNameLabel = input($localize`:@@brief.canvas.signatureName:Naam ondertekenaar`); - signatureRoleLabel = input($localize`:@@brief.canvas.signatureRole:Functie ondertekenaar`); - footerContactLabel = input($localize`:@@brief.canvas.footerContact:Contactgegevens (voettekst)`); - footerLegalLabel = input($localize`:@@brief.canvas.footerLegal:Juridische voettekst`); - zoomGroupLabel = input($localize`:@@brief.canvas.zoom:Zoomniveau`); - zoomInLabel = input($localize`:@@brief.canvas.zoomIn:Inzoomen`); - zoomOutLabel = input($localize`:@@brief.canvas.zoomOut:Uitzoomen`); - zoomResetLabel = input($localize`:@@brief.canvas.zoomReset:100%`); - addedLabel = input($localize`:@@brief.diff.added:nieuw`); - changedLabel = input($localize`:@@brief.diff.changed:gewijzigd sinds afwijzing`); protected showSample = signal(false); protected letterDate = formatDatumNl(new Date()); @@ -395,9 +391,6 @@ export class LetterCanvasComponent { // clamp 0.5–1.5; round to avoid float drift accumulating on repeated clicks. this.zoomLevel.update((z) => Math.round(Math.min(1.5, Math.max(0.5, z + delta)) * 10) / 10); } - protected diffLabel = (kind: BlockDiffKind) => - kind === 'added' ? this.addedLabel() : this.changedLabel(); - /** Admin edit-in-place: the org-identity regions render as controls. */ protected editing = computed(() => this.editableRegions() === 'template'); @@ -418,25 +411,9 @@ export class LetterCanvasComponent { }; }); - // --- read-only rendering helpers (migrated from the superseded letter-preview) --- - - private defs = computed(() => new Map(this.brief().placeholders.map((p) => [p.key, p]))); - private worst = computed(() => { - const m = new Map(); - for (const d of this.diagnostics()) { - if (!d.placeholderKey) continue; - if (d.severity === 'error') m.set(d.placeholderKey, 'error'); - else if (!m.has(d.placeholderKey)) m.set(d.placeholderKey, 'warning'); - } - return m; - }); + // --- read-only rendering helper (migrated from the superseded letter-preview) --- protected segmentsOf = (block: LetterBlock) => groupParagraphs(block.content.paragraphs); - protected labelFor = (key: string) => this.defs().get(key)?.label ?? key; - protected autoFor = (key: string) => this.defs().get(key)?.autoResolvable ?? false; - protected stateFor = (key: string): 'ok' | 'warning' | 'error' => this.worst().get(key) ?? 'ok'; - protected sampleFor = (key: string) => - SAMPLE_VALUES[key] ?? (key === 'datum' ? this.letterDate : this.labelFor(key)); // --- approximate page-break marks (PRD §2b: honest "±", print preview is leading) --- diff --git a/apps/ssp/src/app/brief/ui/letter-canvas/letter-line.component.ts b/apps/ssp/src/app/brief/ui/letter-canvas/letter-line.component.ts new file mode 100644 index 0000000..d583fd6 --- /dev/null +++ b/apps/ssp/src/app/brief/ui/letter-canvas/letter-line.component.ts @@ -0,0 +1,104 @@ +import { Component, computed, input } from '@angular/core'; +import { PlaceholderChipComponent } from '@shared/ui/placeholder-chip/placeholder-chip.component'; +import { RichTextNode } from '@shared/kernel/rich-text'; +import { Diagnostic, PlaceholderDef } from '@brief/domain/placeholders'; + +// Illustrative values for the "Voorbeeld" toggle — what send resolves server-side. +const SAMPLE_VALUES: Record = { + naam_zorgverlener: 'J. Jansen', + big_nummer: '12345678901', +}; + +/** `brief().placeholders` keyed by `key` for O(1) lookup from a node. */ +export function placeholderDefs( + placeholders: readonly PlaceholderDef[], +): Map { + return new Map(placeholders.map((p) => [p.key, p])); +} + +/** The worst (error over warning) diagnostic severity per placeholder key. */ +export function worstSeverities( + diagnostics: readonly Diagnostic[], +): Map { + const m = new Map(); + for (const d of diagnostics) { + if (!d.placeholderKey) continue; + if (d.severity === 'error') m.set(d.placeholderKey, 'error'); + else if (!m.has(d.placeholderKey)) m.set(d.placeholderKey, 'warning'); + } + return m; +} + +export function resolveLabel(defs: ReadonlyMap, key: string): string { + return defs.get(key)?.label ?? key; +} + +export function resolveAuto(defs: ReadonlyMap, key: string): boolean { + return defs.get(key)?.autoResolvable ?? false; +} + +export function resolveState( + worst: ReadonlyMap, + key: string, +): 'ok' | 'warning' | 'error' { + return worst.get(key) ?? 'ok'; +} + +/** The "Voorbeeld" toggle's stand-in for an auto-resolvable placeholder: a canned + sample, the caller's sample date for `datum`, or the field's own label. */ +export function resolveSample( + defs: ReadonlyMap, + sampleDate: string, + key: string, +): string { + return SAMPLE_VALUES[key] ?? (key === 'datum' ? sampleDate : resolveLabel(defs, key)); +} + +/** Organism: one rendered line of letter content — text runs, line breaks and + placeholder chips (an auto-resolvable one swaps to a sample value when + `showSample` is on). Extracted from `letter-canvas` (RD-26): the `#line` + template plus the label/auto/state/sample helpers it needs, so the canvas's + three `ngTemplateOutlet` incantations become one tag each. */ +@Component({ + selector: 'app-letter-line', + imports: [PlaceholderChipComponent], + template: ` + @for (node of nodes(); track $index) { + @switch (node.type) { + @case ('text') { + {{ node.text }} + } + @case ('lineBreak') { +
+ } + @case ('placeholder') { + @if (showSample() && autoFor(node.key)) { + {{ sampleFor(node.key) }} + } @else { + + } + } + } + } + `, +}) +export class LetterLineComponent { + nodes = input.required(); + showSample = input(false); + placeholders = input([]); + diagnostics = input([]); + /** The canvas's `formatDatumNl(new Date())`, passed down so every line agrees. */ + sampleDate = input(''); + + private defs = computed(() => placeholderDefs(this.placeholders())); + private worst = computed(() => worstSeverities(this.diagnostics())); + + protected labelFor = (key: string) => resolveLabel(this.defs(), key); + protected autoFor = (key: string) => resolveAuto(this.defs(), key); + protected stateFor = (key: string) => resolveState(this.worst(), key); + protected sampleFor = (key: string) => resolveSample(this.defs(), this.sampleDate(), key); +} diff --git a/apps/ssp/src/app/brief/ui/letter-canvas/letter-line.spec.ts b/apps/ssp/src/app/brief/ui/letter-canvas/letter-line.spec.ts new file mode 100644 index 0000000..ccaf29a --- /dev/null +++ b/apps/ssp/src/app/brief/ui/letter-canvas/letter-line.spec.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from 'vitest'; +import { PlaceholderDef, Diagnostic } from '@brief/domain/placeholders'; +import { + placeholderDefs, + resolveAuto, + resolveLabel, + resolveSample, + resolveState, + worstSeverities, +} from './letter-line.component'; + +const DEFS: readonly PlaceholderDef[] = [ + { key: 'naam_zorgverlener', label: 'Naam zorgverlener', autoResolvable: true }, + { key: 'reden_besluit', label: 'Reden besluit', autoResolvable: false }, +]; + +const LOCATION = { blockId: 'b1', paragraphIndex: 0, nodeIndex: 0 }; + +function diagnostic(placeholderKey: string, severity: 'error' | 'warning'): Diagnostic { + return { severity, code: 'unresolved-at-send', message: 'x', placeholderKey, location: LOCATION }; +} + +describe('placeholderDefs', () => { + it('keys the placeholder list by its key', () => { + const defs = placeholderDefs(DEFS); + expect(defs.get('naam_zorgverlener')?.label).toBe('Naam zorgverlener'); + expect(defs.get('unknown')).toBeUndefined(); + }); +}); + +describe('worstSeverities', () => { + it('ignores a diagnostic with no placeholder key', () => { + const worst = worstSeverities([ + { severity: 'error', code: 'malformed', message: 'x', location: LOCATION }, + ]); + expect(worst.size).toBe(0); + }); + + it('keeps error over a warning already recorded for the same key', () => { + const worst = worstSeverities([ + diagnostic('naam_zorgverlener', 'warning'), + diagnostic('naam_zorgverlener', 'error'), + ]); + expect(worst.get('naam_zorgverlener')).toBe('error'); + }); + + it('does not let a later warning downgrade an error', () => { + const worst = worstSeverities([ + diagnostic('naam_zorgverlener', 'error'), + diagnostic('naam_zorgverlener', 'warning'), + ]); + expect(worst.get('naam_zorgverlener')).toBe('error'); + }); +}); + +describe('resolveLabel', () => { + it('returns the field label for a known key', () => { + expect(resolveLabel(placeholderDefs(DEFS), 'reden_besluit')).toBe('Reden besluit'); + }); + + it('falls back to the bare key when the field is unknown', () => { + expect(resolveLabel(placeholderDefs(DEFS), 'unknown')).toBe('unknown'); + }); +}); + +describe('resolveAuto', () => { + it('reads autoResolvable off the field', () => { + const defs = placeholderDefs(DEFS); + expect(resolveAuto(defs, 'naam_zorgverlener')).toBe(true); + expect(resolveAuto(defs, 'reden_besluit')).toBe(false); + }); + + it('defaults to false for an unknown key', () => { + expect(resolveAuto(placeholderDefs(DEFS), 'unknown')).toBe(false); + }); +}); + +describe('resolveState', () => { + it('defaults to ok when the key has no diagnostic', () => { + expect(resolveState(worstSeverities([]), 'naam_zorgverlener')).toBe('ok'); + }); + + it('surfaces the worst recorded severity', () => { + const worst = worstSeverities([diagnostic('naam_zorgverlener', 'warning')]); + expect(resolveState(worst, 'naam_zorgverlener')).toBe('warning'); + }); +}); + +describe('resolveSample', () => { + it('prefers the canned sample value over the label', () => { + expect(resolveSample(placeholderDefs(DEFS), '4 september 2026', 'naam_zorgverlener')).toBe( + 'J. Jansen', + ); + }); + + it('resolves datum to the caller-supplied sample date', () => { + expect(resolveSample(placeholderDefs(DEFS), '4 september 2026', 'datum')).toBe( + '4 september 2026', + ); + }); + + it('falls back to the field label for anything else', () => { + expect(resolveSample(placeholderDefs(DEFS), '4 september 2026', 'reden_besluit')).toBe( + 'Reden besluit', + ); + }); +}); diff --git a/docs/project/readable-codebase/RD-26-letter-canvas.md b/docs/project/readable-codebase/RD-26-letter-canvas.md new file mode 100644 index 0000000..1f371a3 --- /dev/null +++ b/docs/project/readable-codebase/RD-26-letter-canvas.md @@ -0,0 +1,171 @@ +# RD-26 — `letter-canvas`: inline the labels, extract `letter-line`, keep the disable + +Status: done +Source: PLAN.md 3d, order step 8 + +## Why + +`letter-canvas.component.ts` is 418 effective lines against a limit of 250. Unlike the other +six offenders, **it is not badly structured** — 77 lines are CSS and the rest is one letter. +Splitting it into letterhead, body, signature and footer would make "what does the letter look +like" a five-file question and buy no behavioural seam. + +Two things do pad it without earning their place: + +- **20 of its 28 `input()`s are pure `$localize` labels, and no caller binds a single one.** + Verified against all three call sites: `behandel-scherm`, `letter-composer` and + `org-template-editor` bind only data (`brief`, `orgTemplate`, `logoUrl`, `editableRegions`, + `diagnostics`, `blockDiffs`, `showDiff`). +- Six lines of `ngTemplateOutlet` ceremony around one `#line` template. + +The CLAUDE.md rule those labels were built for — "Shared/English components must not hardcode +Dutch — expose copy as `input()`s" — governs `libs/shared`. This is a Dutch domain component in +`brief/ui/`. The rule does not apply here. + +## Read first + +- `letter-canvas.component.ts:1` — the disable, whose reason this ticket rewrites. +- `letter-canvas.component.ts:140` — the `#line` template, and lines 255-276, its three + `ngTemplateOutlet` uses. +- `letter-canvas.component.ts:345-372` — the inputs: 8 data, 20 labels. +- PLAN.md 3d and 3e. + +## Decisions (pre-made, don't relitigate) + +1. **Inline 19 of the 20 label inputs as `i18n` in the template.** Same id, same source text, so + **neither `messages.en.xlf` changes**. A label read as `{{ foo() }}` becomes its literal text + plus `i18n="@@id"`; one feeding an attribute becomes `i18n-="@@id"` (`logoAlt` is the + `alt` case). + + The template is inline in this same file, so the 20 `@@brief.canvas.*` ids stay in the file — + they move from a TS declaration into an `i18n` attribute. The id count does not change. + +2. **`recipientText` stays in TS. It is the one exception, and it is not the parameterised case + RD-25 hit.** Its message embeds a newline escape: + + ```ts + $localize`:@@brief.canvas.recipient:Adres van de geadresseerde\n(wordt ingevuld bij verzending)`; + ``` + + In TS that `\n` is one character of the message. Written as template text it becomes a source + line break, which Angular's extractor treats differently — so "same source text", the + property that makes decision 1 free, does not hold for this one. Leave it as an `input()` and + put a one-line comment on it saying why. Verified: no other label interpolates or escapes + anything. + +3. **Do not collapse the labels into a config object or an injection token.** + `HEADER_NAV_ITEMS` and `DEBUG_PANEL` exist because two apps genuinely differ. Here nothing + differs, so a token would add a provider and an indirection to solve a problem nobody has. + +4. **Extract `letter-line.component.ts` beside the canvas, with a spec.** It takes the `#line` + template plus the sample and diff helpers it needs, and replaces the three `ngTemplateOutlet` + incantations with three one-line tags. It is the only part of this file with logic worth + testing, so it gets a `*.spec.ts` — a pure spec over the helpers, no TestBed. + + Drop the `NgTemplateOutlet` import from the canvas once the last outlet is gone. + +5. **Keep `/* eslint-disable max-lines */`, and rewrite its reason.** This is the one ticket in + the arc that keeps a disable. After RD-21 through RD-25 removed theirs, **this is the only + one left in the repository** — verified. The new reason must state the honest fact rather + than promising a future removal: + + ```ts + /* eslint-disable max-lines */ // 77 lines of CSS + one letter's markup; splitting it into + // letterhead/body/signature/footer makes "what does the letter look like" a five-file + // question for no behavioural seam. Deliberate, not deferred. + ``` + + The README requires a disable to name the ticket that removes it. This one names no ticket + **because none will**, and the reason says so in those words. `reportUnusedDisableDirectives` + still keeps it honest: if the file ever drops under 250, lint fails on the unused directive. + +6. **No new stories.** `letter-canvas.stories.ts` renders the canvas, and `letter-line` is + exercised through it. + +## Files + +- `apps/ssp/src/app/brief/ui/letter-canvas/letter-line.component.ts` (new) +- `apps/ssp/src/app/brief/ui/letter-canvas/letter-line.spec.ts` (new) +- `apps/ssp/src/app/brief/ui/letter-canvas/letter-canvas.component.ts` + +## Steps + +1. Extract `letter-line.component.ts` and its spec (decision 4). +2. Replace the three `ngTemplateOutlet` uses and drop the `NgTemplateOutlet` import. +3. Inline the 19 labels (decision 1), leaving `recipientText` alone (decision 2). +4. Rewrite the disable's reason (decision 5). Do not delete the directive. +5. Run `npm run gen:behaviour-spec` — the new spec adds titles. +6. `git add -A`, then run the acceptance commands. +7. Update this ticket's `Status:` to `done` and the README's RD-26 row to `done`. +8. Commit all of it together. + +## Acceptance criteria + +Measured against the tree before handover. Run after `git add -A`. + +```bash +P=apps/ssp/src/app/brief/ui/letter-canvas/letter-canvas.component.ts +git grep -c "= input" -- $P # is 28 -> MUST be 9 (8 data + recipientText) +grep -c 'localize' $P # is 20 -> MUST be 1 (recipientText only) +``` + +The ids stayed in the file and the translation seam did not move (decision 1): + +```bash +grep -o "@@[a-zA-Z0-9_.]*" $P | sort -u | wc -l # is 20 -> MUST still be 20 +git status --short -- '*.xlf' | wc -l # MUST be 0 +``` + +The outlet ceremony is gone and the child exists (decision 4): + +```bash +D=apps/ssp/src/app/brief/ui/letter-canvas +git grep -c "ngTemplateOutlet" -- $P # is 6 -> MUST be 0 +git grep -c "NgTemplateOutlet" -- $P # is 2 -> MUST be 0 +git ls-files $D/letter-line.component.ts $D/letter-line.spec.ts | wc -l # is 0 -> MUST be 2 +``` + +The disable survives, with a new reason, and is the only one in the repository (decision 5): + +```bash +git grep -c "eslint-disable max-lines" -- $P # is 1 -> MUST still be 1 +git grep -c "eslint-disable max-lines" -- apps libs | wc -l # MUST be 1 (one file matches) +git grep -c "RD-26 rewrites this reason" -- $P # is 1 -> MUST be 0 +``` + +```bash +npm run ci --full # exits 0 +``` + +## Verification + +**`npm run lint` is what proves decision 5 both ways.** If the extraction takes the file under +250 lines, the kept directive becomes unused and lint fails — which would mean the disable +should go after all. Report that rather than deleting it silently: it changes the arc's +conclusion that one file legitimately stays over budget. + +`ng build --localize` inside the gate is the check on decisions 1 and 2. **If you find yourself +editing a `.xlf`, you have changed an id or a source string — undo it instead.** + +**`--full` is required** (the Order table says so): `letter-canvas.stories.ts` renders this +component, and the axe pass over it is what proves the inlined `i18n` markup kept its `alt` +text and its labels. + +## Out of scope + +- Splitting the letter into region components. PLAN 3d rejects it explicitly. +- The 77 lines of CSS. +- `recipientText` (decision 2). +- A config object or token for the labels (decision 3). + +## Risks + +- **`recipientText`'s `\n` is the trap** (decision 2). Inlining it is the one change here that + can silently alter an extracted source string, and the `.xlf` files are hand-maintained. +- **Keep the directive** (decision 5). Every other ticket in Phase 3 deleted one; this ticket is + the exception, and deleting it here would fail `max-lines` instead. +- **A label bound by no caller is still a public input.** Removing it is safe only because all + three call sites were checked. Do not extend the same reasoning to the 8 data inputs. +- **`logoAlt` feeds an attribute**, so it needs `i18n-alt`, not `i18n`. An `i18n` attribute on + the element localises its content, not its `alt`, and the a11y check will not catch the + difference because the text is still present. diff --git a/docs/project/readable-codebase/README.md b/docs/project/readable-codebase/README.md index 398aeda..5d9ddb7 100644 --- a/docs/project/readable-codebase/README.md +++ b/docs/project/readable-codebase/README.md @@ -120,7 +120,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di | RD-23 | `registratie-wizard` to 3 steps + the upload-controller move | 08, 20 | yes | done | | RD-24 | `concepts.page` to 6 sections + `concept-card` + globals + code tokens | 02 | yes | done | | RD-25 | `org-template-editor` to `sample-letter.ts` + labels + 2 children | 02 | yes | done | -| RD-26 | `letter-canvas`: inline the labels + `letter-line`; keep one disable | 02 | yes | todo | +| RD-26 | `letter-canvas`: inline the labels + `letter-line`; keep one disable | 02 | yes | done | | RD-27 | **The layer move:** 33 `git mv` + 28 specifiers + 8 MDX imports | 21 | yes | todo | | RD-28 | Layer-tag fixes + the `libs/beheer` title rule | 27 | | todo | | RD-29 | The 3 atomic-ladder rules in dependency-cruiser | 27 | | todo | diff --git a/libs/shared/docs/behaviour-spec.mdx b/libs/shared/docs/behaviour-spec.mdx index c2b6254..a967796 100644 --- a/libs/shared/docs/behaviour-spec.mdx +++ b/libs/shared/docs/behaviour-spec.mdx @@ -20,7 +20,7 @@ tested where._ Every bullet below is a real test name from the suite — an `it()` title (frontend) or a test method name (backend), read as a sentence. Nothing here is hand-written prose: this page -**is** the suite, reshaped for a business reader. 543 frontend behaviours across +**is** the suite, reshaped for a business reader. 556 frontend behaviours across 9 contexts; 261 backend behaviours across 42 test classes. @@ -337,6 +337,10 @@ classes. - preserves library order (= reading order) - never offers non-kern passages +#### placeholderDefs + +- keys the placeholder list by its key + #### proefbriefErrorMessage (TE-002 trust boundary) - surfaces the ProblemDetails detail when present @@ -348,6 +352,33 @@ classes. - derives reason checkboxes (code + label) from the negatief reason passages - positief has no reason-specific redenen +#### resolveAuto + +- reads autoResolvable off the field +- defaults to false for an unknown key + +#### resolveLabel + +- returns the field label for a known key +- falls back to the bare key when the field is unknown + +#### resolveSample + +- prefers the canned sample value over the label +- resolves datum to the caller-supplied sample date +- falls back to the field label for anything else + +#### resolveState + +- defaults to ok when the key has no diagnostic +- surfaces the worst recorded severity + +#### worstSeverities + +- ignores a diagnostic with no placeholder key +- keeps error over a warning already recorded for the same key +- does not let a later warning downgrade an error + ### herregistratie #### IntakeWizardComponent