feat(ui): align Targets metric cells with dashboard pattern
mod-card.ts - ModMetricOpts.extra: raw HTML appended after the .v cell — used to embed a sparkline canvas inside the FPS metric block - ModMetricOpts.valueDataAttrs: data-attrs on the .v element so live-update patchers can target the value directly LED target card - FPS sparkline (mod-metric-spark-canvas) is embedded INSIDE the FPS cell as a sibling of .v — was a separate target-fps-row block before, which floated under the metrics grid - Label hardcoded to "FPS" (the i18n value "Target FPS:" was meant for the editor field, not the readout) - Uptime cell gets ICON_CLOCK; Errors cell gets ICON_OK / ICON_WARNING based on count — matches dashboard cell decorations - Drops the leading FPS icon (display-font number is the focal element; no icon needed) - _patchTargetMetrics now emits the dashboard FPS shape: current<span.dashboard-fps-target>/target</span> <span.dashboard-fps-avg>avg N.N</span> — picks up dashboard.css styling for free HA Light target card - Same icon treatment (Uptime → clock; HA → ok/warning by ha_connected); FPS icon dropped Grid sizing - .devices-grid bumped from minmax(300px, 1fr) / gap 20px to minmax(min(380px, 100%), 1fr) / gap 14px — matches the dashboard's section grid so metric values like "1m 43s" stop truncating at the typical desktop width
This commit is contained in:
@@ -89,8 +89,8 @@ section {
|
||||
.displays-grid,
|
||||
.devices-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(380px, 100%), 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.devices-grid > .loading,
|
||||
|
||||
@@ -47,6 +47,13 @@ export interface ModMetricOpts {
|
||||
* secondary line (e.g. LED chip + RGB/RGBW). The default display
|
||||
* font is too coarse for string identifiers. */
|
||||
variant?: 'text-stack';
|
||||
/** Extra raw HTML appended after the .v element inside the cell —
|
||||
* used to embed a sparkline canvas (`.mod-metric-spark-canvas`)
|
||||
* into the FPS metric, mirroring the dashboard pattern. */
|
||||
extra?: string;
|
||||
/** Extra data attributes on the .v element (e.g. data-fps-text for
|
||||
* cached lookup during live updates). */
|
||||
valueDataAttrs?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ModChipOpts {
|
||||
@@ -281,8 +288,12 @@ export function renderModMetrics(metrics: ModMetricOpts[]): string {
|
||||
const titleAttr = m.title ? ` title="${escapeHtml(m.title)}"` : '';
|
||||
const vCls = ['v', m.accent ? 'signal' : '', m.error ? 'has-errors' : ''].filter(Boolean).join(' ');
|
||||
const vIdAttr = m.valueId ? ` id="${m.valueId}"` : '';
|
||||
const vDataAttrs = m.valueDataAttrs
|
||||
? ' ' + Object.entries(m.valueDataAttrs).map(([k, v]) => `${k}="${escapeHtml(v)}"`).join(' ')
|
||||
: '';
|
||||
const labelHtml = `<span class="k">${m.icon || ''} <span>${escapeHtml(m.k)}</span></span>`;
|
||||
return `<div class="${cellCls}"${titleAttr}>${labelHtml}<span class="${vCls}"${vIdAttr}>${m.v}</span></div>`;
|
||||
const extraHtml = m.extra || '';
|
||||
return `<div class="${cellCls}"${titleAttr}>${labelHtml}<span class="${vCls}"${vIdAttr}${vDataAttrs}>${m.v}</span>${extraHtml}</div>`;
|
||||
}).join('');
|
||||
return `<div class="${cellsClass}">${cells}</div>`;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { logError } from '../core/log.ts';
|
||||
import { t } from '../core/i18n.ts';
|
||||
import { Modal } from '../core/modal.ts';
|
||||
import { showToast, showConfirm, formatUptime } from '../core/ui.ts';
|
||||
import { ICON_EDIT, ICON_START, ICON_STOP, ICON_TRASH, ICON_FPS, ICON_OK, ICON_WARNING, ICON_SUN, getColorStripIcon, getValueSourceIcon, getHAEntityIcon } from '../core/icons.ts';
|
||||
import { ICON_EDIT, ICON_START, ICON_STOP, ICON_TRASH, ICON_OK, ICON_WARNING, ICON_CLOCK, ICON_SUN, getColorStripIcon, getValueSourceIcon, getHAEntityIcon } from '../core/icons.ts';
|
||||
import * as P from '../core/icon-paths.ts';
|
||||
import { EntitySelect } from '../core/entity-palette.ts';
|
||||
import { wrapCard } from '../core/card-colors.ts';
|
||||
@@ -560,19 +560,20 @@ export function createHALightTargetCard(target: any, haSourceMap: Record<string,
|
||||
const metrics: ModMetricOpts[] = [];
|
||||
if (isRunning) {
|
||||
metrics.push({
|
||||
k: t('targets.fps') || 'RATE',
|
||||
icon: ICON_FPS,
|
||||
k: 'FPS',
|
||||
v: '<span data-tm="fps">---</span>',
|
||||
accent: true,
|
||||
title: t('targets.fps'),
|
||||
});
|
||||
metrics.push({
|
||||
k: t('device.metrics.uptime') || 'UPTIME',
|
||||
k: t('device.metrics.uptime') || 'Uptime',
|
||||
icon: ICON_CLOCK,
|
||||
v: '<span data-tm="uptime">---</span>',
|
||||
title: t('device.metrics.uptime'),
|
||||
});
|
||||
metrics.push({
|
||||
k: 'HA',
|
||||
icon: state.ha_connected ? ICON_OK : ICON_WARNING,
|
||||
v: '<span data-tm="ha-status">---</span>',
|
||||
title: 'Home Assistant connection',
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ import { createHALightTargetCard, initHALightTargetDelegation, patchHALightTarge
|
||||
import {
|
||||
getValueSourceIcon, getTargetTypeIcon, getDeviceTypeIcon, getColorStripIcon,
|
||||
ICON_EDIT, ICON_START, ICON_STOP,
|
||||
ICON_FPS, ICON_LED_PREVIEW,
|
||||
ICON_LED_PREVIEW, ICON_CLOCK, ICON_OK,
|
||||
ICON_GLOBE, ICON_RADIO, ICON_PLUG, ICON_FILM, ICON_SUN, ICON_SUN_DIM, ICON_TARGET_ICON, ICON_HELP,
|
||||
ICON_WARNING, ICON_PALETTE, ICON_WRENCH, ICON_TEMPLATE, ICON_TRASH,
|
||||
} from '../core/icons.ts';
|
||||
@@ -919,11 +919,14 @@ function _patchTargetMetrics(target: any) {
|
||||
if (fps) {
|
||||
const effFps = state.fps_effective;
|
||||
const tgtFps = state.fps_target || 0;
|
||||
const fpsLabel = (effFps != null && effFps < tgtFps)
|
||||
? `${state.fps_current ?? 0}<small>/${effFps}↓${tgtFps}</small>`
|
||||
: `${state.fps_current ?? 0}<small>/${tgtFps}</small>`;
|
||||
const fpsCurrent = state.fps_current ?? 0;
|
||||
const fpsActual = state.fps_actual?.toFixed(1) ?? '0.0';
|
||||
const fpsTargetLabel = (effFps != null && effFps < tgtFps)
|
||||
? `${fpsCurrent}<span class="dashboard-fps-target">/${effFps}↓${tgtFps}</span>`
|
||||
: `${fpsCurrent}<span class="dashboard-fps-target">/${tgtFps}</span>`;
|
||||
const unreachableClass = state.device_streaming_reachable === false ? ' fps-unreachable' : '';
|
||||
fps.innerHTML = `<span class="${unreachableClass}">${fpsLabel}</span>`;
|
||||
fps.innerHTML = `<span class="${unreachableClass}">${fpsTargetLabel}</span>`
|
||||
+ `<span class="dashboard-fps-avg">avg ${fpsActual}</span>`;
|
||||
}
|
||||
|
||||
// Update health dot to reflect streaming reachability when processing
|
||||
@@ -1068,39 +1071,41 @@ export function createTargetCard(target: LedOutputTarget & { state?: any; metric
|
||||
}
|
||||
|
||||
// ── Headline metric strip — shown when running so live FPS / errors
|
||||
// pop in the instrument-readout style. The full pipeline detail
|
||||
// sits below in the collapsible block. ──
|
||||
// pop in the instrument-readout style. FPS cell embeds a
|
||||
// sparkline canvas (mod-metric-spark-canvas) underneath the
|
||||
// big numeric, mirroring the dashboard pattern. The full
|
||||
// pipeline detail sits below in the collapsible block. ──
|
||||
const metrics: ModMetricOpts[] = [];
|
||||
if (isProcessing) {
|
||||
const fpsTargetVal = state.fps_target || target.fps || 30;
|
||||
metrics.push({
|
||||
k: t('targets.fps') || 'FPS',
|
||||
icon: ICON_FPS,
|
||||
v: '<span data-tm="fps">---</span>',
|
||||
k: 'FPS',
|
||||
v: '---',
|
||||
accent: true,
|
||||
title: t('targets.fps'),
|
||||
});
|
||||
const errCount = tgtMetrics.errors_count || 0;
|
||||
metrics.push({
|
||||
k: t('device.metrics.errors') || 'ERR',
|
||||
v: '<span data-tm="errors">---</span>',
|
||||
error: errCount > 0,
|
||||
icon: errCount > 0 ? ICON_WARNING : '',
|
||||
title: t('device.metrics.errors'),
|
||||
valueDataAttrs: { 'data-tm': 'fps' },
|
||||
extra: `<canvas class="mod-metric-spark-canvas" id="target-fps-${target.id}" data-fps-target="${fpsTargetVal}"></canvas>`,
|
||||
});
|
||||
metrics.push({
|
||||
k: t('device.metrics.uptime') || 'UPTIME',
|
||||
k: t('device.metrics.uptime') || 'Uptime',
|
||||
icon: ICON_CLOCK,
|
||||
v: '<span data-tm="uptime">---</span>',
|
||||
title: t('device.metrics.uptime'),
|
||||
});
|
||||
const errCount = tgtMetrics.errors_count || 0;
|
||||
metrics.push({
|
||||
k: t('device.metrics.errors') || 'Errors',
|
||||
icon: errCount > 0 ? ICON_WARNING : ICON_OK,
|
||||
v: '<span data-tm="errors">---</span>',
|
||||
error: errCount > 0,
|
||||
title: t('device.metrics.errors'),
|
||||
});
|
||||
}
|
||||
|
||||
// ── Live-update raw HTML: FPS sparkline + collapsible pipeline ──
|
||||
// ── 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. ──
|
||||
const extraHtml = isProcessing ? `
|
||||
<div class="target-fps-row">
|
||||
<div class="target-fps-sparkline">
|
||||
<canvas id="target-fps-${target.id}" data-fps-target="${state.fps_target || 30}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user