From 63d99a5f173484876348f63399d0549ea9c962ec Mon Sep 17 00:00:00 2001 From: Van Tan Minh Date: Sun, 4 Oct 2026 02:20:33 +0700 Subject: [PATCH 1/3] feat(web): redesign the dashboard with a shared app shell and design tokens - New OKLCH token set (warm neutrals, ink actions, pine accent, status and chart colours) with a full dark theme applied before first paint. - AppShell: sidebar with workspace switcher and scope navigation, inset panel with breadcrumb header, theme toggle and account menu; docks to a bottom bar on mobile. - Project dashboard: resource health strip, segmented filters, resource table, quick-create empty state, redesigned topology canvas, metric cards and logs panel; resource workspace becomes a right-hand drawer. - Redesigned auth, onboarding, workspace projects and integrations pages. - Geist Mono for technical values; design doc updated. Co-Authored-By: Claude Opus 5.5 --- apps/web/index.html | 16 +- apps/web/package.json | 1 + apps/web/src/App.tsx | 45 +- apps/web/src/components/app-shell.tsx | 266 ++ apps/web/src/components/auth-shell.tsx | 121 +- apps/web/src/components/brand-mark.tsx | 14 +- .../knotree-registry-account-card.tsx | 45 +- apps/web/src/components/loading-screen.tsx | 2 +- .../web/src/components/resource-workspace.css | 2340 ++++++--------- .../web/src/components/resource-workspace.tsx | 30 +- apps/web/src/components/theme-provider.tsx | 276 +- apps/web/src/index.css | 1939 +++++++++--- apps/web/src/lib/initials.ts | 9 + apps/web/src/pages/auth-page.tsx | 58 +- .../web/src/pages/github-integration-page.tsx | 363 +-- apps/web/src/pages/project-home-page.tsx | 1336 +++++---- apps/web/src/pages/project-home.css | 2599 +++++------------ apps/web/src/pages/workspace-page.tsx | 324 +- docs/frontend-system-design.md | 367 +-- pnpm-lock.yaml | 8 + 20 files changed, 4978 insertions(+), 5181 deletions(-) create mode 100644 apps/web/src/components/app-shell.tsx create mode 100644 apps/web/src/lib/initials.ts diff --git a/apps/web/index.html b/apps/web/index.html index bab1783..1a7f4c4 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -3,8 +3,22 @@ - + + Knotree Cloud +
diff --git a/apps/web/package.json b/apps/web/package.json index 9c81e47..3c43dc8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -16,6 +16,7 @@ "dependencies": { "@base-ui/react": "^1.8.0", "@fontsource-variable/geist": "^5.3.0", + "@fontsource-variable/geist-mono": "^5.3.0", "@tailwindcss/vite": "^4", "class-variance-authority": "^0.7.1", "cn": "^0.2.6", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index adb8e34..43c3b03 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -9,6 +9,7 @@ import { import { useAuth } from "@/auth/auth-context" import { LoadingScreen } from "@/components/loading-screen" +import { ThemeProvider } from "@/components/theme-provider" import { AuthPage } from "@/pages/auth-page" import { GitHubIntegrationPage } from "@/pages/github-integration-page" import { ProjectHomePage } from "@/pages/project-home-page" @@ -16,27 +17,29 @@ import { NewWorkspacePage, WorkspacePage } from "@/pages/workspace-page" export default function App() { return ( - - } /> - }> - } /> - } /> - - }> - } /> - } /> - } - /> - } - /> - } /> - - } /> - + + + } /> + }> + } /> + } /> + + }> + } /> + } /> + } + /> + } + /> + } /> + + } /> + + ) } diff --git a/apps/web/src/components/app-shell.tsx b/apps/web/src/components/app-shell.tsx new file mode 100644 index 0000000..4092e6c --- /dev/null +++ b/apps/web/src/components/app-shell.tsx @@ -0,0 +1,266 @@ +import { useEffect, useRef, useState, type ReactNode } from "react" +import { ChevronsUpDownIcon, LogOutIcon, MoonIcon, SunIcon } from "lucide-react" +import { cn } from "cn" +import { Link, useNavigate } from "react-router-dom" + +import { useAuth } from "@/auth/auth-context" +import { initials } from "@/lib/initials" +import { EnsureThemeProvider, useTheme } from "@/components/theme-provider" +import type { User, Workspace } from "@/lib/types" + +type AppShellProps = { + workspace: Workspace + user: User + /** Primary navigation for the current scope (workspace or project). */ + nav: ReactNode + /** Secondary navigation pinned to the bottom of the sidebar. */ + footerNav?: ReactNode + /** Context shown above the primary navigation (e.g. the open project). */ + context?: ReactNode + breadcrumbs: ReactNode + actions?: ReactNode + className?: string + children: ReactNode +} + +/** + * Product chrome shared by every signed-in screen: a quiet sidebar on the + * canvas, and an inset panel holding the header and the page content. Below + * 768px the primary navigation docks to the bottom of the viewport. + */ +export function AppShell({ + workspace, + user, + nav, + footerNav, + context, + breadcrumbs, + actions, + className, + children, +}: AppShellProps) { + const navigate = useNavigate() + + return ( + +
+ + +
+
+
+ + + +
{breadcrumbs}
+
+ {actions} + + +
+
+ {children} +
+
+
+
+ ) +} + +export function SidebarNavItem({ + label, + icon, + active = false, + onClick, + to, +}: { + label: string + icon: ReactNode + active?: boolean + onClick?: () => void + to?: string +}) { + const content = ( + <> + + {label} + + ) + + if (to) { + return ( + + {content} + + ) + } + + return ( + + ) +} + +export function BreadcrumbSeparator() { + return ( + + ) +} + +function ThemeToggle() { + const { theme, toggleTheme } = useTheme() + const dark = theme === "dark" + + return ( + + ) +} + +function AccountMenu({ user }: { user: User }) { + const navigate = useNavigate() + const { signOut } = useAuth() + const [open, setOpen] = useState(false) + const wrapRef = useRef(null) + + useEffect(() => { + if (!open) { + return undefined + } + function handlePointerDown(event: PointerEvent) { + if (!wrapRef.current?.contains(event.target as Node | null)) { + setOpen(false) + } + } + function handleKeyDown(event: KeyboardEvent) { + if (event.key === "Escape") { + setOpen(false) + } + } + document.addEventListener("pointerdown", handlePointerDown) + document.addEventListener("keydown", handleKeyDown) + return () => { + document.removeEventListener("pointerdown", handlePointerDown) + document.removeEventListener("keydown", handleKeyDown) + } + }, [open]) + + async function handleSignOut() { + try { + await signOut() + } finally { + navigate("/login", { replace: true }) + } + } + + return ( +
+ + {open && ( +
+
+ {user.fullName || "Knotree account"} + {user.email} +
+
+ setOpen(false)} + > + Integrations + + +
+ )} +
+ ) +} + +/** The Knotree mark drawn as three linked nodes, crisp at any size. */ +export function KnotreeGlyph({ className }: { className?: string }) { + return ( + + ) +} diff --git a/apps/web/src/components/auth-shell.tsx b/apps/web/src/components/auth-shell.tsx index eaf753e..9832be1 100644 --- a/apps/web/src/components/auth-shell.tsx +++ b/apps/web/src/components/auth-shell.tsx @@ -1,28 +1,115 @@ import type { ReactNode } from "react" +import { BoxIcon, DatabaseIcon, LayersIcon } from "lucide-react" import { BrandMark } from "@/components/brand-mark" -export function AuthShell({ children }: { children: ReactNode }) { +export function AuthShell({ + children, + aside, +}: { + children: ReactNode + aside?: ReactNode +}) { return (
-
- +
+
+ + {aside} +
{children}
+
+ © Knotree + cloud.knotree.com +
+
) } + +/** + * A quiet schematic of a project: one service talking to its data stores over + * the private network. It previews what the product does without pretending + * to be live data. + */ +function AuthShowcase() { + return ( + + ) +} + +function AuthNode({ + left, + top, + icon, + name, + meta, + status, +}: { + left: string + top: string + icon: ReactNode + name: string + meta: string + status: "Online" | "Creating" +}) { + return ( +
+
+ {icon} + {name} +
+
+ {meta} + + {status} + +
+
+ ) +} diff --git a/apps/web/src/components/brand-mark.tsx b/apps/web/src/components/brand-mark.tsx index d296f88..9679c07 100644 --- a/apps/web/src/components/brand-mark.tsx +++ b/apps/web/src/components/brand-mark.tsx @@ -1,5 +1,7 @@ import { cn } from "cn" +import { KnotreeGlyph } from "@/components/app-shell" + type BrandMarkProps = { compact?: boolean className?: string @@ -7,16 +9,12 @@ type BrandMarkProps = { export function BrandMark({ compact = false, className }: BrandMarkProps) { return ( -
- +
+ Knotree Cloud diff --git a/apps/web/src/components/knotree-registry-account-card.tsx b/apps/web/src/components/knotree-registry-account-card.tsx index a447243..00509b4 100644 --- a/apps/web/src/components/knotree-registry-account-card.tsx +++ b/apps/web/src/components/knotree-registry-account-card.tsx @@ -1,14 +1,7 @@ import { useEffect, useState } from "react" -import { CheckCircle2Icon, ContainerIcon, Link2OffIcon } from "lucide-react" +import { ContainerIcon } from "lucide-react" import { Alert, AlertDescription } from "@/components/ui/alert" -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card" import { Spinner } from "@/components/ui/spinner" import { ApiError } from "@/lib/api" import { getKnotreeRegistryAccount } from "@/lib/resources" @@ -55,32 +48,32 @@ export function KnotreeRegistryAccountCard() { {error} )} - - -
+ ) +} + +function QuickAddTile({ + icon, + title, + description, + onClick, +}: { + icon: ReactNode + title: string + description: string + onClick: () => void +}) { + return ( + ) } @@ -1454,113 +1463,97 @@ function MetricsWorkspace({ ) return ( -
-
-
-

Metrics

-

- Choose a PostgreSQL database or app service to view its live - metrics. -

+
+
+
+
+

Metrics

+

+ Choose a PostgreSQL database or app service to view its live + metrics. +

+
-
- {error && ( -
- Resources unavailable - {error} -
- )} + - {loading ? ( -
- - Loading resources -
- ) : metricNodes.length === 0 && !error ? ( -
- -

No metrics resources yet

-

- Add a PostgreSQL database or app service to view runtime metrics for - {` ${project.name}`}. -

-
- ) : ( -
- - - - - - - - - - - - {metricNodes.map((node) => ( - - - - - - - ))} - -
- Resources with runtime metrics in {project.name} -
NameTypeStatusMetrics
- {node.type} - - + + {node.title} + {node.type} + + + + {node.resource?.resourceType === "app" + ? "CPU · memory · requests · latency" + : "CPU · memory · volume · I/O"} - - -
-
- )} -
+ + + + + ))} + + )} + +
) } function ResourceIcon({ node }: { node: TopologyNode }) { if (node.type === "HTML page") { - return
+ + ) } function ProjectAddOption({ label, - mark, + description, + icon, disabled = false, onClick, }: { label: string - mark: string + description: string + icon: ReactNode disabled?: boolean onClick: () => void }) { @@ -1739,27 +1739,21 @@ function ProjectAddOption({ ) } -function NodeIcon({ nodeId }: { nodeId: TopologyNodeId }) { - if (nodeId === "postgres") { - return