| import { useState, useEffect } from "react"; | |
| import { NavLink, useLocation } from "react-router-dom"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| import { toast } from "react-toastify"; | |
| import { | |
| Home, Search, LayoutDashboard, BookOpen, Users, Briefcase, Settings, Bell, | |
| HelpCircle, Sparkles, Package, Brain, MessageSquare, Video, BarChart3, | |
| Award, Library, PenTool, Grid, ChevronUp, PanelLeft, Calendar, Activity, | |
| Inbox, Globe, FileText, FolderOpen, FileUp, Zap, FlaskConical, Shield, | |
| ShieldCheck, Tag, Heart, Wrench, Database | |
| } from "lucide-react"; | |
| export type DockItem = { | |
| to: string; | |
| label: string; | |
| icon: React.ComponentType<{ className?: string }>; | |
| exact?: boolean; | |
| }; | |
| const DEFAULT_DOCK_PATHS = [ | |
| "/", | |
| "/dashboard", | |
| "/articles", | |
| "/groups", | |
| "/projects", | |
| "/discovery", | |
| "/notifications", | |
| "/settings", | |
| ]; | |
| export const ALL_AVAILABLE_APPS: DockItem[] = [ | |
| { to: "/", label: "Home", icon: Home, exact: true }, | |
| { to: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, | |
| { to: "/articles", label: "Articles", icon: BookOpen }, | |
| { to: "/groups", label: "Groups", icon: Users }, | |
| { to: "/projects", label: "Projects", icon: Briefcase }, | |
| { to: "/discovery", label: "Explore", icon: Search }, | |
| { to: "/notifications", label: "Alerts", icon: Bell }, | |
| { to: "/settings", label: "Settings", icon: Settings }, | |
| { to: "/workspace", icon: PanelLeft, label: "Workspace" }, | |
| { to: "/calendar", icon: Calendar, label: "Calendar" }, | |
| { to: "/conference", icon: Video, label: "Conference" }, | |
| { to: "/roadmap", icon: Activity, label: "Roadmap" }, | |
| { to: "/peer-room", icon: MessageSquare, label: "Peer Room" }, | |
| { to: "/inbox", icon: Inbox, label: "Inbox" }, | |
| { to: "/messages", icon: MessageSquare, label: "Messages" }, | |
| { to: "/networking", icon: Globe, label: "Networking" }, | |
| { to: "/my-posts", icon: PenTool, label: "My Posts" }, | |
| { to: "/collections", icon: FolderOpen, label: "Collections" }, | |
| { to: "/submit-preprint", icon: FileUp, label: "Preprint" }, | |
| { to: "/editor", icon: BookOpen, label: "Sci Editor" }, | |
| { to: "/authoring-studio", icon: PenTool, label: "Authoring" }, | |
| { to: "/ai-co-scientist", icon: Brain, label: "AI Co-Scientist" }, | |
| { to: "/gap-analysis", icon: Activity, label: "Gap Analysis" }, | |
| { to: "/grant-assistant", icon: Award, label: "Grants" }, | |
| { to: "/research-assistant", icon: Zap, label: "Assistant" }, | |
| { to: "/lab-inventory", icon: FlaskConical, label: "Lab" }, | |
| { to: "/peer-review", icon: Shield, label: "Review" }, | |
| { to: "/trust-dashboard", icon: ShieldCheck, label: "Trust Dashboard" }, | |
| { to: "/analytics", icon: BarChart3, label: "Analytics" }, | |
| { to: "/tags", icon: Tag, label: "Tags" }, | |
| { to: "/recommendations", icon: Heart, label: "Recommendations" }, | |
| { to: "/tools", icon: Wrench, label: "App Store" }, | |
| { to: "/tools/builder", icon: Package, label: "Tool Studio" }, | |
| { to: "/tools/installed", icon: Package, label: "Installed Tools" }, | |
| { to: "/data-hub", icon: Database, label: "Data Hub" }, | |
| { to: "/api-docs", icon: FileText, label: "API Docs" }, | |
| { to: "/status", icon: Activity, label: "Status" }, | |
| ]; | |
| export const SECTIONS = [ | |
| { | |
| id: "main", | |
| label: "Main", | |
| items: [ | |
| { to: "/dashboard", icon: LayoutDashboard, label: "Dashboard", description: "Your research hub" }, | |
| { to: "/workspace", icon: PanelLeft, label: "Workspace", description: "Multi-panel view" }, | |
| { to: "/calendar", icon: Calendar, label: "Calendar", description: "Event schedules" }, | |
| { to: "/conference", icon: Video, label: "Conference", description: "Virtual conferences" }, | |
| { to: "/roadmap", icon: Activity, label: "Roadmap", description: "Future plans" }, | |
| ] | |
| }, | |
| { | |
| id: "communication", | |
| label: "Network", | |
| items: [ | |
| { to: "/peer-room", icon: MessageSquare, label: "Peer Room", description: "Peer collaboration" }, | |
| { to: "/inbox", icon: Inbox, label: "Inbox", description: "All conversations" }, | |
| { to: "/messages", icon: MessageSquare, label: "Messages", description: "Chat & groups" }, | |
| { to: "/groups", icon: Users, label: "Groups", description: "Research groups" }, | |
| { to: "/networking", icon: Globe, label: "Networking", description: "Connect with peers" }, | |
| ] | |
| }, | |
| { | |
| id: "content", | |
| label: "Content", | |
| items: [ | |
| { to: "/articles", icon: FileText, label: "Articles", description: "Discover papers" }, | |
| { to: "/my-posts", icon: PenTool, label: "My Posts", description: "Manage posts" }, | |
| { to: "/collections", icon: FolderOpen, label: "Collections", description: "Saved papers" }, | |
| { to: "/submit-preprint", icon: FileUp, label: "Preprint", description: "Submit paper" }, | |
| { to: "/editor", icon: BookOpen, label: "Sci Editor", description: "Write papers with LaTeX" }, | |
| { to: "/authoring-studio", icon: PenTool, label: "Authoring", description: "Write & edit" }, | |
| ] | |
| }, | |
| { | |
| id: "lab", | |
| label: "Lab & AI", | |
| items: [ | |
| { to: "/ai-co-scientist", icon: Brain, label: "AI Co-Scientist", description: "Virtual collaborator" }, | |
| { to: "/gap-analysis", icon: Activity, label: "Gap Analysis", description: "Research gaps" }, | |
| { to: "/grant-assistant", icon: Award, label: "Grants", description: "Proposal writing" }, | |
| { to: "/research-assistant", icon: Zap, label: "Research Assistant", description: "AI-powered help" }, | |
| { to: "/lab-inventory", icon: FlaskConical, label: "Lab", description: "Inventory & safety" }, | |
| { to: "/peer-review", icon: Shield, label: "Review", description: "Anonymous peer review" }, | |
| { to: "/projects", icon: Briefcase, label: "Projects", description: "Manage projects" }, | |
| { to: "/trust-dashboard", icon: ShieldCheck, label: "Trust Dashboard", description: "Security & verify" }, | |
| ] | |
| }, | |
| { | |
| id: "data", | |
| label: "Data", | |
| items: [ | |
| { to: "/analytics", icon: BarChart3, label: "Analytics", description: "Research metrics" }, | |
| { to: "/discovery", icon: Search, label: "Discovery", description: "Find trends" }, | |
| { to: "/tags", icon: Tag, label: "Tags", description: "Browse by topic" }, | |
| { to: "/recommendations", icon: Heart, label: "Recommendations", description: "Personalized" }, | |
| ] | |
| }, | |
| { | |
| id: "tools", | |
| label: "Tools", | |
| items: [ | |
| { to: "/tools", icon: Wrench, label: "App Store", description: "Research tools" }, | |
| { to: "/tools/builder", icon: Package, label: "Build Tool Studio", description: "Visual editor" }, | |
| { to: "/tools/installed", icon: Package, label: "Installed Tools", description: "Manage add-ons" }, | |
| { to: "/data-hub", icon: Database, label: "Data Hub", description: "Research data" }, | |
| { to: "/api-docs", icon: FileText, label: "API Docs", description: "Developer resources" }, | |
| { to: "/status", icon: Activity, label: "Status", description: "Service status" }, | |
| ] | |
| } | |
| ]; | |
| export function FloatingDock() { | |
| const [isMoreOpen, setIsMoreOpen] = useState(false); | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const location = useLocation(); | |
| const [dockItems, setDockItems] = useState<DockItem[]>([]); | |
| // Load custom dock items | |
| useEffect(() => { | |
| const handleStorageChange = () => { | |
| try { | |
| const customPaths = localStorage.getItem("ie-custom-dock-items"); | |
| const paths = customPaths ? JSON.parse(customPaths) : DEFAULT_DOCK_PATHS; | |
| const mappedItems = paths.map((p: string) => { | |
| return ALL_AVAILABLE_APPS.find(a => a.to === p); | |
| }).filter(Boolean) as DockItem[]; | |
| setDockItems(mappedItems.length > 0 ? mappedItems : ALL_AVAILABLE_APPS.slice(0, 8)); | |
| } catch { | |
| setDockItems(ALL_AVAILABLE_APPS.slice(0, 8)); | |
| } | |
| }; | |
| handleStorageChange(); | |
| window.addEventListener("dock-settings-changed", handleStorageChange); | |
| return () => window.removeEventListener("dock-settings-changed", handleStorageChange); | |
| }, []); | |
| useEffect(() => { | |
| setIsMoreOpen(false); | |
| setSearchQuery(""); | |
| }, [location.pathname]); | |
| const isActivePath = (item: DockItem) => { | |
| if (item.exact) return location.pathname === item.to; | |
| return location.pathname.startsWith(item.to) && (location.pathname === item.to || location.pathname.charAt(item.to.length) === '/'); | |
| }; | |
| const hasAnyMatches = SECTIONS.some(section => | |
| section.items.some(item => | |
| item.label.toLowerCase().includes(searchQuery.toLowerCase()) || | |
| item.description.toLowerCase().includes(searchQuery.toLowerCase()) | |
| ) | |
| ); | |
| return ( | |
| <div className="fixed inset-x-0 bottom-6 z-50 px-4 pointer-events-none flex justify-center"> | |
| <div className="relative flex flex-col items-center justify-center gap-4 max-w-full w-auto"> | |
| {/* Expanded Popover Menu */} | |
| <AnimatePresence> | |
| {isMoreOpen && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20, scale: 0.95 }} | |
| animate={{ opacity: 1, y: 0, scale: 1 }} | |
| exit={{ opacity: 0, y: 20, scale: 0.95 }} | |
| transition={{ type: "spring", bounce: 0.3, duration: 0.5 }} | |
| className="pointer-events-auto w-[90vw] max-w-3xl rounded-[2.5rem] premium-glass bg-white/80 p-6 shadow-[0_30px_100px_rgba(0,0,0,0.15)] backdrop-blur-3xl dark:border-white/10 dark:bg-slate-950/80 border border-white/60 dark:shadow-[0_30px_100px_rgba(0,0,0,0.5)]" | |
| > | |
| <div className="mb-5 flex flex-col md:flex-row md:items-center justify-between border-b border-slate-200/50 pb-4 dark:border-slate-800/50 px-2 gap-3"> | |
| <div className="flex items-center gap-2"> | |
| <div className="p-1.5 rounded-xl bg-indigo-500/10 border border-indigo-500/20"> | |
| <Sparkles className="h-5 w-5 text-indigo-500" /> | |
| </div> | |
| <h3 className="font-bold text-lg text-slate-800 dark:text-white">Workspace Apps</h3> | |
| </div> | |
| {/* Search Bar */} | |
| <div className="relative flex-1 max-w-xs md:ml-4"> | |
| <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" /> | |
| <input | |
| type="text" | |
| placeholder="Search apps..." | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="w-full pl-9 pr-10 py-1.5 rounded-xl border border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-900/60 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 text-slate-800 dark:text-white" | |
| /> | |
| {searchQuery && ( | |
| <button | |
| onClick={() => setSearchQuery("")} | |
| className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-bold text-slate-450 hover:text-slate-600 dark:hover:text-slate-200" | |
| > | |
| Clear | |
| </button> | |
| )} | |
| </div> | |
| <span className="text-xs font-medium text-slate-400 bg-slate-100 dark:bg-slate-800 px-3 py-1 rounded-full border border-slate-200 dark:border-slate-700 self-start md:self-auto">All Modules</span> | |
| </div> | |
| <div className="max-h-[60vh] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-slate-300 dark:scrollbar-thumb-slate-700 scrollbar-track-transparent"> | |
| {SECTIONS.map((section, sIdx) => { | |
| const filteredItems = section.items.filter(item => | |
| item.label.toLowerCase().includes(searchQuery.toLowerCase()) || | |
| item.description.toLowerCase().includes(searchQuery.toLowerCase()) | |
| ); | |
| if (filteredItems.length === 0) return null; | |
| return ( | |
| <div key={section.id} className="mb-6 last:mb-0"> | |
| <h4 className="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider mb-3 px-2 flex items-center gap-2"> | |
| <div className="w-4 h-[1px] bg-slate-300 dark:bg-slate-700" /> | |
| {section.label} | |
| <div className="flex-1 h-[1px] bg-slate-200 dark:bg-slate-800" /> | |
| </h4> | |
| <div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4"> | |
| {filteredItems.map((item, idx) => ( | |
| <motion.div | |
| key={item.to} | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: (sIdx * 0.05) + (idx * 0.02) }} | |
| > | |
| <NavLink | |
| to={item.to} | |
| onClick={(e) => { | |
| if (location.pathname === "/workspace") { | |
| // Map routes to workspace panel types | |
| const routeToPanel: Record<string, string> = { | |
| "/ai-co-scientist": "ai-chat", | |
| "/editor": "collaborative-editor", | |
| "/lab-inventory": "lab-inventory", | |
| "/gap-analysis": "gap-analysis", | |
| "/grant-assistant": "grant-assistant", | |
| "/research-assistant": "research-assistant", | |
| "/messages": "messages", | |
| "/articles": "articles", | |
| "/tools": "tools", | |
| "/tools/builder": "tool-builder", | |
| "/tools/installed": "installed-tools", | |
| "/status": "status" | |
| }; | |
| const panelType = routeToPanel[item.to]; | |
| if (panelType) { | |
| e.preventDefault(); | |
| window.dispatchEvent(new CustomEvent("workspace:open-panel", { detail: { type: panelType } })); | |
| setIsMoreOpen(false); | |
| toast.info(`Opened ${item.label} in Workspace 💻`, { position: "bottom-right", autoClose: 2000 }); | |
| } | |
| } | |
| }} | |
| className="group flex flex-col items-center justify-center rounded-[1.25rem] border border-transparent bg-white/60 p-3 text-center transition-all duration-300 hover:border-indigo-500/30 hover:bg-white hover:shadow-xl dark:bg-slate-900/60 dark:hover:bg-slate-800 hover:-translate-y-1 relative overflow-hidden h-full" | |
| > | |
| <div className="absolute inset-0 bg-gradient-to-br from-indigo-500/5 to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity" /> | |
| <div className="mb-2.5 flex h-10 w-10 items-center justify-center rounded-2xl bg-gradient-to-br from-indigo-500/10 to-purple-500/10 text-indigo-600 dark:from-indigo-400/20 dark:to-purple-400/20 dark:text-indigo-300 border border-indigo-500/10 shadow-inner group-hover:scale-110 transition-transform duration-300"> | |
| <item.icon className="h-5 w-5" /> | |
| </div> | |
| <span className="text-[11px] font-bold text-slate-800 dark:text-white relative z-10 leading-tight">{item.label}</span> | |
| <span className="mt-1 hidden text-[9px] leading-tight text-slate-500 dark:text-slate-400 sm:block relative z-10">{item.description}</span> | |
| </NavLink> | |
| </motion.div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| {!hasAnyMatches && ( | |
| <div className="flex flex-col items-center justify-center py-12 text-slate-500 dark:text-slate-400"> | |
| <Package className="h-12 w-12 text-slate-400 mb-3 opacity-55 animate-bounce" /> | |
| <p className="text-sm font-semibold">No apps match "{searchQuery}"</p> | |
| <button | |
| onClick={() => setSearchQuery("")} | |
| className="mt-2 text-xs font-bold text-indigo-500 hover:underline" | |
| > | |
| Clear search query | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| {/* Main Dock Bar */} | |
| <motion.nav | |
| layout | |
| className="pointer-events-auto flex w-full max-w-full items-center justify-between gap-1 overflow-x-auto rounded-[2rem] bg-white/80 px-2 py-2 shadow-[0_20px_60px_rgba(15,23,42,0.12),0_0_0_1px_rgba(255,255,255,0.4)_inset] backdrop-blur-3xl dark:bg-slate-950/80 dark:shadow-[0_20px_60px_rgba(0,0,0,0.5),0_0_0_1px_rgba(255,255,255,0.05)_inset] border border-slate-200/50 dark:border-white/10 scrollbar-hide" | |
| > | |
| {dockItems.map((item) => { | |
| const isActive = isActivePath(item) && !isMoreOpen; | |
| return ( | |
| <NavLink | |
| key={item.to} | |
| to={item.to} | |
| onClick={(e) => { | |
| if (location.pathname === "/workspace") { | |
| const routeToPanel: Record<string, string> = { | |
| "/ai-co-scientist": "ai-chat", | |
| "/editor": "collaborative-editor", | |
| "/lab-inventory": "lab-inventory", | |
| "/gap-analysis": "gap-analysis", | |
| "/grant-assistant": "grant-assistant", | |
| "/research-assistant": "research-assistant", | |
| "/messages": "messages", | |
| "/articles": "articles", | |
| "/tools": "tools", | |
| "/tools/builder": "tool-builder", | |
| "/tools/installed": "installed-tools", | |
| "/status": "status" | |
| }; | |
| const panelType = routeToPanel[item.to]; | |
| if (panelType) { | |
| e.preventDefault(); | |
| window.dispatchEvent(new CustomEvent("workspace:open-panel", { detail: { type: panelType } })); | |
| toast.info(`Opened ${item.label} in Workspace 💻`, { position: "bottom-right", autoClose: 2000 }); | |
| } | |
| } | |
| }} | |
| className="relative group flex min-w-[3.5rem] flex-1 items-center justify-center gap-2 rounded-full px-4 py-2.5 text-xs font-bold transition-colors duration-300" | |
| > | |
| {/* Windows-style Hover Preview */} | |
| <div className="absolute bottom-[calc(100%+16px)] left-1/2 -translate-x-1/2 opacity-0 scale-95 pointer-events-none group-hover:opacity-100 group-hover:scale-100 transition-all duration-300 origin-bottom z-50"> | |
| <div className="premium-glass bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl border border-slate-200/50 dark:border-white/10 shadow-2xl rounded-2xl p-3 w-48 flex flex-col gap-2"> | |
| <div className="flex items-center gap-2 mb-1 border-b border-slate-100 dark:border-slate-800 pb-2"> | |
| <div className="p-1.5 rounded-lg bg-indigo-500/10 text-indigo-500 dark:text-indigo-400"> | |
| <item.icon className="h-4 w-4" /> | |
| </div> | |
| <span className="font-bold text-sm text-slate-800 dark:text-white truncate">{item.label}</span> | |
| </div> | |
| {/* Fake window content preview */} | |
| <div className="w-full h-24 bg-slate-50 dark:bg-slate-950 rounded-lg border border-slate-200/50 dark:border-white/5 overflow-hidden shadow-inner flex flex-col"> | |
| <div className="h-4 w-full bg-slate-200/50 dark:bg-slate-800/50 flex items-center px-1.5 gap-1 border-b border-slate-200/50 dark:border-white/5"> | |
| <div className="w-1.5 h-1.5 rounded-full bg-red-400/80"></div> | |
| <div className="w-1.5 h-1.5 rounded-full bg-amber-400/80"></div> | |
| <div className="w-1.5 h-1.5 rounded-full bg-emerald-400/80"></div> | |
| </div> | |
| <div className="p-2 space-y-1.5 flex-1 opacity-70"> | |
| <div className="w-3/4 h-1.5 bg-slate-200 dark:bg-slate-700 rounded-full"></div> | |
| <div className="w-1/2 h-1.5 bg-slate-200 dark:bg-slate-700 rounded-full"></div> | |
| <div className="w-5/6 h-1.5 bg-slate-200 dark:bg-slate-700 rounded-full"></div> | |
| <div className="w-2/3 h-1.5 bg-slate-200 dark:bg-slate-700 rounded-full"></div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Arrow pointing down */} | |
| <div className="absolute -bottom-2 left-1/2 -translate-x-1/2 w-4 h-4 rotate-45 bg-white/90 dark:bg-slate-900/90 border-b border-r border-slate-200/50 dark:border-white/10 shadow-[4px_4px_10px_rgba(0,0,0,0.1)]"></div> | |
| </div> | |
| {isActive && ( | |
| <motion.div | |
| layoutId="activeDockPill" | |
| className="absolute inset-0 rounded-full bg-gradient-to-r from-indigo-600 to-purple-600 shadow-md" | |
| transition={{ type: "spring", bounce: 0.2, duration: 0.6 }} | |
| /> | |
| )} | |
| <div className={`relative z-10 flex items-center gap-2 ${isActive ? "text-white" : "text-slate-600 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400"}`}> | |
| <item.icon className={`h-4 w-4 shrink-0 transition-transform duration-300 ${isActive ? "scale-110" : "group-hover:scale-110 group-hover:-translate-y-0.5"}`} /> | |
| <span className="hidden md:inline">{item.label}</span> | |
| </div> | |
| {!isActive && ( | |
| <div className="absolute inset-0 rounded-full bg-slate-100 dark:bg-white/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> | |
| )} | |
| </NavLink> | |
| ); | |
| })} | |
| {/* Vertical Divider */} | |
| <div className="w-px h-6 bg-slate-300 dark:bg-slate-700 mx-1 rounded-full opacity-50" /> | |
| {/* Toggle More Menu Button */} | |
| <button | |
| onClick={() => setIsMoreOpen(!isMoreOpen)} | |
| className="relative group flex min-w-[3.5rem] items-center justify-center gap-2 rounded-full px-4 py-2.5 text-xs font-bold transition-colors duration-300" | |
| > | |
| {isMoreOpen && ( | |
| <motion.div | |
| layoutId="activeDockPill" | |
| className="absolute inset-0 rounded-full bg-gradient-to-r from-indigo-600 to-purple-600 shadow-md" | |
| transition={{ type: "spring", bounce: 0.2, duration: 0.6 }} | |
| /> | |
| )} | |
| <div className={`relative z-10 flex items-center gap-2 ${isMoreOpen ? "text-white" : "text-slate-600 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400"}`}> | |
| <Grid className={`h-4 w-4 shrink-0 transition-transform duration-300 ${isMoreOpen ? "scale-110" : "group-hover:scale-110 group-hover:-translate-y-0.5"}`} /> | |
| <span className="hidden md:inline">Apps</span> | |
| <ChevronUp className={`hidden sm:block h-3 w-3 shrink-0 transition-transform duration-500 ${isMoreOpen ? "rotate-180" : ""}`} /> | |
| </div> | |
| {!isMoreOpen && ( | |
| <div className="absolute inset-0 rounded-full bg-slate-100 dark:bg-white/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> | |
| )} | |
| </button> | |
| </motion.nav> | |
| </div> | |
| </div> | |
| ); | |
| } | |
Xet Storage Details
- Size:
- 24.3 kB
- Xet hash:
- ab32395cae87557cb36521f7c3b9f80ba2fe9d4ef96c326bd12d438250172463
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.