@wtfalch/design 0.3.2 → 0.5.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.
package/dist/valet.css CHANGED
@@ -2608,6 +2608,61 @@ input[type='radio']:active:not(:disabled) {
2608
2608
  the package shipped a `tone="bad"` prop that changed nothing. */
2609
2609
  .dialog-bad { border-color: color-mix(in oklab, var(--bad) 45%, var(--border)); }
2610
2610
 
2611
+ /* The sheet: the same window, anchored to an edge.
2612
+
2613
+ Only placement and entry differ. The trap, the restore, the scrim and the
2614
+ header/body/footer are the modal's, which is the point of it being a prop
2615
+ rather than a second component. */
2616
+ .backdrop.sheeted { padding: 0; }
2617
+ .backdrop.sheeted.from-right { justify-content: flex-end; align-items: stretch; }
2618
+ .backdrop.sheeted.from-left { justify-content: flex-start; align-items: stretch; }
2619
+ .backdrop.sheeted.from-bottom { align-items: flex-end; justify-content: stretch; }
2620
+
2621
+ .modal.sheet {
2622
+ max-height: 100%;
2623
+ border-radius: 0;
2624
+ box-shadow: var(--shadow-3);
2625
+ }
2626
+
2627
+ .modal.sheet.from-right,
2628
+ .modal.sheet.from-left {
2629
+ height: 100%;
2630
+ /* A default that is a panel rather than a window. `width` overrides it. */
2631
+ width: min(26rem, 100vw);
2632
+ max-width: 100vw;
2633
+ }
2634
+
2635
+ .modal.sheet.from-right { border-left: var(--border-width) solid var(--border); }
2636
+ .modal.sheet.from-left { border-right: var(--border-width) solid var(--border); }
2637
+
2638
+ .modal.sheet.from-bottom {
2639
+ width: 100%;
2640
+ max-width: 100%;
2641
+ max-height: 85vh;
2642
+ border-top: var(--border-width) solid var(--border);
2643
+ /* Rounded at the top only: the bottom edge is the screen's. */
2644
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
2645
+ }
2646
+
2647
+ /* It comes from the edge it is anchored to. A sheet that fades in place is a
2648
+ modal wearing a sheet's shape -- the movement is what says which way it
2649
+ goes and where it will go back to. */
2650
+ .modal.sheet.from-right[data-entering] { animation: sheet-right var(--dur-md) var(--ease-out); }
2651
+ .modal.sheet.from-right[data-exiting] { animation: sheet-right var(--dur-fast) var(--ease) reverse; }
2652
+ .modal.sheet.from-left[data-entering] { animation: sheet-left var(--dur-md) var(--ease-out); }
2653
+ .modal.sheet.from-left[data-exiting] { animation: sheet-left var(--dur-fast) var(--ease) reverse; }
2654
+ .modal.sheet.from-bottom[data-entering] { animation: sheet-bottom var(--dur-md) var(--ease-out); }
2655
+ .modal.sheet.from-bottom[data-exiting] { animation: sheet-bottom var(--dur-fast) var(--ease) reverse; }
2656
+
2657
+ @keyframes sheet-right { from { transform: translateX(100%); } to { transform: none; } }
2658
+ @keyframes sheet-left { from { transform: translateX(-100%); } to { transform: none; } }
2659
+ @keyframes sheet-bottom { from { transform: translateY(100%); } to { transform: none; } }
2660
+
2661
+ @media (prefers-reduced-motion: reduce) {
2662
+ .modal.sheet[data-entering],
2663
+ .modal.sheet[data-exiting] { animation: none; }
2664
+ }
2665
+
2611
2666
  /* ---- ./slider.css ---- */
2612
2667
  /* slider ----------------------------------------------------------------- */
2613
2668
  /* A number from a range -- `Slider.tsx` says when one is the right control.
@@ -3361,6 +3416,743 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3361
3416
  .tour-actions { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-2); }
3362
3417
 
3363
3418
  .tour-count { color: var(--muted); font-size: var(--text-xs); }
3419
+
3420
+ /* The mail and forum shell, added in 0.4.0. After the rest, because the
3421
+ anchored surfaces (`.pop`, `.menu-sheet`, `.cmd`) sit over everything and a
3422
+ rule of equal specificity that came earlier would lose to whatever the
3423
+ sheets they cover happen to set. */
3424
+ /* ---- ./scrollarea.css ---- */
3425
+ /* A box that scrolls, with its edges drawn.
3426
+
3427
+ The fades are `::before`/`::after` on the scroller itself, pinned with
3428
+ `position: sticky` rather than `absolute`: an absolutely positioned overlay
3429
+ inside a scrolling box scrolls with the content, so the gradient slides off
3430
+ the top the moment anybody moves. Sticky keeps it against the padding edge
3431
+ while the content runs underneath. */
3432
+ .scroller {
3433
+ position: relative;
3434
+ min-height: 0;
3435
+ min-width: 0;
3436
+ /* A scroll that reaches the end and carries on into the page behind it is
3437
+ how a pane scrolls the whole app away. */
3438
+ overscroll-behavior: contain;
3439
+ scrollbar-color: var(--border-strong) transparent;
3440
+ scrollbar-width: thin;
3441
+ }
3442
+
3443
+ /* A labelled scroller is focusable, so it owes a visible focus indicator --
3444
+ the same obligation the pager's buttons have. `--focus-ring` is a
3445
+ box-shadow value, not an outline shorthand. */
3446
+ .scroller:focus-visible { outline: none; box-shadow: var(--focus-ring); }
3447
+
3448
+ .scroller.axis-y { overflow-y: auto; overflow-x: hidden; }
3449
+ .scroller.axis-x { overflow-x: auto; overflow-y: hidden; }
3450
+ .scroller.axis-both { overflow: auto; }
3451
+
3452
+ .scroller::-webkit-scrollbar { width: var(--space-3); height: var(--space-3); }
3453
+ .scroller::-webkit-scrollbar-track { background: transparent; }
3454
+ .scroller::-webkit-scrollbar-thumb {
3455
+ background: var(--border-strong);
3456
+ /* Inset by a transparent border rather than by width, so the thumb is thin
3457
+ while the grab target stays the full `--space-3`. A bar drawn at the
3458
+ thumb's width looks right and cannot be hit. */
3459
+ border: var(--space-1) solid transparent;
3460
+ background-clip: padding-box;
3461
+ border-radius: var(--radius-pill);
3462
+ }
3463
+ .scroller::-webkit-scrollbar-thumb:hover { background: var(--muted); background-clip: padding-box; }
3464
+ .scroller::-webkit-scrollbar-corner { background: transparent; }
3465
+
3466
+ .scroller.barless { scrollbar-width: none; }
3467
+ .scroller.barless::-webkit-scrollbar { width: 0; height: 0; }
3468
+
3469
+ /* The fades.
3470
+
3471
+ Vertical only, and that is the whole point rather than a shortcut: the
3472
+ misreading this exists to prevent is "the list ended", which is a claim
3473
+ about the bottom edge. Content cut off at the right edge of a box already
3474
+ reads as cut off -- a word sliced in half is its own affordance -- so a
3475
+ horizontal scroller gets the scrollbar and nothing else.
3476
+
3477
+ `--panel` because that is what a scroller is nearly always sitting on;
3478
+ `fade={false}` is the escape for when it is not. */
3479
+ .scroller.faded::before,
3480
+ .scroller.faded::after {
3481
+ content: '';
3482
+ position: sticky;
3483
+ left: 0;
3484
+ display: block;
3485
+ height: var(--space-5);
3486
+ pointer-events: none;
3487
+ opacity: 0;
3488
+ transition: opacity var(--dur-fast) var(--ease);
3489
+ z-index: 1;
3490
+ }
3491
+
3492
+ /* Negative margins on the far side, so neither strip takes a row of height
3493
+ from the content it is drawn over. */
3494
+ .scroller.faded::before {
3495
+ top: 0;
3496
+ margin-bottom: calc(var(--space-5) * -1);
3497
+ background: linear-gradient(to bottom, var(--panel), transparent);
3498
+ }
3499
+
3500
+ .scroller.faded::after {
3501
+ bottom: 0;
3502
+ margin-top: calc(var(--space-5) * -1);
3503
+ background: linear-gradient(to top, var(--panel), transparent);
3504
+ }
3505
+
3506
+ .scroller.faded[data-top='true']::before { opacity: 1; }
3507
+ .scroller.faded[data-bottom='true']::after { opacity: 1; }
3508
+
3509
+ /* A scroller that only goes sideways has no vertical edge to mark. */
3510
+ .scroller.faded.axis-x::before,
3511
+ .scroller.faded.axis-x::after { content: none; }
3512
+
3513
+ @media (prefers-reduced-motion: reduce) {
3514
+ .scroller.faded::before,
3515
+ .scroller.faded::after { transition: none; }
3516
+ }
3517
+
3518
+ /* ---- ./splitpane.css ---- */
3519
+ /* Two panes and the handle between them. */
3520
+ .split {
3521
+ display: flex;
3522
+ min-width: 0;
3523
+ min-height: 0;
3524
+ width: 100%;
3525
+ height: 100%;
3526
+ }
3527
+
3528
+ .split.dir-row { flex-direction: row; }
3529
+ .split.dir-column { flex-direction: column; }
3530
+
3531
+ /* `min-width: 0` is the whole reason this file exists.
3532
+
3533
+ A flex child will not shrink below the intrinsic width of its content, and
3534
+ the intrinsic width of a mail list is the longest unbroken subject line in
3535
+ it. Without this the handle stops partway across and nothing on screen says
3536
+ why -- it just refuses, differently for every mailbox. */
3537
+ .split-pane {
3538
+ min-width: 0;
3539
+ min-height: 0;
3540
+ overflow: hidden;
3541
+ display: flex;
3542
+ flex-direction: column;
3543
+ }
3544
+
3545
+ .split-first { flex: 0 0 var(--split); }
3546
+ .split-second { flex: 1 1 0; }
3547
+
3548
+ /* The handle is wider than it looks: a hairline is a correct-looking divider
3549
+ and an impossible pointer target. The grip is the line, drawn at
3550
+ `--border-width` down the middle of a `--space-3` strip, and the strip is
3551
+ the thing the pointer has to hit. */
3552
+ .split-handle {
3553
+ flex: none;
3554
+ position: relative;
3555
+ background: transparent;
3556
+ border: none;
3557
+ padding: 0;
3558
+ touch-action: none;
3559
+ }
3560
+
3561
+ .split.dir-row > .split-handle {
3562
+ width: var(--space-3);
3563
+ margin: 0 calc(var(--space-3) / -2);
3564
+ cursor: col-resize;
3565
+ z-index: 1;
3566
+ }
3567
+
3568
+ .split.dir-column > .split-handle {
3569
+ height: var(--space-3);
3570
+ margin: calc(var(--space-3) / -2) 0;
3571
+ cursor: row-resize;
3572
+ z-index: 1;
3573
+ }
3574
+
3575
+ /* The line itself, drawn in the middle of the target. */
3576
+ .split-grip {
3577
+ position: absolute;
3578
+ background: var(--border);
3579
+ transition: background var(--dur-fast) var(--ease);
3580
+ }
3581
+
3582
+ .split.dir-row .split-grip {
3583
+ top: 0;
3584
+ bottom: 0;
3585
+ left: 50%;
3586
+ width: var(--border-width);
3587
+ transform: translateX(-50%);
3588
+ }
3589
+
3590
+ .split.dir-column .split-grip {
3591
+ left: 0;
3592
+ right: 0;
3593
+ top: 50%;
3594
+ height: var(--border-width);
3595
+ transform: translateY(-50%);
3596
+ }
3597
+
3598
+ .split-handle:hover .split-grip,
3599
+ .split-handle:active .split-grip { background: var(--accent); }
3600
+
3601
+ /* Focus is on the grip rather than the padded target: a ring around a 12px
3602
+ transparent strip reads as a ring around nothing. */
3603
+ .split-handle:focus-visible { outline: none; }
3604
+ .split-handle:focus-visible .split-grip {
3605
+ background: var(--accent);
3606
+ outline: none;
3607
+ box-shadow: var(--focus-ring);
3608
+ }
3609
+
3610
+ /* While dragging. On the body, because the pointer crosses both panes and the
3611
+ text in them would otherwise flip the cursor to an I-beam the whole way. */
3612
+ body.splitting { cursor: col-resize; user-select: none; }
3613
+ body.splitting * { cursor: inherit !important; }
3614
+
3615
+ @media (prefers-reduced-motion: reduce) {
3616
+ .split-grip { transition: none; }
3617
+ }
3618
+
3619
+ /* ---- ./popover.css ---- */
3620
+ /* A small surface anchored to what opened it. Same sheet as the select's list,
3621
+ because they are the same object -- something that appeared over the page,
3622
+ attached to a control. */
3623
+ .pop {
3624
+ background: var(--panel);
3625
+ border: var(--border-width) solid var(--border);
3626
+ border-radius: var(--radius-md);
3627
+ box-shadow: var(--shadow-2);
3628
+ max-width: min(22rem, calc(100vw - var(--space-6)));
3629
+ /* React Aria writes `max-height` inline, to the space left in the viewport.
3630
+ A surface that runs to the bottom of the screen is not a popover any more,
3631
+ so the sheet caps it and scrolls instead. Same argument as the select
3632
+ list's 280px cap, and the same number is not required -- a popover holds
3633
+ controls rather than a list, and stops being one sooner. */
3634
+ max-height: 24rem;
3635
+ overflow: auto;
3636
+ overscroll-behavior: contain;
3637
+ /* React Aria positions this element inline; nothing here may set `position`,
3638
+ `top` or `left` or it fights the measurement and lands in the corner. */
3639
+ z-index: 60;
3640
+ }
3641
+
3642
+ .pop-body {
3643
+ padding: var(--space-3);
3644
+ outline: none;
3645
+ color: var(--text);
3646
+ font-size: var(--text-sm);
3647
+ }
3648
+
3649
+ /* Entry, and the direction it comes from. `data-placement` is React Aria's,
3650
+ set to where the surface actually went rather than where it was asked to go,
3651
+ so the movement agrees with the flip. */
3652
+ .pop[data-entering] { animation: pop-in var(--dur-fast) var(--ease-out); }
3653
+ .pop[data-exiting] { animation: pop-in var(--dur-fast) var(--ease) reverse; }
3654
+
3655
+ @keyframes pop-in {
3656
+ from { opacity: 0; transform: translateY(calc(var(--nudge) * -1)); }
3657
+ to { opacity: 1; transform: none; }
3658
+ }
3659
+
3660
+ .pop[data-placement='top'][data-entering],
3661
+ .pop[data-placement='top left'][data-entering],
3662
+ .pop[data-placement='top right'][data-entering] { animation-name: pop-in-up; }
3663
+
3664
+ @keyframes pop-in-up {
3665
+ from { opacity: 0; transform: translateY(var(--nudge)); }
3666
+ to { opacity: 1; transform: none; }
3667
+ }
3668
+
3669
+ @media (prefers-reduced-motion: reduce) {
3670
+ .pop[data-entering],
3671
+ .pop[data-exiting] { animation: none; }
3672
+ }
3673
+
3674
+ /* ---- ./menu.css ---- */
3675
+ /* A list of verbs, on the same sheet as every other anchored surface. */
3676
+ .menu-sheet {
3677
+ background: var(--panel);
3678
+ border: var(--border-width) solid var(--border);
3679
+ border-radius: var(--radius-md);
3680
+ box-shadow: var(--shadow-2);
3681
+ min-width: 12rem;
3682
+ max-width: min(20rem, calc(100vw - var(--space-6)));
3683
+ max-height: 24rem;
3684
+ overflow: auto;
3685
+ overscroll-behavior: contain;
3686
+ z-index: 60;
3687
+ }
3688
+
3689
+ .menu-list {
3690
+ padding: var(--space-1);
3691
+ outline: none;
3692
+ display: grid;
3693
+ gap: var(--border-width);
3694
+ }
3695
+
3696
+ .menu-item {
3697
+ display: flex;
3698
+ align-items: center;
3699
+ gap: var(--space-2);
3700
+ padding: var(--space-2) var(--space-2);
3701
+ border-radius: var(--radius-sm);
3702
+ font-size: var(--text-sm);
3703
+ color: var(--text);
3704
+ cursor: default;
3705
+ outline: none;
3706
+ }
3707
+
3708
+ /* One highlight for pointer and keyboard.
3709
+
3710
+ React Aria sets `data-focused` for whichever is driving, so the row under
3711
+ the cursor and the row the arrows are on are the same row drawn the same
3712
+ way. Two separate treatments -- `:hover` plus a `.active` class -- is how a
3713
+ menu ends up showing two highlighted rows at once, which is the state where
3714
+ pressing Enter does something other than what it looks like it will do. */
3715
+ .menu-item[data-focused] { background: var(--panel-2); }
3716
+ .menu-item[data-disabled] { color: var(--muted); opacity: 0.6; }
3717
+
3718
+ .menu-icon { flex: none; width: var(--space-4); height: var(--space-4); color: var(--muted); }
3719
+ .menu-item[data-focused] .menu-icon { color: var(--text); }
3720
+
3721
+ .menu-text { flex: 1 1 auto; min-width: 0; display: grid; }
3722
+ .menu-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3723
+ .menu-desc {
3724
+ font-size: var(--text-xs);
3725
+ color: var(--muted);
3726
+ overflow: hidden;
3727
+ text-overflow: ellipsis;
3728
+ white-space: nowrap;
3729
+ }
3730
+
3731
+ /* The binding, set in the mono face at the size of the small print. Right of
3732
+ the row and never the reason the row is wide: `flex: none` with the label
3733
+ taking the slack. */
3734
+ .menu-key {
3735
+ flex: none;
3736
+ font-family: var(--font-mono);
3737
+ font-size: var(--text-2xs);
3738
+ color: var(--muted);
3739
+ letter-spacing: var(--tracking);
3740
+ }
3741
+
3742
+ .menu-more { flex: none; color: var(--muted); }
3743
+
3744
+ .menu-rule {
3745
+ height: var(--border-width);
3746
+ background: var(--border);
3747
+ border: none;
3748
+ margin: var(--space-1) 0;
3749
+ }
3750
+
3751
+ .menu-section-title {
3752
+ padding: var(--space-2) var(--space-2) var(--space-1);
3753
+ font-size: var(--text-2xs);
3754
+ text-transform: uppercase;
3755
+ letter-spacing: var(--tracking);
3756
+ color: var(--muted);
3757
+ }
3758
+
3759
+ /* Destructive.
3760
+
3761
+ Tinted into `--panel` at 10%, the same mix `Pill` uses, and for the same
3762
+ measured reason: mixing a mid tone into the darker surface drags the
3763
+ background towards the text and drops the row's contrast on a light theme.
3764
+ Colour is never the only signal -- the row is also last, also below a rule. */
3765
+ .menu-item.danger { color: var(--bad); }
3766
+ .menu-item.danger .menu-icon { color: var(--bad); }
3767
+ .menu-item.danger[data-focused] { background: color-mix(in srgb, var(--bad) 10%, var(--panel)); }
3768
+
3769
+ .menu-sheet[data-entering] { animation: pop-in var(--dur-fast) var(--ease-out); }
3770
+ .menu-sheet[data-exiting] { animation: pop-in var(--dur-fast) var(--ease) reverse; }
3771
+
3772
+ @media (prefers-reduced-motion: reduce) {
3773
+ .menu-sheet[data-entering],
3774
+ .menu-sheet[data-exiting] { animation: none; }
3775
+ }
3776
+
3777
+ /* ---- ./command.css ---- */
3778
+ /* The command palette: a field over the app and a list under it. */
3779
+ .cmd-scrim {
3780
+ position: fixed;
3781
+ inset: 0;
3782
+ z-index: 80;
3783
+ background: var(--scrim);
3784
+ display: flex;
3785
+ justify-content: center;
3786
+ /* Near the top rather than centred. The list grows downwards as you type,
3787
+ and a centred palette moves its own first row every keystroke -- which is
3788
+ the row you are aiming at. */
3789
+ align-items: flex-start;
3790
+ padding: 10vh var(--space-4) var(--space-4);
3791
+ }
3792
+
3793
+ .cmd {
3794
+ width: min(34rem, 100%);
3795
+ background: var(--panel);
3796
+ border: var(--border-width) solid var(--border);
3797
+ border-radius: var(--radius-lg);
3798
+ box-shadow: var(--shadow-3);
3799
+ overflow: hidden;
3800
+ max-height: 70vh;
3801
+ display: flex;
3802
+ flex-direction: column;
3803
+ }
3804
+
3805
+ .cmd-body { outline: none; display: flex; flex-direction: column; min-height: 0; }
3806
+
3807
+ .cmd-field {
3808
+ display: flex;
3809
+ align-items: center;
3810
+ gap: var(--space-2);
3811
+ padding: var(--space-3) var(--space-4);
3812
+ border-bottom: var(--border-width) solid var(--border);
3813
+ }
3814
+
3815
+ .cmd-mark { flex: none; width: var(--space-4); height: var(--space-4); color: var(--muted); }
3816
+
3817
+ /* No border, no focus ring on the input itself: the whole surface is the
3818
+ field, focus is already in it when it opens, and a ring around a box that
3819
+ fills the top of the palette is a ring around the palette. */
3820
+ .cmd-input {
3821
+ flex: 1 1 auto;
3822
+ min-width: 0;
3823
+ border: none;
3824
+ background: transparent;
3825
+ outline: none;
3826
+ color: var(--text);
3827
+ font: inherit;
3828
+ font-size: var(--text-md);
3829
+ }
3830
+
3831
+ .cmd-input::placeholder { color: var(--muted); }
3832
+
3833
+ .cmd-list {
3834
+ padding: var(--space-2);
3835
+ overflow: auto;
3836
+ overscroll-behavior: contain;
3837
+ outline: none;
3838
+ min-height: 0;
3839
+ }
3840
+
3841
+ .cmd-group-title {
3842
+ padding: var(--space-2) var(--space-2) var(--space-1);
3843
+ font-size: var(--text-2xs);
3844
+ text-transform: uppercase;
3845
+ letter-spacing: var(--tracking);
3846
+ color: var(--muted);
3847
+ }
3848
+
3849
+ .cmd-item {
3850
+ display: flex;
3851
+ align-items: center;
3852
+ gap: var(--space-2);
3853
+ padding: var(--space-2);
3854
+ border-radius: var(--radius-sm);
3855
+ font-size: var(--text-sm);
3856
+ color: var(--text);
3857
+ cursor: default;
3858
+ outline: none;
3859
+ }
3860
+
3861
+ /* Two pointers into the list, and they can disagree.
3862
+
3863
+ React Aria's `Menu` unifies these -- hovering a row moves the keyboard's
3864
+ focus to it -- but `ListBox` in 1.21 has no `shouldFocusOnHover` to turn
3865
+ that on, so a hover sets `data-hovered` and the arrows set `data-focused`
3866
+ independently. Styling only `data-focused`, which is what this did first,
3867
+ means the pointer gets no feedback at all until somebody presses an arrow
3868
+ key.
3869
+
3870
+ Both are drawn, and the keyboard's row is marked as well, because when both
3871
+ are showing they are two different answers to "what does Enter run" -- and
3872
+ it is the focused one. The bar is the tell. */
3873
+ .cmd-item[data-hovered],
3874
+ .cmd-item[data-focused] { background: var(--panel-2); }
3875
+
3876
+ .cmd-item[data-focused] {
3877
+ box-shadow: inset calc(var(--border-width) * 2) 0 0 var(--accent);
3878
+ }
3879
+ .cmd-item[data-disabled] { color: var(--muted); opacity: 0.6; }
3880
+
3881
+ .cmd-icon { flex: none; width: var(--space-4); height: var(--space-4); color: var(--muted); }
3882
+ .cmd-item[data-focused] .cmd-icon { color: var(--text); }
3883
+
3884
+ .cmd-text { flex: 1 1 auto; min-width: 0; display: grid; }
3885
+ .cmd-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3886
+ .cmd-desc {
3887
+ font-size: var(--text-xs);
3888
+ color: var(--muted);
3889
+ overflow: hidden;
3890
+ text-overflow: ellipsis;
3891
+ white-space: nowrap;
3892
+ }
3893
+
3894
+ .cmd-key {
3895
+ flex: none;
3896
+ font-family: var(--font-mono);
3897
+ font-size: var(--text-2xs);
3898
+ color: var(--muted);
3899
+ letter-spacing: var(--tracking);
3900
+ }
3901
+
3902
+ .cmd-empty {
3903
+ margin: 0;
3904
+ padding: var(--space-6) var(--space-4);
3905
+ text-align: center;
3906
+ color: var(--muted);
3907
+ font-size: var(--text-sm);
3908
+ }
3909
+
3910
+ .cmd-empty strong { color: var(--text); font-weight: var(--weight-strong); }
3911
+
3912
+ .cmd-scrim[data-entering] { animation: cmd-scrim-in var(--dur-fast) var(--ease-out); }
3913
+ .cmd-scrim[data-exiting] { animation: cmd-scrim-in var(--dur-fast) var(--ease) reverse; }
3914
+ .cmd[data-entering] { animation: cmd-in var(--dur-md) var(--ease-out); }
3915
+
3916
+ @keyframes cmd-scrim-in { from { opacity: 0; } to { opacity: 1; } }
3917
+ @keyframes cmd-in {
3918
+ from { opacity: 0; transform: translateY(calc(var(--nudge) * -1)) scale(0.98); }
3919
+ to { opacity: 1; transform: none; }
3920
+ }
3921
+
3922
+ @media (prefers-reduced-motion: reduce) {
3923
+ .cmd-scrim[data-entering],
3924
+ .cmd-scrim[data-exiting],
3925
+ .cmd[data-entering] { animation: none; }
3926
+ }
3927
+
3928
+ /* ---- ./pagination.css ---- */
3929
+ /* Moving through a list that does not fit, and saying how far in you are. */
3930
+ .pager {
3931
+ display: flex;
3932
+ align-items: center;
3933
+ justify-content: space-between;
3934
+ gap: var(--space-4);
3935
+ flex-wrap: wrap;
3936
+ padding: var(--space-2) 0;
3937
+ font-size: var(--text-xs);
3938
+ color: var(--muted);
3939
+ }
3940
+
3941
+ .pager-count { margin: 0; }
3942
+ .pager-count strong { color: var(--text); font-weight: var(--weight-strong); }
3943
+
3944
+ .pager-controls { display: flex; align-items: center; gap: var(--space-1); }
3945
+
3946
+ .pager-step,
3947
+ .pager-page {
3948
+ display: inline-flex;
3949
+ align-items: center;
3950
+ justify-content: center;
3951
+ /* Square, and 32px rather than the text's height: these are hit repeatedly
3952
+ and in sequence, and a target the size of its glyph is a target that gets
3953
+ missed on the fourth press. */
3954
+ min-width: var(--space-8);
3955
+ height: var(--space-8);
3956
+ padding: 0 var(--space-2);
3957
+ border: var(--border-width) solid transparent;
3958
+ border-radius: var(--radius-sm);
3959
+ background: transparent;
3960
+ color: var(--muted);
3961
+ font: inherit;
3962
+ font-variant-numeric: tabular-nums;
3963
+ cursor: pointer;
3964
+ transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
3965
+ }
3966
+
3967
+ .pager-step:hover:not(:disabled),
3968
+ .pager-page:hover:not(.on) { background: var(--panel-2); color: var(--text); }
3969
+
3970
+ /* `--focus-ring` is a box-shadow value (`0 0 0 2px var(--accent)`), not an
3971
+ outline shorthand — `outline: var(--focus-ring)` is invalid and drops
3972
+ silently, which is how these shipped with no visible focus at all. Every
3973
+ other stylesheet in the package uses `box-shadow`. */
3974
+ .pager-step:focus-visible,
3975
+ .pager-page:focus-visible { outline: none; box-shadow: var(--focus-ring); }
3976
+
3977
+ /* Disabled rather than hidden. An arrow that disappears at the first page
3978
+ moves every other control one step left, so the button under your cursor
3979
+ changes identity between clicks. */
3980
+ .pager-step:disabled { opacity: 0.35; cursor: default; }
3981
+
3982
+ .pager-pages {
3983
+ display: flex;
3984
+ align-items: center;
3985
+ gap: var(--space-1);
3986
+ margin: 0;
3987
+ padding: 0;
3988
+ list-style: none;
3989
+ }
3990
+
3991
+ /* The current page. Border as well as tint, because tint alone is a colour
3992
+ doing the work on its own -- and `aria-current` says it to a reader that
3993
+ sees neither. */
3994
+ .pager-page.on {
3995
+ color: var(--text);
3996
+ background: var(--panel-2);
3997
+ border-color: var(--border-strong);
3998
+ font-weight: var(--weight-strong);
3999
+ }
4000
+
4001
+ /* The gap is a field, drawn as a ghost.
4002
+
4003
+ At rest it is the elision it replaces: no border, no background, the `…` as
4004
+ its placeholder, sized and aligned like the page buttons beside it. Touch it
4005
+ and it becomes a control. That order matters -- a visible "go to page" box
4006
+ in a pager is a control almost nobody uses taking permanent space beside the
4007
+ ones everybody does, and a control that looks like punctuation is one nobody
4008
+ presses. This is punctuation until you press it.
4009
+
4010
+ The width comes from how many digits the largest page has, set by the
4011
+ component, so the row does not change size when the field is typed into --
4012
+ the same reason `pageWindow` holds seven slots at the ends. */
4013
+ .pager-jump {
4014
+ width: max(var(--space-8), calc(var(--digits, 2) * 0.62em + var(--space-5)));
4015
+ height: var(--space-8);
4016
+ padding: 0 var(--space-1);
4017
+ border: var(--border-width) solid transparent;
4018
+ border-radius: var(--radius-sm);
4019
+ background: transparent;
4020
+ color: var(--muted);
4021
+ font: inherit;
4022
+ font-variant-numeric: tabular-nums;
4023
+ text-align: center;
4024
+ cursor: pointer;
4025
+ transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
4026
+ color var(--dur-fast) var(--ease);
4027
+ }
4028
+
4029
+ /* Full strength, so the `…` is as legible as the numbers it sits between. */
4030
+ .pager-jump::placeholder { color: var(--muted); opacity: 1; }
4031
+
4032
+ .pager-jump:hover { background: var(--panel-2); color: var(--text); }
4033
+
4034
+ /* Typed into: it stops being an elision and becomes the control it always was.
4035
+ The placeholder goes because the field now has a value to show. */
4036
+ .pager-jump:focus {
4037
+ outline: none;
4038
+ box-shadow: var(--focus-ring);
4039
+ background: var(--control);
4040
+ border-color: var(--border-strong);
4041
+ color: var(--text);
4042
+ cursor: text;
4043
+ }
4044
+
4045
+ /* The spinners are two 10px targets stacked inside a 32px box, and pressing
4046
+ one by accident moves the page. The arrow keys still step the value. */
4047
+ .pager-jump { appearance: textfield; -moz-appearance: textfield; }
4048
+ .pager-jump::-webkit-outer-spin-button,
4049
+ .pager-jump::-webkit-inner-spin-button { appearance: none; margin: 0; }
4050
+
4051
+ @media (prefers-reduced-motion: reduce) {
4052
+ .pager-step,
4053
+ .pager-page,
4054
+ .pager-jump { transition: none; }
4055
+ }
4056
+
4057
+ /* ---- ./identity.css ---- */
4058
+ /* A person, said in one line: initials, name, sometimes the address. */
4059
+ .ident {
4060
+ display: inline-flex;
4061
+ align-items: center;
4062
+ gap: var(--space-2);
4063
+ min-width: 0;
4064
+ font-size: var(--text-sm);
4065
+ color: var(--text);
4066
+ }
4067
+
4068
+ /* The disc.
4069
+
4070
+ `--ident-hue` comes from the address and is the only thing the component
4071
+ sets inline; everything that decides how saturated and how light it is
4072
+ lives here, so the discs stay in the theme rather than staying the same
4073
+ colour on every theme. Mixed towards `--panel` for the same measured reason
4074
+ `Pill` mixes: a full-strength hue behind text drops the contrast on the
4075
+ light themes, and these carry two letters that have to be legible. */
4076
+ .ident-disc {
4077
+ flex: none;
4078
+ display: inline-flex;
4079
+ align-items: center;
4080
+ justify-content: center;
4081
+ width: var(--space-8);
4082
+ height: var(--space-8);
4083
+ border-radius: var(--radius-pill);
4084
+ background: color-mix(in srgb, hsl(var(--ident-hue) 60% 50%) 22%, var(--panel));
4085
+ border: var(--border-width) solid color-mix(in srgb, hsl(var(--ident-hue) 60% 50%) 45%, transparent);
4086
+ color: var(--text);
4087
+ font-size: var(--text-2xs);
4088
+ font-weight: var(--weight-strong);
4089
+ letter-spacing: normal;
4090
+ text-transform: uppercase;
4091
+ user-select: none;
4092
+ }
4093
+
4094
+ .ident.size-sm .ident-disc {
4095
+ width: var(--space-6);
4096
+ height: var(--space-6);
4097
+ font-size: var(--text-2xs);
4098
+ }
4099
+
4100
+ .ident-text { display: grid; min-width: 0; }
4101
+
4102
+ .ident-name {
4103
+ overflow: hidden;
4104
+ text-overflow: ellipsis;
4105
+ white-space: nowrap;
4106
+ font-weight: var(--weight);
4107
+ }
4108
+
4109
+ .ident-address {
4110
+ overflow: hidden;
4111
+ text-overflow: ellipsis;
4112
+ white-space: nowrap;
4113
+ font-size: var(--text-xs);
4114
+ color: var(--muted);
4115
+ }
4116
+
4117
+ .ident-aside { flex: none; color: var(--muted); font-size: var(--text-xs); }
4118
+
4119
+ /* A chip in a composer's To field: boxed, tighter, removable. */
4120
+ .ident.kind-chip {
4121
+ gap: var(--space-1);
4122
+ padding: var(--space-1);
4123
+ border: var(--border-width) solid var(--border);
4124
+ border-radius: var(--radius-pill);
4125
+ background: var(--panel-2);
4126
+ font-size: var(--text-xs);
4127
+ max-width: 100%;
4128
+ }
4129
+
4130
+ .ident.kind-chip .ident-disc {
4131
+ width: var(--space-5);
4132
+ height: var(--space-5);
4133
+ font-size: var(--text-2xs);
4134
+ }
4135
+
4136
+ .ident-remove {
4137
+ flex: none;
4138
+ display: inline-flex;
4139
+ align-items: center;
4140
+ justify-content: center;
4141
+ /* `--space-5` of button around a `--space-3` glyph. The cross is the
4142
+ smallest thing on a composer and it sits next to text somebody is typing,
4143
+ so it needs the target even though it does not look like it does. */
4144
+ width: var(--space-5);
4145
+ height: var(--space-5);
4146
+ padding: 0;
4147
+ border: none;
4148
+ border-radius: var(--radius-pill);
4149
+ background: transparent;
4150
+ color: var(--muted);
4151
+ cursor: pointer;
4152
+ }
4153
+
4154
+ .ident-remove:hover { background: var(--panel); color: var(--text); }
4155
+ .ident-remove:focus-visible { outline: none; box-shadow: var(--focus-ring); }
3364
4156
  /* ---- valet: the themes ---- */
3365
4157
  :root[data-theme='valet']{--bg:#f4f5f8;--panel:#ffffff;--panel-2:#eceef3;--border:#dcdfe7;--border-strong:#7b8597;--text:#171a21;--muted:#5b6474;--accent:#4f46e5;--accent-dim:#a9a4f0;--on-accent:#ffffff;--good:#1b7f4b;--warn:#8a5f0a;--bad:#bf3a31;--info:#0e6f8e;--app-bg:#f4f5f8;--shadow-1:0 4px 14px rgba(23, 26, 33, 0.08);--shadow-2:0 8px 24px rgba(23, 26, 33, 0.10);--shadow-3:0 12px 32px rgba(23, 26, 33, 0.12);--scrim:rgba(23, 26, 33, 0.32);color-scheme:light}
3366
4158
  :root[data-theme='valet-night']{--bg:#0c0f14;--panel:#141820;--panel-2:#1b2029;--border:#262c37;--border-strong:#616b7d;--text:#e8eaf0;--muted:#9ba4b5;--accent:#8f88ff;--accent-dim:#3f3a8f;--on-accent:#0d0b2e;--good:#5fcb8f;--warn:#e2ae58;--bad:#f28b84;--info:#57c4e8;--app-bg:#0c0f14;--shadow-1:0 6px 20px rgba(0, 0, 0, 0.28);--shadow-2:0 8px 28px rgba(0, 0, 0, 0.34);--shadow-3:0 10px 34px rgba(0, 0, 0, 0.38);--scrim:rgba(0, 0, 0, 0.5);color-scheme:dark}