@webority/theme 0.7.1 → 0.7.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/theme",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Webority shared design system — Bootstrap 5.3 SCSS overrides, --wui-* tokens, and shared component CSS. The single source of brand chrome for React portals and Razor sites.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": {
@@ -49,8 +49,11 @@
49
49
  // ---- Display heading -------------------------------------------------
50
50
  // Bootstrap's .display-1..6 ladder skews bigger than our locked design.
51
51
  // .h-display drops in where the marketing hero needs the locked
52
- // font-weight 500 + tight letter-spacing.
52
+ // font-weight 500 + tight letter-spacing. font-family reads --wui-font-display
53
+ // so a product can swap in a serif/brand display face by overriding just that
54
+ // token (default: the same body stack .font-display already uses).
53
55
  .h-display {
56
+ font-family: var(--wui-font-display, var(--bs-body-font-family));
54
57
  font-weight: 500;
55
58
  letter-spacing: -0.03em;
56
59
  line-height: 1.05;
@@ -89,6 +92,14 @@
89
92
  --bs-btn-hover-border-color: var(--wui-primary-pressed);
90
93
  --bs-btn-active-bg: var(--wui-primary-pressed);
91
94
  --bs-btn-active-border-color: var(--wui-primary-pressed);
95
+ // Bootstrap computes this label colour at SCSS compile time from the library's
96
+ // default $primary, so it can never react to a product overriding --wui-primary as a
97
+ // runtime CSS var. Routing it through the token makes a light-primary brand's
98
+ // --wui-primary-text override reach the button.
99
+ --bs-btn-color: var(--wui-primary-text);
100
+ --bs-btn-hover-color: var(--wui-primary-text);
101
+ --bs-btn-active-color: var(--wui-primary-text);
102
+ --bs-btn-disabled-color: var(--wui-primary-text);
92
103
  }
93
104
 
94
105
  .btn-danger {
@@ -110,6 +121,24 @@
110
121
  --bs-btn-disabled-border-color: var(--wui-danger-strong);
111
122
  }
112
123
 
124
+ // Themed disabled state (fleet default). Bootstrap's own disabled look is just the
125
+ // variant's colour at --bs-btn-disabled-opacity (.65) — barely different from the active
126
+ // button on a tinted brand palette. Give the ordinary case a real desaturated/muted look
127
+ // instead, reusing the same tokens .input:disabled already uses so a disabled button and
128
+ // a disabled field read as the same state.
129
+ //
130
+ // Excludes .btn-primary and .btn-danger, which already theme their OWN disabled state
131
+ // above (a faded version of their own fill, driven by --bs-btn-disabled-*) — :not() keeps
132
+ // this rule from fighting theirs rather than relying on source order.
133
+ .btn:disabled:not(.btn-primary):not(.btn-danger),
134
+ .btn.disabled:not(.btn-primary):not(.btn-danger) {
135
+ background-color: var(--color-bg-muted);
136
+ border-color: var(--color-border);
137
+ color: var(--color-text-subtle);
138
+ opacity: 1;
139
+ cursor: not-allowed;
140
+ }
141
+
113
142
 
114
143
  // ---- Component radii from tokens ------------------------------------
115
144
  // .btn/.card/.input capture Bootstrap's compiled $*-border-radius (the library
@@ -265,6 +294,30 @@
265
294
  }
266
295
 
267
296
 
297
+ // ---- Ghost button (transparent, brand-tinted hover) ------------------
298
+ // A real ghost button: transparent at rest (no fill, no border), current text colour;
299
+ // only the background lights up — a soft brand tint, the same recipe as a
300
+ // .dropdown-item's hover — on hover/active. AppButton's `ghost` variant emits this class
301
+ // on both surfaces; distinct from Bootstrap's .btn-link, which reads as a hyperlink
302
+ // (underlined, link-coloured text) rather than a quiet button. Marketing anchors reach
303
+ // for it directly for the same look on a plain <a>.
304
+ //
305
+ // No .btn-ghost-sm/-lg rungs: unlike .btn-pill (a bespoke marketing sizing system), this
306
+ // is an ordinary .btn variant, so it sizes the same way .btn-primary/.btn-danger do —
307
+ // Bootstrap's own .btn-sm/.btn-lg.
308
+ .btn-ghost {
309
+ --bs-btn-color: var(--color-text);
310
+ --bs-btn-bg: transparent;
311
+ --bs-btn-border-color: transparent;
312
+ --bs-btn-hover-color: var(--color-text);
313
+ --bs-btn-hover-bg: var(--wui-primary-5);
314
+ --bs-btn-hover-border-color: transparent;
315
+ --bs-btn-active-color: var(--color-text);
316
+ --bs-btn-active-bg: var(--wui-primary-10);
317
+ --bs-btn-active-border-color: transparent;
318
+ }
319
+
320
+
268
321
  // ---- Card "lifted" (shadowless) -------------------------------------
269
322
  // Previously a soft drop-shadow card; now flat. The `.card-lifted`
270
323
  // class is preserved so existing call sites don't break — it now
package/scss/_tokens.scss CHANGED
@@ -119,6 +119,15 @@
119
119
  --wui-container-max: 1512px;
120
120
  --wui-container-pad: 24px;
121
121
 
122
+ // ---- Primary "on-fill" text --------------------------------------
123
+ // Unlike the semantic -text tokens below (a legible STANDALONE colour derived
124
+ // from a fill, for using the status colour AS body text), this is the text/icon
125
+ // colour to use ON TOP OF a solid --wui-primary fill — a selected datepicker day,
126
+ // an active pagination page, a checked segmented option, .btn-primary's label.
127
+ // Default assumes primary is dark enough for white; a brand with a light primary
128
+ // overrides this to a dark value instead.
129
+ --wui-primary-text: #fff;
130
+
122
131
  // ---- Semantic status ramps -------------------------------------
123
132
  // Canonical info/success/warning/danger scales: `active` (text/icon on
124
133
  // light), the base `100` (vivid fills/dots), then tints 50/30/20/10/5.
@@ -189,6 +198,38 @@
189
198
  --wui-dark-text-subtle: #8a90a0;
190
199
  --wui-dark-primary: #6B83EC;
191
200
  --wui-dark-primary-hover:#93A6F2;
201
+
202
+ // ---- Fluid type scale --------------------------------------------
203
+ // clamp(min, preferred, max) so type scales smoothly between viewports instead of
204
+ // stepping at Bootstrap's breakpoints. `base` sits close to Bootstrap's own 1rem body
205
+ // size; every other step is derived at a consistent ratio, up and down. Additive only —
206
+ // nothing in the shared theme consumes these yet, a product opts in per element.
207
+ --wui-fs-xs: clamp(0.75rem, 0.73rem + 0.1vw, 0.8125rem);
208
+ --wui-fs-sm: clamp(0.8125rem, 0.79rem + 0.12vw, 0.875rem);
209
+ --wui-fs-base: clamp(0.95rem, 0.9rem + 0.25vw, 1.05rem);
210
+ --wui-fs-lg: clamp(1.05rem, 0.98rem + 0.35vw, 1.25rem);
211
+ --wui-fs-xl: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
212
+ --wui-fs-2xl: clamp(1.5rem, 1.3rem + 1vw, 2rem);
213
+ --wui-fs-3xl: clamp(1.875rem, 1.5rem + 1.75vw, 2.75rem);
214
+
215
+ // ---- Display font slot --------------------------------------------
216
+ // A dedicated slot for a display/heading typeface, kept separate from
217
+ // $font-family-base so a product can point ONLY its display headings (.h-display) at a
218
+ // serif or another brand face by overriding this one token, without touching body copy.
219
+ // Defaults to the same stack .font-display already uses.
220
+ --wui-font-display: var(--bs-body-font-family);
221
+
222
+ // ---- Z-index scale -------------------------------------------------
223
+ // Bootstrap's own ladder ($zindex-dropdown etc.), exposed as CSS vars so a component
224
+ // or a consuming product can layer against the same scale instead of inventing values.
225
+ --wui-z-dropdown: 1000;
226
+ --wui-z-sticky: 1020;
227
+ --wui-z-fixed: 1030;
228
+ --wui-z-modal-backdrop: 1040;
229
+ --wui-z-modal: 1050;
230
+ --wui-z-popover: 1060;
231
+ --wui-z-tooltip: 1070;
232
+ --wui-z-toast: 1080;
192
233
  }
193
234
 
194
235
  // Bootstrap md / xl (not invented px)
@@ -36,6 +36,9 @@
36
36
  --color-primary: var(--wui-primary);
37
37
  --color-primary-hover: var(--wui-primary-pressed);
38
38
  --color-primary-soft: var(--wui-primary-10);
39
+ // Text/icon colour for content sitting ON a solid --color-primary fill — see
40
+ // --wui-primary-text for why this is a different axis from the -text tokens below.
41
+ --color-primary-text: var(--wui-primary-text);
39
42
 
40
43
  // Semantic status ramps
41
44
  --color-info-active: var(--wui-info-active);
@@ -34,6 +34,7 @@
34
34
  @import "wui/disclosure";
35
35
  @import "wui/display";
36
36
  @import "wui/flow";
37
+ @import "wui/scrollbar";
37
38
 
38
39
  // Flags belong here because <wui-phone> cannot render without them.
39
40
  //
@@ -415,6 +415,11 @@
415
415
  color: var(--color-primary);
416
416
  font-weight: 500;
417
417
  }
418
+ // allow-custom's freeform row — reads as an action ("Add "…"") rather than a listed value.
419
+ .search-select-option-custom {
420
+ font-style: italic;
421
+ color: var(--color-primary);
422
+ }
418
423
  .search-select-empty {
419
424
  padding: 12px 10px;
420
425
  font-size: 13px;
@@ -627,7 +632,7 @@ button.datepicker-month:hover {
627
632
  }
628
633
  .datepicker-day.is-selected {
629
634
  background: var(--color-primary);
630
- color: #fff;
635
+ color: var(--color-primary-text);
631
636
  font-weight: 600;
632
637
  }
633
638
  .datepicker-day:disabled {
@@ -696,7 +701,7 @@ button.datepicker-month:hover {
696
701
  }
697
702
  .datepicker-cell.is-selected {
698
703
  background: var(--color-primary);
699
- color: #fff;
704
+ color: var(--color-primary-text);
700
705
  font-weight: 600;
701
706
  }
702
707
  .datepicker-cell:disabled {
@@ -842,7 +847,7 @@ button.datepicker-month:hover {
842
847
  .datepicker-day.is-range-start,
843
848
  .datepicker-day.is-range-end {
844
849
  background: var(--color-primary);
845
- color: #fff;
850
+ color: var(--color-primary-text);
846
851
  font-weight: 600;
847
852
  }
848
853
  .daterange-hint {
@@ -348,9 +348,12 @@
348
348
  resize: none;
349
349
  overflow: hidden;
350
350
  }
351
- .input:hover:not(:focus):not(:disabled),
352
- .select:hover:not(:focus):not(:disabled),
353
- .textarea:hover:not(:focus):not(:disabled) {
351
+ // The `.error` / `.is-invalid` / `[aria-invalid="true"]` guards keep an invalid field's
352
+ // danger border visible on hover — without them the hover rule repaints it grey and the
353
+ // error signal disappears the moment the cursor passes over the field.
354
+ .input:hover:not(:focus):not(:disabled):not([aria-invalid="true"]):not(.is-invalid):not(.error),
355
+ .select:hover:not(:focus):not(:disabled):not([aria-invalid="true"]):not(.is-invalid):not(.error),
356
+ .textarea:hover:not(:focus):not(:disabled):not([aria-invalid="true"]):not(.is-invalid):not(.error) {
354
357
  background-color: var(--color-bg-muted);
355
358
  border-color: var(--color-border-strong);
356
359
  }
@@ -407,6 +407,46 @@
407
407
  cursor: not-allowed;
408
408
  }
409
409
 
410
+ // Numbered mode (mode="numbered") — windowed page buttons sitting between the prev/next
411
+ // arrows in .tpg-nav, same 32px circular hit target as .tpg-arrow.
412
+ .tpg-page {
413
+ display: grid;
414
+ place-items: center;
415
+ min-width: 32px;
416
+ height: 32px;
417
+ padding: 0 6px;
418
+ border: none;
419
+ background: transparent;
420
+ color: var(--color-text-muted);
421
+ border-radius: 50%;
422
+ font-size: 13px;
423
+ font-weight: 600;
424
+ font-variant-numeric: tabular-nums;
425
+ text-decoration: none;
426
+ cursor: pointer;
427
+ transition:
428
+ background 0.14s ease,
429
+ color 0.14s ease;
430
+ }
431
+ .tpg-page:hover:not(.is-active) {
432
+ background: color-mix(in srgb, var(--color-primary) 10%, transparent);
433
+ color: var(--color-primary);
434
+ }
435
+ .tpg-page.is-active {
436
+ background: var(--color-primary);
437
+ color: var(--color-primary-text);
438
+ cursor: default;
439
+ }
440
+ .tpg-ellipsis {
441
+ display: grid;
442
+ place-items: center;
443
+ min-width: 24px;
444
+ height: 32px;
445
+ color: var(--color-text-subtle);
446
+ font-size: 13px;
447
+ user-select: none;
448
+ }
449
+
410
450
  // Horizontal scroll guard + flush-fit inside a card. Wrapping the table here
411
451
  // also stops `.card > .table` header-corner rounding from applying mid-card.
412
452
  .table-wrap {
@@ -31,7 +31,9 @@
31
31
  .kpi-label {
32
32
  font-size: 13px;
33
33
  font-weight: 500;
34
- color: var(--color-text-subtle);
34
+ // --color-text-muted (not -subtle): -subtle is ~4.5:1 on white and dips below AA on the
35
+ // filled/accent variant's tinted surface. -muted clears AA on every kpi variant.
36
+ color: var(--color-text-muted);
35
37
  }
36
38
  .kpi-value {
37
39
  margin-top: 6px;
@@ -76,6 +78,18 @@
76
78
  margin-top: 2px;
77
79
  }
78
80
 
81
+ // Surface variants — plain (above) is the bordered card; these swap the fill and drop the
82
+ // border so a dashboard can mix a KPI strip with the surrounding card without every tile
83
+ // drawing its own outline.
84
+ .kpi.kpi-filled {
85
+ border-color: transparent;
86
+ background: var(--color-bg-subtle);
87
+ }
88
+ .kpi.kpi-accent {
89
+ border-color: transparent;
90
+ background: var(--color-primary-soft);
91
+ }
92
+
79
93
  // A row of tiles. Wraps rather than scrolls — a KPI strip that scrolls hides numbers.
80
94
  .kpi-row {
81
95
  display: grid;
@@ -213,11 +227,34 @@ a.info-row:hover .info-value {
213
227
  .segmented-option:hover {
214
228
  color: var(--color-text);
215
229
  }
230
+ // A per-option sub-label under the main text — optional, so a segment without one keeps the
231
+ // plain single-line layout above untouched.
232
+ .segmented-option-text {
233
+ display: flex;
234
+ flex-direction: column;
235
+ align-items: flex-start;
236
+ gap: 1px;
237
+ line-height: 1.2;
238
+ }
239
+ .segmented-option-label {
240
+ font: inherit;
241
+ color: inherit;
242
+ }
243
+ .segmented-option-note {
244
+ font-size: 10px;
245
+ font-weight: 400;
246
+ // Inherits the option's current text colour (muted, hover, or white-on-checked) rather
247
+ // than a fixed token, so it always matches without a separate state per variant. No opacity
248
+ // muting — that dropped the sub-label below AA on an unchecked option; the 10px size already
249
+ // distinguishes it from the label.
250
+ color: currentColor;
251
+ }
216
252
  .segmented-option:has(.segmented-input:checked) {
217
253
  background: var(--color-primary);
218
- // Fixed white, not a token: the pill is always the brand fill, so its label
219
- // must contrast with that fill and not with the page.
220
- color: #fff;
254
+ // Token, not fixed white: the pill is always the brand fill, so its label must
255
+ // contrast with THAT fill, not the page — a light-primary brand overrides
256
+ // --wui-primary-text to a dark value and this follows automatically.
257
+ color: var(--color-primary-text);
221
258
  }
222
259
  // The ring is drawn on the label because the input it belongs to has no box.
223
260
  .segmented-option:has(.segmented-input:focus-visible) {
@@ -79,58 +79,10 @@
79
79
  flex-direction: column;
80
80
  overflow: hidden;
81
81
  }
82
- .wui-modal-wide {
83
- max-width: var(--wui-modal-max-width, 1240px);
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
+ }