feat: add visual scannability — confidence chips, in-article TOC, progressive disclosure, methodology footer#2807
Conversation
🏷️ Automatic Labeling SummaryThis PR has been automatically labeled based on the files changed and PR metadata. Applied Labels: size-xs Label Categories
For more information, see |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
…y badges, timeline indicators, progressive disclosure, sticky TOC, methodology footer) Implements visual scannability enhancements for articles: - Confidence labels (HIGH/MEDIUM/LOW) rendered as colored chips - Admiralty codes (A1-F6) rendered as badges with tooltips - Timeline markers (T+7d, T+30d, T+90d) rendered as urgency indicators - Deep-dive sections wrapped in progressive disclosure (<details>) - In-article sticky TOC generated from H2 headings - Methodology transparency footer on every article - Dark mode support for all new components - RTL support via logical CSS properties - IntersectionObserver-based TOC highlighting Closes #2804 Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
There was a problem hiding this comment.
Pull request overview
Adds a post-processing “scannability” layer to rendered news articles to improve orientation and visual parsing of long HTML outputs, by injecting a TOC, confidence/admiralty/timeline indicators, progressive disclosure wrappers, and a methodology footer, plus corresponding CSS and unit tests.
Changes:
- Introduces
article-scannability.tswith HTML-string transforms (chips/badges, timeline markers, disclosure wrapping, TOC + footer rendering) and wires it into the article render pipeline. - Adds a new CSS component file for the scannability UI (chips, badges, TOC, disclosure, methodology footer) and imports it in the global stylesheet.
- Adds Vitest coverage for all transforms and their combined application.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
scripts/render-lib/article-scannability.ts |
Implements the HTML post-processing transforms and i18n strings for TOC/footer. |
scripts/render-lib/article.ts |
Integrates scannability transforms into article rendering and conditionally injects TOC active-section highlighting script. |
styles/components/article-scannability.css |
Provides styling for confidence/admiralty/timeline UI, TOC, disclosure widgets, and methodology footer (including dark/RTL handling). |
styles.css |
Imports the new scannability component stylesheet. |
tests/article-scannability.test.ts |
Adds unit tests covering each transform and the combined transform output. |
| details.rm-disclosure > summary { | ||
| background: var(--badge-bg, #333); | ||
| } | ||
|
|
||
| details.rm-disclosure > summary:hover { | ||
| background: var(--section-border, #444); | ||
| } |
| @media (prefers-color-scheme: dark) { | ||
| .rm-confidence--high { | ||
| background-color: rgba(30, 126, 52, 0.2); | ||
| color: #6fcf97; | ||
| border-color: rgba(111, 207, 151, 0.4); |
| * Detect confidence labels (HIGH, MEDIUM, LOW) in rendered text and wrap | ||
| * them in styled chip spans. Only matches whole-word occurrences that appear | ||
| * after contextual keywords (confidence, assessment, rating, etc.) or inside | ||
| * parentheses following a letter-digit Admiralty code pattern. | ||
| * | ||
| * Pattern: "HIGH (A2)" or "CONFIDENCE: HIGH" or "confidence HIGH" | ||
| */ |
| sourcesLabel: 'Source Verification', | ||
| sourcesDesc: 'All claims cite official Swedish government publications (Riksdagen, Regeringen) accessible via public APIs.', | ||
| freshnessLabel: 'Data Freshness', | ||
| freshnessDesc: 'Analysis generated from data retrieved within 24 hours of publication timestamp.', | ||
| }, |
| sourcesLabel: 'Källverifiering', | ||
| sourcesDesc: 'Alla påståenden refererar till officiella svenska myndighetspublikationer (Riksdagen, Regeringen) tillgängliga via offentliga API:er.', | ||
| freshnessLabel: 'Dataaktualitet', | ||
| freshnessDesc: 'Analys genererad från data hämtad inom 24 timmar från publiceringstidsstämpel.', | ||
| }, |
| export function applyScannabilityTransforms(bodyHtml: string, lang: Language): { | ||
| readonly transformedBody: string; | ||
| readonly tocHtml: string; | ||
| readonly methodologyFooterHtml: string; | ||
| } { |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
… softened footer claims, theme-aware dark mode Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
|
@copilot analyse and improve code and test quality so all workflows works failing checks |
…orm, raise build timeout Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Addressed both failing checks: CodeQL (high-severity, Build for Homepage Tests (cancelled at 15m) — this was a job timeout, not a hang. Also hardened
|
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
| export function transformConfidenceChips(html: string): string { | ||
| // Match standalone "HIGH", "MEDIUM", "LOW" (whole-word, case-insensitive), | ||
| // optionally trailed by parenthetical Admiralty notation like "(B2)". | ||
| return html.replace( | ||
| /\b(HIGH|MEDIUM|LOW)\b(?=\s*(?:\([A-F][1-6]\))?)/gi, |
There was a problem hiding this comment.
Fixed in 99a581a6. All three transforms (transformConfidenceChips, transformAdmiraltyBadges, transformTimelineIndicators) now use a replaceInTextNodes() helper that splits the HTML on tags and only applies the regex to text segments, leaving attributes untouched. Regression tests added to verify attributes like id="rm-medium-threats" and href="#rm-low-priority" remain unchanged.
| // Apply visual scannability transforms (confidence chips, admiralty | ||
| // badges, timeline indicators, progressive disclosure). | ||
| const { transformedBody, tocHtml, methodologyFooterHtml } = applyScannabilityTransforms(bodyHtml, input.lang); | ||
|
|
||
| const { lead: leadHtml, rest: restHtml } = splitBodyAtSecondH2(transformedBody); | ||
|
|
There was a problem hiding this comment.
Fixed in 99a581a6. applyScannabilityTransforms() now only applies inline transforms (confidence/admiralty/timeline). Progressive disclosure is applied separately in article.ts after splitBodyAtSecondH2(), so only the rest chunk gets <details> wrappers — avoiding the malformed-HTML scenario where splitting happens inside a disclosure wrapper.
| /* Mobile: collapse TOC into a disclosure widget */ | ||
| @media (max-width: 767px) { | ||
| .rm-article-toc { | ||
| position: static; | ||
| max-height: none; |
There was a problem hiding this comment.
Fixed in 99a581a6. Updated the CSS comment to accurately state "disable sticky positioning and height constraints" — the mobile media query doesn't render a <details> disclosure widget.
| describe('transformConfidenceChips', () => { | ||
| it('wraps HIGH confidence in a green chip', () => { | ||
| const html = '<p>Confidence: HIGH (A2)</p>'; | ||
| const result = transformConfidenceChips(html); | ||
| expect(result).toContain('class="rm-confidence rm-confidence--high"'); | ||
| expect(result).toContain('aria-label="Confidence: HIGH"'); | ||
| }); | ||
|
|
There was a problem hiding this comment.
Fixed in 99a581a6. Added regression tests for all three transforms asserting that HTML attributes containing high|medium|low, (A2), and T+7d substrings remain unchanged after transformation.
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
… comment, attribute-safety tests - Confidence/admiralty/timeline transforms now use replaceInTextNodes() to avoid corrupting heading IDs, hrefs, and other HTML attributes. - Progressive disclosure is applied after splitBodyAtSecondH2() to prevent malformed HTML when a disclosure-matching heading is the split point. - CSS comment corrected: mobile TOC disables sticky, not disclosure. - Regression tests added asserting attributes remain unchanged. Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
Articles are 200-300KB walls of continuous HTML with no visual differentiation between confidence levels, no orientation aids, and specialist content (60%+ of length) presented at the same weight as the editorial lede.
Changes
CSS component library (
styles/components/article-scannability.css).rm-confidence--high/medium/low— green/amber/red chips with WCAG AA contrast.rm-admiralty— badge withcursor: helpfor NATO Admiralty codes.rm-timeline--urgent/near/horizon— urgency-coded timeline markers.rm-article-toc— sticky sidebar nav, collapses to disclosure on mobile.rm-disclosure— styled `` for specialist sections.rm-methodology-footer— structured footer layoutprefers-color-scheme, scoped underhtml:not([data-theme="light"])so a forced light theme always wins; RTL via logical propertiesPost-processing transforms (
scripts/render-lib/article-scannability.ts)Six pure functions applied to sanitized HTML output (no sanitize-schema changes needed):
CODE_BLOCK_0_0
(A1)–(F6)with NATO-standard tooltipsT+7d→Next 7 days<details>nesting.Heading text reused in the TOC and disclosure summaries is entity-decoded first, then stripped of tags (the strip is repeated until the string stabilises so overlapping/nested brackets cannot reconstitute markup), then re-escaped via the shared
escapeHtmlhelper to neutralise any residual markup. Performing the tag strip as the final sanitization step resolves the CodeQL "Incomplete multi-character sanitization" alert.Integration (
scripts/render-lib/article.ts)Transforms wired after
renderMarkdownToHtml()+rewriteMarkdownHrefsInHtml(), beforesplitBodyAtSecondH2(). Inline IntersectionObserver script for TOC active-section highlighting injected conditionally.CI (
.github/workflows/test-homepage.yml)Raised the
Build for Homepage Testsjobtimeout-minutes15 → 20. The scannability transforms add ~0.55ms/article (~3s total); the build was hitting the 15-min cap because the news corpus has grown (main was already ~11m25s), not because of this feature.Tests (
tests/article-scannability.test.ts)26 unit tests covering all transform functions, edge cases (no false positives on partial matches, nested HTML contexts, empty inputs), and heading-text escaping regressions for the TOC and disclosure summaries — including an overlapping-tag case that confirms tag stripping never reconstitutes markup.