c97fec2eb3
Make the onboarding tour theme-aware across the app themes, add the first-run theme picker, and keep fake media previews on the dark media surface. Update light-mode secondary controls in onboarding, settings, dropdowns, toolbar controls, and duplicate actions so they no longer render as dark buttons on subtle-light.
107 lines
4.0 KiB
TypeScript
107 lines
4.0 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
|
|
export interface DropdownOption {
|
|
value: string;
|
|
label: string;
|
|
}
|
|
|
|
export function ThemedDropdown({
|
|
value,
|
|
options,
|
|
onChange,
|
|
ariaLabel,
|
|
compact = false,
|
|
align = "right",
|
|
}: {
|
|
value: string;
|
|
options: DropdownOption[];
|
|
onChange: (value: string) => void;
|
|
ariaLabel: string;
|
|
compact?: boolean;
|
|
align?: "left" | "right";
|
|
}) {
|
|
const [open, setOpen] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const current = options.find((option) => option.value === value) ?? options[0];
|
|
|
|
useEffect(() => {
|
|
const handlePointerDown = (event: PointerEvent) => {
|
|
if (!ref.current?.contains(event.target as Node)) setOpen(false);
|
|
};
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === "Escape") setOpen(false);
|
|
};
|
|
window.addEventListener("pointerdown", handlePointerDown);
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
return () => {
|
|
window.removeEventListener("pointerdown", handlePointerDown);
|
|
window.removeEventListener("keydown", handleKeyDown);
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div ref={ref} className="relative">
|
|
<button
|
|
type="button"
|
|
aria-label={ariaLabel}
|
|
aria-haspopup="listbox"
|
|
aria-expanded={open}
|
|
onClick={() => setOpen((currentOpen) => !currentOpen)}
|
|
className={`flex items-center justify-between gap-2 rounded-md border transition-colors ${
|
|
compact
|
|
? "border-white/10 bg-white/[0.04] px-1.5 py-0.5 text-[10px] font-medium light-theme:border-gray-700/50 light-theme:bg-gray-900"
|
|
: "min-w-40 border-white/10 bg-white/[0.055] px-3 py-1.5 text-xs light-theme:border-gray-700/50 light-theme:bg-gray-900"
|
|
} ${open ? "border-white/20 text-white light-theme:border-gray-700 light-theme:text-white" : "text-gray-400 hover:border-white/15 hover:text-gray-200 light-theme:text-white light-theme:hover:border-gray-700 light-theme:hover:bg-gray-800 light-theme:hover:text-white"}`}
|
|
>
|
|
<span>{current?.label}</span>
|
|
<svg
|
|
className={`h-3 w-3 shrink-0 text-gray-500 transition-transform duration-150 ${open ? "rotate-180" : ""}`}
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{open ? (
|
|
<div
|
|
role="listbox"
|
|
aria-label={ariaLabel}
|
|
className={`absolute top-full z-50 mt-1.5 min-w-full rounded-xl border border-white/10 bg-gray-950/98 p-1 shadow-2xl shadow-black/30 backdrop-blur-xl light-theme:border-gray-700/50 ${
|
|
align === "right" ? "right-0" : "left-0"
|
|
}`}
|
|
>
|
|
{options.map((option) => {
|
|
const selected = option.value === value;
|
|
return (
|
|
<button
|
|
key={option.value}
|
|
type="button"
|
|
role="option"
|
|
aria-selected={selected}
|
|
className={`flex w-full items-center justify-between gap-4 whitespace-nowrap rounded-lg px-3 py-2 text-left text-xs transition-colors ${
|
|
selected
|
|
? "bg-white/[0.08] text-white light-theme:bg-gray-900 light-theme:text-white"
|
|
: "text-gray-400 hover:bg-white/[0.055] hover:text-gray-200 light-theme:text-gray-600 light-theme:hover:bg-gray-900 light-theme:hover:text-white"
|
|
}`}
|
|
onClick={() => {
|
|
onChange(option.value);
|
|
setOpen(false);
|
|
}}
|
|
>
|
|
<span>{option.label}</span>
|
|
{selected ? (
|
|
<svg className="h-3.5 w-3.5 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
) : null}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|