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