Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 188 additions & 18 deletions conductor-gui/ui/src/lib/components/EventRow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
<!-- SPDX-License-Identifier: MIT -->

<!--
EventRow — Single event row in the event stream (GUI v2 Phase 4)
EventRow — Single event row in the event stream (GUI v2 Phase 4 + ADR-014 Phase 3A)

Displays a colored dot, type label, detail string, relative time,
and an optional Learn button that appears on hover.
and an optional Learn button. Click to expand and see detail panel.
MappingFired events get distinct styling (⚡ icon, left border, accent label).
-->

<script>
import { createEventDispatcher } from 'svelte';
import { getEventDotClass, getEventTypeLabel, getEventDetail, getRelativeTime } from '$lib/utils/event-helpers';
import {
getEventDotClass, getEventTypeLabel, getEventDetail, getRelativeTime,
formatLatency, formatAbsoluteTime, isFiredEvent, getFiredDotClass,
} from '$lib/utils/event-helpers';
import { nowTick } from '$lib/stores/events.js';

export let event;
Expand All @@ -19,46 +23,139 @@

const dispatch = createEventDispatcher();

$: dotClass = getEventDotClass(event);
$: typeLabel = getEventTypeLabel(event);
$: detail = getEventDetail(event);
let expanded = false;

$: fired = isFiredEvent(event);
$: dotClass = fired ? getFiredDotClass(event) : getEventDotClass(event);
$: typeLabel = fired ? 'Fired' : getEventTypeLabel(event);
$: detail = fired
? `${event.trigger?.type ?? '?'} → ${event.action?.summary ?? 'Mapping fired'}`
: getEventDetail(event);
$: relTime = getRelativeTime(event.timestamp, $nowTick);

function handleClick() {
expanded = !expanded;
}

function handleLearn() {
dispatch('learn', event);
}
</script>

<div class="event-row" class:highlight>
<span class="event-dot {dotClass}"></span>
<span class="event-type {dotClass}">{typeLabel}</span>
<span class="event-detail">{detail}</span>
<span class="event-time">{relTime}</span>
{#if showLearnBtn}
<button class="event-learn-btn" on:click|stopPropagation={handleLearn}>Learn</button>
<div
class="event-row"
class:highlight
class:expanded
class:fired={fired && !highlight}
class:border-note={fired && !highlight && dotClass === 'note'}
class:border-cc={fired && !highlight && dotClass === 'cc'}
class:border-encoder={fired && !highlight && dotClass === 'encoder'}
class:border-aftertouch={fired && !highlight && dotClass === 'aftertouch'}
class:border-pitchbend={fired && !highlight && dotClass === 'pitchbend'}
class:border-gamepad={fired && !highlight && dotClass === 'gamepad'}
>
<div class="event-main">
<button
type="button"
class="event-toggle"
on:click={handleClick}
aria-expanded={expanded}
>
{#if fired}
<span class="event-fired-icon">⚡</span>
{:else}
<span class="event-dot {dotClass}"></span>
{/if}
<span class="event-type {dotClass}" class:fired-label={fired}>{typeLabel}</span>
<span class="event-detail">{detail}</span>
<span class="event-time">{relTime}</span>
<span class="event-chevron">{expanded ? '▾' : '▸'}</span>
</button>
{#if showLearnBtn}
<button class="event-learn-btn" on:click|stopPropagation={handleLearn}>Learn</button>
{/if}
</div>

{#if expanded}
<div class="event-detail-panel">
{#if fired}
<div class="detail-row"><span class="detail-label">Trigger</span> <span class="detail-value">{event.trigger?.type ?? '?'} #{event.trigger?.number ?? '?'} v={event.trigger?.value ?? '?'}{event.trigger?.device ? ` (${event.trigger.device})` : ''}</span></div>
<div class="detail-row"><span class="detail-label">Action</span> <span class="detail-value">{event.action?.type ?? '?'}: {event.action?.summary ?? '?'}</span></div>
<div class="detail-row"><span class="detail-label">Result</span> <span class="detail-value" class:error-result={event.result === 'error'}>{event.result ?? '?'}{event.error ? ` — ${event.error}` : ''}</span></div>
<div class="detail-row"><span class="detail-label">Latency</span> <span class="detail-value">{event.latency_us != null ? formatLatency(event.latency_us) : '—'}</span></div>
<div class="detail-row"><span class="detail-label">Mode</span> <span class="detail-value">{event.mode ?? '?'}</span></div>
<div class="detail-row"><span class="detail-label">Time</span> <span class="detail-value">{formatAbsoluteTime(event.timestamp)}</span></div>
{:else}
<div class="detail-row"><span class="detail-label">Device</span> <span class="detail-value">{event.device_id ?? 'Unknown'}</span></div>
<div class="detail-row"><span class="detail-label">Channel</span> <span class="detail-value">{event.channel ?? '—'}</span></div>
<div class="detail-row"><span class="detail-label">Type</span> <span class="detail-value">{event.event_type}</span></div>
<div class="detail-row"><span class="detail-label">Time</span> <span class="detail-value">{formatAbsoluteTime(event.timestamp)}</span></div>
{/if}
</div>
{/if}
</div>

<style>
.event-row {
display: flex;
align-items: center;
padding: 4px 14px;
gap: 8px;
font-size: var(--font-size-base);
padding: 0;
border-bottom: 1px solid var(--border-subtle);
transition: background 0.1s;
cursor: pointer;
}

.event-row:hover {
background: var(--bg-card-hover);
}

.event-row.fired {
border-left: 2px solid var(--text-dim);
}

.event-row.fired.border-note { border-left-color: var(--green); }
.event-row.fired.border-cc { border-left-color: var(--blue); }
.event-row.fired.border-encoder { border-left-color: var(--purple); }
.event-row.fired.border-aftertouch { border-left-color: var(--amber); }
.event-row.fired.border-pitchbend { border-left-color: var(--purple); }
.event-row.fired.border-gamepad { border-left-color: var(--event-gamepad, var(--amber)); }

/* Highlight takes precedence over fired border (Learn mode).
fired class is not applied when highlight is true, avoiding specificity conflicts. */
.event-row.highlight {
background: var(--accent-tint);
border-left: 2px solid var(--accent);
}

.event-main {
display: flex;
align-items: center;
gap: 0;
font-size: var(--font-size-base);
overflow: hidden;
white-space: nowrap;
}

Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded state doesn’t currently untruncate the main row: .event-main keeps overflow: hidden + white-space: nowrap, so the summary line remains clipped even when expanded. Consider adding .event-row.expanded .event-main { white-space: normal; overflow: visible; } (or similar) to match the intended expanded behavior.

Suggested change
/* When expanded, allow the main summary line to wrap and be fully visible */
.event-row.expanded .event-main {
white-space: normal;
overflow: visible;
}

Copilot uses AI. Check for mistakes.
.event-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 14px;
flex: 1;
min-width: 0;
background: none;
border: none;
color: inherit;
font: inherit;
cursor: pointer;
text-align: left;
overflow: hidden;
white-space: nowrap;
}

.event-row.expanded .event-toggle {
white-space: normal;
overflow: visible;
}

.event-dot {
width: 5px;
height: 5px;
Expand All @@ -73,9 +170,16 @@
.event-dot.encoder { background: var(--purple); }
.event-dot.unknown { background: var(--text-dim); }

.event-fired-icon {
flex-shrink: 0;
font-size: 10px;
line-height: 1;
}

.event-type {
min-width: 55px;
font-weight: 600;
flex-shrink: 0;
}

.event-type.note { color: var(--green); }
Expand All @@ -85,31 +189,97 @@
.event-type.encoder { color: var(--purple); }
.event-type.unknown { color: var(--text-dim); }

.event-type.fired-label {
color: var(--accent);
}

.event-detail {
color: var(--text-dim);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
Comment on lines 198 to +202
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event-detail is always ellipsized (text-overflow: ellipsis / white-space: nowrap), so expanding the row won’t reveal long detail strings. Add an expanded-state override (e.g., unset text-overflow and allow wrapping) so the user can read the full text when expanded.

Copilot uses AI. Check for mistakes.
}

.event-row.expanded .event-detail {
overflow: visible;
text-overflow: unset;
white-space: normal;
word-break: break-word;
}

.event-time {
color: var(--text-dim);
font-size: 10px;
opacity: 0.5;
flex-shrink: 0;
}

.event-chevron {
color: var(--text-dim);
font-size: 10px;
opacity: 0;
flex-shrink: 0;
transition: opacity 0.15s;
width: 10px;
text-align: center;
}

.event-row:hover .event-chevron {
opacity: 0.6;
}

.event-row.expanded .event-chevron {
opacity: 0.8;
}

.event-learn-btn {
opacity: 0;
font-size: 9px;
padding: 1px 6px;
margin-right: 14px;
border-radius: 4px;
background: var(--accent);
color: var(--text-on-accent);
border: none;
cursor: pointer;
font-family: inherit;
transition: opacity 0.15s;
flex-shrink: 0;
}

.event-row:hover .event-learn-btn {
opacity: 1;
}

/* Detail panel */
.event-detail-panel {
padding: 6px 14px 8px 28px;
font-size: 10px;
color: var(--text-dim);
display: flex;
flex-direction: column;
gap: 2px;
border-top: 1px solid var(--border-subtle);
}

.detail-row {
display: flex;
gap: 8px;
}

.detail-label {
min-width: 50px;
color: var(--text-dim);
opacity: 0.6;
}

.detail-value {
color: var(--text);
}

.error-result {
color: var(--red, #ff6b6b);
}
</style>
Loading
Loading