From 168c2933548200355869fa8f1666df69b1a0dee1 Mon Sep 17 00:00:00 2001 From: SourabhX16 <146323884+SourabhX16@users.noreply.github.com> Date: Sun, 28 Jun 2026 12:41:06 +0530 Subject: [PATCH 1/5] feat(web): add command center UI foundation --- apps/web/src/app/globals.css | 81 +++++++++++-------- apps/web/src/app/layout.tsx | 2 +- apps/web/src/components/app/loading-panel.tsx | 12 +++ apps/web/src/components/app/page-header.tsx | 14 ++++ .../src/components/app/theme-controller.tsx | 20 +++++ apps/web/src/components/ui/badge.tsx | 25 ++++++ apps/web/src/components/ui/button.tsx | 46 +++++++++++ apps/web/src/components/ui/card.tsx | 22 +++++ apps/web/src/components/ui/input.tsx | 14 ++++ apps/web/src/components/ui/progress.tsx | 10 +++ apps/web/src/components/ui/textarea.tsx | 14 ++++ 11 files changed, 227 insertions(+), 33 deletions(-) create mode 100644 apps/web/src/components/app/loading-panel.tsx create mode 100644 apps/web/src/components/app/page-header.tsx create mode 100644 apps/web/src/components/app/theme-controller.tsx create mode 100644 apps/web/src/components/ui/badge.tsx create mode 100644 apps/web/src/components/ui/button.tsx create mode 100644 apps/web/src/components/ui/card.tsx create mode 100644 apps/web/src/components/ui/input.tsx create mode 100644 apps/web/src/components/ui/progress.tsx create mode 100644 apps/web/src/components/ui/textarea.tsx diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 8abdb15..46aad00 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -4,65 +4,65 @@ @layer base { :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; + --background: 210 25% 98%; + --foreground: 218 33% 10%; --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card-foreground: 218 33% 10%; --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; + --popover-foreground: 218 33% 10%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; + --primary: 188 91% 35%; + --primary-foreground: 190 90% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; + --secondary: 214 32% 91%; + --secondary-foreground: 218 33% 12%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; + --muted: 215 28% 92%; + --muted-foreground: 216 14% 38%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --accent: 41 96% 56%; + --accent-foreground: 32 95% 12%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; + --border: 214 20% 84%; + --input: 214 20% 84%; + --ring: 188 91% 35%; --radius: 0.5rem; } .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; + --background: 220 24% 6%; + --foreground: 213 31% 91%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; + --card: 220 20% 10%; + --card-foreground: 213 31% 91%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; + --popover: 220 20% 10%; + --popover-foreground: 213 31% 91%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; + --primary: 187 85% 52%; + --primary-foreground: 220 24% 6%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; + --secondary: 218 18% 16%; + --secondary-foreground: 213 31% 91%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; + --muted: 218 18% 14%; + --muted-foreground: 215 18% 63%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; + --accent: 42 94% 58%; + --accent-foreground: 220 24% 6%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; + --border: 218 16% 21%; + --input: 218 16% 21%; + --ring: 187 85% 52%; } } @@ -73,4 +73,21 @@ body { @apply bg-background text-foreground; } + + ::selection { + @apply bg-primary/30 text-foreground; + } +} + +@layer utilities { + .surface-grid { + background-image: + linear-gradient(hsl(var(--border) / 0.3) 1px, transparent 1px), + linear-gradient(90deg, hsl(var(--border) / 0.3) 1px, transparent 1px); + background-size: 28px 28px; + } + + .text-balance { + text-wrap: balance; + } } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index d75a899..35a82c3 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -25,7 +25,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + diff --git a/apps/web/src/components/app/loading-panel.tsx b/apps/web/src/components/app/loading-panel.tsx new file mode 100644 index 0000000..be2187a --- /dev/null +++ b/apps/web/src/components/app/loading-panel.tsx @@ -0,0 +1,12 @@ +import { Card, CardContent } from "@/components/ui/card"; + +export function LoadingPanel({ label = "Loading mock data" }: { label?: string }) { + return ( + + +
+

{label}

+ + + ); +} diff --git a/apps/web/src/components/app/page-header.tsx b/apps/web/src/components/app/page-header.tsx new file mode 100644 index 0000000..86bb1be --- /dev/null +++ b/apps/web/src/components/app/page-header.tsx @@ -0,0 +1,14 @@ +import { Badge } from "@/components/ui/badge"; + +export function PageHeader({ eyebrow, title, description, action }: { eyebrow?: string; title: string; description?: string; action?: React.ReactNode }) { + return ( +
+
+ {eyebrow ? {eyebrow} : null} +

{title}

+ {description ?

{description}

: null} +
+ {action} +
+ ); +} diff --git a/apps/web/src/components/app/theme-controller.tsx b/apps/web/src/components/app/theme-controller.tsx new file mode 100644 index 0000000..edc6b65 --- /dev/null +++ b/apps/web/src/components/app/theme-controller.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useEffect } from "react"; +import { Moon, Sun } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useUIStore } from "@/stores/ui-store"; + +export function ThemeController() { + const { darkMode, toggleDarkMode } = useUIStore(); + + useEffect(() => { + document.documentElement.classList.toggle("dark", darkMode); + }, [darkMode]); + + return ( + + ); +} diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx new file mode 100644 index 0000000..e6f39a5 --- /dev/null +++ b/apps/web/src/components/ui/badge.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +type BadgeVariant = "default" | "secondary" | "outline" | "success" | "warning"; + +const variants: Record = { + default: "border-transparent bg-primary text-primary-foreground", + secondary: "border-transparent bg-secondary text-secondary-foreground", + outline: "border-border text-foreground", + success: "border-emerald-500/30 bg-emerald-500/10 text-emerald-400", + warning: "border-amber-500/30 bg-amber-500/10 text-amber-300", +}; + +export function Badge({ className, variant = "default", ...props }: React.HTMLAttributes & { variant?: BadgeVariant }) { + return ( +
+ ); +} diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx new file mode 100644 index 0000000..e8edad6 --- /dev/null +++ b/apps/web/src/components/ui/button.tsx @@ -0,0 +1,46 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cn } from "@/lib/utils"; + +type ButtonVariant = "default" | "secondary" | "ghost" | "outline" | "destructive"; +type ButtonSize = "default" | "sm" | "lg" | "icon"; + +const variants: Record = { + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-muted hover:text-foreground", + outline: "border border-border bg-background hover:bg-muted", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", +}; + +const sizes: Record = { + default: "h-10 px-4 py-2", + sm: "h-8 px-3 text-xs", + lg: "h-11 px-5", + icon: "h-10 w-10", +}; + +export interface ButtonProps extends React.ButtonHTMLAttributes { + asChild?: boolean; + variant?: ButtonVariant; + size?: ButtonSize; +} + +export const Button = React.forwardRef( + ({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx new file mode 100644 index 0000000..d785a3c --- /dev/null +++ b/apps/web/src/components/ui/card.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export function Card({ className, ...props }: React.HTMLAttributes) { + return
; +} + +export function CardHeader({ className, ...props }: React.HTMLAttributes) { + return
; +} + +export function CardTitle({ className, ...props }: React.HTMLAttributes) { + return

; +} + +export function CardDescription({ className, ...props }: React.HTMLAttributes) { + return

; +} + +export function CardContent({ className, ...props }: React.HTMLAttributes) { + return

; +} diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx new file mode 100644 index 0000000..7a492f4 --- /dev/null +++ b/apps/web/src/components/ui/input.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export const Input = React.forwardRef>(({ className, ...props }, ref) => ( + +)); +Input.displayName = "Input"; diff --git a/apps/web/src/components/ui/progress.tsx b/apps/web/src/components/ui/progress.tsx new file mode 100644 index 0000000..2ea6d9f --- /dev/null +++ b/apps/web/src/components/ui/progress.tsx @@ -0,0 +1,10 @@ +import { cn } from "@/lib/utils"; + +export function Progress({ value, className }: { value: number; className?: string }) { + const width = Math.max(0, Math.min(100, value)); + return ( +
+
+
+ ); +} diff --git a/apps/web/src/components/ui/textarea.tsx b/apps/web/src/components/ui/textarea.tsx new file mode 100644 index 0000000..ac13df7 --- /dev/null +++ b/apps/web/src/components/ui/textarea.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export const Textarea = React.forwardRef>(({ className, ...props }, ref) => ( +