diff --git a/ui/package.json b/ui/package.json index d05cd1b..bb602a6 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,8 +12,10 @@ "@base-ui/react": "^1.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.40.0", "lucide-react": "^1.17.0", "next": "16.2.6", + "next-themes": "^0.4.6", "react": "19.2.4", "react-dom": "19.2.4", "shadcn": "^4.9.0", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 0daf357..1827085 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -17,12 +17,18 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + framer-motion: + specifier: ^12.40.0 + version: 12.40.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) lucide-react: specifier: ^1.17.0 version: 1.17.0(react@19.2.4) next: specifier: 16.2.6 version: 16.2.6(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: 19.2.4 version: 19.2.4 @@ -1667,6 +1673,20 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + framer-motion@12.40.0: + resolution: {integrity: sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -2260,6 +2280,12 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + motion-dom@12.40.0: + resolution: {integrity: sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==} + + motion-utils@12.39.0: + resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2294,6 +2320,12 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + next@16.2.6: resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} engines: {node: '>=20.9.0'} @@ -4768,6 +4800,15 @@ snapshots: forwarded@0.2.0: {} + framer-motion@12.40.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + motion-dom: 12.40.0 + motion-utils: 12.39.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + fresh@2.0.0: {} fs-extra@11.3.5: @@ -5278,6 +5319,12 @@ snapshots: minimist@1.2.8: {} + motion-dom@12.40.0: + dependencies: + motion-utils: 12.39.0 + + motion-utils@12.39.0: {} + ms@2.1.3: {} msw@2.14.6(@types/node@20.19.41)(typescript@5.9.3): @@ -5315,6 +5362,11 @@ snapshots: negotiator@1.0.0: {} + next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + next@16.2.6(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.2.6 diff --git a/ui/src/app/(authenticated)/connections/detail/page.tsx b/ui/src/app/(authenticated)/connections/detail/page.tsx index 0c196cf..17306e8 100644 --- a/ui/src/app/(authenticated)/connections/detail/page.tsx +++ b/ui/src/app/(authenticated)/connections/detail/page.tsx @@ -20,7 +20,7 @@ function ConnectionDetailContent() { provider && connection ? ["authsome-connection-detail", provider, connection, principal] : null, () => fetchConnectionDetail(provider, connection, principal), ); - const { mutate: mutateDashboard } = useSWR("authsome-dashboard", fetchDashboard); + const { data: dashboard, mutate: mutateDashboard } = useSWR("authsome-dashboard", fetchDashboard); if (!provider || !connection) { return ( @@ -45,6 +45,7 @@ function ConnectionDetailContent() { data={data} onRefresh={() => { void mutate(); void mutateDashboard(); }} principal={principal} + providers={dashboard?.providers} /> ); } diff --git a/ui/src/app/(authenticated)/connections/page.tsx b/ui/src/app/(authenticated)/connections/page.tsx index 07989ac..e60b47c 100644 --- a/ui/src/app/(authenticated)/connections/page.tsx +++ b/ui/src/app/(authenticated)/connections/page.tsx @@ -14,6 +14,7 @@ export default function ConnectionsPage() { globalConnections={data.globalConnections} isAdmin={data.account.isAdmin} onRefresh={() => void mutate()} + providers={data.providers} /> ); } diff --git a/ui/src/app/(authenticated)/layout.tsx b/ui/src/app/(authenticated)/layout.tsx index ad20a68..78c2fd2 100644 --- a/ui/src/app/(authenticated)/layout.tsx +++ b/ui/src/app/(authenticated)/layout.tsx @@ -1,6 +1,6 @@ "use client"; -import { Suspense, useEffect } from "react"; +import React, { Suspense, useEffect } from "react"; import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { LogOut } from "lucide-react"; @@ -22,10 +22,10 @@ import { BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { Skeleton } from "@/components/ui/skeleton"; import { DashboardData, fetchDashboard, ProviderView } from "@/lib/authsome-api"; +import { cn } from "@/lib/utils"; type CrumbItem = { label: string; href?: string }; @@ -35,7 +35,6 @@ function pathToView(pathname: string): View { providers: "providers", connections: "connections", agents: "agents", - principal: "principals", audit: "audit", settings: "settings", }; @@ -54,9 +53,16 @@ function buildBreadcrumbs( providers: "Providers", connections: "Connections", agents: "Agents", - principal: "Principals", audit: "Audit Log", settings: "Settings", + principal: "Principals", + }; + + const settingsSubLabel: Record = { + general: "General", + security: "Security", + about: "About", + principals: "Principals", }; if (!first) return [{ label: "Dashboard" }]; @@ -64,6 +70,13 @@ function buildBreadcrumbs( const parent = navLabel[first]; if (!parent) return [{ label: "Dashboard", href: "/" }]; + if (first === "settings" && segments[1] && settingsSubLabel[segments[1]]) { + return [ + { label: "Settings", href: "/settings" }, + { label: settingsSubLabel[segments[1]] }, + ]; + } + const isDetail = segments[1] === "detail"; if (!isDetail) return [{ label: parent }]; @@ -103,9 +116,9 @@ function AppBreadcrumb({ data }: { data: DashboardData }) { {crumbs.map((crumb, i) => { const isLast = i === crumbs.length - 1; return ( - <> - {i > 0 && } - + + {i > 0 && } + {isLast ? ( {crumb.label} ) : ( @@ -114,7 +127,7 @@ function AppBreadcrumb({ data }: { data: DashboardData }) { )} - + ); })} @@ -140,32 +153,46 @@ export default function AuthenticatedLayout({ children }: { children: React.Reac if (error) return void mutate()} />; const activeView = pathToView(pathname); + const isSettingsRoute = pathname.startsWith("/settings"); return ( + + Skip to main content + -
- - - }> - - -
-
+
+
+ +
+
+ -
-
-
+
+
{children}
-
+ ); diff --git a/ui/src/app/(authenticated)/principal/page.tsx b/ui/src/app/(authenticated)/principal/page.tsx index ec39119..6f9862e 100644 --- a/ui/src/app/(authenticated)/principal/page.tsx +++ b/ui/src/app/(authenticated)/principal/page.tsx @@ -1,7 +1,5 @@ -"use client"; - -import { PrincipalsView } from "@/components/dashboard/overview-views"; +import { redirect } from "next/navigation"; export default function PrincipalPage() { - return ; + redirect("/settings/principals"); } diff --git a/ui/src/app/(authenticated)/settings/about/page.tsx b/ui/src/app/(authenticated)/settings/about/page.tsx new file mode 100644 index 0000000..a053ff7 --- /dev/null +++ b/ui/src/app/(authenticated)/settings/about/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import useSWR from "swr"; + +import { AboutSettingsContent } from "@/components/dashboard/settings-view"; +import { fetchDashboard } from "@/lib/authsome-api"; + +export default function SettingsAboutPage() { + const { data } = useSWR("authsome-dashboard", fetchDashboard); + if (!data) return null; + return ; +} diff --git a/ui/src/app/(authenticated)/settings/general/page.tsx b/ui/src/app/(authenticated)/settings/general/page.tsx new file mode 100644 index 0000000..95d9d5d --- /dev/null +++ b/ui/src/app/(authenticated)/settings/general/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import useSWR from "swr"; + +import { GeneralSettingsContent } from "@/components/dashboard/settings-view"; +import { fetchDashboard } from "@/lib/authsome-api"; + +export default function SettingsGeneralPage() { + const { data } = useSWR("authsome-dashboard", fetchDashboard); + if (!data) return null; + return ; +} diff --git a/ui/src/app/(authenticated)/settings/layout.tsx b/ui/src/app/(authenticated)/settings/layout.tsx new file mode 100644 index 0000000..db7adb5 --- /dev/null +++ b/ui/src/app/(authenticated)/settings/layout.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { Info, Settings2, ShieldCheck, Users } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { ReactNode } from "react"; +import useSWR from "swr"; + +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar"; +import { fetchDashboard } from "@/lib/authsome-api"; + +type SettingsNavItem = { + href: string; + label: string; + icon: ReactNode; + adminOnly?: boolean; +}; + +const SETTINGS_NAV: SettingsNavItem[] = [ + { href: "/settings/general", label: "General", icon: }, + { href: "/settings/principals", label: "Principals", icon: , adminOnly: true }, + { href: "/settings/security", label: "Security", icon: }, + { href: "/settings/about", label: "About", icon: }, +]; + +export default function SettingsLayout({ children }: { children: ReactNode }) { + const pathname = usePathname(); + const { data } = useSWR("authsome-dashboard", fetchDashboard); + + const items = SETTINGS_NAV.filter((item) => !item.adminOnly || data?.account.isAdmin); + + return ( +
+ + + + Settings + + + {items.map((item) => ( + + } + isActive={pathname === item.href || pathname.startsWith(item.href + "/")} + > + {item.icon} + {item.label} + + + ))} + + + + + +
+ {children} +
+
+ ); +} diff --git a/ui/src/app/(authenticated)/settings/page.tsx b/ui/src/app/(authenticated)/settings/page.tsx index 652c6b0..13bc5f6 100644 --- a/ui/src/app/(authenticated)/settings/page.tsx +++ b/ui/src/app/(authenticated)/settings/page.tsx @@ -1,12 +1,5 @@ -"use client"; - -import useSWR from "swr"; - -import { SettingsView } from "@/components/dashboard/settings-view"; -import { fetchDashboard } from "@/lib/authsome-api"; +import { redirect } from "next/navigation"; export default function SettingsPage() { - const { data } = useSWR("authsome-dashboard", fetchDashboard); - if (!data) return null; - return ; + redirect("/settings/general"); } diff --git a/ui/src/app/(authenticated)/settings/principals/page.tsx b/ui/src/app/(authenticated)/settings/principals/page.tsx new file mode 100644 index 0000000..eb15930 --- /dev/null +++ b/ui/src/app/(authenticated)/settings/principals/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { PrincipalsView } from "@/components/dashboard/overview-views"; + +export default function SettingsPrincipalsPage() { + return ; +} diff --git a/ui/src/app/(authenticated)/settings/security/page.tsx b/ui/src/app/(authenticated)/settings/security/page.tsx new file mode 100644 index 0000000..fed698c --- /dev/null +++ b/ui/src/app/(authenticated)/settings/security/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { Suspense } from "react"; +import useSWR from "swr"; + +import { SecuritySettingsContent } from "@/components/dashboard/settings-view"; +import { fetchDashboard } from "@/lib/authsome-api"; + +export default function SettingsSecurityPage() { + const { data } = useSWR("authsome-dashboard", fetchDashboard); + if (!data) return null; + return ( + + + + ); +} diff --git a/ui/src/app/globals.css b/ui/src/app/globals.css index 5807bce..8c32df0 100644 --- a/ui/src/app/globals.css +++ b/ui/src/app/globals.css @@ -48,72 +48,70 @@ --radius-4xl: calc(var(--radius) * 2.6); } -/* ─── Light mode (fallback) ──────────────────────────────────────────────── */ +/* ─── Light mode ─────────────────────────────────────────────────────────── */ :root { --background: oklch(0.985 0 0); - --foreground: oklch(0.18 0.01 260); + --foreground: oklch(0.145 0.004 285.823); --card: oklch(1 0 0); - --card-foreground: oklch(0.18 0.01 260); + --card-foreground: oklch(0.145 0.004 285.823); --popover: oklch(1 0 0); - --popover-foreground: oklch(0.18 0.01 260); - --primary: oklch(0.65 0.16 163); - --primary-foreground: oklch(0.23 0.09 163); + --popover-foreground: oklch(0.145 0.004 285.823); + --primary: oklch(0.55 0.16 163); + --primary-foreground: oklch(0.985 0.002 163); --secondary: oklch(0.96 0.015 163); --secondary-foreground: oklch(0.28 0.06 163); - --muted: oklch(0.965 0.012 260); - --muted-foreground: oklch(0.5 0.03 260); - --accent: oklch(0.65 0.16 163); - --accent-foreground: oklch(0.23 0.09 163); + --muted: oklch(0.96 0.005 285.823); + --muted-foreground: oklch(0.556 0.02 285.823); + --accent: oklch(0.96 0.005 285.823); + --accent-foreground: oklch(0.145 0.004 285.823); --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.91 0.015 260); - --input: oklch(0.91 0.015 260); - --ring: oklch(0.65 0.16 163); - --chart-1: oklch(0.65 0.16 163); + --border: oklch(0.907 0.006 285.823); + --input: oklch(0.907 0.006 285.823); + --ring: oklch(0.55 0.16 163); + --chart-1: oklch(0.55 0.16 163); --chart-2: oklch(0.84 0.13 163); --chart-3: oklch(0.74 0.14 82); --chart-4: oklch(0.58 0.12 18); --chart-5: oklch(0.48 0.04 260); --radius: 0.375rem; --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.18 0.01 260); - --sidebar-primary: oklch(0.65 0.16 163); - --sidebar-primary-foreground: oklch(0.23 0.09 163); - --sidebar-accent: oklch(0.94 0.02 163); - --sidebar-accent-foreground: oklch(0.18 0.01 260); - --sidebar-border: oklch(0.91 0.015 260); - --sidebar-ring: oklch(0.65 0.16 163); + --sidebar-foreground: oklch(0.145 0.004 285.823); + --sidebar-primary: oklch(0.55 0.16 163); + --sidebar-primary-foreground: oklch(0.985 0.002 163); + --sidebar-accent: oklch(0.96 0.005 285.823); + --sidebar-accent-foreground: oklch(0.145 0.004 285.823); + --sidebar-border: oklch(0.907 0.006 285.823); + --sidebar-ring: oklch(0.55 0.16 163); } /* ─── Dark mode — Authsome Secure Console design system ─────────────────── */ /* Palette: Deep Emerald (#10B981) + Obsidian (#09090B) */ -/* Ref: ui/DESIGN.md */ .dark { - --background: oklch(0.088 0.002 277); /* #09090b obsidian */ - --foreground: oklch(0.912 0.007 338); /* #e5e1e4 on-surface */ + --background: oklch(0.088 0.002 277); + --foreground: oklch(0.912 0.007 338); - --card: oklch(0.118 0.003 285); /* #131315 surface panel */ + --card: oklch(0.118 0.003 285); --card-foreground: oklch(0.912 0.007 338); --popover: oklch(0.118 0.003 285); --popover-foreground: oklch(0.912 0.007 338); - /* Emerald primary */ - --primary: oklch(0.69 0.162 162); /* #10b981 */ - --primary-foreground: oklch(0.23 0.094 162); /* #003824 */ + --primary: oklch(0.69 0.162 162); + --primary-foreground: oklch(0.23 0.094 162); - --secondary: oklch(0.17 0.007 308); /* #201f22 surface-high */ + --secondary: oklch(0.17 0.007 308); --secondary-foreground: oklch(0.912 0.007 338); - --muted: oklch(0.17 0.007 308); /* #201f22 */ - --muted-foreground: oklch(0.60 0.019 152); /* #86948a */ + --muted: oklch(0.17 0.007 308); + --muted-foreground: oklch(0.60 0.019 152); - --accent: oklch(0.205 0.004 292); /* #2a2a2c surface-container-high — neutral hover surface */ - --accent-foreground: oklch(0.912 0.007 338); /* on-surface — NOT emerald; emerald is --primary only */ + --accent: oklch(0.205 0.004 292); + --accent-foreground: oklch(0.912 0.007 338); --destructive: oklch(0.704 0.191 22.216); - --border: oklch(0.21 0.005 300); /* #27272a */ + --border: oklch(0.21 0.005 300); --input: oklch(0.21 0.005 300); - --ring: oklch(0.69 0.162 162); /* emerald ring */ + --ring: oklch(0.69 0.162 162); --chart-1: oklch(0.69 0.162 162); --chart-2: oklch(0.84 0.133 162); @@ -121,12 +119,12 @@ --chart-4: oklch(0.58 0.12 18); --chart-5: oklch(0.40 0.02 300); - --sidebar: oklch(0.09 0.003 285); /* #0e0e10 dim surface */ + --sidebar: oklch(0.09 0.003 285); --sidebar-foreground: oklch(0.912 0.007 338); --sidebar-primary: oklch(0.69 0.162 162); --sidebar-primary-foreground: oklch(0.23 0.094 162); - --sidebar-accent: oklch(0.205 0.004 292); /* matches --accent */ - --sidebar-accent-foreground: oklch(0.80 0.021 153); /* #bbcabf */ + --sidebar-accent: oklch(0.205 0.004 292); + --sidebar-accent-foreground: oklch(0.80 0.021 153); --sidebar-border: oklch(0.21 0.005 300); --sidebar-ring: oklch(0.69 0.162 162); } @@ -156,4 +154,71 @@ [role="button"][aria-disabled="true"] { cursor: not-allowed; } + + /* ─── Enterprise-grade refinements ─────────────────────────────────────── */ + + /* Tighter card spacing for dense dashboards */ + [data-slot="card"] { + @apply rounded-lg; + } + + /* Consistent table density */ + [data-slot="table-cell"] { + @apply py-2.5 px-3 text-sm; + } + [data-slot="table-head"] { + @apply py-2 px-3 text-xs font-medium uppercase tracking-wider text-muted-foreground; + } + + /* Sharper badge styling */ + [data-slot="badge"] { + @apply text-[0.7rem] font-medium tracking-wide; + } + + /* Card header — keep title at base size, tighten the gap */ + [data-slot="card-header"] { + @apply gap-1; + } + [data-slot="card-title"] { + @apply text-base font-semibold tracking-tight; + } + [data-slot="card-description"] { + @apply text-sm; + } + + /* Tighter dialog spacing */ + [data-slot="dialog-header"] { + @apply gap-1; + } + + /* Improve focus-visible ring for accessibility */ + :focus-visible { + @apply outline-2 outline-offset-2 outline-ring; + } + + /* Skip-to-content link (screen readers and keyboard nav) */ + .skip-link { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + .skip-link:focus { + position: fixed; + left: 1rem; + top: 1rem; + z-index: 50; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; + white-space: normal; + @apply rounded-md bg-primary px-4 py-2 text-primary-foreground; + } } diff --git a/ui/src/app/layout.tsx b/ui/src/app/layout.tsx index 4582cea..e5b903c 100644 --- a/ui/src/app/layout.tsx +++ b/ui/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Hanken_Grotesk, JetBrains_Mono } from "next/font/google"; +import { ThemeProvider } from "@/components/theme-provider"; import { TooltipProvider } from "@/components/ui/tooltip"; import "./globals.css"; @@ -17,7 +18,7 @@ const jetbrainsMono = JetBrains_Mono({ export const metadata: Metadata = { title: "Authsome Dashboard", - description: "Local dashboard for Authsome agents, providers, and connections.", + description: "Dashboard for Authsome agents, providers, and connections.", }; export default function RootLayout({ @@ -28,10 +29,18 @@ export default function RootLayout({ return ( - {children} + + {children} + ); diff --git a/ui/src/app/not-found.tsx b/ui/src/app/not-found.tsx index a0b06c8..2d93a28 100644 --- a/ui/src/app/not-found.tsx +++ b/ui/src/app/not-found.tsx @@ -9,7 +9,7 @@ export default function NotFound() {
-
+ Page not found diff --git a/ui/src/components/authsome-dashboard.tsx b/ui/src/components/authsome-dashboard.tsx index f9ebcd3..6c498f1 100644 --- a/ui/src/components/authsome-dashboard.tsx +++ b/ui/src/components/authsome-dashboard.tsx @@ -14,9 +14,8 @@ import { isDashboardView, } from "@/components/dashboard/dashboard-shell"; import type { View } from "@/components/dashboard/dashboard-shell"; -import { AgentsView, AuditView, DashboardView, PrincipalsView } from "@/components/dashboard/overview-views"; +import { AgentsView, AuditView, DashboardView } from "@/components/dashboard/overview-views"; import { ProvidersView } from "@/components/dashboard/provider-views"; -import { SettingsView } from "@/components/dashboard/settings-view"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { DashboardData, fetchDashboard } from "@/lib/authsome-api"; @@ -46,7 +45,6 @@ export { AgentsView, AuditView, DashboardView, - PrincipalsView, } from "@/components/dashboard/overview-views"; export { AuthsomeProviderDetail, @@ -54,8 +52,6 @@ export { ProviderDetailBody, } from "@/components/dashboard/provider-detail-view"; export { ProvidersView } from "@/components/dashboard/provider-views"; -export { SettingsView } from "@/components/dashboard/settings-view"; - function ActiveView({ connectionFilter, data, @@ -82,9 +78,7 @@ function ActiveView({ ); } if (view === "agents") return ; - if (view === "principals") return ; if (view === "audit") return ; - if (view === "settings") return ; return ; } diff --git a/ui/src/components/dashboard/auth-flow-shell.tsx b/ui/src/components/dashboard/auth-flow-shell.tsx index 7efd0fe..89b7fe9 100644 --- a/ui/src/components/dashboard/auth-flow-shell.tsx +++ b/ui/src/components/dashboard/auth-flow-shell.tsx @@ -1,10 +1,47 @@ "use client"; +import { motion } from "framer-motion"; +import { KeyRound, Shield, Cpu, Lock } from "lucide-react"; import Image from "next/image"; import { ReactNode } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +const FEATURES = [ + { + icon: KeyRound, + title: "One CLI, every provider", + description: "GitHub, Google, Okta, Linear, OpenAI — connect any OAuth or API-key provider in seconds.", + }, + { + icon: Shield, + title: "Encrypted at rest", + description: "Credentials are AES-encrypted in the vault. Secrets are protected at rest and in transit.", + }, + { + icon: Cpu, + title: "Built for agents", + description: "Give AI agents scoped, revocable access to your accounts — without sharing raw tokens.", + }, + { + icon: Lock, + title: "Proof-of-Possession auth", + description: "Every request is signed with Ed25519. No bearer tokens floating around.", + }, +]; + +const containerVariants = { + hidden: {}, + visible: { + transition: { staggerChildren: 0.1, delayChildren: 0.2 }, + }, +}; + +const itemVariants = { + hidden: { opacity: 0, y: 16 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: "easeOut" as const } }, +}; + export function AuthFlowShell({ children, description, @@ -19,13 +56,71 @@ export function AuthFlowShell({ if (size === "wide") { return (
-
-
- Authsome -

{title}

-

{description}

-
-
{children}
+
+ + + Authsome + + + + {title} + + + + {description} + + + + {FEATURES.map((feature) => ( + +
+ +
+
+

{feature.title}

+

+ {feature.description} +

+
+
+ ))} +
+ +
+ + + {children} +
); @@ -33,16 +128,21 @@ export function AuthFlowShell({ return (
-
+ - Authsome - {title} + Authsome + {title} {description} {children ? {children} : null} -
+
); } diff --git a/ui/src/components/dashboard/auth-flows.tsx b/ui/src/components/dashboard/auth-flows.tsx index 7e67282..557cabc 100644 --- a/ui/src/components/dashboard/auth-flows.tsx +++ b/ui/src/components/dashboard/auth-flows.tsx @@ -1,6 +1,7 @@ "use client"; -import { CheckCircle2, CircleAlert, LogIn, Plus, UserRound } from "lucide-react"; +import { AnimatePresence, motion } from "framer-motion"; +import { ArrowRight, CheckCircle2, CircleAlert, LogIn, UserRound } from "lucide-react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { useState } from "react"; @@ -10,7 +11,6 @@ import { AuthFlowShell } from "@/components/dashboard/auth-flow-shell"; import { Button, buttonVariants } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { SessionInputField, fetchAuthSessionStatus, @@ -43,72 +43,120 @@ export function AuthsomeLogin({ nextPath = NEXT_URL }: { nextPath?: string }) { return tab === "register" ? "create" : "signin"; }); + const [mode, setMode] = useState<"signin" | "create">(defaultTab === "create" ? "create" : "signin"); + const isSignIn = mode === "signin"; + return ( - - - Open Dashboard - Use your Authsome account to continue. + + + + + + {isSignIn ? "Sign in" : "Create account"} + + + {isSignIn + ? "Enter your credentials to access the dashboard." + : "Set up a new account to get started."} + + + - - - - Sign in - Create account - - -
-

Sign in

-

- Continue with an existing Authsome account. -

-
- {defaultTab === "signin" && errorMessage ? ( -
- - {errorMessage} + + + + {errorMessage ? : null} +
+ +
+ +
- ) : null} - - - -
-

Create account

-

- Set up a new account for this dashboard. -

-
- {defaultTab === "create" && errorMessage ? ( -
- - {errorMessage} +
+ +
- ) : null} - - - + + + + + +
+ {isSignIn ? "Don’t have an account?" : "Already have an account?"}{" "} + +
); } +function ErrorBanner({ message }: { message: string }) { + return ( + + + ); +} + export function AuthsomeClaim({ token }: { token: string }) { const { data, error } = useSWR(token ? ["authsome-claim", token] : null, () => fetchClaimStatus(token)); const nextPath = token ? `/claim?token=${encodeURIComponent(token)}` : "/claim"; @@ -233,14 +281,14 @@ function AuthsomeSessionInput({ sessionId }: { sessionId: string }) { description="Enter the provider details required to continue this login flow." title={data.display_name} > -
+ {data.warning ? (
{data.warning}
) : null} {data.callback_url ? ( -