feat: settings page

This commit is contained in:
Szedann 2025-08-29 13:07:42 +02:00
parent e2e1cdd616
commit 67488594bb
11 changed files with 746 additions and 3 deletions

View file

@ -230,7 +230,7 @@ export type VersionType =
*/
"old_beta"
const ARGS_MAP = { 'core':'{"getClusterById":["id"],"getLogByName":["id","name"],"getLogs":["id"],"openMsaLogin":[],"writeSettings":["setting"],"removeUser":["uuid"],"searchPackages":["provider","query"],"getClusters":[],"updateClusterById":["id","request"],"getUser":["uuid"],"getPackage":["provider","slug"],"getLoadersForVersion":["mc_version"],"getUsers":[],"getScreenshots":["id"],"getPackageBody":["provider","body"],"launchCluster":["id","uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"updateClusterProfile":["name","profile"],"getMultiplePackages":["provider","slugs"],"setDefaultUser":["uuid"],"getWorlds":["id"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getUsersFromAuthor":["provider","author"],"fetchMinecraftProfile":["uuid"],"removeCluster":["id"],"readSettings":[],"getGlobalProfile":[],"getDefaultUser":["fallback"],"getGameVersions":[],"createCluster":["options"],"getProfileOrDefault":["name"]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}' }
const ARGS_MAP = { 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'core':'{"readSettings":[],"getLogs":["id"],"getGameVersions":[],"getClusters":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"launchCluster":["id","uuid"],"getClusterById":["id"],"getUsersFromAuthor":["provider","author"],"setDefaultUser":["uuid"],"removeUser":["uuid"],"updateClusterProfile":["name","profile"],"openMsaLogin":[],"getDefaultUser":["fallback"],"getScreenshots":["id"],"removeCluster":["id"],"getMultiplePackages":["provider","slugs"],"writeSettings":["setting"],"getUsers":[],"createCluster":["options"],"getWorlds":["id"],"updateClusterById":["id","request"],"getGlobalProfile":[],"getUser":["uuid"],"getLogByName":["id","name"],"searchPackages":["provider","query"],"getProfileOrDefault":["name"],"getLoadersForVersion":["mc_version"],"getPackage":["provider","slug"],"getPackageBody":["provider","body"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"fetchMinecraftProfile":["uuid"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[]}' }
export type Router = { 'oneclient': { openDevTools: () => Promise<void>,
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>> },
'core': { getClusters: () => Promise<ClusterModel[]>,

View file

@ -4,15 +4,16 @@ import LauncherLogo from '@/assets/logos/oneclient.svg?react';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { Button, Popup } from '@onelauncher/common/components';
import { Link } from '@tanstack/react-router';
import { Link, useNavigate } from '@tanstack/react-router';
import { Window } from '@tauri-apps/api/window';
import { MinusIcon, SquareIcon, XCloseIcon } from '@untitled-theme/icons-react';
import { MinusIcon, Settings01Icon, Settings02Icon, SquareIcon, XCloseIcon } from '@untitled-theme/icons-react';
import { twMerge } from 'tailwind-merge';
import { AccountAvatar } from './AccountAvatar';
import { AccountPopup } from './overlay/AccountPopup';
export function Navbar() {
const { data: currentAccount } = useCommand(['getDefaultUser'], () => bindings.core.getDefaultUser(true));
const navigate = useNavigate();
const onMinimize = () => Window.getCurrent().minimize();
const onMaximize = () => Window.getCurrent().toggleMaximize();
@ -31,6 +32,10 @@ export function Navbar() {
</div>
<div className="flex flex-1 items-center justify-end gap-2 pointer-events-none">
<NavbarButton
children={<Settings02Icon height={28} width={28} />}
onClick={() => navigate({ to: '/app/settings' })}
/>
<Popup.Trigger>
<NavbarButton>
<AccountAvatar className="w-full h-full rounded-lg" uuid={currentAccount?.id} />

View file

@ -21,6 +21,7 @@ export function PackageItem({ provider, clusterId, ...item }: SearchResult & { p
return (
<Link
className="h-full min-w-50 overflow-hidden rounded-lg bg-component-bg hover:bg-component-bg-hover grid grid-rows-[7rem_auto] max-h-74 min-h-74"
preload={false}
search={{ provider, packageId: item.project_id, clusterId }}
tabIndex={0}
to="/app/cluster/browser/package"

View file

@ -0,0 +1,16 @@
import type { useSettings } from '@/hooks/useSettings';
import { Switch } from '@onelauncher/common/components';
type BooleanSetting = [
boolean,
(value: boolean) => void,
];
export default function SettingsSwitch({ setting }: { setting: BooleanSetting }) {
return (
<Switch
isSelected={setting[0]}
onChange={val => setting[1](val)}
/>
);
}

View file

@ -0,0 +1,45 @@
import type { JSX } from 'react';
export interface SettingsRowProps {
title: JSX.Element | string;
description: JSX.Element | string;
icon: JSX.Element;
disabled?: boolean;
children?: JSX.Element;
}
function SettingsRow({ disabled, description, icon, title, children }: SettingsRowProps) {
return (
<div
className={`
flex flex-row items-center gap-3.5 rounded-xl p-4 my-2
${disabled ? 'bg-component-bg-disabled' : 'bg-page-elevated hover:bg-component-bg-hover'}
`}
>
<div className="flex h-8 w-8 items-center justify-center">
{icon}
</div>
<div className="flex flex-1 flex-col gap-2">
<p className="text-lg capitalize leading-tight">{title}</p>
<p className="text-sm text-fg-secondary">{description}</p>
</div>
<div className="flex h-9 flex-row items-center gap-2">
{children}
</div>
</div>
);
}
interface SettingsRowHeaderProps {
className?: string;
children?: React.ReactNode;
}
SettingsRow.Header = ({ className, children }: SettingsRowHeaderProps) => {
const headerClasses = `mt-4 mb-1 ml-2 text-md text-fg-secondary uppercase ${className || ''}`;
return <h3 className={headerClasses}>{children}</h3>;
};
export default SettingsRow;

View file

@ -0,0 +1,20 @@
import { useLocation } from '@tanstack/react-router';
import { useEffect, useRef } from 'react';
export default function usePopState(onRouteChange: void | any) {
const location = useLocation();
const prevLocationRef = useRef(location);
useEffect(() => {
const prevLocation = prevLocationRef.current;
// Only run if the pathname actually changed
if (prevLocation.pathname !== location.pathname)
onRouteChange?.({
from: prevLocation,
to: location,
});
prevLocationRef.current = location;
}, [location, onRouteChange]);
}

View file

@ -0,0 +1,25 @@
import type { Settings } from '@/bindings.gen';
import { bindings } from '@/main';
import { useCommandSuspense } from '@onelauncher/common';
import { useMemo } from 'react';
export function useSettings() {
const settings = useCommandSuspense(['readSettings'], bindings.core.readSettings);
const setting = useMemo(() => <TKey extends keyof Settings>(key: TKey) => settings.data[key], [settings]);
const setSetting = useMemo(() => <TKey extends keyof Settings>(key: TKey, value: Settings[TKey]) => {
bindings.core.writeSettings({
...settings.data,
[key]: value,
});
settings.refetch();
}, [settings]);
type Setter<TKey extends keyof Settings> = (value: Settings[TKey]) => void;
const createSetting = useMemo(() => <TKey extends keyof Settings>(key: TKey): [Settings[TKey], Setter<TKey>] => {
return [setting(key), value => setSetting(key, value)];
}, [setting]);
return { setting, setSetting, createSetting };
}

View file

@ -0,0 +1,224 @@
// import ScrollableContainer from '@/components/ScrollableContainer';
import SettingsRow from '@/components/SettingsRow';
import SettingsSwitch from '@/components/SettingSwitch';
import { useSettings } from '@/hooks/useSettings';
import { bindings } from '@/main';
import { THEMES } from '@/utils/theming';
import { Switch } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { Monitor01Icon, PackageIcon, Speedometer04Icon } from '@untitled-theme/icons-react';
import Sidebar from './route';
export const Route = createFileRoute('/app/settings/appearance')({
component: RouteComponent,
});
function RouteComponent() {
const { createSetting } = useSettings();
return (
<Sidebar.Page>
{/* <ScrollableContainer> */}
<div className="h-full">
<h1>Appearance</h1>
<div className="flex flex-row items-center gap-4">
<PrimaryThemeCard />
<div className="grid grid-cols-3 h-full gap-4">
{/* <For each={Object.keys(THEMES) as (keyof typeof THEMES)[]}>
{theme => (
<For each={THEMES[theme ?? 'OneLauncher'].variants}>
{variant => (
<ThemeCard
setTheme={setTheme}
themeSelector={`${theme}-${variant.name}`}
/>
)}
</For>
)}
</For> */}
{Object.keys(THEMES).map((theme) => {
return THEMES[theme as keyof typeof THEMES].variants.map((variant) => {
return (
<ThemeCard key={`${theme}-${variant.name}`} setTheme={() => { }} themeSelector={`${theme}-${variant.name}`} />
);
});
})}
</div>
</div>
<SettingsRow
description="Change the look of the package list."
icon={<PackageIcon />}
title="Package List Style"
>
<p>i hope i'll fix this</p>
</SettingsRow>
<SettingsRow
description="Uses custom window frame for the launcher."
icon={<Monitor01Icon />}
title="Custom Window Frame"
>
<SettingsSwitch setting={createSetting('native_window_frame')} />
</SettingsRow>
<SettingsRow
description="Disables all animations in the launcher."
icon={<Speedometer04Icon />}
title="Disable Animations"
>
{/* <SettingsSwitch setting={createSetting("disable_animations")}/> */}
</SettingsRow>
</div>
{/* </ScrollableContainer> */}
</Sidebar.Page>
);
}
interface ThemeCardProps {
themeSelector: string;
setTheme: (merged: string) => void;
};
function ThemeCard({
themeSelector,
setTheme,
}: ThemeCardProps) {
return (
<div className={`h-min theme-${themeSelector}`} onClick={() => setTheme(themeSelector)}>
<svg
fill="none"
height="78"
viewBox="0 0 126 78"
width="126"
xmlns="http://www.w3.org/2000/svg"
>
<rect
className="fill-page-elevated"
height="78"
rx="8"
width="126"
/>
<rect
className="stroke-border"
height="77"
rx="7.5"
stroke-opacity="0.1"
width="125"
x="0.5"
y="0.5"
/>
<path
className="stroke-fg-primary"
d="M8 16H116"
stroke-linecap="round"
stroke-width="5"
/>
<path
className="stroke-fg-secondary"
d="M8 26H56"
stroke-linecap="round"
stroke-width="3"
/>
<rect
className="fill-brand"
fill-opacity="0.5"
height="16"
rx="4"
width="64"
x="54"
y="54"
/>
<path d="M54 58C54 55.7909 55.7909 54 58 54H86V70H58C55.7909 70 54 68.2091 54 66V58Z" fill="rgb(var(--clr-brand))" />
</svg>
</div>
);
}
function PrimaryThemeCard() {
return (
<div className="w-[296] h-[183]">
<svg
fill="none"
height="183"
viewBox="0 0 296 183"
width="296"
xmlns="http://www.w3.org/2000/svg"
>
<rect
className="fill-page"
height="183"
rx="16"
width="296"
/>
<rect
className="stroke-border/10"
height="180"
rx="14.5"
strokeWidth="1.5"
width="293"
x="1.5"
y="1.5"
/>
<rect
className="fill-page-elevated"
height="123"
rx="8"
width="168"
x="112"
y="44"
/>
<rect
className="stroke-border/5"
height="122"
rx="7.5"
width="167"
x="112.5"
y="44.5"
/>
<rect
className="fill-brand/50"
height="16"
rx="4"
width="64"
x="208"
y="143"
/>
<path
className="stroke-fg-primary"
d="M132 64H240"
strokeLinecap="round"
strokeWidth="5"
/>
<path
className="stroke-fg-secondary"
d="M132 74H180"
strokeLinecap="round"
strokeWidth="3"
/>
<path
className="stroke-code-error"
d="M132 94H216"
strokeLinecap="round"
strokeWidth="4"
/>
<path
className="stroke-code-warn"
d="M132 104H192"
strokeLinecap="round"
strokeWidth="4"
/>
<path
className="stroke-code-info"
d="M132 114H210"
strokeLinecap="round"
strokeWidth="4"
/>
<path className="fill-brand" d="M208 147C208 144.791 209.791 143 212 143H240V159H212C209.791 159 208 157.209 208 155V147Z" />
</svg>
</div>
);
}

View file

@ -0,0 +1,81 @@
// import DiscordIcon from '@/assets/logos/discord.svg';
import SettingsRow from '@/components/SettingsRow';
import { useSettings } from '@/hooks/useSettings';
// import useSettings from '@/hooks/useSettings';
import { Button, Switch } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { dataDir, join } from '@tauri-apps/api/path';
import { openPath } from '@tauri-apps/plugin-opener';
import { FolderIcon, Link03Icon, LinkExternal01Icon } from '@untitled-theme/icons-react';
import { useEffect, useState } from 'react';
import Sidebar from './route';
export const Route = createFileRoute('/app/settings/')({
component: RouteComponent,
});
function RouteComponent() {
const { setting, setSetting } = useSettings();
const [launcherDir, setLauncherDir] = useState('');
useEffect(() => {
(async () => {
setLauncherDir(await join(await dataDir(), 'OneLauncher'));
})();
}, []);
const openLauncherDir = () => openPath(launcherDir);
// const [discordRpc, setDiscordRpc] = createSetting('discord_enabled', settings?.discord_enabled);
return (
<Sidebar.Page>
<div className="h-full">
<h1>General</h1>
<SettingsRow
description="Enable Discord Rich Presence."
icon={<Link03Icon />}
title="Discord RPC"
>
<Switch
isSelected={setting('discord_enabled')}
onChange={val => setSetting('discord_enabled', val)}
/>
</SettingsRow>
{/* <SettingsRow
description="Hide the confirmation dialog when closing the launcher."
icon={<XIcon />}
title="Hide Close Dialog"
>
<Switch />
</SettingsRow>
<SettingsRow
description="Sends errors and crash logs using Sentry to help developers fix issues. (// TODO)"
icon={<AlertSquareIcon />}
title="Error Analytics"
>
<Toggle
checked={() => !(settings().disable_analytics ?? false)}
onChecked={value => settings().disable_analytics = !value}
/>
</SettingsRow> */}
<SettingsRow.Header>Folders and Files</SettingsRow.Header>
<SettingsRow
description={launcherDir}
icon={<FolderIcon />}
title="Launcher Folder"
>
<Button onClick={openLauncherDir} size="normal">
<LinkExternal01Icon />
{' '}
Open
</Button>
</SettingsRow>
</div>
</Sidebar.Page>
);
}

View file

@ -0,0 +1,191 @@
import type { SettingProfileModel } from '@/bindings.gen';
// import ScrollableContainer from '@/components/ScrollableContainer';
import SettingsRow from '@/components/SettingsRow';
import usePopState from '@/hooks/usePopState';
import { useSettings } from '@/hooks/useSettings';
import { bindings } from '@/main';
import { useCommandMut, useCommandSuspense } from '@onelauncher/common';
import { Switch, TextField } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { Database01Icon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, XIcon } from '@untitled-theme/icons-react';
import { useMemo } from 'react';
import Sidebar from './route';
export const Route = createFileRoute('/app/settings/minecraft')({
component: RouteComponent,
});
function RouteComponent() {
const result = useCommandSuspense(['readSettings'], bindings.core.readSettings);
const save = useCommandMut(() => bindings.core.writeSettings(result.data));
usePopState(() => {
save.mutate();
});
return (
<Sidebar.Page>
{/* <ScrollableContainer> */}
<div className="h-full">
<h1>Minecraft Settings</h1>
<GameSettings />
{/* <LauncherSettings /> */}
<ProcessSettings />
</div>
{/* </ScrollableContainer> */}
</Sidebar.Page>
);
}
export function GameSettings() {
const { createSetting } = useSettings();
const [gameSettings, setGameSettings] = createSetting('global_game_settings');
// const getSetting = useMemo(()=><TKey extends keyof SettingProfileModel>(key: TKey)=>gameSettings[key], [gameSettings])
const setSetting = useMemo(() => <TKey extends keyof SettingProfileModel>(key: TKey, value: SettingProfileModel[TKey]) => {
setGameSettings({
...gameSettings,
[key]: value,
});
}, [gameSettings, setGameSettings]);
return (
<>
<SettingsRow.Header>Game</SettingsRow.Header>
<SettingsRow
description="Force Minecraft to start in fullscreen mode."
icon={<Maximize01Icon />}
title="Force Fullscreen"
>
<Switch
isSelected={gameSettings.force_fullscreen ?? false}
onChange={val => setSetting('force_fullscreen', val)}
/>
</SettingsRow>
<SettingsRow
description="The game window resolution in pixels."
icon={<LayoutTopIcon />}
title="Resolution"
>
<div className="grid grid-cols-[70px_16px_70px] gap-2 grid-justify-center grid-items-center">
<TextField
className="text-center"
onChange={(e) => {
setSetting('res', { height: gameSettings.res?.height ?? 720, width: Number(e.currentTarget.value) });
}}
type="number"
value={gameSettings.res?.width}
/>
<XIcon className="size-4 self-center" />
<TextField
className="text-center"
onChange={(e) => {
setSetting('res', { width: gameSettings.res?.height ?? 1280, height: Number(e.currentTarget.value) });
}}
type="number"
value={gameSettings.res?.height}
/>
</div>
</SettingsRow>
<SettingsRow
description="The amount of memory in megabytes allocated for the game."
icon={<Database01Icon />}
title="Memory"
>
<div className="flex items-center gap-x-4 flex-justify-center">
<div className="flex flex-row items-center gap-x-2">
<TextField
className="text-center"
onChange={(e) => {
setSetting('mem_max', Number(e.currentTarget.value));
}}
type="number"
value={gameSettings.mem_max ?? undefined}
/>
</div>
</div>
</SettingsRow>
</>
);
}
// function LauncherSettings() {
// return (
// <>
// <SettingsRow.Header>Launcher</SettingsRow.Header>
// <SettingsRow
// description="Hide the launcher whenever you start a game."
// icon={<EyeIcon />}
// title="Hide On Launch"
// >
// <ToggleButton children="false" color="primary" />
// </SettingsRow>
// </>
// );
// }
export function ProcessSettings() {
const { createSetting } = useSettings();
const [gameSettings, setGameSettings] = createSetting('global_game_settings');
// const getSetting = useMemo(()=><TKey extends keyof SettingProfileModel>(key: TKey)=>gameSettings[key], [gameSettings])
const setSetting = useMemo(() => <TKey extends keyof SettingProfileModel>(key: TKey, value: SettingProfileModel[TKey]) => {
setGameSettings({
...gameSettings,
[key]: value,
});
}, [gameSettings, setGameSettings]);
return (
<>
<SettingsRow.Header>Process</SettingsRow.Header>
<SettingsRow
description="Command to run before launching the game."
icon={<FilePlus02Icon />}
title="Pre-Launch Command"
>
<TextField
onChange={e => setSetting('hook_pre', e.currentTarget.value)}
placeholder="echo 'Game started'"
value={gameSettings.hook_pre ?? undefined}
/>
</SettingsRow>
<SettingsRow
description="Command to run when launching the game."
icon={<ParagraphWrapIcon />}
title="Wrapper Command"
>
<TextField
onChange={e => setSetting('hook_wrapper', e.currentTarget.value)}
placeholder="gamescope"
value={gameSettings.hook_wrapper ?? undefined}
/>
</SettingsRow>
<SettingsRow
description="Command to run after exiting the game."
icon={<FileX02Icon />}
title="Post-Exit Command"
>
<TextField
onChange={e => setSetting('hook_post', e.currentTarget.value)}
placeholder="echo 'Game exited'"
value={gameSettings.hook_post ?? undefined}
/>
</SettingsRow>
</>
);
}

View file

@ -0,0 +1,135 @@
import type { JSX } from 'react';
import { createFileRoute, Outlet, useNavigate, useRouterState } from '@tanstack/react-router';
import { Brush01Icon, CodeSnippet02Icon, MessageTextSquare01Icon, RefreshCcw02Icon, Rocket02Icon, Sliders04Icon, Users01Icon } from '@untitled-theme/icons-react';
import { useEffect } from 'react';
export const Route = createFileRoute('/app/settings')({
component: RouteComponent,
});
function RouteComponent() {
return (
<div className="h-full flex flex-1 flex-row gap-x-7">
<div className="mt-8 flex flex-col justify-between">
<Sidebar
base="/app/settings"
links={{
'Launcher Settings': [
[<Rocket02Icon key="general" />, 'General', '/'],
[<Brush01Icon key="appearence" />, 'Appearance', '/appearance'],
// [<Key01Icon />, 'APIs', '/apis'],
// [<Globe01Icon />, 'Language', '/language'],
],
'Game Settings': [
[<Sliders04Icon key="mcsettings" />, 'Minecraft settings', '/minecraft'],
],
'About': [
[<RefreshCcw02Icon key="changelog" />, 'Changelog', '/changelog'],
[<MessageTextSquare01Icon key="feedback" />, 'Feedback', '/feedback'],
[<CodeSnippet02Icon key="dev" />, 'Developer Options', '/developer'],
],
}}
/>
{/* <Info /> */}
</div>
<div className="h-full w-full flex flex-col">
<Outlet />
</div>
</div>
);
}
interface SidebarProps {
base: string;
links: Record<string, Array<[JSX.Element, string, string, URLSearchParams?] | undefined>>;
}
function Sidebar(props: SidebarProps) {
const { base, links } = props;
const navigate = useNavigate();
const routerState = useRouterState();
const location = routerState.location.pathname;
useEffect(() => {
if (base.endsWith('/'))
throw new Error('Base should not end with a slash');
});
function goto(href: string, params?: URLSearchParams) {
const currParams = new URLSearchParams(location);
if (params)
for (const [key, value] of params)
currParams.set(key, value);
const url = `${base}${href}`;
navigate({ to: url });
}
function isActive(link: string, params: URLSearchParams | undefined) {
const fullBasePath = `${base}${link}`;
const pathnameMatch = location.startsWith(fullBasePath) || `${location}/`.startsWith(fullBasePath);
if (!params)
return pathnameMatch;
const searchParams = new URLSearchParams(routerState.location.searchStr);
for (const [key, value] of params)
if (searchParams.get(key) !== value)
return false;
return pathnameMatch;
}
return (
<div className="w-56 flex flex-col pr-2">
{Object.keys(links).map(section => (
<div className="flex flex-col gap-y-2" key={section}>
<div>
<p className="m-1.5 mt-5 text-xs text-fg-secondary font-medium">{section.toUpperCase()}</p>
<div className="flex flex-col gap-y-1 fill-fg-primary text-fg-primary font-medium">
{links[section].map((entry) => {
if (!entry)
return '';
const [Component, label, link, params] = entry;
return (
<a
className={
`px-3 py-1 rounded-md text-md border hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link, params) ? 'bg-component-bg border-component-bg-pressed' : 'border-transparent'}`
}
key={link}
onClick={() => goto(link, params)}
>
<span className="flex flex-row items-center gap-x-3 *:w-5">
{Component}
{label}
</span>
</a>
);
})}
</div>
</div>
</div>
))}
</div>
);
}
interface SidebarPageProps {
className?: string;
children: JSX.Element | Array<JSX.Element>;
}
Sidebar.Page = function (props: SidebarPageProps) {
return (
<div {...props} className={`flex flex-col flex-1 gap-y-1 overflow-y-auto h-full ${props.className || ''}`}>
{props.children}
</div>
);
};
export default Sidebar;