Skip to content

Add SWOT and dashboard section generators for article enrichment#830

Merged
pethers merged 18 commits intomainfrom
copilot/add-swot-dashboard-capabilities
Mar 5, 2026
Merged

Add SWOT and dashboard section generators for article enrichment#830
pethers merged 18 commits intomainfrom
copilot/add-swot-dashboard-capabilities

Conversation

Copy link
Contributor

Copilot AI commented Mar 5, 2026

  • SWOT impact badges → CSS modifier classes (round 1)
  • No inline scripts → data-chart-config attributes (round 1)
  • Per-type annotation serialisation (round 1)
  • Chart ID uniqueness + fallback (round 1)
  • Remove 'box' from DashboardAnnotation.type union (round 2)
  • Update JSDoc to clarify client-side init is NOT automatic (round 2)
  • Remove hard-coded responsive/maintainAspectRatio/legend.position from serialiseChartConfig() (round 2)
  • Fix test: use truly unsupported type instead of 'box' cast (round 2)
  • Fix module header: Chart.js-only, not "Chart.js / D3" (round 3)
  • Remove redundant role="table" from <table> elements (round 3)
  • Add aspect-ratio: 16/9 to .dashboard-chart-wrapper for deterministic chart height (round 4)
  • Make DashboardChartConfig.labels optional to support scatter charts (round 4)
  • Fix SWOT heading hierarchy: <h4><h3> under section <h2> (round 4)
  • Fix SWOT CSS selectors: h4h3 to match updated heading markup (round 5)
  • Widen DashboardDataset.data to number[] | DashboardPoint[] for scatter support (round 5)
  • Make DashboardAnnotation.value required to prevent invalid annotation configs (round 6)
  • Fallback aria-label to safeId when chart.title is empty; suppress plugins.title.display when no title (round 6)
  • Trim whitespace-only data.title / data.summary in dashboard section (round 7)
  • Trim whitespace-only data.subject / data.context in SWOT section (round 7)
  • Align SWOT grid breakpoint from 600px → 768px to match codebase conventions (round 7)
  • Fix test cast: 'ellipse' as unknown as 'line' for explicit intent (round 7)
  • Fix DashboardPoint JSDoc: remove "bubble" mention (scatter only) (round 8)
  • Localise SWOT impact badge labels via swotImpactHigh/Medium/Low keys across all 14 languages (round 8)
  • Full suite: 2,677+ tests passing, CodeQL: 0 alerts

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 4 commits March 5, 2026 12:52
… CSS styles

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
…hart config

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
… mismatch

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Copilot AI changed the title [WIP] Add SWOT and dashboard capabilities to article generation Add SWOT and dashboard section generators for article enrichment Mar 5, 2026
@pethers pethers requested a review from Copilot March 5, 2026 13:07
@github-actions github-actions bot added html-css HTML/CSS changes testing Test coverage refactor Code refactoring size-xl Extra large change (> 1000 lines) labels Mar 5, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new embeddable article enrichment sections (SWOT matrix + Chart.js dashboard) to the data-transformer content generator layer, including new article/data types, localized labels across 14 languages, styling, and targeted unit tests.

Changes:

  • Introduces generateSwotSection() and generateDashboardSection() section generators returning TemplateSection HTML.
  • Extends article/content typing + localization label sets to support SWOT and dashboard sections across 14 languages.
  • Adds CSS styling for SWOT/dashboard sections and Vitest coverage for structure, i18n, and XSS-escaping behavior.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/swot-section.test.ts Adds unit tests for SWOT section HTML structure, i18n labels, and escaping.
tests/dashboard-section.test.ts Adds unit tests for dashboard HTML, Chart.js config serialization, tables, and escaping.
styles.css Adds styling for SWOT grid/quadrants and dashboard chart/table wrappers (incl. RTL tweaks).
scripts/types/content.ts Extends ContentLabelSet with SWOT + dashboard label keys.
scripts/types/article.ts Adds new Swot* and Dashboard* types for section generator inputs.
scripts/data-transformers/index.ts Re-exports the new generators and their option types from the transformers barrel.
scripts/data-transformers/content-generators/swot-section.ts Implements SWOT section HTML generation with escaping and optional subject/context.
scripts/data-transformers/content-generators/dashboard-section.ts Implements dashboard HTML generation with canvas, optional tables, and inline Chart.js init.
scripts/data-transformers/content-generators/index.ts Exposes the new generators/types from the content-generators index.
scripts/data-transformers/content-generators.ts Re-exports the new generators/types from the content-generators barrel.
scripts/data-transformers/constants/content-labels-part1.ts Adds SWOT/dashboard translations for en/sv/da/no/fi/de/fr.
scripts/data-transformers/constants/content-labels-part2.ts Adds SWOT/dashboard translations for es/nl/ar/he/ja/ko/zh.

Comment on lines +83 to +89
result[key] = {
type: a.type,
...(a.value != null ? { yMin: a.value, yMax: a.value } : {}),
...(a.borderColor ? { borderColor: a.borderColor } : {}),
...(a.backgroundColor ? { backgroundColor: a.backgroundColor } : {}),
...(a.label ? { label: { display: true, content: a.label } } : {}),
};
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildAnnotations() emits the same yMin/yMax + nested label structure for every DashboardAnnotation.type, but chartjs-plugin-annotation uses different option shapes for box and especially label annotations. As-is, type: 'label' / type: 'box' inputs will generate configs that the plugin won’t interpret correctly. Either narrow DashboardAnnotation.type to the supported subset (e.g., only horizontal line with value) or implement per-type serialization (x/y value vs min/max ranges, label content fields, etc.).

Suggested change
result[key] = {
type: a.type,
...(a.value != null ? { yMin: a.value, yMax: a.value } : {}),
...(a.borderColor ? { borderColor: a.borderColor } : {}),
...(a.backgroundColor ? { backgroundColor: a.backgroundColor } : {}),
...(a.label ? { label: { display: true, content: a.label } } : {}),
};
let config: Record<string, unknown> | undefined;
switch (a.type) {
case 'line': {
// Horizontal line annotation at a given Y value
config = {
type: 'line',
...(a.value != null ? { yMin: a.value, yMax: a.value } : {}),
...(a.borderColor ? { borderColor: a.borderColor } : {}),
...(a.backgroundColor ? { backgroundColor: a.backgroundColor } : {}),
...(a.label ? { label: { display: true, content: a.label } } : {}),
};
break;
}
case 'label': {
// Label annotation: top-level content with an optional Y anchor
if (a.label == null && a.value == null) {
break;
}
const content =
a.label != null
? a.label
: a.value != null
? String(a.value)
: '';
config = {
type: 'label',
content,
...(a.value != null ? { yValue: a.value } : {}),
...(a.borderColor ? { borderColor: a.borderColor } : {}),
...(a.backgroundColor ? { backgroundColor: a.backgroundColor } : {}),
};
break;
}
default:
// Unsupported annotation types (e.g. 'box') are not serialized
break;
}
if (config) {
result[key] = config;
}

Copilot uses AI. Check for mistakes.
Comment on lines +181 to +185
const sanitisedCharts = data.charts.map(chart => ({
...chart,
safeId: chart.id.replace(/[^a-zA-Z0-9_-]/g, ''),
}));

Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safeId is derived by stripping characters from chart.id, but there’s no validation for the result being empty or for collisions (two different ids sanitising to the same value). Either case can produce invalid/duplicate DOM ids and make getElementById() bind the wrong canvas. Consider enforcing a non-empty/unique id (e.g., fall back to a deterministic chart-${index} suffix when needed, or throw a clear error).

Suggested change
const sanitisedCharts = data.charts.map(chart => ({
...chart,
safeId: chart.id.replace(/[^a-zA-Z0-9_-]/g, ''),
}));
// Ensure each safeId is non-empty and unique to avoid invalid/duplicate DOM ids.
const usedIds = new Set<string>();
const sanitisedCharts = data.charts.map((chart, index) => {
// Strip unsupported characters
let baseId = chart.id.replace(/[^a-zA-Z0-9_-]/g, '');
// Fallback if sanitisation results in an empty id
if (!baseId) {
baseId = `chart-${index}`;
}
// Ensure uniqueness by suffixing with a counter if needed
let safeId = baseId;
let counter = 1;
while (usedIds.has(safeId)) {
safeId = `${baseId}-${counter++}`;
}
usedIds.add(safeId);
return {
...chart,
safeId,
};
});

Copilot uses AI. Check for mistakes.
return ` (function() {
var canvas = document.getElementById('${chart.safeId}');
if (canvas && typeof Chart !== 'undefined') {
new Chart(canvas.getContext('2d'), ${config});
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline initializer uses canvas.getContext('2d') without checking for null. getContext() can return null (e.g., unsupported context, offscreen/locked canvas), which would throw and potentially stop initialization for subsequent charts. Guard on a non-null context before calling new Chart(...) (and consider a minimal error log to aid troubleshooting).

Suggested change
new Chart(canvas.getContext('2d'), ${config});
var ctx = canvas.getContext('2d');
if (ctx) {
new Chart(ctx, ${config});
} else if (typeof console !== 'undefined' && console && typeof console.error === 'function') {
console.error('Failed to obtain 2D context for canvas with id: ${chart.safeId}');
}

Copilot uses AI. Check for mistakes.
Comment on lines +180 to +218
// Sanitise chart IDs once — used consistently in both HTML id and script getElementById
const sanitisedCharts = data.charts.map(chart => ({
...chart,
safeId: chart.id.replace(/[^a-zA-Z0-9_-]/g, ''),
}));

// Chart canvases
const chartBlocks = sanitisedCharts.map(chart => {
return ` <div class="dashboard-chart-wrapper">
<canvas id="${escapeHtml(chart.safeId)}" role="img" aria-label="${escapeHtml(chart.title)}"></canvas>
</div>`;
}).join('\n');

// Tables (optional)
const tableBlocks = (data.tables ?? []).map(t => renderTable(t)).join('\n');

// Inline Chart.js init script
const chartInits = sanitisedCharts.map(chart => {
const config = serialiseChartConfig(chart);
return ` (function() {
var canvas = document.getElementById('${chart.safeId}');
if (canvas && typeof Chart !== 'undefined') {
new Chart(canvas.getContext('2d'), ${config});
}
})();`;
}).join('\n');

const scriptBlock = data.charts.length > 0
? `\n <script>
document.addEventListener('DOMContentLoaded', function() {
${chartInits}
});
</script>`
: '';

const html = `<section class="article-dashboard" aria-label="${escapeHtml(titleText)}">
<h2>${escapeHtml(titleText)}</h2>
${summaryBlock}${chartBlocks}
${tableBlocks}${scriptBlock}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generator injects an inline <script> block to initialise charts. Elsewhere in the codebase the dashboard tooling explicitly documents “No inline scripts” and centralises chart creation in src/browser/shared/chart-factory.ts (consistent theming, responsive options, and error handling). Consider emitting chart config as data/JSON in the HTML and initialising via a shared module (or reusing the chart factory), so article charts inherit the same behavior and you avoid expanding inline-script usage.

Suggested change
// Sanitise chart IDs once — used consistently in both HTML id and script getElementById
const sanitisedCharts = data.charts.map(chart => ({
...chart,
safeId: chart.id.replace(/[^a-zA-Z0-9_-]/g, ''),
}));
// Chart canvases
const chartBlocks = sanitisedCharts.map(chart => {
return ` <div class="dashboard-chart-wrapper">
<canvas id="${escapeHtml(chart.safeId)}" role="img" aria-label="${escapeHtml(chart.title)}"></canvas>
</div>`;
}).join('\n');
// Tables (optional)
const tableBlocks = (data.tables ?? []).map(t => renderTable(t)).join('\n');
// Inline Chart.js init script
const chartInits = sanitisedCharts.map(chart => {
const config = serialiseChartConfig(chart);
return ` (function() {
var canvas = document.getElementById('${chart.safeId}');
if (canvas && typeof Chart !== 'undefined') {
new Chart(canvas.getContext('2d'), ${config});
}
})();`;
}).join('\n');
const scriptBlock = data.charts.length > 0
? `\n <script>
document.addEventListener('DOMContentLoaded', function() {
${chartInits}
});
</script>`
: '';
const html = `<section class="article-dashboard" aria-label="${escapeHtml(titleText)}">
<h2>${escapeHtml(titleText)}</h2>
${summaryBlock}${chartBlocks}
${tableBlocks}${scriptBlock}
// Sanitise chart IDs once — used consistently in both HTML id and any client-side selectors
const sanitisedCharts = data.charts.map(chart => ({
...chart,
safeId: chart.id.replace(/[^a-zA-Z0-9_-]/g, ''),
}));
// Chart canvases with embedded, non-executable configuration
const chartBlocks = sanitisedCharts.map(chart => {
const config = serialiseChartConfig(chart);
return ` <div class="dashboard-chart-wrapper">
<canvas id="${escapeHtml(chart.safeId)}" role="img" aria-label="${escapeHtml(chart.title)}" data-chart-config="${escapeHtml(config)}"></canvas>
</div>`;
}).join('\n');
// Tables (optional)
const tableBlocks = (data.tables ?? []).map(t => renderTable(t)).join('\n');
const html = `<section class="article-dashboard" aria-label="${escapeHtml(titleText)}">
<h2>${escapeHtml(titleText)}</h2>
${summaryBlock}${chartBlocks}
${tableBlocks}

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +32
const IMPACT_COLORS: Readonly<Record<SwotImpact, string>> = {
high: '#e74c3c',
medium: '#f39c12',
low: '#27ae60',
};

function impactBadge(impact: SwotImpact | undefined): string {
if (!impact) return '';
const color = IMPACT_COLORS[impact] ?? IMPACT_COLORS.medium;
return ` <span class="swot-impact" style="color:${color};font-weight:600;">[${escapeHtml(impact)}]</span>`;
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

impactBadge() uses an inline style="color:…" to set badge color. Since the repo already adds CSS for .swot-impact, consider moving the impact-level coloring into CSS classes (e.g., .swot-impact--high/--medium/--low) and emit only class names here. This improves maintainability (theme changes in one place) and reduces inline-style surface area.

Suggested change
const IMPACT_COLORS: Readonly<Record<SwotImpact, string>> = {
high: '#e74c3c',
medium: '#f39c12',
low: '#27ae60',
};
function impactBadge(impact: SwotImpact | undefined): string {
if (!impact) return '';
const color = IMPACT_COLORS[impact] ?? IMPACT_COLORS.medium;
return ` <span class="swot-impact" style="color:${color};font-weight:600;">[${escapeHtml(impact)}]</span>`;
const IMPACT_CLASSES: Readonly<Record<SwotImpact, string>> = {
high: 'swot-impact--high',
medium: 'swot-impact--medium',
low: 'swot-impact--low',
};
function impactBadge(impact: SwotImpact | undefined): string {
if (!impact) return '';
const impactClass = IMPACT_CLASSES[impact] ?? IMPACT_CLASSES.medium;
return ` <span class="swot-impact ${impactClass}">[${escapeHtml(impact)}]</span>`;

Copilot uses AI. Check for mistakes.
@pethers
Copy link
Member

pethers commented Mar 5, 2026

@copilot apply changes based on the comments in this thread

…ttrs, per-type annotations, ID uniqueness

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 5, 2026

@copilot apply changes based on the comments in this thread

Applied all 5 review comments in 9cfee8f:

  1. SWOT impact badges → CSS modifier classes (swot-impact--high/--medium/--low) replacing inline style="color:...". Colors now in styles.css.
  2. No inline scripts → Dashboard uses data-chart-config HTML attributes on canvas elements, consistent with the codebase's chart-factory.ts "No inline scripts" pattern.
  3. Per-type annotation serialisationbuildAnnotations() uses a switch with correct shapes for line (yMin/yMax + nested label) and label (content + yValue). Unsupported types silently skipped.
  4. Chart ID uniqueness → Empty sanitised IDs fall back to chart-${index}, collisions resolved with counter suffix.
  5. getContext('2d') null check → Moot now — chart config is a data attribute; client-side chart-factory handles init with proper error handling.

39 tests passing (up from 35). Full suite: 2,677 tests. CodeQL: 0 alerts.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.


/** Annotation overlay for a Chart.js chart */
export interface DashboardAnnotation {
type: 'line' | 'box' | 'label';
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DashboardAnnotation.type includes 'box', but buildAnnotations() in dashboard-section.ts currently only serialises 'line' and 'label' (and silently skips everything else). This makes the public type contract misleading for consumers and also forces tests to use unsafe casts. Either implement 'box' serialisation (so it actually works end-to-end) or remove 'box' from the union until it’s supported.

Suggested change
type: 'line' | 'box' | 'label';
type: 'line' | 'label';

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +16
* Client-side chart initialisation is handled by the shared `chart-factory.ts`
* module or any loader that reads `data-chart-config` from canvas elements.
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module claims charts will be initialised by chart-factory.ts or a loader that reads data-chart-config, but there is currently no code in the repo that scans for data-chart-config and calls createChart(). As a result, article pages that embed this section will render empty canvases unless additional client-side init code is added (and Chart.js globals are loaded). Please either add a shared initializer (and ensure it’s loaded on article pages) or update the generator/docs so consumers don’t assume charts will render automatically.

Suggested change
* Client-side chart initialisation is handled by the shared `chart-factory.ts`
* module or any loader that reads `data-chart-config` from canvas elements.
* Client-side chart initialisation is NOT performed automatically by this
* module. Embedding pages MUST load Chart.js and run an initializer (for
* example, a shared `chart-factory.ts` or any loader that scans canvases for
* `data-chart-config` and calls `createChart()` or equivalent).

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +68
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { position: 'top' as const },
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serialiseChartConfig() hard-codes maintainAspectRatio: true (and responsive: true / legend position), which will override the repo’s centralized responsive defaults in chart-factory.ts (where maintainAspectRatio is false and legend position is viewport-dependent). To keep the "chart-factory" consistency claim accurate and preserve responsive behavior, consider omitting these options here (letting the factory merge them) or set maintainAspectRatio: false to match existing dashboards.

Suggested change
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { position: 'top' as const },
plugins: {

Copilot uses AI. Check for mistakes.
it('skips unsupported annotation types', () => {
const chart = makeChart({
annotations: [
{ type: 'box' as 'line', value: 10 },
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses an unsafe cast { type: 'box' as 'line' } to simulate an unsupported annotation type. Since 'box' is already part of DashboardAnnotation['type'], this cast is unnecessary and hides type-safety issues. Prefer passing { type: 'box', ... } directly (or update the annotation type union if 'box' shouldn’t be part of the public API yet).

Suggested change
{ type: 'box' as 'line', value: 10 },
{ type: 'box', value: 10 },

Copilot uses AI. Check for mistakes.
@pethers
Copy link
Member

pethers commented Mar 5, 2026

@copilot apply changes based on the comments in this thread

…d-coded chart options, fix test cast

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 5, 2026

@copilot apply changes based on the comments in this thread

Applied all 3 review comments in 4e5f00e:

  1. DashboardAnnotation.value — Made required (was optional). Both 'line' and 'label' now always have a numeric position, preventing invalid annotation configs.
  2. line annotation guardbuildAnnotations() now always emits yMin/yMax for line and yValue for label since value is required.
  3. aria-label fallback — Falls back to safeId when chart.title is empty/whitespace. plugins.title.display is false when no title text.

40 tests passing. CodeQL: 0 alerts.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

return typeof val === 'string' ? val : key;
};

const titleText = data.title || lbl('dashboardTitle');
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data.title || lbl('dashboardTitle') treats whitespace-only titles (e.g. ' ') as a real title, resulting in an empty/blank <h2> and aria-label. Consider trimming and falling back when data.title.trim() is empty, similar to the existing chart.title.trim() handling.

Suggested change
const titleText = data.title || lbl('dashboardTitle');
const rawTitle = typeof data.title === 'string' ? data.title.trim() : '';
const titleText = rawTitle || lbl('dashboardTitle');

Copilot uses AI. Check for mistakes.
Comment on lines +206 to +207
const summaryBlock = data.summary
? ` <p class="dashboard-summary">${escapeHtml(data.summary)}</p>\n`
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data.summary is checked only for truthiness, so a whitespace-only summary will still render an empty <p class="dashboard-summary">…</p>. Consider trimming and omitting the summary block when data.summary.trim() is empty.

Suggested change
const summaryBlock = data.summary
? ` <p class="dashboard-summary">${escapeHtml(data.summary)}</p>\n`
const trimmedSummary = data.summary?.trim();
const summaryBlock = trimmedSummary
? ` <p class="dashboard-summary">${escapeHtml(trimmedSummary)}</p>\n`

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +101
const subjectLine = data.subject
? ` <p class="swot-subject"><strong>${escapeHtml(data.subject)}</strong></p>\n`
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subject line is rendered whenever data.subject is truthy; a whitespace-only subject will produce an effectively empty <strong> block. Consider trimming and only rendering when data.subject.trim() is non-empty.

Suggested change
const subjectLine = data.subject
? ` <p class="swot-subject"><strong>${escapeHtml(data.subject)}</strong></p>\n`
const subjectText = data.subject ?? '';
const trimmedSubject = subjectText.trim();
const subjectLine = trimmedSubject
? ` <p class="swot-subject"><strong>${escapeHtml(subjectText)}</strong></p>\n`

Copilot uses AI. Check for mistakes.
Comment on lines +111 to +112
const contextBlock = data.context
? `\n <p class="swot-context"><em>${escapeHtml(lbl('swotContext'))}:</em> ${escapeHtml(data.context)}</p>`
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context note is rendered whenever data.context is truthy; a whitespace-only context will still render an empty-looking context paragraph. Consider trimming and omitting when data.context.trim() is empty.

Suggested change
const contextBlock = data.context
? `\n <p class="swot-context"><em>${escapeHtml(lbl('swotContext'))}:</em> ${escapeHtml(data.context)}</p>`
const rawContext = data.context?.trim();
const contextBlock = rawContext
? `\n <p class="swot-context"><em>${escapeHtml(lbl('swotContext'))}:</em> ${escapeHtml(rawContext)}</p>`

Copilot uses AI. Check for mistakes.
styles.css Outdated
Comment on lines +9444 to +9448
@media (max-width: 600px) {
.news-article .swot-grid {
grid-template-columns: 1fr;
}
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section introduces a new @media (max-width: 600px) breakpoint, while the rest of the stylesheet predominantly uses 768px/767px/480px breakpoints for responsive layout. To keep responsive behavior consistent, consider aligning this breakpoint with the existing mobile breakpoint used elsewhere in the file (e.g. 768px).

Copilot uses AI. Check for mistakes.
const chart = makeChart({
annotations: [
// Use an unknown type to verify the switch default branch
{ type: 'ellipse' as 'line', value: 10 },
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses type: 'ellipse' as 'line', which is an intentionally incorrect cast and can be confusing to readers (it suggests the runtime value is 'line'). Consider casting through unknown/any (or constructing a DashboardAnnotation with a looser type) so the intent is explicit without misrepresenting the value's type.

Suggested change
{ type: 'ellipse' as 'line', value: 10 },
{ type: 'ellipse' as unknown as 'line', value: 10 },

Copilot uses AI. Check for mistakes.
@pethers
Copy link
Member

pethers commented Mar 5, 2026

@copilot apply changes based on the comments in this thread

@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copilot AI and others added 2 commits March 5, 2026 16:03
Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
…nly subjects

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 5, 2026

@copilot apply changes based on the comments in this thread

Applied all 6 review comments in beed54b + 395b3d6:

  1. Dashboard title trim — Whitespace-only data.title now falls back to localized label.
  2. Dashboard summary trim — Whitespace-only data.summary omitted instead of rendering empty <p>.
  3. SWOT subject trim — Whitespace-only data.subject omitted; uses trimmedSubject in escapeHtml().
  4. SWOT context trim — Whitespace-only data.context omitted instead of rendering empty paragraph.
  5. Breakpoint alignment — Changed @media (max-width: 600px)768px to match codebase conventions.
  6. Test cast — Changed 'ellipse' as 'line''ellipse' as unknown as 'line' for explicit intent.

40 tests passing. CodeQL: 0 alerts.

@pethers pethers requested a review from Copilot March 5, 2026 16:09
@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Comment on lines +226 to +230
/** A point object for scatter/bubble charts */
export interface DashboardPoint {
x: number;
y: number;
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc for DashboardPoint says it is for “scatter/bubble charts”, but DashboardChartType doesn’t include bubble and DashboardPoint lacks the r radius field bubble datasets require. Update the comment to only mention scatter (or add proper bubble support end-to-end).

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +33
function impactBadge(impact: SwotImpact | undefined): string {
if (!impact) return '';
const impactClass = IMPACT_CLASSES[impact] ?? IMPACT_CLASSES.medium;
return ` <span class="swot-impact ${impactClass}">[${escapeHtml(impact)}]</span>`;
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impact badges currently render raw enum values ([high], [medium], [low]) which will appear in English even when lang is non-English, and will be read literally by screen readers. Consider localising these labels via L(lang, ...) (e.g., add swotImpactHigh/Medium/Low keys) or otherwise making the badge text language-appropriate.

Copilot uses AI. Check for mistakes.
@pethers
Copy link
Member

pethers commented Mar 5, 2026

@copilot apply changes based on the comments in this thread

@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

1. DashboardPoint JSDoc: removed "bubble" mention since DashboardChartType
   doesn't include 'bubble' and DashboardPoint lacks the 'r' field.
2. Impact badges: now use localised labels via swotImpactHigh/Medium/Low
   content-label keys across all 14 languages instead of raw English enums.

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 5, 2026

@copilot apply changes based on the comments in this thread

Applied both review comments in 0baf9ff:

  1. DashboardPoint JSDoc — Updated to mention only scatter charts; removed "bubble" since DashboardChartType excludes 'bubble' and DashboardPoint lacks the r field.
  2. Impact badge localisation — Added swotImpactHigh/swotImpactMedium/swotImpactLow label keys across all 14 languages. impactBadge() now uses lbl() to render localised text (e.g. [Hög] for Swedish) instead of raw English enums.

41 tests passing. CodeQL: 0 alerts.

@pethers pethers requested a review from Copilot March 5, 2026 16:39
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@pethers pethers marked this pull request as ready for review March 5, 2026 16:57
@pethers pethers merged commit 3e0af1c into main Mar 5, 2026
@pethers pethers deleted the copilot/add-swot-dashboard-capabilities branch March 5, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

html-css HTML/CSS changes refactor Code refactoring size-xl Extra large change (> 1000 lines) testing Test coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants