import React, { useRef, useEffect } from "react"; import { createPortal } from "react-dom"; import { getAssetHost } from "../utils"; import type { ContextMenuItem } from "../types"; export function Spinner() { return (
); } export function ErrorBanner({ message }: { message: string }) { return (
{message}
); } export function CharCount({ current, max }: { current: number; max: number }) { const remaining = max - current; const pct = current / max; const color = pct > 0.9 ? "#ef4444" : pct > 0.75 ? "#f59e0b" : "var(--mx-muted)"; return ( {remaining} ); } export function Avatar({ avatarUrl, name, size = "md", }: { avatarUrl?: string | null; name?: string | null; size?: "sm" | "md" | "lg"; }) { const assetHost = getAssetHost(); const initial = ((name ?? "")[0] || "M").toUpperCase(); const cls = size === "sm" ? "mx-tweet-avatar mx-tweet-avatar--sm" : size === "lg" ? "mx-tweet-avatar mx-tweet-avatar--lg" : "mx-tweet-avatar"; return (
{avatarUrl ? ( {name ) : ( {initial} )}
); } export function ContextMenu({ x, y, items, onClose, }: { x: number; y: number; items: ContextMenuItem[]; onClose: () => void; }) { const ref = useRef(null); useEffect(() => { function handleMouseDown(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) onClose(); } function handleKeyDown(e: KeyboardEvent) { if (e.key === "Escape") onClose(); } document.addEventListener("mousedown", handleMouseDown); document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("mousedown", handleMouseDown); document.removeEventListener("keydown", handleKeyDown); }; }, [onClose]); const itemCount = items.filter((i) => i.type === "item").length; const sepCount = items.filter((i) => i.type === "separator").length; const menuH = itemCount * 34 + sepCount * 9 + 8; const menuW = 180; const left = Math.min(x, window.innerWidth - menuW - 8); const top = Math.min(y, window.innerHeight - menuH - 8); return createPortal(
e.preventDefault()} > {items.map((item, i) => item.type === "separator" ? (
) : ( ) )}
, document.body ); }