Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 124 additions & 22 deletions apps/docs/components/playground/chord-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"use client";

import { useState, useRef, useEffect } from "react";
import type { ChordId } from "@/lib/playground/types";
import {
chordRegistry,
majorChords,
minorChords,
} from "@/lib/playground/chord-registry";
Expand All @@ -13,35 +15,135 @@ export function ChordSelector({
chordId: ChordId;
onChange: (id: ChordId) => void;
}) {
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const selected = chordRegistry[chordId];

useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
setOpen(false);
}
}
if (open) {
document.addEventListener("mousedown", handleClickOutside);
return () =>
document.removeEventListener("mousedown", handleClickOutside);
}
}, [open]);

return (
<div className="rounded-xl border border-fd-border bg-fd-card p-4">
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-fd-muted-foreground">
Chord
</label>
<select
value={chordId}
onChange={(e) => onChange(e.target.value as ChordId)}
className="w-full rounded-lg border border-fd-border bg-fd-background px-3 py-2 text-sm text-fd-foreground outline-none focus:ring-2 focus:ring-fd-primary/30"
>
<optgroup label="Major Chords">
{majorChords.map((c) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</optgroup>
<optgroup label="Minor Chords">
{minorChords.map((c) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</optgroup>
</select>
<div ref={containerRef} className="relative">
<button
type="button"
onClick={() => setOpen(!open)}
className="flex w-full items-center justify-between rounded-lg border border-fd-border bg-fd-background px-3 py-2 text-left text-sm text-fd-foreground outline-none transition-colors hover:bg-fd-background/50 focus:ring-2 focus:ring-fd-primary/30"
>
<span>{selected.name}</span>
<svg
className={`size-4 text-fd-muted-foreground transition-transform duration-200 ${open ? "rotate-180" : ""}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>

{open && (
<div
className="absolute left-0 right-0 top-full z-50 mt-1.5 max-h-72 overflow-y-auto rounded-xl border border-fd-border bg-fd-popover shadow-lg"
style={{
scrollbarWidth: "thin",
scrollbarColor: "rgba(127,127,127,0.3) transparent",
}}
>
<ChordGroup
label="Major Chords"
chords={majorChords}
activeId={chordId}
onSelect={(id) => {
onChange(id);
setOpen(false);
}}
/>
<div className="mx-3 border-t border-fd-border" />
<ChordGroup
label="Minor Chords"
chords={minorChords}
activeId={chordId}
onSelect={(id) => {
onChange(id);
setOpen(false);
}}
/>
</div>
)}
</div>
<p className="mt-2 text-xs text-fd-muted-foreground">
{majorChords.find((c) => c.id === chordId)?.description ??
minorChords.find((c) => c.id === chordId)?.description}
{selected.description}
</p>
</div>
);
}

function ChordGroup({
label,
chords,
activeId,
onSelect,
}: {
label: string;
chords: { id: ChordId; name: string }[];
activeId: ChordId;
onSelect: (id: ChordId) => void;
}) {
return (
<div className="p-1.5">
<div className="px-2.5 py-1.5 text-[11px] font-medium uppercase tracking-wider text-fd-muted-foreground">
{label}
</div>
{chords.map((c) => (
<button
key={c.id}
type="button"
onClick={() => onSelect(c.id)}
className={`flex w-full items-center rounded-lg px-2.5 py-1.5 text-sm transition-colors ${
c.id === activeId
? "bg-fd-accent text-fd-accent-foreground font-medium"
: "text-fd-foreground hover:bg-fd-accent/50"
}`}
>
{c.name}
{c.id === activeId && (
<svg
className="ml-auto size-3.5 text-fd-primary"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
)}
</button>
))}
</div>
);
}