@webority/theme 0.2.0 → 0.4.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 +18 -29
- package/dist/webority-theme.css +1 -1
- package/package.json +2 -2
- package/scss/_components.scss +42 -0
- package/scss/_variables.scss +17 -0
- package/scss/_wui-components.scss +20 -3648
- package/scss/wui/_card.scss +37 -0
- package/scss/wui/_choice.scss +746 -0
- package/scss/wui/_controls.scss +808 -0
- package/scss/wui/_data.scss +614 -0
- package/scss/wui/_disclosure.scss +113 -0
- package/scss/wui/_display.scss +508 -0
- package/scss/wui/_flow.scss +410 -0
- package/scss/wui/_media.scss +569 -0
- package/scss/wui/_navigation.scss +284 -0
- package/scss/wui/_overlays.scss +350 -0
- package/scss/wui/_shell.scss +474 -0
- package/scss/wui/_typography.scss +275 -0
- package/scss/wui/_utilities.scss +340 -0
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
// Checkbox & radio, field/input, status badge, choice chip.
|
|
2
|
+
//
|
|
3
|
+
// Part of the wui-components layer — see _wui-components.scss for the whole
|
|
4
|
+
// picture and the load order. Order within the layer is LOAD-BEARING: these
|
|
5
|
+
// partials are a split of one stylesheet, not independent modules, so a rule
|
|
6
|
+
// here may be overridden by a later partial. Never reorder the imports.
|
|
7
|
+
|
|
8
|
+
// ── Checkbox & Radio ──
|
|
9
|
+
// Selection controls: a 20px glyph centred in a 36px circular hit area
|
|
10
|
+
// that shows a soft primary-tinted ripple on hover. Unchecked = muted
|
|
11
|
+
// grey-blue outline; checked = solid primary. Checkbox = squircle box + white
|
|
12
|
+
// tick; radio = ring + filled centre dot. Pure CSS, no JS — drives the native
|
|
13
|
+
// input via `appearance: none`, so it stays keyboard- and screen-reader-
|
|
14
|
+
// accessible.
|
|
15
|
+
//
|
|
16
|
+
// Markup:
|
|
17
|
+
// <label class="lx-control">
|
|
18
|
+
// <input type="checkbox" class="lx-check"> (or type="radio" class="lx-radio")
|
|
19
|
+
// <span class="lx-control-label">Full-time</span>
|
|
20
|
+
// </label>
|
|
21
|
+
// Group rows in a `.lx-control-group` (horizontal, wraps) or stack them.
|
|
22
|
+
.lx-control {
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
gap: 6px;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
user-select: none;
|
|
28
|
+
font-size: 14px;
|
|
29
|
+
color: var(--color-text);
|
|
30
|
+
line-height: 1.4;
|
|
31
|
+
}
|
|
32
|
+
.lx-control-group {
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-wrap: wrap;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 8px 20px;
|
|
37
|
+
}
|
|
38
|
+
.lx-control-label {
|
|
39
|
+
padding-right: 2px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The control glyph: 20px box inside a 36px circular tap target (8px ring).
|
|
43
|
+
.lx-check,
|
|
44
|
+
.lx-radio {
|
|
45
|
+
appearance: none;
|
|
46
|
+
-webkit-appearance: none;
|
|
47
|
+
flex: none;
|
|
48
|
+
box-sizing: border-box;
|
|
49
|
+
width: 20px;
|
|
50
|
+
height: 20px;
|
|
51
|
+
// 8px transparent ring → 36px hit area + room for the hover ripple
|
|
52
|
+
margin: 8px;
|
|
53
|
+
position: relative;
|
|
54
|
+
border: 1.6px solid var(--color-border-strong);
|
|
55
|
+
background: transparent;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
transition:
|
|
58
|
+
border-color 0.15s ease,
|
|
59
|
+
background-color 0.15s ease;
|
|
60
|
+
}
|
|
61
|
+
.lx-check {
|
|
62
|
+
border-radius: var(--radius-sm);
|
|
63
|
+
corner-shape: squircle;
|
|
64
|
+
}
|
|
65
|
+
.lx-radio {
|
|
66
|
+
border-radius: 50%;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Soft primary circular ripple behind the glyph on hover (the Minimals halo).
|
|
70
|
+
.lx-check::after,
|
|
71
|
+
.lx-radio::after {
|
|
72
|
+
content: "";
|
|
73
|
+
position: absolute;
|
|
74
|
+
left: 50%;
|
|
75
|
+
top: 50%;
|
|
76
|
+
width: 36px;
|
|
77
|
+
height: 36px;
|
|
78
|
+
border-radius: 50%;
|
|
79
|
+
transform: translate(-50%, -50%) scale(0.5);
|
|
80
|
+
background: var(--color-primary-soft);
|
|
81
|
+
opacity: 0;
|
|
82
|
+
z-index: -1;
|
|
83
|
+
pointer-events: none;
|
|
84
|
+
transition:
|
|
85
|
+
opacity 0.15s ease,
|
|
86
|
+
transform 0.15s ease;
|
|
87
|
+
}
|
|
88
|
+
.lx-control:hover .lx-check::after,
|
|
89
|
+
.lx-control:hover .lx-radio::after,
|
|
90
|
+
.lx-check:hover::after,
|
|
91
|
+
.lx-radio:hover::after {
|
|
92
|
+
opacity: 1;
|
|
93
|
+
transform: translate(-50%, -50%) scale(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Checked — primary fill (checkbox) / primary ring + dot (radio)
|
|
97
|
+
.lx-check:checked {
|
|
98
|
+
border-color: var(--color-primary);
|
|
99
|
+
background: var(--color-primary);
|
|
100
|
+
// white tick drawn with a rotated box-shadow-free pseudo
|
|
101
|
+
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");
|
|
102
|
+
background-size: 15px 15px;
|
|
103
|
+
background-position: center;
|
|
104
|
+
background-repeat: no-repeat;
|
|
105
|
+
}
|
|
106
|
+
.lx-radio:checked {
|
|
107
|
+
border-color: var(--color-primary);
|
|
108
|
+
}
|
|
109
|
+
.lx-radio:checked::before {
|
|
110
|
+
content: "";
|
|
111
|
+
position: absolute;
|
|
112
|
+
left: 50%;
|
|
113
|
+
top: 50%;
|
|
114
|
+
width: 10px;
|
|
115
|
+
height: 10px;
|
|
116
|
+
border-radius: 50%;
|
|
117
|
+
background: var(--color-primary);
|
|
118
|
+
transform: translate(-50%, -50%);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Keyboard focus ring (only on focus-visible, not mouse)
|
|
122
|
+
.lx-check:focus-visible,
|
|
123
|
+
.lx-radio:focus-visible {
|
|
124
|
+
outline: none;
|
|
125
|
+
box-shadow: 0 0 0 3px var(--color-primary-soft);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Disabled
|
|
129
|
+
.lx-check:disabled,
|
|
130
|
+
.lx-radio:disabled,
|
|
131
|
+
.lx-control:has(:disabled) {
|
|
132
|
+
cursor: not-allowed;
|
|
133
|
+
}
|
|
134
|
+
.lx-check:disabled,
|
|
135
|
+
.lx-radio:disabled {
|
|
136
|
+
border-color: var(--color-border);
|
|
137
|
+
background-color: var(--color-bg-muted);
|
|
138
|
+
}
|
|
139
|
+
.lx-check:disabled:checked,
|
|
140
|
+
.lx-radio:disabled:checked {
|
|
141
|
+
border-color: var(--color-border-strong);
|
|
142
|
+
}
|
|
143
|
+
.lx-check:disabled:checked {
|
|
144
|
+
background-color: var(--color-border-strong);
|
|
145
|
+
}
|
|
146
|
+
.lx-radio:disabled:checked::before {
|
|
147
|
+
background: var(--color-border-strong);
|
|
148
|
+
}
|
|
149
|
+
.lx-control:has(:disabled) {
|
|
150
|
+
color: var(--color-text-subtle);
|
|
151
|
+
}
|
|
152
|
+
.lx-check:disabled::after,
|
|
153
|
+
.lx-radio:disabled::after {
|
|
154
|
+
display: none;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@media (prefers-reduced-motion: reduce) {
|
|
158
|
+
.lx-check,
|
|
159
|
+
.lx-radio,
|
|
160
|
+
.lx-check::after,
|
|
161
|
+
.lx-radio::after {
|
|
162
|
+
transition: none;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Row-selection checkbox variant used inside a .table-ledger select column.
|
|
167
|
+
.lx-check {
|
|
168
|
+
width: 16px;
|
|
169
|
+
height: 16px;
|
|
170
|
+
accent-color: var(--color-primary);
|
|
171
|
+
cursor: pointer;
|
|
172
|
+
vertical-align: middle;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Field / Input ──
|
|
176
|
+
// Inputs — always-white fill, 16px squircle, soft brand border tint on focus
|
|
177
|
+
// (no glow).
|
|
178
|
+
.input,
|
|
179
|
+
.select,
|
|
180
|
+
.textarea {
|
|
181
|
+
box-sizing: border-box;
|
|
182
|
+
width: 100%;
|
|
183
|
+
height: var(--wui-control-height);
|
|
184
|
+
padding: 12px;
|
|
185
|
+
border-radius: var(--radius);
|
|
186
|
+
border: 1px solid var(--color-border);
|
|
187
|
+
background: var(--color-surface);
|
|
188
|
+
font-size: 14px;
|
|
189
|
+
line-height: 1.5;
|
|
190
|
+
color: var(--color-text);
|
|
191
|
+
transition:
|
|
192
|
+
border-color 0.12s,
|
|
193
|
+
box-shadow 0.12s;
|
|
194
|
+
font-family: inherit;
|
|
195
|
+
}
|
|
196
|
+
.textarea {
|
|
197
|
+
height: auto;
|
|
198
|
+
min-height: 120px;
|
|
199
|
+
padding: 12px 15px 16px;
|
|
200
|
+
line-height: 1.5;
|
|
201
|
+
resize: vertical;
|
|
202
|
+
}
|
|
203
|
+
.input:hover:not(:focus):not(:disabled),
|
|
204
|
+
.select:hover:not(:focus):not(:disabled),
|
|
205
|
+
.textarea:hover:not(:focus):not(:disabled) {
|
|
206
|
+
background-color: var(--color-bg-muted);
|
|
207
|
+
border-color: var(--color-border-strong);
|
|
208
|
+
}
|
|
209
|
+
.input:focus,
|
|
210
|
+
.select:focus,
|
|
211
|
+
.textarea:focus {
|
|
212
|
+
outline: none;
|
|
213
|
+
background-color: var(--color-bg-muted);
|
|
214
|
+
color: var(--color-text);
|
|
215
|
+
border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
|
|
216
|
+
box-shadow: none;
|
|
217
|
+
}
|
|
218
|
+
// Textarea renders as .form-control.textarea (react-bootstrap). Match the
|
|
219
|
+
// shared theme's .form-control:hover specificity so it hovers to
|
|
220
|
+
// --color-border-strong too.
|
|
221
|
+
textarea.textarea.form-control:hover:not(:focus):not(:disabled):not([readonly]) {
|
|
222
|
+
border-color: var(--color-border-strong);
|
|
223
|
+
}
|
|
224
|
+
textarea.textarea:focus {
|
|
225
|
+
outline: none;
|
|
226
|
+
border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
|
|
227
|
+
box-shadow: none;
|
|
228
|
+
}
|
|
229
|
+
.input::placeholder,
|
|
230
|
+
.textarea::placeholder {
|
|
231
|
+
color: var(--color-text-subtle);
|
|
232
|
+
}
|
|
233
|
+
// Edge/IE add a native password reveal (and clear) button — hide them so only
|
|
234
|
+
// the app's own eye toggle shows.
|
|
235
|
+
.input::-ms-reveal,
|
|
236
|
+
.input::-ms-clear {
|
|
237
|
+
display: none;
|
|
238
|
+
}
|
|
239
|
+
.input:disabled,
|
|
240
|
+
.select:disabled,
|
|
241
|
+
.textarea:disabled {
|
|
242
|
+
background: var(--color-bg-muted);
|
|
243
|
+
border-color: var(--color-border);
|
|
244
|
+
color: var(--color-text-subtle);
|
|
245
|
+
cursor: not-allowed;
|
|
246
|
+
}
|
|
247
|
+
.field:has(.input:disabled) .label {
|
|
248
|
+
color: var(--color-text-subtle);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Read-only is NOT disabled, and the two must not look alike: a read-only value is
|
|
252
|
+
// still submitted, still selectable and still copyable, so a `not-allowed` cursor
|
|
253
|
+
// would lie about it. Only the surface says "not yours to edit".
|
|
254
|
+
//
|
|
255
|
+
// Match on `[readonly]`, never the `:read-only` PSEUDO-class — that also matches a
|
|
256
|
+
// DISABLED input (and any non-editable element), so a `:read-only` rule silently
|
|
257
|
+
// repaints disabled fields too.
|
|
258
|
+
.input[readonly],
|
|
259
|
+
.select[readonly],
|
|
260
|
+
.textarea[readonly] {
|
|
261
|
+
background: var(--color-bg-subtle);
|
|
262
|
+
color: var(--color-text-muted);
|
|
263
|
+
cursor: default;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Invalid field state (required-field validation) — soft danger-tinted
|
|
267
|
+
// border on the surface.
|
|
268
|
+
// `[aria-invalid="true"]` is styled alongside `.is-invalid`: React sets the ARIA
|
|
269
|
+
// attribute (it is the accessible source of truth) and does not always add the
|
|
270
|
+
// Bootstrap class, so keying on the class alone left React error states unstyled.
|
|
271
|
+
.input.is-invalid,
|
|
272
|
+
.select.is-invalid,
|
|
273
|
+
.textarea.is-invalid,
|
|
274
|
+
.input.is-invalid:focus,
|
|
275
|
+
.select.is-invalid:focus,
|
|
276
|
+
.textarea.is-invalid:focus,
|
|
277
|
+
.input[aria-invalid="true"],
|
|
278
|
+
.select[aria-invalid="true"],
|
|
279
|
+
.textarea[aria-invalid="true"],
|
|
280
|
+
.input[aria-invalid="true"]:focus,
|
|
281
|
+
.select[aria-invalid="true"]:focus,
|
|
282
|
+
.textarea[aria-invalid="true"]:focus {
|
|
283
|
+
background: var(--color-surface);
|
|
284
|
+
border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-surface));
|
|
285
|
+
box-shadow: none;
|
|
286
|
+
}
|
|
287
|
+
.input.is-invalid,
|
|
288
|
+
.input[aria-invalid="true"] {
|
|
289
|
+
padding-right: 41px;
|
|
290
|
+
}
|
|
291
|
+
.field-error {
|
|
292
|
+
font-size: 12px;
|
|
293
|
+
line-height: normal;
|
|
294
|
+
color: var(--color-danger);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Native select with a rounded squircle, custom chevron, off-surface fill.
|
|
298
|
+
.select {
|
|
299
|
+
appearance: none;
|
|
300
|
+
-webkit-appearance: none;
|
|
301
|
+
padding-right: 40px;
|
|
302
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19.92 8.95l-6.52 6.52c-.77.77-2.03.77-2.8 0L4.08 8.95' fill='none' stroke='%234B5675' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3C/svg%3E");
|
|
303
|
+
background-repeat: no-repeat;
|
|
304
|
+
background-position: right 14px center;
|
|
305
|
+
background-size: 16px;
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Date inputs — a brand calendar glyph that fills the field so a click
|
|
310
|
+
// anywhere opens the native picker; muted text until a value is chosen.
|
|
311
|
+
.input[type="date"] {
|
|
312
|
+
position: relative;
|
|
313
|
+
cursor: pointer;
|
|
314
|
+
}
|
|
315
|
+
.input[type="date"]:not(.has-value):not(:focus) {
|
|
316
|
+
color: var(--color-text-subtle);
|
|
317
|
+
}
|
|
318
|
+
.input[type="date"]::-webkit-calendar-picker-indicator {
|
|
319
|
+
position: absolute;
|
|
320
|
+
inset: 0;
|
|
321
|
+
width: auto;
|
|
322
|
+
height: auto;
|
|
323
|
+
margin: 0;
|
|
324
|
+
padding: 0;
|
|
325
|
+
cursor: pointer;
|
|
326
|
+
opacity: 0;
|
|
327
|
+
}
|
|
328
|
+
.input[type="date"]::-webkit-datetime-edit {
|
|
329
|
+
padding-right: 28px;
|
|
330
|
+
}
|
|
331
|
+
.input-date-wrap {
|
|
332
|
+
position: relative;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Number input: hide the native spinner so a custom stepper can be rendered.
|
|
336
|
+
.input[type="number"]::-webkit-inner-spin-button,
|
|
337
|
+
.input[type="number"]::-webkit-outer-spin-button {
|
|
338
|
+
-webkit-appearance: none;
|
|
339
|
+
margin: 0;
|
|
340
|
+
}
|
|
341
|
+
.input[type="number"] {
|
|
342
|
+
-moz-appearance: textfield;
|
|
343
|
+
appearance: textfield;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Custom number stepper — up/down arrows, no background.
|
|
347
|
+
.num-stepper {
|
|
348
|
+
position: relative;
|
|
349
|
+
width: 100%;
|
|
350
|
+
}
|
|
351
|
+
.field > .num-stepper {
|
|
352
|
+
align-self: stretch;
|
|
353
|
+
}
|
|
354
|
+
.num-stepper .input {
|
|
355
|
+
width: 100%;
|
|
356
|
+
padding-right: 36px;
|
|
357
|
+
}
|
|
358
|
+
.num-stepper-btns {
|
|
359
|
+
position: absolute;
|
|
360
|
+
top: 50%;
|
|
361
|
+
right: 8px;
|
|
362
|
+
transform: translateY(-50%);
|
|
363
|
+
display: flex;
|
|
364
|
+
flex-direction: column;
|
|
365
|
+
gap: 0;
|
|
366
|
+
}
|
|
367
|
+
.num-stepper-btns button {
|
|
368
|
+
display: grid;
|
|
369
|
+
place-items: center;
|
|
370
|
+
width: 20px;
|
|
371
|
+
height: 15px;
|
|
372
|
+
border: none;
|
|
373
|
+
background: transparent;
|
|
374
|
+
padding: 0;
|
|
375
|
+
cursor: pointer;
|
|
376
|
+
color: var(--color-text-muted);
|
|
377
|
+
}
|
|
378
|
+
.num-stepper-btns button:hover {
|
|
379
|
+
color: var(--color-text);
|
|
380
|
+
}
|
|
381
|
+
.num-stepper-btns button:active {
|
|
382
|
+
color: var(--color-primary);
|
|
383
|
+
}
|
|
384
|
+
.input-date-wrap > .input {
|
|
385
|
+
padding-right: 38px;
|
|
386
|
+
}
|
|
387
|
+
.input-date-icon {
|
|
388
|
+
position: absolute;
|
|
389
|
+
right: 12px;
|
|
390
|
+
top: 50%;
|
|
391
|
+
transform: translateY(-50%);
|
|
392
|
+
color: var(--color-text-muted);
|
|
393
|
+
pointer-events: none;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Error state — danger-tinted border + room for a trailing affix.
|
|
397
|
+
.input.error,
|
|
398
|
+
.input.error:focus,
|
|
399
|
+
.textarea.error,
|
|
400
|
+
.textarea.error:focus {
|
|
401
|
+
background: var(--color-surface);
|
|
402
|
+
border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-surface));
|
|
403
|
+
box-shadow: none;
|
|
404
|
+
}
|
|
405
|
+
.input.error {
|
|
406
|
+
padding-right: 41px;
|
|
407
|
+
}
|
|
408
|
+
// Dark mode: keep the value in an errored field legible.
|
|
409
|
+
:root[data-theme="dark"] .input.error,
|
|
410
|
+
:root[data-theme="dark"] .input.error:focus,
|
|
411
|
+
:root[data-theme="dark"] .input.error:-webkit-autofill,
|
|
412
|
+
:root[data-theme="dark"] .input.error:focus:-webkit-autofill {
|
|
413
|
+
color: var(--color-text);
|
|
414
|
+
-webkit-text-fill-color: var(--color-text);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Field-relative wrap for trailing affixes (error icon, password toggle).
|
|
418
|
+
.input-wrap {
|
|
419
|
+
position: relative;
|
|
420
|
+
width: 100%;
|
|
421
|
+
align-self: stretch;
|
|
422
|
+
}
|
|
423
|
+
.input-wrap .input {
|
|
424
|
+
width: 100%;
|
|
425
|
+
}
|
|
426
|
+
.input-affix {
|
|
427
|
+
position: absolute;
|
|
428
|
+
right: 14px;
|
|
429
|
+
top: 22.5px;
|
|
430
|
+
transform: translateY(-50%);
|
|
431
|
+
display: grid;
|
|
432
|
+
place-items: center;
|
|
433
|
+
pointer-events: none;
|
|
434
|
+
color: var(--color-danger);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Leading-icon input — icon turns brand on focus.
|
|
438
|
+
.input-icon {
|
|
439
|
+
position: relative;
|
|
440
|
+
display: flex;
|
|
441
|
+
align-items: center;
|
|
442
|
+
width: 100%;
|
|
443
|
+
}
|
|
444
|
+
.input-icon > .ic {
|
|
445
|
+
position: absolute;
|
|
446
|
+
left: 13px;
|
|
447
|
+
top: 50%;
|
|
448
|
+
transform: translateY(-50%);
|
|
449
|
+
display: grid;
|
|
450
|
+
place-items: center;
|
|
451
|
+
color: var(--color-text-subtle);
|
|
452
|
+
pointer-events: none;
|
|
453
|
+
}
|
|
454
|
+
.input-icon .input {
|
|
455
|
+
padding-left: 40px;
|
|
456
|
+
}
|
|
457
|
+
.input-icon:focus-within > .ic {
|
|
458
|
+
color: var(--color-primary);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Password reveal toggle (place inside .input-wrap).
|
|
462
|
+
.input.pw {
|
|
463
|
+
padding-right: 44px;
|
|
464
|
+
}
|
|
465
|
+
.input.pw[type="password"]::placeholder {
|
|
466
|
+
letter-spacing: normal;
|
|
467
|
+
color: var(--color-text-subtle);
|
|
468
|
+
}
|
|
469
|
+
.pw-toggle {
|
|
470
|
+
position: absolute;
|
|
471
|
+
right: 7px;
|
|
472
|
+
top: 22px;
|
|
473
|
+
transform: translateY(-50%);
|
|
474
|
+
display: grid;
|
|
475
|
+
place-items: center;
|
|
476
|
+
width: 32px;
|
|
477
|
+
height: 32px;
|
|
478
|
+
border: none;
|
|
479
|
+
background: transparent;
|
|
480
|
+
color: var(--color-text-subtle);
|
|
481
|
+
border-radius: var(--radius);
|
|
482
|
+
cursor: pointer;
|
|
483
|
+
transition:
|
|
484
|
+
color 0.12s ease,
|
|
485
|
+
background 0.12s ease;
|
|
486
|
+
}
|
|
487
|
+
.pw-toggle:hover {
|
|
488
|
+
color: var(--color-primary);
|
|
489
|
+
background: transparent;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.hint,
|
|
493
|
+
.field-hint {
|
|
494
|
+
margin-top: 2px;
|
|
495
|
+
font-size: 12px;
|
|
496
|
+
color: var(--color-text-subtle);
|
|
497
|
+
}
|
|
498
|
+
.label {
|
|
499
|
+
display: block;
|
|
500
|
+
font-size: 14px;
|
|
501
|
+
line-height: normal;
|
|
502
|
+
font-weight: 500;
|
|
503
|
+
color: var(--color-text);
|
|
504
|
+
margin-bottom: 6px;
|
|
505
|
+
}
|
|
506
|
+
.label-hint {
|
|
507
|
+
color: var(--color-text-subtle);
|
|
508
|
+
font-weight: 400;
|
|
509
|
+
margin-left: 6px;
|
|
510
|
+
font-size: 12px;
|
|
511
|
+
}
|
|
512
|
+
.label-optional {
|
|
513
|
+
font-weight: 400;
|
|
514
|
+
font-size: 14px;
|
|
515
|
+
color: var(--color-text-subtle);
|
|
516
|
+
}
|
|
517
|
+
.required {
|
|
518
|
+
color: var(--color-danger);
|
|
519
|
+
// The gap is CSS, not a significant space in the markup. RequiredMark used to render
|
|
520
|
+
// `{' '}*` while AppDatePicker and every Razor helper rendered a bare `*`, so the same
|
|
521
|
+
// asterisk sat 4px further out in some fields than others — and no check could see it,
|
|
522
|
+
// because HTML comparison collapses whitespace by design.
|
|
523
|
+
margin-inline-start: 0.25em;
|
|
524
|
+
}
|
|
525
|
+
.field {
|
|
526
|
+
display: flex;
|
|
527
|
+
flex-direction: column;
|
|
528
|
+
align-items: flex-start;
|
|
529
|
+
gap: 6px;
|
|
530
|
+
margin-bottom: 16px;
|
|
531
|
+
}
|
|
532
|
+
.field > .label,
|
|
533
|
+
.field label {
|
|
534
|
+
margin-bottom: 0;
|
|
535
|
+
}
|
|
536
|
+
.field-row {
|
|
537
|
+
display: grid;
|
|
538
|
+
grid-template-columns: 1fr 1fr;
|
|
539
|
+
gap: 16px;
|
|
540
|
+
margin-bottom: 16px;
|
|
541
|
+
}
|
|
542
|
+
.field-row-3 {
|
|
543
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
544
|
+
}
|
|
545
|
+
// min-width:0 — grid/flex items default to min-width:auto, which lets a long
|
|
546
|
+
// control (e.g. a search-select label) push its track past 1fr and defeat
|
|
547
|
+
// text truncation. Reset to 0 so the ellipsis engages instead of overflowing.
|
|
548
|
+
.field-row .field {
|
|
549
|
+
margin-bottom: 0;
|
|
550
|
+
min-width: 0;
|
|
551
|
+
}
|
|
552
|
+
// Controls fill their field even though .field aligns children flex-start.
|
|
553
|
+
.field > .search-select,
|
|
554
|
+
.field > .input-with-unit,
|
|
555
|
+
.field > .input,
|
|
556
|
+
.field > .datepicker,
|
|
557
|
+
.field > .field-error {
|
|
558
|
+
align-self: stretch;
|
|
559
|
+
}
|
|
560
|
+
// Doubled selectors — see the lg-tier block below: a media query carries no
|
|
561
|
+
// extra specificity, so a consumer's later single-class rule would win.
|
|
562
|
+
@media (max-width: 575.98px) {
|
|
563
|
+
.field-row.field-row,
|
|
564
|
+
.field-row-3.field-row-3 {
|
|
565
|
+
grid-template-columns: 1fr;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// red border around a non-input group (e.g. a structure-form card row)
|
|
570
|
+
.group-invalid {
|
|
571
|
+
border: 1px solid var(--color-danger);
|
|
572
|
+
border-radius: var(--radius);
|
|
573
|
+
padding: 8px;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// One field, one border: the WRAPPER owns the border + hover/focus/error ring
|
|
577
|
+
// so the numeric input and its trailing unit pill light up together.
|
|
578
|
+
.input-with-unit {
|
|
579
|
+
display: flex;
|
|
580
|
+
align-items: stretch;
|
|
581
|
+
border: 1px solid var(--color-border);
|
|
582
|
+
border-radius: var(--radius-lg);
|
|
583
|
+
corner-shape: squircle;
|
|
584
|
+
background: var(--color-surface);
|
|
585
|
+
transition:
|
|
586
|
+
border-color 0.12s,
|
|
587
|
+
background-color 0.12s;
|
|
588
|
+
overflow: hidden;
|
|
589
|
+
}
|
|
590
|
+
.input-with-unit:hover:not(:focus-within) {
|
|
591
|
+
background: var(--color-bg-muted);
|
|
592
|
+
border-color: var(--color-border-strong);
|
|
593
|
+
}
|
|
594
|
+
.input-with-unit:focus-within {
|
|
595
|
+
background: var(--color-bg-muted);
|
|
596
|
+
border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
|
|
597
|
+
}
|
|
598
|
+
.input-with-unit:has(.num-input.error) {
|
|
599
|
+
border-color: var(--color-danger);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Numeric inputs are plain text fields — no native spinner, no stepper. The
|
|
603
|
+
// field flows seamlessly into the trailing unit pill; the wrapper carries the
|
|
604
|
+
// border.
|
|
605
|
+
.input-with-unit .num-input {
|
|
606
|
+
border: none;
|
|
607
|
+
border-radius: 0;
|
|
608
|
+
background: transparent;
|
|
609
|
+
}
|
|
610
|
+
.input-with-unit .num-input:focus {
|
|
611
|
+
box-shadow: none;
|
|
612
|
+
}
|
|
613
|
+
.input-with-unit .unit {
|
|
614
|
+
background: var(--color-bg-muted);
|
|
615
|
+
border: none;
|
|
616
|
+
border-left: 1px solid var(--color-border);
|
|
617
|
+
padding: 9px 14px;
|
|
618
|
+
font-size: 14px;
|
|
619
|
+
font-weight: 500;
|
|
620
|
+
color: var(--color-text-muted);
|
|
621
|
+
font-family: "Inter", sans-serif;
|
|
622
|
+
display: inline-flex;
|
|
623
|
+
align-items: center;
|
|
624
|
+
white-space: nowrap;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ── Status badge ──
|
|
628
|
+
.badge {
|
|
629
|
+
display: inline-flex;
|
|
630
|
+
align-items: center;
|
|
631
|
+
gap: 4px;
|
|
632
|
+
padding: 3px 9px;
|
|
633
|
+
border-radius: 999px;
|
|
634
|
+
font-size: 12px;
|
|
635
|
+
font-weight: 500;
|
|
636
|
+
background: var(--color-bg-muted);
|
|
637
|
+
color: var(--color-text-muted);
|
|
638
|
+
border: 1px solid var(--color-border);
|
|
639
|
+
}
|
|
640
|
+
.badge-primary {
|
|
641
|
+
background: var(--color-primary-soft);
|
|
642
|
+
color: var(--color-primary);
|
|
643
|
+
border-color: color-mix(in srgb, var(--color-primary) 25%, transparent);
|
|
644
|
+
}
|
|
645
|
+
.badge-success {
|
|
646
|
+
background: var(--color-success-soft);
|
|
647
|
+
color: var(--color-success-strong);
|
|
648
|
+
border-color: transparent;
|
|
649
|
+
}
|
|
650
|
+
.badge-warn {
|
|
651
|
+
background: var(--color-warning-soft);
|
|
652
|
+
color: var(--color-warning-strong);
|
|
653
|
+
border-color: transparent;
|
|
654
|
+
}
|
|
655
|
+
.badge-danger {
|
|
656
|
+
background: var(--color-danger-soft);
|
|
657
|
+
color: var(--color-danger-strong);
|
|
658
|
+
border-color: transparent;
|
|
659
|
+
}
|
|
660
|
+
.badge-info {
|
|
661
|
+
background: var(--color-info-soft);
|
|
662
|
+
color: var(--color-info-strong);
|
|
663
|
+
border-color: transparent;
|
|
664
|
+
}
|
|
665
|
+
.badge-dot::before {
|
|
666
|
+
content: "";
|
|
667
|
+
width: 6px;
|
|
668
|
+
height: 6px;
|
|
669
|
+
border-radius: 999px;
|
|
670
|
+
background: currentColor;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// "Soft" status badge — pill, 12/600, muted tone fill.
|
|
674
|
+
.status-label {
|
|
675
|
+
display: inline-flex;
|
|
676
|
+
align-items: center;
|
|
677
|
+
gap: 5px;
|
|
678
|
+
padding: 0 10px;
|
|
679
|
+
height: 24px;
|
|
680
|
+
border-radius: 999px;
|
|
681
|
+
font-size: 12px;
|
|
682
|
+
font-weight: 600;
|
|
683
|
+
line-height: 1;
|
|
684
|
+
white-space: nowrap;
|
|
685
|
+
}
|
|
686
|
+
// Tones are SEMANTIC — success / warning / danger / info / primary / neutral —
|
|
687
|
+
// the same vocabulary .chip, .badge and every other tone-bearing component uses.
|
|
688
|
+
// A semantic name survives a palette change; a colour name does not (a "green"
|
|
689
|
+
// tone is a lie the moment the brand's success colour stops being green).
|
|
690
|
+
//
|
|
691
|
+
// Two other vocabularies existed and are accepted as DEPRECATED ALIASES so a
|
|
692
|
+
// consumer can migrate without a flag day:
|
|
693
|
+
// - colour-named .tone-green / -amber / -red / -blue / -grey (was the library's)
|
|
694
|
+
// - short semantic .tone-ok / -warn / -bad (was lumex's)
|
|
695
|
+
// Author new markup with the semantic names; the aliases will be removed.
|
|
696
|
+
.status-label.success,
|
|
697
|
+
.status-label.tone-ok,
|
|
698
|
+
.status-label.tone-green {
|
|
699
|
+
background: color-mix(in srgb, var(--color-success) 16%, transparent);
|
|
700
|
+
color: var(--color-success-strong);
|
|
701
|
+
}
|
|
702
|
+
.status-label.warning,
|
|
703
|
+
.status-label.tone-warn,
|
|
704
|
+
.status-label.tone-amber {
|
|
705
|
+
background: color-mix(in srgb, var(--color-warning) 16%, transparent);
|
|
706
|
+
color: var(--color-warning-strong);
|
|
707
|
+
}
|
|
708
|
+
.status-label.danger,
|
|
709
|
+
.status-label.tone-bad,
|
|
710
|
+
.status-label.tone-red {
|
|
711
|
+
background: color-mix(in srgb, var(--color-danger) 16%, transparent);
|
|
712
|
+
color: var(--color-danger-strong);
|
|
713
|
+
}
|
|
714
|
+
.status-label.info,
|
|
715
|
+
.status-label.tone-blue {
|
|
716
|
+
background: color-mix(in srgb, var(--color-info) 16%, transparent);
|
|
717
|
+
color: var(--color-info-strong);
|
|
718
|
+
}
|
|
719
|
+
.status-label.primary {
|
|
720
|
+
background: var(--color-primary-soft);
|
|
721
|
+
color: var(--color-primary);
|
|
722
|
+
}
|
|
723
|
+
// Default/neutral — also the bare `.status-label` look, so an unknown or omitted
|
|
724
|
+
// tone still renders as a proper pill instead of transparent text.
|
|
725
|
+
.status-label,
|
|
726
|
+
.status-label.neutral,
|
|
727
|
+
.status-label.tone-grey {
|
|
728
|
+
background: color-mix(in srgb, var(--color-text-subtle) 16%, transparent);
|
|
729
|
+
color: var(--color-text-subtle);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// ── Choice chip ──
|
|
733
|
+
// Inline selectable chip — native control hidden, pill adopts the primary
|
|
734
|
+
// accent when checked.
|
|
735
|
+
.choice-chip {
|
|
736
|
+
display: inline-flex;
|
|
737
|
+
align-items: center;
|
|
738
|
+
gap: 8px;
|
|
739
|
+
padding: 9px 14px;
|
|
740
|
+
border: 1.5px solid var(--color-border);
|
|
741
|
+
border-radius: 999px;
|
|
742
|
+
font-size: 14px;
|
|
743
|
+
font-weight: 500;
|
|
744
|
+
color: var(--color-text);
|
|
745
|
+
background: var(--color-surface);
|
|
746
|
+
cursor: pointer;
|
|
747
|
+
user-select: none;
|
|
748
|
+
transition:
|
|
749
|
+
border-color 0.12s ease,
|
|
750
|
+
background 0.12s ease,
|
|
751
|
+
color 0.12s ease;
|
|
752
|
+
}
|
|
753
|
+
.choice-chip:hover {
|
|
754
|
+
border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
|
|
755
|
+
}
|
|
756
|
+
.choice-chip input {
|
|
757
|
+
position: absolute;
|
|
758
|
+
opacity: 0;
|
|
759
|
+
pointer-events: none;
|
|
760
|
+
}
|
|
761
|
+
.choice-chip.checked {
|
|
762
|
+
border-color: var(--color-primary);
|
|
763
|
+
background: var(--color-primary-soft);
|
|
764
|
+
color: var(--color-primary);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// A count badge inside a selectable chip — "Active 12". Inverts on selection for
|
|
768
|
+
// the same reason the nav badge does: the chip already carries the soft tint, so a
|
|
769
|
+
// tinted badge on top of it would disappear.
|
|
770
|
+
.choice-chip .count {
|
|
771
|
+
padding: 1px 6px;
|
|
772
|
+
border-radius: 999px;
|
|
773
|
+
background: var(--color-bg-muted);
|
|
774
|
+
font-size: 11px;
|
|
775
|
+
font-variant-numeric: tabular-nums;
|
|
776
|
+
color: var(--color-text-subtle);
|
|
777
|
+
}
|
|
778
|
+
.choice-chip.checked .count {
|
|
779
|
+
background: var(--color-surface);
|
|
780
|
+
color: var(--color-primary);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Form section heading — a branded accent bar rather than a full-width hairline,
|
|
784
|
+
// so a long form reads as designed sections instead of raw rules.
|
|
785
|
+
//
|
|
786
|
+
// Block, not flex: an inline subscript in the label (N(SG)) would otherwise be
|
|
787
|
+
// split out as a separate flex item and gain a gap.
|
|
788
|
+
.fld-section {
|
|
789
|
+
margin: 30px 0 10px;
|
|
790
|
+
font-size: 13px;
|
|
791
|
+
font-weight: 700;
|
|
792
|
+
text-transform: uppercase;
|
|
793
|
+
letter-spacing: 0.05em;
|
|
794
|
+
color: var(--color-text-muted);
|
|
795
|
+
}
|
|
796
|
+
.fld-section::before {
|
|
797
|
+
content: "";
|
|
798
|
+
display: inline-block;
|
|
799
|
+
width: 3px;
|
|
800
|
+
height: 12px;
|
|
801
|
+
margin-right: 9px;
|
|
802
|
+
border-radius: 2px;
|
|
803
|
+
background: var(--color-primary);
|
|
804
|
+
vertical-align: middle;
|
|
805
|
+
}
|
|
806
|
+
.fld-section:first-child {
|
|
807
|
+
margin-top: 0;
|
|
808
|
+
}
|