cleanup: stats activity chart — extract 30-day bar inline styles

Continuing the renderer-stats.ts inline-style extraction. The
"Aktivitaet (letzte 30 Tage)" bar chart built each day-column as
a 5-inline-style template:

  <div style="flex: 1; display:flex; flex-direction:column;
              align-items:center; gap:4px; min-width:0;">
    <div style="width: 100%; height: 90px; display:flex;
                align-items: flex-end;">
      <div style="width:100%; height: 70%;
                  background: var(--accent, #9146ff);
                  border-radius: 2px 2px 0 0;" title="...">
    <div style="font-size: 9px; color: var(--text-secondary);
                white-space: nowrap;">

30 columns rendered per refresh meant ~7.5KB of duplicated inline
style attribute strings in the DOM after every refresh.

Extracted to .stats-day-col + .stats-day-bar-track + .stats-day-bar-
fill + .stats-day-label, plus .stats-activity-row + .stats-activity-
summary for the outer wrappers. Only the per-day height percent
stays inline (it's truly dynamic, per-day data).

Polish riders:
- Bar fill picks up height: 0.3s ease-out transition so the bars
  animate up on data refresh instead of snapping
- Hover state shifts the bar from accent to accent-hover so the
  hovered day reads as the focus
- Day-label spans get tabular-nums so the "05-12" type strings
  align column-to-column

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xRangerDE 2026-05-11 04:59:33 +02:00
parent 162b2845aa
commit c9a5223eb6
2 changed files with 57 additions and 7 deletions

View File

@ -108,7 +108,7 @@ function renderStatsActivity(days: ArchiveStatsDay[]): void {
const maxCount = days.reduce((m, d) => Math.max(m, d.count), 0); const maxCount = days.reduce((m, d) => Math.max(m, d.count), 0);
if (maxCount === 0) { if (maxCount === 0) {
applyHtml(container, `<div style="color: var(--text-secondary);">${escapeStatsHtml(UI_TEXT.static.statsActivityEmpty)}</div>`); applyHtml(container, `<div class="form-note">${escapeStatsHtml(UI_TEXT.static.statsActivityEmpty)}</div>`);
return; return;
} }
@ -118,11 +118,11 @@ function renderStatsActivity(days: ArchiveStatsDay[]): void {
const showLabel = idx === 0 || idx === days.length - 1 || idx % 7 === 0; const showLabel = idx === 0 || idx === days.length - 1 || idx % 7 === 0;
const dayLabel = showLabel ? d.date.slice(5) : ''; const dayLabel = showLabel ? d.date.slice(5) : '';
return ` return `
<div style="flex: 1; display:flex; flex-direction:column; align-items:center; gap:4px; min-width:0;"> <div class="stats-day-col">
<div style="width: 100%; height: 90px; display:flex; align-items: flex-end;"> <div class="stats-day-bar-track">
<div style="width:100%; height: ${heightPct}%; background: var(--accent, #9146ff); border-radius: 2px 2px 0 0;" title="${escapeStatsHtml(tooltip)}"></div> <div class="stats-day-bar-fill" style="height: ${heightPct}%;" title="${escapeStatsHtml(tooltip)}"></div>
</div> </div>
<div style="font-size: 9px; color: var(--text-secondary); white-space: nowrap;">${escapeStatsHtml(dayLabel)}</div> <div class="stats-day-label">${escapeStatsHtml(dayLabel)}</div>
</div> </div>
`; `;
}).join(''); }).join('');
@ -130,8 +130,8 @@ function renderStatsActivity(days: ArchiveStatsDay[]): void {
const totalCount = days.reduce((s, d) => s + d.count, 0); const totalCount = days.reduce((s, d) => s + d.count, 0);
const totalBytes = days.reduce((s, d) => s + d.bytes, 0); const totalBytes = days.reduce((s, d) => s + d.bytes, 0);
applyHtml(container, ` applyHtml(container, `
<div style="display:flex; gap:2px; align-items: flex-end; padding: 6px 0;">${bars}</div> <div class="stats-activity-row">${bars}</div>
<div style="font-size: 12px; color: var(--text-secondary); margin-top: 6px;">${escapeStatsHtml(UI_TEXT.static.statsActivitySummary <div class="stats-activity-summary">${escapeStatsHtml(UI_TEXT.static.statsActivitySummary
.replace('{count}', String(totalCount)) .replace('{count}', String(totalCount))
.replace('{size}', formatBytesForStats(totalBytes)))}</div> .replace('{size}', formatBytesForStats(totalBytes)))}</div>
`); `);

View File

@ -2311,6 +2311,56 @@ select option {
pointer-events: none; pointer-events: none;
} }
/* 30-day activity chart vertical bar per day with optional date
label below every 7th column. */
.stats-activity-row {
display: flex;
gap: 2px;
align-items: flex-end;
padding: 6px 0;
}
.stats-day-col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
min-width: 0;
}
.stats-day-bar-track {
width: 100%;
height: 90px;
display: flex;
align-items: flex-end;
}
.stats-day-bar-fill {
width: 100%;
background: var(--accent, #9146ff);
border-radius: 2px 2px 0 0;
transition: height 0.3s ease-out, background 0.2s;
}
.stats-day-bar-fill:hover {
background: var(--accent-hover, #b97aff);
}
.stats-day-label {
font-size: 9px;
color: var(--text-secondary);
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.stats-activity-summary {
font-size: 12px;
color: var(--text-secondary);
margin-top: 6px;
font-variant-numeric: tabular-nums;
}
/* Old generic scrollbar rules were dead superseded by the /* Old generic scrollbar rules were dead superseded by the
purple-themed *::-webkit-scrollbar block further down the file. purple-themed *::-webkit-scrollbar block further down the file.
Removed to avoid confusion when someone greps for scrollbar styles. */ Removed to avoid confusion when someone greps for scrollbar styles. */