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 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 { usePlatform } from '@/app/kosmos/KosmosContext'
|
||||||
import { RecollectionMenubarProvider } from './RecollectionMenubarContext'
|
import { RecollectionMenubarProvider } from './RecollectionMenubarContext'
|
||||||
import { RecollectionMenubar } from './RecollectionMenubar'
|
import { RecollectionMenubar } from './RecollectionMenubar'
|
||||||
|
import { FlippingCardView } from './FlippingCardView'
|
||||||
|
|
||||||
export function RecollectionLayout() {
|
export function RecollectionLayout() {
|
||||||
const { recollectionId } = useParams<{ recollectionId: string }>()
|
const { recollectionId } = useParams<{ recollectionId: string }>()
|
||||||
@@ -35,7 +36,7 @@ export function RecollectionLayout() {
|
|||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
<RecollectionMenubar />
|
<RecollectionMenubar />
|
||||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||||
<Outlet />
|
<FlippingCardView />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RecollectionMenubarProvider>
|
</RecollectionMenubarProvider>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
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 { usePlatform } from '@/app/kosmos/KosmosContext'
|
||||||
import { ArrowLeft } from 'lucide-react'
|
import { ArrowLeft } from 'lucide-react'
|
||||||
import { FluxIcon, LogosIcon } from '@/lib/icons'
|
import { FluxIcon, LogosIcon } from '@/lib/icons'
|
||||||
@@ -11,14 +11,17 @@ import { useRecollectionMenubar } from './RecollectionMenubarContext'
|
|||||||
|
|
||||||
export function RecollectionMenubar() {
|
export function RecollectionMenubar() {
|
||||||
const { recollectionId } = useParams<{ recollectionId: string }>()
|
const { recollectionId } = useParams<{ recollectionId: string }>()
|
||||||
|
const { pathname } = useLocation()
|
||||||
const { recollections } = usePlatform()
|
const { recollections } = usePlatform()
|
||||||
const { customContent } = useRecollectionMenubar()
|
const { customContent } = useRecollectionMenubar()
|
||||||
|
|
||||||
|
const isFlux = pathname.endsWith('/flux')
|
||||||
const recollection = recollectionId ? recollections.find((p) => p.id === recollectionId) : null
|
const recollection = recollectionId ? recollections.find((p) => p.id === recollectionId) : null
|
||||||
const base = recollectionId ? `/recollections/${recollectionId}` : ''
|
const base = recollectionId ? `/recollections/${recollectionId}` : ''
|
||||||
const defaultMiddle = recollection ? (
|
const defaultMiddle = recollection ? (
|
||||||
<span className="truncate text-sm font-medium text-muted-foreground">{recollection.name}</span>
|
<span className="truncate text-sm font-medium text-muted-foreground">{recollection.name}</span>
|
||||||
) : null
|
) : null
|
||||||
|
const middleContent = isFlux ? (customContent ?? defaultMiddle) : defaultMiddle
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-9 w-full shrink-0 items-center gap-2 border-b border-border/40 bg-background px-2">
|
<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" />
|
<ArrowLeft className="size-4" />
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex min-w-0 flex-1 items-center">
|
<div className="flex min-w-0 flex-1 items-center">
|
||||||
{customContent ?? defaultMiddle}
|
{middleContent}
|
||||||
</div>
|
</div>
|
||||||
{base && (
|
{base && (
|
||||||
<nav className="flex shrink-0 items-center gap-0.5" aria-label="Recollection pages">
|
<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 { KosmosPage } from './app/kosmos/KosmosPage'
|
||||||
import { RecollectionsPage } from './app/recollections/RecollectionsPage'
|
import { RecollectionsPage } from './app/recollections/RecollectionsPage'
|
||||||
import { RecollectionLayout } from './app/recollections/RecollectionLayout'
|
import { RecollectionLayout } from './app/recollections/RecollectionLayout'
|
||||||
import { LogosPage } from './app/recollections/logos/LogosPage'
|
|
||||||
import { FluxRoute } from './app/recollections/flux/FluxRoute'
|
|
||||||
import './lib/prismSetup'
|
import './lib/prismSetup'
|
||||||
import 'prismjs/themes/prism.css'
|
import 'prismjs/themes/prism.css'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
@@ -28,8 +26,8 @@ createRoot(document.getElementById('root')!).render(
|
|||||||
<Route path="recollections" element={<RecollectionsPage />} />
|
<Route path="recollections" element={<RecollectionsPage />} />
|
||||||
<Route path="recollections/:recollectionId" element={<RecollectionLayout />}>
|
<Route path="recollections/:recollectionId" element={<RecollectionLayout />}>
|
||||||
<Route index element={<Navigate to="logos" replace />} />
|
<Route index element={<Navigate to="logos" replace />} />
|
||||||
<Route path="logos" element={<LogosPage />} />
|
<Route path="logos" element={<></>} />
|
||||||
<Route path="flux" element={<FluxRoute />} />
|
<Route path="flux" element={<></>} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -394,3 +394,40 @@ pre {
|
|||||||
.code-editor .token.url {
|
.code-editor .token.url {
|
||||||
background: none;
|
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