Add vertical bottom-up timeline with center-out streams and smooth layout transitions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila KhodjaefandCursor committed 2026-08-11 21:31:30 +03:00
1 parent 4088d7d57b
commit 971c1e8dd8
9 files changed
+1695 -41

No files matched your search

+39
View File
@@ -24,6 +24,45 @@
overflow: hidden;
}
.home-timeline-stack-vertical {
flex-direction: row;
align-items: stretch;
}
.home-movements-section-vertical {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
}
.site-layout-switch {
position: absolute;
top: 16px;
left: 16px;
z-index: 120;
}
.site-layout-link {
padding: 6px 12px;
border-radius: 6px;
border: 1px solid rgba(201, 169, 110, 0.35);
background: rgba(15, 15, 26, 0.85);
color: rgba(201, 169, 110, 0.9);
font-size: 12px;
font-family: 'Georgia', serif;
cursor: pointer;
text-decoration: none;
}
.site-layout-link:hover {
background: rgba(201, 169, 110, 0.18);
color: #f5e6c8;
}
.gallery-session-suspended {
position: fixed;
inset: 0;
+98 -33
View File
@@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next';
import Timeline from '../components/Timeline';
import TimelineEventGuides from '../components/TimelineEventGuides';
import MovementBands from '../components/MovementBands';
import VerticalTimeline from '../components/VerticalTimeline';
import VerticalMovementBands from '../components/VerticalMovementBands';
import VirtualGallery from '../components/VirtualGallery';
import PaintingDetailView from '../components/PaintingDetail';
import ArtistBio from '../components/ArtistBio';
@@ -47,6 +49,7 @@ import './HomePage.css';
type View =
| { type: 'timeline' }
| { type: 'timeline-vertical' }
| { type: 'checkup' }
| { type: 'translations' }
| { type: 'influences' }
@@ -207,7 +210,7 @@ export default function HomePage() {
setGallerySession({ kind: 'movement', movementId: view.movementId, data: view.data });
} else if (view.type === 'tour-gallery') {
setGallerySession({ kind: 'tour', tourId: view.tourId, data: view.data });
} else if (view.type === 'timeline') {
} else if (view.type === 'timeline' || view.type === 'timeline-vertical') {
setGallerySession(null);
}
}, [view]);
@@ -271,6 +274,17 @@ export default function HomePage() {
setView({ type: 'timeline' });
}, [bounds.min, bounds.max]);
const openHorizontalTimeline = () => {
setView({ type: 'timeline' });
};
const openVerticalTimeline = () => {
setView({ type: 'timeline-vertical' });
};
const isTimelineHome = view.type === 'timeline' || view.type === 'timeline-vertical';
const isVerticalTimeline = view.type === 'timeline-vertical';
const toggleDebugMode = () => {
setDebugMode((prev) => {
const next = !prev;
@@ -1204,9 +1218,30 @@ export default function HomePage() {
/>
)}
{view.type === 'timeline' && (
{isTimelineHome && (
<div className="home-page">
<header className="site-header">
<div className="site-layout-switch">
{isVerticalTimeline ? (
<button
type="button"
className="site-layout-link"
onClick={openHorizontalTimeline}
title="Switch to classic left-to-right timeline"
>
{t('layoutHorizontal')}
</button>
) : (
<button
type="button"
className="site-layout-link"
onClick={openVerticalTimeline}
title="Switch to bottom-up vertical timeline"
>
{t('layoutVertical')}
</button>
)}
</div>
<div className="site-dev-tools">
{isCurator ? (
<>
@@ -1334,7 +1369,7 @@ export default function HomePage() {
/>
</header>
<div className="home-timeline-stack">
<div className={`home-timeline-stack${isVerticalTimeline ? ' home-timeline-stack-vertical' : ''}`}>
{loading && (
<GalleryLoadingMarker overlay message="Loading art history…" />
)}
@@ -1345,37 +1380,67 @@ export default function HomePage() {
<GalleryLoadingMarker banner message="Loading portraits…" />
)}
<Timeline
eras={timelineData.eras}
viewStart={viewStart}
viewEnd={viewEnd}
onViewChange={handleViewChange}
absoluteMin={bounds.min}
absoluteMax={bounds.max}
lifespanHighlight={hoveredLifespan}
/>
{error && <div className="error-banner">{error}</div>}
{!loading && (
{isVerticalTimeline ? (
<>
<TimelineEventGuides viewStart={viewStart} viewEnd={viewEnd} />
<div className="home-movements-section">
<MovementBands
movements={timelineData.movements}
artists={artists}
portraitRevisions={portraitRevisions}
viewStart={viewStart}
viewEnd={viewEnd}
absoluteMin={bounds.min}
absoluteMax={bounds.max}
onViewChange={handleViewChange}
onArtistClick={handleArtistClick}
onMovementClick={handleMovementClick}
onArtistHover={setHoveredLifespan}
onPortraitsLoadingChange={setPortraitsLoading}
/>
</div>
<VerticalTimeline
eras={timelineData.eras}
viewStart={viewStart}
viewEnd={viewEnd}
onViewChange={handleViewChange}
absoluteMin={bounds.min}
absoluteMax={bounds.max}
lifespanHighlight={hoveredLifespan}
/>
{error && <div className="error-banner">{error}</div>}
{!loading && (
<div className="home-movements-section-vertical">
<VerticalMovementBands
movements={timelineData.movements}
viewStart={viewStart}
viewEnd={viewEnd}
absoluteMin={bounds.min}
absoluteMax={bounds.max}
onViewChange={handleViewChange}
onMovementClick={handleMovementClick}
/>
</div>
)}
</>
) : (
<>
<Timeline
eras={timelineData.eras}
viewStart={viewStart}
viewEnd={viewEnd}
onViewChange={handleViewChange}
absoluteMin={bounds.min}
absoluteMax={bounds.max}
lifespanHighlight={hoveredLifespan}
/>
{error && <div className="error-banner">{error}</div>}
{!loading && (
<>
<TimelineEventGuides viewStart={viewStart} viewEnd={viewEnd} />
<div className="home-movements-section">
<MovementBands
movements={timelineData.movements}
artists={artists}
portraitRevisions={portraitRevisions}
viewStart={viewStart}
viewEnd={viewEnd}
absoluteMin={bounds.min}
absoluteMax={bounds.max}
onViewChange={handleViewChange}
onArtistClick={handleArtistClick}
onMovementClick={handleMovementClick}
onArtistHover={setHoveredLifespan}
onPortraitsLoadingChange={setPortraitsLoading}
/>
</div>
</>
)}
</>
)}
</div>