feat: improve site banner styling + tab switching transitions

pull/8104/head
NGPixel 2 weeks ago
parent 0385c375c9
commit 61b17b631f
No known key found for this signature in database

@ -195,6 +195,14 @@ function onKeydown(ev) {
/* -> Enough that the last tab's corner reads as a corner, and not so much that the strip stops
being flush with the side */
padding-right: 0.5rem;
/*
The one gap in the strip, and it belongs to what may sit under it rather than to the strip: a
site banner is drawn flush to the top of the article column, pulled out of its padding with a
hairline of the column left showing either side (`--site-banner-pad` in `SiteBanner.vue`). Its
top edge was the one side with none, so a raised banner met the strip's rule with nothing
between them. 1px here is that same hairline, turned along the third side.
*/
margin-bottom: 1px;
flex: none;
/*

@ -138,18 +138,64 @@ async function render() {
reaches nothing else.
*/
.site-banner-alert {
/*
One colour at three alphas: the hue itself for the rule, the icon and the heading, the wash the
band is filled with, and the same hue at nothing, which both gradients below fade out to.
The third is spelled out rather than written as `transparent` or mixed down from the hue. Both
of those are transparent BLACK -- `color-mix(in srgb, <hue> 0%, transparent)` computes to
`color(srgb 0 0 0 / 0)`, which is the thing it looks like it avoids -- and a gradient running to
it drags its middle through grey. Stating the hue with a zero alpha is what keeps every step of
the fade the banner's own colour.
The fourth is where the rule ends, and it is the odd one out: opaque, and a step DARKER than the
band it is drawn under -- which is the band as composited, not the wash, so it is the only value
here that knows what the page behind it looks like. That coupling is why it is a literal per
theme rather than an expression: the wash over white is `#faeeef` and a step down from it is
this, while in the dark theme the hue is lighter than the page, so the same expression would
walk the wrong way and the value there is derived from `#25191c` instead. The rule has always
been opaque -- its other end is the hue itself -- so nothing is given up by stating it.
*/
--site-banner-hue: #c02636;
--site-banner-wash: rgba(192, 38, 54, 0.08);
--site-banner-fade: rgba(192, 38, 54, 0);
--site-banner-rule-end: #f5dcdf;
position: relative;
padding: 0.9em 1.1em 0.9em 3.1em;
/*
The rule that closes the band, run left to right from the hue to the band's own colour taken a
step darker -- so it thins away to a line rather than to nothing, and the edge stays drawn all
the way across.
`border-image` rather than a background layer or a pseudo-element: it keeps the line in the
border box, so the height and the padding above it are the ones the border already set, and it
leaves `::before` to the icon. The `border-bottom` above it is not a fallback that gets
overridden -- it is what gives this edge a width, since `border-image` paints the border box
and nothing else says how thick the bottom of it is. The other three stay at zero, which is why
a slice that nominally covers all four edges only ever draws this one.
*/
border-bottom: 4px solid var(--site-banner-hue);
background-color: var(--site-banner-wash);
border-image: linear-gradient(to right, var(--site-banner-hue), var(--site-banner-rule-end)) 1;
/*
The wash, thrown as an ellipse centred on the band's top-right corner: nothing at the corner,
where the page's own surface shows through, deepening to the full wash across the band.
A `background-image` and no `background-color`: a flat colour underneath would be painted
through the transparent end, which is the one part of this that has to show the page.
*/
background-image: radial-gradient(
ellipse at top right,
var(--site-banner-fade),
var(--site-banner-wash)
);
/* -> Lighter, because the notice sits on a dark page rather than in the flow of one */
@at-root .body--dark & {
--site-banner-hue: #ff8b8b;
--site-banner-wash: rgba(255, 139, 139, 0.12);
--site-banner-fade: rgba(255, 139, 139, 0);
--site-banner-rule-end: #1e1416;
}
&::before {

@ -69,7 +69,7 @@
:model-value="activeView"
:talk="showTalkTab"
:links="showLinksTab"
@update:model-value="state.view = $event" />
@update:model-value="switchView" />
<component :is="editorComponents[editorStore.editor]" v-if="editorStore.isActive" />
<!--
The lock screen, in place of the article. There is nothing to hide here: the server sent no
@ -149,8 +149,14 @@
<!--
Above the article rather than above the toolbars: what an administrator raises a banner
about is the content, and this is where a reader is already looking.
The article and nothing else. The band is drawn flush to the top of the column, squared
off to its edges and closed by a rule -- it is the lid of the page, and a discussion or
a list of what links here is not the page. `v-if` rather than `v-show`, so it takes its
own margins with it: hidden, it would still be holding the 1.5rem it puts between
itself and what follows.
-->
<site-banner />
<site-banner v-if="activeView === `article`" />
<!--
`v-show` rather than `v-if` on the article below, so that leaving the discussion and
coming back does not re-run the page's own scripts or lose where the reader was in it.
@ -810,10 +816,36 @@ function onHashChange() {
// -> A fragment can ask for the discussion as well as for a heading, and one that asks for a
// heading while the discussion is open has to put the article back or there is nothing to
// scroll to: `v-show` leaves the hidden column with no layout, so the anchor is unreachable
state.view = viewFromHash()
switchView(viewFromHash())
/*
Not awaited, and deliberately: this polls for its target rather than expecting it to be there
(see `helpers/anchors.js`), so it is already written for a heading that arrives a few frames
late -- which is exactly what a view that is mid-transition is.
*/
scrollToAnchorWhenReady(window.location.hash)
}
/**
* Move to another of the page's views, as a cross-fade.
*
* The same `withViewTransition` a page swap goes through, for the same reason: what changes here is
* an article for a discussion or a list, and cutting between two screenfuls of text reads as a flash
* where a fade reads as a turn. Cheap, too -- both views are already in hand, so the callback is one
* assignment and none of the caveats about keeping it short are in play.
*
* The guard is what keeps a fade off a tab that is already open: the strip's arrow keys select as
* they move, so holding one down would otherwise start a transition per keypress with nothing
* changing in any of them.
*/
function switchView(view) {
if (view === state.view) {
return
}
withViewTransition(() => {
state.view = view
})
}
/**
* The view the current URL asks for.
*

Loading…
Cancel
Save