SPB Git

spb/airiskindex Public

The most methodologically rigorous, fully transparent AI job-exposure index.

TypeScript 88% Python 6.1% SQL 2.7% CSS 1.2% JavaScript 0.9% Shell 0.8%
4.3 KB · 109 lines tsx
Raw Blame History
1"use client";23//  File:    mobile-menu.tsx4//  Path:    apps/web/components/mobile-menu.tsx5//  Project: AI Risk Index — airiskindex.io6//  Author:  Simon-Pierre Boucher7//  Contact: contact@spboucher.ai8//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.9//10//  Description: Mobile right-side drawer menu (portal, scroll lock, Escape/overlay close).111213import Link from "next/link";14import { useEffect, useState } from "react";15import { createPortal } from "react-dom";16import { LogoMark } from "./logo";1718export function MobileMenu({ items }: { items: ReadonlyArray<readonly [string, string]> }): JSX.Element {19  const [open, setOpen] = useState(false);2021  // Close on Escape; lock scroll while open.22  useEffect(() => {23    if (!open) return;24    const onKey = (event: KeyboardEvent) => {25      if (event.key === "Escape") setOpen(false);26    };27    document.addEventListener("keydown", onKey);28    document.body.style.overflow = "hidden";29    return () => {30      document.removeEventListener("keydown", onKey);31      document.body.style.overflow = "";32    };33  }, [open]);3435  return (36    <div className="sm:hidden">37      <button38        type="button"39        aria-expanded={open}40        aria-label={open ? "Close menu" : "Open menu"}41        onClick={() => setOpen((value) => !value)}42        className="flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--border)] bg-[var(--surface-1)] text-[var(--ink)]"43      >44        {open ? (45          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">46            <path d="M3 3l10 10M13 3L3 13" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />47          </svg>48        ) : (49          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">50            <path d="M2 4.5h12M2 8h12M2 11.5h12" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />51          </svg>52        )}53      </button>5455      {/* Portal to <body>: the blurred sticky header is a containing block for56          fixed descendants, which would trap and stretch the drawer. */}57      {open &&58        createPortal(59          <>60            <button61              type="button"62              aria-label="Close menu"63              onClick={() => setOpen(false)}64              className="fixed inset-0 z-40 cursor-default bg-black/25 backdrop-blur-[2px]"65            />66          {/* Drawer sliding in from the right */}67          <nav className="fixed right-0 top-0 z-50 flex h-dvh w-72 max-w-[85vw] animate-[drawer-in_180ms_ease-out] flex-col border-l border-[var(--border)] bg-[var(--surface-1)] shadow-2xl">68            <div className="flex items-center justify-between border-b border-[var(--grid)] px-5 py-3.5">69              <span className="flex items-center gap-2.5 font-semibold tracking-tight">70                <LogoMark size={20} />71                AI Risk Index72              </span>73              <button74                type="button"75                aria-label="Close menu"76                onClick={() => setOpen(false)}77                className="flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--border)] text-[var(--ink)]"78              >79                <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">80                  <path d="M3 3l10 10M13 3L3 13" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />81                </svg>82              </button>83            </div>84            <ul className="flex-1 overflow-y-auto px-5 py-2">85              {items.map(([href, label]) => (86                <li key={href} className="border-b border-[var(--grid)] last:border-b-0">87                  <Link88                    href={href}89                    onClick={() => setOpen(false)}90                    className="block py-3.5 text-base font-medium text-[var(--ink)] active:bg-[var(--wash)]"91                  >92                    {label}93                  </Link>94                </li>95              ))}96            </ul>97            <p className="border-t border-[var(--grid)] px-5 py-4 text-xs text-[var(--muted)]">98              <a href="mailto:contact@spboucher.ai" className="hover:underline">99                contact@spboucher.ai100              </a>101            </p>102          </nav>103          </>,104          document.body,105        )}106    </div>107  );108}109