feat: Premium design overhaul — typography, atmosphere, animations, component polish
- Add Plus Jakarta Sans as display font for headings - Add subtle noise texture overlay + indigo radial gradient for depth - New keyframe animations: glow-pulse, fade-in-up, scale-in, slide-in-right - Card: interactive hover-lift + selected ring variants - Button: scale micro-interactions, destructive glow, transition-all - Header: logo upgrade with wordmark, animated nav indicator bar, glass search button, gradient shadow depth - StatusDot: glow halos per status variant (active/success/error/warning/urgent) - HealthDot: glow effects for connected/disconnected/reconnecting states - Card hover-lift and status glow CSS utilities
This commit is contained in:
@@ -1,32 +1,35 @@
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ConnectionState } from "@/hooks/useConnectionStatus";
|
||||
|
||||
export function HealthDot() {
|
||||
const health = trpc.healthCheck.useQuery(undefined, {
|
||||
refetchInterval: 30000,
|
||||
retry: 1,
|
||||
});
|
||||
interface HealthDotProps {
|
||||
connectionState: ConnectionState;
|
||||
}
|
||||
|
||||
const isHealthy = health.data && !health.isError;
|
||||
const isLoading = health.isLoading;
|
||||
const healthGlow: Record<ConnectionState, string> = {
|
||||
connected: "0 0 6px 1px hsl(var(--status-success-dot) / 0.4)",
|
||||
disconnected: "0 0 6px 1px hsl(var(--status-error-dot) / 0.5)",
|
||||
reconnecting: "0 0 6px 1px hsl(var(--status-neutral-dot) / 0.3)",
|
||||
};
|
||||
|
||||
export function HealthDot({ connectionState }: HealthDotProps) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block h-2 w-2 rounded-full",
|
||||
isLoading && "bg-status-neutral-dot animate-status-pulse",
|
||||
isHealthy && "bg-status-success-dot",
|
||||
!isHealthy && !isLoading && "bg-status-error-dot",
|
||||
connectionState === "reconnecting" && "bg-status-neutral-dot animate-status-pulse",
|
||||
connectionState === "connected" && "bg-status-success-dot",
|
||||
connectionState === "disconnected" && "bg-status-error-dot",
|
||||
)}
|
||||
style={{ boxShadow: healthGlow[connectionState] }}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isLoading
|
||||
? "Checking server..."
|
||||
: isHealthy
|
||||
{connectionState === "reconnecting"
|
||||
? "Reconnecting..."
|
||||
: connectionState === "connected"
|
||||
? "Server connected"
|
||||
: "Server disconnected"}
|
||||
</TooltipContent>
|
||||
|
||||
@@ -11,6 +11,15 @@ const dotColors: Record<StatusVariant, string> = {
|
||||
urgent: "bg-status-urgent-dot",
|
||||
};
|
||||
|
||||
const glowMap: Record<StatusVariant, string | undefined> = {
|
||||
active: "0 0 6px 1px hsl(var(--status-active-dot) / 0.5)",
|
||||
success: "0 0 6px 1px hsl(var(--status-success-dot) / 0.4)",
|
||||
warning: "0 0 6px 1px hsl(var(--status-warning-dot) / 0.4)",
|
||||
error: "0 0 6px 1px hsl(var(--status-error-dot) / 0.5)",
|
||||
neutral: undefined,
|
||||
urgent: "0 0 6px 1px hsl(var(--status-urgent-dot) / 0.4)",
|
||||
};
|
||||
|
||||
/** Maps raw entity status strings to semantic StatusVariant tokens. */
|
||||
export function mapEntityStatus(rawStatus: string): StatusVariant {
|
||||
switch (rawStatus) {
|
||||
@@ -84,6 +93,7 @@ export function StatusDot({
|
||||
|
||||
const variant = variantOverride ?? mapEntityStatus(status);
|
||||
const color = dotColors[variant];
|
||||
const glow = glowMap[variant];
|
||||
const displayLabel = label ?? status.replace(/_/g, " ").toLowerCase();
|
||||
|
||||
return (
|
||||
@@ -94,9 +104,10 @@ export function StatusDot({
|
||||
"inline-block shrink-0 rounded-full",
|
||||
sizeClasses[size],
|
||||
color,
|
||||
pulse && "animate-status-pulse",
|
||||
pulse && "animate-status-pulse transition-transform",
|
||||
className,
|
||||
)}
|
||||
style={glow ? { boxShadow: glow } : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,18 +5,18 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-md active:scale-[0.98]",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90 hover:shadow-[0_0_12px_hsl(var(--destructive)/0.3)]",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground active:scale-[0.98]",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
|
||||
@@ -2,19 +2,25 @@ import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
interactive?: boolean;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||
({ className, interactive, selected, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
interactive && "transition-all duration-200 ease-out hover:shadow-md hover:-translate-y-0.5 hover:border-border/80 cursor-pointer",
|
||||
selected && "ring-1 ring-primary/30 border-primary/20",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
|
||||
@@ -150,6 +150,13 @@
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
|
||||
/* Spacing tokens */
|
||||
--space-section: 2rem;
|
||||
--space-section-lg: 3rem;
|
||||
|
||||
/* Background atmosphere */
|
||||
--bg-gradient: radial-gradient(ellipse 80% 60% at 50% -10%, hsl(239 84% 67% / 0.04), transparent 70%);
|
||||
|
||||
/* Z-index scale */
|
||||
--z-base: 0;
|
||||
--z-raised: 1;
|
||||
@@ -259,6 +266,9 @@
|
||||
--diff-remove-border: 0 84% 25%;
|
||||
--diff-hunk-bg: 239 40% 16%;
|
||||
|
||||
/* Background atmosphere — dark mode */
|
||||
--bg-gradient: radial-gradient(ellipse 80% 60% at 50% -10%, hsl(239 84% 67% / 0.06), transparent 70%);
|
||||
|
||||
/* Shadow tokens — dark mode (inset highlights + ambient glow) */
|
||||
--shadow-xs: none;
|
||||
--shadow-sm: inset 0 1px 0 hsl(0 0% 100% / 0.04);
|
||||
@@ -320,9 +330,28 @@ select:focus-visible {
|
||||
min-width: 320px;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-image: var(--bg-gradient);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
/* Noise texture overlay */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
opacity: 0.015;
|
||||
pointer-events: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
||||
background-repeat: repeat;
|
||||
background-size: 256px 256px;
|
||||
}
|
||||
|
||||
.dark body::before {
|
||||
opacity: 0.03;
|
||||
}
|
||||
|
||||
/* Notion-style page link blocks inside the editor */
|
||||
.page-link-block {
|
||||
display: flex;
|
||||
@@ -426,3 +455,27 @@ select:focus-visible {
|
||||
.ProseMirror pre code::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.card-hover-lift {
|
||||
transition: transform var(--duration-normal) var(--ease-out),
|
||||
box-shadow var(--duration-normal) var(--ease-out);
|
||||
}
|
||||
.card-hover-lift:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.glow-active {
|
||||
box-shadow: 0 0 8px hsl(var(--status-active-dot) / 0.4);
|
||||
}
|
||||
.glow-success {
|
||||
box-shadow: 0 0 8px hsl(var(--status-success-dot) / 0.4);
|
||||
}
|
||||
.glow-error {
|
||||
box-shadow: 0 0 8px hsl(var(--status-error-dot) / 0.4);
|
||||
}
|
||||
.glow-warning {
|
||||
box-shadow: 0 0 8px hsl(var(--status-warning-dot) / 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ThemeToggle } from '@/components/ThemeToggle'
|
||||
import { HealthDot } from '@/components/HealthDot'
|
||||
import { NavBadge } from '@/components/NavBadge'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import type { ConnectionState } from '@/hooks/useConnectionStatus'
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Initiatives', to: '/initiatives', badgeKey: null },
|
||||
@@ -15,9 +16,10 @@ const navItems = [
|
||||
interface AppLayoutProps {
|
||||
children: React.ReactNode
|
||||
onOpenCommandPalette?: () => void
|
||||
connectionState: ConnectionState
|
||||
}
|
||||
|
||||
export function AppLayout({ children, onOpenCommandPalette }: AppLayoutProps) {
|
||||
export function AppLayout({ children, onOpenCommandPalette, connectionState }: AppLayoutProps) {
|
||||
const agents = trpc.listAgents.useQuery(undefined, {
|
||||
refetchInterval: 10000,
|
||||
})
|
||||
@@ -30,29 +32,39 @@ export function AppLayout({ children, onOpenCommandPalette }: AppLayoutProps) {
|
||||
return (
|
||||
<div className="flex h-screen flex-col bg-background">
|
||||
{/* Single-row 48px header */}
|
||||
<header className="z-sticky shrink-0 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex h-12 items-center justify-between px-4">
|
||||
<header className="relative z-sticky shrink-0 border-b border-border/50 bg-background/95 shadow-[0_1px_0_0_hsl(var(--border)/0.5),0_1px_3px_-1px_hsl(var(--primary)/0.1)] backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex h-12 items-center justify-between px-5">
|
||||
{/* Left: Logo + Nav */}
|
||||
<div className="flex items-center gap-6">
|
||||
<Link to="/initiatives" className="flex items-center gap-2 text-sm font-bold tracking-tight">
|
||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded bg-primary text-[10px] font-bold text-primary-foreground">
|
||||
<Link to="/initiatives" className="flex items-center gap-2.5">
|
||||
<span className="inline-flex h-7 w-7 items-center justify-center rounded bg-primary text-[11px] font-bold text-primary-foreground shadow-sm ring-1 ring-primary/20">
|
||||
CW
|
||||
</span>
|
||||
<span className="hidden sm:inline">Codewalk District</span>
|
||||
<span className="hidden items-baseline gap-1 sm:inline-flex">
|
||||
<span className="font-display font-bold tracking-tight">Codewalk</span>
|
||||
<span className="font-display font-medium tracking-tight text-muted-foreground">District</span>
|
||||
</span>
|
||||
</Link>
|
||||
<nav className="flex items-center gap-0.5">
|
||||
<nav className="flex items-center gap-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
to={item.to}
|
||||
className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors duration-fast hover:text-foreground"
|
||||
className="relative flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors duration-fast hover:text-foreground"
|
||||
activeProps={{
|
||||
className: 'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-foreground bg-accent',
|
||||
className: 'relative flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-semibold text-foreground',
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
{item.badgeKey && (
|
||||
<NavBadge count={badgeCounts[item.badgeKey]} />
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
{item.label}
|
||||
{item.badgeKey && (
|
||||
<NavBadge count={badgeCounts[item.badgeKey]} />
|
||||
)}
|
||||
{isActive && (
|
||||
<span className="absolute -bottom-[13px] left-1/2 h-0.5 w-4 -translate-x-1/2 rounded-full bg-primary" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
@@ -60,11 +72,11 @@ export function AppLayout({ children, onOpenCommandPalette }: AppLayoutProps) {
|
||||
</div>
|
||||
|
||||
{/* Right: Cmd+K, Theme toggle, Health, Workspace */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{onOpenCommandPalette && (
|
||||
<button
|
||||
onClick={onOpenCommandPalette}
|
||||
className="flex items-center gap-1.5 rounded-md border bg-muted/50 px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
className="flex items-center gap-1.5 rounded-md border border-border/60 bg-background/80 px-2.5 py-1 text-xs text-muted-foreground backdrop-blur-sm transition-all duration-fast hover:border-border hover:bg-accent/80 hover:text-foreground"
|
||||
>
|
||||
<Search className="h-3 w-3" />
|
||||
<span className="hidden sm:inline">Search</span>
|
||||
@@ -73,8 +85,9 @@ export function AppLayout({ children, onOpenCommandPalette }: AppLayoutProps) {
|
||||
</kbd>
|
||||
</button>
|
||||
)}
|
||||
<div className="mx-1 h-4 w-px bg-border/50" />
|
||||
<ThemeToggle />
|
||||
<HealthDot />
|
||||
<HealthDot connectionState={connectionState} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user