@webority/theme 0.68.3 → 0.69.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.68.3",
3
+ "version": "0.69.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": {
@@ -544,9 +544,9 @@
544
544
  }
545
545
 
546
546
  // ---- 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.
547
+ // Brand-coloured text with no box at rest and no underline. Hover lays a light grey fill behind
548
+ // it, pressing deepens that fill a step, and keyboard focus draws the shared ring. No sheen sweep,
549
+ // no lift or press nudge, and no grey box when disabled or loading.
550
550
  // AppButton's `ghost` variant emits this class on both surfaces.
551
551
  //
552
552
  // No .btn-ghost-sm/-lg rungs: it is an ordinary .btn variant and sizes with Bootstrap's
@@ -555,32 +555,24 @@
555
555
  --bs-btn-color: var(--color-primary-ink);
556
556
  --bs-btn-bg: transparent;
557
557
  --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;
558
+ --bs-btn-hover-color: var(--color-primary-ink);
559
+ --bs-btn-hover-bg: var(--color-bg-muted);
560
560
  --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;
561
+ --bs-btn-active-color: var(--color-primary-ink);
562
+ --bs-btn-active-bg: color-mix(in srgb, var(--color-bg-muted) 92%, var(--color-text));
563
563
  --bs-btn-active-border-color: transparent;
564
564
  --bs-btn-disabled-color: var(--color-primary-ink);
565
565
  --bs-btn-disabled-bg: transparent;
566
566
  --bs-btn-disabled-border-color: transparent;
567
- text-decoration: underline;
568
- text-decoration-thickness: 1px;
569
- text-underline-offset: 3px;
570
567
  }
571
- // Bootstrap's `.btn:hover` drops the underline (from $link-hover-decoration), so restate it
572
- // with more specificity on every interactive state.
568
+ // Bootstrap's link-style buttons can carry an underline; the ghost never does.
569
+ .btn.btn-ghost,
573
570
  .btn.btn-ghost:hover,
574
571
  .btn.btn-ghost:active,
575
572
  .btn.btn-ghost:focus-visible {
576
- text-decoration: underline;
577
- text-decoration-thickness: 1px;
573
+ text-decoration: none;
578
574
  }
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.
575
+ // No fill at rest, so keyboard focus gets the outline ring instead.
584
576
  .btn.btn-ghost:focus-visible {
585
577
  outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
586
578
  outline-offset: var(--wui-focus-ring-offset);
@@ -634,6 +626,41 @@
634
626
  margin-left: auto;
635
627
  }
636
628
 
629
+ // Close button (React `onClose` / Razor `dismissible`): the last control in the header, pushed
630
+ // to the end, or just after the header actions when there are some.
631
+ .card-header:has(.card-close) {
632
+ display: flex;
633
+ align-items: center;
634
+ }
635
+ .card-close {
636
+ display: inline-grid;
637
+ flex-shrink: 0;
638
+ place-items: center;
639
+ width: 32px;
640
+ height: 32px;
641
+ margin: -6px -6px -6px auto;
642
+ padding: 0;
643
+ border: 0;
644
+ border-radius: var(--radius);
645
+ background: transparent;
646
+ color: var(--color-text-muted);
647
+ cursor: pointer;
648
+ transition:
649
+ background-color 0.12s ease,
650
+ color 0.12s ease;
651
+ }
652
+ .card-header-actions + .card-close {
653
+ margin-left: 0.25rem;
654
+ }
655
+ .card-close:hover {
656
+ background: var(--color-bg-muted);
657
+ color: var(--color-text);
658
+ }
659
+ .card-close:focus-visible {
660
+ outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
661
+ outline-offset: var(--wui-focus-ring-offset);
662
+ }
663
+
637
664
  // ---- Inputs ---------------------------------------------------------
638
665
  // Field chrome (height, hover, focus, invalid) lives in wui/_controls.scss on
639
666
  // `.form-control` / `.form-select` with `.input` as an alias — one skin, not two
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);
@@ -723,6 +723,11 @@
723
723
  padding: 0;
724
724
  font-size: 12px;
725
725
  }
726
+ .file-input.file-input-tall .file-input-text {
727
+ white-space: normal;
728
+ text-align: center;
729
+ overflow-wrap: anywhere;
730
+ }
726
731
 
727
732
  // Large preview shown once an image exists; hover reveals a "Change image"
728
733
  // overlay.
@@ -62,7 +62,7 @@
62
62
  padding: 0;
63
63
  }
64
64
  .modal.wui-remote-drawer .modal-dialog.wui-remote-drawer-dialog {
65
- margin: 0;
65
+ margin: 0 0 0 auto;
66
66
  max-width: none;
67
67
  width: min(var(--wui-modal-max-width, 480px), 100%);
68
68
  height: 100%;
@@ -87,3 +87,39 @@
87
87
  transition: none;
88
88
  }
89
89
  }
90
+
91
+ // On a phone the drawer rises from the bottom edge as a sheet instead of sliding in from the end.
92
+ @media (max-width: 767.98px) {
93
+ .modal.wui-remote-drawer .modal-dialog.wui-remote-drawer-dialog {
94
+ position: absolute;
95
+ inset: auto 0 0 0;
96
+ width: 100%;
97
+ height: auto;
98
+ max-height: 90dvh;
99
+ margin: 0;
100
+ transform: translateY(100%);
101
+ }
102
+ .modal.wui-remote-drawer.show .modal-dialog.wui-remote-drawer-dialog {
103
+ transform: none;
104
+ }
105
+ .modal.wui-remote-drawer .wui-remote-drawer-dialog .wui-modal {
106
+ height: auto;
107
+ max-height: 90dvh;
108
+ border-radius: var(--wui-radius-lg) var(--wui-radius-lg) 0 0;
109
+ }
110
+ .modal.wui-remote-drawer .wui-remote-drawer-dialog .wui-modal::before {
111
+ content: "";
112
+ display: block;
113
+ width: 36px;
114
+ height: 4px;
115
+ margin: 8px auto 0;
116
+ border-radius: 2px;
117
+ background: var(--color-border);
118
+ }
119
+ }
120
+
121
+ // A side panel dims the page behind it without blurring it, so the list stays readable.
122
+ body:has(> .modal.wui-remote-drawer.show) > .modal-backdrop {
123
+ backdrop-filter: none;
124
+ -webkit-backdrop-filter: none;
125
+ }
@@ -1,6 +1,7 @@
1
- // ── Bar ── (`size="bar"`): one 40px header row. The breadcrumbs lead as an inline prefix, the
2
- // title takes the spare width and ends in an ellipsis, and the row never wraps from md up.
3
- // Below md the row wraps, so the actions drop under the title instead of overlapping it.
1
+ // ── Bar ── (`size="bar"`): one 40px header row. The breadcrumbs lead as an inline prefix. The
2
+ // title keeps its content width and a spacer takes the spare width, so the views follow the title
3
+ // and the tools and actions sit at the far end. Below md the row wraps, so the actions drop under
4
+ // the title instead of overlapping it.
4
5
  // Selectors name `.page-header.page-header-bar` because `.page-header-bar` alone is also the
5
6
  // inner row of every other size.
6
7
  .page-header.page-header-bar {
@@ -10,27 +11,58 @@
10
11
  // The bar sits 4px above the first content block.
11
12
  margin-bottom: 4px;
12
13
  }
14
+ [data-density="compact"] .app-content:has(.page-header.page-header-bar) {
15
+ --app-content-padding-top: 8px;
16
+ }
13
17
  .page-header.page-header-bar .page-title {
14
- flex: 1 1 0;
15
- min-width: 0;
18
+ flex: 0 1 auto;
19
+ min-width: min(12ch, max-content);
16
20
  font-size: 20px;
17
21
  overflow: hidden;
18
22
  text-overflow: ellipsis;
19
23
  white-space: nowrap;
20
24
  }
25
+ .page-header.page-header-bar:has(> .page-header-bar-views) .page-title {
26
+ max-width: 280px;
27
+ }
21
28
  .page-header.page-header-bar .page-status,
22
- .page-header-bar-views,
23
- .page-header-bar-meta,
24
29
  .page-header-bar-tools,
30
+ .page-header-bar-meta,
25
31
  .page-header.page-header-bar .page-actions {
26
32
  flex: none;
27
33
  }
34
+ .page-header-bar-views {
35
+ flex: 0 1 auto;
36
+ min-width: 0;
37
+ }
38
+ .page-header-bar-spacer {
39
+ flex: 1 1 0;
40
+ min-width: 0;
41
+ }
28
42
  .page-header-bar-crumbs {
29
43
  flex: none;
30
44
  font-size: 13px;
31
45
  color: var(--color-text-muted);
32
46
  white-space: nowrap;
33
47
  }
48
+ .page-header-bar-crumbs .breadcrumb {
49
+ --bs-breadcrumb-divider: "/";
50
+ margin: 0;
51
+ flex-wrap: nowrap;
52
+ }
53
+ .page-header-bar-crumbs .breadcrumb-item,
54
+ .page-header-bar-crumbs .breadcrumb-item a {
55
+ color: var(--color-text-muted);
56
+ text-decoration: none;
57
+ }
58
+ .page-header-bar-crumbs .breadcrumb-item a:hover {
59
+ text-decoration: underline;
60
+ }
61
+ .page-header-bar-crumbs .breadcrumb-item:last-child::after {
62
+ content: "/";
63
+ padding-inline-start: var(--bs-breadcrumb-item-padding-x, 0.5rem);
64
+ color: var(--color-text-muted);
65
+ }
34
66
  .page-header-bar-views,
35
67
  .page-header-bar-tools,
36
68
  .page-header-bar-bulk {
@@ -51,6 +83,9 @@
51
83
  display: none;
52
84
  }
53
85
  .page-header-bar-meta {
86
+ display: flex;
87
+ align-items: center;
88
+ column-gap: 16px;
54
89
  font-size: 13px;
55
90
  color: var(--color-text-muted);
56
91
  white-space: nowrap;
@@ -63,6 +98,9 @@
63
98
  }
64
99
  .page-header.wui-page-header-meta-below > .page-header-bar-meta {
65
100
  flex: 0 0 100%;
101
+ flex-wrap: wrap;
102
+ white-space: normal;
103
+ column-gap: 16px;
66
104
  line-height: 16px;
67
105
  }
68
106
 
@@ -74,4 +112,22 @@
74
112
  .page-header.page-header-bar .page-title {
75
113
  flex: 0 1 auto;
76
114
  }
115
+ .page-header-bar-crumbs {
116
+ display: none;
117
+ }
118
+ .page-header-bar-views {
119
+ order: 5;
120
+ flex: 1 1 100%;
121
+ min-width: 0;
122
+ }
123
+ .page-header-bar-meta {
124
+ order: 6;
125
+ flex: 1 1 100%;
126
+ flex-wrap: wrap;
127
+ white-space: normal;
128
+ }
129
+ .page-header-bar-tools {
130
+ flex-wrap: wrap;
131
+ min-width: 0;
132
+ }
77
133
  }