More things

This commit is contained in:
2026-10-01 02:29:21 +02:00
parent e1df4d3ae1
commit 13b659662e
54 changed files with 4409 additions and 911 deletions

View File

@@ -1,3 +1,32 @@
@use 'mixins' as *;
// ---------- The dashboard's two tabs: the requests and the accounts ----------
// The same segmented control the sign-in popup and the settings switch wear
// (the mixins in _mixins.scss); the panels they open are the two sections.
.admin-tabs { @include segmented; margin-bottom: 22px; }
.admin-tab {
@include segmented-tab(7px 14px, 0.84rem);
display: inline-flex;
align-items: center;
gap: 7px;
}
// how many requests are still waiting, so the pile is visible even while the
// admin is reading the accounts — the tally's shape from the sidebar's count
.admin-tab-count {
min-width: 18px;
height: 18px;
padding: 0 5px;
display: inline-grid;
place-items: center;
border-radius: 9px;
background: color-mix(in srgb, var(--accent) 14%, transparent);
color: var(--accent);
font-size: 0.7rem;
font-weight: 700;
}
// ---------- Admin dashboard: the list of accounts ----------
// One row per account, read left to right: who, what they may do, when they
// arrived, and the one thing an admin can change about them.

View File

@@ -3,11 +3,68 @@
// ---------- Articles index ----------
.topic-tally { font-size: 0.85rem; color: var(--muted); }
.topic-dropdown {
@include dropdown-panel;
left: 0;
right: 0;
max-height: 280px;
// ---------- Folders: the shelf filed by topic ----------
.folder-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 14px;
}
.folder-card {
display: flex;
align-items: center;
gap: 12px;
padding: 15px 18px;
border-radius: 14px;
border: 1px solid var(--border);
background: var(--surface);
text-decoration: none;
color: inherit;
box-shadow: var(--shadow-sm);
transition: transform 0.18s ease, border-color 0.18s, box-shadow 0.18s;
svg { flex: none; color: var(--accent); opacity: 0.85; transition: opacity 0.15s; }
&:hover {
transform: translateY(-2px);
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
box-shadow: var(--shadow-lg);
svg { opacity: 1; }
}
&:focus-visible { box-shadow: var(--ring); }
}
.folder-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.95rem;
font-weight: 700;
letter-spacing: -0.01em;
// the facet tally lowercases every topic; folders wear their names capitalized
text-transform: capitalize;
}
.folder-count { flex: none; margin-left: auto; }
// the folder grid's loading row: a shorter skeleton than an article card's
.folder-skeleton { height: 60px; border-radius: 14px; @include shimmer; }
// closing a folder: the quiet way back onto the topics
.back-link {
display: inline-flex;
align-items: center;
gap: 7px;
margin-bottom: 14px;
color: var(--muted);
font-size: 0.85rem;
font-weight: 650;
text-decoration: none;
transition: color 0.15s;
&:hover { color: var(--accent); }
}
.topic-option {
@@ -18,45 +75,6 @@
&:hover, &.active { background: color-mix(in srgb, var(--accent) 12%, transparent); }
}
.topic-dropdown-none {
padding: 8px 12px;
margin: 0;
font-size: 0.85rem;
color: var(--muted);
}
.topic-search-wrap {
position: relative;
margin-bottom: 14px;
svg {
position: absolute;
left: 14px;
top: 50%;
translate: 0 -50%;
color: var(--faint);
pointer-events: none;
}
}
.topic-search {
width: 100%;
height: 40px;
padding: 0 14px 0 40px;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--surface);
box-shadow: var(--shadow-sm);
font-size: 0.9rem;
transition: border-color 0.15s, box-shadow 0.2s;
&:focus {
outline: none;
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);
}
}
.topic-count {
padding: 1px 7px;
border-radius: 999px;

View File

@@ -7,6 +7,16 @@
gap: 20px;
}
// the shelf's last line: reaching it brings the next batch of cards in, and
// while that batch is on its way it says so
.shelf-sentinel {
display: grid;
place-items: center;
padding-top: 24px;
color: var(--faint);
font-size: 0.85rem;
}
.article-card {
display: flex;
flex-direction: column;

View File

@@ -90,6 +90,11 @@
// editor body
.editor-body {
// One height for the writing field and the preview side by side: each holds
// its text in a box of this height and scrolls it inside, so a long article
// never makes the editor taller than the field the writer is looking at.
--editor-pane-h: 480px;
border: 1px solid var(--border);
border-radius: 15px;
background: var(--bg-elev);
@@ -141,6 +146,7 @@
.editor-grid {
display: grid;
grid-template-columns: 1fr 1fr;
height: var(--editor-pane-h);
&.mode-write {
grid-template-columns: 1fr;
@@ -158,11 +164,11 @@
textarea.md-input {
display: block;
width: 100%;
min-height: 480px;
height: 100%;
padding: 22px;
border: 0;
background: transparent;
resize: vertical;
resize: none;
font-size: 0.9rem;
line-height: 1.75;
tab-size: 2;
@@ -176,13 +182,24 @@ textarea.md-input {
border-left: 1px solid var(--border);
padding: 22px 26px;
min-width: 0;
overflow-y: auto;
// and reaching the end of a long preview doesn't start scrolling the page
overscroll-behavior: contain;
overflow-wrap: break-word;
}
.preview-label { @include eyebrow; margin-bottom: 14px; }
@media (max-width: 900px) {
.editor-grid { grid-template-columns: 1fr; }
.editor-grid {
grid-template-columns: 1fr;
height: auto;
}
// stacked, the two panes keep the height the split gives them — each still
// scrolls its own text rather than stretching the editor
.write-pane,
.preview-pane { height: var(--editor-pane-h); }
.preview-pane { border-left: 0 !important; border-top: 1px solid var(--border); }
}
@@ -265,6 +282,21 @@ textarea.md-input {
@include popup-anim('confirm-popup', '.confirm-modal', 0.18s, transform 0.18s ease, 10px);
// sent-request popup — the discard popup's shape in a success temper: it
// confirms a proposal filed with the admins, and closing it ends the edit
.sent-check {
display: grid;
place-items: center;
width: 46px;
height: 46px;
margin-bottom: 14px;
border-radius: 50%;
background: color-mix(in srgb, var(--success) 14%, transparent);
color: var(--success);
font-size: 1.3rem;
font-weight: 800;
}
// fullscreen editor
.editor-body.is-fullscreen {
position: fixed;
@@ -278,9 +310,10 @@ textarea.md-input {
.editor-toolbar { flex: 0 0 auto; }
.editor-grid { flex: 1; min-height: 0; }
.editor-grid { flex: 1; height: auto; min-height: 0; }
.write-pane, .preview-pane { min-height: 0; overflow-y: auto; }
// the screen decides how tall a pane is here, not --editor-pane-h
.write-pane, .preview-pane { height: auto; min-height: 0; overflow-y: auto; }
textarea.md-input { height: 100%; min-height: 0; resize: none; }
}

View File

@@ -8,10 +8,24 @@
inset: -120px -20% auto -20%;
height: 480px;
pointer-events: none;
background:
radial-gradient(38% 60% at 30% 30%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 70%),
radial-gradient(34% 55% at 72% 22%, color-mix(in srgb, var(--accent-2) 14%, transparent), transparent 70%);
filter: blur(30px);
}
}
// the index page hangs its hero off the middle of the reading surface: only
// `/` carries `.is-home`, so the stretch below stays here and the other pages
// that share `.home-main` (the shelf, the two admin desks) are untouched.
// `.app-main` already owns the viewport's height (flex: 1 beside the sidebar
// on desktop, under the top bar on mobile); the container grows into it and
// centres the page's block in what's left.
.app-main:has(.home-main.is-home) {
display: flex;
flex-direction: column;
.container {
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
}
}
@@ -88,6 +102,18 @@
&:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
}
// the hero box opens the search popup (the Ctrl/⌘+K one) instead of taking
// text, so it is a button wearing the search bar: the label sits where typed
// text would, in the placeholder's colour
.search-input.search-trigger {
font: inherit;
text-align: left;
cursor: pointer;
color: var(--faint);
&:hover { border-color: var(--accent); }
}
.search-kbd {
position: absolute;
right: 16px;

View File

@@ -183,6 +183,15 @@
min-height: 39px; // 8+8 padding + 14.4px x 1.6 label line
}
// the waiting count is not carried off with the labels: pinned to the
// icon's corner, it stays a dot on the rail
.sidebar-requests .requests-count {
position: absolute;
top: 2px;
right: 2px;
margin-left: 0;
}
.sidebar-nav .btn {
min-height: 35px; // 7+7 padding + 13.12px x 1.6 label line
}
@@ -211,13 +220,14 @@
html.sidebar-collapsed .hide-collapsed { display: none; transition: none; }
}
// ---------- "Open" articles: the count beside the button that shows them ----------
// ---------- Sidebar tallies: articles open, requests waiting ----------
.sidebar-open.is-current {
color: var(--accent);
background: color-mix(in srgb, var(--accent) 9%, transparent);
}
.tabs-count {
.tabs-count,
.requests-count {
margin-left: auto;
min-width: 18px;
height: 18px;
@@ -232,6 +242,10 @@
font-variant-numeric: tabular-nums;
}
// the waiting pile is the very reason to open the desk, so its number stays
// on the collapsed rail — the link is the frame it rides the corner of
.sidebar-requests { position: relative; }
.logo {
display: flex;
align-items: center;

View File

@@ -3,11 +3,13 @@
// ---------- Profile popup ----------
// The account row in the sidebar opens it; it draws and closes in the same
// shape as the settings popup. Holds the profile picture (a data URL kept on
// the account), the username, the password, and the way out.
// the account), the username, the password, the interface language, and the
// way out. Cut wider than the settings sheet so the language's five choices
// fit in one row, and taller so the sections need no scrolling.
.profile-overlay { @include overlay(120); }
.profile-modal {
@include sheet(460px, min(620px, calc(100vh - 110px)));
@include sheet(540px, min(720px, calc(100vh - 110px)));
&:focus, &:focus-visible { outline: none; }
}

View File

@@ -0,0 +1,214 @@
// ---------- Admin dashboard: the edit requests members have sent in ----------
// One entry per request: who wants what, which parts of the page it touches,
// the reviewers' buttons while it waits — and, opened up, the change itself
// laid out as two pages side by side.
.proposal-list {
display: grid;
gap: 10px;
margin: 0 0 4px;
padding: 0;
list-style: none;
}
.proposal-entry {
padding: 12px 16px;
border-radius: 14px;
border: 1px solid var(--border);
background: var(--surface);
box-shadow: var(--shadow-sm);
// a waiting request is the accent one, so the pile reads at a glance
&.is-pending { border-color: color-mix(in srgb, var(--accent) 34%, var(--border)); }
// decided entries recede: the record stays, the call is already made
&.is-approved,
&.is-rejected { background: color-mix(in srgb, var(--surface-2) 45%, var(--surface)); }
}
.proposal-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.proposal-summary {
flex: 1;
min-width: 0;
font-size: 0.9rem;
color: var(--muted);
.proposal-proposer { color: var(--text); font-weight: 650; }
.proposal-article-link {
color: var(--accent);
font-weight: 650;
text-decoration: none;
&:hover { text-decoration: underline; text-underline-offset: 2px; }
}
// a requested page has no address to follow yet: its title is written as
// plain text, not as a link to something that is not there
.proposal-new { color: var(--text); font-weight: 650; }
}
// the mark on a request for a page the wiki does not have: dashed rather than
// filled, because the page is only a promise until somebody approves it
.proposal-kind {
flex: none;
padding: 2px 9px;
border: 1px dashed color-mix(in srgb, var(--accent) 45%, var(--border));
border-radius: 999px;
color: var(--accent);
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
}
.proposal-status {
flex-shrink: 0;
padding: 3px 11px;
border-radius: 999px;
background: var(--surface-2);
color: var(--muted);
font-size: 0.75rem;
font-weight: 700;
&.status-pending {
background: color-mix(in srgb, var(--accent) 14%, transparent);
color: var(--accent);
}
&.status-approved {
background: color-mix(in srgb, var(--success) 14%, transparent);
color: var(--success);
}
&.status-rejected {
background: color-mix(in srgb, var(--danger) 12%, transparent);
color: var(--danger);
}
}
.proposal-date { flex-shrink: 0; font-size: 0.78rem; color: var(--faint); }
.proposal-decided { flex-shrink: 0; font-size: 0.76rem; font-style: italic; color: var(--faint); }
.proposal-actions {
margin-left: auto;
flex: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
// applying a proposal is the yes of the pair — solid, the way the wiki's
// other primary asks are; refusing stays the quiet ghost beside it
.btn-approve {
background: var(--success);
color: #fff;
box-shadow: 0 6px 18px -8px color-mix(in srgb, var(--success) 65%, transparent);
&:hover { filter: brightness(1.08); }
}
// the page moved after the request was sent — approving overwrites that
.proposal-stale { font-size: 0.78rem; font-style: italic; color: var(--danger); }
.proposal-diff {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin-top: 14px;
padding-top: 14px;
border-top: 1px dashed var(--border);
}
.proposal-diff-col { min-width: 0; }
.diff-heading {
margin: 0 0 10px;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
&.is-before { color: var(--faint); }
&.is-after { color: var(--success); }
}
.diff-title {
margin: 0 0 8px;
font-size: 1rem;
color: var(--text);
del { color: var(--faint); text-decoration-color: color-mix(in srgb, var(--danger) 60%, transparent); }
}
.diff-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 0 0 12px;
}
.diff-tag {
padding: 2px 9px;
border-radius: 7px;
background: var(--surface-2);
color: var(--muted);
font-size: 0.72rem;
font-weight: 600;
}
// the two pages: tall articles scroll inside their frame rather than
// stretching the dashboard out without end
.diff-body {
max-height: 420px;
overflow-y: auto;
padding: 2px 16px 14px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--bg-elev);
}
// the half of a creation's diff where a page would go: the frame the other
// side fills with text, holding only the words that say it is not written yet
.diff-absent {
margin: 16px 0;
font-size: 0.85rem;
font-style: italic;
color: var(--faint);
}
// the fold over the decided requests: a heading you open, nothing louder
.proposal-history-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 20px 0 12px;
padding: 4px 2px;
border: none;
background: none;
color: var(--muted);
font-size: 0.85rem;
font-weight: 700;
letter-spacing: -0.01em;
cursor: pointer;
&:hover { color: var(--text); }
svg { transition: transform 0.18s ease; }
svg.turned { transform: rotate(90deg); }
}
// the request row's skeleton, in the shape of one entry
.proposal-skeleton { height: 74px; }
@media (max-width: 720px) {
.proposal-diff { grid-template-columns: 1fr; }
.proposal-actions { margin-left: 0; }
}

View File

@@ -32,8 +32,9 @@
.settings-group-hint { font-size: 0.8rem; color: var(--muted); }
// segmented light / dark switch
.theme-switch { @include segmented; }
// segmented control: the settings' light / dark switch and the profile's
// language row; wraps so a narrow screen never clips the last choice
.theme-switch { @include segmented; flex-wrap: wrap; }
.theme-option {
display: inline-flex;

View File

@@ -16,6 +16,14 @@
h3 { margin: 0 0 6px; color: var(--text); font-size: 1.15rem; }
p { margin: 0 auto 22px; max-width: 42ch; font-size: 0.93rem; }
// two ways forward from a dead end, side by side
.empty-actions {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 12px;
}
}
.banner {
@@ -37,6 +45,32 @@
border-radius: 5px;
font-size: 0.85em;
}
// a rule the same size and shape, in another temper: green once a change
// has landed somewhere, blue for what the page wants you to know first
&.banner-success {
border-color: color-mix(in srgb, var(--success) 35%, var(--border));
background: color-mix(in srgb, var(--success) 7%, var(--surface));
.glyph { color: var(--success); }
}
&.banner-info {
border-color: color-mix(in srgb, var(--accent) 30%, var(--border));
background: color-mix(in srgb, var(--accent) 6%, var(--surface));
.glyph { color: var(--accent); }
}
.banner-link {
color: var(--accent);
font-weight: 650;
text-decoration: underline;
text-underline-offset: 2px;
white-space: nowrap;
&:hover { color: var(--accent-strong); }
}
}
.skeleton { border-radius: 16px; height: 172px; @include shimmer; }

View File

@@ -19,6 +19,7 @@
--accent-strong: #3d4dd6;
--accent-2: #0ea5c9;
--danger: #d1344b;
--success: #189a5c;
--code-bg: #f4f6fa;
--shadow-sm: 0 1px 2px rgb(16 24 40 / 6%), 0 1px 3px rgb(16 24 40 / 8%);
--shadow-lg: 0 4px 12px rgb(16 24 40 / 8%), 0 16px 40px -12px rgb(16 24 40 / 18%);
@@ -58,6 +59,7 @@
--accent-strong: #99a5ff;
--accent-2: #38d3ee;
--danger: #f47085;
--success: #3fce8b;
--code-bg: #0c0f16;
--shadow-sm: 0 1px 2px rgb(0 0 0 / 30%);
--shadow-lg: 0 4px 16px rgb(0 0 0 / 40%), 0 24px 48px -16px rgb(0 0 0 / 55%);

View File

@@ -20,6 +20,7 @@
@use 'langs'; // badges, chips, version & language menus
@use 'states'; // empty, banner, skeleton
@use 'admin'; // the accounts dashboard
@use 'proposals'; // the dashboard's edit requests from members
@use 'article'; // article page, ⋮ menu, TOC, recently viewed
@use 'prose'; // rendered markdown, highlight.js, KaTeX
@use 'editor'; // editor, cheatsheet, confirm, fullscreen