@webority/theme 0.6.5 → 0.6.7
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/wui/_choice.scss +48 -4
- package/scss/wui/_controls.scss +182 -0
- package/scss/wui/_data.scss +40 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webority/theme",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
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/wui/_choice.scss
CHANGED
|
@@ -189,6 +189,44 @@
|
|
|
189
189
|
color: var(--color-text-subtle);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// Sizes — mirror AppInput's .input-sm / .input-lg so a select lines up with the
|
|
193
|
+
// text fields beside it. md is the base .search-select-control height.
|
|
194
|
+
.search-select-sm .search-select-control {
|
|
195
|
+
height: var(--wui-control-height-sm);
|
|
196
|
+
min-height: var(--wui-control-height-sm);
|
|
197
|
+
font-size: 13px;
|
|
198
|
+
}
|
|
199
|
+
.search-select-lg .search-select-control {
|
|
200
|
+
height: var(--wui-control-height-lg);
|
|
201
|
+
min-height: var(--wui-control-height-lg);
|
|
202
|
+
font-size: 16px;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Clear (✕) — sits between the value and the caret, shown only once a value is
|
|
206
|
+
// chosen. It is a <span role="button"> inside the trigger <button> (a nested
|
|
207
|
+
// <button> is invalid); the JS stops its click from toggling the menu.
|
|
208
|
+
.search-select-control > span[data-label] {
|
|
209
|
+
flex: 1;
|
|
210
|
+
}
|
|
211
|
+
.search-select-reset {
|
|
212
|
+
flex-shrink: 0;
|
|
213
|
+
display: grid;
|
|
214
|
+
place-items: center;
|
|
215
|
+
width: 20px;
|
|
216
|
+
height: 20px;
|
|
217
|
+
border-radius: 50%;
|
|
218
|
+
font-size: 12px;
|
|
219
|
+
color: var(--color-text-subtle);
|
|
220
|
+
cursor: pointer;
|
|
221
|
+
transition:
|
|
222
|
+
background 0.12s ease,
|
|
223
|
+
color 0.12s ease;
|
|
224
|
+
}
|
|
225
|
+
.search-select-reset:hover {
|
|
226
|
+
background: var(--color-bg-muted);
|
|
227
|
+
color: var(--color-text);
|
|
228
|
+
}
|
|
229
|
+
|
|
192
230
|
// AppSelectMini — the compact single-purpose picker (page size, a range switch). It shares
|
|
193
231
|
// every behaviour with AppSelect and differs only in metrics.
|
|
194
232
|
//
|
|
@@ -265,11 +303,18 @@
|
|
|
265
303
|
.search-select-clear {
|
|
266
304
|
display: grid;
|
|
267
305
|
place-items: center;
|
|
268
|
-
width:
|
|
269
|
-
height:
|
|
306
|
+
width: 26px;
|
|
307
|
+
height: 26px;
|
|
270
308
|
border: none;
|
|
271
309
|
background: transparent;
|
|
272
310
|
padding: 0;
|
|
311
|
+
// Sits at the box's 12px right padding; a small negative margin nudges it
|
|
312
|
+
// closer to the edge so it lines up with the field's own trailing controls.
|
|
313
|
+
margin-right: -4px;
|
|
314
|
+
font-size: 20px;
|
|
315
|
+
// Tight line box: at normal line-height the icon's box is taller than the
|
|
316
|
+
// button, so bootstrap-icons' baseline offset drifts the glyph low.
|
|
317
|
+
line-height: 1;
|
|
273
318
|
cursor: pointer;
|
|
274
319
|
color: var(--color-text-subtle);
|
|
275
320
|
}
|
|
@@ -288,11 +333,10 @@
|
|
|
288
333
|
.search-select-options {
|
|
289
334
|
max-height: 300px;
|
|
290
335
|
overflow-y: auto;
|
|
291
|
-
padding: 4px
|
|
336
|
+
padding: 4px;
|
|
292
337
|
display: flex;
|
|
293
338
|
flex-direction: column;
|
|
294
339
|
gap: 4px;
|
|
295
|
-
scrollbar-gutter: stable;
|
|
296
340
|
}
|
|
297
341
|
.search-select-option {
|
|
298
342
|
display: flex;
|
package/scss/wui/_controls.scss
CHANGED
|
@@ -549,6 +549,106 @@ textarea.textarea:focus {
|
|
|
549
549
|
to { opacity: 1; transform: translateY(-50%) scale(1); }
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
+
// Prefix / suffix addon — a bordered box before/after the input (₹, https://, .com). The
|
|
553
|
+
// WRAPPER owns the border + radius so the affixes and input read as one control; the input
|
|
554
|
+
// itself goes borderless. Focus/hover/error move to the wrapper.
|
|
555
|
+
.input-affixed {
|
|
556
|
+
display: flex;
|
|
557
|
+
align-items: stretch;
|
|
558
|
+
width: 100%;
|
|
559
|
+
border: 1px solid var(--color-border);
|
|
560
|
+
border-radius: var(--radius);
|
|
561
|
+
background: var(--color-surface);
|
|
562
|
+
transition: border-color 0.12s ease;
|
|
563
|
+
}
|
|
564
|
+
.input-affixed:hover:not(:focus-within) {
|
|
565
|
+
border-color: var(--color-border-strong);
|
|
566
|
+
}
|
|
567
|
+
.input-affixed:focus-within {
|
|
568
|
+
border-color: var(--color-primary);
|
|
569
|
+
}
|
|
570
|
+
.input-affixed:has(.input.error) {
|
|
571
|
+
border-color: var(--color-danger);
|
|
572
|
+
}
|
|
573
|
+
.input-affixed .input {
|
|
574
|
+
flex: 1;
|
|
575
|
+
min-width: 0;
|
|
576
|
+
border: 0;
|
|
577
|
+
border-radius: 0;
|
|
578
|
+
background: transparent;
|
|
579
|
+
}
|
|
580
|
+
.input-affixed .affix {
|
|
581
|
+
display: flex;
|
|
582
|
+
align-items: center;
|
|
583
|
+
padding: 0 12px;
|
|
584
|
+
background: var(--wui-bg-muted);
|
|
585
|
+
color: var(--color-text-subtle);
|
|
586
|
+
font-size: 14px;
|
|
587
|
+
white-space: nowrap;
|
|
588
|
+
}
|
|
589
|
+
.input-affixed .affix-pre {
|
|
590
|
+
border-right: 1px solid var(--color-border);
|
|
591
|
+
border-radius: var(--radius) 0 0 var(--radius);
|
|
592
|
+
}
|
|
593
|
+
.input-affixed .affix-suf {
|
|
594
|
+
border-left: 1px solid var(--color-border);
|
|
595
|
+
border-radius: 0 var(--radius) var(--radius) 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Floating label — the label starts centred as the placeholder, then slides up and shrinks
|
|
599
|
+
// on focus or when the field is filled. Opt-in (floatingLabel); the input carries a single
|
|
600
|
+
// space as its placeholder so :placeholder-shown drives the "empty" state. Standalone shell.
|
|
601
|
+
.field-float {
|
|
602
|
+
position: relative;
|
|
603
|
+
width: 100%;
|
|
604
|
+
}
|
|
605
|
+
.field-float .float-label {
|
|
606
|
+
position: absolute;
|
|
607
|
+
left: 12px;
|
|
608
|
+
top: 50%;
|
|
609
|
+
transform: translateY(-50%);
|
|
610
|
+
transform-origin: left center;
|
|
611
|
+
margin: 0;
|
|
612
|
+
padding: 0 4px;
|
|
613
|
+
max-width: calc(100% - 24px);
|
|
614
|
+
overflow: hidden;
|
|
615
|
+
text-overflow: ellipsis;
|
|
616
|
+
white-space: nowrap;
|
|
617
|
+
font-size: 14px;
|
|
618
|
+
color: var(--color-text-subtle);
|
|
619
|
+
background: var(--color-surface);
|
|
620
|
+
pointer-events: none;
|
|
621
|
+
transition:
|
|
622
|
+
top 0.15s var(--wui-ease-ui),
|
|
623
|
+
transform 0.15s var(--wui-ease-ui),
|
|
624
|
+
color 0.15s var(--wui-ease-ui);
|
|
625
|
+
}
|
|
626
|
+
.field-float .input:focus ~ .float-label,
|
|
627
|
+
.field-float .input:not(:placeholder-shown) ~ .float-label {
|
|
628
|
+
top: 0;
|
|
629
|
+
transform: translateY(-50%) scale(0.86);
|
|
630
|
+
}
|
|
631
|
+
.field-float .input:focus ~ .float-label {
|
|
632
|
+
color: var(--color-primary);
|
|
633
|
+
}
|
|
634
|
+
.field-float .input.error ~ .float-label {
|
|
635
|
+
color: var(--color-danger-text);
|
|
636
|
+
}
|
|
637
|
+
// Floating label for AppSelect — the trigger is a <button>, so there is no
|
|
638
|
+
// :placeholder-shown to key off. :has() raises the label when the control is
|
|
639
|
+
// focused/open, or once a value is chosen (the label span drops
|
|
640
|
+
// .search-select-placeholder the moment something is selected).
|
|
641
|
+
.field-float:has(.search-select-control:focus) .float-label,
|
|
642
|
+
.field-float:has(.search-select-control.open) .float-label,
|
|
643
|
+
.field-float:has([data-label]:not(.search-select-placeholder)) .float-label {
|
|
644
|
+
top: 0;
|
|
645
|
+
transform: translateY(-50%) scale(0.86);
|
|
646
|
+
}
|
|
647
|
+
.field-float:has(.search-select-control:focus) .float-label,
|
|
648
|
+
.field-float:has(.search-select-control.open) .float-label {
|
|
649
|
+
color: var(--color-primary);
|
|
650
|
+
}
|
|
651
|
+
|
|
552
652
|
.hint,
|
|
553
653
|
.field-hint {
|
|
554
654
|
margin-top: 2px;
|
|
@@ -912,3 +1012,85 @@ wui-select[compact] {
|
|
|
912
1012
|
.fld-section:first-child {
|
|
913
1013
|
margin-top: 0;
|
|
914
1014
|
}
|
|
1015
|
+
|
|
1016
|
+
// ── OTP / code entry (<wui-otp>) ──
|
|
1017
|
+
//
|
|
1018
|
+
// One real input, transparent, sitting on top of the segment boxes. The input keeps focus,
|
|
1019
|
+
// caret, selection, IME and — the reason for the whole design — OS autofill, which delivers
|
|
1020
|
+
// an SMS code to the focused field whole. The boxes are presentational and aria-hidden; the
|
|
1021
|
+
// input is the single labelled field a screen reader sees.
|
|
1022
|
+
//
|
|
1023
|
+
// The input must stay REAL: not opacity:0, not visibility:hidden, not off-screen. Any of
|
|
1024
|
+
// those stop iOS offering the code, which is the failure this component exists to avoid.
|
|
1025
|
+
// Transparent text with a transparent caret is the version that keeps the affordance while
|
|
1026
|
+
// letting the segments do the drawing.
|
|
1027
|
+
wui-otp {
|
|
1028
|
+
display: block;
|
|
1029
|
+
}
|
|
1030
|
+
.otp {
|
|
1031
|
+
position: relative;
|
|
1032
|
+
display: inline-flex;
|
|
1033
|
+
}
|
|
1034
|
+
.otp-field {
|
|
1035
|
+
position: absolute;
|
|
1036
|
+
inset: 0;
|
|
1037
|
+
width: 100%;
|
|
1038
|
+
height: 100%;
|
|
1039
|
+
padding: 0;
|
|
1040
|
+
border: 0;
|
|
1041
|
+
background: transparent;
|
|
1042
|
+
color: transparent;
|
|
1043
|
+
caret-color: transparent;
|
|
1044
|
+
font-size: 16px; // < 16px makes iOS zoom the page on focus
|
|
1045
|
+
text-align: center;
|
|
1046
|
+
letter-spacing: 0;
|
|
1047
|
+
z-index: 1;
|
|
1048
|
+
}
|
|
1049
|
+
.otp-field:focus {
|
|
1050
|
+
outline: none;
|
|
1051
|
+
}
|
|
1052
|
+
.otp-field:disabled {
|
|
1053
|
+
cursor: not-allowed;
|
|
1054
|
+
}
|
|
1055
|
+
.otp-slots {
|
|
1056
|
+
display: inline-flex;
|
|
1057
|
+
gap: 8px;
|
|
1058
|
+
}
|
|
1059
|
+
.otp-slot {
|
|
1060
|
+
display: inline-flex;
|
|
1061
|
+
align-items: center;
|
|
1062
|
+
justify-content: center;
|
|
1063
|
+
width: 44px;
|
|
1064
|
+
height: var(--wui-control-height);
|
|
1065
|
+
border: 1px solid var(--color-border);
|
|
1066
|
+
border-radius: var(--radius);
|
|
1067
|
+
background: var(--color-surface);
|
|
1068
|
+
font-size: 18px;
|
|
1069
|
+
font-weight: 600;
|
|
1070
|
+
font-variant-numeric: tabular-nums;
|
|
1071
|
+
color: var(--color-text);
|
|
1072
|
+
transition:
|
|
1073
|
+
border-color 0.12s,
|
|
1074
|
+
box-shadow 0.12s;
|
|
1075
|
+
}
|
|
1076
|
+
// The caret cannot be seen through a transparent input, so the ACTIVE box carries the
|
|
1077
|
+
// affordance instead — derived from selectionStart, since only one element is ever focused.
|
|
1078
|
+
.otp-slot.active {
|
|
1079
|
+
border-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
|
|
1080
|
+
box-shadow: 0 0 0 3px var(--color-primary-soft);
|
|
1081
|
+
}
|
|
1082
|
+
.otp-field:disabled ~ .otp-slots .otp-slot {
|
|
1083
|
+
background: var(--color-bg-muted);
|
|
1084
|
+
border-color: var(--color-border);
|
|
1085
|
+
color: var(--color-text-subtle);
|
|
1086
|
+
}
|
|
1087
|
+
.field-error ~ .otp .otp-slot,
|
|
1088
|
+
.otp.error .otp-slot {
|
|
1089
|
+
border-color: var(--color-danger);
|
|
1090
|
+
}
|
|
1091
|
+
@media (max-width: 575.98px) {
|
|
1092
|
+
// Six 44px boxes plus gaps overflow a 360px screen; shrink the box, never the font.
|
|
1093
|
+
.otp-slot {
|
|
1094
|
+
width: 38px;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
package/scss/wui/_data.scss
CHANGED
|
@@ -261,6 +261,7 @@
|
|
|
261
261
|
gap: 2px;
|
|
262
262
|
}
|
|
263
263
|
.table-ledger .u-name {
|
|
264
|
+
min-width: 0;
|
|
264
265
|
font-size: 14px;
|
|
265
266
|
font-weight: 500;
|
|
266
267
|
line-height: 19px;
|
|
@@ -273,6 +274,28 @@
|
|
|
273
274
|
margin-bottom: 2px;
|
|
274
275
|
}
|
|
275
276
|
}
|
|
277
|
+
// Clamp a ledger name to one line. OPT-IN — see below for why it is not the default.
|
|
278
|
+
//
|
|
279
|
+
// In a ledger a long name is the normal case: a 112-char project name wrapped to three lines
|
|
280
|
+
// and took its row to 99px against 61px for every other row, breaking the row rhythm down the
|
|
281
|
+
// whole table.
|
|
282
|
+
//
|
|
283
|
+
// `white-space: nowrap` + ellipsis is the WRONG fix and was measured to be: under the
|
|
284
|
+
// ledger's auto table layout a nowrap cell demands its full unwrapped width, which widened a
|
|
285
|
+
// 360px column to 854px and pushed the table into horizontal scroll. It trades row height for
|
|
286
|
+
// column geometry. A line clamp leaves the text wrappable, so min-content stays one word.
|
|
287
|
+
//
|
|
288
|
+
// Opt-in rather than default because the library styles `.u-name` but never emits it — it is
|
|
289
|
+
// applied by consumers, and `-webkit-box` stacks element children vertically. A product that
|
|
290
|
+
// composes a name with an inline badge would get the badge under the name. Flip this to the
|
|
291
|
+
// default once the composition is known to be text-only everywhere.
|
|
292
|
+
.u-name-clamp {
|
|
293
|
+
display: -webkit-box;
|
|
294
|
+
-webkit-line-clamp: 1;
|
|
295
|
+
-webkit-box-orient: vertical;
|
|
296
|
+
overflow: hidden;
|
|
297
|
+
}
|
|
298
|
+
|
|
276
299
|
// An identity-cell name is often a link, so it inherits Bootstrap's
|
|
277
300
|
// underline-on-hover. The colour shift is the affordance here.
|
|
278
301
|
.table-ledger a.u-name,
|
|
@@ -401,6 +424,23 @@
|
|
|
401
424
|
border-radius: 999px;
|
|
402
425
|
}
|
|
403
426
|
|
|
427
|
+
// The identity/name column — a STABLE hook, because position is not one.
|
|
428
|
+
//
|
|
429
|
+
// The library owns .lx-select-cell, so a ledger's first cell is the 44px checkbox when
|
|
430
|
+
// selection is on and the name when it is off. `:first-child` therefore changes meaning
|
|
431
|
+
// with a permission flag: a consumer's 360px name-column floor landed on the 44px checkbox
|
|
432
|
+
// for any user who could select rows, inflating that column 8x and pushing the table into
|
|
433
|
+
// horizontal scroll — visible only to users holding that one permission, which is why it
|
|
434
|
+
// survived review. Sizing the right column should not require guessing at position.
|
|
435
|
+
//
|
|
436
|
+
// min-width:0 is the rule rather than a placeholder: a table cell defaults to min-width:auto,
|
|
437
|
+
// which refuses to shrink below its content and is what makes truncation impossible in the
|
|
438
|
+
// first place.
|
|
439
|
+
.table-ledger th.lx-name-cell,
|
|
440
|
+
.table-ledger td.lx-name-cell {
|
|
441
|
+
min-width: 0;
|
|
442
|
+
}
|
|
443
|
+
|
|
404
444
|
// Row-selection checkbox column.
|
|
405
445
|
.table-ledger th.lx-select-cell,
|
|
406
446
|
.table-ledger td.lx-select-cell {
|