mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
design(round 1): accessibility + layout pass (design-only)
Addresses the baseline design review's failing objective gates and layout defects; no backend or flow changes. Accessibility (was the ceiling — 3 of 4 objective gates failed): - Contrast to AA: lifted --text-faint (#6e6e6e→#8a8a8a) and --text-dim; darkened --accent-dim (#7c5cd6→#6a48e0) so white button labels reach 4.5:1; new --accent-bright (#c9b3ff) for accent text on the tinted active background (tree/project active rows, admin badge, invite/ghost buttons all lifted off sub-AA pairings). Same fix applied to the server-rendered auth pages, which now share the app's token values. - Keyboard + focus: file tree, project, and org-name rows are now focusable (tabindex/role + Enter/Space) with a global :focus-visible ring; restored input focus rings on app and auth pages. - Touch targets: every header + sidebar control is a 44x44 hit area on mobile; secondary file actions (History/Upload/Download) collapse under a "⋯ More" menu so the row still fits with zero horizontal overflow. Layout defects: - #meta no longer wraps to 5 lines / shoves the action buttons — single truncating line; #crumb truncates too. - Long tree filenames ellipsize (label span flex:1 min-width:0) instead of hard-clipping. Consistency: - Fixed the .markdown specificity leak: admin/history/onboarding views no longer inherit markdown type rules (content class toggles per view), so the admin type scale renders as declared. - Recolored the off-system gold Admin badge to the accent family; added a 3-step radius token scale; swapped the mojibake-ish ▣/⛛ markers. - Added a global [hidden] guard so explicit display rules can't override the hidden attribute. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
This commit is contained in:
co-authored by
Claude Fable 5
parent
b29454d073
commit
442232bab2
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: beardrive-designer
|
||||
description: Product designer who reviews the BearDrive web app's visual design, UX, and layout. Drives the real UI with Playwright (headless Chromium via Bash + node) across desktop and mobile viewports, inspects computed styles, and reports prioritized design findings with concrete CSS/markup fixes and per-category scores. Use for a design/UX critique of the running web app.
|
||||
tools: Bash, Read, Write, Glob, Grep
|
||||
model: opus
|
||||
---
|
||||
|
||||
You are a senior product designer doing a rigorous design review of the
|
||||
BearDrive web application (a dark-themed, dependency-free file viewer + team
|
||||
admin console, styled after Obsidian). You care about craft: visual
|
||||
hierarchy, consistency, spacing rhythm, typography, color, motion, and — above
|
||||
all — whether the interface communicates clearly and feels considered. You are
|
||||
opinionated but specific: every critique names the element and proposes a fix.
|
||||
|
||||
## What you're reviewing
|
||||
The frontend is hand-written vanilla JS/CSS in
|
||||
`internal/webapp/static/` (`index.html`, `app.js`, `style.css`) plus
|
||||
server-rendered auth pages (`internal/webapp/authlocal.go`, the `authPage`
|
||||
helper) and the public share page shell (`internal/webapp/shares.go`,
|
||||
`sharedMarkdownShell`). Repo root: /Users/snow/workspace/runbear/sfs.
|
||||
|
||||
## How to review
|
||||
Drive the REAL UI with Playwright headless. A ready-to-use install is at the
|
||||
path given in your task prompt (require("playwright") from that directory);
|
||||
Chromium is already downloaded. Write small node scripts, run them with Bash,
|
||||
and **capture screenshots of every distinct screen and state** into your
|
||||
working directory (given in the prompt) with descriptive names.
|
||||
|
||||
- Review at **desktop (1280×800)** and **mobile (390×844)** at minimum; spot-check a narrow tablet width for reflow.
|
||||
- Cover every surface: sign-in / sign-up / verify / awaiting-approval pages, the empty/onboarding state, the file tree + markdown reading view, file history, the ⌘K command palette, the org-admin panel, the hub "Signup & access" settings, the share dialog/modal, and a public `/s/` share page.
|
||||
- Exercise **states**, not just happy paths: hover, keyboard focus rings, active/selected, disabled, empty lists, error toasts, long file names / deep trees (overflow), and a very long document.
|
||||
- Use the browser to read **computed styles** (font sizes, line-height, colors, contrast ratios, spacing, touch-target sizes) — back up claims with numbers, don't just eyeball. Read `style.css` for ground truth on the design system (CSS variables, scale).
|
||||
|
||||
## What to evaluate
|
||||
1. **Visual design** — typographic scale & rhythm, color usage and restraint, contrast (call out WCAG AA failures with ratios), spacing consistency (is there a coherent spacing scale?), border/radius/shadow consistency, iconography coherence (the UI mixes emoji and glyphs — assess), dark-theme execution.
|
||||
2. **UX** — clarity of hierarchy and affordances, discoverability, feedback on actions, error/empty states, cognitive load, progressive disclosure, consistency of interaction patterns (e.g. native prompt()/confirm() vs in-app modals), copy/microcopy quality.
|
||||
3. **Layout** — alignment and grid discipline, whitespace and density, responsive behavior and breakpoints, overflow handling (horizontal scroll is a defect), viewport fit, the sidebar/topbar/content composition.
|
||||
4. **Accessibility** — color contrast, visible focus states, keyboard operability, touch-target sizes (≥44px on mobile), semantic markup / ARIA, labels on icon-only controls.
|
||||
|
||||
## Reporting
|
||||
Return (as your FINAL message — it goes to the orchestrator, not the user) a
|
||||
structured report:
|
||||
|
||||
- **Verdict** — 2-3 sentences: overall design maturity and the single most
|
||||
impactful thing to change.
|
||||
- **Category scores (1–5)** — Visual, UX, Layout, Accessibility, each with a one-line justification.
|
||||
- **Findings** — numbered, ordered by impact. Each: severity
|
||||
(critical / high / medium / polish), the exact element/screen, what's wrong
|
||||
and why it matters, and a concrete fix (name the CSS property, value,
|
||||
selector, or markup change). Cite screenshot filenames.
|
||||
- **What's working** — briefly credit the strong design decisions, so the
|
||||
signal isn't only negative.
|
||||
|
||||
Rules: be concrete and back visual claims with measured values where you can.
|
||||
Distinguish taste from defects — label subjective calls as such. Never edit
|
||||
product code; you review only. If a Playwright script fails, debug it up to
|
||||
twice, then fall back to reading the CSS/markup directly and note the fallback.
|
||||
@@ -543,21 +543,25 @@ func authPage(w http.ResponseWriter, title, body string) {
|
||||
fmt.Fprintf(w, `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"><title>%s — BearDrive</title>
|
||||
<style>
|
||||
body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#1e1e24;color:#ddd;
|
||||
/* Shares the app's token values so sign-in and the app read as one product. */
|
||||
body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#1e1e1e;color:#dadada;
|
||||
display:flex;justify-content:center;padding-top:12vh;margin:0}
|
||||
.card{background:#26262e;border:1px solid #3a3a44;border-radius:10px;padding:28px 32px;width:340px}
|
||||
.card{background:#262626;border:1px solid #363636;border-radius:12px;padding:28px 32px;width:340px}
|
||||
h1{font-size:17px;margin:0 0 16px}
|
||||
label{display:block;font-size:12.5px;color:#aaa;margin:12px 0 4px}
|
||||
input{width:100%%;box-sizing:border-box;padding:8px 10px;border-radius:6px;border:1px solid #3a3a44;
|
||||
background:#1e1e24;color:#ddd;font:inherit}
|
||||
button{margin-top:18px;width:100%%;padding:9px;border:none;border-radius:6px;background:#7c5cff;
|
||||
color:#fff;font:inherit;cursor:pointer}
|
||||
button:hover{background:#8f73ff}
|
||||
.err{color:#ff7b72;font-size:13px;margin:10px 0 0}
|
||||
.msg{color:#7ee787;font-size:13px;margin:10px 0 0}
|
||||
.alt{margin-top:16px;font-size:12.5px;color:#888}
|
||||
.alt a{color:#8f73ff;text-decoration:none}
|
||||
code{background:#1e1e24;padding:2px 6px;border-radius:4px}
|
||||
label{display:block;font-size:12.5px;color:#a6a6a6;margin:12px 0 4px}
|
||||
input{width:100%%;box-sizing:border-box;padding:9px 11px;border-radius:6px;border:1px solid #363636;
|
||||
background:#1e1e1e;color:#dadada;font:inherit}
|
||||
input:focus-visible{outline:2px solid #a882ff;outline-offset:1px;border-color:#a882ff}
|
||||
button{margin-top:18px;width:100%%;padding:10px;border:none;border-radius:6px;background:#6a48e0;
|
||||
color:#fff;font:inherit;font-weight:600;cursor:pointer}
|
||||
button:hover{background:#7c5cd6}
|
||||
button:focus-visible{outline:2px solid #c9b3ff;outline-offset:2px}
|
||||
.err{color:#ff9b91;font-size:13px;margin:10px 0 0}
|
||||
.msg{color:#8ce59a;font-size:13px;margin:10px 0 0}
|
||||
.alt{margin-top:16px;font-size:12.5px;color:#8a8a8a}
|
||||
.alt a{color:#c9b3ff;text-decoration:none}
|
||||
.alt a:hover{text-decoration:underline}
|
||||
code{background:#1e1e1e;padding:2px 6px;border-radius:4px}
|
||||
</style></head><body><div class="card"><h1>%s</h1>%s</div></body></html>`,
|
||||
html.EscapeString(title), html.EscapeString(title), body)
|
||||
}
|
||||
|
||||
@@ -113,8 +113,11 @@ async function loadProjects() {
|
||||
const row = document.createElement("div");
|
||||
row.className = "row" + (currentProject && currentProject.id === p.id ? " active" : "");
|
||||
row.textContent = p.name;
|
||||
row.title = p.id;
|
||||
row.title = p.name;
|
||||
row.tabIndex = 0;
|
||||
row.setAttribute("role", "button");
|
||||
row.onclick = () => selectProject(p, null);
|
||||
row.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); row.click(); } };
|
||||
ul.appendChild(li).appendChild(row);
|
||||
}
|
||||
nav.appendChild(ul);
|
||||
@@ -129,6 +132,7 @@ function selectProject(p, path) {
|
||||
$("crumb").textContent = "";
|
||||
$("meta").textContent = "";
|
||||
$("download").hidden = true;
|
||||
$("content").className = "view";
|
||||
$("content").innerHTML = `<div class="empty">Select a file to read it.<br><span class="empty-hint">On a phone, tap ☰ to browse.</span></div>`;
|
||||
loadProjects(); // refresh active highlight
|
||||
updateOrgBar();
|
||||
@@ -165,6 +169,7 @@ async function acceptInviteFromHash() {
|
||||
and — since any member can — offer to start a new project. */
|
||||
function showEmptyState() {
|
||||
$("orgbar").hidden = true;
|
||||
$("content").className = "view";
|
||||
const auth = serverConfig.auth && serverConfig.auth.enabled;
|
||||
$("content").innerHTML = `
|
||||
<div class="onboard">
|
||||
@@ -243,6 +248,7 @@ function updateOrgBar() {
|
||||
nm.textContent = org.name;
|
||||
nm.title = "Manage organization";
|
||||
nm.onclick = () => showOrgAdmin(org);
|
||||
nm.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); nm.click(); } };
|
||||
const btn = $("invite-btn");
|
||||
btn.hidden = org.role !== "owner";
|
||||
btn.textContent = "Manage";
|
||||
@@ -257,9 +263,10 @@ async function showOrgAdmin(org) {
|
||||
closeSidebarOnMobile();
|
||||
$("crumb").textContent = org.name;
|
||||
$("meta").textContent = "";
|
||||
$("share-btn").hidden = $("history-btn").hidden = $("download").hidden = true;
|
||||
$("share-btn").hidden = $("history-btn").hidden = $("download").hidden = $("more-btn").hidden = true;
|
||||
const owner = org.role === "owner";
|
||||
const box = $("content");
|
||||
box.className = "view";
|
||||
box.innerHTML = `<div class="admin"><h1 id="org-title"></h1></div>`;
|
||||
box.querySelector("#org-title").textContent = org.name + (owner ? "" : " · member");
|
||||
const panel = box.querySelector(".admin");
|
||||
@@ -455,8 +462,9 @@ async function showHubSettings() {
|
||||
try { pol = await getJSON("api/admin/policy"); } catch (e) { toast(e.message, true); return; }
|
||||
currentPath = null; markActive(); closeSidebarOnMobile();
|
||||
$("crumb").textContent = "Signup & access";
|
||||
$("share-btn").hidden = $("history-btn").hidden = $("download").hidden = true;
|
||||
$("share-btn").hidden = $("history-btn").hidden = $("download").hidden = $("more-btn").hidden = true;
|
||||
const box = $("content");
|
||||
box.className = "view";
|
||||
box.innerHTML = `<div class="admin"><h1>Signup & access</h1>
|
||||
<p class="admin-sub">Who can create an account on this hub, and how new accounts are vetted.</p></div>`;
|
||||
const panel = box.querySelector(".admin");
|
||||
@@ -523,8 +531,9 @@ async function showPending() {
|
||||
try { pending = (await getJSON("api/admin/pending")).pending || []; } catch { }
|
||||
currentPath = null; markActive();
|
||||
$("crumb").textContent = "Pending signups";
|
||||
$("share-btn").hidden = $("history-btn").hidden = $("download").hidden = true;
|
||||
$("share-btn").hidden = $("history-btn").hidden = $("download").hidden = $("more-btn").hidden = true;
|
||||
const box = $("content");
|
||||
box.className = "view";
|
||||
box.innerHTML = `<div class="admin"><h1>Pending signups</h1></div>`;
|
||||
const panel = box.querySelector(".admin");
|
||||
const list = el(panel, "div", "admin-list");
|
||||
@@ -590,8 +599,14 @@ function renderNode(n) {
|
||||
chev.className = "chev";
|
||||
chev.textContent = "▾"; // ▾
|
||||
const label = document.createElement("span");
|
||||
label.className = "label";
|
||||
label.textContent = n.name;
|
||||
row.append(chev, label);
|
||||
// Keyboard-operable: the row behaves as a button.
|
||||
row.tabIndex = 0;
|
||||
row.setAttribute("role", "button");
|
||||
row.title = n.name;
|
||||
row.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); row.click(); } };
|
||||
li.appendChild(row);
|
||||
if (n.dir) {
|
||||
if (serverConfig.mode === "hub") {
|
||||
@@ -633,7 +648,9 @@ async function openFile(p) {
|
||||
const dl = $("download");
|
||||
dl.href = apiBase + "download?path=" + encodeURIComponent(p);
|
||||
dl.hidden = false;
|
||||
$("more-btn").hidden = false;
|
||||
const content = $("content");
|
||||
content.className = "markdown"; // document view: markdown type rules apply
|
||||
content.innerHTML = "";
|
||||
$("meta").textContent = "";
|
||||
try {
|
||||
@@ -803,6 +820,8 @@ async function showHistory(q) {
|
||||
$("crumb").textContent = "History — " + title;
|
||||
$("meta").textContent = "";
|
||||
$("download").hidden = true;
|
||||
content.className = "view";
|
||||
$("more-btn").hidden = true;
|
||||
content.innerHTML = "";
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "history";
|
||||
@@ -1137,6 +1156,35 @@ $("palette-overlay").addEventListener("click", (e) => {
|
||||
/* Visible search affordance in the top bar → opens the palette. */
|
||||
$("search-btn").addEventListener("click", paletteOpen);
|
||||
|
||||
/* Mobile "⋯ More": the secondary file actions (History, Upload, Download)
|
||||
collapse behind one 44px button so every target stays tappable without
|
||||
the header overflowing. The menu proxies to the real buttons, so their
|
||||
behavior and visibility rules are the single source of truth. */
|
||||
function buildMoreMenu() {
|
||||
const menu = $("more-menu");
|
||||
menu.innerHTML = "";
|
||||
const items = [
|
||||
["History", $("history-btn")],
|
||||
["Upload", $("upload-btn")],
|
||||
["Download", $("download")],
|
||||
].filter(([, el]) => el && !el.hidden);
|
||||
for (const [label, el] of items) {
|
||||
const b = document.createElement("button");
|
||||
b.className = "more-item";
|
||||
b.textContent = label;
|
||||
b.onclick = () => { $("more-menu").hidden = true; el.click(); };
|
||||
menu.appendChild(b);
|
||||
}
|
||||
return items.length;
|
||||
}
|
||||
$("more-btn").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const menu = $("more-menu");
|
||||
if (menu.hidden) { buildMoreMenu(); menu.hidden = false; }
|
||||
else menu.hidden = true;
|
||||
});
|
||||
document.addEventListener("click", () => { $("more-menu").hidden = true; });
|
||||
|
||||
/* Mobile: the sidebar is off-canvas; a hamburger toggles it. */
|
||||
function toggleSidebar() { document.body.classList.toggle("sb-open"); }
|
||||
function closeSidebarOnMobile() { document.body.classList.remove("sb-open"); }
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<nav id="projects" aria-label="Projects" hidden></nav>
|
||||
<nav id="tree" aria-label="Files"></nav>
|
||||
<footer id="orgbar" hidden>
|
||||
<span id="org-name" title="Manage organization"></span>
|
||||
<span id="org-name" title="Manage organization" role="button" tabindex="0"></span>
|
||||
<button id="invite-btn" hidden title="Manage this organization">Manage</button>
|
||||
</footer>
|
||||
</aside>
|
||||
@@ -36,6 +36,8 @@
|
||||
<button id="upload-btn" class="btn" hidden>Upload</button>
|
||||
<input id="upload-input" type="file" hidden>
|
||||
<a id="download" class="btn" hidden download>Download</a>
|
||||
<button id="more-btn" class="btn icon-only" hidden title="More actions" aria-label="More actions">⋯</button>
|
||||
<div id="more-menu" hidden role="menu"></div>
|
||||
</header>
|
||||
<article id="content" class="markdown">
|
||||
<div class="empty">Select a file to read it.</div>
|
||||
|
||||
@@ -6,15 +6,23 @@
|
||||
--bg-active: #3f3550;
|
||||
--border: #363636;
|
||||
--text: #dadada;
|
||||
--text-dim: #9a9a9a;
|
||||
--text-faint: #6e6e6e;
|
||||
--text-dim: #a6a6a6; /* AA on --bg and --bg-side */
|
||||
--text-faint: #8a8a8a; /* raised from #6e6e6e → ~4.7:1 on --bg */
|
||||
--accent: #a882ff;
|
||||
--accent-dim: #7c5cd6;
|
||||
--accent-bright: #c9b3ff; /* accent text on the tinted --bg-active (AA) */
|
||||
--accent-dim: #6a48e0; /* darker so white labels reach 4.5:1 */
|
||||
--code-bg: #2a2a2a;
|
||||
/* radius scale: controls / cards / overlays */
|
||||
--r-ctl: 6px;
|
||||
--r-card: 8px;
|
||||
--r-over: 12px;
|
||||
}
|
||||
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; } /* explicit display rules must not override the hidden attribute */
|
||||
html, body { height: 100%; margin: 0; }
|
||||
#more-btn { display: none; } /* mobile-only; shown in the responsive block */
|
||||
body {
|
||||
display: flex;
|
||||
background: var(--bg);
|
||||
@@ -66,9 +74,10 @@ body {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#projects .row::before { content: "▣ "; color: var(--text-faint); }
|
||||
#projects .row::before { content: "◇"; margin-right: 7px; color: var(--text-faint); }
|
||||
#projects .row:hover { background: var(--bg-hover); color: var(--text); }
|
||||
#projects .row.active { background: var(--bg-active); color: var(--accent); }
|
||||
#projects .row.active { background: var(--bg-active); color: var(--accent-bright); box-shadow: inset 2px 0 0 var(--accent); }
|
||||
#projects .row.active::before { color: var(--accent); }
|
||||
#tree { flex: 1; overflow-y: auto; padding: 8px 6px 24px; font-size: 13.5px; }
|
||||
#orgbar {
|
||||
display: flex;
|
||||
@@ -82,14 +91,14 @@ body {
|
||||
}
|
||||
#orgbar[hidden] { display: none; }
|
||||
#org-name { cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
#org-name::before { content: "⛛ "; color: var(--text-faint); }
|
||||
#org-name::before { content: "◈"; margin-right: 7px; color: var(--text-faint); }
|
||||
#org-name:hover { color: var(--text); }
|
||||
#invite-btn {
|
||||
border: none; cursor: pointer; font: inherit; font-size: 11.5px;
|
||||
padding: 2px 10px; border-radius: 5px;
|
||||
background: var(--bg-active); color: var(--text-dim);
|
||||
padding: 3px 10px; border-radius: var(--r-ctl);
|
||||
background: var(--bg-active); color: var(--text);
|
||||
}
|
||||
#invite-btn:hover { color: var(--text); background: var(--bg-hover); }
|
||||
#invite-btn:hover { color: #fff; background: var(--bg-hover); }
|
||||
#tree ul { list-style: none; margin: 0; padding-left: 14px; }
|
||||
#tree > ul { padding-left: 4px; }
|
||||
#tree li > .row {
|
||||
@@ -105,7 +114,8 @@ body {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#tree li > .row:hover { background: var(--bg-hover); color: var(--text); }
|
||||
#tree li > .row.active { background: var(--bg-active); color: var(--accent); }
|
||||
#tree li > .row.active { background: var(--bg-active); color: var(--accent-bright); box-shadow: inset 2px 0 0 var(--accent); }
|
||||
#tree li > .row .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
#tree .chev { width: 12px; flex: none; font-size: 10px; color: var(--text-faint); transition: transform .12s; }
|
||||
#tree li.collapsed > ul { display: none; }
|
||||
#tree li.collapsed > .row .chev { transform: rotate(-90deg); }
|
||||
@@ -114,6 +124,7 @@ body {
|
||||
/* ---- main pane ---- */
|
||||
#main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
#topbar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@@ -121,21 +132,24 @@ body {
|
||||
border-bottom: 1px solid var(--border);
|
||||
min-height: 46px;
|
||||
}
|
||||
#crumb { font-size: 13px; color: var(--text-dim); }
|
||||
#meta { flex: 1; font-size: 12px; color: var(--text-faint); text-align: right; }
|
||||
#crumb { font-size: 13px; color: var(--text); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
/* Single truncating line — never let author/time metadata wrap and shove
|
||||
the action buttons around. */
|
||||
#meta { flex: 1; min-width: 0; font-size: 12px; color: var(--text-faint); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.btn {
|
||||
flex: none;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
padding: 5px 12px;
|
||||
border-radius: var(--r-ctl);
|
||||
background: var(--accent-dim);
|
||||
color: #fff;
|
||||
font-size: 12.5px;
|
||||
text-decoration: none;
|
||||
border: none; cursor: pointer; font-family: inherit;
|
||||
}
|
||||
.btn:hover { background: var(--accent); }
|
||||
#content { flex: 1; overflow-y: auto; padding: 28px 48px 80px; }
|
||||
#history-btn, #share-btn { border: none; cursor: pointer; font: inherit; font-size: 12.5px; background: var(--bg-active); color: var(--text-dim); }
|
||||
#history-btn:hover, #share-btn:hover { color: var(--text); background: var(--bg-hover); }
|
||||
#history-btn, #share-btn { border: none; cursor: pointer; font: inherit; font-size: 12.5px; background: var(--bg-active); color: var(--text); }
|
||||
#history-btn:hover, #share-btn:hover { color: #fff; background: var(--bg-hover); }
|
||||
.dir-history { margin-left: auto; padding: 0 4px; visibility: hidden; color: var(--text-faint); font-size: 11px; }
|
||||
#tree li > .row:hover .dir-history { visibility: visible; }
|
||||
.dir-history:hover { color: var(--text); }
|
||||
@@ -223,8 +237,8 @@ body {
|
||||
|
||||
/* ---- vault header actions ---- */
|
||||
.vault-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.adminbar { border: none; cursor: pointer; font: inherit; font-size: 11.5px; padding: 2px 8px; border-radius: 5px; background: #4a3a12; color: #f5c451; }
|
||||
.adminbar:hover { background: #5c4816; }
|
||||
.adminbar { border: none; cursor: pointer; font: inherit; font-size: 11.5px; padding: 3px 9px; border-radius: var(--r-ctl); background: var(--bg-active); color: var(--accent-bright); }
|
||||
.adminbar:hover { background: var(--bg-hover); color: #fff; }
|
||||
.icon-btn2 { border: none; background: transparent; color: var(--text-faint); font-size: 15px; cursor: pointer; padding: 0 2px; line-height: 1; }
|
||||
.icon-btn2:hover { color: var(--text); }
|
||||
|
||||
@@ -233,8 +247,8 @@ body {
|
||||
.icon-btn:hover { color: var(--text); background: var(--bg-hover); }
|
||||
#search-btn { display: inline-flex; align-items: center; gap: 6px; }
|
||||
#search-btn kbd { font: 11px var(--mono, ui-monospace, Menlo, monospace); background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 0 5px; color: var(--text-faint); }
|
||||
.btn.ghost { background: var(--bg-active); color: var(--text-dim); }
|
||||
.btn.ghost:hover { background: var(--bg-hover); color: var(--text); }
|
||||
.btn.ghost { background: var(--bg-active); color: var(--text); }
|
||||
.btn.ghost:hover { background: var(--bg-hover); color: #fff; }
|
||||
|
||||
/* ---- onboarding empty state ---- */
|
||||
.onboard { max-width: 560px; margin: 8vh auto 0; }
|
||||
@@ -296,25 +310,50 @@ body {
|
||||
#sidebar { position: fixed; z-index: 60; top: 0; left: 0; height: 100%; transform: translateX(-100%); transition: transform .2s ease; box-shadow: 0 0 40px rgba(0,0,0,.5); }
|
||||
body.sb-open #sidebar { transform: translateX(0); }
|
||||
body.sb-open #sb-backdrop { display: block; position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 50; }
|
||||
.icon-btn { display: inline-block; }
|
||||
.icon-btn { display: inline-flex; }
|
||||
#content { padding: 20px 18px 60px; }
|
||||
#topbar { padding: 8px 14px; gap: 8px; }
|
||||
#topbar { padding: 6px 10px; gap: 4px; }
|
||||
#crumb { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
#search-btn kbd { display: none; }
|
||||
/* On a phone the header shrinks every control to an icon so all the
|
||||
per-file actions stay reachable (the R2 mistake was hiding them). */
|
||||
#topbar .btn { font-size: 0; gap: 0; padding: 5px 8px; }
|
||||
#search-btn::before { content: "🔍"; font-size: 15px; }
|
||||
#share-btn::before { content: "↗"; font-size: 15px; }
|
||||
#history-btn::before { content: "🕘"; font-size: 14px; }
|
||||
#upload-btn::before { content: "⬆"; font-size: 14px; }
|
||||
#download::before { content: "⬇"; font-size: 14px; }
|
||||
/* Every header control is a 44×44 hit area (WCAG target size). Only the
|
||||
primary actions (menu, search, share) stay in the bar; History, Upload
|
||||
and Download collapse under "⋯ More" so the row never overflows. */
|
||||
#topbar .btn, #topbar .icon-btn {
|
||||
min-width: 44px; min-height: 44px; padding: 0;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 0; gap: 0;
|
||||
}
|
||||
#menu-btn { font-size: 20px; }
|
||||
#search-btn::before { content: "🔍"; font-size: 16px; }
|
||||
#share-btn::before { content: "↗"; font-size: 17px; }
|
||||
#more-btn:not([hidden]) { display: inline-flex; font-size: 20px; }
|
||||
#history-btn, #upload-btn, #download { display: none !important; }
|
||||
#meta { display: none; }
|
||||
/* Sidebar header controls also meet the 44px target on touch. */
|
||||
.icon-btn2, #signout, .adminbar { min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
|
||||
#vault { padding: 8px 12px; }
|
||||
.markdown, .admin, .onboard, .history { max-width: 100%; }
|
||||
.markdown table, pre.plain { display: block; overflow-x: auto; max-width: 100%; }
|
||||
.ob-row { flex-direction: column; }
|
||||
}
|
||||
|
||||
/* ---- "More" overflow menu (mobile) ---- */
|
||||
#more-btn.icon-only { font-size: 15px; }
|
||||
#more-menu {
|
||||
position: absolute; right: 10px; top: 100%; z-index: 80;
|
||||
background: var(--bg-side); border: 1px solid var(--border);
|
||||
border-radius: var(--r-card); box-shadow: 0 12px 32px rgba(0,0,0,.5);
|
||||
padding: 6px; min-width: 160px;
|
||||
}
|
||||
#more-menu[hidden] { display: none; }
|
||||
.more-item {
|
||||
display: block; width: 100%; text-align: left;
|
||||
min-height: 44px; padding: 0 14px;
|
||||
background: transparent; border: none; cursor: pointer;
|
||||
color: var(--text); font: inherit; font-size: 14px; border-radius: var(--r-ctl);
|
||||
}
|
||||
.more-item:hover { background: var(--bg-hover); }
|
||||
|
||||
/* ---- markdown ---- */
|
||||
.markdown { max-width: 820px; }
|
||||
.markdown h1, .markdown h2, .markdown h3, .markdown h4 {
|
||||
|
||||
Reference in New Issue
Block a user