@webority/theme 0.7.2 → 0.7.4

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.
@@ -28,6 +28,33 @@
28
28
  background: var(--color-primary-soft);
29
29
  color: var(--color-primary);
30
30
  }
31
+ // Tone modifiers — same semantic vocabulary as .badge/.status-label (success / warning /
32
+ // danger / info / primary / neutral). primary is the default look above, restated here so
33
+ // an explicit icon-tone="primary" matches an omitted one.
34
+ .kpi-icon-primary {
35
+ background: var(--color-primary-soft);
36
+ color: var(--color-primary);
37
+ }
38
+ .kpi-icon-success {
39
+ background: var(--color-success-soft);
40
+ color: var(--color-success-text);
41
+ }
42
+ .kpi-icon-warning {
43
+ background: var(--color-warning-soft);
44
+ color: var(--color-warning-text);
45
+ }
46
+ .kpi-icon-danger {
47
+ background: var(--color-danger-soft);
48
+ color: var(--color-danger-text);
49
+ }
50
+ .kpi-icon-info {
51
+ background: var(--color-info-soft);
52
+ color: var(--color-info-text);
53
+ }
54
+ .kpi-icon-neutral {
55
+ background: var(--color-bg-muted);
56
+ color: var(--color-text-muted);
57
+ }
31
58
  .kpi-label {
32
59
  font-size: 13px;
33
60
  font-weight: 500;
@@ -61,6 +88,20 @@
61
88
  .kpi-delta.down {
62
89
  color: var(--color-danger-text);
63
90
  }
91
+ // Trend chart. currentColor so it inherits kpi-value's colour by default, or a wrapping
92
+ // tone context (e.g. .kpi-delta.up) if placed there — no colour hardcoded here.
93
+ .kpi-sparkline {
94
+ display: block;
95
+ width: 48px;
96
+ height: 16px;
97
+ margin-top: 8px;
98
+ color: var(--color-primary);
99
+ }
100
+ .kpi-sparkline polyline {
101
+ stroke-width: 1.5;
102
+ stroke-linecap: round;
103
+ stroke-linejoin: round;
104
+ }
64
105
 
65
106
  // Icon-left layout: the icon spans the label/value/delta stack instead of sitting above it.
66
107
  .kpi.kpi-inline {
@@ -89,6 +130,48 @@
89
130
  border-color: transparent;
90
131
  background: var(--color-primary-soft);
91
132
  }
133
+ // Needs-action state — orthogonal to variant, so it composes with plain/filled/accent
134
+ // rather than replacing the surface.
135
+ .kpi.kpi-attention {
136
+ border-color: var(--color-warning);
137
+ background: var(--color-warning-soft);
138
+ }
139
+
140
+ // Hero size — the single headline stat above a dashboard. Enlarges the value and its
141
+ // chrome; orthogonal to variant/inline/attention, so it composes with all of them.
142
+ .kpi.kpi-lg {
143
+ padding: 20px;
144
+ }
145
+ .kpi.kpi-lg .kpi-label {
146
+ font-size: 14px;
147
+ }
148
+ .kpi.kpi-lg .kpi-value {
149
+ font-size: 40px;
150
+ }
151
+ .kpi.kpi-lg .kpi-icon {
152
+ width: 44px;
153
+ height: 44px;
154
+ }
155
+
156
+ // The whole tile as a link (href). Colour/underline reset so the anchor reads as the
157
+ // card, not as text; the hover lift matches .res-card's tint-and-lift treatment.
158
+ a.kpi {
159
+ text-decoration: none;
160
+ color: inherit;
161
+ transition:
162
+ border-color 0.15s ease,
163
+ box-shadow 0.15s ease,
164
+ transform 0.15s ease;
165
+ }
166
+ a.kpi:hover {
167
+ border-color: var(--color-primary);
168
+ box-shadow: var(--shadow-md);
169
+ transform: translateY(-2px);
170
+ }
171
+ a.kpi:focus-visible {
172
+ outline: 2px solid var(--color-primary);
173
+ outline-offset: 2px;
174
+ }
92
175
 
93
176
  // A row of tiles. Wraps rather than scrolls — a KPI strip that scrolls hides numbers.
94
177
  .kpi-row {
@@ -163,6 +246,11 @@
163
246
  text-overflow: ellipsis;
164
247
  white-space: nowrap;
165
248
  }
249
+ // A blank fact reads as a muted "no value", not a broken row.
250
+ .info-value.is-empty {
251
+ font-weight: 400;
252
+ color: var(--color-text-subtle);
253
+ }
166
254
  a.info-row,
167
255
  a.info-row:hover {
168
256
  text-decoration: none;
@@ -494,6 +494,30 @@
494
494
  .file-preview:hover .file-preview-img {
495
495
  transform: scale(1.03);
496
496
  }
497
+ // Non-image single file: a centered file-type icon + name filling the same shell as the image
498
+ // thumbnail, so a PDF/doc doesn't render as a broken <img>.
499
+ .file-preview-file {
500
+ display: flex;
501
+ flex-direction: column;
502
+ align-items: center;
503
+ justify-content: center;
504
+ gap: 8px;
505
+ width: 100%;
506
+ height: 100%;
507
+ padding: 16px;
508
+ color: var(--color-text-subtle);
509
+ background: var(--color-bg-muted);
510
+ }
511
+ .file-preview-file-name {
512
+ max-width: 100%;
513
+ overflow: hidden;
514
+ font-size: 13px;
515
+ line-height: 1.4;
516
+ color: var(--color-text);
517
+ text-align: center;
518
+ text-overflow: ellipsis;
519
+ white-space: nowrap;
520
+ }
497
521
  .file-preview-overlay {
498
522
  position: absolute;
499
523
  inset: 0;
@@ -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 tab strip keeps scrolling even if a consumer loads
125
- // Bootstrap after the theme, which would otherwise let `.nav` win on order.
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
@@ -16,13 +16,13 @@
16
16
  // .modal.
17
17
  .modal-backdrop.wui-modal-dim {
18
18
  background: rgba(15, 23, 42, 0.45);
19
- z-index: 1500;
19
+ z-index: var(--wui-z-modal-backdrop);
20
20
  }
21
21
  .modal.wui-modal-shell {
22
22
  --bs-modal-bg: transparent;
23
23
  align-items: center;
24
24
  justify-content: center;
25
- z-index: 1505;
25
+ z-index: var(--wui-z-modal);
26
26
  padding: 20px;
27
27
  }
28
28
  // `display: flex` ONLY while the dialog is actually open.
@@ -1,12 +1,10 @@
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.
1
+ // AppScrollbar / opt-in internal scrollers — same tokens as the document rail
2
+ // (--wui-scrollbar*). Document chrome lives on html/body only; use this class
3
+ // for panels, drawers, and modal bodies that need a matching thin thumb.
6
4
  .wui-scroll {
7
5
  overflow: auto;
8
6
  scrollbar-width: thin;
9
- scrollbar-color: var(--wui-text-3) transparent;
7
+ scrollbar-color: var(--wui-scrollbar) transparent;
10
8
  }
11
9
 
12
10
  .wui-scroll::-webkit-scrollbar {
@@ -19,14 +17,18 @@
19
17
  }
20
18
 
21
19
  .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.
20
+ background-color: var(--wui-scrollbar);
21
+ border-radius: 9999px;
26
22
  border: 2px solid transparent;
27
23
  background-clip: padding-box;
28
24
  }
29
25
 
30
26
  .wui-scroll::-webkit-scrollbar-thumb:hover {
31
- background-color: var(--wui-text-2);
27
+ background-color: var(--wui-scrollbar-hover);
28
+ background-clip: padding-box;
29
+ }
30
+
31
+ .wui-scroll::-webkit-scrollbar-thumb:active {
32
+ background-color: var(--wui-scrollbar-active);
33
+ background-clip: padding-box;
32
34
  }
@@ -13,7 +13,7 @@
13
13
  inset: 0;
14
14
  // Above the app but below a modal's backdrop, so an error dialog raised during
15
15
  // boot is never trapped behind it.
16
- z-index: 1040;
16
+ z-index: var(--wui-z-shell);
17
17
  display: grid;
18
18
  place-items: center;
19
19
  padding: 24px;
@@ -619,7 +619,7 @@
619
619
  top: 0;
620
620
  bottom: 0;
621
621
  left: 0;
622
- z-index: 1045;
622
+ z-index: var(--wui-z-shell-drawer);
623
623
  width: var(--app-sidebar-w);
624
624
  padding: 20px 16px;
625
625
  transform: translateX(-100%);
@@ -665,7 +665,7 @@
665
665
  .app-shell.app-shell.mobile-open .app-scrim {
666
666
  position: fixed;
667
667
  inset: 0;
668
- z-index: 1044;
668
+ z-index: var(--wui-z-shell-scrim);
669
669
  display: block;
670
670
  border: 0;
671
671
  padding: 0;
@@ -116,7 +116,7 @@
116
116
  .infotip-pop.is-portal {
117
117
  bottom: auto;
118
118
  transform: none;
119
- z-index: 1600;
119
+ z-index: var(--wui-z-menu);
120
120
  }
121
121
  .infotip-pop.is-portal::after {
122
122
  display: none;
@@ -235,6 +235,32 @@
235
235
  border: 2px solid var(--color-bg);
236
236
  }
237
237
 
238
+ // Reset wrapper for a custom AppDropdown trigger (React `trigger` / Razor <app-dropdown-toggle>).
239
+ // Strips the icon-button chrome so the caller's own node styles itself; keeps the button
240
+ // focusable with a visible focus ring for keyboard a11y.
241
+ .wui-dropdown-trigger {
242
+ display: inline-flex;
243
+ align-items: center;
244
+ padding: 0;
245
+ border: none;
246
+ background: transparent;
247
+ color: inherit;
248
+ font: inherit;
249
+ cursor: pointer;
250
+ }
251
+ .wui-dropdown-trigger:focus-visible {
252
+ outline: 2px solid var(--color-primary);
253
+ outline-offset: 2px;
254
+ border-radius: var(--radius-sm);
255
+ }
256
+
257
+ // AppPopover wrapper (React panel styling comes from Bootstrap's .popover; this is only the
258
+ // Razor anchor span carrying the data-bs-* wiring). inline-block so it hugs its trigger and
259
+ // gives Bootstrap a stable anchor box.
260
+ .app-popover {
261
+ display: inline-block;
262
+ }
263
+
238
264
  // ── Spinner / shimmer ──
239
265
  .wui-spinner {
240
266
  width: 18px;