From b20edc0c3313d116f41a4c96dfcaf5d90c44ec44 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:43:54 -0800 Subject: [PATCH] fix: Add missing heatmap components and User.name field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔧 Build Fixes: - Created FloorToggle component - Created HealthLegend component - Added name field to User interface Components complete for heatmap feature --- frontend/src/components/Layout.tsx | 80 ++++-- frontend/src/components/ThemeToggle.tsx | 89 +++++++ frontend/src/components/heatmap/BedCell.tsx | 160 +++++++++++ .../src/components/heatmap/BedTooltip.tsx | 153 +++++++++++ frontend/src/components/heatmap/FloorGrid.tsx | 117 ++++++++ .../src/components/heatmap/FloorToggle.tsx | 32 +++ .../components/heatmap/GrowRoomHeatmap.tsx | 250 ++++++++++++++++++ .../src/components/heatmap/HealthLegend.tsx | 38 +++ frontend/src/context/AuthContext.tsx | 1 + frontend/src/index.css | 185 +++++++++---- specs/grow-room-heatmap.md | 103 ++++++++ 11 files changed, 1134 insertions(+), 74 deletions(-) create mode 100644 frontend/src/components/ThemeToggle.tsx create mode 100644 frontend/src/components/heatmap/BedCell.tsx create mode 100644 frontend/src/components/heatmap/BedTooltip.tsx create mode 100644 frontend/src/components/heatmap/FloorGrid.tsx create mode 100644 frontend/src/components/heatmap/FloorToggle.tsx create mode 100644 frontend/src/components/heatmap/GrowRoomHeatmap.tsx create mode 100644 frontend/src/components/heatmap/HealthLegend.tsx create mode 100644 specs/grow-room-heatmap.md diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 2fe587d..6cb8344 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,55 +1,89 @@ import React from 'react'; import { Outlet, Link, useLocation } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; +import ThemeToggle from './ThemeToggle'; export default function Layout() { const { user, logout } = useAuth(); const location = useLocation(); const navItems = [ - { label: 'Dashboard', path: '/' }, - { label: 'Rooms', path: '/rooms' }, - { label: 'Batches', path: '/batches' }, - { label: 'Timeclock', path: '/timeclock' }, + { label: 'Dashboard', path: '/', icon: '📊' }, + { label: 'Daily Walkthrough', path: '/walkthrough', icon: '✅' }, + { label: 'Rooms', path: '/rooms', icon: '🏠' }, + { label: 'Batches', path: '/batches', icon: '🌱' }, + { label: 'Timeclock', path: '/timeclock', icon: '⏰' }, ]; return ( -
+
+ {/* Skip to main content link (accessibility) */} + + Skip to main content + + {/* Sidebar */} -
-
-

CA GROW OPS

-

Manager v0.1

+
+
+
+ 777 Wolfpack +
+

+ 777 WOLFPACK +

+

+ Grow Ops Manager +

+
+
+ + {/* Theme Toggle */} +
-
+
{navItems.map(item => ( - {item.label} + {item.icon} + {item.label} ))}
-
+
-
+
{user?.email[0].toUpperCase()}
-
-

{user?.email}

-

{user?.role}

+
+

+ {user?.name || user?.email} +

+

+ {user?.role} +

@@ -57,7 +91,11 @@ export default function Layout() {
{/* Main Content */} -
+
diff --git a/frontend/src/components/ThemeToggle.tsx b/frontend/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..df0e821 --- /dev/null +++ b/frontend/src/components/ThemeToggle.tsx @@ -0,0 +1,89 @@ +import { useState, useEffect } from 'react'; + +/** + * ThemeToggle - Dark/Light mode toggle button + * + * Provides accessible theme switching with system preference detection. + * Persists user preference to localStorage. + * + * Accessibility features: + * - ARIA labels for screen readers + * - Keyboard navigation support + * - Visual focus indicators + * - Respects prefers-color-scheme + */ + +type Theme = 'light' | 'dark' | 'system'; + +export default function ThemeToggle() { + const [theme, setTheme] = useState('system'); + + useEffect(() => { + // Load saved theme preference + const savedTheme = localStorage.getItem('theme') as Theme | null; + if (savedTheme) { + setTheme(savedTheme); + applyTheme(savedTheme); + } else { + applyTheme('system'); + } + }, []); + + function applyTheme(newTheme: Theme) { + const root = window.document.documentElement; + root.classList.remove('light', 'dark'); + + if (newTheme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light'; + root.classList.add(systemTheme); + } else { + root.classList.add(newTheme); + } + } + + function handleThemeChange(newTheme: Theme) { + setTheme(newTheme); + localStorage.setItem('theme', newTheme); + applyTheme(newTheme); + } + + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/heatmap/BedCell.tsx b/frontend/src/components/heatmap/BedCell.tsx new file mode 100644 index 0000000..82e4f62 --- /dev/null +++ b/frontend/src/components/heatmap/BedCell.tsx @@ -0,0 +1,160 @@ +import { useState } from 'react'; +import { Bed } from './GrowRoomHeatmap'; +import BedTooltip from './BedTooltip'; + +/** + * BedCell - Individual bed cell in the grid + * + * Renders a single bed with color-coded health score. + * Shows tooltip on hover with detailed bed information. + * + * Color scale: + * - 90-100: Dark green (excellent) + * - 70-89: Light green (good) + * - 50-69: Yellow (fair) + * - 30-49: Orange (needs attention) + * - 0-29: Red (critical) + * - Empty: Gray outline (no plant) + */ + +interface BedCellProps { + bed?: Bed; + x: number; + y: number; + size: number; + row: number; + column: number; +} + +export default function BedCell({ bed, x, y, size, row, column }: BedCellProps) { + const [showTooltip, setShowTooltip] = useState(false); + const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); + + // Get color based on health score + const getHealthColor = (score: number): string => { + if (score >= 90) return '#059669'; // emerald-600 + if (score >= 70) return '#10b981'; // emerald-500 + if (score >= 50) return '#eab308'; // yellow-500 + if (score >= 30) return '#f97316'; // orange-500 + return '#dc2626'; // red-600 + }; + + const handleMouseEnter = (e: React.MouseEvent) => { + if (bed && bed.status !== 'empty') { + setTooltipPosition({ x: e.clientX, y: e.clientY }); + setShowTooltip(true); + } + }; + + const handleMouseLeave = () => { + setShowTooltip(false); + }; + + const handleClick = () => { + if (bed && bed.status !== 'empty') { + // TODO: Navigate to bed detail page + console.log('Navigate to bed:', bed.bed_id); + } + }; + + // Empty bed + if (!bed || bed.status === 'empty') { + return ( + + + + Empty + + + ); + } + + const healthColor = getHealthColor(bed.health_score); + const hasAlert = !!bed.last_alert; + + return ( + <> + + {/* Main cell */} + + + {/* Health score text */} + + {bed.health_score} + + + {/* Alert indicator */} + {hasAlert && ( + + )} + + {/* Batch ID (small text) */} + {bed.plant_batch_id && ( + + {bed.plant_batch_id.substring(0, 8)} + + )} + + + {/* Tooltip */} + {showTooltip && bed && ( + setShowTooltip(false)} + /> + )} + + ); +} diff --git a/frontend/src/components/heatmap/BedTooltip.tsx b/frontend/src/components/heatmap/BedTooltip.tsx new file mode 100644 index 0000000..c857499 --- /dev/null +++ b/frontend/src/components/heatmap/BedTooltip.tsx @@ -0,0 +1,153 @@ +import { useEffect, useRef } from 'react'; +import { Bed } from './GrowRoomHeatmap'; + +/** + * BedTooltip - Hover tooltip showing bed details + * + * Displays key information about a bed when hovering over it. + * Positioned near the mouse cursor. + */ + +interface BedTooltipProps { + bed: Bed; + position: { x: number; y: number }; + onClose: () => void; +} + +export default function BedTooltip({ bed, position, onClose }: BedTooltipProps) { + const tooltipRef = useRef(null); + + // Close on click outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (tooltipRef.current && !tooltipRef.current.contains(event.target as Node)) { + onClose(); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [onClose]); + + const getHealthLabel = (score: number): string => { + if (score >= 90) return 'Excellent'; + if (score >= 70) return 'Good'; + if (score >= 50) return 'Fair'; + if (score >= 30) return 'Needs Attention'; + return 'Critical'; + }; + + const getHealthColor = (score: number): string => { + if (score >= 90) return 'text-emerald-600 dark:text-emerald-400'; + if (score >= 70) return 'text-emerald-500 dark:text-emerald-300'; + if (score >= 50) return 'text-yellow-500 dark:text-yellow-300'; + if (score >= 30) return 'text-orange-500 dark:text-orange-300'; + return 'text-red-600 dark:text-red-400'; + }; + + return ( +
+ {/* Header */} +
+
+
+ Bed {bed.bed_id} +
+ {bed.plant_batch_id && ( +
+ Batch: {bed.plant_batch_id} +
+ )} +
+ +
+ + {/* Health Score */} +
+
+ + Health Score + + + {bed.health_score} + +
+
+ {getHealthLabel(bed.health_score)} +
+
+ + {/* Sensors */} + {bed.sensors && ( +
+
+ Sensor Readings +
+
+ {bed.sensors.temp !== undefined && ( + + )} + {bed.sensors.humidity !== undefined && ( + + )} + {bed.sensors.ec !== undefined && ( + + )} + {bed.sensors.par !== undefined && ( + + )} +
+
+ )} + + {/* Last Alert */} + {bed.last_alert && ( +
+
+ ⚠️ Last Alert: {bed.last_alert} +
+
+ )} + + {/* Actions */} +
+ +
+
+ ); +} + +function SensorReading({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} diff --git a/frontend/src/components/heatmap/FloorGrid.tsx b/frontend/src/components/heatmap/FloorGrid.tsx new file mode 100644 index 0000000..8b9be73 --- /dev/null +++ b/frontend/src/components/heatmap/FloorGrid.tsx @@ -0,0 +1,117 @@ +import { Bed } from './GrowRoomHeatmap'; +import BedCell from './BedCell'; + +/** + * FloorGrid - SVG-based grid visualization of grow room beds + * + * Renders a grid of beds with color-coded health scores. + * Uses SVG for crisp rendering at any size. + * + * @param floor - Current floor being displayed + * @param rows - Number of rows in the grid + * @param columns - Number of columns in the grid + * @param beds - Array of bed data for this floor + */ + +interface FloorGridProps { + floor: 'floor_1' | 'floor_2'; + rows: number; + columns: number; + beds: Bed[]; +} + +export default function FloorGrid({ floor, rows, columns, beds }: FloorGridProps) { + // Cell dimensions + const cellSize = 80; + const cellGap = 8; + const padding = 40; + + // Calculate SVG dimensions + const width = columns * (cellSize + cellGap) - cellGap + padding * 2; + const height = rows * (cellSize + cellGap) - cellGap + padding * 2; + + // Create a map for quick bed lookup + const bedMap = new Map(); + beds.forEach(bed => { + const key = `${bed.row}_${bed.column}`; + bedMap.set(key, bed); + }); + + return ( +
+ + {/* Grid background */} + + + {/* Row labels */} + {Array.from({ length: rows }).map((_, row) => ( + + {String.fromCharCode(65 + row)} + + ))} + + {/* Column labels */} + {Array.from({ length: columns }).map((_, col) => ( + + {col + 1} + + ))} + + {/* Bed cells */} + {Array.from({ length: rows }).map((_, row) => + Array.from({ length: columns }).map((_, col) => { + const key = `${row}_${col}`; + const bed = bedMap.get(key); + + return ( + + ); + }) + )} + + {/* Floor label */} + + {floor === 'floor_1' ? 'Floor 1 (Ground Level)' : 'Floor 2 (Scaffolding)'} + + +
+ ); +} diff --git a/frontend/src/components/heatmap/FloorToggle.tsx b/frontend/src/components/heatmap/FloorToggle.tsx new file mode 100644 index 0000000..d0c7d41 --- /dev/null +++ b/frontend/src/components/heatmap/FloorToggle.tsx @@ -0,0 +1,32 @@ +/** + * FloorToggle - Toggle between floor levels + * + * Simple button group to switch between Floor 1 and Floor 2. + */ + +interface FloorToggleProps { + floors: Array<'floor_1' | 'floor_2'>; + currentFloor: 'floor_1' | 'floor_2'; + onFloorChange: (floor: 'floor_1' | 'floor_2') => void; +} + +export default function FloorToggle({ floors, currentFloor, onFloorChange }: FloorToggleProps) { + return ( +
+ {floors.map(floor => ( + + ))} +
+ ); +} diff --git a/frontend/src/components/heatmap/GrowRoomHeatmap.tsx b/frontend/src/components/heatmap/GrowRoomHeatmap.tsx new file mode 100644 index 0000000..722a17b --- /dev/null +++ b/frontend/src/components/heatmap/GrowRoomHeatmap.tsx @@ -0,0 +1,250 @@ +import { useState, useEffect } from 'react'; +import FloorGrid from './FloorGrid'; +import FloorToggle from './FloorToggle'; +import HealthLegend from './HealthLegend'; + +/** + * GrowRoomHeatmap - Visual heat-map representation of plant health + * + * This component displays a multi-level grow room with color-coded beds + * based on plant health scores. Reduces cognitive load by showing problems + * as visual zones instead of text lists. + * + * @param roomId - Unique identifier for the grow room + * + * Integration: + * 1. Replace mockFetchRoomLayout with real API call to /api/rooms/:roomId/layout + * 2. Replace mockFetchRoomHealth with real API call to /api/rooms/:roomId/health + * 3. For real-time updates, add WebSocket connection or polling interval + * 4. Add error handling and loading states + */ + +export interface Bed { + bed_id: string; + floor: 'floor_1' | 'floor_2'; + row: number; + column: number; + plant_batch_id?: string; + status: 'active' | 'empty' | 'maintenance'; + health_score: number; // 0-100 + sensors?: { + temp?: number; + humidity?: number; + ec?: number; + par?: number; + }; + last_alert?: string; +} + +export interface RoomLayout { + room_id: string; + name: string; + floors: Array<'floor_1' | 'floor_2'>; + grid: { + rows: number; + columns: number; + }; +} + +interface GrowRoomHeatmapProps { + roomId: string; +} + +export default function GrowRoomHeatmap({ roomId }: GrowRoomHeatmapProps) { + const [currentFloor, setCurrentFloor] = useState<'floor_1' | 'floor_2'>('floor_1'); + const [layout, setLayout] = useState(null); + const [beds, setBeds] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadRoomData(); + + // TODO: For real-time updates, add polling or WebSocket + // const interval = setInterval(loadRoomData, 30000); // Poll every 30s + // return () => clearInterval(interval); + }, [roomId]); + + async function loadRoomData() { + setIsLoading(true); + setError(null); + + try { + // TODO: Replace with real API calls + const layoutData = await mockFetchRoomLayout(roomId); + const healthData = await mockFetchRoomHealth(roomId); + + setLayout(layoutData); + setBeds(healthData); + } catch (err: any) { + setError(err.message || 'Failed to load room data'); + } finally { + setIsLoading(false); + } + } + + // Filter beds for current floor + const currentFloorBeds = beds.filter(bed => bed.floor === currentFloor); + + if (isLoading) { + return ( +
+
+ Loading room data... +
+
+ ); + } + + if (error) { + return ( +
+
+ Error: {error} +
+
+ ); + } + + if (!layout) { + return ( +
+
+ No room data available +
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

+ {layout.name} +

+

+ Plant Health Heat Map +

+
+ + +
+ + {/* Legend */} + + + {/* Grid */} +
+ +
+ + {/* Stats Summary */} +
+ + b.status === 'active').length} + color="green" + /> + b.health_score < 70).length} + color="yellow" + /> + b.health_score < 30).length} + color="red" + /> +
+
+ ); +} + +// Stats card component +function StatCard({ label, value, color }: { label: string; value: number; color: string }) { + const colorClasses = { + blue: 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300', + green: 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300', + yellow: 'bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300', + red: 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300', + }; + + return ( +
+
{value}
+
{label}
+
+ ); +} + +// ============================================================================ +// MOCK API FUNCTIONS +// TODO: Replace these with real API calls +// ============================================================================ + +async function mockFetchRoomLayout(roomId: string): Promise { + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 500)); + + return { + room_id: roomId, + name: 'Veg Room A', + floors: ['floor_1', 'floor_2'], + grid: { + rows: 6, + columns: 8, + }, + }; +} + +async function mockFetchRoomHealth(roomId: string): Promise { + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 500)); + + const beds: Bed[] = []; + + // Generate mock data for both floors + for (const floor of ['floor_1', 'floor_2'] as const) { + for (let row = 0; row < 6; row++) { + for (let col = 0; col < 8; col++) { + // Randomly make some beds empty + const isEmpty = Math.random() < 0.2; + + beds.push({ + bed_id: `${floor}_${row}_${col}`, + floor, + row, + column: col, + plant_batch_id: isEmpty ? undefined : `batch_${Math.floor(Math.random() * 10)}`, + status: isEmpty ? 'empty' : 'active', + health_score: isEmpty ? 0 : Math.floor(Math.random() * 100), + sensors: isEmpty ? undefined : { + temp: 72 + Math.random() * 8, + humidity: 55 + Math.random() * 15, + ec: 1.2 + Math.random() * 0.8, + par: 400 + Math.random() * 400, + }, + last_alert: isEmpty ? undefined : (Math.random() < 0.3 ? '2 hours ago' : undefined), + }); + } + } + } + + return beds; +} diff --git a/frontend/src/components/heatmap/HealthLegend.tsx b/frontend/src/components/heatmap/HealthLegend.tsx new file mode 100644 index 0000000..dbfcc99 --- /dev/null +++ b/frontend/src/components/heatmap/HealthLegend.tsx @@ -0,0 +1,38 @@ +/** + * HealthLegend - Color scale legend for health scores + * + * Shows the color-to-health-score mapping for the heatmap. + */ + +export default function HealthLegend() { + const legendItems = [ + { range: '90-100', label: 'Excellent', color: 'bg-emerald-600' }, + { range: '70-89', label: 'Good', color: 'bg-emerald-500' }, + { range: '50-69', label: 'Fair', color: 'bg-yellow-500' }, + { range: '30-49', label: 'Needs Attention', color: 'bg-orange-500' }, + { range: '0-29', label: 'Critical', color: 'bg-red-600' }, + ]; + + return ( +
+

+ Health Score Legend +

+
+ {legendItems.map(item => ( +
+
+
+
+ {item.range} +
+
+ {item.label} +
+
+
+ ))} +
+
+ ); +} diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index 8141c01..827dd1e 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -4,6 +4,7 @@ import api from '../lib/api'; interface User { id: string; email: string; + name?: string; role: string; } diff --git a/frontend/src/index.css b/frontend/src/index.css index 03a5f85..6819dc7 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,118 +1,197 @@ @tailwind base; @tailwind components; @tailwind utilities; - + @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; - + --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; - + --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; - - --primary: 151 76% 40%; /* Emerald 500 equivalent in HSL approx */ + + --primary: 151 76% 40%; + /* Emerald 500 equivalent in HSL approx */ --primary-foreground: 210 40% 98%; - + --secondary: 217.2 91.2% 59.8%; --secondary-foreground: 222.2 47.4% 11.2%; - + --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; - + --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; - + --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%; - + --radius: 0.5rem; } - + .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; - + --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; - + --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; - + --primary: 151 76% 40%; --primary-foreground: 222.2 47.4% 11.2%; - + --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; - + --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; - + --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; - + --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%; } } - + @layer base { * { @apply border-border; } - + html { /* Prevent text size adjustment on orientation change (iOS) */ -webkit-text-size-adjust: 100%; - /* Enable smooth scrolling */ scroll-behavior: smooth; } - + body { - @apply bg-background text-foreground; - /* Minimum font size for mobile (prevents zoom on iOS) */ - font-size: 16px; - /* Improve font rendering */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + @apply bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-50; + font-family: var(--font-sans); + font-feature-settings: 'rlig' 1, 'calt' 1; } - - /* Touch-friendly button defaults */ - button, - [role="button"], - input[type="submit"], - input[type="button"] { - /* Minimum touch target size */ - min-height: 44px; - min-width: 44px; - /* Remove tap highlight on mobile */ - -webkit-tap-highlight-color: transparent; - /* Improve touch response */ - touch-action: manipulation; + + /* Code and monospace */ + code, + pre, + kbd, + samp { + font-family: var(--font-mono); } - - /* Touch-friendly form inputs */ + + /* Touch-friendly interactive elements */ + button, input, select, - textarea { - /* Minimum height for easy tapping */ + textarea, + a { min-height: 44px; - /* Prevent zoom on focus (iOS) */ - font-size: 16px; + min-width: 44px; + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; } - - /* Improve touch scrolling */ - * { - -webkit-overflow-scrolling: touch; + + /* Focus visible for keyboard navigation (accessibility) */ + *:focus-visible { + @apply outline-none ring-2 ring-emerald-500 ring-offset-2 ring-offset-white dark:ring-offset-slate-900; + } + + /* Skip to main content link (accessibility) */ + .skip-to-main { + @apply absolute left-0 top-0 -translate-y-full bg-emerald-600 text-white px-4 py-2 rounded-br-lg; + @apply focus:translate-y-0 z-50; + } + + /* Reduced motion for accessibility */ + @media (prefers-reduced-motion: reduce) { + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + } + + /* High contrast mode support */ + @media (prefers-contrast: high) { + :root { + --color-primary: 0 128 0; + } + + .dark { + --color-primary: 0 255 0; + } + } + + /* Headings */ + h1, + h2, + h3, + h4, + h5, + h6 { + @apply font-semibold tracking-tight; + } + + h1 { + @apply text-4xl md:text-5xl; + } + + h2 { + @apply text-3xl md:text-4xl; + } + + h3 { + @apply text-2xl md:text-3xl; + } + + h4 { + @apply text-xl md:text-2xl; + } + + /* Links */ + a { + @apply text-emerald-600 dark:text-emerald-400 hover:underline; + } + + /* Selection */ + ::selection { + @apply bg-emerald-500/20 text-emerald-900 dark:text-emerald-100; } } + +@layer components { + + /* Custom scrollbar */ + .custom-scrollbar::-webkit-scrollbar { + @apply w-2 h-2; + } + + .custom-scrollbar::-webkit-scrollbar-track { + @apply bg-slate-100 dark:bg-slate-800; + } + + .custom-scrollbar::-webkit-scrollbar-thumb { + @apply bg-slate-300 dark:bg-slate-600 rounded-full; + } + + .custom-scrollbar::-webkit-scrollbar-thumb:hover { + @apply bg-slate-400 dark:bg-slate-500; + } +} \ No newline at end of file diff --git a/specs/grow-room-heatmap.md b/specs/grow-room-heatmap.md new file mode 100644 index 0000000..a82990b --- /dev/null +++ b/specs/grow-room-heatmap.md @@ -0,0 +1,103 @@ +# Grow Room Heatmap - Feature Spec + +**Priority**: 🟡 High (Phase 2) +**Team**: 777 Wolfpack +**Date**: 2025-12-09 +**Status**: Ready for Implementation + +--- + +## 📋 Overview + +Visual heat-map representation of plant health across a multi-level grow room. Reduces cognitive load by showing problems as color-coded zones instead of text lists. + +**Key Benefit**: Growers can instantly see which beds need attention at a glance. + +--- + +## 🏗️ Room Model + +### Structure + +- **2 Levels**: `floor_1` (ground), `floor_2` (scaffolding) +- **Fixed Grid**: Same rows/columns on each floor +- **Beds**: Individual growing beds in grid positions + +### Bed Data Model + +```typescript +interface Bed { + bed_id: string; + floor: 'floor_1' | 'floor_2'; + row: number; + column: number; + plant_batch_id?: string; + status: 'active' | 'empty' | 'maintenance'; + health_score: number; // 0-100 + sensors?: { + temp?: number; + humidity?: number; + ec?: number; + par?: number; + }; + last_alert?: string; +} +``` + +--- + +## 🎨 UX Design + +### Main Canvas + +- Grid visualization of room layout +- Floor toggle (Floor 1 / Floor 2) +- Color-coded heat map cells +- Hover tooltips with bed details +- Legend showing health score ranges + +### Color Scale + +- **90-100**: Dark green (excellent) +- **70-89**: Light green (good) +- **50-69**: Yellow (fair) +- **30-49**: Orange (needs attention) +- **0-29**: Red (critical) +- **Empty**: Gray outline (no plant) + +### Interactions + +- **Hover**: Show tooltip with bed info +- **Click**: Navigate to bed detail page +- **Toggle**: Switch between floors +- **Legend**: Show color scale reference + +--- + +## 🔧 Implementation Plan + +### Components + +1. `GrowRoomHeatmap.tsx` - Main container +2. `FloorGrid.tsx` - Grid visualization +3. `BedCell.tsx` - Individual bed cell +4. `BedTooltip.tsx` - Hover tooltip +5. `FloorToggle.tsx` - Floor selector +6. `HealthLegend.tsx` - Color scale legend + +### API Endpoints + +- `GET /api/rooms/:roomId/layout` - Room structure +- `GET /api/rooms/:roomId/health` - Current health data + +--- + +## 📊 Success Metrics + +- Growers can identify problem areas in < 5 seconds +- Reduce time spent reviewing text lists by 80% +- Increase early problem detection by 50% + +--- + +**Status**: ✅ Spec Complete - Ready for Implementation