@webority/theme 0.3.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 +19 -3723
- 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,284 @@
|
|
|
1
|
+
// Drawer / sidebar menu, tabs, search field.
|
|
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
|
+
// ── Drawer / Sidebar menu ──
|
|
9
|
+
// AppSidebarMenu — react-bootstrap Offcanvas mapped onto the app tokens plus
|
|
10
|
+
// a flex column layout for the header / scrolling body / sticky footer.
|
|
11
|
+
.app-sidebar-menu.offcanvas {
|
|
12
|
+
--bs-offcanvas-width: var(--asm-width, 420px);
|
|
13
|
+
--bs-offcanvas-bg: var(--color-surface);
|
|
14
|
+
--bs-offcanvas-color: var(--color-text);
|
|
15
|
+
--bs-offcanvas-border-color: var(--color-border);
|
|
16
|
+
--bs-offcanvas-border-width: 1px;
|
|
17
|
+
--bs-offcanvas-padding-x: 0;
|
|
18
|
+
--bs-offcanvas-padding-y: 0;
|
|
19
|
+
max-width: 100vw;
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
}
|
|
23
|
+
.app-sidebar-menu .offcanvas-body {
|
|
24
|
+
padding: 0;
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
flex: 1;
|
|
28
|
+
min-height: 0;
|
|
29
|
+
}
|
|
30
|
+
.app-sidebar-menu .asm-footer {
|
|
31
|
+
padding: 16px 20px;
|
|
32
|
+
border-top: 1px solid var(--color-border);
|
|
33
|
+
background: var(--color-surface);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.drawer {
|
|
37
|
+
position: fixed;
|
|
38
|
+
top: 0;
|
|
39
|
+
right: 0;
|
|
40
|
+
width: 420px;
|
|
41
|
+
max-width: 100vw;
|
|
42
|
+
height: 100vh;
|
|
43
|
+
background: var(--color-surface);
|
|
44
|
+
border-left: 1px solid var(--color-border);
|
|
45
|
+
z-index: 200;
|
|
46
|
+
transform: translateX(100%);
|
|
47
|
+
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
}
|
|
51
|
+
.drawer.open {
|
|
52
|
+
transform: translateX(0);
|
|
53
|
+
}
|
|
54
|
+
.drawer-backdrop {
|
|
55
|
+
position: fixed;
|
|
56
|
+
inset: 0;
|
|
57
|
+
background: rgba(15, 23, 42, 0.4);
|
|
58
|
+
opacity: 0;
|
|
59
|
+
pointer-events: none;
|
|
60
|
+
transition: opacity 0.2s;
|
|
61
|
+
z-index: 150;
|
|
62
|
+
}
|
|
63
|
+
.drawer-backdrop.open {
|
|
64
|
+
opacity: 1;
|
|
65
|
+
pointer-events: auto;
|
|
66
|
+
}
|
|
67
|
+
.drawer-header {
|
|
68
|
+
padding: 18px 20px;
|
|
69
|
+
border-bottom: 1px solid var(--color-border);
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: space-between;
|
|
73
|
+
background: var(--color-surface);
|
|
74
|
+
}
|
|
75
|
+
.drawer-title {
|
|
76
|
+
font-weight: 600;
|
|
77
|
+
font-size: 16px;
|
|
78
|
+
letter-spacing: -0.01em;
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: 10px;
|
|
82
|
+
color: var(--color-text);
|
|
83
|
+
}
|
|
84
|
+
.drawer-title-ic {
|
|
85
|
+
flex: none;
|
|
86
|
+
display: grid;
|
|
87
|
+
place-items: center;
|
|
88
|
+
width: 30px;
|
|
89
|
+
height: 30px;
|
|
90
|
+
border-radius: var(--radius);
|
|
91
|
+
corner-shape: squircle;
|
|
92
|
+
background: var(--color-primary-soft);
|
|
93
|
+
color: var(--color-primary);
|
|
94
|
+
}
|
|
95
|
+
// Sticky tools zone: search + section meta stay put while the list scrolls.
|
|
96
|
+
.drawer-tools {
|
|
97
|
+
padding: 16px 20px 12px;
|
|
98
|
+
border-bottom: 1px solid var(--color-border);
|
|
99
|
+
background: var(--color-surface);
|
|
100
|
+
}
|
|
101
|
+
.drawer-body {
|
|
102
|
+
flex: 1;
|
|
103
|
+
overflow-y: auto;
|
|
104
|
+
padding: 14px 16px 20px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Tabs ──
|
|
108
|
+
// The tab strip both surfaces render. React drives it as a controlled strip and
|
|
109
|
+
// Razor additionally renders Bootstrap panels beneath it, but the strip markup —
|
|
110
|
+
// and therefore the look — is identical.
|
|
111
|
+
.app-tabs {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: 2px;
|
|
115
|
+
border-bottom: 1px solid var(--color-border);
|
|
116
|
+
overflow-x: auto;
|
|
117
|
+
scrollbar-width: none;
|
|
118
|
+
// The React AppTabs renders a react-bootstrap <Nav>, so the element also
|
|
119
|
+
// carries `.nav`, which declares `flex-wrap: wrap`. Both are (0,1,0), so
|
|
120
|
+
// today this wins only because the theme is imported after Bootstrap —
|
|
121
|
+
// see the doubled-selector rule below, which does not depend on that.
|
|
122
|
+
flex-wrap: nowrap;
|
|
123
|
+
}
|
|
124
|
+
// Doubled to (0,2,0) so the tab strip keeps scrolling even if a consumer loads
|
|
125
|
+
// Bootstrap after the theme, which would otherwise let `.nav` win on order.
|
|
126
|
+
.app-tabs.app-tabs {
|
|
127
|
+
flex-wrap: nowrap;
|
|
128
|
+
}
|
|
129
|
+
.app-tabs::-webkit-scrollbar {
|
|
130
|
+
display: none;
|
|
131
|
+
}
|
|
132
|
+
.app-tab {
|
|
133
|
+
display: inline-flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 8px;
|
|
136
|
+
padding: 10px 14px;
|
|
137
|
+
border: 0;
|
|
138
|
+
// Sits on the strip's own hairline rather than adding a second one.
|
|
139
|
+
border-bottom: 2px solid transparent;
|
|
140
|
+
margin-bottom: -1px;
|
|
141
|
+
background: transparent;
|
|
142
|
+
font-family: inherit;
|
|
143
|
+
font-size: 14px;
|
|
144
|
+
font-weight: 500;
|
|
145
|
+
line-height: 1.4;
|
|
146
|
+
color: var(--color-text-muted);
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
transition:
|
|
150
|
+
color 0.12s ease,
|
|
151
|
+
border-color 0.12s ease;
|
|
152
|
+
}
|
|
153
|
+
.app-tab:hover:not(:disabled):not(.active) {
|
|
154
|
+
color: var(--color-text);
|
|
155
|
+
}
|
|
156
|
+
.app-tab.active {
|
|
157
|
+
color: var(--color-primary);
|
|
158
|
+
border-bottom-color: var(--color-primary);
|
|
159
|
+
// The active tab carries weight as well as colour — colour alone is a weak
|
|
160
|
+
// signal for anyone who reads the strip at a glance or cannot distinguish it.
|
|
161
|
+
font-weight: 600;
|
|
162
|
+
}
|
|
163
|
+
.app-tab:disabled {
|
|
164
|
+
color: var(--color-text-subtle);
|
|
165
|
+
cursor: not-allowed;
|
|
166
|
+
}
|
|
167
|
+
.app-tab:focus-visible {
|
|
168
|
+
outline: 2px solid var(--color-primary);
|
|
169
|
+
outline-offset: -2px;
|
|
170
|
+
border-radius: var(--radius-sm);
|
|
171
|
+
}
|
|
172
|
+
// A destructive tab reads as destructive whether or not it is the active one.
|
|
173
|
+
.app-tab.danger {
|
|
174
|
+
color: var(--color-danger);
|
|
175
|
+
}
|
|
176
|
+
.app-tab.danger:hover:not(:disabled):not(.active) {
|
|
177
|
+
color: var(--color-danger-active);
|
|
178
|
+
}
|
|
179
|
+
.app-tab.danger.active {
|
|
180
|
+
color: var(--color-danger);
|
|
181
|
+
border-bottom-color: var(--color-danger);
|
|
182
|
+
}
|
|
183
|
+
.app-tab.danger .app-tab-count {
|
|
184
|
+
background: var(--color-danger-soft);
|
|
185
|
+
color: var(--color-danger-strong);
|
|
186
|
+
}
|
|
187
|
+
// Trailing count pill.
|
|
188
|
+
.app-tab-count {
|
|
189
|
+
display: inline-flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
justify-content: center;
|
|
192
|
+
min-width: 20px;
|
|
193
|
+
padding: 0 6px;
|
|
194
|
+
height: 20px;
|
|
195
|
+
border-radius: 999px;
|
|
196
|
+
background: var(--color-bg-muted);
|
|
197
|
+
color: var(--color-text-subtle);
|
|
198
|
+
font-size: 12px;
|
|
199
|
+
font-weight: 600;
|
|
200
|
+
}
|
|
201
|
+
.app-tab.active .app-tab-count {
|
|
202
|
+
background: var(--color-primary-soft);
|
|
203
|
+
color: var(--color-primary);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Wrapper for a self-contained tabs widget (strip + panels), as <app-tabs>
|
|
207
|
+
// renders server-side. React composes the strip and its panels itself.
|
|
208
|
+
.app-tabs-group > .tab-content {
|
|
209
|
+
padding-top: 16px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Pills variant — a segmented control rather than an underline.
|
|
213
|
+
.app-tabs-pills {
|
|
214
|
+
gap: 4px;
|
|
215
|
+
border-bottom: 0;
|
|
216
|
+
}
|
|
217
|
+
.app-tabs-pills .app-tab {
|
|
218
|
+
border-bottom: 0;
|
|
219
|
+
margin-bottom: 0;
|
|
220
|
+
border-radius: var(--radius);
|
|
221
|
+
}
|
|
222
|
+
.app-tabs-pills .app-tab.active {
|
|
223
|
+
background: var(--color-primary-soft);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Search field ──
|
|
227
|
+
// The search control both surfaces render: a rounded field with a leading
|
|
228
|
+
// magnifier that takes the brand colour on focus, plus a clear button that
|
|
229
|
+
// appears once there's a value.
|
|
230
|
+
.search-wrap {
|
|
231
|
+
position: relative;
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
width: 100%;
|
|
235
|
+
}
|
|
236
|
+
.search-wrap > .search-icon {
|
|
237
|
+
position: absolute;
|
|
238
|
+
left: 13px;
|
|
239
|
+
top: 50%;
|
|
240
|
+
transform: translateY(-50%);
|
|
241
|
+
display: grid;
|
|
242
|
+
place-items: center;
|
|
243
|
+
color: var(--color-text-subtle);
|
|
244
|
+
pointer-events: none;
|
|
245
|
+
transition: color 0.12s ease;
|
|
246
|
+
}
|
|
247
|
+
.search-wrap:focus-within > .search-icon {
|
|
248
|
+
color: var(--color-primary);
|
|
249
|
+
}
|
|
250
|
+
.search-field,
|
|
251
|
+
.search-input {
|
|
252
|
+
width: 100%;
|
|
253
|
+
padding-left: 40px;
|
|
254
|
+
padding-right: 36px;
|
|
255
|
+
}
|
|
256
|
+
.search-clear {
|
|
257
|
+
position: absolute;
|
|
258
|
+
right: 8px;
|
|
259
|
+
top: 50%;
|
|
260
|
+
transform: translateY(-50%);
|
|
261
|
+
display: grid;
|
|
262
|
+
place-items: center;
|
|
263
|
+
width: 26px;
|
|
264
|
+
height: 26px;
|
|
265
|
+
padding: 0;
|
|
266
|
+
border: 0;
|
|
267
|
+
border-radius: 999px;
|
|
268
|
+
background: transparent;
|
|
269
|
+
color: var(--color-text-subtle);
|
|
270
|
+
cursor: pointer;
|
|
271
|
+
opacity: 0;
|
|
272
|
+
pointer-events: none;
|
|
273
|
+
transition:
|
|
274
|
+
opacity 0.12s ease,
|
|
275
|
+
color 0.12s ease;
|
|
276
|
+
}
|
|
277
|
+
.search-wrap.has-value .search-clear,
|
|
278
|
+
.search-wrap:focus-within .search-clear {
|
|
279
|
+
opacity: 1;
|
|
280
|
+
pointer-events: auto;
|
|
281
|
+
}
|
|
282
|
+
.search-clear:hover {
|
|
283
|
+
color: var(--color-text);
|
|
284
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// Modal, confirm dialog.
|
|
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
|
+
// ── Modal ──
|
|
9
|
+
// Chrome stays on `.lx-modal-*`. The shell is react-bootstrap <Modal>:
|
|
10
|
+
// .modal.lx-modal-shell ← className (layout container)
|
|
11
|
+
// .modal-backdrop.lx-modal-dim ← backdropClassName (dim only)
|
|
12
|
+
// .modal-dialog.lx-rb-dialog ← dialogClassName
|
|
13
|
+
// .modal-content.lx-modal ← contentClassName (the card)
|
|
14
|
+
// Do NOT put layout on backdropClassName — that targets a sibling, not
|
|
15
|
+
// .modal.
|
|
16
|
+
.modal-backdrop.lx-modal-dim {
|
|
17
|
+
background: rgba(15, 23, 42, 0.45);
|
|
18
|
+
z-index: 1500;
|
|
19
|
+
}
|
|
20
|
+
.modal.lx-modal-shell {
|
|
21
|
+
--bs-modal-bg: transparent;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
z-index: 1505;
|
|
25
|
+
padding: 20px;
|
|
26
|
+
}
|
|
27
|
+
// `display: flex` ONLY while the dialog is actually open.
|
|
28
|
+
//
|
|
29
|
+
// This was `display: flex !important` on the rule above, which beat Bootstrap's
|
|
30
|
+
// `.modal { display: none }` — so a CLOSED <app-modal> stayed laid out as a
|
|
31
|
+
// full-viewport flex container and silently swallowed every click on the page behind
|
|
32
|
+
// it. React never hit it (react-bootstrap mounts the dialog only when open), so it
|
|
33
|
+
// only ever broke Razor, on any page that declared a modal.
|
|
34
|
+
//
|
|
35
|
+
// Both stacks open a modal by setting `display: block` inline (Bootstrap's JS and
|
|
36
|
+
// react-bootstrap alike), which is what this keys on — it covers the whole opening
|
|
37
|
+
// transition, not just the settled `.show` state. Specificity (0,3,0) beats `.modal`,
|
|
38
|
+
// so no !important is needed.
|
|
39
|
+
.modal.lx-modal-shell[style*="display: block"] {
|
|
40
|
+
display: flex;
|
|
41
|
+
}
|
|
42
|
+
// RB dialog shell: no Bootstrap max-width/margin so `.lx-modal` owns sizing.
|
|
43
|
+
.modal.lx-modal-shell .modal-dialog.lx-rb-dialog {
|
|
44
|
+
margin: 0;
|
|
45
|
+
max-width: none;
|
|
46
|
+
width: auto;
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
}
|
|
52
|
+
.modal.lx-modal-shell .modal-dialog.lx-rb-dialog .modal-content {
|
|
53
|
+
pointer-events: auto;
|
|
54
|
+
border: none;
|
|
55
|
+
// Lift off the dim backdrop: a hairline ring defines the edge (carries
|
|
56
|
+
// elevation in dark mode where a drop shadow barely reads) + a soft,
|
|
57
|
+
// deep drop shadow.
|
|
58
|
+
box-shadow:
|
|
59
|
+
0 0 0 1px color-mix(in srgb, var(--color-text) 10%, transparent),
|
|
60
|
+
var(--shadow-overlay-lg);
|
|
61
|
+
background: var(--color-surface);
|
|
62
|
+
}
|
|
63
|
+
// `--lx-modal-max-width` is how a caller sets an explicit width. It is set on the
|
|
64
|
+
// `.modal` shell by BOTH surfaces and inherits down to the card, because the card is
|
|
65
|
+
// the element the width has to land on: `.modal-dialog` here is `width: auto`, so a
|
|
66
|
+
// max-width there does nothing while `.lx-modal`'s own 440px still clamps the card.
|
|
67
|
+
// React used to write the value straight onto `.modal-content` from JS and Razor put
|
|
68
|
+
// it on `.modal-dialog`, so the same `maxWidth="800"` produced two different dialogs.
|
|
69
|
+
.lx-modal {
|
|
70
|
+
background: var(--color-surface);
|
|
71
|
+
border-radius: var(--radius-xl);
|
|
72
|
+
corner-shape: squircle;
|
|
73
|
+
border: none;
|
|
74
|
+
width: 100%;
|
|
75
|
+
max-width: var(--lx-modal-max-width, 440px);
|
|
76
|
+
max-height: calc(100vh - 40px);
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
}
|
|
81
|
+
.lx-modal-wide {
|
|
82
|
+
max-width: var(--lx-modal-max-width, 1240px);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Side drawer — the dialog docks to the right edge at 30% of the viewport
|
|
86
|
+
// and slides in, instead of sitting centred. Same chrome, different
|
|
87
|
+
// geometry.
|
|
88
|
+
.modal.lx-modal-shell.is-side {
|
|
89
|
+
align-items: stretch;
|
|
90
|
+
justify-content: flex-end;
|
|
91
|
+
padding: 0;
|
|
92
|
+
}
|
|
93
|
+
.modal.lx-modal-shell.is-side .modal-dialog.lx-rb-dialog {
|
|
94
|
+
height: 100%;
|
|
95
|
+
width: auto;
|
|
96
|
+
max-width: none;
|
|
97
|
+
align-items: stretch;
|
|
98
|
+
justify-content: flex-end;
|
|
99
|
+
}
|
|
100
|
+
.lx-modal-side {
|
|
101
|
+
width: 30vw;
|
|
102
|
+
min-width: 420px;
|
|
103
|
+
max-width: none;
|
|
104
|
+
max-height: 100vh;
|
|
105
|
+
height: 100vh;
|
|
106
|
+
border-radius: 0;
|
|
107
|
+
corner-shape: normal;
|
|
108
|
+
animation: lx-drawer-in 0.32s cubic-bezier(0.22, 1, 0.36, 1);
|
|
109
|
+
}
|
|
110
|
+
@media (max-width: 991.98px) {
|
|
111
|
+
.lx-modal-side {
|
|
112
|
+
width: 70vw;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
@media (max-width: 575.98px) {
|
|
116
|
+
.lx-modal-side {
|
|
117
|
+
width: 100vw;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
@keyframes lx-drawer-in {
|
|
121
|
+
from {
|
|
122
|
+
transform: translateX(100%);
|
|
123
|
+
}
|
|
124
|
+
to {
|
|
125
|
+
transform: translateX(0);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
@media (prefers-reduced-motion: reduce) {
|
|
129
|
+
.lx-modal-side {
|
|
130
|
+
animation: none;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
.lx-modal-header {
|
|
134
|
+
flex: 0 0 auto;
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
justify-content: space-between;
|
|
138
|
+
gap: 12px;
|
|
139
|
+
padding: 16px 20px;
|
|
140
|
+
border-bottom: 1px solid var(--color-border);
|
|
141
|
+
}
|
|
142
|
+
.lx-modal-body {
|
|
143
|
+
flex: 1 1 auto;
|
|
144
|
+
min-height: 0;
|
|
145
|
+
overflow-y: auto;
|
|
146
|
+
overscroll-behavior: contain;
|
|
147
|
+
scroll-behavior: smooth;
|
|
148
|
+
-webkit-overflow-scrolling: touch;
|
|
149
|
+
scrollbar-width: thin;
|
|
150
|
+
scrollbar-color: var(--color-border-strong) transparent;
|
|
151
|
+
padding: 20px 22px 24px;
|
|
152
|
+
}
|
|
153
|
+
.lx-modal-body::-webkit-scrollbar {
|
|
154
|
+
width: 8px;
|
|
155
|
+
}
|
|
156
|
+
.lx-modal-body::-webkit-scrollbar-track {
|
|
157
|
+
background: transparent;
|
|
158
|
+
}
|
|
159
|
+
.lx-modal-body::-webkit-scrollbar-thumb {
|
|
160
|
+
background: transparent;
|
|
161
|
+
border-radius: 999px;
|
|
162
|
+
border: 2px solid transparent;
|
|
163
|
+
background-clip: padding-box;
|
|
164
|
+
}
|
|
165
|
+
.lx-modal-body:hover::-webkit-scrollbar-thumb {
|
|
166
|
+
background: var(--color-border-strong);
|
|
167
|
+
background-clip: padding-box;
|
|
168
|
+
}
|
|
169
|
+
@media (prefers-reduced-motion: reduce) {
|
|
170
|
+
.lx-modal-body {
|
|
171
|
+
scroll-behavior: auto;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Modal heading group: optional contextual (Bulk) icon tile + title/subtitle.
|
|
175
|
+
.lx-modal-heading {
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
gap: 10px;
|
|
179
|
+
min-width: 0;
|
|
180
|
+
}
|
|
181
|
+
.lx-modal-headtext {
|
|
182
|
+
min-width: 0;
|
|
183
|
+
}
|
|
184
|
+
.lx-modal-headicon {
|
|
185
|
+
display: inline-flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
justify-content: center;
|
|
188
|
+
width: 34px;
|
|
189
|
+
height: 34px;
|
|
190
|
+
border-radius: var(--radius);
|
|
191
|
+
corner-shape: squircle;
|
|
192
|
+
flex: none;
|
|
193
|
+
background: var(--color-primary-soft);
|
|
194
|
+
color: var(--color-primary);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Break long unbroken names so they wrap within the heading instead of
|
|
198
|
+
// running under the close button.
|
|
199
|
+
.lx-modal-title {
|
|
200
|
+
font-size: 20px;
|
|
201
|
+
font-weight: 600;
|
|
202
|
+
color: var(--color-text);
|
|
203
|
+
overflow-wrap: anywhere;
|
|
204
|
+
}
|
|
205
|
+
.lx-modal-subtitle {
|
|
206
|
+
margin-top: 1px;
|
|
207
|
+
font-size: 12px;
|
|
208
|
+
font-weight: 400;
|
|
209
|
+
color: var(--color-text-muted);
|
|
210
|
+
overflow-wrap: anywhere;
|
|
211
|
+
}
|
|
212
|
+
.lx-form-section {
|
|
213
|
+
margin-top: 24px;
|
|
214
|
+
}
|
|
215
|
+
.lx-form-section:first-child {
|
|
216
|
+
margin-top: 0;
|
|
217
|
+
}
|
|
218
|
+
// Stretch controls to fill the column even though .field aligns to
|
|
219
|
+
// flex-start.
|
|
220
|
+
.lx-modal-body .field > .input,
|
|
221
|
+
.lx-modal-body .field > .search-select,
|
|
222
|
+
.lx-modal-body .field > .input-date-wrap,
|
|
223
|
+
.lx-modal-body .field > .datepicker,
|
|
224
|
+
.lx-modal-body .field > .phone-input-wrap,
|
|
225
|
+
.lx-modal-body .field > .field-error {
|
|
226
|
+
align-self: stretch;
|
|
227
|
+
}
|
|
228
|
+
.lx-form-section-head {
|
|
229
|
+
margin-bottom: 16px;
|
|
230
|
+
}
|
|
231
|
+
.lx-form-section-title {
|
|
232
|
+
font-size: 16px;
|
|
233
|
+
font-weight: 600;
|
|
234
|
+
text-transform: uppercase;
|
|
235
|
+
letter-spacing: 0.04em;
|
|
236
|
+
color: var(--color-text-subtle);
|
|
237
|
+
}
|
|
238
|
+
.lx-modal-footer {
|
|
239
|
+
flex: 0 0 auto;
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
justify-content: flex-end;
|
|
243
|
+
gap: 10px;
|
|
244
|
+
padding: 16px 22px;
|
|
245
|
+
border-top: 1px solid var(--color-border);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Modal close — a squircle at rest that morphs into a circle on hover while
|
|
249
|
+
// taking a danger tint, so dismissing reads as the destructive-ish gesture
|
|
250
|
+
// it is. Slower on the way in than the way out.
|
|
251
|
+
.lx-modal-close {
|
|
252
|
+
margin-left: auto;
|
|
253
|
+
display: grid;
|
|
254
|
+
place-items: center;
|
|
255
|
+
flex: none;
|
|
256
|
+
width: 36px;
|
|
257
|
+
height: 36px;
|
|
258
|
+
padding: 0;
|
|
259
|
+
border: none;
|
|
260
|
+
cursor: pointer;
|
|
261
|
+
line-height: 1;
|
|
262
|
+
background: var(--color-bg-subtle);
|
|
263
|
+
color: var(--color-text-muted);
|
|
264
|
+
border-radius: 999px;
|
|
265
|
+
corner-shape: squircle;
|
|
266
|
+
transition:
|
|
267
|
+
background 180ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
268
|
+
color 180ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
269
|
+
corner-shape 180ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
270
|
+
}
|
|
271
|
+
.lx-modal-close:hover {
|
|
272
|
+
background: var(--color-danger-soft);
|
|
273
|
+
color: var(--color-danger);
|
|
274
|
+
corner-shape: round;
|
|
275
|
+
transition:
|
|
276
|
+
background 280ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
277
|
+
color 280ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
278
|
+
corner-shape 280ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
279
|
+
}
|
|
280
|
+
.lx-modal-close:focus-visible {
|
|
281
|
+
outline: 2px solid var(--color-primary);
|
|
282
|
+
outline-offset: 2px;
|
|
283
|
+
}
|
|
284
|
+
// The shape morph is decorative; hold the colour cue for reduced-motion
|
|
285
|
+
// users.
|
|
286
|
+
@media (prefers-reduced-motion: reduce) {
|
|
287
|
+
.lx-modal-close,
|
|
288
|
+
.lx-modal-close:hover {
|
|
289
|
+
corner-shape: squircle;
|
|
290
|
+
transition:
|
|
291
|
+
background 1ms linear,
|
|
292
|
+
color 1ms linear;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Confirm dialog ──
|
|
297
|
+
// Confirmation dialog — centred status icon + title + message, two equal
|
|
298
|
+
// side-by-side CTAs. Runs headerless (Modal hideHeader), so the body
|
|
299
|
+
// carries the spacing and the footer drops its divider.
|
|
300
|
+
// The confirm card's width belongs to the theme, not to every caller passing
|
|
301
|
+
// maxWidth={440}. Both surfaces used to declare it and Razor's landed on the wrong
|
|
302
|
+
// element, so the dialogs were different widths.
|
|
303
|
+
.lx-confirm {
|
|
304
|
+
max-width: var(--lx-modal-max-width, 440px);
|
|
305
|
+
}
|
|
306
|
+
.lx-confirm-content {
|
|
307
|
+
display: flex;
|
|
308
|
+
flex-direction: column;
|
|
309
|
+
align-items: center;
|
|
310
|
+
text-align: center;
|
|
311
|
+
padding: 16px 12px 6px;
|
|
312
|
+
}
|
|
313
|
+
.lx-confirm-icon {
|
|
314
|
+
line-height: 0;
|
|
315
|
+
margin-bottom: 22px;
|
|
316
|
+
}
|
|
317
|
+
.lx-confirm-title {
|
|
318
|
+
margin: 0 0 10px;
|
|
319
|
+
font-size: 22px;
|
|
320
|
+
font-weight: 700;
|
|
321
|
+
letter-spacing: -0.01em;
|
|
322
|
+
color: var(--color-text);
|
|
323
|
+
}
|
|
324
|
+
.lx-confirm-msg {
|
|
325
|
+
margin: 0;
|
|
326
|
+
font-size: 15px;
|
|
327
|
+
line-height: 1.55;
|
|
328
|
+
color: var(--color-text-muted);
|
|
329
|
+
max-width: 40ch;
|
|
330
|
+
}
|
|
331
|
+
.lx-confirm-actions {
|
|
332
|
+
display: flex;
|
|
333
|
+
gap: 12px;
|
|
334
|
+
width: 100%;
|
|
335
|
+
}
|
|
336
|
+
.lx-modal-footer:has(.lx-confirm-actions) {
|
|
337
|
+
border-top: none;
|
|
338
|
+
padding-top: 0;
|
|
339
|
+
}
|
|
340
|
+
.lx-confirm-actions .btn {
|
|
341
|
+
flex: 1;
|
|
342
|
+
min-height: var(--wui-control-height);
|
|
343
|
+
height: var(--wui-control-height);
|
|
344
|
+
border-radius: var(--radius);
|
|
345
|
+
corner-shape: squircle;
|
|
346
|
+
padding-left: 16px;
|
|
347
|
+
padding-right: 16px;
|
|
348
|
+
font-size: 14px;
|
|
349
|
+
font-weight: 500;
|
|
350
|
+
}
|