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:
parent
3f75f33c2b
commit
fa7e4983c7
3 changed files with 83 additions and 18 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue