mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 13:23:28 +00:00
[ENG-578] Resizable sidebar (#2425)
* v1 * Update pnpm-lock.yaml * Update store.ts * Update index.tsx * fix animation * toggle_sidebar * locales
This commit is contained in:
parent
e4b0aedf64
commit
958692771d
|
@ -1,5 +1,6 @@
|
||||||
import { CheckSquare } from '@phosphor-icons/react';
|
import { CheckSquare } from '@phosphor-icons/react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { SetStateAction } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
auth,
|
auth,
|
||||||
|
@ -30,6 +31,7 @@ import {
|
||||||
useExplorerOperatingSystem
|
useExplorerOperatingSystem
|
||||||
} from '../../Explorer/useExplorerOperatingSystem';
|
} from '../../Explorer/useExplorerOperatingSystem';
|
||||||
import Setting from '../../settings/Setting';
|
import Setting from '../../settings/Setting';
|
||||||
|
import { useSidebarContext } from './SidebarLayout/Context';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const buildInfo = useBridgeQuery(['buildInfo']);
|
const buildInfo = useBridgeQuery(['buildInfo']);
|
||||||
|
@ -39,10 +41,20 @@ export default () => {
|
||||||
const platform = usePlatform();
|
const platform = usePlatform();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const sidebar = useSidebarContext();
|
||||||
|
|
||||||
|
const popover = usePopover();
|
||||||
|
|
||||||
|
function handleOpenChange(action: SetStateAction<boolean>) {
|
||||||
|
const open = typeof action === 'boolean' ? action : !popover.open;
|
||||||
|
popover.setOpen(open);
|
||||||
|
sidebar.onLockedChange(open);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
popover={usePopover()}
|
popover={{ ...popover, setOpen: handleOpenChange }}
|
||||||
className="p-4 focus:outline-none"
|
className="z-[100] p-4 focus:outline-none"
|
||||||
trigger={
|
trigger={
|
||||||
<h1 className="ml-1 w-full text-[7pt] text-sidebar-inkFaint/50">
|
<h1 className="ml-1 w-full text-[7pt] text-sidebar-inkFaint/50">
|
||||||
v{buildInfo.data?.version || '-.-.-'} - {buildInfo.data?.commit || 'dev'}
|
v{buildInfo.data?.version || '-.-.-'} - {buildInfo.data?.commit || 'dev'}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import { Button, PopoverClose, toast, Tooltip } from '@sd/ui';
|
import { Button, PopoverClose, toast, Tooltip } from '@sd/ui';
|
||||||
import { useIsDark, useLocale } from '~/hooks';
|
import { useIsDark, useLocale } from '~/hooks';
|
||||||
|
|
||||||
|
import { useSidebarContext } from '../SidebarLayout/Context';
|
||||||
import { getSidebarStore, useSidebarStore } from '../store';
|
import { getSidebarStore, useSidebarStore } from '../store';
|
||||||
import IsRunningJob from './IsRunningJob';
|
import IsRunningJob from './IsRunningJob';
|
||||||
import JobGroup from './JobGroup';
|
import JobGroup from './JobGroup';
|
||||||
|
@ -49,6 +50,8 @@ export function JobManager() {
|
||||||
const [toggleConfirmation, setToggleConfirmation] = useState(false);
|
const [toggleConfirmation, setToggleConfirmation] = useState(false);
|
||||||
const store = useSidebarStore();
|
const store = useSidebarStore();
|
||||||
|
|
||||||
|
const sidebar = useSidebarContext();
|
||||||
|
|
||||||
const jobGroups = useLibraryQuery(['jobs.reports']);
|
const jobGroups = useLibraryQuery(['jobs.reports']);
|
||||||
|
|
||||||
const progress = useJobProgress(jobGroups.data);
|
const progress = useJobProgress(jobGroups.data);
|
||||||
|
@ -81,16 +84,18 @@ export function JobManager() {
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-hidden pb-10">
|
<div className="h-full overflow-hidden pb-10">
|
||||||
<div className="z-20 flex h-9 w-full items-center rounded-t-md border-b border-app-line/50 bg-app-button/30 px-2">
|
<div className="z-20 flex h-9 w-full items-center rounded-t-md border-b border-app-line/50 bg-app-button/30 px-2">
|
||||||
<Tooltip label={t('pin')}>
|
{!sidebar.collapsed && (
|
||||||
<Button
|
<Tooltip label={t('pin')}>
|
||||||
onClick={() => {
|
<Button
|
||||||
getSidebarStore().pinJobManager = !store.pinJobManager;
|
onClick={() => {
|
||||||
}}
|
getSidebarStore().pinJobManager = !store.pinJobManager;
|
||||||
size="icon"
|
}}
|
||||||
>
|
size="icon"
|
||||||
<PushPin weight={store.pinJobManager ? 'fill' : 'regular'} size={16} />
|
>
|
||||||
</Button>
|
<PushPin weight={store.pinJobManager ? 'fill' : 'regular'} size={16} />
|
||||||
</Tooltip>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<span className="ml-1 font-medium ">{t('recent_jobs')}</span>
|
<span className="ml-1 font-medium ">{t('recent_jobs')}</span>
|
||||||
<div className="grow" />
|
<div className="grow" />
|
||||||
{toggleConfirmation ? (
|
{toggleConfirmation ? (
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
interface SidebarContextProps {
|
||||||
|
show: boolean;
|
||||||
|
locked: boolean;
|
||||||
|
collapsed: boolean;
|
||||||
|
onLockedChange: (val: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarContext = createContext<SidebarContextProps | null>(null);
|
||||||
|
|
||||||
|
export const useSidebarContext = () => {
|
||||||
|
const ctx = useContext(SidebarContext);
|
||||||
|
|
||||||
|
if (ctx === null) throw new Error('SidebarContext.Provider not found!');
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { SetStateAction } from 'react';
|
||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
import { auth, useBridgeMutation, useZodForm } from '@sd/client';
|
import { auth, useBridgeMutation, useZodForm } from '@sd/client';
|
||||||
import { Button, Form, Popover, TextAreaField, toast, usePopover, z } from '@sd/ui';
|
import { Button, Form, Popover, TextAreaField, toast, usePopover, z } from '@sd/ui';
|
||||||
|
@ -6,6 +7,8 @@ import i18n from '~/app/I18n';
|
||||||
import { LoginButton } from '~/components/LoginButton';
|
import { LoginButton } from '~/components/LoginButton';
|
||||||
import { useLocale } from '~/hooks';
|
import { useLocale } from '~/hooks';
|
||||||
|
|
||||||
|
import { useSidebarContext } from './Context';
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
message: z.string().min(1, { message: i18n.t('feedback_is_required') }),
|
message: z.string().min(1, { message: i18n.t('feedback_is_required') }),
|
||||||
emoji: z.number().min(0).max(3, { message: 'Please select an emoji' })
|
emoji: z.number().min(0).max(3, { message: 'Please select an emoji' })
|
||||||
|
@ -13,7 +16,7 @@ const schema = z.object({
|
||||||
|
|
||||||
const EMOJIS = ['🤩', '😀', '🙁', '😭'];
|
const EMOJIS = ['🤩', '😀', '🙁', '😭'];
|
||||||
|
|
||||||
export default function () {
|
export function FeedbackPopover() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const sendFeedback = useBridgeMutation(['api.sendFeedback'], {
|
const sendFeedback = useBridgeMutation(['api.sendFeedback'], {
|
||||||
|
@ -33,18 +36,27 @@ export default function () {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sidebar = useSidebarContext();
|
||||||
|
|
||||||
const emojiError = form.formState.errors.emoji?.message;
|
const emojiError = form.formState.errors.emoji?.message;
|
||||||
const popover = usePopover();
|
const popover = usePopover();
|
||||||
const authState = auth.useStateSnapshot();
|
const authState = auth.useStateSnapshot();
|
||||||
|
|
||||||
|
function handleOpenChange(action: SetStateAction<boolean>) {
|
||||||
|
const open = typeof action === 'boolean' ? action : !popover.open;
|
||||||
|
popover.setOpen(open);
|
||||||
|
sidebar.onLockedChange(open);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
popover={popover}
|
popover={{ ...popover, setOpen: handleOpenChange }}
|
||||||
trigger={
|
trigger={
|
||||||
<Button variant="outline" className="flex items-center gap-1">
|
<Button variant="outline" className="flex items-center gap-1">
|
||||||
<p className="text-[11px] font-normal text-sidebar-inkFaint">{t('feedback')}</p>
|
<p className="text-[11px] font-normal text-sidebar-inkFaint">{t('feedback')}</p>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
className="z-[100]"
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
|
@ -3,32 +3,26 @@ import { Gear } from '@phosphor-icons/react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
JobManagerContextProvider,
|
|
||||||
LibraryContextProvider,
|
LibraryContextProvider,
|
||||||
Procedures,
|
Procedures,
|
||||||
useClientContext,
|
useClientContext,
|
||||||
useDebugState,
|
useDebugState,
|
||||||
useLibrarySubscription,
|
useLibrarySubscription
|
||||||
useUnsafeStreamedQuery
|
|
||||||
} from '@sd/client';
|
} from '@sd/client';
|
||||||
import { Button, ButtonLink, Popover, Tooltip, usePopover } from '@sd/ui';
|
import { Button, ButtonLink, Tooltip } from '@sd/ui';
|
||||||
import { useKeysMatcher, useLocale, useShortcut } from '~/hooks';
|
import { useKeysMatcher, useLocale, useShortcut } from '~/hooks';
|
||||||
import { useRoutingContext } from '~/RoutingContext';
|
|
||||||
import { usePlatform } from '~/util/Platform';
|
import { usePlatform } from '~/util/Platform';
|
||||||
|
|
||||||
import DebugPopover from '../DebugPopover';
|
import DebugPopover from '../DebugPopover';
|
||||||
import { IsRunningJob, JobManager } from '../JobManager';
|
import { FeedbackPopover } from './FeedbackPopover';
|
||||||
import { useSidebarStore } from '../store';
|
import { JobManagerPopover } from './JobManagerPopover';
|
||||||
import FeedbackButton from './FeedbackButton';
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { library } = useClientContext();
|
const { library } = useClientContext();
|
||||||
const { visible } = useRoutingContext();
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const debugState = useDebugState();
|
const debugState = useDebugState();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const symbols = useKeysMatcher(['Meta', 'Shift']);
|
const symbols = useKeysMatcher(['Meta', 'Shift']);
|
||||||
const store = useSidebarStore();
|
|
||||||
|
|
||||||
useShortcut('navToSettings', (e) => {
|
useShortcut('navToSettings', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -38,30 +32,20 @@ export default () => {
|
||||||
const updater = usePlatform().updater;
|
const updater = usePlatform().updater;
|
||||||
const updaterState = updater?.useSnapshot();
|
const updaterState = updater?.useSnapshot();
|
||||||
|
|
||||||
const jobManagerPopover = usePopover();
|
|
||||||
|
|
||||||
useShortcut('toggleJobManager', () => jobManagerPopover.setOpen((open) => !open));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{updater && updaterState && (
|
{updater && updaterState?.status === 'updateAvailable' && (
|
||||||
<>
|
<Button variant="outline" className="w-full" onClick={updater.installUpdate}>
|
||||||
{updaterState.status === 'updateAvailable' && (
|
{t('install_update')}
|
||||||
<Button
|
</Button>
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
onClick={updater.installUpdate}
|
|
||||||
>
|
|
||||||
{t('install_update')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{library && (
|
{library && (
|
||||||
<LibraryContextProvider library={library}>
|
<LibraryContextProvider library={library}>
|
||||||
<SyncStatusIndicator />
|
<SyncStatusIndicator />
|
||||||
</LibraryContextProvider>
|
</LibraryContextProvider>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<ButtonLink
|
<ButtonLink
|
||||||
|
@ -78,40 +62,12 @@ export default () => {
|
||||||
<Gear className="size-5" />
|
<Gear className="size-5" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
<JobManagerContextProvider>
|
<JobManagerPopover />
|
||||||
<Popover
|
|
||||||
popover={{
|
|
||||||
...jobManagerPopover,
|
|
||||||
open: jobManagerPopover.open || (store.pinJobManager && visible)
|
|
||||||
}}
|
|
||||||
trigger={
|
|
||||||
<Button
|
|
||||||
id="job-manager-button"
|
|
||||||
size="icon"
|
|
||||||
variant="subtle"
|
|
||||||
className="text-sidebar-inkFaint ring-offset-sidebar radix-state-open:bg-sidebar-selected/50"
|
|
||||||
disabled={!library}
|
|
||||||
>
|
|
||||||
{library && (
|
|
||||||
<Tooltip
|
|
||||||
label={t('recent_jobs')}
|
|
||||||
position="top"
|
|
||||||
keybinds={[symbols.Meta.icon, 'J']}
|
|
||||||
>
|
|
||||||
<IsRunningJob />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="block h-96 w-[430px]">
|
|
||||||
<JobManager />
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</JobManagerContextProvider>
|
|
||||||
</div>
|
</div>
|
||||||
<FeedbackButton />
|
|
||||||
|
<FeedbackPopover />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{debugState.enabled && <DebugPopover />}
|
{debugState.enabled && <DebugPopover />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { SetStateAction } from 'react';
|
||||||
|
import { JobManagerContextProvider, useClientContext } from '@sd/client';
|
||||||
|
import { Button, Popover, Tooltip, usePopover } from '@sd/ui';
|
||||||
|
import { useKeysMatcher, useLocale, useShortcut } from '~/hooks';
|
||||||
|
import { useRoutingContext } from '~/RoutingContext';
|
||||||
|
|
||||||
|
import { IsRunningJob, JobManager } from '../JobManager';
|
||||||
|
import { useSidebarStore } from '../store';
|
||||||
|
import { useSidebarContext } from './Context';
|
||||||
|
|
||||||
|
export function JobManagerPopover() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { library } = useClientContext();
|
||||||
|
const { visible } = useRoutingContext();
|
||||||
|
const { Meta } = useKeysMatcher(['Meta', 'Shift']);
|
||||||
|
const { pinJobManager } = useSidebarStore();
|
||||||
|
|
||||||
|
const sidebar = useSidebarContext();
|
||||||
|
|
||||||
|
const popover = usePopover();
|
||||||
|
|
||||||
|
function handleOpenChange(action: SetStateAction<boolean>) {
|
||||||
|
const open = typeof action === 'boolean' ? action : !popover.open;
|
||||||
|
popover.setOpen(open);
|
||||||
|
sidebar.onLockedChange(open);
|
||||||
|
}
|
||||||
|
|
||||||
|
useShortcut('toggleJobManager', () => {
|
||||||
|
const open = !popover.open;
|
||||||
|
|
||||||
|
if (sidebar.collapsed && !sidebar.show && open) {
|
||||||
|
sidebar.onLockedChange(true);
|
||||||
|
// Wait for the sidebar to open
|
||||||
|
setTimeout(() => handleOpenChange(open), 120);
|
||||||
|
} else {
|
||||||
|
handleOpenChange(open);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JobManagerContextProvider>
|
||||||
|
<Popover
|
||||||
|
popover={{
|
||||||
|
open: popover.open || (pinJobManager && visible),
|
||||||
|
setOpen: handleOpenChange
|
||||||
|
}}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
id="job-manager-button"
|
||||||
|
size="icon"
|
||||||
|
variant="subtle"
|
||||||
|
className="text-sidebar-inkFaint ring-offset-sidebar radix-state-open:bg-sidebar-selected/50"
|
||||||
|
disabled={!library}
|
||||||
|
>
|
||||||
|
{library && (
|
||||||
|
<Tooltip
|
||||||
|
label={t('recent_jobs')}
|
||||||
|
position="top"
|
||||||
|
keybinds={[Meta.icon, 'J']}
|
||||||
|
>
|
||||||
|
<IsRunningJob />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
className="z-[100]"
|
||||||
|
// overlay
|
||||||
|
>
|
||||||
|
<div className="block h-96 w-[430px]">
|
||||||
|
<JobManager />
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</JobManagerContextProvider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,10 +5,13 @@ import { dialogManager, Dropdown, DropdownMenu } from '@sd/ui';
|
||||||
import { useLocale } from '~/hooks';
|
import { useLocale } from '~/hooks';
|
||||||
|
|
||||||
import CreateDialog from '../../../settings/node/libraries/CreateDialog';
|
import CreateDialog from '../../../settings/node/libraries/CreateDialog';
|
||||||
|
import { useSidebarContext } from './Context';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { library, libraries, currentLibraryId } = useClientContext();
|
const { library, libraries, currentLibraryId } = useClientContext();
|
||||||
|
|
||||||
|
const sidebar = useSidebarContext();
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -31,8 +34,10 @@ export default () => {
|
||||||
}
|
}
|
||||||
// we override the sidebar dropdown item's hover styles
|
// we override the sidebar dropdown item's hover styles
|
||||||
// because the dark style clashes with the sidebar
|
// because the dark style clashes with the sidebar
|
||||||
className="mt-1 shadow-none data-[side=bottom]:slide-in-from-top-2 dark:divide-menu-selected/30 dark:border-sidebar-line dark:bg-sidebar-box"
|
className="z-[100] mt-1 shadow-none data-[side=bottom]:slide-in-from-top-2 dark:divide-menu-selected/30 dark:border-sidebar-line dark:bg-sidebar-box"
|
||||||
alignToTrigger
|
alignToTrigger
|
||||||
|
// Timeout because of race conditions when opening the dropdown from a open popover.
|
||||||
|
onOpenChange={(open) => setTimeout(() => sidebar.onLockedChange(open))}
|
||||||
>
|
>
|
||||||
{libraries.data?.map((lib) => (
|
{libraries.data?.map((lib) => (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
|
|
@ -1,58 +1,268 @@
|
||||||
|
import { SidebarSimple } from '@phosphor-icons/react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { PropsWithChildren, useEffect } from 'react';
|
import { motion, useAnimationControls, Variants } from 'framer-motion';
|
||||||
|
import { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useKey } from 'rooks';
|
||||||
|
import { Button, Kbd, Resizable, ResizableHandle, Tooltip, useResizableContext } from '@sd/ui';
|
||||||
import { MacTrafficLights } from '~/components';
|
import { MacTrafficLights } from '~/components';
|
||||||
import { useOperatingSystem, useShowControls } from '~/hooks';
|
import { useLocale, useOperatingSystem, useShortcut, useShowControls } from '~/hooks';
|
||||||
import { useWindowState } from '~/hooks/useWindowState';
|
|
||||||
|
|
||||||
|
import { layoutStore, useLayoutStore } from '../../store';
|
||||||
|
import { getSidebarStore } from '../store';
|
||||||
|
import { SidebarContext } from './Context';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
import LibrariesDropdown from './LibrariesDropdown';
|
import LibrariesDropdown from './LibrariesDropdown';
|
||||||
|
|
||||||
export default (props: PropsWithChildren) => {
|
const TRANSITION_EASE = [0.25, 1, 0.5, 1];
|
||||||
|
|
||||||
|
export default ({ children }: PropsWithChildren) => {
|
||||||
const os = useOperatingSystem();
|
const os = useOperatingSystem();
|
||||||
const showControls = useShowControls();
|
const showControls = useShowControls();
|
||||||
const windowState = useWindowState();
|
|
||||||
|
|
||||||
//prevent sidebar scrolling with keyboard
|
const { sidebar } = useLayoutStore();
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
// Prevent scroll with arrow up/down keys
|
||||||
const arrows = ['ArrowUp', 'ArrowDown'];
|
useKey(['ArrowUp', 'ArrowDown'], (e) => e.preventDefault());
|
||||||
if (arrows.includes(e.key)) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Resizable
|
||||||
className={clsx(
|
min={176}
|
||||||
'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 transition-[padding-top] ease-linear motion-reduce:transition-none',
|
max={300}
|
||||||
os === 'macOS' && windowState.isFullScreen
|
initial={sidebar.size}
|
||||||
? '-mt-2 pt-[8.75px] duration-100'
|
collapsed={sidebar.collapsed}
|
||||||
: 'pt-2.5 duration-75',
|
onCollapseChange={(val) => (layoutStore.sidebar.collapsed = val)}
|
||||||
os === 'macOS' || showControls.transparentBg
|
onResizeEnd={({ position }) => (layoutStore.sidebar.size = position)}
|
||||||
? 'bg-opacity-[0.65]'
|
|
||||||
: 'bg-opacity-[1]'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{showControls.isEnabled && <MacTrafficLights className="z-50 mb-1" />}
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'bg-sidebar',
|
||||||
|
(os === 'macOS' || showControls.transparentBg) && 'bg-opacity-[0.65]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SidebarSize />
|
||||||
|
<SidebarContent>
|
||||||
|
{showControls.isEnabled && <MacTrafficLights className="z-50 mb-1" />}
|
||||||
|
|
||||||
{os === 'macOS' && (
|
<SidebarControls />
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
<LibrariesDropdown />
|
||||||
className={clsx(
|
|
||||||
'w-full shrink-0 transition-[height] ease-linear motion-reduce:transition-none',
|
<div className="no-scrollbar mask-fade-out flex grow flex-col space-y-5 overflow-x-hidden overflow-y-scroll pb-10">
|
||||||
windowState.isFullScreen ? 'h-0 duration-100' : 'h-4 duration-75'
|
{children}
|
||||||
)}
|
<div className="grow" />
|
||||||
/>
|
</div>
|
||||||
)}
|
|
||||||
<LibrariesDropdown />
|
<Footer />
|
||||||
<div className="no-scrollbar mask-fade-out flex grow flex-col space-y-5 overflow-x-hidden overflow-y-scroll pb-10">
|
</SidebarContent>
|
||||||
{props.children}
|
|
||||||
<div className="grow" />
|
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
</Resizable>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SidebarSize = () => {
|
||||||
|
const resizable = useResizableContext();
|
||||||
|
|
||||||
|
const controls = useAnimationControls();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (resizable.collapsed) return;
|
||||||
|
controls.start({ width: resizable.position, transition: { duration: 0 } });
|
||||||
|
}, [controls, resizable.position, resizable.collapsed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
controls.start({ width: resizable.size, transition: { ease: TRANSITION_EASE } });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [controls, resizable.collapsed]);
|
||||||
|
|
||||||
|
return <motion.div initial={{ width: resizable.size }} animate={controls} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidebarContent = ({ children }: PropsWithChildren) => {
|
||||||
|
const resizable = useResizableContext();
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [show, setShow] = useState(!resizable.collapsed);
|
||||||
|
|
||||||
|
// Used to prevent any changes to the open state
|
||||||
|
// e.g. when popovers are open from the sidebar
|
||||||
|
const [locked, setLocked] = useState(false);
|
||||||
|
|
||||||
|
const [hovered, setHovered] = useState(false);
|
||||||
|
const [focused, setFocused] = useState(false);
|
||||||
|
|
||||||
|
const controls = useAnimationControls();
|
||||||
|
|
||||||
|
const variants: Variants = {
|
||||||
|
hide: { left: -resizable.position + 12, transition: { ease: TRANSITION_EASE } },
|
||||||
|
show: { left: 0, transition: { ease: TRANSITION_EASE } }
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSidebar = useCallback(
|
||||||
|
(show: boolean) => {
|
||||||
|
controls.start(show ? 'show' : 'hide');
|
||||||
|
setShow(show);
|
||||||
|
},
|
||||||
|
[controls]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const node = ref.current;
|
||||||
|
if (!node || !resizable.collapsed) return;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
const { clientX, clientY } = e;
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
|
||||||
|
const isHoveringX = clientX >= rect.left && clientX <= rect.right;
|
||||||
|
const isHoveringY = clientY >= rect.top && clientY <= rect.bottom;
|
||||||
|
|
||||||
|
setHovered(isHoveringX && isHoveringY);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
|
return () => window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
}, [resizable.collapsed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const node = ref.current;
|
||||||
|
if (!node || !resizable.collapsed) return;
|
||||||
|
|
||||||
|
const handleFocus = (focused: boolean) => setFocused(focused);
|
||||||
|
|
||||||
|
node.addEventListener('focusin', () => handleFocus(true));
|
||||||
|
node.addEventListener('focusout', () => handleFocus(false));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
node.removeEventListener('focusin', () => handleFocus(true));
|
||||||
|
node.removeEventListener('focusout', () => handleFocus(false));
|
||||||
|
};
|
||||||
|
}, [resizable.collapsed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!resizable.collapsed || resizable.isDragging || hovered || locked) return;
|
||||||
|
toggleSidebar(focused);
|
||||||
|
}, [focused, hovered, resizable.collapsed, resizable.isDragging, toggleSidebar, locked]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!resizable.collapsed) return;
|
||||||
|
setTimeout(() => toggleSidebar(locked));
|
||||||
|
}, [toggleSidebar, locked, resizable.collapsed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!resizable.collapsed || resizable.isDragging || locked) return;
|
||||||
|
// Timeout toggle as LibrariesDropdown triggers a focus on close which causes a flicker
|
||||||
|
setTimeout(() => toggleSidebar(hovered));
|
||||||
|
}, [hovered, resizable.collapsed, resizable.isDragging, toggleSidebar, locked]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => toggleSidebar(!resizable.collapsed));
|
||||||
|
|
||||||
|
// Temporary solution until we have a better way to handle pinned popovers
|
||||||
|
if (resizable.collapsed) getSidebarStore().pinJobManager = false;
|
||||||
|
|
||||||
|
setLocked(false);
|
||||||
|
setHovered(false);
|
||||||
|
setFocused(false);
|
||||||
|
}, [resizable.collapsed, toggleSidebar]);
|
||||||
|
|
||||||
|
useShortcut('toggleSidebar', () => (layoutStore.sidebar.collapsed = !resizable.collapsed), {
|
||||||
|
disabled: locked || resizable.isDragging
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarContext.Provider
|
||||||
|
value={{ show, locked, onLockedChange: setLocked, collapsed: resizable.collapsed }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
initial={show ? 'show' : 'hide'}
|
||||||
|
animate={controls}
|
||||||
|
variants={variants}
|
||||||
|
className={clsx('fixed inset-y-0 z-[100]', resizable.collapsed && 'p-1 pr-3')}
|
||||||
|
style={{
|
||||||
|
// We add 16px from the padding on the x-axis
|
||||||
|
width: resizable.position + (show && resizable.collapsed ? 16 : 0)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
className={clsx(
|
||||||
|
'relative z-[51] flex h-full flex-col gap-2.5 p-2.5 pb-2',
|
||||||
|
// Uncomment if SidebarControls are removed
|
||||||
|
// 'transition-[padding-top] ease-linear motion-reduce:transition-none',
|
||||||
|
// os === 'macOS' && !windowState.isFullScreen && 'pt-2.5',
|
||||||
|
resizable.collapsed
|
||||||
|
? 'rounded-md border border-app-line bg-sidebar shadow'
|
||||||
|
: null
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ResizeHandle />
|
||||||
|
</nav>
|
||||||
|
</motion.div>
|
||||||
|
</SidebarContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function SidebarControls() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const os = useOperatingSystem();
|
||||||
|
const { sidebar } = useLayoutStore();
|
||||||
|
|
||||||
|
if (os !== 'macOS') return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Tooltip
|
||||||
|
label={!sidebar.collapsed ? t('hide_sidebar') : t('lock_sidebar')}
|
||||||
|
keybinds={['[']}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
onClick={() => (layoutStore.sidebar.collapsed = !layoutStore.sidebar.collapsed)}
|
||||||
|
>
|
||||||
|
<SidebarSimple className="size-[18px]" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResizeHandle() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { sidebar } = useLayoutStore();
|
||||||
|
const resizable = useResizableContext();
|
||||||
|
|
||||||
|
const [cursor, setCursor] = useState<{ x: number; y: number }>();
|
||||||
|
const [collapse, setCollapse] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => setCollapse(false), [resizable.position]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
align="start"
|
||||||
|
position="right"
|
||||||
|
disableHoverableContent
|
||||||
|
alignOffset={(cursor?.y ?? 0) - (sidebar.collapsed ? 39 : 35)}
|
||||||
|
className={clsx('!absolute inset-y-2 -right-1')}
|
||||||
|
label={
|
||||||
|
resizable.isDragging ? null : (
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<div>{t('drag_to_resize')}</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>{t(sidebar.collapsed ? 'click_to_lock' : 'click_to_hide')}</span>
|
||||||
|
<Kbd>[</Kbd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ResizableHandle
|
||||||
|
className="h-full after:rounded-full"
|
||||||
|
onMouseOver={(e) => setCursor({ x: e.clientX, y: e.clientY })}
|
||||||
|
onMouseDown={() => setCollapse(true)}
|
||||||
|
onMouseUp={() => collapse && (layoutStore.sidebar.collapsed = !sidebar.collapsed)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ const Layout = () => {
|
||||||
ref={layoutRef}
|
ref={layoutRef}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
// App level styles
|
// App level styles
|
||||||
'flex h-screen cursor-default select-none overflow-hidden text-ink',
|
'flex h-screen select-none overflow-hidden text-ink',
|
||||||
os === 'macOS' && [
|
os === 'macOS' && [
|
||||||
'has-blur-effects',
|
'has-blur-effects',
|
||||||
!windowState.isFullScreen &&
|
!windowState.isFullScreen &&
|
||||||
|
|
10
interface/app/$libraryId/Layout/store.ts
Normal file
10
interface/app/$libraryId/Layout/store.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { proxy, useSnapshot } from 'valtio';
|
||||||
|
import { valtioPersist } from '@sd/client';
|
||||||
|
|
||||||
|
const state = proxy({
|
||||||
|
sidebar: { size: 180, collapsed: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
export const layoutStore = valtioPersist('sd-layout', state);
|
||||||
|
|
||||||
|
export const useLayoutStore = () => useSnapshot(layoutStore);
|
|
@ -4,10 +4,18 @@ import { useEffect, useLayoutEffect, useRef } from 'react';
|
||||||
import useResizeObserver from 'use-resize-observer';
|
import useResizeObserver from 'use-resize-observer';
|
||||||
import { useSelector } from '@sd/client';
|
import { useSelector } from '@sd/client';
|
||||||
import { Tooltip } from '@sd/ui';
|
import { Tooltip } from '@sd/ui';
|
||||||
import { useKeyMatcher, useLocale, useShortcut, useShowControls } from '~/hooks';
|
import {
|
||||||
|
useKeyMatcher,
|
||||||
|
useLocale,
|
||||||
|
useOperatingSystem,
|
||||||
|
useShortcut,
|
||||||
|
useShowControls,
|
||||||
|
useWindowState
|
||||||
|
} from '~/hooks';
|
||||||
import { useTabsContext } from '~/TabsContext';
|
import { useTabsContext } from '~/TabsContext';
|
||||||
|
|
||||||
import { explorerStore } from '../Explorer/store';
|
import { explorerStore } from '../Explorer/store';
|
||||||
|
import { useLayoutStore } from '../Layout/store';
|
||||||
import { useTopBarContext } from './Context';
|
import { useTopBarContext } from './Context';
|
||||||
import { NavigationButtons } from './NavigationButtons';
|
import { NavigationButtons } from './NavigationButtons';
|
||||||
|
|
||||||
|
@ -15,11 +23,17 @@ import { NavigationButtons } from './NavigationButtons';
|
||||||
const TopBar = () => {
|
const TopBar = () => {
|
||||||
const transparentBg = useShowControls().transparentBg;
|
const transparentBg = useShowControls().transparentBg;
|
||||||
const isDragSelecting = useSelector(explorerStore, (s) => s.isDragSelecting);
|
const isDragSelecting = useSelector(explorerStore, (s) => s.isDragSelecting);
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const tabs = useTabsContext();
|
const tabs = useTabsContext();
|
||||||
const ctx = useTopBarContext();
|
const ctx = useTopBarContext();
|
||||||
|
|
||||||
|
const windowState = useWindowState();
|
||||||
|
const platform = useOperatingSystem();
|
||||||
|
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
useResizeObserver({
|
useResizeObserver({
|
||||||
ref,
|
ref,
|
||||||
box: 'border-box',
|
box: 'border-box',
|
||||||
|
@ -63,7 +77,11 @@ const TopBar = () => {
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex h-12 items-center gap-3.5 overflow-hidden px-3.5',
|
'flex h-12 items-center gap-3.5 overflow-hidden px-3.5',
|
||||||
'duration-250 transition-[background-color,border-color] ease-out',
|
'duration-250 transition-[background-color,border-color] ease-out',
|
||||||
isDragSelecting && 'pointer-events-none'
|
isDragSelecting && 'pointer-events-none',
|
||||||
|
platform === 'macOS' &&
|
||||||
|
!windowState.isFullScreen &&
|
||||||
|
layoutStore.sidebar.collapsed &&
|
||||||
|
'pl-20'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -29,7 +29,8 @@ export const Component = () => {
|
||||||
{ shortcut: 'newTab', description: t('open_new_tab') },
|
{ shortcut: 'newTab', description: t('open_new_tab') },
|
||||||
{ shortcut: 'closeTab', description: t('close_current_tab') },
|
{ shortcut: 'closeTab', description: t('close_current_tab') },
|
||||||
{ shortcut: 'newTab', description: t('switch_to_next_tab') },
|
{ shortcut: 'newTab', description: t('switch_to_next_tab') },
|
||||||
{ shortcut: 'previousTab', description: t('switch_to_previous_tab') }
|
{ shortcut: 'previousTab', description: t('switch_to_previous_tab') },
|
||||||
|
{ shortcut: 'toggleSidebar', description: t('toggle_sidebar') }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,6 +4,7 @@ button {
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@apply cursor-default;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
// font-family: 'InterVariable', sans-serif;
|
// font-family: 'InterVariable', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,6 +147,9 @@ const shortcuts = {
|
||||||
},
|
},
|
||||||
explorerRight: {
|
explorerRight: {
|
||||||
all: ['ArrowRight']
|
all: ['ArrowRight']
|
||||||
|
},
|
||||||
|
toggleSidebar: {
|
||||||
|
all: ['[']
|
||||||
}
|
}
|
||||||
} satisfies Record<string, Shortcut>;
|
} satisfies Record<string, Shortcut>;
|
||||||
|
|
||||||
|
|
|
@ -479,6 +479,12 @@
|
||||||
"toggle_metadata": "Паказаць метададзеныя",
|
"toggle_metadata": "Паказаць метададзеныя",
|
||||||
"toggle_path_bar": "Адкрыць адрасны радок",
|
"toggle_path_bar": "Адкрыць адрасны радок",
|
||||||
"toggle_quick_preview": "Адкрыць перадпрагляд",
|
"toggle_quick_preview": "Адкрыць перадпрагляд",
|
||||||
|
"toggle_sidebar": "Пераключыць бакавую панэль",
|
||||||
|
"lock_sidebar": "Бакавая панэль заблакаваць",
|
||||||
|
"hide_sidebar": "Схаваць бакавую панэль",
|
||||||
|
"drag_to_resize": "Перацягнуць, каб змяніць памер",
|
||||||
|
"click_to_hide": "Націсніце, каб схаваць",
|
||||||
|
"click_to_lock": "Націсніце, каб заблакаваць",
|
||||||
"tools": "Інструменты",
|
"tools": "Інструменты",
|
||||||
"trash": "Сметніца",
|
"trash": "Сметніца",
|
||||||
"type": "Тып",
|
"type": "Тып",
|
||||||
|
|
|
@ -479,6 +479,12 @@
|
||||||
"toggle_metadata": "Metadaten umschalten",
|
"toggle_metadata": "Metadaten umschalten",
|
||||||
"toggle_path_bar": "Pfadleiste umschalten",
|
"toggle_path_bar": "Pfadleiste umschalten",
|
||||||
"toggle_quick_preview": "Schnellvorschau umschalten",
|
"toggle_quick_preview": "Schnellvorschau umschalten",
|
||||||
|
"toggle_sidebar": "Seitenleiste umschalten",
|
||||||
|
"lock_sidebar": "Seitenleiste sperren",
|
||||||
|
"hide_sidebar": "Seitenleiste ausblenden",
|
||||||
|
"drag_to_resize": "Zum Ändern der Größe ziehen",
|
||||||
|
"click_to_hide": "Zum Ausblenden klicken",
|
||||||
|
"click_to_lock": "Zum Sperren klicken",
|
||||||
"tools": "Werkzeuge",
|
"tools": "Werkzeuge",
|
||||||
"trash": "Müll",
|
"trash": "Müll",
|
||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
|
|
|
@ -479,6 +479,12 @@
|
||||||
"toggle_metadata": "Toggle metadata",
|
"toggle_metadata": "Toggle metadata",
|
||||||
"toggle_path_bar": "Toggle path bar",
|
"toggle_path_bar": "Toggle path bar",
|
||||||
"toggle_quick_preview": "Toggle quick preview",
|
"toggle_quick_preview": "Toggle quick preview",
|
||||||
|
"toggle_sidebar": "Toggle sidebar",
|
||||||
|
"lock_sidebar": "Lock sidebar",
|
||||||
|
"hide_sidebar": "Hide sidebar",
|
||||||
|
"drag_to_resize": "Drag to resize",
|
||||||
|
"click_to_hide": "Click to hide",
|
||||||
|
"click_to_lock": "Click to lock",
|
||||||
"tools": "Tools",
|
"tools": "Tools",
|
||||||
"trash": "Trash",
|
"trash": "Trash",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
|
|
|
@ -479,6 +479,12 @@
|
||||||
"toggle_metadata": "Alternar metadatos",
|
"toggle_metadata": "Alternar metadatos",
|
||||||
"toggle_path_bar": "Alternar barra de ruta",
|
"toggle_path_bar": "Alternar barra de ruta",
|
||||||
"toggle_quick_preview": "Alternar vista rápida",
|
"toggle_quick_preview": "Alternar vista rápida",
|
||||||
|
"toggle_sidebar": "Alternar barra lateral",
|
||||||
|
"lock_sidebar": "Bloquear barra lateral",
|
||||||
|
"hide_sidebar": "Ocultar barra lateral",
|
||||||
|
"drag_to_resize": "Arrastrar para redimensionar",
|
||||||
|
"click_to_hide": "Clic para ocultar",
|
||||||
|
"click_to_lock": "Clic para bloquear",
|
||||||
"tools": "Herramientas",
|
"tools": "Herramientas",
|
||||||
"trash": "Basura",
|
"trash": "Basura",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
|
|
|
@ -479,6 +479,12 @@
|
||||||
"toggle_metadata": "Activer/désactiver les métadonnées",
|
"toggle_metadata": "Activer/désactiver les métadonnées",
|
||||||
"toggle_path_bar": "Activer/désactiver la barre de chemin",
|
"toggle_path_bar": "Activer/désactiver la barre de chemin",
|
||||||
"toggle_quick_preview": "Activer/désactiver l'aperçu rapide",
|
"toggle_quick_preview": "Activer/désactiver l'aperçu rapide",
|
||||||
|
"toggle_sidebar": "Basculer la barre latérale",
|
||||||
|
"lock_sidebar": "Verrouiller la barre latérale",
|
||||||
|
"hide_sidebar": "Masquer la barre latérale",
|
||||||
|
"drag_to_resize": "Faites glisser pour redimensionner",
|
||||||
|
"click_to_hide": "Cliquez pour masquer",
|
||||||
|
"click_to_lock": "Cliquez pour verrouiller",
|
||||||
"tools": "Outils",
|
"tools": "Outils",
|
||||||
"trash": "Poubelle",
|
"trash": "Poubelle",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
|
|
|
@ -479,6 +479,12 @@
|
||||||
"toggle_metadata": "Mostra/nascondi metadati",
|
"toggle_metadata": "Mostra/nascondi metadati",
|
||||||
"toggle_path_bar": "Mostra/nascondi barra del percorso",
|
"toggle_path_bar": "Mostra/nascondi barra del percorso",
|
||||||
"toggle_quick_preview": "Mostra/nascondi anteprima rapida",
|
"toggle_quick_preview": "Mostra/nascondi anteprima rapida",
|
||||||
|
"toggle_sidebar": "Attiva/disattiva la barra laterale",
|
||||||
|
"lock_sidebar": "Blocca la barra laterale",
|
||||||
|
"hide_sidebar": "Nascondi la barra laterale",
|
||||||
|
"drag_to_resize": "Trascina per ridimensionare",
|
||||||
|
"click_to_hide": "Clicca per nascondere",
|
||||||
|
"click_to_lock": "Clicca per bloccare",
|
||||||
"tools": "Utensili",
|
"tools": "Utensili",
|
||||||
"trash": "Spazzatura",
|
"trash": "Spazzatura",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
|
|
|
@ -479,6 +479,12 @@
|
||||||
"toggle_metadata": "詳細パネルを開閉",
|
"toggle_metadata": "詳細パネルを開閉",
|
||||||
"toggle_path_bar": "パスバーの表示切り替え",
|
"toggle_path_bar": "パスバーの表示切り替え",
|
||||||
"toggle_quick_preview": "クイック プレビューを表示",
|
"toggle_quick_preview": "クイック プレビューを表示",
|
||||||
|
"toggle_sidebar": "サイドバーを切り替える",
|
||||||
|
"lock_sidebar": "サイドバーをロックする",
|
||||||
|
"hide_sidebar": "サイドバーを非表示にする",
|
||||||
|
"drag_to_resize": "サイズ変更にドラッグ",
|
||||||
|
"click_to_hide": "非表示にするにはクリック",
|
||||||
|
"click_to_lock": "ロックするにはクリック",
|
||||||
"tools": "ツール",
|
"tools": "ツール",
|
||||||
"trash": "ごみ",
|
"trash": "ごみ",
|
||||||
"type": "種類",
|
"type": "種類",
|
||||||
|
|
|
@ -479,6 +479,12 @@
|
||||||
"toggle_metadata": "Metadata in-/uitschakelen",
|
"toggle_metadata": "Metadata in-/uitschakelen",
|
||||||
"toggle_path_bar": "Padbalk in-/uitschakelen",
|
"toggle_path_bar": "Padbalk in-/uitschakelen",
|
||||||
"toggle_quick_preview": "Snelle voorvertoning in-/uitschakelen",
|
"toggle_quick_preview": "Snelle voorvertoning in-/uitschakelen",
|
||||||
|
"toggle_sidebar": "Zijbalk omwisselen",
|
||||||
|
"lock_sidebar": "Zijbalk vergrendelen",
|
||||||
|
"hide_sidebar": "Zijbalk verbergen",
|
||||||
|
"drag_to_resize": "Verslepen om te wijzigen",
|
||||||
|
"click_to_hide": "Klik om te verbergen",
|
||||||
|
"click_to_lock": "Klik om te vergrendelen",
|
||||||
"tools": "Hulpmiddelen",
|
"tools": "Hulpmiddelen",
|
||||||
"trash": "Afval",
|
"trash": "Afval",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
|
|
|
@ -479,6 +479,12 @@
|
||||||
"toggle_metadata": "Показать метаданные",
|
"toggle_metadata": "Показать метаданные",
|
||||||
"toggle_path_bar": "Открыть адресную строку",
|
"toggle_path_bar": "Открыть адресную строку",
|
||||||
"toggle_quick_preview": "Открыть предпросмотр",
|
"toggle_quick_preview": "Открыть предпросмотр",
|
||||||
|
"toggle_sidebar": "Переключить боковую панель",
|
||||||
|
"lock_sidebar": "Заблокировать боковую панель",
|
||||||
|
"hide_sidebar": "Скрыть боковую панель",
|
||||||
|
"drag_to_resize": "Перетащите, чтобы изменить размер",
|
||||||
|
"click_to_hide": "Щелкните, чтобы скрыть",
|
||||||
|
"click_to_lock": "Щелкните, чтобы заблокировать",
|
||||||
"tools": "Инструменты",
|
"tools": "Инструменты",
|
||||||
"trash": "Корзина",
|
"trash": "Корзина",
|
||||||
"type": "Тип",
|
"type": "Тип",
|
||||||
|
|
|
@ -479,6 +479,12 @@
|
||||||
"toggle_metadata": "Meta verileri aç/kapat",
|
"toggle_metadata": "Meta verileri aç/kapat",
|
||||||
"toggle_path_bar": "Yol çubuğunu aç/kapat",
|
"toggle_path_bar": "Yol çubuğunu aç/kapat",
|
||||||
"toggle_quick_preview": "Hızlı önizlemeyi aç/kapat",
|
"toggle_quick_preview": "Hızlı önizlemeyi aç/kapat",
|
||||||
|
"toggle_sidebar": "Kenar çubuğunu değiştir",
|
||||||
|
"lock_sidebar": "Kenar çubuğunu kilitle",
|
||||||
|
"hide_sidebar": "Kenar çubuğunu gizle",
|
||||||
|
"drag_to_resize": "Boyutlandırmak için sürükleyin",
|
||||||
|
"click_to_hide": "Gizlemek için tıklayın",
|
||||||
|
"click_to_lock": "Kilitlemek için tıklayın",
|
||||||
"tools": "Aletler",
|
"tools": "Aletler",
|
||||||
"trash": "Çöp",
|
"trash": "Çöp",
|
||||||
"type": "Tip",
|
"type": "Tip",
|
||||||
|
|
|
@ -481,6 +481,12 @@
|
||||||
"toggle_metadata": "切换元数据",
|
"toggle_metadata": "切换元数据",
|
||||||
"toggle_path_bar": "切换显示路径栏",
|
"toggle_path_bar": "切换显示路径栏",
|
||||||
"toggle_quick_preview": "切换快速预览",
|
"toggle_quick_preview": "切换快速预览",
|
||||||
|
"toggle_sidebar": "切换侧边栏",
|
||||||
|
"lock_sidebar": "锁定侧边栏",
|
||||||
|
"hide_sidebar": "隐藏侧边栏",
|
||||||
|
"drag_to_resize": "拖动以调整大小",
|
||||||
|
"click_to_hide": "点击隐藏",
|
||||||
|
"click_to_lock": "点击锁定",
|
||||||
"tools": "工具",
|
"tools": "工具",
|
||||||
"trash": "垃圾",
|
"trash": "垃圾",
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
|
|
|
@ -479,6 +479,12 @@
|
||||||
"toggle_metadata": "切換元數據",
|
"toggle_metadata": "切換元數據",
|
||||||
"toggle_path_bar": "切換顯示路徑列",
|
"toggle_path_bar": "切換顯示路徑列",
|
||||||
"toggle_quick_preview": "切換快速預覽",
|
"toggle_quick_preview": "切換快速預覽",
|
||||||
|
"toggle_sidebar": "切換側邊欄",
|
||||||
|
"lock_sidebar": "鎖定側邊欄",
|
||||||
|
"hide_sidebar": "隱藏側邊欄",
|
||||||
|
"drag_to_resize": "拖曳調整大小",
|
||||||
|
"click_to_hide": "點擊隱藏",
|
||||||
|
"click_to_lock": "點擊鎖定",
|
||||||
"tools": "工具",
|
"tools": "工具",
|
||||||
"trash": "垃圾",
|
"trash": "垃圾",
|
||||||
"type": "類型",
|
"type": "類型",
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-loading-icons": "^1.1.0",
|
"react-loading-icons": "^1.1.0",
|
||||||
|
"react-resizable-layout": "^0.7.2",
|
||||||
"react-router-dom": "=6.20.1",
|
"react-router-dom": "=6.20.1",
|
||||||
"sonner": "^1.0.3",
|
"sonner": "^1.0.3",
|
||||||
"use-debounce": "^9.0.4",
|
"use-debounce": "^9.0.4",
|
||||||
|
|
|
@ -72,7 +72,11 @@ const Root = (props: PropsWithChildren<DropdownMenuProps>) => {
|
||||||
<RadixDM.Portal>
|
<RadixDM.Portal>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<RadixDM.Content
|
<RadixDM.Content
|
||||||
className={clsx(contextMenuClassNames, width && '!min-w-0', className)}
|
className={clsx(
|
||||||
|
contextMenuClassNames,
|
||||||
|
width && '!min-w-0 max-w-none',
|
||||||
|
className
|
||||||
|
)}
|
||||||
align="start"
|
align="start"
|
||||||
style={{ width }}
|
style={{ width }}
|
||||||
{...contentProps}
|
{...contentProps}
|
||||||
|
|
114
packages/ui/src/Resizable.tsx
Normal file
114
packages/ui/src/Resizable.tsx
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
HTMLAttributes,
|
||||||
|
PropsWithChildren,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
Resizable as ResizableType,
|
||||||
|
useResizable,
|
||||||
|
UseResizableProps
|
||||||
|
} from 'react-resizable-layout';
|
||||||
|
|
||||||
|
type ResizableContextProps = ResizableType & { size: number; collapsed: boolean };
|
||||||
|
|
||||||
|
const ResizableContext = createContext<ResizableContextProps | null>(null);
|
||||||
|
|
||||||
|
const useResizableContext = () => {
|
||||||
|
const context = useContext(ResizableContext);
|
||||||
|
|
||||||
|
if (!context) throw new Error('ResizableContext.Provider not found!');
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ResizableProps extends Omit<PropsWithChildren<UseResizableProps>, 'axis'> {
|
||||||
|
axis?: UseResizableProps['axis'];
|
||||||
|
collapsed?: boolean;
|
||||||
|
onCollapseChange?: (val: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Resizable = ({ axis = 'x', ...props }: ResizableProps) => {
|
||||||
|
const resizable = useResizable({ axis, ...props });
|
||||||
|
|
||||||
|
const minSizeClientX = useRef<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.onCollapseChange || !resizable.isDragging || !props.min) return;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (minSizeClientX.current === null) {
|
||||||
|
if (props.min === resizable.position && !props.collapsed) {
|
||||||
|
minSizeClientX.current = e.clientX;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const half = minSizeClientX.current / 2;
|
||||||
|
|
||||||
|
if (e.clientX < half && !props.collapsed) props.onCollapseChange!(true);
|
||||||
|
else if (e.clientX > half && props.collapsed) props.onCollapseChange!(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
return () => document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
}, [
|
||||||
|
props.min,
|
||||||
|
props.collapsed,
|
||||||
|
props.onCollapseChange,
|
||||||
|
resizable.isDragging,
|
||||||
|
resizable.position
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!resizable.isDragging) {
|
||||||
|
minSizeClientX.current = null;
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
} else {
|
||||||
|
const cursor = axis === 'x' ? 'col-resize' : 'row-resize';
|
||||||
|
document.body.style.setProperty('cursor', cursor, 'important');
|
||||||
|
}
|
||||||
|
}, [resizable.isDragging, axis]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResizableContext.Provider
|
||||||
|
value={{
|
||||||
|
...resizable,
|
||||||
|
size: props.collapsed ? 0 : resizable.position,
|
||||||
|
collapsed: !!props.collapsed
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</ResizableContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResizablePanel = (props: HTMLAttributes<HTMLDivElement>) => {
|
||||||
|
const resizable = useResizableContext();
|
||||||
|
return <div style={{ width: resizable.size }} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResizableHandle = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => {
|
||||||
|
const resizable = useResizableContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'w-2',
|
||||||
|
'aria-[orientation=horizontal]:cursor-row-resize aria-[orientation=vertical]:cursor-col-resize',
|
||||||
|
'after:absolute after:inset-y-0 after:left-0.5 after:w-0.5 after:bg-accent after:opacity-0 after:transition-opacity hover:after:opacity-100',
|
||||||
|
resizable.isDragging && 'after:opacity-100',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
{...resizable.separatorProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Resizable, ResizableHandle, ResizablePanel, useResizableContext };
|
|
@ -5,8 +5,14 @@ import clsx from 'clsx';
|
||||||
import { PropsWithChildren, ReactNode } from 'react';
|
import { PropsWithChildren, ReactNode } from 'react';
|
||||||
|
|
||||||
import { ModifierKeys } from './keys';
|
import { ModifierKeys } from './keys';
|
||||||
|
import { tw } from './utils';
|
||||||
|
|
||||||
export interface TooltipProps extends PropsWithChildren {
|
export const Kbd = tw.kbd`h-4.5 flex items-center justify-center rounded-md border border-app-selected bg-app-selected/50 px-1.5 py-0.5 text-[10px] text-ink`;
|
||||||
|
|
||||||
|
export interface TooltipProps
|
||||||
|
extends PropsWithChildren,
|
||||||
|
Pick<TooltipPrimitive.TooltipProps, 'disableHoverableContent'>,
|
||||||
|
Pick<TooltipPrimitive.TooltipContentProps, 'alignOffset' | 'sideOffset' | 'align'> {
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
position?: 'top' | 'right' | 'bottom' | 'left';
|
position?: 'top' | 'right' | 'bottom' | 'left';
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -33,7 +39,7 @@ const separateKeybinds = (keybinds: TooltipProps['keybinds']): TooltipProps['key
|
||||||
|
|
||||||
export const Tooltip = ({ position = 'bottom', ...props }: TooltipProps) => {
|
export const Tooltip = ({ position = 'bottom', ...props }: TooltipProps) => {
|
||||||
return (
|
return (
|
||||||
<TooltipPrimitive.Root>
|
<TooltipPrimitive.Root disableHoverableContent={props.disableHoverableContent}>
|
||||||
<TooltipPrimitive.Trigger asChild>
|
<TooltipPrimitive.Trigger asChild>
|
||||||
{props.asChild ? (
|
{props.asChild ? (
|
||||||
props.children
|
props.children
|
||||||
|
@ -44,24 +50,22 @@ export const Tooltip = ({ position = 'bottom', ...props }: TooltipProps) => {
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Content
|
<TooltipPrimitive.Content
|
||||||
side={position}
|
side={position}
|
||||||
|
align={props.align}
|
||||||
|
sideOffset={props.sideOffset}
|
||||||
|
alignOffset={props.alignOffset}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'TooltipContent z-50 m-2 mt-1 flex max-w-[200px] select-text items-center gap-2 break-words rounded border border-app-line bg-app-box px-2 py-1 text-center text-xs text-ink',
|
'TooltipContent z-[101] m-2 mt-1 flex max-w-[200px] select-text items-center gap-2 break-words rounded border border-app-line bg-app-box px-2 py-1 text-center text-xs text-ink',
|
||||||
props.tooltipClassName,
|
props.tooltipClassName,
|
||||||
!props.label && 'hidden'
|
!props.label && 'hidden'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className={props.labelClassName}>{props.label}</p>
|
<div className={props.labelClassName}>{props.label}</div>
|
||||||
{props.keybinds && (
|
{props.keybinds && (
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center gap-1">
|
||||||
{separateKeybinds(props.keybinds)?.map((k, _) => (
|
{separateKeybinds(props.keybinds)?.map((k, _) => (
|
||||||
<kbd
|
<Kbd key={k.toString()}>
|
||||||
key={k.toString()}
|
|
||||||
className={
|
|
||||||
'h-4.5 flex items-center justify-center rounded-md border border-app-selected bg-app-selected/50 px-1.5 py-0.5 text-[10px] text-ink'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<p>{k}</p>
|
<p>{k}</p>
|
||||||
</kbd>
|
</Kbd>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,26 +1,27 @@
|
||||||
export { cva, cx } from 'class-variance-authority';
|
export { cva, cx } from 'class-variance-authority';
|
||||||
export * from './Button';
|
export * from './Button';
|
||||||
export * from './CheckBox';
|
export * from './CheckBox';
|
||||||
export { ContextMenu, useContextMenuContext, ContextMenuDivItem } from './ContextMenu';
|
export * from './CircularProgress';
|
||||||
export { DropdownMenu, useDropdownMenuContext } from './DropdownMenu';
|
export { ContextMenu, ContextMenuDivItem, useContextMenuContext } from './ContextMenu';
|
||||||
export * from './Dialog';
|
export * from './Dialog';
|
||||||
|
export * from './Divider';
|
||||||
export * as Dropdown from './Dropdown';
|
export * as Dropdown from './Dropdown';
|
||||||
|
export { DropdownMenu, useDropdownMenuContext } from './DropdownMenu';
|
||||||
export * from './Input';
|
export * from './Input';
|
||||||
export * from './Layout';
|
export * from './Layout';
|
||||||
export * from './Loader';
|
export * from './Loader';
|
||||||
export * from './Popover';
|
export * from './Popover';
|
||||||
|
export * from './ProgressBar';
|
||||||
|
export * as RadioGroup from './RadioGroup';
|
||||||
|
export * from './Resizable';
|
||||||
export * from './Select';
|
export * from './Select';
|
||||||
|
export * from './Shortcut';
|
||||||
|
export * from './Slider';
|
||||||
export * from './Switch';
|
export * from './Switch';
|
||||||
export * as Tabs from './Tabs';
|
export * as Tabs from './Tabs';
|
||||||
export * as RadioGroup from './RadioGroup';
|
export * from './Toast';
|
||||||
|
export * from './Tooltip';
|
||||||
export * from './Typography';
|
export * from './Typography';
|
||||||
export * from './forms';
|
export * from './forms';
|
||||||
export * from './utils';
|
|
||||||
export * from './Tooltip';
|
|
||||||
export * from './Slider';
|
|
||||||
export * from './Divider';
|
|
||||||
export * from './CircularProgress';
|
|
||||||
export * from './Shortcut';
|
|
||||||
export * from './ProgressBar';
|
|
||||||
export * from './keys';
|
export * from './keys';
|
||||||
export * from './Toast';
|
export * from './utils';
|
||||||
|
|
|
@ -1062,6 +1062,9 @@ importers:
|
||||||
react-loading-icons:
|
react-loading-icons:
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
|
react-resizable-layout:
|
||||||
|
specifier: ^0.7.2
|
||||||
|
version: 0.7.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: '=6.20.1'
|
specifier: '=6.20.1'
|
||||||
version: 6.20.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 6.20.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
@ -11289,6 +11292,12 @@ packages:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react-resizable-layout@0.7.2:
|
||||||
|
resolution: {integrity: sha512-GrVzAecB6+osdAx5PPP3G8R9n7A2uDd3NL+PyrHWNRaVBivZmW/o0+yFjQdS5Bo016A2fIP11fAhefsknWN4aw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17.0.0'
|
||||||
|
react-dom: '>=17.0.0'
|
||||||
|
|
||||||
react-router-dom@6.20.1:
|
react-router-dom@6.20.1:
|
||||||
resolution: {integrity: sha512-npzfPWcxfQN35psS7rJgi/EW0Gx6EsNjfdJSAk73U/HqMEJZ2k/8puxfwHFgDQhBGmS3+sjnGbMdMSV45axPQw==}
|
resolution: {integrity: sha512-npzfPWcxfQN35psS7rJgi/EW0Gx6EsNjfdJSAk73U/HqMEJZ2k/8puxfwHFgDQhBGmS3+sjnGbMdMSV45axPQw==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
@ -26439,6 +26448,11 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.2.67
|
'@types/react': 18.2.67
|
||||||
|
|
||||||
|
react-resizable-layout@0.7.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
|
||||||
react-router-dom@6.20.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
react-router-dom@6.20.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@remix-run/router': 1.13.1(patch_hash=wdk5klbkacqsve2yzdckjvtjz4)
|
'@remix-run/router': 1.13.1(patch_hash=wdk5klbkacqsve2yzdckjvtjz4)
|
||||||
|
|
Loading…
Reference in a new issue