From e99b581a434f06ea50d3c95c0ffd507622d2b1e3 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 9 Apr 2026 00:42:24 +0200 Subject: [PATCH] feat: tappable chord names in ChordChart, extractUniqueChords --- app/app/components/chord-chart.tsx | 77 +++++++++++++++++++----------- app/app/lib/song-utils.ts | 19 +++++++- 2 files changed, 66 insertions(+), 30 deletions(-) diff --git a/app/app/components/chord-chart.tsx b/app/app/components/chord-chart.tsx index 25fa76c..47624a9 100644 --- a/app/app/components/chord-chart.tsx +++ b/app/app/components/chord-chart.tsx @@ -1,27 +1,16 @@ import type { LyricLine, Section } from "~/lib/types"; -// Max characters per rendered line. 38 fits comfortably on a 375px phone -// at 14px monospace (≈8.4px per char with padding). const MAX_WIDTH = 38; interface Props { sections: Section[]; fontSize?: 'sm' | 'base' | 'lg'; -} - -function buildChordRow(chords: { offset: number; chord: string }[]): string { - let row = ""; - for (const { offset, chord } of chords) { - while (row.length < offset) row += " "; - row += chord; - } - return row; + onChordClick?: (chord: string) => void; } /** Split one LyricLine into segments that each fit within maxWidth characters. */ function segmentLine(line: LyricLine, maxWidth: number): LyricLine[] { const { text, chords } = line; - if (text.length <= maxWidth) return [line]; const segments: LyricLine[] = []; @@ -29,39 +18,61 @@ function segmentLine(line: LyricLine, maxWidth: number): LyricLine[] { while (start < text.length) { let end = start + maxWidth; - if (end < text.length) { - // Break at last space before maxWidth to avoid splitting mid-word const breakAt = text.lastIndexOf(" ", end); if (breakAt > start) end = breakAt + 1; } else { end = text.length; } - const segText = text.slice(start, end).trimEnd(); - - // Include chords whose offset falls within this segment; re-map offset const segChords = chords .filter((cp) => cp.offset >= start && cp.offset < end) .map((cp) => ({ ...cp, offset: cp.offset - start })); - segments.push({ text: segText, chords: segChords }); - - // Advance past the break point, skipping leading spaces for next segment start = end; while (start < text.length && text[start] === " ") start++; } - return segments; } -function LineBlock({ line, sizeClass }: { line: LyricLine; sizeClass: string }) { +function ChordRow({ + chords, + sizeClass, + onChordClick, +}: { + chords: { offset: number; chord: string }[]; + sizeClass: string; + onChordClick?: (chord: string) => void; +}) { + return ( +
+ {chords.map(({ offset, chord }, i) => ( + onChordClick?.(chord)} + > + {chord} + + ))} +
+ ); +} + +function LineBlock({ + line, + sizeClass, + onChordClick, +}: { + line: LyricLine; + sizeClass: string; + onChordClick?: (chord: string) => void; +}) { return (
{line.chords.length > 0 && ( -
-          {buildChordRow(line.chords)}
-        
+ )} {line.text && (
@@ -72,7 +83,15 @@ function LineBlock({ line, sizeClass }: { line: LyricLine; sizeClass: string })
   );
 }
 
-function SectionBlock({ section, sizeClass }: { section: Section; sizeClass: string }) {
+function SectionBlock({
+  section,
+  sizeClass,
+  onChordClick,
+}: {
+  section: Section;
+  sizeClass: string;
+  onChordClick?: (chord: string) => void;
+}) {
   return (
     
{section.label && ( @@ -80,19 +99,19 @@ function SectionBlock({ section, sizeClass }: { section: Section; sizeClass: str )} {section.lines.flatMap((line, i) => segmentLine(line, MAX_WIDTH).map((seg, j) => ( - + )) )}
); } -export function ChordChart({ sections, fontSize }: Props) { +export function ChordChart({ sections, fontSize, onChordClick }: Props) { const sizeClass = { sm: 'text-sm', base: 'text-base', lg: 'text-lg' }[fontSize ?? 'sm']; return (
{sections.map((section, i) => ( - + ))}
); diff --git a/app/app/lib/song-utils.ts b/app/app/lib/song-utils.ts index fc33f99..a8a3951 100644 --- a/app/app/lib/song-utils.ts +++ b/app/app/lib/song-utils.ts @@ -1,4 +1,4 @@ -import type { Song } from "./types"; +import type { Song, Section } from "./types"; export function previewChords(song: Song): string[] { const seen = new Set(); @@ -16,3 +16,20 @@ export function previewChords(song: Song): string[] { } return result.slice(0, 5); } + +/** All unique chord names in order of first appearance across all sections. */ +export function extractUniqueChords(sections: Section[]): string[] { + const seen = new Set(); + const result: string[] = []; + for (const section of sections) { + for (const line of section.lines) { + for (const cp of line.chords) { + if (!seen.has(cp.chord)) { + seen.add(cp.chord); + result.push(cp.chord); + } + } + } + } + return result; +}