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 ( +
- {buildChordRow(line.chords)}
-
+
@@ -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;
+}