feat: flip card transition
This commit is contained in:
35
frontend/src/app/recollections/FlippingCardView.tsx
Normal file
35
frontend/src/app/recollections/FlippingCardView.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user