feat: Replaced thin 3px line markers with 12px color-coded circle pins,…

- "frontend/src/components/ChapterMarkers.tsx"
- "frontend/src/components/PlayerControls.tsx"
- "frontend/src/App.css"

GSD-Task: S02/T01
This commit is contained in:
jlightner 2026-04-04 10:44:45 +00:00
parent 3f75f33c2b
commit fa7e4983c7
3 changed files with 83 additions and 18 deletions

View file

@ -29,6 +29,12 @@
--color-accent-subtle: rgba(34, 211, 238, 0.1); --color-accent-subtle: rgba(34, 211, 238, 0.1);
--color-accent-focus: rgba(34, 211, 238, 0.15); --color-accent-focus: rgba(34, 211, 238, 0.15);
/* Content-type pin colors */
--color-pin-technique: #22d3ee; /* cyan */
--color-pin-settings: #f59e0b; /* amber */
--color-pin-reasoning: #a855f7; /* purple */
--color-pin-workflow: #22c55e; /* green */
/* Shadows / overlays */ /* Shadows / overlays */
--color-shadow: rgba(0, 0, 0, 0.2); --color-shadow: rgba(0, 0, 0, 0.2);
--color-shadow-heavy: rgba(0, 0, 0, 0.4); --color-shadow-heavy: rgba(0, 0, 0, 0.4);
@ -6238,42 +6244,69 @@ a.app-footer__about:hover,
pointer-events: none; pointer-events: none;
} }
.chapter-marker__tick { .chapter-marker__pin {
position: absolute; position: absolute;
width: 3px; width: 12px;
height: 100%; height: 12px;
border-radius: 50%;
background: var(--color-accent, #22d3ee); background: var(--color-accent, #22d3ee);
opacity: 0.6; opacity: 0.7;
pointer-events: all; pointer-events: all;
cursor: pointer; cursor: pointer;
transform: translateX(-50%); transform: translate(-50%, -50%);
border: none; top: 50%;
border: 2px solid rgba(0, 0, 0, 0.4);
padding: 0; padding: 0;
font: inherit; font: inherit;
transition: transform 150ms ease, opacity 150ms ease;
/* Ensure touch-friendly hit area */
min-width: 12px;
min-height: 12px;
} }
.chapter-marker__tick:hover { .chapter-marker__pin::before {
content: "";
position: absolute;
inset: -6px;
}
.chapter-marker__pin--technique { background: var(--color-pin-technique); }
.chapter-marker__pin--settings { background: var(--color-pin-settings); }
.chapter-marker__pin--reasoning { background: var(--color-pin-reasoning); }
.chapter-marker__pin--workflow { background: var(--color-pin-workflow); }
.chapter-marker__pin--active {
opacity: 1; opacity: 1;
transform: translate(-50%, -50%) scale(1.3);
z-index: 2;
}
.chapter-marker__pin:hover {
opacity: 1;
transform: translate(-50%, -50%) scale(1.2);
}
.chapter-marker__pin--active:hover {
transform: translate(-50%, -50%) scale(1.3);
} }
.chapter-marker__tooltip { .chapter-marker__tooltip {
position: absolute; position: absolute;
bottom: 100%; bottom: calc(100% + 4px);
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
background: var(--color-bg-surface, #1e293b); background: var(--color-bg-surface, #1e293b);
color: var(--text-primary, #e2e8f0); color: var(--text-primary, #e2e8f0);
padding: 4px 8px; padding: 4px 10px;
border-radius: 4px; border-radius: 4px;
font-size: 0.75rem; font-size: 0.75rem;
white-space: nowrap; white-space: nowrap;
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
transition: opacity 150ms; transition: opacity 150ms;
margin-bottom: 4px;
} }
.chapter-marker__tick:hover .chapter-marker__tooltip { .chapter-marker__pin:hover .chapter-marker__tooltip {
opacity: 1; opacity: 1;
} }

View file

@ -4,33 +4,65 @@ interface ChapterMarkersProps {
chapters: Chapter[]; chapters: Chapter[];
duration: number; duration: number;
onSeek: (time: number) => void; onSeek: (time: number) => void;
currentTime?: number;
} }
/** Format seconds as m:ss */
function formatTime(seconds: number): string {
if (!Number.isFinite(seconds) || seconds < 0) return "0:00";
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, "0")}`;
}
const PIN_COLOR_CLASS: Record<string, string> = {
technique: "chapter-marker__pin--technique",
settings: "chapter-marker__pin--settings",
reasoning: "chapter-marker__pin--reasoning",
workflow: "chapter-marker__pin--workflow",
};
/** /**
* Absolutely-positioned overlay that renders tick marks on the seek bar * Absolutely-positioned overlay that renders color-coded circle pins
* for each chapter. Ticks are clickable and show a tooltip on hover. * on the seek bar for each chapter. Pins are clickable and show a
* rich tooltip on hover. The active pin (matching current playback
* position) is enlarged.
*/ */
export default function ChapterMarkers({ chapters, duration, onSeek }: ChapterMarkersProps) { export default function ChapterMarkers({
chapters,
duration,
onSeek,
currentTime,
}: ChapterMarkersProps) {
if (!duration || chapters.length === 0) return null; if (!duration || chapters.length === 0) return null;
return ( return (
<div className="chapter-markers"> <div className="chapter-markers">
{chapters.map((ch) => { {chapters.map((ch) => {
const leftPct = (ch.start_time / duration) * 100; const leftPct = (ch.start_time / duration) * 100;
const isActive =
currentTime != null &&
ch.start_time <= currentTime &&
currentTime <= ch.end_time;
const colorClass = PIN_COLOR_CLASS[ch.content_type] ?? "";
const activeClass = isActive ? " chapter-marker__pin--active" : "";
return ( return (
<button <button
key={ch.id} key={ch.id}
className="chapter-marker__tick" className={`chapter-marker__pin${colorClass ? ` ${colorClass}` : ""}${activeClass}`}
style={{ left: `${leftPct}%` }} style={{ left: `${leftPct}%` }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onSeek(ch.start_time); onSeek(ch.start_time);
}} }}
aria-label={`Jump to chapter: ${ch.title}`} aria-label={`Jump to chapter: ${ch.title}`}
title={ch.title}
type="button" type="button"
> >
<span className="chapter-marker__tooltip">{ch.title}</span> <span className="chapter-marker__tooltip">
{ch.title} · {formatTime(ch.start_time)}{formatTime(ch.end_time)} · {ch.content_type}
</span>
</button> </button>
); );
})} })}

View file

@ -140,7 +140,7 @@ export default function PlayerControls({ mediaSync, containerRef, chapters }: Pl
style={{ "--progress": `${seekProgress}%` } as React.CSSProperties} style={{ "--progress": `${seekProgress}%` } as React.CSSProperties}
/> />
{chapters && chapters.length > 0 && ( {chapters && chapters.length > 0 && (
<ChapterMarkers chapters={chapters} duration={duration} onSeek={seekTo} /> <ChapterMarkers chapters={chapters} duration={duration} onSeek={seekTo} currentTime={currentTime} />
)} )}
</div> </div>