@webority/theme 0.7.8 → 0.8.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.
@@ -430,10 +430,16 @@
430
430
  z-index: var(--wui-z-toast);
431
431
  display: flex;
432
432
  flex-direction: column;
433
- gap: 10px;
433
+ gap: 8px;
434
434
  max-width: min(92vw, 400px);
435
435
  }
436
436
 
437
+ // Bootstrap's .toast-container stacks children with margin-bottom: $toast-spacing (1.5rem),
438
+ // which ADDED to the flex gap above — the stack spacing is the gap alone.
439
+ .wui-toast-container > :not(:last-child) {
440
+ margin-bottom: 0;
441
+ }
442
+
437
443
  .wui-toast {
438
444
  --wui-toast-accent: var(--color-primary);
439
445
  position: relative;
@@ -441,10 +447,12 @@
441
447
  align-items: flex-start;
442
448
  gap: 12px;
443
449
  width: 100%;
444
- padding: 14px 14px 14px 18px;
450
+ padding: 14px 16px;
445
451
  background: var(--color-surface);
446
452
  color: var(--color-text);
447
- border: 1px solid var(--color-border-light);
453
+ // The full-strength border, not border-light: on a white page the surface-on-white
454
+ // card otherwise merges into the background (the overlay shadow alone is too soft).
455
+ border: 1px solid var(--color-border);
448
456
  border-radius: var(--radius-lg, 12px);
449
457
  box-shadow: var(--wui-shadow-overlay);
450
458
  overflow: hidden;
@@ -456,15 +464,6 @@
456
464
  animation: wui-toast-in 0.24s ease;
457
465
  }
458
466
 
459
- // The left accent bar reads the variant tint.
460
- .wui-toast::before {
461
- content: "";
462
- position: absolute;
463
- inset: 0 auto 0 0;
464
- width: 4px;
465
- background: var(--wui-toast-accent);
466
- }
467
-
468
467
  // While a swipe is in progress the toast follows the pointer with no transition.
469
468
  .wui-toast--dragging {
470
469
  transition: none;
@@ -513,21 +512,81 @@
513
512
  overflow-wrap: anywhere;
514
513
  }
515
514
 
515
+ // Hierarchy: with a title present the message reads as the SUBTITLE and drops to muted —
516
+ // title (ink, 600) > message (muted) > action (compact ink, hover reveals the variant tint).
517
+ // A title-less toast keeps full ink, or a bare "Saved" would look washed out.
518
+ .wui-toast-title + .wui-toast-message {
519
+ color: var(--color-text-muted);
520
+ }
521
+
522
+ // Determinate task progress (toast `progress` 0–100) — work advancing, not time running out.
523
+ .wui-toast-task {
524
+ display: flex;
525
+ align-items: center;
526
+ gap: 8px;
527
+ margin-top: 7px;
528
+ }
529
+ .wui-toast-track {
530
+ flex: 1 1 auto;
531
+ height: 6px;
532
+ border-radius: 999px;
533
+ background: var(--color-bg-muted);
534
+ overflow: hidden;
535
+ }
536
+ .wui-toast-track-fill {
537
+ height: 100%;
538
+ border-radius: 999px;
539
+ background: var(--wui-toast-accent);
540
+ transition: width 200ms var(--wui-ease-ui);
541
+ }
542
+ .wui-toast-track-pct {
543
+ flex: none;
544
+ font-size: 0.75rem;
545
+ font-weight: 600;
546
+ color: var(--color-text-muted);
547
+ }
548
+
516
549
  // Action button — tinted with the variant accent, scoped to the toast so it never
517
550
  // restyles an arbitrary .btn-link a rich body might contain.
551
+ // Lives in the content column, below the message/progress (never beside them).
552
+ // Deliberately NOT a .btn — Bootstrap's button chrome (padding, hover lift, transitions)
553
+ // has no business on a text-line action, so the element carries only this class and the
554
+ // styling is self-contained. Neutral bold ink like the title; underline on hover, no motion.
555
+ // The 3px/8px padding exists for the hover pill; the negative margins give it back, so the
556
+ // resting text sits exactly where the padding-less version did and the approved rhythm holds.
518
557
  .wui-toast-action {
519
558
  flex: none;
520
- align-self: center;
521
- color: var(--wui-toast-accent);
522
- text-decoration: none;
559
+ align-self: flex-start;
560
+ background: none;
561
+ border: 0;
562
+ padding: 3px 8px;
563
+ margin: -1px 0 -3px -8px;
564
+ border-radius: 6px;
565
+ cursor: pointer;
566
+ color: var(--color-text);
567
+ font-family: inherit;
523
568
  font-size: 0.8125rem;
524
569
  font-weight: 600;
525
- padding: 2px 8px;
570
+ line-height: 1.4;
571
+ text-decoration: none;
526
572
  white-space: nowrap;
573
+ transition: background-color 120ms ease, color 120ms ease;
527
574
  }
575
+ // Hover: a soft pill in the toast's variant tint — highlight, not underline, no motion.
528
576
  .wui-toast-action:hover {
577
+ background: color-mix(in srgb, var(--wui-toast-accent) 14%, transparent);
529
578
  color: var(--wui-toast-accent);
530
- text-decoration: underline;
579
+ }
580
+ .wui-toast-action:focus-visible {
581
+ outline: 2px solid var(--wui-toast-accent);
582
+ outline-offset: 1px;
583
+ }
584
+
585
+ // Geometry: optical space above the action = message half-leading (3.5px) + column gap
586
+ // (2px) + 2px margin + action half-leading (2px) ≈ 9.5px; below = action half-leading
587
+ // (2px) + this padding (7px) ≈ 9px. Equal by construction — don't re-balance one side alone.
588
+ .wui-toast:has(.wui-toast-action) {
589
+ padding-bottom: 7px;
531
590
  }
532
591
  .wui-toast-action:focus-visible {
533
592
  outline: 2px solid var(--wui-toast-accent);
@@ -540,18 +599,18 @@
540
599
  font-size: 0.65rem;
541
600
  }
542
601
 
543
- // A pure-CSS auto-dismiss countdown: the bar shrinks over the toast's duration; the JS side
544
- // listens for animationend to remove it. Because :hover/:focus-within pause the animation,
545
- // animationend cannot fire while the toast is being read — a self-synchronising timer.
602
+ // The auto-dismiss TIMER, deliberately invisible (height 0): the element still runs the
603
+ // countdown animation because its animationend is what dismisses the toast on both surfaces,
604
+ // and :hover/:focus-within pausing it is what keeps a toast open while being read. Do not
605
+ // remove the element or the animation — only its visibility is retired (a visible countdown
606
+ // reads as pressure).
546
607
  .wui-toast-progress {
547
608
  position: absolute;
548
609
  left: 0;
549
610
  bottom: 0;
550
- height: 3px;
611
+ height: 0;
551
612
  width: 100%;
552
613
  transform-origin: left center;
553
- background: var(--wui-toast-accent);
554
- opacity: 0.5;
555
614
  animation-name: wui-toast-countdown;
556
615
  animation-timing-function: linear;
557
616
  animation-fill-mode: forwards;
@@ -594,9 +653,146 @@
594
653
  }
595
654
  }
596
655
 
656
+ // ── Entrance variants (toast/WUI.toast `animation`; host default via AppToast/app-toast) ──
657
+ // The base .wui-toast entrance above is `rise`. The variants below animate the STANDALONE
658
+ // scale/translate properties so they never fight the swipe-to-dismiss inline transform.
659
+
660
+ // `organic` — studied frame-by-frame from Ivan Bjelajac's notification: the surface
661
+ // materializes softly while the status icon grows from a dot with a slight overshoot,
662
+ // and the message trails in last.
663
+ .wui-toast--anim-organic {
664
+ animation: wui-toast-materialize 360ms var(--wui-ease-out-quart);
665
+ }
666
+ .wui-toast--anim-organic .wui-toast-icon {
667
+ animation: wui-toast-icon-grow 420ms 60ms cubic-bezier(0.34, 1.45, 0.64, 1) backwards;
668
+ }
669
+ .wui-toast--anim-organic .wui-toast-content {
670
+ animation: wui-toast-content-in 300ms 190ms var(--wui-ease-out-quart) backwards;
671
+ }
672
+
673
+ // `jelly` — drops in with a squash-and-stretch landing.
674
+ .wui-toast--anim-jelly {
675
+ animation: wui-toast-jelly 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
676
+ }
677
+
678
+ // `spring` — slides up from below with an elastic settle.
679
+ .wui-toast--anim-spring {
680
+ animation: wui-toast-spring 550ms cubic-bezier(0.34, 1.56, 0.64, 1);
681
+ }
682
+
683
+ @keyframes wui-toast-materialize {
684
+ from {
685
+ opacity: 0;
686
+ scale: 0.97;
687
+ }
688
+ to {
689
+ opacity: 1;
690
+ scale: 1;
691
+ }
692
+ }
693
+ @keyframes wui-toast-icon-grow {
694
+ 0% {
695
+ scale: 0;
696
+ }
697
+ 70% {
698
+ scale: 1.18;
699
+ }
700
+ 100% {
701
+ scale: 1;
702
+ }
703
+ }
704
+ @keyframes wui-toast-content-in {
705
+ from {
706
+ opacity: 0;
707
+ translate: 6px 0;
708
+ }
709
+ to {
710
+ opacity: 1;
711
+ translate: 0 0;
712
+ }
713
+ }
714
+ @keyframes wui-toast-jelly {
715
+ 0% {
716
+ opacity: 0;
717
+ translate: 0 -46px;
718
+ scale: 0.6 0.6;
719
+ }
720
+ 55% {
721
+ opacity: 1;
722
+ translate: 0 6px;
723
+ scale: 1.05 0.85;
724
+ }
725
+ 75% {
726
+ translate: 0 -2px;
727
+ scale: 0.97 1.05;
728
+ }
729
+ 100% {
730
+ opacity: 1;
731
+ translate: 0 0;
732
+ scale: 1 1;
733
+ }
734
+ }
735
+ @keyframes wui-toast-spring {
736
+ from {
737
+ opacity: 0;
738
+ translate: 0 64px;
739
+ scale: 0.92;
740
+ }
741
+ to {
742
+ opacity: 1;
743
+ translate: 0 0;
744
+ scale: 1;
745
+ }
746
+ }
747
+
748
+ // ── Inverse surface (toast/WUI.toast `surface: 'inverse'`) ──
749
+ // The toast renders on the OPPOSITE surface of the active mode — a dark card on a light
750
+ // page, a light card on a dark page — so the stack always contrasts with the content.
751
+ // Everything inside consumes --color-* vars, so re-pointing them here restyles the whole
752
+ // card (icon, text hierarchy, action pill, progress track) in one place.
753
+ .wui-toast--surface-inverse {
754
+ --color-surface: var(--wui-dark-bg-surface);
755
+ --color-border: var(--wui-dark-border);
756
+ --color-text: var(--wui-dark-text);
757
+ --color-text-muted: var(--wui-dark-text-muted);
758
+ --color-bg-muted: var(--wui-dark-bg-muted);
759
+ }
760
+ // Bootstrap's .btn-close glyph is ink-dark; on the dark card it needs the white filter.
761
+ .wui-toast--surface-inverse .wui-toast-close {
762
+ filter: invert(1) grayscale(100%) brightness(200%);
763
+ }
764
+ :root[data-theme="dark"] .wui-toast--surface-inverse,
765
+ :root[data-bs-theme="dark"] .wui-toast--surface-inverse {
766
+ --color-surface: var(--wui-bg-light);
767
+ --color-border: var(--wui-border-light);
768
+ --color-text: var(--wui-text-heading);
769
+ --color-text-muted: var(--wui-text-2);
770
+ --color-bg-muted: var(--wui-bg-muted);
771
+ }
772
+ :root[data-theme="dark"] .wui-toast--surface-inverse .wui-toast-close,
773
+ :root[data-bs-theme="dark"] .wui-toast--surface-inverse .wui-toast-close {
774
+ filter: none;
775
+ }
776
+
777
+ // ── Corner treatments (toast/WUI.toast `shape`) — `rounded` is the base radius above ──
778
+ .wui-toast--shape-pill {
779
+ border-radius: 999px;
780
+ align-items: center;
781
+ padding-block: 12px;
782
+ }
783
+ .wui-toast--shape-pill .wui-toast-icon,
784
+ .wui-toast--shape-pill .wui-toast-close {
785
+ margin-top: 0;
786
+ }
787
+ .wui-toast--shape-sharp {
788
+ border-radius: 4px;
789
+ }
790
+
597
791
  @media (prefers-reduced-motion: reduce) {
598
- // Drop the entrance slide; the countdown bar stays — it is the auto-dismiss timer, not decoration.
599
- .wui-toast {
792
+ // Drop the entrance motion; the countdown bar stays — it is the auto-dismiss timer, not decoration.
793
+ .wui-toast,
794
+ .wui-toast--anim-organic .wui-toast-icon,
795
+ .wui-toast--anim-organic .wui-toast-content {
600
796
  animation: none;
601
797
  transition: none;
602
798
  }
@@ -0,0 +1,23 @@
1
+ // ── Shared tone-pill color mixin ──
2
+ // Consolidates the tone→color mapping used by .badge, .status-label, and .chip:
3
+ // background: var(--color-{tone}-soft)
4
+ // color: var(--color-{tone}-text) (override via $text — see below)
5
+ // border-color: transparent
6
+ //
7
+ // $text exists because the status tones and `primary` name their on-tint color
8
+ // differently: --color-{status}-text IS the on-tint hue, but --color-primary-text
9
+ // is the text-on-SOLID-primary color (near-black in dark mode) and fails AA on the
10
+ // soft tint — primary pills pass var(--color-primary) instead.
11
+ //
12
+ // Component-specific overrides (e.g. .badge-primary's colored border) are applied
13
+ // after the mixin call.
14
+
15
+ @mixin wui-tone-pill-colors($tone, $text: null) {
16
+ background: var(--color-#{$tone}-soft);
17
+ @if $text {
18
+ color: $text;
19
+ } @else {
20
+ color: var(--color-#{$tone}-text);
21
+ }
22
+ border-color: transparent;
23
+ }
@@ -0,0 +1,138 @@
1
+ // ── Toolbar ──
2
+ // AppToolbar / <app-toolbar> — a Figma-style tool strip: a pill of icon tools where
3
+ // the active tool expands to reveal its label. Radio-backed (no JS needed on Razor);
4
+ // the expand/collapse is pure CSS off :checked.
5
+ //
6
+ // Part of the wui-components layer — see _wui-components.scss for load order.
7
+
8
+ .toolbar {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ gap: 4px;
12
+ padding: 6px;
13
+ background: var(--color-surface);
14
+ border: 1px solid var(--color-border);
15
+ border-radius: var(--wui-radius-lg);
16
+ corner-shape: squircle;
17
+ box-shadow: var(--wui-shadow-overlay);
18
+ }
19
+ .toolbar.is-disabled {
20
+ opacity: 0.6;
21
+ pointer-events: none;
22
+ }
23
+
24
+ .toolbar-item {
25
+ display: inline-flex;
26
+ align-items: center;
27
+ height: 36px;
28
+ padding-inline: 9px;
29
+ border-radius: var(--wui-radius);
30
+ color: var(--color-text-muted);
31
+ cursor: pointer;
32
+ margin-bottom: 0;
33
+ transition:
34
+ background-color 160ms var(--wui-ease-ui),
35
+ color 160ms var(--wui-ease-ui);
36
+ }
37
+ .toolbar-item:hover {
38
+ background: var(--color-bg-muted);
39
+ color: var(--color-text);
40
+ }
41
+ // The active tool: brand fill + the label slides open beside the icon.
42
+ .toolbar-item:has(.toolbar-input:checked) {
43
+ background: var(--color-primary);
44
+ color: var(--color-primary-text);
45
+ }
46
+ .toolbar-item:has(.toolbar-input:disabled) {
47
+ opacity: 0.5;
48
+ cursor: default;
49
+ }
50
+ .toolbar-item:has(.toolbar-input:focus-visible) {
51
+ outline: 2px solid var(--color-primary);
52
+ outline-offset: 2px;
53
+ }
54
+
55
+ // Visually hidden but focusable/announceable — same trick as .segmented-input.
56
+ .toolbar-input {
57
+ position: absolute;
58
+ width: 1px;
59
+ height: 1px;
60
+ margin: 0;
61
+ padding: 0;
62
+ border: 0;
63
+ clip-path: inset(50%);
64
+ overflow: hidden;
65
+ white-space: nowrap;
66
+ appearance: none;
67
+ opacity: 0;
68
+ }
69
+
70
+ .toolbar-item-icon {
71
+ display: grid;
72
+ place-items: center;
73
+ line-height: 1;
74
+ }
75
+
76
+ .toolbar-item-label {
77
+ max-width: 0;
78
+ margin-left: 0;
79
+ opacity: 0;
80
+ overflow: hidden;
81
+ white-space: nowrap;
82
+ font-size: 0.875rem;
83
+ font-weight: 500;
84
+ transition:
85
+ max-width 240ms var(--wui-ease-ui),
86
+ margin-left 240ms var(--wui-ease-ui),
87
+ opacity 180ms var(--wui-ease-ui);
88
+ }
89
+ .toolbar-item:has(.toolbar-input:checked) .toolbar-item-label {
90
+ max-width: 10rem;
91
+ margin-left: 7px;
92
+ opacity: 1;
93
+ }
94
+
95
+ .toolbar-sep {
96
+ width: 1px;
97
+ align-self: stretch;
98
+ margin-block: 6px;
99
+ margin-inline: 3px;
100
+ background: var(--color-border);
101
+ }
102
+
103
+ .toolbar-action {
104
+ display: inline-flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ width: 36px;
108
+ height: 36px;
109
+ padding: 0;
110
+ border: none;
111
+ border-radius: var(--wui-radius);
112
+ background: transparent;
113
+ color: var(--color-text-muted);
114
+ cursor: pointer;
115
+ transition:
116
+ background-color 160ms var(--wui-ease-ui),
117
+ color 160ms var(--wui-ease-ui);
118
+ }
119
+ .toolbar-action:hover:not(:disabled) {
120
+ background: var(--color-bg-muted);
121
+ color: var(--color-text);
122
+ }
123
+ .toolbar-action:focus-visible {
124
+ outline: 2px solid var(--color-primary);
125
+ outline-offset: 2px;
126
+ }
127
+ .toolbar-action:disabled {
128
+ opacity: 0.5;
129
+ cursor: default;
130
+ }
131
+
132
+ @media (prefers-reduced-motion: reduce) {
133
+ .toolbar-item,
134
+ .toolbar-item-label,
135
+ .toolbar-action {
136
+ transition: none;
137
+ }
138
+ }
@@ -121,29 +121,19 @@
121
121
 
122
122
  /* Tones — tinted surface + on-tint text; icon/remove inherit the tone colour. */
123
123
  .chip.primary {
124
- border-color: transparent;
125
- background: var(--color-primary-soft);
126
- color: var(--color-primary);
124
+ @include wui-tone-pill-colors(primary, var(--color-primary));
127
125
  }
128
126
  .chip.success {
129
- border-color: transparent;
130
- background: var(--color-success-soft);
131
- color: var(--color-success-text);
127
+ @include wui-tone-pill-colors(success);
132
128
  }
133
129
  .chip.warning {
134
- border-color: transparent;
135
- background: var(--color-warning-soft);
136
- color: var(--color-warning-text);
130
+ @include wui-tone-pill-colors(warning);
137
131
  }
138
132
  .chip.danger {
139
- border-color: transparent;
140
- background: var(--color-danger-soft);
141
- color: var(--color-danger-text);
133
+ @include wui-tone-pill-colors(danger);
142
134
  }
143
135
  .chip.info {
144
- border-color: transparent;
145
- background: var(--color-info-soft);
146
- color: var(--color-info-text);
136
+ @include wui-tone-pill-colors(info);
147
137
  }
148
138
  .chip.primary .chip-ic,
149
139
  .chip.success .chip-ic,
@@ -238,6 +228,25 @@
238
228
  min-width: 0;
239
229
  align-items: center;
240
230
  }
231
+ // Chips scale with the control size — the taller the field, the larger its tokens
232
+ // (the element always emits `.chip.chip-sm`; these override it per size tier). The
233
+ // two-class `.ms-chips .chip` outranks the single `.chip-sm`, so md is the base and
234
+ // the sm/lg tiers layer over it.
235
+ .ms-chips .chip {
236
+ gap: 5px;
237
+ padding: 3px 9px;
238
+ font-size: 0.8125rem;
239
+ }
240
+ .search-select-sm .ms-chips .chip {
241
+ gap: 4px;
242
+ padding: 2px 8px;
243
+ font-size: 0.75rem;
244
+ }
245
+ .search-select-lg .ms-chips .chip {
246
+ gap: 6px;
247
+ padding: 5px 12px;
248
+ font-size: 0.875rem;
249
+ }
241
250
  // Collapse mode (collapse-chips): chips stay on one row; the JS folds the overflow into
242
251
  // a "+N" counter (see WuiMultiSelect._collapseChips).
243
252
  wui-multiselect[collapse-chips] .ms-chips {
@@ -295,6 +304,27 @@ wui-multiselect[collapse-chips] .ms-chips {
295
304
  .ms-input:focus {
296
305
  outline: none;
297
306
  }
307
+ // AppTagInput lays the input on the LEFT with the chips filling and wrapping to its
308
+ // right (its template renders the input before the chips). Multiselect keeps its
309
+ // chips-first layout, so these overrides are scoped to `wui-taginput`.
310
+ wui-taginput .ms-input {
311
+ // Don't grow to fill — hold a modest resting width on the left; the chips take
312
+ // everything to the right. (align-items:center on the control centres the input
313
+ // against the chip row, so it needs no min-height of its own.)
314
+ flex: 0 1 auto;
315
+ flex-basis: 120px;
316
+ min-width: 96px;
317
+ }
318
+ wui-taginput .ms-chips {
319
+ flex: 1 1 auto;
320
+ }
321
+ // Match AppInput's per-size heights: the shared `.search-select-multi` floors md at
322
+ // 40px, but AppInput's md is the 44px control-height token — raise the floor so the
323
+ // sm/md/lg progression lines up with a text field beside it. (sm 36 / lg 52 already
324
+ // come from the size-specific `.search-select-{sm,lg} .search-select-control` rules.)
325
+ wui-taginput .search-select-multi {
326
+ min-height: var(--wui-control-height);
327
+ }
298
328
 
299
329
  /* ── Choice card: image / custom-template variant ── */
300
330
  .struct-card-img-card {
@@ -73,7 +73,6 @@
73
73
  background: transparent;
74
74
  color: var(--color-text-subtle);
75
75
  cursor: help;
76
- transform: scaleY(-1);
77
76
  transition: color 0.15s;
78
77
  }
79
78
  .infotip-btn:hover {
@@ -598,6 +597,35 @@
598
597
  color: var(--color-primary);
599
598
  }
600
599
 
600
+ // AppAlert extras — a leading icon and/or a bold heading line. Bootstrap already
601
+ // styles `.alert`/`.alert-{variant}`; these rules only lay out the icon column and
602
+ // give the plain `.alert-heading` div its type treatment (Bootstrap's own rule is
603
+ // just `color: inherit`, meant for an <h4>).
604
+ .alert-has-icon {
605
+ display: flex;
606
+ align-items: flex-start;
607
+ gap: 12px;
608
+ }
609
+ .alert-icon {
610
+ flex: none;
611
+ display: inline-flex;
612
+ // Nudge onto the first line's optical centre.
613
+ margin-top: 1px;
614
+ }
615
+ .alert-content {
616
+ flex: 1;
617
+ min-width: 0;
618
+ }
619
+ .alert .alert-heading {
620
+ margin-bottom: 2px;
621
+ font-size: 1rem;
622
+ font-weight: 700;
623
+ }
624
+ // The body's trailing paragraph shouldn't add space below the alert.
625
+ .alert .alert-content > :last-child {
626
+ margin-bottom: 0;
627
+ }
628
+
601
629
  // ============================================================================
602
630
  // Inline help glyph — a hint attached to a word rather than to a field label.
603
631
  // (For a field label use `.infotip`, which is a real popover button.)