feat: add workspace system and sidebar layout (UI design system Phase A+B)

Backend: Workspace model, migration (036), schemas, CRUD API endpoints.
Adds workspace_id to trees and categories, seeds 4 default workspaces
per account, auto-assigns existing trees by tree_type.

Frontend: Complete AppLayout rewrite from top-nav to CSS Grid shell
with persistent sidebar + topbar. New components: WorkspaceSwitcher,
NavItem, CategoryList, TagCloud, TopBar, Sidebar. Dashboard components:
QuickStats, FiltersBar, SectionGroup, TreeListItem, SessionsPanel.
WorkspaceStore with localStorage persistence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-02-15 01:16:33 -05:00
parent ef829f06a4
commit d6f4286570
31 changed files with 1431 additions and 250 deletions

View File

@@ -0,0 +1,50 @@
import { create } from 'zustand'
import type { Workspace } from '@/types'
import { workspacesApi } from '@/api/workspaces'
interface WorkspaceState {
workspaces: Workspace[]
activeWorkspaceId: string | null
loading: boolean
setActiveWorkspace: (id: string) => void
fetchWorkspaces: () => Promise<void>
getActiveWorkspace: () => Workspace | undefined
}
export const useWorkspaceStore = create<WorkspaceState>((set, get) => ({
workspaces: [],
activeWorkspaceId: localStorage.getItem('active-workspace-id'),
loading: false,
setActiveWorkspace: (id: string) => {
localStorage.setItem('active-workspace-id', id)
set({ activeWorkspaceId: id })
},
fetchWorkspaces: async () => {
set({ loading: true })
try {
const workspaces = await workspacesApi.list()
const state = get()
let activeId = state.activeWorkspaceId
// If no active workspace or active workspace doesn't exist, use default
if (!activeId || !workspaces.find(w => w.id === activeId)) {
const defaultWs = workspaces.find(w => w.is_default) || workspaces[0]
if (defaultWs) {
activeId = defaultWs.id
localStorage.setItem('active-workspace-id', activeId)
}
}
set({ workspaces, activeWorkspaceId: activeId, loading: false })
} catch {
set({ loading: false })
}
},
getActiveWorkspace: () => {
const { workspaces, activeWorkspaceId } = get()
return workspaces.find(w => w.id === activeWorkspaceId)
},
}))