@webority/theme 0.9.1 → 0.9.3
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/dist/webority-theme.css +1 -1
- package/package.json +1 -1
- package/scss/_components.scss +21 -0
- package/scss/wui/_choice.scss +22 -5
- package/scss/wui/_controls.scss +29 -4
- package/scss/wui/_data.scss +32 -1
- package/scss/wui/_media.scss +68 -2
- package/scss/wui/_navigation.scss +14 -12
- package/scss/wui/_overlays.scss +10 -15
- package/scss/wui/_utilities.scss +27 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webority/theme",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
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": {
|
package/scss/_components.scss
CHANGED
|
@@ -109,6 +109,25 @@
|
|
|
109
109
|
--bs-btn-disabled-border-color: var(--wui-danger-strong);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// Secondary-danger — the destructive counterpart of .btn-secondary: a soft red wash
|
|
113
|
+
// instead of a solid red fill, for a quieter destructive action (e.g. a "Remove" that
|
|
114
|
+
// shouldn't shout like a solid .btn-danger). Mirrors .btn-secondary exactly — low-alpha
|
|
115
|
+
// tint deepening on hover/press, no border — but keyed off the danger tokens. The label
|
|
116
|
+
// uses --wui-danger-text (the AA-safe error red designed to read on a light red tint),
|
|
117
|
+
// not the brighter --color-danger. Disabled falls through to the shared neutral-grey
|
|
118
|
+
// state below (it is not .btn-danger), like .btn-secondary.
|
|
119
|
+
.btn-secondary-danger {
|
|
120
|
+
--bs-btn-color: var(--wui-danger-text);
|
|
121
|
+
--bs-btn-bg: color-mix(in srgb, var(--color-danger) 12%, transparent);
|
|
122
|
+
--bs-btn-border-color: transparent;
|
|
123
|
+
--bs-btn-hover-color: var(--wui-danger-text);
|
|
124
|
+
--bs-btn-hover-bg: color-mix(in srgb, var(--color-danger) 18%, transparent);
|
|
125
|
+
--bs-btn-hover-border-color: transparent;
|
|
126
|
+
--bs-btn-active-color: var(--wui-danger-text);
|
|
127
|
+
--bs-btn-active-bg: color-mix(in srgb, var(--color-danger) 26%, transparent);
|
|
128
|
+
--bs-btn-active-border-color: transparent;
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
// Soft neutral tinted button — a quieter alternative to .btn-light for a secondary
|
|
113
132
|
// action that shouldn't compete with a primary/danger CTA in the same row.
|
|
114
133
|
.btn-subtle {
|
|
@@ -177,6 +196,8 @@
|
|
|
177
196
|
// key the wash and the label off the accent (text) colour so the hue still shows.
|
|
178
197
|
.btn-secondary.is-loading:disabled,
|
|
179
198
|
.btn-secondary.is-loading.disabled,
|
|
199
|
+
.btn-secondary-danger.is-loading:disabled,
|
|
200
|
+
.btn-secondary-danger.is-loading.disabled,
|
|
180
201
|
.btn-ghost.is-loading:disabled,
|
|
181
202
|
.btn-ghost.is-loading.disabled,
|
|
182
203
|
.btn-subtle.is-loading:disabled,
|
package/scss/wui/_choice.scss
CHANGED
|
@@ -107,6 +107,12 @@
|
|
|
107
107
|
opacity: 1;
|
|
108
108
|
transform: scale(1);
|
|
109
109
|
}
|
|
110
|
+
// The tick reads a touch bolder than the icon's default 1.5 hairline, so the confirmation
|
|
111
|
+
// stays crisp at the small top-corner size (CSS beats the glyph's stroke-width attribute;
|
|
112
|
+
// applies on both surfaces since both render the same stroke-based check).
|
|
113
|
+
.struct-card-check svg path {
|
|
114
|
+
stroke-width: 2.25;
|
|
115
|
+
}
|
|
110
116
|
|
|
111
117
|
// Tile variant: big centered illustration + label. Opt in with
|
|
112
118
|
// className="struct-card-tile".
|
|
@@ -171,8 +177,10 @@
|
|
|
171
177
|
cursor: not-allowed;
|
|
172
178
|
opacity: 0.6;
|
|
173
179
|
}
|
|
174
|
-
.search-select-control:hover:not(:disabled):not(.open) {
|
|
180
|
+
.search-select-control:hover:not(:disabled):not(.open):not([aria-invalid="true"]) {
|
|
175
181
|
// Not while open — an open control keeps its active (primary) border, not the grey hover one.
|
|
182
|
+
// Not while invalid — an errored control (aria-invalid mirrored from the host) holds its red
|
|
183
|
+
// border on hover rather than reverting to the grey hover one.
|
|
176
184
|
border-color: var(--color-border-strong);
|
|
177
185
|
}
|
|
178
186
|
.search-select-control.open {
|
|
@@ -484,7 +492,12 @@
|
|
|
484
492
|
--wui-datepicker-width: 100%;
|
|
485
493
|
}
|
|
486
494
|
.datepicker.open .datepicker-trigger {
|
|
487
|
-
|
|
495
|
+
// While the calendar is open the trigger reads as a focused field — clean white surface, not
|
|
496
|
+
// the muted resting fill. Opening moves focus INTO the popup, so :focus drops off the trigger;
|
|
497
|
+
// this .open rule keeps it white (matching .search-select-control.open) instead of snapping
|
|
498
|
+
// back to grey the moment the dropdown appears. It stays white on close too (_close refocuses
|
|
499
|
+
// the trigger → :focus), greying only on an outside click that blurs it.
|
|
500
|
+
background-color: var(--color-surface);
|
|
488
501
|
border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
|
|
489
502
|
box-shadow: none;
|
|
490
503
|
}
|
|
@@ -805,7 +818,8 @@ button.datepicker-month:hover {
|
|
|
805
818
|
color: var(--color-text-subtle);
|
|
806
819
|
}
|
|
807
820
|
.daterange.open .daterange-trigger {
|
|
808
|
-
|
|
821
|
+
// Stay white while the two-month popover is open — see the note on .datepicker.open above.
|
|
822
|
+
background-color: var(--color-surface);
|
|
809
823
|
border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
|
|
810
824
|
box-shadow: none;
|
|
811
825
|
}
|
|
@@ -906,7 +920,9 @@ button.datepicker-month:hover {
|
|
|
906
920
|
border-color 0.12s,
|
|
907
921
|
background-color 0.12s;
|
|
908
922
|
}
|
|
909
|
-
|
|
923
|
+
// Not while invalid — an errored control holds its red border/fill on hover (the host carries
|
|
924
|
+
// aria-invalid, so scope the hover treatment to a non-invalid host).
|
|
925
|
+
.app-phone-number-input wui-phone:not([aria-invalid="true"]) .phone-input-wrap:hover:not(:focus-within) {
|
|
910
926
|
background: var(--color-field-bg);
|
|
911
927
|
border-color: var(--color-border-strong);
|
|
912
928
|
}
|
|
@@ -1256,7 +1272,8 @@ wui-timepicker[aria-invalid="true"] .timepicker.open .timepicker-trigger {
|
|
|
1256
1272
|
--wui-timepicker-width: 100%;
|
|
1257
1273
|
}
|
|
1258
1274
|
.timepicker.open .timepicker-trigger {
|
|
1259
|
-
|
|
1275
|
+
// Stay white while the time list is open — see the note on .datepicker.open above.
|
|
1276
|
+
background-color: var(--color-surface);
|
|
1260
1277
|
border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
|
|
1261
1278
|
box-shadow: none;
|
|
1262
1279
|
}
|
package/scss/wui/_controls.scss
CHANGED
|
@@ -52,6 +52,23 @@
|
|
|
52
52
|
user-select: none;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Checkbox / radio / switch hint & error sit BELOW the label on their own line, indented to
|
|
56
|
+
// align under the label text (past the glyph + gap) — not inline beside it. `.form-check` is a
|
|
57
|
+
// flex row for glyph+label; a full-width flex basis wraps the message to a second line.
|
|
58
|
+
.form-check {
|
|
59
|
+
flex-wrap: wrap;
|
|
60
|
+
--wui-check-hint-indent: 28px; // 20px glyph + 8px gap
|
|
61
|
+
}
|
|
62
|
+
.form-switch {
|
|
63
|
+
--wui-check-hint-indent: 48px; // 40px track + 8px gap
|
|
64
|
+
}
|
|
65
|
+
.form-check > .field-hint,
|
|
66
|
+
.form-check > .field-error {
|
|
67
|
+
flex-basis: 100%;
|
|
68
|
+
margin-top: 2px;
|
|
69
|
+
padding-left: var(--wui-check-hint-indent);
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
// Glyph — shared by BS form-check-input and the wui-check alias.
|
|
56
73
|
.form-check-input,
|
|
57
74
|
.wui-check,
|
|
@@ -299,9 +316,13 @@ textarea.form-control.autosize {
|
|
|
299
316
|
overflow: hidden;
|
|
300
317
|
}
|
|
301
318
|
// Hover/error guards: without them the hover fill repaints over an invalid border.
|
|
319
|
+
// `:not([aria-expanded="true"])` on the `.input` line: a combobox/dialog trigger whose popup is
|
|
320
|
+
// OPEN (datepicker/date-range/time-picker triggers set aria-expanded on the trigger, but carry
|
|
321
|
+
// their `.open` class on the WRAPPER, not the trigger) must keep its open white fill — without
|
|
322
|
+
// this the resting-hover grey repaints over it the moment the cursor sits on the open trigger.
|
|
302
323
|
.form-control:hover:not(:focus):not(:disabled):not([readonly]):not([aria-invalid="true"]):not(.is-invalid):not(.error),
|
|
303
324
|
.form-select:hover:not(:focus):not(:disabled):not([aria-invalid="true"]):not(.is-invalid):not(.error),
|
|
304
|
-
.input:hover:not(:focus):not(:disabled):not([readonly]):not([aria-invalid="true"]):not(.is-invalid):not(.error):not(.open),
|
|
325
|
+
.input:hover:not(:focus):not(:disabled):not([readonly]):not([aria-invalid="true"]):not(.is-invalid):not(.error):not(.open):not([aria-expanded="true"]),
|
|
305
326
|
.select:hover:not(:focus):not(:disabled):not([aria-invalid="true"]):not(.is-invalid):not(.error),
|
|
306
327
|
.textarea:hover:not(:focus):not(:disabled):not([readonly]):not([aria-invalid="true"]):not(.is-invalid):not(.error) {
|
|
307
328
|
background-color: var(--color-field-bg);
|
|
@@ -1052,7 +1073,9 @@ wui-select[compact] {
|
|
|
1052
1073
|
background-color 0.12s;
|
|
1053
1074
|
overflow: hidden;
|
|
1054
1075
|
}
|
|
1055
|
-
|
|
1076
|
+
// Not while invalid — an errored control holds its red border/fill on hover rather than
|
|
1077
|
+
// reverting to the grey hover one (matches AppInput, whose hover rule excludes the error state).
|
|
1078
|
+
.num-wrap:hover:not(:focus-within):not(:has(.num-input.error)):not(:has(.num-input.is-invalid)):not(:has(.num-input[aria-invalid="true"])) {
|
|
1056
1079
|
background: var(--color-field-bg);
|
|
1057
1080
|
border-color: var(--color-border-strong);
|
|
1058
1081
|
}
|
|
@@ -1260,7 +1283,7 @@ wui-select[compact] {
|
|
|
1260
1283
|
align-items: center;
|
|
1261
1284
|
gap: 8px;
|
|
1262
1285
|
padding: 9px 14px;
|
|
1263
|
-
border:
|
|
1286
|
+
border: 1px solid var(--color-border);
|
|
1264
1287
|
border-radius: 999px;
|
|
1265
1288
|
font-size: 14px;
|
|
1266
1289
|
font-weight: 500;
|
|
@@ -1286,7 +1309,9 @@ wui-select[compact] {
|
|
|
1286
1309
|
pointer-events: none;
|
|
1287
1310
|
}
|
|
1288
1311
|
.choice-chip.checked {
|
|
1289
|
-
border
|
|
1312
|
+
// Match AppChoiceCard's checked border: a soft primary-30 edge, not the
|
|
1313
|
+
// full-strength primary line (they read as the same selection language).
|
|
1314
|
+
border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
|
|
1290
1315
|
background: var(--color-primary-soft);
|
|
1291
1316
|
color: var(--color-primary);
|
|
1292
1317
|
}
|
package/scss/wui/_data.scss
CHANGED
|
@@ -58,6 +58,19 @@
|
|
|
58
58
|
align-items: center;
|
|
59
59
|
text-align: center;
|
|
60
60
|
padding: 40px 24px;
|
|
61
|
+
// An empty ledger still renders its header row, whose nowrap cells can push the
|
|
62
|
+
// table wider than the viewport, so `.table-wrap` scrolls horizontally with no
|
|
63
|
+
// data to justify it. This state row spans the full scroll width, so without a
|
|
64
|
+
// pin the message centres over that width and slides off screen on mobile.
|
|
65
|
+
// Stick it to the scrollport's left edge and size it to the visible wrap width
|
|
66
|
+
// (`100cqw`, from the container on `.table-wrap`), so the message stays centred
|
|
67
|
+
// and fully in view no matter how far the header scrolls. `left: 0` is relative
|
|
68
|
+
// to the scroll container; on a wide screen `100cqw` equals the content width, so
|
|
69
|
+
// this is a no-op there.
|
|
70
|
+
position: sticky;
|
|
71
|
+
left: 0;
|
|
72
|
+
width: 100cqw;
|
|
73
|
+
box-sizing: border-box;
|
|
61
74
|
}
|
|
62
75
|
.tbl-empty-icon {
|
|
63
76
|
display: inline-grid;
|
|
@@ -96,10 +109,23 @@
|
|
|
96
109
|
}
|
|
97
110
|
// The empty state owns its own padding and is not a row you can act on — no
|
|
98
111
|
// cell padding, no border, and no hover fill from .table / .table-ledger.
|
|
99
|
-
.table td.wui-table-empty:has(> .tbl-empty)
|
|
112
|
+
.table td.wui-table-empty:has(> .tbl-empty),
|
|
113
|
+
.table td.wui-table-empty:has(> .tbl-empty-plain) {
|
|
100
114
|
padding: 0;
|
|
101
115
|
border-bottom: 0;
|
|
102
116
|
}
|
|
117
|
+
// A plain-string empty (no `.tbl-empty` wrapper) — carries the padding the cell
|
|
118
|
+
// used to own, plus the same viewport pin as `.tbl-empty` so it stays on screen
|
|
119
|
+
// when a narrow ledger scrolls horizontally (see the pin note on `.tbl-empty`).
|
|
120
|
+
.tbl-empty-plain {
|
|
121
|
+
padding: 36px 24px;
|
|
122
|
+
text-align: center;
|
|
123
|
+
color: var(--color-text-muted);
|
|
124
|
+
position: sticky;
|
|
125
|
+
left: 0;
|
|
126
|
+
width: 100cqw;
|
|
127
|
+
box-sizing: border-box;
|
|
128
|
+
}
|
|
103
129
|
.table tbody tr:hover td.wui-table-empty:has(> .tbl-empty),
|
|
104
130
|
.table-ledger tbody tr:hover td.wui-table-empty:has(> .tbl-empty) {
|
|
105
131
|
background: transparent;
|
|
@@ -253,6 +279,11 @@
|
|
|
253
279
|
// class (set --wui-table-pad) without overriding this selector. Bottom padding is now
|
|
254
280
|
// included (was 0) so the last row isn't flush against the card edge.
|
|
255
281
|
padding: var(--wui-table-pad, 20px);
|
|
282
|
+
// Query container for the empty-state pin below. The header row can force the
|
|
283
|
+
// ledger wider than the viewport, so on a narrow screen `.table-wrap` scrolls
|
|
284
|
+
// horizontally; `100cqw` here is the *visible* wrap width regardless of that
|
|
285
|
+
// scroll, which is what keeps the empty message on screen (see `.tbl-empty`).
|
|
286
|
+
container-type: inline-size;
|
|
256
287
|
}
|
|
257
288
|
.table-ledger {
|
|
258
289
|
border-spacing: 0;
|
package/scss/wui/_media.scss
CHANGED
|
@@ -893,6 +893,10 @@
|
|
|
893
893
|
border: 1px dashed color-mix(in srgb, var(--color-border-strong) 75%, transparent);
|
|
894
894
|
cursor: pointer;
|
|
895
895
|
transition: border-color 0.18s;
|
|
896
|
+
// Query container so the overlay can drop its label on a small ring (see the @container
|
|
897
|
+
// rule after .set-uploader-overlay). container-type also makes the ring the containing
|
|
898
|
+
// block for the absolute overlay — same role its position:relative already plays.
|
|
899
|
+
container-type: inline-size;
|
|
896
900
|
}
|
|
897
901
|
.set-uploader-ring:hover,
|
|
898
902
|
.set-uploader:hover .set-uploader-ring,
|
|
@@ -925,10 +929,16 @@
|
|
|
925
929
|
flex-direction: column;
|
|
926
930
|
align-items: center;
|
|
927
931
|
justify-content: center;
|
|
928
|
-
gap
|
|
932
|
+
// Label + gap scale with the ring so a small (sm, 72px) ring's overlay stays legible and
|
|
933
|
+
// uncramped instead of wrapping a fixed 11px "Update photo" into a tight disc where it
|
|
934
|
+
// overlaps the initials. Clamped so md/lg land on the same 11px / 4px gap as before.
|
|
935
|
+
gap: clamp(2px, calc(var(--wui-logo-size) * 0.035), 4px);
|
|
936
|
+
padding: 0 6%;
|
|
929
937
|
background: var(--color-scrim-strong);
|
|
930
938
|
color: #fff;
|
|
931
|
-
font-size: 11px;
|
|
939
|
+
font-size: clamp(9px, calc(var(--wui-logo-size) * 0.1), 11px);
|
|
940
|
+
line-height: 1.15;
|
|
941
|
+
text-align: center;
|
|
932
942
|
font-weight: 600;
|
|
933
943
|
opacity: 0;
|
|
934
944
|
transition: opacity 0.18s;
|
|
@@ -938,6 +948,62 @@
|
|
|
938
948
|
.set-uploader:focus-visible .set-uploader-overlay {
|
|
939
949
|
opacity: 1;
|
|
940
950
|
}
|
|
951
|
+
// A ring too small to fit "Update photo" without cramping it over the initials (sm = 72px,
|
|
952
|
+
// and any custom size below ~95px) drops the label and shows the icon alone — a compact edit
|
|
953
|
+
// affordance. The lone icon grows a little so it reads as the whole overlay's cue. Driven by a
|
|
954
|
+
// container query on the ring, so it works for every size, preset or custom px.
|
|
955
|
+
@container (max-width: 95px) {
|
|
956
|
+
.set-uploader-overlay > span {
|
|
957
|
+
display: none;
|
|
958
|
+
}
|
|
959
|
+
.set-uploader-overlay > .wui-icon {
|
|
960
|
+
width: calc(var(--wui-logo-size) * 0.34);
|
|
961
|
+
height: calc(var(--wui-logo-size) * 0.34);
|
|
962
|
+
}
|
|
963
|
+
// The label has no room in the ring, so surface it as a hover/focus tooltip below the ring
|
|
964
|
+
// (reusing the AppTooltip surface look). Text comes from data-overlay-label on the ring — a
|
|
965
|
+
// distinct attribute from data-wui-tip so the global tooltip rule never fires this on a large
|
|
966
|
+
// ring. Only rendered inside this container query, so md/lg (label visible) never get it.
|
|
967
|
+
.set-uploader-ring[data-overlay-label]::after {
|
|
968
|
+
content: attr(data-overlay-label);
|
|
969
|
+
position: absolute;
|
|
970
|
+
top: calc(100% + 8px);
|
|
971
|
+
left: 50%;
|
|
972
|
+
transform: translateX(-50%);
|
|
973
|
+
z-index: var(--wui-z-menu, 1080);
|
|
974
|
+
width: max-content;
|
|
975
|
+
max-width: 160px;
|
|
976
|
+
padding: 6px 10px;
|
|
977
|
+
border-radius: var(--radius, 10px);
|
|
978
|
+
corner-shape: squircle;
|
|
979
|
+
background: var(--color-surface);
|
|
980
|
+
color: var(--color-text);
|
|
981
|
+
border: 1px solid var(--color-border-light);
|
|
982
|
+
box-shadow: var(--wui-shadow-overlay);
|
|
983
|
+
font-size: 0.75rem;
|
|
984
|
+
font-weight: 500;
|
|
985
|
+
line-height: 1.3;
|
|
986
|
+
white-space: nowrap;
|
|
987
|
+
pointer-events: none;
|
|
988
|
+
opacity: 0;
|
|
989
|
+
visibility: hidden;
|
|
990
|
+
transition:
|
|
991
|
+
opacity 0.12s ease,
|
|
992
|
+
visibility 0.12s;
|
|
993
|
+
}
|
|
994
|
+
// :not(:disabled) — a disabled/loading picker can't be updated, so it shows no tooltip
|
|
995
|
+
// (mirrors the overlay, which isn't rendered in those states).
|
|
996
|
+
.set-uploader:not(:disabled):hover .set-uploader-ring[data-overlay-label]::after,
|
|
997
|
+
.set-uploader:not(:disabled):focus-visible .set-uploader-ring[data-overlay-label]::after {
|
|
998
|
+
opacity: 1;
|
|
999
|
+
visibility: visible;
|
|
1000
|
+
}
|
|
1001
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1002
|
+
.set-uploader-ring[data-overlay-label]::after {
|
|
1003
|
+
transition: none;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
941
1007
|
// While uploading: keep the overlay (spinner) visible and signal it's not
|
|
942
1008
|
// clickable.
|
|
943
1009
|
.set-uploader-overlay.is-loading {
|
|
@@ -213,14 +213,12 @@
|
|
|
213
213
|
font-weight: 600;
|
|
214
214
|
}
|
|
215
215
|
// Static fallback underline (no-JS / first paint); the sliding ink bar takes over once
|
|
216
|
-
// wired.
|
|
217
|
-
// — reads as belonging to the label yet never touches a rounded container edge. Matches
|
|
218
|
-
// the wired ink bar's inset below.
|
|
216
|
+
// wired. Spans the tab's full width (inset defaults to 0), matching the wired ink bar below.
|
|
219
217
|
.app-tab.active::after {
|
|
220
218
|
content: "";
|
|
221
219
|
position: absolute;
|
|
222
|
-
left: var(--wui-tab-underline-inset,
|
|
223
|
-
right: var(--wui-tab-underline-inset,
|
|
220
|
+
left: var(--wui-tab-underline-inset, 0px);
|
|
221
|
+
right: var(--wui-tab-underline-inset, 0px);
|
|
224
222
|
bottom: -1px;
|
|
225
223
|
height: 2px;
|
|
226
224
|
border-radius: 2px;
|
|
@@ -268,18 +266,22 @@
|
|
|
268
266
|
left: 0;
|
|
269
267
|
bottom: -1px;
|
|
270
268
|
height: 2px;
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
width: max(0px, calc(var(--wui-tab-w, 0) - var(--wui-tab-underline-inset,
|
|
269
|
+
// Spans the tab's FULL width (the inset defaults to 0). A consumer can inset it via
|
|
270
|
+
// `--wui-tab-underline-inset`; falls to 0 rather than going negative on a tab narrower than
|
|
271
|
+
// any inset it sets. Pills keep the inset at 0 too (the pill fills the tab).
|
|
272
|
+
width: max(0px, calc(var(--wui-tab-w, 0) - var(--wui-tab-underline-inset, 0px) * 2));
|
|
275
273
|
background: var(--color-primary);
|
|
276
274
|
border-radius: 2px;
|
|
277
|
-
transform: translateX(calc(var(--wui-tab-x, 0) + var(--wui-tab-underline-inset,
|
|
275
|
+
transform: translateX(calc(var(--wui-tab-x, 0) + var(--wui-tab-underline-inset, 0px)));
|
|
278
276
|
opacity: 0;
|
|
279
277
|
pointer-events: none;
|
|
278
|
+
// Smooth ease-OUT (no overshoot), not the elastic bounce: a full-width bar sits flush to the
|
|
279
|
+
// edge tabs, so an elastic overshoot would push it past the strip and the `overflow-x: auto`
|
|
280
|
+
// clip would shear off its rounded corner. A decelerating slide reaches the tab exactly and
|
|
281
|
+
// never crosses the edge, so the radius is always intact.
|
|
280
282
|
transition:
|
|
281
|
-
transform 0.42s var(--wui-ease-
|
|
282
|
-
width 0.42s var(--wui-ease-
|
|
283
|
+
transform 0.42s var(--wui-ease-out-quart),
|
|
284
|
+
width 0.42s var(--wui-ease-out-quart),
|
|
283
285
|
opacity 0.15s ease;
|
|
284
286
|
}
|
|
285
287
|
.app-tabs[data-wui-tabs] .app-tab-indicator {
|
package/scss/wui/_overlays.scss
CHANGED
|
@@ -309,12 +309,6 @@
|
|
|
309
309
|
line-height: 0;
|
|
310
310
|
margin-bottom: 22px;
|
|
311
311
|
}
|
|
312
|
-
// Lift the badge off the modal surface with a soft outer shadow. `drop-shadow` traces
|
|
313
|
-
// the SVG's circular alpha (a `box-shadow` would cast a square), and takes no spread —
|
|
314
|
-
// so this is authored spread-free rather than reusing the `--wui-shadow-overlay` token.
|
|
315
|
-
.wui-confirm-icon svg {
|
|
316
|
-
filter: drop-shadow(0 10px 18px rgba(7, 20, 55, 0.18));
|
|
317
|
-
}
|
|
318
312
|
// Icon size tracks the copy: a title+message dialog keeps the full 80px badge (set on
|
|
319
313
|
// the element), but a lone title (no message) is one line and gets a smaller badge.
|
|
320
314
|
// `.wui-confirm-msg` is a real element rendered only when a message exists, so `:has()`
|
|
@@ -345,17 +339,18 @@
|
|
|
345
339
|
.wui-modal-footer:has(.wui-confirm-actions) {
|
|
346
340
|
border-top: none;
|
|
347
341
|
padding-top: 0;
|
|
348
|
-
|
|
342
|
+
// The full-width CTA row makes the footer's insets read as a frame, so the gap below the
|
|
343
|
+
// buttons matches the 22px on their left and right (the base footer's 16px bottom looked
|
|
344
|
+
// cramped against the equal side gaps).
|
|
345
|
+
padding-bottom: 22px;
|
|
346
|
+
}
|
|
347
|
+
// Layout ONLY — the two CTAs share the row equally. Their chrome (radius, height, type
|
|
348
|
+
// scale, weight, padding) comes entirely from AppButton, so a confirm button reads identically
|
|
349
|
+
// to an AppButton anywhere else in the app. No separate button styling lives here: the dialog
|
|
350
|
+
// used to re-skin its buttons (10px radius, 14px/500 type, forced height), which made the confirm
|
|
351
|
+
// CTAs look like a different button than every other AppButton.
|
|
349
352
|
.wui-confirm-actions .btn {
|
|
350
353
|
flex: 1;
|
|
351
|
-
min-height: var(--wui-control-height);
|
|
352
|
-
height: var(--wui-control-height);
|
|
353
|
-
border-radius: var(--radius);
|
|
354
|
-
corner-shape: squircle;
|
|
355
|
-
padding-left: 16px;
|
|
356
|
-
padding-right: 16px;
|
|
357
|
-
font-size: 14px;
|
|
358
|
-
font-weight: 500;
|
|
359
354
|
}
|
|
360
355
|
|
|
361
356
|
// ── Tooltip ──
|
package/scss/wui/_utilities.scss
CHANGED
|
@@ -600,13 +600,17 @@
|
|
|
600
600
|
|
|
601
601
|
display: flex;
|
|
602
602
|
align-items: center;
|
|
603
|
+
// On phones the trailing action drops onto its own row BELOW the content (the flex `gap`
|
|
604
|
+
// supplies the vertical space) so a button never crushes the message into a sliver; from md
|
|
605
|
+
// up it sits inline at the right — a VIEWPORT rule, not the callout's own width. See the
|
|
606
|
+
// `.info-callout-action` breakpoint below.
|
|
607
|
+
flex-wrap: wrap;
|
|
603
608
|
gap: 12px;
|
|
604
609
|
padding: 14px 18px;
|
|
605
610
|
border: 1px solid var(--callout-border);
|
|
606
611
|
// A thick coloured left edge — the callout's signature aside marker.
|
|
607
612
|
border-left: 4px solid var(--callout-accent);
|
|
608
|
-
border-radius:
|
|
609
|
-
corner-shape: squircle;
|
|
613
|
+
border-radius: 8px;
|
|
610
614
|
background: var(--callout-bg);
|
|
611
615
|
font-size: 14px;
|
|
612
616
|
// Sub-content (the body/subtitle) reads at #252F4A / 400; the heading and the
|
|
@@ -690,14 +694,30 @@
|
|
|
690
694
|
min-width: 0;
|
|
691
695
|
font-size: 12px;
|
|
692
696
|
}
|
|
693
|
-
// Trailing action slot — a button (or any node)
|
|
697
|
+
// Trailing action slot — a button (or any node). Mobile-first: on phones it takes its own full
|
|
698
|
+
// row below the content (left-aligned there); from md up it collapses to its natural width and
|
|
699
|
+
// sits inline at the right edge of the content row.
|
|
694
700
|
.info-callout-action {
|
|
695
|
-
flex:
|
|
701
|
+
flex: 1 1 100%;
|
|
696
702
|
align-self: center;
|
|
697
|
-
display:
|
|
703
|
+
display: flex;
|
|
704
|
+
justify-content: flex-start;
|
|
698
705
|
align-items: center;
|
|
699
706
|
gap: 8px;
|
|
700
707
|
}
|
|
708
|
+
// From md up the row never wraps — the content shrinks to seat the action inline at the right,
|
|
709
|
+
// so a button stays beside the text on any large screen, even when the callout sits in a narrow
|
|
710
|
+
// column (the wrap-below is a phones-only treatment).
|
|
711
|
+
@include media-breakpoint-up(md) {
|
|
712
|
+
.info-callout {
|
|
713
|
+
flex-wrap: nowrap;
|
|
714
|
+
}
|
|
715
|
+
.info-callout-action {
|
|
716
|
+
flex: none;
|
|
717
|
+
justify-content: initial;
|
|
718
|
+
margin-left: auto;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
701
721
|
.info-callout-heading {
|
|
702
722
|
font-size: 14px;
|
|
703
723
|
font-weight: 600;
|
|
@@ -751,9 +771,8 @@
|
|
|
751
771
|
// the heading to the variant. Only the heading takes the variant colour — the body
|
|
752
772
|
// stays dark ink so the message reads calmly.
|
|
753
773
|
.alert {
|
|
754
|
-
--bs-alert-border-radius:
|
|
755
|
-
border-radius:
|
|
756
|
-
corner-shape: squircle;
|
|
774
|
+
--bs-alert-border-radius: 8px;
|
|
775
|
+
border-radius: 8px;
|
|
757
776
|
padding: 16px 18px;
|
|
758
777
|
border-width: 1px;
|
|
759
778
|
}
|