style: format frontend, docs and skills with prettier; add .prettierignore

One-time prettier --write so the new format:check CI gate starts green.
.prettierignore excludes generated (api-client.ts, documentation.json),
vendored (public/cibg-huisstijl), and backend (dotnet format owns it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-03 13:39:31 +02:00
co-authored by Claude Opus 4.8
parent 546097434d
commit e82309786d
176 changed files with 5067 additions and 1469 deletions
+42 -17
View File
@@ -11,20 +11,38 @@ import { LetterComposerComponent } from '@brief/ui/letter-composer/letter-compos
this just wires signals to the organism and events back to store commands. */
@Component({
selector: 'app-brief-page',
imports: [PageShellComponent, SpinnerComponent, AlertComponent, ButtonComponent, LetterComposerComponent],
styles: [`
.brief-toolbar{display:flex;justify-content:space-between;align-items:center;gap:var(--rhc-space-max-md);margin-block-end:var(--rhc-space-max-lg)}
.save{color:var(--rhc-color-foreground-subtle);font-size:0.9em}
`],
imports: [
PageShellComponent,
SpinnerComponent,
AlertComponent,
ButtonComponent,
LetterComposerComponent,
],
styles: [
`
.brief-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--rhc-space-max-md);
margin-block-end: var(--rhc-space-max-lg);
}
.save {
color: var(--rhc-color-foreground-subtle);
font-size: 0.9em;
}
`,
],
template: `
<app-page-shell
[heading]="heading"
[intro]="intro"
backLink="/dashboard">
@if (lastError(); as err) { <app-alert type="error">{{ err }}</app-alert> }
<app-page-shell [heading]="heading" [intro]="intro" backLink="/dashboard">
@if (lastError(); as err) {
<app-alert type="error">{{ err }}</app-alert>
}
@switch (model().tag) {
@case ('loading') { <app-spinner /> }
@case ('loading') {
<app-spinner />
}
@case ('failed') {
<app-alert type="error">{{ failedText }}</app-alert>
<app-button variant="secondary" (click)="reload()">{{ retryText }}</app-button>
@@ -32,7 +50,9 @@ import { LetterComposerComponent } from '@brief/ui/letter-composer/letter-compos
@case ('loaded') {
<div class="brief-toolbar">
<span class="save" role="status" aria-live="polite">{{ saveText() }}</span>
<app-button variant="subtle" [disabled]="store.busy()" (click)="resetDemo()">{{ resetLabel }}</app-button>
<app-button variant="subtle" [disabled]="store.busy()" (click)="resetDemo()">{{
resetLabel
}}</app-button>
</div>
<app-letter-composer
[brief]="brief()!"
@@ -46,7 +66,8 @@ import { LetterComposerComponent } from '@brief/ui/letter-composer/letter-compos
(submit)="store.submit()"
(approve)="store.approve()"
(reject)="store.reject($event)"
(send)="store.send()" />
(send)="store.send()"
/>
}
}
</app-page-shell>
@@ -70,10 +91,14 @@ export class BriefPage {
/** Debounced-save state, surfaced in a polite live region. */
protected saveText = computed(() => {
switch (this.store.saveState()) {
case 'saving': return this.savingText;
case 'saved': return this.savedText;
case 'error': return this.saveErrorText;
default: return '';
case 'saving':
return this.savingText;
case 'saved':
return this.savedText;
case 'error':
return this.saveErrorText;
default:
return '';
}
});
@@ -8,17 +8,38 @@ import { Diagnostic } from '@brief/domain/placeholders';
@Component({
selector: 'app-diagnostics-panel',
imports: [AlertComponent],
styles: [`
:host{display:block}
ul{margin:0;padding-inline-start:1.1rem;display:grid;gap:0.15rem}
button{background:none;border:0;padding:0;color:var(--rhc-color-foreground-link);cursor:pointer;text-align:start;text-decoration:underline}
`],
styles: [
`
:host {
display: block;
}
ul {
margin: 0;
padding-inline-start: 1.1rem;
display: grid;
gap: 0.15rem;
}
button {
background: none;
border: 0;
padding: 0;
color: var(--rhc-color-foreground-link);
cursor: pointer;
text-align: start;
text-decoration: underline;
}
`,
],
template: `
@if (errors().length) {
<app-alert type="error">
<strong>{{ errorsTitle() }}</strong>
<ul>
@for (d of errors(); track $index) { <li><button type="button" (click)="locate.emit(d)">{{ d.message }}</button></li> }
@for (d of errors(); track $index) {
<li>
<button type="button" (click)="locate.emit(d)">{{ d.message }}</button>
</li>
}
</ul>
</app-alert>
}
@@ -26,7 +47,11 @@ import { Diagnostic } from '@brief/domain/placeholders';
<app-alert type="warning">
<strong>{{ warningsTitle() }}</strong>
<ul>
@for (d of warnings(); track $index) { <li><button type="button" (click)="locate.emit(d)">{{ d.message }}</button></li> }
@for (d of warnings(); track $index) {
<li>
<button type="button" (click)="locate.emit(d)">{{ d.message }}</button>
</li>
}
</ul>
</app-alert>
}
@@ -1,6 +1,9 @@
import { Component, computed, input, output } from '@angular/core';
import { RichTextBlock } from '@shared/kernel/rich-text';
import { RichTextEditorComponent, PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';
import {
RichTextEditorComponent,
PlaceholderOption,
} from '@shared/ui/rich-text-editor/rich-text-editor.component';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { LetterBlock } from '@brief/domain/brief';
@@ -9,21 +12,43 @@ import { LetterBlock } from '@brief/domain/brief';
@Component({
selector: 'app-letter-block',
imports: [RichTextEditorComponent, ButtonComponent],
styles: [`
:host{display:block}
.block{border-inline-start:var(--rhc-border-width-lg) solid var(--rhc-color-border-subtle);padding-inline-start:var(--rhc-space-max-md)}
.meta{display:flex;justify-content:space-between;align-items:center;gap:var(--rhc-space-max-md);margin-block-end:var(--rhc-space-max-sm)}
.controls{display:flex;gap:var(--rhc-space-max-sm)}
`],
styles: [
`
:host {
display: block;
}
.block {
border-inline-start: var(--rhc-border-width-lg) solid var(--rhc-color-border-subtle);
padding-inline-start: var(--rhc-space-max-md);
}
.meta {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--rhc-space-max-md);
margin-block-end: var(--rhc-space-max-sm);
}
.controls {
display: flex;
gap: var(--rhc-space-max-sm);
}
`,
],
template: `
<div class="block">
<div class="meta">
<span class="app-text-subtle">{{ provenance() }}</span>
@if (editable()) {
<span class="controls">
<app-button variant="subtle" (click)="moved.emit(-1)" i18n="@@brief.block.moveUp">Omhoog</app-button>
<app-button variant="subtle" (click)="moved.emit(1)" i18n="@@brief.block.moveDown">Omlaag</app-button>
<app-button variant="subtle" (click)="removed.emit()" i18n="@@brief.block.remove">Verwijderen</app-button>
<app-button variant="subtle" (click)="moved.emit(-1)" i18n="@@brief.block.moveUp"
>Omhoog</app-button
>
<app-button variant="subtle" (click)="moved.emit(1)" i18n="@@brief.block.moveDown"
>Omlaag</app-button
>
<app-button variant="subtle" (click)="removed.emit()" i18n="@@brief.block.remove"
>Verwijderen</app-button
>
</span>
}
</div>
@@ -31,7 +56,8 @@ import { LetterBlock } from '@brief/domain/brief';
[content]="block().content"
[placeholders]="placeholders()"
[editable]="editable()"
(contentChanged)="contentChanged.emit($event)" />
(contentChanged)="contentChanged.emit($event)"
/>
</div>
`,
})
@@ -19,16 +19,44 @@ import { RejectionCommentsComponent } from '@brief/ui/rejection-comments/rejecti
@Component({
selector: 'app-letter-composer',
imports: [
HeadingComponent, StatusBadgeComponent, ButtonComponent, AlertComponent,
LetterSectionComponent, LetterPreviewComponent, DiagnosticsPanelComponent, RejectionCommentsComponent,
HeadingComponent,
StatusBadgeComponent,
ButtonComponent,
AlertComponent,
LetterSectionComponent,
LetterPreviewComponent,
DiagnosticsPanelComponent,
RejectionCommentsComponent,
],
styles: [
`
:host {
display: block;
}
.head {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--rhc-space-max-md);
flex-wrap: wrap;
margin-block-end: var(--rhc-space-max-lg);
}
.sections {
display: grid;
gap: var(--rhc-space-max-2xl);
}
.panel {
margin-block: var(--rhc-space-max-xl);
}
.bar {
display: flex;
flex-wrap: wrap;
gap: var(--rhc-space-max-md);
align-items: center;
margin-block-start: var(--rhc-space-max-xl);
}
`,
],
styles: [`
:host{display:block}
.head{display:flex;justify-content:space-between;align-items:center;gap:var(--rhc-space-max-md);flex-wrap:wrap;margin-block-end:var(--rhc-space-max-lg)}
.sections{display:grid;gap:var(--rhc-space-max-2xl)}
.panel{margin-block:var(--rhc-space-max-xl)}
.bar{display:flex;flex-wrap:wrap;gap:var(--rhc-space-max-md);align-items:center;margin-block-start:var(--rhc-space-max-xl)}
`],
template: `
<div class="head">
<app-heading [level]="2">{{ title() }}</app-heading>
@@ -47,7 +75,8 @@ import { RejectionCommentsComponent } from '@brief/ui/rejection-comments/rejecti
[availablePassages]="availablePassages()"
[placeholders]="menu()"
[editable]="!section.locked"
(edit)="edit.emit($event)" />
(edit)="edit.emit($event)"
/>
}
</div>
} @else {
@@ -62,25 +91,41 @@ import { RejectionCommentsComponent } from '@brief/ui/rejection-comments/rejecti
@switch (status()) {
@case ('draft') {
@if (editable()) {
<app-button variant="primary" [disabled]="!canSubmit() || busy()" (click)="submit.emit()">{{ submitLabel() }}</app-button>
@if (!canSubmit()) { <span class="app-text-subtle">{{ submitHint() }}</span> }
<app-button
variant="primary"
[disabled]="!canSubmit() || busy()"
(click)="submit.emit()"
>{{ submitLabel() }}</app-button
>
@if (!canSubmit()) {
<span class="app-text-subtle">{{ submitHint() }}</span>
}
}
}
@case ('rejected') {
@if (editable()) {
<app-button variant="primary" [disabled]="!canSubmit() || busy()" (click)="submit.emit()">{{ resubmitLabel() }}</app-button>
<app-button
variant="primary"
[disabled]="!canSubmit() || busy()"
(click)="submit.emit()"
>{{ resubmitLabel() }}</app-button
>
}
}
@case ('submitted') {
@if (role() === 'approver') {
<app-button variant="primary" [disabled]="busy()" (click)="approve.emit()">{{ approveLabel() }}</app-button>
<app-button variant="primary" [disabled]="busy()" (click)="approve.emit()">{{
approveLabel()
}}</app-button>
<app-rejection-comments mode="entry" [busy]="busy()" (reject)="reject.emit($event)" />
} @else {
<app-alert type="info">{{ awaitingText() }}</app-alert>
}
}
@case ('approved') {
<app-button variant="primary" [disabled]="busy()" (click)="send.emit()">{{ sendLabel() }}</app-button>
<app-button variant="primary" [disabled]="busy()" (click)="send.emit()">{{
sendLabel()
}}</app-button>
}
@case ('sent') {
<app-alert type="ok">{{ sentText() }}</app-alert>
@@ -108,10 +153,14 @@ export class LetterComposerComponent {
title = input($localize`:@@brief.title:Brief aan de zorgverlener`);
submitLabel = input($localize`:@@brief.submit:Indienen ter beoordeling`);
resubmitLabel = input($localize`:@@brief.resubmit:Opnieuw indienen`);
submitHint = input($localize`:@@brief.submitHint:Vul eerst alle verplichte secties en los fouten op.`);
submitHint = input(
$localize`:@@brief.submitHint:Vul eerst alle verplichte secties en los fouten op.`,
);
approveLabel = input($localize`:@@brief.approve:Goedkeuren`);
sendLabel = input($localize`:@@brief.send:Versturen`);
awaitingText = input($localize`:@@brief.awaiting:De brief wacht op beoordeling door een collega.`);
awaitingText = input(
$localize`:@@brief.awaiting:De brief wacht op beoordeling door een collega.`,
);
sentText = input($localize`:@@brief.sent:De brief is verzonden.`);
protected status = computed(() => this.brief().status.tag);
@@ -130,21 +179,30 @@ export class LetterComposerComponent {
protected statusLabel = computed(() => {
switch (this.status()) {
case 'draft': return $localize`:@@brief.status.draft:Concept`;
case 'submitted': return $localize`:@@brief.status.submitted:Ter beoordeling`;
case 'approved': return $localize`:@@brief.status.approved:Goedgekeurd`;
case 'rejected': return $localize`:@@brief.status.rejected:Afgewezen`;
case 'sent': return $localize`:@@brief.status.sent:Verzonden`;
case 'draft':
return $localize`:@@brief.status.draft:Concept`;
case 'submitted':
return $localize`:@@brief.status.submitted:Ter beoordeling`;
case 'approved':
return $localize`:@@brief.status.approved:Goedgekeurd`;
case 'rejected':
return $localize`:@@brief.status.rejected:Afgewezen`;
case 'sent':
return $localize`:@@brief.status.sent:Verzonden`;
}
});
protected statusColor = computed(() => {
switch (this.status()) {
case 'draft': return 'var(--rhc-color-border-strong)';
case 'submitted': return 'var(--rhc-color-oranje-500)';
case 'draft':
return 'var(--rhc-color-border-strong)';
case 'submitted':
return 'var(--rhc-color-oranje-500)';
case 'approved':
case 'sent': return 'var(--rhc-color-groen-500)';
case 'rejected': return 'var(--rhc-color-rood-500)';
case 'sent':
return 'var(--rhc-color-groen-500)';
case 'rejected':
return 'var(--rhc-color-rood-500)';
}
});
}
@@ -4,8 +4,33 @@ import { Brief, BriefStatus, LibraryPassage } from '@brief/domain/brief';
import { allDiagnostics } from '@brief/domain/brief';
const passages: LibraryPassage[] = [
{ passageId: 'p1', scope: 'global', sectionKey: 'aanhef', label: 'Standaard aanhef', version: 1, content: { paragraphs: [{ nodes: [{ type: 'text', text: 'Geachte heer/mevrouw ' }, { type: 'placeholder', key: 'naam_zorgverlener' }, { type: 'text', text: ',' }] }] } },
{ passageId: 'p2', scope: 'beroep', beroep: 'arts', sectionKey: 'kern', label: 'Toelichting arts', version: 1, content: { paragraphs: [{ nodes: [{ type: 'text', text: 'Als arts ...' }] }] } },
{
passageId: 'p1',
scope: 'global',
sectionKey: 'aanhef',
label: 'Standaard aanhef',
version: 1,
content: {
paragraphs: [
{
nodes: [
{ type: 'text', text: 'Geachte heer/mevrouw ' },
{ type: 'placeholder', key: 'naam_zorgverlener' },
{ type: 'text', text: ',' },
],
},
],
},
},
{
passageId: 'p2',
scope: 'beroep',
beroep: 'arts',
sectionKey: 'kern',
label: 'Toelichting arts',
version: 1,
content: { paragraphs: [{ nodes: [{ type: 'text', text: 'Als arts ...' }] }] },
},
];
function brief(status: BriefStatus): Brief {
@@ -25,22 +50,69 @@ function brief(status: BriefStatus): Brief {
title: 'Aanhef',
required: true,
locked: true,
blocks: [{ type: 'passage', blockId: 'local-1', sourcePassageId: 'p1', sourceVersion: 1, edited: false, content: passages[0].content }],
blocks: [
{
type: 'passage',
blockId: 'local-1',
sourcePassageId: 'p1',
sourceVersion: 1,
edited: false,
content: passages[0].content,
},
],
},
{
sectionKey: 'kern',
title: 'Kern van het besluit',
required: true,
locked: false,
blocks: [{ type: 'freeText', blockId: 'local-2', content: { paragraphs: [{ nodes: [{ type: 'text', text: 'Wij hebben besloten om reden ' }, { type: 'placeholder', key: 'reden_besluit' }, { type: 'text', text: '.' }] }] } }],
blocks: [
{
type: 'freeText',
blockId: 'local-2',
content: {
paragraphs: [
{
nodes: [
{ type: 'text', text: 'Wij hebben besloten om reden ' },
{ type: 'placeholder', key: 'reden_besluit' },
{ type: 'text', text: '.' },
],
},
],
},
},
],
},
{
sectionKey: 'slot',
title: 'Slot',
required: false,
locked: true,
blocks: [
{
type: 'freeText',
blockId: 'local-3',
content: {
paragraphs: [{ nodes: [{ type: 'text', text: 'Met vriendelijke groet,' }] }],
},
},
],
},
{ sectionKey: 'slot', title: 'Slot', required: false, locked: true, blocks: [{ type: 'freeText', blockId: 'local-3', content: { paragraphs: [{ nodes: [{ type: 'text', text: 'Met vriendelijke groet,' }] }] } }] },
],
};
}
const render = (b: Brief, editable: boolean, role: 'drafter' | 'approver') => ({
props: { brief: b, availablePassages: passages, diagnostics: allDiagnostics(b), editable, role, canSubmit: true, busy: false },
props: {
brief: b,
availablePassages: passages,
diagnostics: allDiagnostics(b),
editable,
role,
canSubmit: true,
busy: false,
},
template: `<app-letter-composer [brief]="brief" [availablePassages]="availablePassages" [diagnostics]="diagnostics"
[editable]="editable" [role]="role" [canSubmit]="canSubmit" [busy]="busy"></app-letter-composer>`,
});
@@ -52,7 +124,30 @@ const meta: Meta<LetterComposerComponent> = {
export default meta;
type Story = StoryObj<LetterComposerComponent>;
export const DraftDrafter: Story = { render: () => render(brief({ tag: 'draft' }), true, 'drafter') };
export const SubmittedApprover: Story = { render: () => render(brief({ tag: 'submitted', submittedBy: 'demo-drafter', submittedAt: '2026-07-01' }), false, 'approver') };
export const Rejected: Story = { render: () => render(brief({ tag: 'rejected', rejectedBy: 'demo-approver', rejectedAt: '2026-07-01', comments: 'Graag de aanhef formeler.' }), true, 'drafter') };
export const Sent: Story = { render: () => render(brief({ tag: 'sent', sentAt: '2026-07-01' }), false, 'drafter') };
export const DraftDrafter: Story = {
render: () => render(brief({ tag: 'draft' }), true, 'drafter'),
};
export const SubmittedApprover: Story = {
render: () =>
render(
brief({ tag: 'submitted', submittedBy: 'demo-drafter', submittedAt: '2026-07-01' }),
false,
'approver',
),
};
export const Rejected: Story = {
render: () =>
render(
brief({
tag: 'rejected',
rejectedBy: 'demo-approver',
rejectedAt: '2026-07-01',
comments: 'Graag de aanhef formeler.',
}),
true,
'drafter',
),
};
export const Sent: Story = {
render: () => render(brief({ tag: 'sent', sentAt: '2026-07-01' }), false, 'drafter'),
};
@@ -8,7 +8,10 @@ import { Brief, LetterBlock } from '@brief/domain/brief';
import { Diagnostic } from '@brief/domain/placeholders';
/** A run of consecutive lines to render together: a list (bullet/number) or a single plain line. */
type PreviewSegment = { readonly list: 'bullet' | 'number' | null; readonly items: readonly Paragraph[] };
type PreviewSegment = {
readonly list: 'bullet' | 'number' | null;
readonly items: readonly Paragraph[];
};
function groupParagraphs(paras: readonly Paragraph[]): PreviewSegment[] {
const out: { list: 'bullet' | 'number' | null; items: Paragraph[] }[] = [];
@@ -34,25 +37,54 @@ const SAMPLE_VALUES: Record<string, string> = {
@Component({
selector: 'app-letter-preview',
imports: [NgTemplateOutlet, HeadingComponent, ButtonComponent, PlaceholderChipComponent],
styles: [`
:host{display:block}
.toolbar{display:flex;justify-content:flex-end;margin-block-end:var(--rhc-space-max-sm)}
.letter{background:var(--rhc-color-wit);border:var(--rhc-border-width-sm) solid var(--rhc-color-border-default);border-radius:var(--rhc-border-radius-md);padding:var(--rhc-space-max-2xl)}
section{margin-block-end:var(--rhc-space-max-xl)}
p{margin:0 0 var(--rhc-space-max-sm)}
ul,ol{margin:0 0 var(--rhc-space-max-sm);padding-inline-start:1.4em}
`],
styles: [
`
:host {
display: block;
}
.toolbar {
display: flex;
justify-content: flex-end;
margin-block-end: var(--rhc-space-max-sm);
}
.letter {
background: var(--rhc-color-wit);
border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);
border-radius: var(--rhc-border-radius-md);
padding: var(--rhc-space-max-2xl);
}
section {
margin-block-end: var(--rhc-space-max-xl);
}
p {
margin: 0 0 var(--rhc-space-max-sm);
}
ul,
ol {
margin: 0 0 var(--rhc-space-max-sm);
padding-inline-start: 1.4em;
}
`,
],
template: `
<ng-template #line let-nodes>
@for (node of nodes; track $index) {
@switch (node.type) {
@case ('text') { <span>{{ node.text }}</span> }
@case ('lineBreak') { <br> }
@case ('text') {
<span>{{ node.text }}</span>
}
@case ('lineBreak') {
<br />
}
@case ('placeholder') {
@if (showSample() && autoFor(node.key)) {
<span>{{ sampleFor(node.key) }}</span>
} @else {
<app-placeholder-chip [label]="labelFor(node.key)" [autoResolvable]="autoFor(node.key)" [state]="stateFor(node.key)" />
<app-placeholder-chip
[label]="labelFor(node.key)"
[autoResolvable]="autoFor(node.key)"
[state]="stateFor(node.key)"
/>
}
}
}
@@ -60,7 +92,11 @@ const SAMPLE_VALUES: Record<string, string> = {
</ng-template>
<div class="toolbar">
<app-button variant="subtle" (click)="showSample.set(!showSample())" [attr.aria-pressed]="showSample()">
<app-button
variant="subtle"
(click)="showSample.set(!showSample())"
[attr.aria-pressed]="showSample()"
>
{{ showSample() ? hideSampleLabel() : showSampleLabel() }}
</app-button>
</div>
@@ -72,11 +108,34 @@ const SAMPLE_VALUES: Record<string, string> = {
@for (block of section.blocks; track block.blockId) {
@for (seg of segmentsOf(block); track $index) {
@if (seg.list === 'bullet') {
<ul>@for (para of seg.items; track $index) { <li><ng-container [ngTemplateOutlet]="line" [ngTemplateOutletContext]="{ $implicit: para.nodes }" /></li>}</ul>
<ul>
@for (para of seg.items; track $index) {
<li>
<ng-container
[ngTemplateOutlet]="line"
[ngTemplateOutletContext]="{ $implicit: para.nodes }"
/>
</li>
}
</ul>
} @else if (seg.list === 'number') {
<ol>@for (para of seg.items; track $index) { <li><ng-container [ngTemplateOutlet]="line" [ngTemplateOutletContext]="{ $implicit: para.nodes }" /></li>}</ol>
<ol>
@for (para of seg.items; track $index) {
<li>
<ng-container
[ngTemplateOutlet]="line"
[ngTemplateOutletContext]="{ $implicit: para.nodes }"
/>
</li>
}
</ol>
} @else {
<p><ng-container [ngTemplateOutlet]="line" [ngTemplateOutletContext]="{ $implicit: seg.items[0].nodes }" /></p>
<p>
<ng-container
[ngTemplateOutlet]="line"
[ngTemplateOutletContext]="{ $implicit: seg.items[0].nodes }"
/>
</p>
}
}
}
@@ -93,7 +152,11 @@ export class LetterPreviewComponent {
hideSampleLabel = input($localize`:@@brief.preview.hideSample:Testwaarden verbergen`);
protected showSample = signal(false);
private today = new Date().toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' });
private today = new Date().toLocaleDateString('nl-NL', {
day: 'numeric',
month: 'long',
year: 'numeric',
});
private defs = computed(() => new Map(this.brief().placeholders.map((p) => [p.key, p])));
private worst = computed(() => {
@@ -110,5 +173,6 @@ export class LetterPreviewComponent {
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.today : this.labelFor(key));
protected sampleFor = (key: string) =>
SAMPLE_VALUES[key] ?? (key === 'datum' ? this.today : this.labelFor(key));
}
@@ -14,17 +14,36 @@ import { PassagePickerComponent } from '@brief/ui/passage-picker/passage-picker.
@Component({
selector: 'app-letter-section',
imports: [ButtonComponent, HeadingComponent, LetterBlockComponent, PassagePickerComponent],
styles: [`
:host{display:block}
.blocks{display:grid;gap:var(--rhc-space-max-lg);margin-block:var(--rhc-space-max-md)}
.actions{display:flex;flex-wrap:wrap;gap:var(--rhc-space-max-sm)}
.required{color:var(--rhc-color-foreground-subtle)}
.empty{color:var(--rhc-color-foreground-subtle);font-style:italic}
`],
styles: [
`
:host {
display: block;
}
.blocks {
display: grid;
gap: var(--rhc-space-max-lg);
margin-block: var(--rhc-space-max-md);
}
.actions {
display: flex;
flex-wrap: wrap;
gap: var(--rhc-space-max-sm);
}
.required {
color: var(--rhc-color-foreground-subtle);
}
.empty {
color: var(--rhc-color-foreground-subtle);
font-style: italic;
}
`,
],
template: `
<app-heading [level]="3">
{{ section().title }}
@if (section().required) { <span class="required">· {{ requiredLabel() }}</span> }
@if (section().required) {
<span class="required">· {{ requiredLabel() }}</span>
}
</app-heading>
<div class="blocks">
@@ -35,7 +54,8 @@ import { PassagePickerComponent } from '@brief/ui/passage-picker/passage-picker.
[editable]="editable()"
(contentChanged)="onContent(block.blockId, $event)"
(removed)="edit.emit({ tag: 'BlockRemoved', blockId: block.blockId })"
(moved)="onMove(block.blockId, $event)" />
(moved)="onMove(block.blockId, $event)"
/>
} @empty {
<p class="empty">{{ emptyLabel() }}</p>
}
@@ -43,8 +63,14 @@ import { PassagePickerComponent } from '@brief/ui/passage-picker/passage-picker.
@if (editable()) {
<div class="actions">
<app-button variant="secondary" (click)="pickerOpen.set(!pickerOpen())">{{ addPassageLabel() }}</app-button>
<app-button variant="subtle" (click)="edit.emit({ tag: 'FreeTextBlockAdded', sectionKey: section().sectionKey })">{{ addFreeLabel() }}</app-button>
<app-button variant="secondary" (click)="pickerOpen.set(!pickerOpen())">{{
addPassageLabel()
}}</app-button>
<app-button
variant="subtle"
(click)="edit.emit({ tag: 'FreeTextBlockAdded', sectionKey: section().sectionKey })"
>{{ addFreeLabel() }}</app-button
>
</div>
@if (pickerOpen()) {
<app-passage-picker [passages]="sectionPassages()" (insert)="onInsert($event)" />
@@ -65,7 +91,9 @@ export class LetterSectionComponent {
addFreeLabel = input($localize`:@@brief.section.addFree:Vrije tekst toevoegen`);
protected pickerOpen = signal(false);
protected sectionPassages = computed(() => this.availablePassages().filter((p) => p.sectionKey === this.section().sectionKey));
protected sectionPassages = computed(() =>
this.availablePassages().filter((p) => p.sectionKey === this.section().sectionKey),
);
protected onContent(blockId: string, content: RichTextBlock) {
this.edit.emit({ tag: 'BlockContentEdited', blockId, content });
@@ -10,11 +10,26 @@ import { LibraryPassage } from '@brief/domain/brief';
@Component({
selector: 'app-passage-picker',
imports: [FormsModule, CheckboxComponent, ButtonComponent],
styles: [`
:host{display:block;border:var(--rhc-border-width-sm) solid var(--rhc-color-border-default);border-radius:var(--rhc-border-radius-md);padding:var(--rhc-space-max-md)}
ul{list-style:none;margin:0 0 var(--rhc-space-max-md);padding:0;display:grid;gap:var(--rhc-space-max-sm)}
.scope{color:var(--rhc-color-foreground-subtle)}
`],
styles: [
`
:host {
display: block;
border: var(--rhc-border-width-sm) solid var(--rhc-color-border-default);
border-radius: var(--rhc-border-radius-md);
padding: var(--rhc-space-max-md);
}
ul {
list-style: none;
margin: 0 0 var(--rhc-space-max-md);
padding: 0;
display: grid;
gap: var(--rhc-space-max-sm);
}
.scope {
color: var(--rhc-color-foreground-subtle);
}
`,
],
template: `
<ul>
@for (p of passages(); track p.passageId) {
@@ -22,12 +37,15 @@ import { LibraryPassage } from '@brief/domain/brief';
<app-checkbox
[label]="p.label"
[ngModel]="!!checked()[p.passageId]"
(ngModelChange)="set(p.passageId, $event)" />
(ngModelChange)="set(p.passageId, $event)"
/>
<span class="scope"> · {{ p.scope === 'beroep' ? beroepLabel() : globalLabel() }}</span>
</li>
}
</ul>
<app-button variant="secondary" [disabled]="count() === 0" (click)="add()">{{ addLabel() }} ({{ count() }})</app-button>
<app-button variant="secondary" [disabled]="count() === 0" (click)="add()"
>{{ addLabel() }} ({{ count() }})</app-button
>
`,
})
export class PassagePickerComponent {
@@ -8,18 +8,33 @@ import { ButtonComponent } from '@shared/ui/button/button.component';
@Component({
selector: 'app-rejection-comments',
imports: [FormsModule, AlertComponent, ButtonComponent],
styles: [`
:host{display:block}
textarea{inline-size:100%;box-sizing:border-box;min-block-size:4rem;margin-block:var(--rhc-space-max-sm)}
label{font-weight:600}
`],
styles: [
`
:host {
display: block;
}
textarea {
inline-size: 100%;
box-sizing: border-box;
min-block-size: 4rem;
margin-block: var(--rhc-space-max-sm);
}
label {
font-weight: 600;
}
`,
],
template: `
@if (mode() === 'show') {
<app-alert type="warning"><strong>{{ rejectedTitle() }}</strong> {{ comments() }}</app-alert>
<app-alert type="warning"
><strong>{{ rejectedTitle() }}</strong> {{ comments() }}</app-alert
>
} @else {
<label for="reject-comments">{{ entryLabel() }}</label>
<textarea id="reject-comments" [(ngModel)]="draft"></textarea>
<app-button variant="danger" [disabled]="!draft().trim() || busy()" (click)="submit()">{{ rejectLabel() }}</app-button>
<app-button variant="danger" [disabled]="!draft().trim() || busy()" (click)="submit()">{{
rejectLabel()
}}</app-button>
}
`,
})