Refactor login page layout and styles for improved branding and accessibility

This commit is contained in:
Leivur R. Djurhuus 2026-02-28 22:34:23 -06:00
parent 6422428333
commit 4eb9684291
7 changed files with 205 additions and 139 deletions

View file

@ -1,13 +1,6 @@
import { auth, signIn } from "@/lib/auth";
import { redirect } from "next/navigation";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export default async function LoginPage() {
const session = await auth();
@ -17,69 +10,110 @@ export default async function LoginPage() {
}
return (
<div className="flex min-h-screen items-center justify-center bg-[var(--muted)]">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">
HP CG Production Tracker
</CardTitle>
<CardDescription>Sign in to manage your production pipeline</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<form
action={async () => {
"use server";
await signIn("google", { redirectTo: "/dashboard" });
}}
>
<Button type="submit" variant="outline" className="w-full">
<GoogleIcon />
Sign in with Google
</Button>
</form>
<form
action={async () => {
"use server";
await signIn("microsoft-entra-id", { redirectTo: "/dashboard" });
}}
>
<Button type="submit" variant="outline" className="w-full">
<MicrosoftIcon />
Sign in with Microsoft
</Button>
</form>
</CardContent>
</Card>
<div className="flex min-h-screen bg-[var(--background)]">
{/* Left panel — green brand block */}
<div className="hidden w-[40%] flex-col justify-between bg-[var(--primary)] p-10 md:flex">
<div>
<p className="text-[9px] font-bold tracking-[0.2em] uppercase text-[var(--primary-foreground)]/60">
Oliver Agency
</p>
</div>
<div>
<h1 className="font-heading text-4xl font-black leading-[1.05] tracking-[-0.03em] text-[var(--primary-foreground)]">
HP CG<br />Production<br />Tracker
</h1>
<p className="mt-4 text-[11px] font-medium tracking-[0.06em] uppercase text-[var(--primary-foreground)]/60">
Pipeline management for CG production
</p>
</div>
<div>
<p className="text-[9px] font-semibold tracking-[0.15em] uppercase text-[var(--primary-foreground)]/40">
Brandtech Group
</p>
</div>
</div>
{/* Right panel — sign in */}
<div className="flex flex-1 flex-col items-center justify-center px-8 py-12">
<div className="w-full max-w-[320px]">
{/* Mobile wordmark */}
<div className="mb-10 md:hidden">
<h1 className="font-heading text-2xl font-black tracking-[-0.02em]">
HP CG Production Tracker
</h1>
<p className="mt-1 text-[10px] font-semibold tracking-[0.1em] uppercase text-[var(--muted-foreground)]">
Oliver Agency
</p>
</div>
<div className="mb-8">
<h2 className="font-heading text-lg font-black tracking-[-0.02em]">
Sign in
</h2>
<p className="mt-1 text-xs text-[var(--muted-foreground)]">
Use your Oliver or HP work account to continue.
</p>
</div>
<div className="space-y-2">
<form
action={async () => {
"use server";
await signIn("google", { redirectTo: "/dashboard" });
}}
>
<Button
type="submit"
variant="outline"
className="w-full border-[var(--border)] text-[11px] font-semibold tracking-[0.06em] uppercase hover:bg-[var(--foreground)] hover:text-[var(--background)] transition-colors"
>
<GoogleIcon />
Continue with Google
</Button>
</form>
<form
action={async () => {
"use server";
await signIn("microsoft-entra-id", { redirectTo: "/dashboard" });
}}
>
<Button
type="submit"
variant="outline"
className="w-full border-[var(--border)] text-[11px] font-semibold tracking-[0.06em] uppercase hover:bg-[var(--foreground)] hover:text-[var(--background)] transition-colors"
>
<MicrosoftIcon />
Continue with Microsoft
</Button>
</form>
</div>
<div className="mt-10 border-t pt-6">
<p className="text-[9px] font-semibold tracking-[0.12em] uppercase text-[var(--muted-foreground)]/60">
© {new Date().getFullYear()} Oliver Agency · Brandtech Group
</p>
</div>
</div>
</div>
</div>
);
}
function GoogleIcon() {
return (
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
<svg className="mr-2 h-3.5 w-3.5 shrink-0" viewBox="0 0 24 24">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4" />
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
</svg>
);
}
function MicrosoftIcon() {
return (
<svg className="mr-2 h-4 w-4" viewBox="0 0 23 23">
<svg className="mr-2 h-3.5 w-3.5 shrink-0" viewBox="0 0 23 23">
<path fill="#f35325" d="M1 1h10v10H1z" />
<path fill="#81bc06" d="M12 1h10v10H12z" />
<path fill="#05a6f0" d="M1 12h10v10H1z" />

View file

@ -6,100 +6,103 @@
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
/* Border radius */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
/* Border radius — Oliver Agency uses 0px (sharp corners throughout) */
--radius-sm: 0px;
--radius-md: 0px;
--radius-lg: 0px;
--radius-xl: 0px;
--radius-2xl: 0px;
--radius-full: 2px;
/* Sidebar */
--sidebar-width: 256px;
--sidebar-width-collapsed: 64px;
--sidebar-width: 240px;
--sidebar-width-collapsed: 56px;
}
/*
* Light mode (default)
* Oliver Agency / Brandtech Group inspired palette
* Light mode Oliver Agency palette
* Pure white + pure black + forest green (#08402c)
* Confirmed from oliver.agency source
*/
:root {
--background: #ffffff;
--foreground: #0a0a0a;
--muted: #f5f5f5;
--muted-foreground: #6b7280;
--border: #e5e7eb;
--input: #e5e7eb;
--foreground: #000000;
--muted: #f5f5f4;
--muted-foreground: #6b6b6b;
--border: #d4d4d4;
--input: #d4d4d4;
--ring: #08402c;
--primary: #08402c;
--primary-foreground: #ffffff;
--secondary: #f5f5f5;
--secondary-foreground: #0a0a0a;
--secondary: #f5f5f4;
--secondary-foreground: #000000;
--accent: #ee5540;
--accent-foreground: #ffffff;
--destructive: #dc2626;
--destructive: #cc2200;
--destructive-foreground: #ffffff;
--card: #ffffff;
--card-foreground: #0a0a0a;
--card-foreground: #000000;
--popover: #ffffff;
--popover-foreground: #0a0a0a;
--popover-foreground: #000000;
/* Status colors */
--status-blocked: #dc2626;
--status-not-started: #6b7280;
--status-in-progress: #2563eb;
--status-in-review: #d97706;
--status-approved: #16a34a;
--status-skipped: #9ca3af;
/* Status colors — calibrated for Oliver's precision palette */
--status-blocked: #cc2200;
--status-not-started: #8c8c8c;
--status-in-progress: #1a56db;
--status-in-review: #b45309;
--status-approved: #08402c;
--status-skipped: #b3b3b3;
/* Chart colors */
--chart-1: #08402c;
--chart-2: #2563eb;
--chart-3: #d97706;
--chart-2: #1a56db;
--chart-3: #b45309;
--chart-4: #ee5540;
--chart-5: #6b7280;
--chart-5: #8c8c8c;
}
/*
* Dark mode
* Dark mode deep charcoal + bright green accent
*/
.dark {
--background: #0a0a0a;
--foreground: #f5f5f5;
--muted: #171717;
--muted-foreground: #9ca3af;
--foreground: #fafafa;
--muted: #141414;
--muted-foreground: #a3a3a3;
--border: #262626;
--input: #262626;
--ring: #0fa968;
--ring: #22c55e;
--primary: #0fa968;
--primary-foreground: #ffffff;
--secondary: #171717;
--secondary-foreground: #f5f5f5;
--primary-foreground: #000000;
--secondary: #1a1a1a;
--secondary-foreground: #fafafa;
--accent: #f2725e;
--accent-foreground: #ffffff;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
--card: #141414;
--card-foreground: #f5f5f5;
--popover: #141414;
--popover-foreground: #f5f5f5;
--card: #111111;
--card-foreground: #fafafa;
--popover: #111111;
--popover-foreground: #fafafa;
/* Status colors */
--status-blocked: #ef4444;
--status-not-started: #9ca3af;
--status-blocked: #f87171;
--status-not-started: #a3a3a3;
--status-in-progress: #60a5fa;
--status-in-review: #fbbf24;
--status-approved: #4ade80;
--status-skipped: #6b7280;
--status-skipped: #525252;
/* Chart colors */
--chart-1: #0fa968;
--chart-2: #60a5fa;
--chart-3: #fbbf24;
--chart-4: #f2725e;
--chart-5: #9ca3af;
--chart-5: #a3a3a3;
}
@layer base {
@ -109,23 +112,26 @@
body {
@apply bg-[var(--background)] text-[var(--foreground)] font-sans antialiased;
font-feature-settings: "kern" 1, "liga" 1;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
letter-spacing: -0.02em;
font-weight: 700;
}
/* Focus-visible ring for keyboard navigation */
/* Focus-visible ring */
:focus-visible {
@apply outline-2 outline-offset-2 outline-[var(--ring)];
}
/* Remove default focus outlines for mouse users */
:focus:not(:focus-visible) {
outline: none;
}
/* Screen-reader only utility */
/* Screen-reader only */
.sr-only {
position: absolute;
width: 1px;
@ -137,4 +143,16 @@
white-space: nowrap;
border-width: 0;
}
/*
* Oliver Agency typographic signature:
* uppercase + wide tracking for labels, nav, section headers
*/
.label-upper {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted-foreground);
}
}

View file

@ -32,25 +32,28 @@ export function Breadcrumbs() {
});
return (
<nav aria-label="Breadcrumb" className="flex items-center gap-1 text-sm">
<nav aria-label="Breadcrumb" className="flex items-center gap-1">
<Link
href="/dashboard"
className="text-[var(--muted-foreground)] transition-colors hover:text-[var(--foreground)]"
aria-label="Home"
>
<Home className="h-4 w-4" />
<Home className="h-3.5 w-3.5" />
</Link>
{crumbs.map((crumb) => (
<Fragment key={crumb.href}>
<ChevronRight className="h-3 w-3 text-[var(--muted-foreground)]" aria-hidden="true" />
<ChevronRight className="h-3 w-3 text-[var(--border)]" aria-hidden="true" />
{crumb.isLast ? (
<span className="font-medium text-[var(--foreground)]" aria-current="page">
<span
className="text-[11px] font-semibold tracking-[0.04em] uppercase text-[var(--foreground)]"
aria-current="page"
>
{crumb.label}
</span>
) : (
<Link
href={crumb.href}
className="text-[var(--muted-foreground)] transition-colors hover:text-[var(--foreground)]"
className="text-[11px] font-semibold tracking-[0.04em] uppercase text-[var(--muted-foreground)] transition-colors hover:text-[var(--foreground)]"
>
{crumb.label}
</Link>

View file

@ -41,7 +41,7 @@ function NavLinks({
return (
<>
<nav className="flex-1 space-y-1 p-2" aria-label="Main navigation">
<nav className="flex-1 py-3" aria-label="Main navigation">
{navItems.map((item) => {
const isActive =
pathname === item.href || pathname.startsWith(item.href + "/");
@ -52,11 +52,11 @@ function NavLinks({
href={item.href}
onClick={onNavigate}
className={cn(
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
"flex items-center gap-3 px-4 py-2 text-[11px] font-semibold tracking-[0.08em] uppercase transition-colors",
isActive
? "bg-[var(--primary)] text-[var(--primary-foreground)]"
: "text-[var(--muted-foreground)] hover:bg-[var(--background)] hover:text-[var(--foreground)]",
collapsed && "justify-center px-2"
? "border-l-2 border-[var(--primary)] bg-[var(--background)] text-[var(--foreground)] pl-[calc(1rem-2px)]"
: "border-l-2 border-transparent text-[var(--muted-foreground)] hover:bg-[var(--background)] hover:text-[var(--foreground)]",
collapsed && "justify-center border-l-0 px-0 pl-0"
)}
>
<Icon className="h-4 w-4 shrink-0" />
@ -79,7 +79,7 @@ function NavLinks({
<Separator />
<nav className="space-y-1 p-2" aria-label="Settings">
<nav className="py-3" aria-label="Settings">
{bottomItems.map((item) => {
const isActive = pathname === item.href;
const Icon = item.icon;
@ -89,11 +89,11 @@ function NavLinks({
href={item.href}
onClick={onNavigate}
className={cn(
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
"flex items-center gap-3 px-4 py-2 text-[11px] font-semibold tracking-[0.08em] uppercase transition-colors",
isActive
? "bg-[var(--primary)] text-[var(--primary-foreground)]"
: "text-[var(--muted-foreground)] hover:bg-[var(--background)] hover:text-[var(--foreground)]",
collapsed && "justify-center px-2"
? "border-l-2 border-[var(--primary)] bg-[var(--background)] text-[var(--foreground)] pl-[calc(1rem-2px)]"
: "border-l-2 border-transparent text-[var(--muted-foreground)] hover:bg-[var(--background)] hover:text-[var(--foreground)]",
collapsed && "justify-center border-l-0 px-0 pl-0"
)}
>
<Icon className="h-4 w-4 shrink-0" />
@ -125,29 +125,37 @@ export function Sidebar() {
<aside
className={cn(
"hidden h-screen flex-col border-r bg-[var(--muted)] transition-all duration-200 md:flex",
isCollapsed ? "w-16" : "w-64"
isCollapsed ? "w-14" : "w-60"
)}
role="navigation"
aria-label="Sidebar"
>
{/* Logo / Title */}
<div className="flex h-14 items-center gap-2 border-b px-4">
{/* Logo / wordmark */}
<div className={cn(
"flex h-14 items-center border-b",
isCollapsed ? "justify-center px-2" : "px-4"
)}>
{!isCollapsed && (
<span className="font-heading text-sm font-bold tracking-tight">
HP Tracker
</span>
<div className="flex flex-col">
<span className="font-heading text-[11px] font-black tracking-[0.15em] uppercase text-[var(--foreground)]">
HP Production
</span>
<span className="text-[9px] font-semibold tracking-[0.12em] uppercase text-[var(--muted-foreground)]">
by Oliver Agency
</span>
</div>
)}
<Button
variant="ghost"
size="icon"
className={cn("h-8 w-8", isCollapsed ? "mx-auto" : "ml-auto")}
className={cn("h-7 w-7 shrink-0", !isCollapsed && "ml-auto")}
onClick={toggle}
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>
{isCollapsed ? (
<PanelLeft className="h-4 w-4" />
<PanelLeft className="h-3.5 w-3.5" />
) : (
<PanelLeftClose className="h-4 w-4" />
<PanelLeftClose className="h-3.5 w-3.5" />
)}
</Button>
</div>
@ -180,11 +188,14 @@ export function MobileSidebar() {
return (
<Sheet open={isMobileOpen} onOpenChange={setMobileOpen}>
<SheetContent side="left" className="w-64 p-0">
<SheetContent side="left" className="w-60 p-0">
<SheetTitle className="sr-only">Navigation</SheetTitle>
<div className="flex h-14 items-center border-b px-4">
<span className="font-heading text-sm font-bold tracking-tight">
HP Tracker
<div className="flex h-14 flex-col justify-center border-b px-4">
<span className="font-heading text-[11px] font-black tracking-[0.15em] uppercase text-[var(--foreground)]">
HP Production
</span>
<span className="text-[9px] font-semibold tracking-[0.12em] uppercase text-[var(--muted-foreground)]">
by Oliver Agency
</span>
</div>
<div className="flex h-[calc(100vh-3.5rem)] flex-col">

View file

@ -52,7 +52,7 @@ export function Topbar() {
const items = (notifications as any[]) ?? [];
return (
<header className="flex h-14 items-center justify-between border-b bg-[var(--background)] px-4 md:px-6" role="banner">
<header className="flex h-14 items-center justify-between border-b bg-[var(--background)] px-4 md:px-5" role="banner">
<div className="flex items-center gap-2">
<MobileMenuButton />
<Breadcrumbs />
@ -89,8 +89,8 @@ export function Topbar() {
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-80 p-0">
<div className="flex items-center justify-between border-b px-3 py-2">
<span className="text-xs font-semibold uppercase text-[var(--muted-foreground)]">
<div className="flex items-center justify-between border-b px-3 py-2.5">
<span className="text-[10px] font-semibold tracking-[0.1em] uppercase text-[var(--muted-foreground)]">
Notifications
</span>
{unreadCount > 0 && (

View file

@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
"inline-flex items-center justify-center border border-transparent px-2 py-0.5 text-[10px] font-semibold tracking-[0.06em] uppercase w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {

View file

@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
"bg-card text-card-foreground flex flex-col gap-6 border py-6",
className
)}
{...props}