@webority/theme 0.13.5 → 0.14.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/README.md +48 -4
- package/dist/purge-safelist.json +805 -0
- package/dist/webority-flags.css +1 -0
- package/dist/webority-theme.css +1 -1
- package/index.scss +3 -3
- package/package.json +4 -2
- package/scss/_components.scss +6 -6
- package/scss/_tokens.scss +6 -0
- package/scss/_wui-aliases.scss +36 -0
- package/scss/_wui-components.scss +4 -14
- package/scss/wui/_choice.scss +106 -27
- package/scss/wui/_controls.scss +26 -6
- package/scss/wui/_display.scss +24 -15
- package/scss/wui/_flow.scss +10 -3
- package/scss/wui/_media.scss +6 -4
- package/scss/wui/_overlays.scss +13 -8
- package/scss/wui/_utilities.scss +213 -206
- package/scss/wui/_workspace.scss +1 -1
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
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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.
|
|
3
|
+
"version": "0.14.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
|
}
|
package/scss/_components.scss
CHANGED
|
@@ -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):
|
|
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.
|
|
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:
|
|
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.
|
|
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:
|
|
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:
|
|
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;
|
package/scss/_wui-aliases.scss
CHANGED
|
@@ -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,20 +43,10 @@
|
|
|
43
43
|
// set, so it has to win on equal specificity.
|
|
44
44
|
@import "wui/workspace";
|
|
45
45
|
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
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";
|
package/scss/wui/_choice.scss
CHANGED
|
@@ -58,18 +58,14 @@
|
|
|
58
58
|
background: var(--color-surface);
|
|
59
59
|
color: var(--color-primary);
|
|
60
60
|
}
|
|
61
|
-
//
|
|
62
|
-
// selected,
|
|
63
|
-
.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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 --
|
|
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(--
|
|
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(--
|
|
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.
|
|
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:
|
|
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:
|
|
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(--
|
|
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(--
|
|
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:
|
|
1401
|
+
border-color: var(--color-focus-border);
|
|
1323
1402
|
box-shadow: none;
|
|
1324
1403
|
}
|
|
1325
1404
|
.timepicker-pop {
|
package/scss/wui/_controls.scss
CHANGED
|
@@ -348,7 +348,7 @@ textarea.form-control.autosize {
|
|
|
348
348
|
background-color: var(--color-surface);
|
|
349
349
|
color: var(--color-text);
|
|
350
350
|
// Soft brand border is the focus cue (per the reference) — no ring/shadow.
|
|
351
|
-
border-color: var(--
|
|
351
|
+
border-color: var(--color-focus-border);
|
|
352
352
|
box-shadow: none;
|
|
353
353
|
}
|
|
354
354
|
.form-control::placeholder,
|
|
@@ -625,6 +625,14 @@ $wui-field-error-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/
|
|
|
625
625
|
color: var(--color-text);
|
|
626
626
|
-webkit-text-fill-color: var(--color-text);
|
|
627
627
|
}
|
|
628
|
+
// ...but -webkit-text-fill-color is inherited by ::placeholder and beats its `color`, so the rule
|
|
629
|
+
// above painted the placeholder as bright as a typed value. Hand the placeholder its own subtle
|
|
630
|
+
// ink back (the same --color-text-subtle every other placeholder uses).
|
|
631
|
+
:root[data-theme="dark"] .form-control.error::placeholder,
|
|
632
|
+
:root[data-theme="dark"] .input.error::placeholder {
|
|
633
|
+
color: var(--color-text-subtle);
|
|
634
|
+
-webkit-text-fill-color: var(--color-text-subtle);
|
|
635
|
+
}
|
|
628
636
|
|
|
629
637
|
// Field-relative wrap for trailing affixes (error icon, password toggle).
|
|
630
638
|
.input-wrap {
|
|
@@ -793,9 +801,10 @@ $wui-field-error-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/
|
|
|
793
801
|
}
|
|
794
802
|
.input-affixed:focus-within {
|
|
795
803
|
background: var(--color-surface);
|
|
796
|
-
border-color: var(--
|
|
804
|
+
border-color: var(--color-focus-border);
|
|
797
805
|
}
|
|
798
|
-
.input-affixed:has(.input.error)
|
|
806
|
+
.input-affixed:has(.input.error),
|
|
807
|
+
.input-affixed:has(.input[aria-invalid="true"]) {
|
|
799
808
|
background: var(--color-surface);
|
|
800
809
|
border-color: color-mix(in srgb, var(--color-danger) 30%, transparent);
|
|
801
810
|
}
|
|
@@ -806,9 +815,18 @@ $wui-field-error-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/
|
|
|
806
815
|
border-radius: 0;
|
|
807
816
|
background: transparent;
|
|
808
817
|
}
|
|
818
|
+
// A select in the affixed shell (AppSelect prefix/suffix): the custom element is the flex
|
|
819
|
+
// item, and its inner .input control already loses its own border through the rule above.
|
|
820
|
+
.input-affixed > wui-select {
|
|
821
|
+
flex: 1;
|
|
822
|
+
min-width: 0;
|
|
823
|
+
}
|
|
809
824
|
.input-affixed .affix {
|
|
810
825
|
display: flex;
|
|
811
826
|
align-items: center;
|
|
827
|
+
// A one-character affix (₹, $, #) still reads as a box, centred in it, not a sliver.
|
|
828
|
+
justify-content: center;
|
|
829
|
+
min-width: 44px;
|
|
812
830
|
padding: 0 12px;
|
|
813
831
|
// Transparent so the affix inherits the wrapper fill — the addon and the input read as
|
|
814
832
|
// one continuous surface (only the separator border divides them).
|
|
@@ -993,6 +1011,7 @@ $wui-field-error-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/
|
|
|
993
1011
|
// into a plain div, a grid cell, a table cell — it would still collapse. Being a flex
|
|
994
1012
|
// item blockifies it; nothing else does.
|
|
995
1013
|
wui-select,
|
|
1014
|
+
wui-country-select,
|
|
996
1015
|
wui-autocomplete,
|
|
997
1016
|
wui-multiselect,
|
|
998
1017
|
wui-taginput,
|
|
@@ -1010,6 +1029,7 @@ wui-select[compact] {
|
|
|
1010
1029
|
width: auto;
|
|
1011
1030
|
}
|
|
1012
1031
|
.field > wui-select,
|
|
1032
|
+
.field > wui-country-select,
|
|
1013
1033
|
.field > wui-autocomplete,
|
|
1014
1034
|
.field > wui-multiselect,
|
|
1015
1035
|
.field > wui-taginput,
|
|
@@ -1069,7 +1089,7 @@ wui-select[compact] {
|
|
|
1069
1089
|
}
|
|
1070
1090
|
.input-with-unit:focus-within {
|
|
1071
1091
|
background: var(--color-surface);
|
|
1072
|
-
border-color: var(--
|
|
1092
|
+
border-color: var(--color-focus-border);
|
|
1073
1093
|
}
|
|
1074
1094
|
.input-with-unit:has(.num-input.error) {
|
|
1075
1095
|
background: var(--color-surface);
|
|
@@ -1142,7 +1162,7 @@ wui-select[compact] {
|
|
|
1142
1162
|
}
|
|
1143
1163
|
.num-wrap:focus-within {
|
|
1144
1164
|
background: var(--color-surface);
|
|
1145
|
-
border-color: var(--
|
|
1165
|
+
border-color: var(--color-focus-border);
|
|
1146
1166
|
}
|
|
1147
1167
|
.num-wrap:has(.num-input.error),
|
|
1148
1168
|
.num-wrap:has(.num-input.is-invalid),
|
|
@@ -1512,7 +1532,7 @@ wui-otp {
|
|
|
1512
1532
|
// surface fill + soft brand border, no ring.
|
|
1513
1533
|
.otp-slot.active {
|
|
1514
1534
|
background: var(--color-surface);
|
|
1515
|
-
border-color: var(--
|
|
1535
|
+
border-color: var(--color-focus-border);
|
|
1516
1536
|
box-shadow: none;
|
|
1517
1537
|
}
|
|
1518
1538
|
.otp-field:disabled ~ .otp-slots .otp-slot {
|
package/scss/wui/_display.scss
CHANGED
|
@@ -407,15 +407,20 @@ a.info-row:hover .info-value {
|
|
|
407
407
|
// ============================================================================
|
|
408
408
|
// Segmented control — 2–4 mutually-exclusive options shown side by side.
|
|
409
409
|
// The theme toggle (light/dark/system) is the canonical use.
|
|
410
|
+
//
|
|
411
|
+
// The selected segment is an OUTLINED pill: a primary ring over a faint primary tint, with the
|
|
412
|
+
// label in primary. The tint is 8% so the label clears AA on it in both themes (light 5.6:1,
|
|
413
|
+
// dark 4.5:1); a stronger tint drops the dark label under 4.5:1.
|
|
410
414
|
// ============================================================================
|
|
411
415
|
.segmented {
|
|
416
|
+
--segmented-selected-bg: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface));
|
|
412
417
|
position: relative;
|
|
413
418
|
display: inline-flex;
|
|
414
419
|
gap: 2px;
|
|
415
420
|
padding: 3px;
|
|
416
421
|
border: 1px solid var(--color-border);
|
|
417
422
|
border-radius: 999px;
|
|
418
|
-
background: var(--color-
|
|
423
|
+
background: var(--color-surface);
|
|
419
424
|
}
|
|
420
425
|
.segmented-option {
|
|
421
426
|
position: relative;
|
|
@@ -432,7 +437,7 @@ a.info-row:hover .info-value {
|
|
|
432
437
|
font-weight: 600;
|
|
433
438
|
line-height: 1;
|
|
434
439
|
white-space: nowrap;
|
|
435
|
-
color: var(--color-text
|
|
440
|
+
color: var(--color-text);
|
|
436
441
|
cursor: pointer;
|
|
437
442
|
transition:
|
|
438
443
|
background 0.14s ease,
|
|
@@ -457,7 +462,7 @@ a.info-row:hover .info-value {
|
|
|
457
462
|
white-space: nowrap;
|
|
458
463
|
}
|
|
459
464
|
.segmented-option:hover {
|
|
460
|
-
color: var(--color-
|
|
465
|
+
color: var(--color-primary);
|
|
461
466
|
}
|
|
462
467
|
// A per-option sub-label under the main text — optional, so a segment without one keeps the
|
|
463
468
|
// plain single-line layout above untouched.
|
|
@@ -475,18 +480,18 @@ a.info-row:hover .info-value {
|
|
|
475
480
|
.segmented-option-note {
|
|
476
481
|
font-size: 10px;
|
|
477
482
|
font-weight: 400;
|
|
478
|
-
// Inherits the option's current text colour (
|
|
483
|
+
// Inherits the option's current text colour (plain, hover, or primary-on-checked) rather
|
|
479
484
|
// than a fixed token, so it always matches without a separate state per variant. No opacity
|
|
480
485
|
// muting — that dropped the sub-label below AA on an unchecked option; the 10px size already
|
|
481
486
|
// distinguishes it from the label.
|
|
482
487
|
color: currentColor;
|
|
483
488
|
}
|
|
489
|
+
// Without JS this per-option pill stands in for the sliding thumb below, so it carries the
|
|
490
|
+
// same ring and tint.
|
|
484
491
|
.segmented-option:has(.segmented-input:checked) {
|
|
485
|
-
background: var(--
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
// --wui-primary-text to a dark value and this follows automatically.
|
|
489
|
-
color: var(--color-primary-text);
|
|
492
|
+
background: var(--segmented-selected-bg);
|
|
493
|
+
box-shadow: inset 0 0 0 2px var(--color-primary);
|
|
494
|
+
color: var(--color-primary);
|
|
490
495
|
}
|
|
491
496
|
// The ring is drawn on the label because the input it belongs to has no box.
|
|
492
497
|
.segmented-option:has(.segmented-input:focus-visible) {
|
|
@@ -504,21 +509,25 @@ a.info-row:hover .info-value {
|
|
|
504
509
|
width: 0;
|
|
505
510
|
z-index: 0;
|
|
506
511
|
border-radius: 999px;
|
|
507
|
-
background: var(--
|
|
508
|
-
|
|
512
|
+
background: var(--segmented-selected-bg);
|
|
513
|
+
// An inset ring, not a border: a border would add to the width JS measures and sets.
|
|
514
|
+
box-shadow: inset 0 0 0 2px var(--color-primary);
|
|
509
515
|
opacity: 0;
|
|
510
516
|
pointer-events: none;
|
|
511
517
|
}
|
|
512
518
|
.segmented.is-animated .segmented-thumb {
|
|
513
519
|
opacity: 1;
|
|
514
|
-
// A
|
|
520
|
+
// A fast ease-out that never overshoots. A spring easing slid the thumb past its target, so
|
|
521
|
+
// at the first or last segment the ring crossed the track's edge mid-slide. Clipping the
|
|
522
|
+
// track instead is not an option: it would also clip the options' focus outline.
|
|
515
523
|
transition:
|
|
516
|
-
transform 0.
|
|
517
|
-
width 0.
|
|
524
|
+
transform 0.32s cubic-bezier(0.22, 1, 0.36, 1),
|
|
525
|
+
width 0.32s cubic-bezier(0.22, 1, 0.36, 1);
|
|
518
526
|
}
|
|
519
|
-
// While the thumb draws the
|
|
527
|
+
// While the thumb draws the pill, the checked option drops its own (keeps the primary label).
|
|
520
528
|
.segmented.is-animated .segmented-option:has(.segmented-input:checked) {
|
|
521
529
|
background: transparent;
|
|
530
|
+
box-shadow: none;
|
|
522
531
|
}
|
|
523
532
|
// Icon pop — the selected segment's glyph springs up a touch as it becomes active.
|
|
524
533
|
.segmented-option:has(.segmented-input:checked) .wui-icon {
|
package/scss/wui/_flow.scss
CHANGED
|
@@ -260,11 +260,18 @@ button.step:focus-visible .step-circle {
|
|
|
260
260
|
overflow: hidden;
|
|
261
261
|
text-overflow: ellipsis;
|
|
262
262
|
}
|
|
263
|
+
// Where connectors are drawn, a step takes only its content's width (and still shrinks to
|
|
264
|
+
// ellipsize its text) and the connector takes the rest, so the rail joins each step to the
|
|
265
|
+
// next. A journey bar without connectors keeps its evenly spread steps.
|
|
266
|
+
.stepper-journey:has(> .step-line) > .step {
|
|
267
|
+
flex: 0 1 auto;
|
|
268
|
+
}
|
|
263
269
|
.stepper-journey .step-line {
|
|
264
|
-
flex:
|
|
265
|
-
width:
|
|
270
|
+
flex: 1 1 32px;
|
|
271
|
+
min-width: 24px;
|
|
266
272
|
height: 2px;
|
|
267
|
-
margin: 0
|
|
273
|
+
margin: 0 16px;
|
|
274
|
+
border-radius: 999px;
|
|
268
275
|
background: var(--color-border-strong);
|
|
269
276
|
}
|
|
270
277
|
.stepper-journey .step-line.done {
|