Cinder pass across /admin, the login gate, and the library's a11y floor
Uncommitted work from three design runs on this branch, against one design system: docs/design-system.md is updated to match the CSS, not the reverse. Library (Reader-facing): - .chrome sticks at top: 0. Search and the tab row were unreachable three screens into a 300-item library, which is exactly where they earn their keep; everything above them still scrolls away on purpose. - One :focus-visible ring (2px --paper) on the nine controls that defined none and fell back to the UA blue. .searchbar keeps its border recolour as a resting cue but no longer stands in for a ring. - Mono labels lift 10px -> 11px everywhere. The brief names night reading and glare as the usage scene; 10px small-caps was where taste overrode it. - A card in flight past 2s says "Saving..." and carries aria-busy. htmx sets neither, so the wait up to its 15s timeout was silent in both channels. - Titles clamp at 3 lines; .is-new .title takes width: fit-content, or -webkit-box stretches the ember underline past the text it sizes to. /admin: - Overview routes into Lanes when a lane is unhealthy, prefixes each figure with its column word on the phone layout that drops the thead, labels state cells for a screen reader, and has an empty state where the sites table assumed rows. - The admin shell picks up the library's chrome: htmx 15s timeout, the shared #notice slot, #sr-announce, filter.js. admin.css follows the same pass. - admin_render_test.go and card_render_test.go render the templates directly, so markup regressions in either surface fail without a browser. Login: - DISCORD_GUILD_NAME (optional) names the community on the login screen and in the refusal message, so a stranger knows which Discord to ask for an invite. Unset degrades to a generic label; neither form names the guild id. Handlers: - maxChapterNum (9999) bounds both typed-chapter paths. uiChapter and adminSeriesCorrectLatest each parsed a float64 with no ceiling, so a hand-rolled POST stored 1e308 and every later reader of that row inherited it. Matches the max on the card's chapter input. go test ./... green.
This commit is contained in:
@@ -2,12 +2,6 @@
|
||||
--measure-wide: 1080px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--measure-wide: 1080px;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-sheet {
|
||||
max-width: var(--measure-wide);
|
||||
}
|
||||
@@ -54,12 +48,18 @@
|
||||
|
||||
.navrow a:hover {
|
||||
color: var(--paper-dim);
|
||||
background: var(--hover);
|
||||
}
|
||||
|
||||
.navrow a.active {
|
||||
color: var(--paper);
|
||||
border-bottom: 2px solid var(--paper);
|
||||
color: var(--patina);
|
||||
border-bottom: 2px solid var(--patina);
|
||||
margin-bottom: -1px;
|
||||
background: var(--patina-wash);
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
margin-left: -6px;
|
||||
margin-right: -6px;
|
||||
}
|
||||
|
||||
.admin-page {
|
||||
@@ -97,15 +97,13 @@
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.admin-sheet .readerlist,
|
||||
.admin-sheet .lanelist {
|
||||
.admin-sheet .readerlist {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.admin-sheet .readerlist li,
|
||||
.admin-sheet .lanelist li {
|
||||
.admin-sheet .readerlist li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
@@ -115,12 +113,12 @@
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
|
||||
.admin-sheet .reader-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
gap: 18px;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
gap: 28px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-sheet .reader-actions .ghost,
|
||||
@@ -134,13 +132,11 @@
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
|
||||
.admin-sheet .readerlist form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-sheet .lane-browser {
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
|
||||
.admin-sheet .ghost.danger {
|
||||
color: var(--danger);
|
||||
@@ -159,9 +155,7 @@
|
||||
|
||||
.admin-sheet .reader-sessions,
|
||||
.admin-sheet .reader-sightings,
|
||||
.admin-sheet .reader-blocked,
|
||||
.admin-sheet .lane-fact,
|
||||
.admin-sheet .lane-mark {
|
||||
.admin-sheet .reader-blocked {
|
||||
font: 500 13px/1.4 var(--font-mono);
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
@@ -171,25 +165,14 @@
|
||||
color: var(--paper-dim);
|
||||
}
|
||||
|
||||
.admin-sheet .reader-sightings,
|
||||
.admin-sheet .lane-fact {
|
||||
.admin-sheet .reader-sightings {
|
||||
color: var(--mute);
|
||||
}
|
||||
|
||||
.admin-sheet .reader-blocked,
|
||||
.admin-sheet .lane-mark {
|
||||
.admin-sheet .reader-blocked {
|
||||
color: var(--patina);
|
||||
}
|
||||
|
||||
.admin-sheet .lane-site {
|
||||
font: 400 19px/1.2 var(--font-display);
|
||||
color: var(--paper-dim);
|
||||
}
|
||||
|
||||
.admin-sheet .lanelist li.attention .lane-site {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* A single grid keeps row rules continuous; cell padding supplies gutters. */
|
||||
.admin-sheet .sechead {
|
||||
display: flex;
|
||||
@@ -301,9 +284,10 @@
|
||||
}
|
||||
|
||||
.admin-sheet .tbl .c-act .ghost + .ghost {
|
||||
margin-left: 12px;
|
||||
margin-left: 28px;
|
||||
}
|
||||
|
||||
|
||||
.admin-sheet .tbl .trow > .confirm-row {
|
||||
grid-column: 1 / -1;
|
||||
padding: 10px 12px;
|
||||
@@ -324,6 +308,10 @@
|
||||
gap: 16px;
|
||||
padding: 11px 24px 11px 0;
|
||||
}
|
||||
.admin-sheet .stat + .stat {
|
||||
border-left: 1px solid var(--rule);
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.admin-sheet .stat .lbl {
|
||||
font: 500 13px/1.3 var(--font-mono);
|
||||
@@ -345,34 +333,53 @@
|
||||
.admin-sheet .stat a.fig {
|
||||
/* The hover rule below sets the border on :hover only, which shifts the
|
||||
row a pixel on hover; the transparent baseline holds the layout. */
|
||||
border-bottom: 1px solid transparent;
|
||||
border-bottom: 1px solid color-mix(in oklch, var(--patina) 22%, transparent);
|
||||
}
|
||||
|
||||
.admin-sheet .stat a.fig:hover {
|
||||
color: var(--patina);
|
||||
border-bottom: 1px solid var(--patina);
|
||||
background: var(--patina-wash);
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.admin-sheet .stat + .stat {
|
||||
border-left: none;
|
||||
border-top: 1px solid var(--rule);
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-sheet .mark,
|
||||
.admin-sheet .mark-faint {
|
||||
font: 500 13px/1 var(--font-mono);
|
||||
font: 500 12px/1.2 var(--font-mono);
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.admin-sheet .mark {
|
||||
color: var(--patina);
|
||||
background: var(--patina-wash);
|
||||
border-color: var(--patina-line);
|
||||
}
|
||||
|
||||
.admin-sheet .mark-faint {
|
||||
color: var(--mute-2);
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.admin-sheet .mark.mark-strong {
|
||||
font-size: 13px;
|
||||
letter-spacing: .14em;
|
||||
font-size: 12px;
|
||||
letter-spacing: .12em;
|
||||
color: var(--patina);
|
||||
background: var(--patina-wash);
|
||||
border-color: var(--patina-line);
|
||||
padding: 3px 7px;
|
||||
}
|
||||
|
||||
.admin-sheet .mark.mark-strong::before {
|
||||
@@ -387,9 +394,21 @@
|
||||
}
|
||||
|
||||
.admin-sheet .mark.bad {
|
||||
color: var(--danger);
|
||||
color: var(--slate);
|
||||
background: var(--slate-wash);
|
||||
border-color: var(--slate);
|
||||
font-weight: 600;
|
||||
}
|
||||
.admin-sheet .mark.bad::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: var(--slate);
|
||||
margin-right: 8px;
|
||||
vertical-align: .08em;
|
||||
border-radius: 1px; /* square, not circle — distinguishes from .mark-strong patina dot */
|
||||
}
|
||||
|
||||
.admin-sheet .tbl.sites {
|
||||
grid-template-columns: 170px repeat(4, 106px) minmax(180px, 1fr);
|
||||
}
|
||||
@@ -402,18 +421,56 @@
|
||||
}
|
||||
|
||||
.admin-sheet .tbl.sites .c-state {
|
||||
color: var(--patina);
|
||||
color: var(--mute-2);
|
||||
white-space: nowrap;
|
||||
padding: 2px 7px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
justify-self: start;
|
||||
align-self: center;
|
||||
font: 500 11px/1 var(--font-mono);
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-sheet .tbl.sites .c-state.bad,
|
||||
.admin-sheet .tbl.lanes .c-skip .bad,
|
||||
.admin-sheet .tbl.lanes .trow.attention .c-site {
|
||||
color: var(--danger);
|
||||
.admin-sheet .tbl.sites .c-state.ok {
|
||||
color: var(--patina);
|
||||
background: var(--patina-wash);
|
||||
border-color: var(--patina-line);
|
||||
}
|
||||
.admin-sheet .tbl.sites .c-state.bad {
|
||||
color: var(--slate);
|
||||
background: var(--slate-wash);
|
||||
border-color: var(--slate);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-sheet .tbl.lanes .c-skip .ok {
|
||||
color: var(--patina);
|
||||
background: var(--patina-wash);
|
||||
border: 1px solid var(--patina-line);
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.admin-sheet .tbl.lanes .trow.attention .c-site {
|
||||
color: var(--patina);
|
||||
}
|
||||
|
||||
.admin-sheet .tbl.lanes .c-skip .bad {
|
||||
color: var(--slate);
|
||||
background: var(--slate-wash);
|
||||
border: 1px solid var(--slate);
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-sheet .tbl.lanes .trow.attention {
|
||||
background: var(--patina-wash);
|
||||
box-shadow: inset 1px 0 0 var(--patina);
|
||||
}
|
||||
.admin-sheet .tbl.series .trow.attention {
|
||||
background: var(--patina-wash);
|
||||
box-shadow: inset 1px 0 0 var(--patina);
|
||||
}
|
||||
|
||||
.admin-sheet .tbl.sites .thead > *:nth-child(n+2):nth-child(-n+5),
|
||||
@@ -572,8 +629,13 @@
|
||||
.admin-sheet .segrow a.active {
|
||||
border-bottom-color: var(--patina);
|
||||
color: var(--patina);
|
||||
background: var(--patina-wash);
|
||||
}
|
||||
|
||||
.admin-sheet .segrow a:hover {
|
||||
color: var(--paper-dim);
|
||||
background: var(--hover);
|
||||
}
|
||||
.admin-sheet .listhead {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
@@ -612,10 +674,11 @@
|
||||
}
|
||||
|
||||
.admin-sheet .pager .pg.disabled {
|
||||
color: var(--faint);
|
||||
color: var(--mute);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.admin-sheet .empty {
|
||||
padding: 28px 0;
|
||||
text-align: center;
|
||||
@@ -669,6 +732,27 @@
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.admin-sheet .readerlist li .confirm-row {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Busy: same muted hairline the reader library uses, never ember. */
|
||||
.admin-sheet .trow.htmx-request,
|
||||
.admin-sheet #readers.htmx-request,
|
||||
.admin-sheet #lanes.htmx-request,
|
||||
.admin-sheet #detail-meta.htmx-request,
|
||||
.admin-sheet .dform.htmx-request {
|
||||
opacity: .6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.admin-sheet .ghost:focus-visible {
|
||||
outline: 2px solid var(--patina);
|
||||
outline-offset: 2px;
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
.admin-sheet .tbl.series .row-msg {
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 6px;
|
||||
@@ -677,7 +761,6 @@
|
||||
}
|
||||
|
||||
.admin-sheet .detail-back {
|
||||
display: inline-block;
|
||||
margin: 18px 0 0;
|
||||
}
|
||||
|
||||
@@ -819,6 +902,23 @@
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* The thead is gone, so every cell that was only legible under a column
|
||||
heading prints its own. The label is data, not decoration: without it
|
||||
"12 40 6h" is three numbers in the same face with no way to tell which
|
||||
is which. Set in the mono face at label size so the figure still leads
|
||||
visually. */
|
||||
.admin-sheet .tbl.lanes [data-label] {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-sheet .tbl.lanes [data-label]::before {
|
||||
content: attr(data-label) " ";
|
||||
font-size: 11px;
|
||||
letter-spacing: .1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--mute-2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 899px) {
|
||||
@@ -834,7 +934,12 @@
|
||||
.admin-sheet .tbl .trow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 10px;
|
||||
/* Cells no longer share a font size — a label-value pair is 11px over
|
||||
15px — so they hang off the baseline, not off the box top. */
|
||||
align-items: baseline;
|
||||
/* Wider than the mouse layout's column gutter: each cell now carries its
|
||||
own label, so the eye needs the pair to read as one unit. */
|
||||
gap: 2px 18px;
|
||||
padding: 11px 0;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
@@ -871,4 +976,69 @@
|
||||
.admin-sheet .tbl .c-act {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* A cell is one label-value pair and must not break between them. */
|
||||
.admin-sheet .tbl [data-label] {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-sheet .tbl [data-label]::before {
|
||||
content: attr(data-label) " ";
|
||||
font-size: 11px;
|
||||
letter-spacing: .1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--mute-2);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- touch: the reader's library is sized for a thumb throughout, this
|
||||
sheet was not — it is a data surface that grew on a mouse. Gated on the
|
||||
input method rather than the width, because the phone is not the only
|
||||
coarse pointer and a 1400px touch laptop has the same problem. Vertical
|
||||
only: horizontal space is what this surface is short of. ---- */
|
||||
@media (pointer: coarse) {
|
||||
/* The figure is the door — the label beside it is not a link — so the row
|
||||
gives up its own padding to the cell that has to be hit. */
|
||||
.admin-sheet .stat {
|
||||
padding: 0 24px 0 0;
|
||||
}
|
||||
|
||||
/* A one-digit count is 11px wide: the minimum is on the box, not the
|
||||
glyph, or the door is a target the thumb has to aim at. */
|
||||
.admin-sheet .stat .fig {
|
||||
min-width: 44px;
|
||||
padding: 14px 0 14px 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.admin-sheet .tbl .c-title a,
|
||||
.admin-sheet .tbl a.fig,
|
||||
.admin-sheet .tbl a.c-site,
|
||||
/* The collapsed lanes row sets padding on every direct cell, which is a
|
||||
more specific selector than the line above — its own Site link has to be
|
||||
named here or it keeps the mouse metric. */
|
||||
.admin-sheet .tbl.lanes .trow > a.c-site {
|
||||
display: inline-block;
|
||||
min-width: 44px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.admin-sheet .pager {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.admin-sheet .pager .pg {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.admin-sheet .segrow a {
|
||||
padding: 15px 11px 16px;
|
||||
}
|
||||
|
||||
.admin-sheet .fsel select,
|
||||
.admin-sheet .pausebar select,
|
||||
.admin-sheet .dform input {
|
||||
padding-top: 11px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user