[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:
nikec 2023-05-03 07:47:54 +02:00 committed by GitHub
parent 351f8c3621
commit d69440faff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 114 additions and 74 deletions

View file

@ -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(() => {

View file

@ -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', '*'), []);

View file

@ -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>

View file

@ -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 />

View file

@ -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 (

View file

@ -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());

View file

@ -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>
);

View file

@ -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 = () => {

View file

@ -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 : '';

View file

@ -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'

View file

@ -36,7 +36,7 @@ export default function OnboardingCreatingLibrary() {
}
resetOnboardingStore();
navigate(`/${library.uuid}/overview`);
navigate(`/${library.uuid}/overview`, { replace: true });
},
onError: () => {
resetOnboardingStore();

View file

@ -7,7 +7,7 @@ import Start from './start';
export default [
{
index: true,
element: <Navigate to="start" />
element: <Navigate to="start" replace />
},
{
element: <Start />,

View file

@ -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 = () => {

View file

@ -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 (

View file

@ -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>

View 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>
);
};

View file

@ -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}
/>
);
});

View file

@ -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>
);