feat(ui): Integrate AuraUI Hero component
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

- Installed framer-motion dependency
- Added Hero1 component with 'Digital Command Center' branding
- Replaced MetrcDashboardPage header with new Hero component
This commit is contained in:
fullsizemalt 2025-12-19 17:03:53 -08:00
parent da5af8c288
commit 4a2533f121
5 changed files with 770 additions and 436 deletions

64
docs/AURA_UI_PLAN.md Normal file
View file

@ -0,0 +1,64 @@
# AuraUI Integration Plan and Audit
## Snapshot Info
- **Commit Hash**: `da5af8c` (Before AuraUI)
- **Rollback Command**: `git reset --hard da5af8c && git push -f origin main`
## Component Audit
We have selected the following components from the [AuraUI Library](https://auraui.com) to enhance the `ca-grow-ops-manager`:
### 1. 3D Digital Twin Hero (`feature-4-3d`)
- **Visuals**: Dark mode, floating 3D elements (cubes/spheres) with lime/purple accents.
- **Use Case**: **Facility 3D Viewer Loading/Landing State**.
- **Reason**: Perfectly aligns with the "Digital Twin" core pillar. Replaces the generic "Loading..." spinner with a technical, high-end visual.
### 2. High-Trust Hero (`hero-1`)
- **Visuals**: Centered H1, clear subtext, primary/secondary action buttons.
- **Use Case**: **Main Dashboard Header**.
- **Reason**: Provides immediate access to critical actions ("Emergency Stop", "Metrc Sync") and sets the tone for the application.
### 3. Split-Screen Auth Form (`signup-1`)
- **Visuals**: Left side form, Right side high-quality operational image.
- **Use Case**: **Visitor Kiosk Check-In** and **SOP Acknowledgement**.
- **Reason**: The "Compliance" aspect needs to feel authoritative and polished. The split view allows us to show legal text/images alongside the signature input.
### 4. Minimal Navbar (`header-6`)
- **Visuals**: Dark background, subtle hover effects, active state indicators.
- **Use Case**: **Global App Shell Navigation**.
- **Reason**: The current sidebar is functional but bulky. Moving to a sleek top-nav (or refined side-nav) using these styles cleans up screen real estate for the 3D viewer.
### 5. Bento Grid (`feature-14`)
- **Visuals**: Grid of varied-size cards, bento-box style.
- **Use Case**: **Operations Dashboard**.
- **Reason**: We have too many disparate metrics (Temp, Humidity, Task Count, Compliance Status). A bento grid unifies them into a single "Command Center" view.
## Integration Strategy
### Prerequisites (Checked)
- [x] `clsx` (Installed)
- [x] `tailwind-merge` (Installed)
- [x] `class-variance-authority` (Installed)
- [x] `lucide-react` (Installed)
- [ ] `framer-motion` (Missing - **Install Required**)
### Implementation Order
1. **Install `framer-motion`**: Required for AuraUI animations.
2. **Setup Utils**: Ensure `lib/utils.ts` has the standard `cn()` helper (it likely does, but verify).
3. **Implement `Hero-1`**: Replace the Dashboard header first as a low-risk, high-impact test.
4. **Implement `Feature-4-3d`**: Add to the 3D Viewer page.
### Rollback Plan
If the new components break the build or styling:
1. Revert to the snapshot commit.
2. Clean `node_modules` if `framer-motion` causes issues.

View file

@ -19,6 +19,7 @@
"browser-image-compression": "^2.0.2", "browser-image-compression": "^2.0.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.23.26",
"i18next": "^25.7.2", "i18next": "^25.7.2",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"immer": "^11.0.1", "immer": "^11.0.1",
@ -5200,6 +5201,33 @@
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/framer-motion": {
"version": "12.23.26",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -6577,6 +6605,21 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View file

@ -22,6 +22,7 @@
"browser-image-compression": "^2.0.2", "browser-image-compression": "^2.0.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.23.26",
"i18next": "^25.7.2", "i18next": "^25.7.2",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"immer": "^11.0.1", "immer": "^11.0.1",

View file

@ -0,0 +1,245 @@
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Download,
Github,
Sparkles,
Loader,
Copy,
Check,
StopCircle,
Activity,
Leaf
} from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
/**
* AuraUI Hero 1 Component
* Customized for CA Grow Ops Manager
* Displays the main dashboard header with 'Digital Command Center' branding
*/
const Hero1 = () => {
const [isVisible, setIsVisible] = useState(false);
const [currentSentence, setCurrentSentence] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
const [showCorrected, setShowCorrected] = useState(false);
const [typingText, setTypingText] = useState("");
const [isTyping, setIsTyping] = useState(false);
const [isSelected, setIsSelected] = useState(false);
const [showPopup, setShowPopup] = useState(false);
// Customized demo text for Grow Ops context
const demoSentences = [
{
original: "Room 4 temp is 82 humidity 65 check veg lighting",
corrected:
"Room 4: Temp 82°F, Humidity 65%. Veg lighting inspection required.",
},
{
original: "feed batch 156 with heavy bloom schedule today",
corrected:
"Action: Feed Batch B-23-156. Schedule: Heavy Bloom. Date: Today.",
},
{
original: "visitor arrived for checkin name is john doe",
corrected:
"Visitor Log: John Doe checked in. NDA signature pending.",
},
];
useEffect(() => {
const timer = setTimeout(() => {
setIsVisible(true);
}, 300);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
const startDemo = () => {
// Reset all states
setIsTyping(true);
setIsSelected(false);
setShowCorrected(false);
setTypingText("");
// Type the text character by character - much faster
const text = demoSentences[currentSentence].original;
let i = 0;
const typeInterval = setInterval(() => {
if (i < text.length) {
setTypingText(text.substring(0, i + 1));
i++;
} else {
clearInterval(typeInterval);
setIsTyping(false);
// Wait a bit, then select text
setTimeout(() => {
setIsSelected(true);
// Wait for selection, then start refining
setTimeout(() => {
setIsAnimating(true);
// Show refined text after processing
setTimeout(() => {
setShowCorrected(true);
setIsAnimating(false);
setIsSelected(false);
// Wait before next cycle
setTimeout(() => {
setCurrentSentence(
(prev) => (prev + 1) % demoSentences.length,
);
}, 2500);
}, 1800);
}, 1200);
}, 800);
}
}, 25); // Much faster typing speed
};
const interval = setInterval(startDemo, 8500);
startDemo(); // Start immediately
return () => clearInterval(interval);
}, [currentSentence]);
return (
<section className="relative w-full overflow-hidden bg-background">
{/* Extended background gradient with enhanced grid */}
<div className="cosmic-gradient cosmic-grid-enhanced min-h-[60vh] flex flex-col justify-center items-center relative py-20">
{/* Hero Content */}
<div
className={cn(
"relative z-10 max-w-5xl mx-auto px-6 text-center transition-all duration-700 transform",
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
)}
>
<div className="flex justify-center mb-6">
<Badge variant="secondary" className="gap-2 cosmic-glass">
<Activity className="h-3 w-3 animate-pulse text-primary" />
System Status: Nominal
<Sparkles className="h-3 w-3 text-yellow-400" />
</Badge>
</div>
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tighter text-balance text-foreground mb-6 max-w-5xl">
Command Center for <span className="text-primary">Modern Cultivation</span>
</h1>
<p className="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto mb-8 leading-relaxed text-balance">
Real-time compliance, environmental monitoring, and task management in a single digital twin interface.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center pt-2 mb-16">
<Button
size="lg"
className="gap-2 h-12 px-8"
onClick={() => { }} // Hook up to actual actions
>
<Leaf className="h-5 w-5" />
Metrc Sync
</Button>
<Button variant="outline" size="lg" className="gap-2 h-12 px-8">
<Activity className="h-5 w-5" />
View Alerts
</Button>
</div>
{/* Demo Box - Made taller */}
<div className="max-w-4xl mx-auto mb-8 cursor-default select-none pointer-events-none opacity-80 scale-90">
<div className="relative">
{/* Window-like demo box */}
<div className="bg-card/90 backdrop-blur-md rounded-xl border border-border/50 shadow-2xl overflow-hidden">
{/* Window header */}
<div className="bg-muted/30 px-4 py-3 border-b border-border/30 flex items-center gap-2">
<div className="flex gap-2">
<div className="w-3 h-3 rounded-full bg-red-500/70"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500/70"></div>
<div className="w-3 h-3 rounded-full bg-green-500/70"></div>
</div>
<div className="text-sm text-muted-foreground ml-4">
AI Compliance Assistant
</div>
</div>
{/* Content area - Made taller */}
<div className="p-8 min-h-[120px] flex items-center justify-center">
<div className="w-full max-w-2xl">
<div
className={cn(
"text-lg leading-relaxed transition-all duration-500",
isAnimating ? "bg-accent/30 rounded-md p-3" : "",
isSelected ? "bg-primary/20 rounded-md p-3" : ""
)}
>
{showCorrected ? (
<span className="text-foreground font-mono">
{demoSentences[currentSentence].corrected}
</span>
) : (
<span className="text-muted-foreground font-mono">
{typingText}
{isTyping && <span className="animate-pulse">_</span>}
</span>
)}
</div>
{/* Status indicator */}
<div className="flex items-center gap-2 mt-4 text-xs font-mono uppercase tracking-widest">
{isAnimating ? (
<>
<Loader className="h-3 w-3 animate-spin text-primary" />
<span className="text-muted-foreground">
Processing SOP...
</span>
</>
) : showCorrected ? (
<>
<Check className="h-3 w-3 text-green-500" />
<span className="text-muted-foreground">
Compliance Verified
</span>
</>
) : isSelected ? (
<>
<Copy className="h-3 w-3 text-blue-500" />
<span className="text-muted-foreground">
Analyzing Input
</span>
</>
) : (
<>
<span className="text-muted-foreground">
Awaiting Voice/Text Command...
</span>
</>
)}
</div>
</div>
</div>
</div>
{/* Glow effect below the demo box - moved up and positioned better */}
<div className="absolute bottom-16 left-1/2 -translate-x-1/2 w-[600px] h-[120px] opacity-20 bg-primary blur-[80px] rounded-full -z-20"></div>
</div>
</div>
</div>
</div>
{showPopup && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
{/* Popup content */}
</div>
)}
</section>
);
};
export default Hero1;

View file

@ -7,6 +7,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { metrcApi, MetrcLocation, MetrcDiscrepancy, MetrcReportResponse, MetrcAuditResponse } from '../lib/metrcApi'; import { metrcApi, MetrcLocation, MetrcDiscrepancy, MetrcReportResponse, MetrcAuditResponse } from '../lib/metrcApi';
import { PageHeader, EmptyState, CardSkeleton } from '../components/ui/LinearPrimitives'; import { PageHeader, EmptyState, CardSkeleton } from '../components/ui/LinearPrimitives';
import Hero1 from '../components/aura/Hero';
const SEVERITY_CONFIG = { const SEVERITY_CONFIG = {
CRITICAL: { color: 'text-red-600 bg-red-100 dark:bg-red-900/30', label: 'Critical' }, CRITICAL: { color: 'text-red-600 bg-red-100 dark:bg-red-900/30', label: 'Critical' },
@ -88,35 +89,9 @@ export default function MetrcDashboardPage() {
return ( return (
<div className="space-y-6 pb-20 animate-in"> <div className="space-y-6 pb-20 animate-in">
<PageHeader <Hero1 />
title="METRC Integration"
subtitle="California Track-and-Trace Compliance"
actions={
<div className="flex gap-2">
<button
onClick={handleExportCsv}
className="btn btn-ghost"
title="Export CSV for manual METRC upload"
>
<Download size={16} />
<span className="hidden sm:inline">Export</span>
</button>
<button
onClick={handleSync}
disabled={syncing}
className="btn btn-primary"
>
{syncing ? (
<Loader2 size={16} className="animate-spin" />
) : (
<RefreshCw size={16} />
)}
<span className="hidden sm:inline">Sync</span>
</button>
</div>
}
/>
<div className="px-6 space-y-6">
{/* Connection Status Banner */} {/* Connection Status Banner */}
<div className={`card p-4 ${connectionStatus === 'demo' ? 'bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800' : 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800'}`}> <div className={`card p-4 ${connectionStatus === 'demo' ? 'bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800' : 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800'}`}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -156,7 +131,8 @@ export default function MetrcDashboardPage() {
{/* Tab Navigation */} {/* Tab Navigation */}
<div className="flex gap-1 p-1 bg-secondary rounded-lg"> <div className="flex gap-1 p-1 bg-secondary rounded-lg">
{[ {
[
{ id: 'overview', label: 'Overview' }, { id: 'overview', label: 'Overview' },
{ id: 'plants', label: 'Plant Locations' }, { id: 'plants', label: 'Plant Locations' },
{ id: 'audit', label: 'Audit Trail' } { id: 'audit', label: 'Audit Trail' }
@ -171,11 +147,13 @@ export default function MetrcDashboardPage() {
> >
{tab.label} {tab.label}
</button> </button>
))} ))
</div> }
</div >
{loading ? ( {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4" >
{Array.from({ length: 4 }).map((_, i) => <CardSkeleton key={i} />)} {Array.from({ length: 4 }).map((_, i) => <CardSkeleton key={i} />)}
</div> </div>
) : ( ) : (
@ -471,9 +449,11 @@ export default function MetrcDashboardPage() {
</div> </div>
)} )}
</> </>
)} )
}
{/* Plant History Modal */} {/* Plant History Modal */}
{selectedHistoryPlant && ( {
selectedHistoryPlant && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
<div className="card w-full max-w-2xl max-h-[80vh] flex flex-col shadow-2xl animate-in zoom-in-95"> <div className="card w-full max-w-2xl max-h-[80vh] flex flex-col shadow-2xl animate-in zoom-in-95">
<div className="p-4 border-b border-subtle flex justify-between items-center"> <div className="p-4 border-b border-subtle flex justify-between items-center">
@ -545,7 +525,8 @@ export default function MetrcDashboardPage() {
</div> </div>
</div> </div>
</div> </div>
)} )
</div> }
</div >
); );
} }