Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 15 additions & 24 deletions web/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import {
Account,
AppNav,
MobileNav,
type NavInstallation,
} from "@/components/app-nav";
import { Brand } from "@/components/brand";
import { ThemeToggle } from "@/components/theme-toggle";
import { cookies } from "next/headers";
import { MobileNav, type NavInstallation } from "@/components/app-nav";
import { DashboardSidebar } from "@/components/dashboard-sidebar";
import { api } from "@/lib/api";
import { accountAvatar } from "@/lib/avatar";
import { readSession } from "@/lib/auth";
import { getEnv } from "@/lib/env";
import { SIDEBAR_COLLAPSED, SIDEBAR_COOKIE } from "@/lib/sidebar";
import type { Installation, User } from "@/lib/types";

export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const [user, installations, session] = await Promise.all([
const [user, installations, session, cookieStore] = await Promise.all([
api<User>("/api/v1/me"),
api<Installation[]>("/api/v1/installations"),
readSession(),
cookies(),
]);
// GitHub sign-in stores the display name and avatar in the session cookie.
const profile = {
Expand All @@ -40,24 +37,18 @@ export default async function DashboardLayout({
}));
const installUrl = `https://github.com/apps/${getEnv().appSlug}/installations/new`;
return (
<div className="min-h-dvh md:grid md:grid-cols-[15rem_minmax(0,1fr)]">
<div className="min-h-dvh md:grid md:grid-cols-[auto_minmax(0,1fr)]">
<a className="skip-link" href="#main-content">
Skip to content
</a>
<div className="hidden border-r bg-sidebar md:block">
<aside className="sticky top-0 flex h-dvh flex-col">
<div className="flex h-14 items-center justify-between px-4">
<Brand href="/installations" />
<ThemeToggle />
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-2 py-3">
<AppNav installations={nav} installUrl={installUrl} />
</div>
<div className="border-t p-3">
<Account {...profile} />
</div>
</aside>
</div>
<DashboardSidebar
defaultCollapsed={
cookieStore.get(SIDEBAR_COOKIE)?.value === SIDEBAR_COLLAPSED
}
installations={nav}
installUrl={installUrl}
profile={profile}
/>
<div className="min-w-0">
<MobileNav
installations={nav}
Expand Down
44 changes: 25 additions & 19 deletions web/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,27 @@
--success-foreground: var(--color-emerald-700);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-700);
--accent: --alpha(var(--color-black) / 4%);
--accent: --alpha(var(--color-black) / 6%);
--accent-foreground: var(--color-neutral-800);
--background: var(--color-white);
--border: --alpha(var(--color-black) / 8%);
--border: --alpha(var(--color-black) / 12%);
--card: var(--color-white);
--card-foreground: var(--color-neutral-800);
--destructive: var(--color-red-500);
--foreground: var(--color-neutral-800);
--input: --alpha(var(--color-black) / 10%);
--input: --alpha(var(--color-black) / 24%);
--muted: --alpha(var(--color-black) / 4%);
--muted-foreground: color-mix(
in srgb,
var(--color-neutral-500) 90%,
var(--color-black)
);
--muted-foreground: var(--color-neutral-600);
--popover: var(--color-white);
--popover-foreground: var(--color-neutral-800);
--primary: var(--color-neutral-800);
--primary-foreground: var(--color-neutral-50);
--brand: var(--color-emerald-700);
--ring: var(--brand);
--sidebar: var(--color-neutral-50);
--secondary: --alpha(var(--color-black) / 4%);
--sidebar: var(--color-neutral-100);
--sidebar-active: --alpha(var(--color-emerald-700) / 10%);
--sidebar-active-foreground: var(--color-emerald-800);
--secondary: --alpha(var(--color-black) / 7%);
--secondary-foreground: var(--color-neutral-800);
}

Expand All @@ -44,33 +42,39 @@
--success-foreground: var(--color-emerald-400);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-400);
--accent: --alpha(var(--color-white) / 4%);
--accent: --alpha(var(--color-white) / 7%);
--accent-foreground: var(--color-neutral-100);
--background: color-mix(
in srgb,
var(--color-neutral-950) 95%,
var(--color-neutral-950) 97%,
var(--color-white)
);
--border: --alpha(var(--color-white) / 6%);
--card: color-mix(in srgb, var(--background) 98%, var(--color-white));
--border: --alpha(var(--color-white) / 12%);
--card: color-mix(in srgb, var(--background) 97%, var(--color-white));
--card-foreground: var(--color-neutral-100);
--destructive: color-mix(
in srgb,
var(--color-red-500) 90%,
var(--color-white)
);
--foreground: var(--color-neutral-100);
--input: --alpha(var(--color-white) / 8%);
--input: --alpha(var(--color-white) / 20%);
--muted: --alpha(var(--color-white) / 4%);
--muted-foreground: var(--color-neutral-400);
--popover: color-mix(in srgb, var(--background) 98%, var(--color-white));
--muted-foreground: color-mix(
in srgb,
var(--color-neutral-400) 80%,
var(--color-white)
);
--popover: color-mix(in srgb, var(--background) 97%, var(--color-white));
--popover-foreground: var(--color-neutral-100);
--primary: var(--color-neutral-100);
--primary-foreground: var(--color-neutral-800);
--brand: var(--color-emerald-400);
--ring: var(--brand);
--sidebar: color-mix(in srgb, var(--background) 97%, var(--color-white));
--secondary: --alpha(var(--color-white) / 4%);
--sidebar: color-mix(in srgb, var(--background) 96%, var(--color-white));
--sidebar-active: --alpha(var(--color-emerald-400) / 12%);
--sidebar-active-foreground: var(--color-emerald-300);
--secondary: --alpha(var(--color-white) / 9%);
--secondary-foreground: var(--color-neutral-100);
}

Expand All @@ -79,6 +83,8 @@
--font-mono: var(--font-geist-mono), ui-monospace, monospace;
--color-brand: var(--brand);
--color-sidebar: var(--sidebar);
--color-sidebar-active: var(--sidebar-active);
--color-sidebar-active-foreground: var(--sidebar-active-foreground);
--color-destructive-foreground: var(--destructive-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
Expand Down
16 changes: 16 additions & 0 deletions web/components/accessibility.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { SidebarToggle } from "./sidebar-toggle";
import { Autocomplete, AutocompleteInput } from "./ui/autocomplete";
import { Button } from "./ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";
Expand Down Expand Up @@ -43,3 +44,18 @@ test("relative times expose exact UTC time without hover", () => {
const html = renderToStaticMarkup(<TimeAgo value="2026-11-01T10:00:00Z" />);
expect(html).toContain('aria-label="1 Nov 2026, 10:00 UTC"');
});

test("sidebar toggle names its action and exposes the expanded state", () => {
const expanded = renderToStaticMarkup(
<SidebarToggle collapsed={false} controls="sidebar" onToggle={() => {}} />,
);
expect(expanded).toContain('aria-label="Collapse sidebar"');
expect(expanded).toContain('aria-expanded="true"');
expect(expanded).toContain('aria-controls="sidebar"');
expect(expanded).toContain('aria-keyshortcuts="Control+B Meta+B"');
const collapsed = renderToStaticMarkup(
<SidebarToggle collapsed controls="sidebar" onToggle={() => {}} />,
);
expect(collapsed).toContain('aria-label="Expand sidebar"');
expect(collapsed).toContain('aria-expanded="false"');
});
Loading
Loading