"use client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { DURATION_PRESETS, quartersMatch } from "@/lib/duration"; import { CHART_NOTES } from "@/lib/fingering"; import { barStatus, newId, parseSignature, signatureKey, TIME_SIGNATURES, type Score, type ScoreEvent, } from "@/lib/score"; import { ChevronDown, ChevronUp, Plus, Trash2 } from "lucide-react"; const fieldClass = "h-10 min-w-0 rounded-lg border border-input bg-card px-2 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"; export function NoteEditor({ score, onChange, }: { score: Score; onChange: (score: Score) => void; }) { const signatures = TIME_SIGNATURES.some( (item) => item.beats === score.timeSignature.beats && item.beatType === score.timeSignature.beatType, ) ? TIME_SIGNATURES : [score.timeSignature, ...TIME_SIGNATURES]; function updateEvent(measureId: string, eventId: string, patch: Partial) { onChange({ ...score, measures: score.measures.map((measure) => measure.id !== measureId ? measure : { ...measure, events: measure.events.map((event) => event.id === eventId ? { ...event, ...patch } : event, ), }, ), }); } function move(measureIndex: number, eventIndex: number, direction: -1 | 1) { const measures = score.measures.map((measure) => ({ ...measure, events: [...measure.events] })); const measure = measures[measureIndex]; const target = eventIndex + direction; if (target >= 0 && target < measure.events.length) { const [item] = measure.events.splice(eventIndex, 1); measure.events.splice(target, 0, item); } else if (direction === -1 && measureIndex > 0) { const [item] = measure.events.splice(eventIndex, 1); measures[measureIndex - 1].events.push(item); } else if (direction === 1 && measureIndex < measures.length - 1) { const [item] = measure.events.splice(eventIndex, 1); measures[measureIndex + 1].events.unshift(item); } else { return; } onChange({ ...score, measures }); } function add(measureId: string, kind: "note" | "rest") { const measure = score.measures.find((item) => item.id === measureId); const previous = [...(measure?.events ?? [])].reverse().find((event) => event.kind === "note"); const event: ScoreEvent = { id: newId("e"), kind, pitch: kind === "note" ? (previous?.pitch ?? "C5") : null, quarters: 1, label: "quarter", tied: false, }; onChange({ ...score, measures: score.measures.map((item) => item.id === measureId ? { ...item, events: [...item.events, event] } : item, ), }); } return (
onChange({ ...score, title: event.target.value })} className="h-10" />
{score.measures.map((measure, measureIndex) => { const status = barStatus(measure, score.timeSignature); return (
Bar {measureIndex + 1}

{status.text}

{measure.events.map((event, eventIndex) => ( { if (value === "rest") { updateEvent(measure.id, event.id, { kind: "rest", pitch: null }); } else { updateEvent(measure.id, event.id, { kind: "note", pitch: value }); } }} onDuration={(quarters) => { const preset = DURATION_PRESETS.find((item) => quartersMatch(item.quarters, quarters)); updateEvent(measure.id, event.id, { quarters, label: preset?.label ?? event.label, }); }} onDelete={() => onChange({ ...score, measures: score.measures.map((item) => item.id === measure.id ? { ...item, events: item.events.filter((row) => row.id !== event.id) } : item, ), }) } onUp={() => move(measureIndex, eventIndex, -1)} onDown={() => move(measureIndex, eventIndex, 1)} /> ))}
{score.measures.length > 1 ? ( ) : null}
); })}
); } function EventRow({ event, onPitch, onDuration, onDelete, onUp, onDown, }: { event: ScoreEvent; onPitch: (value: string) => void; onDuration: (quarters: number) => void; onDelete: () => void; onUp: () => void; onDown: () => void; }) { const pitches = [...CHART_NOTES] as string[]; if (event.pitch && !pitches.includes(event.pitch)) pitches.unshift(event.pitch); const durations = [...DURATION_PRESETS]; if (!durations.some((item) => quartersMatch(item.quarters, event.quarters))) { durations.push({ quarters: event.quarters, label: event.label }); } return (
); }