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:
Danila Khodjaef
2026-06-21 16:54:19 +03:00
co-authored by Cursor
parent 0972b5df99
commit df29848d89
121 changed files with 4765 additions and 396 deletions
@@ -1,7 +1,57 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { debugImageProxyUrl, type DebugImageSearchManyResult, type DebugImageSearchResultItem } from '../api/client';
import './DebugSearchResultsModal.css';
function formatResolution(width?: number, height?: number): string | null {
if (!width || !height || width <= 0 || height <= 0) return null;
return `${width} × ${height}`;
}
function ResultResolution({
item,
searchUrl,
}: {
item: DebugImageSearchResultItem;
searchUrl: string;
}) {
const initial = formatResolution(item.width, item.height);
const [label, setLabel] = useState<string | null>(initial);
useEffect(() => {
if (initial) {
setLabel(initial);
return;
}
let cancelled = false;
const img = new Image();
img.onload = () => {
if (cancelled) return;
const text = formatResolution(img.naturalWidth, img.naturalHeight);
setLabel(text ?? '—');
};
img.onerror = () => {
if (!cancelled) setLabel('—');
};
img.src = debugImageProxyUrl(item.imageUrl, {
searchUrl,
source: item.source,
});
return () => {
cancelled = true;
img.onload = null;
img.onerror = null;
};
}, [item.imageUrl, item.source, item.width, item.height, searchUrl, initial]);
return (
<span className="debug-search-modal-item-resolution" aria-hidden="true">
{label ?? '…'}
</span>
);
}
interface Props {
open: boolean;
title: string;
@@ -76,19 +126,22 @@ export default function DebugSearchResultsModal({
onClick={() => onSelect(item)}
title="Use this image"
>
<img
src={debugImageProxyUrl(item.thumbUrl || item.imageUrl, {
searchUrl: data.searchUrl,
source: item.source,
})}
alt={`Result ${index + 1}`}
loading="lazy"
onError={(e) => {
(e.target as HTMLImageElement).src = '/placeholder-art.svg';
}}
/>
<span className="debug-search-modal-item-index">{index + 1}</span>
{busy && <span className="debug-search-modal-item-busy-label">Saving</span>}
<span className="debug-search-modal-item-media">
<img
src={debugImageProxyUrl(item.thumbUrl || item.imageUrl, {
searchUrl: data.searchUrl,
source: item.source,
})}
alt={`Result ${index + 1}`}
loading="lazy"
onError={(e) => {
(e.target as HTMLImageElement).src = '/placeholder-art.svg';
}}
/>
<span className="debug-search-modal-item-index">{index + 1}</span>
{busy && <span className="debug-search-modal-item-busy-label">Saving</span>}
</span>
<ResultResolution item={item} searchUrl={data.searchUrl} />
</button>
);
})}