Add movement gallery wings with period interiors and expand timeline features.
Movement galleries split large catalogs into chronological wings (~55 works), use era-themed 3D interiors with side-wall windows, wing navigator on the back exit, and front archways between wings. Also adds painting annotations, timeline event guides, portrait hover highlights, and documentation/API updates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
0972b5df99
commit
df29848d89
@@ -0,0 +1,122 @@
|
||||
import { useRef } from 'react';
|
||||
import type { PaintingAnnotation } from '../types';
|
||||
import './PaintingAnnotations.css';
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
technique: 'Technique',
|
||||
composition: 'Composition',
|
||||
symbolism: 'Symbolism',
|
||||
history: 'History',
|
||||
subject: 'Subject',
|
||||
};
|
||||
|
||||
interface PanelProps {
|
||||
annotations: PaintingAnnotation[];
|
||||
activeId: number | null;
|
||||
onSelect: (id: number | null) => void;
|
||||
}
|
||||
|
||||
export function PaintingAnnotationMarkers({
|
||||
annotations,
|
||||
activeId,
|
||||
onSelect,
|
||||
}: PanelProps) {
|
||||
const positioned = annotations.filter(
|
||||
(a) => a.pos_x != null && a.pos_y != null && Number.isFinite(Number(a.pos_x)) && Number.isFinite(Number(a.pos_y))
|
||||
);
|
||||
if (!positioned.length) return null;
|
||||
|
||||
return (
|
||||
<div className="painting-annotation-markers" aria-hidden={false}>
|
||||
{positioned.map((ann) => {
|
||||
const number = annotations.indexOf(ann) + 1;
|
||||
const isActive = activeId === ann.id;
|
||||
return (
|
||||
<button
|
||||
key={ann.id}
|
||||
type="button"
|
||||
className={`painting-annotation-marker painting-annotation-marker-${ann.category}${isActive ? ' painting-annotation-marker-active' : ''}`}
|
||||
style={{ left: `${ann.pos_x}%`, top: `${ann.pos_y}%` }}
|
||||
title={ann.label || ann.body}
|
||||
aria-label={`Annotation ${number}: ${ann.label || ann.body}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelect(isActive ? null : ann.id);
|
||||
}}
|
||||
>
|
||||
{number}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PaintingAnnotationsPanel({
|
||||
annotations,
|
||||
activeId,
|
||||
onSelect,
|
||||
}: PanelProps) {
|
||||
const cardRefs = useRef<Map<number, HTMLLIElement>>(new Map());
|
||||
|
||||
if (!annotations.length) return null;
|
||||
|
||||
const focusAnnotation = (id: number | null) => {
|
||||
onSelect(id);
|
||||
if (id != null) {
|
||||
cardRefs.current.get(id)?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="painting-annotations-panel" aria-label="Art history annotations">
|
||||
<h3 className="painting-annotations-title">Art history notes</h3>
|
||||
<ul className="painting-annotations-list">
|
||||
{annotations.map((ann, index) => {
|
||||
const isActive = activeId === ann.id;
|
||||
const category = CATEGORY_LABELS[ann.category] || ann.category;
|
||||
return (
|
||||
<li
|
||||
key={ann.id}
|
||||
ref={(el) => {
|
||||
if (el) cardRefs.current.set(ann.id, el);
|
||||
else cardRefs.current.delete(ann.id);
|
||||
}}
|
||||
className={`painting-annotation-card painting-annotation-card-${ann.category}${isActive ? ' painting-annotation-card-active' : ''}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="painting-annotation-card-btn"
|
||||
onClick={() => focusAnnotation(isActive ? null : ann.id)}
|
||||
>
|
||||
<span className="painting-annotation-card-head">
|
||||
<span className="painting-annotation-number">{index + 1}</span>
|
||||
<span className="painting-annotation-category">{category}</span>
|
||||
{ann.label && <strong className="painting-annotation-label">{ann.label}</strong>}
|
||||
</span>
|
||||
<p className="painting-annotation-body">{ann.body}</p>
|
||||
{(ann.source_author || ann.source) && (
|
||||
<p className="painting-annotation-source">
|
||||
— {ann.source_author}
|
||||
{ann.source && <cite>, {ann.source}</cite>}
|
||||
</p>
|
||||
)}
|
||||
</button>
|
||||
{ann.source_url && (
|
||||
<a
|
||||
className="painting-annotation-source-link"
|
||||
href={ann.source_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Read source
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user