fix(visuals): weekly cover bars now map 1:1 to topics (Closes #560) (#561)

* fix(visuals): make weekly cover bars map 1:1 to topics (#560) The weekly article cover rendered a row of decorative, flat-shade bars whose count (clamped 6–18) and random seed-driven heights did not correspond to the separately-listed topic tags, so readers could not tell which bar meant which topic — and nobody knew what the bars measured. Redesign the cover as a real topic chart (outcome A): one bar per topic, in editorial-prominence order, sharing a single element with its label so every topic maps to exactly one bar. Bar height and the printed number encode the topic's prominence rank (1 = lead topic) — a real, disclosed ordering signal from the analysis `tags`, not a fabricated magnitude. Adds a legend caption explaining what the bars measure and distinct tonal shading (color-mix) so bars are no longer one flat shade. Closes #560 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(visuals): address Copilot review on #560 cover chart - Remove the unused `--bar-n` custom property from each topic <li>; it was set inline but never consumed by the stylesheet. - Move the rank number above its bar (rendered in --color-text against the panel surface) instead of inside the bar. The bar fill intentionally mixes toward --color-surface for lower-ranked bars, which made a surface-colored in-bar number low-contrast/invisible; placing it on the surface keeps it legible for every bar in both themes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Juan Manuel Servera committed Jun 30, 2026 at 00:05 UTC 969554c28064bc93fb7e8d0d336a22a944072b0d
2 files changed +97 -74
assets/css/extended/article-visuals.css
+59 -34
@@ -11,7 +11,7 @@
11 position: relative;
12 display: flex;
13 flex-direction: column;
14 - justify-content: flex-end;
14 + gap: var(--space-2);
15 /* Fixed aspect ratio reserves space up front -> no layout shift (CLS). */
16 aspect-ratio: 16 / 7;
17 min-height: calc(var(--space-12) + var(--space-3));
@@ -28,20 +28,6 @@
28 gap: var(--space-1);
29 }
30
31 -/* Decorative generated motif — sits behind the meta, never interactive. */
32 -.article-cover__motif {
33 - position: absolute;
34 - inset: 0;
35 - width: 100%;
36 - height: 100%;
37 - color: var(--color-accent);
38 - opacity: 0.16;
39 -}
40 -
41 -.article-cover__motif rect {
42 - fill: currentColor;
43 -}
44 -
31 .article-cover__meta {
32 position: relative;
33 display: flex;
@@ -67,32 +53,76 @@
53 letter-spacing: var(--tracking-tight);
54 }
55
56 +/* ---------- Topic prominence bar chart (#560) ----------
57 + Every bar IS a topic: one bar per topic, in editorial-prominence order, each
58 + labelled with its name and rank number. Replaces the former decorative motif
59 + that visually mismatched the topic list. */
60 .article-cover__topics {
61 display: flex;
72 - flex-wrap: wrap;
73 - gap: var(--space-1) var(--space-2);
74 - margin: var(--space-2) 0 0;
62 + align-items: flex-end;
63 + gap: var(--space-1);
64 + flex: 1 1 auto;
65 + min-height: 0;
66 + margin: 0;
67 padding: 0;
68 list-style: none;
69 + counter-reset: none;
70 }
71
79 -/* Topic titles are caption metadata, not controls: plain muted labels with a
80 - subtle separator. No background/border/padding-pill/hover -> never look clickable. */
72 +/* Topic = a labelled bar. Plain, non-interactive caption metadata: no link,
73 + no button/pill affordance, never looks clickable (operator ask, #328). */
74 .article-cover__topic {
82 - font-size: var(--text-xs);
83 - font-weight: 600;
84 - color: var(--color-text-muted);
75 + display: flex;
76 + flex: 1 1 0;
77 + min-width: 0;
78 + flex-direction: column;
79 + align-items: center;
80 + justify-content: flex-end;
81 + height: 100%;
82 + gap: var(--space-1);
83 }
84
87 -.article-cover__topic:not(:last-child)::after {
88 - content: "·";
89 - margin-inline-start: var(--space-2);
90 - color: var(--color-border);
85 +/* The bar itself — height encodes rank (set inline via --bar-h); tonal shading
86 + from --bar-i gives distinct, theme-aware tonalities (not one flat shade). */
87 +.article-cover__bar {
88 + width: 100%;
89 + height: var(--bar-h, 100%);
90 + min-height: var(--space-3);
91 + border-radius: var(--radius-sm) var(--radius-sm) 0 0;
92 + background: color-mix(
93 + in srgb,
94 + var(--color-accent) calc(82% - var(--bar-i, 0) * 6%),
95 + var(--color-surface)
96 + );
97 +}
98 +
99 +/* Rank number sits just above its bar, against the panel surface, so contrast
100 + is independent of each bar's tonal fill (review: low-contrast on light bars). */
101 +.article-cover__bar-num {
102 + margin-bottom: 2px;
103 + font-size: var(--text-xs);
104 + font-weight: 700;
105 + line-height: 1;
106 + color: var(--color-text);
107 }
108
93 -.article-cover__topic--more {
109 +.article-cover__topic-label {
110 + width: 100%;
111 + font-size: var(--text-xs);
112 font-weight: 600;
113 + line-height: var(--leading-tight);
114 + text-align: center;
115 + color: var(--color-text-muted);
116 + overflow: hidden;
117 + text-overflow: ellipsis;
118 + white-space: nowrap;
119 +}
120 +
121 +.article-cover__legend {
122 + margin: 0;
123 color: var(--color-text-muted);
124 + font-size: var(--text-xs);
125 + line-height: var(--leading-normal);
126 }
127
128 .article-cover__fallback-note {
@@ -289,9 +319,4 @@
319 }
320 }
321
292 -/* Honor reduced-data/transparency only; no motion is used, but keep motif calm. */
293 -@media (prefers-reduced-motion: reduce) {
294 - .article-cover__motif {
295 - opacity: 0.12;
296 - }
297 -}
322 +/* No motion is used in the cover; nothing to reduce here. */
layouts/partials/visuals/cover-card.html
+38 -40
@@ -1,12 +1,15 @@
1 {{- /*
2 - Generated cover card — a locally-rendered, deterministic inline-SVG header
3 - visual for weekly articles. No third-party or hotlinked imagery.
2 + Generated cover card — a locally-rendered, deterministic header visual for
3 + weekly articles. No third-party or hotlinked imagery.
4
5 - Data-anchored: the bar field COUNT reflects the number of topic clusters
6 - tracked this week (from .Params.tags); the accessible name reports the real
7 - week, topic count, and repositories-featured figures. Per-bar height variation
8 - is a deterministic decorative motif (disclosed via aria-hidden) and is NOT a
9 - claim about per-topic magnitude — preserving the evidence-first editorial stance.
5 + Data-anchored topic chart (issue #560): every bar IS a topic. Bars and topic
6 + labels share one element, in the same order, so each topic maps 1:1 to its bar
7 + (no more decorative bars that outnumber/misalign the topics). Bar height and the
8 + printed number encode the topic's EDITORIAL-PROMINENCE RANK — the order the
9 + weekly analysis emits its `tags`, lead topic first (1 = lead topic). This is a
10 + real, disclosed ordering signal, not a fabricated per-topic magnitude, so it
11 + preserves the evidence-first editorial stance. A caption legend states exactly
12 + what the bars measure, and bars carry distinct tonal shading.
13
14 Params (dict):
15 page : the page context (required)
@@ -18,51 +21,46 @@
21 {{- $repos := $page.Params.repos_featured -}}
22 {{- $kicker := cond (ne $week "") (printf "Weekly signal · %s" $week) "Weekly signal" -}}
23
21 -{{- /* Deterministic seed from the week (or title) so each week renders a stable, distinct motif. */ -}}
22 -{{- $seedKey := cond (ne $week "") $week $page.Title -}}
23 -{{- $seed := (hash.FNV32a $seedKey) -}}
24 -
25 -{{- /* Number of bars = topic breadth this week, clamped to a sensible range for layout. */ -}}
26 -{{- $bars := $topicCount -}}
27 -{{- if lt $bars 6 }}{{ $bars = 6 }}{{ end -}}
28 -{{- if gt $bars 18 }}{{ $bars = 18 }}{{ end -}}
24 +{{- /* Show at most 12 topics as bars so labels stay legible; any remainder is
25 + disclosed as a "+N more" note rather than silently dropped. */ -}}
26 +{{- $shown := $topics -}}
27 +{{- if gt $topicCount 12 }}{{ $shown = first 12 $topics }}{{ end -}}
28 +{{- $n := len $shown -}}
29
30 {{- /* Build an accessible summary from REAL data only. */ -}}
31 {{- $summaryParts := slice -}}
32 {{- if ne $week "" }}{{ $summaryParts = $summaryParts | append (printf "%s weekly report" $week) }}{{ else }}{{ $summaryParts = $summaryParts | append "Weekly report" }}{{ end -}}
33 -{{- if gt $topicCount 0 }}{{ $summaryParts = $summaryParts | append (printf "tracking %d topic clusters" $topicCount) }}{{ end -}}
33 +{{- if gt $topicCount 0 }}{{ $summaryParts = $summaryParts | append (printf "tracking %d topic clusters, ordered by editorial prominence" $topicCount) }}{{ end -}}
34 {{- if $repos }}{{ $summaryParts = $summaryParts | append (printf "across %s featured repositories" (lang.FormatNumber 0 $repos)) }}{{ end -}}
35 {{- $summary := printf "Generated cover: %s." (delimit $summaryParts ", ") -}}
36
37 <figure class="article-cover article-cover--generated" aria-label="{{ $summary }}">
38 <div class="article-cover__panel">
39 - <svg class="article-cover__motif" viewBox="0 0 1000 360" preserveAspectRatio="none" aria-hidden="true" focusable="false">
40 - {{- $slot := div 1000.0 $bars -}}
41 - {{- $barW := mul $slot 0.46 -}}
42 - {{- range $i := seq $bars -}}
43 - {{- $idx := sub $i 1 -}}
44 - {{- $h := add 56 (mod (mul (add $idx 1) (add $seed 131)) 268) -}}
45 - {{- $x := add (mul $idx $slot) (mul $slot 0.27) -}}
46 - {{- $y := sub 360 $h -}}
47 - <rect x="{{ $x }}" y="{{ $y }}" width="{{ $barW }}" height="{{ $h }}" rx="4" />
48 - {{- end -}}
49 - </svg>
39 <div class="article-cover__meta">
40 <p class="article-cover__kicker">{{ $kicker }}</p>
41 <p class="article-cover__brand">Claracle</p>
53 - {{- if gt $topicCount 0 }}
54 - {{- /* Topic titles are descriptive caption metadata, NOT controls. Render them
55 - as plain, non-interactive labels — no link, no button/pill affordance —
56 - so they never look clickable (operator ask, #328). */ -}}
57 - <ul class="article-cover__topics" aria-label="Topics tracked this week">
58 - {{- range first 6 $topics }}
59 - <li class="article-cover__topic">{{ . }}</li>
60 - {{- end }}
61 - {{- if gt $topicCount 6 }}
62 - <li class="article-cover__topic article-cover__topic--more">+{{ sub $topicCount 6 }} more</li>
63 - {{- end }}
64 - </ul>
65 - {{- end }}
42 </div>
43 + {{- if gt $topicCount 0 }}
44 + {{- /* Topic titles are descriptive caption metadata, NOT controls. Render them
45 + as plain, non-interactive labels — no link, no button/pill affordance —
46 + so they never look clickable (operator ask, #328). The ordered list
47 + conveys rank to assistive tech; the printed number is the visible echo. */ -}}
48 + <ol class="article-cover__topics" aria-label="Topic clusters tracked this week, ordered by editorial prominence (1 = lead topic)">
49 + {{- range $i, $t := $shown }}
50 + {{- $rank := add $i 1 -}}
51 + {{- /* Height = rank: lead topic full height, descending to the last topic. */ -}}
52 + {{- $hpct := printf "%.0f" (math.Round (div (mul (sub $n $i) 100.0) $n)) -}}
53 + <li class="article-cover__topic" style="--bar-h:{{ $hpct }}%;--bar-i:{{ $i }};">
54 + <span class="article-cover__bar-num" aria-hidden="true">{{ $rank }}</span>
55 + <span class="article-cover__bar" aria-hidden="true"></span>
56 + <span class="article-cover__topic-label" title="{{ $t }}">{{ $t }}</span>
57 + </li>
58 + {{- end }}
59 + </ol>
60 + <p class="article-cover__legend">
61 + Each bar is one of this week's {{ $topicCount }} topic clusters, ordered by editorial prominence (1&nbsp;=&nbsp;lead topic).
62 + {{- if gt $topicCount $n }} Showing the top {{ $n }}; +{{ sub $topicCount $n }} more not charted.{{ end }}
63 + </p>
64 + {{- end }}
65 </div>
66 </figure>