301 lines
9.3 KiB
SCSS
301 lines
9.3 KiB
SCSS
@use 'mixins' as *;
|
||
|
||
// ---------- Reader: an open book of article pages ----------
|
||
// Every article opened in this session is a page, laid out beside the last
|
||
// in a track you move along sideways (ArticleBookStack.vue). Pages are solid
|
||
// paper, and each sticks a spine's width further from the left edge than the
|
||
// one before it — left: calc(var(--i) * var(--spine-w)) — so turning to a
|
||
// newer page slides it over the older ones, which park along the edge with
|
||
// nothing but their spines showing: a stack of pages, titled along the edge
|
||
// like books on a shelf. A page is as wide as its share of the reading
|
||
// surface — minus the spines of every other page, but never more than a
|
||
// comfortable measure, and open across the whole desk when it is the only
|
||
// page in the book — so on a wide desk the next page waits fully in view
|
||
// beside the one you read, not only its spine; on a narrow one the rest wait
|
||
// out at the right edge. A spine the desk has no room for is copied to that
|
||
// edge — the dock, .book-dock below — so no page is ever more than a click
|
||
// away, though its own spine is off the end of the row. Click a spine to turn
|
||
// back to that page, the × to take it out of the book. Below 900px the book
|
||
// folds to the one page in view and its neighbours become chips along the
|
||
// top. Routes stay the source of truth — every page is a plain /wiki/<slug>
|
||
// URL, and useArticleTabs keeps the stack. (The --sidebar-w, --spine-w and
|
||
// --pane-max-w tokens live in _tokens.scss.)
|
||
|
||
// the surface the book lies open on
|
||
.book-layer {
|
||
position: fixed;
|
||
inset: 0 0 0 var(--sidebar-w);
|
||
transition: left 0.22s ease;
|
||
z-index: 70;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--bg-elev);
|
||
box-shadow: -10px 0 26px rgb(5 7 13 / 9%);
|
||
}
|
||
|
||
// nothing lives at this URL: the route host says so over the open book
|
||
.reader-note {
|
||
position: fixed;
|
||
inset: 0 0 0 var(--sidebar-w);
|
||
transition: left 0.22s ease;
|
||
z-index: 74;
|
||
background: var(--bg-elev);
|
||
box-shadow: -10px 0 26px rgb(5 7 13 / 4%);
|
||
}
|
||
|
||
// the row of pages, moved along sideways
|
||
.book-track {
|
||
--pane-w: calc(100% - (var(--panes, 1) - 1) * var(--spine-w));
|
||
position: relative;
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
align-items: stretch;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
overscroll-behavior: contain;
|
||
// the desk under the pages: once a page stops short of the reading
|
||
// surface, what's left of the desk shows beside it
|
||
background: var(--bg);
|
||
@include no-scrollbar;
|
||
}
|
||
|
||
// one page: solid paper, parked beside the pages under it
|
||
.book-pane {
|
||
position: sticky;
|
||
left: calc(var(--i) * var(--spine-w));
|
||
z-index: calc(var(--i) + 1);
|
||
flex: 0 0 var(--pane-w);
|
||
// A page takes every inch of the reading surface it can — the one in view
|
||
// opens across the whole desk, not just a column of it. Only once so many
|
||
// are open that each page's share falls below a comfortable reading width
|
||
// does it hold at that width and the book scroll sideways instead.
|
||
min-width: min(46rem, 100%);
|
||
// ...and on a desk wide enough for it to stretch past a page of paper, it
|
||
// holds at --pane-max-w and leaves the rest of the desk bare. The single
|
||
// page of a one-page book is exempt below.
|
||
max-width: var(--pane-max-w);
|
||
display: flex;
|
||
background: var(--bg-elev);
|
||
// the fore-edge of the paper, against the desk and the next page
|
||
border-right: 1px solid var(--border);
|
||
// the edge of this page lying on top of the ones beneath it
|
||
box-shadow: -10px 0 22px rgb(5 7 13 / 7%);
|
||
animation: page-turned-in 0.3s cubic-bezier(0.22, 0.8, 0.36, 1) both;
|
||
}
|
||
|
||
// a book with a single page in it lies open across the whole desk: there is
|
||
// no other page waiting for that space, so the page takes all of it
|
||
.book-pane:only-child { max-width: none; }
|
||
|
||
// a page that isn't in view asks to be turned to: the whole of it is
|
||
// clickable (clicks on its inert body retarget to the pane itself), not
|
||
// just the spine
|
||
.book-pane:not(.active) {
|
||
cursor: pointer;
|
||
|
||
.pane-body, .pane-body * { cursor: pointer; }
|
||
}
|
||
|
||
// a new page is laid down at the right and eased into the stack
|
||
@keyframes page-turned-in {
|
||
from { transform: translateX(56px); opacity: 0; }
|
||
}
|
||
|
||
// the page's fore-edge: its title standing on end, and the × that closes it.
|
||
// Once a newer page is turned over this one, the spine is all that's left in
|
||
// sight — so it is the handle for turning back. A page whose spine is off the
|
||
// edge of the desk has it copied at the edge itself, in the dock below, which
|
||
// borrows .spine-title and .spine-close as its own standing type.
|
||
.pane-spine {
|
||
flex: 0 0 var(--spine-w);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 0 10px;
|
||
background: var(--surface);
|
||
border-right: 1px solid var(--border);
|
||
color: var(--muted);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
transition: color 0.15s, background-color 0.15s;
|
||
|
||
&:hover { color: var(--text); background: var(--surface-2); }
|
||
}
|
||
|
||
// the page in view has its spine marked, the way a ribbon marks a book
|
||
.book-pane.active .pane-spine {
|
||
color: var(--accent);
|
||
background: color-mix(in srgb, var(--accent) 10%, var(--surface));
|
||
}
|
||
|
||
.spine-close {
|
||
@include icon-target(15px, 5px);
|
||
color: var(--faint);
|
||
transition: color 0.15s, background-color 0.15s;
|
||
@include close-target;
|
||
|
||
svg { display: block; }
|
||
}
|
||
|
||
.spine-title {
|
||
writing-mode: vertical-rl;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: 0.78rem;
|
||
font-weight: 700;
|
||
line-height: 1.35;
|
||
letter-spacing: 0.01em;
|
||
padding-top: 0;
|
||
}
|
||
|
||
// the page itself: the article, scrolling on its own
|
||
.pane-body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.pane-scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
overscroll-behavior: contain;
|
||
padding-inline: clamp(16px, 3.5vw, 36px);
|
||
|
||
// the article takes the page as a single centered column
|
||
.article-page { max-width: 78ch; margin-inline: auto; }
|
||
}
|
||
|
||
.pane-skeleton { padding-top: 32px; }
|
||
|
||
// folded up narrow: the neighbours of the page in view, as chips along the
|
||
// top. Wide screens read the spines instead.
|
||
.book-chips {
|
||
display: none;
|
||
flex-shrink: 0;
|
||
gap: 8px;
|
||
padding: 9px 10px;
|
||
overflow-x: auto;
|
||
overscroll-behavior: contain;
|
||
background: var(--surface);
|
||
border-bottom: 1px solid var(--border);
|
||
@include no-scrollbar;
|
||
}
|
||
|
||
.book-chip {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
max-width: 46vw;
|
||
padding: 5px 7px 5px 11px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 999px;
|
||
background: var(--surface-2);
|
||
color: var(--muted);
|
||
font-size: 0.79rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
transition: color 0.15s, background-color 0.15s, border-color 0.15s;
|
||
|
||
&:hover { color: var(--text); }
|
||
|
||
&.active {
|
||
color: var(--accent);
|
||
background: var(--bg-elev);
|
||
border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
|
||
}
|
||
}
|
||
|
||
.chip-title {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.chip-close { @include icon-target(16px, 5px); color: var(--faint); @include close-target; }
|
||
|
||
// ---------- The dock: pages waiting beyond the right edge of the desk -----
|
||
// A page further along the book than the one in view lies somewhere off the
|
||
// right edge of the desk, reachable by dragging the row sideways. While it is
|
||
// out there, its spine is kept at the edge itself: a stack one spine wide, in
|
||
// the order the pages were opened, each one the standing title and × of the
|
||
// page it stands for. Click it and the book turns to that page as though you
|
||
// had scrolled to it; the page's own spine comes into view and its copy in the
|
||
// dock goes away (ArticleBookStack measures which pages are out there). It
|
||
// rides above every page of the book — the pages are numbered one apiece by
|
||
// --panes — and, being inside the reading surface, below anything popped over
|
||
// it.
|
||
.book-dock {
|
||
position: absolute;
|
||
inset-block: 0;
|
||
right: 0;
|
||
z-index: calc(var(--panes, 1) + 2);
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: var(--spine-w);
|
||
padding: 6px 0;
|
||
background: var(--surface);
|
||
border-left: 1px solid var(--border);
|
||
box-shadow: -10px 0 22px rgb(5 7 13 / 7%);
|
||
color: var(--muted);
|
||
}
|
||
|
||
// the waiting spines share the height of the desk, so however many pages are
|
||
// out there every one of them keeps a place at the edge, its title cut short
|
||
// to fit — the whole title is in the tooltip
|
||
.dock-spine {
|
||
flex: 1 1 0;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 4px 0;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
transition: color 0.15s, background-color 0.15s;
|
||
|
||
+ .dock-spine { border-top: 1px solid var(--border); }
|
||
|
||
&:hover { color: var(--text); background: var(--surface-2); }
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.book-layer, .reader-note { inset: 0; box-shadow: none; }
|
||
|
||
.book-track { overflow-x: hidden; }
|
||
|
||
// folded: only the page in view is out, the rest are chips along the top
|
||
.book-pane {
|
||
position: static;
|
||
flex: 1 1 100%;
|
||
width: 100%;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
box-shadow: none;
|
||
|
||
&:not(.active) { display: none; }
|
||
}
|
||
|
||
.book-chips { display: flex; }
|
||
|
||
// folded, the book has no edges to stack spines against: the chips along
|
||
// the top are every page of it
|
||
.book-dock { display: none; }
|
||
|
||
.pane-scroll { padding-inline: 16px; }
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.book-pane { animation: none; }
|
||
}
|