+ {{ tag() }} +
+ @if (code(); as c) { + @if (src(); as s) { ++
Vijf functionele patronen die atomic design makkelijker maakt om te tonen — telkens "fout" (de oude vorm liet het toe) naast"goed" (het type maakt het onmogelijk).
- - -Laat elke variant precies de gegevens dragen die kloppen — niets meer.
-Fout — vlakke interface
- -- Een doorgehaalde registratie houdt tóch een herregistratiedatum: onmogelijke toestand. -
-Goed — sum type
-
- De variant Doorgehaald kent geen herregistratiedatum, dus de rij bestaat
- simpelweg niet.
-
- Eén waarde met vier elkaar uitsluitende toestanden in plaats van drie losse booleans. -
-Vier toestanden, één molecuul
-Loading
-Empty
-Failure
-Success
-De exhaustieve fold
-
- Een nieuwe variant toevoegen breekt de compile via assertNever tot je hem
- afhandelt.
-
Na het parsen onthoudt het type dat de waarde geldig is.
-Smart constructor → Result
-ok
-Postcode ="{{ r.value }}"
-
- Een gevalideerde Postcode is een ander type dan een ruwe string.
-
err
-{{ r.error }}
- - Eén tagged union stuurt de UI. Speel met de wizard — de gemarkeerde toestand is de - huidige. -
-Fout — losse booleans
- -- Niets verhindert"submitting" mét validatiefouten of een successcherm met errors. -
-Goed — één tagged union
-
- Het aantal stappen ligt vast (STEPS); vervolgvragen verschijnen
- binnen een stap op basis van eerdere antwoorden. Antwoord"ja" op buitenland of
- vul weinig uren in, en er komt een extra vraag bij in dezelfde stap — de voortgang"van N"
- blijft gelijk.
-
Vaste stappen
-- De stappen zijn altijd dezelfde; alleen de vragen binnen een stap verschijnen - of verdwijnen. -
-De wizard
-- Een BSN is bijzondere persoonsgegevens (AVG art. 9). Dataminimalisatie: standaard - gemaskeerd tonen, alleen tonen na een vastgelegde handeling; en "parse, don't validate" op - het gevoeligste veld — een pure functie die de elfproef afdwingt. -
-Maskeren — atom
-
- BSN:
-
- Standaard gemaskeerd; het echte tonen is step-up-geverifieerd én vastgelegd (zie het
- behandelscherm). De atom bevat de maskeer-detectie — geen los *-gesnuffel
- bij elke gebruiker.
-
Parse (elfproef) → Result
-ok
-Bsn ="{{ b.value }}"
- } @else {
- err
-{{ b.error }}
- }
- + Eén tagged union stuurt de UI. Speel met de wizard — de gemarkeerde toestand is de huidige. +
++ Niets verhindert"submitting" mét validatiefouten of een successcherm met errors. +
+Na het parsen onthoudt het type dat de waarde geldig is.
+Postcode ="{{ r.value }}"
+
+ Een gevalideerde Postcode is een ander type dan een ruwe string.
+
{{ r.error }}
+ + Een BSN is bijzondere persoonsgegevens (AVG art. 9). Dataminimalisatie: standaard gemaskeerd + tonen, alleen tonen na een vastgelegde handeling; en"parse, don't validate" op het + gevoeligste veld — een pure functie die de elfproef afdwingt. +
+
+ BSN:
+
+ Standaard gemaskeerd; het echte tonen is step-up-geverifieerd én vastgelegd (zie het
+ behandelscherm). De atom bevat de maskeer-detectie — geen los *-gesnuffel
+ bij elke gebruiker.
+
Bsn ="{{ b.value }}"
+ } @else {
+ {{ b.error }}
+ }
+ + Eén waarde met vier elkaar uitsluitende toestanden in plaats van drie losse booleans. +
+
+ Een nieuwe variant toevoegen breekt de compile via assertNever tot je hem
+ afhandelt.
+
Laat elke variant precies de gegevens dragen die kloppen — niets meer.
++ Een doorgehaalde registratie houdt tóch een herregistratiedatum: onmogelijke toestand. +
+
+ De variant Doorgehaald kent geen herregistratiedatum, dus de rij bestaat
+ simpelweg niet.
+
+ Het aantal stappen ligt vast (STEPS); vervolgvragen verschijnen
+ binnen een stap op basis van eerdere antwoorden. Antwoord"ja" op buitenland of vul
+ weinig uren in, en er komt een extra vraag bij in dezelfde stap — de voortgang"van N" blijft
+ gelijk.
+
+ De stappen zijn altijd dezelfde; alleen de vragen binnen een stap verschijnen + of verdwijnen. +
+`), so it must be projected, and projected content keeps the _declaring_ component's scope |
+
+ **Delete `.section` entirely** — the global `.app-section` already exists and does the job.
+
+4. **`concept-card.component.ts` owns the card vocabulary and renders it.** New component in
+ `apps/ssp/src/app/showcase/`. It owns `.card`, `.card--good`, `.card--bad`, `.tag`, its three
+ modifiers and both `::before` rules, plus `.linked` and `.linked .src`.
+
+ Its API, driven by what the 12 current usages need:
+
+ ```ts
+ variant = input<'good' | 'bad' | 'plain'>('plain'); // card--good / card--bad / tag colour
+ tag = input.required(); // the uppercase label
+ code = input(); // pre [innerHTML], optional
+ src = input(); // figcaption; wraps code in figure.linked
+ ```
+
+ Everything else is projected through ` `. The card **renders the `` itself**
+ when `code` is set — that is what keeps `.app-code`'s box styling working without relying on
+ projection.
+
+5. **Six section components, one per ``**, in `apps/ssp/src/app/showcase/`:
+
+ | File | Class | Heading | Own CSS |
+ | ------------------------- | -------------------- | --------------------------- | --------------------------------------------- |
+ | `unions.section.ts` | `UnionsSection` | 1 · Discriminated unions | none |
+ | `remote-data.section.ts` | `RemoteDataSection` | 2 · RemoteData fold | none |
+ | `parse.section.ts` | `ParseSection` | 3 · Parse, don't validate | none |
+ | `form-machine.section.ts` | `FormMachineSection` | 4 · Form als state machine | `.machine`, `.node`, `.node.on` |
+ | `vragenlijst.section.ts` | `VragenlijstSection` | 5 · Vragenlijst | `.steplist`, `.pill`, `.pill.extra`, `.arrow` |
+ | `pii.section.ts` | `PiiSection` | 6 · PII — maskeren & parsen | none |
+
+ The "Own CSS" column is measured: those selectors appear in exactly one section each. Every
+ other selector is now a global or lives in the card.
+
+6. **The page keeps only what composes.** After the split `concepts.page.ts` holds its heading,
+ its intro, and six elements. It keeps no `styles:` block. `code` and `src` (the generated
+ snippets) move to whichever sections use them — each section imports
+ `snippets.generated.ts` directly.
+
+7. **Widen the colour guard, and fix the one file that widening catches.**
+ `scripts/check-tokens.sh:14` greps `--include='*.component.ts'`, so **every `*.page.ts`,
+ `*.section.ts` and `*.step.ts` in the repository is invisible to it** — including the six
+ sections this ticket creates and the six `*.step.ts` files RD-22 and RD-23 just added. That
+ is why this page accumulated 21 hardcoded colours unnoticed.
+
+ Change the include to `--include='*.ts'` and exclude specs and stories, which legitimately
+ show colour swatches:
+
+ ```bash
+ hits=$(grep -rnE '#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(' apps libs --include='*.ts' \
+ | grep -vE '\.(spec|stories)\.ts:' | grep -v 'token-ok' || true)
+ ```
+
+ Measured: this newly catches exactly one other line, `libs/beheer/src/ui/audit.page.ts:39`
+ (`var(--rhc-color-rood-600, #a30000)`). Fix it by dropping the fallback, as decision 8 does
+ for this page. **Leave the CIBG-GAP marker check at `*.component.ts`** — a gap extension is a
+ component concept (ADR-0003).
+
+8. **Drop every `var(--rhc-…, #hex)` fallback.** All the referenced tokens are defined in the
+ bridge, so the fallback is dead weight that also trips the widened guard. `.card`'s
+ `background: #fff` becomes `var(--rhc-color-wit)` — verified: that token is defined in
+ `styles.scss`.
+
+9. **No stories.** `showcase` is a teaching page, not a feature, and it has no story today.
+ Adding six is not this ticket's job.
+
+10. **Delete `/* eslint-disable max-lines */` from the page.** Mandatory — the rules pin each
+ other in both directions.
+
+## Files
+
+- `libs/shared/styles.scss` — 5 tokens, 4 globals
+- `scripts/check-tokens.sh` — one line (decision 7)
+- `libs/beheer/src/ui/audit.page.ts` — one fallback (decision 7)
+- `apps/ssp/src/app/showcase/concept-card.component.ts` (new)
+- `apps/ssp/src/app/showcase/{unions,remote-data,parse,form-machine,vragenlijst,pii}.section.ts` (new)
+- `apps/ssp/src/app/showcase/concepts.page.ts`
+
+## Steps
+
+1. Add the tokens and the four globals to `libs/shared/styles.scss` (decisions 2 and 3).
+2. Write `concept-card.component.ts` (decision 4).
+3. Move each `` into its own file, replacing every `` with
+ ``, `class="lead|cols|note"` with the `.app-*` names, and `` with
+ either the card's `code` input or `` for the four dynamic result blocks.
+4. Reduce the page to composition, with no `styles:` block.
+5. Widen the guard and fix `audit.page.ts` (decision 7).
+6. Delete the disable (decision 10).
+7. `git add -A`, then run the acceptance commands.
+8. Update this ticket's `Status:` to `done` and the README's RD-24 row to `done`.
+9. Commit all of it together.
+
+## Acceptance criteria
+
+Measured against the tree before handover. Run after `git add -A`.
+
+```bash
+D=apps/ssp/src/app/showcase
+git ls-files "$D/*.section.ts" | wc -l # is 0 -> MUST be 6
+git ls-files "$D/concept-card.component.ts" | wc -l # is 0 -> MUST be 1
+git grep -c "eslint-disable max-lines" -- $D/concepts.page.ts # is 1 -> MUST be 0
+git grep -c "styles:" -- $D/concepts.page.ts # is 1 -> MUST be 0
+```
+
+The colours left the page, and the guard now covers it:
+
+```bash
+git grep -cE "#[0-9a-fA-F]{3,6}" -- $D/concepts.page.ts # is 21 -> MUST be 0
+git grep -c "include='\*\.component\.ts'" -- scripts/check-tokens.sh # is 2 -> MUST be 1 (the CIBG-GAP check keeps it)
+npm run check:tokens # exits 0
+```
+
+The highlighting rules are global, where innerHTML children can reach them (decision 1):
+
+```bash
+git grep -c "app-code" -- libs/shared/styles.scss # MUST be >= 4
+git grep -c "app-code" -- $D/concepts.page.ts # MUST be 0
+```
+
+The teaching content did not change while being moved:
+
+```bash
+git grep -ho "code\['[a-zA-Z]*'\]" -- $D/ | sort -u | wc -l # is 9 -> MUST still be 9
+```
+
+```bash
+npm run ci --full # exits 0
+```
+
+## Verification
+
+**`--full` is required** — this edits `libs/shared/styles.scss`, which every story renders
+against.
+
+**Look at the page.** This is the one ticket in the arc whose main fix is invisible to every
+automated check: no test asserts a computed colour. Run `npm start`, open `/concepts`, and
+confirm that keywords, strings and comments in the code blocks are now coloured — purple, green
+and grey-italic against the dark background. If they are still monochrome, the rules are still
+scoped to a component.
+
+**Do not add a line-count command.** `npm run lint` is the exact check.
+
+## Out of scope
+
+- Changing any teaching copy, snippet or demo. This is a move, not a rewrite.
+- `highlight-ts.ts` itself. Its output is correct; only the CSS was unreachable.
+- Adding stories (decision 9).
+- The `--app-devpanel-*` tokens, and any other page's colours.
+
+## Risks
+
+- **Angular does not style projected or `[innerHTML]` content from the receiving component.**
+ This is the constraint that shapes decisions 1, 3 and 4. If you find yourself moving a rule
+ into a component and its markup comes from somewhere else, the rule belongs in the global
+ sheet.
+- **The `.app-note` case is subtle**: a note's text contains `` markup, so it must be
+ projected — which is exactly why it cannot be styled by the card. Global it is.
+- **The four dynamic `` blocks** (the ok/err demo output in sections 3 and 6) are not code
+ snippets and have no `src`. Give them `class="app-code"` directly rather than forcing them
+ through the card's `code` input.
+- **Widening the guard is a two-line change with a measured blast radius of one other file**
+ (decision 7). If it catches more than `audit.page.ts:39`, stop and report — something landed
+ since this ticket was written.
+- **Deleting the disable is mandatory** (decision 10).
diff --git a/docs/project/readable-codebase/README.md b/docs/project/readable-codebase/README.md
index 6603dc7..ccdc2c5 100644
--- a/docs/project/readable-codebase/README.md
+++ b/docs/project/readable-codebase/README.md
@@ -118,7 +118,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di
| RD-21 | `rich-text-dom.ts` helpers + spec cases | 02 | yes | done |
| RD-22 | `intake-wizard` to 3 step components | 08, 20 | yes | done |
| 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 | todo |
+| 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 | todo |
| RD-26 | `letter-canvas`: inline the labels + `letter-line`; keep one disable | 02 | yes | todo |
| RD-27 | **The layer move:** 33 `git mv` + 28 specifiers + 8 MDX imports | 21 | yes | todo |
diff --git a/libs/beheer/src/ui/audit.page.ts b/libs/beheer/src/ui/audit.page.ts
index 3054f22..6df5795 100644
--- a/libs/beheer/src/ui/audit.page.ts
+++ b/libs/beheer/src/ui/audit.page.ts
@@ -36,7 +36,7 @@ import { AuditStore } from '@beheer/application/audit.store';
font-weight: var(--rhc-text-font-weight-semi-bold);
}
.deny {
- color: var(--rhc-color-rood-600, #a30000);
+ color: var(--rhc-color-rood-600);
font-weight: var(--rhc-text-font-weight-semi-bold);
}
`,
diff --git a/libs/shared/styles.scss b/libs/shared/styles.scss
index 84af730..d357b32 100644
--- a/libs/shared/styles.scss
+++ b/libs/shared/styles.scss
@@ -113,6 +113,15 @@ body {
--app-devpanel-accent: #9cdcfe;
--app-devpanel-border: #444;
--app-devpanel-shadow: rgb(0 0 0 / 0.4);
+
+ /* Showcase code-block palette (concepts.page.ts, RD-24): the same "exempt file"
+ reasoning as --app-devpanel-* above. A dark code-editor palette, kept off the CIBG
+ design system, for the teaching page's highlighted TS snippets. */
+ --app-code-bg: #1e2430;
+ --app-code-fg: #e6e9ef;
+ --app-code-keyword: #c792ea;
+ --app-code-string: #c3e88d;
+ --app-code-comment: #7e8aa0;
}
/* App utility classes: centralise the repeated inline layout idioms so components stay
@@ -127,6 +136,49 @@ body {
.app-text-subtle {
color: var(--rhc-color-foreground-subtle);
}
+/* Highlighted code block (concepts.page.ts, RD-24). Global because the `.k`/`.s`/`.c`
+ keyword/string/comment spans arrive through `[innerHTML]` — they never carry the
+ rendering component's `_ngcontent` attribute, so a component-scoped rule can never
+ match them. This is why the highlighting never rendered before RD-24. */
+.app-code {
+ background: var(--app-code-bg);
+ color: var(--app-code-fg);
+ padding: 1rem;
+ border-radius: 8px;
+ overflow: auto;
+ font-size: 0.82rem;
+ line-height: 1.55;
+ margin: 0;
+}
+.app-code .k {
+ color: var(--app-code-keyword);
+}
+.app-code .s {
+ color: var(--app-code-string);
+}
+.app-code .c {
+ color: var(--app-code-comment);
+ font-style: italic;
+}
+/* Page-level typography idioms shared by every showcase section (concepts.page.ts). */
+.app-lead {
+ color: var(--rhc-color-grijs-700);
+ max-width: 46rem;
+ margin: 0.25rem 0 1.25rem;
+}
+.app-cols {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
+ gap: 1.5rem;
+ align-items: start;
+}
+/* A note whose content includes markup (e.g. ``) must be projected into its
+ card, so it keeps the DECLARING component's scope, not the card's — global it is. */
+.app-note {
+ font-size: 0.9rem;
+ color: var(--rhc-color-grijs-700);
+ margin: 0.75rem 0 0;
+}
/* Route transitions (withViewTransitions): cross-fade the routed CONTENT only.
The chrome gets its own stable view-transition-name so it's lifted out of the
diff --git a/scripts/check-tokens.sh b/scripts/check-tokens.sh
index 3e86c97..80afa43 100755
--- a/scripts/check-tokens.sh
+++ b/scripts/check-tokens.sh
@@ -1,7 +1,9 @@
#!/usr/bin/env bash
-# WP-02 token guard: fail if any *.component.ts hardcodes a colour (hex/rgb/hsl)
-# instead of a --rhc-*/--app-* design token. Palette values live ONLY in the
-# styles.scss token bridge (the one exempt file — it IS the bridge).
+# WP-02 token guard: fail if any *.ts file hardcodes a colour (hex/rgb/hsl) instead of
+# a --rhc-*/--app-* design token. Specs and stories are exempt — they legitimately show
+# colour swatches. Palette values live ONLY in the styles.scss token bridge (the one
+# exempt file — it IS the bridge). Widened from *.component.ts to *.ts in RD-24: a
+# *.page.ts, *.section.ts or *.step.ts hardcoding a colour was invisible before that.
#
# px/rem are deliberately NOT grepped: too many false positives (font sizes,
# transforms, media queries). Raw border widths are fixed by hand and mapped to
@@ -11,7 +13,8 @@
# false positive (a colour word inside a comment or a data-URI). Keep the bar high.
set -uo pipefail
-hits=$(grep -rnE '#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(' apps libs --include='*.component.ts' | grep -v 'token-ok' || true)
+hits=$(grep -rnE '#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(' apps libs --include='*.ts' \
+ | grep -vE '\.(spec|stories)\.ts:' | grep -v 'token-ok' || true)
if [ -n "$hits" ]; then
echo "$hits"
echo 'FAIL: hardcoded colours in components (use --rhc-*/--app-* tokens, or add a `token-ok` marker + reason)'