@webority/theme 0.7.0 → 0.7.2
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 +54 -1
- package/scss/_tokens.scss +41 -0
- package/scss/_wui-aliases.scss +3 -0
- package/scss/_wui-components.scss +1 -0
- package/scss/wui/_card.scss +1 -0
- package/scss/wui/_choice.scss +8 -3
- package/scss/wui/_controls.scss +16 -3
- package/scss/wui/_data.scss +40 -0
- package/scss/wui/_display.scss +41 -4
- package/scss/wui/_media.scss +228 -10
- package/scss/wui/_navigation.scss +19 -1
- package/scss/wui/_overlays.scss +36 -52
- package/scss/wui/_scrollbar.scss +32 -0
- package/scss/wui/_shell.scss +222 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webority/theme",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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
|
@@ -49,8 +49,11 @@
|
|
|
49
49
|
// ---- Display heading -------------------------------------------------
|
|
50
50
|
// Bootstrap's .display-1..6 ladder skews bigger than our locked design.
|
|
51
51
|
// .h-display drops in where the marketing hero needs the locked
|
|
52
|
-
// font-weight 500 + tight letter-spacing.
|
|
52
|
+
// font-weight 500 + tight letter-spacing. font-family reads --wui-font-display
|
|
53
|
+
// so a product can swap in a serif/brand display face by overriding just that
|
|
54
|
+
// token (default: the same body stack .font-display already uses).
|
|
53
55
|
.h-display {
|
|
56
|
+
font-family: var(--wui-font-display, var(--bs-body-font-family));
|
|
54
57
|
font-weight: 500;
|
|
55
58
|
letter-spacing: -0.03em;
|
|
56
59
|
line-height: 1.05;
|
|
@@ -89,6 +92,14 @@
|
|
|
89
92
|
--bs-btn-hover-border-color: var(--wui-primary-pressed);
|
|
90
93
|
--bs-btn-active-bg: var(--wui-primary-pressed);
|
|
91
94
|
--bs-btn-active-border-color: var(--wui-primary-pressed);
|
|
95
|
+
// Bootstrap computes this label colour at SCSS compile time from the library's
|
|
96
|
+
// default $primary, so it can never react to a product overriding --wui-primary as a
|
|
97
|
+
// runtime CSS var. Routing it through the token makes a light-primary brand's
|
|
98
|
+
// --wui-primary-text override reach the button.
|
|
99
|
+
--bs-btn-color: var(--wui-primary-text);
|
|
100
|
+
--bs-btn-hover-color: var(--wui-primary-text);
|
|
101
|
+
--bs-btn-active-color: var(--wui-primary-text);
|
|
102
|
+
--bs-btn-disabled-color: var(--wui-primary-text);
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
.btn-danger {
|
|
@@ -110,6 +121,24 @@
|
|
|
110
121
|
--bs-btn-disabled-border-color: var(--wui-danger-strong);
|
|
111
122
|
}
|
|
112
123
|
|
|
124
|
+
// Themed disabled state (fleet default). Bootstrap's own disabled look is just the
|
|
125
|
+
// variant's colour at --bs-btn-disabled-opacity (.65) — barely different from the active
|
|
126
|
+
// button on a tinted brand palette. Give the ordinary case a real desaturated/muted look
|
|
127
|
+
// instead, reusing the same tokens .input:disabled already uses so a disabled button and
|
|
128
|
+
// a disabled field read as the same state.
|
|
129
|
+
//
|
|
130
|
+
// Excludes .btn-primary and .btn-danger, which already theme their OWN disabled state
|
|
131
|
+
// above (a faded version of their own fill, driven by --bs-btn-disabled-*) — :not() keeps
|
|
132
|
+
// this rule from fighting theirs rather than relying on source order.
|
|
133
|
+
.btn:disabled:not(.btn-primary):not(.btn-danger),
|
|
134
|
+
.btn.disabled:not(.btn-primary):not(.btn-danger) {
|
|
135
|
+
background-color: var(--color-bg-muted);
|
|
136
|
+
border-color: var(--color-border);
|
|
137
|
+
color: var(--color-text-subtle);
|
|
138
|
+
opacity: 1;
|
|
139
|
+
cursor: not-allowed;
|
|
140
|
+
}
|
|
141
|
+
|
|
113
142
|
|
|
114
143
|
// ---- Component radii from tokens ------------------------------------
|
|
115
144
|
// .btn/.card/.input capture Bootstrap's compiled $*-border-radius (the library
|
|
@@ -265,6 +294,30 @@
|
|
|
265
294
|
}
|
|
266
295
|
|
|
267
296
|
|
|
297
|
+
// ---- Ghost button (transparent, brand-tinted hover) ------------------
|
|
298
|
+
// A real ghost button: transparent at rest (no fill, no border), current text colour;
|
|
299
|
+
// only the background lights up — a soft brand tint, the same recipe as a
|
|
300
|
+
// .dropdown-item's hover — on hover/active. AppButton's `ghost` variant emits this class
|
|
301
|
+
// on both surfaces; distinct from Bootstrap's .btn-link, which reads as a hyperlink
|
|
302
|
+
// (underlined, link-coloured text) rather than a quiet button. Marketing anchors reach
|
|
303
|
+
// for it directly for the same look on a plain <a>.
|
|
304
|
+
//
|
|
305
|
+
// No .btn-ghost-sm/-lg rungs: unlike .btn-pill (a bespoke marketing sizing system), this
|
|
306
|
+
// is an ordinary .btn variant, so it sizes the same way .btn-primary/.btn-danger do —
|
|
307
|
+
// Bootstrap's own .btn-sm/.btn-lg.
|
|
308
|
+
.btn-ghost {
|
|
309
|
+
--bs-btn-color: var(--color-text);
|
|
310
|
+
--bs-btn-bg: transparent;
|
|
311
|
+
--bs-btn-border-color: transparent;
|
|
312
|
+
--bs-btn-hover-color: var(--color-text);
|
|
313
|
+
--bs-btn-hover-bg: var(--wui-primary-5);
|
|
314
|
+
--bs-btn-hover-border-color: transparent;
|
|
315
|
+
--bs-btn-active-color: var(--color-text);
|
|
316
|
+
--bs-btn-active-bg: var(--wui-primary-10);
|
|
317
|
+
--bs-btn-active-border-color: transparent;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
268
321
|
// ---- Card "lifted" (shadowless) -------------------------------------
|
|
269
322
|
// Previously a soft drop-shadow card; now flat. The `.card-lifted`
|
|
270
323
|
// class is preserved so existing call sites don't break — it now
|
package/scss/_tokens.scss
CHANGED
|
@@ -119,6 +119,15 @@
|
|
|
119
119
|
--wui-container-max: 1512px;
|
|
120
120
|
--wui-container-pad: 24px;
|
|
121
121
|
|
|
122
|
+
// ---- Primary "on-fill" text --------------------------------------
|
|
123
|
+
// Unlike the semantic -text tokens below (a legible STANDALONE colour derived
|
|
124
|
+
// from a fill, for using the status colour AS body text), this is the text/icon
|
|
125
|
+
// colour to use ON TOP OF a solid --wui-primary fill — a selected datepicker day,
|
|
126
|
+
// an active pagination page, a checked segmented option, .btn-primary's label.
|
|
127
|
+
// Default assumes primary is dark enough for white; a brand with a light primary
|
|
128
|
+
// overrides this to a dark value instead.
|
|
129
|
+
--wui-primary-text: #fff;
|
|
130
|
+
|
|
122
131
|
// ---- Semantic status ramps -------------------------------------
|
|
123
132
|
// Canonical info/success/warning/danger scales: `active` (text/icon on
|
|
124
133
|
// light), the base `100` (vivid fills/dots), then tints 50/30/20/10/5.
|
|
@@ -189,6 +198,38 @@
|
|
|
189
198
|
--wui-dark-text-subtle: #8a90a0;
|
|
190
199
|
--wui-dark-primary: #6B83EC;
|
|
191
200
|
--wui-dark-primary-hover:#93A6F2;
|
|
201
|
+
|
|
202
|
+
// ---- Fluid type scale --------------------------------------------
|
|
203
|
+
// clamp(min, preferred, max) so type scales smoothly between viewports instead of
|
|
204
|
+
// stepping at Bootstrap's breakpoints. `base` sits close to Bootstrap's own 1rem body
|
|
205
|
+
// size; every other step is derived at a consistent ratio, up and down. Additive only —
|
|
206
|
+
// nothing in the shared theme consumes these yet, a product opts in per element.
|
|
207
|
+
--wui-fs-xs: clamp(0.75rem, 0.73rem + 0.1vw, 0.8125rem);
|
|
208
|
+
--wui-fs-sm: clamp(0.8125rem, 0.79rem + 0.12vw, 0.875rem);
|
|
209
|
+
--wui-fs-base: clamp(0.95rem, 0.9rem + 0.25vw, 1.05rem);
|
|
210
|
+
--wui-fs-lg: clamp(1.05rem, 0.98rem + 0.35vw, 1.25rem);
|
|
211
|
+
--wui-fs-xl: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
|
|
212
|
+
--wui-fs-2xl: clamp(1.5rem, 1.3rem + 1vw, 2rem);
|
|
213
|
+
--wui-fs-3xl: clamp(1.875rem, 1.5rem + 1.75vw, 2.75rem);
|
|
214
|
+
|
|
215
|
+
// ---- Display font slot --------------------------------------------
|
|
216
|
+
// A dedicated slot for a display/heading typeface, kept separate from
|
|
217
|
+
// $font-family-base so a product can point ONLY its display headings (.h-display) at a
|
|
218
|
+
// serif or another brand face by overriding this one token, without touching body copy.
|
|
219
|
+
// Defaults to the same stack .font-display already uses.
|
|
220
|
+
--wui-font-display: var(--bs-body-font-family);
|
|
221
|
+
|
|
222
|
+
// ---- Z-index scale -------------------------------------------------
|
|
223
|
+
// Bootstrap's own ladder ($zindex-dropdown etc.), exposed as CSS vars so a component
|
|
224
|
+
// or a consuming product can layer against the same scale instead of inventing values.
|
|
225
|
+
--wui-z-dropdown: 1000;
|
|
226
|
+
--wui-z-sticky: 1020;
|
|
227
|
+
--wui-z-fixed: 1030;
|
|
228
|
+
--wui-z-modal-backdrop: 1040;
|
|
229
|
+
--wui-z-modal: 1050;
|
|
230
|
+
--wui-z-popover: 1060;
|
|
231
|
+
--wui-z-tooltip: 1070;
|
|
232
|
+
--wui-z-toast: 1080;
|
|
192
233
|
}
|
|
193
234
|
|
|
194
235
|
// Bootstrap md / xl (not invented px)
|
package/scss/_wui-aliases.scss
CHANGED
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
--color-primary: var(--wui-primary);
|
|
37
37
|
--color-primary-hover: var(--wui-primary-pressed);
|
|
38
38
|
--color-primary-soft: var(--wui-primary-10);
|
|
39
|
+
// Text/icon colour for content sitting ON a solid --color-primary fill — see
|
|
40
|
+
// --wui-primary-text for why this is a different axis from the -text tokens below.
|
|
41
|
+
--color-primary-text: var(--wui-primary-text);
|
|
39
42
|
|
|
40
43
|
// Semantic status ramps
|
|
41
44
|
--color-info-active: var(--wui-info-active);
|
package/scss/wui/_card.scss
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// pattern nearly every panel header uses, so it belongs here rather than being
|
|
12
12
|
// re-flexed with utilities on each one.
|
|
13
13
|
.card-header {
|
|
14
|
+
--bs-card-cap-bg: var(--color-surface);
|
|
14
15
|
display: flex;
|
|
15
16
|
align-items: center;
|
|
16
17
|
justify-content: space-between;
|
package/scss/wui/_choice.scss
CHANGED
|
@@ -415,6 +415,11 @@
|
|
|
415
415
|
color: var(--color-primary);
|
|
416
416
|
font-weight: 500;
|
|
417
417
|
}
|
|
418
|
+
// allow-custom's freeform row — reads as an action ("Add "…"") rather than a listed value.
|
|
419
|
+
.search-select-option-custom {
|
|
420
|
+
font-style: italic;
|
|
421
|
+
color: var(--color-primary);
|
|
422
|
+
}
|
|
418
423
|
.search-select-empty {
|
|
419
424
|
padding: 12px 10px;
|
|
420
425
|
font-size: 13px;
|
|
@@ -627,7 +632,7 @@ button.datepicker-month:hover {
|
|
|
627
632
|
}
|
|
628
633
|
.datepicker-day.is-selected {
|
|
629
634
|
background: var(--color-primary);
|
|
630
|
-
color:
|
|
635
|
+
color: var(--color-primary-text);
|
|
631
636
|
font-weight: 600;
|
|
632
637
|
}
|
|
633
638
|
.datepicker-day:disabled {
|
|
@@ -696,7 +701,7 @@ button.datepicker-month:hover {
|
|
|
696
701
|
}
|
|
697
702
|
.datepicker-cell.is-selected {
|
|
698
703
|
background: var(--color-primary);
|
|
699
|
-
color:
|
|
704
|
+
color: var(--color-primary-text);
|
|
700
705
|
font-weight: 600;
|
|
701
706
|
}
|
|
702
707
|
.datepicker-cell:disabled {
|
|
@@ -842,7 +847,7 @@ button.datepicker-month:hover {
|
|
|
842
847
|
.datepicker-day.is-range-start,
|
|
843
848
|
.datepicker-day.is-range-end {
|
|
844
849
|
background: var(--color-primary);
|
|
845
|
-
color:
|
|
850
|
+
color: var(--color-primary-text);
|
|
846
851
|
font-weight: 600;
|
|
847
852
|
}
|
|
848
853
|
.daterange-hint {
|
package/scss/wui/_controls.scss
CHANGED
|
@@ -348,9 +348,12 @@
|
|
|
348
348
|
resize: none;
|
|
349
349
|
overflow: hidden;
|
|
350
350
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
.
|
|
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),
|
|
355
|
+
.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) {
|
|
354
357
|
background-color: var(--color-bg-muted);
|
|
355
358
|
border-color: var(--color-border-strong);
|
|
356
359
|
}
|
|
@@ -889,9 +892,19 @@ wui-select[compact] {
|
|
|
889
892
|
.field > .input-with-unit,
|
|
890
893
|
.field > .input,
|
|
891
894
|
.field > .datepicker,
|
|
895
|
+
.field > .file-input,
|
|
896
|
+
.field > .file-preview,
|
|
892
897
|
.field > .field-error {
|
|
893
898
|
align-self: stretch;
|
|
894
899
|
}
|
|
900
|
+
// A file picker fills its column by default (align-self:stretch fills the field on the cross
|
|
901
|
+
// axis, but the field itself collapses to its content when it is a flex item — the showcase
|
|
902
|
+
// lays fields out in a flex row — so stretch the field to its container first). Override per
|
|
903
|
+
// use with a .file-w-* class or the --wui-file-width variable.
|
|
904
|
+
.field:has(> .file-input),
|
|
905
|
+
.field:has(> .file-preview) {
|
|
906
|
+
width: var(--wui-file-width, 100%);
|
|
907
|
+
}
|
|
895
908
|
// Doubled selectors — see the lg-tier block below: a media query carries no
|
|
896
909
|
// extra specificity, so a consumer's later single-class rule would win.
|
|
897
910
|
@media (max-width: 575.98px) {
|
package/scss/wui/_data.scss
CHANGED
|
@@ -407,6 +407,46 @@
|
|
|
407
407
|
cursor: not-allowed;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
// Numbered mode (mode="numbered") — windowed page buttons sitting between the prev/next
|
|
411
|
+
// arrows in .tpg-nav, same 32px circular hit target as .tpg-arrow.
|
|
412
|
+
.tpg-page {
|
|
413
|
+
display: grid;
|
|
414
|
+
place-items: center;
|
|
415
|
+
min-width: 32px;
|
|
416
|
+
height: 32px;
|
|
417
|
+
padding: 0 6px;
|
|
418
|
+
border: none;
|
|
419
|
+
background: transparent;
|
|
420
|
+
color: var(--color-text-muted);
|
|
421
|
+
border-radius: 50%;
|
|
422
|
+
font-size: 13px;
|
|
423
|
+
font-weight: 600;
|
|
424
|
+
font-variant-numeric: tabular-nums;
|
|
425
|
+
text-decoration: none;
|
|
426
|
+
cursor: pointer;
|
|
427
|
+
transition:
|
|
428
|
+
background 0.14s ease,
|
|
429
|
+
color 0.14s ease;
|
|
430
|
+
}
|
|
431
|
+
.tpg-page:hover:not(.is-active) {
|
|
432
|
+
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
|
433
|
+
color: var(--color-primary);
|
|
434
|
+
}
|
|
435
|
+
.tpg-page.is-active {
|
|
436
|
+
background: var(--color-primary);
|
|
437
|
+
color: var(--color-primary-text);
|
|
438
|
+
cursor: default;
|
|
439
|
+
}
|
|
440
|
+
.tpg-ellipsis {
|
|
441
|
+
display: grid;
|
|
442
|
+
place-items: center;
|
|
443
|
+
min-width: 24px;
|
|
444
|
+
height: 32px;
|
|
445
|
+
color: var(--color-text-subtle);
|
|
446
|
+
font-size: 13px;
|
|
447
|
+
user-select: none;
|
|
448
|
+
}
|
|
449
|
+
|
|
410
450
|
// Horizontal scroll guard + flush-fit inside a card. Wrapping the table here
|
|
411
451
|
// also stops `.card > .table` header-corner rounding from applying mid-card.
|
|
412
452
|
.table-wrap {
|
package/scss/wui/_display.scss
CHANGED
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
.kpi-label {
|
|
32
32
|
font-size: 13px;
|
|
33
33
|
font-weight: 500;
|
|
34
|
-
|
|
34
|
+
// --color-text-muted (not -subtle): -subtle is ~4.5:1 on white and dips below AA on the
|
|
35
|
+
// filled/accent variant's tinted surface. -muted clears AA on every kpi variant.
|
|
36
|
+
color: var(--color-text-muted);
|
|
35
37
|
}
|
|
36
38
|
.kpi-value {
|
|
37
39
|
margin-top: 6px;
|
|
@@ -76,6 +78,18 @@
|
|
|
76
78
|
margin-top: 2px;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
// Surface variants — plain (above) is the bordered card; these swap the fill and drop the
|
|
82
|
+
// border so a dashboard can mix a KPI strip with the surrounding card without every tile
|
|
83
|
+
// drawing its own outline.
|
|
84
|
+
.kpi.kpi-filled {
|
|
85
|
+
border-color: transparent;
|
|
86
|
+
background: var(--color-bg-subtle);
|
|
87
|
+
}
|
|
88
|
+
.kpi.kpi-accent {
|
|
89
|
+
border-color: transparent;
|
|
90
|
+
background: var(--color-primary-soft);
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
// A row of tiles. Wraps rather than scrolls — a KPI strip that scrolls hides numbers.
|
|
80
94
|
.kpi-row {
|
|
81
95
|
display: grid;
|
|
@@ -213,11 +227,34 @@ a.info-row:hover .info-value {
|
|
|
213
227
|
.segmented-option:hover {
|
|
214
228
|
color: var(--color-text);
|
|
215
229
|
}
|
|
230
|
+
// A per-option sub-label under the main text — optional, so a segment without one keeps the
|
|
231
|
+
// plain single-line layout above untouched.
|
|
232
|
+
.segmented-option-text {
|
|
233
|
+
display: flex;
|
|
234
|
+
flex-direction: column;
|
|
235
|
+
align-items: flex-start;
|
|
236
|
+
gap: 1px;
|
|
237
|
+
line-height: 1.2;
|
|
238
|
+
}
|
|
239
|
+
.segmented-option-label {
|
|
240
|
+
font: inherit;
|
|
241
|
+
color: inherit;
|
|
242
|
+
}
|
|
243
|
+
.segmented-option-note {
|
|
244
|
+
font-size: 10px;
|
|
245
|
+
font-weight: 400;
|
|
246
|
+
// Inherits the option's current text colour (muted, hover, or white-on-checked) rather
|
|
247
|
+
// than a fixed token, so it always matches without a separate state per variant. No opacity
|
|
248
|
+
// muting — that dropped the sub-label below AA on an unchecked option; the 10px size already
|
|
249
|
+
// distinguishes it from the label.
|
|
250
|
+
color: currentColor;
|
|
251
|
+
}
|
|
216
252
|
.segmented-option:has(.segmented-input:checked) {
|
|
217
253
|
background: var(--color-primary);
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
254
|
+
// Token, not fixed white: the pill is always the brand fill, so its label must
|
|
255
|
+
// contrast with THAT fill, not the page — a light-primary brand overrides
|
|
256
|
+
// --wui-primary-text to a dark value and this follows automatically.
|
|
257
|
+
color: var(--color-primary-text);
|
|
221
258
|
}
|
|
222
259
|
// The ring is drawn on the label because the input it belongs to has no box.
|
|
223
260
|
.segmented-option:has(.segmented-input:focus-visible) {
|
package/scss/wui/_media.scss
CHANGED
|
@@ -183,9 +183,78 @@
|
|
|
183
183
|
stroke: #fff;
|
|
184
184
|
stroke-width: 2px;
|
|
185
185
|
}
|
|
186
|
+
// Compact attached items — used for every file in `multiple` mode and for a single
|
|
187
|
+
// non-image attachment (a big letterboxed preview only makes sense for one image).
|
|
188
|
+
.attach-list {
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
gap: 8px;
|
|
192
|
+
}
|
|
193
|
+
.attach-list:not(:first-child) {
|
|
194
|
+
margin-top: 8px;
|
|
195
|
+
}
|
|
196
|
+
.attach-item {
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
gap: 12px;
|
|
200
|
+
padding: 8px 12px;
|
|
201
|
+
border: 1px solid var(--color-border);
|
|
202
|
+
border-radius: var(--radius-lg);
|
|
203
|
+
background: var(--color-surface);
|
|
204
|
+
min-width: 0;
|
|
205
|
+
}
|
|
206
|
+
.attach-item-media {
|
|
207
|
+
flex: 0 0 auto;
|
|
208
|
+
width: 40px;
|
|
209
|
+
height: 40px;
|
|
210
|
+
display: inline-flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
justify-content: center;
|
|
213
|
+
border-radius: var(--radius);
|
|
214
|
+
overflow: hidden;
|
|
215
|
+
background: var(--color-bg-muted);
|
|
216
|
+
color: var(--color-primary);
|
|
217
|
+
}
|
|
218
|
+
.attach-item-media img {
|
|
219
|
+
width: 100%;
|
|
220
|
+
height: 100%;
|
|
221
|
+
object-fit: cover;
|
|
222
|
+
display: block;
|
|
223
|
+
}
|
|
224
|
+
.attach-item-info {
|
|
225
|
+
display: flex;
|
|
226
|
+
flex-direction: column;
|
|
227
|
+
gap: 2px;
|
|
228
|
+
flex: 1;
|
|
229
|
+
min-width: 0;
|
|
230
|
+
}
|
|
231
|
+
.attach-item-remove {
|
|
232
|
+
flex: 0 0 auto;
|
|
233
|
+
display: inline-flex;
|
|
234
|
+
align-items: center;
|
|
235
|
+
justify-content: center;
|
|
236
|
+
width: 30px;
|
|
237
|
+
height: 30px;
|
|
238
|
+
border: 1px solid var(--color-border);
|
|
239
|
+
border-radius: 999px;
|
|
240
|
+
background: var(--color-surface);
|
|
241
|
+
color: var(--color-text-muted);
|
|
242
|
+
cursor: pointer;
|
|
243
|
+
transition:
|
|
244
|
+
border-color 0.2s,
|
|
245
|
+
color 0.2s,
|
|
246
|
+
background 0.2s;
|
|
247
|
+
}
|
|
248
|
+
.attach-item-remove:hover {
|
|
249
|
+
border-color: var(--color-danger);
|
|
250
|
+
color: var(--color-danger);
|
|
251
|
+
background: var(--color-danger-soft);
|
|
252
|
+
}
|
|
253
|
+
|
|
186
254
|
@media (prefers-reduced-motion: reduce) {
|
|
187
255
|
.attach-drop,
|
|
188
|
-
.attach-remove
|
|
256
|
+
.attach-remove,
|
|
257
|
+
.attach-item-remove {
|
|
189
258
|
transition: none;
|
|
190
259
|
}
|
|
191
260
|
}
|
|
@@ -246,7 +315,11 @@
|
|
|
246
315
|
align-items: center;
|
|
247
316
|
justify-content: center;
|
|
248
317
|
gap: 10px;
|
|
249
|
-
height:
|
|
318
|
+
height: 200px;
|
|
319
|
+
// Fills its column — a full-width drop target. The .field:has() rule below makes the field
|
|
320
|
+
// itself stretch first (the showcase lays fields out in a flex row, where a field otherwise
|
|
321
|
+
// collapses to its content), then align-self:stretch fills the field.
|
|
322
|
+
width: 100%;
|
|
250
323
|
padding: 20px;
|
|
251
324
|
text-align: center;
|
|
252
325
|
background: var(--color-bg-subtle);
|
|
@@ -255,10 +328,79 @@
|
|
|
255
328
|
border-color 0.15s ease,
|
|
256
329
|
background-color 0.15s ease;
|
|
257
330
|
}
|
|
331
|
+
// Hover, focus and drag-over all take the same soft primary-30% border AppInput shows on
|
|
332
|
+
// focus — a clear hue shift from the resting grey dashed border, without the loud full
|
|
333
|
+
// --color-primary the dropzone used before.
|
|
258
334
|
.file-input.file-input-tall:hover,
|
|
335
|
+
.file-input.file-input-tall:focus-within,
|
|
336
|
+
.file-input.file-input-tall:active,
|
|
259
337
|
.file-input.file-input-tall.dragging {
|
|
260
|
-
border-color: var(--color-primary);
|
|
338
|
+
border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
|
|
261
339
|
background: var(--color-bg-muted);
|
|
340
|
+
box-shadow: none;
|
|
341
|
+
}
|
|
342
|
+
// A rejected file (wrong type / too large) turns the dropzone border danger to match the
|
|
343
|
+
// inline reason shown below it.
|
|
344
|
+
.file-input.file-input-tall.error {
|
|
345
|
+
border-color: color-mix(in srgb, var(--color-danger) 55%, var(--color-surface));
|
|
346
|
+
}
|
|
347
|
+
// Multiple mode with files: the box grows to hold the picked-file list INSIDE it, below a
|
|
348
|
+
// compact upload prompt — not a separate list beneath the box. The prompt drops from the
|
|
349
|
+
// absolute-fill centred stack to a static compact row at the top; the list scrolls within.
|
|
350
|
+
.file-input.file-input-tall.has-files {
|
|
351
|
+
height: auto;
|
|
352
|
+
justify-content: flex-start;
|
|
353
|
+
align-items: stretch;
|
|
354
|
+
gap: 14px;
|
|
355
|
+
cursor: default;
|
|
356
|
+
}
|
|
357
|
+
.file-input.file-input-tall.has-files > .file-input-zone-label {
|
|
358
|
+
position: static;
|
|
359
|
+
inset: auto;
|
|
360
|
+
flex-direction: row;
|
|
361
|
+
gap: 10px;
|
|
362
|
+
padding: 4px;
|
|
363
|
+
cursor: pointer;
|
|
364
|
+
}
|
|
365
|
+
.file-input.file-input-tall.has-files .file-input-icon {
|
|
366
|
+
width: 40px;
|
|
367
|
+
height: 40px;
|
|
368
|
+
}
|
|
369
|
+
.file-input.file-input-tall.has-files .file-input-text {
|
|
370
|
+
display: none;
|
|
371
|
+
}
|
|
372
|
+
.file-input.file-input-tall.has-files .attach-list {
|
|
373
|
+
width: 100%;
|
|
374
|
+
max-height: 240px;
|
|
375
|
+
overflow-y: auto;
|
|
376
|
+
// The dropzone sets text-align:center for the centred prompt; the file rows must not
|
|
377
|
+
// inherit it — name and size read left-aligned next to the thumbnail.
|
|
378
|
+
text-align: left;
|
|
379
|
+
}
|
|
380
|
+
// While managing files, keep the border calm on hover/focus (it is no longer just a
|
|
381
|
+
// "click to upload" affordance); still highlight when a file is actually dragged over.
|
|
382
|
+
.file-input.file-input-tall.has-files:hover,
|
|
383
|
+
.file-input.file-input-tall.has-files:focus-within {
|
|
384
|
+
border-color: var(--color-border-strong);
|
|
385
|
+
background: var(--color-bg-subtle);
|
|
386
|
+
}
|
|
387
|
+
.file-input.file-input-tall.has-files.dragging {
|
|
388
|
+
border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
|
|
389
|
+
background: var(--color-bg-muted);
|
|
390
|
+
}
|
|
391
|
+
// Width presets — put a .file-w-* class on the field (React className / Razor class) to cap the
|
|
392
|
+
// dropzone to a fixed width instead of the full column. Mirror of the datepicker .date-w-* set.
|
|
393
|
+
.file-w-sm {
|
|
394
|
+
--wui-file-width: 18rem;
|
|
395
|
+
}
|
|
396
|
+
.file-w-md {
|
|
397
|
+
--wui-file-width: 24rem;
|
|
398
|
+
}
|
|
399
|
+
.file-w-lg {
|
|
400
|
+
--wui-file-width: 30rem;
|
|
401
|
+
}
|
|
402
|
+
.file-w-full {
|
|
403
|
+
--wui-file-width: 100%;
|
|
262
404
|
}
|
|
263
405
|
// Fill-label associates click with the hidden file input; children don't
|
|
264
406
|
// steal drag hits.
|
|
@@ -310,7 +452,7 @@
|
|
|
310
452
|
box-sizing: border-box;
|
|
311
453
|
display: block;
|
|
312
454
|
width: 100%;
|
|
313
|
-
height:
|
|
455
|
+
height: 200px;
|
|
314
456
|
border-radius: var(--radius-lg);
|
|
315
457
|
corner-shape: squircle;
|
|
316
458
|
border: 1px solid var(--color-border);
|
|
@@ -417,6 +559,10 @@
|
|
|
417
559
|
// Round dropzone uploader (workspace/brand logo) and the settings-page photo
|
|
418
560
|
// ring share this same interactive-uploader pattern.
|
|
419
561
|
.set-uploader {
|
|
562
|
+
// One knob for the whole control — the ring diameter. Presets and the numeric `size`
|
|
563
|
+
// prop set it (inline); the ring, initials font and icons all derive from it, so any
|
|
564
|
+
// px value scales the picker proportionally. md (120px) is the default.
|
|
565
|
+
--wui-logo-size: 120px;
|
|
420
566
|
display: inline-flex;
|
|
421
567
|
padding: 0;
|
|
422
568
|
border: none;
|
|
@@ -436,10 +582,10 @@
|
|
|
436
582
|
}
|
|
437
583
|
.set-uploader-ring {
|
|
438
584
|
position: relative;
|
|
439
|
-
width:
|
|
440
|
-
height:
|
|
585
|
+
width: var(--wui-logo-size);
|
|
586
|
+
height: var(--wui-logo-size);
|
|
441
587
|
border-radius: 50%;
|
|
442
|
-
padding:
|
|
588
|
+
padding: 5px;
|
|
443
589
|
border: 1px dashed color-mix(in srgb, var(--color-border-strong) 75%, transparent);
|
|
444
590
|
cursor: pointer;
|
|
445
591
|
transition: border-color 0.18s;
|
|
@@ -455,15 +601,21 @@
|
|
|
455
601
|
border-radius: 50%;
|
|
456
602
|
display: grid;
|
|
457
603
|
place-items: center;
|
|
458
|
-
|
|
604
|
+
// Initials scale with the ring (~32% of the diameter reads well at every size).
|
|
605
|
+
font-size: calc(var(--wui-logo-size) * 0.32);
|
|
459
606
|
font-weight: 700;
|
|
460
607
|
letter-spacing: -0.02em;
|
|
461
608
|
color: var(--color-primary);
|
|
462
609
|
background: var(--color-primary-soft);
|
|
463
610
|
}
|
|
611
|
+
// Avatar variant — the standard avatar disc (muted fill) instead of the soft-primary
|
|
612
|
+
// initials plate; used with a person icon when no initials are given.
|
|
613
|
+
.set-uploader--avatar .set-uploader-logo {
|
|
614
|
+
background: color-mix(in srgb, var(--color-primary) 15%, var(--color-surface));
|
|
615
|
+
}
|
|
464
616
|
.set-uploader-overlay {
|
|
465
617
|
position: absolute;
|
|
466
|
-
inset:
|
|
618
|
+
inset: 5px;
|
|
467
619
|
border-radius: 50%;
|
|
468
620
|
display: flex;
|
|
469
621
|
flex-direction: column;
|
|
@@ -491,12 +643,78 @@
|
|
|
491
643
|
cursor: progress;
|
|
492
644
|
}
|
|
493
645
|
.set-uploader-hint {
|
|
494
|
-
margin:
|
|
646
|
+
margin: 12px 0 0;
|
|
495
647
|
max-width: 240px;
|
|
496
648
|
font-size: 12px;
|
|
497
649
|
line-height: 1.5;
|
|
498
650
|
color: var(--color-text-subtle);
|
|
499
651
|
}
|
|
652
|
+
// The picker field sizes to its content (the ring) rather than stretching to the
|
|
653
|
+
// form width the way a text field does.
|
|
654
|
+
.set-uploader-field {
|
|
655
|
+
gap: 8px;
|
|
656
|
+
width: max-content;
|
|
657
|
+
max-width: 100%;
|
|
658
|
+
}
|
|
659
|
+
// Positioning context for the remove control, which is a sibling of the button.
|
|
660
|
+
.set-uploader-shell {
|
|
661
|
+
position: relative;
|
|
662
|
+
display: inline-block;
|
|
663
|
+
}
|
|
664
|
+
// Ring highlights while a file is dragged over it.
|
|
665
|
+
.set-uploader.is-dragging .set-uploader-ring {
|
|
666
|
+
border-color: var(--color-primary);
|
|
667
|
+
background: var(--color-primary-soft);
|
|
668
|
+
}
|
|
669
|
+
// A plain disabled (not busy) picker: dim it and drop the pointer affordance.
|
|
670
|
+
.set-uploader:disabled:not([aria-busy="true"]) {
|
|
671
|
+
cursor: not-allowed;
|
|
672
|
+
opacity: 0.55;
|
|
673
|
+
}
|
|
674
|
+
.set-uploader:disabled:not([aria-busy="true"]) .set-uploader-ring {
|
|
675
|
+
border-style: solid;
|
|
676
|
+
}
|
|
677
|
+
.set-uploader-remove {
|
|
678
|
+
position: absolute;
|
|
679
|
+
top: 2px;
|
|
680
|
+
right: 2px;
|
|
681
|
+
display: inline-flex;
|
|
682
|
+
align-items: center;
|
|
683
|
+
justify-content: center;
|
|
684
|
+
width: 28px;
|
|
685
|
+
height: 28px;
|
|
686
|
+
padding: 0;
|
|
687
|
+
border: 2px solid var(--color-surface);
|
|
688
|
+
border-radius: 50%;
|
|
689
|
+
background: var(--color-danger, #dc3545);
|
|
690
|
+
color: #fff;
|
|
691
|
+
cursor: pointer;
|
|
692
|
+
transition: transform 0.15s;
|
|
693
|
+
}
|
|
694
|
+
.set-uploader-remove:hover {
|
|
695
|
+
transform: scale(1.08);
|
|
696
|
+
}
|
|
697
|
+
// The control's own display would otherwise beat the UA [hidden] rule and keep it visible.
|
|
698
|
+
.set-uploader-remove[hidden] {
|
|
699
|
+
display: none;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Size is driven entirely by the --wui-logo-size custom property (set inline from the
|
|
703
|
+
// `size` prop / preset), so there are no per-size class rules to keep in sync.
|
|
704
|
+
|
|
705
|
+
// Shape variants — every round face (ring, image, logo, overlay) shares one radius.
|
|
706
|
+
.set-uploader--rounded .set-uploader-ring,
|
|
707
|
+
.set-uploader--rounded .set-uploader-img,
|
|
708
|
+
.set-uploader--rounded .set-uploader-logo,
|
|
709
|
+
.set-uploader--rounded .set-uploader-overlay {
|
|
710
|
+
border-radius: 18px;
|
|
711
|
+
}
|
|
712
|
+
.set-uploader--square .set-uploader-ring,
|
|
713
|
+
.set-uploader--square .set-uploader-img,
|
|
714
|
+
.set-uploader--square .set-uploader-logo,
|
|
715
|
+
.set-uploader--square .set-uploader-overlay {
|
|
716
|
+
border-radius: 8px;
|
|
717
|
+
}
|
|
500
718
|
|
|
501
719
|
// Minimals-style rounded logo dropzone (workspace branding form).
|
|
502
720
|
.logo-upload {
|