This commit is contained in:
James Pine
2026-03-26 22:23:29 -07:00
parent 22020cf592
commit 0c19c64a17
3 changed files with 754 additions and 628 deletions

View File

@@ -1,8 +1,8 @@
import {CaretDown, GearSix, Plus} from '@phosphor-icons/react';
import type {Space} from '@sd/ts-client';
import {DropdownMenu, SelectPill} from '@spaceui/primitives';
import clsx from 'clsx';
import { CaretDown, Plus, GearSix } from '@phosphor-icons/react';
import { DropdownMenu } from '@spaceui/primitives';
import type { Space } from '@sd/ts-client';
import { useCreateSpaceDialog } from './CreateSpaceModal';
import {useCreateSpaceDialog} from './CreateSpaceModal';
interface SpaceSwitcherProps {
spaces: Space[] | undefined;
@@ -10,68 +10,60 @@ interface SpaceSwitcherProps {
onSwitch: (spaceId: string) => void;
}
export function SpaceSwitcher({ spaces, currentSpace, onSwitch }: SpaceSwitcherProps) {
export function SpaceSwitcher({
spaces,
currentSpace,
onSwitch
}: SpaceSwitcherProps) {
const createSpaceDialog = useCreateSpaceDialog;
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button
className={clsx(
"w-full flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-sm font-medium",
"bg-sidebar-box border border-sidebar-line",
"text-sidebar-ink hover:bg-sidebar-button",
"focus:outline-none focus:ring-1 focus:ring-accent",
"transition-colors",
!currentSpace && "text-sidebar-inkFaint"
)}
>
<SelectPill variant="sidebar" size="lg">
<div
className="size-2 rounded-full shrink-0"
style={{ backgroundColor: currentSpace?.color || '#666' }}
className="size-2 rounded-full"
style={{backgroundColor: currentSpace?.color || '#666'}}
/>
<span className="truncate flex-1 text-left">
<span className="flex-1 truncate text-left">
{currentSpace?.name || 'Select Space'}
</span>
<CaretDown className="size-3 opacity-50" />
</button>
</SelectPill>
</DropdownMenu.Trigger>
<DropdownMenu.Content className="p-1 min-w-[var(--radix-dropdown-menu-trigger-width)]">
<DropdownMenu.Content className="min-w-[var(--radix-dropdown-menu-trigger-width)] p-1">
{spaces && spaces.length > 1
? spaces.map((space) => (
<DropdownMenu.Item
key={space.id}
onClick={() => onSwitch(space.id)}
className={clsx(
"px-2 py-1 text-sm rounded-md",
'rounded-md px-2 py-1 text-sm',
space.id === currentSpace?.id
? "bg-accent text-white"
: "text-sidebar-ink hover:bg-sidebar-selected"
? 'bg-accent text-white'
: 'text-sidebar-ink hover:bg-sidebar-selected'
)}
>
<div className="flex items-center gap-2">
<div
className="size-2 rounded-full"
style={{ backgroundColor: space.color }}
style={{backgroundColor: space.color}}
/>
<span>{space.name}</span>
</div>
</DropdownMenu.Item>
))
))
: null}
{spaces && spaces.length > 1 && (
<DropdownMenu.Separator className="border-sidebar-line my-1" />
)}
<DropdownMenu.Item
onClick={() => createSpaceDialog()}
className="px-2 py-1 text-sm rounded-md hover:bg-sidebar-selected text-sidebar-ink font-medium"
className="hover:bg-sidebar-selected text-sidebar-ink rounded-md px-2 py-1 text-sm font-medium"
>
<Plus className="mr-2 size-4" weight="bold" />
New Space
</DropdownMenu.Item>
<DropdownMenu.Item
className="px-2 py-1 text-sm rounded-md hover:bg-sidebar-selected text-sidebar-ink font-medium"
>
<DropdownMenu.Item className="hover:bg-sidebar-selected text-sidebar-ink rounded-md px-2 py-1 text-sm font-medium">
<GearSix className="mr-2 size-4" weight="bold" />
Space Settings
</DropdownMenu.Item>

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,189 +1,208 @@
import { useState, useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import clsx from "clsx";
import {
House,
Clock,
Heart,
Tag,
Network,
GearSix,
Planet,
CaretDown,
Plus,
} from "@phosphor-icons/react";
import { DropdownMenu } from "@spaceui/primitives";
import { useSpacedriveClient } from "../../contexts/SpacedriveContext";
import { useLibraries } from "../../hooks/useLibraries";
import { usePlatform } from "../../contexts/PlatformContext";
import { LocationsSection } from "./components/LocationsSection";
import { Section } from "./components/Section";
import { SidebarItem } from "./components/SidebarItem";
import { JobManagerPopover } from "../../components/JobManager";
import { SyncMonitorPopover } from "../../components/SyncMonitor";
CaretDown,
Clock,
GearSix,
Heart,
House,
Network,
Planet,
Plus,
Tag
} from '@phosphor-icons/react';
import {DropdownMenu} from '@spaceui/primitives';
import clsx from 'clsx';
import {useEffect, useState} from 'react';
import {useLocation, useNavigate} from 'react-router-dom';
import {JobManagerPopover} from '../../components/JobManager';
import {SyncMonitorPopover} from '../../components/SyncMonitor';
import {usePlatform} from '../../contexts/PlatformContext';
import {useSpacedriveClient} from '../../contexts/SpacedriveContext';
import {useLibraries} from '../../hooks/useLibraries';
import {LocationsSection} from './components/LocationsSection';
import {Section} from './components/Section';
import {SidebarItem} from './components/SidebarItem';
export function Sidebar() {
const client = useSpacedriveClient();
const platform = usePlatform();
const { data: libraries } = useLibraries();
const navigate = useNavigate();
const location = useLocation();
const [currentLibraryId, setCurrentLibraryId] = useState<string | null>(
() => client.getCurrentLibraryId(),
);
const client = useSpacedriveClient();
const platform = usePlatform();
const {data: libraries} = useLibraries();
const navigate = useNavigate();
const location = useLocation();
const [currentLibraryId, setCurrentLibraryId] = useState<string | null>(
() => client.getCurrentLibraryId()
);
const isActive = (path: string) => location.pathname === path;
const isActive = (path: string) => location.pathname === path;
// Listen for library changes from client and update local state
useEffect(() => {
const handleLibraryChange = (newLibraryId: string) => {
setCurrentLibraryId(newLibraryId);
};
// Listen for library changes from client and update local state
useEffect(() => {
const handleLibraryChange = (newLibraryId: string) => {
setCurrentLibraryId(newLibraryId);
};
client.on("library-changed", handleLibraryChange);
return () => {
client.off("library-changed", handleLibraryChange);
};
}, [client]);
client.on('library-changed', handleLibraryChange);
return () => {
client.off('library-changed', handleLibraryChange);
};
}, [client]);
// Auto-select first library on mount if none selected
useEffect(() => {
if (libraries && libraries.length > 0 && !currentLibraryId) {
const firstLib = libraries[0];
// Auto-select first library on mount if none selected
useEffect(() => {
if (libraries && libraries.length > 0 && !currentLibraryId) {
const firstLib = libraries[0];
// Set library ID via platform (syncs to all windows on Tauri)
if (platform.setCurrentLibraryId) {
platform.setCurrentLibraryId(firstLib.id).catch((err) =>
console.error("Failed to set library ID:", err),
);
} else {
// Web fallback - just update client
client.setCurrentLibrary(firstLib.id);
}
}
}, [libraries, currentLibraryId, client, platform]);
// Set library ID via platform (syncs to all windows on Tauri)
if (platform.setCurrentLibraryId) {
platform
.setCurrentLibraryId(firstLib.id)
.catch((err) =>
console.error('Failed to set library ID:', err)
);
} else {
// Web fallback - just update client
client.setCurrentLibrary(firstLib.id);
}
}
}, [libraries, currentLibraryId, client, platform]);
const handleLibrarySwitch = (libraryId: string) => {
// Set library ID via platform (syncs to all windows on Tauri)
if (platform.setCurrentLibraryId) {
platform.setCurrentLibraryId(libraryId).catch((err) =>
console.error("Failed to set library ID:", err),
);
} else {
// Web fallback - just update client
client.setCurrentLibrary(libraryId);
}
};
const handleLibrarySwitch = (libraryId: string) => {
// Set library ID via platform (syncs to all windows on Tauri)
if (platform.setCurrentLibraryId) {
platform
.setCurrentLibraryId(libraryId)
.catch((err) =>
console.error('Failed to set library ID:', err)
);
} else {
// Web fallback - just update client
client.setCurrentLibrary(libraryId);
}
};
const currentLibrary = libraries?.find((lib) => lib.id === currentLibraryId);
const currentLibrary = libraries?.find(
(lib) => lib.id === currentLibraryId
);
return (
<div className="w-[220px] min-w-[176px] max-w-[300px] flex flex-col h-full p-2 bg-app">
<div
className={clsx(
"flex flex-col h-full rounded-2xl overflow-hidden",
"bg-sidebar/65",
)}
>
<nav className="relative z-[51] flex h-full flex-col gap-2.5 p-2.5 pb-2 pt-[52px]">
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button
className={clsx(
"w-full flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-sm font-medium",
"bg-sidebar-box border border-sidebar-line",
"text-sidebar-ink hover:bg-sidebar-button",
"focus:outline-none focus:ring-1 focus:ring-accent",
"transition-colors",
!currentLibrary && "text-sidebar-inkFaint",
)}
>
<span className="truncate flex-1 text-left">
{currentLibrary?.name || "Select Library"}
</span>
<CaretDown className="size-3 opacity-50" />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Content className="min-w-[var(--radix-dropdown-menu-trigger-width)]">
{libraries && libraries.length > 1
? libraries.map((lib) => (
<DropdownMenu.Item
key={lib.id}
onClick={() => handleLibrarySwitch(lib.id)}
className={clsx(
"px-2 py-1 text-sm rounded-md",
lib.id === currentLibraryId
? "bg-accent text-white"
: "text-sidebar-ink hover:bg-sidebar-selected",
)}
>
{lib.name}
</DropdownMenu.Item>
))
: null}
{libraries && libraries.length > 1 && (
<DropdownMenu.Separator className="border-sidebar-line my-1" />
)}
<DropdownMenu.Item
className="px-2 py-1 text-sm rounded-md hover:bg-sidebar-selected text-sidebar-ink font-medium"
>
<Plus className="mr-2 size-4" weight="bold" />
New Library
</DropdownMenu.Item>
<DropdownMenu.Item
className="px-2 py-1 text-sm rounded-md hover:bg-sidebar-selected text-sidebar-ink font-medium"
>
<GearSix className="mr-2 size-4" weight="bold" />
Library Settings
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
return (
<div className="bg-app flex h-full w-[220px] min-w-[176px] max-w-[300px] flex-col p-2">
<div
className={clsx(
'flex h-full flex-col overflow-hidden rounded-2xl',
'bg-sidebar/65'
)}
>
<nav className="relative z-[51] flex h-full flex-col gap-2.5 p-2.5 pb-2 pt-[52px]">
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button
className={clsx(
'flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-sm font-medium',
'bg-sidebar-box border-sidebar-line border',
'text-sidebar-ink hover:bg-sidebar-button',
'focus:ring-accent focus:outline-none focus:ring-1',
'transition-colors',
!currentLibrary && 'text-sidebar-inkFaint'
)}
>
<span className="flex-1 truncate text-left">
{currentLibrary?.name || 'Select Library'}
</span>
<CaretDown className="size-3 opacity-50" />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Content className="min-w-[var(--radix-dropdown-menu-trigger-width)]">
{libraries && libraries.length > 1
? libraries.map((lib) => (
<DropdownMenu.Item
key={lib.id}
onClick={() =>
handleLibrarySwitch(lib.id)
}
className={clsx(
'rounded-md px-2 py-1 text-sm',
lib.id === currentLibraryId
? 'bg-accent text-white'
: 'text-sidebar-ink hover:bg-sidebar-selected'
)}
>
{lib.name}
</DropdownMenu.Item>
))
: null}
{libraries && libraries.length > 1 && (
<DropdownMenu.Separator className="border-sidebar-line my-1" />
)}
<DropdownMenu.Item className="hover:bg-sidebar-selected text-sidebar-ink rounded-md px-2 py-1 text-sm font-medium">
<Plus className="mr-2 size-4" weight="bold" />
New Library
</DropdownMenu.Item>
<DropdownMenu.Item className="hover:bg-sidebar-selected text-sidebar-ink rounded-md px-2 py-1 text-sm font-medium">
<GearSix
className="mr-2 size-4"
weight="bold"
/>
Library Settings
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<div className="no-scrollbar mask-fade-out flex grow flex-col space-y-5 overflow-x-hidden overflow-y-scroll pb-10">
<div className="space-y-0.5">
<SidebarItem
icon={Planet}
label="Overview"
active={isActive("/")}
weight={isActive("/") ? "fill" : "bold"}
onClick={() => navigate("/")}
/>
<SidebarItem
icon={Clock}
label="Recents"
active={isActive("/recents")}
onClick={() => navigate("/recents")}
/>
<SidebarItem
icon={Heart}
label="Favorites"
active={isActive("/favorites")}
onClick={() => navigate("/favorites")}
/>
</div>
<div className="no-scrollbar mask-fade-out flex grow flex-col space-y-5 overflow-x-hidden overflow-y-scroll pb-10">
<div className="space-y-0.5">
<SidebarItem
icon={Planet}
label="Overview"
active={isActive('/')}
weight={isActive('/') ? 'fill' : 'bold'}
onClick={() => navigate('/')}
/>
<SidebarItem
icon={Clock}
label="Recents"
active={isActive('/recents')}
onClick={() => navigate('/recents')}
/>
<SidebarItem
icon={Heart}
label="Favorites"
active={isActive('/favorites')}
onClick={() => navigate('/favorites')}
/>
</div>
<LocationsSection />
<LocationsSection />
<Section title="Tags">
<SidebarItem icon={Tag} label="Work" color="#3B82F6" />
<SidebarItem icon={Tag} label="Personal" color="#10B981" />
<SidebarItem icon={Tag} label="Archive" color="#F59E0B" />
</Section>
<Section title="Tags">
<SidebarItem
icon={Tag}
label="Work"
color="#3B82F6"
/>
<SidebarItem
icon={Tag}
label="Personal"
color="#10B981"
/>
<SidebarItem
icon={Tag}
label="Archive"
color="#F59E0B"
/>
</Section>
<Section title="Cloud">
<SidebarItem icon={Network} label="Sync" />
</Section>
</div>
<Section title="Cloud">
<SidebarItem icon={Network} label="Sync" />
</Section>
</div>
<div className="space-y-0.5">
<SidebarItem icon={GearSix} label="Settings" />
</div>
<div className="space-y-0.5">
<SidebarItem icon={GearSix} label="Settings" />
</div>
<div className="mt-2">
<JobManagerPopover />
</div>
</nav>
</div>
</div>
);
}
<div className="mt-2">
<JobManagerPopover />
</div>
</nav>
</div>
</div>
);
}