mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 13:23:28 +00:00
[ENG-533] Update navigation (#769)
* move tooltip into portal * Update navigation * Switch to useMatch * browser router * routing * Hide nav buttons on web * Include traffic lights and change icon
This commit is contained in:
parent
351f8c3621
commit
d69440faff
|
@ -6,7 +6,7 @@ import { listen } from '@tauri-apps/api/event';
|
|||
import { convertFileSrc } from '@tauri-apps/api/tauri';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { useEffect } from 'react';
|
||||
import { createMemoryRouter } from 'react-router-dom';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import { getDebugState, hooks } from '@sd/client';
|
||||
import {
|
||||
ErrorPage,
|
||||
|
@ -79,7 +79,7 @@ const platform: Platform = {
|
|||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const router = createMemoryRouter(routes);
|
||||
const router = createBrowserRouter(routes);
|
||||
|
||||
export default function App() {
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createWSClient, loggerLink, wsLink } from '@rspc/client';
|
||||
import { QueryClient, QueryClientProvider, hydrate } from '@tanstack/react-query';
|
||||
import { useEffect } from 'react';
|
||||
import { createMemoryRouter } from 'react-router-dom';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import { getDebugState, hooks } from '@sd/client';
|
||||
import { Platform, PlatformProvider, SpacedriveInterface, routes } from '@sd/interface';
|
||||
import demoData from './demoData.json';
|
||||
|
@ -55,7 +55,7 @@ const queryClient = new QueryClient({
|
|||
}
|
||||
});
|
||||
|
||||
const router = createMemoryRouter(routes);
|
||||
const router = createBrowserRouter(routes);
|
||||
|
||||
function App() {
|
||||
useEffect(() => window.parent.postMessage('spacedrive-hello', '*'), []);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { cva } from 'class-variance-authority';
|
||||
import clsx from 'clsx';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { NavLink, NavLinkProps } from 'react-router-dom';
|
||||
import { NavLink, NavLinkProps, useMatch } from 'react-router-dom';
|
||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||
|
||||
const styles = cva(
|
||||
|
@ -20,20 +20,25 @@ const styles = cva(
|
|||
}
|
||||
);
|
||||
|
||||
export default (props: PropsWithChildren<NavLinkProps & { disabled?: boolean }>) => {
|
||||
export default ({
|
||||
className,
|
||||
onClick,
|
||||
disabled,
|
||||
...props
|
||||
}: PropsWithChildren<NavLinkProps & { disabled?: boolean }>) => {
|
||||
const os = useOperatingSystem();
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
{...props}
|
||||
onClick={(e) => (props.disabled ? e.preventDefault() : props.onClick?.(e))}
|
||||
onClick={(e) => (disabled ? e.preventDefault() : onClick?.(e))}
|
||||
className={({ isActive }) =>
|
||||
clsx(
|
||||
styles({ active: isActive, transparent: os === 'macOS' }),
|
||||
props.disabled && 'pointer-events-none opacity-50',
|
||||
props.className
|
||||
disabled && 'pointer-events-none opacity-50',
|
||||
className
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</NavLink>
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
import clsx from 'clsx';
|
||||
import NavigationButtons from '~/components/NavigationButtons';
|
||||
import { MacTrafficLights } from '~/components/TrafficLights';
|
||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||
import Contents from './Contents';
|
||||
import Footer from './Footer';
|
||||
import LibrariesDropdown from './LibrariesDropdown';
|
||||
import WindowControls from './WindowControls';
|
||||
import { macOnly } from './helpers';
|
||||
|
||||
export default () => {
|
||||
const os = useOperatingSystem();
|
||||
const showControls = window.location.search.includes('showControls');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'relative flex min-h-full w-44 shrink-0 grow-0 flex-col space-y-2.5 border-r border-sidebar-divider bg-sidebar px-2.5 pb-2',
|
||||
'relative flex min-h-full w-44 shrink-0 grow-0 flex-col gap-2.5 border-r border-sidebar-divider bg-sidebar px-2.5 pb-2 pt-2.5',
|
||||
macOnly(os, 'bg-opacity-[0.65]')
|
||||
)}
|
||||
>
|
||||
<WindowControls />
|
||||
{showControls && <MacTrafficLights className="absolute top-[13px] left-[13px] z-50" />}
|
||||
{(os !== 'browser' || showControls) && (
|
||||
<div className="flex justify-end">
|
||||
<NavigationButtons />
|
||||
</div>
|
||||
)}
|
||||
<LibrariesDropdown />
|
||||
<Contents />
|
||||
<Footer />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import clsx from 'clsx';
|
||||
import { Suspense, useRef } from 'react';
|
||||
import { Suspense } from 'react';
|
||||
import { Navigate, Outlet, useLocation, useParams } from 'react-router-dom';
|
||||
import {
|
||||
ClientContextProvider,
|
||||
|
@ -16,7 +16,6 @@ import Toasts from './Toasts';
|
|||
|
||||
const Layout = () => {
|
||||
const { libraries, library } = useClientContext();
|
||||
|
||||
const os = useOperatingSystem();
|
||||
|
||||
initPlausible({
|
||||
|
@ -28,8 +27,8 @@ const Layout = () => {
|
|||
if (library === null && libraries.data) {
|
||||
const firstLibrary = libraries.data[0];
|
||||
|
||||
if (firstLibrary) return <Navigate to={`/${firstLibrary.uuid}/overview`} />;
|
||||
else return <Navigate to="/" />;
|
||||
if (firstLibrary) return <Navigate to={`/${firstLibrary.uuid}/overview`} replace />;
|
||||
else return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -39,7 +39,7 @@ export default () => {
|
|||
}, 300);
|
||||
|
||||
useEffect(() => {
|
||||
updateParams(value);
|
||||
if (searchPath.pathname === location.pathname) updateParams(value);
|
||||
}, [value]);
|
||||
|
||||
useKeys([os === 'macOS' ? 'Meta' : 'Ctrl', 'f'], () => searchRef.current?.focus());
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
import { CaretLeft, CaretRight } from 'phosphor-react';
|
||||
import { forwardRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Tooltip } from '@sd/ui';
|
||||
import { useSearchStore } from '~/hooks/useSearchStore';
|
||||
import SearchBar from './SearchBar';
|
||||
import TopBarButton from './TopBarButton';
|
||||
|
||||
export interface ToolOption {
|
||||
icon: JSX.Element;
|
||||
|
@ -22,9 +17,6 @@ export const TOP_BAR_ICON_STYLE = 'm-0.5 w-5 h-5 text-ink-dull';
|
|||
export const TOP_BAR_HEIGHT = 46;
|
||||
|
||||
const TopBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
const navigate = useNavigate();
|
||||
const { isFocused } = useSearchStore();
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
|
@ -35,21 +27,8 @@ const TopBar = forwardRef<HTMLDivElement>((_, ref) => {
|
|||
transition-[background-color,border-color] ease-out
|
||||
"
|
||||
>
|
||||
<div data-tauri-drag-region className="flex flex-1">
|
||||
<Tooltip label="Navigate back">
|
||||
<TopBarButton onClick={() => navigate(-1)} disabled={isFocused}>
|
||||
<CaretLeft weight="bold" className={TOP_BAR_ICON_STYLE} />
|
||||
</TopBarButton>
|
||||
</Tooltip>
|
||||
<Tooltip label="Navigate forward">
|
||||
<TopBarButton onClick={() => navigate(1)} disabled={isFocused}>
|
||||
<CaretRight weight="bold" className={TOP_BAR_ICON_STYLE} />
|
||||
</TopBarButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
<SearchBar />
|
||||
|
||||
<div className="flex-1" ref={ref} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -11,13 +11,13 @@ const Index = () => {
|
|||
|
||||
if (libraries.status !== 'success') return null;
|
||||
|
||||
if (libraries.data.length === 0) return <Navigate to="onboarding" />;
|
||||
if (libraries.data.length === 0) return <Navigate to="onboarding" replace />;
|
||||
|
||||
const currentLibrary = libraries.data.find((l) => l.uuid === currentLibraryCache.id);
|
||||
|
||||
const libraryId = currentLibrary ? currentLibrary.uuid : libraries.data[0]?.uuid;
|
||||
|
||||
return <Navigate to={`${libraryId}/overview`} />;
|
||||
return <Navigate to={`${libraryId}/overview`} replace />;
|
||||
};
|
||||
|
||||
const Wrapper = () => {
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import { BloomOne } from '@sd/assets/images';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect } from 'react';
|
||||
import { Outlet, useNavigate } from 'react-router';
|
||||
import { getOnboardingStore, useDebugState } from '@sd/client';
|
||||
import { Navigate, Outlet, useNavigate } from 'react-router';
|
||||
import {
|
||||
currentLibraryCache,
|
||||
getOnboardingStore,
|
||||
useCachedLibraries,
|
||||
useDebugState
|
||||
} from '@sd/client';
|
||||
import { tw } from '@sd/ui';
|
||||
import DragRegion from '~/components/DragRegion';
|
||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||
import DebugPopover from '../$libraryId/Layout/Sidebar/DebugPopover';
|
||||
import { macOnly } from '../$libraryId/Layout/Sidebar/helpers';
|
||||
import Progress from './Progress';
|
||||
|
||||
export const OnboardingContainer = tw.div`flex flex-col items-center`;
|
||||
|
@ -19,20 +25,26 @@ export const Component = () => {
|
|||
const debugState = useDebugState();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const libraries = useCachedLibraries();
|
||||
const library =
|
||||
libraries.data?.find((l) => l.uuid === currentLibraryCache.id) || libraries.data?.[0];
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const obStore = getOnboardingStore();
|
||||
|
||||
// This is neat because restores the last active screen, but only if it is not the starting screen
|
||||
// Ignoring if people navigate back to the start if progress has been made
|
||||
if (obStore.unlockedScreens.length > 1) {
|
||||
navigate(`/onboarding/${obStore.lastActiveScreen}`);
|
||||
if (obStore.unlockedScreens.length > 1 && !library) {
|
||||
navigate(`/onboarding/${obStore.lastActiveScreen}`, { replace: true });
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
if (libraries.isLoading) return null;
|
||||
if (library?.uuid) return <Navigate to={`${library.uuid}/overview`} replace />;
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
|
@ -62,6 +74,3 @@ export const Component = () => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const macOnly = (platform: string | undefined, classnames: string) =>
|
||||
platform === 'macOS' ? classnames : '';
|
||||
|
|
|
@ -38,7 +38,7 @@ export default function OnboardingProgress() {
|
|||
<button
|
||||
key={path}
|
||||
disabled={!obStore.unlockedScreens.includes(path)}
|
||||
onClick={() => navigate(`/onboarding/${path}`)}
|
||||
onClick={() => navigate(`/onboarding/${path}`, { replace: true })}
|
||||
className={clsx(
|
||||
'h-2 w-2 rounded-full transition hover:bg-ink disabled:opacity-10',
|
||||
currentScreenKey === path ? 'bg-ink' : 'bg-ink-faint'
|
||||
|
|
|
@ -36,7 +36,7 @@ export default function OnboardingCreatingLibrary() {
|
|||
}
|
||||
|
||||
resetOnboardingStore();
|
||||
navigate(`/${library.uuid}/overview`);
|
||||
navigate(`/${library.uuid}/overview`, { replace: true });
|
||||
},
|
||||
onError: () => {
|
||||
resetOnboardingStore();
|
||||
|
|
|
@ -7,7 +7,7 @@ import Start from './start';
|
|||
export default [
|
||||
{
|
||||
index: true,
|
||||
element: <Navigate to="start" />
|
||||
element: <Navigate to="start" replace />
|
||||
},
|
||||
{
|
||||
element: <Start />,
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
import { useUnlockOnboardingScreen } from './Progress';
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string()
|
||||
name: z.string().min(1, 'Name is required')
|
||||
});
|
||||
|
||||
export default function OnboardingNewLibrary() {
|
||||
|
@ -33,7 +33,7 @@ export default function OnboardingNewLibrary() {
|
|||
|
||||
const onSubmit = form.handleSubmit(async (data) => {
|
||||
getOnboardingStore().newLibraryName = data.name;
|
||||
navigate('/onboarding/privacy');
|
||||
navigate('/onboarding/privacy', { replace: true });
|
||||
});
|
||||
|
||||
const handleImport = () => {
|
||||
|
|
|
@ -39,7 +39,7 @@ export default function OnboardingPrivacy() {
|
|||
const onSubmit = form.handleSubmit(async (data) => {
|
||||
getOnboardingStore().shareTelemetry = data.shareTelemetry === 'share-telemetry';
|
||||
|
||||
navigate('/onboarding/creating-library');
|
||||
navigate('/onboarding/creating-library', { replace: true });
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -14,7 +14,7 @@ export default function OnboardingStart() {
|
|||
Welcome to Spacedrive, an open source cross-platform file manager.
|
||||
</OnboardingDescription>
|
||||
<div className="mt-6 space-x-3">
|
||||
<ButtonLink to="/onboarding/new-library" variant="accent" size="md">
|
||||
<ButtonLink to="/onboarding/new-library" replace variant="accent" size="md">
|
||||
Get started
|
||||
</ButtonLink>
|
||||
</div>
|
||||
|
|
35
interface/components/NavigationButtons.tsx
Normal file
35
interface/components/NavigationButtons.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { ArrowLeft, ArrowRight } from 'phosphor-react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Button, Tooltip } from '@sd/ui';
|
||||
import { useSearchStore } from '~/hooks/useSearchStore';
|
||||
|
||||
export default () => {
|
||||
const navigate = useNavigate();
|
||||
const { isFocused } = useSearchStore();
|
||||
const idx = history.state.idx as number;
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<Tooltip label="Navigate back">
|
||||
<Button
|
||||
size="icon"
|
||||
className="text-[14px] text-ink-dull"
|
||||
onClick={() => navigate(-1)}
|
||||
disabled={isFocused || idx === 0}
|
||||
>
|
||||
<ArrowLeft weight="bold" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label="Navigate forward">
|
||||
<Button
|
||||
size="icon"
|
||||
className="text-[14px] text-ink-dull"
|
||||
onClick={() => navigate(1)}
|
||||
disabled={isFocused || idx === history.length - 1}
|
||||
>
|
||||
<ArrowRight weight="bold" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -81,18 +81,21 @@ export const Button = forwardRef<
|
|||
});
|
||||
|
||||
export const ButtonLink = forwardRef<
|
||||
HTMLLinkElement,
|
||||
HTMLAnchorElement,
|
||||
ButtonBaseProps & LinkProps & React.RefAttributes<HTMLAnchorElement>
|
||||
>(({ className, to, ...props }, ref) => {
|
||||
className = cx(
|
||||
styles(props),
|
||||
'no-underline disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
className
|
||||
);
|
||||
|
||||
>(({ className, size, variant, ...props }, ref) => {
|
||||
return (
|
||||
<Link to={to} ref={ref as any} className={className}>
|
||||
{props.children}
|
||||
</Link>
|
||||
<Link
|
||||
ref={ref}
|
||||
className={styles({
|
||||
size,
|
||||
variant,
|
||||
className: clsx(
|
||||
'no-underline disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)
|
||||
})}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -19,13 +19,15 @@ export const Tooltip = ({
|
|||
<TooltipPrimitive.Trigger asChild>
|
||||
<span className={className}>{children}</span>
|
||||
</TooltipPrimitive.Trigger>
|
||||
<TooltipPrimitive.Content
|
||||
side={position}
|
||||
className="z-50 mb-[2px] max-w-[200px] rounded bg-gray-300 px-2 py-1 text-center text-xs dark:!bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<TooltipPrimitive.Arrow className="fill-gray-300 dark:!fill-gray-900" />
|
||||
{label}
|
||||
</TooltipPrimitive.Content>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
side={position}
|
||||
className="z-50 mb-[2px] max-w-[200px] rounded bg-gray-300 px-2 py-1 text-center text-xs dark:!bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<TooltipPrimitive.Arrow className="fill-gray-300 dark:!fill-gray-900" />
|
||||
{label}
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
</TooltipPrimitive.Root>
|
||||
</TooltipPrimitive.Provider>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue