refactor: split concepts.page into 6 sections, fix dead highlighting (RD-24)

The page held six teaching sections and a 142-line `styles:` block, at 471
effective lines against a limit of 250. It is now 36 lines of composition.

Angular scopes a component's CSS to markup that component rendered, so the
split had to move each rule to its owner. `concept-card` owns the card
vocabulary and renders it. `.app-code`, `.app-lead`, `.app-cols` and
`.app-note` become globals, because their targets are projected or arrive
through `[innerHTML]`.

That constraint exposed a live bug. The syntax-highlighting rules compiled to
`pre[_ngcontent-%COMP%] .k[_ngcontent-%COMP%]`, but `highlight-ts` injects the
`.k`/`.s`/`.c` spans through `[innerHTML]`, so they carry no scope attribute
and the rule never matched. Keywords, strings and comments have always
rendered in the plain foreground colour. The rules are global now, on five new
`--app-code-*` tokens.

Widen the colour guard while here: it scanned only `*.component.ts`, so every
`*.page.ts`, `*.section.ts` and `*.step.ts` was invisible to it. That is how
this page collected 21 hardcoded colours. One other file needed a fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-09-04 23:44:01 +02:00
co-authored by Claude Sonnet 5
parent d5a7a25a78
commit 630d68045f
13 changed files with 908 additions and 488 deletions
@@ -0,0 +1,92 @@
import { Component } from '@angular/core';
import type { Resource } from '@angular/core';
import { HeadingComponent } from '@shared/ui/heading/heading.component';
import { ASYNC } from '@shared/ui/async/async.component';
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
import { ConceptCardComponent } from './concept-card.component';
import { SNIPPETS } from './snippets.generated';
import { highlightTs } from './highlight-ts';
/** Minimal fake Resource so <app-async> can be driven through every state without HTTP. */
function fakeResource<T>(status: string, value?: T, error?: Error): Resource<T> {
return {
value: () => value as T,
status: () => status,
error: () => error,
hasValue: () => value !== undefined,
reload: () => {},
} as unknown as Resource<T>;
}
/** Section 2: RemoteData fold. One value with four mutually exclusive states, instead of
three loose booleans. Composition-only; owns its own fake resources. Each of the four
demo states nests its own `<app-concept-card>` for its label — a plain `.tag` element
written here would carry this section's scope, not the card's, and stay unstyled. */
@Component({
selector: 'app-concepts-remote-data-section',
imports: [HeadingComponent, ...ASYNC, SkeletonComponent, ConceptCardComponent],
template: `
<section class="app-section">
<app-heading [level]="2">2 · RemoteData fold</app-heading>
<p class="app-lead">
Eén waarde met vier elkaar uitsluitende toestanden in plaats van drie losse booleans.
</p>
<div class="app-cols">
<app-concept-card variant="good" tag="Vier toestanden, één molecuul">
<div class="app-stack">
<app-concept-card variant="plain" tag="Loading">
<app-async [resource]="loadingRes"
><ng-template appAsyncLoaded let-v>{{ v }}</ng-template
><ng-template appAsyncLoading
><app-skeleton [count]="2" height="1.2rem" [delay]="0" /></ng-template
></app-async>
</app-concept-card>
<app-concept-card variant="plain" tag="Empty">
<app-async [resource]="emptyRes" [isEmpty]="isEmpty"
><ng-template appAsyncLoaded let-v>{{ v }}</ng-template></app-async
>
</app-concept-card>
<app-concept-card variant="plain" tag="Failure">
<app-async [resource]="errorRes"
><ng-template appAsyncLoaded let-v>{{ v }}</ng-template></app-async
>
</app-concept-card>
<app-concept-card variant="plain" tag="Success">
<app-async [resource]="successRes" [isEmpty]="isEmpty"
><ng-template appAsyncLoaded
><ul>
@for (i of successRes.value(); track i) {
<li>{{ i }}</li>
}
</ul></ng-template
></app-async
>
</app-concept-card>
</div>
</app-concept-card>
<app-concept-card
variant="good"
tag="De exhaustieve fold"
[code]="code['fold']"
[src]="src['fold']"
>
<p class="app-note">
Een nieuwe variant toevoegen breekt de compile via <code>assertNever</code> tot je hem
afhandelt.
</p>
</app-concept-card>
</div>
</section>
`,
})
export class RemoteDataSection {
isEmpty = (v: string[]) => !v || v.length === 0;
loadingRes = fakeResource<string[]>('loading');
emptyRes = fakeResource<string[]>('resolved', []);
errorRes = fakeResource<string[]>('error', undefined, new Error('Demo'));
successRes = fakeResource<string[]>('resolved', ['Huisartsgeneeskunde', 'Spoedeisende hulp']);
protected readonly code: Record<string, string> = { fold: highlightTs(SNIPPETS['fold']) };
protected readonly src: Record<string, string> = { fold: 'shared/application/remote-data.ts' };
}