feat(ui): glassmorphism redesign with settings decomposition (#274)

* feat(ui): add glassmorphism design tokens and utility classes

Introduce glass design system foundation: translucent oklch color
variables for both light and dark themes, glass/glass-border/glass-highlight
tokens, semantic status colors (success/warning/info), .glass and
.glass-strong utility classes with backdrop-filter, reduced shadow
values, and standardized spring animation presets in lib/motion.ts.

* feat(ui): apply glass treatment to core components

Update card, dialog, input, button, popover, sheet, tooltip,
dropdown-menu, context-menu, select, alert-dialog, and tabs
components with glassmorphism styling: translucent backgrounds via
new CSS variables, backdrop-blur layers, glass-border luminous
edges, and glass-highlight hover states.

* refactor(settings): decompose Settings Modal into section components

Extract 10 inline sections from the 1,987-line SettingsModal into
dedicated files under components/settings/. Introduce section
registry pattern replacing 14 conditional blocks. Add shared types,
sidebar navigation grouping with separators, glass treatment on
sidebar and nav buttons, and responsive modal height. SettingsModal
shell shrinks to ~380 lines.

* refactor(ui): unify all tabs to animate-ui TabsHighlight with glass styling

Migrate 4 tab instances (EditorLayout, FleetView, ResourcesView,
NotificationsSection) from inconsistent patterns (manual layoutId,
underline border-b-2, default fade) to the shared TabsHighlight
primitive with glass-highlight indicator and springs.snappy
transition. Standardize EditorLayout nav highlight spring config,
apply glass-highlight to sidebar stack list hover/active states,
and update mobile nav styling.

* refactor(ui): migrate hardcoded colors to semantic CSS variables

Replace hardcoded Tailwind color classes across ~19 component files
with semantic CSS variable classes: emerald/green to success,
orange/amber to warning, blue to info. Preserves brand/decorative
colors (Crown amber, Admiral blue). Enables consistent theming of
status indicators across the entire application.

* refactor(ui): Linear dark precision aesthetic — solid surfaces, depth cues, text hierarchy

Replace glassmorphism with Linear.app-inspired design: solid surface tokens
(card #111111, sidebar #0d0d0d, root #0a0a0a), backdrop-blur restricted to
floating overlays only (blur(10px) saturate(1.15)), desaturated teal accent,
font-weight 500 everywhere, monochrome chart palette, and three depth cues:
root ambient glow, luminous card top-edge, steep text brightness ramp.

* refactor(ui): precision polish — fix muddy dark, snowblind light, add design anchors

- Replace 34 hardcoded rgba values with theme-aware stat-* CSS tokens
- Fix light theme: solid white cards, off-white background, readable text
- Add card-border tokens with sharper directional lighting (top edge 2x)
- Add chart-grid/chart-tick tokens for theme-aware axis rendering
- Upgrade body glow: teal-tinted (dark), warm amber (light)
- Terminal-inspired sidebar: monospaced UP/DN status codes, Geist Mono
- Add tabular-nums to stat values to prevent layout jitter
- Light mode cards get shadow-sm for depth against off-white background

* refactor(ui): Linear materiality pass — ghosted nav, translucent sidebar, font unity

- De-escalate Delete button from solid destructive to ghost with hover fill
- Make sidebar translucent (80% opacity + backdrop-blur) so body glow bleeds through
- Bump dark nav accent to 0.07 for ghosted backlit selection
- Unify all terminal/editor fonts to Geist Mono (was JetBrains/Consolas mix)
- Add Monaco editor fontFamily for YAML/env editing consistency
- Add threshold-based color to Host RAM and Host Disk stat values (warn/crit)

* refactor(ui): material simulation — inherent depth, layer separation, recessed terminal

- Bump dark background 0.065→0.08, card surfaces 0.10→0.12 for 4% layer separation
- Add card-bevel token (inset top shimmer) for permanent structural depth
- Add button-inner-glow token for physical key feel on outline buttons
- Recess terminal with inset shadow and dimmed label
- Reduce action icon strokeWidth to 1.5 for refined industrial feel
- Add teal LED backlight bar on active nav item via blur pseudo-element

* fix(ui): parse usagePercent string to number for getValueColor

usagePercent is typed as string in SystemStats but getValueColor
expects number, causing TS2345 in CI builds.
This commit is contained in:
Anso
2026-03-30 12:48:42 -04:00
committed by GitHub
parent cd4aa746ab
commit 7637091e84
46 changed files with 2483 additions and 2053 deletions
+44 -44
View File
@@ -1,7 +1,8 @@
import { useState, useEffect, useRef } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tabs, TabsContent, TabsList, TabsTrigger, TabsHighlight, TabsHighlightItem } from "@/components/ui/tabs";
import { springs } from '@/lib/motion';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
@@ -99,8 +100,8 @@ function FootprintWidget({ usage, onFilter }: FootprintWidgetProps) {
const pct = (n: number) => `${Math.max(0, (n / total) * 100).toFixed(1)}%`;
const segments: { bytes: number; color: string; label: string; filter: ResourceFilter | null; hoverClass: string }[] = [
{ bytes: managedBytes, color: 'bg-emerald-500', label: 'Sencho Managed', filter: 'managed', hoverClass: 'hover:bg-emerald-400' },
{ bytes: unmanagedBytes, color: 'bg-orange-500', label: 'External Projects', filter: 'unmanaged', hoverClass: 'hover:bg-orange-400' },
{ bytes: managedBytes, color: 'bg-success', label: 'Sencho Managed', filter: 'managed', hoverClass: 'hover:bg-success/80' },
{ bytes: unmanagedBytes, color: 'bg-warning', label: 'External Projects', filter: 'unmanaged', hoverClass: 'hover:bg-warning/80' },
{ bytes: reclaimable, color: 'bg-muted-foreground/20', label: 'Reclaimable', filter: null, hoverClass: '' },
];
@@ -207,16 +208,16 @@ function ManagedBadge({ status, managedBy }: {
}) {
if (status === 'managed') {
return (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded border border-emerald-500/25 bg-emerald-500/8 text-emerald-500 text-[10px] font-medium">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 shrink-0" />
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded border border-success/25 bg-success/8 text-success text-[10px] font-medium">
<span className="w-1.5 h-1.5 rounded-full bg-success shrink-0" />
{managedBy}
</span>
);
}
if (status === 'unmanaged') {
return (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded border border-orange-500/25 bg-orange-500/8 text-orange-500 text-[10px] font-medium">
<span className="w-1.5 h-1.5 rounded-full bg-orange-500 shrink-0" />
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded border border-warning/25 bg-warning/8 text-warning text-[10px] font-medium">
<span className="w-1.5 h-1.5 rounded-full bg-warning shrink-0" />
External
</span>
);
@@ -256,7 +257,7 @@ function PruneButton({ target, icon, label, accentClass, onManaged, onAll }: Pru
<span className={cn('transition-transform duration-200 group-hover:scale-110', accentClass)}>
{icon}
</span>
<span className="text-xs font-semibold text-center leading-tight text-foreground">{label}</span>
<span className="text-xs font-medium text-center leading-tight text-foreground">{label}</span>
<span className="text-[10px] text-brand font-mono tracking-wide">Sencho only</span>
</button>
{target !== 'containers' && (
@@ -451,7 +452,7 @@ export default function ResourcesView() {
{/* Header */}
<div className="flex items-center gap-3">
<HardDrive className="w-5 h-5 text-muted-foreground" />
<h1 className="text-xl font-semibold tracking-tight">Resources Hub</h1>
<h1 className="text-xl font-medium tracking-tight">Resources Hub</h1>
{activeNode?.type === 'remote' && (
<span className="text-sm text-muted-foreground">- {activeNode.name}</span>
)}
@@ -519,7 +520,7 @@ export default function ResourcesView() {
target="networks"
icon={<Network className="w-6 h-6" />}
label="Prune Dead Networks"
accentClass="text-emerald-500"
accentClass="text-success"
onManaged={() => setConfirmPrune({ target: 'networks', scope: 'managed' })}
onAll={() => setConfirmPrune({ target: 'networks', scope: 'all' })}
/>
@@ -527,7 +528,7 @@ export default function ResourcesView() {
target="containers"
icon={<MonitorX className="w-6 h-6" />}
label="Purge Unmanaged Containers"
accentClass="text-orange-500"
accentClass="text-warning"
onManaged={() => setConfirmPrune({ target: 'containers', scope: 'managed' })}
onAll={() => setConfirmPrune({ target: 'containers', scope: 'all' })}
/>
@@ -541,28 +542,27 @@ export default function ResourcesView() {
defaultValue="images"
className="flex-1 flex flex-col w-full rounded-lg border bg-card shadow-sm overflow-hidden min-h-[400px] animate-in fade-in-0 slide-in-from-bottom-2 duration-300 delay-150"
>
<div className="px-4 pt-3 pb-0 border-b bg-muted/10">
<TabsList className="grid grid-cols-4 w-full md:w-[680px] h-9 bg-transparent gap-1 p-0">
{(['images', 'volumes', 'networks'] as const).map(tab => (
<TabsTrigger
key={tab}
value={tab}
className="capitalize text-xs data-[state=active]:bg-background data-[state=active]:shadow-sm rounded-t-md rounded-b-none border-b-2 border-transparent data-[state=active]:border-foreground transition-all duration-200"
>
{tab}
</TabsTrigger>
))}
<TabsTrigger
value="unmanaged"
className="relative text-xs data-[state=active]:bg-background data-[state=active]:shadow-sm rounded-t-md rounded-b-none border-b-2 border-transparent data-[state=active]:border-foreground transition-all duration-200"
>
Unmanaged
{totalOrphansCount > 0 && (
<span className="absolute -top-1.5 -right-1 flex h-4 min-w-4 px-1 items-center justify-center rounded-full bg-orange-500 text-[9px] text-white font-bold animate-in zoom-in-75 duration-200">
{totalOrphansCount}
</span>
)}
</TabsTrigger>
<div className="px-4 pt-3 pb-0 border-b border-glass-border bg-glass">
<TabsList className="grid grid-cols-4 w-full md:w-[680px] h-9 gap-1 p-0">
<TabsHighlight className="rounded-md bg-glass-highlight" transition={springs.snappy}>
{(['images', 'volumes', 'networks'] as const).map(tab => (
<TabsHighlightItem key={tab} value={tab}>
<TabsTrigger value={tab} className="capitalize text-xs">
{tab}
</TabsTrigger>
</TabsHighlightItem>
))}
<TabsHighlightItem value="unmanaged">
<TabsTrigger value="unmanaged" className="relative text-xs">
Unmanaged
{totalOrphansCount > 0 && (
<span className="absolute -top-1.5 -right-1 flex h-4 min-w-4 px-1 items-center justify-center rounded-full bg-warning text-[9px] text-warning-foreground font-medium animate-in zoom-in-75 duration-200">
{totalOrphansCount}
</span>
)}
</TabsTrigger>
</TabsHighlightItem>
</TabsHighlight>
</TabsList>
</div>
@@ -726,7 +726,7 @@ export default function ResourcesView() {
{/* Unmanaged Containers */}
<TabsContent value="unmanaged" className="m-0 border-0 p-0 h-full flex flex-col animate-in fade-in-0 duration-200">
<div className="flex justify-between items-center px-4 py-2.5 border-b bg-muted/10 sticky top-0 z-10 backdrop-blur-sm">
<div className="flex justify-between items-center px-4 py-2.5 border-b bg-muted/10 sticky top-0 z-10">
<div className="flex items-center gap-2.5">
<input
type="checkbox"
@@ -750,8 +750,8 @@ export default function ResourcesView() {
{totalOrphansCount === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center p-12 text-muted-foreground animate-in fade-in-0 duration-300">
<div className="w-12 h-12 rounded-full bg-emerald-500/10 flex items-center justify-center mb-3">
<ShieldCheck className="w-6 h-6 text-emerald-500" />
<div className="w-12 h-12 rounded-full bg-success-muted flex items-center justify-center mb-3">
<ShieldCheck className="w-6 h-6 text-success" />
</div>
<p className="font-medium text-sm">No unmanaged containers</p>
<p className="text-xs mt-1 opacity-70">All running containers are managed by Sencho.</p>
@@ -765,9 +765,9 @@ export default function ResourcesView() {
style={{ animationDelay: `${gi * 60}ms` }}
>
{/* Project header */}
<div className="bg-orange-500/8 border-b border-orange-500/15 px-4 py-2 font-medium text-xs flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-orange-500 animate-pulse shrink-0" />
<span className="text-orange-600 dark:text-orange-400">External Project:</span>
<div className="bg-warning/8 border-b border-warning/15 px-4 py-2 font-medium text-xs flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-warning animate-pulse shrink-0" />
<span className="text-warning">External Project:</span>
<span className="font-mono text-foreground">{project}</span>
<span className="ml-auto text-muted-foreground font-normal">{containers.length} container{containers.length !== 1 ? 's' : ''}</span>
</div>
@@ -785,7 +785,7 @@ export default function ResourcesView() {
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-mono text-xs font-semibold truncate">
<span className="font-mono text-xs font-medium truncate">
{container.Names[0]?.replace(/^\//, '') || container.Id.substring(0, 12)}
</span>
<Badge
@@ -823,8 +823,8 @@ export default function ResourcesView() {
Prune All Docker {confirmPrune?.target}
</AlertDialogTitle>
<AlertDialogDescription>
This will prune <span className="font-semibold text-foreground">all</span> unused {confirmPrune?.target} from the Docker daemon -
including those from <span className="font-semibold text-foreground">external projects not managed by Sencho</span>. This cannot be undone.
This will prune <span className="font-medium text-foreground">all</span> unused {confirmPrune?.target} from the Docker daemon -
including those from <span className="font-medium text-foreground">external projects not managed by Sencho</span>. This cannot be undone.
</AlertDialogDescription>
</>
) : (
@@ -832,7 +832,7 @@ export default function ResourcesView() {
<AlertDialogTitle>Prune Sencho-Managed {confirmPrune?.target}</AlertDialogTitle>
<AlertDialogDescription>
Only unused {confirmPrune?.target} belonging to your Sencho stacks will be removed.
External Docker resources are <span className="font-semibold text-foreground">not affected</span>.
External Docker resources are <span className="font-medium text-foreground">not affected</span>.
</AlertDialogDescription>
</>
)}
@@ -856,7 +856,7 @@ export default function ResourcesView() {
<AlertDialogHeader>
<AlertDialogTitle>Delete {confirmDelete?.type.slice(0, -1)}</AlertDialogTitle>
<AlertDialogDescription>
Permanently delete <span className="font-mono font-bold text-foreground">{confirmDelete?.name || confirmDelete?.id.substring(0, 12)}</span>? This cannot be undone.
Permanently delete <span className="font-mono font-medium text-foreground">{confirmDelete?.name || confirmDelete?.id.substring(0, 12)}</span>? This cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>