diff --git a/docs/architecture/player-controls-contract.md b/docs/architecture/player-controls-contract.md index 8085bcb53..7ce0031f1 100644 --- a/docs/architecture/player-controls-contract.md +++ b/docs/architecture/player-controls-contract.md @@ -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 diff --git a/libs/ui/playback/src/lib/player-controls/controls-timeline-segments.spec.ts b/libs/ui/playback/src/lib/player-controls/controls-timeline-segments.spec.ts index 2bc105a59..94805016a 100644 --- a/libs/ui/playback/src/lib/player-controls/controls-timeline-segments.spec.ts +++ b/libs/ui/playback/src/lib/player-controls/controls-timeline-segments.spec.ts @@ -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( [ diff --git a/libs/ui/playback/src/lib/player-controls/controls-timeline-segments.ts b/libs/ui/playback/src/lib/player-controls/controls-timeline-segments.ts index e973ab6f8..4ff922615 100644 --- a/libs/ui/playback/src/lib/player-controls/controls-timeline-segments.ts +++ b/libs/ui/playback/src/lib/player-controls/controls-timeline-segments.ts @@ -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}))`, }; }); } diff --git a/libs/ui/playback/src/lib/player-controls/player-controls.component.dock.spec.ts b/libs/ui/playback/src/lib/player-controls/player-controls.component.dock.spec.ts index 87da970fc..ad4bb6afb 100644 --- a/libs/ui/playback/src/lib/player-controls/player-controls.component.dock.spec.ts +++ b/libs/ui/playback/src/lib/player-controls/player-controls.component.dock.spec.ts @@ -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 - ).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 + ).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) =>