199 lines
8.4 KiB
TypeScript
199 lines
8.4 KiB
TypeScript
/**
|
|
* Platform sidebar: Recollections, Recently used, New recollection.
|
|
*/
|
|
|
|
import React, { useCallback, useMemo } from 'react'
|
|
import { Link, useNavigate, useParams, useLocation } from 'react-router-dom'
|
|
import {
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarFooter,
|
|
SidebarGroup,
|
|
SidebarGroupLabel,
|
|
SidebarHeader,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
SidebarRail,
|
|
SidebarTrigger,
|
|
} from '@/components/ui/sidebar'
|
|
import { Plus, Settings } from 'lucide-react'
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
|
import { useSidebar } from '@/components/ui/sidebar'
|
|
import { usePlatform } from './KosmosContext'
|
|
import { getRecollectionIcon } from '@/lib/iconMap'
|
|
import { RecollectionsIcon } from '@/lib/icons'
|
|
import { NewRecollectionDialog } from './NewRecollectionDialog'
|
|
import { SettingsDialog } from './SettingsDialog'
|
|
import type { Recollection } from './types'
|
|
|
|
export function KosmosSidebar() {
|
|
const { state, setOpen } = useSidebar()
|
|
const isCollapsed = state === 'collapsed'
|
|
const { orderedRecollections, createRecollection, recentRecollectionIds } = usePlatform()
|
|
const recentRecollections = useMemo(() => {
|
|
const byId = new Map(orderedRecollections.map((p) => [p.id, p]))
|
|
return recentRecollectionIds.map((id) => byId.get(id)).filter((p): p is Recollection => p != null)
|
|
}, [orderedRecollections, recentRecollectionIds])
|
|
const navigate = useNavigate()
|
|
const location = useLocation()
|
|
const { recollectionId: selectedRecollectionId } = useParams<{ recollectionId: string }>()
|
|
|
|
const handleCreateRecollection = useCallback(
|
|
(recollection: Parameters<typeof createRecollection>[0]) => {
|
|
createRecollection(recollection)
|
|
navigate(`/recollections/${recollection.id}`)
|
|
},
|
|
[createRecollection, navigate]
|
|
)
|
|
|
|
return (
|
|
<>
|
|
<Sidebar collapsible="icon" className="relative">
|
|
{isCollapsed && (
|
|
<button
|
|
type="button"
|
|
className="absolute inset-0 z-0 cursor-pointer border-0 bg-transparent p-0 outline-none"
|
|
onClick={() => setOpen(true)}
|
|
aria-label="Expand sidebar"
|
|
/>
|
|
)}
|
|
<SidebarHeader className="relative z-10 flex flex-row items-center gap-2">
|
|
<SidebarMenu className="flex-1 min-w-0">
|
|
<SidebarMenuItem>
|
|
{isCollapsed ? (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<SidebarMenuButton
|
|
size="lg"
|
|
className="font-semibold font-serif"
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
setOpen(true)
|
|
}}
|
|
>
|
|
<span className="flex size-8 min-w-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground dark:bg-white dark:text-black">
|
|
<span
|
|
className="size-6 shrink-0 rounded-[2px] opacity-90"
|
|
style={{
|
|
maskImage: 'url(/app-icon.svg)',
|
|
maskRepeat: 'no-repeat',
|
|
maskPosition: 'center',
|
|
maskSize: 'contain',
|
|
WebkitMaskImage: 'url(/app-icon.svg)',
|
|
WebkitMaskRepeat: 'no-repeat',
|
|
WebkitMaskPosition: 'center',
|
|
WebkitMaskSize: 'contain',
|
|
backgroundColor: 'currentColor',
|
|
}}
|
|
aria-hidden
|
|
/>
|
|
</span>
|
|
<span className="font-serif group-data-[collapsible=icon]:hidden uppercase tracking-wide">ZOË</span>
|
|
</SidebarMenuButton>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">Click to expand sidebar</TooltipContent>
|
|
</Tooltip>
|
|
) : (
|
|
<SidebarMenuButton asChild size="lg" tooltip="Zoë" className="font-semibold font-serif">
|
|
<Link to="/recollections">
|
|
<span className="flex size-8 min-w-8 items-center justify-center rounded-lg dark:bg-white dark:text-black">
|
|
<span
|
|
className="size-6 shrink-0 rounded-[2px] opacity-90"
|
|
style={{
|
|
maskImage: 'url(/app-icon.svg)',
|
|
maskRepeat: 'no-repeat',
|
|
maskPosition: 'center',
|
|
maskSize: 'contain',
|
|
WebkitMaskImage: 'url(/app-icon.svg)',
|
|
WebkitMaskRepeat: 'no-repeat',
|
|
WebkitMaskPosition: 'center',
|
|
WebkitMaskSize: 'contain',
|
|
backgroundColor: 'currentColor',
|
|
}}
|
|
aria-hidden
|
|
/>
|
|
</span>
|
|
<span className="font-serif group-data-[collapsible=icon]:hidden uppercase tracking-wide">ZOË</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
)}
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
{!isCollapsed && <SidebarTrigger className="ml-auto shrink-0" />}
|
|
</SidebarHeader>
|
|
<SidebarContent
|
|
className="relative z-10"
|
|
onClick={isCollapsed ? (e) => { if ((e.target as HTMLElement).closest('button') || (e.target as HTMLElement).closest('a')) return; setOpen(true) } : undefined}
|
|
>
|
|
<SidebarGroup>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton asChild tooltip="Recollections" isActive={location.pathname === '/recollections' && !selectedRecollectionId}>
|
|
<Link to="/recollections">
|
|
<RecollectionsIcon className="size-4 shrink-0" />
|
|
<span>Recollections</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
{recentRecollections.length > 0 && (
|
|
<SidebarGroup>
|
|
<SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">Recently used</SidebarGroupLabel>
|
|
<SidebarMenu>
|
|
{recentRecollections.map((recollection) => {
|
|
const Icon = getRecollectionIcon(recollection.iconId)
|
|
const isActive = selectedRecollectionId === recollection.id
|
|
return (
|
|
<SidebarMenuItem key={recollection.id}>
|
|
<SidebarMenuButton asChild tooltip={recollection.name} isActive={isActive}>
|
|
<Link to={`/recollections/${recollection.id}`}>
|
|
<Icon className="size-4" />
|
|
<span>{recollection.name}</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
)
|
|
})}
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
)}
|
|
<SidebarGroup>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<NewRecollectionDialog
|
|
onCreate={handleCreateRecollection}
|
|
existingNames={orderedRecollections.map((p) => p.name)}
|
|
trigger={
|
|
<SidebarMenuButton className="text-sidebar-foreground/70 w-full cursor-pointer">
|
|
<Plus className="size-4" />
|
|
<span>{orderedRecollections.length === 0 ? 'Create your first recollection' : 'New recollection'}</span>
|
|
</SidebarMenuButton>
|
|
}
|
|
/>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
</SidebarContent>
|
|
<SidebarFooter className="relative z-10">
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SettingsDialog
|
|
trigger={
|
|
<SidebarMenuButton tooltip="Settings" className="w-full">
|
|
<Settings className="size-4" />
|
|
<span className="group-data-[collapsible=icon]:hidden">Settings</span>
|
|
</SidebarMenuButton>
|
|
}
|
|
/>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarFooter>
|
|
<SidebarRail className="relative z-10" />
|
|
</Sidebar>
|
|
</>
|
|
)
|
|
}
|