refactor(web-ui): extract shared component primitives

Introduce components.js with barTrack, barRow, barRankList, metricPill,
metricStrip, and chartHeader helpers. Migrate dashboard.js and usage.js
to use these primitives, replacing 13 families of duplicated CSS
(stat-list, fw-bar, token-bar, metric-pill, chart-insight, chart-header,
usage-chart-total, etc.) with a unified .am-* namespace. Net: -256 lines.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-05-22 12:21:48 -07:00
parent 8753c0c9d5
commit c44e7fe72e
4 changed files with 384 additions and 538 deletions
+102
View File
@@ -0,0 +1,102 @@
// ── components.js — shared UI primitives ─────────────────
//
// One-stop helpers for the repeating bar / pill / chart-header
// patterns across pages. Renderers return HTML strings — callers
// inject via innerHTML and wire events on the resulting DOM.
import { escapeHTML } from './utils.js';
// ── Bar primitives ───────────────────────────────────────
// barTrack — just the percentage bar (no label/count head).
// value: numeric value to display
// max: ceiling for percentage; 0/falsy means 0%
// fwClass: optional framework slug (openclaw, claude-code, hermes, …)
// — applied as fw-<slug> on the fill for color
// size: 'xs' | 'sm' | 'md' | 'lg' (default 'md')
// modifier: extra class on the fill (e.g. 'model', 'input', 'output')
export function barTrack({ value = 0, max = 0, fwClass = '', size = 'md', modifier = '' } = {}) {
const pct = max > 0 ? Math.min(100, (value / max) * 100).toFixed(1) : '0';
const fillClasses = ['am-bar-fill'];
if (fwClass) fillClasses.push('fw-' + fwClass);
if (modifier) fillClasses.push(modifier);
return `<div class="am-bar-track am-bar-track--${escapeHTML(size)}"><div class="${fillClasses.join(' ')}" style="width:${pct}%"></div></div>`;
}
// barRow — head (name + count) plus track.
// countDisplay overrides the rendered count text (e.g. "238" vs "238 events").
export function barRow({ name = '', count = 0, countDisplay, max = 0, fwClass = '', size = 'sm', modifier = '' } = {}) {
const display = countDisplay != null ? countDisplay : count;
return `
<div class="am-bar-row">
<div class="am-bar-row-head">
<span class="am-bar-row-name">${escapeHTML(String(name))}</span>
<span class="am-bar-row-count">${escapeHTML(String(display))}</span>
</div>
${barTrack({ value: count, max, fwClass, size, modifier })}
</div>`;
}
// barRankList — ranked list of barRows wrapped in <ul class="am-bar-list">.
// items: array of arbitrary objects
// mapItem: fn(item) → { name, count, countDisplay?, fwClass?, modifier? }
// maxOverride: ceiling override; default = max of items' counts
// size: bar height (default 'xs' for ranked lists)
// emptyText: shown when items is empty
export function barRankList(items, { mapItem, maxOverride, size = 'xs', emptyText = 'No data' } = {}) {
if (!items || items.length === 0) {
return `<p class="empty-state" style="padding:0.5rem 0;font-size:0.8rem">${escapeHTML(emptyText)}</p>`;
}
const mapped = items.map(item => (mapItem ? mapItem(item) : { name: item.name, count: item.count }));
const max = maxOverride != null ? maxOverride : Math.max(1, ...mapped.map(m => Number(m.count) || 0));
const rows = mapped.map(m => `<li>${barRow({ ...m, max, size })}</li>`).join('');
return `<ul class="am-bar-list">${rows}</ul>`;
}
// ── Pill primitives ──────────────────────────────────────
// metricPill — small label + bold value, optional meta line.
// value: pre-escaped HTML when valueHTML=true, else escaped as text
// valueId: id attribute (for animateCounter targets)
// variant: 'insight' | 'total' | 'range' | '' (default — standalone metric pill)
// alert: adds .alert to the pill (turns value red)
export function metricPill({ label, value = '-', valueId = '', valueHTML = false, meta = '', variant = '', alert = false } = {}) {
const classes = ['am-pill'];
if (variant) classes.push('am-pill--' + variant);
if (alert) classes.push('alert');
const idAttr = valueId ? ` id="${escapeHTML(valueId)}"` : '';
const rendered = valueHTML ? value : escapeHTML(String(value));
const metaLine = meta ? `<span class="am-pill-meta">${escapeHTML(String(meta))}</span>` : '';
return `
<div class="${classes.join(' ')}">
<span class="am-pill-label">${escapeHTML(String(label))}</span>
<strong class="am-pill-value"${idAttr}>${rendered}</strong>
${metaLine}
</div>`;
}
// metricStrip — flex/grid container around a list of pill specs.
// variant: '' | 'insights' (4-up grid) | 'totals' (tight inline pills)
export function metricStrip(pills, { variant = '', className = '' } = {}) {
const classes = ['am-pill-strip'];
if (variant) classes.push('am-pill-strip--' + variant);
if (className) classes.push(className);
return `<div class="${classes.join(' ')}">${pills.map(p => metricPill(p)).join('')}</div>`;
}
// ── Chart header ─────────────────────────────────────────
// chartHeader — title + optional subtitle on the left, raw controls HTML on the right.
// controls is passed through unchanged (legend/buttons/tabs vary by chart).
export function chartHeader({ title = '', subtitle = '', controls = '' } = {}) {
const subtitleHTML = subtitle ? `<span class="am-chart-subtitle">${escapeHTML(subtitle)}</span>` : '';
const controlsHTML = controls ? `<div class="am-chart-controls">${controls}</div>` : '';
return `
<div class="am-chart-header">
<div class="am-chart-title-group">
<span class="am-chart-title">${escapeHTML(title)}</span>
${subtitleHTML}
</div>
${controlsHTML}
</div>`;
}
+63 -145
View File
@@ -39,6 +39,13 @@ import {
import { clearErrorBadge } from '../palette.js';
import { app, navigate, isRouteCurrent } from '../router.js';
import { api } from '../api.js';
import {
barRankList,
barRow,
metricPill,
metricStrip,
chartHeader,
} from '../components.js';
// uPlot is loaded as a global IIFE; access via window.uPlot
/* global uPlot */
@@ -182,7 +189,8 @@ function renderSummaryCards() {
const totalOps = (s.runs_today || 0) + (s.tool_calls_today || 0);
const rate = totalOps > 0 ? ((s.errors_today || 0) / totalOps * 100) : 0;
animateCounter('dash-error-rate', rate.toFixed(1) + '%');
errorRateEl.classList.toggle('alert', rate > 5);
const pill = errorRateEl.closest('.am-pill');
if (pill) pill.classList.toggle('alert', rate > 5);
}
if (document.getElementById('dash-cost-per-run')) {
@@ -287,12 +295,12 @@ function renderDashboardChartInsights() {
}
const peakBucket = dashboardState.timeseries.series[stats.peakIndex];
container.innerHTML = `
<div class="chart-insight-pill"><span class="chart-insight-label">window total</span><strong>${escapeHTML(formatCount(stats.totalEvents))}</strong></div>
<div class="chart-insight-pill"><span class="chart-insight-label">peak bucket</span><strong>${escapeHTML(formatCount(stats.peakTotal))}</strong><span class="chart-insight-meta">${escapeHTML(formatBucketLabel(peakBucket.ts))}</span></div>
<div class="chart-insight-pill"><span class="chart-insight-label">mix</span><strong>${escapeHTML(formatCount(stats.totalRuns))}r / ${escapeHTML(formatCount(stats.totalTools))}t / ${escapeHTML(formatCount(stats.totalErrors))}e</strong></div>
<div class="chart-insight-pill"><span class="chart-insight-label">bucket</span><strong>${escapeHTML(dashboardState.timeseries.bucket || '-')}</strong><span class="chart-insight-meta">${escapeHTML(String(stats.bucketCount))} points</span></div>
`;
container.innerHTML = metricStrip([
{ label: 'window total', value: formatCount(stats.totalEvents), variant: 'insight' },
{ label: 'peak bucket', value: formatCount(stats.peakTotal), meta: formatBucketLabel(peakBucket.ts), variant: 'insight' },
{ label: 'mix', value: `${formatCount(stats.totalRuns)}r / ${formatCount(stats.totalTools)}t / ${formatCount(stats.totalErrors)}e`, variant: 'insight' },
{ label: 'bucket', value: dashboardState.timeseries.bucket || '-', meta: stats.bucketCount + ' points', variant: 'insight' },
], { variant: 'insights' });
}
function renderDashboardChartHover(idx) {
@@ -589,23 +597,11 @@ function renderTokenPanel() {
<div class="token-stat-value">${escapeHTML(formatTokenCount(totalTokens))}</div>
</div>
<div class="token-io-bars">
<div class="token-bar-row">
<span class="token-bar-label">Input</span>
<div class="token-bar-track">
<div class="token-bar-fill input" style="width:${(inputTokens / maxIO * 100).toFixed(1)}%"></div>
</div>
<span class="token-bar-count">${escapeHTML(formatTokenCount(inputTokens))}</span>
</div>
<div class="token-bar-row">
<span class="token-bar-label">Output</span>
<div class="token-bar-track">
<div class="token-bar-fill output" style="width:${(outputTokens / maxIO * 100).toFixed(1)}%"></div>
</div>
<span class="token-bar-count">${escapeHTML(formatTokenCount(outputTokens))}</span>
</div>
${barRow({ name: 'Input', count: inputTokens, countDisplay: formatTokenCount(inputTokens), max: maxIO, modifier: 'input', size: 'md' })}
${barRow({ name: 'Output', count: outputTokens, countDisplay: formatTokenCount(outputTokens), max: maxIO, modifier: 'output', size: 'md' })}
</div>
<div class="token-cost-display">
<span class="token-bar-label">Est. cost today</span>
<span class="am-pill-label">Est. cost today</span>
<strong>${escapeHTML(totalCost ? formatCost(totalCost) : '$0.0000')}</strong>
</div>
</div>
@@ -636,18 +632,9 @@ function renderLatencyPanel() {
container.innerHTML = `
<div class="latency-panel">
<div class="latency-range">
<div class="latency-range-item">
<span class="latency-range-label">Min</span>
<span class="latency-range-val">${escapeHTML(formatDuration(min))}</span>
</div>
<div class="latency-range-item">
<span class="latency-range-label">Avg</span>
<span class="latency-range-val">${escapeHTML(formatDuration(avg))}</span>
</div>
<div class="latency-range-item">
<span class="latency-range-label">Max</span>
<span class="latency-range-val">${escapeHTML(formatDuration(max))}</span>
</div>
${metricPill({ label: 'Min', value: formatDuration(min), variant: 'range' })}
${metricPill({ label: 'Avg', value: formatDuration(avg), variant: 'range' })}
${metricPill({ label: 'Max', value: formatDuration(max), variant: 'range' })}
</div>
<div class="latency-mini-bars">
${durSeries.map((v, i) => {
@@ -657,7 +644,7 @@ function renderLatencyPanel() {
return `<div class="latency-mini-bar" style="height:${pct}%" title="${escapeHTML(title)}"></div>`;
}).join('')}
</div>
<div class="latency-range-label" style="margin-top:0.5rem;font-size:0.7rem;color:var(--text-dim)">Avg run duration per bucket (${escapeHTML(ts.bucket || '-')})</div>
<div class="am-pill-label" style="margin-top:0.5rem">Avg run duration per bucket (${escapeHTML(ts.bucket || '-')})</div>
</div>
`;
}
@@ -680,21 +667,17 @@ function renderFrameworkBars() {
const maxTotal = Math.max(...entries.map(([, s]) => s.runs + s.tools + s.errors));
container.innerHTML = '<div class="fw-bars">' + entries.map(([name, stats]) => {
container.innerHTML = '<div style="display:flex;flex-direction:column;gap:0.75rem;margin-top:0.25rem">' + entries.map(([name, stats]) => {
const total = stats.runs + stats.tools + stats.errors;
const pct = maxTotal > 0 ? (total / maxTotal * 100) : 0;
const cssClass = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
return `
<div class="fw-bar-row">
<div class="fw-bar-label">
<span class="fw-bar-name">${escapeHTML(name)}</span>
<span class="fw-bar-count">${total} events</span>
</div>
<div class="fw-bar-track">
<div class="fw-bar-fill ${escapeHTML(cssClass)}" style="width:${pct}%"></div>
</div>
</div>
`;
return barRow({
name,
count: total,
countDisplay: total + ' events',
max: maxTotal,
fwClass: cssClass,
size: 'lg',
});
}).join('') + '</div>';
}
@@ -763,61 +746,21 @@ function renderDashFeed() {
function renderDashTopTools() {
const list = document.getElementById('dash-top-tools');
if (!list) return;
const topTools = Object.entries(dashboardState.toolCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
if (topTools.length === 0) {
list.innerHTML = '<li style="color:var(--text-dim);font-size:0.8rem">No tool data yet</li>';
return;
}
const maxCount = topTools[0]?.[1] || 1;
list.innerHTML = topTools.map(([name, count]) => {
const pct = (count / maxCount * 100).toFixed(1);
return `
<li>
<div class="stat-list-header">
<span class="stat-list-name">${escapeHTML(name)}</span>
<span class="stat-list-count">${count}</span>
</div>
<div class="stat-list-bar-track">
<div class="stat-list-bar-fill" style="width:${pct}%"></div>
</div>
</li>
`;
}).join('');
.slice(0, 10)
.map(([name, count]) => ({ name, count }));
list.innerHTML = barRankList(topTools, { emptyText: 'No tool data yet' });
}
function renderDashTopModels() {
const list = document.getElementById('dash-top-models');
if (!list) return;
const topModels = Object.entries(dashboardState.modelCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
if (topModels.length === 0) {
list.innerHTML = '<li style="color:var(--text-dim);font-size:0.8rem">No model data yet</li>';
return;
}
const maxCount = topModels[0]?.[1] || 1;
list.innerHTML = topModels.map(([name, count]) => {
const pct = (count / maxCount * 100).toFixed(1);
return `
<li>
<div class="stat-list-header">
<span class="stat-list-name">${escapeHTML(name)}</span>
<span class="stat-list-count">${count}</span>
</div>
<div class="stat-list-bar-track">
<div class="stat-list-bar-fill model" style="width:${pct}%"></div>
</div>
</li>
`;
}).join('');
.slice(0, 10)
.map(([name, count]) => ({ name, count, modifier: 'model' }));
list.innerHTML = barRankList(topModels, { emptyText: 'No model data yet' });
}
// ── Exports ──────────────────────────────────────────────
@@ -863,38 +806,21 @@ export async function renderDashboard(routeToken) {
<div class="summary-card-sub" id="dash-errors-sub">&nbsp;</div>
</div>
</div>
<div class="metrics-strip">
<div class="metric-pill">
<span class="metric-pill-label">Tokens today</span>
<span class="metric-pill-value" id="dash-tokens-today">-</span>
</div>
<div class="metric-pill">
<span class="metric-pill-label">Cost today</span>
<span class="metric-pill-value" id="dash-cost-today">-</span>
</div>
<div class="metric-pill">
<span class="metric-pill-label">Avg run duration</span>
<span class="metric-pill-value" id="dash-avg-duration">-</span>
</div>
<div class="metric-pill">
<span class="metric-pill-label">Error rate</span>
<span class="metric-pill-value metric-pill-alert" id="dash-error-rate">-</span>
</div>
<div class="metric-pill">
<span class="metric-pill-label">Cost / run</span>
<span class="metric-pill-value" id="dash-cost-per-run">-</span>
</div>
</div>
<div style="margin-bottom:1.25rem">${metricStrip([
{ label: 'Tokens today', valueId: 'dash-tokens-today' },
{ label: 'Cost today', valueId: 'dash-cost-today' },
{ label: 'Avg run duration', valueId: 'dash-avg-duration' },
{ label: 'Error rate', valueId: 'dash-error-rate' },
{ label: 'Cost / run', valueId: 'dash-cost-per-run' },
])}</div>
<div class="section-title" style="margin-bottom:0.75rem">Infrastructure</div>
<div class="vm-strip" id="dash-vm-strip"></div>
<div class="charts-row">
<div class="chart-panel">
<div class="chart-header">
<div class="chart-title-group">
<span class="chart-title">Event Rate</span>
<span class="chart-subtitle">Runs, tool spans, and errors over time</span>
</div>
<div class="chart-header-controls">
${chartHeader({
title: 'Event Rate',
subtitle: 'Runs, tool spans, and errors over time',
controls: `
<div class="chart-legend">
<span class="chart-legend-item"><span class="chart-legend-dot total"></span>total</span>
<span class="chart-legend-item"><span class="chart-legend-dot" style="background:#34d399"></span>runs</span>
@@ -910,21 +836,21 @@ export async function renderDashboard(routeToken) {
<button class="window-btn" data-w="6h">6h</button>
<button class="window-btn" data-w="24h">24h</button>
<button class="window-btn" data-w="7d">7d</button>
</div>
</div>
</div>
<div class="chart-insights" id="dash-chart-insights"></div>
</div>`,
})}
<div id="dash-chart-insights" style="margin-bottom:0.9rem"></div>
<div class="chart-container" id="dash-chart"></div>
<div class="chart-hover-panel" id="dash-chart-hover"></div>
</div>
<div class="chart-panel right-panel">
<div class="chart-header">
<div class="right-panel-tabs" id="dash-right-tabs">
<button class="right-panel-tab ${dashboardState.rightPanelMode === 'framework' ? 'active' : ''}" data-panel="framework">Framework</button>
<button class="right-panel-tab ${dashboardState.rightPanelMode === 'tokens' ? 'active' : ''}" data-panel="tokens">Tokens</button>
<button class="right-panel-tab ${dashboardState.rightPanelMode === 'latency' ? 'active' : ''}" data-panel="latency">Latency</button>
</div>
</div>
${chartHeader({
controls: `
<div class="right-panel-tabs" id="dash-right-tabs">
<button class="right-panel-tab ${dashboardState.rightPanelMode === 'framework' ? 'active' : ''}" data-panel="framework">Framework</button>
<button class="right-panel-tab ${dashboardState.rightPanelMode === 'tokens' ? 'active' : ''}" data-panel="tokens">Tokens</button>
<button class="right-panel-tab ${dashboardState.rightPanelMode === 'latency' ? 'active' : ''}" data-panel="latency">Latency</button>
</div>`,
})}
<div class="right-panel-body" id="dash-right-panel">
<p class="empty-state" style="padding:1rem">Loading...</p>
</div>
@@ -932,28 +858,20 @@ export async function renderDashboard(routeToken) {
</div>
<div class="bottom-panels">
<div class="feed-panel">
<div class="chart-header">
<span class="chart-title">Recent Activity</span>
</div>
${chartHeader({ title: 'Recent Activity' })}
<div class="timeline" id="dash-feed">
<p class="empty-state" style="padding:1rem">Loading...</p>
</div>
</div>
<div class="tools-panel">
<div class="chart-header">
<span class="chart-title">Top Usage</span>
</div>
${chartHeader({ title: 'Top Usage' })}
<div class="usage-rank-group">
<div class="usage-rank-header">Tools</div>
<ul class="stat-list" id="dash-top-tools">
<li style="color:var(--text-dim);font-size:0.8rem">Loading...</li>
</ul>
<div id="dash-top-tools"><p class="empty-state" style="padding:0.5rem 0;font-size:0.8rem">Loading...</p></div>
</div>
<div class="usage-rank-group">
<div class="usage-rank-header">Models</div>
<ul class="stat-list" id="dash-top-models">
<li style="color:var(--text-dim);font-size:0.8rem">Loading...</li>
</ul>
<div id="dash-top-models"><p class="empty-state" style="padding:0.5rem 0;font-size:0.8rem">Loading...</p></div>
</div>
</div>
</div>
+28 -64
View File
@@ -1,6 +1,7 @@
import { app, isRouteCurrent } from '../router.js';
import { api } from '../api.js';
import { escapeHTML, formatTokenCount, formatCost } from '../utils.js';
import { barTrack, barRankList, metricStrip, chartHeader } from '../components.js';
/* global uPlot */
@@ -104,7 +105,6 @@ function renderFrameworkBreakdown(byFw) {
el.innerHTML = entries.map(([name, stats]) => {
const total = stats.runs + stats.tools + stats.errors;
const pct = (total / maxTotal * 100).toFixed(1);
const cssClass = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
const active = (stats.active_sessions || 0) > 0;
return `
@@ -119,9 +119,7 @@ function renderFrameworkBreakdown(byFw) {
<span class="usage-fw-stat"><span class="usage-fw-stat-label">tools</span>${stats.tools || 0}</span>
${(stats.errors || 0) > 0 ? `<span class="usage-fw-stat error"><span class="usage-fw-stat-label">err</span>${stats.errors}</span>` : ''}
</div>
<div class="usage-fw-bar-track">
<div class="usage-fw-bar-fill ${escapeHTML(cssClass)}" style="width:${pct}%"></div>
</div>
${barTrack({ value: total, max: maxTotal, fwClass: cssClass, size: 'sm' })}
</div>`;
}).join('');
}
@@ -194,36 +192,22 @@ export async function renderUsage(routeToken) {
<div class="usage-section-row">
<div class="usage-panel usage-chart-panel">
<div class="usage-chart-header">
<span class="section-title" style="margin:0">7-Day Trend</span>
<div class="usage-chart-tabs" id="usage-chart-tabs">
<button class="usage-chart-tab active" data-mode="activity">Activity</button>
<button class="usage-chart-tab" data-mode="tokens">Tokens</button>
<button class="usage-chart-tab" data-mode="cost">Cost</button>
</div>
</div>
<div class="usage-chart-totals">
<span class="usage-chart-total-pill">
<span class="usage-chart-total-label">runs</span>
<strong>${t.runs}</strong>
</span>
<span class="usage-chart-total-pill">
<span class="usage-chart-total-label">tools</span>
<strong>${t.tools}</strong>
</span>
<span class="usage-chart-total-pill">
<span class="usage-chart-total-label">errors</span>
<strong class="${t.errors > 0 ? 'usage-total-errors' : ''}">${t.errors}</strong>
</span>
<span class="usage-chart-total-pill">
<span class="usage-chart-total-label">tokens</span>
<strong>${formatTokenCount(t.tokens)}</strong>
</span>
<span class="usage-chart-total-pill">
<span class="usage-chart-total-label">cost</span>
<strong>${formatCost(t.cost)}</strong>
</span>
</div>
${chartHeader({
title: '7-Day Trend',
controls: `
<div class="usage-chart-tabs" id="usage-chart-tabs">
<button class="usage-chart-tab active" data-mode="activity">Activity</button>
<button class="usage-chart-tab" data-mode="tokens">Tokens</button>
<button class="usage-chart-tab" data-mode="cost">Cost</button>
</div>`,
})}
${metricStrip([
{ label: 'runs', value: String(t.runs), variant: 'total' },
{ label: 'tools', value: String(t.tools), variant: 'total' },
{ label: 'errors', value: String(t.errors), variant: 'total', alert: t.errors > 0 },
{ label: 'tokens', value: formatTokenCount(t.tokens), variant: 'total' },
{ label: 'cost', value: formatCost(t.cost), variant: 'total' },
])}
<div id="usage-chart"></div>
</div>
<div class="usage-panel usage-fw-panel">
@@ -238,40 +222,20 @@ export async function renderUsage(routeToken) {
<div class="usage-section-row">
<div class="usage-panel">
<div class="section-title">Top Models <span class="count">${models.length}</span></div>
${models.length === 0 ? '<p class="empty-state" style="padding:1rem">No model data</p>' : `
<ul class="stat-list">
${models.map(m => {
const pct = (m.count / maxModel * 100).toFixed(1);
return `<li>
<div class="stat-list-header">
<span class="stat-list-name">${escapeHTML(m.name)}</span>
<span class="stat-list-count">${m.count}</span>
</div>
<div class="stat-list-bar-track">
<div class="stat-list-bar-fill model" style="width:${pct}%"></div>
</div>
</li>`;
}).join('')}
</ul>`}
${barRankList(models, {
mapItem: m => ({ name: m.name, count: m.count, modifier: 'model' }),
maxOverride: maxModel,
emptyText: 'No model data',
})}
</div>
<div class="usage-panel">
<div class="section-title">Top Tools <span class="count">${tools.length}</span></div>
${tools.length === 0 ? '<p class="empty-state" style="padding:1rem">No tool data</p>' : `
<ul class="stat-list">
${tools.map(t => {
const pct = (t.count / maxTool * 100).toFixed(1);
return `<li>
<div class="stat-list-header">
<span class="stat-list-name">${escapeHTML(t.name)}</span>
<span class="stat-list-count">${t.count}</span>
</div>
<div class="stat-list-bar-track">
<div class="stat-list-bar-fill" style="width:${pct}%"></div>
</div>
</li>`;
}).join('')}
</ul>`}
${barRankList(tools, {
mapItem: x => ({ name: x.name, count: x.count }),
maxOverride: maxTool,
emptyText: 'No tool data',
})}
</div>
</div>
`;