Initial commit
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface VariantOption {
|
||||
/** Stable identifier (typically the absolute partIdx). */
|
||||
id: number;
|
||||
/** Short label shown in the chip and menu, e.g. `original`, `fixed`, `1080p`. */
|
||||
label: string;
|
||||
/** Full filename, shown muted in the menu for disambiguation. */
|
||||
filename: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact dropdown that picks between alternate encodes of one part.
|
||||
* Renders nothing when there's only one option — caller can still
|
||||
* mount it unconditionally.
|
||||
*/
|
||||
export function VariantPicker({
|
||||
options,
|
||||
selectedId,
|
||||
onChange,
|
||||
}: {
|
||||
options: VariantOption[];
|
||||
selectedId: number;
|
||||
onChange: (id: number) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDocDown = (e: MouseEvent) => {
|
||||
if (!ref.current) return;
|
||||
if (!ref.current.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", onDocDown);
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", onDocDown);
|
||||
document.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
if (options.length <= 1) return null;
|
||||
const selected = options.find((o) => o.id === selectedId) ?? options[0]!;
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
title={`Switch encode (${options.length} available)`}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 text-xs font-mono px-2.5 py-1 rounded-md border cursor-pointer",
|
||||
"border-[var(--color-glass-border)] bg-[var(--color-glass)] hover:bg-[var(--color-glass-strong)] text-[var(--color-fg)]",
|
||||
open && "bg-[var(--color-glass-strong)]",
|
||||
)}
|
||||
>
|
||||
<ChevronDown className={cn("w-3 h-3 transition-transform", open && "rotate-180")} />
|
||||
<span className="truncate max-w-[140px]">{selected.label}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div
|
||||
role="menu"
|
||||
className="absolute right-0 bottom-full mb-1 z-30 min-w-[260px] max-w-[420px] rounded-lg border border-[var(--color-glass-border-strong)] bg-[var(--color-bg-1)] shadow-2xl p-1"
|
||||
>
|
||||
{options.map((o) => (
|
||||
<button
|
||||
key={o.id}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => { onChange(o.id); setOpen(false); }}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2 text-left text-xs px-2 py-1.5 rounded-md cursor-pointer",
|
||||
o.id === selectedId
|
||||
? "bg-[var(--color-cyan)]/15 text-[var(--color-cyan)]"
|
||||
: "hover:bg-[var(--color-glass)] text-[var(--color-fg)]",
|
||||
)}
|
||||
>
|
||||
<span className="font-mono shrink-0">{o.label}</span>
|
||||
<span className="font-mono text-[10px] text-[var(--color-fg-dim)] truncate min-w-0">
|
||||
{o.filename}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user