An admin area a phone could reach and not navigate
Administration has a nav of its own rather than the chat sidebar, and 1.1.0 gave every `.sidebar` the drawer behaviour -- starts closed, slides in -- without giving that one any of the drawer's furniture. No id for the toggle to resolve, no toggle, no close, no scrim: it sat at left:-280 with nothing in the application able to open it. The close button and the scrim are partials now, used by both, and the test that guards it *finds* sidebars by scanning the templates rather than working from a list, which is exactly why this one was missed. The chat, measured at 390px, spent forty pixels of side padding and a forty-four pixel avatar column before drawing a word -- close to a quarter of the screen on margin, so anything that could not wrap had to be reached sideways. Padding halved and the avatar moved above the turn; a code block gained about sixty pixels. Worse in the same row: `.topbar__actions` asked for 317px of a 390px bar, because the control that used to give in that row is display:none below a tablet width, so the group went rigid and the title -- flex: 1 -- was squeezed to exactly zero. And `.btn--icon` sets a width with no `flex: none`, so the row shrank the button instead of the text: the sidebar toggle measured eighteen pixels across. The picker gives now, and shows its avatar rather than its name on a phone. Also the instrument, which lied twice more: it could not see horizontal overflow at all, because `.shell` is overflow:hidden and its "is this contained" test therefore answered yes for everything on the page; and run from a copy it resolved `STATIC` to a directory that did not exist, rewrote every asset URL to a dead file:// path and reported the whole application overflowing by thirty thousand pixels. It resolves from the imported package now and asserts that what it rewrote to is really there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -185,6 +185,12 @@ button, input, textarea, select {
|
||||
/* Square, and the same height as everything beside it. */
|
||||
.btn--icon {
|
||||
width: var(--control-h);
|
||||
/* Square, and it stays square. Without this a flex row that runs out of room
|
||||
shrinks it instead of its neighbours -- the sidebar toggle measured 18px
|
||||
across on a 390px chat, less than half the target it is supposed to be,
|
||||
while the row beside it kept every pixel it had asked for. A control's
|
||||
size is not the give in a layout; text is. */
|
||||
flex: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
@@ -354,12 +360,28 @@ button, input, textarea, select {
|
||||
}
|
||||
.checkbox input {
|
||||
accent-color: var(--accent);
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
width: var(--check-size);
|
||||
height: var(--check-size);
|
||||
flex: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Every tick box, not only the ones inside a `.checkbox` label -- the admin
|
||||
lists put bare ones in a row and those were 16px square on a phone. */
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
accent-color: var(--accent);
|
||||
width: var(--check-size);
|
||||
height: var(--check-size);
|
||||
}
|
||||
/* Except the ones that are deliberately 1px: a visually-hidden radio is the
|
||||
state behind a label, and the label is the target. */
|
||||
input.visually-hidden[type="radio"],
|
||||
input.visually-hidden[type="checkbox"] {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
/* Multi-column form layout, one definition. */
|
||||
.grid { display: grid; gap: var(--sp-4); }
|
||||
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); }
|
||||
@@ -953,7 +975,48 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.topbar__actions { display: flex; align-items: center; gap: var(--sp-2); flex: none; }
|
||||
/*
|
||||
The controls on the right of the topbar.
|
||||
|
||||
`flex: none` on the group with `min-width: 0` inside it: the group keeps the
|
||||
width its controls need, and the one child whose width is a *name* rather
|
||||
than a control -- the model picker -- is the thing allowed to give. Without
|
||||
the second half the group asked for 317px of a 390px bar and the chat's
|
||||
title, which is `flex: 1`, was squeezed to exactly zero: a heading that had
|
||||
not been shortened or truncated but had simply ceased to occupy space.
|
||||
*/
|
||||
.topbar__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
/* Allowed to give, which it was not. `--topbar__where` used to be the
|
||||
designated shrinker in this row, and it is `display: none` below 64rem --
|
||||
so on a phone the group became rigid, asked for 317px of a 390px bar, and
|
||||
the title (`flex: 1`) was squeezed to exactly zero: a heading that had not
|
||||
been truncated but had ceased to occupy space.
|
||||
|
||||
Nothing inside it shrinks except the model picker: every button here is
|
||||
`flex: none` because a control's size is not the give in a layout. */
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
/* A title identifies the page, so it gets a floor and truncates rather than
|
||||
disappearing. */
|
||||
.topbar__title { min-width: 4rem; }
|
||||
|
||||
/* The one control in this row whose width is somebody else's decision -- a
|
||||
model's label is whatever an administrator called it -- so it is the one
|
||||
that gives, and it gives by truncating its name rather than its avatar or
|
||||
its chevron. */
|
||||
.topbar__actions .picker { min-width: 0; }
|
||||
.topbar__actions .picker__button { max-width: 100%; }
|
||||
.topbar__actions .picker__label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.picker__avatar, .picker__chevron { flex: none; }
|
||||
|
||||
/*
|
||||
Which machine an agent chat runs on, and where.
|
||||
@@ -1243,6 +1306,22 @@ body.is-resizing .canvas__body { pointer-events: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
/* The bar is the densest row in the application and the one with the least
|
||||
room: a toggle, a title, a model, and up to four panel buttons. Tighter
|
||||
padding and a smaller gap buy back about 24px, which is the difference
|
||||
between a title that truncates and one there is no room for at all. */
|
||||
.topbar {
|
||||
gap: var(--sp-2);
|
||||
padding-right: max(var(--sp-2), var(--safe-right));
|
||||
padding-left: max(var(--sp-2), var(--safe-left));
|
||||
}
|
||||
|
||||
/* The model's name costs about a hundred pixels and its avatar does not,
|
||||
and the picker opens onto a list of full names the moment it is touched.
|
||||
So on a phone the avatar carries the identity and the chat's own title --
|
||||
which nothing else on the screen tells you -- gets the room back. */
|
||||
.topbar__actions .picker__label { display: none; }
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
inset: 0 auto 0 0;
|
||||
|
||||
@@ -1726,3 +1726,52 @@
|
||||
.thread__intro > * { animation: intro-rise var(--dur-3) var(--ease-out) both; }
|
||||
.thread__intro > *:nth-child(2) { animation-delay: 60ms; }
|
||||
.thread__intro > *:nth-child(3) { animation-delay: 120ms; }
|
||||
|
||||
/*
|
||||
--- A phone ----------------------------------------------------------------
|
||||
|
||||
The one width-aware block in this file, and the reason the blanket ban on
|
||||
`@media` here was lifted: everything below is a *size*, and there is no
|
||||
intrinsic-sizing trick that makes 24px of thread padding the right amount on
|
||||
a 390px screen. The ban existed to stop the composer toolbar being "fixed"
|
||||
with a breakpoint instead of by saying which child gives, and that guarantee
|
||||
is asserted directly now (`tests/test_chat.py`) -- so this block may not touch
|
||||
`.composer__toolbar` or `.composer__actions`, and a test refuses it if it
|
||||
does.
|
||||
|
||||
What was wrong: a 390px screen spent 40px of its width on thread padding and
|
||||
another 44 on the avatar gutter before a single word was drawn, which is
|
||||
nearly a quarter of the screen given over to margin -- so anything that could
|
||||
not wrap had to be scrolled to sideways.
|
||||
*/
|
||||
@media (max-width: 48rem) {
|
||||
/* Half the horizontal padding. The vertical stays: it is what separates one
|
||||
turn from the next, and turns are no closer together on a phone. */
|
||||
.thread {
|
||||
padding-left: var(--sp-3);
|
||||
padding-right: var(--sp-3);
|
||||
}
|
||||
|
||||
/* The avatar goes to the top of the turn rather than beside it, so the body
|
||||
gets the whole width. The gutter is what identifies the speaker and it
|
||||
still does; it simply stops costing 44px of every line. */
|
||||
.msg {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
.msg__gutter {
|
||||
width: var(--control-h-sm);
|
||||
height: var(--control-h-sm);
|
||||
}
|
||||
.msg__meta { gap: var(--sp-2); }
|
||||
|
||||
/* A bubble against the edge of the screen wants less inside it. */
|
||||
.msg--user .msg__body--plain { padding: var(--sp-2) var(--sp-3); }
|
||||
|
||||
/* The composer is the other thing pressed against both edges. */
|
||||
.composer { padding-left: var(--sp-2); padding-right: var(--sp-2); }
|
||||
|
||||
/* A hint that runs to four lines on a phone is a hint nobody reads, and it
|
||||
sits directly under the thing a thumb is reaching for. */
|
||||
.composer__hint { font-size: var(--text-xs); }
|
||||
}
|
||||
|
||||
@@ -145,6 +145,9 @@
|
||||
Raising the token is the only version that reaches all of them, and it is
|
||||
what `--control-h` exists for. */
|
||||
--tap-min: 2.75rem;
|
||||
/* A tick box, which does not take its size from `--control-h`: the browser
|
||||
draws it and only `width`/`height` move it. */
|
||||
--check-size: 1rem;
|
||||
|
||||
/* --- The window's own edges ---------------------------------------------
|
||||
Installed on a phone, the page runs under the notch and the home
|
||||
@@ -421,9 +424,16 @@
|
||||
@media (pointer: coarse), (max-width: 48rem) {
|
||||
:root {
|
||||
--control-h: var(--tap-min);
|
||||
--control-h-sm: 2.25rem;
|
||||
/* 40px, not the 36 a comfortable pointer gets. A `.btn--sm` is a secondary
|
||||
action, not an unimportant one -- Edit, Enable and Use default are all
|
||||
`.btn--sm`, and on a phone they are the whole interaction. */
|
||||
--control-h-sm: 2.5rem;
|
||||
--control-px: var(--sp-4);
|
||||
--control-px-sm: var(--sp-3);
|
||||
/* A native checkbox is 13-16px whatever the surrounding type is, and no
|
||||
amount of padding on its label changes the box itself. It is the
|
||||
smallest target in the application on a phone by some margin. */
|
||||
--check-size: 1.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 33 KiB |
@@ -14,10 +14,20 @@
|
||||
|
||||
{% block body %}
|
||||
<div class="shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar__header">
|
||||
{{ brandlink(uid="admin") }}
|
||||
</div>
|
||||
{#
|
||||
`id="sidebar"` and the drawer's furniture, because below the phone
|
||||
breakpoint `.sidebar` is a fixed overlay that starts closed -- and this one
|
||||
had neither an id for `data-toggle="#sidebar"` to find nor any control to
|
||||
open it. The administration area was reachable on a phone and then
|
||||
unnavigable once you arrived.
|
||||
#}
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<header class="sidebar__header">
|
||||
<div class="sidebar__brand-slot">
|
||||
{{ brandlink(uid="admin") }}
|
||||
</div>
|
||||
{% include "partials/_sidebar_close.html" %}
|
||||
</header>
|
||||
|
||||
<nav class="sidebar__scroll" aria-label="Administration">
|
||||
<div class="nav-group">
|
||||
@@ -106,8 +116,11 @@
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{% include "partials/_sidebar_scrim.html" %}
|
||||
|
||||
<main class="main">
|
||||
<header class="topbar">
|
||||
{% include "partials/_sidebar_toggle.html" %}
|
||||
<h1 class="topbar__title">{% block heading %}Administration{% endblock %}</h1>
|
||||
<button class="btn btn--icon" type="button" data-theme-toggle aria-label="Switch theme">
|
||||
<span class="theme-icon theme-icon--dark">{{ icon("moon") }}</span>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{% from "_macros.html" import icon %}
|
||||
{#
|
||||
The way out of the drawer, and the reason it is *inside* it.
|
||||
|
||||
Below the phone breakpoint the sidebar is a fixed overlay and the toggle that
|
||||
opens it is in the topbar underneath -- so once open, the control for closing
|
||||
it is behind it. Its own partial because there are two sidebars in this
|
||||
application, the chat one and the admin one, and the second was given the
|
||||
drawer behaviour without the drawer's furniture: at a phone width it was
|
||||
hidden off-screen with no toggle and no close anywhere, which is an admin area
|
||||
that simply could not be navigated on a phone.
|
||||
|
||||
Hidden above that breakpoint, where the sidebar is an ordinary column.
|
||||
#}
|
||||
<div class="sidebar__actions-rail">
|
||||
<button class="btn btn--icon sidebar__close" type="button"
|
||||
aria-label="Close sidebar" data-toggle="#sidebar">
|
||||
{{ icon("x") }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
{#
|
||||
The scrim behind an open drawer. It carries the same `data-toggle` as every
|
||||
other control that closes it, so tapping beside the drawer goes through one
|
||||
code path rather than a second written for touch.
|
||||
|
||||
Rendered always and shown by CSS: it exists only below the breakpoint and only
|
||||
while the drawer is open, which is a question about width and state that the
|
||||
server cannot answer and the stylesheet can.
|
||||
#}
|
||||
<div class="sidebar-scrim" data-toggle="#sidebar" aria-hidden="true"></div>
|
||||
@@ -27,27 +27,7 @@
|
||||
{{ brandlink(uid="side") }}
|
||||
</div>
|
||||
|
||||
{#
|
||||
The way out, and the reason it is *inside* the drawer.
|
||||
|
||||
Below the phone breakpoint this whole element is a fixed overlay, and the
|
||||
toggle that opens it lives in the topbar underneath -- so once it was
|
||||
open, the control for closing it was behind it. That was true on /chat,
|
||||
where at least a toggle existed; on the seven other pages that carry this
|
||||
sidebar there was no such control at all, and no way back.
|
||||
|
||||
Hidden above that breakpoint, where the sidebar is an ordinary column and
|
||||
the topbar's toggle is perfectly visible. It shipped *visible* on the
|
||||
desktop in 1.1.0 -- not because this rule was wrong, but because the
|
||||
browser was still drawing the page with the previous release's
|
||||
stylesheet; see `templating.asset`.
|
||||
#}
|
||||
<div class="sidebar__actions-rail">
|
||||
<button class="btn btn--icon sidebar__close" type="button"
|
||||
aria-label="Close sidebar" data-toggle="#sidebar">
|
||||
{{ icon("x") }}
|
||||
</button>
|
||||
</div>
|
||||
{% include "partials/_sidebar_close.html" %}
|
||||
</header>
|
||||
|
||||
{% include "partials/_sidebar_actions.html" %}
|
||||
@@ -126,13 +106,4 @@
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{#
|
||||
The scrim behind the open drawer. It carries the same `data-toggle` as every
|
||||
other control that closes it, so tapping beside the drawer closes it through
|
||||
exactly one code path rather than a second one written for touch.
|
||||
|
||||
Rendered always and shown by CSS: it exists only below the breakpoint and only
|
||||
while the drawer is open, which is a question about width and state that the
|
||||
server cannot answer and the stylesheet can.
|
||||
#}
|
||||
<div class="sidebar-scrim" data-toggle="#sidebar" aria-hidden="true"></div>
|
||||
{% include "partials/_sidebar_scrim.html" %}
|
||||
|
||||
Reference in New Issue
Block a user