@webority/theme 0.3.0 → 0.5.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.
@@ -0,0 +1,474 @@
1
+ // Splash / boot loader, persistent app shell.
2
+ //
3
+ // Part of the wui-components layer — see _wui-components.scss for the whole
4
+ // picture and the load order. Order within the layer is LOAD-BEARING: these
5
+ // partials are a split of one stylesheet, not independent modules, so a rule
6
+ // here may be overridden by a later partial. Never reorder the imports.
7
+
8
+ // Splash / boot loader — the full-viewport screen shown while the app resolves
9
+ // its session, before the first route can render.
10
+ // ============================================================================
11
+ .splash {
12
+ position: fixed;
13
+ inset: 0;
14
+ // Above the app but below a modal's backdrop, so an error dialog raised during
15
+ // boot is never trapped behind it.
16
+ z-index: 1040;
17
+ display: grid;
18
+ place-items: center;
19
+ padding: 24px;
20
+ background: var(--color-bg);
21
+ }
22
+ .splash-inner {
23
+ display: flex;
24
+ flex-direction: column;
25
+ align-items: center;
26
+ gap: 18px;
27
+ text-align: center;
28
+ }
29
+ .splash-logo {
30
+ max-width: 180px;
31
+ height: auto;
32
+ }
33
+ .splash-mark {
34
+ width: 56px;
35
+ height: 56px;
36
+ display: inline-grid;
37
+ place-items: center;
38
+ border-radius: var(--radius-lg);
39
+ background: var(--color-primary);
40
+ color: var(--color-on-scrim);
41
+ font-size: 22px;
42
+ font-weight: 700;
43
+ }
44
+ .splash-text {
45
+ font-size: 14px;
46
+ color: var(--color-text-muted);
47
+ }
48
+ // A determinate-looking bar with no real progress value would lie about how far
49
+ // along boot is, so this is an indeterminate sweep.
50
+ .splash-bar {
51
+ position: relative;
52
+ width: 180px;
53
+ height: 3px;
54
+ overflow: hidden;
55
+ border-radius: 999px;
56
+ background: var(--color-bg-muted);
57
+ }
58
+ .splash-bar::after {
59
+ content: "";
60
+ position: absolute;
61
+ top: 0;
62
+ bottom: 0;
63
+ width: 40%;
64
+ border-radius: 999px;
65
+ background: var(--color-primary);
66
+ animation: wui-splash-sweep 1.1s var(--wui-ease-ui) infinite;
67
+ }
68
+ @keyframes wui-splash-sweep {
69
+ from {
70
+ transform: translateX(-100%);
71
+ }
72
+ to {
73
+ transform: translateX(350%);
74
+ }
75
+ }
76
+ @media (prefers-reduced-motion: reduce) {
77
+ .splash-bar::after {
78
+ // Motion off, but keep a visible bar — an empty track would read as stalled.
79
+ animation: none;
80
+ width: 100%;
81
+ opacity: 0.55;
82
+ }
83
+ }
84
+
85
+ // ============================================================================
86
+ // Persistent app shell — the sidebar + topbar + content frame every authenticated
87
+ // portal sits inside.
88
+ //
89
+ // Every class here is `app-`-prefixed. The obvious names are already taken:
90
+ // `.nav-item`, `.nav-link` and `.navbar-*` are Bootstrap's own, so a shared
91
+ // package claiming them would silently restyle every consumer's Bootstrap nav.
92
+ // ============================================================================
93
+
94
+ // The rail width and the expanded width are variables because three separate
95
+ // things must agree on them: the grid column, the collapse toggle's left offset,
96
+ // and the footer's left inset. Hardcoding them in three places is how they drift.
97
+ .app-shell {
98
+ --app-sidebar-w: 260px;
99
+ --app-sidebar-rail-w: 88px;
100
+
101
+ position: relative;
102
+ display: grid;
103
+ grid-template-columns: var(--app-sidebar-w) 1fr;
104
+ min-height: 100vh;
105
+ background: var(--color-bg);
106
+ transition: grid-template-columns 0.2s var(--wui-ease-ui);
107
+ }
108
+ .app-shell.collapsed {
109
+ grid-template-columns: var(--app-sidebar-rail-w) 1fr;
110
+ }
111
+ // A page with no side nav at all (a focused wizard, a full-bleed report).
112
+ .app-shell.no-sidebar {
113
+ grid-template-columns: 1fr;
114
+ }
115
+ .app-shell.no-sidebar .app-sidebar,
116
+ .app-shell.no-sidebar .app-sidebar-toggle {
117
+ display: none;
118
+ }
119
+ .app-shell.no-sidebar .app-footer {
120
+ left: 0;
121
+ }
122
+
123
+ .app-sidebar {
124
+ position: sticky;
125
+ top: 0;
126
+ align-self: start;
127
+ height: 100vh;
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 2px;
131
+ padding: 20px 16px;
132
+ overflow: hidden;
133
+ background: var(--color-surface);
134
+ transition: padding 0.2s var(--wui-ease-ui);
135
+ }
136
+ .app-shell.collapsed .app-sidebar {
137
+ padding: 20px 12px;
138
+ }
139
+ .app-sidebar-brand {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 10px;
143
+ min-height: 32px;
144
+ margin-bottom: 8px;
145
+ // The brand row must not shrink when the nav list below it overflows.
146
+ flex: none;
147
+ overflow: hidden;
148
+ }
149
+ // The nav list scrolls independently while the brand row stays put. `min-height: 0`
150
+ // is load-bearing — without it this flex child refuses to shrink below its content
151
+ // and the overflow never engages, so the lower nav items become unreachable.
152
+ .app-sidebar-scroll {
153
+ flex: 1 1 auto;
154
+ min-height: 0;
155
+ display: flex;
156
+ flex-direction: column;
157
+ gap: 2px;
158
+ overflow-y: auto;
159
+ // The rail is chrome, not content: a scrollbar here reads as clutter and, at
160
+ // 88px collapsed, would eat a meaningful share of the width.
161
+ scrollbar-width: none;
162
+ }
163
+ .app-sidebar-scroll::-webkit-scrollbar {
164
+ display: none;
165
+ width: 0;
166
+ height: 0;
167
+ }
168
+ .app-sidebar-foot {
169
+ flex: none;
170
+ padding-top: 12px;
171
+ }
172
+
173
+ .app-nav-group {
174
+ display: flex;
175
+ flex-direction: column;
176
+ gap: 2px;
177
+ margin-top: 16px;
178
+ }
179
+ // Groups after the first are separated by a rule; the first sits directly under
180
+ // the brand row and needs none.
181
+ .app-nav-group + .app-nav-group {
182
+ margin-top: 20px;
183
+ padding-top: 12px;
184
+ border-top: 1px solid var(--color-border);
185
+ }
186
+ .app-nav-group-label {
187
+ padding: 6px 14px;
188
+ font-size: 12px;
189
+ font-weight: 600;
190
+ text-transform: uppercase;
191
+ letter-spacing: 0.06em;
192
+ color: var(--color-text-subtle);
193
+ }
194
+
195
+ .app-nav-item {
196
+ position: relative;
197
+ display: flex;
198
+ align-items: center;
199
+ gap: 12px;
200
+ // 44px is the touch-target floor, and it is the same control height buttons
201
+ // and inputs use — the nav reads as part of the same system.
202
+ min-height: var(--wui-control-height);
203
+ padding: 9px 14px;
204
+ border-radius: var(--radius-xl);
205
+ corner-shape: squircle;
206
+ font-size: 14px;
207
+ font-weight: 500;
208
+ color: var(--color-text-muted);
209
+ text-decoration: none;
210
+ transition:
211
+ background 0.12s ease,
212
+ color 0.12s ease,
213
+ padding 0.2s var(--wui-ease-ui),
214
+ gap 0.2s var(--wui-ease-ui);
215
+ }
216
+ .app-nav-item:hover {
217
+ background: var(--color-bg-muted);
218
+ color: var(--color-text);
219
+ text-decoration: none;
220
+ }
221
+ .app-nav-item.active {
222
+ background: var(--color-primary-soft);
223
+ color: var(--color-primary);
224
+ font-weight: 600;
225
+ }
226
+ .app-nav-item:focus-visible {
227
+ outline: 2px solid var(--color-primary);
228
+ outline-offset: -2px;
229
+ }
230
+ .app-nav-icon {
231
+ flex: none;
232
+ display: inline-grid;
233
+ place-items: center;
234
+ color: var(--color-text-subtle);
235
+ }
236
+ .app-nav-item.active .app-nav-icon {
237
+ color: var(--color-primary);
238
+ }
239
+ .app-nav-text {
240
+ min-width: 0;
241
+ overflow: hidden;
242
+ text-overflow: ellipsis;
243
+ white-space: nowrap;
244
+ }
245
+ .app-nav-badge {
246
+ margin-left: auto;
247
+ padding: 1px 7px;
248
+ border-radius: 999px;
249
+ background: var(--color-bg-muted);
250
+ font-size: 11px;
251
+ font-weight: 500;
252
+ color: var(--color-text-subtle);
253
+ }
254
+ // On an active row the badge inverts: the row already carries the soft tint, so a
255
+ // tinted badge on top of it would vanish.
256
+ .app-nav-item.active .app-nav-badge {
257
+ background: var(--color-surface);
258
+ color: var(--color-primary);
259
+ }
260
+
261
+ // Collapsed rail: icons only, centred. The label is removed from the flow rather
262
+ // than hidden, so it cannot leave a phantom gap beside the icon.
263
+ .app-shell.collapsed .app-nav-item {
264
+ justify-content: center;
265
+ gap: 0;
266
+ padding: 9px 0;
267
+ }
268
+ .app-shell.collapsed .app-nav-text,
269
+ .app-shell.collapsed .app-nav-badge,
270
+ .app-shell.collapsed .app-nav-group-label,
271
+ .app-shell.collapsed .app-sidebar-brand-text {
272
+ display: none;
273
+ }
274
+
275
+ // A circular button centred on the sidebar's right edge. It is fixed to the
276
+ // viewport, not a sidebar child, because the sidebar clips its own overflow and
277
+ // would cut the button in half.
278
+ .app-sidebar-toggle {
279
+ position: fixed;
280
+ // 36px = half the 72px topbar, so the button lines up with the topbar's centre.
281
+ top: 36px;
282
+ left: calc(var(--app-sidebar-w) - 12px);
283
+ z-index: 30;
284
+ width: 24px;
285
+ height: 24px;
286
+ display: inline-grid;
287
+ place-items: center;
288
+ transform: translateY(-50%);
289
+ border: 1px solid var(--color-border);
290
+ border-radius: 999px;
291
+ background: var(--color-surface);
292
+ color: var(--color-text-muted);
293
+ cursor: pointer;
294
+ transition:
295
+ left 0.2s var(--wui-ease-ui),
296
+ background 0.12s ease,
297
+ color 0.12s ease,
298
+ box-shadow 0.12s ease;
299
+ }
300
+ .app-sidebar-toggle:hover {
301
+ background: var(--color-bg-muted);
302
+ color: var(--color-text);
303
+ box-shadow: var(--shadow-sm);
304
+ }
305
+ .app-sidebar-toggle:focus-visible {
306
+ outline: 2px solid var(--color-primary);
307
+ outline-offset: 2px;
308
+ }
309
+ .app-shell.collapsed .app-sidebar-toggle {
310
+ left: calc(var(--app-sidebar-rail-w) - 12px);
311
+ }
312
+ .app-sidebar-toggle > * {
313
+ transition: transform 0.2s ease;
314
+ }
315
+ .app-shell.collapsed .app-sidebar-toggle > * {
316
+ transform: rotate(180deg);
317
+ }
318
+
319
+ .app-main {
320
+ display: flex;
321
+ flex-direction: column;
322
+ // Without this the content column refuses to shrink below its widest child, so
323
+ // one wide table pushes the whole shell into a horizontal scroll.
324
+ min-width: 0;
325
+ }
326
+ .app-topbar {
327
+ position: sticky;
328
+ top: 0;
329
+ z-index: 10;
330
+ display: flex;
331
+ align-items: center;
332
+ justify-content: space-between;
333
+ gap: 16px;
334
+ min-height: 72px;
335
+ padding: 12px 28px;
336
+ background: var(--color-surface);
337
+ // Transparent rather than absent, so gaining the divider on scroll doesn't
338
+ // shift the layout by a pixel.
339
+ border-bottom: 1px solid transparent;
340
+ transition:
341
+ border-color 0.2s ease,
342
+ box-shadow 0.2s ease;
343
+ }
344
+ .app-topbar.scrolled {
345
+ border-bottom-color: var(--color-border);
346
+ }
347
+ // Two groups: the leading one shrinks and ellipsises a long page title, the
348
+ // trailing one is pinned so actions are never pushed off-screen.
349
+ .app-topbar-start {
350
+ display: flex;
351
+ align-items: center;
352
+ gap: 12px;
353
+ min-width: 0;
354
+ }
355
+ .app-topbar-end {
356
+ flex: none;
357
+ display: flex;
358
+ align-items: center;
359
+ gap: 8px;
360
+ }
361
+ .app-topbar-menu {
362
+ display: none;
363
+ flex: none;
364
+ width: 36px;
365
+ height: 36px;
366
+ place-items: center;
367
+ border: 1px solid var(--color-border);
368
+ border-radius: 999px;
369
+ background: var(--color-surface);
370
+ color: var(--color-text-muted);
371
+ cursor: pointer;
372
+ }
373
+ .app-topbar-menu:focus-visible {
374
+ outline: 2px solid var(--color-primary);
375
+ outline-offset: 2px;
376
+ }
377
+
378
+ .app-content {
379
+ flex: 1 1 auto;
380
+ min-width: 0;
381
+ padding: 24px 28px;
382
+ }
383
+ .app-footer {
384
+ position: fixed;
385
+ right: 0;
386
+ bottom: 0;
387
+ left: var(--app-sidebar-w);
388
+ z-index: 30;
389
+ background: color-mix(in srgb, var(--color-surface) 90%, transparent);
390
+ backdrop-filter: blur(50px);
391
+ -webkit-backdrop-filter: blur(50px);
392
+ transition: left 0.2s var(--wui-ease-ui);
393
+ }
394
+ .app-shell.collapsed .app-footer {
395
+ left: var(--app-sidebar-rail-w);
396
+ }
397
+
398
+ // Below lg the sidebar becomes a drawer: off-canvas by default, slid in over a
399
+ // scrim. The grid drops to one column so the content takes the full width.
400
+ @include media-breakpoint-down(lg) {
401
+ .app-shell.app-shell,
402
+ .app-shell.app-shell.collapsed {
403
+ grid-template-columns: 1fr;
404
+ }
405
+ .app-shell.app-shell .app-sidebar {
406
+ position: fixed;
407
+ top: 0;
408
+ bottom: 0;
409
+ left: 0;
410
+ z-index: 1045;
411
+ width: var(--app-sidebar-w);
412
+ padding: 20px 16px;
413
+ transform: translateX(-100%);
414
+ transition: transform 0.24s var(--wui-ease-out-quart);
415
+ box-shadow: var(--shadow-overlay-lg);
416
+ }
417
+ .app-shell.app-shell.mobile-open .app-sidebar {
418
+ transform: translateX(0);
419
+ }
420
+ // The rail is a desktop affordance; on a drawer it makes no sense, so the
421
+ // drawer always shows full labels even when the desktop state is collapsed.
422
+ .app-shell.app-shell.collapsed .app-nav-item {
423
+ justify-content: flex-start;
424
+ gap: 12px;
425
+ padding: 9px 14px;
426
+ }
427
+ .app-shell.app-shell.collapsed .app-nav-text,
428
+ .app-shell.app-shell.collapsed .app-nav-badge,
429
+ .app-shell.app-shell.collapsed .app-nav-group-label {
430
+ display: revert;
431
+ }
432
+ .app-shell.app-shell .app-sidebar-toggle {
433
+ display: none;
434
+ }
435
+ .app-shell.app-shell .app-topbar-menu {
436
+ display: inline-grid;
437
+ }
438
+ .app-shell.app-shell .app-footer,
439
+ .app-shell.app-shell.collapsed .app-footer {
440
+ left: 0;
441
+ }
442
+ .app-shell.app-shell .app-content,
443
+ .app-shell.app-shell .app-topbar {
444
+ padding-inline: 16px;
445
+ }
446
+ }
447
+ // The scrim exists only while the drawer is open, and only below lg — it is
448
+ // rendered unconditionally and made visible here so there is no JS breakpoint check.
449
+ .app-scrim {
450
+ display: none;
451
+ }
452
+ @include media-breakpoint-down(lg) {
453
+ .app-shell.app-shell.mobile-open .app-scrim {
454
+ position: fixed;
455
+ inset: 0;
456
+ z-index: 1044;
457
+ display: block;
458
+ border: 0;
459
+ padding: 0;
460
+ background: var(--color-scrim);
461
+ }
462
+ }
463
+
464
+ @media (prefers-reduced-motion: reduce) {
465
+ .app-shell,
466
+ .app-sidebar,
467
+ .app-nav-item,
468
+ .app-sidebar-toggle,
469
+ .app-sidebar-toggle > *,
470
+ .app-footer,
471
+ .app-topbar {
472
+ transition: none;
473
+ }
474
+ }