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 +69 -0
- package/README.md +361 -4
- package/fesm2022/fold-ng.mjs +5820 -0
- package/fesm2022/fold-ng.mjs.map +1 -0
- package/llms.txt +45 -0
- package/package.json +60 -10
- package/tokens/index.css +14 -0
- package/tokens/primitives.css +113 -0
- package/tokens/scales.css +106 -0
- package/tokens/semantic.css +704 -0
- package/tsconfig.lib.tsbuildinfo +1 -0
- package/types/fold-ng.d.ts +3840 -0
- package/types/fold-ng.d.ts.map +1 -0
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
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/fold-ng)
|
|
4
|
+
[](https://www.npmjs.com/package/fold-ng)
|
|
5
|
+
[](https://bundlephobia.com/package/fold-ng)
|
|
6
|
+
[](https://www.npmjs.com/package/fold-ng)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](./CHANGELOG.md)
|
|
9
|
+
[](https://hugoheynard.github.io/fold-ng/)
|
|
4
10
|
|
|
5
|
-
|
|
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
|
-
|
|
18
|
+
### ▶ [Live demo & component gallery](https://hugoheynard.github.io/fold-ng/)
|
|
8
19
|
|
|
9
|
-
|
|
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.
|