fix(app): wrap long chord/lyric lines at word boundaries for mobile

This commit is contained in:
2026-04-08 02:47:17 +02:00
parent 3bc7ad4c7c
commit 852b1a8d0e

View File

@@ -1,4 +1,8 @@
import type { Section } from "~/lib/types"; 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 { interface Props {
sections: Section[]; sections: Section[];
@@ -13,26 +17,71 @@ function buildChordRow(chords: { offset: number; chord: string }[]): string {
return row; return row;
} }
/** 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[] = [];
let start = 0;
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 }: { line: LyricLine }) {
return (
<div className="leading-tight">
{line.chords.length > 0 && (
<pre className="text-primary text-sm font-mono whitespace-pre">
{buildChordRow(line.chords)}
</pre>
)}
{line.text && (
<pre className="text-foreground text-sm font-mono whitespace-pre">
{line.text}
</pre>
)}
</div>
);
}
function SectionBlock({ section }: { section: Section }) { function SectionBlock({ section }: { section: Section }) {
return ( return (
<div className="mb-6"> <div className="mb-6">
{section.label && ( {section.label && (
<p className="text-xs text-muted-foreground mb-1">[{section.label}]</p> <p className="text-xs text-muted-foreground mb-1">[{section.label}]</p>
)} )}
{section.lines.map((line, i) => ( {section.lines.flatMap((line, i) =>
<div key={i} className="leading-tight"> segmentLine(line, MAX_WIDTH).map((seg, j) => (
{line.chords.length > 0 && ( <LineBlock key={`${i}-${j}`} line={seg} />
<pre className="text-primary text-sm font-mono whitespace-pre"> ))
{buildChordRow(line.chords)} )}
</pre>
)}
{line.text && (
<pre className="text-foreground text-sm font-mono whitespace-pre">
{line.text}
</pre>
)}
</div>
))}
</div> </div>
); );
} }