AppShell
Admin panel va dashboard qolipi: yig'iladigan Sidebar, yopishqoq header, telefonda Drawer va skip link.
Bosh sahifa
Tugma yoki Ctrl/⌘ + B sidebar'ni yig'adi. Holat localStorage'da saqlanadi.
'use client';
import { AppShell, Sidebar } from '@akhatmirzo/ui';
import { BarChart3, Box, Home, Settings, Store, Users } from 'lucide-react';
import { useState } from 'react';
const items = [
{ id: 'home', label: 'Bosh sahifa', icon: <Home /> },
{ id: 'orders', label: 'Buyurtmalar', icon: <Box />, badge: 12 },
{ id: 'customers', label: 'Mijozlar', icon: <Users /> },
{ id: 'reports', label: 'Hisobotlar', icon: <BarChart3 /> },
];
export default function AppShellBasic() {
const [page, setPage] = useState('home');
const current = items.find((i) => i.id === page)?.label ?? 'Sozlamalar';
return (
// Hujjat ichida cheklangan ramka; haqiqiy ilovada AppShell butun ekranni egallaydi
<div className="h-[480px] w-full overflow-auto rounded-xl border border-border">
<AppShell className="[--ak-app-shell-height:480px]" sidebarStorageKey="docs-app-shell">
<Sidebar>
<Sidebar.Header>
<span className="grid size-9 shrink-0 place-items-center rounded-xl bg-primary text-primary-foreground">
<Store className="size-5" aria-hidden="true" />
</span>
<span className="truncate text-sm font-bold">Chorsu Market</span>
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group label="Do'kon">
{items.map((item) => (
<Sidebar.Item
key={item.id}
icon={item.icon}
badge={item.badge}
active={page === item.id}
onClick={() => setPage(item.id)}
>
{item.label}
</Sidebar.Item>
))}
</Sidebar.Group>
<Sidebar.Group label="Boshqaruv">
<Sidebar.Item
icon={<Settings />}
active={page === 'settings'}
onClick={() => setPage('settings')}
>
Sozlamalar
</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar>
<AppShell.Main>
<AppShell.Header>
<AppShell.SidebarToggle />
<span className="text-sm font-medium">{current}</span>
</AppShell.Header>
<div className="flex flex-col gap-3 p-6">
<h2 className="m-0 text-xl font-semibold">{current}</h2>
<p className="m-0 text-sm text-text-muted">
Tugma yoki Ctrl/⌘ + B sidebar'ni yig'adi. Holat localStorage'da saqlanadi.
</p>
</div>
</AppShell.Main>
</AppShell>
</div>
);
}
Import
import { AppShell, Sidebar, useAppShell } from '@akhatmirzo/ui';Qachon ishlatish
- Admin panel, CRM, dashboard — chapda doimiy menyu, o'ngda sahifa kontenti.
- Ko'p bo'limli ilova, foydalanuvchi bo'limlar orasida tez-tez o'tadi.
- Marketing sahifa, blog uchun emas — u yerda yuqori navigatsiya qulayroq.
- Qolip butun ekranni egallaydi; uni ilova
layoutida bir marta qo'ying.
Tuzilishi
| Qism | Vazifasi |
|---|---|
AppShell | Holat (yig'ilgan/to'liq), saqlash, mod+B yorlig'i, skip link |
Sidebar | Desktop'da chapdagi panel (256px ↔ 64px), < 1024px da chapdan Drawer |
Sidebar.Header | Logotip, ilova nomi |
Sidebar.Content | <nav aria-label="Asosiy menyu">, aylantiriladi |
Sidebar.Group | Sarlavha + elementlar ro'yxati; collapsible bo'lishi mumkin |
Sidebar.Item | Havola (href), tugma (onClick) yoki renderLink orqali Next Link |
Sidebar.Footer | Pastki qism: foydalanuvchi menyusi |
AppShell.Main | <main> — skip link shu yerga olib keladi |
AppShell.Header | Yopishqoq (sticky top-0), 56px, pastki chegara |
AppShell.SidebarToggle | Sidebar tugmasi: desktop'da yig'adi, telefonda Drawer'ni ochadi |
// app/(admin)/layout.tsx
<AppShell sidebarStorageKey="sidebar">
<Sidebar>
<Sidebar.Header>
<Logo />
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group label="Asosiy">
<Sidebar.Item icon={<Home />} href="/" active={pathname === '/'}>
Bosh sahifa
</Sidebar.Item>
<Sidebar.Item icon={<Box />} href="/orders" badge={12}>
Buyurtmalar
</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Footer>
<UserMenu />
</Sidebar.Footer>
</Sidebar>
<AppShell.Main>
<AppShell.Header>
<AppShell.SidebarToggle />
<Breadcrumb items={…} />
</AppShell.Header>
{children}
</AppShell.Main>
</AppShell>Yig'ilgan holat
Desktop'da (≥ 1024px) sidebar 64px'lik ikonkalar qatoriga yig'iladi: element nomlari
yashiriladi, har birida Tooltip va aria-label paydo bo'ladi, hisoblagich nuqtaga aylanadi.
Yig'ish — AppShell.SidebarToggle yoki Ctrl/⌘ + B. Animatsiya grid-template-columns orqali,
prefers-reduced-motion da sakrab o'tadi.
collapsible guruh sarlavhasi bosilganda yig'iladi (Accordion uslubida); yig'ilgan panelda
guruhlar doim ochiq, sarlavha o'rnida ajratuvchi chiziq.
Yig'ilgan holatda faqat ikonkalar ko'rinadi — ustiga kelsangiz Tooltip chiqadi. "Hisobotlar" guruhi to'liq holatda yig'iladi.
'use client';
import { AppShell, Avatar, Sidebar } from '@akhatmirzo/ui';
import { FileText, Home, PieChart, Store, TrendingUp, Wallet } from 'lucide-react';
export default function AppShellCollapsed() {
return (
<div className="h-[480px] w-full overflow-auto rounded-xl border border-border">
<AppShell
className="[--ak-app-shell-height:480px]"
defaultSidebarOpen={false}
shortcut={false}
>
<Sidebar>
<Sidebar.Header>
<span className="grid size-9 shrink-0 place-items-center rounded-xl bg-primary text-primary-foreground">
<Store className="size-5" aria-hidden="true" />
</span>
<span className="truncate text-sm font-bold">Chorsu Market</span>
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.Item icon={<Home />} href="#home" active>
Bosh sahifa
</Sidebar.Item>
<Sidebar.Item icon={<Wallet />} href="#payments" badge={3}>
To'lovlar
</Sidebar.Item>
</Sidebar.Group>
<Sidebar.Group label="Hisobotlar" collapsible>
<Sidebar.Item icon={<TrendingUp />} href="#sales">
Savdo
</Sidebar.Item>
<Sidebar.Item icon={<PieChart />} href="#categories">
Toifalar
</Sidebar.Item>
<Sidebar.Item icon={<FileText />} href="#tax" disabled>
Soliq (tez orada)
</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Footer>
<div className="flex min-w-0 items-center gap-3">
<Avatar name="Dilnoza Karimova" size="sm" />
<span className="truncate text-sm font-medium">Dilnoza Karimova</span>
</div>
</Sidebar.Footer>
</Sidebar>
<AppShell.Main>
<AppShell.Header>
<AppShell.SidebarToggle />
<span className="text-sm font-medium">Bosh sahifa</span>
</AppShell.Header>
<p className="m-0 p-6 text-sm text-text-muted">
Yig'ilgan holatda faqat ikonkalar ko'rinadi — ustiga kelsangiz Tooltip chiqadi.
"Hisobotlar" guruhi to'liq holatda yig'iladi.
</p>
</AppShell.Main>
</AppShell>
</div>
);
}
Holatni saqlash
sidebarStorageKey berilsa, holat localStorage ga yoziladi va keyingi tashrifda tiklanadi.
SSR-xavfsiz: serverda va birinchi renderda defaultSidebarOpen chiziladi, saqlangan qiymat
mount'dan keyin o'qiladi (hydration xatosi bo'lmaydi). Storage taqiqlangan bo'lsa ham holat
ishlayveradi.
Telefon va planshet
< 1024px da sidebar o'rniga chapdan chiqadigan Drawer ishlatiladi:
AppShell.SidebarToggle uni ochadi, Sidebar.Item bosilganda (sahifa o'tishi) o'zi yopiladi.
Desktop/mobil almashuvi CSS breakpoint orqali — server va brauzer bir xil HTML chizadi.
Next.js Link bilan
renderLink — o'z havola komponentingizni ishlatish uchun. Unga href, className,
aria-current, onClick va ichki kontent beriladi — hammasini yoying.
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
function NavItem({ href, icon, children }) {
const pathname = usePathname();
return (
<Sidebar.Item
href={href}
icon={icon}
active={pathname.startsWith(href)}
renderLink={(props) => <Link {...props} />}
>
{children}
</Sidebar.Item>
);
}useAppShell
AppShell ichidagi istalgan komponent holatni o'qishi va o'zgartirishi mumkin:
{ sidebarOpen, setSidebarOpen, toggleSidebar, mobileOpen, setMobileOpen, collapsed }.
Holat: to'liq
'use client';
import { AppShell, Button, Sidebar, useAppShell } from '@akhatmirzo/ui';
import { Home, Inbox } from 'lucide-react';
function Status() {
const { sidebarOpen, setSidebarOpen, collapsed } = useAppShell();
return (
<div className="flex flex-col items-start gap-3 p-6 text-sm">
<p className="m-0 text-text-muted">
Holat: <b className="text-text">{collapsed ? "yig'ilgan" : "to'liq"}</b>
</p>
<Button size="sm" onClick={() => setSidebarOpen(!sidebarOpen)}>
{collapsed ? 'Yoyish' : "Yig'ish"}
</Button>
</div>
);
}
export default function AppShellUseAppShell() {
return (
<div className="h-[360px] w-full overflow-auto rounded-xl border border-border">
<AppShell className="[--ak-app-shell-height:360px]" shortcut={false}>
<Sidebar width={220}>
<Sidebar.Content>
<Sidebar.Group label="Pochta">
<Sidebar.Item icon={<Inbox />} href="#inbox" badge={4} active>
Kiruvchi
</Sidebar.Item>
<Sidebar.Item icon={<Home />} href="#home">
Bosh sahifa
</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar>
<AppShell.Main>
<AppShell.Header>
<AppShell.SidebarToggle />
</AppShell.Header>
<Status />
</AppShell.Main>
</AppShell>
</div>
);
}
Hujjatdagi namunalar
Bu sahifadagi namunalar cheklangan ramka ichida: className="[--ak-app-shell-height:480px]" qolip
balandligini belgilaydi (standart — 100dvh). Haqiqiy ilovada uni bermang.
Props
AppShell
<div> atributlari va ref ham qabul qilinadi.
| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
sidebarOpen | boolean | — | Desktop sidebar to'liq ochiqmi (boshqariladigan rejim) |
defaultSidebarOpen | boolean | true | Boshlang'ich holat |
onSidebarOpenChange | (open: boolean) => void | — | Yig'ilganda / yoyilganda |
sidebarStorageKey | string | — | localStorage kaliti — holat saqlanadi (SSR-xavfsiz) |
shortcut | string | false | 'mod+b' | Yig'ish yorlig'i (useHotkeys formati); false — o'chirilgan |
labels | { skipToContent?: string } | { skipToContent: "Asosiy kontentga o'tish" } | Skip link matni |
Sidebar
<aside> atributlari (desktop panel) va ref ham qabul qilinadi.
| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
width | number | string | 256 | To'liq kenglik; yig'ilganda — 64px |
labels | { nav?: string; drawerTitle?: string } | { nav: "Asosiy menyu", drawerTitle: "Menyu" } | <nav> nomi va mobil Drawer sarlavhasi (ekran o'quvchi uchun) |
Sidebar.Item
| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
icon | ReactNode | — | Chapdagi ikonka (18px); yig'ilganda faqat shu |
href | string | — | Havola manzili; berilmasa — <button> |
onClick | (event: MouseEvent) => void | — | Bosilganda (mobil Drawer baribir yopiladi) |
active | boolean | false | Joriy sahifa — aria-current="page" va ajratilgan fon |
badge | ReactNode | — | Hisoblagich (12) yoki o'z elementingiz |
disabled | boolean | false | O'chirilgan |
label | string | — | Yig'ilgandagi Tooltip va aria-label (standart — children matni) |
renderLink | (props: SidebarItemLinkProps) => ReactElement | — | O'z havola komponentingiz: (p) => <Link {...p} /> |
Sidebar.Group
| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
label | ReactNode | — | Guruh sarlavhasi |
collapsible | boolean | false | Sarlavha bosilganda yig'iladi |
defaultOpen | boolean | true | collapsible guruh boshida ochiqmi |
AppShell.SidebarToggle
IconButton prop'lari; aria-label standart — "Yon menyu", variant — ghost. children
berilsa, ichki ikonka o'rniga chiziladi.
Data atributlari
| Atribut | Qayerda | Qiymat |
|---|---|---|
data-sidebar-state | AppShell | expanded · collapsed |
data-collapsed | Sidebar | yig'ilganda mavjud |
data-active | Sidebar.Item | faol bo'lsa mavjud |
data-disabled | Sidebar.Item | o'chirilganda mavjud |
Klaviatura
| Tugma | Harakat |
|---|---|
Tab (birinchi) | "Asosiy kontentga o'tish" havolasi chiqadi |
Ctrl/⌘ + B | Desktop'da yig'ish/yoyish, telefonda menyuni ochish |
Esc | Mobil menyuni yopadi |
Accessibility
- Skip link.
AppShellsahifa boshiga "Asosiy kontentga o'tish" havolasini qo'yadi — u faqat klaviatura fokusida ko'rinadi vaAppShell.Mainga olib o'tadi. - Navigatsiya. Menyu
<nav aria-label="Asosiy menyu">ichida, faol elementaria-current="page"oladi; guruhlarrole="group"va sarlavha bilan bog'langan. - Yig'ilgan holat. Matn yashirinsa ham har element nomini saqlaydi:
aria-labelvaTooltip. Hisoblagich ekran o'quvchi uchun matn sifatida qoladi. - Toggle.
AppShell.SidebarTogglearia-expandedvaaria-controlsoladi (desktop'da — panel, telefonda — Drawer). - Mobil menyu —
Drawer: fokus ichida qoladi,Escyopadi, fokus tugmaga qaytadi.