feat: add collapsible sidebar rail mode

This commit is contained in:
2026-04-03 02:29:01 +03:00
parent db6e2818c1
commit 2774b93830

View File

@@ -44,6 +44,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
const [settingsOpen, setSettingsOpen] = useState(false)
const [userMenuOpen, setUserMenuOpen] = useState(false)
const [paletteOpen, setPaletteOpen] = useState(false)
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const logoutMutation = useMutation({
mutationFn: logout,
onSettled: () => {
@@ -52,6 +53,17 @@ export function AppShell({ children }: { children: React.ReactNode }) {
},
})
useEffect(() => {
const stored = window.localStorage.getItem('temporserv.sidebar-collapsed')
if (stored === 'true') {
setSidebarCollapsed(true)
}
}, [])
useEffect(() => {
window.localStorage.setItem('temporserv.sidebar-collapsed', String(sidebarCollapsed))
}, [sidebarCollapsed])
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.key === '/') {
@@ -80,7 +92,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
<div className="flex items-center gap-2">
<TopIconButton icon={<ArrowLeft size={16} />} />
<TopIconButton icon={<ArrowRight size={16} />} />
<TopIconButton icon={<Disc3 size={16} />} />
<TopIconButton active={sidebarCollapsed} icon={<Disc3 size={16} />} onClick={() => setSidebarCollapsed((value) => !value)} />
</div>
<div className="flex items-center gap-2 text-sm font-medium text-slate-300">
@@ -119,22 +131,32 @@ export function AppShell({ children }: { children: React.ReactNode }) {
</div>
</header>
<div className="grid min-h-0 flex-1 grid-cols-[276px_minmax(0,1fr)]">
<div className={['grid min-h-0 flex-1', sidebarCollapsed ? 'grid-cols-[62px_minmax(0,1fr)]' : 'grid-cols-[276px_minmax(0,1fr)]'].join(' ')}>
<aside className="flex min-h-0 flex-col border-r border-[#24314f] bg-[#0a1226]">
<div className="p-4">
<div className="flex items-center gap-2 rounded-[10px] border border-[#24314f] bg-[#0c1730] px-3 py-2 text-slate-400">
<Search size={16} />
<input
onFocus={() => setPaletteOpen(true)}
className="w-full bg-transparent text-sm outline-none placeholder:text-slate-500"
placeholder="Поиск..."
/>
<span className="rounded-md border border-[#2b3652] px-2 py-0.5 text-xs text-slate-500">/</span>
</div>
<div className={sidebarCollapsed ? 'p-3' : 'p-4'}>
{sidebarCollapsed ? (
<button
className="grid h-10 w-10 place-items-center rounded-[10px] border border-[#24314f] bg-[#0c1730] text-slate-400 transition hover:bg-[#18233a] hover:text-white"
onClick={() => setPaletteOpen(true)}
type="button"
>
<Search size={18} />
</button>
) : (
<div className="flex items-center gap-2 rounded-[10px] border border-[#24314f] bg-[#0c1730] px-3 py-2 text-slate-400">
<Search size={16} />
<input
onFocus={() => setPaletteOpen(true)}
className="w-full bg-transparent text-sm outline-none placeholder:text-slate-500"
placeholder="Поиск..."
/>
<span className="rounded-md border border-[#2b3652] px-2 py-0.5 text-xs text-slate-500">/</span>
</div>
)}
</div>
<div className="px-3 pb-3 text-xs uppercase tracking-[0.18em] text-slate-500">Библиотека</div>
<nav className="space-y-1 px-3">
{!sidebarCollapsed ? <div className="px-3 pb-3 text-xs uppercase tracking-[0.18em] text-slate-500">Библиотека</div> : null}
<nav className={sidebarCollapsed ? 'space-y-1 px-2' : 'space-y-1 px-3'}>
{libraryLinks.map((item) => {
const Icon = item.icon
return (
@@ -143,29 +165,34 @@ export function AppShell({ children }: { children: React.ReactNode }) {
to={item.to}
className={({ isActive }) =>
[
'flex items-center gap-3 rounded-[10px] px-4 py-3 text-[0.95rem] transition',
sidebarCollapsed
? 'flex items-center justify-center rounded-[10px] px-0 py-3 text-[0.95rem] transition'
: 'flex items-center gap-3 rounded-[10px] px-4 py-3 text-[0.95rem] transition',
isActive ? 'bg-[#313d52] text-white' : 'text-slate-100 hover:bg-[#18233a]',
].join(' ')
}
title={sidebarCollapsed ? item.label : undefined}
>
<Icon size={18} />
{item.label}
{!sidebarCollapsed ? item.label : null}
</NavLink>
)
})}
</nav>
<div className="mt-5 px-3 pb-3 text-xs uppercase tracking-[0.18em] text-slate-500">Плейлисты</div>
<div className="px-3">
<div className="mb-3 flex items-center justify-between text-slate-400">
<span className="text-sm">Плейлисты</span>
<button className="grid h-7 w-7 place-items-center rounded-md bg-[#0ec28c] text-[#081225]" type="button">
{!sidebarCollapsed ? <div className="mt-5 px-3 pb-3 text-xs uppercase tracking-[0.18em] text-slate-500">Плейлисты</div> : null}
<div className={sidebarCollapsed ? 'mt-5 px-2' : 'px-3'}>
<div className={sidebarCollapsed ? 'flex justify-center text-slate-400' : 'mb-3 flex items-center justify-between text-slate-400'}>
{!sidebarCollapsed ? <span className="text-sm">Плейлисты</span> : null}
<button className="grid h-7 w-7 place-items-center rounded-md bg-[#0ec28c] text-[#081225]" title="Создать плейлист" type="button">
+
</button>
</div>
<div className="rounded-[10px] bg-[#313d52] px-4 py-3 text-[0.95rem] text-slate-100">
Пока не создано ни одного плейлиста
</div>
{!sidebarCollapsed ? (
<div className="rounded-[10px] bg-[#313d52] px-4 py-3 text-[0.95rem] text-slate-100">
Пока не создано ни одного плейлиста
</div>
) : null}
</div>
</aside>
@@ -191,13 +218,18 @@ export function AppShell({ children }: { children: React.ReactNode }) {
function TopIconButton({
icon,
onClick,
active = false,
}: {
icon: React.ReactNode
onClick?: () => void
active?: boolean
}) {
return (
<button
className="grid h-8 w-8 place-items-center rounded-md text-slate-300 transition hover:bg-[#18233a] hover:text-white"
className={[
'grid h-8 w-8 place-items-center rounded-md text-slate-300 transition hover:bg-[#18233a] hover:text-white',
active ? 'bg-[#313d52] text-white' : '',
].join(' ')}
onClick={onClick}
type="button"
>