-
Notifications
You must be signed in to change notification settings - Fork 1
feat(gui): Expandable event rows with detail panel and fired styling (#485) #505
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
5e7f565
feat(gui): Expandable event rows with detail panel and fired styling …
amiable-dev 7dbe6a2
fix(gui): CSS precedence — highlight border takes priority over fired…
amiable-dev c95b358
fix(gui): Address 7 PR review comments (#505)
amiable-dev 1ba96b6
fix(gui): Keyboard a11y — don't block Learn button activation from ro…
amiable-dev 3742c8a
fix(gui): Address 4 PR review comments (#505)
amiable-dev d9a357e
fix(gui): Address 2 PR review comments (#505)
amiable-dev ac94a83
docs: ADR-015 Async Action Execution — decouple actions from event loop
amiable-dev 37487ee
docs: ADR-015 revision — address all 8 council review findings
amiable-dev 80eca4d
docs: ADR-015 revision 2 — address 7 council review 2 findings
amiable-dev 213461a
docs: ADR-015 revision 3 — address Council Review 3 findings
amiable-dev 1c91b53
docs: ADR-015 revision 4 — address Council Review 4 findings
amiable-dev f8d2a72
Merge remote-tracking branch 'origin/main' into feat/485-expandable-e…
amiable-dev 04943e1
fix: Address PR #505 review comments (round 3)
amiable-dev d224d18
fix: Address PR #505 review comments (round 4)
amiable-dev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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; | ||
| } | ||
|
|
||
| .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; | ||
|
|
@@ -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); } | ||
|
|
@@ -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
|
||
| } | ||
|
|
||
| .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> | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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-mainkeepsoverflow: 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.