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
6 changes: 1 addition & 5 deletions src/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ export default async function DashboardLayout({
return (
<Providers>
<RoutePrefetcher />
<DashboardShell
companyName={session.user.name ?? ""}
userEmail={session.user.email ?? ""}
openAlerts={openAlerts}
>
<DashboardShell openAlerts={openAlerts}>
{children}
</DashboardShell>
</Providers>
Expand Down
4 changes: 2 additions & 2 deletions src/app/(dashboard)/loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ export default function DashboardLoading() {

<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="rounded-xl border border-border bg-card p-5">
<div key={i} className="rounded-xl border border-border/60 bg-card p-5">
<div className="mb-3 h-3 w-20 animate-pulse rounded bg-muted" />
<div className="h-8 w-16 animate-pulse rounded bg-muted" />
</div>
))}
</div>

<div className="flex-1 rounded-xl border border-border bg-card p-5">
<div className="flex-1 rounded-xl border border-border/60 bg-card p-5">
<div className="mb-4 h-4 w-32 animate-pulse rounded bg-muted" />
<div className="space-y-2.5">
{Array.from({ length: 6 }).map((_, i) => (
Expand Down
4 changes: 2 additions & 2 deletions src/components/alerts/AlertTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export function AlertTable({
</div>

{filtered.length === 0 ? (
<div className="rounded-xl border border-border bg-card py-12 text-center text-sm text-muted-foreground">
<div className="rounded-xl border border-border/60 bg-card py-12 text-center text-sm text-muted-foreground">
No alerts found
</div>
) : (
Expand All @@ -181,7 +181,7 @@ export function AlertTable({
<div
key={a.id}
className={cn(
"flex items-center gap-4 rounded-xl border border-l-4 border-border bg-card px-4 py-3",
"flex items-center gap-4 rounded-xl border border-l-4 border-border/60 bg-card px-4 py-3",
borderBySeverity[a.severity],
a.status === "RESOLVED" && "opacity-60"
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/credentials/ApiCredentials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function ApiCredentials({ initial, isAdmin }: Props) {
return (
<div
key={meta.id}
className="flex items-start gap-4 rounded-xl border border-border bg-card px-4 py-4"
className="flex items-start gap-4 rounded-xl border border-border/60 bg-card px-4 py-4"
>
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
<KeyRound className="size-4" />
Expand Down
2 changes: 1 addition & 1 deletion src/components/credentials/Webhooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function Webhooks({ initial, isAdmin }: Props) {
}

return (
<div className="rounded-xl border border-border bg-card p-5">
<div className="rounded-xl border border-border/60 bg-card p-5">
<div className="mb-1 flex items-center gap-2">
<Webhook className="size-4 text-muted-foreground" />
<h3 className="text-sm font-semibold text-foreground">Notification webhooks</h3>
Expand Down
4 changes: 2 additions & 2 deletions src/components/dashboard/SetupChecklist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function SetupChecklist({
<li
key={step.href}
className={cn(
"flex items-start gap-4 rounded-xl border border-border bg-card p-4",
"flex items-start gap-4 rounded-xl border border-border/60 bg-card p-4",
step.done && "opacity-60"
)}
>
Expand Down Expand Up @@ -110,7 +110,7 @@ export function SetupChecklist({
</p>
)}

<div className="mt-8 rounded-xl border border-border bg-muted/30 p-4">
<div className="mt-8 rounded-xl border border-border/60 bg-muted/30 p-4">
<p className="text-sm text-foreground">Thanks for trying DataShield.</p>
<p className="mt-1 text-xs text-muted-foreground">
It is an open-source project built to make breach exposure visible and actionable.
Expand Down
2 changes: 1 addition & 1 deletion src/components/dashboard/WidgetLibrary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export function WidgetLibrary({
"group overflow-hidden rounded-xl border transition-all duration-200",
isEnabled
? "border-primary/40 shadow-sm hover:border-primary/60"
: "border-border hover:border-muted-foreground/40"
: "border-border/60 hover:border-muted-foreground/40"
)}
>
{/* Preview */}
Expand Down
2 changes: 1 addition & 1 deletion src/components/employees/EmployeeTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export function EmployeeTable({ data }: { data: EmployeeRow[] }) {
</div>

{/* Table */}
<div className="rounded-xl border border-border bg-card overflow-hidden">
<div className="rounded-xl border border-border/60 bg-card overflow-hidden">
<table className="w-full text-sm">
<thead className="border-b border-border bg-muted/30">
{table.getHeaderGroups().map((hg) => (
Expand Down
17 changes: 3 additions & 14 deletions src/components/layout/DashboardShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,20 @@
import type { ReactNode } from "react"
import { Sidebar } from "./Sidebar"
import { Topbar } from "./Topbar"
import { SidebarProvider } from "./SidebarContext"

interface DashboardShellProps {
companyName: string
userEmail: string
openAlerts: number
children: ReactNode
}

function Shell({ companyName, userEmail, openAlerts, children }: DashboardShellProps) {
export function DashboardShell({ openAlerts, children }: DashboardShellProps) {
return (
<div className="flex h-screen overflow-hidden">
<Sidebar companyName={companyName} userEmail={userEmail} openAlerts={openAlerts} />
<div className="flex h-screen overflow-hidden pl-16">
<Sidebar openAlerts={openAlerts} />
<div className="flex flex-1 flex-col overflow-hidden">
<Topbar />
<main className="flex flex-1 flex-col min-h-0 overflow-hidden">{children}</main>
</div>
</div>
)
}

export function DashboardShell(props: DashboardShellProps) {
return (
<SidebarProvider>
<Shell {...props} />
</SidebarProvider>
)
}
148 changes: 56 additions & 92 deletions src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
"use client"

import type { ReactNode } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { signOut } from "next-auth/react"
import { useEffect, useRef } from "react"
import { cn } from "@/lib/utils"
import { useSidebar } from "./SidebarContext"

// Auto-close the floating sidebar after this idle delay (no pointer over it).
const AUTO_CLOSE_MS = 4000
import {
LayoutDashboard,
Users,
Expand All @@ -19,7 +15,7 @@ import {
KeyRound,
Send,
LogOut,
PanelLeftClose,
ShieldCheck,
} from "lucide-react"

const navItems = [
Expand All @@ -33,115 +29,83 @@ const navItems = [
{ href: "/notifications", label: "Notifications", icon: Send },
]

// Layers (within the aside stacking context): labels z-10 sit UNDER the rail
// surface z-20, icons/pills z-30 sit ABOVE it. A label starts tucked behind the
// surface and slides right on hover, so it emerges from under the panel.
function RailLabel({ children }: { children: ReactNode }) {
return (
<span className="pointer-events-none absolute left-full top-1/2 z-10 -ml-4 -translate-y-1/2">
<span className="block -translate-x-full whitespace-nowrap rounded-md border border-border bg-popover py-2.5 pl-11 pr-5 text-sm font-medium text-popover-foreground transition-transform duration-500 ease-[cubic-bezier(0.36,0,0.66,-0.56)] group-hover/item:translate-x-0 group-hover/item:duration-150 group-hover/item:ease-out">
{children}
</span>
</span>
)
}

interface SidebarProps {
companyName: string
userEmail: string
openAlerts: number
}

export function Sidebar({ companyName, userEmail, openAlerts }: SidebarProps) {
export function Sidebar({ openAlerts }: SidebarProps) {
const pathname = usePathname()
const { open, toggle, close } = useSidebar()
const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
const asideRef = useRef<HTMLElement>(null)

const clearTimer = () => {
if (timer.current) clearTimeout(timer.current)
timer.current = null
}
const startTimer = () => {
clearTimer()
timer.current = setTimeout(close, AUTO_CLOSE_MS)
}

// Start the idle countdown whenever the sidebar opens; hovering pauses it.
useEffect(() => {
if (open) startTimer()
else clearTimer()
return clearTimer
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])

// Close when clicking anywhere outside the panel.
useEffect(() => {
if (!open) return
const handler = (e: MouseEvent) => {
if (!asideRef.current?.contains(e.target as Node)) close()
}
document.addEventListener("mousedown", handler)
return () => document.removeEventListener("mousedown", handler)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])

return (
<aside
ref={asideRef}
onMouseEnter={clearTimer}
onMouseLeave={startTimer}
className={cn(
"fixed inset-y-0 left-0 z-40 flex h-screen w-60 flex-col border-r border-sidebar-border bg-sidebar shadow-[4px_0_24px_-6px_oklch(var(--primary)/0.18)] transition-transform duration-200 ease-out",
open ? "translate-x-0" : "-translate-x-full",
)}
>
<div className="flex h-12 items-center justify-between border-b border-sidebar-border px-4">
<span className="text-sm font-semibold text-sidebar-foreground">DataShield</span>
<button
onClick={toggle}
className="-mr-1 rounded-md p-1 text-sidebar-foreground/60 transition-colors hover:bg-sidebar-accent/50 hover:text-sidebar-foreground"
title="Close sidebar"
aria-label="Close sidebar"
>
<PanelLeftClose className="size-4" />
</button>
<aside className="fixed inset-y-0 left-0 z-40 flex h-screen w-16 flex-col">
{/* Rail shadow: painted BELOW the labels so it never tints them. */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-0 shadow-[4px_0_24px_-6px_oklch(var(--primary)/0.18)]"
/>
{/* Rail surface: opaque, painted ABOVE the labels so they hide under it. */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-20 border-r border-sidebar-border bg-sidebar"
/>

<div className="relative z-30 flex h-12 items-center justify-center border-b border-sidebar-border">
<ShieldCheck className="size-5 text-sidebar-primary" />
</div>

<nav className="flex-1 space-y-0.5 overflow-y-auto px-2 py-3">
<nav className="flex-1 space-y-1 px-2 py-3">
{navItems.map(({ href, label, icon: Icon }) => {
const active =
pathname === href || pathname.startsWith(href + "/")
return (
<Link
key={href}
href={href}
className={cn(
"flex items-center gap-2.5 rounded-md px-2.5 py-2 text-sm transition-colors",
active
? "bg-sidebar-accent text-sidebar-primary font-medium"
: "text-sidebar-foreground/60 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground"
)}
>
<Icon
<Link key={href} href={href} className="group/item relative flex">
<span
className={cn(
"size-4 shrink-0",
active ? "text-sidebar-primary" : ""
"relative z-30 flex w-full items-center justify-center rounded-md py-2.5 transition-colors",
active
? "bg-sidebar-accent text-sidebar-primary"
: "text-sidebar-foreground/60 group-hover/item:bg-sidebar-accent/50 group-hover/item:text-sidebar-foreground"
)}
>
<Icon className="size-4 shrink-0" />
{href === "/alerts" && openAlerts > 0 && (
<span className="absolute -right-0.5 -top-0.5 inline-flex min-w-4 items-center justify-center rounded-full bg-severity-critical px-1 text-[10px] font-medium leading-4 tabular-nums text-white">
{openAlerts > 9 ? "9+" : openAlerts}
</span>
)}
/>
{label}
{href === "/alerts" && openAlerts > 0 && (
<span className="ml-auto inline-flex min-w-5 items-center justify-center rounded-full bg-severity-critical px-1.5 text-xs font-medium tabular-nums text-white">
{openAlerts > 99 ? "99+" : openAlerts}
</span>
)}
</span>
<RailLabel>{label}</RailLabel>
</Link>
)
})}
</nav>

<div className="space-y-1 border-t border-sidebar-border p-3">
<div className="px-2 py-1">
<p className="truncate text-xs font-medium text-sidebar-foreground">
{companyName}
</p>
<p className="truncate text-xs text-sidebar-foreground/50">
{userEmail}
</p>
</div>
<div className="relative px-2 py-3">
<div
aria-hidden
className="absolute inset-x-2 top-0 z-30 border-t border-sidebar-border"
/>
<button
onClick={() => signOut({ callbackUrl: "/login" })}
className="flex w-full items-center gap-2.5 rounded-md px-2.5 py-2 text-sm text-sidebar-foreground/60 transition-colors hover:bg-sidebar-accent/50 hover:text-sidebar-foreground"
className="group/item relative flex w-full"
>
<LogOut className="size-4 shrink-0" />
Sign out
<span className="relative z-30 flex w-full items-center justify-center rounded-md py-2.5 text-sidebar-foreground/60 transition-colors group-hover/item:bg-sidebar-accent/50 group-hover/item:text-sidebar-foreground">
<LogOut className="size-4 shrink-0" />
</span>
<RailLabel>Sign out</RailLabel>
</button>
</div>
</aside>
Expand Down
28 changes: 0 additions & 28 deletions src/components/layout/SidebarContext.tsx

This file was deleted.

13 changes: 0 additions & 13 deletions src/components/layout/Topbar.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
"use client"

import { usePathname } from "next/navigation"
import { PanelLeft } from "lucide-react"
import { useSidebar } from "./SidebarContext"

const pageTitles: Record<string, string> = {
"/dashboard": "Dashboard",
Expand All @@ -23,20 +21,9 @@ function getTitle(pathname: string): string {
export function Topbar() {
const pathname = usePathname()
const title = getTitle(pathname)
const { open, toggle } = useSidebar()

return (
<header className="flex h-12 shrink-0 items-center gap-3 border-b border-border px-6">
{!open && (
<button
onClick={toggle}
className="-ml-2 rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
title="Open sidebar"
aria-label="Open sidebar"
>
<PanelLeft className="size-4" />
</button>
)}
<h1 className="text-sm font-medium text-foreground">{title}</h1>
</header>
)
Expand Down
Loading