@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/ // 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); } } // ---------- A wiki page laid on the book ---------- // The link map and the graphing calculator are pages too: the search popup // lays one of them on the book beside the articles (ToolPane.vue). It fills // the page edge to edge — no column of prose scrolls here, the surface keeps // its own docks and toolbars — and it takes the whole share of the desk, a // plane being no narrower read than a page of text. Each stage's own sheet // answers for the rest (_graph.scss, _calculator.scss). .book-pane.pane-tools { max-width: none; overflow: hidden; } .pane-stage { position: relative; flex: 1; min-width: 0; min-height: 0; } // the surface's mark on the spine, under the ×, so a page of the map or the // plane reads as itself fanned in among the articles .pane-spine .spine-glyph { flex: none; opacity: 0.65; } .book-pane.active .pane-spine .spine-glyph { opacity: 1; } @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; } }