@webority/theme 0.13.5 → 0.15.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/index.scss CHANGED
@@ -4,9 +4,9 @@
4
4
  //
5
5
  // Load order is LOAD-BEARING — never reorder:
6
6
  // variables (before bootstrap) -> bootstrap -> tokens -> aliases -> components -> wui-components
7
- // Flags are NOT imported here: _wui-components already imports them (the phone control needs
8
- // them), and legacy @import does not dedupe, so a second import emitted all ~245 flag classes
9
- // twice (≈490 selectors) and bloated the bundle.
7
+ // Country flags are NOT imported here, and neither does _wui-components: they are an opt-in
8
+ // layer (scss/_flags.scss, compiled on its own to dist/webority-flags.css) for sites that render
9
+ // the phone input or country picker.
10
10
  @import "scss/variables";
11
11
  @import "bootstrap/scss/bootstrap";
12
12
  @import "scss/tokens";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/theme",
3
- "version": "0.13.5",
3
+ "version": "0.15.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": {
@@ -26,6 +26,8 @@
26
26
  "./scss/wui/*": "./scss/wui/_*.scss",
27
27
  "./scss/*": "./scss/_*.scss",
28
28
  "./css": "./dist/webority-theme.css",
29
+ "./css/flags": "./dist/webority-flags.css",
30
+ "./purge-safelist.json": "./dist/purge-safelist.json",
29
31
  "./package.json": "./package.json"
30
32
  },
31
33
  "sass": "index.scss",
@@ -39,6 +41,6 @@
39
41
  "sass-embedded": "^1.97.0"
40
42
  },
41
43
  "scripts": {
42
- "build": "sass index.scss dist/webority-theme.css --load-path=node_modules --style=compressed --no-source-map --quiet-deps --silence-deprecation=if-function && node ../../scripts/sync-rcl-assets.mjs theme"
44
+ "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 && node ../../scripts/gen-purge-safelist.mjs && node ../../scripts/sync-rcl-assets.mjs theme"
43
45
  }
44
46
  }
@@ -245,32 +245,32 @@
245
245
  // sizes step font-size, horizontal padding and radius together (vertical size
246
246
  // comes from --wui-control-height, so a button lines up with an input). Weight
247
247
  // is 600 on every rung — buttons read as a deliberate action, not body text.
248
- // Medium (default): 15px / 600, 24px side padding, 18px squircle radius, 45px tall.
248
+ // Medium (default): 14px / 600, 24px side padding, 18px squircle radius, 45px tall.
249
249
  .btn {
250
250
  --bs-btn-border-radius: 18px;
251
- --bs-btn-font-size: 0.9375rem;
251
+ --bs-btn-font-size: 0.875rem;
252
252
  --bs-btn-font-weight: 600;
253
253
  --bs-btn-padding-x: 1.5rem;
254
254
  // Progressive enhancement only (Chrome/Safari recent). Unsupported engines
255
255
  // keep a plain border-radius arc — not a cross-browser design contract.
256
256
  corner-shape: squircle;
257
257
  }
258
- // Small: 14px, 18px side padding, 14px squircle radius (proportional to md), 35px tall.
258
+ // Small: 12px, 18px side padding, 14px squircle radius (proportional to md), 35px tall.
259
259
  .btn-sm:not(.btn-icon),
260
260
  .btn-group-sm > .btn:not(.btn-icon) {
261
261
  --bs-btn-border-radius: 14px;
262
- --bs-btn-font-size: 0.875rem;
262
+ --bs-btn-font-size: 0.75rem;
263
263
  --bs-btn-padding-x: 1.125rem;
264
264
  display: inline-flex;
265
265
  align-items: center;
266
266
  justify-content: center;
267
267
  min-height: 35px;
268
268
  }
269
- // Large: 17px, 32px side padding, 22px squircle radius (proportional to md), 55px tall — a spacious CTA.
269
+ // Large: 16px, 32px side padding, 22px squircle radius (proportional to md), 55px tall — a spacious CTA.
270
270
  .btn-lg:not(.btn-icon),
271
271
  .btn-group-lg > .btn:not(.btn-icon) {
272
272
  --bs-btn-border-radius: 22px;
273
- --bs-btn-font-size: 1.0625rem;
273
+ --bs-btn-font-size: 1rem;
274
274
  --bs-btn-padding-x: 2rem;
275
275
  display: inline-flex;
276
276
  align-items: center;
package/scss/_tokens.scss CHANGED
@@ -43,6 +43,10 @@
43
43
  --wui-field-bg: #F9FBFC;
44
44
  --wui-border-light: #EBEDF3;
45
45
  --wui-border-soft: #F4F6F8;
46
+ // The unfilled part of a progress bar / meter. Darker than every surface it can sit on
47
+ // (card, page, muted fill) so the empty track reads as a track, yet light enough that the
48
+ // mid-yellow warning fill still separates from it.
49
+ --wui-progress-track: #E1E5EE;
46
50
 
47
51
  // Gradients (hero, CTA)
48
52
  --wui-hero-gradient:
@@ -217,6 +221,8 @@
217
221
  --wui-dark-bg-surface: #181b23;
218
222
  --wui-dark-border: #2a2f3a;
219
223
  --wui-dark-border-strong:#3a414f;
224
+ // Lighter than the dark page, surface and muted fill, so the empty track shows on all three.
225
+ --wui-dark-progress-track: #323845;
220
226
  --wui-dark-text: #e6e8ee;
221
227
  --wui-dark-text-muted: #b4b9c6;
222
228
  --wui-dark-text-subtle: #8a90a0;
@@ -18,9 +18,18 @@
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
+ --color-progress-track: var(--wui-progress-track);
22
+ // Label text inside a progress bar fill.
23
+ --color-progress-label: var(--wui-bg-light);
21
24
 
22
25
  // Text ramp
23
26
  --color-text: var(--wui-text-heading);
27
+ // Body copy on a tinted surface (alert bodies): a step softer than the heading ink.
28
+ --color-text-body: var(--wui-text-1);
29
+ // A dark-ink glyph such as Bootstrap's .btn-close needs no filter on a light surface.
30
+ --color-close-filter: none;
31
+ // The border of a focused / open / active control (input, select, OTP slot, dropzone).
32
+ --color-focus-border: var(--wui-primary-30);
24
33
  --color-text-muted: var(--wui-text-2);
25
34
  --color-text-subtle: var(--wui-text-3);
26
35
 
@@ -54,6 +63,7 @@
54
63
  --color-info-soft: var(--color-info-10);
55
64
  // Use this — never --color-info — when the colour IS the text.
56
65
  --color-info-text: var(--wui-info-text);
66
+ --color-info-text-on-soft: var(--wui-info-text);
57
67
 
58
68
  --color-success-active: var(--wui-success-active);
59
69
  --color-success: var(--wui-success);
@@ -66,6 +76,7 @@
66
76
  --color-success-soft: var(--color-success-10);
67
77
  // Use this — never --color-success — when the colour IS the text.
68
78
  --color-success-text: var(--wui-success-text);
79
+ --color-success-text-on-soft: var(--wui-success-text);
69
80
 
70
81
  --color-warning-active: var(--wui-warning-active);
71
82
  --color-warning: var(--wui-warning);
@@ -79,6 +90,7 @@
79
90
  --color-warning-soft: var(--color-warning-10);
80
91
  // Use this — never --color-warning — when the colour IS the text.
81
92
  --color-warning-text: var(--wui-warning-text);
93
+ --color-warning-text-on-soft: var(--wui-warning-text);
82
94
 
83
95
  --color-danger-active: var(--wui-danger-active);
84
96
  --color-danger: var(--wui-danger);
@@ -91,6 +103,7 @@
91
103
  --color-danger-soft: var(--color-danger-10);
92
104
  // Use this — never --color-danger — when the colour IS the text.
93
105
  --color-danger-text: var(--wui-danger-text);
106
+ --color-danger-text-on-soft: var(--wui-danger-text);
94
107
 
95
108
  // Accent → warning ramp
96
109
  --color-accent: var(--color-warning);
@@ -137,10 +150,19 @@
137
150
  --color-border: var(--wui-dark-border);
138
151
  --color-border-light: color-mix(in srgb, var(--wui-dark-border) 50%, transparent);
139
152
  --color-border-strong: var(--wui-dark-border-strong);
153
+ --color-progress-track: var(--wui-dark-progress-track);
154
+ // Same near-black as --color-primary-text below: 4.97:1 or better on every bright fill.
155
+ --color-progress-label: #0F0F0F;
140
156
 
141
157
  --color-text: var(--wui-dark-text);
142
158
  --color-text-muted: var(--wui-dark-text-muted);
143
159
  --color-text-subtle: var(--wui-dark-text-subtle);
160
+ --color-text-body: var(--wui-dark-text);
161
+ // Bootstrap's .btn-close glyph is ink-dark; on a dark surface it needs the white filter.
162
+ --color-close-filter: invert(1) grayscale(100%) brightness(200%);
163
+ // The light pastel focus border glows against a dark field; dark mode uses the dark theme's
164
+ // own primary (the button fill), which still clears 3:1 against every dark surface.
165
+ --color-focus-border: var(--wui-dark-primary);
144
166
 
145
167
  // Re-bridge the Bootstrap colour vars onto the (now dark) app tokens so the
146
168
  // named-var text/body/border utilities — .text-body-secondary, .text-muted,
@@ -183,6 +205,14 @@
183
205
  --color-warning-text: var(--wui-warning);
184
206
  --color-info-text: var(--wui-info);
185
207
 
208
+ // Text ON a soft tint (an alert heading). The bright tone alone falls under WCAG AA on its own
209
+ // 18% tint over the dark page (info and danger reach only ~3.8:1), so it is lifted with 30%
210
+ // white: every tone then clears 4.5:1 on the dark page, surface and muted fill.
211
+ --color-success-text-on-soft: color-mix(in srgb, var(--wui-success) 70%, white);
212
+ --color-danger-text-on-soft: color-mix(in srgb, var(--wui-danger) 70%, white);
213
+ --color-warning-text-on-soft: color-mix(in srgb, var(--wui-warning) 70%, white);
214
+ --color-info-text-on-soft: color-mix(in srgb, var(--wui-info) 70%, white);
215
+
186
216
  // Light mode maps -strong to deep AA fills; on dark surfaces those sink into the page.
187
217
  // Remap to the bright fills so timeline dots / strong accents stay visible (same as -text).
188
218
  --color-success-strong: var(--wui-success);
@@ -194,6 +224,12 @@
194
224
  --color-warning-soft: color-mix(in srgb, var(--wui-warning) 18%, transparent);
195
225
  --color-danger-soft: color-mix(in srgb, var(--wui-danger) 18%, transparent);
196
226
  --color-info-soft: color-mix(in srgb, var(--wui-info) 18%, transparent);
227
+ // The -50 border tone: the light hex would glow on a dark page, so it becomes a translucent
228
+ // mix of the vivid tone that reads as a border on the dark tint.
229
+ --color-success-50: color-mix(in srgb, var(--wui-success) 45%, transparent);
230
+ --color-warning-50: color-mix(in srgb, var(--wui-warning) 45%, transparent);
231
+ --color-danger-50: color-mix(in srgb, var(--wui-danger) 45%, transparent);
232
+ --color-info-50: color-mix(in srgb, var(--wui-info) 45%, transparent);
197
233
  --color-accent-soft: var(--color-warning-soft);
198
234
 
199
235
  --color-scrim: color-mix(in srgb, black 45%, transparent);
@@ -43,23 +43,15 @@
43
43
  // set, so it has to win on equal specificity.
44
44
  @import "wui/workspace";
45
45
 
46
- // Flags belong here because <wui-phone> cannot render without them.
47
- //
48
- // The 245 `.wui-flag-*` rules live in _flags.scss, and this entry did not pull it in — so a
49
- // product following the documented chain (variables -> bootstrap -> tokens -> components ->
50
- // wui-components) got AppPhoneNumberInput with an empty grey box where every flag should be,
51
- // and nothing failing anywhere to point at the cause. The component shipped; its styling did
52
- // not. That is the same escape as th.sortable and .u-name, and the fix is the same principle:
53
- // a component and the CSS it requires travel together.
54
-
55
- // It is not free — roughly 183KB raw, 45KB gzipped, mostly inlined SVG. A product that never
56
- // renders a phone field can skip it by importing the `wui/*` partials above individually
57
- // instead of this aggregate; that is the deliberate opt-out, and it is a choice rather than
58
- // an accident.
59
- @import "flags";
46
+ // Country flags are NOT part of this aggregate. The 245 `.wui-flag-*` rules are about 187KB raw
47
+ // (38KB Brotli) of inlined SVG, and most sites never render a phone field or country picker.
48
+ // A site that does imports `@webority/theme/scss/flags` once, after this file; without it
49
+ // <wui-phone> and <wui-country-select> show an empty grey box where the flag should be.
60
50
  @import "wui/shell";
61
51
  @import "wui/dock";
62
52
  @import "wui/consent-banner";
63
53
  @import "wui/toolbar";
54
+ // After `overlays`: anchors the shared modal shell to the top when it holds the palette card.
55
+ @import "wui/command-palette";
64
56
  // Last: restates the settled state of menus other partials style, so it must win.
65
57
  @import "wui/menu-motion";
@@ -58,18 +58,14 @@
58
58
  background: var(--color-surface);
59
59
  color: var(--color-primary);
60
60
  }
61
- // Icon illustrations are coloured PNGs: greyscale them until the card is
62
- // selected, then show the full-colour (blue) art.
63
- .struct-card-ic img {
64
- filter: grayscale(1);
65
- opacity: 0.6;
66
- transition:
67
- filter 0.12s,
68
- opacity 0.12s;
69
- }
70
- .struct-card.checked .struct-card-ic img {
71
- filter: none;
72
- opacity: 1;
61
+ // An image in the round icon disc (React iconNode={<img …/>}) keeps its own colours whether or not
62
+ // the card is selected, fills the whole disc and is cropped to its circle, whatever the image's own
63
+ // size or aspect. The tile variant drops the disc and sizes its illustration itself, so it is left out.
64
+ .struct-card:not(.struct-card-tile) .struct-card-ic img {
65
+ width: 100%;
66
+ height: 100%;
67
+ object-fit: cover;
68
+ border-radius: inherit;
73
69
  }
74
70
  .struct-card-body {
75
71
  display: flex;
@@ -113,6 +109,77 @@
113
109
  .struct-card-check svg path {
114
110
  stroke-width: 2.25;
115
111
  }
112
+ // No tick (hideCheck / hide-check): the plain card no longer needs the top/bottom room the
113
+ // corner tick sat in, so it tightens. The tile and image cards keep their own padding.
114
+ .wui-struct-card-no-check:not(.struct-card-tile):not(.struct-card-img-card) {
115
+ padding-top: 14px;
116
+ padding-bottom: 14px;
117
+ }
118
+
119
+ // Sizes (size="sm" | "lg"; md is the base card above). Padding, gap, icon disc, type and the tick
120
+ // offset step together so a card reads as one unit at every size. The tile variant owns its layout
121
+ // and type (its later rules win), and the image card steps only its body padding and type.
122
+ .wui-struct-card-sm {
123
+ gap: 9px;
124
+ }
125
+ .wui-struct-card-sm:not(.struct-card-tile):not(.struct-card-img-card) {
126
+ padding: 14px;
127
+ }
128
+ .wui-struct-card-sm.wui-struct-card-no-check:not(.struct-card-tile):not(.struct-card-img-card) {
129
+ padding-top: 10px;
130
+ padding-bottom: 10px;
131
+ }
132
+ .wui-struct-card-sm .struct-card-ic {
133
+ width: 36px;
134
+ height: 36px;
135
+ }
136
+ .wui-struct-card-sm .struct-card-title {
137
+ font-size: 14px;
138
+ }
139
+ .wui-struct-card-sm .struct-card-desc {
140
+ font-size: 13px;
141
+ }
142
+ .wui-struct-card-sm .struct-card-check {
143
+ top: 7px;
144
+ right: 7px;
145
+ }
146
+ .wui-struct-card-sm.struct-card-img-card .struct-card-body {
147
+ padding: 9px 11px;
148
+ }
149
+ .wui-struct-card-lg {
150
+ gap: 14px;
151
+ }
152
+ .wui-struct-card-lg:not(.struct-card-tile):not(.struct-card-img-card) {
153
+ padding: 26px;
154
+ }
155
+ .wui-struct-card-lg.wui-struct-card-no-check:not(.struct-card-tile):not(.struct-card-img-card) {
156
+ padding-top: 18px;
157
+ padding-bottom: 18px;
158
+ }
159
+ .wui-struct-card-lg .struct-card-ic {
160
+ width: 56px;
161
+ height: 56px;
162
+ }
163
+ .wui-struct-card-lg .struct-card-title {
164
+ font-size: 18px;
165
+ }
166
+ .wui-struct-card-lg .struct-card-desc {
167
+ font-size: 15px;
168
+ }
169
+ .wui-struct-card-lg .struct-card-check {
170
+ top: 13px;
171
+ right: 13px;
172
+ }
173
+ .wui-struct-card-lg.struct-card-img-card .struct-card-body {
174
+ padding: 15px 18px;
175
+ }
176
+
177
+ // Stacked layout (layout="stacked"): the icon disc sits on its own line and the title and
178
+ // description start on the line below, left-aligned. The gap between them follows the size step.
179
+ .wui-struct-card-stacked {
180
+ flex-direction: column;
181
+ align-items: flex-start;
182
+ }
116
183
 
117
184
  // Tile variant: big centered illustration + label. Opt in with
118
185
  // className="struct-card-tile".
@@ -131,13 +198,9 @@
131
198
  border-radius: 0;
132
199
  background: transparent;
133
200
  }
134
- // Full-colour at all times (override the greyscale-until-selected treatment).
135
- .struct-card-tile .struct-card-ic img,
136
- .struct-card-tile.checked .struct-card-ic img {
201
+ .struct-card-tile .struct-card-ic img {
137
202
  width: 100px;
138
203
  height: 100px;
139
- filter: none;
140
- opacity: 1;
141
204
  }
142
205
  .struct-card-tile .struct-card-body {
143
206
  align-items: center;
@@ -185,10 +248,10 @@
185
248
  }
186
249
  .search-select-control.open {
187
250
  // White fill + the active/focus border, matching a focused text input (which lifts off the
188
- // muted resting fill onto white with a --wui-primary-30 border). Keeps the control seamless
251
+ // muted resting fill onto white with a --color-focus-border border). Keeps the control seamless
189
252
  // with the open menu and the floated label's white mask.
190
253
  background: var(--color-surface);
191
- border-color: var(--wui-primary-30);
254
+ border-color: var(--color-focus-border);
192
255
  }
193
256
  .search-select-caret {
194
257
  flex-shrink: 0;
@@ -367,7 +430,7 @@
367
430
  }
368
431
  .search-select-search:focus-within {
369
432
  background: var(--color-surface);
370
- border-color: var(--wui-primary-30);
433
+ border-color: var(--color-focus-border);
371
434
  }
372
435
  .search-select-search:focus-within svg {
373
436
  color: var(--color-primary);
@@ -506,12 +569,21 @@
506
569
  // While the calendar is open the trigger reads as a focused field — clean white surface, not
507
570
  // the muted resting fill. Opening moves focus INTO the popup, so :focus drops off the trigger;
508
571
  // this .open rule keeps it white (matching .search-select-control.open) instead of snapping
509
- // back to grey the moment the dropdown appears. It stays white on close too (_close refocuses
510
- // the trigger → :focus), greying only on an outside click that blurs it.
572
+ // back to grey the moment the dropdown appears.
511
573
  background-color: var(--color-surface);
512
- border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
574
+ border-color: var(--color-focus-border);
513
575
  box-shadow: none;
514
576
  }
577
+ // Closing hands focus back to the trigger (so the keyboard user lands where they started), which
578
+ // would leave a mouse user with an "active" field after the calendar is gone. Once the popup is
579
+ // shut, the trigger keeps the focus look only for keyboard focus (:focus-visible); a pick or
580
+ // close by pointer returns it to rest. An errored field (aria-invalid on the host) keeps its red.
581
+ wui-datepicker:not([aria-invalid="true"]) .datepicker:not(.open) .datepicker-trigger:focus:not(:focus-visible),
582
+ wui-date-range:not([aria-invalid="true"]) .daterange:not(.open) .daterange-trigger:focus:not(:focus-visible),
583
+ wui-timepicker:not([aria-invalid="true"]) .timepicker:not(.open) .timepicker-trigger:focus:not(:focus-visible) {
584
+ background-color: var(--color-field-bg);
585
+ border-color: var(--color-border);
586
+ }
515
587
  // Portalled to <body> and fixed-positioned from JS so a modal's
516
588
  // overflow:hidden can't clip it; top/left come from inline styles. z-index
517
589
  // sits above the modal layer.
@@ -677,6 +749,13 @@ button.datepicker-month:hover {
677
749
  color: var(--color-border-strong);
678
750
  cursor: not-allowed;
679
751
  }
752
+ // Today outside the allowed range keeps its ring, so the user can still find it, but in the
753
+ // disabled ink: a primary ring would read as a date they can pick.
754
+ .datepicker-day.is-today:disabled,
755
+ .datepicker-cell.is-today:disabled {
756
+ box-shadow: inset 0 0 0 1.5px var(--color-border-strong);
757
+ color: var(--color-border-strong);
758
+ }
680
759
  .datepicker-foot {
681
760
  display: flex;
682
761
  align-items: center;
@@ -835,7 +914,7 @@ button.datepicker-month:hover {
835
914
  .daterange.open .daterange-trigger {
836
915
  // Stay white while the two-month popover is open — see the note on .datepicker.open above.
837
916
  background-color: var(--color-surface);
838
- border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
917
+ border-color: var(--color-focus-border);
839
918
  box-shadow: none;
840
919
  }
841
920
  .daterange-pop {
@@ -951,7 +1030,7 @@ button.datepicker-month:hover {
951
1030
  // Match AppInput's focus: clean surface fill + a soft primary border.
952
1031
  .app-phone-number-input .phone-input-wrap:focus-within {
953
1032
  background: var(--color-surface);
954
- border-color: var(--wui-primary-30);
1033
+ border-color: var(--color-focus-border);
955
1034
  }
956
1035
  .app-phone-number-input .phone-input-wrap:has(> .input:disabled) {
957
1036
  background: var(--color-bg-muted);
@@ -1176,7 +1255,7 @@ wui-timepicker[aria-invalid="true"] .timepicker.open .timepicker-trigger {
1176
1255
  // AppInput's exact focus: clean surface fill + its soft primary border token.
1177
1256
  .dropdown-search-input:focus {
1178
1257
  background: var(--color-surface);
1179
- border-color: var(--wui-primary-30);
1258
+ border-color: var(--color-focus-border);
1180
1259
  }
1181
1260
  // AppInput's placeholder colour, not the browser default grey.
1182
1261
  .dropdown-search-input::placeholder {
@@ -1319,7 +1398,7 @@ wui-timepicker[aria-invalid="true"] .timepicker.open .timepicker-trigger {
1319
1398
  .timepicker.open .timepicker-trigger {
1320
1399
  // Stay white while the time list is open — see the note on .datepicker.open above.
1321
1400
  background-color: var(--color-surface);
1322
- border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
1401
+ border-color: var(--color-focus-border);
1323
1402
  box-shadow: none;
1324
1403
  }
1325
1404
  .timepicker-pop {
@@ -0,0 +1,195 @@
1
+ // ── Command palette ──
2
+ // AppCommandPalette / <app-command-palette> / <wui-command-palette>: app-wide "search and jump".
3
+ // The overlay is the shared modal (.wui-modal-shell, .wui-modal), with .wui-cmd-card on the
4
+ // card; this partial styles only what sits inside it, plus the top anchoring. The element marks
5
+ // the active result with aria-selected="true", which is the one state hook below. Every colour
6
+ // is a theme token, so dark mode follows the token flip with no rules of its own.
7
+ //
8
+ // Part of the wui-components layer, see _wui-components.scss for load order.
9
+
10
+ // Anchored near the top rather than centred, so the input stays put while results grow and
11
+ // shrink underneath it.
12
+ .modal.wui-modal-shell:has(.wui-cmd-card) {
13
+ align-items: flex-start;
14
+ padding-top: max(20px, min(12vh, 120px));
15
+ }
16
+ .wui-modal.wui-cmd-card {
17
+ max-height: min(560px, calc(100vh - 2 * max(20px, min(12vh, 120px))));
18
+ }
19
+ // The list scrolls, not the body: the input and the footer stay pinned.
20
+ .wui-cmd-card > .wui-modal-body {
21
+ display: flex;
22
+ flex-direction: column;
23
+ padding: 0;
24
+ overflow: hidden;
25
+ }
26
+
27
+ wui-command-palette {
28
+ display: flex;
29
+ flex: 1 1 auto;
30
+ flex-direction: column;
31
+ min-height: 0;
32
+ }
33
+
34
+ .wui-cmd-search {
35
+ display: flex;
36
+ flex: 0 0 auto;
37
+ align-items: center;
38
+ gap: 10px;
39
+ padding: 0 16px;
40
+ border-bottom: 1px solid var(--color-border);
41
+ }
42
+ .wui-cmd-search-icon {
43
+ display: inline-flex;
44
+ flex: 0 0 auto;
45
+ color: var(--color-text-muted);
46
+ }
47
+ .wui-cmd-input {
48
+ flex: 1 1 auto;
49
+ min-width: 0;
50
+ height: 52px;
51
+ padding: 0;
52
+ border: none;
53
+ outline: none;
54
+ background: transparent;
55
+ color: var(--color-text);
56
+ font-size: 16px;
57
+ }
58
+ .wui-cmd-input::placeholder {
59
+ color: var(--color-text-subtle);
60
+ }
61
+ .wui-cmd-spinner {
62
+ flex: 0 0 auto;
63
+ color: var(--color-primary);
64
+ }
65
+
66
+ .wui-cmd-list {
67
+ flex: 1 1 auto;
68
+ min-height: 0;
69
+ padding: 6px;
70
+ overflow-y: auto;
71
+ overscroll-behavior: contain;
72
+ scrollbar-width: thin;
73
+ scrollbar-color: var(--color-border-strong) transparent;
74
+ }
75
+ .wui-cmd-list[hidden],
76
+ .wui-cmd-group[hidden],
77
+ .wui-cmd-item[hidden] {
78
+ display: none;
79
+ }
80
+ .wui-cmd-group + .wui-cmd-group {
81
+ margin-top: 4px;
82
+ }
83
+ .wui-cmd-group-label {
84
+ padding: 8px 10px 4px;
85
+ color: var(--color-text-muted);
86
+ font-size: 11px;
87
+ font-weight: 600;
88
+ letter-spacing: 0.04em;
89
+ text-transform: uppercase;
90
+ }
91
+
92
+ .wui-cmd-item {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 12px;
96
+ padding: 8px 10px;
97
+ border-radius: var(--radius);
98
+ corner-shape: squircle;
99
+ color: var(--color-text);
100
+ cursor: pointer;
101
+ // Keeps a little of the next row visible when the keyboard scrolls the active one in.
102
+ scroll-margin: 6px;
103
+ }
104
+ .wui-cmd-item[aria-selected="true"] {
105
+ background: var(--color-primary-soft);
106
+ color: var(--color-primary);
107
+ }
108
+ .wui-cmd-item-icon {
109
+ display: inline-grid;
110
+ flex: 0 0 auto;
111
+ place-items: center;
112
+ width: 32px;
113
+ height: 32px;
114
+ border-radius: var(--radius);
115
+ corner-shape: squircle;
116
+ background: var(--color-bg-muted);
117
+ color: var(--color-text-muted);
118
+ }
119
+ .wui-cmd-item[aria-selected="true"] .wui-cmd-item-icon {
120
+ background: var(--color-surface);
121
+ color: var(--color-primary);
122
+ }
123
+ .wui-cmd-item-text {
124
+ display: flex;
125
+ flex: 1 1 auto;
126
+ flex-direction: column;
127
+ min-width: 0;
128
+ }
129
+ .wui-cmd-item-label,
130
+ .wui-cmd-item-desc {
131
+ overflow: hidden;
132
+ text-overflow: ellipsis;
133
+ white-space: nowrap;
134
+ line-height: 1.4;
135
+ }
136
+ .wui-cmd-item-label {
137
+ font-size: 14px;
138
+ }
139
+ .wui-cmd-item-desc {
140
+ color: var(--color-text-muted);
141
+ font-size: 12px;
142
+ }
143
+ .wui-cmd-item-meta {
144
+ flex: 0 0 auto;
145
+ color: var(--color-text-muted);
146
+ font-size: 12px;
147
+ }
148
+ // The query inside a label: bolder, never a highlighter block.
149
+ .wui-cmd-mark {
150
+ padding: 0;
151
+ background: transparent;
152
+ color: inherit;
153
+ font-weight: 700;
154
+ }
155
+
156
+ .wui-cmd-empty {
157
+ padding: 20px 16px;
158
+ color: var(--color-text-muted);
159
+ font-size: 13px;
160
+ text-align: center;
161
+ }
162
+ // The status region is always in the page, so a screen reader hears the empty text arrive;
163
+ // with nothing to say it takes no room.
164
+ .wui-cmd-empty:empty {
165
+ display: none;
166
+ }
167
+
168
+ .wui-cmd-footer {
169
+ display: flex;
170
+ flex: 0 0 auto;
171
+ flex-wrap: wrap;
172
+ align-items: center;
173
+ gap: 6px 16px;
174
+ padding: 8px 16px;
175
+ border-top: 1px solid var(--color-border);
176
+ color: var(--color-text-muted);
177
+ font-size: 12px;
178
+ }
179
+ .wui-cmd-footer kbd {
180
+ padding: 1px 6px;
181
+ border: 1px solid var(--color-border);
182
+ border-radius: var(--wui-radius-sm);
183
+ background: var(--color-bg-muted);
184
+ color: var(--color-text);
185
+ font-family: inherit;
186
+ font-size: 11px;
187
+ }
188
+
189
+ @media (prefers-reduced-motion: no-preference) {
190
+ .wui-cmd-item {
191
+ transition:
192
+ background-color 0.12s ease,
193
+ color 0.12s ease;
194
+ }
195
+ }