docs: teach overzicht, max-lines and the step contract (RD-33)
CLAUDE.md did not name the overzicht context that RD-03 created, and it did not name the max-lines budget that RD-02 enforces. An agent that follows it writes a long page into the wrong context, and meets a red build with no warning. CLAUDE.md now names the context, the @overzicht/* alias, the overzicht -> registratie arrow, the 250-line budget and its glob, and the step-component contract. layers.mdx gains the same arrow. atomic-design.mdx credited eslint.config.mjs with the layer rules. dependency-cruiser enforces them. Each tool is now named for what it does. The page also gains the layer-folder table and the step contract. Four paths were pre-monorepo: three example paths in the ui-component skill, and two citations of libs/shared/src/ui/async, which RD-27 moved to libs/shared/src/ui/molecules/async. npm run ci --full passes: 67 and 45 storybook suites, 306 axe tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,13 +5,13 @@ description: Add a shared UI building block (atom, molecule, organism) with its
|
|||||||
|
|
||||||
# UI component (atom / molecule / organism)
|
# UI component (atom / molecule / organism)
|
||||||
|
|
||||||
First: check `shared/ui/` and `shared/layout/` — a new page should be composition of
|
First: check `libs/shared/src/ui/` and `libs/shared/src/layout/` — a new page should be
|
||||||
existing blocks. Only add a block when nothing fits.
|
composition of existing blocks. Only add a block when nothing fits.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- **Folder = atomic layer**: `shared/ui/` atoms → molecules → organisms;
|
- **Folder = atomic layer**: `libs/shared/src/ui/` atoms → molecules → organisms;
|
||||||
`shared/layout/` templates. Each level only uses levels below.
|
`libs/shared/src/layout/` templates. Each level only uses levels below.
|
||||||
- Standalone component, **English name** (shared = language-agnostic), signal
|
- Standalone component, **English name** (shared = language-agnostic), signal
|
||||||
`input()`s only, `inject()` over constructor DI.
|
`input()`s only, `inject()` over constructor DI.
|
||||||
- **Atoms are thin wrappers over CIBG Huisstijl (Bootstrap 5.2) classes** (`btn`,
|
- **Atoms are thin wrappers over CIBG Huisstijl (Bootstrap 5.2) classes** (`btn`,
|
||||||
@@ -21,7 +21,8 @@ existing blocks. Only add a block when nothing fits.
|
|||||||
- **Tokens only** — `var(--rhc-*)` / `var(--app-*)`, never hardcoded colors
|
- **Tokens only** — `var(--rhc-*)` / `var(--app-*)`, never hardcoded colors
|
||||||
(`npm run check:tokens` fails the build; escape hatch: `token-ok` marker + reason).
|
(`npm run check:tokens` fails the build; escape hatch: `token-ok` marker + reason).
|
||||||
- **No hardcoded Dutch** in shared components — expose copy as `input()`s with
|
- **No hardcoded Dutch** in shared components — expose copy as `input()`s with
|
||||||
`$localize` defaults; the domain caller supplies the text (see `shared/ui/async`).
|
`$localize` defaults; the domain caller supplies the text (see
|
||||||
|
`libs/shared/src/ui/molecules/async`).
|
||||||
- Components with content-projected slots export a spread constant so callers import
|
- Components with content-projected slots export a spread constant so callers import
|
||||||
one thing: `export const ASYNC = [AsyncComponent, AsyncLoadedDirective, …] as const;`
|
one thing: `export const ASYNC = [AsyncComponent, AsyncLoadedDirective, …] as const;`
|
||||||
|
|
||||||
@@ -41,9 +42,10 @@ UI test surface.
|
|||||||
|
|
||||||
## Worked examples
|
## Worked examples
|
||||||
|
|
||||||
- Atom: `src/app/shared/ui/button/` — typed variant API over `btn` classes.
|
- Atom: `libs/shared/src/ui/atoms/button/` — typed variant API over `btn` classes.
|
||||||
- Molecule: `src/app/shared/ui/async/` — slot directives, localizable input defaults, spread constant.
|
- Molecule: `libs/shared/src/ui/molecules/async/` — slot directives, localizable input
|
||||||
- Template: `src/app/shared/layout/wizard-shell/` — the canonical wizard outline.
|
defaults, spread constant.
|
||||||
|
- Template: `libs/shared/src/layout/wizard-shell/` — the canonical wizard outline.
|
||||||
|
|
||||||
## Verify
|
## Verify
|
||||||
|
|
||||||
|
|||||||
@@ -81,10 +81,11 @@ session protocol is the worked example of this in practice.
|
|||||||
|
|
||||||
`apps/<app>/src/app/<context>/<layer>/` for an app-local context; `libs/<lib>/src/<layer>/`
|
`apps/<app>/src/app/<context>/<layer>/` for an app-local context; `libs/<lib>/src/<layer>/`
|
||||||
for a cross-app library (WP-67). Two apps today: `apps/ssp` (Zorgverlener self-service —
|
for a cross-app library (WP-67). Two apps today: `apps/ssp` (Zorgverlener self-service —
|
||||||
contexts `auth`, `registratie`, `herregistratie`, `brief` (letter-composition teaching
|
contexts `auth`, `overzicht` (the portal home; composes `registratie`'s dashboard sections
|
||||||
slice), `showcase` (teaching page, not a feature; **sanctioned** to read every context in
|
plus its own cross-context nav sections), `registratie`, `herregistratie`, `brief`
|
||||||
its own app — nothing imports it)) and `apps/behandelportal` (Behandelaar backoffice,
|
(letter-composition teaching slice), `showcase` (teaching page, not a feature; **sanctioned**
|
||||||
ADR-0002 — contexts `auth`, `behandeling`). Two cross-app libraries: `libs/shared` (the
|
to read every context in its own app — nothing imports it)) and `apps/behandelportal`
|
||||||
|
(Behandelaar backoffice, ADR-0002 — contexts `auth`, `behandeling`). Two cross-app libraries: `libs/shared` (the
|
||||||
design system + kernel + generated API client — no business logic) and `libs/beheer` (the
|
design system + kernel + generated API client — no business logic) and `libs/beheer` (the
|
||||||
admin/stamdata context, identical for both apps today — WP-67 folded a silently-diverging
|
admin/stamdata context, identical for both apps today — WP-67 folded a silently-diverging
|
||||||
duplicate copy back into one). `auth` is deliberately **not** shared even though today it's
|
duplicate copy back into one). `auth` is deliberately **not** shared even though today it's
|
||||||
@@ -106,10 +107,11 @@ depend on `libs/beheer` either — it stays the base). `ui`/`layout` never impor
|
|||||||
lint-enforced (per app, since each app is cruised against its own tsconfig — WP-67's
|
lint-enforced (per app, since each app is cruised against its own tsconfig — WP-67's
|
||||||
`.dependency-cruiser.base.js` + one thin `.dependency-cruiser.<app>.js` per app). An app
|
`.dependency-cruiser.base.js` + one thin `.dependency-cruiser.<app>.js` per app). An app
|
||||||
may not import the other app's source directly. Cross-context only
|
may not import the other app's source directly. Cross-context only
|
||||||
`herregistratie → registratie → libs/shared|beheer`, `auth → libs/shared|beheer`,
|
`overzicht → registratie → libs/shared|beheer`, `herregistratie → registratie →
|
||||||
`brief → libs/shared|beheer` (ssp); `behandeling → libs/shared|beheer`, `auth →
|
libs/shared|beheer`, `auth → libs/shared|beheer`, `brief → libs/shared|beheer` (ssp);
|
||||||
libs/shared|beheer` (behandelportal). Imports use aliases as direction statements:
|
`behandeling → libs/shared|beheer`, `auth → libs/shared|beheer` (behandelportal). Imports use
|
||||||
`@shared/* @beheer/* @auth/* @registratie/* @herregistratie/* @brief/*` (ssp) —
|
aliases as direction statements:
|
||||||
|
`@shared/* @beheer/* @auth/* @overzicht/* @registratie/* @herregistratie/* @brief/*` (ssp) —
|
||||||
`@shared/* @beheer/* @auth/* @behandeling/*` (behandelportal); each app's own
|
`@shared/* @beheer/* @auth/* @behandeling/*` (behandelportal); each app's own
|
||||||
`tsconfig.json` declares its full map (the root `tsconfig.json` intentionally has no
|
`tsconfig.json` declares its full map (the root `tsconfig.json` intentionally has no
|
||||||
`paths` — see its comment). `domain/` imports nothing from Angular.
|
`paths` — see its comment). `domain/` imports nothing from Angular.
|
||||||
@@ -129,6 +131,18 @@ the design system does the visuals. (Where CIBG lacks a class — e.g. `skeleton
|
|||||||
`// CIBG-GAP EXTENSION:` marker; see ADR-0003. `alert` is **not** such a case: it wraps the
|
`// CIBG-GAP EXTENSION:` marker; see ADR-0003. `alert` is **not** such a case: it wraps the
|
||||||
vendored `.feedback feedback-*` classes.)
|
vendored `.feedback feedback-*` classes.)
|
||||||
|
|
||||||
|
**The step-component contract.** A wizard step follows the same rule as
|
||||||
|
`address-fields.component.ts`: values in, events out, no internal state. Three clauses:
|
||||||
|
|
||||||
|
1. **Inputs down.** A step reads its data only from `input()`s the container passes it.
|
||||||
|
2. **One narrow output up.** A step emits one specific event, not the container's whole
|
||||||
|
`dispatch`.
|
||||||
|
3. **`dispatch` is never passed down.** The container owns the Model and decides what a
|
||||||
|
step's event means; a step never calls `dispatch` itself.
|
||||||
|
|
||||||
|
Corollary: a step gets **no** story of its own. The wizard's own story already mounts every
|
||||||
|
step, because it seeds the machine.
|
||||||
|
|
||||||
### 3. State: make illegal states unrepresentable
|
### 3. State: make illegal states unrepresentable
|
||||||
|
|
||||||
Default reflex — **if you're about to add a second/third boolean to track state,
|
Default reflex — **if you're about to add a second/third boolean to track state,
|
||||||
@@ -136,7 +150,7 @@ model a discriminated union instead.** Three tools, all in `libs/shared/src/appl
|
|||||||
|
|
||||||
- **`RemoteData<E,T>`** (`remote-data.ts`) — `Loading | Empty | Failure{error} | Success{value}`.
|
- **`RemoteData<E,T>`** (`remote-data.ts`) — `Loading | Empty | Failure{error} | Success{value}`.
|
||||||
Combine sources with `map`/`map2`/`andThen` (Failure > Loading > Success).
|
Combine sources with `map`/`map2`/`andThen` (Failure > Loading > Success).
|
||||||
Render it via the `<app-async>` molecule (`libs/shared/src/ui/async`) — one of four
|
Render it via the `<app-async>` molecule (`libs/shared/src/ui/molecules/async`) — one of four
|
||||||
templates, mutually exclusive by construction. Default loading spinner/skeleton
|
templates, mutually exclusive by construction. Default loading spinner/skeleton
|
||||||
is delay-gated (~250ms) so fast connections don't flash.
|
is delay-gated (~250ms) so fast connections don't flash.
|
||||||
- **Elm-style store** (`store.ts` → `createStore(initial, reduce)`) — all state in
|
- **Elm-style store** (`store.ts` → `createStore(initial, reduce)`) — all state in
|
||||||
@@ -242,7 +256,7 @@ organism doesn't get its own `Organisms/` bucket).
|
|||||||
(`` $localize`:@@context.key:Tekst` ``). Source locale is `nl`; a second locale is a
|
(`` $localize`:@@context.key:Tekst` ``). Source locale is `nl`; a second locale is a
|
||||||
translation file, not a code change (the seam). Shared/English components must **not**
|
translation file, not a code change (the seam). Shared/English components must **not**
|
||||||
hardcode Dutch — expose copy as `input()`s with localizable defaults; the domain caller
|
hardcode Dutch — expose copy as `input()`s with localizable defaults; the domain caller
|
||||||
supplies the text (see `libs/shared/src/ui/async`). Format-validation messages in
|
supplies the text (see `libs/shared/src/ui/molecules/async`). Format-validation messages in
|
||||||
`domain/value-objects/` stay co-located but are still `$localize`-wrapped.
|
`domain/value-objects/` stay co-located but are still `$localize`-wrapped.
|
||||||
- **Forms = one idiom.** Any form with validation or submission uses a `*.machine.ts`
|
- **Forms = one idiom.** Any form with validation or submission uses a `*.machine.ts`
|
||||||
(Model/Msg/reduce) + value objects + a `submit-*` command returning `Result` — the
|
(Model/Msg/reduce) + value objects + a `submit-*` command returning `Result` — the
|
||||||
@@ -272,7 +286,10 @@ organism doesn't get its own `Organisms/` bucket).
|
|||||||
- Prettier; `.editorconfig`. tsconfig: `noImplicitReturns`,
|
- Prettier; `.editorconfig`. tsconfig: `noImplicitReturns`,
|
||||||
`noPropertyAccessFromIndexSignature`, `noFallthroughCasesInSwitch`, `isolatedModules`.
|
`noPropertyAccessFromIndexSignature`, `noFallthroughCasesInSwitch`, `isolatedModules`.
|
||||||
- **Enforced, not just hoped-for:** `npm run lint` (`eslint.config.mjs`, scoped to
|
- **Enforced, not just hoped-for:** `npm run lint` (`eslint.config.mjs`, scoped to
|
||||||
`{apps,libs}/**`) fails the build on `any`; `npm run dep:check`
|
`{apps,libs}/**`) fails the build on `any`; the same config's `max-lines` rule caps every
|
||||||
|
`{apps,libs}/**/*.{page,component,section,step}.ts` file at 250 lines
|
||||||
|
(`skipBlankLines: true`, `skipComments: true`). 250 is reachable, not a style-guide
|
||||||
|
default — the dashboard page lands at 42 lines. `npm run dep:check`
|
||||||
(`.dependency-cruiser.base.js` + one `.dependency-cruiser.<app>.js` per app, WP-67) fails
|
(`.dependency-cruiser.base.js` + one `.dependency-cruiser.<app>.js` per app, WP-67) fails
|
||||||
on illegal imports — `domain/` importing Angular, a context importing "upward" (the
|
on illegal imports — `domain/` importing Angular, a context importing "upward" (the
|
||||||
`herregistratie → registratie → shared`, `auth → shared` direction), an app importing the
|
`herregistratie → registratie → shared`, `auth → shared` direction), an app importing the
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# RD-33 — CLAUDE.md, `atomic-design.mdx` and the `ui-component` skill
|
||||||
|
|
||||||
|
Status: done
|
||||||
|
Phase: 5 — fix the docs that describe this flow
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
This arc added a context, a lint rule and a component contract. Three agent-facing documents
|
||||||
|
still describe the app as it was before.
|
||||||
|
|
||||||
|
CLAUDE.md is the file every agent reads first. It does not name the `overzicht` context that
|
||||||
|
RD-03 created, and it does not name the `max-lines` budget that RD-02 enforces. An agent that
|
||||||
|
follows it writes a 400-line page into the wrong context and meets a red build it was never
|
||||||
|
warned about.
|
||||||
|
|
||||||
|
`atomic-design.mdx` credits `eslint.config.mjs` with the layer rules. Dependency-cruiser
|
||||||
|
enforces them. Crediting the wrong tool sends the next reader to the wrong file.
|
||||||
|
|
||||||
|
## Read first
|
||||||
|
|
||||||
|
- `docs/project/readable-codebase/PLAN.md` §3c (line 604, the step contract), §4a (line 721,
|
||||||
|
the layer move) and phase 5 items 4 and 5 (line 837).
|
||||||
|
- `CLAUDE.md` — sections "1. DDD", "2. Atomic design", "5. Testing" and "Enforced, not just
|
||||||
|
hoped-for".
|
||||||
|
- `libs/shared/docs/atomic-design.mdx:60`, `libs/shared/docs/layers.mdx:41-45`.
|
||||||
|
- `.claude/skills/ui-component/SKILL.md`.
|
||||||
|
|
||||||
|
## Decisions (pre-made, do not relitigate)
|
||||||
|
|
||||||
|
1. **CLAUDE.md gains `overzicht` in three places.** The context list in section 1, the alias
|
||||||
|
list (`@overzicht/*`, which `apps/ssp/tsconfig.json:9` already declares), and the
|
||||||
|
cross-context arrow. The rule in `.dependency-cruiser.ssp.js:8` is `overzicht: ['registratie']`,
|
||||||
|
so the arrow is `overzicht → registratie → libs/shared|beheer`.
|
||||||
|
2. **CLAUDE.md gains the `max-lines` budget** under "Enforced, not just hoped-for", stated as
|
||||||
|
the rule states it: 250 lines, `skipBlankLines` and `skipComments`, over
|
||||||
|
`{apps,libs}/**/*.{page,component,section,step}.ts` (`eslint.config.mjs:64-68`). Say that
|
||||||
|
250 is reachable, not a style-guide default — the dashboard page is 42 lines.
|
||||||
|
3. **CLAUDE.md gains the step-component contract**, in section 2 beside the atomic rules. Three
|
||||||
|
clauses, from PLAN §3c: inputs down, one narrow output up, and `dispatch` is never passed
|
||||||
|
down. Add the corollary: a step gets **no** story of its own, because the wizard's story
|
||||||
|
already mounts it by seeding the machine.
|
||||||
|
4. **The `libs/beheer` title rule is already in CLAUDE.md.** RD-28 landed it. Verify the
|
||||||
|
sentence is there and correct; do not write it a second time.
|
||||||
|
5. **`atomic-design.mdx:60` names the wrong tool.** The layer rules are dependency-cruiser
|
||||||
|
(`npm run dep:check`, `.dependency-cruiser.base.js` plus one file per app). ESLint owns the
|
||||||
|
`any` ban and `max-lines`. Name each tool for what it actually does.
|
||||||
|
6. **`atomic-design.mdx` gains the step contract and the layer table**, so the Storybook reader
|
||||||
|
and the CLAUDE.md reader get the same rule. The table is the shape RD-27 produced:
|
||||||
|
`libs/shared/src/ui/atoms|molecules|organisms/` and `libs/shared/src/layout/`, with the note
|
||||||
|
that `layout/` deliberately holds several layers.
|
||||||
|
7. **`layers.mdx` is missing the `overzicht` arrow.** Its list at lines 41-45 predates RD-03.
|
||||||
|
Add the same arrow as decision 1. This is one line, in the same commit, because the two
|
||||||
|
documents state the same rule and must not disagree.
|
||||||
|
8. **The `ui-component` skill's example paths are pre-monorepo.** Lines 44-46 read
|
||||||
|
`src/app/shared/ui/button/`, `src/app/shared/ui/async/` and `src/app/shared/layout/wizard-shell/`.
|
||||||
|
They are now `libs/shared/src/ui/atoms/button/`, `libs/shared/src/ui/molecules/async/` and
|
||||||
|
`libs/shared/src/layout/wizard-shell/`. Lines 8, 13 and 24 say `shared/ui/` where the folder
|
||||||
|
is now the layer — name the layer folder, because that is the whole point of RD-27.
|
||||||
|
9. **CLAUDE.md:153 and CLAUDE.md:259 carry the same stale-path defect.** Both cite
|
||||||
|
`libs/shared/src/ui/async`, which RD-27 moved to `libs/shared/src/ui/molecules/async`. Fix
|
||||||
|
both in this commit, alongside decision 8's identical fix in the skill file.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `CLAUDE.md`
|
||||||
|
- `libs/shared/docs/atomic-design.mdx`
|
||||||
|
- `libs/shared/docs/layers.mdx`
|
||||||
|
- `.claude/skills/ui-component/SKILL.md`
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. CLAUDE.md: decisions 1, 2, 3, and the check in decision 4.
|
||||||
|
2. `atomic-design.mdx`: decisions 5 and 6.
|
||||||
|
3. `layers.mdx`: decision 7.
|
||||||
|
4. `.claude/skills/ui-component/SKILL.md`: decision 8.
|
||||||
|
5. `npm run ci --full`.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] `grep -n overzicht CLAUDE.md` returns the context, the alias and the arrow.
|
||||||
|
- [x] `grep -n max-lines CLAUDE.md` returns the budget, with the glob and the two skip options.
|
||||||
|
- [x] CLAUDE.md states the step contract and the "no story for a step" corollary.
|
||||||
|
- [x] No document credits `eslint.config.mjs` with the layer rules.
|
||||||
|
- [x] Every path in `.claude/skills/ui-component/SKILL.md` resolves.
|
||||||
|
- [x] `npm run ci --full` is green.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. `npm run ci --full`. This ticket edits two `.mdx` files, and only `build-storybook` catches
|
||||||
|
a broken MDX import. Plain `npm run ci` does not build Storybook.
|
||||||
|
2. `npm run storybook` — open Foundations, Atomic design and Layers. Both pages render, and the
|
||||||
|
new table is readable at a narrow width.
|
||||||
|
3. `grep -oE '\`[^\`]_(apps|libs)/[^\`]_\`' .claude/skills/ui-component/SKILL.md | tr -d '\`' |
|
||||||
|
xargs ls -d` — every path resolves.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- `ARCHITECTURE.md` (RD-31) and `fp-tea-atomic-design.md` (RD-32).
|
||||||
|
- The other skills in `.claude/skills/`. Only `ui-component` carries stale layer paths.
|
||||||
|
- Renaming `ui/dashboard/`. RD-36 owns that, and it runs after this ticket.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
1. **A broken MDX import passes `npm run ci` and fails CI.** This is PLAN risk 7. Run `--full`.
|
||||||
|
2. **CLAUDE.md is long, and a rule added in the wrong section is a rule nobody finds.** Put
|
||||||
|
each addition in the section that already owns its subject: contexts in 1, the step contract
|
||||||
|
in 2, the lint budget in "Enforced, not just hoped-for".
|
||||||
|
3. **Two documents, one rule.** `layers.mdx` and CLAUDE.md both state the cross-context arrows.
|
||||||
|
Write the same arrow in both, or the next reader gets to choose which one to believe.
|
||||||
@@ -127,7 +127,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di
|
|||||||
| RD-30 | Archive the finished backlogs (16,300 lines) + an archive README | 01 | | done |
|
| RD-30 | Archive the finished backlogs (16,300 lines) + an archive README | 01 | | done |
|
||||||
| RD-31 | `ARCHITECTURE.md` section 6a: symbols not lines, 2 dead paths, new names | 03, 08, 16 | | done |
|
| RD-31 | `ARCHITECTURE.md` section 6a: symbols not lines, 2 dead paths, new names | 03, 08, 16 | | done |
|
||||||
| RD-32 | `fp-tea-atomic-design.md`: 11 broken paths + the broken anchor | 27 | | done |
|
| RD-32 | `fp-tea-atomic-design.md`: 11 broken paths + the broken anchor | 27 | | done |
|
||||||
| RD-33 | CLAUDE.md + `atomic-design.mdx` + the `ui-component` skill | 03, 27, 29 | yes | todo |
|
| RD-33 | CLAUDE.md + `atomic-design.mdx` + the `ui-component` skill | 03, 27, 29 | yes | done |
|
||||||
| RD-34 | _(optional)_ `NO_SUBORGS`/`NO_TABLES` become `RemoteData.Empty` | 11 | | todo |
|
| RD-34 | _(optional)_ `NO_SUBORGS`/`NO_TABLES` become `RemoteData.Empty` | 11 | | todo |
|
||||||
| RD-35 | _(optional, last, alone)_ upload `type:` discriminant to `tag:` | 27 | | todo |
|
| RD-35 | _(optional, last, alone)_ upload `type:` discriminant to `tag:` | 27 | | todo |
|
||||||
| RD-36 | `ui/dashboard/` → `ui/overzicht-secties/` + 2 stale `dashboard.page` paths | 04 | yes | todo |
|
| RD-36 | `ui/dashboard/` → `ui/overzicht-secties/` + 2 stale `dashboard.page` paths | 04 | yes | todo |
|
||||||
|
|||||||
@@ -57,9 +57,36 @@ atoms — that is the whole point: fewer things to understand, nothing bespoke p
|
|||||||
## The rule, enforced
|
## The rule, enforced
|
||||||
|
|
||||||
**Each layer only uses layers below it, and dependencies point inward.** This is not a
|
**Each layer only uses layers below it, and dependencies point inward.** This is not a
|
||||||
convention you have to remember — `eslint.config.mjs` fails the build if `domain/` imports
|
convention you have to remember — `npm run dep:check` (dependency-cruiser) fails the build if
|
||||||
Angular, or if a context imports "upward". See [the FP-in-the-UI primer](?path=/docs/foundations-fp-in-the-ui--docs)
|
`domain/` imports Angular, or if a context imports "upward". `eslint.config.mjs` enforces a
|
||||||
for how the same discipline shapes state and effects.
|
different rule: the `any` ban, and a `max-lines` budget (250 lines, `skipBlankLines`,
|
||||||
|
`skipComments`) on every `{apps,libs}/**/*.{page,component,section,step}.ts` file — reachable,
|
||||||
|
not a style-guide default, since the dashboard page lands at 42 lines. See
|
||||||
|
[the FP-in-the-UI primer](?path=/docs/foundations-fp-in-the-ui--docs) for how the same
|
||||||
|
discipline shapes state and effects.
|
||||||
|
|
||||||
|
## The layer folders
|
||||||
|
|
||||||
|
| Layer | Where |
|
||||||
|
| --------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Atoms | `libs/shared/src/ui/atoms/` |
|
||||||
|
| Molecules | `libs/shared/src/ui/molecules/` |
|
||||||
|
| Organisms | `libs/shared/src/ui/organisms/` |
|
||||||
|
| Templates | `libs/shared/src/layout/` — deliberately holds several layers; its own organisms are chrome that only its own templates use |
|
||||||
|
|
||||||
|
## The step-component contract
|
||||||
|
|
||||||
|
A wizard step follows the same rule as `address-fields.component.ts`: values in, events
|
||||||
|
out, no internal state.
|
||||||
|
|
||||||
|
1. **Inputs down.** A step reads its data only from `input()`s the container passes it.
|
||||||
|
2. **One narrow output up.** A step emits one specific event, not the container's whole
|
||||||
|
`dispatch`.
|
||||||
|
3. **`dispatch` is never passed down.** The container owns the Model and decides what a
|
||||||
|
step's event means; a step never calls `dispatch` itself.
|
||||||
|
|
||||||
|
Corollary: a step gets **no** story of its own. The wizard's own story already mounts every
|
||||||
|
step, because it seeds the machine.
|
||||||
|
|
||||||
## A composition chain, live
|
## A composition chain, live
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ different login flows, so the two copies are expected to diverge.
|
|||||||
**Dependencies only point inward, in one declared direction between contexts:**
|
**Dependencies only point inward, in one declared direction between contexts:**
|
||||||
|
|
||||||
```
|
```
|
||||||
|
overzicht → registratie → libs/shared|beheer (ssp)
|
||||||
herregistratie → registratie → libs/shared|beheer (ssp)
|
herregistratie → registratie → libs/shared|beheer (ssp)
|
||||||
auth → libs/shared|beheer (ssp)
|
auth → libs/shared|beheer (ssp)
|
||||||
brief → libs/shared|beheer (ssp)
|
brief → libs/shared|beheer (ssp)
|
||||||
|
|||||||
Reference in New Issue
Block a user