feat: improvements
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Flipping card container: Logos on the front, Flux on the back.
|
* Recollection view switcher: Logos or Flux based on pathname.
|
||||||
* Uses CSS 3D transform for a performant flip animation when switching via the menubar.
|
* Single container, conditional render — only the active view is mounted (no CSS flip).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@@ -14,22 +14,14 @@ export function FlippingCardView() {
|
|||||||
const isFlux = pathname.endsWith('/flux')
|
const isFlux = pathname.endsWith('/flux')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||||
className="recollection-flip-container"
|
{isFlux ? (
|
||||||
style={{ perspective: '1200px' }}
|
recollectionId ? (
|
||||||
>
|
<FluxRoute />
|
||||||
<div
|
) : null
|
||||||
className="recollection-flip-card"
|
) : (
|
||||||
data-flipped={isFlux}
|
<LogosPage />
|
||||||
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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { RecollectionMenubarProvider } from './RecollectionMenubarContext'
|
|||||||
import { RecollectionActionsProvider } from './RecollectionActionsContext'
|
import { RecollectionActionsProvider } from './RecollectionActionsContext'
|
||||||
import { RecollectionTitleContent } from './RecollectionTitleContent'
|
import { RecollectionTitleContent } from './RecollectionTitleContent'
|
||||||
import { RecollectionMenubar } from './RecollectionMenubar'
|
import { RecollectionMenubar } from './RecollectionMenubar'
|
||||||
|
import { RecollectionViewSwitcher } from './RecollectionViewSwitcher'
|
||||||
import { FlippingCardView } from './FlippingCardView'
|
import { FlippingCardView } from './FlippingCardView'
|
||||||
|
|
||||||
export function RecollectionLayout() {
|
export function RecollectionLayout() {
|
||||||
@@ -37,11 +38,12 @@ export function RecollectionLayout() {
|
|||||||
<RecollectionMenubarProvider>
|
<RecollectionMenubarProvider>
|
||||||
<RecollectionActionsProvider>
|
<RecollectionActionsProvider>
|
||||||
<RecollectionTitleContent />
|
<RecollectionTitleContent />
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<div className="relative 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">
|
||||||
<FlippingCardView />
|
<FlippingCardView />
|
||||||
</div>
|
</div>
|
||||||
|
<RecollectionViewSwitcher />
|
||||||
</div>
|
</div>
|
||||||
</RecollectionActionsProvider>
|
</RecollectionActionsProvider>
|
||||||
</RecollectionMenubarProvider>
|
</RecollectionMenubarProvider>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Recollection menubar: Back | title + save status | registered menus (middle) | Logos | Flux.
|
* Recollection menubar: Back | title + save status | registered menus (middle).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Link, NavLink, 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 { ArrowLeft } from 'lucide-react'
|
import { ArrowLeft } from 'lucide-react'
|
||||||
import { FluxIcon, LogosIcon } from '@/lib/icons'
|
|
||||||
import { useRecollectionMenubar } from './RecollectionMenubarContext'
|
import { useRecollectionMenubar } from './RecollectionMenubarContext'
|
||||||
import { RecollectionEditViewMenus } from './RecollectionEditViewMenus'
|
import { RecollectionEditViewMenus } from './RecollectionEditViewMenus'
|
||||||
|
|
||||||
@@ -16,7 +15,6 @@ export function RecollectionMenubar() {
|
|||||||
const { titleContent } = useRecollectionMenubar()
|
const { titleContent } = useRecollectionMenubar()
|
||||||
|
|
||||||
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 defaultTitle = recollection ? (
|
const defaultTitle = 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
|
||||||
@@ -36,29 +34,6 @@ export function RecollectionMenubar() {
|
|||||||
<div className="flex min-w-0 flex-1 items-center gap-1 overflow-visible">
|
<div className="flex min-w-0 flex-1 items-center gap-1 overflow-visible">
|
||||||
<RecollectionEditViewMenus />
|
<RecollectionEditViewMenus />
|
||||||
</div>
|
</div>
|
||||||
{base && (
|
|
||||||
<nav className="flex shrink-0 items-center gap-0.5" aria-label="Recollection pages">
|
|
||||||
<NavLink
|
|
||||||
to={`${base}/logos`}
|
|
||||||
end
|
|
||||||
className={({ isActive }) =>
|
|
||||||
`flex items-center gap-1.5 rounded-sm px-2.5 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground ${isActive ? 'bg-accent font-medium text-foreground' : 'text-muted-foreground'}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<LogosIcon className="size-3.5" />
|
|
||||||
Logos
|
|
||||||
</NavLink>
|
|
||||||
<NavLink
|
|
||||||
to={`${base}/flux`}
|
|
||||||
className={({ isActive }) =>
|
|
||||||
`flex items-center gap-1.5 rounded-sm px-2.5 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground ${isActive ? 'bg-accent font-medium text-foreground' : 'text-muted-foreground'}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FluxIcon className="size-3.5" />
|
|
||||||
Flux
|
|
||||||
</NavLink>
|
|
||||||
</nav>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
88
frontend/src/app/recollections/RecollectionViewSwitcher.tsx
Normal file
88
frontend/src/app/recollections/RecollectionViewSwitcher.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Elevator-style view switcher: circular button moves between top (Logos) and bottom (Flux).
|
||||||
|
* Click toggles; shortcut and tooltip on hover.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useEffect } from 'react'
|
||||||
|
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||||
|
import { usePlatform } from '@/app/kosmos/KosmosContext'
|
||||||
|
import { FluxIcon, LogosIcon } from '@/lib/icons'
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
import { Kbd } from '@/components/ui/kbd'
|
||||||
|
|
||||||
|
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+⇧+V'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RecollectionViewSwitcher() {
|
||||||
|
const { recollectionId } = useParams<{ recollectionId: string }>()
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const base = recollectionId ? `/recollections/${recollectionId}` : ''
|
||||||
|
|
||||||
|
const isFlux = pathname.endsWith('/flux')
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
if (!base) return
|
||||||
|
navigate(isFlux ? `${base}/logos` : `${base}/flux`)
|
||||||
|
}, [base, isFlux, navigate])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = (ev: KeyboardEvent) => {
|
||||||
|
if (matchKey(ev, VIEW_TOGGLE_KEYS)) {
|
||||||
|
ev.preventDefault()
|
||||||
|
toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKeyDown, true)
|
||||||
|
return () => window.removeEventListener('keydown', onKeyDown, true)
|
||||||
|
}, [toggle])
|
||||||
|
|
||||||
|
if (!base) return null
|
||||||
|
|
||||||
|
const tooltipText = isFlux
|
||||||
|
? `Switch to Logos`
|
||||||
|
: `Switch to Flux`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute left-1/2 top-[18px] z-10 flex -translate-x-1/2 -translate-y-1/2 items-center gap-2">
|
||||||
|
<TooltipProvider delayDuration={300}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggle}
|
||||||
|
aria-label={isFlux ? 'Switch to Logos' : 'Switch to Flux'}
|
||||||
|
className="flex items-center gap-2 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="relative h-14 w-8 rounded-full bg-muted/50 p-0.5">
|
||||||
|
<span
|
||||||
|
className="absolute left-1/2 flex h-6 w-6 -translate-x-1/2 items-center justify-center rounded-full border bg-background shadow-sm transition-[top] duration-200 ease-out"
|
||||||
|
style={{ top: isFlux ? 28 : 4 }}
|
||||||
|
>
|
||||||
|
{isFlux ? (
|
||||||
|
<FluxIcon className="size-3.5 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<LogosIcon className="size-3.5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">{tooltipText}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<Kbd className="text-[10px]">{shortcutLabel()}</Kbd>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState, forwardRef } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState, forwardRef } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { usePlatform } from '@/app/kosmos/KosmosContext'
|
||||||
import { FluxIcon } from '@/lib/icons'
|
import { FluxIcon } from '@/lib/icons'
|
||||||
import { useCreateBlockNote, getDefaultReactSlashMenuItems, SuggestionMenuController } from '@blocknote/react'
|
import { useCreateBlockNote, getDefaultReactSlashMenuItems, SuggestionMenuController } from '@blocknote/react'
|
||||||
import { BlockNoteView } from '@blocknote/shadcn'
|
import { BlockNoteView } from '@blocknote/shadcn'
|
||||||
@@ -49,8 +50,11 @@ function patchBlockNoteRefWarning() {
|
|||||||
|
|
||||||
export function LogosPage() {
|
export function LogosPage() {
|
||||||
const { recollectionId } = useParams<{ recollectionId: string }>()
|
const { recollectionId } = useParams<{ recollectionId: string }>()
|
||||||
|
const { recollections } = usePlatform()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { setLogosSlot } = useRecollectionActions()
|
const { setLogosSlot } = useRecollectionActions()
|
||||||
|
const recollection = recollectionId ? recollections.find((p) => p.id === recollectionId) : null
|
||||||
|
const title = recollection?.name ?? 'Untitled'
|
||||||
const [reloadKey, setReloadKey] = useState(0)
|
const [reloadKey, setReloadKey] = useState(0)
|
||||||
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const [saveStatus, setSaveStatus] = useState<'saved' | 'unsaved' | 'saving'>('saved')
|
const [saveStatus, setSaveStatus] = useState<'saved' | 'unsaved' | 'saving'>('saved')
|
||||||
@@ -171,6 +175,9 @@ export function LogosPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-1 flex-col overflow-auto bg-background">
|
<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">
|
<div className="mx-auto w-full max-w-3xl p-4">
|
||||||
|
<h1 className="mb-4 truncate font-serif text-2xl font-semibold tracking-tight text-foreground">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
<BlockNoteViewWrapper
|
<BlockNoteViewWrapper
|
||||||
editor={editor as any}
|
editor={editor as any}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|||||||
@@ -396,49 +396,3 @@ 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Non-visible face must not capture pointer/wheel so scroll and click work on the visible face only */
|
|
||||||
.recollection-flip-card[data-flipped="false"] .recollection-flip-face-back {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recollection-flip-card[data-flipped="true"] .recollection-flip-face-front {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recollection-flip-face-front {
|
|
||||||
transform: rotateY(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recollection-flip-face-back {
|
|
||||||
transform: rotateY(-180deg);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user