@wtfalch/design 0.3.1 → 0.4.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.
@@ -2334,6 +2334,61 @@ input[type='radio']:active:not(:disabled) {
2334
2334
  the package shipped a `tone="bad"` prop that changed nothing. */
2335
2335
  .dialog-bad { border-color: color-mix(in oklab, var(--bad) 45%, var(--border)); }
2336
2336
 
2337
+ /* The sheet: the same window, anchored to an edge.
2338
+
2339
+ Only placement and entry differ. The trap, the restore, the scrim and the
2340
+ header/body/footer are the modal's, which is the point of it being a prop
2341
+ rather than a second component. */
2342
+ .backdrop.sheeted { padding: 0; }
2343
+ .backdrop.sheeted.from-right { justify-content: flex-end; align-items: stretch; }
2344
+ .backdrop.sheeted.from-left { justify-content: flex-start; align-items: stretch; }
2345
+ .backdrop.sheeted.from-bottom { align-items: flex-end; justify-content: stretch; }
2346
+
2347
+ .modal.sheet {
2348
+ max-height: 100%;
2349
+ border-radius: 0;
2350
+ box-shadow: var(--shadow-3);
2351
+ }
2352
+
2353
+ .modal.sheet.from-right,
2354
+ .modal.sheet.from-left {
2355
+ height: 100%;
2356
+ /* A default that is a panel rather than a window. `width` overrides it. */
2357
+ width: min(26rem, 100vw);
2358
+ max-width: 100vw;
2359
+ }
2360
+
2361
+ .modal.sheet.from-right { border-left: var(--border-width) solid var(--border); }
2362
+ .modal.sheet.from-left { border-right: var(--border-width) solid var(--border); }
2363
+
2364
+ .modal.sheet.from-bottom {
2365
+ width: 100%;
2366
+ max-width: 100%;
2367
+ max-height: 85vh;
2368
+ border-top: var(--border-width) solid var(--border);
2369
+ /* Rounded at the top only: the bottom edge is the screen's. */
2370
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
2371
+ }
2372
+
2373
+ /* It comes from the edge it is anchored to. A sheet that fades in place is a
2374
+ modal wearing a sheet's shape -- the movement is what says which way it
2375
+ goes and where it will go back to. */
2376
+ .modal.sheet.from-right[data-entering] { animation: sheet-right var(--dur-md) var(--ease-out); }
2377
+ .modal.sheet.from-right[data-exiting] { animation: sheet-right var(--dur-fast) var(--ease) reverse; }
2378
+ .modal.sheet.from-left[data-entering] { animation: sheet-left var(--dur-md) var(--ease-out); }
2379
+ .modal.sheet.from-left[data-exiting] { animation: sheet-left var(--dur-fast) var(--ease) reverse; }
2380
+ .modal.sheet.from-bottom[data-entering] { animation: sheet-bottom var(--dur-md) var(--ease-out); }
2381
+ .modal.sheet.from-bottom[data-exiting] { animation: sheet-bottom var(--dur-fast) var(--ease) reverse; }
2382
+
2383
+ @keyframes sheet-right { from { transform: translateX(100%); } to { transform: none; } }
2384
+ @keyframes sheet-left { from { transform: translateX(-100%); } to { transform: none; } }
2385
+ @keyframes sheet-bottom { from { transform: translateY(100%); } to { transform: none; } }
2386
+
2387
+ @media (prefers-reduced-motion: reduce) {
2388
+ .modal.sheet[data-entering],
2389
+ .modal.sheet[data-exiting] { animation: none; }
2390
+ }
2391
+
2337
2392
  /* ---- ./slider.css ---- */
2338
2393
  /* slider ----------------------------------------------------------------- */
2339
2394
  /* A number from a range -- `Slider.tsx` says when one is the right control.
@@ -3088,3 +3143,740 @@ input[type='range'].slider:disabled::-moz-range-thumb { opacity: 0; }
3088
3143
 
3089
3144
  .tour-count { color: var(--muted); font-size: var(--text-xs); }
3090
3145
 
3146
+ /* The mail and forum shell, added in 0.4.0. After the rest, because the
3147
+ anchored surfaces (`.pop`, `.menu-sheet`, `.cmd`) sit over everything and a
3148
+ rule of equal specificity that came earlier would lose to whatever the
3149
+ sheets they cover happen to set. */
3150
+ /* ---- ./scrollarea.css ---- */
3151
+ /* A box that scrolls, with its edges drawn.
3152
+
3153
+ The fades are `::before`/`::after` on the scroller itself, pinned with
3154
+ `position: sticky` rather than `absolute`: an absolutely positioned overlay
3155
+ inside a scrolling box scrolls with the content, so the gradient slides off
3156
+ the top the moment anybody moves. Sticky keeps it against the padding edge
3157
+ while the content runs underneath. */
3158
+ .scroller {
3159
+ position: relative;
3160
+ min-height: 0;
3161
+ min-width: 0;
3162
+ /* A scroll that reaches the end and carries on into the page behind it is
3163
+ how a pane scrolls the whole app away. */
3164
+ overscroll-behavior: contain;
3165
+ scrollbar-color: var(--border-strong) transparent;
3166
+ scrollbar-width: thin;
3167
+ }
3168
+
3169
+ /* A labelled scroller is focusable, so it owes a visible focus indicator --
3170
+ the same obligation the pager's buttons have. `--focus-ring` is a
3171
+ box-shadow value, not an outline shorthand. */
3172
+ .scroller:focus-visible { outline: none; box-shadow: var(--focus-ring); }
3173
+
3174
+ .scroller.axis-y { overflow-y: auto; overflow-x: hidden; }
3175
+ .scroller.axis-x { overflow-x: auto; overflow-y: hidden; }
3176
+ .scroller.axis-both { overflow: auto; }
3177
+
3178
+ .scroller::-webkit-scrollbar { width: var(--space-3); height: var(--space-3); }
3179
+ .scroller::-webkit-scrollbar-track { background: transparent; }
3180
+ .scroller::-webkit-scrollbar-thumb {
3181
+ background: var(--border-strong);
3182
+ /* Inset by a transparent border rather than by width, so the thumb is thin
3183
+ while the grab target stays the full `--space-3`. A bar drawn at the
3184
+ thumb's width looks right and cannot be hit. */
3185
+ border: var(--space-1) solid transparent;
3186
+ background-clip: padding-box;
3187
+ border-radius: var(--radius-pill);
3188
+ }
3189
+ .scroller::-webkit-scrollbar-thumb:hover { background: var(--muted); background-clip: padding-box; }
3190
+ .scroller::-webkit-scrollbar-corner { background: transparent; }
3191
+
3192
+ .scroller.barless { scrollbar-width: none; }
3193
+ .scroller.barless::-webkit-scrollbar { width: 0; height: 0; }
3194
+
3195
+ /* The fades.
3196
+
3197
+ Vertical only, and that is the whole point rather than a shortcut: the
3198
+ misreading this exists to prevent is "the list ended", which is a claim
3199
+ about the bottom edge. Content cut off at the right edge of a box already
3200
+ reads as cut off -- a word sliced in half is its own affordance -- so a
3201
+ horizontal scroller gets the scrollbar and nothing else.
3202
+
3203
+ `--panel` because that is what a scroller is nearly always sitting on;
3204
+ `fade={false}` is the escape for when it is not. */
3205
+ .scroller.faded::before,
3206
+ .scroller.faded::after {
3207
+ content: '';
3208
+ position: sticky;
3209
+ left: 0;
3210
+ display: block;
3211
+ height: var(--space-5);
3212
+ pointer-events: none;
3213
+ opacity: 0;
3214
+ transition: opacity var(--dur-fast) var(--ease);
3215
+ z-index: 1;
3216
+ }
3217
+
3218
+ /* Negative margins on the far side, so neither strip takes a row of height
3219
+ from the content it is drawn over. */
3220
+ .scroller.faded::before {
3221
+ top: 0;
3222
+ margin-bottom: calc(var(--space-5) * -1);
3223
+ background: linear-gradient(to bottom, var(--panel), transparent);
3224
+ }
3225
+
3226
+ .scroller.faded::after {
3227
+ bottom: 0;
3228
+ margin-top: calc(var(--space-5) * -1);
3229
+ background: linear-gradient(to top, var(--panel), transparent);
3230
+ }
3231
+
3232
+ .scroller.faded[data-top='true']::before { opacity: 1; }
3233
+ .scroller.faded[data-bottom='true']::after { opacity: 1; }
3234
+
3235
+ /* A scroller that only goes sideways has no vertical edge to mark. */
3236
+ .scroller.faded.axis-x::before,
3237
+ .scroller.faded.axis-x::after { content: none; }
3238
+
3239
+ @media (prefers-reduced-motion: reduce) {
3240
+ .scroller.faded::before,
3241
+ .scroller.faded::after { transition: none; }
3242
+ }
3243
+
3244
+ /* ---- ./splitpane.css ---- */
3245
+ /* Two panes and the handle between them. */
3246
+ .split {
3247
+ display: flex;
3248
+ min-width: 0;
3249
+ min-height: 0;
3250
+ width: 100%;
3251
+ height: 100%;
3252
+ }
3253
+
3254
+ .split.dir-row { flex-direction: row; }
3255
+ .split.dir-column { flex-direction: column; }
3256
+
3257
+ /* `min-width: 0` is the whole reason this file exists.
3258
+
3259
+ A flex child will not shrink below the intrinsic width of its content, and
3260
+ the intrinsic width of a mail list is the longest unbroken subject line in
3261
+ it. Without this the handle stops partway across and nothing on screen says
3262
+ why -- it just refuses, differently for every mailbox. */
3263
+ .split-pane {
3264
+ min-width: 0;
3265
+ min-height: 0;
3266
+ overflow: hidden;
3267
+ display: flex;
3268
+ flex-direction: column;
3269
+ }
3270
+
3271
+ .split-first { flex: 0 0 var(--split); }
3272
+ .split-second { flex: 1 1 0; }
3273
+
3274
+ /* The handle is wider than it looks: a hairline is a correct-looking divider
3275
+ and an impossible pointer target. The grip is the line, drawn at
3276
+ `--border-width` down the middle of a `--space-3` strip, and the strip is
3277
+ the thing the pointer has to hit. */
3278
+ .split-handle {
3279
+ flex: none;
3280
+ position: relative;
3281
+ background: transparent;
3282
+ border: none;
3283
+ padding: 0;
3284
+ touch-action: none;
3285
+ }
3286
+
3287
+ .split.dir-row > .split-handle {
3288
+ width: var(--space-3);
3289
+ margin: 0 calc(var(--space-3) / -2);
3290
+ cursor: col-resize;
3291
+ z-index: 1;
3292
+ }
3293
+
3294
+ .split.dir-column > .split-handle {
3295
+ height: var(--space-3);
3296
+ margin: calc(var(--space-3) / -2) 0;
3297
+ cursor: row-resize;
3298
+ z-index: 1;
3299
+ }
3300
+
3301
+ /* The line itself, drawn in the middle of the target. */
3302
+ .split-grip {
3303
+ position: absolute;
3304
+ background: var(--border);
3305
+ transition: background var(--dur-fast) var(--ease);
3306
+ }
3307
+
3308
+ .split.dir-row .split-grip {
3309
+ top: 0;
3310
+ bottom: 0;
3311
+ left: 50%;
3312
+ width: var(--border-width);
3313
+ transform: translateX(-50%);
3314
+ }
3315
+
3316
+ .split.dir-column .split-grip {
3317
+ left: 0;
3318
+ right: 0;
3319
+ top: 50%;
3320
+ height: var(--border-width);
3321
+ transform: translateY(-50%);
3322
+ }
3323
+
3324
+ .split-handle:hover .split-grip,
3325
+ .split-handle:active .split-grip { background: var(--accent); }
3326
+
3327
+ /* Focus is on the grip rather than the padded target: a ring around a 12px
3328
+ transparent strip reads as a ring around nothing. */
3329
+ .split-handle:focus-visible { outline: none; }
3330
+ .split-handle:focus-visible .split-grip {
3331
+ background: var(--accent);
3332
+ outline: none;
3333
+ box-shadow: var(--focus-ring);
3334
+ }
3335
+
3336
+ /* While dragging. On the body, because the pointer crosses both panes and the
3337
+ text in them would otherwise flip the cursor to an I-beam the whole way. */
3338
+ body.splitting { cursor: col-resize; user-select: none; }
3339
+ body.splitting * { cursor: inherit !important; }
3340
+
3341
+ @media (prefers-reduced-motion: reduce) {
3342
+ .split-grip { transition: none; }
3343
+ }
3344
+
3345
+ /* ---- ./popover.css ---- */
3346
+ /* A small surface anchored to what opened it. Same sheet as the select's list,
3347
+ because they are the same object -- something that appeared over the page,
3348
+ attached to a control. */
3349
+ .pop {
3350
+ background: var(--panel);
3351
+ border: var(--border-width) solid var(--border);
3352
+ border-radius: var(--radius-md);
3353
+ box-shadow: var(--shadow-2);
3354
+ max-width: min(22rem, calc(100vw - var(--space-6)));
3355
+ /* React Aria writes `max-height` inline, to the space left in the viewport.
3356
+ A surface that runs to the bottom of the screen is not a popover any more,
3357
+ so the sheet caps it and scrolls instead. Same argument as the select
3358
+ list's 280px cap, and the same number is not required -- a popover holds
3359
+ controls rather than a list, and stops being one sooner. */
3360
+ max-height: 24rem;
3361
+ overflow: auto;
3362
+ overscroll-behavior: contain;
3363
+ /* React Aria positions this element inline; nothing here may set `position`,
3364
+ `top` or `left` or it fights the measurement and lands in the corner. */
3365
+ z-index: 60;
3366
+ }
3367
+
3368
+ .pop-body {
3369
+ padding: var(--space-3);
3370
+ outline: none;
3371
+ color: var(--text);
3372
+ font-size: var(--text-sm);
3373
+ }
3374
+
3375
+ /* Entry, and the direction it comes from. `data-placement` is React Aria's,
3376
+ set to where the surface actually went rather than where it was asked to go,
3377
+ so the movement agrees with the flip. */
3378
+ .pop[data-entering] { animation: pop-in var(--dur-fast) var(--ease-out); }
3379
+ .pop[data-exiting] { animation: pop-in var(--dur-fast) var(--ease) reverse; }
3380
+
3381
+ @keyframes pop-in {
3382
+ from { opacity: 0; transform: translateY(calc(var(--nudge) * -1)); }
3383
+ to { opacity: 1; transform: none; }
3384
+ }
3385
+
3386
+ .pop[data-placement='top'][data-entering],
3387
+ .pop[data-placement='top left'][data-entering],
3388
+ .pop[data-placement='top right'][data-entering] { animation-name: pop-in-up; }
3389
+
3390
+ @keyframes pop-in-up {
3391
+ from { opacity: 0; transform: translateY(var(--nudge)); }
3392
+ to { opacity: 1; transform: none; }
3393
+ }
3394
+
3395
+ @media (prefers-reduced-motion: reduce) {
3396
+ .pop[data-entering],
3397
+ .pop[data-exiting] { animation: none; }
3398
+ }
3399
+
3400
+ /* ---- ./menu.css ---- */
3401
+ /* A list of verbs, on the same sheet as every other anchored surface. */
3402
+ .menu-sheet {
3403
+ background: var(--panel);
3404
+ border: var(--border-width) solid var(--border);
3405
+ border-radius: var(--radius-md);
3406
+ box-shadow: var(--shadow-2);
3407
+ min-width: 12rem;
3408
+ max-width: min(20rem, calc(100vw - var(--space-6)));
3409
+ max-height: 24rem;
3410
+ overflow: auto;
3411
+ overscroll-behavior: contain;
3412
+ z-index: 60;
3413
+ }
3414
+
3415
+ .menu-list {
3416
+ padding: var(--space-1);
3417
+ outline: none;
3418
+ display: grid;
3419
+ gap: var(--border-width);
3420
+ }
3421
+
3422
+ .menu-item {
3423
+ display: flex;
3424
+ align-items: center;
3425
+ gap: var(--space-2);
3426
+ padding: var(--space-2) var(--space-2);
3427
+ border-radius: var(--radius-sm);
3428
+ font-size: var(--text-sm);
3429
+ color: var(--text);
3430
+ cursor: default;
3431
+ outline: none;
3432
+ }
3433
+
3434
+ /* One highlight for pointer and keyboard.
3435
+
3436
+ React Aria sets `data-focused` for whichever is driving, so the row under
3437
+ the cursor and the row the arrows are on are the same row drawn the same
3438
+ way. Two separate treatments -- `:hover` plus a `.active` class -- is how a
3439
+ menu ends up showing two highlighted rows at once, which is the state where
3440
+ pressing Enter does something other than what it looks like it will do. */
3441
+ .menu-item[data-focused] { background: var(--panel-2); }
3442
+ .menu-item[data-disabled] { color: var(--muted); opacity: 0.6; }
3443
+
3444
+ .menu-icon { flex: none; width: var(--space-4); height: var(--space-4); color: var(--muted); }
3445
+ .menu-item[data-focused] .menu-icon { color: var(--text); }
3446
+
3447
+ .menu-text { flex: 1 1 auto; min-width: 0; display: grid; }
3448
+ .menu-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3449
+ .menu-desc {
3450
+ font-size: var(--text-xs);
3451
+ color: var(--muted);
3452
+ overflow: hidden;
3453
+ text-overflow: ellipsis;
3454
+ white-space: nowrap;
3455
+ }
3456
+
3457
+ /* The binding, set in the mono face at the size of the small print. Right of
3458
+ the row and never the reason the row is wide: `flex: none` with the label
3459
+ taking the slack. */
3460
+ .menu-key {
3461
+ flex: none;
3462
+ font-family: var(--font-mono);
3463
+ font-size: var(--text-2xs);
3464
+ color: var(--muted);
3465
+ letter-spacing: var(--tracking);
3466
+ }
3467
+
3468
+ .menu-more { flex: none; color: var(--muted); }
3469
+
3470
+ .menu-rule {
3471
+ height: var(--border-width);
3472
+ background: var(--border);
3473
+ border: none;
3474
+ margin: var(--space-1) 0;
3475
+ }
3476
+
3477
+ .menu-section-title {
3478
+ padding: var(--space-2) var(--space-2) var(--space-1);
3479
+ font-size: var(--text-2xs);
3480
+ text-transform: uppercase;
3481
+ letter-spacing: var(--tracking);
3482
+ color: var(--muted);
3483
+ }
3484
+
3485
+ /* Destructive.
3486
+
3487
+ Tinted into `--panel` at 10%, the same mix `Pill` uses, and for the same
3488
+ measured reason: mixing a mid tone into the darker surface drags the
3489
+ background towards the text and drops the row's contrast on a light theme.
3490
+ Colour is never the only signal -- the row is also last, also below a rule. */
3491
+ .menu-item.danger { color: var(--bad); }
3492
+ .menu-item.danger .menu-icon { color: var(--bad); }
3493
+ .menu-item.danger[data-focused] { background: color-mix(in srgb, var(--bad) 10%, var(--panel)); }
3494
+
3495
+ .menu-sheet[data-entering] { animation: pop-in var(--dur-fast) var(--ease-out); }
3496
+ .menu-sheet[data-exiting] { animation: pop-in var(--dur-fast) var(--ease) reverse; }
3497
+
3498
+ @media (prefers-reduced-motion: reduce) {
3499
+ .menu-sheet[data-entering],
3500
+ .menu-sheet[data-exiting] { animation: none; }
3501
+ }
3502
+
3503
+ /* ---- ./command.css ---- */
3504
+ /* The command palette: a field over the app and a list under it. */
3505
+ .cmd-scrim {
3506
+ position: fixed;
3507
+ inset: 0;
3508
+ z-index: 80;
3509
+ background: var(--scrim);
3510
+ display: flex;
3511
+ justify-content: center;
3512
+ /* Near the top rather than centred. The list grows downwards as you type,
3513
+ and a centred palette moves its own first row every keystroke -- which is
3514
+ the row you are aiming at. */
3515
+ align-items: flex-start;
3516
+ padding: 10vh var(--space-4) var(--space-4);
3517
+ }
3518
+
3519
+ .cmd {
3520
+ width: min(34rem, 100%);
3521
+ background: var(--panel);
3522
+ border: var(--border-width) solid var(--border);
3523
+ border-radius: var(--radius-lg);
3524
+ box-shadow: var(--shadow-3);
3525
+ overflow: hidden;
3526
+ max-height: 70vh;
3527
+ display: flex;
3528
+ flex-direction: column;
3529
+ }
3530
+
3531
+ .cmd-body { outline: none; display: flex; flex-direction: column; min-height: 0; }
3532
+
3533
+ .cmd-field {
3534
+ display: flex;
3535
+ align-items: center;
3536
+ gap: var(--space-2);
3537
+ padding: var(--space-3) var(--space-4);
3538
+ border-bottom: var(--border-width) solid var(--border);
3539
+ }
3540
+
3541
+ .cmd-mark { flex: none; width: var(--space-4); height: var(--space-4); color: var(--muted); }
3542
+
3543
+ /* No border, no focus ring on the input itself: the whole surface is the
3544
+ field, focus is already in it when it opens, and a ring around a box that
3545
+ fills the top of the palette is a ring around the palette. */
3546
+ .cmd-input {
3547
+ flex: 1 1 auto;
3548
+ min-width: 0;
3549
+ border: none;
3550
+ background: transparent;
3551
+ outline: none;
3552
+ color: var(--text);
3553
+ font: inherit;
3554
+ font-size: var(--text-md);
3555
+ }
3556
+
3557
+ .cmd-input::placeholder { color: var(--muted); }
3558
+
3559
+ .cmd-list {
3560
+ padding: var(--space-2);
3561
+ overflow: auto;
3562
+ overscroll-behavior: contain;
3563
+ outline: none;
3564
+ min-height: 0;
3565
+ }
3566
+
3567
+ .cmd-group-title {
3568
+ padding: var(--space-2) var(--space-2) var(--space-1);
3569
+ font-size: var(--text-2xs);
3570
+ text-transform: uppercase;
3571
+ letter-spacing: var(--tracking);
3572
+ color: var(--muted);
3573
+ }
3574
+
3575
+ .cmd-item {
3576
+ display: flex;
3577
+ align-items: center;
3578
+ gap: var(--space-2);
3579
+ padding: var(--space-2);
3580
+ border-radius: var(--radius-sm);
3581
+ font-size: var(--text-sm);
3582
+ color: var(--text);
3583
+ cursor: default;
3584
+ outline: none;
3585
+ }
3586
+
3587
+ /* Two pointers into the list, and they can disagree.
3588
+
3589
+ React Aria's `Menu` unifies these -- hovering a row moves the keyboard's
3590
+ focus to it -- but `ListBox` in 1.21 has no `shouldFocusOnHover` to turn
3591
+ that on, so a hover sets `data-hovered` and the arrows set `data-focused`
3592
+ independently. Styling only `data-focused`, which is what this did first,
3593
+ means the pointer gets no feedback at all until somebody presses an arrow
3594
+ key.
3595
+
3596
+ Both are drawn, and the keyboard's row is marked as well, because when both
3597
+ are showing they are two different answers to "what does Enter run" -- and
3598
+ it is the focused one. The bar is the tell. */
3599
+ .cmd-item[data-hovered],
3600
+ .cmd-item[data-focused] { background: var(--panel-2); }
3601
+
3602
+ .cmd-item[data-focused] {
3603
+ box-shadow: inset calc(var(--border-width) * 2) 0 0 var(--accent);
3604
+ }
3605
+ .cmd-item[data-disabled] { color: var(--muted); opacity: 0.6; }
3606
+
3607
+ .cmd-icon { flex: none; width: var(--space-4); height: var(--space-4); color: var(--muted); }
3608
+ .cmd-item[data-focused] .cmd-icon { color: var(--text); }
3609
+
3610
+ .cmd-text { flex: 1 1 auto; min-width: 0; display: grid; }
3611
+ .cmd-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3612
+ .cmd-desc {
3613
+ font-size: var(--text-xs);
3614
+ color: var(--muted);
3615
+ overflow: hidden;
3616
+ text-overflow: ellipsis;
3617
+ white-space: nowrap;
3618
+ }
3619
+
3620
+ .cmd-key {
3621
+ flex: none;
3622
+ font-family: var(--font-mono);
3623
+ font-size: var(--text-2xs);
3624
+ color: var(--muted);
3625
+ letter-spacing: var(--tracking);
3626
+ }
3627
+
3628
+ .cmd-empty {
3629
+ margin: 0;
3630
+ padding: var(--space-6) var(--space-4);
3631
+ text-align: center;
3632
+ color: var(--muted);
3633
+ font-size: var(--text-sm);
3634
+ }
3635
+
3636
+ .cmd-empty strong { color: var(--text); font-weight: var(--weight-strong); }
3637
+
3638
+ .cmd-scrim[data-entering] { animation: cmd-scrim-in var(--dur-fast) var(--ease-out); }
3639
+ .cmd-scrim[data-exiting] { animation: cmd-scrim-in var(--dur-fast) var(--ease) reverse; }
3640
+ .cmd[data-entering] { animation: cmd-in var(--dur-md) var(--ease-out); }
3641
+
3642
+ @keyframes cmd-scrim-in { from { opacity: 0; } to { opacity: 1; } }
3643
+ @keyframes cmd-in {
3644
+ from { opacity: 0; transform: translateY(calc(var(--nudge) * -1)) scale(0.98); }
3645
+ to { opacity: 1; transform: none; }
3646
+ }
3647
+
3648
+ @media (prefers-reduced-motion: reduce) {
3649
+ .cmd-scrim[data-entering],
3650
+ .cmd-scrim[data-exiting],
3651
+ .cmd[data-entering] { animation: none; }
3652
+ }
3653
+
3654
+ /* ---- ./pagination.css ---- */
3655
+ /* Moving through a list that does not fit, and saying how far in you are. */
3656
+ .pager {
3657
+ display: flex;
3658
+ align-items: center;
3659
+ justify-content: space-between;
3660
+ gap: var(--space-4);
3661
+ flex-wrap: wrap;
3662
+ padding: var(--space-2) 0;
3663
+ font-size: var(--text-xs);
3664
+ color: var(--muted);
3665
+ }
3666
+
3667
+ .pager-count { margin: 0; }
3668
+ .pager-count strong { color: var(--text); font-weight: var(--weight-strong); }
3669
+
3670
+ .pager-controls { display: flex; align-items: center; gap: var(--space-1); }
3671
+
3672
+ .pager-step,
3673
+ .pager-page {
3674
+ display: inline-flex;
3675
+ align-items: center;
3676
+ justify-content: center;
3677
+ /* Square, and 32px rather than the text's height: these are hit repeatedly
3678
+ and in sequence, and a target the size of its glyph is a target that gets
3679
+ missed on the fourth press. */
3680
+ min-width: var(--space-8);
3681
+ height: var(--space-8);
3682
+ padding: 0 var(--space-2);
3683
+ border: var(--border-width) solid transparent;
3684
+ border-radius: var(--radius-sm);
3685
+ background: transparent;
3686
+ color: var(--muted);
3687
+ font: inherit;
3688
+ font-variant-numeric: tabular-nums;
3689
+ cursor: pointer;
3690
+ transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
3691
+ }
3692
+
3693
+ .pager-step:hover:not(:disabled),
3694
+ .pager-page:hover:not(.on) { background: var(--panel-2); color: var(--text); }
3695
+
3696
+ /* `--focus-ring` is a box-shadow value (`0 0 0 2px var(--accent)`), not an
3697
+ outline shorthand — `outline: var(--focus-ring)` is invalid and drops
3698
+ silently, which is how these shipped with no visible focus at all. Every
3699
+ other stylesheet in the package uses `box-shadow`. */
3700
+ .pager-step:focus-visible,
3701
+ .pager-page:focus-visible { outline: none; box-shadow: var(--focus-ring); }
3702
+
3703
+ /* Disabled rather than hidden. An arrow that disappears at the first page
3704
+ moves every other control one step left, so the button under your cursor
3705
+ changes identity between clicks. */
3706
+ .pager-step:disabled { opacity: 0.35; cursor: default; }
3707
+
3708
+ .pager-pages {
3709
+ display: flex;
3710
+ align-items: center;
3711
+ gap: var(--space-1);
3712
+ margin: 0;
3713
+ padding: 0;
3714
+ list-style: none;
3715
+ }
3716
+
3717
+ /* The current page. Border as well as tint, because tint alone is a colour
3718
+ doing the work on its own -- and `aria-current` says it to a reader that
3719
+ sees neither. */
3720
+ .pager-page.on {
3721
+ color: var(--text);
3722
+ background: var(--panel-2);
3723
+ border-color: var(--border-strong);
3724
+ font-weight: var(--weight-strong);
3725
+ }
3726
+
3727
+ /* The gap is a field, drawn as a ghost.
3728
+
3729
+ At rest it is the elision it replaces: no border, no background, the `…` as
3730
+ its placeholder, sized and aligned like the page buttons beside it. Touch it
3731
+ and it becomes a control. That order matters -- a visible "go to page" box
3732
+ in a pager is a control almost nobody uses taking permanent space beside the
3733
+ ones everybody does, and a control that looks like punctuation is one nobody
3734
+ presses. This is punctuation until you press it.
3735
+
3736
+ The width comes from how many digits the largest page has, set by the
3737
+ component, so the row does not change size when the field is typed into --
3738
+ the same reason `pageWindow` holds seven slots at the ends. */
3739
+ .pager-jump {
3740
+ width: max(var(--space-8), calc(var(--digits, 2) * 0.62em + var(--space-5)));
3741
+ height: var(--space-8);
3742
+ padding: 0 var(--space-1);
3743
+ border: var(--border-width) solid transparent;
3744
+ border-radius: var(--radius-sm);
3745
+ background: transparent;
3746
+ color: var(--muted);
3747
+ font: inherit;
3748
+ font-variant-numeric: tabular-nums;
3749
+ text-align: center;
3750
+ cursor: pointer;
3751
+ transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
3752
+ color var(--dur-fast) var(--ease);
3753
+ }
3754
+
3755
+ /* Full strength, so the `…` is as legible as the numbers it sits between. */
3756
+ .pager-jump::placeholder { color: var(--muted); opacity: 1; }
3757
+
3758
+ .pager-jump:hover { background: var(--panel-2); color: var(--text); }
3759
+
3760
+ /* Typed into: it stops being an elision and becomes the control it always was.
3761
+ The placeholder goes because the field now has a value to show. */
3762
+ .pager-jump:focus {
3763
+ outline: none;
3764
+ box-shadow: var(--focus-ring);
3765
+ background: var(--control);
3766
+ border-color: var(--border-strong);
3767
+ color: var(--text);
3768
+ cursor: text;
3769
+ }
3770
+
3771
+ /* The spinners are two 10px targets stacked inside a 32px box, and pressing
3772
+ one by accident moves the page. The arrow keys still step the value. */
3773
+ .pager-jump { appearance: textfield; -moz-appearance: textfield; }
3774
+ .pager-jump::-webkit-outer-spin-button,
3775
+ .pager-jump::-webkit-inner-spin-button { appearance: none; margin: 0; }
3776
+
3777
+ @media (prefers-reduced-motion: reduce) {
3778
+ .pager-step,
3779
+ .pager-page,
3780
+ .pager-jump { transition: none; }
3781
+ }
3782
+
3783
+ /* ---- ./identity.css ---- */
3784
+ /* A person, said in one line: initials, name, sometimes the address. */
3785
+ .ident {
3786
+ display: inline-flex;
3787
+ align-items: center;
3788
+ gap: var(--space-2);
3789
+ min-width: 0;
3790
+ font-size: var(--text-sm);
3791
+ color: var(--text);
3792
+ }
3793
+
3794
+ /* The disc.
3795
+
3796
+ `--ident-hue` comes from the address and is the only thing the component
3797
+ sets inline; everything that decides how saturated and how light it is
3798
+ lives here, so the discs stay in the theme rather than staying the same
3799
+ colour on every theme. Mixed towards `--panel` for the same measured reason
3800
+ `Pill` mixes: a full-strength hue behind text drops the contrast on the
3801
+ light themes, and these carry two letters that have to be legible. */
3802
+ .ident-disc {
3803
+ flex: none;
3804
+ display: inline-flex;
3805
+ align-items: center;
3806
+ justify-content: center;
3807
+ width: var(--space-8);
3808
+ height: var(--space-8);
3809
+ border-radius: var(--radius-pill);
3810
+ background: color-mix(in srgb, hsl(var(--ident-hue) 60% 50%) 22%, var(--panel));
3811
+ border: var(--border-width) solid color-mix(in srgb, hsl(var(--ident-hue) 60% 50%) 45%, transparent);
3812
+ color: var(--text);
3813
+ font-size: var(--text-2xs);
3814
+ font-weight: var(--weight-strong);
3815
+ letter-spacing: normal;
3816
+ text-transform: uppercase;
3817
+ user-select: none;
3818
+ }
3819
+
3820
+ .ident.size-sm .ident-disc {
3821
+ width: var(--space-6);
3822
+ height: var(--space-6);
3823
+ font-size: var(--text-2xs);
3824
+ }
3825
+
3826
+ .ident-text { display: grid; min-width: 0; }
3827
+
3828
+ .ident-name {
3829
+ overflow: hidden;
3830
+ text-overflow: ellipsis;
3831
+ white-space: nowrap;
3832
+ font-weight: var(--weight);
3833
+ }
3834
+
3835
+ .ident-address {
3836
+ overflow: hidden;
3837
+ text-overflow: ellipsis;
3838
+ white-space: nowrap;
3839
+ font-size: var(--text-xs);
3840
+ color: var(--muted);
3841
+ }
3842
+
3843
+ .ident-aside { flex: none; color: var(--muted); font-size: var(--text-xs); }
3844
+
3845
+ /* A chip in a composer's To field: boxed, tighter, removable. */
3846
+ .ident.kind-chip {
3847
+ gap: var(--space-1);
3848
+ padding: var(--space-1);
3849
+ border: var(--border-width) solid var(--border);
3850
+ border-radius: var(--radius-pill);
3851
+ background: var(--panel-2);
3852
+ font-size: var(--text-xs);
3853
+ max-width: 100%;
3854
+ }
3855
+
3856
+ .ident.kind-chip .ident-disc {
3857
+ width: var(--space-5);
3858
+ height: var(--space-5);
3859
+ font-size: var(--text-2xs);
3860
+ }
3861
+
3862
+ .ident-remove {
3863
+ flex: none;
3864
+ display: inline-flex;
3865
+ align-items: center;
3866
+ justify-content: center;
3867
+ /* `--space-5` of button around a `--space-3` glyph. The cross is the
3868
+ smallest thing on a composer and it sits next to text somebody is typing,
3869
+ so it needs the target even though it does not look like it does. */
3870
+ width: var(--space-5);
3871
+ height: var(--space-5);
3872
+ padding: 0;
3873
+ border: none;
3874
+ border-radius: var(--radius-pill);
3875
+ background: transparent;
3876
+ color: var(--muted);
3877
+ cursor: pointer;
3878
+ }
3879
+
3880
+ .ident-remove:hover { background: var(--panel); color: var(--text); }
3881
+ .ident-remove:focus-visible { outline: none; box-shadow: var(--focus-ring); }
3882
+