@webority/theme 0.68.3 → 0.70.1

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.68.3",
3
+ "version": "0.70.1",
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": {
@@ -28,9 +28,13 @@
28
28
  "./css": "./dist/webority-theme.css",
29
29
  "./css/flags": "./dist/webority-flags.css",
30
30
  "./css/brand-signal": "./dist/webority-brand-signal.css",
31
+ "./css/runtime-brand": "./dist/webority-runtime-brand.css",
31
32
  "./purge-safelist.json": "./dist/purge-safelist.json",
32
33
  "./package.json": "./package.json"
33
34
  },
35
+ "bin": {
36
+ "webority-theme-export": "./dist/webority-theme-export.mjs"
37
+ },
34
38
  "sass": "index.scss",
35
39
  "style": "dist/webority-theme.css",
36
40
  "peerDependencies": {
@@ -39,9 +43,10 @@
39
43
  "devDependencies": {
40
44
  "bootstrap": "5.3.3",
41
45
  "country-flag-icons": "^1.6.20",
42
- "sass-embedded": "^1.97.0"
46
+ "sass-embedded": "^1.97.0",
47
+ "vite": "^8.0.12"
43
48
  },
44
49
  "scripts": {
45
- "build": "sass index.scss dist/webority-theme.css --load-path=node_modules --style=compressed --no-source-map --quiet-deps --silence-deprecation=if-function && sass scss/_flags.scss dist/webority-flags.css --style=compressed --no-source-map --quiet && sass scss/_brand-signal.scss dist/webority-brand-signal.css --style=compressed --no-source-map --quiet && node ../../scripts/gen-purge-safelist.mjs && node ../../scripts/sync-rcl-assets.mjs theme"
50
+ "build": "sass index.scss dist/webority-theme.css --load-path=node_modules --style=compressed --no-source-map --quiet-deps --silence-deprecation=if-function && sass scss/_flags.scss dist/webority-flags.css --style=compressed --no-source-map --quiet && sass scss/_brand-signal.scss dist/webority-brand-signal.css --style=compressed --no-source-map --quiet && sass scss/_runtime-brand.scss dist/webority-runtime-brand.css --style=compressed --no-source-map --quiet && node ../../scripts/gen-purge-safelist.mjs && node ../../scripts/sync-rcl-assets.mjs theme && vite build --config vite.cli.config.js"
46
51
  }
47
52
  }
@@ -6,15 +6,14 @@
6
6
  // ---- Button polish (used by both Razor + React) ---------------------
7
7
  // Flat buttons — no drop shadow. Active-press feedback (1px nudge) is
8
8
  // retained because it's a tactile signal, not a depth cue.
9
- // Button heights are fixed to the design reference — large 55px, medium 45px,
10
- // small 35px — via min-height, with the label flex-centred. (These are button-
11
- // specific and intentionally a touch taller than the shared --wui-control-height
12
- // input ladder; sm/lg pin their own height in their size blocks below.)
9
+ // A button is exactly as tall as a field of the same size (the --wui-control-height ladder, 36/44/52px), so a
10
+ // button beside an input lines up in every form row. Heights are min-height with the label flex-centred; sm/lg
11
+ // set theirs in their size blocks below.
13
12
  .btn:not(.btn-sm):not(.btn-lg):not(.btn-pill):not(.btn-pill-sm):not(.u-action):not(.btn-icon) {
14
13
  display: inline-flex;
15
14
  align-items: center;
16
15
  justify-content: center;
17
- min-height: 45px;
16
+ min-height: var(--wui-control-height);
18
17
  }
19
18
 
20
19
  // Tactile press feedback — every variant AND every size (sm/md/lg). Kept
@@ -266,7 +265,7 @@
266
265
  // sizes step font-size, horizontal padding and radius together (vertical size
267
266
  // comes from --wui-control-height, so a button lines up with an input). Weight
268
267
  // is 600 on every rung — buttons read as a deliberate action, not body text.
269
- // Medium (default): 14px / 600, 24px side padding, 18px squircle radius, 45px tall.
268
+ // Medium (default): 14px / 600, 24px side padding, 18px squircle radius, --wui-control-height tall.
270
269
  .btn {
271
270
  --bs-btn-border-radius: 18px;
272
271
  --bs-btn-font-size: 0.875rem;
@@ -276,7 +275,7 @@
276
275
  // keep a plain border-radius arc — not a cross-browser design contract.
277
276
  corner-shape: squircle;
278
277
  }
279
- // Small: 12px, 18px side padding, 14px squircle radius (proportional to md), 35px tall.
278
+ // Small: 12px, 18px side padding, 14px squircle radius (proportional to md), as tall as a small field.
280
279
  .btn-sm:not(.btn-icon),
281
280
  .btn-group-sm > .btn:not(.btn-icon) {
282
281
  --bs-btn-border-radius: 14px;
@@ -285,9 +284,9 @@
285
284
  display: inline-flex;
286
285
  align-items: center;
287
286
  justify-content: center;
288
- min-height: 35px;
287
+ min-height: var(--wui-control-height-sm);
289
288
  }
290
- // Large: 16px, 32px side padding, 22px squircle radius (proportional to md), 55px tall — a spacious CTA.
289
+ // Large: 16px, 32px side padding, 22px squircle radius (proportional to md), as tall as a large field.
291
290
  .btn-lg:not(.btn-icon),
292
291
  .btn-group-lg > .btn:not(.btn-icon) {
293
292
  --bs-btn-border-radius: 22px;
@@ -296,7 +295,7 @@
296
295
  display: inline-flex;
297
296
  align-items: center;
298
297
  justify-content: center;
299
- min-height: 55px;
298
+ min-height: var(--wui-control-height-lg);
300
299
  }
301
300
 
302
301
  // ---- Hover animation options (opt-in modifiers) ---------------------
@@ -544,9 +543,9 @@
544
543
  }
545
544
 
546
545
  // ---- Ghost button: the tertiary action --------------------------------
547
- // Brand-coloured, underlined text with no box in any state. It has no visible container,
548
- // so nothing may draw one: no hover fill, no sheen sweep, no lift or press nudge, and no
549
- // grey box when disabled or loading. Hover darkens the colour and thickens the underline.
546
+ // Brand-coloured text with no box at rest and no underline. Hover lays a light grey fill behind
547
+ // it, pressing deepens that fill a step, and keyboard focus draws the shared ring. No sheen sweep,
548
+ // no lift or press nudge, and no grey box when disabled or loading.
550
549
  // AppButton's `ghost` variant emits this class on both surfaces.
551
550
  //
552
551
  // No .btn-ghost-sm/-lg rungs: it is an ordinary .btn variant and sizes with Bootstrap's
@@ -555,32 +554,24 @@
555
554
  --bs-btn-color: var(--color-primary-ink);
556
555
  --bs-btn-bg: transparent;
557
556
  --bs-btn-border-color: transparent;
558
- --bs-btn-hover-color: color-mix(in srgb, var(--color-primary-ink) 80%, var(--color-text));
559
- --bs-btn-hover-bg: transparent;
557
+ --bs-btn-hover-color: var(--color-primary-ink);
558
+ --bs-btn-hover-bg: var(--color-bg-muted);
560
559
  --bs-btn-hover-border-color: transparent;
561
- --bs-btn-active-color: color-mix(in srgb, var(--color-primary-ink) 70%, var(--color-text));
562
- --bs-btn-active-bg: transparent;
560
+ --bs-btn-active-color: var(--color-primary-ink);
561
+ --bs-btn-active-bg: color-mix(in srgb, var(--color-bg-muted) 92%, var(--color-text));
563
562
  --bs-btn-active-border-color: transparent;
564
563
  --bs-btn-disabled-color: var(--color-primary-ink);
565
564
  --bs-btn-disabled-bg: transparent;
566
565
  --bs-btn-disabled-border-color: transparent;
567
- text-decoration: underline;
568
- text-decoration-thickness: 1px;
569
- text-underline-offset: 3px;
570
566
  }
571
- // Bootstrap's `.btn:hover` drops the underline (from $link-hover-decoration), so restate it
572
- // with more specificity on every interactive state.
567
+ // Bootstrap's link-style buttons can carry an underline; the ghost never does.
568
+ .btn.btn-ghost,
573
569
  .btn.btn-ghost:hover,
574
570
  .btn.btn-ghost:active,
575
571
  .btn.btn-ghost:focus-visible {
576
- text-decoration: underline;
577
- text-decoration-thickness: 1px;
572
+ text-decoration: none;
578
573
  }
579
- .btn.btn-ghost:hover:not(:disabled),
580
- .btn.btn-ghost:active:not(:disabled) {
581
- text-decoration-thickness: 2px;
582
- }
583
- // No box to light up, so keyboard focus gets the outline ring instead.
574
+ // No fill at rest, so keyboard focus gets the outline ring instead.
584
575
  .btn.btn-ghost:focus-visible {
585
576
  outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
586
577
  outline-offset: var(--wui-focus-ring-offset);
@@ -634,6 +625,41 @@
634
625
  margin-left: auto;
635
626
  }
636
627
 
628
+ // Close button (React `onClose` / Razor `dismissible`): the last control in the header, pushed
629
+ // to the end, or just after the header actions when there are some.
630
+ .card-header:has(.card-close) {
631
+ display: flex;
632
+ align-items: center;
633
+ }
634
+ .card-close {
635
+ display: inline-grid;
636
+ flex-shrink: 0;
637
+ place-items: center;
638
+ width: 32px;
639
+ height: 32px;
640
+ margin: -6px -6px -6px auto;
641
+ padding: 0;
642
+ border: 0;
643
+ border-radius: var(--radius);
644
+ background: transparent;
645
+ color: var(--color-text-muted);
646
+ cursor: pointer;
647
+ transition:
648
+ background-color 0.12s ease,
649
+ color 0.12s ease;
650
+ }
651
+ .card-header-actions + .card-close {
652
+ margin-left: 0.25rem;
653
+ }
654
+ .card-close:hover {
655
+ background: var(--color-bg-muted);
656
+ color: var(--color-text);
657
+ }
658
+ .card-close:focus-visible {
659
+ outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
660
+ outline-offset: var(--wui-focus-ring-offset);
661
+ }
662
+
637
663
  // ---- Inputs ---------------------------------------------------------
638
664
  // Field chrome (height, hover, focus, invalid) lives in wui/_controls.scss on
639
665
  // `.form-control` / `.form-select` with `.input` as an alias — one skin, not two
@@ -0,0 +1,83 @@
1
+ // Runtime brand: an opt-in layer that lets a product's exported token file (webority-theme-export,
2
+ // format webority-theme-tokens/1) restyle the precompiled theme without recompiling it.
3
+ //
4
+ // Loaded AFTER webority-theme.css and BEFORE the token file's values. It holds no colour of its own.
5
+ // Bootstrap compiles $primary into a few component rules as literals; this layer re-points each of
6
+ // them at --bs-primary / --bs-primary-rgb, which the token file sets on :root. Every other brand
7
+ // value (--wui-*, links, fonts, the Bootstrap subtle and emphasis colours, dark mode) is already a
8
+ // custom property on :root and arrives with the token file unchanged.
9
+ //
10
+ // Each tint below is the colour-mix equivalent of the Bootstrap function that made the literal
11
+ // (tint-color = mix with white, shade-color = mix with black), so a product's own primary renders
12
+ // the same through this layer as through its compiled build. The compiled values are the same in
13
+ // light and dark, so there is no dark block.
14
+
15
+ .btn-primary {
16
+ --bs-btn-bg: var(--bs-primary);
17
+ --bs-btn-border-color: var(--bs-primary);
18
+ --bs-btn-disabled-bg: var(--bs-primary);
19
+ --bs-btn-disabled-border-color: var(--bs-primary);
20
+ // Bootstrap mixes 15% white into this triplet; a triplet cannot be mixed at runtime.
21
+ --bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
22
+ }
23
+
24
+ .btn-outline-primary {
25
+ --bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
26
+ }
27
+
28
+ .nav-pills {
29
+ --bs-nav-pills-link-active-bg: var(--bs-primary);
30
+ }
31
+
32
+ .pagination {
33
+ --bs-pagination-active-bg: var(--bs-primary);
34
+ --bs-pagination-active-border-color: var(--bs-primary);
35
+ }
36
+
37
+ .list-group {
38
+ --bs-list-group-active-bg: var(--bs-primary);
39
+ --bs-list-group-active-border-color: var(--bs-primary);
40
+ }
41
+
42
+ .progress,
43
+ .progress-stacked {
44
+ --bs-progress-bar-bg: var(--bs-primary);
45
+ }
46
+
47
+ // Mouse focus only: the theme paints keyboard focus and checked boxes from --color-primary already,
48
+ // and a plain :focus rule here would win over both by source order.
49
+ .form-check-input:focus:not(:focus-visible):not(:checked):not([type="checkbox"]:indeterminate) {
50
+ border-color: color-mix(in srgb, var(--bs-primary) 50%, #fff);
51
+ }
52
+
53
+ .form-range::-webkit-slider-thumb {
54
+ background-color: var(--bs-primary);
55
+ }
56
+
57
+ .form-range::-webkit-slider-thumb:active {
58
+ background-color: color-mix(in srgb, var(--bs-primary) 30%, #fff);
59
+ }
60
+
61
+ .form-range::-moz-range-thumb {
62
+ background-color: var(--bs-primary);
63
+ }
64
+
65
+ .form-range::-moz-range-thumb:active {
66
+ background-color: color-mix(in srgb, var(--bs-primary) 30%, #fff);
67
+ }
68
+
69
+ .link-primary:hover,
70
+ .link-primary:focus {
71
+ color: color-mix(in srgb, color-mix(in srgb, var(--bs-primary) 80%, #000) calc(var(--bs-link-opacity, 1) * 100%), transparent) !important;
72
+ text-decoration-color: color-mix(in srgb, color-mix(in srgb, var(--bs-primary) 80%, #000) calc(var(--bs-link-underline-opacity, 1) * 100%), transparent) !important;
73
+ }
74
+
75
+ // Bootstrap's table-variant mixin: the cell colour is black, and each state mixes it into the tint.
76
+ .table-primary {
77
+ --wui-table-primary-bg: color-mix(in srgb, var(--bs-primary) 20%, #fff);
78
+ --bs-table-bg: var(--wui-table-primary-bg);
79
+ --bs-table-border-color: color-mix(in srgb, #000 20%, var(--wui-table-primary-bg));
80
+ --bs-table-striped-bg: color-mix(in srgb, #000 5%, var(--wui-table-primary-bg));
81
+ --bs-table-active-bg: color-mix(in srgb, #000 10%, var(--wui-table-primary-bg));
82
+ --bs-table-hover-bg: color-mix(in srgb, #000 7.5%, var(--wui-table-primary-bg));
83
+ }
package/scss/_tokens.scss CHANGED
@@ -41,12 +41,11 @@
41
41
  // combobox/phone/date/number wrappers) — a touch lighter than --wui-bg-muted so fields read
42
42
  // as their own surface. Change here to restyle every field's background at once.
43
43
  --wui-field-bg: #F9FBFC;
44
- // Resting and hover border of every input-like control. They clear WCAG 1.4.11's 3:1
45
- // against the field fill and the surface, so a field's edge is visible without focus.
46
- // Both sit just above the floor (3.11:1 and 3.63:1 on the fill) so the edge stays light;
47
- // check-contrast fails if either drops under it.
48
- --wui-field-border: #888FA4;
49
- --wui-field-border-hover: #7C839A;
44
+ // Resting and hover border of every input-like control: a soft edge a step darker than
45
+ // --wui-border-light, and the --wui-text-5 hover. They sit under WCAG 1.4.11's 3:1 by
46
+ // design choice; check-contrast reports them as FAIL without failing the gate.
47
+ --wui-field-border: #DFE2EA;
48
+ --wui-field-border-hover: #C4CADA;
50
49
  --wui-border-light: #EBEDF3;
51
50
  --wui-border-soft: #F4F6F8;
52
51
  // The unfilled part of a progress bar / meter. Darker than every surface it can sit on
@@ -266,9 +265,9 @@
266
265
  --wui-dark-bg-surface: #181b23;
267
266
  --wui-dark-border: #2a2f3a;
268
267
  --wui-dark-border-strong:#3a414f;
269
- // Field borders on the dark fills: 3:1 or better against the dark field fill and surface.
270
- --wui-dark-field-border: #6E7586;
271
- --wui-dark-field-border-hover: #8a90a0;
268
+ // Field borders on the dark fills: the dark border and strong border values.
269
+ --wui-dark-field-border: #2a2f3a;
270
+ --wui-dark-field-border-hover: #3a414f;
272
271
  // Lighter than the dark page, surface and muted fill, so the empty track shows on all three.
273
272
  --wui-dark-progress-track: #323845;
274
273
  --wui-dark-text: #e6e8ee;
@@ -18,7 +18,7 @@
18
18
  --color-border: var(--wui-border-light);
19
19
  --color-border-light: var(--wui-border-soft);
20
20
  --color-border-strong: var(--wui-text-5);
21
- // The resting and hover border of an input-like control (3:1 against the field and surface).
21
+ // The resting and hover border of an input-like control.
22
22
  // Decorative card and table borders stay on --color-border.
23
23
  --color-field-border: var(--wui-field-border);
24
24
  --color-field-border-hover: var(--wui-field-border-hover);
@@ -40,9 +40,8 @@
40
40
  // A dark-ink glyph such as Bootstrap's .btn-close needs no filter on a light surface.
41
41
  --color-close-filter: none;
42
42
  // The border of a focused / open / active control (input, select, OTP slot, dropzone).
43
- // It is the only focus cue on a field, so it must clear 3:1 against the field and surface
44
- // (WCAG 1.4.11): the full primary, not a tint.
45
- --color-focus-border: var(--wui-primary);
43
+ // The soft primary tint, matching the soft resting border.
44
+ --color-focus-border: var(--wui-primary-30);
46
45
  // The keyboard focus ring every control draws on :focus-visible: an outline of this colour,
47
46
  // width and offset. The primary ink clears 4.5:1 on every light surface and the dark primary
48
47
  // on every dark one, so the ring keeps 3:1 whatever the brand. A coloured surface (a scrim, a
@@ -253,15 +253,22 @@
253
253
  background: var(--color-surface);
254
254
  border-color: var(--color-focus-border);
255
255
  }
256
+ // Closed again (a value picked, Escape), focus stays on the trigger: it returns to the resting
257
+ // fill and border, as the date picker does. Only the open list shows the focus border; keyboard
258
+ // focus still draws the ring below.
259
+ .search-select-control:focus:not(.open):not([aria-invalid="true"]):not(.error):not(.is-invalid) {
260
+ background: var(--color-field-bg);
261
+ border-color: var(--color-field-border);
262
+ }
256
263
  // Keyboard focus draws a ring, not just the 1px border change: a control that only recolours its
257
264
  // border fails WCAG 2.4.7 / 2.4.11. A single-select button and a multi-select div hold focus
258
265
  // themselves; the typing variants (autocomplete, tag input, allow-custom multi-select) keep it on
259
- // an inner input, so the ring follows `:has(:focus-visible)`. --color-focus-border is the
260
- // token documented to clear 3:1 in both themes.
266
+ // an inner input, so the ring follows `:has(:focus-visible)`. It uses the shared keyboard ring
267
+ // colour, not the soft field focus border, so it clears 3:1 in both themes.
261
268
  .search-select-control:focus-visible,
262
269
  .search-select-control:has(> .search-select-input:focus-visible),
263
270
  .search-select-control:has(.ms-input:focus-visible) {
264
- outline: 2px solid var(--color-focus-border);
271
+ outline: 2px solid var(--color-focus-ring);
265
272
  outline-offset: 2px;
266
273
  }
267
274
  .search-select-caret {
@@ -508,10 +515,14 @@
508
515
  background 0.12s ease,
509
516
  color 0.12s ease;
510
517
  }
518
+ // The row under the pointer or the keyboard: a light tint and a light 1px edge, softer than the
519
+ // selected row's fill so the two stay apart.
511
520
  .search-select-option:hover,
512
521
  .search-select-option.is-active {
513
- background: var(--color-primary-soft);
522
+ background: color-mix(in srgb, var(--color-primary-soft) 55%, transparent);
514
523
  color: var(--color-primary-ink);
524
+ outline: 1px solid var(--color-focus-border);
525
+ outline-offset: -1px;
515
526
  }
516
527
  .search-select-option.selected {
517
528
  background: var(--color-primary-soft);
@@ -470,10 +470,7 @@ textarea.form-control.autosize {
470
470
  .input[readonly],
471
471
  .select[readonly],
472
472
  .textarea[readonly] {
473
- // The light decorative border and the muted fill, never the field border, so a read-only
474
- // value never looks typeable.
475
- background: var(--color-bg-muted);
476
- border-color: var(--color-border);
473
+ background: var(--color-bg-subtle);
477
474
  color: var(--color-text-muted);
478
475
  cursor: default;
479
476
  }
@@ -689,24 +686,57 @@ $wui-field-error-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/
689
686
  }
690
687
 
691
688
  // Leading-icon input — icon turns brand on focus.
689
+ // The leading icon sits centred in a fixed slot, a grey divider closes the slot, and the text
690
+ // starts a gap after it. Slot, glyph and gap scale with the field size, so every icon in a
691
+ // field of one size lands in the same place.
692
692
  .input-icon {
693
+ --wui-input-icon-slot: 40px;
694
+ --wui-input-icon-size: 16px;
695
+ --wui-input-icon-gap: 12px;
693
696
  position: relative;
694
697
  display: flex;
695
698
  align-items: center;
696
699
  width: 100%;
697
700
  }
701
+ .input-icon:has(> .input-sm) {
702
+ --wui-input-icon-slot: 34px;
703
+ --wui-input-icon-size: 14px;
704
+ --wui-input-icon-gap: 10px;
705
+ }
706
+ .input-icon:has(> .input-lg) {
707
+ --wui-input-icon-slot: 48px;
708
+ --wui-input-icon-size: 18px;
709
+ --wui-input-icon-gap: 14px;
710
+ }
698
711
  .input-icon > .ic {
699
712
  position: absolute;
700
- left: 13px;
701
- top: 50%;
702
- transform: translateY(-50%);
713
+ left: 0;
714
+ top: 0;
715
+ bottom: 0;
716
+ width: var(--wui-input-icon-slot);
703
717
  display: grid;
704
718
  place-items: center;
705
719
  color: var(--color-text-subtle);
706
720
  pointer-events: none;
707
721
  }
722
+ .input-icon > .ic > :is(svg, img) {
723
+ width: var(--wui-input-icon-size);
724
+ height: var(--wui-input-icon-size);
725
+ }
726
+ // The divider runs the full height between the field's top and bottom border, in the field
727
+ // border colour.
728
+ .input-icon::after {
729
+ content: "";
730
+ position: absolute;
731
+ left: var(--wui-input-icon-slot);
732
+ top: 1px;
733
+ bottom: 1px;
734
+ width: 1px;
735
+ background: var(--color-field-border);
736
+ pointer-events: none;
737
+ }
708
738
  .input-icon .input {
709
- padding-left: 40px;
739
+ padding-left: calc(var(--wui-input-icon-slot) + var(--wui-input-icon-gap));
710
740
  }
711
741
  .input-icon:focus-within > .ic {
712
742
  color: var(--color-primary-ink);
@@ -943,11 +973,10 @@ $wui-field-error-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/
943
973
  top: 0;
944
974
  transform: translateY(-50%) scale(0.86);
945
975
  }
946
- .field-float:has(.search-select-control:focus) .float-label,
947
976
  .field-float:has(.search-select-control.open) .float-label {
948
977
  color: var(--color-primary-ink);
949
- // The control turns white while focused/open, so the label mask follows (a filled-but-closed
950
- // select keeps the default muted mask, matching its muted resting fill).
978
+ // The control turns white while open, so the label mask follows (a closed select keeps the
979
+ // default muted mask, matching its muted resting fill, even while it holds focus).
951
980
  background: var(--color-surface);
952
981
  }
953
982
 
@@ -1014,6 +1043,12 @@ $wui-field-error-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/
1014
1043
  gap: 4px;
1015
1044
  min-width: 0;
1016
1045
  }
1046
+ // Inside the flex column these two size to their content, so they stretch to the field instead.
1047
+ .field > wui-rich-text,
1048
+ .field > wui-mention-input {
1049
+ align-self: stretch;
1050
+ width: 100%;
1051
+ }
1017
1052
  .field > .label,
1018
1053
  .field label {
1019
1054
  margin-bottom: 0;
@@ -135,6 +135,10 @@
135
135
  width: 100%;
136
136
  // `.tbl-empty` / `.tbl-empty-plain` inside own the centring and padding.
137
137
  }
138
+ // An empty list shows its empty state with no column headers above it.
139
+ .table-wrap:has(> .tbl-empty-block:not([hidden])) > .table-ledger > thead {
140
+ display: none;
141
+ }
138
142
 
139
143
  // ── Table (base) ──
140
144
  // The shared data-table look, on the plain `.table` class Bootstrap already
@@ -292,9 +296,9 @@
292
296
  // block above) — if it ever needs viewport-relative sizing again, do it without turning
293
297
  // the scroll container into a query container.
294
298
  }
295
- // Flush: no inset above the column header row, so the header sits at the top of the wrap.
299
+ // Flush: the table sits on the content edge on every side, with no inset.
296
300
  .table-wrap.wui-table-flush:has(> .table-ledger) {
297
- padding-top: 0;
301
+ padding: 0;
298
302
  }
299
303
  .table-ledger {
300
304
  border-spacing: 0;
@@ -1002,6 +1006,14 @@
1002
1006
  .table-wrap:has(> .table-ledger.wui-table-stack) .table-ledger thead th {
1003
1007
  position: static;
1004
1008
  }
1009
+ .table-wrap:has(> .table-ledger.wui-table-stack) {
1010
+ overflow-x: clip;
1011
+ }
1012
+ .table-ledger.wui-table-stack > tbody > tr > td {
1013
+ min-width: 0;
1014
+ max-width: 100%;
1015
+ overflow-wrap: anywhere;
1016
+ }
1005
1017
  .table-ledger.wui-table-stack,
1006
1018
  .table-ledger.wui-table-stack > tbody,
1007
1019
  .table-ledger.wui-table-stack > tfoot,
@@ -1317,7 +1317,7 @@ a.info-row:hover .info-value {
1317
1317
  background: currentColor;
1318
1318
  transform: scaleX(0);
1319
1319
  transform-origin: left center;
1320
- transition: transform 0.28s var(--wui-ease-out-quart);
1320
+ transition: transform 0.5s var(--wui-ease-out-quart);
1321
1321
  }
1322
1322
  .wui-link--underline-center .wui-link__label::after {
1323
1323
  transform-origin: center;
@@ -1417,7 +1417,7 @@ a.info-row:hover .info-value {
1417
1417
  }
1418
1418
  .wui-link--static:hover .wui-link__label::after,
1419
1419
  .wui-link--static:focus-visible .wui-link__label::after {
1420
- animation: wui-link-underline-slide 0.32s var(--wui-ease-out-quart);
1420
+ animation: wui-link-underline-slide 0.5s var(--wui-ease-out-quart);
1421
1421
  }
1422
1422
  @keyframes wui-link-underline-slide {
1423
1423
  from {
@@ -1,7 +1,7 @@
1
1
  // The floating filter panel: <wui-filter-popover> with its Filters button, drawn by AppFilterBar
2
2
  // `floating` and <app-filter-bar floating>. The element places the panel (anchorOverlay, fixed), so
3
- // this partial only draws it: a 600 px panel of facets in two columns, and a foot with the Keep open
4
- // switch and Clear all.
3
+ // this partial only draws it: a 600 px panel with a head (title and the Keep open switch), facets in
4
+ // two columns, and a foot (the note and Clear all).
5
5
 
6
6
  .wui-filter-popover {
7
7
  display: inline-block;
@@ -29,6 +29,29 @@
29
29
  display: none;
30
30
  }
31
31
 
32
+ .wui-filter-popover-head {
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ margin-bottom: 0.75rem;
37
+ }
38
+
39
+ .wui-filter-popover-title {
40
+ font-weight: 600;
41
+ }
42
+
43
+ .wui-filter-popover-field {
44
+ display: flex;
45
+ flex-direction: column;
46
+ gap: 4px;
47
+ min-width: 0;
48
+ }
49
+
50
+ .wui-filter-popover-note {
51
+ font-size: 13px;
52
+ color: var(--color-text-muted);
53
+ }
54
+
32
55
  .wui-filter-popover-grid {
33
56
  display: grid;
34
57
  grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -53,10 +76,6 @@
53
76
  border-top: 1px solid var(--color-border);
54
77
  }
55
78
 
56
- .wui-filter-popover-count {
57
- font-weight: 600;
58
- }
59
-
60
79
  .wui-filter-popover-more {
61
80
  white-space: nowrap;
62
81
  cursor: default;
@@ -1027,6 +1027,10 @@ button.step.done:focus-visible .step-circle {
1027
1027
  border-top: 1px solid var(--color-border);
1028
1028
  background: var(--color-surface);
1029
1029
  }
1030
+ // Below md the phone tab bar is fixed at the bottom, so sticky form actions sit on top of it.
1031
+ @media (max-width: 767.98px) {
1032
+ .app-shell:has(> .wui-tabbar) .form-actions-sticky { bottom: calc(var(--wui-tabbar-h) + env(safe-area-inset-bottom)); z-index: calc(var(--wui-z-fixed) + 1); }
1033
+ }
1030
1034
  @media (prefers-reduced-motion: reduce) {
1031
1035
  .form-actions-sticky {
1032
1036
  transition: none;
@@ -11,7 +11,7 @@
11
11
  // ---- Default ring -----------------------------------------------------------
12
12
  // Anything focusable that no partial styles gets the shared ring instead of the browser's.
13
13
  // Zero specificity, so every component rule (including a panel's deliberate `outline: none`)
14
- // still wins. Text fields are left out: they get the field edge below.
14
+ // still wins. Text fields are left out: their focus cue is their own soft border.
15
15
  :where(
16
16
  a[href],
17
17
  area[href],
@@ -90,53 +90,23 @@ html:has(.form-actions-sticky) {
90
90
  scroll-padding-bottom: 96px;
91
91
  }
92
92
 
93
- // ---- Text fields --------------------------------------------------------------
94
- // A field's focus cue is its edge: the 1px border plus an outline of the rest of
95
- // --wui-focus-ring-width right outside it, so the edge reads 2px and nothing shifts. Each
96
- // selector repeats its partial's focus rule at the same specificity, so an invalid field's own
97
- // border colour still wins and only the outer edge is added. The open state of a picker counts:
98
- // focus has moved into its pop-up, but the field still reads as the one being edited.
99
- .form-control:focus,
100
- .form-select:focus,
101
- .input:focus,
102
- .select:focus,
103
- .textarea:focus,
104
- .search-select-control:focus-within,
105
- .search-select-control.open,
106
- .search-select-search:focus-within,
107
- .datepicker.open .datepicker-trigger,
108
- .daterange.open .daterange-trigger,
109
- .timepicker.open .timepicker-trigger,
110
- .input-affixed:focus-within,
111
- .input-with-unit:focus-within,
112
- .num-wrap:focus-within,
113
- .app-phone-number-input .phone-input-wrap:focus-within,
114
- .wui-input-group:focus-within,
115
- .otp-slot.active,
116
- .dropdown-search-input:focus,
117
- .file-input:not(.has-files):focus-within,
118
- .wui-rich-text-shell:focus-within,
119
- .wui-rich-text-link-input:focus,
93
+ // ---- Workspace switcher trigger ------------------------------------------------
94
+ // A button styled like a field: its soft focus border alone is under 3:1, so keyboard focus
95
+ // adds the outline right outside it. Form fields keep the soft border without an outline.
120
96
  .workspace-switcher-trigger:focus-visible,
121
97
  .workspace-switcher-trigger[aria-expanded="true"] {
122
98
  outline: calc(var(--wui-focus-ring-width) - 1px) solid var(--color-focus-ring);
123
99
  outline-offset: 0;
124
100
  }
125
- // A control inside a wrapper that draws the border leaves the edge to the wrapper, and a bare
126
- // input has no edge.
127
- .input-bare:focus,
128
- :is(.input-affixed, .input-with-unit, .num-wrap, .phone-input-wrap, .wui-input-group) :is(.form-control, .input, .textarea, .num-input):focus {
129
- outline: none;
130
- }
131
101
 
132
102
  // ---- Active option in a pop-up list --------------------------------------------
133
- // Search results, the country list, select and autocomplete options, mentions and the command
103
+ // Search results, the country list, mentions and the command
134
104
  // palette keep focus in their input and mark the keyboard's row (aria-activedescendant) with
135
105
  // the hover tint alone, about 1.2:1. The inset ring makes that row plain; inset, because the
136
- // list scrolls and would clip an outer ring.
106
+ // list scrolls and would clip an outer ring. Select and autocomplete options draw their own
107
+ // light edge in _choice.scss.
137
108
  .wui-topsearch-hit.is-active,
138
109
  .country-dropdown-item.is-active,
139
- .search-select-option.is-active,
140
110
  .wui-mention-option.is-active,
141
111
  .wui-cmd-item[aria-selected="true"] {
142
112
  outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);