feat(05-03): ResearchDisplay component + dark-theme encyclopedia UI

- Add ResearchDisplay.jsx: pure presentational component for research
  data with loading skeleton, accessible error state, and source cards
- Refactor ExerciseResearchPanel to delegate rendering to ResearchDisplay
  (separates fetch/state logic from display)
- Add ExerciseEncyclopediaPage.css: full dark-theme stylesheet using
  CSS variables (--bg-*, --text-*, --accent, --border, --radius-*)
  replacing the light-theme WorkoutEditPage.css import
- Update ExerciseEncyclopediaPage.jsx: new semantic class names,
  keyboard-accessible card toggle (Enter key + role=button + aria-expanded)
- Mobile-responsive at 600px breakpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 20:38:14 +01:00
parent 210a2d15a9
commit 6472eb8c6c
4 changed files with 751 additions and 128 deletions
+104
View File
@@ -0,0 +1,104 @@
function ResearchLoadingSkeleton({ exerciseName }) {
return (
<div className="rd-loading">
<div className="rd-spinner" aria-hidden="true" />
<span className="rd-loading-text">
Searching for information on <em>{exerciseName}</em>
</span>
</div>
)
}
function ResearchError({ message, onDismiss }) {
return (
<div className="rd-error" role="alert">
<span className="rd-error-icon" aria-hidden="true"></span>
<span className="rd-error-message">{message}</span>
{onDismiss && (
<button className="rd-dismiss" onClick={onDismiss} aria-label="Dismiss error">
×
</button>
)}
</div>
)
}
function ResearchSourceCard({ result, index }) {
return (
<li className="rd-source-item">
<a
href={result.url}
target="_blank"
rel="noopener noreferrer"
className="rd-source-link"
>
<span className="rd-source-index">{index + 1}</span>
<span className="rd-source-title">{result.title}</span>
<span className="rd-source-arrow" aria-hidden="true"></span>
</a>
{result.snippet && (
<p className="rd-source-snippet">{result.snippet}</p>
)}
</li>
)
}
/**
* ResearchDisplay — pure presentational component.
*
* Props:
* loading {boolean} Show loading skeleton
* error {string} Error message to display
* data {object} Research data: { summary, results }
* name {string} Exercise name (shown during loading)
* onDismiss {function} Clear error callback
*/
function ResearchDisplay({ loading, error, data, name, onDismiss }) {
if (loading) {
return <ResearchLoadingSkeleton exerciseName={name} />
}
if (error) {
return <ResearchError message={error} onDismiss={onDismiss} />
}
if (!data) return null
const hasSummary = Boolean(data.summary)
const hasSources = Array.isArray(data.results) && data.results.length > 0
return (
<div className="rd-results">
{hasSummary && (
<div className="rd-summary">
<h4 className="rd-section-title">
<span className="rd-section-icon" aria-hidden="true">📋</span>
Summary
</h4>
<p className="rd-summary-text">{data.summary}</p>
</div>
)}
{hasSources && (
<div className="rd-sources">
<h4 className="rd-section-title">
<span className="rd-section-icon" aria-hidden="true">🔗</span>
Sources
<span className="rd-count">{data.results.length}</span>
</h4>
<ul className="rd-sources-list" aria-label="Research sources">
{data.results.map((result, i) => (
<ResearchSourceCard key={i} result={result} index={i} />
))}
</ul>
</div>
)}
{!hasSummary && !hasSources && (
<p className="rd-empty">No research data found for this exercise.</p>
)}
</div>
)
}
export default ResearchDisplay