@webority/theme 0.7.7 → 0.7.8

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.
@@ -230,6 +230,10 @@
230
230
  background: var(--color-bg-muted);
231
231
  color: var(--color-text);
232
232
  }
233
+ .wui-expand-toggle:focus-visible {
234
+ outline: 2px solid var(--color-primary);
235
+ outline-offset: 2px;
236
+ }
233
237
  .wui-expand-toggle .bi {
234
238
  transition: transform 0.18s var(--wui-ease-out-quart, ease);
235
239
  }
@@ -429,6 +433,10 @@
429
433
  background: color-mix(in srgb, var(--color-primary) 10%, transparent);
430
434
  color: var(--color-primary);
431
435
  }
436
+ .tpg-arrow:focus-visible {
437
+ outline: 2px solid var(--color-primary);
438
+ outline-offset: 2px;
439
+ }
432
440
  // Text-5 already reads as "unavailable" — no opacity dimming on top of it.
433
441
  .tpg-arrow:disabled {
434
442
  color: var(--color-border-strong);
@@ -460,6 +468,10 @@
460
468
  background: color-mix(in srgb, var(--color-primary) 10%, transparent);
461
469
  color: var(--color-primary);
462
470
  }
471
+ .tpg-page:focus-visible {
472
+ outline: 2px solid var(--color-primary);
473
+ outline-offset: 2px;
474
+ }
463
475
  .tpg-page.is-active {
464
476
  background: var(--color-primary);
465
477
  color: var(--color-primary-text);
@@ -627,6 +639,10 @@
627
639
  background: color-mix(in srgb, var(--color-primary) 10%, transparent);
628
640
  color: var(--color-primary);
629
641
  }
642
+ .u-action:focus-visible {
643
+ outline: 2px solid var(--color-primary);
644
+ outline-offset: 2px;
645
+ }
630
646
  .u-action:disabled {
631
647
  opacity: 0.4;
632
648
  cursor: not-allowed;
@@ -729,6 +745,10 @@
729
745
  cursor: pointer;
730
746
  user-select: none;
731
747
  }
748
+ .table th.sortable:focus-visible {
749
+ outline: 2px solid var(--color-primary);
750
+ outline-offset: -2px;
751
+ }
732
752
  .table th.sortable .sort-icon {
733
753
  // Reserved space so the header does not jump width when the arrow appears.
734
754
  display: inline-block;
@@ -127,7 +127,7 @@
127
127
  padding: 0 4px;
128
128
  border-radius: 999px;
129
129
  background: var(--color-danger);
130
- color: #fff;
130
+ color: var(--wui-bg-light);
131
131
  font-size: 10px;
132
132
  font-weight: 700;
133
133
  line-height: 1;
@@ -329,46 +329,275 @@
329
329
  font-weight: 500;
330
330
  }
331
331
 
332
+ // ── Tooltip ──
333
+ // Both surfaces render Bootstrap's .tooltip / .tooltip-inner (React via react-bootstrap
334
+ // <Tooltip>, Razor via Bootstrap's own tooltip JS). The wrappers tag the tooltip element with
335
+ // `wui-tooltip` (React className, Razor data-bs-custom-class), so this design lands on App
336
+ // tooltips only and never on a raw Bootstrap one. Every colour flows --wui-tooltip-* →
337
+ // Bootstrap's own --bs-tooltip-* vars, so the arrow (which Bootstrap paints from
338
+ // --bs-tooltip-bg) always tracks the body. Customisation seam: override a --wui-tooltip-* var
339
+ // from a caller className for any colour, or add a tone-* class for a preset.
340
+ //
341
+ // The --wui-tooltip-* defaults sit at ZERO specificity (:where) so a caller's own single class
342
+ // (or inline style) overrides them; the tone presets are ordinary (0,3,0) rules that win over
343
+ // the defaults but lose to nothing a caller would reasonably write.
344
+ :where(.tooltip.wui-tooltip) {
345
+ --wui-tooltip-bg: var(--color-surface);
346
+ --wui-tooltip-color: var(--color-text);
347
+ --wui-tooltip-border: var(--color-border-light);
348
+ --wui-tooltip-radius: var(--radius, 10px);
349
+ --wui-tooltip-shadow: var(--wui-shadow-overlay);
350
+ // Size is these four vars — the `md` default. A size-* class swaps them; a caller can
351
+ // override any one from its own className for a fully custom size.
352
+ --wui-tooltip-max-width: 240px;
353
+ --wui-tooltip-font-size: 0.8125rem;
354
+ --wui-tooltip-padding-y: 7px;
355
+ --wui-tooltip-padding-x: 11px;
356
+ }
357
+ .tooltip.wui-tooltip {
358
+ --bs-tooltip-bg: var(--wui-tooltip-bg);
359
+ --bs-tooltip-color: var(--wui-tooltip-color);
360
+ --bs-tooltip-max-width: var(--wui-tooltip-max-width);
361
+ --bs-tooltip-border-radius: var(--wui-tooltip-radius);
362
+ --bs-tooltip-font-size: var(--wui-tooltip-font-size);
363
+ --bs-tooltip-padding-y: var(--wui-tooltip-padding-y);
364
+ --bs-tooltip-padding-x: var(--wui-tooltip-padding-x);
365
+ --bs-tooltip-opacity: 1;
366
+ }
367
+ .tooltip.wui-tooltip .tooltip-inner {
368
+ // Padding + font-size come from the --bs-tooltip-* vars above (driven by --wui-tooltip-*),
369
+ // so a size-* class or a custom var resizes the tip without touching this rule.
370
+ font-weight: 500;
371
+ line-height: 1.4;
372
+ text-align: left;
373
+ border: 1px solid var(--wui-tooltip-border);
374
+ box-shadow: var(--wui-tooltip-shadow);
375
+ }
376
+
377
+ // Size presets — swap the four size vars. `md` is the :where default (no class).
378
+ .tooltip.wui-tooltip.size-sm {
379
+ --wui-tooltip-max-width: 200px;
380
+ --wui-tooltip-font-size: 0.75rem;
381
+ --wui-tooltip-padding-y: 4px;
382
+ --wui-tooltip-padding-x: 8px;
383
+ }
384
+ .tooltip.wui-tooltip.size-lg {
385
+ --wui-tooltip-max-width: 300px;
386
+ --wui-tooltip-font-size: 0.9375rem;
387
+ --wui-tooltip-padding-y: 10px;
388
+ --wui-tooltip-padding-x: 14px;
389
+ }
390
+ // A hairline shadow so the light default's white arrow still reads on a white page; on a tinted
391
+ // tone the arrow is coloured and this just adds subtle depth.
392
+ .tooltip.wui-tooltip .tooltip-arrow {
393
+ filter: drop-shadow(0 1px 1px color-mix(in srgb, var(--color-text) 14%, transparent));
394
+ }
395
+
396
+ // Tone presets — swap the three colour vars; the arrow follows --bs-tooltip-bg automatically.
397
+ // Fills that carry white text use the AA-cleared -strong tokens; warning stays dark-on-yellow.
398
+ .tooltip.wui-tooltip.tone-dark {
399
+ --wui-tooltip-bg: var(--color-text);
400
+ --wui-tooltip-color: #fff;
401
+ --wui-tooltip-border: transparent;
402
+ }
403
+ .tooltip.wui-tooltip.tone-primary {
404
+ --wui-tooltip-bg: var(--color-primary);
405
+ --wui-tooltip-color: var(--color-primary-text);
406
+ --wui-tooltip-border: transparent;
407
+ }
408
+ .tooltip.wui-tooltip.tone-success {
409
+ --wui-tooltip-bg: var(--wui-success-strong);
410
+ --wui-tooltip-color: #fff;
411
+ --wui-tooltip-border: transparent;
412
+ }
413
+ .tooltip.wui-tooltip.tone-danger {
414
+ --wui-tooltip-bg: var(--wui-danger-strong);
415
+ --wui-tooltip-color: #fff;
416
+ --wui-tooltip-border: transparent;
417
+ }
418
+ .tooltip.wui-tooltip.tone-warning {
419
+ --wui-tooltip-bg: var(--color-warning);
420
+ --wui-tooltip-color: var(--color-text);
421
+ --wui-tooltip-border: transparent;
422
+ }
423
+
332
424
  // ── Toast (notifications) ──
333
- // Bootstrap toast styling + wui extensions for title/action buttons.
334
- // Used by both React AppToastContainer and Razor WUI.toast.
425
+ // Surface-card toasts shared by React <AppToast/> and Razor <app-toast>/WUI.toast: an elevated
426
+ // card with a left accent bar + variant icon, an auto-dismiss progress bar (pauses on hover/focus),
427
+ // and swipe-to-dismiss. The variant tint drives one custom prop so every part stays in step.
335
428
 
336
- // Toast header with title
337
- .wui-toast-header {
429
+ .wui-toast-container {
430
+ z-index: var(--wui-z-toast);
338
431
  display: flex;
339
- align-items: center;
340
- justify-content: space-between;
432
+ flex-direction: column;
433
+ gap: 10px;
434
+ max-width: min(92vw, 400px);
435
+ }
436
+
437
+ .wui-toast {
438
+ --wui-toast-accent: var(--color-primary);
439
+ position: relative;
440
+ display: flex;
441
+ align-items: flex-start;
341
442
  gap: 12px;
342
- padding: 10px 12px;
343
- border: none;
443
+ width: 100%;
444
+ padding: 14px 14px 14px 18px;
445
+ background: var(--color-surface);
446
+ color: var(--color-text);
447
+ border: 1px solid var(--color-border-light);
448
+ border-radius: var(--radius-lg, 12px);
449
+ box-shadow: var(--wui-shadow-overlay);
450
+ overflow: hidden;
451
+ touch-action: pan-y;
452
+ user-select: none;
453
+ transition:
454
+ transform 0.2s ease,
455
+ opacity 0.2s ease;
456
+ animation: wui-toast-in 0.24s ease;
344
457
  }
345
458
 
346
- .wui-toast-header strong {
347
- font-size: 14px;
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
+ // While a swipe is in progress the toast follows the pointer with no transition.
469
+ .wui-toast--dragging {
470
+ transition: none;
471
+ cursor: grabbing;
472
+ }
473
+
474
+ .wui-toast--success {
475
+ --wui-toast-accent: var(--color-success);
476
+ }
477
+ .wui-toast--info {
478
+ --wui-toast-accent: var(--color-info);
479
+ }
480
+ .wui-toast--warning {
481
+ --wui-toast-accent: var(--color-warning);
482
+ }
483
+ .wui-toast--error,
484
+ .wui-toast--danger {
485
+ --wui-toast-accent: var(--color-danger);
486
+ }
487
+
488
+ .wui-toast-icon {
489
+ flex: none;
490
+ display: inline-flex;
491
+ margin-top: 1px;
492
+ color: var(--wui-toast-accent);
493
+ line-height: 1;
494
+ }
495
+
496
+ .wui-toast-content {
497
+ flex: 1 1 auto;
498
+ min-width: 0;
499
+ display: flex;
500
+ flex-direction: column;
501
+ gap: 2px;
502
+ }
503
+
504
+ .wui-toast-title {
505
+ font-size: 0.9375rem;
348
506
  font-weight: 600;
349
- color: inherit;
507
+ color: var(--color-text);
350
508
  }
351
509
 
352
- // Toast action button — inherits text color from the variant tone. Scoped to the
353
- // component class, not every .btn-link a consumer might place in a toast body.
354
- .toast .wui-toast-action {
355
- color: inherit;
510
+ .wui-toast-message {
511
+ font-size: 0.875rem;
512
+ color: var(--color-text);
513
+ overflow-wrap: anywhere;
514
+ }
515
+
516
+ // Action button — tinted with the variant accent, scoped to the toast so it never
517
+ // restyles an arbitrary .btn-link a rich body might contain.
518
+ .wui-toast-action {
519
+ flex: none;
520
+ align-self: center;
521
+ color: var(--wui-toast-accent);
356
522
  text-decoration: none;
357
- font-size: 13px;
358
- font-weight: 500;
523
+ font-size: 0.8125rem;
524
+ font-weight: 600;
359
525
  padding: 2px 8px;
360
526
  white-space: nowrap;
527
+ }
528
+ .wui-toast-action:hover {
529
+ color: var(--wui-toast-accent);
530
+ text-decoration: underline;
531
+ }
532
+ .wui-toast-action:focus-visible {
533
+ outline: 2px solid var(--wui-toast-accent);
534
+ outline-offset: 2px;
535
+ }
536
+
537
+ .wui-toast-close {
361
538
  flex: none;
362
- opacity: 0.9;
363
- transition: opacity 150ms;
539
+ margin-top: 1px;
540
+ font-size: 0.65rem;
364
541
  }
365
542
 
366
- .toast .wui-toast-action:hover {
367
- opacity: 1;
368
- text-decoration: underline;
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.
546
+ .wui-toast-progress {
547
+ position: absolute;
548
+ left: 0;
549
+ bottom: 0;
550
+ height: 3px;
551
+ width: 100%;
552
+ transform-origin: left center;
553
+ background: var(--wui-toast-accent);
554
+ opacity: 0.5;
555
+ animation-name: wui-toast-countdown;
556
+ animation-timing-function: linear;
557
+ animation-fill-mode: forwards;
558
+ }
559
+ .wui-toast:hover .wui-toast-progress,
560
+ .wui-toast:focus-within .wui-toast-progress {
561
+ animation-play-state: paused;
369
562
  }
370
563
 
371
- .toast .wui-toast-action:focus-visible {
372
- outline: 2px solid currentColor;
373
- outline-offset: 2px;
564
+ // "+N more" chip when the stack exceeds maxVisible.
565
+ .wui-toast-more {
566
+ align-self: center;
567
+ font-size: 0.75rem;
568
+ font-weight: 600;
569
+ color: var(--color-text-subtle);
570
+ background: var(--color-surface);
571
+ border: 1px solid var(--color-border-light);
572
+ border-radius: 999px;
573
+ padding: 2px 12px;
574
+ box-shadow: var(--wui-shadow-overlay);
575
+ }
576
+
577
+ @keyframes wui-toast-countdown {
578
+ from {
579
+ transform: scaleX(1);
580
+ }
581
+ to {
582
+ transform: scaleX(0);
583
+ }
584
+ }
585
+
586
+ @keyframes wui-toast-in {
587
+ from {
588
+ opacity: 0;
589
+ transform: translateY(8px);
590
+ }
591
+ to {
592
+ opacity: 1;
593
+ transform: translateY(0);
594
+ }
595
+ }
596
+
597
+ @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 {
600
+ animation: none;
601
+ transition: none;
602
+ }
374
603
  }
@@ -263,13 +263,25 @@
263
263
  height: 0;
264
264
  }
265
265
  // Between the brand row and the nav — a workspace/tenant switcher, an environment
266
- // badge. It must not shrink when the nav overflows, and it has no place on the
267
- // 88px rail, so the collapsed state removes it (the mobile drawer restores it).
266
+ // badge. It must not shrink when the nav overflows. The full card has no place on
267
+ // the 88px rail, so the collapsed state hides it (the mobile drawer restores it).
268
+ // The compact twin (`sidebarStartCompact` / `<app-shell-sidebar-start-compact>`) is
269
+ // the inverse: hidden expanded, shown only on the desktop collapsed rail, sized for
270
+ // a 36px monogram control (AppWorkspaceSwitcher compact).
268
271
  .app-sidebar-start {
269
272
  flex: none;
270
273
  margin-bottom: 8px;
271
274
  overflow: hidden;
272
275
  }
276
+ .app-sidebar-start-compact {
277
+ display: none;
278
+ flex: none;
279
+ margin-bottom: 8px;
280
+ // Centre the fixed square in the 88px rail so it lines up with the icon column.
281
+ justify-content: center;
282
+ align-items: center;
283
+ overflow: hidden;
284
+ }
273
285
  .app-sidebar-foot {
274
286
  flex: none;
275
287
  padding-top: 12px;
@@ -374,13 +386,31 @@
374
386
  gap: 0;
375
387
  padding: 9px 0;
376
388
  }
377
- .app-shell.collapsed .app-nav-text,
378
389
  .app-shell.collapsed .app-nav-badge,
379
390
  .app-shell.collapsed .app-nav-group-label,
380
391
  .app-shell.collapsed .app-sidebar-brand-text,
381
392
  .app-shell.collapsed .app-sidebar-start {
382
393
  display: none;
383
394
  }
395
+ // The label leaves the VISUAL flow but must stay in the accessibility tree — an
396
+ // icon-only rail link with display:none text has no accessible name (axe
397
+ // link-name, serious). Clip-rect keeps the name; no phantom gap since it takes
398
+ // no space.
399
+ .app-shell.collapsed .app-nav-text {
400
+ position: absolute;
401
+ width: 1px;
402
+ height: 1px;
403
+ margin: -1px;
404
+ padding: 0;
405
+ overflow: hidden;
406
+ clip-path: inset(50%);
407
+ white-space: nowrap;
408
+ border: 0;
409
+ }
410
+ // Swap: expanded slot off, compact monogram on — only when the author filled the compact slot.
411
+ .app-shell.collapsed .app-sidebar-start-compact {
412
+ display: flex;
413
+ }
384
414
 
385
415
  // ── Badge tones ──────────────────────────────────────────────────────────────
386
416
  // A nav badge can carry a status tone so an alert count reads red, not neutral.
@@ -651,6 +681,10 @@
651
681
  .app-shell.app-shell.collapsed .app-sidebar-start {
652
682
  display: revert;
653
683
  }
684
+ // Drawer always shows full labels — the compact rail control has no role here.
685
+ .app-shell.app-shell.collapsed .app-sidebar-start-compact {
686
+ display: none;
687
+ }
654
688
  .app-shell.app-shell .app-sidebar-toggle {
655
689
  display: none;
656
690
  }
@@ -0,0 +1,239 @@
1
+ // Every class here is namespaced `.workspace-switcher*`, and the rules that have to beat
2
+ // Bootstrap's own .dropdown-item/.dropdown-menu specificity are additionally scoped under
3
+ // the .workspace-switcher root — so this file cannot reach anything a product owns unless
4
+ // the product claims that same prefix (see the CHANGELOG's claimed-class-names section).
5
+ // Tenant / workspace switcher — shared by React <AppWorkspaceSwitcher> and Razor
6
+ // <app-workspace-switcher>. Both render a Bootstrap dropdown, so the menu chrome below
7
+ // keys on the shared classes rather than on either surface's own wrapper.
8
+ //
9
+ // Part of the wui-components layer — see _wui-components.scss for the whole picture and the
10
+ // load order. Order within the layer is LOAD-BEARING: these partials are a split of one
11
+ // stylesheet, not independent modules. Never reorder the imports.
12
+
13
+ .workspace-switcher {
14
+ // The switcher fills whatever slot it is dropped into (a sidebar rail, a topbar cell);
15
+ // the compact trigger opts out below because its whole point is a fixed square.
16
+ display: block;
17
+ width: 100%;
18
+ }
19
+
20
+ // ── Trigger ──
21
+ .workspace-switcher-trigger {
22
+ display: flex;
23
+ align-items: center;
24
+ gap: 10px;
25
+ width: 100%;
26
+ min-height: 48px;
27
+ padding: 6px 10px;
28
+ background: var(--color-bg-subtle);
29
+ border: 1px solid var(--color-border-light);
30
+ border-radius: var(--radius-lg);
31
+ corner-shape: squircle;
32
+ color: var(--color-text);
33
+ text-align: left;
34
+ cursor: pointer;
35
+ transition: background 180ms cubic-bezier(0.4, 0, 0.2, 1);
36
+ }
37
+ .workspace-switcher-trigger:hover {
38
+ background: var(--color-primary-soft);
39
+ }
40
+ .workspace-switcher-trigger:focus-visible {
41
+ outline: 2px solid var(--color-primary);
42
+ outline-offset: 2px;
43
+ }
44
+ .workspace-switcher-trigger.is-compact {
45
+ width: 36px;
46
+ min-height: 36px;
47
+ height: 36px;
48
+ padding: 0;
49
+ justify-content: center;
50
+ gap: 0;
51
+ border-radius: var(--radius);
52
+ }
53
+ .workspace-switcher-trigger-name {
54
+ flex: 1 1 auto;
55
+ min-width: 0;
56
+ font-size: 14px;
57
+ font-weight: 600;
58
+ // A workspace name is user data: truncate rather than let it push the chevron out.
59
+ overflow: hidden;
60
+ text-overflow: ellipsis;
61
+ white-space: nowrap;
62
+ }
63
+ .workspace-switcher-chevron {
64
+ flex: none;
65
+ color: var(--color-text-muted);
66
+ }
67
+
68
+ // ── Monogram / logo ──
69
+ // One class for both so a logo swap never changes the layout box.
70
+ .workspace-switcher-mark {
71
+ flex: none;
72
+ display: inline-flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ width: 28px;
76
+ height: 28px;
77
+ border-radius: var(--radius);
78
+ corner-shape: squircle;
79
+ background: var(--color-primary-soft);
80
+ color: var(--color-primary);
81
+ font-size: 11px;
82
+ font-weight: 700;
83
+ letter-spacing: 0.02em;
84
+ line-height: 1;
85
+ object-fit: cover;
86
+ overflow: hidden;
87
+ }
88
+
89
+ // ── Menu ──
90
+ .workspace-switcher .workspace-switcher-menu {
91
+ width: 280px;
92
+ max-width: 280px;
93
+ padding: 6px;
94
+ background: var(--color-surface);
95
+ border: 1px solid var(--color-border-light);
96
+ border-radius: var(--radius-lg);
97
+ corner-shape: squircle;
98
+ box-shadow: var(--wui-shadow-overlay);
99
+ }
100
+ .workspace-switcher-menu-label {
101
+ padding: 6px 10px 4px;
102
+ font-size: 11px;
103
+ font-weight: 700;
104
+ letter-spacing: 0.06em;
105
+ text-transform: uppercase;
106
+ color: var(--color-text-subtle);
107
+ }
108
+ .workspace-switcher-search {
109
+ display: flex;
110
+ align-items: center;
111
+ gap: 8px;
112
+ margin: 2px 4px 6px;
113
+ padding: 6px 10px;
114
+ background: var(--color-bg-subtle);
115
+ border: 1px solid var(--color-border-light);
116
+ border-radius: var(--radius);
117
+ corner-shape: squircle;
118
+ color: var(--color-text-muted);
119
+ }
120
+ .workspace-switcher-search-input {
121
+ flex: 1 1 auto;
122
+ min-width: 0;
123
+ border: none;
124
+ background: transparent;
125
+ color: var(--color-text);
126
+ font-size: 13px;
127
+ outline: none;
128
+ }
129
+ .workspace-switcher-list {
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: 2px;
133
+ max-height: 264px;
134
+ overflow-y: auto;
135
+ scrollbar-width: thin;
136
+ scrollbar-color: var(--color-border-strong) transparent;
137
+ }
138
+
139
+ // The rows are Bootstrap dropdown items on both surfaces, so the shared item chrome has to
140
+ // be re-stated here — an identity row is a three-column grid, not a text line with a glyph.
141
+ .workspace-switcher .workspace-switcher-option,
142
+ .workspace-switcher .workspace-switcher-create {
143
+ display: flex;
144
+ align-items: center;
145
+ gap: 10px;
146
+ width: 100%;
147
+ padding: 7px 10px;
148
+ border: none;
149
+ border-radius: var(--radius);
150
+ corner-shape: squircle;
151
+ background: transparent;
152
+ color: var(--color-text);
153
+ font-size: 14px;
154
+ text-align: left;
155
+ text-decoration: none;
156
+ cursor: pointer;
157
+ }
158
+ .workspace-switcher .workspace-switcher-option:hover,
159
+ .workspace-switcher .workspace-switcher-create:hover {
160
+ background: var(--color-bg-subtle);
161
+ color: var(--color-text);
162
+ }
163
+ .workspace-switcher .workspace-switcher-option:focus-visible,
164
+ .workspace-switcher .workspace-switcher-create:focus-visible,
165
+ .workspace-switcher .workspace-switcher-footer-action:focus-visible {
166
+ outline: 2px solid var(--color-primary);
167
+ outline-offset: 2px;
168
+ }
169
+ // The active workspace keeps its tint through hover, so the tick is never the only cue.
170
+ .workspace-switcher .workspace-switcher-option.is-active,
171
+ .workspace-switcher .workspace-switcher-option.is-active:hover {
172
+ background: var(--color-primary-soft);
173
+ color: var(--color-primary);
174
+ }
175
+ .workspace-switcher-option-name {
176
+ flex: 1 1 auto;
177
+ min-width: 0;
178
+ overflow: hidden;
179
+ text-overflow: ellipsis;
180
+ white-space: nowrap;
181
+ }
182
+ .workspace-switcher-badge {
183
+ flex: none;
184
+ padding: 1px 7px;
185
+ border-radius: 999px;
186
+ background: var(--color-bg-subtle);
187
+ color: var(--color-text-muted);
188
+ font-size: 11px;
189
+ font-weight: 600;
190
+ }
191
+ .workspace-switcher-tick {
192
+ flex: none;
193
+ color: var(--color-primary);
194
+ }
195
+ .workspace-switcher-empty {
196
+ padding: 14px 10px;
197
+ text-align: center;
198
+ font-size: 13px;
199
+ color: var(--color-text-muted);
200
+ }
201
+ .workspace-switcher .workspace-switcher-create {
202
+ color: var(--color-primary);
203
+ font-weight: 600;
204
+ }
205
+
206
+ // ── Footer ──
207
+ // A single workspace still gets a menu: current row + this footer. The divider is what makes
208
+ // that read as a complete panel rather than a truncated list.
209
+ .workspace-switcher-footer {
210
+ margin-top: 6px;
211
+ padding-top: 6px;
212
+ border-top: 1px solid var(--color-border-light);
213
+ }
214
+ .workspace-switcher .workspace-switcher-footer-action {
215
+ display: flex;
216
+ align-items: center;
217
+ gap: 10px;
218
+ width: 100%;
219
+ padding: 7px 10px;
220
+ border: none;
221
+ border-radius: var(--radius);
222
+ corner-shape: squircle;
223
+ background: transparent;
224
+ color: var(--color-text-muted);
225
+ font-size: 13px;
226
+ text-align: left;
227
+ text-decoration: none;
228
+ cursor: pointer;
229
+ }
230
+ .workspace-switcher .workspace-switcher-footer-action:hover {
231
+ background: var(--color-bg-subtle);
232
+ color: var(--color-text);
233
+ }
234
+
235
+ @media (prefers-reduced-motion: reduce) {
236
+ .workspace-switcher-trigger {
237
+ transition: none;
238
+ }
239
+ }