diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 0bb21d7c..a99ba409 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -82,6 +82,8 @@ impact_score = priority_weight + (gsc_clicks × 10) + (ga4_sessions × 5) Priority weights: Critical = 1000, High = 100, Medium = 10, Low = 1. +**UI hints:** Metric explanations in the app (circled **?** tooltips on KPIs, table headers, and chart titles) are sourced from `web/src/strings.json` under `metricHelp`. Shared keys live in `metricHelp.shared.*` (e.g. `metricHelp.shared.impactScore`); view-specific keys under `metricHelp.views.{viewId}.*`. Keep glossary definitions and `metricHelp` copy aligned when changing formulas or data sources. + --- ## Provenance badges diff --git a/web/src/components/ChartCard.tsx b/web/src/components/ChartCard.tsx new file mode 100644 index 00000000..9b25c131 --- /dev/null +++ b/web/src/components/ChartCard.tsx @@ -0,0 +1,40 @@ +'use client'; + +import type { ReactNode } from 'react'; +import HelpHint, { normalizeHintContent, type HelpHintContent } from './HelpHint'; + +export interface ChartCardProps { + title: string; + hint?: HelpHintContent; + ariaLabel?: string; + heightClass?: string; + children?: ReactNode; + className?: string; +} + +export default function ChartCard({ + title, + hint, + ariaLabel, + heightClass = 'h-56', + children, + className = '', +}: ChartCardProps) { + const hintContent = normalizeHintContent(hint); + + return ( +
{description}
+ ) : null} ++
{icon}
- {label}
+ {label}
+ {hintContent ? (
+
{value ?? '—'}
{sub ?{sub}
: null} diff --git a/web/src/components/Table.tsx b/web/src/components/Table.tsx index 8c3f9ef3..ac2170c0 100644 --- a/web/src/components/Table.tsx +++ b/web/src/components/Table.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from 'react'; +import HelpHint, { normalizeHintContent, type HelpHintContent } from './HelpHint'; interface TableProps { children?: ReactNode; @@ -31,9 +32,32 @@ export const TableHead = ({ children, sticky = false }: TableHeadProps) => ( ); -export const TableHeadCell = ({ children, className = '', title }: { children?: ReactNode; className?: string; title?: string }) => ( -{hint}
: } -{hint}
} -| toggle(col.key)}
className="px-3 py-2 text-left text-xs font-bold text-muted-foreground uppercase tracking-wider cursor-pointer select-none hover:text-foreground whitespace-nowrap"
>
- {col.label}
+
+ {col.label}
+ {hintContent ? (
+ e.stopPropagation()}
+ onKeyDown={(e) => e.stopPropagation()}
+ >
+ |
- ))}
+ );
+ })}
{label}
+ {hintContent ? (
+ e.stopPropagation()}
+ onKeyDown={(e) => e.stopPropagation()}
+ >
+
+ {statusChart ? (
+
+ );
+}
diff --git a/web/src/components/links/explorer/LinksExplorerTableTab.tsx b/web/src/components/links/explorer/LinksExplorerTableTab.tsx
index b8e2f78d..aa15535f 100644
--- a/web/src/components/links/explorer/LinksExplorerTableTab.tsx
+++ b/web/src/components/links/explorer/LinksExplorerTableTab.tsx
@@ -4,6 +4,8 @@ import { type MouseEvent, type RefObject } from 'react';
import { Search, ExternalLink } from 'lucide-react';
import type { ReportLink } from '@/types';
import { strings, format } from '@/lib/strings';
+import { metricHelpHint } from '@/lib/metricHelp';
+import HelpHint, { normalizeHintContent } from '@/components/HelpHint';
import { Card, Badge, Button } from '@/components';
import { formatMs, rtColor, formatPageHrefLines } from '@/utils/linkUtils';
import { linkHasBrowserErrors } from '@/lib/browserErrors';
@@ -13,6 +15,7 @@ import SavedCrawlFiltersBar from '@/components/links/SavedCrawlFiltersBar';
import type { LinksFilterValues } from '@/components/links/LinksFilterBar';
import type { LinkSortKey } from './types';
import { LinksExplorerTabPanel } from './LinksExplorerTabPanel';
+import { LinksExplorerSummaryCharts } from './LinksExplorerSummaryCharts';
export interface LinksExplorerTableTabProps {
filterValues: LinksFilterValues;
@@ -69,6 +72,8 @@ export function LinksExplorerTableTab({
const sj = strings.common;
const hasCustomExtract = links.some((l) => l.custom_extract);
const customFieldKeys = collectCustomFieldKeys(links);
+ const customExtractHint = normalizeHintContent(metricHelpHint('views.links.explorerExtract'));
+ const jsErrorsHint = normalizeHintContent(metricHelpHint('views.links.explorerJsErrors'));
return (
+
+
- {vl.thCustomExtract}
+
+ {vl.thCustomExtract}
+ {customExtractHint ? (
+ |
) : null}
{customFieldKeys.map((key) => (
@@ -146,7 +183,14 @@ export function LinksExplorerTableTab({
|
))}
- {vl.thJsErrors}
+
+ {vl.thJsErrors}
+ {jsErrorsHint ? (
+ |
{vl.thActions}
diff --git a/web/src/components/links/explorer/index.ts b/web/src/components/links/explorer/index.ts
index c3ce7477..295e972f 100644
--- a/web/src/components/links/explorer/index.ts
+++ b/web/src/components/links/explorer/index.ts
@@ -1,4 +1,5 @@
export type { LinkSortKey } from './types';
+export { LinksExplorerSummaryCharts } from './LinksExplorerSummaryCharts';
export type { LinksExplorerTabId } from './LinksExplorerTabPanel';
export { LinksExplorerAnchorsTab } from './LinksExplorerAnchorsTab';
export { LinksExplorerTabPanel } from './LinksExplorerTabPanel';
diff --git a/web/src/components/links/tabs/ContentTab.tsx b/web/src/components/links/tabs/ContentTab.tsx
index ed110c80..335a2e42 100644
--- a/web/src/components/links/tabs/ContentTab.tsx
+++ b/web/src/components/links/tabs/ContentTab.tsx
@@ -1,10 +1,10 @@
-import { useMemo, useState, createElement, type ComponentType } from 'react';
+import { useMemo, useState } from 'react';
import Link from 'next/link';
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, type TooltipItem } from 'chart.js';
import { BookOpen, BarChart2, Check, FileText, Layers, Share2, Tag, X } from 'lucide-react';
import type { LinkDetail } from '@/types/report';
import { useReport } from '../../../context/useReport';
-import { Card } from '../../../components';
+import { Card, SectionHeader, LabelWithHint, ChartTitleWithHint } from '../../../components';
import { RatioBar, RankedBarChart } from '../../../components/charts';
import { wcLabel, readingLabel, parseKeywords, normaliseKw } from '../../../utils/linkUtils';
import { PALETTE_CATEGORICAL } from '../../../utils/chartPalette';
@@ -101,26 +101,6 @@ function h1QualityIndex(count: number | string | undefined): number {
return 2;
}
-interface SectionHeaderProps {
- icon: ComponentType<{ className?: string }>;
- title: string;
- description?: string;
-}
-
-function SectionHeader({ icon, title, description }: SectionHeaderProps) {
- return (
-
-
- );
-}
-
function QualityStatusRow({
label,
detail,
@@ -249,7 +229,8 @@ export default function ContentTab({ link }: ContentTabProps) {
- {createElement(icon, { className: 'h-4 w-4 text-link' })}
-
-
-
- {title}- {description &&{description} } -
-
{wc.toLocaleString()}
{wcInfo.label}
@@ -262,7 +243,8 @@ export default function ContentTab({ link }: ContentTabProps) {
-
{rl > 0 ? format(lo.readingGrade, { n: rl }) : sj.emDash}
@@ -276,7 +258,8 @@ export default function ContentTab({ link }: ContentTabProps) {
-
{link.depth != null ? link.depth : sj.emDash}
{lc.crawlDepth}
@@ -291,11 +274,10 @@ export default function ContentTab({ link }: ContentTabProps) {
- {lc.wordCountComparison}-{lc.vsSiteAggregates} +
{compareBarData.values.length > 0 ? (
- {String(link.content_excerpt).trim()} @@ -402,7 +384,7 @@ export default function ContentTab({ link }: ContentTabProps) { )}
- {o.crawlHeading}
- {crawlStats.map(({ label, value, raw }) => (
-
+ {crawlStats.map(({ key, label, value, raw }) => (
+ {label}
{raw ? value : {value}}
diff --git a/web/src/components/overview/OverviewChartsTab.tsx b/web/src/components/overview/OverviewChartsTab.tsx
index 176f84b4..13ec940c 100644
--- a/web/src/components/overview/OverviewChartsTab.tsx
+++ b/web/src/components/overview/OverviewChartsTab.tsx
@@ -3,7 +3,8 @@
import { BarChart3 } from 'lucide-react';
import { Bar } from 'react-chartjs-2';
import { strings, format } from '@/lib/strings';
-import { Card, StatCard } from '@/components';
+import { Card, StatCard, ChartTitleWithHint } from '@/components';
+import { metricHelpHint } from '@/lib/metricHelp';
import { StatusDistributionChart, LighthouseScoreGrid } from '@/components/charts';
import { ChartPanel } from '@/components/charts';
import { barOptionsHorizontal } from '@/utils/chartJsDefaults';
@@ -72,29 +73,25 @@ export function OverviewChartsTab({ charts, depth }: OverviewChartsTabProps) {
{statusDistribution && (
{vo.statusDist}-{vo.statusDistHint} +{vo.contentDepth}-{vo.contentDepthHint} +{vo.serverLatency}-{vo.serverLatencyHint} +{vo.crawlDepth}-{vo.crawlDepthHint} +
{format(vo.depthSummaryLine, {
maxDepth: depth.max_depth ?? sj.emDash,
@@ -110,8 +107,7 @@ export function OverviewChartsTab({ charts, depth }: OverviewChartsTabProps) {
)}
{titleMetaChart && (
{vo.titleMetaHealth}-{vo.titleMetaHint} +{vo.socialPreview}-{vo.socialPreviewHint} +
{socialStats.og != null && (
-
{vo.readingLevel}-{vo.readingLevelHint} +{vo.topMime}-{vo.topMimeHint} +{vo.outlinksTitle}-{vo.outlinksHint} +{vo.topDomains}-{vo.topDomainsHint} +{vo.lhCategoryScores}-{vo.lhCategoryHint} +
{googleData.gsc ? (
<>
-
@@ -252,20 +269,20 @@ export function OverviewSummaryTab({ data, exportHref, compareHref, reportCount
-
{crawledCount.toLocaleString()}
{s.avg_outlinks ?? 0} {vo.avgOutlinks}
-
{s.success_rate ?? 0}%
-
{brokenCount}
@@ -274,7 +291,7 @@ export function OverviewSummaryTab({ data, exportHref, compareHref, reportCount
-
{h1Zero}
-
{data.content_analytics?.word_count_stats?.median != null
@@ -294,7 +311,7 @@ export function OverviewSummaryTab({ data, exportHref, compareHref, reportCount
-
{data.social_coverage?.og_coverage_pct != null ? `${data.social_coverage.og_coverage_pct}%` : sj.emDash}
@@ -312,7 +329,7 @@ export function OverviewSummaryTab({ data, exportHref, compareHref, reportCount
-
{data.response_time_stats?.p50 != null ? `${Math.round(data.response_time_stats.p50)}ms` : sj.emDash}
diff --git a/web/src/components/portfolio/PortfolioPropertyCard.tsx b/web/src/components/portfolio/PortfolioPropertyCard.tsx
index a7ee4f3e..95fa2571 100644
--- a/web/src/components/portfolio/PortfolioPropertyCard.tsx
+++ b/web/src/components/portfolio/PortfolioPropertyCard.tsx
@@ -8,7 +8,7 @@ import {
Timer,
Trash2,
} from 'lucide-react';
-import { Card } from '@/components';
+import { Card, LabelWithHint } from '@/components';
import Sparkline, { type SparklineMode } from '@/components/Sparkline';
import { DataSourceBadgeRow } from '@/components/DataSourceBadge';
import { PRIORITY_CONFIG } from '@/lib/issuePriority';
@@ -42,18 +42,22 @@ export interface PortfolioPropertyCardProps {
function PortfolioTrendCell({
label,
+ helpKey,
values,
displayValue,
mode,
}: {
label: string;
+ helpKey?: string;
values: number[];
displayValue: string;
mode: SparklineMode;
}) {
return (
- {label} +
+ {helpKey ?
- {group.crawlOnly ? vh.titleCoverageLabel : vh.healthScoreLabel}
+ {group.crawlOnly ? (
+
{group.crawlOnly && trends.titleTrend.length >= 1 ? (
@@ -267,13 +275,17 @@ export default function PortfolioPropertyCard({
-
{vh.urlCountLabel} +
+ {group.urlCount.toLocaleString()}
- {vh.titleCoverageLabel} +
+ {group.titleCoverage != null ? `${group.titleCoverage}%` : sj.emDash} @@ -319,7 +331,9 @@ export default function PortfolioPropertyCard({
- {vh.urlCountLabel} +
+ {group.urlCount.toLocaleString()} {group.medianWordCount != null ? (@@ -335,7 +349,7 @@ export default function PortfolioPropertyCard({
{group.totalIssues.toLocaleString()}
@@ -360,11 +374,15 @@ export default function PortfolioPropertyCard({
- );
-}
-
function useTopBarChart(
rows: Array
- {vh.perfScoreLabel}
+
+
- {vh.seoScoreLabel}
+
+ {title}- {hint &&{hint} } -
- {children}
-
- |
|---|