feat(web): PropelAuth-style sidebar layout — brand header, project dropdown, account menu

- header is the brand only (BearDrive), linking home
- Projects section is a dropdown (native select, styled) with the project
  mark, plus the project-settings gear beside it and + to create
- sidebar footer is the account row (avatar, name, email) opening a popover:
  Organization → <org> Settings, Hub → Signup & access (admins), Account →
  Log out (danger red)
- volume mode unchanged (sign-out stays in its header)
- Playwright suite reworked for the new layout — 44/44 green; static rebuilt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
This commit is contained in:
Snow Lee
2026-07-19 09:42:30 -07:00
co-authored by Claude Fable 5
parent 77735660c9
commit 40a0b19aa4
12 changed files with 281 additions and 194 deletions
+22 -10
View File
@@ -6,9 +6,15 @@ import { login, wikiId, ADMIN, MEMBER } from "./helpers";
// navigation closes them. Mutating specs revert their changes: the suite
// shares one hub per run.
// The org panel opens from the account menu (sidebar footer).
async function openOrgSettings(page: import("@playwright/test").Page) {
await page.click("#account-btn");
await page.click("#menu-org-settings");
}
test("org admin: members with roles, self marked, rename round-trip", async ({ page }) => {
await login(page);
await page.click("#org-settings-btn"); // owner's Manage button
await openOrgSettings(page);
await expect(page.locator("#org-title")).toHaveText("default");
await expect(page.locator("#crumb")).toHaveText("default");
await expect(page.locator(".admin-item", { hasText: ADMIN })).toContainText("(you)");
@@ -19,15 +25,19 @@ test("org admin: members with roles, self marked, rename round-trip", async ({ p
await page.fill("#org-rename", "renamed-org");
await page.click("#org-rename-btn");
await expect(page.locator("#toast")).toContainText("Renamed");
await expect(page.locator("#orgbar #org-name")).toHaveText("renamed-org");
await page.click("#account-btn");
await expect(page.locator("#menu-org-settings")).toContainText("renamed-org");
await page.keyboard.press("Escape");
await page.fill("#org-rename", "default");
await page.click("#org-rename-btn");
await expect(page.locator("#orgbar #org-name")).toHaveText("default");
await page.click("#account-btn");
await expect(page.locator("#menu-org-settings")).toContainText("default");
await page.keyboard.press("Escape");
});
test("org admin: member role change round-trip", async ({ page }) => {
await login(page);
await page.click("#org-settings-btn");
await openOrgSettings(page);
const sel = page.locator(".admin-item", { hasText: MEMBER }).locator("select");
await sel.selectOption("owner");
await expect(page.locator("#toast")).toContainText("Role updated");
@@ -38,7 +48,7 @@ test("org admin: member role change round-trip", async ({ page }) => {
test("org admin: invite create shows in list, revoke removes it", async ({ page }) => {
await login(page);
await page.click("#org-settings-btn");
await openOrgSettings(page);
await page.click(".admin-h .pbtn"); // New invite
await expect(page.locator("#toast")).toContainText("Invite");
const row = page.locator(".admin-item", { hasText: "/join/" }).first();
@@ -54,7 +64,7 @@ test("org admin: public share audit lists and revokes", async ({ page }) => {
await login(page);
const pid = await wikiId(page);
await page.request.post(`/api/p/${pid}/shares`, { data: { path: "index.md" } });
await page.click("#org-settings-btn");
await openOrgSettings(page);
const row = page.locator(".admin-item", { hasText: "index.md" });
await expect(row).toBeVisible();
await expect(row.locator(".ai-tag")).toContainText("wiki");
@@ -68,7 +78,7 @@ test("org admin: project rename and delete", async ({ page }) => {
await login(page);
await page.request.post("/api/projects", { data: { name: "doomed" } });
await page.reload(); // pick up the new project
await page.click("#org-settings-btn");
await openOrgSettings(page);
const row = page.locator(".admin-item", { hasText: "doomed" });
await row.locator(".ai-btn", { hasText: "Rename" }).click();
await page.fill(".modal-input", "doomed-2");
@@ -85,7 +95,7 @@ test("org admin: project rename and delete", async ({ page }) => {
test("member sees the org panel read-only", async ({ page }) => {
await login(page, MEMBER);
await page.click("#orgbar #org-name");
await openOrgSettings(page);
await expect(page.locator("#org-title")).toContainText("member");
await expect(page.locator("#org-rename")).toHaveCount(0);
await expect(page.locator(".admin-item select")).toHaveCount(0);
@@ -94,7 +104,8 @@ test("member sees the org panel read-only", async ({ page }) => {
test("hub settings: policy view, save round-trip, pending queue empty", async ({ page }) => {
await login(page);
await page.click("#adminbar");
await page.click("#account-btn");
await page.click("#menu-hub-admin");
await expect(page.locator("#crumb")).toHaveText("Signup & access");
await expect(page.locator(".admin h1")).toHaveText("Signup & access");
// Server has no SMTP: verification toggle disabled
@@ -115,7 +126,8 @@ test("hub settings: policy view, save round-trip, pending queue empty", async ({
test("navigating away closes an open admin panel", async ({ page }) => {
await login(page);
await page.click("#adminbar");
await page.click("#account-btn");
await page.click("#menu-hub-admin");
await expect(page.locator(".admin h1")).toBeVisible();
await page.click('#tree .row[data-path="index.md"]');
await expect(page.locator("#content h1")).toHaveText("Wiki");
+16 -15
View File
@@ -9,16 +9,16 @@ test("landing selects the first project and rewrites the URL", async ({ page })
await login(page);
const pid = await wikiId(page);
await page.waitForURL("/" + pid);
await expect(page.locator("#vault-name")).toHaveText("wiki");
await expect(page.locator("#project-select option:checked")).toHaveText("wiki");
await expect(page).toHaveTitle("wiki — BearDrive");
await expect(page.locator("#projects .row.active .label")).toHaveText("wiki");
await expect(page.locator("#vault-name")).toHaveText("BearDrive");
});
test("deep link to a project resolves after reload", async ({ page }) => {
await login(page);
const pid = await wikiId(page);
await page.goto("/" + pid);
await expect(page.locator("#vault-name")).toHaveText("wiki");
await expect(page.locator("#project-select option:checked")).toHaveText("wiki");
await expect(page).toHaveURL("/" + pid);
});
@@ -26,22 +26,23 @@ test("unknown project id falls back to a real project", async ({ page }) => {
await login(page);
await page.goto("/p-00000000");
await page.waitForURL(/\/p-[0-9a-f]{8}$/);
await expect(page.locator("#vault-name")).not.toHaveText("…");
await expect(page.locator("#project-select option:checked")).toHaveText(/.+/);
});
test("admin sees admin bar and org Manage; member does not", async ({ page, browser }) => {
test("account menu: admin gets hub admin entry; member does not", async ({ page, browser }) => {
await login(page); // admin, owner of "default"
await expect(page.locator("#adminbar")).toBeVisible();
await expect(page.locator("#orgbar #org-name")).toHaveText("default");
await expect(page.locator("#org-settings-btn")).toBeVisible();
await page.click("#account-btn");
await expect(page.locator("#menu-org-settings")).toContainText("default");
await expect(page.locator("#menu-hub-admin")).toBeVisible();
await expect(page.locator("#signout")).toBeVisible();
await page.keyboard.press("Escape");
const ctx = await browser.newContext();
const p2 = await ctx.newPage();
await login(p2, MEMBER);
await expect(p2.locator("#orgbar #org-name")).toHaveText("default");
await expect(p2.locator("#adminbar")).toHaveCount(0);
await expect(p2.locator("#org-settings-btn")).toHaveCount(0);
await p2.click("#account-btn");
await expect(p2.locator("#menu-org-settings")).toContainText("default");
await expect(p2.locator("#menu-hub-admin")).toHaveCount(0);
await ctx.close();
});
@@ -77,8 +78,8 @@ test("no-org account gets the onboarding empty state and can create a project",
await page.fill("#ob-name", "solo-notes");
await page.click("#ob-create");
await page.waitForURL(/\/p-[0-9a-f]{8}$/);
await expect(page.locator("#vault-name")).toHaveText("solo-notes");
await expect(page.locator("#orgbar")).toBeVisible(); // fresh org, owner
await expect(page.locator("#project-select option:checked")).toHaveText("solo-notes");
await expect(page.locator("#accountbar")).toBeVisible(); // fresh org, owner
});
test("new project via the sidebar + modal", async ({ page }) => {
@@ -87,7 +88,7 @@ test("new project via the sidebar + modal", async ({ page }) => {
await page.fill(".modal-input", "scratch");
await page.click(".modal .pbtn");
await page.waitForURL(/\/p-[0-9a-f]{8}$/);
await expect(page.locator("#vault-name")).toHaveText("scratch");
await expect(page.locator("#projects .row .label")).toContainText(["scratch", "wiki"]);
await expect(page.locator("#project-select option:checked")).toHaveText("scratch");
await expect(page.locator("#project-select option")).toContainText(["scratch", "wiki"]);
await expect(page.locator("#toast")).toContainText("Created");
});
+23 -25
View File
@@ -8,7 +8,7 @@ import { AppShell, Topbar, VaultHeader, closeSidebarOnMobile } from "../componen
import { OrgAdmin } from "../components/OrgAdmin";
import { HubSettings } from "../components/HubSettings";
import { ProjectNav } from "../components/ProjectNav";
import { OrgBar } from "../components/OrgBar";
import { AccountBar } from "../components/AccountBar";
import { ProjectSettings } from "../components/ProjectSettings";
import { EmptyState } from "../components/EmptyState";
import { toast } from "../toast";
@@ -72,11 +72,14 @@ export default function HubApp({ config }: { config: ServerConfig }) {
// hub admins and owners of the project's org.
const canInsights = isAdmin || (org ? org.role === "owner" : false);
const vault = (
<VaultHeader
name={projects ? (current ? current.name : brand) : ""}
onHome={current ? () => navigate("/" + current.id) : undefined}
showSignout={config.auth.enabled && !current}
// Top of the sidebar is the brand; project and account actions live in
// their own sections below (PropelAuth-style layout).
const vault = <VaultHeader name={brand} onHome={() => navigate("/")} />;
const accountBar = config.me ? (
<AccountBar
me={config.me}
org={org}
admin={
isAdmin
? {
@@ -88,18 +91,12 @@ export default function HubApp({ config }: { config: ServerConfig }) {
}
: undefined
}
projectSettings={
current
? {
onClick: () => {
setPanel({ kind: "project" });
closeSidebarOnMobile();
},
}
: undefined
}
onOrgSettings={(o) => {
setPanel({ kind: "org", orgId: o.id });
closeSidebarOnMobile();
}}
/>
);
) : undefined;
if (!projects || !orgs) {
return (
@@ -114,6 +111,7 @@ export default function HubApp({ config }: { config: ServerConfig }) {
<AppShell
vault={vault}
projectsNav={<ProjectNav projects={projects} />}
orgBar={accountBar}
topbar={<Topbar />}
contentClass="view"
>
@@ -176,17 +174,17 @@ export default function HubApp({ config }: { config: ServerConfig }) {
canInsights={canInsights}
sidebar={{
vault,
projectsNav: <ProjectNav projects={projects} currentId={current.id} />,
orgBar: (
<OrgBar
org={org}
showSignout={config.auth.enabled}
onManage={(o) => {
setPanel({ kind: "org", orgId: o.id });
projectsNav: (
<ProjectNav
projects={projects}
currentId={current.id}
onOpenSettings={() => {
setPanel({ kind: "project" });
closeSidebarOnMobile();
}}
/>
),
orgBar: accountBar,
}}
panel={activePanel}
/>
@@ -217,7 +215,7 @@ function JoinInvite({ token, onDone }: { token: string; onDone: (orgId: string |
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
return (
<AppShell vault={<VaultHeader name="…" showSignout />} topbar={<Topbar />}>
<AppShell vault={<VaultHeader name="BearDrive" />} topbar={<Topbar />}>
<div className="empty">Joining</div>
</AppShell>
);
@@ -0,0 +1,102 @@
import { useEffect, useRef, useState } from "react";
import type { Org } from "../api/types";
import { Icon } from "./shell";
import { projColor } from "./ProjectNav";
// The sidebar footer is the account row: avatar, name, email. Clicking it
// opens a popover with the workspace (org) and account actions — settings,
// hub administration for admins, and sign-out.
export function AccountBar({
me,
org,
admin,
onOrgSettings,
}: {
me: { email: string; name: string };
org: Org | null;
admin?: { pending: number; onClick: () => void }; // hub admins only
onOrgSettings: (org: Org) => void;
}) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const onDown = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};
document.addEventListener("mousedown", onDown);
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("mousedown", onDown);
document.removeEventListener("keydown", onKey);
};
}, [open]);
const display = me.name || me.email;
return (
<footer id="accountbar" ref={ref}>
{open && (
<div id="account-menu" role="menu" aria-label="Account menu">
{org && (
<>
<div className="menu-sec">Organization</div>
<button
id="menu-org-settings"
role="menuitem"
onClick={() => {
setOpen(false);
onOrgSettings(org);
}}
>
<Icon name="gear" />
<span>
<b>{org.name}</b> Settings
</span>
</button>
</>
)}
{admin && (
<>
<div className="menu-sec">Hub</div>
<button
id="menu-hub-admin"
role="menuitem"
onClick={() => {
setOpen(false);
admin.onClick();
}}
>
<Icon name="shield" />
<span>Signup &amp; access{admin.pending ? ` · ${admin.pending}` : ""}</span>
</button>
</>
)}
<div className="menu-sec">Account</div>
<a id="signout" role="menuitem" href="/auth/logout">
<Icon name="power" />
<span>Log out</span>
</a>
</div>
)}
<button
id="account-btn"
aria-haspopup="menu"
aria-expanded={open}
onClick={() => setOpen((o) => !o)}
>
<span className="avatar" style={{ background: projColor(me.email) }} aria-hidden="true">
{(display.trim()[0] || "?").toUpperCase()}
</span>
<span className="acct">
<b>{display}</b>
{me.name && <small>{me.email}</small>}
</span>
<Icon name="chev" />
</button>
</footer>
);
}
@@ -1,54 +0,0 @@
import type { Org } from "../api/types";
import { Icon } from "./shell";
// The sidebar footer is the workspace (org) row: the org name, a gear that
// opens the org admin panel (owners), and sign-out. Project-scoped actions
// live in the header; workspace-scoped ones end here.
export function OrgBar({
org,
onManage,
showSignout,
}: {
org: Org | null;
onManage: (org: Org) => void;
showSignout?: boolean;
}) {
if (!org && !showSignout) return null;
return (
<footer id="orgbar">
{org && (
<span
id="org-name"
title="Manage organization"
role="button"
tabIndex={0}
onClick={() => onManage(org)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onManage(org);
}
}}
>
{org.name}
</span>
)}
{org && org.role === "owner" && (
<button
id="org-settings-btn"
className="icon-btn2"
title="Manage this organization"
aria-label="Manage this organization"
onClick={() => onManage(org)}
>
<Icon name="gear" />
</button>
)}
{showSignout && (
<a id="signout" href="/auth/logout" title="Sign out" aria-label="Sign out">
<Icon name="power" />
</a>
)}
</footer>
);
}
@@ -1,4 +1,5 @@
import { navigate } from "../nav";
import { Icon } from "./shell";
import { postJSON } from "../api/http";
import type { Project, ProjectCreated } from "../api/types";
import { modalPrompt } from "../modal";
@@ -15,7 +16,15 @@ export function projColor(s: string): string {
return PROJ_COLORS[h % PROJ_COLORS.length];
}
export function ProjectNav({ projects, currentId }: { projects: Project[]; currentId?: string }) {
export function ProjectNav({
projects,
currentId,
onOpenSettings,
}: {
projects: Project[];
currentId?: string;
onOpenSettings?: () => void;
}) {
const refresh = useHubRefresh();
const create = async () => {
@@ -39,33 +48,47 @@ export function ProjectNav({ projects, currentId }: { projects: Project[]; curre
+
</button>
</div>
<ul>
{projects.map((p) => (
<li key={p.id}>
<div
className={"row" + (currentId === p.id ? " active" : "")}
title={p.name}
tabIndex={0}
role="button"
onClick={() => {
navigate("/" + p.id);
<div className="proj-row">
<span className="proj-select-wrap">
{currentId && (
<span
className="proj-mark"
aria-hidden="true"
style={{ background: projColor(projects.find((p) => p.id === currentId)?.name || "") }}
/>
)}
<select
id="project-select"
aria-label="Switch project"
value={currentId || ""}
onChange={(e) => {
if (e.target.value) {
navigate("/" + e.target.value);
closeSidebarOnMobile();
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
(e.currentTarget as HTMLElement).click();
}
}}
>
<span className="proj-mark" style={{ background: projColor(p.name) }}>
{p.name.trim()[0] || "?"}
</span>
<span className="label">{p.name}</span>
</div>
</li>
))}
</ul>
}
}}
>
{!currentId && <option value="" disabled />}
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
<Icon name="chevd" />
</span>
{onOpenSettings && (
<button
id="project-settings-btn"
className="icon-btn2"
title="Project settings"
aria-label="Project settings"
onClick={onOpenSettings}
>
<Icon name="gear" />
</button>
)}
</div>
</nav>
);
}
@@ -113,11 +113,9 @@ export function AppShell(props: {
export function VaultHeader(props: {
name: string;
onHome?: () => void; // hub: the project name doubles as a home link
showSignout?: boolean; // volume mode: sign-out stays in the header (no org bar)
admin?: { pending: number; onClick: () => void }; // hub admins only
projectSettings?: { onClick: () => void }; // hub: settings for the open project
showSignout?: boolean; // volume mode: sign-out stays in the header (no account bar)
}) {
const { name, onHome, showSignout, admin, projectSettings } = props;
const { name, onHome, showSignout } = props;
return (
<header id="vault">
<span id="vault-badge" aria-hidden="true">
@@ -139,31 +137,6 @@ export function VaultHeader(props: {
{name}
</span>
<div className="vault-actions">
{admin && (
<button
id="adminbar"
className="adminbar"
title={
"Hub administration — signup policy" +
(admin.pending ? " and pending approvals" : "")
}
onClick={admin.onClick}
>
<Icon name="shield" />
<span>Admin{admin.pending ? " · " + admin.pending : ""}</span>
</button>
)}
{projectSettings && (
<button
id="project-settings-btn"
className="icon-btn2"
title="Project settings"
aria-label="Project settings"
onClick={projectSettings.onClick}
>
<Icon name="gear" />
</button>
)}
{showSignout && (
<a id="signout" href="/auth/logout" title="Sign out" aria-label="Sign out">
<Icon name="power" />
+55 -23
View File
@@ -78,20 +78,13 @@ button, input, a.btn { font-family: inherit; }
}
#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 {
#vault #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; }
#vault #signout:hover, .icon-btn2:hover { color: var(--text); background: var(--hover); }
#vault #signout .ico, .icon-btn2 .ico { width: 16px; height: 16px; }
/* projects */
#projects { flex: none; max-height: 32%; overflow-y: auto; padding: 10px 8px 8px; border-bottom: 1px solid var(--border); }
@@ -143,19 +136,58 @@ button, input, a.btn { font-family: inherit; }
#tree li.collapsed > .row .chev { transform: rotate(-90deg); }
/* 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;
/* ---- project switcher ---- */
.proj-row { display: flex; align-items: center; gap: 4px; padding: 0 10px 4px 12px; }
.proj-select-wrap { position: relative; flex: 1; min-width: 0; display: flex; align-items: center; }
.proj-select-wrap .proj-mark { position: absolute; left: 9px; pointer-events: none; }
.proj-select-wrap > .ico { position: absolute; right: 8px; width: 14px; height: 14px; color: var(--text-ghost); pointer-events: none; }
#project-select {
flex: 1; min-width: 0; height: 30px; padding: 0 26px 0 30px;
appearance: none; -webkit-appearance: none;
border: 1px solid var(--border); border-radius: var(--r-ctl);
background: var(--surface); color: var(--text);
font: inherit; 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;
#project-select:hover { background: var(--hover); border-color: var(--border-2); }
/* ---- account bar ---- */
#accountbar { position: relative; border-top: 1px solid var(--border); padding: 7px 10px; }
#account-btn {
width: 100%; display: flex; align-items: center; gap: 9px; text-align: left;
padding: 6px 8px; border: none; border-radius: 7px;
background: transparent; color: var(--text-dim); cursor: pointer; font: inherit;
}
#org-name:hover { color: var(--text); }
#orgbar { gap: 4px; }
#orgbar #org-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#account-btn:hover { background: var(--hover); color: var(--text); }
#account-btn .avatar {
width: 26px; height: 26px; flex: none; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
color: #fff; font-size: 12px; font-weight: 700;
}
#account-btn .acct { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
#account-btn .acct b { font-size: 12.5px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#account-btn .acct small { font-size: 11px; color: var(--text-ghost); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#account-btn > .ico { width: 14px; height: 14px; color: var(--text-ghost); }
#account-menu {
position: absolute; left: 10px; right: 10px; bottom: calc(100% + 4px);
padding: 5px; border: 1px solid var(--border-2); border-radius: 9px;
background: var(--surface); box-shadow: 0 10px 32px rgba(0, 0, 0, .35);
display: flex; flex-direction: column; z-index: 30;
}
#account-menu .menu-sec {
padding: 7px 9px 3px; font-size: 10.5px; font-weight: 600; letter-spacing: .04em;
text-transform: uppercase; color: var(--text-ghost);
}
#account-menu [role="menuitem"] {
display: flex; align-items: center; gap: 8px; padding: 7px 9px;
border: none; border-radius: 6px; background: transparent; text-align: left;
color: var(--text-dim); font: inherit; font-size: 12.5px; cursor: pointer; text-decoration: none;
}
#account-menu [role="menuitem"]:hover { background: var(--hover); color: var(--text); }
#account-menu [role="menuitem"] b { font-weight: 600; }
#account-menu [role="menuitem"] .ico { width: 15px; height: 15px; }
#account-menu #signout { color: var(--danger, #e5534b); }
#account-menu #signout:hover { color: var(--danger, #e5534b); background: var(--hover); }
/* ---- main pane ---- */
#main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
@@ -429,9 +461,9 @@ button, input, a.btn { font-family: inherit; }
#meta { display: none; }
#crumb { flex: 1; }
#vault { padding: 0 8px 0 12px; }
.icon-btn2, #signout, .adminbar { min-width: 44px; min-height: 44px; }
#tree li > .row, #projects .row { height: 44px; }
#org-name { min-height: 44px; }
.icon-btn2, #signout, #tree li > .row, #projects .row { height: 44px; }
#account-btn { min-height: 44px; }
#project-select { min-height: 44px; }
.nav-add { min-width: 44px; min-height: 44px; }
.markdown, .admin, .onboard, .history, .dirlist { max-width: 100%; }
.markdown table, pre.plain { display: block; overflow-x: auto; max-width: 100%; }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BearDrive</title>
<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>">
<script type="module" crossorigin src="/assets/index-CGPKgglj.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DbihATmy.css">
<script type="module" crossorigin src="/assets/index-lt5n9mM7.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BWf7Qnlg.css">
</head>
<body>
<div id="root"></div>