feat: add collapsible sidebar rail mode
This commit is contained in:
@@ -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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user