# Sidebar

Điều hướng app shell cạnh trang: một cột section và item từ cạnh đầu, với trigger thu cột thành rail icon (mỗi icon được Tooltip đặt tên) hoặc giấu cột. Dưới breakpoint md, cùng nội dung đó mở trong một Sheet. Variant panel tùy chọn thêm header brand với nút gạt bên trong và các nhóm điều hướng lồng nhau. Khi đặt inset, cột nằm trên nền page và SidebarInset chứa nội dung trang trong một surface.

**Phụ thuộc:** npm: 3 trực tiếp, 8 tổng; registry: 8 trực tiếp, 9 tổng.

## Cách dùng

### Khi nào dùng

- Điều hướng cấp trên cùng, đứng cạnh trang xuyên suốt một sản phẩm.
- Danh sách đích dài, gom thành các section có tên.

### Khi nào không dùng

- Chuyển giữa các view của cùng một nội dung. Dùng Tabs.
- Danh sách hành động ngắn, neo vào một control. Dùng DropdownMenu.
- Một section tùy chọn trên một trang. Dùng Accordion.

## Biến thể

### collapsible

Đặt trên SidebarProvider. icon giữ rail icon kèm Tooltip khi thu lại; offcanvas giấu hẳn sidebar trên desktop khi không có SidebarBrand, hoặc giữ nút gạt khôi phục bên trong khi có SidebarBrand; none giữ sidebar luôn mở.

Giá trị: `icon`, `offcanvas`, `none`. Mặc định: `icon`.

### side

Đặt trên SidebarProvider. Cạnh mà sidebar ngồi; Sheet mở từ cùng cạnh đó.

Giá trị: `left`, `right`. Mặc định: `left`.

### variant

Đặt trên Sidebar. default giữ ví dụ điều hướng phẳng; panel thêm các nhóm lồng nhau. Khi có SidebarBrand, cả hai variant giữ nút gạt trong header, thay logo trên rail đã thu hoặc rail mobile.

Giá trị: `default`, `panel`. Mặc định: `default`.

## Ví dụ

### Điều hướng app

```tsx
<SidebarProvider defaultValue="overview">
  <div className="flex min-h-[32rem] w-full rounded-container border border-border">
    <Sidebar className="h-auto">
      <SidebarHeader>
        <SidebarBrand name="Không gian làm việc" />
      </SidebarHeader>
      <SidebarContent>
        <SidebarSection label="Không gian làm việc">
          <SidebarItem value="overview" leadingIcon={<IconHome size={20} />} leadingIconOn={<IconHomeFilled size={20} />}>Overview</SidebarItem>
          <SidebarItem value="reports" leadingIcon={<IconChartLine size={20} />}>Reports</SidebarItem>
        </SidebarSection>
        <SidebarSection label="Tài khoản">
          <SidebarItem value="settings" leadingIcon={<IconSettings size={20} />} count={2}>Settings</SidebarItem>
        </SidebarSection>
      </SidebarContent>
      <SidebarFooter>
        <Button variant="secondary" className="w-full">Mời mọi người</Button>
      </SidebarFooter>
    </Sidebar>
    <div className="flex min-w-0 flex-1 items-center justify-center p-md text-center text-sm text-text-secondary">Nội dung của bạn xuất hiện ở đây.</div>
  </div>
</SidebarProvider>
```

### Offcanvas với trigger

collapsible offcanvas giấu sidebar trên desktop; trigger bật tắt sidebar và mở Sheet trên màn hình nhỏ.

```tsx
<SidebarProvider defaultValue="projects" collapsible="offcanvas" defaultCollapsed>
  <SidebarTrigger label="Bật tắt điều hướng" />
  <Sidebar>
    <SidebarContent>
      <SidebarSection label="Trình đơn">
        <SidebarItem value="projects" leadingIcon={<IconLayoutGrid size={20} />} leadingIconOn={<IconLayoutGridFilled size={20} />} active>Projects</SidebarItem>
      </SidebarSection>
    </SidebarContent>
  </Sidebar>
</SidebarProvider>
```

### Panel với header brand

Nút gạt trong header đứng cạnh logo placeholder. Nhóm lồng nhau mở tại chỗ; rail gọn và sheet mobile giữ điều hướng tới được.

```tsx
<SidebarProvider defaultValue="overview">
  <Sidebar variant="panel">
    <SidebarHeader>
      <SidebarBrand name="Không gian làm việc" />
    </SidebarHeader>
    <SidebarContent>
      <SidebarSection label="Không gian làm việc">
        <SidebarItem value="overview" leadingIcon={<IconHome size={20} />} leadingIconOn={<IconHomeFilled size={20} />}>Overview</SidebarItem>
        <SidebarGroup label="Dự án" leadingIcon={<IconLayoutGrid size={20} />} defaultOpen>
          <SidebarItem value="all-projects">Tất cả dự án</SidebarItem>
          <SidebarItem value="recent-projects">Dự án gần đây</SidebarItem>
        </SidebarGroup>
      </SidebarSection>
    </SidebarContent>
    <SidebarFooter>
      <SidebarItem value="settings" leadingIcon={<IconSettings size={20} />}>Settings</SidebarItem>
    </SidebarFooter>
  </Sidebar>
</SidebarProvider>
```

### Bố cục inset

Bố cục app: Sidebar trên nền page và nội dung trang trong một surface inset với đệm 32px.

```tsx
<SidebarProvider defaultValue="overview">
  <div className="flex h-[32rem] w-full overflow-hidden rounded-container border border-border bg-background-page">
    <Sidebar inset className="h-auto">
      <SidebarHeader>
        <SidebarBrand name="Không gian làm việc" />
      </SidebarHeader>
      <SidebarContent>
        <SidebarSection label="Không gian làm việc">
          <SidebarItem value="overview" leadingIcon={<IconHome size={20} />} leadingIconOn={<IconHomeFilled size={20} />}>Overview</SidebarItem>
          <SidebarItem value="reports" leadingIcon={<IconChartLine size={20} />}>Reports</SidebarItem>
          <SidebarItem value="settings" leadingIcon={<IconSettings size={20} />}>Settings</SidebarItem>
        </SidebarSection>
      </SidebarContent>
    </Sidebar>
    <SidebarInset className="gap-2xl">
      <h1 className="text-heading-2">Tổng quan</h1>
      <Card>
        <CardHeader>
          <CardTitle>Doanh thu</CardTitle>
          <CardDescription>Last 30 days</CardDescription>
        </CardHeader>
      </Card>
    </SidebarInset>
  </div>
</SidebarProvider>
```

## Nên và không nên

### Nên

- Cho mỗi section một label; label đặt tên nav và giúp người đọc định hướng.
- Đánh dấu đúng một item active, trang người dùng đang ở.
- Giữ footer cho danh tính và hành động tài khoản, không cho điều hướng.
- Dùng SidebarBrand trong SidebarHeader ở cả hai variant để nút gạt vẫn nằm trong sidebar.
- Cho mỗi SidebarGroup một label và icon dẫn đầu. Dùng các link SidebarItem lồng nhau cho các đích.
- Dùng value của item với defaultValue của SidebarProvider cho đích dạng button, chọn view mà không cần link. Dùng value và onValueChange khi ứng dụng kiểm soát lựa chọn.
- Giữ các đích submenu trong SidebarGroup để rail của panel đưa chúng ra trong flyout bên.
- Trong một app, đặt inset trên Sidebar và để nội dung trang trong SidebarInset. Đệm của nó là đệm trang: đừng đệm nội dung lần nữa, và tách các section bằng gap-2xl.

### Không nên

- Đừng chuyển giữa các view của cùng một nội dung; dùng Tabs.
- Đừng đặt control khác ngoài item bên trong content; đặt chúng ở header hoặc footer.
- Đừng lồng một sidebar vào một sidebar.
- Đừng dùng offcanvas khi không có SidebarBrand, trừ khi một nút mở bên ngoài vẫn tới được.
- Đừng bọc cả trang bằng một Card bên trong SidebarInset; nó đã là surface.

## Props

| Tên | Kiểu | Giá trị | Mặc định | Mô tả |
| --- | --- | --- | --- | --- |
| `collapsed` | `boolean` |  |  | Đặt trên SidebarProvider. Trạng thái đã thu, khi bạn kiểm soát nó. Đi cùng onCollapsedChange. |
| `defaultCollapsed` | `boolean` |  | `false` | Đặt trên SidebarProvider. Thu lại ở lần render đầu. |
| `onCollapsedChange` | `function` |  |  | Đặt trên SidebarProvider. Được gọi với trạng thái thu kế tiếp. |
| `collapsible` | `enum` | `icon`, `offcanvas`, `none` | `icon` | Đặt trên SidebarProvider. icon giữ rail icon kèm Tooltip khi thu lại; offcanvas giấu hẳn sidebar trên desktop khi không có SidebarBrand, hoặc giữ nút gạt khôi phục bên trong khi có SidebarBrand; none giữ sidebar luôn mở. |
| `side` | `enum` | `left`, `right` | `left` | Đặt trên SidebarProvider. Cạnh mà sidebar ngồi; Sheet mở từ cùng cạnh đó. |
| `label` | `string` |  | `Main` | Đặt trên SidebarContent thì là tên landmark điều hướng; đặt trên SidebarSection thì là heading và tên của nhóm (giấu trên rail icon). |
| `active` | `boolean` |  | `false` | Đặt trên SidebarItem. Trang mà item này trỏ tới: aria-current="page", nền accent muted với chữ strong của nó, và icon ở weight fill. Khi bỏ qua trên item có value, lựa chọn lấy từ SidebarProvider. Prop active truyền tường minh thì ghi đè trạng thái selected của item đó. |
| `asChild` | `boolean` |  | `false` | Đặt trên SidebarItem. Render item thành đúng một phần tử con, thường là link của router. |
| `href` | `string` |  |  | Đặt trên SidebarItem. Có href thì item là link; không có thì là button. |
| `count` | `number` |  |  | Đặt trên SidebarItem. Một số sau label, nằm trong Badge; trên rail thì số chuyển vào Tooltip. |
| `leadingIcon` | `node` |  |  | Đặt trên SidebarItem. Icon Tabler đứng trước label, 20px. |
| `leadingIconOn` | `node` |  |  | Đặt trên SidebarItem. Icon biến thể filled (Tabler *Filled, ví dụ IconHomeFilled) hiện trên item active thay cho leadingIcon. |
| `disabled` | `boolean` |  | `false` | Đặt trên SidebarItem. Item không kích hoạt được và bị mờ, kể cả link và link router được slot. Item disabled rời thứ tự Tab; trên rail icon, Tooltip của chúng vẫn mở khi hover. |
| `variant` | `enum` | `default`, `panel` | `default` | Đặt trên Sidebar. default giữ ví dụ điều hướng phẳng; panel thêm các nhóm lồng nhau. Khi có SidebarBrand, cả hai variant giữ nút gạt trong header, thay logo trên rail đã thu hoặc rail mobile. |
| `name` (bắt buộc) | `string` |  |  | Đặt trên SidebarBrand. Tên workspace bắt buộc, giấu khỏi mắt trên rail. |
| `logo` | `node` |  |  | Đặt trên SidebarBrand. Một node logo tùy chỉnh; nếu bỏ qua thì hiện placeholder neutral. |
| `open` | `boolean` |  |  | Đặt trên SidebarGroup. Điều khiển disclosure nội tuyến của sidebar đang mở; đi cùng onOpenChange. Flyout hover tạm của panel đã thu có trạng thái riêng. |
| `defaultOpen` | `boolean` |  | `false` | Đặt trên SidebarGroup. Cho disclosure nội tuyến bắt đầu ở trạng thái mở. Không mở flyout của panel đã thu. |
| `onOpenChange` | `function` |  |  | Đặt trên SidebarGroup. Được gọi với trạng thái disclosure nội tuyến được yêu cầu. Đổi flyout tạm của panel thì không gọi prop này. |
| `value` | `string` |  |  | Đặt trên SidebarProvider cho lựa chọn controlled, đi cùng onValueChange. Đặt trên SidebarItem để nhận diện đích đó. Kích hoạt item enabled thì cập nhật lựa chọn; href vẫn không bắt buộc. |
| `defaultValue` | `string` |  |  | Đặt trên SidebarProvider. Value của item được chọn lúc đầu khi lựa chọn uncontrolled. |
| `onValueChange` | `function` |  |  | Đặt trên SidebarProvider. Được gọi một lần khi item enabled yêu cầu một value selected khác. |
| `inset` | `boolean` |  | `false` | Đặt trên Sidebar. Đặt cột lên nền page, không có cạnh nhìn thấy, cho bố cục app trong đó SidebarInset chứa nội dung trang. Độc lập với variant. |

### Slot

| Tên | Bắt buộc | Mô tả |
| --- | --- | --- |
| `header` | không | SidebarHeader phía trên nội dung cuộn. |
| `content` | có | SidebarContent với một hoặc nhiều SidebarSection gồm các SidebarItem. |
| `footer` | không | SidebarFooter ghim ở cuối. |
| `inset` | không | SidebarInset cạnh Sidebar, chứa nội dung trang. |

## Cấu tạo

| Phần | Mô tả |
| --- | --- |
| `root` | Sidebar: cột trên desktop, ẩn dưới md, rộng 256px, chỉ có viền ở cạnh hướng vào nội dung. Trên rail icon, rộng bằng một item md vuông cộng spacing-sm mỗi bên (49px kể cả viền với chuột, 53px với cảm ứng), và header, content cùng footer đệm bằng spacing-sm. Khi có SidebarBrand, cả hai variant dùng bề rộng của một control md trên rail và giữ một rail gọn vẫn hiện dưới md để nút gạt bên trong vẫn tới được. |
| `header` | SidebarHeader: khối phía trên nội dung cuộn, như tên workspace. |
| `content` | SidebarContent: nhóm các section cuộn được, một nav được label đặt tên (mặc định "Main"). |
| `footer` | SidebarFooter: khối ghim ở cuối, có một Separator phía trên. |
| `section` | SidebarSection: một nhóm item liên quan, được label đặt tên, các item nằm trong một list. |
| `label` | Heading của section: chữ secondary medium 12px, giấu trên rail icon. |
| `item` | SidebarItem: một đích, là link khi có href, là button khi không có, hoặc phần tử bất kỳ với asChild. Cao ít nhất bằng control md, chữ medium 14px, một Badge sau label khi có count. Trên rail icon, item có icon là ô vuông bằng chiều cao control md. |
| `current` | Item active, trang người dùng đang ở: nền accent muted với chữ strong của nó. |
| `currentIcon` | Icon của item active, màu icon accent, ở weight fill. |
| `icon` | Icon Tabler dẫn đầu của item, 20px; leadingIconOn (biến thể *Filled) thay nó trên item active. |
| `trigger` | SidebarTrigger: một icon button ghost, thu sidebar từ md trở lên và mở Sheet dưới md, được label đặt tên. Với collapsible none thì chỉ hiện dưới md. |
| `brand` | SidebarBrand: placeholder hoặc logo được đưa vào, và tên workspace cạnh SidebarTrigger khi đang mở. Trên rail, logo bị gỡ và nút gạt chiếm chỗ đó, thẳng hàng với các icon điều hướng. Logo trở lại khi mở rộng, kể cả trong Sheet mobile. |
| `group` | SidebarGroup: button disclosure có tên, với icon dẫn đầu và caret, rồi đến list các đích lồng nhau. Phần con bị giấu trên rail; kích hoạt thì mở rộng sidebar, hoặc mở sheet mobile. Trên panel desktop đã thu, hover hoặc kích hoạt icon để mở các đích trong flyout bên hướng vào trang, mà không mở rộng sidebar. Flyout dùng lại cách định vị, xử lý va chạm và chuyển động của Popover. |
| `inset` | SidebarInset: nội dung trang cạnh một Sidebar inset, một landmark main tự cuộn. Từ md trở lên là một surface có bo góc page và viền, cách mép canvas spacing-sm, đệm spacing-2xl (32px). Dưới md Sidebar là một Sheet, nên nó tràn viền với đệm spacing-lg cộng safe area dưới. Sidebar inset dùng nền page và cạnh trong suốt. |

## Khả năng truy cập

Role: `complementary`

| Phím | Hành động |
| --- | --- |
| `Tab` | Đi từ item này sang item khác; item là link hoặc button theo thứ tự trên trang. |
| `Enter, Space` | Item button thì kích hoạt; item link thì đi theo href. |
| `Escape` | Đóng Sheet mà sidebar biến thành trên màn hình nhỏ. |

- Root là aside (complementary). SidebarContent bên trong là landmark điều hướng duy nhất, được label đặt tên (mặc định "Main").
- Mỗi SidebarSection là một group được label nhìn thấy đặt tên (aria-labelledby), hoặc bằng aria-label trên rail icon nơi label bị giấu. Các item của nó là một list.
- Item của trang hiện tại đặt aria-current="page" và icon chuyển sang weight fill; trạng thái không bao giờ chỉ dựa vào màu.
- Trên rail, item có icon chuyển label vào một Tooltip và một span sr-only. Item không có icon giữ chữ nhìn thấy. Item disabled đứng ngoài thứ tự Tab, và Tooltip của nó vẫn mở khi hover. Sheet mobile luôn hiện đủ label.
- Sheet tự đặt tên bằng title sr-only; button trigger mang cùng tên đó trong aria-label.
- Các phần của Sidebar phải nằm trong một SidebarProvider; không có thì chúng throw.
- SidebarBrand đặt tên trigger bên trong theo hành động: Thu gọn sidebar, Mở rộng sidebar hoặc Đóng sidebar. Rail mobile của panel mở Sheet; header của Sheet đóng Sheet và trả focus về nút mở ban đầu.
- SidebarGroup đưa ra aria-expanded và aria-controls. Nhóm đang đóng và nhóm trên rail gỡ các đích lồng nhau khỏi điều hướng bàn phím. Trạng thái mở của nhóm vẫn còn khi sidebar thu lại.
- Khi có header brand, offcanvas giấu điều hướng nhưng giữ button khôi phục bên trong trên rail, logo thì bị giấu. Khi không có header brand, hành vi offcanvas hiện có giấu cả cột và cần một nút mở bên ngoài.
- Trên rail đã thu, SidebarBrand gỡ logo trang trí và giữ nút gạt ở chỗ đó. Nút gạt vẫn mounted để focus bàn phím còn sau khi thu và khi mở.
- Đích dạng button có value dùng chung một lựa chọn qua SidebarProvider; lựa chọn controlled chỉ đổi khi value được truyền vào đổi. Item disabled không yêu cầu lựa chọn được. Lựa chọn còn giữa rail desktop và Sheet mobile.
- Flyout nhóm của panel đã thu mở khi hover mà không chuyển focus. Click hoặc kích hoạt bằng bàn phím thì đưa focus vào flyout; Escape đóng flyout. Chọn một đích thì đóng flyout và giữ rail đã thu. Nhóm rail mặc định và nhóm mobile giữ hành vi mở rộng hiện có.

## Cài đặt

```bash
npx werewolf-ui add sidebar
```

Chạy `npx werewolf-ui init` một lần trong app trước khi thêm component. Init cài token, thêm CSS import và luật cho agent. Add cài component này cùng các dependency và giữ nguyên theme của bạn. Xem [Cài đặt](https://www.werewolf-ui.dev/vi/docs/installation.md) để biết yêu cầu và cách cài qua registry.
