@webority/theme 0.8.8 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/theme",
3
- "version": "0.8.8",
3
+ "version": "0.9.0",
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": {
package/scss/_tokens.scss CHANGED
@@ -93,10 +93,6 @@
93
93
  --wui-surface-soft: #F2EFFE;
94
94
  --wui-surface-violet: #ECE4FE;
95
95
  --wui-surface-violet-soft: #FCFCFC;
96
- // Deprecated aliases — the value was always lavender, never blue. Published
97
- // names, so consumers migrate on their own bump; removed in a future major.
98
- --wui-surface-blue: var(--wui-surface-violet);
99
- --wui-surface-blue-soft: var(--wui-surface-violet-soft);
100
96
  --wui-surface-mute: #F9FBFC;
101
97
  --wui-ink: #000000;
102
98
 
@@ -920,10 +920,26 @@ button.datepicker-month:hover {
920
920
  cursor: not-allowed;
921
921
  }
922
922
  .app-phone-number-input .phone-input-wrap:has(.input.error),
923
- .app-phone-number-input .phone-input-wrap:has(.input.is-invalid) {
923
+ .app-phone-number-input .phone-input-wrap:has(.input.is-invalid),
924
+ // An explicit `error` prop sets aria-invalid on the host <wui-phone> (React + Razor); redden the
925
+ // shell for it too, not just the internal live-validation state above.
926
+ .app-phone-number-input wui-phone[aria-invalid="true"] .phone-input-wrap {
924
927
  // Match AppInput's soft invalid border exactly (danger at 30%, mirroring primary-30 focus).
925
928
  border-color: color-mix(in srgb, var(--color-danger) 30%, transparent);
926
929
  }
930
+ // Popover-field controls (date / date-range / time) carry aria-invalid on the HOST element —
931
+ // React/Razor set it from the `error` prop — while the visible border lives on the inner trigger
932
+ // button. Redden that trigger, and keep it red while the popover is open (mirroring AppInput,
933
+ // whose invalid border persists through focus), so an errored/required field reads red like the
934
+ // plain field and the phone control.
935
+ wui-datepicker[aria-invalid="true"] .datepicker-trigger,
936
+ wui-datepicker[aria-invalid="true"] .datepicker.open .datepicker-trigger,
937
+ wui-date-range[aria-invalid="true"] .daterange-trigger,
938
+ wui-date-range[aria-invalid="true"] .daterange.open .daterange-trigger,
939
+ wui-timepicker[aria-invalid="true"] .timepicker-trigger,
940
+ wui-timepicker[aria-invalid="true"] .timepicker.open .timepicker-trigger {
941
+ border-color: color-mix(in srgb, var(--color-danger) 30%, transparent);
942
+ }
927
943
  // The inner control drops its own chrome — the shell carries it.
928
944
  .app-phone-number-input .phone-input-wrap > .input,
929
945
  .app-phone-number-input .phone-input-wrap > .input:hover,
@@ -1060,8 +1060,13 @@ wui-select[compact] {
1060
1060
  background: var(--color-surface);
1061
1061
  border-color: var(--wui-primary-30);
1062
1062
  }
1063
- .num-wrap:has(.num-input.error) {
1064
- border-color: var(--color-danger);
1063
+ .num-wrap:has(.num-input.error),
1064
+ .num-wrap:has(.num-input.is-invalid),
1065
+ .num-wrap:has(.num-input[aria-invalid="true"]) {
1066
+ // Soft invalid border — match AppInput / AppPhoneNumberInput exactly (danger at 30%,
1067
+ // mirroring the primary-30 focus border), not the solid full-strength danger this used to
1068
+ // carry, so every field's error border reads the same light red.
1069
+ border-color: color-mix(in srgb, var(--color-danger) 30%, transparent);
1065
1070
  }
1066
1071
 
1067
1072
  // Numeric input inside the stepper — plain text field, no native spinner.
@@ -1188,8 +1193,7 @@ wui-select[compact] {
1188
1193
  .badge-success {
1189
1194
  @include wui-tone-pill-colors(success);
1190
1195
  }
1191
- .badge-warning,
1192
- .badge-warn {
1196
+ .badge-warning {
1193
1197
  @include wui-tone-pill-colors(warning);
1194
1198
  }
1195
1199
  .badge-danger {
@@ -1223,29 +1227,16 @@ wui-select[compact] {
1223
1227
  // the same vocabulary .chip, .badge and every other tone-bearing component uses.
1224
1228
  // A semantic name survives a palette change; a colour name does not (a "green"
1225
1229
  // tone is a lie the moment the brand's success colour stops being green).
1226
- //
1227
- // Two other vocabularies existed and are accepted as DEPRECATED ALIASES so a
1228
- // consumer can migrate without a flag day:
1229
- // - colour-named .tone-green / -amber / -red / -blue / -grey (was the library's)
1230
- // - short semantic .tone-ok / -warn / -bad (a short-semantic set)
1231
- // Author new markup with the semantic names; the aliases will be removed.
1232
- .status-label.success,
1233
- .status-label.tone-ok,
1234
- .status-label.tone-green {
1230
+ .status-label.success {
1235
1231
  @include wui-tone-pill-colors(success);
1236
1232
  }
1237
- .status-label.warning,
1238
- .status-label.tone-warn,
1239
- .status-label.tone-amber {
1233
+ .status-label.warning {
1240
1234
  @include wui-tone-pill-colors(warning);
1241
1235
  }
1242
- .status-label.danger,
1243
- .status-label.tone-bad,
1244
- .status-label.tone-red {
1236
+ .status-label.danger {
1245
1237
  @include wui-tone-pill-colors(danger);
1246
1238
  }
1247
- .status-label.info,
1248
- .status-label.tone-blue {
1239
+ .status-label.info {
1249
1240
  @include wui-tone-pill-colors(info);
1250
1241
  }
1251
1242
  .status-label.primary {
@@ -1254,8 +1245,7 @@ wui-select[compact] {
1254
1245
  // Default/neutral — also the bare `.status-label` look, so an unknown or omitted
1255
1246
  // tone still renders as a proper pill instead of transparent text.
1256
1247
  .status-label,
1257
- .status-label.neutral,
1258
- .status-label.tone-grey {
1248
+ .status-label.neutral {
1259
1249
  background: color-mix(in srgb, var(--color-text-subtle) 16%, transparent);
1260
1250
  // The tertiary tier is readable on a plain surface but not on its own 16% tint,
1261
1251
  // so the neutral pill takes the secondary tier instead of a sixth `-text` token.
@@ -208,16 +208,30 @@
208
208
  }
209
209
  .app-tab.active {
210
210
  color: var(--color-primary);
211
- // Static fallback underline (no-JS / first paint); the ink bar takes over once wired.
212
- border-bottom-color: var(--color-primary);
213
211
  // The active tab carries weight as well as colour — colour alone is a weak
214
212
  // signal for anyone who reads the strip at a glance or cannot distinguish it.
215
213
  font-weight: 600;
216
214
  }
217
- // Once JS wires the ink bar it owns the underline — drop the per-tab fallback so the
218
- // two never double up during a slide.
219
- .app-tabs[data-wui-tabs] .app-tab.active {
220
- border-bottom-color: transparent;
215
+ // Static fallback underline (no-JS / first paint); the sliding ink bar takes over once
216
+ // wired. Inset 10px (tab padding is 14px) so the bar runs the label width + 4px each side
217
+ // — reads as belonging to the label yet never touches a rounded container edge. Matches
218
+ // the wired ink bar's inset below.
219
+ .app-tab.active::after {
220
+ content: "";
221
+ position: absolute;
222
+ left: var(--wui-tab-underline-inset, 10px);
223
+ right: var(--wui-tab-underline-inset, 10px);
224
+ bottom: -1px;
225
+ height: 2px;
226
+ border-radius: 2px;
227
+ background: var(--color-primary);
228
+ }
229
+ // Once JS wires the ink bar it owns the underline; pills fill and vertical use a side
230
+ // rail — none of them want the bottom fallback. Drop it so nothing doubles up.
231
+ .app-tabs[data-wui-tabs] .app-tab.active::after,
232
+ .app-tabs-pills .app-tab.active::after,
233
+ .app-tabs-vertical .app-tab.active::after {
234
+ display: none;
221
235
  }
222
236
  .app-tab:disabled {
223
237
  color: var(--color-text-subtle);
@@ -237,10 +251,9 @@
237
251
  }
238
252
  .app-tab.danger.active {
239
253
  color: var(--color-danger-text);
240
- border-bottom-color: var(--color-danger);
241
254
  }
242
- .app-tabs[data-wui-tabs] .app-tab.danger.active {
243
- border-bottom-color: transparent;
255
+ .app-tab.danger.active::after {
256
+ background: var(--color-danger);
244
257
  }
245
258
  .app-tab.danger .app-tab-count {
246
259
  background: var(--color-danger-soft);
@@ -255,10 +268,13 @@
255
268
  left: 0;
256
269
  bottom: -1px;
257
270
  height: 2px;
258
- width: var(--wui-tab-w, 0);
271
+ // Inset 10px each side (tab padding is 14px) so the bar runs the label width + 4px per
272
+ // side — clear of a rounded container edge; falls to 0 rather than going negative on a
273
+ // tab narrower than the inset. Pills reset the inset to 0 (the pill fills the tab).
274
+ width: max(0px, calc(var(--wui-tab-w, 0) - var(--wui-tab-underline-inset, 10px) * 2));
259
275
  background: var(--color-primary);
260
276
  border-radius: 2px;
261
- transform: translateX(var(--wui-tab-x, 0));
277
+ transform: translateX(calc(var(--wui-tab-x, 0) + var(--wui-tab-underline-inset, 10px)));
262
278
  opacity: 0;
263
279
  pointer-events: none;
264
280
  transition:
@@ -365,6 +381,8 @@
365
381
  bottom: 0;
366
382
  height: auto;
367
383
  z-index: 0;
384
+ // The pill fills the whole tab — no underline inset.
385
+ --wui-tab-underline-inset: 0px;
368
386
  background: var(--color-primary-soft);
369
387
  border-radius: var(--radius);
370
388
  }
@@ -15,15 +15,23 @@
15
15
  // Do NOT put layout on backdropClassName — that targets a sibling, not
16
16
  // .modal.
17
17
  .modal-backdrop.wui-modal-dim {
18
- // Frost the page behind the dialog — the blur is the star, so the dim is kept
19
- // SOFT (a heavy scrim would flatten the blur into a muddy grey). A lighter dim
20
- // over a strong blur reads as clean frosted glass; the modal is still clearly
21
- // the one live surface. The dialog sits in `.modal` (a higher z-index sibling),
22
- // so it is never blurred.
23
- background: color-mix(in srgb, var(--color-scrim) 55%, transparent);
18
+ // Frost the page behind the dialog — the blur is the star, so the dim is kept SOFT (a heavy
19
+ // scrim would flatten the blur into a muddy grey). A moderate dim over a STRONG blur reads as
20
+ // clean frosted glass; the modal is still clearly the one live surface. The dialog sits in
21
+ // `.modal` (a higher z-index sibling), so it is never blurred.
22
+ //
23
+ // Full strength is load-bearing: Bootstrap composites the whole backdrop element at
24
+ // `--bs-backdrop-opacity` (0.5 by default), which HALVES both the blur and the dim into a
25
+ // weak, patchy wash — the bug this fixes. Pin the opacity to 1 and carry the dim entirely in
26
+ // `background`, so the frost renders at its true strength. (The fade-in still animates 0 → 1.)
27
+ --bs-backdrop-opacity: 1;
28
+ opacity: 1;
29
+ background: color-mix(in srgb, var(--color-scrim) 85%, transparent);
24
30
  z-index: var(--wui-z-modal-backdrop);
25
- backdrop-filter: blur(12px);
26
- -webkit-backdrop-filter: blur(12px);
31
+ // A little saturation boost is what makes the frost read as glass rather than fog — the
32
+ // colours behind bloom softly through the blur instead of greying out.
33
+ backdrop-filter: blur(12px) saturate(1.4);
34
+ -webkit-backdrop-filter: blur(12px) saturate(1.4);
27
35
  }
28
36
  .modal.wui-modal-shell {
29
37
  --bs-modal-bg: transparent;
@@ -165,6 +165,87 @@
165
165
  color: currentColor;
166
166
  }
167
167
 
168
+ // ── Variants ──────────────────────────────────────────────────────────────
169
+ // The base `.chip` above is the SOFT variant (tinted surface, on-tint text, no
170
+ // visible border) — the default, unchanged. `.chip-outline` and
171
+ // `.chip-solid` layer on top and MUST stay after the tone block so a
172
+ // `.chip-solid.primary` (0,2,0) wins over `.chip.primary` (0,2,0) on source
173
+ // order. The disabled block below still wins because it comes after both.
174
+
175
+ // Outline — the soft tint kept, but the transparent border swapped for the
176
+ // tone's own colour (a crisper, bounded chip). Only the border changes for a
177
+ // toned chip; neutral drops the muted fill for a clean outlined pill.
178
+ .chip-outline {
179
+ background: transparent;
180
+ border-color: var(--color-border-strong);
181
+ }
182
+ .chip-outline.primary {
183
+ border-color: var(--color-primary);
184
+ }
185
+ .chip-outline.success {
186
+ border-color: var(--color-success);
187
+ }
188
+ .chip-outline.warning {
189
+ border-color: var(--color-warning);
190
+ }
191
+ .chip-outline.danger {
192
+ border-color: var(--color-danger);
193
+ }
194
+ .chip-outline.info {
195
+ border-color: var(--color-info);
196
+ }
197
+ .chip-outline.chip-clickable:hover {
198
+ border-color: var(--color-border-strong);
199
+ }
200
+
201
+ // Solid — a filled pill carrying white text (near-black on the light-primary in
202
+ // dark mode, via --color-primary-text). The fills are the deep, white-safe
203
+ // tokens: --wui-*-strong / --color-primary are AA with white; info & warning
204
+ // have no ready deep token, so they are mixed toward black until white clears
205
+ // AA (warning-with-white only passes on a dark gold — the vibrant --wui-warning
206
+ // cannot). These raw --wui-* deep values are mode-independent, so no dark block
207
+ // is needed; neutral flips automatically because text↔surface swap by theme.
208
+ .chip-solid {
209
+ background: var(--color-text);
210
+ color: var(--color-surface);
211
+ border-color: transparent;
212
+ }
213
+ .chip-solid.primary {
214
+ background: var(--color-primary);
215
+ color: var(--color-primary-text);
216
+ border-color: transparent;
217
+ }
218
+ .chip-solid.success {
219
+ background: var(--wui-success-strong);
220
+ color: var(--color-on-scrim);
221
+ border-color: transparent;
222
+ }
223
+ .chip-solid.danger {
224
+ background: var(--wui-danger-strong);
225
+ color: var(--color-on-scrim);
226
+ border-color: transparent;
227
+ }
228
+ .chip-solid.info {
229
+ background: color-mix(in srgb, var(--wui-info) 78%, #000);
230
+ color: var(--color-on-scrim);
231
+ border-color: transparent;
232
+ }
233
+ .chip-solid.warning {
234
+ background: color-mix(in srgb, var(--wui-warning) 54%, #000);
235
+ color: var(--color-on-scrim);
236
+ border-color: transparent;
237
+ }
238
+ // The leading glyph and dismiss ✕ ride the label colour on a solid fill so they
239
+ // stay legible (the default --color-text-subtle would sink into a dark fill).
240
+ .chip-solid .chip-ic,
241
+ .chip-solid .chip-remove {
242
+ color: currentColor;
243
+ }
244
+ .chip-solid .chip-remove:hover {
245
+ background: color-mix(in srgb, currentColor 22%, transparent);
246
+ color: currentColor;
247
+ }
248
+
168
249
  // Disabled — deliberately NOT opacity, and deliberately after the tones.
169
250
  //
170
251
  // `opacity: .55` fades the whole chip, which scales the LABEL's contrast down with it:
@@ -95,9 +95,9 @@
95
95
  }
96
96
 
97
97
  // ── Size tiers ──
98
- // md is the base above. sm/lg scale the TRIGGER only — the card (and the compact square),
99
- // its mark and its name; the dropdown menu is a fixed 280px panel at every size, the same
100
- // way `compact` is a shape switch rather than a resize.
98
+ // md is the base above. sm/lg scale the TRIGGER (the card / compact square, its mark, its name)
99
+ // AND the dropdown menu (width, row padding, avatar, the "+" chip and the footer), so the whole
100
+ // switcher — trigger and panel — reads at one size.
101
101
  .workspace-switcher-sm .workspace-switcher-trigger {
102
102
  min-height: 40px;
103
103
  padding: 4px 8px;
@@ -139,9 +139,9 @@
139
139
 
140
140
  // ── Menu ──
141
141
  .workspace-switcher .workspace-switcher-menu {
142
- width: 280px;
143
- max-width: 280px;
144
- padding: 6px;
142
+ width: 300px;
143
+ max-width: 300px;
144
+ padding: 5px;
145
145
  background: var(--color-surface);
146
146
  border: 1px solid var(--color-border-light);
147
147
  border-radius: var(--radius-lg);
@@ -193,23 +193,25 @@
193
193
  .workspace-switcher .workspace-switcher-create {
194
194
  display: flex;
195
195
  align-items: center;
196
- gap: 12px;
196
+ gap: 10px;
197
197
  width: 100%;
198
- padding: 9px 12px;
198
+ padding: 6px 10px;
199
199
  border: none;
200
200
  border-radius: var(--radius-lg);
201
201
  corner-shape: squircle;
202
202
  background: transparent;
203
203
  color: var(--color-text);
204
- font-size: 15px;
204
+ font-size: 13px;
205
+ font-weight: 500;
205
206
  text-align: left;
206
207
  text-decoration: none;
207
208
  cursor: pointer;
208
209
  }
209
- // Menu avatars read larger than the trigger's — a comfortable identity row per the design.
210
+ // Compact menu avatars — small identity chips, close to the trigger mark.
210
211
  .workspace-switcher .workspace-switcher-option .workspace-switcher-mark {
211
- width: 42px;
212
- height: 42px;
212
+ width: 28px;
213
+ height: 28px;
214
+ font-size: 11px;
213
215
  }
214
216
  .workspace-switcher .workspace-switcher-option:hover,
215
217
  .workspace-switcher .workspace-switcher-create:hover {
@@ -244,9 +246,10 @@
244
246
  font-size: 11px;
245
247
  font-weight: 600;
246
248
  }
249
+ // Active state reads from the tint + accent name (per the design); the check tick is dropped
250
+ // so the row stays a clean avatar + name, not a text line with a trailing glyph.
247
251
  .workspace-switcher-tick {
248
- flex: none;
249
- color: var(--color-primary);
252
+ display: none;
250
253
  }
251
254
  .workspace-switcher-empty {
252
255
  padding: 14px 10px;
@@ -255,17 +258,29 @@
255
258
  color: var(--color-text-muted);
256
259
  }
257
260
  .workspace-switcher .workspace-switcher-create {
261
+ position: relative;
258
262
  justify-content: center;
263
+ margin-top: 10px;
259
264
  color: var(--color-primary);
260
265
  font-weight: 600;
261
266
  }
267
+ // Dashed divider above the create row — the list and the "Add …" action read as two zones,
268
+ // matching the design. A ::before (not a border) keeps the row's hover pill radius intact.
269
+ .workspace-switcher .workspace-switcher-create::before {
270
+ content: "";
271
+ position: absolute;
272
+ top: -5px;
273
+ left: 4px;
274
+ right: 4px;
275
+ border-top: 1px dashed var(--color-border);
276
+ }
262
277
  // The create / add rows lead with a round soft-tint "+" chip and centre their accent label,
263
278
  // matching the design's "Add Business" action.
264
279
  .workspace-switcher .workspace-switcher-create .wui-icon,
265
280
  .workspace-switcher .workspace-switcher-footer-action .wui-icon {
266
281
  box-sizing: border-box;
267
- width: 28px;
268
- height: 28px;
282
+ width: 26px;
283
+ height: 26px;
269
284
  padding: 6px;
270
285
  border-radius: 50%;
271
286
  background: var(--color-primary-soft);
@@ -276,8 +291,8 @@
276
291
  // A single workspace still gets a menu: current row + this footer. The divider is what makes
277
292
  // that read as a complete panel rather than a truncated list.
278
293
  .workspace-switcher-footer {
279
- margin-top: 6px;
280
- padding-top: 8px;
294
+ margin-top: 8px;
295
+ padding-top: 10px;
281
296
  border-top: 1px dashed var(--color-border);
282
297
  }
283
298
  .workspace-switcher .workspace-switcher-footer-action {
@@ -286,13 +301,13 @@
286
301
  justify-content: center;
287
302
  gap: 10px;
288
303
  width: 100%;
289
- padding: 8px 12px;
304
+ padding: 6px 10px;
290
305
  border: none;
291
306
  border-radius: var(--radius-lg);
292
307
  corner-shape: squircle;
293
308
  background: transparent;
294
309
  color: var(--color-primary);
295
- font-size: 15px;
310
+ font-size: 13px;
296
311
  font-weight: 600;
297
312
  text-align: left;
298
313
  text-decoration: none;
@@ -303,6 +318,65 @@
303
318
  color: var(--color-primary);
304
319
  }
305
320
 
321
+ // ── Menu size tiers ── the panel scales with the trigger's `size`. Placed AFTER the base Menu
322
+ // rules above so they win on source order (equal specificity). sm tightens the width, rows,
323
+ // avatar, "+" chip and footer; lg opens them up. The dropdown is a descendant of the size-classed
324
+ // root, so `.workspace-switcher-sm/-lg <menu-part>` reaches every row.
325
+ .workspace-switcher-sm .workspace-switcher-menu {
326
+ width: 240px;
327
+ max-width: 240px;
328
+ padding: 5px;
329
+ }
330
+ .workspace-switcher-sm .workspace-switcher-option,
331
+ .workspace-switcher-sm .workspace-switcher-create {
332
+ gap: 9px;
333
+ padding: 5px 9px;
334
+ font-size: 12px;
335
+ }
336
+ .workspace-switcher-sm .workspace-switcher-option .workspace-switcher-mark {
337
+ width: 24px;
338
+ height: 24px;
339
+ font-size: 10px;
340
+ }
341
+ .workspace-switcher-sm .workspace-switcher-create .wui-icon,
342
+ .workspace-switcher-sm .workspace-switcher-footer-action .wui-icon {
343
+ width: 22px;
344
+ height: 22px;
345
+ padding: 5px;
346
+ }
347
+ .workspace-switcher-sm .workspace-switcher-footer-action {
348
+ gap: 9px;
349
+ padding: 5px 10px;
350
+ font-size: 12px;
351
+ }
352
+ .workspace-switcher-lg .workspace-switcher-menu {
353
+ width: 300px;
354
+ max-width: 300px;
355
+ padding: 4px;
356
+ }
357
+ .workspace-switcher-lg .workspace-switcher-option,
358
+ .workspace-switcher-lg .workspace-switcher-create {
359
+ gap: 12px;
360
+ padding: 8px 12px;
361
+ font-size: 14px;
362
+ }
363
+ .workspace-switcher-lg .workspace-switcher-option .workspace-switcher-mark {
364
+ width: 34px;
365
+ height: 34px;
366
+ font-size: 13px;
367
+ }
368
+ .workspace-switcher-lg .workspace-switcher-create .wui-icon,
369
+ .workspace-switcher-lg .workspace-switcher-footer-action .wui-icon {
370
+ width: 30px;
371
+ height: 30px;
372
+ padding: 7px;
373
+ }
374
+ .workspace-switcher-lg .workspace-switcher-footer-action {
375
+ gap: 12px;
376
+ padding: 8px 12px;
377
+ font-size: 14px;
378
+ }
379
+
306
380
  @media (prefers-reduced-motion: reduce) {
307
381
  .workspace-switcher-trigger {
308
382
  transition: none;