@webority/theme 0.7.3 → 0.7.5

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.
@@ -318,7 +318,7 @@
318
318
  // scrollable modal / table card. Sits above the modal layer.
319
319
  .search-select-menu-fixed {
320
320
  position: fixed;
321
- z-index: 1600;
321
+ z-index: var(--wui-z-menu);
322
322
  right: auto;
323
323
  top: auto;
324
324
  left: auto;
@@ -483,7 +483,7 @@
483
483
  // sits above the modal layer.
484
484
  .datepicker-pop {
485
485
  position: fixed;
486
- z-index: 1600;
486
+ z-index: var(--wui-z-menu);
487
487
  width: 312px;
488
488
  max-width: calc(100vw - 16px);
489
489
  padding: 18px;
@@ -801,7 +801,7 @@ button.datepicker-month:hover {
801
801
  }
802
802
  .daterange-pop {
803
803
  position: fixed;
804
- z-index: 1600;
804
+ z-index: var(--wui-z-menu);
805
805
  width: 620px;
806
806
  max-width: calc(100vw - 16px);
807
807
  padding: 18px;
@@ -998,7 +998,7 @@ button.datepicker-month:hover {
998
998
  // modal layer, matching .search-select-menu-fixed and .datepicker-pop.
999
999
  .country-dropdown-menu {
1000
1000
  position: fixed;
1001
- z-index: 1600;
1001
+ z-index: var(--wui-z-menu);
1002
1002
  width: 320px;
1003
1003
  max-width: calc(100vw - 16px);
1004
1004
  padding: 4px;
@@ -6,191 +6,56 @@
6
6
  // partials are a split of one stylesheet, not independent modules, so a rule
7
7
  // here may be overridden by a later partial. Never reorder the imports.
8
8
 
9
- // ── Checkbox & Radio ──
10
- // Selection controls: a 20px glyph centred in a 36px circular hit area
11
- // that shows a soft primary-tinted ripple on hover. Unchecked = muted
12
- // grey-blue outline; checked = solid primary. Checkbox = squircle box + white
13
- // tick; radio = ring + filled centre dot. Pure CSS, no JS — drives the native
14
- // input via `appearance: none`, so it stays keyboard- and screen-reader-
15
- // accessible.
9
+ // ── Checkbox / radio / switch ──
10
+ // Bootstrap `.form-check` + `.form-check-input` are the public selection skin
11
+ // (AppCheckbox / AppRadio / AppSwitch). `.wui-check` / `.wui-radio` are aliases
12
+ // for the same glyph (AppDataTable select column + legacy `.wui-control`
13
+ // markup) — never a second look. One 20px brand glyph language.
16
14
  //
17
- // Markup:
18
- // <label class="wui-control">
19
- // <input type="checkbox" class="wui-check"> (or type="radio" class="wui-radio")
20
- // <span class="wui-control-label">Full-time</span>
21
- // </label>
22
- // Group rows in a `.wui-control-group` (horizontal, wraps) or stack them.
15
+ // Layout wrappers:
16
+ // .form-check / .form-check-label — App* path
17
+ // .wui-control / .wui-control-label — optional compact label wrap (alias)
18
+
19
+ .form-check,
23
20
  .wui-control {
24
- display: inline-flex;
21
+ display: flex;
25
22
  align-items: center;
26
- gap: 6px;
23
+ gap: 8px;
24
+ min-height: 0;
25
+ margin-bottom: 0;
26
+ padding-left: 0;
27
27
  cursor: pointer;
28
28
  user-select: none;
29
29
  font-size: 14px;
30
30
  color: var(--color-text);
31
31
  line-height: 1.4;
32
32
  }
33
+ .wui-control {
34
+ display: inline-flex;
35
+ }
36
+ .form-check-inline {
37
+ display: inline-flex;
38
+ margin-right: 16px;
39
+ }
33
40
  .wui-control-group {
34
41
  display: flex;
35
42
  flex-wrap: wrap;
36
43
  align-items: center;
37
44
  gap: 8px 20px;
38
45
  }
46
+ .form-check-label,
39
47
  .wui-control-label {
40
- padding-right: 2px;
48
+ cursor: pointer;
49
+ font-size: 14px;
50
+ line-height: 1.4;
51
+ color: var(--color-text);
52
+ user-select: none;
41
53
  }
42
54
 
43
- // The control glyph: 20px box inside a 36px circular tap target (8px ring).
55
+ // Glyph — shared by BS form-check-input and the wui-check alias.
56
+ .form-check-input,
44
57
  .wui-check,
45
58
  .wui-radio {
46
- appearance: none;
47
- -webkit-appearance: none;
48
- flex: none;
49
- box-sizing: border-box;
50
- width: 20px;
51
- height: 20px;
52
- // 8px transparent ring → 36px hit area + room for the hover ripple
53
- margin: 8px;
54
- position: relative;
55
- border: 1.6px solid var(--color-border-strong);
56
- background: transparent;
57
- cursor: pointer;
58
- transition:
59
- border-color 0.15s ease,
60
- background-color 0.15s ease;
61
- }
62
- .wui-check {
63
- border-radius: var(--radius-sm);
64
- corner-shape: squircle;
65
- }
66
- .wui-radio {
67
- border-radius: 50%;
68
- }
69
-
70
- // Soft primary circular ripple behind the glyph on hover (the Minimals halo).
71
- .wui-check::after,
72
- .wui-radio::after {
73
- content: "";
74
- position: absolute;
75
- left: 50%;
76
- top: 50%;
77
- width: 36px;
78
- height: 36px;
79
- border-radius: 50%;
80
- transform: translate(-50%, -50%) scale(0.5);
81
- background: var(--color-primary-soft);
82
- opacity: 0;
83
- z-index: -1;
84
- pointer-events: none;
85
- transition:
86
- opacity 0.15s ease,
87
- transform 0.15s ease;
88
- }
89
- .wui-control:hover .wui-check::after,
90
- .wui-control:hover .wui-radio::after,
91
- .wui-check:hover::after,
92
- .wui-radio:hover::after {
93
- opacity: 1;
94
- transform: translate(-50%, -50%) scale(1);
95
- }
96
-
97
- // Checked — primary fill (checkbox) / primary ring + dot (radio)
98
- .wui-check:checked {
99
- border-color: var(--color-primary);
100
- background: var(--color-primary);
101
- // white tick drawn with a rotated box-shadow-free pseudo
102
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7'/%3E%3C/svg%3E");
103
- background-size: 15px 15px;
104
- background-position: center;
105
- background-repeat: no-repeat;
106
- }
107
- .wui-radio:checked {
108
- border-color: var(--color-primary);
109
- }
110
- .wui-radio:checked::before {
111
- content: "";
112
- position: absolute;
113
- left: 50%;
114
- top: 50%;
115
- width: 10px;
116
- height: 10px;
117
- border-radius: 50%;
118
- background: var(--color-primary);
119
- transform: translate(-50%, -50%);
120
- }
121
-
122
- // Keyboard focus ring (only on focus-visible, not mouse)
123
- .wui-check:focus-visible,
124
- .wui-radio:focus-visible {
125
- outline: none;
126
- box-shadow: 0 0 0 3px var(--color-primary-soft);
127
- }
128
-
129
- // Disabled
130
- .wui-check:disabled,
131
- .wui-radio:disabled,
132
- .wui-control:has(:disabled) {
133
- cursor: not-allowed;
134
- }
135
- .wui-check:disabled,
136
- .wui-radio:disabled {
137
- border-color: var(--color-border);
138
- background-color: var(--color-bg-muted);
139
- }
140
- .wui-check:disabled:checked,
141
- .wui-radio:disabled:checked {
142
- border-color: var(--color-border-strong);
143
- }
144
- .wui-check:disabled:checked {
145
- background-color: var(--color-border-strong);
146
- }
147
- .wui-radio:disabled:checked::before {
148
- background: var(--color-border-strong);
149
- }
150
- .wui-control:has(:disabled) {
151
- color: var(--color-text-subtle);
152
- }
153
- .wui-check:disabled::after,
154
- .wui-radio:disabled::after {
155
- display: none;
156
- }
157
-
158
- @media (prefers-reduced-motion: reduce) {
159
- .wui-check,
160
- .wui-radio,
161
- .wui-check::after,
162
- .wui-radio::after {
163
- transition: none;
164
- }
165
- }
166
-
167
- // Row-selection checkbox variant used inside a .table-ledger select column.
168
- .wui-check {
169
- width: 16px;
170
- height: 16px;
171
- accent-color: var(--color-primary);
172
- cursor: pointer;
173
- vertical-align: middle;
174
- }
175
-
176
- // ── Premium checkbox / radio / switch ──
177
- // AppCheckbox / AppRadio / AppSwitch render Bootstrap's .form-check markup, whose default
178
- // input is a plain 16px box. This restyles it to the Webority selection-control language
179
- // (see .wui-check above): a 20px squircle box, brand fill, a crisp white tick, a soft primary
180
- // hover ring + focus ring, and a themed switch. One block upgrades all three, both surfaces.
181
- .form-check {
182
- display: flex;
183
- align-items: center;
184
- gap: 8px;
185
- min-height: 0;
186
- margin-bottom: 0;
187
- padding-left: 0;
188
- }
189
- .form-check-inline {
190
- display: inline-flex;
191
- margin-right: 16px;
192
- }
193
- .form-check-input {
194
59
  box-sizing: border-box;
195
60
  flex: none;
196
61
  float: none;
@@ -211,65 +76,82 @@
211
76
  box-shadow 0.15s ease,
212
77
  background-position 0.15s ease;
213
78
  }
214
- // Bootstrap floats the input into the label's left padding via `.form-check .form-check-input`
215
- // (specificity 0,2,0 — it outranks the rule above), which drags the box a negative margin
216
- // left, out of the card. Our flex row places it directly; undo the float and negative margin
217
- // at matching specificity.
79
+ // Bootstrap floats the input into the label's left padding via
80
+ // `.form-check .form-check-input` (0,2,0). Undo at matching specificity.
218
81
  .form-check .form-check-input {
219
82
  float: none;
220
83
  margin: 0;
221
84
  }
222
- .form-check-input[type="checkbox"] {
85
+ .form-check-input[type="checkbox"],
86
+ .wui-check {
223
87
  border-radius: var(--radius-sm);
224
88
  corner-shape: squircle;
225
89
  }
226
- .form-check-input[type="radio"] {
90
+ .form-check-input[type="radio"],
91
+ .wui-radio {
227
92
  border-radius: 50%;
228
93
  }
229
- .form-check-input:hover:not(:disabled):not(:checked) {
94
+ .form-check-input:hover:not(:disabled):not(:checked),
95
+ .wui-check:hover:not(:disabled):not(:checked),
96
+ .wui-radio:hover:not(:disabled):not(:checked) {
230
97
  border-color: var(--color-primary);
231
98
  box-shadow: 0 0 0 4px var(--color-primary-soft);
232
99
  }
233
- .form-check-input:focus-visible {
100
+ .form-check-input:focus-visible,
101
+ .wui-check:focus-visible,
102
+ .wui-radio:focus-visible {
234
103
  outline: none;
235
104
  border-color: var(--color-primary);
236
105
  box-shadow: 0 0 0 3px var(--color-primary-soft);
237
106
  }
238
- .form-check-input:checked {
107
+ .form-check-input:checked,
108
+ .wui-check:checked,
109
+ .wui-radio:checked {
239
110
  border-color: var(--color-primary);
240
111
  background-color: var(--color-primary);
241
112
  }
242
- .form-check-input[type="checkbox"]:checked {
113
+ .form-check-input[type="checkbox"]:checked,
114
+ .wui-check:checked {
243
115
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7'/%3E%3C/svg%3E");
244
116
  }
245
- .form-check-input[type="checkbox"]:indeterminate {
117
+ .form-check-input[type="checkbox"]:indeterminate,
118
+ .wui-check:indeterminate {
246
119
  border-color: var(--color-primary);
247
120
  background-color: var(--color-primary);
248
121
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.4' stroke-linecap='round'%3E%3Cpath d='M6 12h12'/%3E%3C/svg%3E");
249
122
  }
250
- .form-check-input[type="radio"]:checked {
123
+ .form-check-input[type="radio"]:checked,
124
+ .wui-radio:checked {
251
125
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='5.5' fill='white'/%3E%3C/svg%3E");
252
126
  background-size: 20px 20px;
253
127
  }
254
- .form-check-input:disabled {
128
+ .form-check-input:disabled,
129
+ .wui-check:disabled,
130
+ .wui-radio:disabled {
255
131
  cursor: not-allowed;
256
132
  opacity: 0.55;
257
133
  background-color: var(--color-bg-muted);
258
134
  border-color: var(--color-border);
259
135
  box-shadow: none;
260
136
  }
261
- .form-check-label {
262
- cursor: pointer;
263
- font-size: 14px;
264
- line-height: 1.4;
265
- color: var(--color-text);
266
- user-select: none;
267
- }
268
- .form-check-input:disabled ~ .form-check-label {
137
+ .form-check-input:disabled ~ .form-check-label,
138
+ .wui-control:has(:disabled) {
269
139
  cursor: not-allowed;
270
140
  color: var(--color-text-subtle);
271
141
  }
272
- // Switch — a pill track with a sliding white knob.
142
+
143
+ // Compact row-select — ONLY inside the select column (AppDataTable / Razor
144
+ // data-table → `.wui-select-cell`). Scope is load-bearing.
145
+ .wui-select-cell .wui-check,
146
+ .wui-select-cell .form-check-input {
147
+ width: 16px;
148
+ height: 16px;
149
+ margin: 0;
150
+ vertical-align: middle;
151
+ background-size: 12px 12px;
152
+ }
153
+
154
+ // Switch — pill track + sliding knob (Bootstrap .form-switch only).
273
155
  .form-switch {
274
156
  padding-left: 0;
275
157
  }
@@ -291,19 +173,28 @@
291
173
  border-color: transparent;
292
174
  background-color: var(--color-primary);
293
175
  background-position: right center;
294
- // Re-assert the knob — the checkbox :checked tick has equal specificity and would
295
- // otherwise win, turning the switch into a ticked box.
176
+ // Re-assert the knob — the checkbox :checked tick has equal specificity.
296
177
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='10' cy='10' r='7' fill='white'/%3E%3C/svg%3E");
297
178
  }
298
179
  @media (prefers-reduced-motion: reduce) {
299
- .form-check-input {
180
+ .form-check-input,
181
+ .wui-check,
182
+ .wui-radio {
300
183
  transition: none;
301
184
  }
302
185
  }
303
186
 
304
187
  // ── Field / Input ──
305
- // Inputs — always-white fill, 16px squircle, soft brand border tint on focus
306
- // (no glow).
188
+ // Bootstrap `.form-control` / `.form-select` are the field skin. `.input` /
189
+ // `.select` / `.textarea` are aliases (and App* size/shell hooks) that MUST
190
+ // share the same chrome — never a second look. The only WUI
191
+ // deltas on top of Bootstrap's $input-* vars: shared control height, hover
192
+ // fill, brand-border focus (no glow), soft invalid border.
193
+ //
194
+ // Keep shell utilities (.input-wrap, .input-icon, .input-sm, …) — those are
195
+ // structure, not a parallel visual system.
196
+ .form-control,
197
+ .form-select,
307
198
  .input,
308
199
  .select,
309
200
  .textarea {
@@ -322,75 +213,87 @@
322
213
  box-shadow 0.12s;
323
214
  font-family: inherit;
324
215
  }
325
- // Size steps — sm/lg opt out of the shared 44px control height for dense toolbars or
326
- // marketing-scale fields. md is the base rule above; AppInput adds .input-sm / .input-lg.
216
+ // Size steps — sm/lg opt out of the shared 44px control height. App* emits
217
+ // .input-sm/.input-lg; Bootstrap's form-control-sm/lg are included so a raw
218
+ // BS field can match without the .input alias.
219
+ .form-control-sm,
327
220
  .input-sm {
328
221
  height: var(--wui-control-height-sm);
329
222
  padding: 6px 10px;
330
223
  font-size: 13px;
331
224
  }
225
+ .form-control-lg,
332
226
  .input-lg {
333
227
  height: var(--wui-control-height-lg);
334
228
  padding: 14px 16px;
335
229
  font-size: 16px;
336
230
  }
337
- .textarea {
231
+ .textarea,
232
+ textarea.form-control {
338
233
  height: auto;
339
234
  min-height: 120px;
340
235
  padding: 12px 15px 16px;
341
236
  line-height: 1.5;
342
237
  resize: vertical;
343
238
  }
344
- // Auto-grow — JS drives the height to fit the content (Razor: data-app-autosize wired in
345
- // webority-ui.js; React: the autoGrow prop). The manual grabber and the scrollbar are off
346
- // so they can't fight the measured height.
347
- .textarea.autosize {
239
+ // Auto-grow — JS drives the height to fit the content (Razor: data-app-autosize;
240
+ // React: autoGrow). Manual grabber and scrollbar off so they can't fight measure.
241
+ .textarea.autosize,
242
+ textarea.form-control.autosize {
348
243
  resize: none;
349
244
  overflow: hidden;
350
245
  }
351
- // The `.error` / `.is-invalid` / `[aria-invalid="true"]` guards keep an invalid field's
352
- // danger border visible on hover — without them the hover rule repaints it grey and the
353
- // error signal disappears the moment the cursor passes over the field.
354
- .input:hover:not(:focus):not(:disabled):not([aria-invalid="true"]):not(.is-invalid):not(.error),
246
+ // Hover/error guards: without them the hover fill repaints over an invalid border.
247
+ .form-control:hover:not(:focus):not(:disabled):not([readonly]):not([aria-invalid="true"]):not(.is-invalid):not(.error),
248
+ .form-select:hover:not(:focus):not(:disabled):not([aria-invalid="true"]):not(.is-invalid):not(.error),
249
+ .input:hover:not(:focus):not(:disabled):not([readonly]):not([aria-invalid="true"]):not(.is-invalid):not(.error),
355
250
  .select:hover:not(:focus):not(:disabled):not([aria-invalid="true"]):not(.is-invalid):not(.error),
356
- .textarea:hover:not(:focus):not(:disabled):not([aria-invalid="true"]):not(.is-invalid):not(.error) {
251
+ .textarea:hover:not(:focus):not(:disabled):not([readonly]):not([aria-invalid="true"]):not(.is-invalid):not(.error) {
357
252
  background-color: var(--color-bg-muted);
358
253
  border-color: var(--color-border-strong);
359
254
  }
255
+ .form-control:focus,
256
+ .form-select:focus,
360
257
  .input:focus,
361
258
  .select:focus,
362
259
  .textarea:focus {
363
260
  outline: none;
364
261
  background-color: var(--color-bg-muted);
365
262
  color: var(--color-text);
366
- // Full-strength brand border is the focus cue (no glow) — the prior 30% tint read at
367
- // ~1.5:1, below WCAG 2.2 SC 1.4.11's 3:1 for a non-text focus indicator.
368
- border-color: var(--color-primary);
369
- box-shadow: none;
370
- }
371
- // Textarea renders as .form-control.textarea (react-bootstrap). Match the
372
- // shared theme's .form-control:hover specificity so it hovers to
373
- // --color-border-strong too.
374
- textarea.textarea.form-control:hover:not(:focus):not(:disabled):not([readonly]) {
375
- border-color: var(--color-border-strong);
376
- }
377
- textarea.textarea:focus {
378
- outline: none;
379
- // Full-strength brand border is the focus cue (no glow) — the prior 30% tint read at
380
- // ~1.5:1, below WCAG 2.2 SC 1.4.11's 3:1 for a non-text focus indicator.
263
+ // Full-strength brand border is the focus cue (no glow) — WCAG 2.2 SC 1.4.11.
381
264
  border-color: var(--color-primary);
382
265
  box-shadow: none;
383
266
  }
267
+ .form-control::placeholder,
384
268
  .input::placeholder,
385
269
  .textarea::placeholder {
386
270
  color: var(--color-text-subtle);
387
271
  }
388
- // Edge/IE add a native password reveal (and clear) button — hide them so only
389
- // the app's own eye toggle shows.
272
+ // Borderless variant — no border/background, inherits the container's chrome
273
+ // (inline-rename, command-palette fields where the bordered shell is wrong).
274
+ .input-bare,
275
+ .input-bare:hover,
276
+ .input-bare:focus {
277
+ border-color: transparent;
278
+ background: transparent;
279
+ box-shadow: none;
280
+ }
281
+ .input-bare {
282
+ padding-left: 0;
283
+ padding-right: 0;
284
+ }
285
+ .input-bare:focus {
286
+ outline: none;
287
+ }
288
+ // Edge/IE native password reveal — hide so only the app eye toggle shows.
289
+ .form-control::-ms-reveal,
290
+ .form-control::-ms-clear,
390
291
  .input::-ms-reveal,
391
292
  .input::-ms-clear {
392
293
  display: none;
393
294
  }
295
+ .form-control:disabled,
296
+ .form-select:disabled,
394
297
  .input:disabled,
395
298
  .select:disabled,
396
299
  .textarea:disabled {
@@ -399,17 +302,16 @@ textarea.textarea:focus {
399
302
  color: var(--color-text-subtle);
400
303
  cursor: not-allowed;
401
304
  }
305
+ .field:has(.form-control:disabled) .label,
402
306
  .field:has(.input:disabled) .label {
403
307
  color: var(--color-text-subtle);
404
308
  }
405
309
 
406
- // Read-only is NOT disabled, and the two must not look alike: a read-only value is
407
- // still submitted, still selectable and still copyable, so a `not-allowed` cursor
408
- // would lie about it. Only the surface says "not yours to edit".
409
- //
410
- // Match on `[readonly]`, never the `:read-only` PSEUDO-class — that also matches a
411
- // DISABLED input (and any non-editable element), so a `:read-only` rule silently
412
- // repaints disabled fields too.
310
+ // Read-only is NOT disabled — still submitted/selectable/copyable, so no
311
+ // not-allowed cursor. Match `[readonly]`, never `:read-only` (that also hits
312
+ // disabled and non-editable controls).
313
+ .form-control[readonly],
314
+ .form-select[readonly],
413
315
  .input[readonly],
414
316
  .select[readonly],
415
317
  .textarea[readonly] {
@@ -418,26 +320,44 @@ textarea.textarea:focus {
418
320
  cursor: default;
419
321
  }
420
322
 
421
- // Invalid field state (required-field validation) — soft danger-tinted
422
- // border on the surface.
423
- // `[aria-invalid="true"]` is styled alongside `.is-invalid`: React sets the ARIA
424
- // attribute (it is the accessible source of truth) and does not always add the
425
- // Bootstrap class, so keying on the class alone left React error states unstyled.
323
+ // Invalid — soft danger border (one recipe for aria-invalid, .is-invalid, .error).
324
+ // Soft rather than solid --bs-danger so the field still reads as a surface, not a
325
+ // hard alarm box; text errors use --color-danger-text for AA.
326
+ .form-control.is-invalid,
327
+ .form-select.is-invalid,
426
328
  .input.is-invalid,
427
329
  .select.is-invalid,
428
330
  .textarea.is-invalid,
331
+ .form-control.is-invalid:focus,
332
+ .form-select.is-invalid:focus,
429
333
  .input.is-invalid:focus,
430
334
  .select.is-invalid:focus,
431
335
  .textarea.is-invalid:focus,
336
+ .form-control[aria-invalid="true"],
337
+ .form-select[aria-invalid="true"],
432
338
  .input[aria-invalid="true"],
433
339
  .select[aria-invalid="true"],
434
340
  .textarea[aria-invalid="true"],
341
+ .form-control[aria-invalid="true"]:focus,
342
+ .form-select[aria-invalid="true"]:focus,
435
343
  .input[aria-invalid="true"]:focus,
436
344
  .select[aria-invalid="true"]:focus,
437
- .textarea[aria-invalid="true"]:focus {
345
+ .textarea[aria-invalid="true"]:focus,
346
+ .form-control.error,
347
+ .input.error,
348
+ .textarea.error,
349
+ .form-control.error:focus,
350
+ .input.error:focus,
351
+ .textarea.error:focus {
438
352
  background: var(--color-surface);
439
353
  border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-surface));
440
354
  box-shadow: none;
355
+ // Kill Bootstrap's default invalid SVG + extra padding when .is-invalid is used.
356
+ background-image: none;
357
+ }
358
+ .form-control.is-invalid,
359
+ .form-select.is-invalid {
360
+ padding-right: 0.875rem;
441
361
  }
442
362
  .field-error {
443
363
  font-size: 12px;
@@ -544,20 +464,12 @@ textarea.textarea:focus {
544
464
  pointer-events: none;
545
465
  }
546
466
 
547
- // Error state — danger-tinted border, plus a danger glyph at the trailing edge.
548
- .input.error,
549
- .input.error:focus,
550
- .textarea.error,
551
- .textarea.error:focus {
552
- background: var(--color-surface);
553
- border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-surface));
554
- box-shadow: none;
555
- }
556
-
557
- // The trailing error glyph. Scoped with the child combinator to a PLAIN field only:
558
- // a shell-wrapped input (leading icon / unit / password / trailing action) is nested
559
- // one level deeper, so it keeps its own right edge and never collides with the glyph.
560
- // Covers both error paths — the `.error` class and `[aria-invalid]` / `.is-invalid`.
467
+ // Trailing error glyph on a PLAIN field only (child combinator). Shell-wrapped
468
+ // inputs sit one level deeper and keep their own right edge.
469
+ // Note: background-image here re-applies after the shared invalid rule zeroed BS's SVG.
470
+ .field > .form-control.error,
471
+ .field > .form-control.is-invalid,
472
+ .field > .form-control[aria-invalid="true"],
561
473
  .field > .input.error,
562
474
  .field > .input.is-invalid,
563
475
  .field > .input[aria-invalid="true"] {
@@ -568,8 +480,12 @@ textarea.textarea:focus {
568
480
  background-size: 18px;
569
481
  }
570
482
  // Dark mode: keep the value in an errored field legible.
483
+ :root[data-theme="dark"] .form-control.error,
484
+ :root[data-theme="dark"] .form-control.error:focus,
571
485
  :root[data-theme="dark"] .input.error,
572
486
  :root[data-theme="dark"] .input.error:focus,
487
+ :root[data-theme="dark"] .form-control.error:-webkit-autofill,
488
+ :root[data-theme="dark"] .form-control.error:focus:-webkit-autofill,
573
489
  :root[data-theme="dark"] .input.error:-webkit-autofill,
574
490
  :root[data-theme="dark"] .input.error:focus:-webkit-autofill {
575
491
  color: var(--color-text);
@@ -682,7 +598,7 @@ textarea.textarea:focus {
682
598
  }
683
599
  @keyframes wui-clear-in {
684
600
  from { opacity: 0; transform: translateY(-50%) scale(0.72); }
685
- to { opacity: 1; transform: translateY(-50%) scale(1); }
601
+ to { opacity: 1; transform: translateY(-50%) scale(1); }
686
602
  }
687
603
 
688
604
  // Prefix / suffix addon — a bordered box before/after the input (₹, https://, .com). The
@@ -1043,8 +959,8 @@ wui-select[compact] {
1043
959
  //
1044
960
  // Two other vocabularies existed and are accepted as DEPRECATED ALIASES so a
1045
961
  // consumer can migrate without a flag day:
1046
- // - colour-named .tone-green / -amber / -red / -blue / -grey (was the library's)
1047
- // - short semantic .tone-ok / -warn / -bad (a short-semantic set)
962
+ // - colour-named .tone-green / -amber / -red / -blue / -grey (was the library's)
963
+ // - short semantic .tone-ok / -warn / -bad (a short-semantic set)
1048
964
  // Author new markup with the semantic names; the aliases will be removed.
1049
965
  .status-label.success,
1050
966
  .status-label.tone-ok,