@webority/theme 0.7.1 → 0.7.3
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/dist/webority-theme.css +1 -1
- package/index.scss +15 -0
- package/package.json +2 -1
- package/scss/_components.scss +108 -10
- package/scss/_tokens.scss +44 -0
- package/scss/_variables.scss +10 -8
- package/scss/_wui-aliases.scss +3 -0
- package/scss/_wui-components.scss +1 -0
- package/scss/wui/_choice.scss +8 -3
- package/scss/wui/_controls.scss +13 -5
- package/scss/wui/_data.scss +40 -0
- package/scss/wui/_display.scss +41 -4
- package/scss/wui/_media.scss +24 -0
- package/scss/wui/_navigation.scss +187 -7
- package/scss/wui/_overlays.scss +36 -52
- package/scss/wui/_scrollbar.scss +32 -0
- package/scss/wui/_shell.scss +222 -10
|
@@ -108,28 +108,58 @@
|
|
|
108
108
|
// The tab strip both surfaces render. React drives it as a controlled strip and
|
|
109
109
|
// Razor additionally renders Bootstrap panels beneath it, but the strip markup —
|
|
110
110
|
// and therefore the look — is identical.
|
|
111
|
+
//
|
|
112
|
+
// The active state is an "ink bar" (.app-tab-indicator): one element per strip that
|
|
113
|
+
// SLIDES between tabs with an elastic overshoot (--wui-ease-elastic). It is JS-measured
|
|
114
|
+
// — variable-width tabs cannot be positioned in pure CSS — so until the JS wires the
|
|
115
|
+
// strip (`[data-wui-tabs]`) the active tab keeps a plain static underline as a fallback,
|
|
116
|
+
// and no-JS / first paint never show a bare strip.
|
|
117
|
+
|
|
118
|
+
// Optional overflow wrapper: the scroll chevrons live HERE, outside the scrolling strip,
|
|
119
|
+
// so they stay pinned while the tabs scroll under them.
|
|
120
|
+
.app-tabs-wrap {
|
|
121
|
+
position: relative;
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 4px;
|
|
125
|
+
}
|
|
126
|
+
|
|
111
127
|
.app-tabs {
|
|
128
|
+
position: relative;
|
|
112
129
|
display: flex;
|
|
113
130
|
align-items: center;
|
|
114
131
|
gap: 2px;
|
|
115
132
|
border-bottom: 1px solid var(--color-border);
|
|
116
133
|
overflow-x: auto;
|
|
117
134
|
scrollbar-width: none;
|
|
118
|
-
// The React AppTabs renders a react-bootstrap <Nav>, so the element also
|
|
119
|
-
// carries `.nav`, which declares `flex-wrap: wrap`. Both are (0,1,0), so
|
|
120
|
-
// today this wins only because the theme is imported after Bootstrap —
|
|
121
|
-
// see the doubled-selector rule below, which does not depend on that.
|
|
122
135
|
flex-wrap: nowrap;
|
|
136
|
+
// Auto-scrolling the active tab into view (and the chevrons) glides rather than jumps.
|
|
137
|
+
scroll-behavior: smooth;
|
|
123
138
|
}
|
|
124
|
-
// Doubled to (0,2,0) so the
|
|
125
|
-
//
|
|
139
|
+
// Doubled to (0,2,0) so the strip keeps scrolling even if a consumer loads Bootstrap
|
|
140
|
+
// after the theme, which would otherwise let `.nav`'s flex-wrap:wrap win on order.
|
|
126
141
|
.app-tabs.app-tabs {
|
|
127
142
|
flex-wrap: nowrap;
|
|
128
143
|
}
|
|
129
144
|
.app-tabs::-webkit-scrollbar {
|
|
130
145
|
display: none;
|
|
131
146
|
}
|
|
147
|
+
// When the wrapper reports overflow, fade the clipped edge(s) so cut tabs read as
|
|
148
|
+
// "more this way" rather than an abrupt crop. Masked on the strip (the scroller) itself.
|
|
149
|
+
.app-tabs-wrap.is-overflow-start .app-tabs {
|
|
150
|
+
mask-image: linear-gradient(90deg, transparent, #000 28px);
|
|
151
|
+
}
|
|
152
|
+
.app-tabs-wrap.is-overflow-end .app-tabs {
|
|
153
|
+
mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
|
|
154
|
+
}
|
|
155
|
+
.app-tabs-wrap.is-overflow-start.is-overflow-end .app-tabs {
|
|
156
|
+
mask-image: linear-gradient(90deg, transparent, #000 28px, #000 calc(100% - 28px), transparent);
|
|
157
|
+
}
|
|
158
|
+
|
|
132
159
|
.app-tab {
|
|
160
|
+
position: relative;
|
|
161
|
+
// Above the pills ink bar, which sits at z-index 0.
|
|
162
|
+
z-index: 1;
|
|
133
163
|
display: inline-flex;
|
|
134
164
|
align-items: center;
|
|
135
165
|
gap: 8px;
|
|
@@ -155,11 +185,17 @@
|
|
|
155
185
|
}
|
|
156
186
|
.app-tab.active {
|
|
157
187
|
color: var(--color-primary);
|
|
188
|
+
// Static fallback underline (no-JS / first paint); the ink bar takes over once wired.
|
|
158
189
|
border-bottom-color: var(--color-primary);
|
|
159
190
|
// The active tab carries weight as well as colour — colour alone is a weak
|
|
160
191
|
// signal for anyone who reads the strip at a glance or cannot distinguish it.
|
|
161
192
|
font-weight: 600;
|
|
162
193
|
}
|
|
194
|
+
// Once JS wires the ink bar it owns the underline — drop the per-tab fallback so the
|
|
195
|
+
// two never double up during a slide.
|
|
196
|
+
.app-tabs[data-wui-tabs] .app-tab.active {
|
|
197
|
+
border-bottom-color: transparent;
|
|
198
|
+
}
|
|
163
199
|
.app-tab:disabled {
|
|
164
200
|
color: var(--color-text-subtle);
|
|
165
201
|
cursor: not-allowed;
|
|
@@ -180,10 +216,41 @@
|
|
|
180
216
|
color: var(--color-danger-text);
|
|
181
217
|
border-bottom-color: var(--color-danger);
|
|
182
218
|
}
|
|
219
|
+
.app-tabs[data-wui-tabs] .app-tab.danger.active {
|
|
220
|
+
border-bottom-color: transparent;
|
|
221
|
+
}
|
|
183
222
|
.app-tab.danger .app-tab-count {
|
|
184
223
|
background: var(--color-danger-soft);
|
|
185
224
|
color: var(--color-danger-text);
|
|
186
225
|
}
|
|
226
|
+
|
|
227
|
+
// ── The sliding ink bar ──
|
|
228
|
+
// Positioned from JS-set custom props: --wui-tab-x/-w (horizontal), --wui-tab-y/-h
|
|
229
|
+
// (vertical). Hidden until the strip is wired AND measured, so it never flashes at 0,0.
|
|
230
|
+
.app-tab-indicator {
|
|
231
|
+
position: absolute;
|
|
232
|
+
left: 0;
|
|
233
|
+
bottom: -1px;
|
|
234
|
+
height: 2px;
|
|
235
|
+
width: var(--wui-tab-w, 0);
|
|
236
|
+
background: var(--color-primary);
|
|
237
|
+
border-radius: 2px;
|
|
238
|
+
transform: translateX(var(--wui-tab-x, 0));
|
|
239
|
+
opacity: 0;
|
|
240
|
+
pointer-events: none;
|
|
241
|
+
transition:
|
|
242
|
+
transform 0.42s var(--wui-ease-elastic),
|
|
243
|
+
width 0.42s var(--wui-ease-elastic),
|
|
244
|
+
opacity 0.15s ease;
|
|
245
|
+
}
|
|
246
|
+
.app-tabs[data-wui-tabs] .app-tab-indicator {
|
|
247
|
+
opacity: 1;
|
|
248
|
+
}
|
|
249
|
+
// A destructive active tab tints the bar red too (attribute set by the driver).
|
|
250
|
+
.app-tabs[data-active-danger] .app-tab-indicator {
|
|
251
|
+
background: var(--color-danger);
|
|
252
|
+
}
|
|
253
|
+
|
|
187
254
|
// Trailing count pill.
|
|
188
255
|
.app-tab-count {
|
|
189
256
|
display: inline-flex;
|
|
@@ -221,13 +288,31 @@
|
|
|
221
288
|
color: var(--color-info-text);
|
|
222
289
|
}
|
|
223
290
|
|
|
291
|
+
// ── Sizes ──
|
|
292
|
+
.app-tabs-sm .app-tab {
|
|
293
|
+
padding: 6px 10px;
|
|
294
|
+
gap: 6px;
|
|
295
|
+
font-size: 13px;
|
|
296
|
+
}
|
|
297
|
+
.app-tabs-lg .app-tab {
|
|
298
|
+
padding: 14px 18px;
|
|
299
|
+
font-size: 15px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Fitted: tabs share the width equally (no scroll). ──
|
|
303
|
+
.app-tabs-fitted .app-tab {
|
|
304
|
+
flex: 1 1 0;
|
|
305
|
+
justify-content: center;
|
|
306
|
+
}
|
|
307
|
+
|
|
224
308
|
// Wrapper for a self-contained tabs widget (strip + panels), as <app-tabs>
|
|
225
309
|
// renders server-side. React composes the strip and its panels itself.
|
|
226
310
|
.app-tabs-group > .tab-content {
|
|
227
311
|
padding-top: 16px;
|
|
228
312
|
}
|
|
229
313
|
|
|
230
|
-
// Pills variant — a segmented control rather than an underline
|
|
314
|
+
// Pills variant — a segmented control rather than an underline; the ink bar becomes
|
|
315
|
+
// the moving pill behind the active tab.
|
|
231
316
|
.app-tabs-pills {
|
|
232
317
|
gap: 4px;
|
|
233
318
|
border-bottom: 0;
|
|
@@ -237,9 +322,104 @@
|
|
|
237
322
|
margin-bottom: 0;
|
|
238
323
|
border-radius: var(--radius);
|
|
239
324
|
}
|
|
325
|
+
// Fallback fill until the moving pill is wired.
|
|
240
326
|
.app-tabs-pills .app-tab.active {
|
|
241
327
|
background: var(--color-primary-soft);
|
|
242
328
|
}
|
|
329
|
+
.app-tabs-pills[data-wui-tabs] .app-tab.active {
|
|
330
|
+
background: transparent;
|
|
331
|
+
}
|
|
332
|
+
.app-tabs-pills .app-tab-indicator {
|
|
333
|
+
top: 0;
|
|
334
|
+
bottom: 0;
|
|
335
|
+
height: auto;
|
|
336
|
+
z-index: 0;
|
|
337
|
+
background: var(--color-primary-soft);
|
|
338
|
+
border-radius: var(--radius);
|
|
339
|
+
}
|
|
340
|
+
.app-tabs-pills[data-active-danger] .app-tab-indicator {
|
|
341
|
+
background: var(--color-danger-soft);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Vertical orientation — a side rail; the ink bar becomes a moving left edge. ──
|
|
345
|
+
.app-tabs-vertical {
|
|
346
|
+
flex-direction: column;
|
|
347
|
+
align-items: stretch;
|
|
348
|
+
border-bottom: 0;
|
|
349
|
+
border-left: 1px solid var(--color-border);
|
|
350
|
+
overflow-x: visible;
|
|
351
|
+
overflow-y: auto;
|
|
352
|
+
}
|
|
353
|
+
.app-tabs-vertical .app-tab {
|
|
354
|
+
justify-content: flex-start;
|
|
355
|
+
border-bottom: 0;
|
|
356
|
+
border-left: 2px solid transparent;
|
|
357
|
+
margin-bottom: 0;
|
|
358
|
+
margin-left: -1px;
|
|
359
|
+
}
|
|
360
|
+
.app-tabs-vertical .app-tab.active {
|
|
361
|
+
border-left-color: var(--color-primary);
|
|
362
|
+
border-bottom-color: transparent;
|
|
363
|
+
}
|
|
364
|
+
.app-tabs-vertical[data-wui-tabs] .app-tab.active {
|
|
365
|
+
border-left-color: transparent;
|
|
366
|
+
}
|
|
367
|
+
.app-tabs-vertical .app-tab-indicator {
|
|
368
|
+
left: -1px;
|
|
369
|
+
top: 0;
|
|
370
|
+
bottom: auto;
|
|
371
|
+
width: 2px;
|
|
372
|
+
height: var(--wui-tab-h, 0);
|
|
373
|
+
transform: translateY(var(--wui-tab-y, 0));
|
|
374
|
+
transition:
|
|
375
|
+
transform 0.42s var(--wui-ease-elastic),
|
|
376
|
+
height 0.42s var(--wui-ease-elastic),
|
|
377
|
+
opacity 0.15s ease;
|
|
378
|
+
}
|
|
379
|
+
// Vertical pills: the moving pill fills the row width.
|
|
380
|
+
.app-tabs-vertical.app-tabs-pills .app-tab-indicator {
|
|
381
|
+
left: 0;
|
|
382
|
+
width: 100%;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── Scroll chevrons (overflow affordance) — shown by JS only when that side overflows. ──
|
|
386
|
+
.app-tabs-scroll-btn {
|
|
387
|
+
flex: none;
|
|
388
|
+
display: none;
|
|
389
|
+
align-items: center;
|
|
390
|
+
justify-content: center;
|
|
391
|
+
width: 28px;
|
|
392
|
+
height: 28px;
|
|
393
|
+
padding: 0;
|
|
394
|
+
border: 1px solid var(--color-border);
|
|
395
|
+
border-radius: 999px;
|
|
396
|
+
background: var(--color-surface);
|
|
397
|
+
color: var(--color-text-muted);
|
|
398
|
+
cursor: pointer;
|
|
399
|
+
}
|
|
400
|
+
.app-tabs-scroll-btn:hover {
|
|
401
|
+
color: var(--color-text);
|
|
402
|
+
background: var(--color-bg-muted);
|
|
403
|
+
}
|
|
404
|
+
.app-tabs-scroll-btn:focus-visible {
|
|
405
|
+
outline: 2px solid var(--color-primary);
|
|
406
|
+
outline-offset: 2px;
|
|
407
|
+
}
|
|
408
|
+
.app-tabs-wrap.is-overflow-start .app-tabs-scroll-btn.prev,
|
|
409
|
+
.app-tabs-wrap.is-overflow-end .app-tabs-scroll-btn.next {
|
|
410
|
+
display: inline-flex;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── Motion off — no slide, no overshoot; the bar just fades in place. ──
|
|
414
|
+
@media (prefers-reduced-motion: reduce) {
|
|
415
|
+
.app-tab-indicator,
|
|
416
|
+
.app-tabs-vertical .app-tab-indicator {
|
|
417
|
+
transition: opacity 0.15s ease;
|
|
418
|
+
}
|
|
419
|
+
.app-tabs {
|
|
420
|
+
scroll-behavior: auto;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
243
423
|
|
|
244
424
|
// ── Search field ──
|
|
245
425
|
// The search control both surfaces render: a rounded field with a leading
|
package/scss/wui/_overlays.scss
CHANGED
|
@@ -79,58 +79,10 @@
|
|
|
79
79
|
flex-direction: column;
|
|
80
80
|
overflow: hidden;
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Side drawer — the dialog docks to the right edge at 30% of the viewport
|
|
87
|
-
// and slides in, instead of sitting centred. Same chrome, different
|
|
88
|
-
// geometry.
|
|
89
|
-
.modal.wui-modal-shell.is-side {
|
|
90
|
-
align-items: stretch;
|
|
91
|
-
justify-content: flex-end;
|
|
92
|
-
padding: 0;
|
|
93
|
-
}
|
|
94
|
-
.modal.wui-modal-shell.is-side .modal-dialog.wui-rb-dialog {
|
|
95
|
-
height: 100%;
|
|
96
|
-
width: auto;
|
|
97
|
-
max-width: none;
|
|
98
|
-
align-items: stretch;
|
|
99
|
-
justify-content: flex-end;
|
|
100
|
-
}
|
|
101
|
-
.wui-modal-side {
|
|
102
|
-
width: 30vw;
|
|
103
|
-
min-width: 420px;
|
|
104
|
-
max-width: none;
|
|
105
|
-
max-height: 100vh;
|
|
106
|
-
height: 100vh;
|
|
107
|
-
border-radius: 0;
|
|
108
|
-
corner-shape: normal;
|
|
109
|
-
animation: wui-drawer-in 0.32s cubic-bezier(0.22, 1, 0.36, 1);
|
|
110
|
-
}
|
|
111
|
-
@media (max-width: 991.98px) {
|
|
112
|
-
.wui-modal-side {
|
|
113
|
-
width: 70vw;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
@media (max-width: 575.98px) {
|
|
117
|
-
.wui-modal-side {
|
|
118
|
-
width: 100vw;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
@keyframes wui-drawer-in {
|
|
122
|
-
from {
|
|
123
|
-
transform: translateX(100%);
|
|
124
|
-
}
|
|
125
|
-
to {
|
|
126
|
-
transform: translateX(0);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
@media (prefers-reduced-motion: reduce) {
|
|
130
|
-
.wui-modal-side {
|
|
131
|
-
animation: none;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
82
|
+
// The centred card keeps Bootstrap's own modal transition — a smooth fade + slide-down that
|
|
83
|
+
// Bootstrap already disables under prefers-reduced-motion. Nothing to override here; both
|
|
84
|
+
// react-bootstrap (animation on) and Bootstrap's JS drive it. Width comes from `size`, which
|
|
85
|
+
// both surfaces resolve to `--wui-modal-max-width` on the shell (it inherits to `.wui-modal`).
|
|
134
86
|
.wui-modal-header {
|
|
135
87
|
flex: 0 0 auto;
|
|
136
88
|
display: flex;
|
|
@@ -210,6 +162,38 @@
|
|
|
210
162
|
color: var(--color-text-muted);
|
|
211
163
|
overflow-wrap: anywhere;
|
|
212
164
|
}
|
|
165
|
+
// Eyebrow — a small kicker label above the title (a section / step / category marker).
|
|
166
|
+
.wui-modal-eyebrow {
|
|
167
|
+
font-size: 11px;
|
|
168
|
+
font-weight: 700;
|
|
169
|
+
letter-spacing: 0.06em;
|
|
170
|
+
text-transform: uppercase;
|
|
171
|
+
color: var(--color-primary);
|
|
172
|
+
margin-bottom: 2px;
|
|
173
|
+
}
|
|
174
|
+
// `tone` tints the header icon tile and the eyebrow, so a destructive or a success dialog
|
|
175
|
+
// carries its intent in the header. Default keeps the neutral primary tint above.
|
|
176
|
+
.wui-modal.tone-danger .wui-modal-headicon {
|
|
177
|
+
background: var(--color-danger-soft);
|
|
178
|
+
color: var(--color-danger-text);
|
|
179
|
+
}
|
|
180
|
+
.wui-modal.tone-danger .wui-modal-eyebrow {
|
|
181
|
+
color: var(--color-danger-text);
|
|
182
|
+
}
|
|
183
|
+
.wui-modal.tone-success .wui-modal-headicon {
|
|
184
|
+
background: var(--color-success-soft);
|
|
185
|
+
color: var(--color-success-text);
|
|
186
|
+
}
|
|
187
|
+
.wui-modal.tone-success .wui-modal-eyebrow {
|
|
188
|
+
color: var(--color-success-text);
|
|
189
|
+
}
|
|
190
|
+
.wui-modal.tone-warning .wui-modal-headicon {
|
|
191
|
+
background: var(--color-warning-soft);
|
|
192
|
+
color: var(--color-warning-text);
|
|
193
|
+
}
|
|
194
|
+
.wui-modal.tone-warning .wui-modal-eyebrow {
|
|
195
|
+
color: var(--color-warning-text);
|
|
196
|
+
}
|
|
213
197
|
.wui-form-section {
|
|
214
198
|
margin-top: 24px;
|
|
215
199
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// AppScrollbar — a thin, themed scrollbar drawn with NATIVE CSS scrollbar styling: no JavaScript
|
|
2
|
+
// and no dependency (it replaced a wrapped simplebar-react, whose foreign `.simplebar-*` markup
|
|
3
|
+
// and stylesheet had no place in the library). `scrollbar-width` / `scrollbar-color` style the
|
|
4
|
+
// track on Firefox; the `::-webkit-scrollbar` rules cover Chromium and Safari. The track stays
|
|
5
|
+
// transparent so the thumb reads as a slim overlay rather than filling a grey gutter.
|
|
6
|
+
.wui-scroll {
|
|
7
|
+
overflow: auto;
|
|
8
|
+
scrollbar-width: thin;
|
|
9
|
+
scrollbar-color: var(--wui-text-3) transparent;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.wui-scroll::-webkit-scrollbar {
|
|
13
|
+
width: 8px;
|
|
14
|
+
height: 8px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.wui-scroll::-webkit-scrollbar-track {
|
|
18
|
+
background: transparent;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.wui-scroll::-webkit-scrollbar-thumb {
|
|
22
|
+
background-color: var(--wui-text-3);
|
|
23
|
+
border-radius: 8px;
|
|
24
|
+
// A 2px transparent border clipped to padding-box insets the thumb, so it sits as a slim pill
|
|
25
|
+
// in the middle of the track instead of filling the whole gutter width.
|
|
26
|
+
border: 2px solid transparent;
|
|
27
|
+
background-clip: padding-box;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.wui-scroll::-webkit-scrollbar-thumb:hover {
|
|
31
|
+
background-color: var(--wui-text-2);
|
|
32
|
+
}
|
package/scss/wui/_shell.scss
CHANGED
|
@@ -25,31 +25,47 @@
|
|
|
25
25
|
align-items: center;
|
|
26
26
|
gap: 18px;
|
|
27
27
|
text-align: center;
|
|
28
|
+
// Ease in on mount so the boot screen settles rather than snapping into place.
|
|
29
|
+
animation: wui-splash-enter 0.5s var(--wui-ease-ui) both;
|
|
28
30
|
}
|
|
31
|
+
// One diameter drives the mark, its initials, the logo cap, and the bar width — the `size`
|
|
32
|
+
// preset or a custom pixel value resolves to `--wui-splash-size` (set inline by App*/<app-*>).
|
|
29
33
|
.splash-logo {
|
|
30
|
-
max-width:
|
|
34
|
+
max-width: calc(var(--wui-splash-size, 56px) * 3.2);
|
|
31
35
|
height: auto;
|
|
36
|
+
// A slow breath keeps the brand mark feeling alive through the wait.
|
|
37
|
+
animation: wui-splash-breathe 2.6s ease-in-out infinite;
|
|
32
38
|
}
|
|
33
39
|
.splash-mark {
|
|
34
|
-
width: 56px;
|
|
35
|
-
height: 56px;
|
|
40
|
+
width: var(--wui-splash-size, 56px);
|
|
41
|
+
height: var(--wui-splash-size, 56px);
|
|
36
42
|
display: inline-grid;
|
|
37
43
|
place-items: center;
|
|
38
44
|
border-radius: var(--radius-lg);
|
|
39
45
|
background: var(--color-primary);
|
|
40
46
|
color: var(--color-on-scrim);
|
|
41
|
-
font-size:
|
|
47
|
+
font-size: calc(var(--wui-splash-size, 56px) * 0.39);
|
|
42
48
|
font-weight: 700;
|
|
49
|
+
animation: wui-splash-breathe 2.6s ease-in-out infinite;
|
|
43
50
|
}
|
|
44
51
|
.splash-text {
|
|
45
52
|
font-size: 14px;
|
|
46
53
|
color: var(--color-text-muted);
|
|
47
54
|
}
|
|
48
|
-
// A
|
|
49
|
-
//
|
|
55
|
+
// A quieter second line — a reassurance under the main status. Uses --color-text-muted (not
|
|
56
|
+
// -subtle): -subtle is ~4.54:1 on the splash bg and flakes the AA contrast check at 12px; the
|
|
57
|
+
// smaller size (vs .splash-text) already sets the hierarchy without dropping below AA.
|
|
58
|
+
.splash-subtext {
|
|
59
|
+
margin-top: -12px;
|
|
60
|
+
font-size: 12px;
|
|
61
|
+
color: var(--color-text-muted);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Boot indicators — one shows, chosen by `variant`. A determinate-looking value would lie
|
|
65
|
+
// about how far along boot is, so every variant is indeterminate. ──
|
|
50
66
|
.splash-bar {
|
|
51
67
|
position: relative;
|
|
52
|
-
width:
|
|
68
|
+
width: calc(var(--wui-splash-size, 56px) * 3.2);
|
|
53
69
|
height: 3px;
|
|
54
70
|
overflow: hidden;
|
|
55
71
|
border-radius: 999px;
|
|
@@ -62,9 +78,62 @@
|
|
|
62
78
|
bottom: 0;
|
|
63
79
|
width: 40%;
|
|
64
80
|
border-radius: 999px;
|
|
65
|
-
|
|
81
|
+
// Feathered edges read smoother than a hard block sliding across.
|
|
82
|
+
background: linear-gradient(
|
|
83
|
+
90deg,
|
|
84
|
+
transparent,
|
|
85
|
+
var(--color-primary) 35%,
|
|
86
|
+
var(--color-primary) 65%,
|
|
87
|
+
transparent
|
|
88
|
+
);
|
|
66
89
|
animation: wui-splash-sweep 1.1s var(--wui-ease-ui) infinite;
|
|
67
90
|
}
|
|
91
|
+
// A spinning arc — the circular take on the same "working, no percentage" idea.
|
|
92
|
+
.splash-ring {
|
|
93
|
+
width: 30px;
|
|
94
|
+
height: 30px;
|
|
95
|
+
border-radius: 50%;
|
|
96
|
+
border: 3px solid var(--color-bg-muted);
|
|
97
|
+
border-top-color: var(--color-primary);
|
|
98
|
+
animation: wui-splash-spin 0.8s linear infinite;
|
|
99
|
+
}
|
|
100
|
+
// Three dots taking turns rising — a lighter, friendlier wait.
|
|
101
|
+
.splash-dots {
|
|
102
|
+
display: inline-flex;
|
|
103
|
+
gap: 7px;
|
|
104
|
+
}
|
|
105
|
+
.splash-dots i {
|
|
106
|
+
width: 9px;
|
|
107
|
+
height: 9px;
|
|
108
|
+
border-radius: 50%;
|
|
109
|
+
background: var(--color-primary);
|
|
110
|
+
animation: wui-splash-dot 1.2s ease-in-out infinite;
|
|
111
|
+
}
|
|
112
|
+
.splash-dots i:nth-child(2) {
|
|
113
|
+
animation-delay: 0.16s;
|
|
114
|
+
}
|
|
115
|
+
.splash-dots i:nth-child(3) {
|
|
116
|
+
animation-delay: 0.32s;
|
|
117
|
+
}
|
|
118
|
+
@keyframes wui-splash-enter {
|
|
119
|
+
from {
|
|
120
|
+
opacity: 0;
|
|
121
|
+
transform: translateY(8px);
|
|
122
|
+
}
|
|
123
|
+
to {
|
|
124
|
+
opacity: 1;
|
|
125
|
+
transform: translateY(0);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
@keyframes wui-splash-breathe {
|
|
129
|
+
0%,
|
|
130
|
+
100% {
|
|
131
|
+
transform: scale(1);
|
|
132
|
+
}
|
|
133
|
+
50% {
|
|
134
|
+
transform: scale(1.045);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
68
137
|
@keyframes wui-splash-sweep {
|
|
69
138
|
from {
|
|
70
139
|
transform: translateX(-100%);
|
|
@@ -73,9 +142,37 @@
|
|
|
73
142
|
transform: translateX(350%);
|
|
74
143
|
}
|
|
75
144
|
}
|
|
145
|
+
@keyframes wui-splash-spin {
|
|
146
|
+
to {
|
|
147
|
+
transform: rotate(360deg);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
@keyframes wui-splash-dot {
|
|
151
|
+
0%,
|
|
152
|
+
60%,
|
|
153
|
+
100% {
|
|
154
|
+
transform: translateY(0);
|
|
155
|
+
opacity: 0.45;
|
|
156
|
+
}
|
|
157
|
+
30% {
|
|
158
|
+
transform: translateY(-7px);
|
|
159
|
+
opacity: 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
76
162
|
@media (prefers-reduced-motion: reduce) {
|
|
163
|
+
// Motion off: settle instantly, drop breathing and the spinning/bouncing indicators.
|
|
164
|
+
.splash-inner,
|
|
165
|
+
.splash-logo,
|
|
166
|
+
.splash-mark,
|
|
167
|
+
.splash-ring,
|
|
168
|
+
.splash-dots i {
|
|
169
|
+
animation: none;
|
|
170
|
+
}
|
|
171
|
+
.splash-dots i {
|
|
172
|
+
opacity: 0.7;
|
|
173
|
+
}
|
|
77
174
|
.splash-bar::after {
|
|
78
|
-
//
|
|
175
|
+
// Keep a visible bar — an empty track would read as stalled.
|
|
79
176
|
animation: none;
|
|
80
177
|
width: 100%;
|
|
81
178
|
opacity: 0.55;
|
|
@@ -237,13 +334,17 @@
|
|
|
237
334
|
color: var(--color-primary);
|
|
238
335
|
}
|
|
239
336
|
.app-nav-text {
|
|
337
|
+
// Grows to fill the row so every trailing element — badge, trailing marker,
|
|
338
|
+
// expand caret — lines up at the end in source order without each needing its
|
|
339
|
+
// own `margin-left:auto` (which would fight when more than one is present).
|
|
340
|
+
flex: 1 1 auto;
|
|
240
341
|
min-width: 0;
|
|
241
342
|
overflow: hidden;
|
|
242
343
|
text-overflow: ellipsis;
|
|
243
344
|
white-space: nowrap;
|
|
244
345
|
}
|
|
245
346
|
.app-nav-badge {
|
|
246
|
-
|
|
347
|
+
flex: none;
|
|
247
348
|
padding: 1px 7px;
|
|
248
349
|
border-radius: 999px;
|
|
249
350
|
background: var(--color-bg-muted);
|
|
@@ -272,6 +373,117 @@
|
|
|
272
373
|
display: none;
|
|
273
374
|
}
|
|
274
375
|
|
|
376
|
+
// ── Badge tones ──────────────────────────────────────────────────────────────
|
|
377
|
+
// A nav badge can carry a status tone so an alert count reads red, not neutral.
|
|
378
|
+
.app-nav-badge.tone-danger {
|
|
379
|
+
background: var(--color-danger-soft);
|
|
380
|
+
color: var(--color-danger);
|
|
381
|
+
}
|
|
382
|
+
.app-nav-badge.tone-success {
|
|
383
|
+
background: var(--color-success-soft);
|
|
384
|
+
color: var(--color-success);
|
|
385
|
+
}
|
|
386
|
+
.app-nav-badge.tone-warning {
|
|
387
|
+
background: var(--color-warning-soft);
|
|
388
|
+
color: var(--color-warning);
|
|
389
|
+
}
|
|
390
|
+
// The existing `.app-nav-item.active .app-nav-badge` inverts a tinted badge on an
|
|
391
|
+
// active row (higher specificity), so an active row's badge stays legible.
|
|
392
|
+
|
|
393
|
+
// ── Trailing marker ──────────────────────────────────────────────────────────
|
|
394
|
+
// A right-aligned secondary glyph after the label (a lock, an external arrow, a
|
|
395
|
+
// "new" dot) — distinct from the count badge, and may sit beside one.
|
|
396
|
+
.app-nav-trailing {
|
|
397
|
+
flex: none;
|
|
398
|
+
display: inline-grid;
|
|
399
|
+
place-items: center;
|
|
400
|
+
color: var(--color-text-subtle);
|
|
401
|
+
}
|
|
402
|
+
.app-nav-item.active .app-nav-trailing {
|
|
403
|
+
color: var(--color-primary);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── Expandable sub-items ─────────────────────────────────────────────────────
|
|
407
|
+
// A parent row that toggles a nested list. It reuses the .app-nav-item chrome but
|
|
408
|
+
// is a <button>; the caret rotates open. The nested list animates its height via
|
|
409
|
+
// grid-template-rows 0fr→1fr, so there is no fixed max-height to guess.
|
|
410
|
+
.app-nav-item.has-children {
|
|
411
|
+
width: 100%;
|
|
412
|
+
border: 0;
|
|
413
|
+
background: transparent;
|
|
414
|
+
font: inherit;
|
|
415
|
+
text-align: left;
|
|
416
|
+
cursor: pointer;
|
|
417
|
+
}
|
|
418
|
+
.app-nav-caret {
|
|
419
|
+
flex: none;
|
|
420
|
+
display: inline-grid;
|
|
421
|
+
place-items: center;
|
|
422
|
+
color: var(--color-text-subtle);
|
|
423
|
+
transition: transform 0.2s var(--wui-ease-ui);
|
|
424
|
+
}
|
|
425
|
+
.app-nav-item.has-children[aria-expanded="true"] .app-nav-caret {
|
|
426
|
+
transform: rotate(90deg);
|
|
427
|
+
}
|
|
428
|
+
.app-nav-sub {
|
|
429
|
+
display: grid;
|
|
430
|
+
grid-template-rows: 0fr;
|
|
431
|
+
transition: grid-template-rows 0.24s var(--wui-ease-ui);
|
|
432
|
+
}
|
|
433
|
+
.app-nav-sub-inner {
|
|
434
|
+
overflow: hidden;
|
|
435
|
+
display: flex;
|
|
436
|
+
flex-direction: column;
|
|
437
|
+
gap: 2px;
|
|
438
|
+
padding-top: 2px;
|
|
439
|
+
}
|
|
440
|
+
.app-nav-sub.expanded {
|
|
441
|
+
grid-template-rows: 1fr;
|
|
442
|
+
}
|
|
443
|
+
// Child link indented under the parent label (icon slot + gap), smaller, no icon.
|
|
444
|
+
.app-nav-subitem {
|
|
445
|
+
min-height: 38px;
|
|
446
|
+
padding-left: 46px;
|
|
447
|
+
font-size: 13px;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ── Collapsed rail: label tooltip ────────────────────────────────────────────
|
|
451
|
+
// The rail hides labels, so surface each on hover/focus as a chip to the right —
|
|
452
|
+
// otherwise the icons are unlabelled. Sub-lists and carets have no place in a rail.
|
|
453
|
+
.app-shell.collapsed .app-nav-sub,
|
|
454
|
+
.app-shell.collapsed .app-nav-caret {
|
|
455
|
+
display: none;
|
|
456
|
+
}
|
|
457
|
+
@include media-breakpoint-up(lg) {
|
|
458
|
+
.app-shell.collapsed .app-nav-item[data-label]::after {
|
|
459
|
+
content: attr(data-label);
|
|
460
|
+
position: absolute;
|
|
461
|
+
left: calc(100% + 12px);
|
|
462
|
+
top: 50%;
|
|
463
|
+
z-index: 40;
|
|
464
|
+
padding: 5px 10px;
|
|
465
|
+
border-radius: var(--radius-lg);
|
|
466
|
+
background: var(--color-text);
|
|
467
|
+
color: var(--color-surface);
|
|
468
|
+
font-size: 12px;
|
|
469
|
+
font-weight: 500;
|
|
470
|
+
white-space: nowrap;
|
|
471
|
+
box-shadow: var(--shadow-md);
|
|
472
|
+
opacity: 0;
|
|
473
|
+
transform: translateY(-50%) scale(0.96);
|
|
474
|
+
transform-origin: left center;
|
|
475
|
+
pointer-events: none;
|
|
476
|
+
transition:
|
|
477
|
+
opacity 0.12s ease,
|
|
478
|
+
transform 0.12s ease;
|
|
479
|
+
}
|
|
480
|
+
.app-shell.collapsed .app-nav-item[data-label]:hover::after,
|
|
481
|
+
.app-shell.collapsed .app-nav-item[data-label]:focus-visible::after {
|
|
482
|
+
opacity: 1;
|
|
483
|
+
transform: translateY(-50%) scale(1);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
275
487
|
// A circular button centred on the sidebar's right edge. It is fixed to the
|
|
276
488
|
// viewport, not a sidebar child, because the sidebar clips its own overflow and
|
|
277
489
|
// would cut the button in half.
|