@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.
- package/README.md +400 -0
- package/fesm2022/zenit-hosting-zenit-ui.mjs +11289 -0
- package/fesm2022/zenit-hosting-zenit-ui.mjs.map +1 -0
- package/llms-full.txt +5120 -0
- package/llms.txt +179 -0
- package/package.json +44 -0
- package/schematics/collection.json +15 -0
- package/schematics/migrate-material/component.js +241 -0
- package/schematics/migrate-material/index.js +463 -0
- package/schematics/migrate-material/report.js +125 -0
- package/schematics/migrate-material/schema.js +2 -0
- package/schematics/migrate-material/schema.json +32 -0
- package/schematics/migrate-material/tables.js +138 -0
- package/schematics/migrate-material/template.js +871 -0
- package/schematics/ng-add/html.js +151 -0
- package/schematics/ng-add/index.js +609 -0
- package/schematics/ng-add/init-script.js +108 -0
- package/schematics/ng-add/schema.js +2 -0
- package/schematics/ng-add/schema.json +34 -0
- package/schematics/package.json +3 -0
- package/styles/_daten.css +138 -0
- package/styles/_formulare.css +97 -0
- package/styles/_grundlage.css +424 -0
- package/styles/_konfigurator.css +216 -0
- package/styles/_navigation.css +179 -0
- package/styles/_overlays.css +86 -0
- package/styles/_rueckmeldung.css +100 -0
- package/styles/_werkzeuge.css +119 -0
- package/styles/themes/accents.css +91 -0
- package/styles/themes/base.css +26 -0
- package/styles/themes/contrast.css +67 -0
- package/styles/themes/light.css +74 -0
- package/styles/themes.css +21 -0
- package/styles/tokens.css +76 -0
- package/styles/zenit-ui.css +12 -0
- package/types/zenit-hosting-zenit-ui.d.ts +5957 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/* Package grundlage: head (z-root), Button, Badge, Field/Input/Select, Panel,
|
|
2
|
+
Icon, Spinner and the layout helpers. Verbatim from spec/components/bundle.css. */
|
|
3
|
+
|
|
4
|
+
.z-root, .z-root *:not(:where(.z-legacy, .z-legacy *)), :where(.z-legacy) .z-root * { box-sizing: border-box; }
|
|
5
|
+
.z-root {
|
|
6
|
+
background: var(--bg);
|
|
7
|
+
color: var(--text);
|
|
8
|
+
font-family: var(--font-body);
|
|
9
|
+
-webkit-font-smoothing: antialiased;
|
|
10
|
+
}
|
|
11
|
+
/* Addition to the reference: an island without its own surface, for migrated
|
|
12
|
+
blocks inside an old page (docs/legacy.md). Everything of .z-root stays,
|
|
13
|
+
tokens, text colour, type and the base rules; only the ground of the
|
|
14
|
+
surrounding page shows through. The text colour is still the scheme's, so
|
|
15
|
+
the island needs a scheme that fits that ground: data-theme="light" from
|
|
16
|
+
themes.css on a light ground. Same weight as .z-root and later, so it wins. */
|
|
17
|
+
.z-root--transparent { background: transparent; }
|
|
18
|
+
/* Deviation from the reference: bundle.css writes font-size: 14px and
|
|
19
|
+
line-height: 20px into the rule above, which is meant for the page, while the
|
|
20
|
+
documented setup puts z-root on <html> as well as on <body>. On <html> those
|
|
21
|
+
14px move 1rem from 16px to 14px for the whole document and override the size
|
|
22
|
+
the visitor set in the browser: an application that adopts the library page by
|
|
23
|
+
page keeps its old styles in rem, and all of them shrink by 12.5% the moment
|
|
24
|
+
the class is set. So the two declarations are split off into the rule below,
|
|
25
|
+
which leaves <html> out. The library needs neither value there, its components
|
|
26
|
+
compute in px. The one rem in styles/_*.css is the 1rem of .z-legacy below,
|
|
27
|
+
which hands the visitor's size back to a page that is not migrated; the only
|
|
28
|
+
other relative lengths are em on letter-spacing and text-underline-offset,
|
|
29
|
+
which refer to the element's own font size.
|
|
30
|
+
:where() adds (0,0,0), so the rule still weighs (0,1,0), exactly what .z-root
|
|
31
|
+
weighed before: body.z-root and every nested page container keep the same
|
|
32
|
+
weight against an application class on the same element, a tie decided by
|
|
33
|
+
source order. And since the library now sets nothing at all on <html>, an
|
|
34
|
+
application's own html { font-size: … } is the only author rule there and wins
|
|
35
|
+
at every specificity and in either include order, the html { font-size:
|
|
36
|
+
var(--base-font-size) } of a user setting included.
|
|
37
|
+
Not html.z-root { font-size: 100% }: that is (0,1,1) and beat exactly that
|
|
38
|
+
application rule (measured: --base-font-size 14px and 18px both rendered
|
|
39
|
+
16px). Not html:where(.z-root) either: :where() adds nothing, so it is the
|
|
40
|
+
tag alone at (0,0,1) and loses to .z-root (0,1,0) in any order, leaving
|
|
41
|
+
<html> at the 14px it is meant to shed.
|
|
42
|
+
body.z-root carries the class itself and keeps 14px/20px; every component
|
|
43
|
+
inherits from there, and the CDK overlay container hangs on <body> too, so
|
|
44
|
+
overlays are unaffected. The class therefore has to sit on BOTH elements, as
|
|
45
|
+
the README prescribes: with z-root on <html> alone the page renders at
|
|
46
|
+
16px/normal, because nothing sets the page size any more. */
|
|
47
|
+
.z-root:where(:not(html)) { font-size: 14px; line-height: 20px; }
|
|
48
|
+
/* Addition to the reference: .z-legacy, the class for a subtree that is not
|
|
49
|
+
migrated yet (docs/legacy.md). An application that adopts the library route
|
|
50
|
+
by route has z-root on <html> and <body>, and body.z-root hands 14px/20px
|
|
51
|
+
down to every old page. The absolute line height is what breaks them: a
|
|
52
|
+
legacy h2 of 27.2px on a 20px line overlaps itself as soon as it wraps. A
|
|
53
|
+
reset on <body> would take the shell apart, so the reset sits on the content
|
|
54
|
+
host, and a migrated page puts z-root onto its own container again:
|
|
55
|
+
html.z-root > body.z-root > .z-legacy > .z-root.
|
|
56
|
+
|
|
57
|
+
1. Inherited values. Size and line height go back to what a page has
|
|
58
|
+
without the library, font smoothing too. Family, colour, background and
|
|
59
|
+
color-scheme stay as body.z-root set them: the library cannot know what
|
|
60
|
+
the old <body> rule said, and text in the shell's family and colour on
|
|
61
|
+
the shell's ground is the readable fallback. The application restates
|
|
62
|
+
them on the same element (snippet in docs/legacy.md). No custom property
|
|
63
|
+
for that: it would be a second way to write the same two declarations.
|
|
64
|
+
|
|
65
|
+
2. Base rules for bare elements stop at .z-legacy. Every rule of the library
|
|
66
|
+
that styles an element without a library class is written twice:
|
|
67
|
+
.z-root X:not(:where(.z-legacy, .z-legacy *)),
|
|
68
|
+
:where(.z-legacy) .z-root X
|
|
69
|
+
The first selector leaves out the host and everything in it, the second
|
|
70
|
+
is what lets a z-root inside .z-legacy switch the rule on again. Both
|
|
71
|
+
:where() add (0,0,0), so each rule weighs exactly what it weighed before
|
|
72
|
+
and every specificity written down in this file still holds, including
|
|
73
|
+
the tie with an application class at (0,1,0) and all a.z-* counter-rules.
|
|
74
|
+
The host is part of the old page: an old main { width: 600px; padding:
|
|
75
|
+
0 24px } is 648px wide without the library and has to stay 648px.
|
|
76
|
+
The focus rule has a third selector, :where(.z-legacy) .z-root:focus-visible,
|
|
77
|
+
for the nested container itself, the usual target of a skip link; outside
|
|
78
|
+
.z-legacy a page container is a descendant of body.z-root and gets the
|
|
79
|
+
ring that way.
|
|
80
|
+
|
|
81
|
+
What it costs. The exclusion sits on the universal selector, so every
|
|
82
|
+
element pays for it on a full style recalculation. Production build,
|
|
83
|
+
RecalcStyleDuration over CDP, forced full recalc, median of 40 rounds of
|
|
84
|
+
10, without and with the exclusion: /daten (647 elements) 1.21 to 1.39 ms;
|
|
85
|
+
/muster/server-panel with 2000 console lines (4130 elements) 4.22 to
|
|
86
|
+
4.9 ms, at 4x CPU throttling 23.9 to 27.8 ms. That is +14 to +18 % on
|
|
87
|
+
that one step, about 0.7 ms on the large page, and all of it comes from
|
|
88
|
+
the universal rule. A first version that said the same in one selector,
|
|
89
|
+
:not(:where(.z-legacy :not(.z-legacy .z-root, .z-legacy .z-root *))),
|
|
90
|
+
cost +25 % (5.27 ms) and did not cover the host. Covering the host is
|
|
91
|
+
free: with and without it measured the same within the noise.
|
|
92
|
+
Resetting instead (.z-legacy :where(*) { box-sizing: content-box }) is
|
|
93
|
+
cheaper and wrong: it flips an old * { box-sizing: border-box } and the
|
|
94
|
+
border-box the browser gives select and button by default.
|
|
95
|
+
The rules, all in this file (the other partials style classes only):
|
|
96
|
+
.z-root * box-sizing exempt
|
|
97
|
+
.z-root :where(button, input, select, ...) font, color exempt
|
|
98
|
+
.z-root :focus-visible outline exempt
|
|
99
|
+
.z-root :where(a), :where(a):hover color, line exempt
|
|
100
|
+
.z-root :where(p, li, ...) a:not(...) underline exempt
|
|
101
|
+
Kept: .z-root itself, .z-root:where(:not(html)) and html.z-root /
|
|
102
|
+
body.z-root (they match the class, not a bare element; the size rule is
|
|
103
|
+
the one .z-legacy overrides by source order), :where(:root) color-scheme and
|
|
104
|
+
.z-root:has(.z-stickybar[data-stuck]) (document level), and every class
|
|
105
|
+
rule.
|
|
106
|
+
The focus ring is exempt as well, for a measured reason: focus is white
|
|
107
|
+
in the dark scheme, and outline from this file REPLACES the two-tone ring
|
|
108
|
+
the browser draws by default. On the light surface of the old page in
|
|
109
|
+
the demo (/muster/legacy) that is white on near white, 1.04:1, where the
|
|
110
|
+
browser's own ring was visible. Opting in is one line in the
|
|
111
|
+
application (.z-legacy :focus-visible { outline: 2px solid ...; }),
|
|
112
|
+
opting out would be impossible: revert rolls back to the browser's
|
|
113
|
+
stylesheet, not to the application's rule.
|
|
114
|
+
|
|
115
|
+
3. Alternatives, measured. @scope (.z-root) to (.z-legacy) is the natural
|
|
116
|
+
fit, but Angular 22 builds for "baseline widely available on 2026-05-07",
|
|
117
|
+
which includes Firefox 119 to 145 and Safari 17.0 to 17.3 without @scope;
|
|
118
|
+
they would drop every base rule. color: revert inside .z-legacy turned a
|
|
119
|
+
legacy link from its own colour to the browser's blue and a legacy button
|
|
120
|
+
from 13px to 13.33px, because revert skips every author rule, and it
|
|
121
|
+
cannot be undone by a nested z-root.
|
|
122
|
+
|
|
123
|
+
Ceiling: one level of re-entry. A .z-legacy inside a z-root inside a
|
|
124
|
+
.z-legacy counts as migrated (the base rules apply, while the inherited
|
|
125
|
+
values are the 1rem/normal of that inner .z-legacy); nearest-ancestor
|
|
126
|
+
matching needs @scope. .z-legacy never goes on <body>: the CDK overlay
|
|
127
|
+
container hangs there and would be part of the old page.
|
|
128
|
+
A library component placed straight into .z-legacy, without a z-root around
|
|
129
|
+
it, loses exactly the rules listed under 2; docs/legacy.md has the list. */
|
|
130
|
+
.z-legacy { font-size: 1rem; line-height: normal; -webkit-font-smoothing: auto; }
|
|
131
|
+
/* Deviation from the reference: bundle.css lists the four elements one by one
|
|
132
|
+
(specificity 0,1,1) and thereby beats .z-btn--primary/--ghost/--danger
|
|
133
|
+
(0,1,0). :where() lowers the rule to 0,1,0, the values stay the same. */
|
|
134
|
+
.z-root :where(button, input, select, textarea):not(:where(.z-legacy, .z-legacy *)), :where(.z-legacy) .z-root :where(button, input, select, textarea) { font: inherit; color: inherit; }
|
|
135
|
+
.z-root :focus-visible:not(:where(.z-legacy, .z-legacy *)), :where(.z-legacy) .z-root:focus-visible, :where(.z-legacy) .z-root :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
136
|
+
/* Deviation from the reference: bundle.css writes .z-root a (0,1,1), which beats
|
|
137
|
+
every class an application can put on a link (0,1,0). In the first real
|
|
138
|
+
integration that made the application's own skip link red on red (1.29:1) and
|
|
139
|
+
recoloured every link of the pages not migrated yet, the moment z-root went
|
|
140
|
+
onto <html>. :where() lowers the two rules to (0,1,0) and (0,2,0); the values
|
|
141
|
+
are unchanged. It is the same fix the rule above got for
|
|
142
|
+
:where(button, input, select, textarea).
|
|
143
|
+
|
|
144
|
+
What that buys, exactly. At (0,1,0) an application class on a link now TIES
|
|
145
|
+
with this rule, and a tie is decided by source order, not by specificity: the
|
|
146
|
+
application stylesheet has to be loaded AFTER zenit-ui.css. The style order
|
|
147
|
+
the README and ng-add prescribe does that (tokens, cdk, zenit-ui, then
|
|
148
|
+
src/styles.css). Measured: a rule .lg { color: … } placed before zenit-ui.css
|
|
149
|
+
still loses and the link stays accent-text with an underline on hover; placed
|
|
150
|
+
after it, it wins. The hover rule weighs (0,2,0), so an application that wants
|
|
151
|
+
to change the hover needs .lg:hover, not .lg.
|
|
152
|
+
|
|
153
|
+
The price. An application rule at (0,1,1), such as .app a, now also reaches
|
|
154
|
+
plain links INSIDE library components: panel, alert, row, table, empty state,
|
|
155
|
+
the sub line of the page header, header, tabs, sidebar, stepper, toast, the
|
|
156
|
+
body of a dialog, tooltip and menu. It does not reach the footer lists
|
|
157
|
+
(.z-root .z-footer__list a and .z-footer__base a, both (0,2,1)) and no link
|
|
158
|
+
that carries a library class, because each of those has an a. counter-rule at
|
|
159
|
+
(0,2,1) or more: a.z-btn with its five variants and the Minecraft primary,
|
|
160
|
+
a.z-skip-link, a.z-tab, a.z-side__item, a.z-header__link, a.z-row,
|
|
161
|
+
a.z-row__link and a.z-game.
|
|
162
|
+
|
|
163
|
+
That counter-rule is the pattern to keep. No anchor the library renders today
|
|
164
|
+
carries a class without one, which is why lowering the base rule changes
|
|
165
|
+
nothing here (measured: identical computed color, text-decoration-line and
|
|
166
|
+
text-decoration-color for all 299 anchors of the demo and all 26 of the
|
|
167
|
+
example application, in dark, light and contrast). A new directive that hosts
|
|
168
|
+
on an anchor needs the same: its class rule has to be written .z-root a.z-xyz,
|
|
169
|
+
not .z-xyz, or an application rule at (0,1,1) will beat it.
|
|
170
|
+
|
|
171
|
+
The underline for links in running text below stays at (0,2,1) and therefore
|
|
172
|
+
cannot be switched off from an application stylesheet. Its own escape hatch is
|
|
173
|
+
a:not([class*="z-"]), so any class whose name contains "z-" opts a link out,
|
|
174
|
+
including by accident ("quiz-link"). That is a side effect of the selector,
|
|
175
|
+
not a documented API; the documented way to keep the library out of a
|
|
176
|
+
subtree is .z-legacy above. */
|
|
177
|
+
.z-root :where(a):not(:where(.z-legacy, .z-legacy *)), :where(.z-legacy) .z-root :where(a) { color: var(--accent-text); text-decoration: none; }
|
|
178
|
+
.z-root :where(a):hover:not(:where(.z-legacy, .z-legacy *)), :where(.z-legacy) .z-root :where(a):hover { text-decoration: underline; }
|
|
179
|
+
/* Deviation from the reference: bundle.css underlines a link on hover only. A
|
|
180
|
+
link inside running text then differs from the words around it by colour
|
|
181
|
+
alone, and accent-text against text stays below 3:1 in 11 of the 12 scheme
|
|
182
|
+
and accent combinations (dark/rot reaches 3.27:1), which fails WCAG 1.4.1
|
|
183
|
+
(axe: link-in-text-block). No colour can fix that in every scheme, so links
|
|
184
|
+
in running text are underlined at rest.
|
|
185
|
+
Where: inside the text containers listed below. Not inside nav, .z-header
|
|
186
|
+
and .z-footer, whose links stand alone and are recognisable by position, and
|
|
187
|
+
not on a link that carries a library class (z-btn, z-tab, z-row, z-side__item,
|
|
188
|
+
z-header__link ...), because those style themselves.
|
|
189
|
+
0.15em: text-underline-offset has no token, and a length relative to the
|
|
190
|
+
font size is the only kind that fits 12px hints and 16px body alike; it
|
|
191
|
+
moves the line clear of the descenders of g, j and y. No px literal is added.
|
|
192
|
+
Weight (0,2,1), the same as .z-root a.z-btn below. */
|
|
193
|
+
.z-root :where(p, li, dd, label, .z-alert__body, .z-faq__body, .z-empty__body, .z-field__hint):not(:where(nav, .z-header, .z-footer) *) a:not([class*="z-"]):not(:where(.z-legacy, .z-legacy *)),
|
|
194
|
+
:where(.z-legacy) .z-root :where(p, li, dd, label, .z-alert__body, .z-faq__body, .z-empty__body, .z-field__hint):not(:where(nav, .z-header, .z-footer) *) a:not([class*="z-"]) {
|
|
195
|
+
text-decoration: underline;
|
|
196
|
+
text-underline-offset: 0.15em;
|
|
197
|
+
}
|
|
198
|
+
/* Deviation from the reference: a link in the text of an alert takes text, not
|
|
199
|
+
accent-text. On the tints of the four statuses accent-text misses 4.5:1 in
|
|
200
|
+
the normative dark scheme (measured with tools/check-theme-contrast.mjs:
|
|
201
|
+
info and warning over surface 4.38 and 4.37, all four over surface-raised
|
|
202
|
+
4.04 to 4.23); text reaches 12.36 or more in every scheme and accent. The
|
|
203
|
+
running-text underline above stays, so the link is told apart from the
|
|
204
|
+
text-muted body by colour and line, and the alert's one button is not red
|
|
205
|
+
either. The same weight and the same .z-legacy exemption as the link rule
|
|
206
|
+
it overrides, so an application class on the link still wins. */
|
|
207
|
+
.z-root :where(.z-alert__body a):not(:where(.z-legacy, .z-legacy *)), :where(.z-legacy) .z-root :where(.z-alert__body a) { color: var(--text); }
|
|
208
|
+
.z-mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
209
|
+
.z-muted { color: var(--text-muted); }
|
|
210
|
+
.z-subtle { color: var(--text-subtle); }
|
|
211
|
+
.z-icon { font-size: var(--icon); width: var(--icon); height: var(--icon); overflow: hidden; color: var(--text-muted); flex: none; }
|
|
212
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
213
|
+
.z-btn, .z-input, .z-select select, .z-row, .z-side__item, .z-tab, .z-game { transition: color 150ms, background-color 150ms, border-color 150ms; }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Button */
|
|
217
|
+
.z-btn {
|
|
218
|
+
display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
|
|
219
|
+
height: var(--control-md); padding: 0 var(--space-4);
|
|
220
|
+
border: 1px solid transparent; border-radius: var(--radius-md);
|
|
221
|
+
background: transparent; color: var(--text);
|
|
222
|
+
font-size: 14px; line-height: 20px; font-weight: 500; white-space: nowrap; cursor: pointer;
|
|
223
|
+
}
|
|
224
|
+
.z-btn--primary { background: var(--accent); color: var(--on-accent); }
|
|
225
|
+
.z-btn--primary:hover { background: var(--accent-hover); }
|
|
226
|
+
.z-btn--primary .z-icon { color: var(--on-accent); }
|
|
227
|
+
.z-btn--secondary { border-color: var(--border-control); }
|
|
228
|
+
.z-btn--secondary:hover { background: var(--surface-hover); }
|
|
229
|
+
.z-btn--ghost { color: var(--text-muted); }
|
|
230
|
+
.z-btn--ghost:hover { color: var(--text); background: var(--surface-raised); }
|
|
231
|
+
.z-btn--danger { border-color: var(--danger); color: var(--danger); }
|
|
232
|
+
.z-btn--danger:hover { background: var(--danger-subtle); }
|
|
233
|
+
.z-btn--danger .z-icon { color: var(--danger); }
|
|
234
|
+
.z-btn--sm { height: var(--control-sm); padding: 0 var(--space-3); }
|
|
235
|
+
.z-btn--lg { height: var(--control-lg); padding: 0 var(--space-5); font-size: 16px; line-height: 24px; }
|
|
236
|
+
.z-btn--icon { width: var(--control-md); padding: 0; }
|
|
237
|
+
.z-btn--icon.z-btn--sm { width: var(--control-sm); }
|
|
238
|
+
.z-btn--block { width: 100%; }
|
|
239
|
+
.z-btn:disabled, .z-btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
|
|
240
|
+
.z-theme-mc .z-btn--primary { background: var(--mc-accent); color: var(--on-mc); }
|
|
241
|
+
.z-theme-mc .z-btn--primary:hover { background: var(--mc-accent-hover); }
|
|
242
|
+
.z-theme-mc .z-btn--primary .z-icon { color: var(--on-mc); }
|
|
243
|
+
|
|
244
|
+
/* Badge */
|
|
245
|
+
.z-badge {
|
|
246
|
+
display: inline-flex; align-items: center; gap: var(--space-1);
|
|
247
|
+
height: 20px; padding: 0 var(--space-2);
|
|
248
|
+
border-radius: var(--radius-sm);
|
|
249
|
+
background: var(--surface-raised); color: var(--text-muted);
|
|
250
|
+
font-size: 12px; line-height: 16px; font-weight: 500; white-space: nowrap;
|
|
251
|
+
}
|
|
252
|
+
.z-badge__dot { width: 6px; height: 6px; border-radius: var(--radius-full); background: currentColor; }
|
|
253
|
+
.z-badge--success { background: var(--success-subtle); color: var(--success); }
|
|
254
|
+
.z-badge--warning { background: var(--warning-subtle); color: var(--warning); }
|
|
255
|
+
.z-badge--danger { background: var(--danger-subtle); color: var(--danger); }
|
|
256
|
+
.z-badge--info { background: var(--info-subtle); color: var(--info); }
|
|
257
|
+
|
|
258
|
+
/* Field, Input, Select */
|
|
259
|
+
.z-field { display: grid; gap: var(--space-1); min-width: 0; align-content: start; }
|
|
260
|
+
.z-field__label { font-size: 14px; line-height: 20px; font-weight: 600; }
|
|
261
|
+
.z-field__hint { font-size: 12px; line-height: 16px; color: var(--text-subtle); }
|
|
262
|
+
.z-field__error { font-size: 12px; line-height: 16px; color: var(--danger); }
|
|
263
|
+
.z-input, .z-select select {
|
|
264
|
+
width: 100%; height: var(--control-md); padding: 0 var(--space-3);
|
|
265
|
+
border: 1px solid var(--border-control); border-radius: var(--radius-sm);
|
|
266
|
+
background: var(--surface); color: var(--text);
|
|
267
|
+
}
|
|
268
|
+
.z-input::placeholder { color: var(--text-subtle); }
|
|
269
|
+
.z-input:hover, .z-select select:hover { border-color: var(--text-muted); }
|
|
270
|
+
.z-input[aria-invalid="true"] { border-color: var(--danger); }
|
|
271
|
+
.z-input:disabled, .z-select select:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
272
|
+
.z-input--sm, .z-select--sm select { height: var(--control-sm); }
|
|
273
|
+
.z-input-wrap { position: relative; display: block; }
|
|
274
|
+
.z-input-wrap .z-icon { position: absolute; left: var(--space-3); top: 50%; margin-top: -10px; pointer-events: none; }
|
|
275
|
+
.z-input-wrap .z-input { padding-left: 40px; }
|
|
276
|
+
.z-select { position: relative; display: block; }
|
|
277
|
+
.z-select select { appearance: none; -webkit-appearance: none; padding-right: 36px; cursor: pointer; }
|
|
278
|
+
.z-select::after {
|
|
279
|
+
content: ""; position: absolute; right: 14px; top: 50%; width: 7px; height: 7px; margin-top: -6px;
|
|
280
|
+
border-right: 1.5px solid var(--text-muted); border-bottom: 1.5px solid var(--text-muted);
|
|
281
|
+
transform: rotate(45deg); pointer-events: none;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* Panel */
|
|
285
|
+
.z-panel { border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); min-width: 0; }
|
|
286
|
+
.z-panel__header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); }
|
|
287
|
+
.z-panel__title { margin: 0; font-size: 14px; line-height: 20px; font-weight: 600; }
|
|
288
|
+
.z-panel__body { padding: var(--space-4); }
|
|
289
|
+
.z-panel__body--flush { padding: 0; }
|
|
290
|
+
|
|
291
|
+
/* ---------- Angular hosts and additions of the library ---------- */
|
|
292
|
+
|
|
293
|
+
html.z-root, body.z-root { margin: 0; min-height: 100%; }
|
|
294
|
+
.z-root a.z-btn, .z-root a.z-btn:hover { text-decoration: none; }
|
|
295
|
+
/* Gap in the reference: .z-root a (0,1,1) beats .z-btn (0,1,0), so every button
|
|
296
|
+
written as a link fell back to accent-text. bundle.css closes the gap for
|
|
297
|
+
--primary and --secondary only; the base button and the ghost and danger
|
|
298
|
+
variants stayed red. The rule below restores text for the bare a.z-btn and
|
|
299
|
+
with it for the sizes, which carry no colour of their own (--sm, --lg,
|
|
300
|
+
--icon, --block).
|
|
301
|
+
It has to stand first: it and the four variant rules all weigh (0,2,1), so
|
|
302
|
+
only source order decides. What still wins after it, in this order:
|
|
303
|
+
--primary (on-accent), --secondary (text), --ghost (text-muted, text on
|
|
304
|
+
hover), --danger (danger) and, at (0,3,1), the Minecraft primary link
|
|
305
|
+
.z-root .z-theme-mc a.z-btn--primary (on-mc). The underline is handled one
|
|
306
|
+
line above, for the resting and the hover state together, so a.z-btn:hover
|
|
307
|
+
stays free of it in every variant. */
|
|
308
|
+
.z-root a.z-btn { color: var(--text); }
|
|
309
|
+
.z-root a.z-btn--primary { color: var(--on-accent); }
|
|
310
|
+
.z-root a.z-btn--secondary { color: var(--text); }
|
|
311
|
+
.z-root a.z-btn--ghost { color: var(--text-muted); }
|
|
312
|
+
.z-root a.z-btn--ghost:hover { color: var(--text); }
|
|
313
|
+
.z-root a.z-btn--danger { color: var(--danger); }
|
|
314
|
+
.z-root .z-theme-mc a.z-btn--primary { color: var(--on-mc); }
|
|
315
|
+
|
|
316
|
+
.z-panel, .z-field, .z-select, .z-input-wrap { display: block; }
|
|
317
|
+
.z-field { display: grid; }
|
|
318
|
+
.z-icon--sm { font-size: 16px; width: 16px; height: 16px; }
|
|
319
|
+
.z-icon { line-height: 1; user-select: none; }
|
|
320
|
+
textarea.z-input { height: auto; min-height: 96px; padding: var(--space-2) var(--space-3); resize: vertical; }
|
|
321
|
+
.z-input--mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
322
|
+
/* .z-skip-link shares the technique: it is out of sight until it takes focus,
|
|
323
|
+
and the block at the end of this file unfolds it again. */
|
|
324
|
+
.z-visually-hidden, .z-skip-link { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
325
|
+
|
|
326
|
+
/* Spinner */
|
|
327
|
+
.z-spinner { display: inline-block; flex: none; width: 16px; height: 16px; border: 2px solid currentColor; border-right-color: transparent; border-radius: var(--radius-full); opacity: 0.8; }
|
|
328
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
329
|
+
.z-spinner { animation: z-spin 0.7s linear infinite; }
|
|
330
|
+
@keyframes z-spin { to { transform: rotate(360deg); } }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* Layout helpers */
|
|
334
|
+
.z-container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--space-4); }
|
|
335
|
+
@media (min-width: 640px) { .z-container { padding-inline: var(--space-5); } }
|
|
336
|
+
.z-stack { display: grid; gap: var(--space-5); }
|
|
337
|
+
.z-cluster { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
|
|
338
|
+
.z-section { padding-block: var(--space-8); border-top: 1px solid var(--border); }
|
|
339
|
+
@media (min-width: 900px) { .z-section { padding-block: var(--space-9); } }
|
|
340
|
+
.z-panel-shell { display: grid; gap: var(--space-5); grid-template-columns: minmax(0, 1fr); }
|
|
341
|
+
@media (min-width: 900px) { .z-panel-shell { grid-template-columns: var(--sidebar) minmax(0, 1fr); align-items: start; } }
|
|
342
|
+
|
|
343
|
+
/* Addition to the reference: the dark colour scheme as the default of the base
|
|
344
|
+
styles. It tells the engine which palette to use for the parts a page cannot
|
|
345
|
+
style itself: the popup of a native <select>, the calendar and clock popups
|
|
346
|
+
and their indicators, the spin buttons of a number field, scrollbars, the
|
|
347
|
+
caret and autofill. Without it a page that follows the documented minimum
|
|
348
|
+
(tokens.css plus zenit-ui.css) draws light native UI on the dark ground.
|
|
349
|
+
tokens.css is compiled from spec/tokens.json and stays byte identical to
|
|
350
|
+
spec/tokens.css, so the declaration cannot live there; themes/base.css is
|
|
351
|
+
opt-in and does not reach that minimum either.
|
|
352
|
+
|
|
353
|
+
:where() keeps the rule at specificity (0,0,0), which is what makes it safe
|
|
354
|
+
in both include orders the repository uses: themes.css before zenit-ui.css
|
|
355
|
+
(docs/theming.md, beispiel-app) and after it (ui-demo styles.css, where the
|
|
356
|
+
application stylesheet is loaded last). A bare :root would weigh (0,1,0),
|
|
357
|
+
the same as [data-theme="light"], and would then beat the light scheme
|
|
358
|
+
wherever it happened to come later. At (0,0,0) every scheme block wins on
|
|
359
|
+
specificity alone, in any order.
|
|
360
|
+
|
|
361
|
+
themes/base.css keeps its own declaration: it carries [data-theme="dark"],
|
|
362
|
+
which this rule cannot cover, for a dark subtree inside a light page. */
|
|
363
|
+
:where(:root) { color-scheme: dark; }
|
|
364
|
+
|
|
365
|
+
/* Addition to the reference: click targets at least 40px on mobile (CLAUDE.md, Abstand und Layout). */
|
|
366
|
+
@media (max-width: 640px) {
|
|
367
|
+
.z-btn--sm { height: var(--control-md); }
|
|
368
|
+
.z-btn--icon.z-btn--sm { width: var(--control-md); }
|
|
369
|
+
.z-input--sm, .z-select--sm select { height: var(--control-md); }
|
|
370
|
+
.z-panel__header a:not(.z-btn) { display: inline-flex; align-items: center; min-height: var(--control-md); }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/* Addition to the reference: the control border of the secondary button inside
|
|
374
|
+
an alert. border-control is dimensioned for surface and reaches 3:1 there,
|
|
375
|
+
but not on a tinted alert. With the markup the Alert README prescribes (a
|
|
376
|
+
secondary sm button in the alert) the dark scheme measures 2.88 (info), 2.87
|
|
377
|
+
(warning) and 2.98 (danger) against an alert on bg, and 2.65 to 2.77 against
|
|
378
|
+
one inside a panel on surface. text-muted is the next token up and clears
|
|
379
|
+
3:1 on every tint in every scheme, worst case 6.29 (dark, warning on
|
|
380
|
+
surface); .z-input:hover already uses it as a control border. The neutral
|
|
381
|
+
alert is opaque surface-raised, where text-muted is guarded at 7:1 already.
|
|
382
|
+
The rule belongs to the Alert component and would sit in _rueckmeldung.css;
|
|
383
|
+
that file is being changed elsewhere at the same time, so it lives here.
|
|
384
|
+
tools/check-theme-contrast.mjs guards the pairs for every scheme x accent. */
|
|
385
|
+
.z-alert .z-btn--secondary { border-color: var(--text-muted); }
|
|
386
|
+
|
|
387
|
+
/* Addition to the reference: the skip link, the first tab stop of every page
|
|
388
|
+
(15-zustaende.md, "Tastatur"). The reference ships no block for it, and an
|
|
389
|
+
application cannot supply one either: .z-root a (0,1,1) used to beat any
|
|
390
|
+
application class (0,1,0), which is how the first integration ended up with a
|
|
391
|
+
red link on the red accent surface at 1.29:1. Both ends are fixed now, the
|
|
392
|
+
base rule with :where() and this rule at (0,2,1) / (0,3,1), which wins in
|
|
393
|
+
either direction.
|
|
394
|
+
Hidden state: the .z-visually-hidden declarations above, so the technique is
|
|
395
|
+
written once.
|
|
396
|
+
z-index: the link has to clear everything a page may stack over it, header
|
|
397
|
+
(100), overlay and scrim (1000) and toast (1100). --z-toast is the highest
|
|
398
|
+
token tokens.css defines, and a literal above it is not allowed, so it is the
|
|
399
|
+
top of the documented stacking order. A toast can only tie with it, and the
|
|
400
|
+
two never meet: the link is visible while it has focus, and focusing it is
|
|
401
|
+
the first thing that happens on a page.
|
|
402
|
+
:focus, not :focus, :focus-visible: :focus-visible only ever matches an
|
|
403
|
+
element that also matches :focus, so the one selector covers the keyboard tab
|
|
404
|
+
and a focus set from code alike. The global 2px focus ring stays untouched.
|
|
405
|
+
Colour: text on surface-raised, a pair the contrast gate already measures in
|
|
406
|
+
all three schemes (14.77:1 light, 16.26:1 dark, 17.85:1 contrast, gated at
|
|
407
|
+
12:1, tools/check-theme-contrast.mjs). The hover state keeps the
|
|
408
|
+
colour and drops the underline, so there is no fight with .z-root a:hover. */
|
|
409
|
+
.z-root a.z-skip-link, .z-root a.z-skip-link:hover { color: var(--text); text-decoration: none; }
|
|
410
|
+
.z-root a.z-skip-link:focus {
|
|
411
|
+
position: fixed; top: var(--space-2); left: var(--space-4); z-index: var(--z-toast);
|
|
412
|
+
display: inline-flex; align-items: center;
|
|
413
|
+
width: auto; height: auto; min-height: var(--control-md); margin: 0; padding: 0 var(--space-4);
|
|
414
|
+
border: 1px solid var(--border-control); border-radius: var(--radius-md);
|
|
415
|
+
background: var(--surface-raised);
|
|
416
|
+
clip-path: none;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* Addition to the reference: the error frame of a native select. bundle.css
|
|
420
|
+
styles .z-input[aria-invalid="true"] only, while 15-zustaende.md asks every
|
|
421
|
+
field in error for a danger border plus the sentence below it, the select
|
|
422
|
+
included. Same declaration as the input, so both fields read alike. The
|
|
423
|
+
caller sets aria-invalid on the native <select>, as with input[zInput]. */
|
|
424
|
+
.z-select select[aria-invalid="true"] { border-color: var(--danger); }
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/* Package konfigurator: OptionCard, Combobox, Wizard, Config, StickyBar,
|
|
2
|
+
IncludedList, Disclosure, InputAction, CostChart and the PriceSummary states.
|
|
3
|
+
The section "Erweiterung 3: Konfigurator" of spec/components/bundle.css
|
|
4
|
+
stands verbatim, followed by the Angular host rules and the additions. */
|
|
5
|
+
|
|
6
|
+
/* OptionCard */
|
|
7
|
+
.z-options { display: grid; grid-template-columns: repeat(auto-fill, minmax(176px, 1fr)); gap: var(--space-3); margin: 0; padding: 0; border: 0; min-width: 0; }
|
|
8
|
+
.z-options--compact { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); }
|
|
9
|
+
.z-options__legend { grid-column: 1 / -1; padding: 0; margin-bottom: var(--space-1); font-size: 14px; line-height: 20px; font-weight: 600; }
|
|
10
|
+
.z-options__legend small { margin-left: var(--space-2); font-size: 12px; font-weight: 400; color: var(--text-subtle); }
|
|
11
|
+
.z-option { position: relative; display: grid; align-content: start; gap: 2px; padding: var(--space-3) var(--space-4); border: 1px solid var(--border-control); border-radius: var(--radius-md); background: var(--surface); cursor: pointer; min-width: 0; }
|
|
12
|
+
.z-option input { position: absolute; opacity: 0; pointer-events: none; }
|
|
13
|
+
.z-option:hover { border-color: var(--text-muted); }
|
|
14
|
+
.z-option:has(input:checked) { border-color: var(--accent-text); outline: 1px solid var(--accent-text); outline-offset: -2px; background: var(--surface-raised); }
|
|
15
|
+
.z-option:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
16
|
+
.z-option:has(input:disabled) { opacity: 0.45; cursor: not-allowed; }
|
|
17
|
+
.z-theme-mc .z-option:has(input:checked) { border-color: var(--mc-accent); outline-color: var(--mc-accent); }
|
|
18
|
+
.z-option__title { font-weight: 600; }
|
|
19
|
+
.z-option__desc { font-size: 12px; line-height: 16px; color: var(--text-muted); }
|
|
20
|
+
.z-option__price { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 12px; line-height: 16px; color: var(--text-muted); }
|
|
21
|
+
.z-option__badge { position: absolute; top: calc(-1 * var(--space-3)); right: var(--space-2); background-color: var(--surface-raised); border: 1px solid var(--border); }
|
|
22
|
+
.z-options--compact .z-option { padding: var(--space-2) var(--space-3); text-align: center; justify-items: center; }
|
|
23
|
+
.z-options--compact .z-option__title { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
24
|
+
|
|
25
|
+
/* Combobox */
|
|
26
|
+
.z-combo { position: relative; display: block; }
|
|
27
|
+
.z-combo .z-input { padding-right: 36px; }
|
|
28
|
+
.z-combo::after { content: ""; position: absolute; right: 14px; top: 20px; width: 7px; height: 7px; margin-top: -6px; border-right: 1.5px solid var(--text-muted); border-bottom: 1.5px solid var(--text-muted); transform: rotate(45deg); pointer-events: none; }
|
|
29
|
+
.z-listbox { display: grid; max-height: 248px; margin-top: var(--space-1); padding: var(--space-1); overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-raised); box-shadow: var(--shadow-overlay); }
|
|
30
|
+
.z-listbox__group { padding: var(--space-2) var(--space-3) var(--space-1); font-size: 12px; line-height: 16px; color: var(--text-muted); }
|
|
31
|
+
.z-listbox__option { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); min-height: 36px; padding: 0 var(--space-3); border-radius: var(--radius-sm); cursor: pointer; }
|
|
32
|
+
.z-listbox__option:hover, .z-listbox__option--active { background: var(--surface-hover); }
|
|
33
|
+
.z-listbox__option[aria-selected="true"] { font-weight: 600; }
|
|
34
|
+
.z-listbox__option small { font-size: 12px; font-weight: 400; color: var(--text-muted); }
|
|
35
|
+
.z-listbox__empty { padding: var(--space-3); color: var(--text-muted); }
|
|
36
|
+
/* Addition to the reference: the waiting row carries a spinner next to its
|
|
37
|
+
word. A modifier and not the base rule, so the empty row keeps wrapping the
|
|
38
|
+
way it always did.
|
|
39
|
+
|
|
40
|
+
Documented deviation from CLAUDE.md, "Bewegung": that rule puts a spinner in
|
|
41
|
+
a button and skeleton rows in a list. A listbox is not that kind of list.
|
|
42
|
+
Its children have to be options, and a skeleton row is a decorative
|
|
43
|
+
placeholder with no accessible name, so it would either be an option that
|
|
44
|
+
says nothing or a node that breaks role="listbox". One locked option with a
|
|
45
|
+
spinner and the word "Lädt" is the only shape that stays announceable, and
|
|
46
|
+
the row count is one, not the row count of an answer nobody knows yet. */
|
|
47
|
+
.z-listbox__loading { display: flex; align-items: center; gap: var(--space-2); }
|
|
48
|
+
|
|
49
|
+
/* Wizard */
|
|
50
|
+
.z-wizard { display: grid; gap: var(--space-3); margin: 0; padding: 0; list-style: none; }
|
|
51
|
+
.z-wstep { border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); min-width: 0; }
|
|
52
|
+
.z-wstep__head { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); }
|
|
53
|
+
.z-wstep__title { margin: 0; font-size: 14px; line-height: 20px; font-weight: 600; }
|
|
54
|
+
.z-wstep__summary { margin-left: auto; color: var(--text-muted); text-align: right; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
55
|
+
.z-wstep__edit { flex: none; padding: 0; border: 0; background: none; cursor: pointer; color: var(--accent-text); font-weight: 500; }
|
|
56
|
+
.z-wstep__body { display: grid; gap: var(--space-5); padding: var(--space-2) var(--space-4) var(--space-5); border-top: 1px solid var(--border); }
|
|
57
|
+
.z-wstep__actions { display: flex; justify-content: space-between; gap: var(--space-2); }
|
|
58
|
+
.z-wstep--locked .z-wstep__head { color: var(--text-muted); }
|
|
59
|
+
.z-wstep--locked .z-wstep__title { font-weight: 400; }
|
|
60
|
+
.z-wstep[aria-current="step"] .z-step__num { border-color: var(--text); background: var(--text); color: var(--bg); }
|
|
61
|
+
.z-wstep--done .z-step__num { border-color: transparent; background: var(--surface-hover); color: var(--text); }
|
|
62
|
+
.z-config { display: grid; gap: var(--space-5); grid-template-columns: minmax(0, 1fr); align-items: start; }
|
|
63
|
+
@media (min-width: 900px) { .z-config { grid-template-columns: minmax(0, 1fr) 340px; } .z-config__aside { position: sticky; top: calc(var(--header) + var(--space-5)); } }
|
|
64
|
+
|
|
65
|
+
/* PriceSummary: Zustände */
|
|
66
|
+
.z-summary__price--pending { color: var(--text-muted); }
|
|
67
|
+
.z-summary__discount dd { color: var(--success); }
|
|
68
|
+
.z-summary__total { display: flex; justify-content: space-between; gap: var(--space-4); padding-top: var(--space-3); border-top: 1px solid var(--border-control); font-weight: 600; }
|
|
69
|
+
.z-summary__total dd { margin: 0; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
70
|
+
|
|
71
|
+
/* StickyBar */
|
|
72
|
+
.z-stickybar { position: sticky; bottom: 0; z-index: var(--z-header); display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding: var(--space-3) var(--space-4); border-top: 1px solid var(--border); background: var(--surface-raised); }
|
|
73
|
+
.z-stickybar__price { display: grid; font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 20px; line-height: 28px; font-weight: 600; }
|
|
74
|
+
.z-stickybar__price small { font-family: var(--font-body); font-size: 12px; line-height: 16px; font-weight: 400; color: var(--text-muted); }
|
|
75
|
+
@media (min-width: 900px) { .z-stickybar--mobile { display: none; } }
|
|
76
|
+
|
|
77
|
+
/* IncludedList */
|
|
78
|
+
.z-included { display: grid; gap: var(--space-2); margin: 0; padding: 0; list-style: none; color: var(--text-muted); }
|
|
79
|
+
.z-included li { display: flex; align-items: flex-start; gap: var(--space-2); }
|
|
80
|
+
.z-included .z-icon { font-size: 16px; width: 16px; height: 16px; margin-top: 2px; color: var(--success); }
|
|
81
|
+
|
|
82
|
+
/* Disclosure */
|
|
83
|
+
.z-disclosure { border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); }
|
|
84
|
+
.z-disclosure summary { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); cursor: pointer; list-style: none; font-weight: 600; }
|
|
85
|
+
.z-disclosure summary::-webkit-details-marker { display: none; }
|
|
86
|
+
.z-disclosure summary small { font-size: 12px; font-weight: 400; color: var(--text-muted); }
|
|
87
|
+
.z-disclosure summary::after { content: "+"; margin-left: auto; font-family: var(--font-mono); color: var(--text-muted); }
|
|
88
|
+
.z-disclosure[open] summary::after { content: "\2212"; }
|
|
89
|
+
.z-disclosure__body { display: grid; gap: var(--space-4); padding: var(--space-4); border-top: 1px solid var(--border); }
|
|
90
|
+
|
|
91
|
+
/* InputAction */
|
|
92
|
+
.z-input-action { display: flex; gap: var(--space-2); }
|
|
93
|
+
.z-input-action .z-input { flex: 1; min-width: 0; }
|
|
94
|
+
.z-field__success { font-size: 12px; line-height: 16px; color: var(--success); }
|
|
95
|
+
|
|
96
|
+
/* CostChart */
|
|
97
|
+
.z-chart { display: grid; gap: var(--space-3); min-width: 0; }
|
|
98
|
+
.z-chart__figures { display: flex; flex-wrap: wrap; gap: var(--space-6); }
|
|
99
|
+
.z-chart__figure { display: grid; }
|
|
100
|
+
.z-chart__figure span { font-size: 12px; line-height: 16px; color: var(--text-muted); }
|
|
101
|
+
.z-chart__figure strong { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 20px; line-height: 28px; font-weight: 600; }
|
|
102
|
+
.z-chart__plot { position: relative; }
|
|
103
|
+
.z-chart svg { display: block; width: 100%; height: auto; overflow: visible; }
|
|
104
|
+
.z-chart__grid { stroke: var(--border); stroke-width: 1; }
|
|
105
|
+
.z-chart__axis { fill: var(--text-subtle); font-family: var(--font-mono); font-size: 12px; }
|
|
106
|
+
.z-chart__line { fill: none; stroke: var(--text); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
|
|
107
|
+
.z-chart__cap { stroke: var(--border-control); stroke-width: 1; stroke-dasharray: 4 4; }
|
|
108
|
+
.z-chart__marker { fill: var(--text); stroke: var(--surface); stroke-width: 2; }
|
|
109
|
+
.z-chart__label { fill: var(--text-muted); font-family: var(--font-body); font-size: 12px; }
|
|
110
|
+
.z-chart__cross { stroke: var(--border-control); stroke-width: 1; }
|
|
111
|
+
.z-chart__tip { position: absolute; top: 0; left: 0; padding: var(--space-1) var(--space-2); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-raised); box-shadow: var(--shadow-overlay); font-size: 12px; line-height: 16px; white-space: nowrap; pointer-events: none; }
|
|
112
|
+
.z-chart__tip strong { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
113
|
+
.z-chart__caption { font-size: 12px; line-height: 16px; color: var(--text-muted); }
|
|
114
|
+
|
|
115
|
+
/* Angular hosts */
|
|
116
|
+
/* These elements wrap the markup of the reference and would be inline
|
|
117
|
+
otherwise. z-wizard-step is the <li> of the reference; z-wizard, z-config,
|
|
118
|
+
z-sticky-bar and z-cost-chart carry the reference class on the host and take
|
|
119
|
+
their display from it. */
|
|
120
|
+
z-option-group, z-combobox, z-wizard-step, z-included-list, z-disclosure, z-input-action { display: block; }
|
|
121
|
+
|
|
122
|
+
/* Addition to the reference: the action row of a step is a slot. A step
|
|
123
|
+
without actions would otherwise spend the space-5 gap of the body on an
|
|
124
|
+
empty div, and a step with only "Weiter" would put it on the left, where
|
|
125
|
+
space-between leaves a single child. */
|
|
126
|
+
.z-wstep__actions:empty { display: none; }
|
|
127
|
+
.z-wstep__actions > :only-child { margin-inline-start: auto; }
|
|
128
|
+
.z-wstep__actions > .z-btn--ghost:only-child { margin-inline: 0 auto; }
|
|
129
|
+
|
|
130
|
+
/* Addition to the reference: bundle.css gives .z-combo no width of its own. In
|
|
131
|
+
a z-field the wrapper is a grid item and stretches, next to a button in
|
|
132
|
+
.z-input-action it has to fill the row like .z-input does. */
|
|
133
|
+
.z-input-action .z-combo { flex: 1; min-width: 0; }
|
|
134
|
+
|
|
135
|
+
/* Addition to the reference: the listbox hangs in a CDK overlay, where the
|
|
136
|
+
pane has no width. The component measures the field and writes the width
|
|
137
|
+
onto the pane; the panel takes it over so it never reaches past the field. */
|
|
138
|
+
.z-combo-pane .z-listbox { width: 100%; box-sizing: border-box; }
|
|
139
|
+
|
|
140
|
+
/* Addition to the reference: a combobox that is locked reads like every other
|
|
141
|
+
locked control. bundle.css dims .z-input:disabled but not the chevron of
|
|
142
|
+
.z-combo::after, which would stay at full strength next to a 45 % field. */
|
|
143
|
+
.z-combo:has(.z-input:disabled)::after { opacity: 0.45; }
|
|
144
|
+
|
|
145
|
+
/* Addition to the reference: StickyBar/README.md asks for
|
|
146
|
+
env(safe-area-inset-bottom) in the padding, which bundle.css does not carry.
|
|
147
|
+
Only the bottom grows, and only where the device reports an inset. */
|
|
148
|
+
.z-stickybar { padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px)); }
|
|
149
|
+
|
|
150
|
+
/* Addition to the reference: CostChart/preview.html sizes the summary of the
|
|
151
|
+
"Als Tabelle" disclosure with an inline style (14px/20px, 12px above and
|
|
152
|
+
below). A library component carries no inline styles, so the same three
|
|
153
|
+
declarations stand here as a rule. 12px is space-3. */
|
|
154
|
+
.z-chart .z-faq > summary { padding: var(--space-3) 0; font-size: 14px; line-height: 20px; }
|
|
155
|
+
|
|
156
|
+
/* Addition to the reference: the plot takes the keyboard as well as the
|
|
157
|
+
pointer (CostChart/README.md), so it is a tab stop and needs room for the
|
|
158
|
+
2px ring with its 2px offset. The svg already draws outside its box. */
|
|
159
|
+
.z-chart__plot:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: var(--radius-sm); }
|
|
160
|
+
|
|
161
|
+
/* Addition to the reference: the summary column is 340px wide at every viewport
|
|
162
|
+
width, so an alert in it never has room for its button beside the text. It
|
|
163
|
+
wraps there the way bundle.css lets an alert wrap below 640px; the 32px
|
|
164
|
+
indent is the icon column, as in the reference rule. */
|
|
165
|
+
.z-summary .z-alert { flex-wrap: wrap; }
|
|
166
|
+
.z-summary .z-alert > .z-btn { flex-basis: 100%; margin-left: 32px; }
|
|
167
|
+
|
|
168
|
+
/* Addition to the reference: bundle.css leaves the hidden radio at its natural
|
|
169
|
+
13px in the corner of the card. The browser scrolls THAT box into view and
|
|
170
|
+
draws the focus ring around the card, so a focused card can still sit behind
|
|
171
|
+
the sticky bar (WCAG 2.4.11). Stretched over the card the focused box is the
|
|
172
|
+
card. It stays invisible (opacity 0 from the reference) and keeps its place
|
|
173
|
+
in the layer order, so title, description and badge stay readable.
|
|
174
|
+
bundle.css also takes it out of the pointer flow entirely; it sits inside its
|
|
175
|
+
own <label>, so a click reaches it either way, while "pointer-events: none"
|
|
176
|
+
makes an enabled control read as locked to anything that measures it
|
|
177
|
+
(e2e/a11y-baum.spec.ts does). A locked card keeps the cursor it inherits, so
|
|
178
|
+
only the enabled ones get their pointer back. */
|
|
179
|
+
.z-option input { inset: 0; width: 100%; height: 100%; margin: 0; }
|
|
180
|
+
.z-option input:enabled { pointer-events: auto; }
|
|
181
|
+
|
|
182
|
+
/* Addition to the reference: the stretched radio above is positioned and the
|
|
183
|
+
texts are not, so the radio paints over them and the mouse selects nothing
|
|
184
|
+
inside an enabled card. Position the texts and they are back on top; a click
|
|
185
|
+
on them still reaches the card, because they sit inside its <label>. The
|
|
186
|
+
badge is left out on purpose: it is already positioned (absolute, above) and
|
|
187
|
+
"relative" here would take its place away. */
|
|
188
|
+
.z-option > span { position: relative; }
|
|
189
|
+
|
|
190
|
+
/* Addition to the reference: a badge sits space-3 above its card and would
|
|
191
|
+
cover the last pixels of the legend hint at narrow widths, where the first
|
|
192
|
+
row starts right under it. Only a group that has badges pays for the room. */
|
|
193
|
+
.z-options:has(.z-option__badge) .z-options__legend { margin-bottom: var(--space-3); }
|
|
194
|
+
|
|
195
|
+
/* Addition to the reference: a sticky bar that really lies at the bottom edge
|
|
196
|
+
of the viewport covers what is behind it, so a control focused there would be
|
|
197
|
+
invisible (WCAG 2.4.11). z-sticky-bar measures itself, marks itself with
|
|
198
|
+
data-stuck while it sticks AND is displayed, and writes the height of the
|
|
199
|
+
tallest stuck bar into --z-stickybar; the document adds that to its
|
|
200
|
+
scroll-padding. It needs no extra room at the end of the page: the bar is
|
|
201
|
+
sticky inside the flow and already occupies its own height there. A bar that
|
|
202
|
+
stands in the page instead (a framed demo of the gallery) and a --mobile bar
|
|
203
|
+
from 900px on cover nothing and are not marked, so the page keeps its
|
|
204
|
+
padding at 0. */
|
|
205
|
+
.z-root:has(.z-stickybar[data-stuck]) {
|
|
206
|
+
scroll-padding-bottom: calc(var(--z-stickybar, 0px) + var(--space-4));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Addition to the reference: click targets at least 40px on mobile (CLAUDE.md,
|
|
210
|
+
Abstand und Layout). A listbox entry measures 36px, a compact option 36px,
|
|
211
|
+
and "Ändern" is a bare text button of one line height. */
|
|
212
|
+
@media (max-width: 640px) {
|
|
213
|
+
.z-listbox__option { min-height: var(--control-md); }
|
|
214
|
+
.z-options--compact .z-option { min-height: var(--control-md); align-content: center; }
|
|
215
|
+
.z-wstep__edit { min-height: var(--control-md); }
|
|
216
|
+
}
|