@@ -113,14 +128,32 @@ export function Timeline({ from, to, lanes, hideLabels, onSelectLane }: Props) {
/>
))}
- {sessions.map((bar) => (
-
- ))}
+ {sessions.map((bar) => {
+ const showPreview =
+ bar.previewId !== undefined && !broken.has(bar.previewId);
+ return (
+
setPreview({ id: bar.previewId!, ...place(event) })
+ : undefined
+ }
+ onMouseMove={
+ showPreview
+ ? (event) => setPreview({ id: bar.previewId!, ...place(event) })
+ : undefined
+ }
+ onMouseLeave={showPreview ? () => setPreview(null) : undefined}
+ />
+ );
+ })}
{spans.map((bar) => (
Aucune diffusion observée sur cette période.
)}
+
+ {/* Hors de la piste, et en `fixed` : `.timeline-track` recadre son
+ contenu (`overflow: hidden`) pour que les barres ne débordent pas de
+ la fenêtre affichée — une vignette posée dedans s'en trouverait
+ rognée, et son ancrage suivrait le défilement horizontal de la frise. */}
+ {preview && (
+
+
})
setBroken((current) => new Set(current).add(preview.id))}
+ />
+
+ )}
);
}
+/**
+ * Position de la vignette près du curseur, sans jamais sortir de la fenêtre.
+ *
+ * Au-dessus du curseur par défaut — la frise se lit de haut en bas, et une
+ * vignette posée dessous masquerait les voies suivantes. En dessous seulement
+ * quand le haut manque de place, sur la première ligne de la frise.
+ */
+function place(event: { clientX: number; clientY: number }): { x: number; y: number } {
+ const height = (PREVIEW_WIDTH * 9) / 16;
+ const margin = 8;
+ const x = Math.min(
+ Math.max(event.clientX - PREVIEW_WIDTH / 2, margin),
+ window.innerWidth - PREVIEW_WIDTH - margin,
+ );
+ const above = event.clientY - height - 16;
+ return { x, y: above >= margin ? above : event.clientY + 16 };
+}
+
const percent = (at: number, from: number, to: number) => ((at - from) / (to - from)) * 100;
/**
@@ -172,6 +240,9 @@ function toBars(
key: `${kind}-${interval.id}`,
left: percent(start, from, to),
width: percent(end, from, to) - percent(start, from, to),
+ // Seules les diffusions en ont une ; les intervalles de capture partagent
+ // cette fonction mais ne portent pas le champ.
+ previewId: 'hasPreview' in interval && interval.hasPreview ? interval.id : undefined,
// Conservé à part des pourcentages : repasser de la largeur affichée aux
// millisecondes ferait perdre une minute sur deux heures, et le total
// annoncé ne collerait plus aux durées listées juste à côté.
diff --git a/packages/web/src/styles.css b/packages/web/src/styles.css
index 81b21a0..33f4b59 100644
--- a/packages/web/src/styles.css
+++ b/packages/web/src/styles.css
@@ -976,6 +976,32 @@ fieldset.group > legend {
background: var(--rec);
}
+/*
+ * Vignette conservée d'une diffusion, montrée au survol de sa barre. En `fixed`
+ * et positionnée depuis le curseur : la piste qui l'accueillerait sinon recadre
+ * son contenu et défile horizontalement.
+ */
+.timeline-preview {
+ position: fixed;
+ z-index: 60;
+ padding: 3px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6);
+ /* Le curseur la traverserait sinon en la survolant, ce qui déclencherait le
+ `mouseleave` de la barre : la vignette clignoterait sous la souris. */
+ pointer-events: none;
+}
+.timeline-preview img {
+ display: block;
+ width: 100%;
+ aspect-ratio: 16 / 9;
+ object-fit: cover;
+ border-radius: 4px;
+ background: var(--panel-2);
+}
+
/* --- Journal --- */
/* Vue pleine page depuis l'onglet Journal. Elle défile avec la page, comme la