First commit

This commit is contained in:
2026-09-30 01:49:35 +02:00
commit 6edd03aace
109 changed files with 27020 additions and 0 deletions

View File

@@ -0,0 +1,300 @@
@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; }
}