freat: renaming
This commit is contained in:
@@ -1,16 +1,19 @@
|
||||
/**
|
||||
* Platform context: projects list and handlers for create/delete/rename/updateLastEdited/reorder/restore.
|
||||
* Used by AppSidebar, ProjectsTablePage, and canvas route.
|
||||
* Platform context: recollections list and handlers for create/delete/rename/updateLastEdited/reorder/restore.
|
||||
* Used by AppSidebar, RecollectionsPage, and canvas route.
|
||||
*/
|
||||
|
||||
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'
|
||||
import type { Project } from './types'
|
||||
import { saveGraphToStorage } from '@/app/pleroma/projectGraphStorage'
|
||||
import { PROJECT_VERSION } from '@/app/pleroma/projectGraphStorage'
|
||||
import type { Recollection } from './types'
|
||||
import { saveGraphToStorage, RECOLLECTION_VERSION } from '@/app/recollections/recollectionGraphStorage'
|
||||
|
||||
const STORAGE_KEY = 'zui_platform_projects'
|
||||
const ORDER_STORAGE_KEY = 'zui_platform_project_order'
|
||||
const RECENT_STORAGE_KEY = 'zui_platform_recent_project_ids'
|
||||
const STORAGE_KEY = 'zui_platform_recollections'
|
||||
const ORDER_STORAGE_KEY = 'zui_platform_recollection_order'
|
||||
const RECENT_STORAGE_KEY = 'zui_platform_recent_recollection_ids'
|
||||
|
||||
const LEGACY_STORAGE_KEY = 'zui_platform_emanations'
|
||||
const LEGACY_ORDER_STORAGE_KEY = 'zui_platform_emanation_order'
|
||||
const LEGACY_RECENT_STORAGE_KEY = 'zui_platform_recent_emanation_ids'
|
||||
const CANVAS_MINIMAP_KEY = 'zui_canvas_show_minimap'
|
||||
const AI_CONNECTION_KEY = 'zui_ai_connection'
|
||||
const RECENT_MAX = 5
|
||||
@@ -72,8 +75,15 @@ function saveShowMinimap(value: boolean) {
|
||||
|
||||
function loadOrder(): string[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(ORDER_STORAGE_KEY)
|
||||
if (!raw) return []
|
||||
let raw = localStorage.getItem(ORDER_STORAGE_KEY)
|
||||
if (!raw) {
|
||||
const legacy = localStorage.getItem(LEGACY_ORDER_STORAGE_KEY)
|
||||
if (legacy) {
|
||||
localStorage.setItem(ORDER_STORAGE_KEY, legacy)
|
||||
localStorage.removeItem(LEGACY_ORDER_STORAGE_KEY)
|
||||
raw = legacy
|
||||
} else return []
|
||||
}
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string') : []
|
||||
} catch {
|
||||
@@ -87,8 +97,15 @@ function saveOrder(ids: string[]) {
|
||||
|
||||
function loadRecentIds(): string[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(RECENT_STORAGE_KEY)
|
||||
if (!raw) return []
|
||||
let raw = localStorage.getItem(RECENT_STORAGE_KEY)
|
||||
if (!raw) {
|
||||
const legacy = localStorage.getItem(LEGACY_RECENT_STORAGE_KEY)
|
||||
if (legacy) {
|
||||
localStorage.setItem(RECENT_STORAGE_KEY, legacy)
|
||||
localStorage.removeItem(LEGACY_RECENT_STORAGE_KEY)
|
||||
raw = legacy
|
||||
} else return []
|
||||
}
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string').slice(0, RECENT_MAX) : []
|
||||
} catch {
|
||||
@@ -100,41 +117,48 @@ function saveRecentIds(ids: string[]) {
|
||||
localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(ids))
|
||||
}
|
||||
|
||||
function loadProjects(): Project[] {
|
||||
function loadRecollections(): Recollection[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return []
|
||||
let raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) {
|
||||
const legacy = localStorage.getItem(LEGACY_STORAGE_KEY)
|
||||
if (legacy) {
|
||||
localStorage.setItem(STORAGE_KEY, legacy)
|
||||
localStorage.removeItem(LEGACY_STORAGE_KEY)
|
||||
raw = legacy
|
||||
} else return []
|
||||
}
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
if (!Array.isArray(parsed)) return []
|
||||
return parsed
|
||||
.filter(
|
||||
(p): p is Project =>
|
||||
(p): p is Recollection =>
|
||||
p &&
|
||||
typeof p === 'object' &&
|
||||
typeof (p as Project).id === 'string' &&
|
||||
typeof (p as Project).name === 'string' &&
|
||||
typeof (p as Project).iconId === 'string' &&
|
||||
typeof (p as Project).createdAt === 'number'
|
||||
typeof (p as Recollection).id === 'string' &&
|
||||
typeof (p as Recollection).name === 'string' &&
|
||||
typeof (p as Recollection).iconId === 'string' &&
|
||||
typeof (p as Recollection).createdAt === 'number'
|
||||
)
|
||||
.map((p) => ({
|
||||
...p,
|
||||
lastEditedAt: typeof (p as Project).lastEditedAt === 'number' ? (p as Project).lastEditedAt : p.createdAt,
|
||||
lastEditedAt: typeof (p as Recollection).lastEditedAt === 'number' ? (p as Recollection).lastEditedAt : p.createdAt,
|
||||
}))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function saveProjects(projects: Project[]) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(projects))
|
||||
function saveRecollections(recollections: Recollection[]) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(recollections))
|
||||
}
|
||||
|
||||
export type GraphSnapshot = { nodes: unknown[]; edges: unknown[] }
|
||||
|
||||
/** Projects sorted by projectOrder, then by lastEditedAt desc for any not in order */
|
||||
export function sortProjectsByOrder(projects: Project[], order: string[]): Project[] {
|
||||
const byId = new Map(projects.map((p) => [p.id, p]))
|
||||
const ordered: Project[] = []
|
||||
/** Recollections sorted by recollectionOrder, then by lastEditedAt desc for any not in order */
|
||||
export function sortRecollectionsByOrder(recollections: Recollection[], order: string[]): Recollection[] {
|
||||
const byId = new Map(recollections.map((p) => [p.id, p]))
|
||||
const ordered: Recollection[] = []
|
||||
const seen = new Set<string>()
|
||||
for (const id of order) {
|
||||
const p = byId.get(id)
|
||||
@@ -143,27 +167,27 @@ export function sortProjectsByOrder(projects: Project[], order: string[]): Proje
|
||||
seen.add(id)
|
||||
}
|
||||
}
|
||||
const rest = projects
|
||||
const rest = recollections
|
||||
.filter((p) => !seen.has(p.id))
|
||||
.sort((a, b) => (b.lastEditedAt ?? b.createdAt) - (a.lastEditedAt ?? a.createdAt))
|
||||
return [...ordered, ...rest]
|
||||
}
|
||||
|
||||
export type KosmosContextValue = {
|
||||
projects: Project[]
|
||||
projectOrder: string[]
|
||||
/** Projects in display order (sidebar order, then lastEditedAt) */
|
||||
orderedProjects: Project[]
|
||||
/** Last RECENT_MAX accessed project IDs (most recent first) */
|
||||
recentProjectIds: string[]
|
||||
recordProjectAccess: (id: string) => void
|
||||
persist: (next: Project[]) => void
|
||||
createProject: (project: Project) => void
|
||||
deleteProject: (id: string) => void
|
||||
renameProject: (id: string, name: string) => void
|
||||
recollections: Recollection[]
|
||||
recollectionOrder: string[]
|
||||
/** Recollections in display order (sidebar order, then lastEditedAt) */
|
||||
orderedRecollections: Recollection[]
|
||||
/** Last RECENT_MAX accessed recollection IDs (most recent first) */
|
||||
recentRecollectionIds: string[]
|
||||
recordRecollectionAccess: (id: string) => void
|
||||
persist: (next: Recollection[]) => void
|
||||
createRecollection: (recollection: Recollection) => void
|
||||
deleteRecollection: (id: string) => void
|
||||
renameRecollection: (id: string, name: string) => void
|
||||
updateLastEdited: (id: string) => void
|
||||
reorderProjects: (orderedIds: string[]) => void
|
||||
restoreProject: (project: Project, graphSnapshot: GraphSnapshot | null) => void
|
||||
reorderRecollections: (orderedIds: string[]) => void
|
||||
restoreRecollection: (recollection: Recollection, graphSnapshot: GraphSnapshot | null) => void
|
||||
/** Canvas: show React Flow minimap (persisted) */
|
||||
showMinimap: boolean
|
||||
setShowMinimap: (value: boolean) => void
|
||||
@@ -175,9 +199,9 @@ export type KosmosContextValue = {
|
||||
const KosmosContext = createContext<KosmosContextValue | null>(null)
|
||||
|
||||
export function KosmosProvider({ children }: { children: React.ReactNode }) {
|
||||
const [projects, setProjects] = useState<Project[]>(loadProjects)
|
||||
const [projectOrder, setProjectOrder] = useState<string[]>(loadOrder)
|
||||
const [recentProjectIds, setRecentProjectIds] = useState<string[]>(loadRecentIds)
|
||||
const [recollections, setRecollections] = useState<Recollection[]>(loadRecollections)
|
||||
const [recollectionOrder, setRecollectionOrder] = useState<string[]>(loadOrder)
|
||||
const [recentRecollectionIds, setRecentRecollectionIds] = useState<string[]>(loadRecentIds)
|
||||
const [showMinimap, setShowMinimapState] = useState<boolean>(loadShowMinimap)
|
||||
const [aiConnection, setAiConnectionState] = useState<AiConnection>(loadAiConnection)
|
||||
|
||||
@@ -191,128 +215,128 @@ export function KosmosProvider({ children }: { children: React.ReactNode }) {
|
||||
saveAiConnection(value)
|
||||
}, [])
|
||||
|
||||
const persist = useCallback((next: Project[]) => {
|
||||
setProjects(next)
|
||||
saveProjects(next)
|
||||
const persist = useCallback((next: Recollection[]) => {
|
||||
setRecollections(next)
|
||||
saveRecollections(next)
|
||||
}, [])
|
||||
|
||||
const createProject = useCallback(
|
||||
(project: Project) => {
|
||||
const withEdited = { ...project, lastEditedAt: project.createdAt }
|
||||
persist([...projects, withEdited])
|
||||
setProjectOrder((prev) => {
|
||||
const next = prev.includes(project.id) ? prev : [project.id, ...prev]
|
||||
const createRecollection = useCallback(
|
||||
(recollection: Recollection) => {
|
||||
const withEdited = { ...recollection, lastEditedAt: recollection.createdAt }
|
||||
persist([...recollections, withEdited])
|
||||
setRecollectionOrder((prev) => {
|
||||
const next = prev.includes(recollection.id) ? prev : [recollection.id, ...prev]
|
||||
saveOrder(next)
|
||||
return next
|
||||
})
|
||||
},
|
||||
[projects, persist]
|
||||
[recollections, persist]
|
||||
)
|
||||
|
||||
const deleteProject = useCallback(
|
||||
const deleteRecollection = useCallback(
|
||||
(id: string) => {
|
||||
const next = projects.filter((p) => p.id !== id)
|
||||
const next = recollections.filter((p) => p.id !== id)
|
||||
persist(next)
|
||||
setProjectOrder((prev) => {
|
||||
setRecollectionOrder((prev) => {
|
||||
const nextOrder = prev.filter((oid) => oid !== id)
|
||||
saveOrder(nextOrder)
|
||||
return nextOrder
|
||||
})
|
||||
setRecentProjectIds((prev) => {
|
||||
setRecentRecollectionIds((prev) => {
|
||||
const next = prev.filter((oid) => oid !== id)
|
||||
saveRecentIds(next)
|
||||
return next
|
||||
})
|
||||
},
|
||||
[projects, persist]
|
||||
[recollections, persist]
|
||||
)
|
||||
|
||||
const renameProject = useCallback(
|
||||
const renameRecollection = useCallback(
|
||||
(id: string, name: string) => {
|
||||
const next = projects.map((p) => (p.id === id ? { ...p, name } : p))
|
||||
const next = recollections.map((p) => (p.id === id ? { ...p, name } : p))
|
||||
persist(next)
|
||||
},
|
||||
[projects, persist]
|
||||
[recollections, persist]
|
||||
)
|
||||
|
||||
const updateLastEdited = useCallback(
|
||||
(id: string) => {
|
||||
const now = Date.now()
|
||||
const next = projects.map((p) => (p.id === id ? { ...p, lastEditedAt: now } : p))
|
||||
const next = recollections.map((p) => (p.id === id ? { ...p, lastEditedAt: now } : p))
|
||||
persist(next)
|
||||
},
|
||||
[projects, persist]
|
||||
[recollections, persist]
|
||||
)
|
||||
|
||||
const reorderProjects = useCallback((orderedIds: string[]) => {
|
||||
setProjectOrder(orderedIds)
|
||||
const reorderRecollections = useCallback((orderedIds: string[]) => {
|
||||
setRecollectionOrder(orderedIds)
|
||||
saveOrder(orderedIds)
|
||||
}, [])
|
||||
|
||||
const recordProjectAccess = useCallback((id: string) => {
|
||||
setRecentProjectIds((prev) => {
|
||||
const recordRecollectionAccess = useCallback((id: string) => {
|
||||
setRecentRecollectionIds((prev) => {
|
||||
const next = [id, ...prev.filter((x) => x !== id)].slice(0, RECENT_MAX)
|
||||
saveRecentIds(next)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const restoreProject = useCallback(
|
||||
(project: Project, graphSnapshot: GraphSnapshot | null) => {
|
||||
persist([...projects, project])
|
||||
setProjectOrder((prev) => {
|
||||
const next = prev.includes(project.id) ? prev : [project.id, ...prev]
|
||||
const restoreRecollection = useCallback(
|
||||
(recollection: Recollection, graphSnapshot: GraphSnapshot | null) => {
|
||||
persist([...recollections, recollection])
|
||||
setRecollectionOrder((prev) => {
|
||||
const next = prev.includes(recollection.id) ? prev : [recollection.id, ...prev]
|
||||
saveOrder(next)
|
||||
return next
|
||||
})
|
||||
if (graphSnapshot) {
|
||||
saveGraphToStorage(project.id, {
|
||||
version: PROJECT_VERSION,
|
||||
saveGraphToStorage(recollection.id, {
|
||||
version: RECOLLECTION_VERSION,
|
||||
nodes: graphSnapshot.nodes,
|
||||
edges: graphSnapshot.edges,
|
||||
})
|
||||
}
|
||||
},
|
||||
[projects, persist]
|
||||
[recollections, persist]
|
||||
)
|
||||
|
||||
const orderedProjects = useMemo(
|
||||
() => sortProjectsByOrder(projects, projectOrder),
|
||||
[projects, projectOrder]
|
||||
const orderedRecollections = useMemo(
|
||||
() => sortRecollectionsByOrder(recollections, recollectionOrder),
|
||||
[recollections, recollectionOrder]
|
||||
)
|
||||
|
||||
const value: KosmosContextValue = useMemo(
|
||||
() => ({
|
||||
projects,
|
||||
projectOrder,
|
||||
orderedProjects,
|
||||
recentProjectIds,
|
||||
recordProjectAccess,
|
||||
recollections,
|
||||
recollectionOrder,
|
||||
orderedRecollections,
|
||||
recentRecollectionIds,
|
||||
recordRecollectionAccess,
|
||||
persist,
|
||||
createProject,
|
||||
deleteProject,
|
||||
renameProject,
|
||||
createRecollection,
|
||||
deleteRecollection,
|
||||
renameRecollection,
|
||||
updateLastEdited,
|
||||
reorderProjects,
|
||||
restoreProject,
|
||||
reorderRecollections,
|
||||
restoreRecollection,
|
||||
showMinimap,
|
||||
setShowMinimap,
|
||||
aiConnection,
|
||||
setAiConnection,
|
||||
}),
|
||||
[
|
||||
projects,
|
||||
projectOrder,
|
||||
orderedProjects,
|
||||
recentProjectIds,
|
||||
recordProjectAccess,
|
||||
recollections,
|
||||
recollectionOrder,
|
||||
orderedRecollections,
|
||||
recentRecollectionIds,
|
||||
recordRecollectionAccess,
|
||||
persist,
|
||||
createProject,
|
||||
deleteProject,
|
||||
renameProject,
|
||||
createRecollection,
|
||||
deleteRecollection,
|
||||
renameRecollection,
|
||||
updateLastEdited,
|
||||
reorderProjects,
|
||||
restoreProject,
|
||||
reorderRecollections,
|
||||
restoreRecollection,
|
||||
showMinimap,
|
||||
setShowMinimap,
|
||||
aiConnection,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Platform: main layout with sidebar and header. Renders child routes (projects list or canvas) via Outlet.
|
||||
* Platform: main layout with sidebar and header. Renders child routes (recollections list or canvas) via Outlet.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
@@ -9,13 +9,13 @@ import { KosmosProvider, usePlatform } from './KosmosContext'
|
||||
import { KosmosSidebar } from './KosmosSidebar'
|
||||
|
||||
function KosmosLayoutInner() {
|
||||
const { projectId } = useParams<{ projectId: string }>()
|
||||
const { recordProjectAccess } = usePlatform()
|
||||
const { recollectionId } = useParams<{ recollectionId: string }>()
|
||||
const { recordRecollectionAccess } = usePlatform()
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) recordProjectAccess(projectId)
|
||||
}, [projectId, recordProjectAccess])
|
||||
if (recollectionId) recordRecollectionAccess(recollectionId)
|
||||
}, [recollectionId, recordRecollectionAccess])
|
||||
|
||||
return (
|
||||
<SidebarProvider open={sidebarOpen} onOpenChange={setSidebarOpen}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Platform sidebar: All projects, Recently used, New project.
|
||||
* Platform sidebar: Recollections, Recently used, New recollection.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
@@ -21,32 +21,30 @@ import { Plus, Settings, Triangle } from 'lucide-react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useSidebar } from '@/components/ui/sidebar'
|
||||
import { usePlatform } from './KosmosContext'
|
||||
import { getProjectIcon } from '@/lib/iconMap'
|
||||
import { NewProjectDialog } from './NewProjectDialog'
|
||||
import { getRecollectionIcon } from '@/lib/iconMap'
|
||||
import { NewRecollectionDialog } from './NewRecollectionDialog'
|
||||
import { SettingsDialog } from './SettingsDialog'
|
||||
import type { Project } from './types'
|
||||
import type { Recollection } from './types'
|
||||
|
||||
export function KosmosSidebar() {
|
||||
const { state, setOpen } = useSidebar()
|
||||
const isCollapsed = state === 'collapsed'
|
||||
const { orderedProjects, createProject, recentProjectIds } = usePlatform()
|
||||
const recentProjects = useMemo(() => {
|
||||
const byId = new Map(orderedProjects.map((p) => [p.id, p]))
|
||||
return recentProjectIds.map((id) => byId.get(id)).filter((p): p is Project => p != null)
|
||||
}, [orderedProjects, recentProjectIds])
|
||||
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 { projectId: selectedProjectId } = useParams<{ projectId: string }>()
|
||||
const isKeroma = location.pathname === '/keroma'
|
||||
const { recollectionId: selectedRecollectionId } = useParams<{ recollectionId: string }>()
|
||||
const handleSelectRecollection = useCallback((id: string) => navigate(`/recollections/${id}`), [navigate])
|
||||
|
||||
const handleSelectProject = useCallback((id: string) => navigate(`/projects/${id}`), [navigate])
|
||||
|
||||
const handleCreateProject = useCallback(
|
||||
(project: Parameters<typeof createProject>[0]) => {
|
||||
createProject(project)
|
||||
navigate(`/projects/${project.id}`)
|
||||
const handleCreateRecollection = useCallback(
|
||||
(recollection: Parameters<typeof createRecollection>[0]) => {
|
||||
createRecollection(recollection)
|
||||
navigate(`/recollections/${recollection.id}`)
|
||||
},
|
||||
[createProject, navigate]
|
||||
[createRecollection, navigate]
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -99,7 +97,7 @@ export function KosmosSidebar() {
|
||||
</Tooltip>
|
||||
) : (
|
||||
<SidebarMenuButton asChild size="lg" tooltip="Zoë" className="font-semibold font-serif">
|
||||
<Link to="/projects">
|
||||
<Link to="/recollections">
|
||||
<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"
|
||||
@@ -132,39 +130,31 @@ export function KosmosSidebar() {
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="All projects" isActive={location.pathname === '/projects' && !selectedProjectId}>
|
||||
<Link to="/projects">
|
||||
<SidebarMenuButton asChild tooltip="Recollections" isActive={location.pathname === '/recollections' && !selectedRecollectionId}>
|
||||
<Link to="/recollections">
|
||||
<Triangle className="size-4" />
|
||||
<span>Pleroma</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="Keroma" isActive={isKeroma}>
|
||||
<Link to="/keroma">
|
||||
<Triangle className="size-4 rotate-180" />
|
||||
<span>Keroma</span>
|
||||
<span>Recollections</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
{recentProjects.length > 0 && (
|
||||
{recentRecollections.length > 0 && (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">Recently used</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{recentProjects.map((project) => {
|
||||
const Icon = getProjectIcon(project.iconId)
|
||||
const isActive = selectedProjectId === project.id
|
||||
{recentRecollections.map((recollection) => {
|
||||
const Icon = getRecollectionIcon(recollection.iconId)
|
||||
const isActive = selectedRecollectionId === recollection.id
|
||||
return (
|
||||
<SidebarMenuItem key={project.id}>
|
||||
<SidebarMenuItem key={recollection.id}>
|
||||
<SidebarMenuButton
|
||||
tooltip={project.name}
|
||||
tooltip={recollection.name}
|
||||
isActive={isActive}
|
||||
onClick={() => handleSelectProject(project.id)}
|
||||
onClick={() => handleSelectRecollection(recollection.id)}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
<span>{project.name}</span>
|
||||
<span>{recollection.name}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
@@ -175,13 +165,13 @@ export function KosmosSidebar() {
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<NewProjectDialog
|
||||
onCreate={handleCreateProject}
|
||||
existingNames={orderedProjects.map((p) => p.name)}
|
||||
<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>{orderedProjects.length === 0 ? 'Create your first project' : 'New project'}</span>
|
||||
<span>{orderedRecollections.length === 0 ? 'Create your first recollection' : 'New recollection'}</span>
|
||||
</SidebarMenuButton>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Dialog to create a new project: name + icon.
|
||||
* Dialog to create a new recollection: name + icon.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react'
|
||||
@@ -16,20 +16,20 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { PROJECT_ICON_IDS, type Project, type ProjectIconId } from './types'
|
||||
import { getProjectIcon } from '@/lib/iconMap'
|
||||
import { RECOLLECTION_ICON_IDS, type Recollection, type RecollectionIconId } from './types'
|
||||
import { getRecollectionIcon } from '@/lib/iconMap'
|
||||
|
||||
type NewProjectDialogProps = {
|
||||
onCreate: (project: Project) => void
|
||||
type NewRecollectionDialogProps = {
|
||||
onCreate: (recollection: Recollection) => void
|
||||
trigger?: React.ReactNode
|
||||
/** Other project names to check for duplicates (case-insensitive warning only) */
|
||||
/** Other recollection names to check for duplicates (case-insensitive warning only) */
|
||||
existingNames?: string[]
|
||||
}
|
||||
|
||||
export function NewProjectDialog({ onCreate, trigger, existingNames = [] }: NewProjectDialogProps) {
|
||||
export function NewRecollectionDialog({ onCreate, trigger, existingNames = [] }: NewRecollectionDialogProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [name, setName] = useState('')
|
||||
const [iconId, setIconId] = useState<ProjectIconId>('cat')
|
||||
const [iconId, setIconId] = useState<RecollectionIconId>('cat')
|
||||
|
||||
const trimmed = name.trim()
|
||||
const isDuplicate = trimmed.length > 0 && existingNames.some((n) => n.toLowerCase() === trimmed.toLowerCase())
|
||||
@@ -37,13 +37,13 @@ export function NewProjectDialog({ onCreate, trigger, existingNames = [] }: NewP
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!trimmed) return
|
||||
const project: Project = {
|
||||
id: `proj_${Date.now()}`,
|
||||
const recollection: Recollection = {
|
||||
id: `recollection_${Date.now()}`,
|
||||
name: trimmed,
|
||||
iconId,
|
||||
createdAt: Date.now(),
|
||||
}
|
||||
onCreate(project)
|
||||
onCreate(recollection)
|
||||
setName('')
|
||||
setIconId('cat')
|
||||
setOpen(false)
|
||||
@@ -55,41 +55,41 @@ export function NewProjectDialog({ onCreate, trigger, existingNames = [] }: NewP
|
||||
{trigger ?? (
|
||||
<Button variant="outline" size="sm" className="w-full justify-start gap-2">
|
||||
<Plus className="size-4" />
|
||||
New project
|
||||
New recollection
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>New project</DialogTitle>
|
||||
<DialogDescription>Create a project to start editing a graph canvas.</DialogDescription>
|
||||
<DialogTitle>New recollection</DialogTitle>
|
||||
<DialogDescription>Create a recollection to start editing a graph canvas.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="project-name" className="text-sm font-medium">
|
||||
<label htmlFor="recollection-name" className="text-sm font-medium">
|
||||
Name
|
||||
</label>
|
||||
<Input
|
||||
id="project-name"
|
||||
id="recollection-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="My project"
|
||||
placeholder="My recollection"
|
||||
autoFocus
|
||||
/>
|
||||
{isDuplicate && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-500">A project with this name already exists.</p>
|
||||
<p className="text-xs text-amber-600 dark:text-amber-500">A recollection with this name already exists.</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium">Icon</label>
|
||||
<Select value={iconId} onValueChange={(v) => setIconId(v as ProjectIconId)}>
|
||||
<Select value={iconId} onValueChange={(v) => setIconId(v as RecollectionIconId)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PROJECT_ICON_IDS.map((id) => {
|
||||
const Icon = getProjectIcon(id)
|
||||
{RECOLLECTION_ICON_IDS.map((id) => {
|
||||
const Icon = getRecollectionIcon(id)
|
||||
return (
|
||||
<SelectItem key={id} value={id}>
|
||||
<span className="flex items-center gap-2">
|
||||
@@ -1,21 +1,21 @@
|
||||
/**
|
||||
* Platform types: projects and sidebar state.
|
||||
* Platform types: recollections and sidebar state.
|
||||
*/
|
||||
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
|
||||
export type Project = {
|
||||
export type Recollection = {
|
||||
id: string
|
||||
name: string
|
||||
/** Icon identifier: key of PROJECT_ICONS map */
|
||||
/** Icon identifier: key of RECOLLECTION_ICONS map */
|
||||
iconId: string
|
||||
createdAt: number
|
||||
/** Last time the project was opened/edited; used for sorting. Defaults to createdAt if missing. */
|
||||
/** Last time the recollection was opened/edited; used for sorting. Defaults to createdAt if missing. */
|
||||
lastEditedAt?: number
|
||||
}
|
||||
|
||||
/** Project icon ids: Lucide "Animals" category only */
|
||||
export const PROJECT_ICON_IDS = [
|
||||
/** Recollection icon ids: Lucide "Animals" category only */
|
||||
export const RECOLLECTION_ICON_IDS = [
|
||||
'bird',
|
||||
'bug',
|
||||
'cat',
|
||||
@@ -30,4 +30,4 @@ export const PROJECT_ICON_IDS = [
|
||||
'egg'
|
||||
] as const
|
||||
|
||||
export type ProjectIconId = (typeof PROJECT_ICON_IDS)[number]
|
||||
export type RecollectionIconId = (typeof RECOLLECTION_ICON_IDS)[number]
|
||||
|
||||
Reference in New Issue
Block a user