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 {
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user