mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-18 13:37:49 -04:00
ui
This commit is contained in:
@@ -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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user