fold-ng 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,69 @@
1
+ # Changelog
2
+
3
+ All notable changes to **fold-ng** are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/); the project follows
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ _Nothing yet._
10
+
11
+ ## [0.2.0] - 2026-07-24
12
+
13
+ ### Added
14
+
15
+ - **`fold-app-shell` — the mobile drawer is now a real modal.** While open, the
16
+ off-canvas primary rail is a named `role="dialog"` + `aria-modal="true"` (new
17
+ `drawerLabel` input, default `"Menu"`), and every other region is `inert` so a
18
+ screen reader can't wander behind it. New `drawerId` — exposed via
19
+ `exportAs="foldAppShell"` — lets the app point its hamburger's `aria-controls`
20
+ at the drawer (see the trigger contract in the component docs).
21
+
22
+ ### Fixed
23
+
24
+ - **`fold-app-shell` skip-link** moves focus to `<main>` directly and prevents
25
+ the fragment navigation, so it works under hash routing (`withHashLocation`),
26
+ where a `#id` jump would otherwise be treated as a route change.
27
+
28
+ ### Changed
29
+
30
+ - **`FoldComponentPanelDescriptor.ariaLabel`** is now typed `string | undefined`
31
+ (was `string`) — a non-breaking widening; the descriptor always carries the key.
32
+ - **Internals hardened**, no API change: the library now compiles under
33
+ `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `strictStandalone`,
34
+ `typeCheckHostBindings`, and the full `strictTypeChecked` ESLint preset.
35
+
36
+ ## [0.1.0] - 2026-07-24
37
+
38
+ First public release line — **production-quality, pre-1.0 (`0.x`)**: fully
39
+ tested and dogfooded in a real application, but the component API may still be
40
+ refined before `1.0.0`. Consumed as standalone Angular 22 components plus a
41
+ design-token stylesheet.
42
+
43
+ > **1.0.0 is held** until every component in
44
+ > [`docs/RELEASE-READINESS.md`](./docs/RELEASE-READINESS.md) is 🟢🟢🟢. Until
45
+ > then `latest` tracks the newest `0.x` (pin your version — treat `0.x` minor
46
+ > bumps as potentially breaking); throwaway pre-release cuts ship on the `beta`
47
+ > dist-tag.
48
+
49
+ ### Added
50
+
51
+ - **Design tokens** — two-tier model (primitives → semantic), four themes
52
+ (`umbra` dark default, `lumen`, `bubbly`, `navi`) switched via `data-theme`, a
53
+ typed catalogue + `foldColorVar()` helper, and a contract test locking theme
54
+ parity.
55
+ - **Actions** — `[foldButton]` (applied to a real `<button>`/`<a>`; orthogonal
56
+ `emphasis` × `intent`; `loading`; forced-colors), `fold-button-icon`,
57
+ `fold-toggle-icon`, `fold-link`, `fold-spinner`.
58
+ - **Overlays** — `fold-panel-host` / `fold-panel-header`: modal side panels with
59
+ an accessible name, an `inert` background barrier, a top-most focus trap,
60
+ scroll-lock, and a typed imperative `open<TData, TResult>()` → `FoldPanelRef`.
61
+ - **Layout / Navigation / Content / Feedback / Forms / Foundations** — see the
62
+ component table in the README.
63
+ - **Accessibility** — overlays honour the full modal contract; icons inherit
64
+ `currentColor`; `prefers-reduced-motion` + `forced-colors` are respected;
65
+ strings localise via inputs / providers (`provideFoldPanelLabels`).
66
+
67
+ [unreleased]: https://github.com/hugoheynard/fold-ng/compare/v0.2.0...HEAD
68
+ [0.2.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.2.0
69
+ [0.1.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,9 +1,366 @@
1
1
  # fold-ng
2
2
 
3
- A **dark-first Angular design system** — signals-first, standalone, zoneless, SSR-ready.
3
+ [![npm version](https://img.shields.io/npm/v/fold-ng.svg)](https://www.npmjs.com/package/fold-ng)
4
+ [![npm downloads](https://img.shields.io/npm/dm/fold-ng.svg)](https://www.npmjs.com/package/fold-ng)
5
+ [![minzipped size](https://img.shields.io/bundlephobia/minzip/fold-ng)](https://bundlephobia.com/package/fold-ng)
6
+ [![types included](https://img.shields.io/npm/types/fold-ng.svg)](https://www.npmjs.com/package/fold-ng)
7
+ [![license](https://img.shields.io/npm/l/fold-ng.svg)](./LICENSE)
8
+ [![status: pre-1.0](https://img.shields.io/badge/status-pre--1.0-2ea043)](./CHANGELOG.md)
9
+ [![live demo](https://img.shields.io/badge/live_demo-gallery-6f42c1)](https://hugoheynard.github.io/fold-ng/)
4
10
 
5
- > 🚧 **Coming soon.** This name is reserved. The `0.0.0` placeholder ships nothing usable yet; the real release is on its way.
11
+ **fold-ng** is an accessible, **dark-first Angular 22 UI component library and
12
+ design system** — signals-first, standalone, zoneless, and SSR-ready. It ships a
13
+ two-tier **design-token** model (themeable to the bone) plus WCAG-minded
14
+ components: buttons, forms, overlays/dialogs, navigation, data tables, toasts and
15
+ more. No `NgModule`, no `zone.js`, no runtime CSS-in-JS — just standalone
16
+ components styled against CSS variables.
6
17
 
7
- ## License
18
+ ### ▶ [Live demo & component gallery](https://hugoheynard.github.io/fold-ng/)
8
19
 
9
- [MIT](./LICENSE) © 2026 Hugo Heynard
20
+ Every component, driven by a live playground — the fastest way to see what
21
+ `fold-ng` looks and behaves like.
22
+
23
+ > **Production-quality, pre-1.0 (`0.x`).** Every component is tested, the package
24
+ > builds AOT green, and it's dogfooded as the design system of a real
25
+ > application. It stays `0.x` only because the API isn't frozen until `1.0.0` —
26
+ > not because it's unstable. **Pin your version**; `0.x` minor bumps may still
27
+ > refine the API. See [`CHANGELOG.md`](./CHANGELOG.md) for the road to 1.0.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ npm install fold-ng
33
+ ```
34
+
35
+ Angular 22 (`@angular/core`, `@angular/common`, `@angular/forms`,
36
+ `@angular/platform-browser`) is a peer dependency.
37
+
38
+ ## Quickstart
39
+
40
+ Import the tokens once, then use any standalone component directly:
41
+
42
+ ```ts
43
+ // styles.css
44
+ @import "fold-ng/tokens.css";
45
+ ```
46
+
47
+ ```ts
48
+ import { Component } from "@angular/core";
49
+ import { FoldButtonComponent, FoldCardComponent } from "fold-ng";
50
+
51
+ @Component({
52
+ standalone: true,
53
+ imports: [FoldButtonComponent, FoldCardComponent],
54
+ template: `
55
+ <fold-card>
56
+ <button foldButton emphasis="solid" (click)="save()">Save</button>
57
+ <a foldButton emphasis="outline" intent="neutral" routerLink="/back">
58
+ Cancel
59
+ </a>
60
+ </fold-card>
61
+ `,
62
+ })
63
+ export class DemoComponent {
64
+ save() {}
65
+ }
66
+ ```
67
+
68
+ Everything is dark by default; opt into light with `data-theme="lumen"`. See
69
+ [`docs/TODO.md`](./docs/TODO.md) for the roadmap and the full component list below.
70
+
71
+ ## Consuming the tokens
72
+
73
+ Import the CSS once at your app's style entry point:
74
+
75
+ ```css
76
+ @import "fold-ng/tokens.css";
77
+ ```
78
+
79
+ Everything renders **umbra** (the dark theme) **by default**. To switch a subtree (or the whole app),
80
+ set `data-theme` on an ancestor — usually `<html>`:
81
+
82
+ ```html
83
+ <html data-theme="lumen"></html>
84
+ ```
85
+
86
+ Four themes ship: `umbra` (the dark base, no attribute), `lumen` (light),
87
+ `bubbly`
88
+ (festive lavender, violet brand, rounded) and `navi` (dark chrome, light page).
89
+ The extras exist to prove the point — each is the umbra or lumen block with its
90
+ primitive families re-pointed. Adding a fifth is a new `[data-theme]` block in
91
+ `semantic.css` plus the primitives it names.
92
+
93
+ `bubbly` and `navi` also change their corners — **radius is the one scale a theme may
94
+ re-declare**, because corner softness is a brand axis (friendly vs
95
+ institutional) and it is the only scale that changes nothing about where a box
96
+ sits. Type, space, motion and elevation stay theme-invariant: retheming must
97
+ never re-flow a page. The contract test enforces that split.
98
+
99
+ `navi` is the interesting one: mixing chrome and page means one theme needs the
100
+ `text`/`border`/`surface` roles to differ **per region**, which a single set of
101
+ roles cannot express. It gets there by re-declaring those roles on
102
+ `[data-surface="chrome"]` — a contract any element opts into with the
103
+ `foldSurface` directive (the app-shell stamps it on its rails + header) — so the
104
+ theme never names a component's internals. Variables only. If mixed chrome ever
105
+ stops being a demo, the catalogue should grow real `*-on-chrome` roles.
106
+
107
+ Then style against the semantic tokens — never a raw colour:
108
+
109
+ ```css
110
+ .header {
111
+ background: var(--fold-color-bg-header);
112
+ }
113
+ .cta {
114
+ background: var(--fold-color-primary);
115
+ color: var(--fold-color-on-primary);
116
+ }
117
+ .cta:hover {
118
+ background: var(--fold-color-primary-strong);
119
+ }
120
+ ```
121
+
122
+ From TypeScript you get the same tokens, typed:
123
+
124
+ ```ts
125
+ import { foldColorVar } from "fold-ng";
126
+
127
+ el.style.background = foldColorVar("bg-page"); // "var(--fold-color-bg-page)"
128
+ foldColorVar("bg-pag"); // ✗ compile error — misspelt token
129
+ ```
130
+
131
+ ## The two-tier model
132
+
133
+ Tokens come in two layers. This separation is the whole point — it is what lets
134
+ another project re-theme by swapping the palette, and what keeps the app from
135
+ hard-coding colours.
136
+
137
+ | Tier | File | Prefix | Role |
138
+ | ------------------ | ---------------- | ---------------- | --------------------------------------------------------------------------- |
139
+ | **1 · Primitives** | `primitives.css` | `--fold-ref-*` | The raw palette. The only place a literal hex is allowed. Theme-invariant. |
140
+ | **2 · Semantic** | `semantic.css` | `--fold-color-*` | Role tokens (`bg-header`, `primary`…). Point at primitives. Flip per theme. |
141
+
142
+ **Components consume tier 2 only.** A component never names `--fold-ref-teal-500`;
143
+ it names `--fold-color-primary`. Re-theming means re-pointing the semantic layer,
144
+ never touching a component.
145
+
146
+ ## The naming convention
147
+
148
+ ```
149
+ --fold-<tier>-<category>-<role>[-<variant>]
150
+ │ │ │ └─ strong · primary · secondary · tertiary …
151
+ │ │ └────────── page · header · rail · primary …
152
+ │ └───────────────────── color · (space, radius, text … to come)
153
+ └───────────────────────────── ref (primitive) · color (semantic)
154
+ ```
155
+
156
+ Namespaced with `--fold-` so the package never collides with an app's own tokens.
157
+
158
+ **The `bg-` rule.** A surface fill role carries a `bg-` marker (`bg-page`,
159
+ `bg-rail-primary`); a foreground/brand role does not (`primary`, `on-primary`).
160
+ So `bg-` reads as "this paints a background."
161
+
162
+ ## The contract test
163
+
164
+ `src/tokens/__tests__/tokens.contract.spec.ts` is the lock. It fails the build
165
+ if:
166
+
167
+ - any theme block falls out of parity with the catalogue (the dark `:root` base
168
+ and **every** `[data-theme]` override are checked);
169
+ - a semantic token points at a primitive that doesn't exist (dangling `var`);
170
+ - a semantic token hard-codes a hex instead of referencing a primitive;
171
+ - a primitive is declared but never used;
172
+ - the CSS drifts from the typed catalogue in `tokens.catalog.ts`.
173
+
174
+ Add a token → add it to `tokens.catalog.ts` **and** every theme block, or the
175
+ test goes red. That is how a theme stays complete.
176
+
177
+ ```bash
178
+ pnpm --filter fold-ng test
179
+ ```
180
+
181
+ ## Current token set
182
+
183
+ Deliberately small — we grow it together, one confirmed role at a time.
184
+
185
+ | Semantic token | Role |
186
+ | -------------------------------- | ----------------------------- |
187
+ | `--fold-color-bg-page` | Page background |
188
+ | `--fold-color-bg-header` | Top header bar |
189
+ | `--fold-color-bg-rail-primary` | Rail 1 — app menu |
190
+ | `--fold-color-bg-rail-secondary` | Rail 2 — workspace menu |
191
+ | `--fold-color-bg-rail-tertiary` | Rail 3 — tertiary nav |
192
+ | `--fold-color-primary` | Primary / accent (brand teal) |
193
+ | `--fold-color-primary-strong` | Primary hover / active |
194
+ | `--fold-color-on-primary` | Text / icon on a primary fill |
195
+
196
+ (plus the status families, neutral surfaces, the two card tints `surface-card` /
197
+ `surface-sunken`, glass, and the
198
+ radius / text / icon-size / space / motion / blur scales — see
199
+ `tokens.catalog.ts` for the full, typed set.)
200
+
201
+ ## Components
202
+
203
+ All standalone, signals-first, styled against the semantic tokens. Import from
204
+ the package root.
205
+
206
+ | Component | Selector | What it is |
207
+ | -------------------------------------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
208
+ | `FoldAppShellComponent` | `fold-app-shell` | Responsive app skeleton (rails + header + content + self-collapsing `footer` slots; `headerLayout`/`footerLayout` inset·full, `footerBehavior` pinned·scroll; `mobileNav` drawer·none — `[(mobileNavOpen)]` off-canvas drawer for the primary rail on mobile, or `none` to compose an `fold-nav-launcher`; `contentScroll` clip·auto; built-in skip-link to a focusable `<main>`. Regions float per-surface via `foldElevated`, not a shell flag). |
209
+ | `FoldMenuComponent` (+ `Item` / `Section` / `Separator`) | `fold-menu` | Collapsible nav rail — coloured sections, `tint="follow"`, depth `level`, collapse-toggle placement. Items are `a[fold-menu-item]`. |
210
+ | `FoldNavLauncherComponent` (+ `FoldNavTileComponent`) | `fold-nav-launcher` | Full-screen mobile nav launcher — a centred tile grid over a blurred scrim (scrim / `Escape` / close dismissal, focus-trap, scroll-lock). `columns="auto"` scales tiles to the count. Pairs with `fold-app-shell mobileNav="none"`. Tiles are `a[fold-nav-tile]` — `variant="surface"`·`filled`. |
211
+ | `FoldPageLayoutComponent` | `fold-page-layout` | Page scaffold — gutter + header + body rhythm; fills its container (width is a content concern). Tokens `--fold-page-gutter` / `--fold-page-gap`; sections can `bleed` edge-to-edge. |
212
+ | `FoldPageSectionComponent` | `fold-page-section` | Semantic `<section>` grouping — eyebrow title (names the region via `aria-labelledby`) + description + actions; `stack` / `bleed` helpers. Not a box — compose a `fold-card` inside for that. |
213
+ | `FoldHeroSectionComponent` | `fold-hero-section` | Full-bleed page splash — the borderless intro band at the top of a page (carries the `<h1>`). Direct child of `fold-page-layout`: cancels the gutter + top pad to sit flush, brand-tinted wash + hairline. `align` center·start, `wash`, `[heroBackdrop]` decorative lane. (For a bordered header card, see `fold-hero-card`.) |
214
+ | `FoldAsideLayoutComponent` | `fold-aside-layout` | Detail-page grid — a centred column flanked by up to two sticky rails (`[asideLeft]` / `[asideRight]`), collapsing to one column on its own container width (`:has()`-driven, container queries). Labelled rails become `complementary` landmarks; every track is a CSS var. |
215
+ | `FoldTabLayoutComponent` | `fold-tab-layout` | Pairs a tab bar (`[tabNav]`) with its content — `placement="top"` or a `side` rail that folds back on top (hysteretic, on its own width) below `foldAt`. `exportAs="foldTabLayout"` exposes `stacked()` so the projected nav follows in one binding. |
216
+ | `FoldCardComponent` | `fold-card` | Raised content surface (`surface-card` + consistent radius). |
217
+ | `FoldContextCardComponent` | `fold-context-card` | Titled info card: icon header + body + optional footer action. |
218
+ | `FoldHeroCardComponent` | `fold-hero-card` | Prominent header **card** — bordered surface × accent overlay + optional accent bar. (For a full-bleed page splash, see `fold-hero-section`.) |
219
+ | `FoldElementTitleComponent` | `fold-element-title` | Uppercase section/card mini-title (eyebrow · bar variants). |
220
+ | `FoldFieldListComponent` / `…Field` | `fold-field-list` | Read-only `dl/dt/dd` recap — label/value pairs (`[empty]` placeholder). The _display_ half of a record; `fold-input` is the _edit_ half. |
221
+ | `FoldInputComponent` | `fold-input` | Text-input control (`value: string`) — Signal Forms (`[formField]`) or standalone `[(value)]`; size × align × variant, `label` / `required` / `hint`. The _edit_ half of a record (`fold-field` reads). |
222
+ | `FoldNumberInputComponent` | `fold-number-input` | Numeric sibling of `fold-input` (`value: number \| null`, empty ⇒ `null`); owns `min` / `max` / `step` + `label` / `required` / `hint`. Split so each control keeps its true type. |
223
+ | `FoldSearchComponent` | `fold-search` | Debounced search box — an `fold-input` that emits `searchChange` once typing settles (`delayMs`), trimmed + de-duplicated. |
224
+ | `FoldSliderComponent` | `fold-slider` | Single-value range slider — a styled native `<input type="range">` (design-system track/fill/thumb), two-way `[(value)]`. |
225
+ | `FoldRangeSliderComponent` | `fold-range-slider` | Dual-thumb slider selecting a `{ min, max }` window over `[min, max]`; shares the slider track/fill/thumb. |
226
+ | `FoldFileDropzoneComponent` | `fold-file-dropzone` | File-picker dropzone — drag-over visuals, keyboard activation, hidden `<input type=file>` plumbing; emits the picked `File[]` (presentational — never uploads). |
227
+ | `FoldLinkComponent` | `fold-link` | Inline text link / link-button (icons, accent · muted). |
228
+ | `FoldButtonComponent` | `button[foldButton]` · `a[foldButton]` | Action button — applied to a real `<button>` **or** `<a>` (link that looks like a button, gets `href`/`routerLink`); orthogonal `emphasis` (solid·soft·outline) × `intent` (primary·neutral·warning·danger) × 3 sizes × shape/`block`; `icon`/`iconTrailing` shorthand (auto-sized) or project content; `loading` (spinner + `aria-busy`). Use native `(click)`. |
229
+ | `FoldButtonIconComponent` | `fold-button-icon` | Icon-only **momentary** button — shape × size × tone; a one-shot action (no pressed state). For a text button use `fold-button`; for on/off use `fold-toggle-icon`. |
230
+ | `FoldToggleIconComponent` | `fold-toggle-icon` | Icon-only **toggle** — the same surface as `fold-button-icon`, plus `[(active)]` + `aria-pressed` (true/false) and a pressed state. Emits `toggled`. |
231
+ | `FoldDataTableComponent` | `fold-data-table` | Controlled roster table (sortable, tone rows, mobile cards). |
232
+ | `FoldPaginatorComponent` | `fold-paginator` | Server-side paginator (size selector + range + page nav). |
233
+ | `FoldTimelineComponent` | `fold-timeline` | Connected rail of nodes (dot + optional date + label) — `vertical` navigable history or `horizontal` step progress; nodes optionally clickable. |
234
+ | `FoldBadgeComponent` | `fold-badge` | Status / count pill (accent/info/warning/alert/success). |
235
+ | `FoldStatusBadgeComponent` | `fold-status-badge` | Status→colour badge (maps a domain status key to a tone). |
236
+ | `FoldChoiceRowComponent` | `fold-choice-row` | Segmented / chip selector. |
237
+ | `FoldTabNavComponent` | `fold-tab-nav` | Tab bar (horizontal / vertical sidebar). |
238
+ | `FoldIconComponent` | `fold-icon` | SVG icon (114-icon built-in set + `FoldIconRegistry`). |
239
+ | `FoldSpinnerComponent` | `fold-spinner` | Indeterminate loading arc (`currentColor`, icon-sized, reduced-motion aware). Decorative by default; `label` → `role="status"`. Powers `loading` on the buttons. |
240
+ | `FoldAvatarComponent` / `…Detail` | `fold-avatar` | Initials/image avatar (square, muted, status ring) + identity cell. |
241
+ | `FoldAvatarListComponent` | `fold-avatar-list` | Overlapping avatar cluster (per-face variant, `limit` + a `+N` overflow chip). |
242
+ | `FoldToastComponent` / `…Container` | `fold-toast` | Frosted snackbar (variant glyph + dismiss) + queue host (+ `FoldToastService`). |
243
+ | `FoldLoadingStateComponent` | `fold-loading` | Loading placeholder — `fold-spinner` + message, in a `role="status"` region; `size` input; stretches to fill. |
244
+ | `FoldEmptyStateComponent` | `fold-empty-state` | Empty-state block (icon + title + message + optional action). |
245
+ | `FoldCalloutComponent` | `fold-callout` | Tinted message row — status colour + icon + message + optional trailing actions; `inset` (bordered, in-flow) appearance. |
246
+ | `FoldDisclosureComponent` | `fold-disclosure` | One summary toggling one collapsible panel — the accordion primitive (open-state is the consumer's to bind); keeps content mounted, unlike native `<details>`. |
247
+ | `FoldPanelHostComponent` | `fold-panel-host` | Side-panel / overlay host (+ `FoldPanelHostService` / `FoldPanelRef` / `FoldPanelToggle`). Modal: accessible name, `inert` background barrier, top-most focus trap, scroll-lock. Localise the close label once via `provideFoldPanelLabels({ close })`. |
248
+ | `FoldPanelHeaderComponent` | `fold-panel-header` | Standard panel header (title/eyebrow, self-closing). Names its dialog (`aria-labelledby`) and reads the app-wide close label. |
249
+
250
+ Directives worth knowing: **`foldSurface`** (`page`·`chrome` — the seam a mixed
251
+ theme re-colours across), **`foldElevated`** (raise any bg-owning element into
252
+ an inset, rounded, shadowed card — the per-surface "floating" mechanism, driven
253
+ by `--fold-surface-inset`/`-radius`/`-shadow`), **`foldStickyColumn`** (turn an
254
+ `<aside>` into a sticky side column — `sticky="top·center·bottom"` + `stickyOffset`,
255
+ the rest tunable via `--fold-sticky-column-*`; un-stick with
256
+ `--fold-sticky-column-position: static` at the page's stacking breakpoint), and
257
+ **`foldRepeatPress`** (press-and-hold auto-repeat for a stepper button — fires
258
+ once on press then on a tunable cadence while held, and stops the instant
259
+ `foldRepeatPressDisabled` goes true mid-hold).
260
+
261
+ ## Form-field ids
262
+
263
+ Browsers warn — _"A form field element should have an id or name attribute"_ —
264
+ about any `<input>` / `<select>` / `<textarea>` that carries neither (autofill +
265
+ a11y can't identify it). Two pieces keep that silent, SSR-safely:
266
+
267
+ - **`FoldIdService`** — a unique-id generator. A per-injector counter (one per app,
268
+ fresh per SSR request), so ids are deterministic in render order and match
269
+ server ↔ client. Use it when a component owns a labelled control and needs the
270
+ id for `<label for>`: `readonly id = inject(FoldIdService).next('fold-input')`.
271
+ (`crypto.randomUUID()` differs server vs client and trips hydration — don't.)
272
+ - **`FoldFieldIdDirective`** — auto-applies (by selector) to any native control
273
+ missing **both** `id` and `name`, and assigns one. Add it to a component's
274
+ `imports` once and every loose native control in its template is fixed — no
275
+ per-element edit. It skips anything already identified (static or bound `id`/
276
+ `name`), so it never fights a control you've labelled yourself.
277
+
278
+ ## Icons
279
+
280
+ `fold-icon` ships a **built-in set of single-colour SVGs** (114 today, across
281
+ `ui`, `nav`, `music`, `status`, `people`, `brands`), inlined so the package is
282
+ self-contained — no `.svg` loader config leaks to a consumer. Icons use
283
+ `currentColor`, so they inherit `color` and are sized with `size` (`xs…xl`, which
284
+ map to the `--fold-icon-size-*` scale, or a pixel number):
285
+
286
+ ```html
287
+ <fold-icon name="search" />
288
+ <fold-icon name="heart" [size]="18" />
289
+ <fold-icon name="edit" title="Edit track" />
290
+ <!-- name autocompletes the built-ins (FoldBuiltinIconName) -->
291
+ ```
292
+
293
+ **A consumer adds its own icons** — the package's set stays the shared core; the
294
+ app extends it through the root `FoldIconRegistry`. Register once at bootstrap
295
+ (idiomatic, like `provideRouter`):
296
+
297
+ ```ts
298
+ // app.config.ts
299
+ providers: [provideFoldIcons({ "my-logo": "<svg …>…</svg>" })];
300
+ ```
301
+
302
+ Or at runtime — the icon recolours/resolves reactively:
303
+
304
+ ```ts
305
+ inject(FoldIconRegistry).register("my-logo", svgMarkup);
306
+ inject(FoldIconRegistry).registerMany({ … });
307
+ ```
308
+
309
+ A custom entry with a built-in key overrides it. `name` is typed
310
+ `FoldBuiltinIconName | (string & {})`, so built-ins autocomplete while any
311
+ registered custom string is still accepted (an unknown name renders nothing and
312
+ `console.warn`s in dev).
313
+
314
+ **Rendering — shared sprite.** Each unique icon is added **once** to a hidden
315
+ document-level SVG sprite as a `<symbol>`; every `<fold-icon>` renders a
316
+ lightweight `<svg><use href="#…"/></svg>` that references it. So the DOM holds
317
+ one copy of an icon's paths no matter how many times it renders — a data-table
318
+ with 500 rows × 3 icons is 1500 tiny `<use>` elements over 3 symbols, not 1500
319
+ copies of the markup. It is **lazy** (only rendered icons enter the sprite) and
320
+ **SSR-safe** (the sprite is serialised into the server HTML, so `<use>` resolves
321
+ on first paint; the client adopts it rather than building a second). `color` and
322
+ `fill: currentColor` inherit across the `<use>` into the symbol, so an icon still
323
+ takes its colour from the host — and outlined icons keep their source
324
+ `fill="none"` / `stroke="currentColor"`.
325
+
326
+ **Trust contract.** Icon markup is injected into the sprite **unsanitised** (the
327
+ sprite `insertAdjacentHTML`s the authored `<svg>` so it survives intact), so
328
+ every registered value **must be a static, authored `<svg>` string, never
329
+ derived from user input**. The registry enforces a backstop (not a sanitiser) on
330
+ all three doors (`provideFoldIcons`, `register`, `registerMany`): a non-`<svg>`
331
+ root or a `<script>` / inline `on*=` handler throws at registration. Passing
332
+ user-controlled markup would be a stored-XSS sink — keep icon strings static.
333
+
334
+ ## Auto-colour
335
+
336
+ `fold-avatar` (and any entity that needs a stable, recognisable colour) draws
337
+ from **one app-wide palette** via `FoldPaletteRegistry` — a root singleton. Reading
338
+ from a single source is the point: the same seed (name / id) is the **same
339
+ colour everywhere**, so people/entities stay recognisable across screens.
340
+
341
+ Consumers never touch the palette arrays or the hash — they call `colorFor`:
342
+
343
+ ```ts
344
+ private readonly palette = inject(FoldPaletteRegistry);
345
+ readonly color = computed(() => this.palette.colorFor(this.seed())); // reactive
346
+ ```
347
+
348
+ **Choose the palette once, at bootstrap** (like `provideRouter`):
349
+
350
+ ```ts
351
+ // app.config.ts — a built-in name, or your own colour list
352
+ providers: [provideFoldPalette("vivid")]; // 'vivid' (default) | 'extended' | 'pastel'
353
+ providers: [provideFoldPalette(MY_BRAND_COLOURS)]; // readonly string[]
354
+ ```
355
+
356
+ **Or switch it live** — every avatar recolours in the same frame:
357
+
358
+ ```ts
359
+ inject(FoldPaletteRegistry).use("pastel");
360
+ inject(FoldPaletteRegistry).use(MY_BRAND_COLOURS);
361
+ ```
362
+
363
+ Palettes are **categorical data**, not semantic tokens (qualitative hues to tell
364
+ entities apart, theme-invariant), so they live in TS (`FOLD_AUTO_PALETTES`), consumed
365
+ by a hash — not in the token CSS. Add a curated palette by adding one entry to
366
+ `FOLD_AUTO_PALETTES`; it becomes a typed `FoldAutoPaletteName` everywhere.