Vectrum
Sign In
67 lines · 2.3 KB · TypeScript
"use client";

import { useEffect, useRef, type ReactNode } from "react";

/**
 * A <details>-based popover on MetalUI's frosted menu plate, for rich content (forms, copy fields)
 * that isn't a list of commands; for commands use MetalUI's `Menu`. It also closes on outside click,
 * Escape, and when one of its links is followed.
 */
export function Dropdown({
  summary,
  summaryClassName = "",
  children,
  align = "right",
  width = "w-56",
  label,
}: {
  summary: ReactNode;
  summaryClassName?: string;
  children: ReactNode;
  align?: "left" | "right";
  width?: string;
  label?: string;
}) {
  const ref = useRef<HTMLDetailsElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const close = (e: Event) => {
      if (e instanceof KeyboardEvent ? e.key === "Escape" : !el.contains(e.target as Node)) el.open = false;
    };
    const onClick = (e: MouseEvent) => {
      if ((e.target as HTMLElement).closest("a")) el.open = false;
    };
    document.addEventListener("mousedown", close);
    document.addEventListener("keydown", close);
    el.addEventListener("click", onClick);
    return () => {
      document.removeEventListener("mousedown", close);
      document.removeEventListener("keydown", close);
      el.removeEventListener("click", onClick);
    };
  }, []);

  return (
    <details ref={ref} className="group relative">
      <summary aria-label={label} className={`cursor-pointer list-none [&::-webkit-details-marker]:hidden ${summaryClassName}`}>
        {summary}
      </summary>
      <div
        className={`menu-panel mu-frost-plate absolute z-30 mt-1.5 ${width} ${align === "right" ? "right-0 origin-top-right" : "left-0 origin-top-left"} overflow-hidden rounded-[18px] p-1.5`}
      >
        {children}
      </div>
    </details>
  );
}

/** MetalUI's menu separator: an engraved rule, inset. */
export function MenuDivider() {
  return <div className="recipe-menu-sep mx-2 my-1.5 h-px" />;
}

/** MetalUI's menu row: 30 tall at the row radius, a 14 glyph in ink2. */
export const menuItemClass =
  "mu-icon-trigger flex h-[30px] w-full items-center gap-2.5 rounded-[12px] px-2.5 text-left type-ui text-ink outline-none select-none hover:recipe-menu-row-hover focus-visible:recipe-menu-row-hover [&_svg]:size-3.5 [&_svg]:text-ink2";