feat(ui): apply visual polish phase 4
Some checks are pending
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

This commit is contained in:
fullsizemalt 2026-01-01 18:49:56 -08:00
parent dcbb75180d
commit 6bdabb0e60
4 changed files with 101 additions and 100 deletions

View file

@ -141,40 +141,42 @@ export function LayoutEditor({ floorId, className }: LayoutEditorProps) {
} }
return ( return (
<div className={cn('flex h-full bg-slate-950', className)}> <div className={cn('flex h-full bg-background font-sans', className)}>
{/* Plant Type Sidebar */} {/* Plant Type Sidebar */}
<TypeLibrary <TypeLibrary
plantTypes={plantTypes} plantTypes={plantTypes}
onDragStart={handleDragStart} onDragStart={handleDragStart}
className="w-64 flex-shrink-0" className="w-72 flex-shrink-0"
/> />
{/* Main Canvas */} {/* Main Canvas */}
<div className="flex-1 overflow-auto p-6"> <div className="flex-1 overflow-auto p-8 relative">
{/* Header */} {/* Header */}
<div className="mb-6"> <div className="mb-8">
<h1 className="text-xl font-bold text-white"> <h1 className="text-3xl font-display font-bold text-foreground tracking-tight">
{floorData?.floor.property} {floorData?.floor.building} {floorData?.floor.property} {floorData?.floor.building}
</h1> </h1>
<p className="text-sm text-slate-400"> <p className="text-sm text-muted-foreground mt-1">
{floorData?.floor.name} {floorData?.stats.occupiedPositions}/{floorData?.stats.totalPositions} positions filled {floorData?.floor.name} <span className="text-primary">{floorData?.stats.occupiedPositions}</span>/{floorData?.stats.totalPositions} positions filled
</p> </p>
</div> </div>
{/* Rooms */} {/* Rooms */}
{floorData?.rooms.map(room => ( {floorData?.rooms.map(room => (
<div key={room.id} className="mb-8"> <div key={room.id} className="mb-10">
<h2 className="text-lg font-semibold text-white mb-3 flex items-center gap-2"> <h2 className="text-xl font-display font-semibold text-foreground mb-4 flex items-center gap-3">
<span <span
className="w-3 h-3 rounded-full" className="w-2 h-2 rounded-full ring-2 ring-primary/20"
style={{ backgroundColor: room.color || '#3b82f6' }} style={{ backgroundColor: room.color || 'var(--color-primary)' }}
/> />
{room.name} {room.name}
<span className="text-xs text-slate-400 font-normal">({room.code})</span> <span className="text-xs text-muted-foreground font-mono bg-secondary px-2 py-0.5 rounded ml-auto">
{room.code}
</span>
</h2> </h2>
{/* Racks Grid */} {/* Racks Grid */}
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-6">
{racks {racks
.filter(r => room.sections.some(s => s.id === r.id)) .filter(r => room.sections.some(s => s.id === r.id))
.map(rack => ( .map(rack => (
@ -192,8 +194,8 @@ export function LayoutEditor({ floorId, className }: LayoutEditorProps) {
{/* Empty state */} {/* Empty state */}
{(!floorData?.rooms || floorData.rooms.length === 0) && ( {(!floorData?.rooms || floorData.rooms.length === 0) && (
<div className="flex flex-col items-center justify-center h-64 text-slate-400"> <div className="flex flex-col items-center justify-center h-64 text-muted-foreground/50 border-2 border-dashed border-border rounded-xl">
<svg className="w-16 h-16 mb-4 text-slate-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-12 h-12 mb-4 opacity-20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
</svg> </svg>
<p className="text-lg font-medium">No rooms configured</p> <p className="text-lg font-medium">No rooms configured</p>
@ -204,9 +206,9 @@ export function LayoutEditor({ floorId, className }: LayoutEditorProps) {
{/* Selected Slot Panel */} {/* Selected Slot Panel */}
{selectedSlot && ( {selectedSlot && (
<div className="w-72 border-l border-slate-700 bg-slate-900 p-4"> <div className="w-80 border-l border-border bg-card/80 backdrop-blur-xl p-6 absolute right-0 top-0 bottom-0 shadow-2xl z-10 transition-transform animate-in slide-in-from-right">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-6">
<h3 className="text-sm font-semibold text-white">Slot Details</h3> <h3 className="text-lg font-display font-semibold text-foreground">Slot Details</h3>
<button <button
onClick={() => setSelectedSlot(null)} onClick={() => setSelectedSlot(null)}
className="text-slate-400 hover:text-white" className="text-slate-400 hover:text-white"

View file

@ -55,66 +55,65 @@ export function TypeLibrary({ plantTypes, onDragStart, onCreateType, className }
}; };
return ( return (
<div className={cn('flex flex-col h-full bg-slate-900 border-r border-slate-700', className)}> <div className={cn('flex flex-col h-full bg-card/50 backdrop-blur-md border-r border-border', className)}>
{/* Header */} {/* Header */}
<div className="p-3 border-b border-slate-700"> <div className="p-4 border-b border-border bg-card/30">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-white">Plant Library</h2> <h2 className="text-xs font-display font-bold uppercase tracking-widest text-muted-foreground">Library</h2>
{onCreateType && ( {onCreateType && (
<button <button
onClick={onCreateType} onClick={onCreateType}
className="text-xs px-2 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded transition-colors" className="text-xs px-2 py-1 bg-primary/10 text-primary hover:bg-primary/20 rounded-md transition-colors font-medium"
> >
+ New + NEW
</button> </button>
)} )}
</div> </div>
<div className="relative">
<input <input
type="text" type="text"
placeholder="Search strains..." placeholder="Search types..."
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
className="w-full px-3 py-1.5 text-sm bg-slate-800 border border-slate-600 rounded text-white placeholder-slate-400 focus:outline-none focus:border-emerald-500" className="w-full px-3 py-2 text-sm bg-secondary/50 border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20 transition-all"
/> />
</div> </div>
</div>
{/* Categories */} {/* Categories */}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto custom-scrollbar">
{CATEGORY_ORDER.map(category => { {CATEGORY_ORDER.map(category => {
const types = groupedTypes[category] || []; const types = groupedTypes[category] || [];
if (types.length === 0 && search) return null; if (types.length === 0 && search) return null;
const isExpanded = expandedCategories.has(category); const isExpanded = expandedCategories.has(category);
const categoryData = CATEGORY_LABELS[category]; const categoryData = CATEGORY_LABELS[category];
return ( return (
<div key={category} className="border-b border-slate-800"> <div key={category} className="border-b border-border/50">
<button <button
onClick={() => toggleCategory(category)} onClick={() => toggleCategory(category)}
className="w-full px-3 py-2 flex items-center justify-between text-left hover:bg-slate-800 transition-colors" className="w-full px-4 py-3 flex items-center justify-between text-left hover:bg-secondary/30 transition-colors group"
> >
<span className="text-sm font-medium text-slate-300 flex items-center gap-2"> <span className="text-sm font-medium text-foreground flex items-center gap-2.5">
{categoryData ? ( {categoryData ? (
<> <div className="p-1 rounded bg-secondary text-primary group-hover:text-primary-hover transition-colors">
<categoryData.Icon className="w-4 h-4 text-emerald-500" /> <categoryData.Icon className="w-3.5 h-3.5" />
{categoryData.label} </div>
</> ) : null}
) : ( {categoryData?.label || category}
category <span className="text-xs text-muted-foreground font-normal">({types.length})</span>
)}
<span className="text-xs text-slate-500">({types.length})</span>
</span> </span>
<ChevronDown className={cn('w-4 h-4 text-slate-400 transition-transform', isExpanded && 'rotate-180')} /> <ChevronDown className={cn('w-4 h-4 text-muted-foreground transition-transform duration-200', isExpanded && 'rotate-180')} />
</button> </button>
{isExpanded && ( {isExpanded && (
<div className="pb-2"> <div className="pb-3 px-2 space-y-1">
{types.map(pt => ( {types.map(pt => (
<PlantTypeCard key={pt.slug} plantType={pt} onDragStart={onDragStart} /> <PlantTypeCard key={pt.slug} plantType={pt} onDragStart={onDragStart} />
))} ))}
{types.length === 0 && ( {types.length === 0 && (
<p className="px-3 py-2 text-xs text-slate-500 italic">No types in this category</p> <p className="px-4 py-2 text-xs text-muted-foreground italic">No types available</p>
)} )}
</div> </div>
)} )}
@ -124,9 +123,9 @@ export function TypeLibrary({ plantTypes, onDragStart, onCreateType, className }
</div> </div>
{/* Footer hint */} {/* Footer hint */}
<div className="p-2 border-t border-slate-700 bg-slate-800/50"> <div className="p-3 border-t border-border bg-card/30 backdrop-blur-sm">
<p className="text-xs text-slate-400 text-center"> <p className="text-[10px] uppercase tracking-wider text-muted-foreground text-center font-medium">
Drag types onto sections to place Drag to place
</p> </p>
</div> </div>
</div> </div>
@ -149,29 +148,29 @@ function PlantTypeCard({ plantType, onDragStart }: PlantTypeCardProps) {
<div <div
draggable draggable
onDragStart={handleDragStart} onDragStart={handleDragStart}
className="mx-2 mb-1 px-3 py-2 bg-slate-800 hover:bg-slate-700 rounded cursor-grab active:cursor-grabbing transition-colors group" className="mx-1 mb-1 px-3 py-2.5 bg-card border border-transparent hover:border-primary/50 hover:shadow-[0_0_15px_-3px_rgba(37,99,235,0.2)] rounded-lg cursor-grab active:cursor-grabbing transition-all duration-200 group relative overflow-hidden"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-3 relative z-10">
{/* Color indicator */} {/* Color indicator */}
<div <div
className="w-3 h-3 rounded-full flex-shrink-0" className="w-2.5 h-2.5 rounded-full flex-shrink-0 ring-1 ring-white/10"
style={{ backgroundColor: plantType.colour }} style={{ backgroundColor: plantType.colour }}
/> />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm text-white truncate">{plantType.name}</p> <p className="text-sm font-medium text-foreground truncate">{plantType.name}</p>
{plantType.strain && ( {plantType.strain && (
<p className="text-xs text-slate-400 truncate">{plantType.strain}</p> <p className="text-[11px] text-muted-foreground truncate font-mono">{plantType.strain}</p>
)} )}
</div> </div>
{/* Drag handle hint */} {/* Drag handle hint */}
<GripVertical className="w-4 h-4 text-slate-500 opacity-0 group-hover:opacity-100 transition-opacity" /> <GripVertical className="w-4 h-4 text-muted-foreground/30 opacity-0 group-hover:opacity-100 transition-opacity" />
</div> </div>
{/* Quick stats */} {/* Quick stats */}
{(plantType.growthDays || plantType.yieldGrams) && ( {(plantType.growthDays || plantType.yieldGrams) && (
<div className="mt-1 flex gap-3 text-xs text-slate-500"> <div className="mt-2 flex gap-3 text-[10px] text-muted-foreground border-t border-border/50 pt-1.5">
{plantType.growthDays && <span>{plantType.growthDays}d</span>} {plantType.growthDays && <span>{plantType.growthDays}d</span>}
{plantType.yieldGrams && <span>{plantType.yieldGrams}g</span>} {plantType.yieldGrams && <span>{plantType.yieldGrams}g</span>}
</div> </div>

View file

@ -107,41 +107,41 @@
--radius: 16px; --radius: 16px;
} }
/* ===== DARK MODE (Primary Theme) ===== */ /* ===== DARK MODE (Fediversion Theme) ===== */
.dark { .dark {
/* Backgrounds - Deep blue-charcoal */ /* Backgrounds - Zinc scale */
--color-bg-primary: #0B1020; --color-bg-primary: #09090b; /* zinc-950 */
--color-bg-secondary: #11182A; --color-bg-secondary: #18181b; /* zinc-900 */
--color-bg-tertiary: #151C30; --color-bg-tertiary: #27272a; /* zinc-800 */
--color-bg-elevated: #11182A; --color-bg-elevated: #18181b;
/* Text - improved contrast */ /* Text - improved contrast */
--color-text-primary: #F8FAFC; --color-text-primary: #FAFAFA;
--color-text-secondary: #CBD5E1; --color-text-secondary: #A1A1AA;
--color-text-tertiary: #94A3B8; --color-text-tertiary: #71717A;
--color-text-quaternary: #64748B; --color-text-quaternary: #52525B;
--color-text-inverse: #0F172A; --color-text-inverse: #09090b;
/* Borders - more visible */ /* Borders - cleaner zinc */
--color-border-default: #334155; --color-border-default: #27272a;
--color-border-subtle: #1E293B; --color-border-subtle: #18181b;
--color-border-strong: #475569; --color-border-strong: #3f3f46;
/* Primary (Olive Green - Smart Farm) */ /* Primary (Electric Blue) */
--color-primary: #B8D449; --color-primary: #2563eb;
--color-primary-hover: #9BBF2A; --color-primary-hover: #3b82f6;
--color-primary-soft: #2A3518; --color-primary-soft: rgba(37, 99, 235, 0.15);
/* Accent (Orange) */ /* Accent (Cyan Glow) */
--color-accent: #FF8042; --color-accent: #06b6d4;
--color-accent-hover: #E66B2E; --color-accent-hover: #0891b2;
--color-accent-soft: #3D2515; --color-accent-soft: rgba(6, 182, 212, 0.15);
/* Status Colors */ /* Status Colors */
--color-success: #B8D449; --color-success: #22c55e;
--color-warning: #FACC15; --color-warning: #eab308;
--color-error: #F97373; --color-error: #ef4444;
--color-info: #38BDF8; --color-info: #3b82f6;
/* Chart colors */ /* Chart colors */
--color-chart-gridline: #1E293B; --color-chart-gridline: #1E293B;

View file

@ -68,31 +68,31 @@ export default {
muted: 'rgba(239, 68, 68, 0.15)', muted: 'rgba(239, 68, 68, 0.15)',
foreground: '#FFFFFF', foreground: '#FFFFFF',
}, },
// Legacy support (mapped to neutral) // Legacy support (mapped to variables)
border: "hsl(var(--border))", border: "var(--color-border-default)",
input: "hsl(var(--input))", input: "var(--color-border-default)",
ring: "hsl(var(--ring))", ring: "var(--color-primary)",
background: "hsl(var(--background))", background: "var(--color-bg-primary)",
foreground: "hsl(var(--foreground))", foreground: "var(--color-text-primary)",
primary: { primary: {
DEFAULT: "#5E6AD2", // Now accent color DEFAULT: "var(--color-primary)",
foreground: "#FFFFFF", foreground: "var(--color-text-inverse)",
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary))", DEFAULT: "var(--color-bg-tertiary)",
foreground: "hsl(var(--secondary-foreground))", foreground: "var(--color-text-secondary)",
}, },
muted: { muted: {
DEFAULT: "hsl(var(--muted))", DEFAULT: "var(--color-bg-secondary)",
foreground: "hsl(var(--muted-foreground))", foreground: "var(--color-text-tertiary)",
}, },
popover: { popover: {
DEFAULT: "hsl(var(--popover))", DEFAULT: "var(--color-bg-elevated)",
foreground: "hsl(var(--popover-foreground))", foreground: "var(--color-text-primary)",
}, },
card: { card: {
DEFAULT: "hsl(var(--card))", DEFAULT: "var(--color-bg-elevated)",
foreground: "hsl(var(--card-foreground))", foreground: "var(--color-text-primary)",
}, },
}, },
borderRadius: { borderRadius: {