fix(app): wrap long chord/lyric lines at word boundaries for mobile
This commit is contained in:
@@ -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 {
|
||||
sections: Section[];
|
||||
@@ -13,26 +17,71 @@ function buildChordRow(chords: { offset: number; chord: string }[]): string {
|
||||
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 }) {
|
||||
return (
|
||||
<div className="mb-6">
|
||||
{section.label && (
|
||||
<p className="text-xs text-muted-foreground mb-1">[{section.label}]</p>
|
||||
)}
|
||||
{section.lines.map((line, i) => (
|
||||
<div key={i} 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>
|
||||
))}
|
||||
{section.lines.flatMap((line, i) =>
|
||||
segmentLine(line, MAX_WIDTH).map((seg, j) => (
|
||||
<LineBlock key={`${i}-${j}`} line={seg} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user