diff --git a/app/features/scanner/components/EventFeed.module.css b/app/features/scanner/components/EventFeed.module.css new file mode 100644 index 000000000..9f9b7f760 --- /dev/null +++ b/app/features/scanner/components/EventFeed.module.css @@ -0,0 +1,63 @@ +.feed { + display: flex; + flex-direction: column; + gap: var(--s-1); + margin: 0; + padding: 0; + list-style: none; +} + +.item { + display: grid; + grid-template-columns: auto auto minmax(0, 1fr); + align-items: center; + gap: var(--s-2); + padding: var(--s-1) var(--s-2); + border-radius: var(--radius-field); + font-size: var(--font-xs); + animation: slide-in 0.35s ease-out; + + &:first-child { + background: var(--color-bg-high); + } + + &:nth-child(n + 4) { + opacity: 0.6; + } +} + +.icon { + display: flex; + color: var(--color-fg-accent); +} + +.time { + font-size: var(--font-2xs); + color: var(--color-text-high); + font-variant-numeric: tabular-nums; +} + +.label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.empty { + padding: var(--s-1) var(--s-2); + font-size: var(--font-xs); + color: var(--color-text-high); +} + +@keyframes slide-in { + from { + opacity: 0; + translate: 0 -6px; + } +} + +@media (prefers-reduced-motion: reduce) { + .item { + animation: none; + } +} diff --git a/app/features/scanner/components/EventFeed.tsx b/app/features/scanner/components/EventFeed.tsx new file mode 100644 index 000000000..a249734d3 --- /dev/null +++ b/app/features/scanner/components/EventFeed.tsx @@ -0,0 +1,103 @@ +/** The latest reads of a running capture as one-line rows, newest first. */ +import * as R from "remeda"; +import { + DEATH_EVENT_TYPE, + type DeathData, +} from "../core/detectors/death/index"; +import { KILL_EVENT_TYPE } from "../core/detectors/kill/index"; +import { + MAP_START_EVENT_TYPE, + type MapStartData, +} from "../core/detectors/map-start/index"; +import { SCOREBOARD_EVENT_TYPE } from "../core/detectors/scoreboard/index"; +import { SCOREBOARD_BATTLE_LOG_EVENT_TYPE } from "../core/detectors/scoreboard-battle-log/index"; +import { SCOREBOARD_OWN_EVENT_TYPE } from "../core/detectors/scoreboard-own/index"; +import { formatPosition } from "../core/format"; +import { modeLabel, stageLabel } from "../core/labels"; +import type { ScannerMatch } from "../core/scanner-match"; +import styles from "./EventFeed.module.css"; +import { EventTypeIcon } from "./EventTypeIcon"; +import type { ScanEvent } from "./session-data"; + +const FEED_LENGTH = 6; + +interface FeedItem { + t: number; + type: string; + label: string; +} + +export function EventFeed({ + events, + matches, + originT, +}: { + events: ScanEvent[]; + matches: ScannerMatch[]; + originT: number; +}) { + const items = R.pipe( + [ + ...events.flatMap((event) => { + const label = eventLabel(event); + return label === null ? [] : [{ t: event.t, type: event.type, label }]; + }), + // the builder's splats, not the raw feed reads: each stack change re-reads the same rows + ...matches.flatMap((match) => + (match.kills ?? []).map( + (kill): FeedItem => ({ + t: kill.t, + type: KILL_EVENT_TYPE, + label: kill.name ? `Splatted ${kill.name}` : "Splat", + }), + ), + ), + ], + R.sortBy([R.prop("t"), "desc"]), + R.take(FEED_LENGTH), + ); + + return ( +