feat(ui): redesign target pipeline as compact strip + chip row
Drops the legacy "Pipeline details" collapsible block on running LED target cards. Instead: - Always-visible 4px segmented timing bar (extract / map / smooth / send for video, read / fft / render / send for audio) — same stage colors as before, scaled by per-segment ms cost - One chip row beneath it: total ms / frames count / keepalive count, using a new .chip--inline variant (display-weighted number + tiny mono-caps unit) - _patchTargetMetrics now writes only the bar's segments and the data-tm spans — bar wrapper survives across polls so the flex-transition animates smoothly between samples - _buildLedTimingHTML replaced by _buildLedTimingSegments (no more header / total / legend wrappers — those live in the chip row) Cleanup - Drop .target-metrics-collapse / -toggle / -animate / -expanded CSS — no callers remain - Drop targets.metrics.pipeline from en/ru/zh locales — toggle label is gone
This commit is contained in:
@@ -1253,39 +1253,8 @@ ul.section-tip li {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Collapsible target pipeline metrics */
|
||||
.target-metrics-collapse {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.target-metrics-toggle {
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 4px 0;
|
||||
user-select: none;
|
||||
background: none;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
.target-metrics-toggle::before {
|
||||
content: '▸ ';
|
||||
}
|
||||
.target-metrics-collapse.open .target-metrics-toggle::before {
|
||||
content: '▾ ';
|
||||
}
|
||||
.target-metrics-animate {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows 0.25s ease;
|
||||
}
|
||||
.target-metrics-collapse.open .target-metrics-animate {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
.target-metrics-animate > .target-metrics-expanded {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Timing breakdown bar */
|
||||
/* Timing breakdown bar — used by graph-editor and the api-input test
|
||||
modal; the targets card uses its own .target-pipeline wrapper. */
|
||||
.timing-breakdown {
|
||||
margin-top: 8px;
|
||||
padding: 6px 8px;
|
||||
@@ -2188,6 +2157,63 @@ ul.section-tip li {
|
||||
background: color-mix(in srgb, var(--ch-coral, var(--danger-color)) 10%, transparent);
|
||||
}
|
||||
|
||||
/* Inline chip variant — used by the pipeline strip on running target
|
||||
cards. A bigger numeric (display-font-ish) sits beside a small mono
|
||||
caps unit, so a chip can carry "127K · frames" without looking like
|
||||
a label-shaped pill. The data values inside still update via
|
||||
_patchTargetMetrics. */
|
||||
.mod-card .chip--inline {
|
||||
gap: 4px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
.mod-card .chip--inline > span {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
color: var(--lux-ink, var(--text-color));
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.mod-card .chip--inline > small {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 0.55rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--lux-ink-mute, var(--text-secondary));
|
||||
}
|
||||
|
||||
/* ── Pipeline strip — replaces the old target-metrics-collapse. A
|
||||
thin always-visible segmented timing bar plus a tight chip row
|
||||
below it (timing total / frames / keepalive). The bar is the
|
||||
diagnostic; the chips are the counters. */
|
||||
.mod-card .target-pipeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.mod-card .target-pipeline .timing-bar {
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--lux-bg-0, var(--bg-color));
|
||||
box-shadow: inset 0 0 0 1px var(--lux-line, var(--border-color));
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
}
|
||||
.mod-card .target-pipeline .timing-seg {
|
||||
transition: flex 0.3s ease;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.mod-card .target-pipeline .timing-seg:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.mod-card .target-pipeline-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* ── Brightness fader (mod-card variant) ────────────────────────── */
|
||||
|
||||
.mod-fader {
|
||||
|
||||
@@ -874,37 +874,27 @@ function _cssSourceName(cssId: any, colorStripSourceMap: any) {
|
||||
|
||||
// ─── In-place metric patching (avoids full card replacement on polls) ───
|
||||
|
||||
function _buildLedTimingHTML(state: any) {
|
||||
/** Builds just the inner segments of the timing bar — no wrapper.
|
||||
* Each segment is flex-sized by its ms cost and tooltipped with its
|
||||
* stage label. Used by both the initial render and the patcher.
|
||||
* The wrapper `.timing-bar` lives in the card markup so the bar
|
||||
* itself doesn't get replaced on every poll (preserves transitions). */
|
||||
function _buildLedTimingSegments(state: any): string {
|
||||
const isAudio = state.timing_audio_read_ms != null;
|
||||
return `
|
||||
<div class="timing-header">
|
||||
<div class="metric-label">${t('device.metrics.timing')}</div>
|
||||
<div class="timing-total"><strong>${state.timing_total_ms}ms</strong></div>
|
||||
</div>
|
||||
<div class="timing-bar">
|
||||
${isAudio ? `
|
||||
<span class="timing-seg timing-audio-read" style="flex:${state.timing_audio_read_ms}" title="read ${state.timing_audio_read_ms}ms"></span>
|
||||
<span class="timing-seg timing-audio-fft" style="flex:${state.timing_audio_fft_ms}" title="fft ${state.timing_audio_fft_ms}ms"></span>
|
||||
const send = `<span class="timing-seg timing-send" style="flex:${state.timing_send_ms || 0.1}" title="send ${state.timing_send_ms}ms"></span>`;
|
||||
if (isAudio) {
|
||||
return `
|
||||
<span class="timing-seg timing-audio-read" style="flex:${state.timing_audio_read_ms || 0.1}" title="read ${state.timing_audio_read_ms}ms"></span>
|
||||
<span class="timing-seg timing-audio-fft" style="flex:${state.timing_audio_fft_ms || 0.1}" title="fft ${state.timing_audio_fft_ms}ms"></span>
|
||||
<span class="timing-seg timing-audio-render" style="flex:${state.timing_audio_render_ms || 0.1}" title="render ${state.timing_audio_render_ms}ms"></span>
|
||||
` : `
|
||||
${state.timing_extract_ms != null ? `<span class="timing-seg timing-extract" style="flex:${state.timing_extract_ms}" title="extract ${state.timing_extract_ms}ms"></span>` : ''}
|
||||
${state.timing_map_leds_ms != null ? `<span class="timing-seg timing-map" style="flex:${state.timing_map_leds_ms}" title="map ${state.timing_map_leds_ms}ms"></span>` : ''}
|
||||
${state.timing_smooth_ms != null ? `<span class="timing-seg timing-smooth" style="flex:${state.timing_smooth_ms || 0.1}" title="smooth ${state.timing_smooth_ms}ms"></span>` : ''}
|
||||
`}
|
||||
<span class="timing-seg timing-send" style="flex:${state.timing_send_ms}" title="send ${state.timing_send_ms}ms"></span>
|
||||
</div>
|
||||
<div class="timing-legend">
|
||||
${isAudio ? `
|
||||
<span class="timing-legend-item"><span class="timing-dot timing-audio-read"></span>read ${state.timing_audio_read_ms}ms</span>
|
||||
<span class="timing-legend-item"><span class="timing-dot timing-audio-fft"></span>fft ${state.timing_audio_fft_ms}ms</span>
|
||||
<span class="timing-legend-item"><span class="timing-dot timing-audio-render"></span>render ${state.timing_audio_render_ms}ms</span>
|
||||
` : `
|
||||
${state.timing_extract_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-extract"></span>extract ${state.timing_extract_ms}ms</span>` : ''}
|
||||
${state.timing_map_leds_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-map"></span>map ${state.timing_map_leds_ms}ms</span>` : ''}
|
||||
${state.timing_smooth_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-smooth"></span>smooth ${state.timing_smooth_ms}ms</span>` : ''}
|
||||
`}
|
||||
<span class="timing-legend-item"><span class="timing-dot timing-send"></span>send ${state.timing_send_ms}ms</span>
|
||||
</div>`;
|
||||
${send}`;
|
||||
}
|
||||
const segs: string[] = [];
|
||||
if (state.timing_extract_ms != null) segs.push(`<span class="timing-seg timing-extract" style="flex:${state.timing_extract_ms || 0.1}" title="extract ${state.timing_extract_ms}ms"></span>`);
|
||||
if (state.timing_map_leds_ms != null) segs.push(`<span class="timing-seg timing-map" style="flex:${state.timing_map_leds_ms || 0.1}" title="map ${state.timing_map_leds_ms}ms"></span>`);
|
||||
if (state.timing_smooth_ms != null) segs.push(`<span class="timing-seg timing-smooth" style="flex:${state.timing_smooth_ms || 0.1}" title="smooth ${state.timing_smooth_ms}ms"></span>`);
|
||||
segs.push(send);
|
||||
return segs.join('');
|
||||
}
|
||||
|
||||
function _patchTargetMetrics(target: any) {
|
||||
@@ -942,8 +932,11 @@ function _patchTargetMetrics(target: any) {
|
||||
}
|
||||
}
|
||||
|
||||
const timing = card.querySelector('[data-tm="timing"]') as HTMLElement | null;
|
||||
if (timing && state.timing_total_ms != null) timing.innerHTML = _buildLedTimingHTML(state);
|
||||
const timingBar = card.querySelector('[data-tm="timing"]') as HTMLElement | null;
|
||||
if (timingBar && state.timing_total_ms != null) timingBar.innerHTML = _buildLedTimingSegments(state);
|
||||
|
||||
const timingTotal = card.querySelector('[data-tm="timing-total"]') as HTMLElement | null;
|
||||
if (timingTotal && state.timing_total_ms != null) timingTotal.textContent = String(state.timing_total_ms);
|
||||
|
||||
const frames = card.querySelector('[data-tm="frames"]') as HTMLElement | null;
|
||||
if (frames) { frames.textContent = formatCompact(metrics.frames_processed || 0); frames.title = String(metrics.frames_processed || 0); }
|
||||
@@ -1102,25 +1095,26 @@ export function createTargetCard(target: LedOutputTarget & { state?: any; metric
|
||||
});
|
||||
}
|
||||
|
||||
// ── Live-update raw HTML: collapsible pipeline detail (frames /
|
||||
// keepalive / timing breakdown). The FPS sparkline now lives
|
||||
// inside the FPS metric cell, not in a separate row. ──
|
||||
// ── Pipeline strip — a single segmented timing bar plus one chip
|
||||
// row underneath. Replaces the legacy collapsible block: the
|
||||
// bar is the headline diagnostic, the chips carry the rest. ──
|
||||
const showKeepalive = state.needs_keepalive !== false;
|
||||
const extraHtml = isProcessing ? `
|
||||
<div class="target-metrics-collapse">
|
||||
<button type="button" class="target-metrics-toggle" onclick="this.parentElement.classList.toggle('open')">${escapeHtml(t('targets.metrics.pipeline'))}</button>
|
||||
<div class="target-metrics-animate">
|
||||
<div class="metrics-grid target-metrics-expanded">
|
||||
<div class="timing-breakdown" data-tm="timing" style="grid-column:1/-1"></div>
|
||||
<div class="metric">
|
||||
<div class="metric-label">${escapeHtml(t('device.metrics.frames'))}</div>
|
||||
<div class="metric-value" data-tm="frames">---</div>
|
||||
</div>
|
||||
${state.needs_keepalive !== false ? `
|
||||
<div class="metric">
|
||||
<div class="metric-label">${escapeHtml(t('device.metrics.keepalive'))}</div>
|
||||
<div class="metric-value" data-tm="keepalive">---</div>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
<div class="target-pipeline" title="${escapeHtml(t('device.metrics.timing'))}">
|
||||
<div class="timing-bar" data-tm="timing"></div>
|
||||
<div class="target-pipeline-chips">
|
||||
<span class="chip chip--inline" title="${escapeHtml(t('device.metrics.timing'))}">
|
||||
${ICON_CLOCK}<span data-tm="timing-total">--</span><small>ms</small>
|
||||
</span>
|
||||
<span class="chip chip--inline" title="${escapeHtml(t('device.metrics.frames'))}">
|
||||
<span data-tm="frames">--</span>
|
||||
<small>${escapeHtml(t('device.metrics.frames'))}</small>
|
||||
</span>
|
||||
${showKeepalive ? `
|
||||
<span class="chip chip--inline" title="${escapeHtml(t('device.metrics.keepalive'))}">
|
||||
<span data-tm="keepalive">--</span>
|
||||
<small>${escapeHtml(t('device.metrics.keepalive'))}</small>
|
||||
</span>` : ''}
|
||||
</div>
|
||||
</div>` : '';
|
||||
|
||||
|
||||
@@ -702,7 +702,6 @@
|
||||
"targets.source": "Source:",
|
||||
"targets.source.hint": "Which picture source to capture and process",
|
||||
"targets.source.none": "-- No source assigned --",
|
||||
"targets.metrics.pipeline": "Pipeline details",
|
||||
"targets.fps": "Target FPS:",
|
||||
"targets.fps.hint": "Target frames per second for capture and LED updates (1-90)",
|
||||
"targets.fps.rec": "Hardware max ≈ {fps} fps ({leds} LEDs)",
|
||||
|
||||
@@ -686,7 +686,6 @@
|
||||
"targets.source": "Источник:",
|
||||
"targets.source.hint": "Какой источник изображения захватывать и обрабатывать",
|
||||
"targets.source.none": "-- Источник не назначен --",
|
||||
"targets.metrics.pipeline": "Детали конвейера",
|
||||
"targets.fps": "Целевой FPS:",
|
||||
"targets.fps.hint": "Целевая частота кадров для захвата и обновления LED (1-90)",
|
||||
"targets.fps.rec": "Макс. аппаратный ≈ {fps} fps ({leds} LED)",
|
||||
|
||||
@@ -686,7 +686,6 @@
|
||||
"targets.source": "源:",
|
||||
"targets.source.hint": "要采集和处理的图片源",
|
||||
"targets.source.none": "-- 未分配源 --",
|
||||
"targets.metrics.pipeline": "管线详情",
|
||||
"targets.fps": "目标 FPS:",
|
||||
"targets.fps.hint": "采集和 LED 更新的目标帧率(1-90)",
|
||||
"targets.fps.rec": "硬件最大 ≈ {fps} fps({leds} 个 LED)",
|
||||
|
||||
Reference in New Issue
Block a user