fix(player): keep timeline separators on the track

A separator sits at `calc(end% - 2px)`, so a boundary in the bar's first
pixels (a 1-second lead-in of an hour on a 400px bar) painted it before
the track's start. Clamp it with max(0px, …); it ends where the next
segment starts, so the right end cannot overflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-10 07:07:34 +02:00
1 parent 9c0278079c
commit 82f809e9a6
4 files changed
+28 -12

No files matched your search

@@ -390,7 +390,9 @@ over a titled segment. mpv's chapter list is not a producer yet.
Boundaries hold 3:1 over any frame. Segments only round the bar's two
ends, and every gap (2px, `TIMELINE_SEGMENT_GAP_PX`) is filled by a
separator (`separatorLeft`) in `--pc-timeline-separator` white: 12.5:1 on
separator (`separatorLeft`, never left of the track's start, where a
boundary in the bar's first pixels would put it) in
`--pc-timeline-separator` white: 12.5:1 on
the opaque `--pc-timeline-track`, 3.2:1 on the `--pc-progress` fill, which
keeps 4.0:1 on the track. The former translucent track with see-through
gaps measured 1.3–1.6:1 at its boundaries, and its fill fell to 1.1:1 over
@@ -85,8 +85,8 @@ describe('normalizeTimelineSegments', () => {
]);
// The separator fills the gap, ending where the next segment starts.
expect(segments.map((s) => s.separatorLeft)).toEqual([
'calc(25% - 2px)',
'calc(75% - 2px)',
'max(0px, calc(25% - 2px))',
'max(0px, calc(75% - 2px))',
null,
]);
expect(normalizeTimelineSegments(null, 600)[0].separatorLeft).toBe(
@@ -94,6 +94,18 @@ describe('normalizeTimelineSegments', () => {
);
});
it('keeps the separator of a boundary in the first pixels on the track', () => {
// A 1-second lead-in of an hour: on a 400px bar its boundary sits
// at 0.11px, so `calc(… - 2px)` alone would start before the bar.
const [leadIn] = normalizeTimelineSegments(
[{ startSeconds: 1, endSeconds: 3600, title: 'Programme' }],
3600
);
expect(leadIn.title).toBeNull();
expect(leadIn.separatorLeft).toMatch(/^max\(0px, calc\(/);
});
it('clamps to the duration, cuts overlaps and drops empty segments', () => {
const segments = normalizeTimelineSegments(
[
@@ -14,7 +14,8 @@ export interface TimelineSegmentView {
/**
* CSS `left` of the separator drawn in the gap after this segment, so a
* boundary keeps its contrast whatever the frame behind it; null for
* the last segment, which has no gap.
* the last segment, which has no gap. Never left of the track: a
* boundary in its first pixels would otherwise paint before the bar.
*/
separatorLeft: string | null;
}
@@ -103,7 +104,7 @@ export function normalizeTimelineSegments(
: `max(0px, calc(${share * 100}% - ${gap}))`,
separatorLeft: last
? null
: `calc(${(segment.endSeconds / durationSeconds) * 100}% - ${gap})`,
: `max(0px, calc(${(segment.endSeconds / durationSeconds) * 100}% - ${gap}))`,
};
});
}
@@ -340,13 +340,14 @@ describe('PlayerControlsComponent dock', () => {
]);
expect(segments.at(-1)?.style.width).toBe('25%');
// A separator fills the gap before every following segment.
expect(
Array.from(
fixture.nativeElement.querySelectorAll(
'.player-controls__timeline-separator'
) as NodeListOf<HTMLElement>
).map((separator) => separator.style.left)
).toEqual(['calc(25% - 2px)', 'calc(75% - 2px)']);
const separators = Array.from(
fixture.nativeElement.querySelectorAll(
'.player-controls__timeline-separator'
) as NodeListOf<HTMLElement>
).map((separator) => separator.style.left);
expect(separators).toHaveLength(2);
expect(separators[0]).toContain('calc(25% - 2px)');
expect(separators[1]).toContain('calc(75% - 2px)');
expect(
segments.map(
(s) =>