feat(shell): collapsible sidebar, account menu, and phone drawer
CI / verify (pull_request) Failing after 57s
CI / infrastructure-smoke (pull_request) Skipped
CI / migration-smoke (pull_request) Skipped
CI / package-release (pull_request) Skipped
CI / package-publication (pull_request) Skipped
CI / image-build (pull_request) Skipped
CI / deploy-smoke (same-origin) (pull_request) Skipped
CI / deploy-smoke (split) (pull_request) Skipped
CI / verify (pull_request) Failing after 57s
CI / infrastructure-smoke (pull_request) Skipped
CI / migration-smoke (pull_request) Skipped
CI / package-release (pull_request) Skipped
CI / package-publication (pull_request) Skipped
CI / image-build (pull_request) Skipped
CI / deploy-smoke (same-origin) (pull_request) Skipped
CI / deploy-smoke (split) (pull_request) Skipped
The Dashboard's shell now follows DESIGN.md §4 (ADR 0058): - A collapsible left sidebar replaces the header-and-column layout. It is icons and labels when expanded and icons only when collapsed; each collapsed item keeps its name, visually hidden, and shows a tooltip on hover and on keyboard focus (WCAG 2.2 §1.4.13). The Overview comes first, then the server's groups and modules. There is no top bar. - The expanded/collapsed choice is remembered in this browser's localStorage, behind a SidebarPreference interface that a synced store can replace later. - An account menu at the bottom of the sidebar follows the WAI-ARIA menu button pattern and offers only Sign out, which now lives there rather than in the header. - On phones (below 48em) the same navigation opens in a modal drawer: the page behind it is inert and focus is trapped inside it. Escape, its close button, and the backdrop close it and return focus to the menu button. Choosing a page closes it and focuses the page. Changing layout keeps the route, the desktop mode, and the mounted module. - Lucide (lucide-react 1.48.0, shell only) is Ekklesia's icon vocabulary. Every module shows one generic icon until manifest v9 lets modules declare their own; there is no per-module icon map. - The identity slot is a neutral placeholder with no inputs, waiting for branding (#101). The tooltip, menu, and drawer stay shell-local until a second consumer needs them (ADR 0039). The deploy smoke now signs out through the account menu, and drives the drawer at phone size in a real browser. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f3b7e19651
commit
af31724cb4
@@ -147,6 +147,13 @@ async function render(
|
||||
});
|
||||
}
|
||||
|
||||
/** Sign-out lives in the account menu at the bottom of the sidebar (ADR 0058). */
|
||||
async function openAccountMenu(): Promise<void> {
|
||||
await act(async () => {
|
||||
(query("host-account-menu") as HTMLButtonElement).click();
|
||||
});
|
||||
}
|
||||
|
||||
function text(): string {
|
||||
return container.textContent ?? "";
|
||||
}
|
||||
@@ -391,7 +398,7 @@ describe("Ekklesia shell — browser authentication", () => {
|
||||
await signInPending;
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(query("host-logout")).not.toBeNull();
|
||||
expect(query("host-account-menu")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("clears the password while post-sign-in principal resolution is still pending", async () => {
|
||||
@@ -424,7 +431,7 @@ describe("Ekklesia shell — browser authentication", () => {
|
||||
await principalPending;
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(query("host-logout")).not.toBeNull();
|
||||
expect(query("host-account-menu")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("submits from the password field with the Enter key", async () => {
|
||||
@@ -454,7 +461,7 @@ describe("Ekklesia shell — browser authentication", () => {
|
||||
await signInPending;
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(query("host-logout")).not.toBeNull();
|
||||
expect(query("host-account-menu")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("re-resolves principal and navigation after login without a page reload", async () => {
|
||||
@@ -467,7 +474,7 @@ describe("Ekklesia shell — browser authentication", () => {
|
||||
await submitLogin();
|
||||
|
||||
expect(query("host-login")).toBeNull();
|
||||
expect(query("host-logout")).not.toBeNull();
|
||||
expect(query("host-account-menu")).not.toBeNull();
|
||||
expect(isolatedText(query("module-container"))).toBe("example mounted");
|
||||
expect(importer).toHaveBeenCalledOnce();
|
||||
expect(platform.calls.map((call) => new URL(call.url).pathname)).toEqual([
|
||||
@@ -524,7 +531,7 @@ describe("Ekklesia shell — browser authentication", () => {
|
||||
await act(async () => {
|
||||
(retry as HTMLButtonElement).click();
|
||||
});
|
||||
expect(query("host-logout")).not.toBeNull();
|
||||
expect(query("host-account-menu")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("fails safely on a malformed successful principal response and recovers on retry", async () => {
|
||||
@@ -555,7 +562,7 @@ describe("Ekklesia shell — browser authentication", () => {
|
||||
(retry as HTMLButtonElement).click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(query("host-logout")).not.toBeNull();
|
||||
expect(query("host-account-menu")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -610,6 +617,7 @@ describe("Ekklesia shell — logout and session expiration", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
await openAccountMenu();
|
||||
await act(async () => {
|
||||
(query("host-logout") as HTMLButtonElement).click();
|
||||
await Promise.resolve();
|
||||
@@ -668,6 +676,7 @@ describe("Ekklesia shell — logout and session expiration", () => {
|
||||
if (!lateRequest) {
|
||||
throw new Error("module context was not captured");
|
||||
}
|
||||
await openAccountMenu();
|
||||
await act(async () => {
|
||||
(query("host-logout") as HTMLButtonElement).click();
|
||||
await Promise.resolve();
|
||||
@@ -707,6 +716,7 @@ describe("Ekklesia shell — logout and session expiration", () => {
|
||||
});
|
||||
await render({ apiBaseUrl, fetch: platform.fetch }, async () => artifact("example", "0.1.0"));
|
||||
|
||||
await openAccountMenu();
|
||||
await act(async () => {
|
||||
(query("host-logout") as HTMLButtonElement).click();
|
||||
await Promise.resolve();
|
||||
@@ -1024,9 +1034,10 @@ describe("Ekklesia shell — mount lifecycle", () => {
|
||||
const aside = shell.querySelector("aside") as HTMLElement;
|
||||
const main = shell.querySelector("main") as HTMLElement;
|
||||
const frame = query("module-container") as HTMLElement;
|
||||
expect(shell.style.gridTemplateColumns).toBe("15rem minmax(0, 1fr)");
|
||||
expect(shell.style.gridTemplateRows).toBe("auto minmax(0, 1fr)");
|
||||
expect(aside.style.width).toBe("15rem");
|
||||
// The sidebar sizes its own track (ADR 0058); the page track can shrink to nothing but never
|
||||
// pushes the sidebar.
|
||||
expect(shell.style.gridTemplateColumns).toBe("auto minmax(0, 1fr)");
|
||||
expect(aside.style.width).toBe("16rem");
|
||||
expect(main.style.minWidth).toBe("0");
|
||||
expect(main.style.overflowX).toBe("auto");
|
||||
expect(frame.style.minWidth).toBe("0");
|
||||
|
||||
Reference in New Issue
Block a user