feat: refactor katalogos
This commit is contained in:
@@ -120,7 +120,7 @@ export function RecollectionActionsProvider({ children }: { children: React.Reac
|
|||||||
pathnameRef.current = pathname
|
pathnameRef.current = pathname
|
||||||
|
|
||||||
const isFluxActive = pathname.endsWith('/flux')
|
const isFluxActive = pathname.endsWith('/flux')
|
||||||
const isLogosActive = pathname.endsWith('/logos') || /\/recollections\/[^/]+\/?$/.test(pathname)
|
const isLogosActive = pathname.includes('/logos') || /\/recollections\/[^/]+\/?$/.test(pathname)
|
||||||
const activeSlot = isFluxActive ? flux : isLogosActive ? logos : null
|
const activeSlot = isFluxActive ? flux : isLogosActive ? logos : null
|
||||||
|
|
||||||
const setFluxSlot = useCallback((slot: FluxSlot | null) => {
|
const setFluxSlot = useCallback((slot: FluxSlot | null) => {
|
||||||
@@ -179,7 +179,7 @@ export function RecollectionActionsProvider({ children }: { children: React.Reac
|
|||||||
)
|
)
|
||||||
const currentPath = pathnameRef.current
|
const currentPath = pathnameRef.current
|
||||||
const fluxActive = currentPath.endsWith('/flux')
|
const fluxActive = currentPath.endsWith('/flux')
|
||||||
const logosActive = currentPath.endsWith('/logos') || /\/recollections\/[^/]+\/?$/.test(currentPath)
|
const logosActive = currentPath.includes('/logos') || /\/recollections\/[^/]+\/?$/.test(currentPath)
|
||||||
if (written.graph && fluxActive && fluxRef.current?.onRefreshFromStore) {
|
if (written.graph && fluxActive && fluxRef.current?.onRefreshFromStore) {
|
||||||
fluxRef.current.onRefreshFromStore(written.graph)
|
fluxRef.current.onRefreshFromStore(written.graph)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import React, { useCallback, useEffect } from 'react'
|
import React, { useCallback, useEffect } from 'react'
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||||
import { FluxIcon, LogosIcon, RecollectionsIcon } from '@/lib/icons'
|
import { FluxIcon, LogosIcon } from '@/lib/icons'
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
|
||||||
function shortcutLabel() {
|
function shortcutLabel() {
|
||||||
@@ -19,17 +19,13 @@ export function RecollectionViewSwitcher() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const base = recollectionId ? `/recollections/${recollectionId}` : ''
|
const base = recollectionId ? `/recollections/${recollectionId}` : ''
|
||||||
|
|
||||||
type Mode = 'logos' | 'katalogos' | 'flux'
|
type Mode = 'logos' | 'flux'
|
||||||
const mode: Mode = pathname.endsWith('/flux')
|
const mode: Mode = pathname.endsWith('/flux') ? 'flux' : 'logos'
|
||||||
? 'flux'
|
|
||||||
: pathname.endsWith('/katalogos')
|
|
||||||
? 'katalogos'
|
|
||||||
: 'logos'
|
|
||||||
|
|
||||||
const goRelative = useCallback(
|
const goRelative = useCallback(
|
||||||
(delta: 1 | -1) => {
|
(delta: 1 | -1) => {
|
||||||
if (!base) return
|
if (!base) return
|
||||||
const order: Mode[] = ['logos', 'katalogos', 'flux']
|
const order: Mode[] = ['logos', 'flux']
|
||||||
const currentIndex = order.indexOf(mode)
|
const currentIndex = order.indexOf(mode)
|
||||||
const nextMode = order[(currentIndex + (delta === 1 ? 1 : order.length - 1)) % order.length]
|
const nextMode = order[(currentIndex + (delta === 1 ? 1 : order.length - 1)) % order.length]
|
||||||
navigate(`${base}/${nextMode}`)
|
navigate(`${base}/${nextMode}`)
|
||||||
@@ -61,11 +57,7 @@ export function RecollectionViewSwitcher() {
|
|||||||
|
|
||||||
const shortcut = shortcutLabel()
|
const shortcut = shortcutLabel()
|
||||||
const tooltipText =
|
const tooltipText =
|
||||||
mode === 'logos'
|
mode === 'logos' ? `Logos · Next: Flux (${shortcut})` : `Flux · Next: Logos (${shortcut})`
|
||||||
? `Logos · Next: Katalogos (${shortcut})`
|
|
||||||
: mode === 'katalogos'
|
|
||||||
? `Katalogos · Next: Flux (${shortcut})`
|
|
||||||
: `Flux · Next: Logos (${shortcut})`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
@@ -75,34 +67,20 @@ export function RecollectionViewSwitcher() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
aria-label={
|
aria-label={mode === 'logos' ? 'Switch to Flux' : 'Switch to Logos'}
|
||||||
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"
|
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="h-full w-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="flex w-full flex-col transition-transform duration-200 ease-out"
|
className="flex w-full flex-col transition-transform duration-200 ease-out"
|
||||||
style={{
|
style={{
|
||||||
height: '300%',
|
height: '200%',
|
||||||
transform:
|
transform: mode === 'logos' ? 'translateY(0)' : 'translateY(-50%)',
|
||||||
mode === 'logos'
|
|
||||||
? 'translateY(0)'
|
|
||||||
: mode === 'katalogos'
|
|
||||||
? 'translateY(-33.3333%)'
|
|
||||||
: 'translateY(-66.6667%)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex h-9 w-full items-center justify-center">
|
<div className="flex h-9 w-full items-center justify-center">
|
||||||
<LogosIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
<LogosIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
||||||
</div>
|
</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">
|
<div className="flex h-9 w-full items-center justify-center">
|
||||||
<FluxIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
<FluxIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
@@ -117,20 +95,12 @@ export function RecollectionViewSwitcher() {
|
|||||||
<div
|
<div
|
||||||
className="flex w-full flex-col transition-transform duration-200 ease-out"
|
className="flex w-full flex-col transition-transform duration-200 ease-out"
|
||||||
style={{
|
style={{
|
||||||
transform:
|
transform: mode === 'logos' ? 'translateY(0)' : 'translateY(-50%)',
|
||||||
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">
|
<span className="flex h-9 flex-shrink-0 items-center text-sm font-medium text-foreground">
|
||||||
Logos
|
Logos
|
||||||
</span>
|
</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">
|
<span className="flex h-9 flex-shrink-0 items-center text-sm font-medium text-foreground">
|
||||||
Flux
|
Flux
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -4,7 +4,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, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { usePlatform } from '@/app/kosmos/KosmosContext'
|
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'
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
} from '../state/recollectionStore'
|
} from '../state/recollectionStore'
|
||||||
import { logosSchema } from './logosSchema'
|
import { logosSchema } from './logosSchema'
|
||||||
import { LogosSidebar } from './LogosSidebar'
|
import { LogosSidebar } from './LogosSidebar'
|
||||||
|
import { KatalogosPage } from '../katalogos/KatalogosPage'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
/** Wraps BlockNoteView so refs go to a div, not the function component (avoids ref warning). */
|
/** Wraps BlockNoteView so refs go to a div, not the function component (avoids ref warning). */
|
||||||
@@ -63,6 +64,8 @@ const MAIN_PAGE_ID: LogosPageId = 'main'
|
|||||||
|
|
||||||
export function LogosPage() {
|
export function LogosPage() {
|
||||||
const { recollectionId } = useParams<{ recollectionId: string }>()
|
const { recollectionId } = useParams<{ recollectionId: string }>()
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
const { recollections } = usePlatform()
|
const { recollections } = usePlatform()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { setLogosSlot } = useRecollectionActions()
|
const { setLogosSlot } = useRecollectionActions()
|
||||||
@@ -70,6 +73,21 @@ export function LogosPage() {
|
|||||||
const [tree, setTree] = useState<LogosPageMeta[]>([])
|
const [tree, setTree] = useState<LogosPageMeta[]>([])
|
||||||
const [activePageId, setActivePageId] = useState<LogosPageId | null>(null)
|
const [activePageId, setActivePageId] = useState<LogosPageId | null>(null)
|
||||||
const [reloadKey, setReloadKey] = useState(0)
|
const [reloadKey, setReloadKey] = useState(0)
|
||||||
|
|
||||||
|
const isKatalogosView = pathname.endsWith('/logos/katalogos')
|
||||||
|
const baseLogosPath = recollectionId ? `/recollections/${recollectionId}/logos` : ''
|
||||||
|
|
||||||
|
const handleSelectPage = useCallback(
|
||||||
|
(id: LogosPageId) => {
|
||||||
|
setActivePageId(id)
|
||||||
|
if (recollectionId) navigate(`${baseLogosPath}`)
|
||||||
|
},
|
||||||
|
[recollectionId, baseLogosPath, navigate]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSelectKatalogos = useCallback(() => {
|
||||||
|
if (recollectionId) navigate(`${baseLogosPath}/katalogos`)
|
||||||
|
}, [recollectionId, baseLogosPath, navigate])
|
||||||
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')
|
||||||
|
|
||||||
@@ -160,7 +178,10 @@ export function LogosPage() {
|
|||||||
}, [editor, recollectionId, activePageId, persistContent])
|
}, [editor, recollectionId, activePageId, persistContent])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor || !recollectionId || !activePageId) return
|
if (isKatalogosView || !editor || !recollectionId || !activePageId) {
|
||||||
|
if (isKatalogosView) setLogosSlot(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
const slot = {
|
const slot = {
|
||||||
saveStatus,
|
saveStatus,
|
||||||
onSave,
|
onSave,
|
||||||
@@ -173,7 +194,7 @@ export function LogosPage() {
|
|||||||
}
|
}
|
||||||
setLogosSlot(slot)
|
setLogosSlot(slot)
|
||||||
return () => setLogosSlot(null)
|
return () => setLogosSlot(null)
|
||||||
}, [setLogosSlot, saveStatus, onSave, editor, recollectionId, activePageId])
|
}, [isKatalogosView, setLogosSlot, saveStatus, onSave, editor, recollectionId, activePageId])
|
||||||
|
|
||||||
const activePage = tree.find((p) => p.id === activePageId)
|
const activePage = tree.find((p) => p.id === activePageId)
|
||||||
|
|
||||||
@@ -214,17 +235,32 @@ export function LogosPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sidebarProps = {
|
||||||
|
recollectionId: recollectionId!,
|
||||||
|
tree,
|
||||||
|
onTreeChange: handleTreeChange,
|
||||||
|
activePageId,
|
||||||
|
onSelectPage: handleSelectPage,
|
||||||
|
onDeletePage: handleDeletePage,
|
||||||
|
isKatalogosView,
|
||||||
|
onSelectKatalogos: handleSelectKatalogos,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isKatalogosView) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full min-h-0 flex-1 flex-row bg-background">
|
||||||
|
<LogosSidebar {...sidebarProps} />
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col overflow-auto">
|
||||||
|
<KatalogosPage />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (activePageId == null || !editor) {
|
if (activePageId == null || !editor) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-1 flex-col bg-background">
|
<div className="flex h-full min-h-0 flex-1 flex-col bg-background">
|
||||||
<LogosSidebar
|
<LogosSidebar {...sidebarProps} />
|
||||||
recollectionId={recollectionId}
|
|
||||||
tree={tree}
|
|
||||||
onTreeChange={handleTreeChange}
|
|
||||||
activePageId={activePageId}
|
|
||||||
onSelectPage={setActivePageId}
|
|
||||||
onDeletePage={handleDeletePage}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
||||||
{tree.length === 0 ? 'Loading…' : 'Select a page'}
|
{tree.length === 0 ? 'Loading…' : 'Select a page'}
|
||||||
</div>
|
</div>
|
||||||
@@ -234,19 +270,9 @@ export function LogosPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-1 flex-row bg-background">
|
<div className="flex h-full min-h-0 flex-1 flex-row bg-background">
|
||||||
<LogosSidebar
|
<LogosSidebar {...sidebarProps} />
|
||||||
recollectionId={recollectionId}
|
|
||||||
tree={tree}
|
|
||||||
onTreeChange={handleTreeChange}
|
|
||||||
activePageId={activePageId}
|
|
||||||
onSelectPage={setActivePageId}
|
|
||||||
onDeletePage={handleDeletePage}
|
|
||||||
/>
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col overflow-auto">
|
<div className="flex min-w-0 flex-1 flex-col overflow-auto">
|
||||||
<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">
|
|
||||||
{activePage?.title ?? recollection?.name ?? 'Untitled'}
|
|
||||||
</h1>
|
|
||||||
<BlockNoteViewWrapper
|
<BlockNoteViewWrapper
|
||||||
editor={editor as any}
|
editor={editor as any}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Logos sidebar: one-level page/subpage hierarchy. Create, rename, delete, reorder.
|
* Logos sidebar: single-level pages under Logos (no subpages). Sidebar-07 style nav with Logos + Katalogos sections.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -19,8 +19,15 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
SidebarContent,
|
||||||
ChevronRight,
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuItem,
|
||||||
|
sidebarMenuButtonVariants,
|
||||||
|
} from '@/components/ui/sidebar'
|
||||||
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Pencil,
|
Pencil,
|
||||||
@@ -29,8 +36,8 @@ import {
|
|||||||
ArrowUp,
|
ArrowUp,
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { LogosPageId, LogosPageMeta } from '../state/recollectionStore'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import type { LogosPageId, LogosPageMeta } from '../state/recollectionStore'
|
||||||
|
|
||||||
const DEFAULT_PAGE_TITLE = 'Untitled'
|
const DEFAULT_PAGE_TITLE = 'Untitled'
|
||||||
|
|
||||||
@@ -49,6 +56,10 @@ export type LogosSidebarProps = {
|
|||||||
activePageId: LogosPageId | null
|
activePageId: LogosPageId | null
|
||||||
onSelectPage: (id: LogosPageId) => void
|
onSelectPage: (id: LogosPageId) => void
|
||||||
onDeletePage?: (pageId: LogosPageId) => void
|
onDeletePage?: (pageId: LogosPageId) => void
|
||||||
|
/** When true, the Katalogos item is selected. */
|
||||||
|
isKatalogosView?: boolean
|
||||||
|
/** Called when user selects the Katalogos item (parent should navigate to .../logos/katalogos). */
|
||||||
|
onSelectKatalogos?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LogosSidebar({
|
export function LogosSidebar({
|
||||||
@@ -58,40 +69,15 @@ export function LogosSidebar({
|
|||||||
activePageId,
|
activePageId,
|
||||||
onSelectPage,
|
onSelectPage,
|
||||||
onDeletePage,
|
onDeletePage,
|
||||||
|
isKatalogosView = false,
|
||||||
|
onSelectKatalogos,
|
||||||
}: LogosSidebarProps) {
|
}: LogosSidebarProps) {
|
||||||
const [expandedPages, setExpandedPages] = useState<Set<LogosPageId>>(new Set())
|
const [renameTarget, setRenameTarget] = useState<LogosPageMeta | null>(null)
|
||||||
const expandedInitializedRef = useRef(false)
|
const [renameTitle, setRenameTitle] = useState('')
|
||||||
const [editingId, setEditingId] = useState<LogosPageId | null>(null)
|
|
||||||
const [editTitle, setEditTitle] = useState('')
|
|
||||||
const [deleteTarget, setDeleteTarget] = useState<LogosPageMeta | null>(null)
|
const [deleteTarget, setDeleteTarget] = useState<LogosPageMeta | null>(null)
|
||||||
|
|
||||||
const pages = tree.filter((p) => p.parentId === null).sort(sortByPosition)
|
const pages = tree.filter((p) => p.parentId === null).sort(sortByPosition)
|
||||||
|
|
||||||
// Default: expand parent pages that have subpages so children are visible on first load.
|
|
||||||
useEffect(() => {
|
|
||||||
if (tree.length === 0 || expandedInitializedRef.current) return
|
|
||||||
expandedInitializedRef.current = true
|
|
||||||
const parentIdsWithChildren = tree
|
|
||||||
.filter((p) => p.parentId === null && tree.some((s) => s.parentId === p.id))
|
|
||||||
.map((p) => p.id)
|
|
||||||
if (parentIdsWithChildren.length > 0) {
|
|
||||||
setExpandedPages((prev) => {
|
|
||||||
const next = new Set(prev)
|
|
||||||
parentIdsWithChildren.forEach((id) => next.add(id))
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [tree])
|
|
||||||
|
|
||||||
const toggleExpanded = useCallback((id: LogosPageId) => {
|
|
||||||
setExpandedPages((prev) => {
|
|
||||||
const next = new Set(prev)
|
|
||||||
if (next.has(id)) next.delete(id)
|
|
||||||
else next.add(id)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const updatePage = useCallback(
|
const updatePage = useCallback(
|
||||||
(id: LogosPageId, patch: Partial<Pick<LogosPageMeta, 'title' | 'position'>>) => {
|
(id: LogosPageId, patch: Partial<Pick<LogosPageMeta, 'title' | 'position'>>) => {
|
||||||
const next = tree.map((p) => (p.id === id ? { ...p, ...patch } : p))
|
const next = tree.map((p) => (p.id === id ? { ...p, ...patch } : p))
|
||||||
@@ -109,42 +95,23 @@ export function LogosSidebar({
|
|||||||
position: maxPos + 1,
|
position: maxPos + 1,
|
||||||
}
|
}
|
||||||
onTreeChange([...tree, newPage])
|
onTreeChange([...tree, newPage])
|
||||||
setEditingId(newPage.id)
|
setRenameTarget(newPage)
|
||||||
setEditTitle(newPage.title)
|
setRenameTitle(newPage.title)
|
||||||
onSelectPage(newPage.id)
|
onSelectPage(newPage.id)
|
||||||
}, [tree, pages, onTreeChange, onSelectPage])
|
}, [tree, pages, onTreeChange, onSelectPage])
|
||||||
|
|
||||||
const addSubpage = useCallback(
|
|
||||||
(parentId: LogosPageId) => {
|
|
||||||
const siblings = tree.filter((p) => p.parentId === parentId).sort(sortByPosition)
|
|
||||||
const maxPos = siblings.length === 0 ? 0 : Math.max(...siblings.map((p) => p.position)) + 1
|
|
||||||
const newPage: LogosPageMeta = {
|
|
||||||
id: generatePageId(),
|
|
||||||
title: DEFAULT_PAGE_TITLE,
|
|
||||||
parentId,
|
|
||||||
position: maxPos,
|
|
||||||
}
|
|
||||||
onTreeChange([...tree, newPage])
|
|
||||||
setExpandedPages((prev) => new Set(prev).add(parentId))
|
|
||||||
setEditingId(newPage.id)
|
|
||||||
setEditTitle(newPage.title)
|
|
||||||
onSelectPage(newPage.id)
|
|
||||||
},
|
|
||||||
[tree, onTreeChange, onSelectPage]
|
|
||||||
)
|
|
||||||
|
|
||||||
const startRename = useCallback((page: LogosPageMeta) => {
|
const startRename = useCallback((page: LogosPageMeta) => {
|
||||||
setEditingId(page.id)
|
setRenameTarget(page)
|
||||||
setEditTitle(page.title)
|
setRenameTitle(page.title)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const commitRename = useCallback(() => {
|
const commitRename = useCallback(() => {
|
||||||
if (editingId && editTitle.trim()) {
|
if (renameTarget && renameTitle.trim()) {
|
||||||
updatePage(editingId, { title: editTitle.trim() })
|
updatePage(renameTarget.id, { title: renameTitle.trim() })
|
||||||
}
|
}
|
||||||
setEditingId(null)
|
setRenameTarget(null)
|
||||||
setEditTitle('')
|
setRenameTitle('')
|
||||||
}, [editingId, editTitle, updatePage])
|
}, [renameTarget, renameTitle, updatePage])
|
||||||
|
|
||||||
const movePage = useCallback(
|
const movePage = useCallback(
|
||||||
(id: LogosPageId, delta: number) => {
|
(id: LogosPageId, delta: number) => {
|
||||||
@@ -171,11 +138,10 @@ export function LogosSidebar({
|
|||||||
|
|
||||||
const removePage = useCallback(
|
const removePage = useCallback(
|
||||||
(page: LogosPageMeta) => {
|
(page: LogosPageMeta) => {
|
||||||
const toRemove = [page.id, ...tree.filter((p) => p.parentId === page.id).map((p) => p.id)]
|
onTreeChange(tree.filter((p) => p.id !== page.id))
|
||||||
onTreeChange(tree.filter((p) => !toRemove.includes(p.id)))
|
onDeletePage?.(page.id)
|
||||||
toRemove.forEach((id) => onDeletePage?.(id))
|
if (activePageId === page.id) {
|
||||||
if (activePageId && toRemove.includes(activePageId)) {
|
const remaining = tree.filter((p) => p.id !== page.id).sort(sortByPosition)
|
||||||
const remaining = tree.filter((p) => !toRemove.includes(p.id)).sort(sortByPosition)
|
|
||||||
const first = remaining[0]
|
const first = remaining[0]
|
||||||
if (first) onSelectPage(first.id)
|
if (first) onSelectPage(first.id)
|
||||||
}
|
}
|
||||||
@@ -184,125 +150,185 @@ export function LogosSidebar({
|
|||||||
[tree, activePageId, onTreeChange, onDeletePage, onSelectPage]
|
[tree, activePageId, onTreeChange, onDeletePage, onSelectPage]
|
||||||
)
|
)
|
||||||
|
|
||||||
const subpages = (parentId: LogosPageId) =>
|
const renderPageActions = useCallback(
|
||||||
tree.filter((p) => p.parentId === parentId).sort(sortByPosition)
|
(page: LogosPageMeta, options: { canMoveUp: boolean; canMoveDown: boolean }) => {
|
||||||
|
const { canMoveUp, canMoveDown } = options
|
||||||
const isPage = (p: LogosPageMeta) => p.parentId === null
|
return (
|
||||||
|
<span
|
||||||
|
data-sidebar="menu-action"
|
||||||
|
className={cn(
|
||||||
|
'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 shrink-0"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="size-3.5" />
|
||||||
|
<span className="sr-only">Actions</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-48" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
startRename(page)
|
||||||
|
onSelectPage(page.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil className="mr-2 size-3.5" />
|
||||||
|
Rename
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => movePage(page.id, -1)} disabled={!canMoveUp}>
|
||||||
|
<ArrowUp className="mr-2 size-3.5" />
|
||||||
|
Move up
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => movePage(page.id, 1)} disabled={!canMoveDown}>
|
||||||
|
<ArrowDown className="mr-2 size-3.5" />
|
||||||
|
Move down
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => setDeleteTarget(page)} className="text-destructive focus:text-destructive">
|
||||||
|
<Trash2 className="mr-2 size-3.5" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[startRename, onSelectPage, movePage]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-64 shrink-0 flex-col border-r border-border bg-muted/30">
|
<div className="flex h-full w-[var(--sidebar-width)] shrink-0 flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground" style={{ '--sidebar-width': '16rem' } as React.CSSProperties}>
|
||||||
<div className="flex shrink-0 items-center justify-between border-b border-border/60 px-2 py-2">
|
<div className="flex shrink-0 items-center justify-between border-b border-sidebar-border px-2 py-2">
|
||||||
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
<span className="text-xs font-medium text-sidebar-foreground/70">
|
||||||
Pages
|
Pages
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 gap-1 px-2 text-xs"
|
className="h-7 gap-1 px-2 text-xs text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
onClick={addPage}
|
onClick={addPage}
|
||||||
>
|
>
|
||||||
<Plus className="size-3.5" />
|
<Plus className="size-3.5" />
|
||||||
New page
|
New page
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto py-1">
|
<SidebarContent className="flex-1 overflow-y-auto border-0 bg-transparent">
|
||||||
{pages.length === 0 ? (
|
<SidebarGroup>
|
||||||
<div className="px-3 py-4 text-center text-xs text-muted-foreground">
|
<SidebarGroupLabel>Prologos</SidebarGroupLabel>
|
||||||
No pages yet.
|
<SidebarGroupContent>
|
||||||
<br />
|
<SidebarMenu>
|
||||||
<Button
|
{pages.length === 0 ? (
|
||||||
type="button"
|
<div className="px-2 py-2 text-center text-xs text-sidebar-foreground/70">
|
||||||
variant="link"
|
<Button
|
||||||
className="h-auto p-0 text-xs"
|
type="button"
|
||||||
onClick={addPage}
|
variant="link"
|
||||||
>
|
className="h-auto p-0 text-xs text-sidebar-foreground/70 hover:text-sidebar-foreground"
|
||||||
Create your first page
|
onClick={addPage}
|
||||||
|
>
|
||||||
|
Create your first page
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
pages.map((page) => {
|
||||||
|
const canMoveUp = pages.indexOf(page) > 0
|
||||||
|
const canMoveDown = pages.indexOf(page) < pages.length - 1
|
||||||
|
const isActive = activePageId === page.id
|
||||||
|
return (
|
||||||
|
<SidebarMenuItem key={page.id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(sidebarMenuButtonVariants({ size: 'default' }), 'w-full')}
|
||||||
|
onClick={() => onSelectPage(page.id)}
|
||||||
|
>
|
||||||
|
<FileText className="size-4 shrink-0 text-sidebar-foreground/70" />
|
||||||
|
<span className="truncate">{page.title || DEFAULT_PAGE_TITLE}</span>
|
||||||
|
</button>
|
||||||
|
{renderPageActions(page, { canMoveUp, canMoveDown })}
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel>Katalogos</SidebarGroupLabel>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-active={isKatalogosView}
|
||||||
|
className={cn(sidebarMenuButtonVariants({ size: 'default' }), 'w-full')}
|
||||||
|
onClick={onSelectKatalogos}
|
||||||
|
>
|
||||||
|
<FileText className="size-4 shrink-0 text-sidebar-foreground/70" />
|
||||||
|
<span className="truncate">Katalogos</span>
|
||||||
|
</button>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarContent>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={!!renameTarget}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setRenameTarget(null)
|
||||||
|
setRenameTitle('')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Rename page</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full rounded border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
value={renameTitle}
|
||||||
|
onChange={(e) => setRenameTitle(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') commitRename()
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setRenameTarget(null)
|
||||||
|
setRenameTitle('')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label="Page title"
|
||||||
|
/>
|
||||||
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
<Button type="button" variant="outline" onClick={() => { setRenameTarget(null); setRenameTitle('') }}>
|
||||||
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
<Button type="button" onClick={commitRename}>
|
||||||
) : (
|
Save
|
||||||
<ul className="flex flex-col gap-0.5 px-1">
|
</Button>
|
||||||
{pages.map((page) => {
|
</DialogFooter>
|
||||||
const subs = subpages(page.id)
|
</DialogContent>
|
||||||
const expanded = expandedPages.has(page.id)
|
</Dialog>
|
||||||
const isActive = activePageId === page.id
|
|
||||||
return (
|
|
||||||
<li key={page.id} className="flex flex-col gap-0.5">
|
|
||||||
<PageRow
|
|
||||||
page={page}
|
|
||||||
isSubpage={false}
|
|
||||||
isActive={isActive}
|
|
||||||
isEditing={editingId === page.id}
|
|
||||||
editTitle={editTitle}
|
|
||||||
onEditTitleChange={setEditTitle}
|
|
||||||
onCommitRename={commitRename}
|
|
||||||
onStartRename={() => startRename(page)}
|
|
||||||
onSelect={() => onSelectPage(page.id)}
|
|
||||||
onExpandToggle={() => toggleExpanded(page.id)}
|
|
||||||
expanded={expanded}
|
|
||||||
hasSubpages={subs.length > 0}
|
|
||||||
onAddSubpage={() => addSubpage(page.id)}
|
|
||||||
onMoveUp={() => movePage(page.id, -1)}
|
|
||||||
onMoveDown={() => movePage(page.id, 1)}
|
|
||||||
onDelete={() => setDeleteTarget(page)}
|
|
||||||
canMoveUp={pages.indexOf(page) > 0}
|
|
||||||
canMoveDown={pages.indexOf(page) < pages.length - 1}
|
|
||||||
/>
|
|
||||||
{expanded && (
|
|
||||||
<ul className="ml-3 flex flex-col gap-0.5 border-l border-border/60 pl-2">
|
|
||||||
{subs.map((sub, i) => {
|
|
||||||
const subActive = activePageId === sub.id
|
|
||||||
const subEditing = editingId === sub.id
|
|
||||||
return (
|
|
||||||
<li key={sub.id}>
|
|
||||||
<PageRow
|
|
||||||
page={sub}
|
|
||||||
isSubpage
|
|
||||||
isActive={subActive}
|
|
||||||
isEditing={subEditing}
|
|
||||||
editTitle={editTitle}
|
|
||||||
onEditTitleChange={setEditTitle}
|
|
||||||
onCommitRename={commitRename}
|
|
||||||
onStartRename={() => startRename(sub)}
|
|
||||||
onSelect={() => onSelectPage(sub.id)}
|
|
||||||
onAddSubpage={undefined}
|
|
||||||
onMoveUp={() => movePage(sub.id, -1)}
|
|
||||||
onMoveDown={() => movePage(sub.id, 1)}
|
|
||||||
onDelete={() => setDeleteTarget(sub)}
|
|
||||||
canMoveUp={i > 0}
|
|
||||||
canMoveDown={i < subs.length - 1}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
<Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>Delete page?</DialogTitle>
|
||||||
{deleteTarget && isPage(deleteTarget)
|
|
||||||
? 'Delete page and subpages?'
|
|
||||||
: 'Delete subpage?'}
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{deleteTarget && (
|
{deleteTarget && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{isPage(deleteTarget) && subpages(deleteTarget.id).length > 0 ? (
|
"{deleteTarget.title}" will be permanently removed.
|
||||||
<>
|
|
||||||
"{deleteTarget.title}" and its {subpages(deleteTarget.id).length}{' '}
|
|
||||||
subpage(s) will be permanently removed.
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<> "{deleteTarget.title}" will be permanently removed.</>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<DialogFooter className="gap-2 sm:gap-0">
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
@@ -322,160 +348,3 @@ export function LogosSidebar({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PageRowProps = {
|
|
||||||
page: LogosPageMeta
|
|
||||||
isSubpage: boolean
|
|
||||||
isActive: boolean
|
|
||||||
isEditing: boolean
|
|
||||||
editTitle: string
|
|
||||||
onEditTitleChange: (v: string) => void
|
|
||||||
onCommitRename: () => void
|
|
||||||
onStartRename: () => void
|
|
||||||
onSelect: () => void
|
|
||||||
onExpandToggle?: () => void
|
|
||||||
expanded?: boolean
|
|
||||||
hasSubpages?: boolean
|
|
||||||
onAddSubpage?: () => void
|
|
||||||
onMoveUp: () => void
|
|
||||||
onMoveDown: () => void
|
|
||||||
onDelete: () => void
|
|
||||||
canMoveUp: boolean
|
|
||||||
canMoveDown: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function PageRow({
|
|
||||||
page,
|
|
||||||
isSubpage,
|
|
||||||
isActive,
|
|
||||||
isEditing,
|
|
||||||
editTitle,
|
|
||||||
onEditTitleChange,
|
|
||||||
onCommitRename,
|
|
||||||
onStartRename,
|
|
||||||
onSelect,
|
|
||||||
onExpandToggle,
|
|
||||||
expanded,
|
|
||||||
hasSubpages,
|
|
||||||
onAddSubpage,
|
|
||||||
onMoveUp,
|
|
||||||
onMoveDown,
|
|
||||||
onDelete,
|
|
||||||
canMoveUp,
|
|
||||||
canMoveDown,
|
|
||||||
}: PageRowProps) {
|
|
||||||
const isParentRow = onExpandToggle != null
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter') onCommitRename()
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
onEditTitleChange(page.title)
|
|
||||||
onCommitRename()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'group flex w-full items-center gap-0.5 rounded-md pr-1',
|
|
||||||
isActive && 'bg-accent text-accent-foreground',
|
|
||||||
!isActive && 'hover:bg-accent/80'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isParentRow ? (
|
|
||||||
// VSCode-like "twisty gutter" so labels align nicely.
|
|
||||||
<span className="flex h-8 w-4 shrink-0 items-center justify-center">
|
|
||||||
{hasSubpages ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex size-4 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
e.preventDefault()
|
|
||||||
onExpandToggle?.()
|
|
||||||
}}
|
|
||||||
aria-expanded={expanded}
|
|
||||||
aria-label={expanded ? 'Collapse subpages' : 'Expand subpages'}
|
|
||||||
>
|
|
||||||
{expanded ? (
|
|
||||||
<ChevronDown className="size-3.5 shrink-0" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="size-3.5 shrink-0" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<span className="inline-block size-4" aria-hidden />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : isSubpage ? (
|
|
||||||
<span className="flex h-8 w-4 shrink-0 items-center justify-center">
|
|
||||||
<span className="inline-block size-4" aria-hidden />
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{isSubpage && <span className="w-2 shrink-0" />}
|
|
||||||
{isEditing ? (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="h-7 min-w-0 flex-1 rounded border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
||||||
value={editTitle}
|
|
||||||
onChange={(e) => onEditTitleChange(e.target.value)}
|
|
||||||
onBlur={onCommitRename}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
autoFocus
|
|
||||||
aria-label="Page title"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex min-w-0 flex-1 items-center gap-1.5 rounded px-1.5 py-1.5 text-left text-sm"
|
|
||||||
onClick={onSelect}
|
|
||||||
>
|
|
||||||
<FileText className="size-3.5 shrink-0 text-muted-foreground" />
|
|
||||||
<span className="truncate">{page.title || DEFAULT_PAGE_TITLE}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!isEditing && (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-6 w-6 shrink-0 opacity-0 group-hover:opacity-100"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="size-3.5" />
|
|
||||||
<span className="sr-only">Actions</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-48" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<DropdownMenuItem onClick={() => { onStartRename(); onSelect(); }}>
|
|
||||||
<Pencil className="mr-2 size-3.5" />
|
|
||||||
Rename
|
|
||||||
</DropdownMenuItem>
|
|
||||||
{onAddSubpage != null && (
|
|
||||||
<DropdownMenuItem onClick={onAddSubpage}>
|
|
||||||
<Plus className="mr-2 size-3.5" />
|
|
||||||
New subpage
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onClick={onMoveUp} disabled={!canMoveUp}>
|
|
||||||
<ArrowUp className="mr-2 size-3.5" />
|
|
||||||
Move up
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={onMoveDown} disabled={!canMoveDown}>
|
|
||||||
<ArrowDown className="mr-2 size-3.5" />
|
|
||||||
Move down
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onClick={onDelete} className="text-destructive focus:text-destructive">
|
|
||||||
<Trash2 className="mr-2 size-3.5" />
|
|
||||||
Delete
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -758,6 +758,7 @@ export {
|
|||||||
SidebarMenuAction,
|
SidebarMenuAction,
|
||||||
SidebarMenuBadge,
|
SidebarMenuBadge,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
|
sidebarMenuButtonVariants,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarMenuSkeleton,
|
SidebarMenuSkeleton,
|
||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ 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 { LogosPage } from './app/recollections/logos/LogosPage'
|
||||||
import { FluxRoute } from './app/recollections/flux/FluxRoute'
|
import { FluxRoute } from './app/recollections/flux/FluxRoute'
|
||||||
import { KatalogosPage } from './app/recollections/katalogos/KatalogosPage'
|
|
||||||
import './lib/prismSetup'
|
import './lib/prismSetup'
|
||||||
import 'prismjs/themes/prism.css'
|
import 'prismjs/themes/prism.css'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
@@ -30,7 +29,7 @@ createRoot(document.getElementById('root')!).render(
|
|||||||
<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={<LogosPage />} />
|
||||||
<Route path="katalogos" element={<KatalogosPage />} />
|
<Route path="logos/katalogos" element={<LogosPage />} />
|
||||||
<Route path="flux" element={<FluxRoute />} />
|
<Route path="flux" element={<FluxRoute />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -373,18 +373,23 @@ pre {
|
|||||||
.token.nunjucks-var {
|
.token.nunjucks-var {
|
||||||
color: #7c3aed;
|
color: #7c3aed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.nunjucks-tag {
|
.token.nunjucks-tag {
|
||||||
color: #c2410c;
|
color: #c2410c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.nunjucks-comment {
|
.token.nunjucks-comment {
|
||||||
color: #0d9488;
|
color: #0d9488;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .token.nunjucks-var {
|
.dark .token.nunjucks-var {
|
||||||
color: #a78bfa;
|
color: #a78bfa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .token.nunjucks-tag {
|
.dark .token.nunjucks-tag {
|
||||||
color: #fb923c;
|
color: #fb923c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .token.nunjucks-comment {
|
.dark .token.nunjucks-comment {
|
||||||
color: #2dd4bf;
|
color: #2dd4bf;
|
||||||
}
|
}
|
||||||
@@ -394,40 +399,4 @@ pre {
|
|||||||
.code-editor .token.entity,
|
.code-editor .token.entity,
|
||||||
.code-editor .token.url {
|
.code-editor .token.url {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* BlockNote (Logos editor): font sizes to match app typography (Tailwind text-sm–text-lg scale) */
|
|
||||||
.logos-blocknote .bn-editor {
|
|
||||||
font-size: 0.875rem; /* text-sm */
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
.logos-blocknote .bn-inline-content {
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
}
|
|
||||||
.logos-blocknote [data-content-type="heading"][data-level="1"] .bn-inline-content {
|
|
||||||
font-size: 1.25rem; /* text-xl */
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
.logos-blocknote [data-content-type="heading"][data-level="2"] .bn-inline-content {
|
|
||||||
font-size: 1.125rem; /* text-lg */
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
.logos-blocknote [data-content-type="heading"][data-level="3"] .bn-inline-content {
|
|
||||||
font-size: 1rem; /* text-base */
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
.logos-blocknote [data-content-type="paragraph"] .bn-inline-content {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
.logos-blocknote [data-content-type="bulletListItem"] .bn-inline-content,
|
|
||||||
.logos-blocknote [data-content-type="numberedListItem"] .bn-inline-content {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
.logos-blocknote [data-content-type="codeBlock"] .bn-inline-content,
|
|
||||||
.logos-blocknote [data-content-type="code"] .bn-inline-content {
|
|
||||||
font-size: 0.8125rem; /* slightly smaller for code */
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user