A phone, and how much of this could not be used on one
The sidebar was a 280px panel laid over the page below the phone breakpoint, opened from first paint, with the only control that closed it underneath it -- and that control existed on /chat and on none of the seven other pages carrying a sidebar, Settings included. It starts closed at that width now, slides, dims the page behind it, and closes by tapping beside it, by Escape, or by its own button, which is inside the drawer where it can be reached. Everything a finger has to hit was 36px, or 28 for renaming a chat, every action on a message and every panel's close button. Raising --control-h under a coarse pointer is the only fix that reaches all forty of them, which is what that token is for. The row and message actions were also hover-only, so on a phone they did not exist at all. Installing: the splash and the browser chrome follow the instance's theme rather than always being Moria's near-black; there are screenshots, so the install offer is a dialog rather than a one-line bar; a new release no longer takes over a page somebody is reading; the notification badge is a silhouette rather than a grey square; and a browser rotating its own subscription no longer ends notifications for good. Every request now says it is happening -- nothing did before, so anything slower than a few milliseconds looked like a click that had not registered. A chat can be archived. The column has been filtered on in four places since folders arrived and written by nothing, which is what made it look built. chat.css may contain media queries. The ban protected the composer toolbar from being "fixed" with a breakpoint; that guarantee is asserted directly now, and the old test would have passed a version of the file that wrapped the toolbar without one. scripts/shoot.py is the instrument all of this was found with: it renders a page through TestClient into a real headless browser at a real size and refuses to run if an asset URL was left pointing at testserver. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,11 @@ body:has(> .shell) {
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* The browser's own grey flash on tap is a rectangle around whatever box the
|
||||
control happens to be, drawn in a colour no theme here chose. Removed in
|
||||
favour of the `:active` states below, which are the application's own --
|
||||
removed *with* a replacement, never on its own. */
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--leading-normal);
|
||||
@@ -93,7 +98,7 @@ button, input, textarea, select {
|
||||
|
||||
/* A single, consistent focus ring. Never remove it without a replacement. */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline: var(--outline-w) solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
@@ -136,7 +141,7 @@ button, input, textarea, select {
|
||||
gap: var(--sp-2);
|
||||
height: var(--control-h);
|
||||
padding: 0 var(--control-px);
|
||||
border: 1px solid var(--border);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-raised);
|
||||
color: var(--ink);
|
||||
@@ -249,7 +254,7 @@ button, input, textarea, select {
|
||||
width: 100%;
|
||||
height: var(--control-h);
|
||||
padding: 0 var(--control-px);
|
||||
border: 1px solid var(--border);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-sunken);
|
||||
color: var(--ink);
|
||||
@@ -259,7 +264,7 @@ button, input, textarea, select {
|
||||
.textarea {
|
||||
width: 100%;
|
||||
padding: var(--sp-2) var(--control-px);
|
||||
border: 1px solid var(--border);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-sunken);
|
||||
color: var(--ink);
|
||||
@@ -275,7 +280,7 @@ button, input, textarea, select {
|
||||
.select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||
box-shadow: var(--ring);
|
||||
}
|
||||
.input::placeholder, .textarea::placeholder { color: var(--ink-faint); }
|
||||
|
||||
@@ -306,7 +311,7 @@ button, input, textarea, select {
|
||||
margin: 0 var(--sp-3) 0 0;
|
||||
padding: 0 var(--control-px);
|
||||
border: 0;
|
||||
border-right: 1px solid var(--border);
|
||||
border-right: var(--border-w) solid var(--border);
|
||||
background: var(--surface-hover);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
@@ -365,8 +370,8 @@ button, input, textarea, select {
|
||||
display: flex;
|
||||
gap: var(--sp-3);
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
border: 1px solid var(--border);
|
||||
border-left-width: 3px;
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-left-width: var(--border-w-accent);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--text-sm);
|
||||
margin-bottom: var(--sp-4);
|
||||
@@ -436,7 +441,7 @@ button, input, textarea, select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-sunken);
|
||||
border-right: 1px solid var(--border);
|
||||
border-right: var(--border-w) solid var(--border);
|
||||
min-height: 0;
|
||||
}
|
||||
.sidebar[hidden] { display: none; }
|
||||
@@ -511,7 +516,7 @@ button, input, textarea, select {
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: var(--bg-sunken);
|
||||
border-left: 1px solid var(--border);
|
||||
border-left: var(--border-w) solid var(--border);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -524,10 +529,14 @@ button, input, textarea, select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
height: var(--header-height);
|
||||
/* The same sum as `.topbar`, for the same reason and so the three still line
|
||||
up across the shell -- which is the whole point of this element. */
|
||||
height: calc(var(--header-height) + var(--safe-top));
|
||||
padding-top: var(--safe-top);
|
||||
flex: none;
|
||||
padding: 0 var(--sp-3);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-right: var(--sp-3);
|
||||
padding-left: var(--sp-3);
|
||||
border-bottom: var(--border-w) solid var(--border);
|
||||
}
|
||||
.panel-head__title {
|
||||
display: flex;
|
||||
@@ -578,7 +587,7 @@ button, input, textarea, select {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--code-border);
|
||||
border: var(--border-w) solid var(--code-border);
|
||||
border-radius: var(--radius);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
@@ -610,7 +619,7 @@ button, input, textarea, select {
|
||||
/* So the resize handle can sit on the edge. */
|
||||
position: relative;
|
||||
background: var(--bg-sunken);
|
||||
border-left: 1px solid var(--border);
|
||||
border-left: var(--border-w) solid var(--border);
|
||||
}
|
||||
|
||||
/* The drag handle on a panel's left edge. Wider than it looks -- a one-pixel
|
||||
@@ -680,7 +689,7 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
border-top: 1px solid var(--border);
|
||||
border-top: var(--border-w) solid var(--border);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--ink-faint);
|
||||
line-height: var(--leading-normal);
|
||||
@@ -717,7 +726,7 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
background: var(--bg-sunken);
|
||||
border-left: 1px solid var(--border);
|
||||
border-left: var(--border-w) solid var(--border);
|
||||
}
|
||||
.canvas__inner {
|
||||
display: flex;
|
||||
@@ -760,7 +769,7 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
background: var(--bg-sunken);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-bottom: var(--border-w) solid var(--border);
|
||||
}
|
||||
.canvas__tab {
|
||||
display: inline-flex;
|
||||
@@ -769,7 +778,7 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
max-width: 14rem;
|
||||
/* Square at the bottom: a tab is attached to what it opens. */
|
||||
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
||||
border: 1px solid transparent;
|
||||
border: var(--border-w) solid transparent;
|
||||
border-bottom: 0;
|
||||
/* The strip's own bottom border is 1px; this covers it for the active tab
|
||||
without moving anything, so the row does not shift by a pixel on switch. */
|
||||
@@ -831,7 +840,7 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
gap: var(--sp-2);
|
||||
flex: none;
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-bottom: var(--border-w) solid var(--border);
|
||||
}
|
||||
|
||||
.canvas__body {
|
||||
@@ -866,7 +875,7 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
width: 100%;
|
||||
min-height: 24rem;
|
||||
padding: var(--sp-3);
|
||||
border: 1px solid var(--border);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--code-bg);
|
||||
color: var(--ink);
|
||||
@@ -896,10 +905,15 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
height: var(--header-height);
|
||||
/* The bar is `--header-height` of *content* and whatever the device puts
|
||||
above it. Installed on a phone the page runs under the status bar, so
|
||||
without this the title and the sidebar toggle sit beneath the clock. */
|
||||
height: calc(var(--header-height) + var(--safe-top));
|
||||
padding-top: var(--safe-top);
|
||||
flex: none;
|
||||
padding: 0 var(--sp-4);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-right: max(var(--sp-4), var(--safe-right));
|
||||
padding-left: max(var(--sp-4), var(--safe-left));
|
||||
border-bottom: var(--border-w) solid var(--border);
|
||||
background: var(--bg);
|
||||
}
|
||||
.topbar__title {
|
||||
@@ -959,7 +973,7 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
gap: var(--sp-2);
|
||||
height: var(--control-h);
|
||||
padding: 0 var(--sp-1) 0 var(--sp-2);
|
||||
border: 1px solid var(--border);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
cursor: pointer;
|
||||
@@ -968,14 +982,14 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
.model-select:hover { border-color: var(--border-strong); }
|
||||
.model-select:focus-within {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||
box-shadow: var(--ring);
|
||||
}
|
||||
.model-select__avatar { width: 1.4rem; height: 1.4rem; border-radius: var(--radius-sm); }
|
||||
|
||||
/* Collapsible settings panel, shared by chat settings and anything like it. */
|
||||
.panel {
|
||||
flex: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-bottom: var(--border-w) solid var(--border);
|
||||
background: var(--bg-sunken);
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
@@ -1052,6 +1066,40 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
}
|
||||
.nav-item:hover .nav-item__actions,
|
||||
.nav-item:focus-within .nav-item__actions { opacity: 1; }
|
||||
/*
|
||||
There is no hover on a phone, and the row's own tap target is the link -- so
|
||||
tapping a chat navigated to it and these never appeared at all. Renaming or
|
||||
deleting a chat from a phone was not difficult, it was impossible.
|
||||
|
||||
`hover: none` rather than a width: a touchscreen laptop at 1440px has the same
|
||||
problem, and a narrow desktop window does not.
|
||||
*/
|
||||
@media (hover: none) {
|
||||
.nav-item__actions { opacity: 1; }
|
||||
}
|
||||
|
||||
/* The archived group. A `<summary>` is a real control, so it takes the row
|
||||
treatment rather than the label's -- it is something you press. */
|
||||
.nav-group--archived > summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius);
|
||||
list-style: none;
|
||||
}
|
||||
.nav-group--archived > summary::-webkit-details-marker { display: none; }
|
||||
.nav-group--archived > summary:hover { background: var(--surface-hover); color: var(--ink-muted); }
|
||||
.nav-group__count {
|
||||
margin-left: auto;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
/* Archived rows read as put away rather than as unavailable: dimmed until
|
||||
they are looked at, never greyed out -- every action on them still works. */
|
||||
.nav-group--archived .nav-item { opacity: 0.72; }
|
||||
.nav-group--archived .nav-item:hover,
|
||||
.nav-group--archived .nav-item:focus-within { opacity: 1; }
|
||||
|
||||
.nav-empty {
|
||||
padding: var(--sp-2);
|
||||
@@ -1075,7 +1123,7 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
width: 100%;
|
||||
max-width: 25rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--sp-8);
|
||||
box-shadow: var(--shadow-lg);
|
||||
@@ -1096,7 +1144,7 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
.auth__footer {
|
||||
margin-top: var(--sp-5);
|
||||
padding-top: var(--sp-4);
|
||||
border-top: 1px solid var(--border);
|
||||
border-top: var(--border-w) solid var(--border);
|
||||
text-align: center;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--ink-muted);
|
||||
@@ -1133,16 +1181,102 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* --- Small screens -------------------------------------------------------- */
|
||||
/*
|
||||
--- The sidebar, and the two different things "closed" means ---------------
|
||||
|
||||
Above the breakpoint the sidebar is a column and closed means "give the space
|
||||
to the conversation". Below it the sidebar is an overlay and closed is the
|
||||
*resting* state -- 280px of opaque drawer over a 390px screen is not a
|
||||
navigation aid, it is the page gone.
|
||||
|
||||
The `hidden` attribute cannot express that, because it is one value for both
|
||||
widths: it was absent, so the drawer was open on every phone, on every page,
|
||||
from the first paint -- with its own toggle underneath it. So the state is an
|
||||
attribute on <html> with three values, and the third is the one that matters:
|
||||
|
||||
data-sidebar="open" shown at every width
|
||||
data-sidebar="closed" hidden at every width
|
||||
(absent) follow the width -- open wide, closed narrow
|
||||
|
||||
Absent is what the server renders, because the server does not know how wide
|
||||
the window is. See `setSidebar` in app.js, which is also why this panel does
|
||||
not go through `setPanel` like the three on the other side.
|
||||
*/
|
||||
:root[data-sidebar="closed"] .sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Above the breakpoint the drawer's own furniture has no job. Declared BEFORE
|
||||
the media query that turns it back on: both rules are one class deep, so
|
||||
source order is what decides, and this one written afterwards made the close
|
||||
button `display: none` at every width -- including inside the open drawer,
|
||||
which is the only place it exists for. */
|
||||
.sidebar__close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
inset: 0 auto 0 0;
|
||||
/* Never the full width, and never wider than the screen: a drawer with no
|
||||
page showing beside it gives nothing to tap to dismiss it, and reads as
|
||||
a navigation *page* you have arrived at rather than a layer over the one
|
||||
you were on. */
|
||||
width: min(var(--sidebar-width), 84vw);
|
||||
z-index: var(--z-panel);
|
||||
box-shadow: var(--shadow-lg);
|
||||
/* Off-screen rather than `display: none`, so opening it is a movement the
|
||||
eye can follow from the button that caused it. `visibility` is what
|
||||
takes it out of the tab order while it is away -- `transform` alone
|
||||
leaves every control in it focusable, just somewhere nobody can see. */
|
||||
transform: translateX(-100%);
|
||||
visibility: hidden;
|
||||
transition: transform var(--dur-3) var(--ease-out),
|
||||
visibility var(--dur-3) var(--ease-out);
|
||||
}
|
||||
/* Hiding it is the `hidden` attribute, forced to win at the top of this
|
||||
file. There used to be a `[data-collapsed="true"]` rule here that nothing
|
||||
ever set. */
|
||||
:root:not([data-sidebar="closed"]) .sidebar {
|
||||
/* `display` must not be the thing that hides it here, or there is nothing
|
||||
to animate. The attribute rule above is reversed for this width. */
|
||||
display: flex;
|
||||
}
|
||||
:root[data-sidebar="open"] .sidebar {
|
||||
transform: none;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Its own edges, once it is the thing against the side of the screen. */
|
||||
.sidebar__header,
|
||||
.sidebar__actions,
|
||||
.sidebar__scroll {
|
||||
padding-left: max(var(--sp-3), var(--safe-left));
|
||||
}
|
||||
.sidebar__footer {
|
||||
padding-bottom: max(var(--sp-2), var(--safe-bottom));
|
||||
}
|
||||
|
||||
.sidebar__close {
|
||||
display: inline-flex;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Dismissible by tapping beside it. Without this the only way out is a
|
||||
button, and a drawer you can only leave deliberately is one people close
|
||||
by reloading. */
|
||||
:root[data-sidebar="open"] .sidebar-scrim {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-scrim {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: calc(var(--z-panel) - 1);
|
||||
background: var(--scrim);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--dur-3) var(--ease-out);
|
||||
}
|
||||
|
||||
/* --- Toasts ----------------------------------------------------------------
|
||||
@@ -1166,8 +1300,8 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
align-items: flex-start;
|
||||
gap: var(--sp-3);
|
||||
padding: var(--sp-3) var(--sp-3) var(--sp-3) var(--sp-4);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--accent);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-left: var(--border-w-accent) solid var(--accent);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
@@ -1184,21 +1318,31 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
.toast__text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
||||
.toast__close {
|
||||
flex: none;
|
||||
/* It had no height at all -- `font-size` and 0.15rem of side padding, which
|
||||
is about 18x7px. The smallest target in the application, on the one control
|
||||
somebody reaches for when they are already mildly annoyed. */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--control-h-sm);
|
||||
height: var(--control-h-sm);
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
color: var(--ink-faint);
|
||||
cursor: pointer;
|
||||
font-size: var(--text-lg);
|
||||
line-height: 1;
|
||||
padding: 0 0.15rem;
|
||||
padding: 0;
|
||||
}
|
||||
.toast__close:hover { color: var(--ink); }
|
||||
.toast__action { flex: none; align-self: center; }
|
||||
|
||||
/* --- Dialogs ----------------------------------------------------------------
|
||||
<dialog> gives focus trapping, Escape and page inertness for free.
|
||||
*/
|
||||
.dialog {
|
||||
border: 1px solid var(--border);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius-xl);
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
@@ -1251,7 +1395,7 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
height: var(--control-h);
|
||||
max-width: 16rem;
|
||||
padding: 0 var(--sp-2);
|
||||
border: 1px solid var(--border);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
@@ -1262,7 +1406,7 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
.picker__button:hover { border-color: var(--border-strong); }
|
||||
.picker__button[aria-expanded="true"] {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||
box-shadow: var(--ring);
|
||||
}
|
||||
.picker__avatar { width: 1.4rem; height: 1.4rem; border-radius: var(--radius-sm); flex: none; }
|
||||
.picker__label {
|
||||
@@ -1280,7 +1424,7 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
right: 0;
|
||||
z-index: var(--z-dropdown);
|
||||
width: min(24rem, calc(100vw - var(--sp-8)));
|
||||
border: 1px solid var(--border);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
@@ -1336,7 +1480,7 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-bottom: var(--border-w) solid var(--border);
|
||||
background: var(--bg-sunken);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--ink-muted);
|
||||
@@ -1351,12 +1495,12 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
max-height: min(24rem, 50vh);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
border: 1px solid var(--border);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.dialog__results .picker__list { max-height: none; }
|
||||
|
||||
.picker__search { padding: var(--sp-2); border-bottom: 1px solid var(--border); }
|
||||
.picker__search { padding: var(--sp-2); border-bottom: var(--border-w) solid var(--border); }
|
||||
/*
|
||||
Small controls, declared here because this is the file every page loads.
|
||||
|
||||
@@ -1433,3 +1577,119 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
font-size: var(--text-sm);
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
|
||||
/* --- Saying that something is happening ------------------------------------
|
||||
A three-pixel bar across the top of the window, above everything including
|
||||
the panels, because it describes the whole page rather than any part of it.
|
||||
|
||||
It never claims to know how far along it is. A request whose length is
|
||||
unknown and a bar that fills at a constant rate is a lie that gets found out
|
||||
on every slow request -- so this one travels, and stops when the answer
|
||||
lands. `transform` only, so it costs no layout on a page that may be
|
||||
streaming a reply at twelve frames a second underneath it.
|
||||
*/
|
||||
.progress {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--border-w-accent);
|
||||
z-index: var(--z-toast);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity var(--dur-2) var(--ease-out);
|
||||
}
|
||||
.progress.is-busy { opacity: 1; }
|
||||
.progress span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 40%;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(90deg, transparent, var(--leaf), transparent);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.progress.is-busy span { animation: progress-sweep var(--dur-slow) var(--ease-in-out) infinite; }
|
||||
@keyframes progress-sweep {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(350%); }
|
||||
}
|
||||
|
||||
/* --- Content that has not arrived yet ---------------------------------------
|
||||
A shape where the thing will be, rather than a blank. Used with `aria-hidden`
|
||||
on whatever is waiting, so a screen reader is not read a paragraph of
|
||||
nothing.
|
||||
|
||||
The shimmer is a moving gradient rather than an opacity pulse, because a list
|
||||
of eight pulsing blocks all at the same phase reads as a fault. */
|
||||
.skeleton {
|
||||
border-radius: var(--radius);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--surface) 0%,
|
||||
var(--surface-hover) 50%,
|
||||
var(--surface) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-sweep var(--dur-slow) var(--ease-in-out) infinite;
|
||||
}
|
||||
.skeleton--row { height: var(--control-h); margin-bottom: var(--sp-1); }
|
||||
.skeleton--line { height: var(--text-base); margin-bottom: var(--sp-2); }
|
||||
.skeleton--short { width: 60%; }
|
||||
@keyframes skeleton-sweep {
|
||||
0% { background-position: 100% 0; }
|
||||
100% { background-position: -100% 0; }
|
||||
}
|
||||
|
||||
/* --- Press ----------------------------------------------------------------
|
||||
The tap highlight was removed in the reset, so something has to take its
|
||||
place: a control that moves under the finger is the cheapest possible
|
||||
confirmation that the tap landed, and the only one that works before the
|
||||
request it started has answered. Kept small -- this is feedback, not an
|
||||
animation somebody has to sit through. */
|
||||
.btn:active:not(:disabled),
|
||||
.nav-item:active,
|
||||
.tabs__tab:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
.btn { transition: background var(--transition-fast), border-color var(--transition-fast),
|
||||
color var(--transition-fast), transform var(--dur-1) var(--ease-out); }
|
||||
|
||||
/* --- Arrival ---------------------------------------------------------------
|
||||
`@starting-style` plus `allow-discrete` is what lets a `display: none`
|
||||
element animate in with no JavaScript at all and no class to add and remove.
|
||||
Where it is unsupported the element simply appears, which is what it did
|
||||
before. */
|
||||
.dialog {
|
||||
opacity: 0;
|
||||
transform: scale(0.97);
|
||||
transition: opacity var(--dur-2) var(--ease-out),
|
||||
transform var(--dur-2) var(--ease-spring),
|
||||
overlay var(--dur-2) allow-discrete,
|
||||
display var(--dur-2) allow-discrete;
|
||||
}
|
||||
.dialog[open] { opacity: 1; transform: none; }
|
||||
@starting-style {
|
||||
.dialog[open] { opacity: 0; transform: scale(0.97); }
|
||||
}
|
||||
.dialog::backdrop {
|
||||
opacity: 0;
|
||||
transition: opacity var(--dur-2) var(--ease-out),
|
||||
overlay var(--dur-2) allow-discrete,
|
||||
display var(--dur-2) allow-discrete;
|
||||
}
|
||||
.dialog[open]::backdrop { opacity: 1; }
|
||||
@starting-style {
|
||||
.dialog[open]::backdrop { opacity: 0; }
|
||||
}
|
||||
|
||||
/* A card lifts a little under the pointer -- only where there is a pointer, and
|
||||
only where the card is something you can act on. */
|
||||
@media (hover: hover) {
|
||||
a.card:hover,
|
||||
.card--action:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
}
|
||||
a.card, .card--action { transition: transform var(--dur-2) var(--ease-out),
|
||||
box-shadow var(--dur-2) var(--ease-out); }
|
||||
|
||||
Reference in New Issue
Block a user