@zenit-hosting/zenit-ui 0.1.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.
Files changed (36) hide show
  1. package/README.md +400 -0
  2. package/fesm2022/zenit-hosting-zenit-ui.mjs +11289 -0
  3. package/fesm2022/zenit-hosting-zenit-ui.mjs.map +1 -0
  4. package/llms-full.txt +5120 -0
  5. package/llms.txt +179 -0
  6. package/package.json +44 -0
  7. package/schematics/collection.json +15 -0
  8. package/schematics/migrate-material/component.js +241 -0
  9. package/schematics/migrate-material/index.js +463 -0
  10. package/schematics/migrate-material/report.js +125 -0
  11. package/schematics/migrate-material/schema.js +2 -0
  12. package/schematics/migrate-material/schema.json +32 -0
  13. package/schematics/migrate-material/tables.js +138 -0
  14. package/schematics/migrate-material/template.js +871 -0
  15. package/schematics/ng-add/html.js +151 -0
  16. package/schematics/ng-add/index.js +609 -0
  17. package/schematics/ng-add/init-script.js +108 -0
  18. package/schematics/ng-add/schema.js +2 -0
  19. package/schematics/ng-add/schema.json +34 -0
  20. package/schematics/package.json +3 -0
  21. package/styles/_daten.css +138 -0
  22. package/styles/_formulare.css +97 -0
  23. package/styles/_grundlage.css +424 -0
  24. package/styles/_konfigurator.css +216 -0
  25. package/styles/_navigation.css +179 -0
  26. package/styles/_overlays.css +86 -0
  27. package/styles/_rueckmeldung.css +100 -0
  28. package/styles/_werkzeuge.css +119 -0
  29. package/styles/themes/accents.css +91 -0
  30. package/styles/themes/base.css +26 -0
  31. package/styles/themes/contrast.css +67 -0
  32. package/styles/themes/light.css +74 -0
  33. package/styles/themes.css +21 -0
  34. package/styles/tokens.css +76 -0
  35. package/styles/zenit-ui.css +12 -0
  36. package/types/zenit-hosting-zenit-ui.d.ts +5957 -0
package/README.md ADDED
@@ -0,0 +1,400 @@
1
+ # zenit-ui
2
+
3
+ `zenit-ui` is the Angular library of the Zenit design system. It provides 30 building blocks for the public website, the customer area and the server panels of Zenit-Hosting, plus Icon, Spinner and Tooltip. The building blocks are standalone components and directives with `OnPush` and signal inputs. They ship no styles of their own: every class lives in the bundled CSS files and uses only the tokens from `tokens.css`. Angular Material is not used; overlays and focus traps come from `@angular/cdk`, form fields are native elements. Business logic, services and copy stay in your application.
4
+
5
+ ## Requirements
6
+
7
+ - Angular 22 (`@angular/core`, `@angular/common`, `@angular/forms`)
8
+ - `@angular/cdk` 22 for dialog, menu, tooltip and overlays
9
+ - `rxjs` 7.8
10
+
11
+ All five are peer dependencies and are not bundled with the library. Its only own dependency is `tslib`.
12
+
13
+ ## Installation
14
+
15
+ The library is not published to npm. You build it and install the package locally:
16
+
17
+ ```bash
18
+ ng build zenit-ui
19
+ cd dist/zenit-ui
20
+ npm pack
21
+ ```
22
+
23
+ This produces `zenit-ui-0.1.0.tgz`. In your application:
24
+
25
+ ```bash
26
+ npm i ./zenit-ui-0.1.0.tgz
27
+ ```
28
+
29
+ > **Install from the tarball, never from the public registry.** `zenit-ui` is an unscoped name that
30
+ > nobody has claimed on npmjs.com, so `npm i zenit-ui` or `ng add zenit-ui` would fetch whatever
31
+ > somebody else publishes under it and, in the case of `ng add`, run its schematics against your
32
+ > workspace. Point every command at the local file, as the sections below do, until the name is
33
+ > claimed or the package is scoped.
34
+
35
+ ### Working against a linked build
36
+
37
+ While you develop against the library, `npm link` or a junction is faster than repacking. Three
38
+ settings belong to that setup, not to a real install. In `angular.json`, on the build target and on
39
+ the test target:
40
+
41
+ ```json
42
+ "preserveSymlinks": true,
43
+ "runnerConfig": true
44
+ ```
45
+
46
+ and in the file the second line makes the builder read, `vitest-base.config.ts` in the project or
47
+ workspace root:
48
+
49
+ ```ts
50
+ test: {
51
+ server: { deps: { inline: [/zenit-ui/] } },
52
+ }
53
+ ```
54
+
55
+ Without `preserveSymlinks` the bundler resolves the linked package to its real path and takes
56
+ `@angular/core` from the library workspace: two Angular instances, `NG0203: inject() must be called
57
+ from an injection context`. `server.deps.inline` is needed because a package in `node_modules` is
58
+ external to the test bundle, so Vitest lets Node load it, and Node follows the link regardless of
59
+ what Vite is told. And without `runnerConfig` the config file is not read at all: the option
60
+ defaults to `false`, so the file sits there and changes nothing. All three are needed together, and
61
+ both workspaces should be on the same Angular patch version. A tarball or registry install has none
62
+ of this: the package then lives inside your own `node_modules` and resolves `@angular/core` from
63
+ there. The measured table and the alternative via `resolve.dedupe` are in
64
+ [`docs/ng-add.md`](../../docs/ng-add.md), "Working against a linked build".
65
+
66
+ ## Setup
67
+
68
+ ### 1. Register the styles in `angular.json`
69
+
70
+ The order is binding: tokens first, then the CDK positioning CSS, then the library styles, then your application.
71
+
72
+ ```json
73
+ "styles": [
74
+ "zenit-ui/styles/tokens.css",
75
+ "@angular/cdk/overlay-prebuilt.css",
76
+ "zenit-ui/styles/zenit-ui.css",
77
+ "src/styles.css"
78
+ ]
79
+ ```
80
+
81
+ Both files can be pulled in via `@import` just as well, if you use your own entry stylesheet:
82
+
83
+ ```css
84
+ @import "zenit-ui/styles/tokens.css";
85
+ @import "@angular/cdk/overlay-prebuilt.css";
86
+ @import "zenit-ui/styles/zenit-ui.css";
87
+ ```
88
+
89
+ `zenit-ui.css` imports the partials `styles/_*.css`. They sit next to it in the package and need no entry of their own.
90
+
91
+ Your own stylesheet is last on purpose: that is where the **page width** is set. `--container` defaults to 1120px and is the width of the application, not a fixed value of the design system. `.z-container` is the only rule that reads it, so one declaration moves header, content and footer of every page:
92
+
93
+ ```css
94
+ /* src/styles.css */
95
+ :root {
96
+ --container: 1440px;
97
+ }
98
+ ```
99
+
100
+ `:root`, not `html`: both blocks weigh (0,1,0) and the later one wins, while a bare `html` selector weighs (0,0,1) and would lose. Running text stays at `--measure` (65ch) whatever the page width is. Which blocks grow with `--container` and which keep a width of their own is in [`docs/layout.md`](../../docs/layout.md), and why it is a stylesheet declaration and not an option of `provideZenitTheme` is in [`docs/theming.md`](../../docs/theming.md).
101
+
102
+ ### 2. Set `z-root`
103
+
104
+ The class `z-root` belongs on `<html>` and on `<body>`. It sets background, text color, font, `font: inherit` for controls and the focus ring. Overlays attach to `body` and inherit the same variables. On `<html>` it sets no `font-size` and no `line-height` at all, so the rem base stays yours — your own `html { font-size: … }` wins at any specificity and in any include order — and its link rules no longer beat your own classes, so legacy styles keep working while you migrate page by page. **Both elements are required**: the page size comes from `body.z-root` alone, and a setup that sets the class on `<html>` only renders at 16px/normal instead of 14px/20px.
105
+
106
+ ```html
107
+ <!doctype html>
108
+ <html lang="de" class="z-root">
109
+ <head>
110
+ <meta charset="utf-8" />
111
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
112
+ </head>
113
+ <body class="z-root">
114
+ <app-root></app-root>
115
+ </body>
116
+ </html>
117
+ ```
118
+
119
+ A small migrated block inside an old page is a `z-root` island. `z-root` paints `--bg`; on an old
120
+ light ground use `class="z-root z-root--transparent" data-theme="light"` (needs `themes.css`), so
121
+ the island shows the old ground and carries the scheme that fits it. When to take which is in
122
+ [`docs/legacy.md`](../../docs/legacy.md#an-island-without-its-own-surface).
123
+
124
+ ### 3. Self-host the fonts
125
+
126
+ The library loads no font. Your application brings four, all self-hosted, so that no request to Google is needed:
127
+
128
+ - Material Icons (the ligature font for `z-icon`)
129
+ - Inter in 400, 500 and 600 (`body`)
130
+ - Space Grotesk in 600 and 700 (`display`)
131
+ - JetBrains Mono in 400 and 600 (`mono`)
132
+
133
+ ```css
134
+ @import "material-icons/iconfont/filled.css" layer(schriften);
135
+ @import "@fontsource/inter/400.css";
136
+ @import "@fontsource/inter/500.css";
137
+ @import "@fontsource/inter/600.css";
138
+ @import "@fontsource/space-grotesk/600.css";
139
+ @import "@fontsource/space-grotesk/700.css";
140
+ @import "@fontsource/jetbrains-mono/400.css";
141
+ @import "@fontsource/jetbrains-mono/600.css";
142
+ ```
143
+
144
+ The `layer(schriften)` is required: `material-icons` sets its own `font-size` on `.material-icons` and is loaded after `zenit-ui.css`. The layer makes sure `.z-icon` from the library wins. Without it the icon would be 24px inside a 20px box. A complete example is in `projects/ui-demo/src/styles.css` in the repository of the design system.
145
+
146
+ ### 4. Mount the toast outlet
147
+
148
+ `<z-toast-outlet />` goes once into your application shell, best at the end of the layout. The service `ZToast` writes to it.
149
+
150
+ ```html
151
+ <app-header />
152
+ <router-outlet />
153
+ <app-footer />
154
+ <z-toast-outlet />
155
+ ```
156
+
157
+ ### 5. Minecraft subtheme
158
+
159
+ On `/minecraft` and in the Minecraft panel you put `z-theme-mc` on the page container. The primary button then becomes `mc-accent` with `on-mc`, and active icons turn green. Everything else stays the same: surfaces, radii, typography, spacing, status colors.
160
+
161
+ ```html
162
+ <div class="z-theme-mc">
163
+ <button zBtn="primary">Server erstellen</button>
164
+ </div>
165
+ ```
166
+
167
+ ## Example
168
+
169
+ ```ts
170
+ import { Component } from '@angular/core';
171
+ import { ZButton, ZField, ZInput, ZPanel, ZPanelActions } from 'zenit-ui';
172
+
173
+ @Component({
174
+ selector: 'app-server-name',
175
+ imports: [ZPanel, ZPanelActions, ZField, ZInput, ZButton],
176
+ template: `
177
+ <z-panel title="Servername">
178
+ <button zBtn="ghost" size="sm" zPanelActions>Zurücksetzen</button>
179
+ <z-field label="Name" for="name" hint="Erscheint in der Serverliste.">
180
+ <input zInput id="name" name="name" value="Beispiel-Server" />
181
+ </z-field>
182
+ <button zBtn="primary">Speichern</button>
183
+ </z-panel>
184
+ `,
185
+ })
186
+ export class ServerName {}
187
+ ```
188
+
189
+ ## Component API
190
+
191
+ Selectors and inputs are binding, so that pages and building blocks can be built in parallel. Inputs are signals. Two-way binding via `model()`.
192
+
193
+ Entries marked "(addition)" are not part of the reference table (`spec/guidelines/40-bibliothek.md` in the repository of the design system). They exist in the code today, mostly to keep ARIA labels overridable from the application.
194
+
195
+ | Component | Selector | Inputs, outputs, slots |
196
+ | --- | --- | --- |
197
+ | Icon | `z-icon` | `name`, `size: 'sm' \| 'md'` |
198
+ | Spinner | `z-spinner` | `label` |
199
+ | Button | `button[zBtn]`, `a[zBtn]` | `zBtn: 'primary' \| 'secondary' \| 'ghost' \| 'danger'` (default secondary), `size: 'sm' \| 'md' \| 'lg'`, `block`, `iconOnly`, `loading`, `disabled` |
200
+ | Badge | `z-badge` | `status: 'neutral' \| 'success' \| 'warning' \| 'danger' \| 'info'`, `dot` |
201
+ | Field | `z-field` | `label`, `for`, `hint`, `error` |
202
+ | Input | `input[zInput]`, `textarea[zInput]` | `size`, `mono`, `invalid`; search via `z-input-group` with `icon` |
203
+ | Select | `z-select` | `size`; content is a native `<select>` |
204
+ | Checkbox | `z-checkbox` | `[(checked)]`, `disabled`, `ariaLabel`; Forms |
205
+ | Toggle | `z-toggle` | `[(checked)]`, `disabled`, `ariaLabel`, `ariaLabelledby`; Forms |
206
+ | Setting | `z-setting` | `title`, `key`, `description`, `titleId`; content is the control |
207
+ | Slider | `z-slider` | `label`, `min`, `max`, `step`, `unit`, `ticks`, `hint`, `[(value)]`, `disabled`; Forms; `ariaLabel` (addition) |
208
+ | SkipLink (addition) | `a[zSkipLink]` | none; the caller writes the text and the `href`, the target needs `tabindex="-1"` |
209
+ | Tabs | `nav[zTabs]`, `a[zTab]` | `active` |
210
+ | Segment | `z-segment` | `options: {value, label}[]`, `[(value)]`, `ariaLabel`; Forms; `disabled` (addition) |
211
+ | Stepper | `z-stepper` | `steps: string[]`, `current` |
212
+ | Panel | `z-panel` | `title`, `flush`, `busy`; slot `[zPanelActions]`, `z-pagination` is moved to the end; `headingLevel`, `titleMono` (additions) |
213
+ | Metric | `z-metrics`, `z-metric` | `label`, `value`, `unit`, `sub`, `percent` (warning from 80, error from 95) |
214
+ | ServerList | `z-rows`, `z-rows-head`, `a[zRow]`, `div[zRow]`, `z-row-main`, `[zRowNum]` | `columns` (grid columns) on `z-rows`; `title`, `meta`, `image` on `z-row-main`; `thumbText`, `thumb` and slot `[zRowThumb]` (additions) |
215
+ | FileTable | `z-table-container`, `table[zTable]`, `[zNum]`, `[zTableName]`, `th[zSortHeader]` | none; `ariaLabel` on `z-table-container`, `[(sort)]` on `table[zTable]` and the sort header with `zSortHeader`, `sortStart`, `disabled` (additions) |
216
+ | Pagination | `z-pagination` | `[(page)]`, `[(pageSize)]` (25), `total`, `itemLabel`; `pageSizeOptions`, `pageSizeLabel`, `rangeLabel`, `ariaLabelPrev`, `ariaLabelNext` (additions) |
217
+ | Alert | `z-alert` | `status`, `title`, `icon`; content is the text; slot `[zAlertAction]` |
218
+ | EmptyState | `z-empty-state` | `title`; content is the text; slot `[zEmptyAction]`; `headingLevel` (addition) |
219
+ | Skeleton | `z-skeleton` | `width`, `thumb`, `tile` |
220
+ | Sidebar | `z-sidebar`, `z-sidebar-group`, `[zSidebarItem]` | `ariaLabel`; `label`; `icon`, `active`, `count` |
221
+ | AppHeader | `z-app-header`, `a[zHeaderLink]`, `[zBrand]` | `navLabel`; `active`; slot `[zHeaderEnd]`; `menuLabel` (addition); `[(open)]` (addition) |
222
+ | PageHeader | `z-page-header` | `title`, `sub`; content are the actions |
223
+ | Footer | `z-footer`, `z-footer-col` | `heading`; slot `[zFooterBase]` |
224
+ | Dialog | service `ZDialog`, layout `z-dialog` | `open(component, config)`, `confirm({title, body, confirmLabel, cancelLabel, danger, requireText})` returns `Observable<boolean>`; slot `[zDialogActions]`; `requireLabel` and `cancelLabel` as a required field of the config (additions) |
225
+ | Menu | `z-menu`, `button[zMenuItem]`, `z-menu-separator` | `icon`, `danger`, `disabled`, `(triggered)`; trigger `[cdkMenuTriggerFor]` |
226
+ | Toast | service `ZToast`, `z-toast-outlet` | `show`, `success`, `error`, `dismiss`; options `status`, `icon`, `actionLabel`, `action`, `duration`; `closeLabel` on `z-toast-outlet` (addition); option `live` and `provideZenitToast({ maxVisible, overflow })` (additions) |
227
+ | Tooltip | `[zTooltip]` | text as the value |
228
+ | Console | `z-console` | `lines: {time, text, level}[]`, `disabled`, `placeholder`; `(command)`; `logLabel`, `inputLabel`, `endLabel` (additions) |
229
+ | Hero | `z-hero` | `title`, `lead`, `note`; slots `[zHeroActions]`, `[zHeroAside]`; `size` (addition) |
230
+ | GameTile | `z-game-grid`, `button[zGameTile]`; `a[zGameTile]` (addition) | `title`, `price`, `cover`, `selected`; `(coverError)` (addition); the link has `title`, `price`, `cover` and `(coverError)`, no `selected` |
231
+ | PriceSummary | `z-price-summary` | `label`, `price`, `period`, `lines: {label, value}[]`, `note`; content is the button |
232
+ | SpecList | `z-spec-list` | `items: {term, value, note, mono}[]` |
233
+ | Faq | `z-faq` | `question`, `open`; content is the answer |
234
+ | Theme | service `ZTheme`, `provideZenitTheme(config)` | `scheme()`, `resolvedScheme()`, `accent()`, `setScheme(id)`, `setAccent(id)`, `reset()`; config `schemes`, `accents`, `defaultScheme`, `defaultAccent`, `storageKey`, `target` |
235
+ | Labels | `provideZenitLabels(partial)`, `Z_LABELS`, `Z_LABELS_DE`, `Z_LABELS_EN` | one key per built-in text; the inputs of the components still win |
236
+
237
+ The generated reference with every signature, type and default is produced inside the repository of the design system by `npm run docs:api`; it is not part of this package.
238
+
239
+ One thing is worth knowing before you build a page from this table. It turned up while building `projects/beispiel-app` against the packed library:
240
+
241
+ - **Slots and control flow.** `z-panel` picks `z-pagination` out of the projected content, and Alert, EmptyState, AppHeader and Footer have slots of their own. A node inside `@if`, `@for` or `@switch` only reaches its slot while it is the single root node of that block; otherwise it stays in the default content. Give such a node an `@if` of its own.
242
+
243
+ ## Themes
244
+
245
+ `tokens.css` carries one colour scheme, `dark`. The opt-in stylesheet `zenit-ui/styles/themes.css` adds `light` and `contrast` plus the accents `blau`, `gruen` and `violett`, and `provideZenitTheme()` switches between them and stores the choice:
246
+
247
+ ```json
248
+ "styles": [
249
+ "zenit-ui/styles/tokens.css",
250
+ "@angular/cdk/overlay-prebuilt.css",
251
+ "zenit-ui/styles/themes.css",
252
+ "zenit-ui/styles/zenit-ui.css",
253
+ "src/styles.css"
254
+ ]
255
+ ```
256
+
257
+ ```ts
258
+ providers: [provideZenitTheme({ defaultScheme: 'system' })];
259
+ ```
260
+
261
+ The order is binding, because `:root` and `[data-theme="light"]` weigh the same and the later rule wins. A scheme is a block of token overrides, so an own scheme is CSS plus its id in `schemes`. Components never learn about any of this.
262
+
263
+ **These values are not part of the design system yet.** They were derived by the contrast rules in `docs/theming.md` and checked by `node tools/check-theme-contrast.mjs` (3 schemes × 4 accents, 456 pairs), but they still need the design owner's approval before they move into `tokens.json`. Everything about schemes, accents, the service, SSR and the gate is in [`docs/theming.md`](../../docs/theming.md).
264
+
265
+ ## Server rendering
266
+
267
+ Every building block can be constructed and rendered on a server: no component
268
+ touches `window`, `document.body`, `matchMedia`, `localStorage`,
269
+ `MutationObserver`, `ResizeObserver` or a layout measurement while it is being
270
+ constructed or during its first change detection run. Where a block needs one of
271
+ those to do its work, it creates it lazily and only in a browser, and the
272
+ missing behaviour is behaviour there is nothing to do about on a server anyway:
273
+ no box changes size, nothing scrolls, and no caller rewrites an attribute.
274
+
275
+ That is a guarantee about **one render with no user interaction**, which is what
276
+ a prerender or an SSR response is. Everything an overlay does — menu, dialog,
277
+ tooltip, toast — happens after a click and therefore only ever in a browser; the
278
+ triggers themselves render on the server.
279
+
280
+ `ZTheme` is safe to inject during SSR and reports the defaults. It writes
281
+ exactly one thing into the server document: a `defaultScheme` other than
282
+ `'system'` becomes `data-theme` on `<html>`, so the delivered HTML already
283
+ carries the scheme. With `defaultScheme: 'system'` it writes nothing, because
284
+ the server knows neither the stored choice nor the operating system — the init
285
+ script from `zenitThemeInitScript()` in `<head>` resolves that in the browser
286
+ before the first paint. Both halves are checked against the real prerendered
287
+ HTML by `npm run check:ssr`.
288
+
289
+ The gate itself is described in [`CONTRIBUTING.md`](../../CONTRIBUTING.md).
290
+
291
+ ## Labels and languages
292
+
293
+ The library holds no copy except the accessible names and the one sentence a component cannot leave empty. They live in one registry, so an application in another language sets them once at bootstrap:
294
+
295
+ ```ts
296
+ providers: [provideZenitLabels(Z_LABELS_EN)];
297
+ ```
298
+
299
+ `provideZenitLabels` merges over the German defaults, which keeps a partial override valid, and every input that used to carry a German default still wins over the registry. The keys, the per-usage inputs and how to set them for one subtree only are in [`docs/labels.md`](../../docs/labels.md).
300
+
301
+ ## `ng add`
302
+
303
+ The setup above is a schematic as well. Name the tarball, not the package: `ng add zenit-ui` would resolve the unclaimed name on the public registry and run a stranger's schematics.
304
+
305
+ ```bash
306
+ ng add ./zenit-ui-0.1.0.tgz --themes
307
+ ```
308
+
309
+ It registers the stylesheets in `angular.json` in the prescribed order, merges `z-root` into `<html>` and `<body>`, adds `@angular/cdk` and the four font packages with their `@import` rules, and mounts `<z-toast-outlet />` in the root component. With `--themes` it also registers `themes.css`, puts the theme init script into `index.html`, adds `provideZenitTheme()` and sets `inlineCritical: false` for production. Every step is idempotent, and a source file is either fully patched or left untouched with the manual step in the log (NgModule applications, `imports` that are not an array literal). An existing `lang` on `<html>` is kept. To run it again after the package is installed: `ng generate zenit-ui:ng-add --project my-app`. What it changes exactly, which options it takes and its limits are in [`docs/ng-add.md`](../../docs/ng-add.md).
310
+
311
+ From the GitLab npm registry the package is `@hosting/zenit-ui`: install it under the alias `zenit-ui` (`npm install zenit-ui@npm:@hosting/zenit-ui@<version>`), so every import and stylesheet path stays `zenit-ui`, and run `ng generate zenit-ui:ng-add` instead of `ng add @hosting/zenit-ui`, which would add the package a second time without the alias. Registry and token setup: [`docs/veroeffentlichen.md`](../../docs/veroeffentlichen.md).
312
+
313
+ ### Coming from Angular Material
314
+
315
+ `ng generate zenit-ui:migrate-material --path src/app/billing --dry-run` rewrites what can be rewritten mechanically (`mat-icon`, `mat-*-button`, `matTooltip`, standalone `mat-spinner`, static `mat-chip` and the `imports` of the components) by source span, leaves form fields, selects, dialogs, tables, menus and all styles alone, and writes a Markdown and a JSON report with file, line, rule, reason and suggested fix for every spot it did not convert. Run it per route, without `--dry-run` once the report looks right; a second run changes nothing. Details: [`docs/migrate-material.md`](../../docs/migrate-material.md).
316
+
317
+ ## Documented deviations from the reference styles
318
+
319
+ `spec/components/bundle.css` in the repository of the design system is the reference for all styles. These deviations are deliberate:
320
+
321
+ - The base rule for `font` and `color` on controls uses `:where(button, input, select, textarea)`. The reference selector has a specificity that beats component classes; `:where()` lowers it to the class level, the values are unchanged.
322
+ - The link base rules are `.z-root :where(a)` and `.z-root :where(a):hover`, for the same reason. At the reference specificity (0,1,1) they beat every class an application can put on a link (0,1,0): an application's own skip link came out red on red (1.29:1), and every link on a page that is not migrated yet changed colour and underline the moment `z-root` was set. The values are unchanged, and none of the library's own link rules moves: they all weigh (0,2,1) or more. Three things follow for your own stylesheet:
323
+ - **Your stylesheet has to load after `zenit-ui.css`.** At (0,1,0) your class now *ties* with the base rule, and a tie is decided by source order, not by specificity. The `styles` order above does that. Measured: a rule `.lg { color: … }` before `zenit-ui.css` still loses, after it wins. The hover rule weighs (0,2,0), so changing the hover needs `.lg:hover`, not `.lg`.
324
+ - **A rule of yours at (0,1,1), such as `.app a`, now also reaches plain links inside library components**: panel, alert, row, table, empty state, the sub line of the page header, header, tabs, sidebar, stepper, toast, the body of a dialog, tooltip and menu. It does not reach the footer lists, nor any link carrying a library class (`a.z-btn`, `a.z-tab`, `a.z-side__item`, `a.z-header__link`, `a.z-row`, `a.z-skip-link`, `a.z-game`), which all have a counter-rule at (0,2,1) or more.
325
+ - **The underline for links in running text stays at (0,2,1)** and cannot be switched off from your stylesheet. Its selector ends in `a:not([class*="z-"])`, so any class whose name contains `z-` takes a link out of it, by accident too (`quiz-link`). Do not build on that: `.z-legacy` below is the documented way to keep the library out of a subtree.
326
+ - The page size lives in `.z-root:where(:not(html))`, not in `.z-root`. The reference writes `font-size: 14px` and `line-height: 20px` into the rule for the page, and the documented setup puts `z-root` on `<html>` as well: there those 14px would move `1rem` from 16px to 14px for the whole document and override the size the visitor set in the browser. Split off like this, the library sets **nothing** on `<html>`, so your own `html { font-size: … }` is the only author rule there and wins at every specificity and in either include order — `html { font-size: var(--base-font-size) }` for a user setting included. `:where()` weighs (0,0,0), so the rule is still (0,1,0), exactly what `.z-root` weighed: `body.z-root` and every page container keep the same weight against a class of yours on the same element, and the tie is decided by source order as above. `body.z-root` carries the class itself and keeps 14px/20px, which is what every component and every overlay inherits. The components of the library compute in px; the one `rem` in its stylesheets is the `1rem` of `.z-legacy`, which hands the visitor's size back to a page that is not migrated.
327
+ - `.z-legacy` is an addition: the class for a subtree that is not migrated yet. It resets the inherited `font-size` to `1rem`, `line-height` to `normal` and `-webkit-font-smoothing` to `auto`, and the base rules that style bare elements (`.z-root *`, `:where(button, input, select, textarea)`, `:where(a)` and its hover, the underline in running text, `:focus-visible`) are each written as two selectors, `.z-root X:not(:where(.z-legacy, .z-legacy *))` and `:where(.z-legacy) .z-root X`. Both `:where()` weigh (0,0,0), so every specificity above still holds; the first leaves out the host and everything in it, the second lets a `z-root` container inside the subtree switch the rule on again. The exclusion on the universal rule costs +14 to +18 % on a forced full style recalculation (4.2 to 4.9 ms on a page of 4130 elements). Family, colour, background and `color-scheme` are left to the application. `@scope` would say the same more directly, but Firefox before 146 and Safari before 17.4 lack it and are inside the range Angular 22 builds for; `revert` rolls back to the browser's stylesheet instead of the application's rule. See [`docs/legacy.md`](../../docs/legacy.md).
328
+ - Below 900px the header is one row of brand, end slot and menu button (`gap: space-3`, `order: 1` on the button, an end slot that wraps its own items), and an image inside `[zBrand]` is a block. The reference has no mobile header at all. See [`docs/components/app-header.md`](../../docs/components/app-header.md).
329
+ - Below 640px small controls are 40px high (`.z-btn--sm`, `.z-input--sm`, `.z-select--sm select`, `.z-menu__item`), because touch targets on mobile are at least 40px.
330
+ - Below 640px the alert wraps its action button onto its own line, so that title, text and button stay readable at 360px.
331
+ - `div[zRow]` resets `cursor` to `auto`. A row is only clickable as `a[zRow]`; the non-interactive variant must not look clickable.
332
+ - The CDK backdrop runs without a fade (`transition: none`). Transitions are limited to `color`, `background-color` and `border-color`.
333
+ - Below 640px a `[zRowAction]` keeps its cell and its row gets a third column. The reference hides every cell of a row from the third on, which also hid the menu button of a row, so its entries were unreachable on a phone.
334
+ - `z-footer` carries `background: var(--bg)`, like `z-app-header`. The reference has none, because its page ground already is `--bg`; the addition keeps the footer readable above a legacy surface while an application migrates route by route, and changes nothing on a migrated page, since the colour is the same as the page. Measured on a light `.z-legacy` island (`e2e/legacy.spec.ts`): `.z-footer__base` and its links already carry their own `color` (`text-subtle`, `text-muted`), so nothing inherits from the legacy surface and no `color` was added.
335
+
336
+ ## Rules
337
+
338
+ Color, spacing, radius, typography and shadow come only from `tokens.css`. `tokens.css` is the single place with hex and pixel values. Your own styles reference `var(--…)` and set no literal values of their own.
339
+
340
+ Not allowed are `@angular/material`, `linear-gradient`, `radial-gradient`, `backdrop-filter`, `text-shadow`, colored `box-shadow`, grid backgrounds, pill badges above headings, all-caps labels, icon backplates, cards with a colored border, metric tiles for marketing numbers, and hex or pixel values outside the tokens. The complete list is in the section "Verboten" of the system overview, `CLAUDE.md` in the repository of the design system.
341
+
342
+ Stylelint enforces the rules automatically. Without that layer, generated code drifts again. Copy the configuration into your application:
343
+
344
+ ```json
345
+ {
346
+ "rules": {
347
+ "color-no-hex": true,
348
+ "color-named": "never",
349
+ "function-disallowed-list": ["linear-gradient", "radial-gradient", "conic-gradient", "rgb", "rgba", "hsl", "hsla"],
350
+ "property-disallowed-list": ["backdrop-filter", "text-shadow", "filter"],
351
+ "declaration-property-value-disallowed-list": {
352
+ "box-shadow": ["/^(?!var\\(--shadow-overlay\\)|none|inset 0 -2px 0 var\\().*/"],
353
+ "transition": ["/transform|all/"],
354
+ "font-family": ["/^(?!var\\(--font-(display|body|mono)\\)|inherit).*/"]
355
+ },
356
+ "selector-pseudo-element-disallowed-list": ["ng-deep"],
357
+ "declaration-no-important": true
358
+ },
359
+ "overrides": [{ "files": ["**/tokens.css"], "rules": { "color-no-hex": null, "function-disallowed-list": null, "declaration-property-value-disallowed-list": null } }]
360
+ }
361
+ ```
362
+
363
+ Plus an ESLint entry `no-restricted-imports` for the pattern `@angular/material*`, so that Material cannot come back in:
364
+
365
+ ```js
366
+ 'no-restricted-imports': [
367
+ 'error',
368
+ {
369
+ patterns: [
370
+ {
371
+ group: ['@angular/material', '@angular/material/*', '@angular/material*'],
372
+ message: 'zenit-ui does not use Angular Material. Only @angular/cdk.',
373
+ },
374
+ ],
375
+ },
376
+ ],
377
+ ```
378
+
379
+ ## Development
380
+
381
+ Inside the workspace `zenit-ui-workspace`:
382
+
383
+ ```bash
384
+ npm run build:lib # library into dist/zenit-ui plus the compiled schematics
385
+ ng test zenit-ui # unit tests of the library
386
+ npm run test:schematics # the schematics (ng add, migrate-material) against fixtures
387
+ npm run check:themes # contrast gate over every scheme and accent
388
+ npm run lint # ESLint over library, demo and example app
389
+ npm run lint:css # Stylelint over projects/**/*.css
390
+ npm run e2e # Playwright with axe over the demo pages
391
+ npm run e2e:beispiel # the same checks over the example app, in all three schemes
392
+ npm run docs:api # TypeDoc reference into docs/api, not committed
393
+ npm run check # everything above except the Playwright runs
394
+ ng serve ui-demo # demo app with every building block in all states
395
+ npm run start:beispiel # example app: one complete page against dist/zenit-ui
396
+ ```
397
+
398
+ The demo app `ui-demo` shows every building block in the states idle, hover, focus, active, disabled, loading, error, empty and success. It is the reference for markup and classes.
399
+
400
+ The example app `beispiel-app` shows one complete page of the customer area, built the way an application builds it: it imports from the package in `dist/zenit-ui` and follows the setup steps above one by one. Its page "Einbindung" and the disclosures on the Gameserver page show the real files of that setup, generated from the sources themselves. Copy it as the starting point for a real page; `projects/beispiel-app/README.md` maps every region of the page to the rule it follows.