@webority/theme 0.13.5 → 0.15.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.
@@ -348,7 +348,7 @@ textarea.form-control.autosize {
348
348
  background-color: var(--color-surface);
349
349
  color: var(--color-text);
350
350
  // Soft brand border is the focus cue (per the reference) — no ring/shadow.
351
- border-color: var(--wui-primary-30);
351
+ border-color: var(--color-focus-border);
352
352
  box-shadow: none;
353
353
  }
354
354
  .form-control::placeholder,
@@ -625,6 +625,14 @@ $wui-field-error-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/
625
625
  color: var(--color-text);
626
626
  -webkit-text-fill-color: var(--color-text);
627
627
  }
628
+ // ...but -webkit-text-fill-color is inherited by ::placeholder and beats its `color`, so the rule
629
+ // above painted the placeholder as bright as a typed value. Hand the placeholder its own subtle
630
+ // ink back (the same --color-text-subtle every other placeholder uses).
631
+ :root[data-theme="dark"] .form-control.error::placeholder,
632
+ :root[data-theme="dark"] .input.error::placeholder {
633
+ color: var(--color-text-subtle);
634
+ -webkit-text-fill-color: var(--color-text-subtle);
635
+ }
628
636
 
629
637
  // Field-relative wrap for trailing affixes (error icon, password toggle).
630
638
  .input-wrap {
@@ -793,9 +801,10 @@ $wui-field-error-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/
793
801
  }
794
802
  .input-affixed:focus-within {
795
803
  background: var(--color-surface);
796
- border-color: var(--wui-primary-30);
804
+ border-color: var(--color-focus-border);
797
805
  }
798
- .input-affixed:has(.input.error) {
806
+ .input-affixed:has(.input.error),
807
+ .input-affixed:has(.input[aria-invalid="true"]) {
799
808
  background: var(--color-surface);
800
809
  border-color: color-mix(in srgb, var(--color-danger) 30%, transparent);
801
810
  }
@@ -806,9 +815,18 @@ $wui-field-error-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/
806
815
  border-radius: 0;
807
816
  background: transparent;
808
817
  }
818
+ // A select in the affixed shell (AppSelect prefix/suffix): the custom element is the flex
819
+ // item, and its inner .input control already loses its own border through the rule above.
820
+ .input-affixed > wui-select {
821
+ flex: 1;
822
+ min-width: 0;
823
+ }
809
824
  .input-affixed .affix {
810
825
  display: flex;
811
826
  align-items: center;
827
+ // A one-character affix (₹, $, #) still reads as a box, centred in it, not a sliver.
828
+ justify-content: center;
829
+ min-width: 44px;
812
830
  padding: 0 12px;
813
831
  // Transparent so the affix inherits the wrapper fill — the addon and the input read as
814
832
  // one continuous surface (only the separator border divides them).
@@ -993,6 +1011,7 @@ $wui-field-error-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/
993
1011
  // into a plain div, a grid cell, a table cell — it would still collapse. Being a flex
994
1012
  // item blockifies it; nothing else does.
995
1013
  wui-select,
1014
+ wui-country-select,
996
1015
  wui-autocomplete,
997
1016
  wui-multiselect,
998
1017
  wui-taginput,
@@ -1010,6 +1029,7 @@ wui-select[compact] {
1010
1029
  width: auto;
1011
1030
  }
1012
1031
  .field > wui-select,
1032
+ .field > wui-country-select,
1013
1033
  .field > wui-autocomplete,
1014
1034
  .field > wui-multiselect,
1015
1035
  .field > wui-taginput,
@@ -1069,7 +1089,7 @@ wui-select[compact] {
1069
1089
  }
1070
1090
  .input-with-unit:focus-within {
1071
1091
  background: var(--color-surface);
1072
- border-color: var(--wui-primary-30);
1092
+ border-color: var(--color-focus-border);
1073
1093
  }
1074
1094
  .input-with-unit:has(.num-input.error) {
1075
1095
  background: var(--color-surface);
@@ -1142,7 +1162,7 @@ wui-select[compact] {
1142
1162
  }
1143
1163
  .num-wrap:focus-within {
1144
1164
  background: var(--color-surface);
1145
- border-color: var(--wui-primary-30);
1165
+ border-color: var(--color-focus-border);
1146
1166
  }
1147
1167
  .num-wrap:has(.num-input.error),
1148
1168
  .num-wrap:has(.num-input.is-invalid),
@@ -1512,7 +1532,7 @@ wui-otp {
1512
1532
  // surface fill + soft brand border, no ring.
1513
1533
  .otp-slot.active {
1514
1534
  background: var(--color-surface);
1515
- border-color: var(--wui-primary-30);
1535
+ border-color: var(--color-focus-border);
1516
1536
  box-shadow: none;
1517
1537
  }
1518
1538
  .otp-field:disabled ~ .otp-slots .otp-slot {
@@ -407,15 +407,26 @@ a.info-row:hover .info-value {
407
407
  // ============================================================================
408
408
  // Segmented control — 2–4 mutually-exclusive options shown side by side.
409
409
  // The theme toggle (light/dark/system) is the canonical use.
410
+ //
411
+ // The selected segment is an OUTLINED pill: a primary ring over a faint primary tint, with the
412
+ // label in primary. The tint is 8% so the label clears AA on it in both themes (light 5.6:1,
413
+ // dark 4.5:1); a stronger tint drops the dark label under 4.5:1.
410
414
  // ============================================================================
415
+ // Each size sets one option height, icon size and note size, so a text, icon or note option in
416
+ // the same control lines up, and the four sizes step evenly: 28 / 32 / 38 / 44px tall overall
417
+ // (the track adds 8px), xl matching the 44px field height. A note option grows past the height.
411
418
  .segmented {
419
+ --segmented-selected-bg: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface));
420
+ --segmented-option-h: 24px;
421
+ --segmented-icon: 13px;
422
+ --segmented-note: 10px;
412
423
  position: relative;
413
424
  display: inline-flex;
414
425
  gap: 2px;
415
426
  padding: 3px;
416
427
  border: 1px solid var(--color-border);
417
428
  border-radius: 999px;
418
- background: var(--color-bg-muted);
429
+ background: var(--color-surface);
419
430
  }
420
431
  .segmented-option {
421
432
  position: relative;
@@ -423,7 +434,10 @@ a.info-row:hover .info-value {
423
434
  display: inline-flex;
424
435
  align-items: center;
425
436
  gap: 5px;
426
- padding: 5px 12px;
437
+ min-height: var(--segmented-option-h);
438
+ // A small fixed vertical padding: the min-height sets the height, so an icon cannot push
439
+ // one option taller than its text-only siblings. Only a two-line note option grows.
440
+ padding: 4px 12px;
427
441
  border: 0;
428
442
  border-radius: 999px;
429
443
  background: transparent;
@@ -432,7 +446,7 @@ a.info-row:hover .info-value {
432
446
  font-weight: 600;
433
447
  line-height: 1;
434
448
  white-space: nowrap;
435
- color: var(--color-text-muted);
449
+ color: var(--color-text);
436
450
  cursor: pointer;
437
451
  transition:
438
452
  background 0.14s ease,
@@ -457,7 +471,7 @@ a.info-row:hover .info-value {
457
471
  white-space: nowrap;
458
472
  }
459
473
  .segmented-option:hover {
460
- color: var(--color-text);
474
+ color: var(--color-primary);
461
475
  }
462
476
  // A per-option sub-label under the main text — optional, so a segment without one keeps the
463
477
  // plain single-line layout above untouched.
@@ -473,20 +487,20 @@ a.info-row:hover .info-value {
473
487
  color: inherit;
474
488
  }
475
489
  .segmented-option-note {
476
- font-size: 10px;
490
+ font-size: var(--segmented-note);
477
491
  font-weight: 400;
478
- // Inherits the option's current text colour (muted, hover, or white-on-checked) rather
492
+ // Inherits the option's current text colour (plain, hover, or primary-on-checked) rather
479
493
  // than a fixed token, so it always matches without a separate state per variant. No opacity
480
494
  // muting — that dropped the sub-label below AA on an unchecked option; the 10px size already
481
495
  // distinguishes it from the label.
482
496
  color: currentColor;
483
497
  }
498
+ // Without JS this per-option pill stands in for the sliding thumb below, so it carries the
499
+ // same ring and tint.
484
500
  .segmented-option:has(.segmented-input:checked) {
485
- background: var(--color-primary);
486
- // Token, not fixed white: the pill is always the brand fill, so its label must
487
- // contrast with THAT fill, not the page — a light-primary brand overrides
488
- // --wui-primary-text to a dark value and this follows automatically.
489
- color: var(--color-primary-text);
501
+ background: var(--segmented-selected-bg);
502
+ box-shadow: inset 0 0 0 2px var(--color-primary);
503
+ color: var(--color-primary);
490
504
  }
491
505
  // The ring is drawn on the label because the input it belongs to has no box.
492
506
  .segmented-option:has(.segmented-input:focus-visible) {
@@ -504,21 +518,41 @@ a.info-row:hover .info-value {
504
518
  width: 0;
505
519
  z-index: 0;
506
520
  border-radius: 999px;
507
- background: var(--color-primary);
508
- box-shadow: 0 2px 8px color-mix(in srgb, var(--color-primary) 32%, transparent);
521
+ background: var(--segmented-selected-bg);
522
+ // An inset ring, not a border: a border would add to the width JS measures and sets.
523
+ box-shadow: inset 0 0 0 2px var(--color-primary);
509
524
  opacity: 0;
510
525
  pointer-events: none;
511
526
  }
512
527
  .segmented.is-animated .segmented-thumb {
513
528
  opacity: 1;
514
- // A spring easing that overshoots a touch and settles — livelier than a flat slide.
529
+ // A fast ease-out that never overshoots. A spring easing slid the thumb past its target, so
530
+ // at the first or last segment the ring crossed the track's edge mid-slide. Clipping the
531
+ // track instead is not an option: it would also clip the options' focus outline.
515
532
  transition:
516
- transform 0.36s cubic-bezier(0.34, 1.42, 0.5, 1),
517
- width 0.36s cubic-bezier(0.34, 1.42, 0.5, 1);
533
+ transform 0.32s cubic-bezier(0.22, 1, 0.36, 1),
534
+ width 0.32s cubic-bezier(0.22, 1, 0.36, 1);
518
535
  }
519
- // While the thumb draws the fill, the checked option drops its own background (keeps white text).
536
+ // While the thumb draws the pill, the checked option drops its own (keeps the primary label).
520
537
  .segmented.is-animated .segmented-option:has(.segmented-input:checked) {
521
538
  background: transparent;
539
+ box-shadow: none;
540
+ }
541
+ // A two-line note option keeps a little more air inside the ring than a one-line option needs.
542
+ .segmented-option:has(.segmented-option-note) {
543
+ padding-top: 6px;
544
+ padding-bottom: 6px;
545
+ }
546
+ .segmented-option > .wui-icon {
547
+ flex: none;
548
+ width: var(--segmented-icon);
549
+ height: var(--segmented-icon);
550
+ }
551
+ // Borderless (bordered={false} / bordered="false") — the selected pill keeps its tint and primary
552
+ // label but drops the ring, on both the sliding thumb and the no-JS per-option pill.
553
+ .segmented-borderless .segmented-thumb,
554
+ .segmented-borderless .segmented-option:has(.segmented-input:checked) {
555
+ box-shadow: none;
522
556
  }
523
557
  // Icon pop — the selected segment's glyph springs up a touch as it becomes active.
524
558
  .segmented-option:has(.segmented-input:checked) .wui-icon {
@@ -526,19 +560,39 @@ a.info-row:hover .info-value {
526
560
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
527
561
  }
528
562
  .segmented-sm .segmented-thumb,
529
- .segmented-lg .segmented-thumb {
563
+ .segmented-lg .segmented-thumb,
564
+ .segmented-xl .segmented-thumb {
530
565
  top: 3px;
531
566
  bottom: 3px;
532
567
  }
568
+ .segmented-sm {
569
+ --segmented-option-h: 20px;
570
+ --segmented-icon: 12px;
571
+ }
572
+ .segmented-lg {
573
+ --segmented-option-h: 30px;
574
+ --segmented-icon: 15px;
575
+ --segmented-note: 11px;
576
+ }
577
+ .segmented-xl {
578
+ --segmented-option-h: 36px;
579
+ --segmented-icon: 17px;
580
+ --segmented-note: 12px;
581
+ }
533
582
  .segmented-sm .segmented-option {
534
583
  padding: 4px 9px;
535
584
  font-size: 11px;
536
585
  }
537
586
  .segmented-lg .segmented-option {
538
587
  gap: 6px;
539
- padding: 8px 16px;
588
+ padding: 4px 16px;
540
589
  font-size: 13px;
541
590
  }
591
+ .segmented-xl .segmented-option {
592
+ gap: 7px;
593
+ padding: 4px 20px;
594
+ font-size: 15px;
595
+ }
542
596
  // Full width — segments share the row equally (a toggle spanning a card or toolbar).
543
597
  .segmented-full {
544
598
  display: flex;
@@ -260,11 +260,18 @@ button.step:focus-visible .step-circle {
260
260
  overflow: hidden;
261
261
  text-overflow: ellipsis;
262
262
  }
263
+ // Where connectors are drawn, a step takes only its content's width (and still shrinks to
264
+ // ellipsize its text) and the connector takes the rest, so the rail joins each step to the
265
+ // next. A journey bar without connectors keeps its evenly spread steps.
266
+ .stepper-journey:has(> .step-line) > .step {
267
+ flex: 0 1 auto;
268
+ }
263
269
  .stepper-journey .step-line {
264
- flex: none;
265
- width: 32px;
270
+ flex: 1 1 32px;
271
+ min-width: 24px;
266
272
  height: 2px;
267
- margin: 0 6px;
273
+ margin: 0 16px;
274
+ border-radius: 999px;
268
275
  background: var(--color-border-strong);
269
276
  }
270
277
  .stepper-journey .step-line.done {
@@ -552,7 +552,7 @@
552
552
  }
553
553
  .file-input:focus-within {
554
554
  background: var(--color-surface);
555
- border-color: var(--wui-primary-30);
555
+ border-color: var(--color-focus-border);
556
556
  box-shadow: none;
557
557
  }
558
558
  .file-input-btn {
@@ -614,7 +614,7 @@
614
614
  .file-input.file-input-tall:focus-within,
615
615
  .file-input.file-input-tall:active,
616
616
  .file-input.file-input-tall.dragging {
617
- border-color: var(--wui-primary-30);
617
+ border-color: var(--color-focus-border);
618
618
  background: var(--color-surface);
619
619
  box-shadow: none;
620
620
  }
@@ -664,7 +664,7 @@
664
664
  background: var(--color-bg-muted);
665
665
  }
666
666
  .file-input.file-input-tall.has-files.dragging {
667
- border-color: var(--wui-primary-30);
667
+ border-color: var(--color-focus-border);
668
668
  background: var(--color-surface);
669
669
  }
670
670
  // Width presets — put a .file-w-* class on the field (React className / Razor class) to cap the
@@ -820,7 +820,9 @@
820
820
  gap: 8px;
821
821
  padding: 9px 18px;
822
822
  background: rgba(255, 255, 255, 0.96);
823
- color: var(--color-text);
823
+ // The pill stays white in every theme, so its ink is fixed too: --color-text flips light in
824
+ // dark mode and would vanish on the white pill.
825
+ color: var(--wui-text-heading);
824
826
  font-size: 14px;
825
827
  font-weight: 600;
826
828
  border-radius: 999px;
@@ -316,18 +316,23 @@
316
316
  // keeps a non-square source whole rather than cropping it.
317
317
  .wui-confirm-icon img {
318
318
  display: block;
319
- width: 80px;
320
- height: 80px;
319
+ width: var(--wui-confirm-icon-size, 120px);
320
+ height: var(--wui-confirm-icon-size, 120px);
321
321
  object-fit: contain;
322
322
  }
323
- // Icon size tracks the copy: a title+message dialog keeps the full 80px badge (set on
324
- // the element), but a lone title (no message) is one line and gets a smaller badge.
325
- // `.wui-confirm-msg` is a real element rendered only when a message exists, so `:has()`
326
- // is reliable here; CSS width/height override the SVG's own width/height attributes.
323
+ .wui-confirm-icon svg {
324
+ width: var(--wui-confirm-icon-size, 120px);
325
+ height: var(--wui-confirm-icon-size, 120px);
326
+ }
327
+ // Icon size tracks the copy: a title+message dialog keeps the full 120px badge, but a lone
328
+ // title (no message) is one line and gets a smaller badge. `.wui-confirm-msg` is a real
329
+ // element rendered only when a message exists, so `:has()` is reliable here; CSS width/height
330
+ // override the SVG's own width/height attributes. An explicit iconSize / icon-size sets
331
+ // --wui-confirm-icon-size on the badge and wins in both cases.
327
332
  .wui-confirm-content:not(:has(.wui-confirm-msg)) .wui-confirm-icon svg,
328
333
  .wui-confirm-content:not(:has(.wui-confirm-msg)) .wui-confirm-icon img {
329
- width: 56px;
330
- height: 56px;
334
+ width: var(--wui-confirm-icon-size, 84px);
335
+ height: var(--wui-confirm-icon-size, 84px);
331
336
  }
332
337
  .wui-confirm-title {
333
338
  margin: 0 0 10px;
@@ -194,8 +194,12 @@
194
194
  .app-shell {
195
195
  --app-sidebar-w: 260px;
196
196
  --app-sidebar-rail-w: 88px;
197
+ // The sidebar's bottom band, from the rule above the collapse toggle to the sidebar's
198
+ // bottom edge, is the height of a one-line footer. The rule then continues the footer's
199
+ // top edge across the shell. The toggle fills what is left after the rule and two gaps.
200
+ --app-shell-foot-h: 39px;
197
201
  // The equal space above (below the rule) and below the collapse toggle.
198
- --app-sidebar-toggle-gap: 12px;
202
+ --app-sidebar-toggle-gap: 4px;
199
203
 
200
204
  position: relative;
201
205
  display: grid;
@@ -552,7 +556,9 @@
552
556
  justify-content: center;
553
557
  width: 100%;
554
558
  position: relative;
555
- min-height: var(--wui-control-height);
559
+ // The band height less the rule and the gaps above and below the toggle, so rule + toggle
560
+ // + bottom inset add up to exactly --app-shell-foot-h.
561
+ height: calc(var(--app-shell-foot-h) - 1px - 2 * var(--app-sidebar-toggle-gap));
556
562
  // 8px above the rule, the 1px rule, then the same gap below it as under the row.
557
563
  margin-top: calc(9px + var(--app-sidebar-toggle-gap));
558
564
  padding: 0;