|
| 1 | +// deno-lint-ignore-file no-window |
| 2 | +import { useEffect, useRef, useState } from "preact/hooks"; |
| 3 | +import { BookOpen, Code, Menu, Newspaper, Satellite, X } from "lucide-preact"; |
| 4 | + |
| 5 | +interface NavLink { |
| 6 | + href: string; |
| 7 | + label: string; |
| 8 | + icon: any; |
| 9 | + color: string; |
| 10 | + hoverBg: string; |
| 11 | +} |
| 12 | + |
| 13 | +export default function MobileMenu() { |
| 14 | + const [isOpen, setIsOpen] = useState(false); |
| 15 | + const menuRef = useRef<HTMLDivElement>(null); |
| 16 | + |
| 17 | + const navLinks: NavLink[] = [ |
| 18 | + { |
| 19 | + href: "/docs/index", |
| 20 | + label: "Documentation", |
| 21 | + icon: BookOpen, |
| 22 | + color: "text-green", |
| 23 | + hoverBg: "hover:bg-green/10", |
| 24 | + }, |
| 25 | + { |
| 26 | + href: "/satellites", |
| 27 | + label: "Satellites", |
| 28 | + icon: Satellite, |
| 29 | + color: "text-blue", |
| 30 | + hoverBg: "hover:bg-blue/10", |
| 31 | + }, |
| 32 | + { |
| 33 | + href: "/blog", |
| 34 | + label: "Blog", |
| 35 | + icon: Newspaper, |
| 36 | + color: "text-yellow", |
| 37 | + hoverBg: "hover:bg-yellow/10", |
| 38 | + }, |
| 39 | + { |
| 40 | + href: "/std", |
| 41 | + label: "Standard Library", |
| 42 | + icon: Code, |
| 43 | + color: "text-mauve", |
| 44 | + hoverBg: "hover:bg-mauve/10", |
| 45 | + }, |
| 46 | + ]; |
| 47 | + |
| 48 | + const handleOpen = () => { |
| 49 | + setIsOpen(true); |
| 50 | + }; |
| 51 | + |
| 52 | + const handleClose = () => { |
| 53 | + setIsOpen(false); |
| 54 | + }; |
| 55 | + |
| 56 | + useEffect(() => { |
| 57 | + const handleEscape = (e: KeyboardEvent) => { |
| 58 | + if (e.key === "Escape" && isOpen) { |
| 59 | + handleClose(); |
| 60 | + } |
| 61 | + }; |
| 62 | + |
| 63 | + const handleClickOutside = (e: MouseEvent) => { |
| 64 | + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { |
| 65 | + handleClose(); |
| 66 | + } |
| 67 | + }; |
| 68 | + |
| 69 | + if (isOpen) { |
| 70 | + document.addEventListener("keydown", handleEscape); |
| 71 | + document.addEventListener("mousedown", handleClickOutside); |
| 72 | + document.body.style.overflow = "hidden"; |
| 73 | + } |
| 74 | + |
| 75 | + return () => { |
| 76 | + document.removeEventListener("keydown", handleEscape); |
| 77 | + document.removeEventListener("mousedown", handleClickOutside); |
| 78 | + document.body.style.overflow = ""; |
| 79 | + }; |
| 80 | + }, [isOpen]); |
| 81 | + |
| 82 | + |
| 83 | + return ( |
| 84 | + <> |
| 85 | + {/* Mobile Menu Button - Only visible on mobile */} |
| 86 | + <button |
| 87 | + type="button" |
| 88 | + onClick={handleOpen} |
| 89 | + class="md:hidden bg-surface0 hover:bg-surface1 text-text rounded-lg p-2 transition-all duration-200 border border-surface2" |
| 90 | + aria-label="Open menu" |
| 91 | + > |
| 92 | + <Menu class="w-4 h-4" /> |
| 93 | + </button> |
| 94 | + |
| 95 | + {/* Mobile Menu Modal */} |
| 96 | + {isOpen && ( |
| 97 | + <div class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-start justify-center pt-20 px-4 md:hidden"> |
| 98 | + <div |
| 99 | + ref={menuRef} |
| 100 | + class="w-full max-w-md bg-base rounded-2xl shadow-2xl border border-surface1 overflow-hidden" |
| 101 | + > |
| 102 | + {/* Header */} |
| 103 | + <div class="flex items-center justify-between p-4 border-b border-surface1"> |
| 104 | + <div class="flex items-center gap-2"> |
| 105 | + <Menu size={20} class="text-subtext1" /> |
| 106 | + <span class="text-lg font-semibold text-text">Menu</span> |
| 107 | + </div> |
| 108 | + <button |
| 109 | + type="button" |
| 110 | + onClick={handleClose} |
| 111 | + class="text-subtext1 hover:text-text transition-colors p-2 hover:bg-surface0 rounded-lg" |
| 112 | + aria-label="Close menu" |
| 113 | + > |
| 114 | + <X size={20} /> |
| 115 | + </button> |
| 116 | + </div> |
| 117 | + |
| 118 | + {/* Menu Items */} |
| 119 | + <div class="p-2"> |
| 120 | + {navLinks.map((link) => { |
| 121 | + const IconComponent = link.icon; |
| 122 | + return ( |
| 123 | + <a |
| 124 | + key={link.href} |
| 125 | + href={link.href} |
| 126 | + class={`flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 ${link.hoverBg} group`} |
| 127 | + > |
| 128 | + <div class={`${link.color} transition-transform group-hover:scale-110`}> |
| 129 | + <IconComponent size={20} /> |
| 130 | + </div> |
| 131 | + <span class="text-text font-medium group-hover:text-text transition-colors"> |
| 132 | + {link.label} |
| 133 | + </span> |
| 134 | + </a> |
| 135 | + ); |
| 136 | + })} |
| 137 | + </div> |
| 138 | + |
| 139 | + {/* Footer with social links */} |
| 140 | + <div class="border-t border-surface1 p-4"> |
| 141 | + <div class="flex items-center justify-center gap-3"> |
| 142 | + <a |
| 143 | + href="https://github.com/tryandromeda/andromeda" |
| 144 | + class="flex items-center gap-2 px-4 py-2 bg-surface0 hover:bg-surface1 rounded-lg transition-all duration-200 border border-surface2 hover:border-blue" |
| 145 | + aria-label="GitHub" |
| 146 | + > |
| 147 | + <svg |
| 148 | + xmlns="http://www.w3.org/2000/svg" |
| 149 | + class="w-5 h-5" |
| 150 | + viewBox="0 0 24 24" |
| 151 | + fill="none" |
| 152 | + stroke="currentColor" |
| 153 | + stroke-width="2" |
| 154 | + stroke-linecap="round" |
| 155 | + stroke-linejoin="round" |
| 156 | + > |
| 157 | + <path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" /> |
| 158 | + <path d="M9 18c-4.51 2-5-2-7-2" /> |
| 159 | + </svg> |
| 160 | + <span class="text-sm font-medium">GitHub</span> |
| 161 | + </a> |
| 162 | + <a |
| 163 | + href="https://discord.gg/tgjAnX2Ny3" |
| 164 | + class="flex items-center gap-2 px-4 py-2 bg-surface0 hover:bg-surface1 rounded-lg transition-all duration-200 border border-surface2 hover:border-mauve" |
| 165 | + aria-label="Discord" |
| 166 | + > |
| 167 | + <svg |
| 168 | + xmlns="http://www.w3.org/2000/svg" |
| 169 | + class="w-5 h-5" |
| 170 | + viewBox="0 0 16 16" |
| 171 | + > |
| 172 | + <path |
| 173 | + fill="currentColor" |
| 174 | + d="M9 8.5c0 .826.615 1.5 1.36 1.5c.762 0 1.35-.671 1.36-1.5S11.125 7 10.36 7C9.592 7 9 7.677 9 8.5M5.63 10c-.747 0-1.36-.671-1.36-1.5S4.866 7 5.63 7S7.01 7.677 7 8.5c-.013.826-.602 1.5-1.36 1.5z" |
| 175 | + /> |
| 176 | + <path |
| 177 | + fill="currentColor" |
| 178 | + fill-rule="evenodd" |
| 179 | + d="M13.3 2.72a1 1 0 0 1 .41.342c1.71 2.47 2.57 5.29 2.25 8.53a1 1 0 0 1-.405.71c-1.16.851-2.47 1.5-3.85 1.91a.99.99 0 0 1-1.08-.357a10 10 0 0 1-.665-1.01a9.4 9.4 0 0 1-3.87 0a9 9 0 0 1-.664 1.01a1 1 0 0 1-1.09.357a12.8 12.8 0 0 1-3.85-1.91a1 1 0 0 1-.405-.711c-.269-2.79.277-5.64 2.25-8.52c.103-.151.246-.271.413-.347c.999-.452 2.05-.774 3.14-.957c.415-.07.83.128 1.04.494l.089.161q1.01-.087 2.03 0l.088-.161a1 1 0 0 1 1.04-.494c1.08.181 2.14.502 3.14.955zm-3.67.776a11 11 0 0 0-3.21 0a8 8 0 0 0-.37-.744c-.998.168-1.97.465-2.89.882c-1.83 2.68-2.32 5.29-2.08 7.86c1.07.783 2.27 1.38 3.54 1.76a8 8 0 0 0 .461-.681q.158-.26.297-.53a7.5 7.5 0 0 1-1.195-.565q.15-.109.293-.218a8.5 8.5 0 0 0 1.886.62a8.4 8.4 0 0 0 4.146-.217a8 8 0 0 0 1.04-.404q.144.117.293.218a11 11 0 0 1-.282.157a8 8 0 0 1-.915.41a9 9 0 0 0 .518.872q.117.17.241.337c1.27-.38 2.47-.975 3.54-1.76c.291-2.98-.497-5.57-2.08-7.86c-.92-.417-1.89-.712-2.89-.879q-.204.362-.37.744z" |
| 180 | + clip-rule="evenodd" |
| 181 | + /> |
| 182 | + </svg> |
| 183 | + <span class="text-sm font-medium">Discord</span> |
| 184 | + </a> |
| 185 | + </div> |
| 186 | + </div> |
| 187 | + </div> |
| 188 | + </div> |
| 189 | + )} |
| 190 | + </> |
| 191 | + ); |
| 192 | +} |
0 commit comments