Merge pull request #5 from runbear-io/redesign/linear-viewer

redesign(web): Linear-grade viewer/admin + file-tree UX
This commit is contained in:
Snow W. Lee (Sungwon)
2026-07-09 13:41:45 -07:00
committed by GitHub
5 changed files with 476 additions and 429 deletions
+27 -21
View File
@@ -544,25 +544,31 @@ func authPage(w http.ResponseWriter, title, body string) {
<meta name="viewport" content="width=device-width, initial-scale=1"><title>%s — BearDrive</title>
<style>
/* 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:#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:#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:#969696}
.alt a{color:#c9b3ff;text-decoration:none}
body{font:14px/1.5 -apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",sans-serif;
background:#0a0b0d;color:#eef0f3;display:flex;justify-content:center;padding-top:13vh;margin:0;
letter-spacing:-.006em;-webkit-font-smoothing:antialiased}
.card{background:#0c0e10;border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:28px 30px;width:344px;
box-shadow:0 24px 70px -24px rgba(0,0,0,.7)}
.logo{width:34px;height:34px;border-radius:9px;display:grid;place-items:center;font-size:19px;line-height:1;
background:linear-gradient(160deg,#ffcf85,#f5a623 55%%,#d3861a);
box-shadow:0 1px 0 rgba(255,255,255,.35) inset,0 3px 10px -2px rgba(245,166,35,.45);margin-bottom:16px}
h1{font-size:18px;font-weight:640;letter-spacing:-.02em;margin:0 0 18px}
label{display:block;font-size:12px;color:#9aa0a9;margin:14px 0 5px;font-weight:500}
input{width:100%%;box-sizing:border-box;height:38px;padding:0 12px;border-radius:8px;
border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.03);color:#eef0f3;font:inherit;font-size:14px;outline:none}
input:focus-visible{outline:2px solid #f5a623;outline-offset:1px;border-color:#f5a623}
button{margin-top:20px;width:100%%;height:40px;border:none;border-radius:8px;background:#f5a623;
color:#241704;font:inherit;font-size:14px;font-weight:600;cursor:pointer}
button:hover{background:#ffcf85}
button:focus-visible{outline:2px solid #ffcf85;outline-offset:2px}
.err{color:#ff9b91;font-size:13px;margin:12px 0 0}
.msg{color:#6fd699;font-size:13px;margin:12px 0 0}
.alt{margin-top:16px;font-size:12.5px;color:#868b93}
.alt a{color:#ffcf85;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>`,
code{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08);padding:2px 6px;border-radius:5px;
font-family:ui-monospace,Menlo,monospace}
</style></head><body><div class="card"><div class="logo">🐻</div><h1>%s</h1>%s</div></body></html>`,
html.EscapeString(title), html.EscapeString(title), body)
}
@@ -598,13 +604,13 @@ func (a *BuiltinAuth) pageLogin(w http.ResponseWriter, r *http.Request) {
if a.AllowSignup {
note := ""
if len(a.AllowedDomains) > 0 {
note = ` <span style="color:#969696">(` + html.EscapeString(a.domainList()) + ` only)</span>`
note = ` <span style="color:#868b93">(` + html.EscapeString(a.domainList()) + ` only)</span>`
}
signup = fmt.Sprintf(`<p class="alt">No account? <a href="/auth/signup?next=%s">Sign up</a>%s</p>`, url.QueryEscape(next), note)
}
brand := ""
if a.Brand != "" {
brand = `<p class="alt" style="margin:0 0 14px;color:#aaa">` + html.EscapeString(a.Brand) + `</p>`
brand = `<p class="alt" style="margin:0 0 14px;color:#9aa0a9">` + html.EscapeString(a.Brand) + `</p>`
}
authPage(w, "Sign in", brand+inviteBanner(next)+fmt.Sprintf(`<form method="post" action="/auth/login?next=%s">%s%s%s<button>Sign in</button></form>
%s<p class="alt"><a href="/auth/reset">Forgot password?</a></p>`,
@@ -653,7 +659,7 @@ func (a *BuiltinAuth) pageSignup(w http.ResponseWriter, r *http.Request) {
}
brand := ""
if a.Brand != "" {
brand = `<p class="alt" style="margin:0 0 14px;color:#aaa">` + html.EscapeString(a.Brand) + `</p>`
brand = `<p class="alt" style="margin:0 0 14px;color:#9aa0a9">` + html.EscapeString(a.Brand) + `</p>`
}
authPage(w, "Create account", brand+inviteBanner(next)+fmt.Sprintf(`<form method="post" action="/auth/signup?next=%s">%s%s%s%s%s<button>Sign up</button></form>
<p class="alt">Have an account? <a href="/auth/login?next=%s">Sign in</a></p>`,
+6 -5
View File
@@ -329,11 +329,12 @@ func (s *Server) handleShared(w http.ResponseWriter, r *http.Request) {
const sharedMarkdownShell = `<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"><title>%s</title>
<style>
body{font:16px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#24292f;
max-width:760px;margin:0 auto;padding:48px 24px 96px}
@media (prefers-color-scheme: dark){body{background:#1e1e24;color:#ddd}
a{color:#8f73ff}code,pre{background:#26262e}}
h1,h2,h3{line-height:1.25}
body{font:16px/1.7 -apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",sans-serif;color:#24292f;
max-width:720px;margin:0 auto;padding:52px 24px 96px}
a{color:#b26a00}
@media (prefers-color-scheme: dark){body{background:#0a0b0d;color:#c6cbd3}
a{color:#ffcf85}code,pre{background:#15171b}h1,h2,h3{color:#f4f6f9}}
h1,h2,h3{line-height:1.25;letter-spacing:-.018em}
pre{padding:12px;border-radius:8px;overflow-x:auto;background:#f6f8fa}
code{background:#f6f8fa;padding:2px 5px;border-radius:4px;font-size:.9em}
pre code{padding:0;background:none}
+88 -24
View File
@@ -4,7 +4,8 @@
const $ = (id) => document.getElementById(id);
let flatFiles = []; // [{path, name}] for wikilink resolution
let currentPath = null;
let collapsed = new Set(); // dir paths the user collapsed
let expanded = new Set(); // dir paths that are open (folders start closed)
let treeFirstLoad = true; // apply the "lone root folder opens" rule once per project
const MD_EXT = /\.(md|markdown)$/i;
const IMG_EXT = /\.(png|jpe?g|gif|svg|webp|ico|bmp|avif)$/i;
@@ -22,6 +23,18 @@ let joinedOrgId = null; // org just joined via an invite this page-load
const fileURL = (p) => apiBase + "file?path=" + encodeURIComponent(p);
/* inline SVG icon, pulled from the <symbol> sprite in index.html */
function svgIcon(name) { return `<svg class="ico" aria-hidden="true"><use href="#i-${name}"/></svg>`; }
/* Deterministic accent for a project's letter-mark, so each project keeps a
stable color across reloads without any server state. */
const PROJ_COLORS = ["#5b8def", "#f5a623", "#4cc38a", "#e0679b", "#8b7bf0", "#3ec8c8", "#e6934a"];
function projColor(s) {
let h = 0;
for (const c of s) h = (h * 31 + c.charCodeAt(0)) >>> 0;
return PROJ_COLORS[h % PROJ_COLORS.length];
}
async function getJSON(url) {
const r = await fetch(url);
if (r.status === 401) { // auth required: sign in, then come back here
@@ -112,7 +125,14 @@ async function loadProjects() {
const li = document.createElement("li");
const row = document.createElement("div");
row.className = "row" + (currentProject && currentProject.id === p.id ? " active" : "");
row.textContent = p.name;
const mark = document.createElement("span");
mark.className = "proj-mark";
mark.style.background = projColor(p.name);
mark.textContent = (p.name.trim()[0] || "?");
const lab = document.createElement("span");
lab.className = "label";
lab.textContent = p.name;
row.append(mark, lab);
row.title = p.name;
row.tabIndex = 0;
row.setAttribute("role", "button");
@@ -125,6 +145,8 @@ async function loadProjects() {
function selectProject(p, path) {
currentProject = p;
expanded = new Set(); // fresh collapse state for the new project's tree
treeFirstLoad = true;
apiBase = "api/p/" + p.id + "/";
$("vault-name").textContent = p.name;
document.title = p.name + " — BearDrive";
@@ -507,7 +529,7 @@ async function updateAdminBar() {
let pending = [];
try { pending = (await getJSON("api/admin/pending")).pending || []; } catch { }
bar.hidden = false;
bar.textContent = pending.length ? "⚑ Admin · " + pending.length : "⚙ Admin";
bar.innerHTML = svgIcon("shield") + `<span>Admin${pending.length ? " · " + pending.length : ""}</span>`;
bar.title = "Hub administration — signup policy" + (pending.length ? " and pending approvals" : "");
bar.onclick = () => showHubSettings();
}
@@ -635,9 +657,17 @@ async function refreshTree() {
root = await getJSON(apiBase + "tree");
} catch { return; } // keep the last good tree
flatFiles = [];
const kids = root.children || [];
// First render of a project's tree: every folder starts closed, except a
// lone root folder — opening it spares the user a single shut folder.
if (treeFirstLoad) {
treeFirstLoad = false;
const rootDirs = kids.filter((c) => c.dir);
if (rootDirs.length === 1) expanded.add(rootDirs[0].path);
}
const nav = $("tree");
nav.innerHTML = "";
nav.appendChild(renderChildren(root.children || []));
nav.appendChild(renderChildren(kids));
markActive();
}
@@ -655,11 +685,14 @@ function renderNode(n) {
row.dataset.path = n.path;
const chev = document.createElement("span");
chev.className = "chev";
chev.textContent = "▾"; // ▾
chev.innerHTML = svgIcon("chevd");
const ticon = document.createElement("span");
ticon.className = "ticon";
ticon.innerHTML = svgIcon(n.dir ? "folder" : "doc");
const label = document.createElement("span");
label.className = "label";
label.textContent = n.name;
row.append(chev, label);
row.append(chev, ticon, label);
// Keyboard-operable: the row behaves as a button.
row.tabIndex = 0;
row.setAttribute("role", "button");
@@ -670,18 +703,19 @@ function renderNode(n) {
if (serverConfig.mode === "hub") {
const hist = document.createElement("span");
hist.className = "dir-history";
hist.textContent = "⌚";
hist.innerHTML = svgIcon("hist");
hist.title = "Folder history";
hist.onclick = (e) => { e.stopPropagation(); showHistory({ prefix: n.path + "/" }); };
row.appendChild(hist);
}
li.appendChild(renderChildren(n.children || []));
if (collapsed.has(n.path)) li.classList.add("collapsed");
row.setAttribute("aria-expanded", String(!collapsed.has(n.path)));
const open = expanded.has(n.path);
if (!open) li.classList.add("collapsed");
row.setAttribute("aria-expanded", String(open));
row.onclick = () => {
li.classList.toggle("collapsed");
const isCollapsed = li.classList.contains("collapsed");
isCollapsed ? collapsed.add(n.path) : collapsed.delete(n.path);
isCollapsed ? expanded.delete(n.path) : expanded.add(n.path);
row.setAttribute("aria-expanded", String(!isCollapsed));
};
} else {
@@ -698,11 +732,41 @@ function markActive() {
if (row) row.classList.add("active");
}
/* Mark every ancestor folder of a path as open. */
function expandTo(filePath) {
const parts = filePath.split("/");
let acc = "";
for (let i = 0; i < parts.length - 1; i++) {
acc = acc ? acc + "/" + parts[i] : parts[i];
expanded.add(acc);
}
}
/* Reflect the `expanded` set onto the already-rendered tree DOM. */
function applyTreeExpansion() {
for (const li of document.querySelectorAll("#tree li.dir")) {
const row = li.querySelector(":scope > .row");
const open = row && expanded.has(row.dataset.path);
li.classList.toggle("collapsed", !open);
if (row) row.setAttribute("aria-expanded", String(!!open));
}
}
/* Opening a file (search, wikilink, deep link) unfolds the path to it and
scrolls the row into view so the selection is never hidden in a shut folder. */
function revealInTree(p) {
expandTo(p);
applyTreeExpansion();
const row = document.querySelector(`#tree .row[data-path="${CSS.escape(p)}"]`);
if (row) row.scrollIntoView({ block: "center" });
}
/* ---- file pane ---- */
async function openFile(p) {
currentPath = p;
setHash(p);
markActive();
revealInTree(p);
closeSidebarOnMobile();
$("crumb").textContent = p.split("/").join(" / ");
updateShareButton();
@@ -793,7 +857,7 @@ function showShareDialog(url, copied) {
back.className = "modal-back";
back.innerHTML = `
<div class="modal">
<h3>🔗 Public link created</h3>
<h3>Public link created</h3>
<p><b>Anyone with this link can view this file</b> no account needed. It always shows the latest version until you revoke it.</p>
<div class="modal-url"></div>
<div class="modal-actions">
@@ -898,7 +962,7 @@ async function showHistory(q) {
row.innerHTML =
`<div class="hline"><span class="hkind"></span><span class="hpath"></span><span class="htime"></span></div>` +
`<div class="hmeta"><span class="hwho"></span><span class="hdev"></span><span class="hsize"></span><span class="hact"></span></div>`;
row.querySelector(".hkind").textContent = e.kind === "delete" ? "" : "●";
row.querySelector(".hkind").innerHTML = svgIcon(e.kind === "delete" ? "x" : "dot");
row.querySelector(".hpath").textContent = e.path;
row.querySelector(".htime").textContent = when;
row.querySelector(".hwho").textContent = who;
@@ -1101,28 +1165,28 @@ function paletteCandidates() {
const hub = serverConfig.mode === "hub";
if (hub && currentProject && currentPath) {
add("", "Share: " + currentPath, "action", () => $("share-btn").click());
add("", "History: " + currentPath, "action", () => showHistory({ path: currentPath }));
add("", "Download: " + currentPath, "action", () => $("download").click());
add("share", "Share: " + currentPath, "action", () => $("share-btn").click());
add("hist", "History: " + currentPath, "action", () => showHistory({ path: currentPath }));
add("download", "Download: " + currentPath, "action", () => $("download").click());
}
if (hub && currentProject) {
add("", "History: whole project", "action", () => showHistory({ prefix: "" }));
add("hist", "History: whole project", "action", () => showHistory({ prefix: "" }));
}
if (!$("upload-btn").hidden) {
add("+", "Upload a file…", "action", () => $("upload-btn").click());
add("upload", "Upload a file…", "action", () => $("upload-btn").click());
}
if (hub) {
for (const p of projects) {
if (!currentProject || p.id !== currentProject.id) {
add("", "Switch to project: " + p.name, "project", () => selectProject(p, null));
add("folder", "Switch to project: " + p.name, "project", () => selectProject(p, null));
}
}
}
if (serverConfig.auth && serverConfig.auth.enabled) {
add("", "Sign out", "action", () => { location.href = "/auth/logout"; });
add("power", "Sign out", "action", () => { location.href = "/auth/logout"; });
}
for (const f of flatFiles) {
add("", f.path, "file", () => openFile(f.path));
add("doc", f.path, "file", () => openFile(f.path));
}
return items;
}
@@ -1166,13 +1230,13 @@ function renderPalette() {
paletteItems.forEach((item, i) => {
const li = document.createElement("li");
if (i === paletteSel) li.classList.add("selected");
const icon = document.createElement("span");
icon.className = "picon";
icon.textContent = item.icon;
const pic = document.createElement("span");
pic.className = "picon";
pic.innerHTML = svgIcon(item.icon);
const kind = document.createElement("span");
kind.className = "pkind";
kind.textContent = item.kind;
li.append(icon, highlight(item.label, item.hits), kind);
li.append(pic, highlight(item.label, item.hits), kind);
li.onclick = () => runPaletteItem(item);
li.onmousemove = () => { if (paletteSel !== i) { paletteSel = i; renderPalette(); } };
ul.appendChild(li);
+45 -12
View File
@@ -5,17 +5,47 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BearDrive</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>&#128193;</text></svg>">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>&#128059;</text></svg>">
</head>
<body>
<svg width="0" height="0" class="sprite" aria-hidden="true" focusable="false">
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></symbol>
<symbol id="i-chevd" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
<symbol id="i-folder" viewBox="0 0 24 24"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></symbol>
<symbol id="i-doc" viewBox="0 0 24 24"><path d="M14 3v5h5"/><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.2-3.2"/></symbol>
<symbol id="i-share" viewBox="0 0 24 24"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 10.5 6.8-4M8.6 13.5l6.8 4"/></symbol>
<symbol id="i-hist" viewBox="0 0 24 24"><path d="M3 3v6h6"/><path d="M3.5 9a9 9 0 1 0 2-4"/><path d="M12 8v4l3 2"/></symbol>
<symbol id="i-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
<symbol id="i-gear" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 13a7.6 7.6 0 0 0 0-2l1.7-1.3-1.7-3-2 .8a7.6 7.6 0 0 0-1.7-1l-.3-2.1H10l-.3 2.1a7.6 7.6 0 0 0-1.7 1l-2-.8-1.7 3L6 11a7.6 7.6 0 0 0 0 2l-1.7 1.3 1.7 3 2-.8a7.6 7.6 0 0 0 1.7 1l.3 2.1h3.4l.3-2.1a7.6 7.6 0 0 0 1.7-1l2 .8 1.7-3z"/></symbol>
<symbol id="i-users" viewBox="0 0 24 24"><path d="M16 20v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="3.2"/><path d="M22 20v-2a4 4 0 0 0-3-3.8"/><path d="M16 3.5a4 4 0 0 1 0 7"/></symbol>
<symbol id="i-link" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7 0l2-2a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-2 2a5 5 0 0 0 7 7l1-1"/></symbol>
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 5 6v5c0 4.5 3 7.5 7 9 4-1.5 7-4.5 7-9V6z"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><path d="m5 13 4 4L19 7"/></symbol>
<symbol id="i-x" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></symbol>
<symbol id="i-trash" viewBox="0 0 24 24"><path d="M4 7h16M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2m2 0v12a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V7"/></symbol>
<symbol id="i-lock" viewBox="0 0 24 24"><rect x="4.5" y="10" width="15" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></symbol>
<symbol id="i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v4l3 2"/></symbol>
<symbol id="i-globe" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 3a15 15 0 0 0 0 18"/></symbol>
<symbol id="i-copy" viewBox="0 0 24 24"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h8"/></symbol>
<symbol id="i-alert" viewBox="0 0 24 24"><path d="M12 9v4M12 17h.01"/><path d="M10.3 4 3 17a2 2 0 0 0 1.7 3h14.6a2 2 0 0 0 1.7-3L13.7 4a2 2 0 0 0-3.4 0z"/></symbol>
<symbol id="i-power" viewBox="0 0 24 24"><path d="M12 4v8M7.5 7a7 7 0 1 0 9 0"/></symbol>
<symbol id="i-download" viewBox="0 0 24 24"><path d="M12 4v11m0 0 4-4m-4 4-4-4M5 19h14"/></symbol>
<symbol id="i-upload" viewBox="0 0 24 24"><path d="M12 20V9m0 0 4 4m-4-4-4 4M5 5h14"/></symbol>
<symbol id="i-dot" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4.5" fill="currentColor" stroke="none"/></symbol>
<symbol id="i-enter" viewBox="0 0 24 24"><path d="M9 10 4 15l5 5"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/></symbol>
<symbol id="i-menu" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"/></symbol>
<symbol id="i-dots" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.4" fill="currentColor" stroke="none"/></symbol>
</svg>
<div id="sb-backdrop"></div>
<aside id="sidebar">
<header id="vault">
<span id="vault-badge" aria-hidden="true">🐻</span>
<span id="vault-name"></span>
<div class="vault-actions">
<button id="adminbar" class="adminbar" hidden></button>
<button id="settings-btn" class="icon-btn2" hidden title="Manage organization"></button>
<a id="signout" href="/auth/logout" hidden title="Sign out" aria-label="Sign out"></a>
<button id="settings-btn" class="icon-btn2" hidden title="Manage organization" aria-label="Manage organization"><svg class="ico"><use href="#i-users"/></svg></button>
<a id="signout" href="/auth/logout" hidden title="Sign out" aria-label="Sign out"><svg class="ico"><use href="#i-power"/></svg></a>
</div>
</header>
<nav id="projects" aria-label="Projects" hidden></nav>
@@ -27,16 +57,16 @@
</aside>
<main id="main">
<header id="topbar">
<button id="menu-btn" class="icon-btn" title="Menu" aria-label="Menu"></button>
<button id="menu-btn" class="icon-btn" title="Menu" aria-label="Menu"><svg class="ico"><use href="#i-menu"/></svg></button>
<span id="crumb"></span>
<span id="meta"></span>
<button id="search-btn" class="btn ghost" title="Search (⌘K)">🔍 Search <kbd>⌘K</kbd></button>
<button id="share-btn" class="btn" hidden>Share</button>
<button id="history-btn" class="btn" hidden>History</button>
<button id="upload-btn" class="btn" hidden>Upload</button>
<button id="search-btn" class="btn ghost" title="Search (⌘K)"><svg class="ico"><use href="#i-search"/></svg> <span class="lbl">Search</span> <kbd>⌘K</kbd></button>
<button id="share-btn" class="btn" hidden><svg class="ico"><use href="#i-share"/></svg> <span class="lbl">Share</span></button>
<button id="history-btn" class="btn" hidden><svg class="ico"><use href="#i-hist"/></svg> <span class="lbl">History</span></button>
<button id="upload-btn" class="btn" hidden><svg class="ico"><use href="#i-upload"/></svg> <span class="lbl">Upload</span></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>
<a id="download" class="btn" hidden download><svg class="ico"><use href="#i-download"/></svg> <span class="lbl">Download</span></a>
<button id="more-btn" class="btn icon-only" hidden title="More actions" aria-label="More actions"><svg class="ico"><use href="#i-dots"/></svg></button>
<div id="more-menu" hidden role="menu"></div>
</header>
<article id="content" class="markdown">
@@ -45,8 +75,11 @@
</main>
<div id="palette-overlay" hidden>
<div id="palette" role="dialog" aria-label="Search and quick actions">
<input id="palette-input" type="text" placeholder="Search file names, projects, actions…"
autocomplete="off" spellcheck="false">
<div id="palette-inputwrap">
<svg class="ico"><use href="#i-search"/></svg>
<input id="palette-input" type="text" placeholder="Search file names, projects, actions…"
autocomplete="off" spellcheck="false">
</div>
<ul id="palette-results"></ul>
<footer id="palette-hint">↑↓ navigate · ⏎ select · esc close</footer>
</div>
+310 -367
View File
@@ -1,428 +1,371 @@
/* Obsidian-inspired dark theme */
/* BearDrive viewer near-black, hairline-crafted, honey accent.
Design language: Linear-grade restraint; the one accent (honey) is spent
only on active state, the live/brand mark, and the palette selection. */
:root {
--bg: #1e1e1e;
--bg-side: #262626;
--bg-hover: #333333;
--bg-active: #3f3550;
--border: #363636;
--text: #dadada;
--text-dim: #a6a6a6; /* AA on --bg and --bg-side */
--text-faint: #969696; /* AA on both --bg (5.0) and --bg-side #262626 (4.6) */
--accent: #a882ff;
--accent-bright: #c9b3ff; /* accent text on the tinted --bg-active (AA) */
--accent-dim: #6a48e0; /* darker so white labels reach 4.5:1 */
--accent-press: #5a3bc9; /* button hover/press — white stays ≥4.5:1 */
--code-bg: #2a2a2a;
/* radius scale: controls / cards / overlays */
--r-ctl: 6px;
--r-card: 8px;
--r-over: 12px;
--bg: #0a0b0d; /* main ground */
--bg-side: #0c0e10; /* sidebar / panels */
--bg-raise: #15171b; /* palette, modals, elevated cards */
--surface: rgba(255,255,255,.03); /* inputs, ghost buttons */
--hover: rgba(255,255,255,.06); /* row / control hover */
--border: rgba(255,255,255,.07);
--border-2: rgba(255,255,255,.11);
--code-bg: #0d0f12;
--text: #eef0f3;
--text-dim: #9aa0a9; /* AA on both grounds */
--text-faint: #868b93; /* ≥4.5:1 on near-black */
--text-ghost: #666b74; /* decorative icons only */
--accent: #f5a623; /* honey */
--accent-bright: #ffcf85; /* honey text / primary hover */
--accent-dim: #d3861a;
--accent-press: #e0951a;
--glow: rgba(245,166,35,.13);
--add: #4cc38a;
--del: #f26d6d;
--mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
--ui: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif;
--r-ctl: 7px;
--r-card: 10px;
--r-over: 14px;
}
: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 */
[hidden] { display: none !important; }
html, body { height: 100%; margin: 0; }
#more-btn { display: none; } /* mobile-only; shown in the responsive block */
body {
display: flex;
background: var(--bg);
color: var(--text);
font: 15px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font: 13px/1.5 var(--ui);
letter-spacing: -.006em;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
::selection { background: var(--glow); color: var(--accent-bright); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 5px; }
.sprite { position: absolute; }
.ico {
width: 16px; height: 16px; flex: none;
stroke: currentColor; stroke-width: 1.6; fill: none;
stroke-linecap: round; stroke-linejoin: round;
}
button, input, a.btn { font-family: inherit; }
/* ---- sidebar ---- */
#sidebar {
width: 280px;
min-width: 200px;
width: 264px; min-width: 210px;
background: var(--bg-side);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
display: flex; flex-direction: column; overflow: hidden;
}
#vault {
padding: 14px 16px 10px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
#vault-name { font-weight: 600; font-size: 14px; }
#signout { color: var(--text-faint); text-decoration: none; font-size: 13px; }
#signout:hover { color: var(--text); }
#upload-btn {
border: none;
cursor: pointer;
font: inherit;
font-size: 12.5px;
}
#projects {
flex: none;
max-height: 30%;
overflow-y: auto;
padding: 8px 6px;
font-size: 13.5px;
display: flex; align-items: center; gap: 9px;
height: 52px; padding: 0 12px 0 14px;
border-bottom: 1px solid var(--border);
}
#projects ul { list-style: none; margin: 0; padding-left: 4px; }
#vault-badge {
width: 22px; height: 22px; border-radius: 6px; flex: none;
display: grid; place-items: center; font-size: 13px; line-height: 1;
background: linear-gradient(160deg, #ffcf85, #f5a623 55%, #d3861a);
box-shadow: 0 1px 0 rgba(255,255,255,.35) inset, 0 2px 6px -1px rgba(245,166,35,.4);
}
#vault-name { font-weight: 600; font-size: 13px; letter-spacing: -.01em; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vault-actions { display: flex; align-items: center; gap: 4px; }
#signout, .icon-btn2 {
width: 28px; height: 28px; border-radius: 6px;
display: inline-flex; align-items: center; justify-content: center;
color: var(--text-ghost); background: transparent; border: none; cursor: pointer; text-decoration: none;
}
#signout:hover, .icon-btn2:hover { color: var(--text); background: var(--hover); }
#signout .ico, .icon-btn2 .ico { width: 16px; height: 16px; }
.adminbar {
display: inline-flex; align-items: center; gap: 6px;
height: 26px; padding: 0 9px; border-radius: var(--r-ctl);
border: 1px solid var(--border); background: var(--surface);
color: var(--accent-bright); font-size: 11.5px; font-weight: 600; cursor: pointer;
}
.adminbar:hover { background: var(--glow); border-color: transparent; }
/* projects */
#projects { flex: none; max-height: 32%; overflow-y: auto; padding: 10px 8px 8px; border-bottom: 1px solid var(--border); }
.nav-head { display: flex; align-items: center; justify-content: space-between; padding: 6px 8px; font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-ghost); }
.nav-add { display: grid; place-items: center; width: 18px; height: 18px; border: none; background: transparent; color: var(--text-ghost); cursor: pointer; border-radius: 5px; }
.nav-add .ico { width: 14px; height: 14px; }
.nav-add:hover { color: var(--text); background: var(--hover); }
#projects ul { list-style: none; margin: 0; padding: 0; }
#projects .row {
padding: 3px 8px;
border-radius: 5px;
cursor: pointer;
color: var(--text-dim);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex; align-items: center; gap: 9px;
height: 31px; padding: 0 8px; border-radius: 7px;
color: var(--text-dim); cursor: pointer; position: relative;
white-space: nowrap; overflow: hidden;
}
#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-bright); box-shadow: inset 2px 0 0 var(--accent); }
#projects .row.active::before { color: var(--accent-bright); }
#tree { flex: 1; overflow-y: auto; padding: 8px 6px 24px; font-size: 13.5px; }
#orgbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 9px 16px;
border-top: 1px solid var(--border);
font-size: 12.5px;
color: var(--text-dim);
.proj-mark {
width: 17px; height: 17px; border-radius: 5px; flex: none;
display: grid; place-items: center; font-size: 10px; font-weight: 700;
color: #0a0b0d; letter-spacing: -.02em; text-transform: uppercase;
}
#orgbar[hidden] { display: none; }
#org-name { cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#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: 3px 10px; border-radius: var(--r-ctl);
background: var(--bg-active); color: var(--text);
}
#invite-btn:hover { color: #fff; background: var(--bg-hover); }
#tree ul { list-style: none; margin: 0; padding-left: 14px; }
#tree > ul { padding-left: 4px; }
#projects .row .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
#projects .row:hover { background: var(--hover); color: var(--text); }
#projects .row.active { background: var(--glow); color: var(--accent-bright); }
#projects .row.active::before { content: ""; position: absolute; left: 0; top: 6px; bottom: 6px; width: 2px; border-radius: 2px; background: var(--accent); }
/* file tree */
#tree { flex: 1; overflow-y: auto; padding: 8px 8px 24px; }
#tree ul { list-style: none; margin: 0; padding-left: 13px; position: relative; }
#tree > ul { padding-left: 0; }
#tree ul ul::before { content: ""; position: absolute; left: 5px; top: 0; bottom: 0; width: 1px; background: var(--border); }
#tree li > .row {
display: flex;
align-items: center;
gap: 5px;
padding: 2px 8px;
border-radius: 5px;
cursor: pointer;
color: var(--text-dim);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex; align-items: center; gap: 6px;
height: 28px; padding: 0 8px; border-radius: 7px;
color: var(--text-dim); cursor: pointer; position: relative;
white-space: nowrap; overflow: hidden;
}
#tree li > .row:hover { background: var(--bg-hover); color: var(--text); }
#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 > .row:hover { background: var(--hover); color: var(--text); }
#tree li > .row.active { background: var(--glow); color: var(--accent-bright); }
#tree li > .row.active::before { content: ""; position: absolute; left: 0; top: 5px; bottom: 5px; width: 2px; border-radius: 2px; background: var(--accent); }
#tree .chev { width: 14px; height: 14px; flex: none; color: var(--text-ghost); transition: transform .12s; display: flex; align-items: center; justify-content: center; }
#tree .chev .ico { width: 13px; height: 13px; }
#tree .ticon { flex: none; display: flex; color: var(--text-ghost); }
#tree .ticon .ico { width: 15px; height: 15px; }
#tree li > .row:hover .ticon, #tree li > .row:hover .chev { color: var(--text-faint); }
#tree li > .row.active .ticon, #tree li > .row.active .chev { color: var(--accent); }
#tree .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
#tree .file .label { font-size: 12.5px; }
#tree .file .chev { visibility: hidden; }
#tree li.collapsed > ul { display: none; }
#tree li.collapsed > .row .chev { transform: rotate(-90deg); }
#tree .file .chev { visibility: hidden; }
.dir-history {
margin-left: auto; flex: none; visibility: hidden;
width: 24px; height: 24px; border-radius: 5px;
display: inline-flex; align-items: center; justify-content: center; color: var(--text-ghost);
}
.dir-history .ico { width: 14px; height: 14px; }
#tree li > .row:hover .dir-history { visibility: visible; }
.dir-history:hover { color: var(--text); background: var(--hover); }
/* org bar */
#orgbar { display: flex; align-items: center; gap: 9px; padding: 9px 12px 9px 14px; border-top: 1px solid var(--border); }
#org-name {
flex: 1; min-width: 0; display: inline-flex; align-items: center; gap: 8px;
color: var(--text-dim); font-size: 12.5px; font-weight: 500; cursor: pointer;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#org-name::before {
content: ""; width: 15px; height: 15px; flex: none; border-radius: 4px;
background: linear-gradient(160deg, #ffcf85, #d3861a); opacity: .85;
}
#org-name:hover { color: var(--text); }
#invite-btn {
flex: none; height: 27px; padding: 0 11px; border-radius: var(--r-ctl);
border: 1px solid var(--border); background: var(--surface);
color: var(--text-dim); font-size: 12px; font-weight: 500; cursor: pointer;
}
#invite-btn:hover { background: var(--hover); color: var(--text); border-color: var(--border-2); }
/* ---- main pane ---- */
#main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
#topbar {
position: relative;
display: flex;
align-items: center;
gap: 12px;
padding: 10px 24px;
border-bottom: 1px solid var(--border);
min-height: 46px;
position: relative; display: flex; align-items: center; gap: 9px;
height: 52px; padding: 0 16px; border-bottom: 1px solid var(--border);
}
#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. */
.icon-btn { display: none; width: 34px; height: 34px; border: none; background: transparent; color: var(--text-dim); cursor: pointer; border-radius: 7px; align-items: center; justify-content: center; }
.icon-btn:hover { color: var(--text); background: var(--hover); }
#crumb { font-size: 12.5px; color: var(--text); font-weight: 500; letter-spacing: -.01em; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#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: 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;
display: inline-flex; align-items: center; gap: 6px; flex: none;
height: 30px; padding: 0 11px; border-radius: var(--r-ctl);
border: 1px solid var(--border); background: var(--surface);
color: var(--text-dim); font-size: 12.5px; font-weight: 500; cursor: pointer; text-decoration: none;
}
.btn:hover { background: var(--accent-press); }
#content { flex: 1; overflow-y: auto; padding: 28px 48px 80px; }
/* The per-file actions are all secondary uniform ghost styling so no
single one shouts louder than the rest. */
#history-btn, #share-btn, #upload-btn, #download { border: none; cursor: pointer; font: inherit; font-size: 12.5px; background: var(--bg-active); color: var(--text); }
#history-btn:hover, #share-btn:hover, #upload-btn:hover, #download: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); }
.history { max-width: 860px; }
.hentry { padding: 10px 12px; border-bottom: 1px solid var(--border); }
.hentry .hline { display: flex; gap: 10px; align-items: baseline; }
.hkind { color: var(--accent); font-size: 11px; }
.hentry.delete .hkind { color: #ff7b72; }
.hpath { font-weight: 500; cursor: pointer; }
.hpath:hover { color: var(--accent); }
.htime { margin-left: auto; color: var(--text-faint); font-size: 12px; }
.hmeta { display: flex; gap: 16px; margin-top: 3px; font-size: 12px; color: var(--text-dim); }
.hdev, .hsize { color: var(--text-faint); }
.hact { margin-left: auto; }
.hact a { color: var(--accent); text-decoration: none; margin-left: 8px; }
.empty { color: var(--text-faint); text-align: center; margin-top: 20vh; }
.empty-hint { font-size: 12px; color: var(--text-faint); }
.btn:hover { background: var(--hover); color: var(--text); border-color: var(--border-2); }
.btn .ico { width: 15px; height: 15px; }
.btn.ghost { color: var(--text-dim); }
#search-btn kbd {
font: 11px var(--ui); color: var(--text-faint);
background: var(--hover); border: 1px solid var(--border-2); border-radius: 5px;
padding: 1px 5px; margin-left: 2px;
}
#more-menu {
position: absolute; right: 12px; top: calc(100% - 4px); z-index: 80;
background: var(--bg-raise); border: 1px solid var(--border-2);
border-radius: var(--r-card); box-shadow: 0 18px 44px -14px rgba(0,0,0,.75);
padding: 6px; min-width: 168px;
}
.more-item { display: block; width: 100%; text-align: left; min-height: 40px; padding: 0 12px; background: transparent; border: none; cursor: pointer; color: var(--text); font: inherit; font-size: 13.5px; border-radius: var(--r-ctl); }
.more-item:hover { background: var(--hover); }
/* ---- command palette (⌘K) ---- */
#palette-overlay[hidden] { display: none; }
#palette-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .45);
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 12vh;
z-index: 100;
}
#palette {
width: min(560px, 90vw);
background: var(--bg-side);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 16px 48px rgba(0, 0, 0, .5);
overflow: hidden;
}
#palette-input {
width: 100%;
box-sizing: border-box;
padding: 14px 16px;
border: none;
border-bottom: 1px solid var(--border);
background: transparent;
color: var(--text);
font: inherit;
font-size: 15px;
outline: none;
}
#palette-results {
list-style: none;
margin: 0;
padding: 6px;
max-height: 46vh;
overflow-y: auto;
}
#palette-results li {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 10px;
border-radius: 7px;
cursor: pointer;
color: var(--text-dim);
font-size: 13.5px;
}
#palette-results li.selected { background: var(--bg-active); color: var(--text); }
#palette-results li.selected .picon, #palette-results li.selected .pkind { color: #c4c4c4; }
#palette-results li.selected .plabel b { color: var(--accent-bright); } /* match highlight stays AA on the tinted selected row */
#palette-results li .picon { width: 18px; flex: none; text-align: center; color: var(--text-faint); }
#palette-results li .plabel { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#palette-results li .plabel b { color: var(--accent); font-weight: 600; }
#palette-results li .pkind { flex: none; font-size: 11px; color: var(--text-faint); }
#palette-results .pempty { color: var(--text-faint); cursor: default; justify-content: center; }
#palette-hint {
padding: 7px 14px;
border-top: 1px solid var(--border);
font-size: 11px;
color: var(--text-faint);
}
#content { flex: 1; overflow-y: auto; padding: 44px 40px 110px; scroll-behavior: smooth; }
.empty { color: var(--text-faint); text-align: center; margin-top: 22vh; }
.empty-hint { display: block; margin-top: 6px; font-size: 12px; color: var(--text-ghost); }
/* ---- projects nav header + new-project ---- */
.nav-head { display: flex; align-items: center; justify-content: space-between; padding: 2px 8px 6px; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-faint); }
.nav-add { border: none; background: transparent; color: var(--text-faint); font-size: 18px; line-height: 1; cursor: pointer; padding: 0 4px; border-radius: 4px; }
.nav-add:hover { color: var(--text); background: var(--bg-hover); }
/* ---- vault header actions ---- */
.vault-actions { display: flex; align-items: center; gap: 8px; }
.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); }
/* ---- topbar controls ---- */
.icon-btn { display: none; border: none; background: transparent; color: var(--text-dim); font-size: 18px; cursor: pointer; padding: 2px 6px; border-radius: 5px; }
.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); }
.btn.ghost:hover { background: var(--bg-hover); color: #fff; }
/* ---- onboarding empty state ---- */
/* ---- onboarding ---- */
.onboard { max-width: 560px; margin: 8vh auto 0; }
.onboard h1 { font-size: 26px; margin: 0 0 8px; color: #f0f0f0; }
.onboard > p { color: var(--text-dim); margin: 0 0 28px; }
.ob-card { background: var(--bg-side); border: 1px solid var(--border); border-radius: 10px; padding: 20px 22px; margin-bottom: 16px; }
.ob-card h3 { margin: 0 0 6px; font-size: 15px; }
.ob-card p { margin: 0 0 14px; font-size: 13.5px; color: var(--text-dim); }
.ob-row { display: flex; gap: 10px; }
.ob-row input { flex: 1; padding: 8px 11px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--text); font: inherit; font-size: 13.5px; outline: none; }
.ob-row input:focus { border-color: var(--accent-dim); }
.pbtn { border: none; cursor: pointer; font: inherit; font-size: 13px; padding: 7px 16px; border-radius: 6px; background: var(--accent-dim); color: #fff; white-space: nowrap; }
.pbtn:hover { background: var(--accent-press); }
.onboard h1 { font-size: 25px; font-weight: 640; letter-spacing: -.02em; margin: 0 0 8px; color: #f4f6f9; }
.onboard > p { color: var(--text-dim); margin: 0 0 28px; font-size: 14px; }
.ob-card { background: var(--bg-side); border: 1px solid var(--border); border-radius: var(--r-card); padding: 20px 22px; margin-bottom: 14px; }
.ob-card h3 { margin: 0 0 6px; font-size: 14.5px; font-weight: 600; }
.ob-card p { margin: 0 0 14px; font-size: 13px; color: var(--text-dim); }
.ob-row { display: flex; gap: 9px; }
.ob-row input { flex: 1; height: 34px; padding: 0 12px; border-radius: var(--r-ctl); border: 1px solid var(--border); background: var(--surface); color: var(--text); font: inherit; font-size: 13px; outline: none; }
.ob-row input:focus { border-color: var(--accent); background: var(--hover); }
/* ---- admin panels (org, pending) ---- */
.admin { max-width: 720px; }
.admin h1 { font-size: 22px; margin: 0 0 20px; color: #f0f0f0; }
.admin h3 { font-size: 13px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint); margin: 26px 0 10px; }
.admin-h { display: flex; align-items: center; justify-content: space-between; margin: 26px 0 10px; }
.admin-h h3 { margin: 0; }
.admin-row { display: flex; gap: 10px; margin-bottom: 8px; }
.admin-row input { flex: 1; padding: 7px 11px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--text); font: inherit; font-size: 13.5px; outline: none; }
.admin-list { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.admin-item { display: flex; align-items: center; gap: 10px; padding: 9px 12px; border-bottom: 1px solid var(--border); font-size: 13.5px; }
.admin-item:last-child { border-bottom: none; }
.ai-main { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ai-main.mono { font: 12px var(--mono, ui-monospace, Menlo, monospace); color: var(--text-dim); }
.ai-tag { font-size: 11.5px; color: var(--text-faint); }
.admin-item select { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 5px; padding: 3px 6px; font: inherit; font-size: 12.5px; }
.ai-btn, .ai-del { border: none; cursor: pointer; font: inherit; font-size: 12px; padding: 4px 10px; border-radius: 5px; background: var(--bg-active); color: var(--text-dim); }
.ai-btn:hover { background: var(--bg-hover); color: var(--text); }
.ai-del { color: #ff9b91; }
.ai-del:hover { background: #4a2420; color: #ff7b72; }
.admin-empty { padding: 14px 12px; color: var(--text-faint); font-size: 13px; }
.admin-sub { color: var(--text-dim); font-size: 13.5px; margin: -8px 0 4px; }
.admin-item.toggle { cursor: pointer; }
.admin-item.toggle input { width: 16px; height: 16px; accent-color: var(--accent); }
.tg-label { font-size: 13.5px; color: var(--text); }
.tg-desc { font-size: 12px; color: var(--text-faint); margin-top: 2px; }
/* ---- modal ---- */
.modal-back { position: fixed; inset: 0; background: rgba(0,0,0,.5); display: flex; align-items: center; justify-content: center; z-index: 150; padding: 20px; }
.modal { background: var(--bg-side); border: 1px solid var(--border); border-radius: 12px; padding: 24px; width: min(460px, 100%); box-shadow: 0 20px 60px rgba(0,0,0,.5); }
.modal h3 { margin: 0 0 10px; font-size: 17px; }
.modal p { margin: 0 0 16px; font-size: 13.5px; color: var(--text-dim); }
.modal-url { font: 12px var(--mono, ui-monospace, Menlo, monospace); background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 9px 11px; color: var(--text-dim); word-break: break-all; margin-bottom: 16px; }
.modal-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.modal-label { display: block; font-size: 12.5px; color: var(--text-dim); margin: 0 0 6px; }
.modal-msg { margin: 0 0 16px; font-size: 13.5px; color: var(--text-dim); }
.modal-input { width: 100%; box-sizing: border-box; padding: 9px 11px; border-radius: var(--r-ctl); border: 1px solid var(--border); background: var(--bg); color: var(--text); font: inherit; font-size: 14px; margin-bottom: 16px; outline: none; }
.modal-input:focus-visible { border-color: var(--accent); outline: 2px solid var(--accent); outline-offset: 1px; }
.danger-btn { border: none; cursor: pointer; font: inherit; font-size: 13px; padding: 7px 16px; border-radius: var(--r-ctl); background: #b3382e; color: #fff; }
/* primary + secondary buttons */
.pbtn { display: inline-flex; align-items: center; gap: 6px; flex: none; height: 32px; padding: 0 14px; border-radius: var(--r-ctl); border: none; background: var(--accent); color: #241704; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; }
.pbtn:hover { background: var(--accent-bright); }
.pbtn .ico { width: 15px; height: 15px; }
.danger-btn { display: inline-flex; align-items: center; height: 32px; padding: 0 14px; border-radius: var(--r-ctl); border: none; background: #b3382e; color: #fff; font-size: 13px; font-weight: 600; cursor: pointer; }
.danger-btn:hover { background: #c94336; }
/* ---- admin panels ---- */
.admin { max-width: 760px; }
.admin h1 { font-size: 21px; font-weight: 640; letter-spacing: -.02em; margin: 0 0 6px; color: #f4f6f9; }
.admin h3 { font-size: 10.5px; text-transform: uppercase; letter-spacing: .07em; color: var(--text-ghost); font-weight: 600; margin: 30px 0 10px; }
.admin-sub { color: var(--text-dim); font-size: 13.5px; margin: 0 0 6px; line-height: 1.55; }
.admin-h { display: flex; align-items: center; justify-content: space-between; margin: 30px 0 10px; }
.admin-h h3 { margin: 0; }
.admin-row { display: flex; gap: 9px; margin-bottom: 8px; }
.admin-row input { flex: 1; height: 34px; padding: 0 12px; border-radius: var(--r-ctl); border: 1px solid var(--border); background: var(--surface); color: var(--text); font: inherit; font-size: 13px; outline: none; }
.admin-row input:focus { border-color: var(--accent); background: var(--hover); }
.admin-list { border: 1px solid var(--border); border-radius: var(--r-card); overflow: hidden; background: var(--bg-side); }
.admin-item { display: flex; align-items: center; gap: 11px; padding: 11px 14px; border-bottom: 1px solid var(--border); font-size: 13.5px; }
.admin-item:last-child { border-bottom: none; }
.admin-item:hover { background: rgba(255,255,255,.015); }
.ai-main { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.ai-main.mono { font: 12px var(--mono); color: var(--text-dim); cursor: pointer; }
.ai-tag { font-size: 11.5px; color: var(--text-faint); flex: none; }
.admin-item select { height: 28px; background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 0 8px; font: inherit; font-size: 12.5px; cursor: pointer; }
.admin-item select:hover { border-color: var(--border-2); }
.ai-btn, .ai-del { flex: none; height: 27px; padding: 0 11px; border-radius: 6px; border: 1px solid var(--border); background: var(--surface); color: var(--text-dim); font: inherit; font-size: 12px; font-weight: 500; cursor: pointer; }
.ai-btn:hover { background: var(--hover); color: var(--text); border-color: var(--border-2); }
.ai-del { color: var(--del); border-color: transparent; background: transparent; }
.ai-del:hover { background: rgba(242,109,109,.12); color: #ff8b8b; }
.admin-empty { padding: 14px; color: var(--text-faint); font-size: 13px; }
.admin-item.toggle { cursor: pointer; align-items: flex-start; }
.admin-item.toggle .ai-main { white-space: normal; }
.tg-label { font-size: 13.5px; font-weight: 550; color: var(--text); }
.tg-desc { font-size: 12px; color: var(--text-faint); margin-top: 3px; line-height: 1.5; }
.admin-item.toggle input { width: 16px; height: 16px; margin-top: 2px; accent-color: var(--accent); flex: none; }
/* ---- history ---- */
.history { max-width: 860px; }
.hentry { padding: 11px 12px; border-bottom: 1px solid var(--border); }
.hentry:hover { background: rgba(255,255,255,.015); }
.hline { display: flex; gap: 10px; align-items: center; }
.hkind { display: inline-flex; color: var(--add); }
.hkind .ico { width: 13px; height: 13px; }
.hentry.delete .hkind { color: var(--del); }
.hpath { font-weight: 500; cursor: pointer; color: var(--text); font-size: 13px; }
.hpath:hover { color: var(--accent-bright); }
.htime { margin-left: auto; color: var(--text-faint); font-size: 12px; font-variant-numeric: tabular-nums; }
.hmeta { display: flex; gap: 14px; margin-top: 4px; padding-left: 23px; font-size: 12px; color: var(--text-dim); }
.hdev, .hsize { color: var(--text-faint); }
.hsize { font-variant-numeric: tabular-nums; }
.hact { margin-left: auto; }
.hact a { color: var(--accent-bright); text-decoration: none; margin-left: 10px; }
.hact a:hover { text-decoration: underline; }
/* ---- command palette ---- */
#palette-overlay { position: fixed; inset: 0; background: rgba(6,7,9,.62); backdrop-filter: blur(3px); display: flex; justify-content: center; align-items: flex-start; padding-top: 12vh; z-index: 100; }
#palette { width: min(560px, 92vw); background: var(--bg-raise); border: 1px solid var(--border-2); border-radius: var(--r-over); box-shadow: 0 24px 70px -18px rgba(0,0,0,.8); overflow: hidden; }
#palette-inputwrap { display: flex; align-items: center; gap: 11px; padding: 14px 16px; border-bottom: 1px solid var(--border); }
#palette-inputwrap .ico { width: 17px; height: 17px; color: var(--text-faint); }
#palette-input { flex: 1; width: 100%; border: none; background: transparent; color: var(--text); font: inherit; font-size: 15px; letter-spacing: -.01em; outline: none; padding: 0; }
#palette-input::placeholder { color: var(--text-ghost); }
#palette-results { list-style: none; margin: 0; padding: 8px; max-height: 46vh; overflow-y: auto; }
#palette-results li { display: flex; align-items: center; gap: 11px; height: 38px; padding: 0 10px; border-radius: 9px; cursor: pointer; color: var(--text-dim); font-size: 13.5px; }
#palette-results li.selected { background: var(--glow); }
#palette-results li.selected .picon { color: var(--accent); }
#palette-results li.selected .plabel { color: var(--accent-bright); }
#palette-results li.selected .plabel b { color: var(--accent-bright); }
#palette-results li .picon { width: 18px; flex: none; display: flex; justify-content: center; color: var(--text-faint); }
#palette-results li .picon .ico { width: 15px; height: 15px; }
#palette-results li .plabel { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); }
#palette-results li .plabel b { color: var(--accent-bright); font-weight: 600; }
#palette-results li .pkind { flex: none; font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-ghost); }
#palette-results .pempty { color: var(--text-faint); cursor: default; justify-content: center; height: auto; padding: 14px; }
#palette-hint { padding: 9px 16px; border-top: 1px solid var(--border); font-size: 11px; color: var(--text-ghost); }
/* ---- modal ---- */
.modal-back { position: fixed; inset: 0; background: rgba(6,7,9,.6); backdrop-filter: blur(3px); display: flex; align-items: center; justify-content: center; z-index: 150; padding: 20px; }
.modal { background: var(--bg-raise); border: 1px solid var(--border-2); border-radius: var(--r-over); padding: 22px 24px; width: min(460px, 100%); box-shadow: 0 24px 70px -18px rgba(0,0,0,.8); }
.modal h3 { margin: 0 0 10px; font-size: 16px; font-weight: 620; letter-spacing: -.01em; }
.modal p { margin: 0 0 16px; font-size: 13.5px; color: var(--text-dim); line-height: 1.55; }
.modal p b { color: var(--text); }
.modal-url { font: 12px var(--mono); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-ctl); padding: 9px 11px; color: var(--text-dim); word-break: break-all; margin-bottom: 16px; }
.modal-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.modal-label { display: block; font-size: 12.5px; color: var(--text-dim); margin: 0 0 6px; }
.modal-msg { margin: 0 0 16px; font-size: 13.5px; color: var(--text-dim); line-height: 1.55; }
.modal-input { width: 100%; height: 36px; padding: 0 12px; border-radius: var(--r-ctl); border: 1px solid var(--border); background: var(--surface); color: var(--text); font: inherit; font-size: 14px; margin-bottom: 16px; outline: none; }
.modal-input:focus { border-color: var(--accent); background: var(--hover); }
/* ---- toast ---- */
#toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px); background: var(--bg-active); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 10px 18px; font-size: 13.5px; box-shadow: 0 8px 30px rgba(0,0,0,.5); opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; z-index: 200; }
#toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px); background: var(--bg-raise); color: var(--text); border: 1px solid var(--border-2); border-radius: 10px; padding: 11px 18px; font-size: 13.5px; box-shadow: 0 18px 44px -12px rgba(0,0,0,.7); opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; z-index: 200; }
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
#toast.err { border-color: #7a2e28; color: #ffb4ac; }
#toast.err { border-color: rgba(242,109,109,.5); color: #ffb0aa; }
/* ---- mobile backdrop ---- */
#sb-backdrop { display: none; }
/* ---- responsive ---- */
@media (max-width: 760px) {
#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); }
#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,.6); }
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-flex; }
#content { padding: 20px 18px 60px; }
#topbar { padding: 6px 10px; gap: 4px; }
#crumb { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#search-btn kbd { display: none; }
/* 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; }
body.sb-open #sb-backdrop { display: block; position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 50; }
.icon-btn { display: inline-flex; width: 44px; height: 44px; }
#content { padding: 24px 18px 70px; }
#topbar { padding: 0 8px; gap: 4px; }
#search-btn kbd, .btn .lbl { display: none; }
#topbar .btn { min-width: 44px; min-height: 44px; padding: 0; justify-content: center; gap: 0; }
#topbar .btn .ico { width: 18px; height: 18px; }
#more-btn:not([hidden]) { display: inline-flex; }
#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; }
/* The list rows are the primary tap targets — size them to 44px too. */
#tree li > .row, #projects .row { min-height: 44px; }
#invite-btn { min-height: 44px; padding: 0 16px; }
#org-name { min-height: 44px; display: inline-flex; align-items: center; }
#vault { padding: 0 8px 0 12px; }
.icon-btn2, #signout, .adminbar { min-width: 44px; min-height: 44px; }
#tree li > .row, #projects .row { height: 44px; }
#invite-btn { min-height: 40px; padding: 0 14px; }
#org-name { min-height: 44px; }
.nav-add { min-width: 44px; min-height: 44px; }
.dir-history { min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
.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 {
color: #f0f0f0;
line-height: 1.3;
margin: 1.4em 0 .5em;
}
/* ---- markdown reading view ---- */
.markdown { max-width: 704px; margin: 0 auto; }
.markdown h1, .markdown h2, .markdown h3, .markdown h4 { color: #f4f6f9; line-height: 1.25; letter-spacing: -.018em; margin: 1.5em 0 .5em; text-wrap: balance; }
.markdown h1:first-child { margin-top: 0; }
.markdown h1 { font-size: 1.7em; }
.markdown h2 { font-size: 1.35em; border-bottom: 1px solid var(--border); padding-bottom: .25em; }
.markdown a { color: var(--accent); text-decoration: none; }
.markdown a:hover { text-decoration: underline; }
.markdown code {
background: var(--code-bg);
padding: .15em .4em;
border-radius: 4px;
font: 12.5px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.markdown pre {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
overflow-x: auto;
}
.markdown pre code { background: none; padding: 0; }
.markdown blockquote {
margin: 1em 0;
padding: .1em 1em;
border-left: 3px solid var(--accent-dim);
color: var(--text-dim);
}
.markdown table { border-collapse: collapse; margin: 1em 0; }
.markdown th, .markdown td { border: 1px solid var(--border); padding: 6px 12px; }
.markdown th { background: var(--bg-side); }
.markdown img { max-width: 100%; border-radius: 6px; }
.markdown hr { border: none; border-top: 1px solid var(--border); margin: 2em 0; }
.markdown h1 { font-size: 1.85em; font-weight: 660; letter-spacing: -.024em; }
.markdown h2 { font-size: 1.32em; font-weight: 620; margin-top: 1.7em; }
.markdown h3 { font-size: 1.08em; font-weight: 620; }
.markdown p, .markdown li { color: #c6cbd3; font-size: 14.5px; line-height: 1.72; }
.markdown p { margin: 0 0 1em; }
.markdown strong { color: var(--text); font-weight: 620; }
.markdown a { color: var(--accent-bright); text-decoration: none; border-bottom: 1px solid rgba(245,166,35,.28); }
.markdown a:hover { border-bottom-color: var(--accent); }
.markdown ul, .markdown ol { margin: 0 0 1em; padding-left: 1.4em; }
.markdown li { margin-bottom: .4em; }
.markdown li::marker { color: var(--text-ghost); }
.markdown code { background: var(--hover); border: 1px solid var(--border); padding: .1em .4em; border-radius: 5px; font: 12.5px/1.5 var(--mono); color: #e4d9c4; }
.markdown pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: var(--r-card); padding: 14px 16px; overflow-x: auto; margin: 1.3em 0; }
.markdown pre code { background: none; border: none; padding: 0; color: #c6cbd3; }
.markdown blockquote { margin: 1.3em 0; padding: .3em 1em; border-left: 2px solid var(--accent); background: linear-gradient(90deg, var(--glow), transparent); border-radius: 0 8px 8px 0; color: #d8cdb6; }
.markdown blockquote p { margin: .3em 0; color: #d8cdb6; }
.markdown table { border-collapse: collapse; margin: 1.3em 0; font-size: 13.5px; }
.markdown th, .markdown td { border-bottom: 1px solid var(--border); padding: 9px 13px; text-align: left; }
.markdown th { color: var(--text-faint); font-size: 11px; text-transform: uppercase; letter-spacing: .05em; font-weight: 600; border-bottom-color: var(--border-2); }
.markdown tr:hover td { background: rgba(255,255,255,.02); }
.markdown img { max-width: 100%; border-radius: 8px; border: 1px solid var(--border); }
.markdown hr { border: none; border-top: 1px solid var(--border); margin: 2.2em 0; }
.markdown input[type="checkbox"] { accent-color: var(--accent); }
/* plain file / binary views */
pre.plain {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
overflow-x: auto;
font: 12.5px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
white-space: pre-wrap;
}
.filecard {
margin-top: 15vh;
text-align: center;
color: var(--text-dim);
}
pre.plain { background: var(--code-bg); border: 1px solid var(--border); border-radius: var(--r-card); padding: 14px 16px; overflow-x: auto; font: 12.5px/1.6 var(--mono); color: #c6cbd3; white-space: pre-wrap; overflow-wrap: anywhere; max-width: 900px; }
#content.markdown { min-width: 0; } /* let long unbreakable lines wrap, not blow out the column */
.filecard { margin-top: 15vh; text-align: center; color: var(--text-dim); }
.filecard .name { font-size: 1.2em; color: var(--text); margin-bottom: .3em; }
.filecard .btn { margin-top: 14px; }