feat: flip card transition

This commit is contained in:
2026-03-15 20:19:00 +01:00
parent 202462bad7
commit a8bd579043
5 changed files with 83 additions and 9 deletions

View File

@@ -0,0 +1,35 @@
/**
* Flipping card container: Logos on the front, Flux on the back.
* Uses CSS 3D transform for a performant flip animation when switching via the menubar.
*/
import React from 'react'
import { useLocation, useParams } from 'react-router-dom'
import { LogosPage } from './logos/LogosPage'
import { FluxRoute } from './flux/FluxRoute'
export function FlippingCardView() {
const { pathname } = useLocation()
const { recollectionId } = useParams<{ recollectionId: string }>()
const isFlux = pathname.endsWith('/flux')
return (
<div
className="recollection-flip-container"
style={{ perspective: '1200px' }}
>
<div
className="recollection-flip-card"
data-flipped={isFlux}
aria-hidden={false}
>
<div className="recollection-flip-face recollection-flip-face-front">
<LogosPage />
</div>
<div className="recollection-flip-face recollection-flip-face-back">
{recollectionId && <FluxRoute />}
</div>
</div>
</div>
)
}

View File

@@ -1,12 +1,13 @@
/**
* Layout for a single recollection: shared menubar (with optional custom content from active page) and outlet.
* Layout for a single recollection: shared menubar and flipping card (Logos front / Flux back).
*/
import React, { useEffect } from 'react'
import { Link, Outlet, useParams } from 'react-router-dom'
import { Link, useParams } from 'react-router-dom'
import { usePlatform } from '@/app/kosmos/KosmosContext'
import { RecollectionMenubarProvider } from './RecollectionMenubarContext'
import { RecollectionMenubar } from './RecollectionMenubar'
import { FlippingCardView } from './FlippingCardView'
export function RecollectionLayout() {
const { recollectionId } = useParams<{ recollectionId: string }>()
@@ -35,7 +36,7 @@ export function RecollectionLayout() {
<div className="flex min-h-0 flex-1 flex-col">
<RecollectionMenubar />
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<Outlet />
<FlippingCardView />
</div>
</div>
</RecollectionMenubarProvider>

View File

@@ -3,7 +3,7 @@
*/
import React from 'react'
import { Link, NavLink, useParams } from 'react-router-dom'
import { Link, NavLink, useParams, useLocation } from 'react-router-dom'
import { usePlatform } from '@/app/kosmos/KosmosContext'
import { ArrowLeft } from 'lucide-react'
import { FluxIcon, LogosIcon } from '@/lib/icons'
@@ -11,14 +11,17 @@ import { useRecollectionMenubar } from './RecollectionMenubarContext'
export function RecollectionMenubar() {
const { recollectionId } = useParams<{ recollectionId: string }>()
const { pathname } = useLocation()
const { recollections } = usePlatform()
const { customContent } = useRecollectionMenubar()
const isFlux = pathname.endsWith('/flux')
const recollection = recollectionId ? recollections.find((p) => p.id === recollectionId) : null
const base = recollectionId ? `/recollections/${recollectionId}` : ''
const defaultMiddle = recollection ? (
<span className="truncate text-sm font-medium text-muted-foreground">{recollection.name}</span>
) : null
const middleContent = isFlux ? (customContent ?? defaultMiddle) : defaultMiddle
return (
<div className="flex h-9 w-full shrink-0 items-center gap-2 border-b border-border/40 bg-background px-2">
@@ -30,7 +33,7 @@ export function RecollectionMenubar() {
<ArrowLeft className="size-4" />
</Link>
<div className="flex min-w-0 flex-1 items-center">
{customContent ?? defaultMiddle}
{middleContent}
</div>
{base && (
<nav className="flex shrink-0 items-center gap-0.5" aria-label="Recollection pages">

View File

@@ -8,8 +8,6 @@ import { registerBuiltinConfigTypes } from './lib/graph/configTypes'
import { KosmosPage } from './app/kosmos/KosmosPage'
import { RecollectionsPage } from './app/recollections/RecollectionsPage'
import { RecollectionLayout } from './app/recollections/RecollectionLayout'
import { LogosPage } from './app/recollections/logos/LogosPage'
import { FluxRoute } from './app/recollections/flux/FluxRoute'
import './lib/prismSetup'
import 'prismjs/themes/prism.css'
import './styles.css'
@@ -28,8 +26,8 @@ createRoot(document.getElementById('root')!).render(
<Route path="recollections" element={<RecollectionsPage />} />
<Route path="recollections/:recollectionId" element={<RecollectionLayout />}>
<Route index element={<Navigate to="logos" replace />} />
<Route path="logos" element={<LogosPage />} />
<Route path="flux" element={<FluxRoute />} />
<Route path="logos" element={<></>} />
<Route path="flux" element={<></>} />
</Route>
</Route>
</Routes>

View File

@@ -393,4 +393,41 @@ pre {
.code-editor .token.entity,
.code-editor .token.url {
background: none;
}
/* Recollection flipping card: Logos (front) / Flux (back). GPU-accelerated 3D flip. */
.recollection-flip-container {
flex: 1;
min-height: 0;
overflow: hidden;
}
.recollection-flip-card {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.recollection-flip-card[data-flipped="true"] {
transform: rotateY(-180deg);
}
.recollection-flip-face {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
overflow: hidden;
}
.recollection-flip-face-front {
transform: rotateY(0deg);
}
.recollection-flip-face-back {
transform: rotateY(-180deg);
}