@webority/theme 0.76.0 → 0.77.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.76.0",
3
+ "version": "0.77.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": {
@@ -27,12 +27,11 @@
27
27
  }
28
28
 
29
29
  .btn-primary {
30
- // Pin hover/active to the pressed-primary token so a rebrand tracks
31
- // automatically instead of Bootstrap auto-shading $primary off-spec.
32
- --bs-btn-hover-bg: var(--wui-primary-pressed);
33
- --bs-btn-hover-border-color: var(--wui-primary-pressed);
34
- --bs-btn-active-bg: var(--wui-primary-pressed);
35
- --bs-btn-active-border-color: var(--wui-primary-pressed);
30
+ // Hover and press read --color-primary-hover: the pressed primary in light and the lifted dark primary in dark (_wui-aliases.scss), so a rebrand and dark mode both track it.
31
+ --bs-btn-hover-bg: var(--color-primary-hover);
32
+ --bs-btn-hover-border-color: var(--color-primary-hover);
33
+ --bs-btn-active-bg: var(--color-primary-hover);
34
+ --bs-btn-active-border-color: var(--color-primary-hover);
36
35
  // Bootstrap computes this label colour at SCSS compile time from the library's
37
36
  // default $primary, so it can never react to a product overriding --wui-primary as a
38
37
  // runtime CSS var. Routing it through the token makes a light-primary brand's
@@ -858,9 +857,10 @@
858
857
  .card-header-actions + .card-close {
859
858
  margin-left: 0.25rem;
860
859
  }
860
+ // Hover tints the button danger, the same as the modal and drawer close buttons.
861
861
  .card-close:hover {
862
- background: var(--color-bg-muted);
863
- color: var(--color-text);
862
+ background: var(--color-danger-soft);
863
+ color: var(--color-danger-text);
864
864
  }
865
865
  .card-close:focus-visible {
866
866
  outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
@@ -25,9 +25,10 @@
25
25
  --color-progress-track: var(--wui-progress-track);
26
26
  // Label text inside a progress bar fill.
27
27
  --color-progress-label: var(--wui-bg-light);
28
- // Ink on a solid status fill (success, warning, danger, info): near-black in both modes, since
29
- // white fails AA on the bright fills.
30
- --color-on-status-fill: #0F0F0F;
28
+ // Icon ink on a solid status fill (the filled timeline dots): white in both modes, by design
29
+ // choice. White on the bright success and warning fills sits under the 3:1 icon floor, so the
30
+ // contrast gate does not measure this pair.
31
+ --color-on-status-fill: #FFFFFF;
31
32
  // Ink on a DEEP status fill (the -text tokens, used as a fill the way primary is): white in
32
33
  // light mode, where those shades measure 5.5 to 6.2:1; near-black in dark mode, where -text is
33
34
  // the bright fill.
@@ -0,0 +1,88 @@
1
+ // ============================================================================
2
+ // _wui-core.scss
3
+ // ----------------------------------------------------------------------------
4
+ // The core widget layer: every partial of _wui-components.scss except the
5
+ // on-demand set, in the same order. A portal imports this globally, and each
6
+ // on-demand partial (wui/<name>) from the lazy route that renders it.
7
+ //
8
+ // scripts/check-theme-split.mjs keeps this file in step with _wui-components.scss.
9
+ // The split guards two things: a widget rule left in core defeats the split,
10
+ // and a core rule that styles an on-demand class silently changes when that
11
+ // layer is absent.
12
+ // ============================================================================
13
+
14
+ // Shared tone-pill mixin — used by .badge, .status-label, and .chip.
15
+ // Must be imported before components that use it.
16
+ @import "wui/tone-pill";
17
+
18
+ // The order below is LOAD-BEARING. A later partial may deliberately override an
19
+ // earlier one, so reordering these imports silently changes the cascade — and
20
+ // nothing in the build will complain. `pnpm check:theme` measures the rules that
21
+ // have to win against Bootstrap; the rest is guarded by review.
22
+ @import "wui/controls";
23
+ @import "wui/choice";
24
+ @import "wui/media";
25
+ @import "wui/avatar-picker";
26
+ @import "wui/navigation";
27
+ @import "wui/card";
28
+ @import "wui/data";
29
+ @import "wui/illustration";
30
+ @import "wui/table-server";
31
+ @import "wui/inline-edit";
32
+ @import "wui/mention-input";
33
+ @import "wui/comment-thread";
34
+ @import "wui/overlays";
35
+ @import "wui/drawer";
36
+ @import "wui/modal-remote";
37
+ @import "wui/utilities";
38
+ @import "wui/typography";
39
+ @import "wui/disclosure";
40
+ @import "wui/display";
41
+ @import "wui/flow";
42
+ // After `flow`: the pricing section extends its `.plan-grid` rules.
43
+ @import "wui/sections";
44
+ @import "wui/chart";
45
+ @import "wui/chart-card";
46
+ @import "wui/scrollbar";
47
+ @import "wui/sortable";
48
+ // Before `split-view`, which places the legend sheet inside the list column.
49
+ @import "wui/record-pager";
50
+ @import "wui/shortcuts";
51
+ @import "wui/split-view";
52
+ @import "wui/groups";
53
+ @import "wui/carousel";
54
+ @import "wui/onboarding-checklist";
55
+ @import "wui/rating";
56
+ @import "wui/score-scale";
57
+ @import "wui/relative-time";
58
+ @import "wui/resizable";
59
+ @import "wui/rolling-number";
60
+ @import "wui/show-more";
61
+ @import "wui/truncate";
62
+ @import "wui/page-header-bar";
63
+ @import "wui/filter-popover";
64
+ @import "wui/metric-strip";
65
+ @import "wui/due-date";
66
+
67
+ // Country flags are NOT part of this layer. The 245 `.wui-flag-*` rules are about 187KB raw
68
+ // (38KB Brotli) of inlined SVG, and most sites never render a phone field or country picker.
69
+ // A site that does imports `@webority/theme/scss/flags` once, after this file; without it
70
+ // <wui-phone> and <wui-country-select> show an empty grey box where the flag should be.
71
+ @import "wui/shell";
72
+ @import "wui/shell-parts";
73
+ @import "wui/consent-banner";
74
+ @import "wui/lightbox";
75
+ @import "wui/toolbar";
76
+ @import "wui/theme-toggle";
77
+ @import "wui/floating";
78
+ @import "wui/code-block";
79
+ @import "wui/context-menu";
80
+ @import "wui/menubar";
81
+ @import "wui/navigation-menu";
82
+ @import "wui/hover-card";
83
+ @import "wui/notification-center";
84
+ @import "wui/whats-new";
85
+ // After every component partial: the shared focus ring and pressed wash settle ties with their hover rules.
86
+ @import "wui/interaction";
87
+ // Last: restates the settled state of menus other partials style, so it must win.
88
+ @import "wui/menu-motion";
@@ -254,3 +254,8 @@ wui-calendar[data-view="week"] .wui-calendar-surface > * {
254
254
  transition: none;
255
255
  }
256
256
  }
257
+
258
+ // The shared pressed wash of wui/interaction, restated here so it ships with this on-demand layer.
259
+ :is(.wui-calendar-nav-btn, .wui-calendar-today-btn, .wui-calendar-view-btn):active:not(:disabled, [aria-disabled="true"]) {
260
+ background-image: linear-gradient(var(--color-pressed-overlay), var(--color-pressed-overlay)) !important;
261
+ }
@@ -21,14 +21,24 @@
21
21
  // Default card is borderless — a flat surface. The border-STYLE stays solid (Bootstrap's
22
22
  // `border: var(--bs-card-border-width) solid …`), so a variant or a consumer class can
23
23
  // bring the edge back just by giving it a width (set --bs-card-border-width).
24
- --bs-card-border-width: 0;
24
+ // With a unit: the inner radius below subtracts it, and calc() drops a bare 0.
25
+ --bs-card-border-width: 0px;
26
+ // Bootstrap compiles the inner radius from $card-border-radius, which is smaller than the
27
+ // token radius the card actually draws. The header, footer and media round their corners
28
+ // with it, so their fill poked out over a bordered card's corners and the border looked cut.
29
+ // Derive it from the live radius and border width instead.
30
+ --bs-card-inner-border-radius: calc(var(--bs-card-border-radius) - var(--bs-card-border-width));
25
31
  }
26
32
 
27
33
  // A card header that carries a title on the left and controls on the right — the
28
34
  // pattern nearly every panel header uses, so it belongs here rather than being
29
35
  // re-flexed with utilities on each one.
36
+ // The header and footer share the card's own fill (a muted card stays muted to its top edge),
37
+ // and the header carries no rule of its own: Bootstrap's divider followed the card's border
38
+ // width, so only the elevated variant drew one.
30
39
  .card-header {
31
- --bs-card-cap-bg: var(--color-surface);
40
+ --bs-card-cap-bg: transparent;
41
+ border-bottom: 0;
32
42
  display: flex;
33
43
  align-items: center;
34
44
  justify-content: space-between;
@@ -63,8 +73,9 @@
63
73
  --bs-card-border-width: 1px;
64
74
  border-color: var(--color-border-strong);
65
75
  }
76
+ // Through the variable, so the inner radius and the border agree.
66
77
  .card-outlined {
67
- border-width: 2px;
78
+ --bs-card-border-width: 2px;
68
79
  border-color: var(--color-border-strong);
69
80
  }
70
81
  .card-muted {
@@ -88,14 +99,16 @@
88
99
  .card-accent.tone-info {
89
100
  --card-accent: var(--color-info);
90
101
  }
102
+ // The stripe is a left-only border on a layer with the card's own corners. CSS corner shaping
103
+ // curves its inner edge with the radius minus each side's width, so the stripe thins smoothly
104
+ // to nothing round both left corners. A straight bar clipped by the corners showed sliced ends.
91
105
  .card-accent::before {
92
106
  content: "";
93
107
  position: absolute;
94
- top: 0;
95
- bottom: 0;
96
- left: 0;
97
- width: 4px;
98
- background: var(--card-accent);
108
+ inset: 0;
109
+ border-left: 4px solid var(--card-accent);
110
+ border-radius: inherit;
111
+ pointer-events: none;
99
112
  }
100
113
 
101
114
  // ── Padding scale ── (header + body + footer). `md` is the Bootstrap default.
@@ -114,6 +127,12 @@
114
127
  .card-pad-lg > .card-footer {
115
128
  padding: 28px 32px;
116
129
  }
130
+ // With no rule under the header, its bottom padding alone separates the title from the body;
131
+ // the body's own top padding on top of it left a double gap. After the scale, so every
132
+ // padding step keeps the single gap.
133
+ .card > .card-header + .card-body {
134
+ padding-top: 0;
135
+ }
117
136
 
118
137
  // ── Constrained width ── (`maxWidth`): caps + centres a standalone card. Cards
119
138
  // are fluid (fill their column) unless this is set.
@@ -124,7 +143,7 @@
124
143
 
125
144
  // ── Footer ── (React `footer` / Razor <app-card-footer>): mirrors the header row.
126
145
  .card-footer {
127
- --bs-card-cap-bg: var(--color-surface);
146
+ --bs-card-cap-bg: transparent;
128
147
  display: flex;
129
148
  align-items: center;
130
149
  gap: 12px;
@@ -164,8 +183,8 @@
164
183
  }
165
184
  // Media flush at the top follows the card's rounded corners.
166
185
  .card > .card-media:first-child {
167
- border-top-left-radius: var(--radius-lg);
168
- border-top-right-radius: var(--radius-lg);
186
+ border-top-left-radius: var(--bs-card-inner-border-radius);
187
+ border-top-right-radius: var(--bs-card-inner-border-radius);
169
188
  }
170
189
 
171
190
  // ── Clickable card ── (`href` / `onClick`): the whole surface is one control.
@@ -180,9 +180,13 @@ button.wui-cmd-crumb:focus-visible {
180
180
  // Keeps a little of the next row visible when the keyboard scrolls the active one in.
181
181
  scroll-margin: 6px;
182
182
  }
183
+ // Focus stays in the input, and the hover tint alone marks the keyboard's row at about 1.2:1, so
184
+ // an inset ring makes it plain; inset, because the list scrolls and would clip an outer ring.
183
185
  .wui-cmd-item[aria-selected="true"] {
184
186
  background: var(--color-primary-soft);
185
187
  color: var(--color-primary-ink);
188
+ outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
189
+ outline-offset: var(--wui-focus-ring-inset);
186
190
  }
187
191
  .wui-cmd-item[aria-disabled="true"] {
188
192
  color: var(--color-text-muted);
@@ -296,3 +300,8 @@ button.wui-cmd-crumb:focus-visible {
296
300
  color 0.12s ease;
297
301
  }
298
302
  }
303
+
304
+ // The shared pressed wash of wui/interaction, restated here so it ships with this on-demand layer.
305
+ :is(.wui-cmd-back, button.wui-cmd-crumb):active:not(:disabled, [aria-disabled="true"]) {
306
+ background-image: linear-gradient(var(--color-pressed-overlay), var(--color-pressed-overlay)) !important;
307
+ }
@@ -1145,8 +1145,7 @@ $wui-field-error-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/
1145
1145
  gap: 4px;
1146
1146
  min-width: 0;
1147
1147
  }
1148
- // Inside the flex column these two size to their content, so they stretch to the field instead.
1149
- .field > wui-rich-text,
1148
+ // Inside the flex column it sizes to its content, so it stretches to the field instead.
1150
1149
  .field > wui-mention-input {
1151
1150
  align-self: stretch;
1152
1151
  width: 100%;
@@ -198,6 +198,7 @@
198
198
  font-size: 12px;
199
199
  font-weight: 500;
200
200
  letter-spacing: 0.04em;
201
+ text-transform: uppercase;
201
202
  white-space: nowrap;
202
203
  vertical-align: middle;
203
204
  transition:
@@ -562,6 +563,10 @@
562
563
  white-space: nowrap;
563
564
  flex: none;
564
565
  }
566
+ // 3px padding and a 1px border on each side, so the strip reaches the small control height beside the other filter controls.
567
+ .wui-filter-bar .segmented-sm {
568
+ --segmented-option-h: calc(var(--wui-control-height-sm) - 8px);
569
+ }
565
570
  // Facets folded behind the Filters button stack in one panel, each at the panel's width. Both surfaces
566
571
  // draw it as the same <details>; in the Razor bar it sits inside the form, so the controls still submit.
567
572
  .wui-filter-more-panel {
@@ -1082,7 +1087,9 @@
1082
1087
  align-items: flex-end;
1083
1088
  }
1084
1089
  // The leading cell is the card's title line: full width, no label.
1090
+ // Block, not the flex row of the other cells, so a name with a sub-line stacks instead of sitting beside it.
1085
1091
  .table-ledger.wui-table-stack > * > tr > td:not(.wui-select-cell, .wui-expand-cell):is(:first-child, .wui-select-cell + td, .wui-expand-cell + td) {
1092
+ display: block;
1086
1093
  justify-content: flex-start;
1087
1094
  padding: 0 32px 6px 0;
1088
1095
  text-align: left;
@@ -175,21 +175,27 @@
175
175
  }
176
176
 
177
177
  // Spotlight — an editorial feature tile: the label sits top-left with the icon chip pinned
178
- // top-right, the value is set in the display face (a product's serif brand face when it
179
- // overrides --wui-font-display), and an optional caption reads under the number. For the
180
- // single showcase stat on a marketing/portal surface where the number is the hero.
178
+ // top-right, the value is set large in the product's own font, and an optional caption reads
179
+ // under the number. For the single showcase stat on a marketing/portal surface where the
180
+ // number is the hero.
181
181
  .kpi.kpi-spotlight {
182
182
  display: grid;
183
183
  grid-template-columns: minmax(0, 1fr) auto;
184
+ // Rows pack at the top: a tile stretched to a taller neighbour's height puts the spare
185
+ // space below the caption, so the label and icon row stays level across tiles.
186
+ align-content: start;
184
187
  column-gap: 16px;
185
188
  padding: 22px 24px;
186
- --wui-kpi-value-size: 44px;
189
+ --wui-kpi-value-size: 36px;
187
190
  }
188
191
  .kpi.kpi-spotlight .kpi-icon {
189
192
  grid-column: 2;
190
193
  grid-row: 1;
191
194
  align-self: start;
192
195
  justify-self: end;
196
+ // A larger chip than the plain tile's 40px, to match the hero-sized value; the glyph keeps its size.
197
+ width: 48px;
198
+ height: 48px;
193
199
  margin-bottom: 0;
194
200
  }
195
201
  .kpi.kpi-spotlight .kpi-label,
@@ -206,12 +212,9 @@
206
212
  }
207
213
  .kpi.kpi-spotlight .kpi-value {
208
214
  margin-top: 14px;
209
- // The editorial serif is this variant's signature (see the reference tile), so the value gets
210
- // a serif stack directly — not --wui-font-display, which defaults to the body sans and so would
211
- // never read as serif. A product that wants its own face overrides this one selector.
212
- // Size comes from --wui-kpi-value-size (44px default, set on the spotlight root above), so a
215
+ // No font-family: the value inherits the product's own font like every other tile.
216
+ // Size comes from --wui-kpi-value-size (36px default, set on the spotlight root above), so a
213
217
  // custom `valueSize` (px) can tame a too-large headline.
214
- font-family: var(--wui-font-display);
215
218
  line-height: 1;
216
219
  font-weight: 600;
217
220
  }
@@ -357,6 +360,11 @@ a.info-row:hover .info-value {
357
360
  border-bottom: 1px solid var(--color-border);
358
361
  grid-column: span var(--descs-span, 1);
359
362
  }
363
+ @for $n from 1 through 12 {
364
+ .wui-descs-span-#{$n} {
365
+ --descs-span: #{$n};
366
+ }
367
+ }
360
368
  .descs-label {
361
369
  font-size: 12px;
362
370
  font-weight: 600;
@@ -1312,7 +1320,7 @@ a.info-row:hover .info-value {
1312
1320
  position: absolute;
1313
1321
  left: 0;
1314
1322
  right: 0;
1315
- bottom: -2px;
1323
+ bottom: 1px;
1316
1324
  height: 1px;
1317
1325
  background: currentColor;
1318
1326
  transform: scaleX(0);
@@ -100,3 +100,8 @@ wui-dock {
100
100
  transition: none;
101
101
  }
102
102
  }
103
+
104
+ // The shared pressed wash of wui/interaction, restated here so it ships with this on-demand layer.
105
+ .dock-item:active:not(:disabled, [aria-disabled="true"]) {
106
+ background-image: linear-gradient(var(--color-pressed-overlay), var(--color-pressed-overlay)) !important;
107
+ }
@@ -500,3 +500,8 @@ wui-file-viewer:fullscreen {
500
500
  min-width: 48px;
501
501
  }
502
502
 
503
+ // The shared pressed wash of wui/interaction, restated here so it ships with this on-demand layer.
504
+ .wui-file-viewer-zoom:active:not(:disabled, [aria-disabled="true"]) {
505
+ background-image: linear-gradient(var(--color-pressed-overlay), var(--color-pressed-overlay)) !important;
506
+ }
507
+
@@ -14,6 +14,10 @@
14
14
  flex-direction: column-reverse;
15
15
  gap: 12px;
16
16
  margin: 1rem;
17
+ // Clear the home indicator and the notch on a phone.
18
+ margin-bottom: max(1rem, env(safe-area-inset-bottom));
19
+ margin-left: max(1rem, env(safe-area-inset-left));
20
+ margin-right: max(1rem, env(safe-area-inset-right));
17
21
  // The container spans only its items, but a gap between them must not swallow clicks on the page.
18
22
  pointer-events: none;
19
23
  }
@@ -53,9 +57,9 @@
53
57
  // Quieter than the primary circle: a light brand tint, the brand icon and a thin brand border.
54
58
  .wui-floating-btn--secondary {
55
59
  --wui-floating-size: 40px;
56
- --bs-btn-border-color: var(--wui-primary-30);
57
- --bs-btn-hover-border-color: var(--wui-primary-30);
58
- --bs-btn-active-border-color: var(--wui-primary-30);
60
+ --bs-btn-border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
61
+ --bs-btn-hover-border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
62
+ --bs-btn-active-border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
59
63
  }
60
64
  @media (prefers-reduced-motion: reduce) {
61
65
  .wui-floating-btn {
@@ -9,6 +9,7 @@
9
9
  // .stepper — compact: numbered circle above a label, line behind.
10
10
  // .stepper-journey — wide: 40px dot beside a label + title, for a page masthead.
11
11
  // .stepper-icons — large connected icon badges with a "STEP n" eyebrow.
12
+ // .wui-step-tiles — a row of tiles: rounded icon square, label and a state line, chevrons between.
12
13
  //
13
14
  // Fully themable through a small set of `--wui-step-*` custom properties — set them on
14
15
  // the stepper (or the wizard host, or any ancestor: they inherit), or flip the whole
@@ -517,11 +518,160 @@ button.step.done:focus-visible .step-circle {
517
518
  }
518
519
  }
519
520
 
521
+ // .wui-step-tiles — each step is a list item holding a rounded icon square with the label and a
522
+ // state line (Done, Current, To do) beside it; chevrons join the tiles. Like every variant it is one
523
+ // brand-primary accent: the current tile is solid accent and done tiles the soft tint, so a tone
524
+ // class recolours the current one and a done-tone class the finished ones.
525
+ // ----------------------------------------------------------------------------
526
+ .wui-step-tiles {
527
+ padding: 0 16px;
528
+ }
529
+ .wui-step-tiles > ol {
530
+ display: flex;
531
+ align-items: center;
532
+ gap: 6px;
533
+ width: 100%;
534
+ margin: 0;
535
+ padding: 0;
536
+ list-style: none;
537
+ }
538
+ .wui-step-tile {
539
+ display: flex;
540
+ align-items: center;
541
+ min-width: 0;
542
+ }
543
+ // Every tile after the first takes an equal share of the free width and centres its chevron in it.
544
+ .wui-step-tile + .wui-step-tile {
545
+ flex: 1 1 auto;
546
+ padding-inline-start: 12px;
547
+ }
548
+ .wui-step-tile + .wui-step-tile > .wui-step-tile-body {
549
+ margin-inline-start: 12px;
550
+ }
551
+ .wui-step-tile + .wui-step-tile::before {
552
+ content: "";
553
+ flex: none;
554
+ width: 7px;
555
+ height: 7px;
556
+ margin-inline: auto;
557
+ border-top: 2px solid var(--wui-step-connector, var(--color-border-strong));
558
+ border-right: 2px solid var(--wui-step-connector, var(--color-border-strong));
559
+ transform: rotate(45deg);
560
+ }
561
+ .wui-step-tile-body {
562
+ display: flex;
563
+ align-items: center;
564
+ gap: 10px;
565
+ min-width: 0;
566
+ padding: 4px 8px 4px 0;
567
+ border: 0;
568
+ border-radius: var(--radius);
569
+ background: none;
570
+ font: inherit;
571
+ text-align: start;
572
+ color: inherit;
573
+ }
574
+ button.wui-step-tile-body {
575
+ cursor: pointer;
576
+ }
577
+ button.wui-step-tile-body:focus-visible {
578
+ outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
579
+ outline-offset: var(--wui-focus-ring-offset);
580
+ }
581
+ .wui-step-tiles .step-circle {
582
+ width: var(--wui-step-tile-size, 34px);
583
+ height: var(--wui-step-tile-size, 34px);
584
+ border: 0;
585
+ border-radius: var(--radius);
586
+ background: var(--color-bg-muted);
587
+ color: var(--color-text-subtle);
588
+ font-size: var(--wui-step-number-font, 13px);
589
+ }
590
+ .wui-step-tile.active .step-circle {
591
+ background: var(--wui-step-accent, var(--color-primary-ink));
592
+ color: var(--wui-step-accent-fg, var(--color-on-primary-ink));
593
+ }
594
+ .wui-step-tile.done .step-circle {
595
+ background: var(--wui-step-done-soft, var(--wui-step-soft, var(--color-primary-soft)));
596
+ color: var(--wui-step-done-soft-fg, var(--wui-step-soft-fg, var(--color-primary-ink)));
597
+ }
598
+ .wui-step-tile.error .step-circle {
599
+ background: var(--color-danger-soft);
600
+ color: var(--color-danger-text);
601
+ }
602
+ .wui-step-tile.skipped .step-circle {
603
+ border: 1px dashed var(--color-border-strong);
604
+ color: var(--color-text-muted);
605
+ }
606
+ .wui-step-tiles .step-text {
607
+ display: flex;
608
+ flex-direction: column;
609
+ gap: 1px;
610
+ min-width: 0;
611
+ }
612
+ .wui-step-tiles .step-label {
613
+ max-width: none;
614
+ font-size: var(--wui-step-label-font, 13px);
615
+ font-weight: 600;
616
+ line-height: 1.25;
617
+ color: var(--color-text);
618
+ white-space: nowrap;
619
+ }
620
+ .wui-step-tile.active .step-label {
621
+ color: var(--wui-step-accent, var(--color-primary-ink));
622
+ }
623
+ .wui-step-tile.error .step-label {
624
+ color: var(--color-danger-text);
625
+ }
626
+ button.wui-step-tile-body:hover .step-label {
627
+ color: var(--wui-step-accent, var(--color-primary-ink));
628
+ }
629
+ .wui-step-tiles .wui-step-tile-state {
630
+ font-size: 12px;
631
+ line-height: 1.3;
632
+ color: var(--color-text-muted);
633
+ white-space: nowrap;
634
+ }
635
+ // A vertical wizard, or a phone, stacks the tiles into a list; the chevrons only make sense in a row.
636
+ .wui-step-tiles.stepper-vertical > ol {
637
+ flex-direction: column;
638
+ align-items: flex-start;
639
+ gap: 12px;
640
+ }
641
+ .wui-step-tiles.stepper-vertical .wui-step-tile + .wui-step-tile {
642
+ flex: none;
643
+ padding-inline-start: 0;
644
+ }
645
+ .wui-step-tiles.stepper-vertical .wui-step-tile + .wui-step-tile > .wui-step-tile-body {
646
+ margin-inline-start: 0;
647
+ }
648
+ .wui-step-tiles.stepper-vertical .wui-step-tile + .wui-step-tile::before {
649
+ content: none;
650
+ }
651
+ @include media-breakpoint-down(md) {
652
+ .wui-step-tiles.wui-step-tiles > ol {
653
+ flex-direction: column;
654
+ align-items: flex-start;
655
+ gap: 12px;
656
+ }
657
+ .wui-step-tiles.wui-step-tiles .wui-step-tile + .wui-step-tile {
658
+ flex: none;
659
+ padding-inline-start: 0;
660
+ }
661
+ .wui-step-tiles.wui-step-tiles .wui-step-tile + .wui-step-tile > .wui-step-tile-body {
662
+ margin-inline-start: 0;
663
+ }
664
+ .wui-step-tiles.wui-step-tiles .wui-step-tile + .wui-step-tile::before {
665
+ content: none;
666
+ }
667
+ }
668
+
520
669
  // No connectors: the wizard's hide-connectors option drops the line between steps on every
521
670
  // variant and orientation. Placed after the variants so it outranks their equal-specificity
522
671
  // rails; the journey bar's .step-line siblings are simply not emitted.
523
672
  .wui-stepper-no-connectors .step + .step::before,
524
- .wui-stepper-no-connectors .step:not(:last-child)::after {
673
+ .wui-stepper-no-connectors .step:not(:last-child)::after,
674
+ .wui-stepper-no-connectors .wui-step-tile + .wui-step-tile::before {
525
675
  content: none;
526
676
  }
527
677
 
@@ -999,6 +1149,11 @@ button.step.done:focus-visible .step-circle {
999
1149
  // width here and lines up with the fields beside it. A .date-w-* class on the field still wins.
1000
1150
  --wui-datepicker-width: 100%;
1001
1151
  }
1152
+ @for $n from 1 through 12 {
1153
+ .wui-form-cols-#{$n} {
1154
+ --form-cols: #{$n};
1155
+ }
1156
+ }
1002
1157
  // Below md, collapse to 1 column.
1003
1158
  @include media-breakpoint-down(md) {
1004
1159
  .form-row-grid.form-row-grid {
@@ -1056,6 +1211,60 @@ wui-wizard-step[hidden] {
1056
1211
  .wizard-stepper {
1057
1212
  margin-bottom: 24px;
1058
1213
  }
1214
+ // The optional bar above the stepper (`top-bar`): the title at the start, then the step count and the
1215
+ // close button at the end.
1216
+ .wui-wizard-topbar {
1217
+ display: flex;
1218
+ align-items: center;
1219
+ gap: 12px;
1220
+ box-sizing: border-box;
1221
+ height: 56px;
1222
+ padding: 0 16px;
1223
+ border-bottom: 1px solid var(--color-border);
1224
+ }
1225
+ .wui-wizard-topbar + .wizard-stepper {
1226
+ margin-top: 16px;
1227
+ }
1228
+ .wui-wizard-topbar-title {
1229
+ flex: 1 1 auto;
1230
+ min-width: 0;
1231
+ overflow: hidden;
1232
+ font-size: 16px;
1233
+ font-weight: 600;
1234
+ text-overflow: ellipsis;
1235
+ white-space: nowrap;
1236
+ color: var(--color-text);
1237
+ }
1238
+ .wui-wizard-topbar-count {
1239
+ flex: none;
1240
+ font-size: 13px;
1241
+ color: var(--color-text-subtle);
1242
+ white-space: nowrap;
1243
+ }
1244
+ .wui-wizard-topbar-close {
1245
+ display: inline-grid;
1246
+ flex: none;
1247
+ place-items: center;
1248
+ width: 32px;
1249
+ height: 32px;
1250
+ padding: 0;
1251
+ border: 0;
1252
+ border-radius: var(--radius);
1253
+ background: transparent;
1254
+ color: var(--color-text-muted);
1255
+ cursor: pointer;
1256
+ transition:
1257
+ background-color 0.12s ease,
1258
+ color 0.12s ease;
1259
+ }
1260
+ .wui-wizard-topbar-close:hover {
1261
+ background: var(--color-bg-muted);
1262
+ color: var(--color-text);
1263
+ }
1264
+ .wui-wizard-topbar-close:focus-visible {
1265
+ outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
1266
+ outline-offset: var(--wui-focus-ring-offset);
1267
+ }
1059
1268
  // A skipped step reads as "passed over", not "achieved": a dashed, muted circle so it
1060
1269
  // is visibly distinct from a ticked (done) step.
1061
1270
  .step.skipped .step-circle {
@@ -213,3 +213,8 @@ wui-image-crop {
213
213
  min-height: 44px;
214
214
  }
215
215
  }
216
+
217
+ // The shared pressed wash of wui/interaction, restated here so it ships with this on-demand layer.
218
+ :is(.wui-crop-toolbtn, .wui-crop-zoombtn):active:not(:disabled, [aria-disabled="true"]) {
219
+ background-image: linear-gradient(var(--color-pressed-overlay), var(--color-pressed-overlay)) !important;
220
+ }