freat: initroduce katalogos

This commit is contained in:
2026-03-16 09:30:11 +01:00
parent e64e29d773
commit f6193bf180
5 changed files with 105 additions and 60 deletions

View File

@@ -1,27 +0,0 @@
/**
* Recollection view switcher: Logos or Flux based on pathname.
* Single container, conditional render — only the active view is mounted (no CSS flip).
*/
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="flex min-h-0 flex-1 flex-col overflow-hidden">
{isFlux ? (
recollectionId ? (
<FluxRoute />
) : null
) : (
<LogosPage />
)}
</div>
)
}

View File

@@ -1,15 +1,14 @@
/**
* Layout for a single recollection: shared menubar and flipping card (Logos front / Flux back).
* Layout for a single recollection: shared menubar and nested routes for Logos / Katalogos / Flux.
*/
import React, { useEffect } from 'react'
import { Link, useParams } from 'react-router-dom'
import { Link, Outlet, useParams } from 'react-router-dom'
import { usePlatform } from '@/app/kosmos/KosmosContext'
import { RecollectionMenubarProvider } from './RecollectionMenubarContext'
import { RecollectionActionsProvider } from './RecollectionActionsContext'
import { RecollectionTitleContent } from './RecollectionTitleContent'
import { RecollectionMenubar } from './RecollectionMenubar'
import { FlippingCardView } from './FlippingCardView'
export function RecollectionLayout() {
const { recollectionId } = useParams<{ recollectionId: string }>()
@@ -40,7 +39,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">
<FlippingCardView />
<Outlet />
</div>
</div>
</RecollectionActionsProvider>

View File

@@ -5,21 +5,12 @@
import React, { useCallback, useEffect } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { FluxIcon, LogosIcon } from '@/lib/icons'
import { FluxIcon, LogosIcon, RecollectionsIcon } from '@/lib/icons'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
const VIEW_TOGGLE_KEYS = { key: 'v', shiftKey: true }
function matchKey(ev: KeyboardEvent, want: { key: string; shiftKey: boolean }) {
const mod = ev.ctrlKey || ev.metaKey
return ev.key.toLowerCase() === want.key && !!mod && ev.shiftKey === want.shiftKey
}
function shortcutLabel() {
if (typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform)) {
return '⌘⇧V'
}
return 'Ctrl+Shift+V'
const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform)
return isMac ? '⌘⇧↑ / ⌘⇧↓' : 'Ctrl+Shift+↑ / Ctrl+Shift+↓'
}
export function RecollectionViewSwitcher() {
@@ -28,30 +19,53 @@ export function RecollectionViewSwitcher() {
const navigate = useNavigate()
const base = recollectionId ? `/recollections/${recollectionId}` : ''
const isFlux = pathname.endsWith('/flux')
type Mode = 'logos' | 'katalogos' | 'flux'
const mode: Mode = pathname.endsWith('/flux')
? 'flux'
: pathname.endsWith('/katalogos')
? 'katalogos'
: 'logos'
const goRelative = useCallback(
(delta: 1 | -1) => {
if (!base) return
const order: Mode[] = ['logos', 'katalogos', 'flux']
const currentIndex = order.indexOf(mode)
const nextMode = order[(currentIndex + (delta === 1 ? 1 : order.length - 1)) % order.length]
navigate(`${base}/${nextMode}`)
},
[base, mode, navigate]
)
const toggle = useCallback(() => {
if (!base) return
navigate(isFlux ? `${base}/logos` : `${base}/flux`)
}, [base, isFlux, navigate])
goRelative(1)
}, [goRelative])
useEffect(() => {
const onKeyDown = (ev: KeyboardEvent) => {
if (matchKey(ev, VIEW_TOGGLE_KEYS)) {
ev.preventDefault()
toggle()
const isMod = ev.ctrlKey || ev.metaKey
const isShortcut = isMod && ev.shiftKey && (ev.key === 'ArrowDown' || ev.key === 'ArrowUp')
if (!isShortcut) return
ev.preventDefault()
if (ev.key === 'ArrowDown') {
goRelative(1)
} else if (ev.key === 'ArrowUp') {
goRelative(-1)
}
}
window.addEventListener('keydown', onKeyDown, true)
return () => window.removeEventListener('keydown', onKeyDown, true)
}, [toggle])
}, [goRelative])
if (!base) return null
const shortcut = shortcutLabel()
const tooltipText = isFlux
? `Flux · Switch to Logos (${shortcut})`
: `Logos · Switch to Flux (${shortcut})`
const tooltipText =
mode === 'logos'
? `Logos · Next: Katalogos (${shortcut})`
: mode === 'katalogos'
? `Katalogos · Next: Flux (${shortcut})`
: `Flux · Next: Logos (${shortcut})`
return (
<div className="flex shrink-0 items-center gap-2">
@@ -61,20 +75,34 @@ export function RecollectionViewSwitcher() {
<button
type="button"
onClick={toggle}
aria-label={isFlux ? 'Switch to Logos' : 'Switch to Flux'}
aria-label={
mode === 'logos'
? 'Switch to Katalogos'
: mode === 'katalogos'
? 'Switch to Flux'
: 'Switch to Logos'
}
className="h-7 w-7 shrink-0 overflow-hidden rounded-full border border-border/60 bg-background shadow-md transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<div className="h-full w-full overflow-hidden">
<div
className="flex w-full flex-col transition-transform duration-200 ease-out"
style={{
height: '200%',
transform: isFlux ? 'translateY(-50%)' : 'translateY(0)',
height: '300%',
transform:
mode === 'logos'
? 'translateY(0)'
: mode === 'katalogos'
? 'translateY(-33.3333%)'
: 'translateY(-66.6667%)',
}}
>
<div className="flex h-9 w-full items-center justify-center">
<LogosIcon className="size-3.5 shrink-0 text-muted-foreground" />
</div>
<div className="flex h-9 w-full items-center justify-center">
<RecollectionsIcon className="size-3.5 shrink-0 text-muted-foreground" />
</div>
<div className="flex h-9 w-full items-center justify-center">
<FluxIcon className="size-3.5 shrink-0 text-muted-foreground" />
</div>
@@ -88,11 +116,21 @@ export function RecollectionViewSwitcher() {
<div className="h-9 min-w-[3.5rem] overflow-hidden">
<div
className="flex w-full flex-col transition-transform duration-200 ease-out"
style={{ transform: isFlux ? 'translateY(-50%)' : 'translateY(0)' }}
style={{
transform:
mode === 'logos'
? 'translateY(0)'
: mode === 'katalogos'
? 'translateY(-33.3333%)'
: 'translateY(-66.6667%)',
}}
>
<span className="flex h-9 flex-shrink-0 items-center text-sm font-medium text-foreground">
Logos
</span>
<span className="flex h-9 flex-shrink-0 items-center text-sm font-medium text-foreground">
Katalogos
</span>
<span className="flex h-9 flex-shrink-0 items-center text-sm font-medium text-foreground">
Flux
</span>

View File

@@ -0,0 +1,31 @@
/**
* Katalogos page: third view for a recollection.
* Simple placeholder layout, matching Logos/Flux flex and typography.
*/
import React from 'react'
import { useParams } from 'react-router-dom'
import { usePlatform } from '@/app/kosmos/KosmosContext'
export function KatalogosPage() {
const { recollectionId } = useParams<{ recollectionId: string }>()
const { recollections } = usePlatform()
const recollection = recollectionId ? recollections.find((p) => p.id === recollectionId) : null
const title = recollection?.name ?? 'Untitled'
return (
<div className="flex h-full min-h-0 flex-1 flex-col overflow-auto bg-background">
<div className="mx-auto w-full max-w-3xl p-4">
<h1 className="mb-2 truncate font-serif text-2xl font-semibold tracking-tight text-foreground">
{title}
</h1>
<p className="mb-6 text-sm text-muted-foreground">Katalogos</p>
<div className="rounded-lg border border-dashed border-muted-foreground/30 bg-muted/10 p-4 text-sm text-muted-foreground">
Katalogos view coming soon.
</div>
</div>
</div>
)
}

View File

@@ -8,6 +8,9 @@ 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 { KatalogosPage } from './app/recollections/katalogos/KatalogosPage'
import './lib/prismSetup'
import 'prismjs/themes/prism.css'
import './styles.css'
@@ -26,8 +29,9 @@ 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={<></>} />
<Route path="flux" element={<></>} />
<Route path="logos" element={<LogosPage />} />
<Route path="katalogos" element={<KatalogosPage />} />
<Route path="flux" element={<FluxRoute />} />
</Route>
</Route>
</Routes>