跳至主要内容
💬 回報這一頁

AppShell/Sidebar 應用外殼

後台的殼:側邊欄+頂列+主內容。規範的正本在 後台系統的資訊架構——分區怎麼切、頂列放什麼、 單一出口與深連結,全以那一頁為準;這裡是那份規範的元件載體。

同一副殼有兩種版型:多應用(側欄是應用清單、應用內的功能在頂部 功能選單列)與單應用(側欄直接是分區導覽)。 怎麼選見〈後台系統的資訊架構〉的「多應用系統」一節。

收錄狀態

外殼元件 v0.13.0 起在正式版(頁面章原本刻意不收外殼,後來以純呈現元件收進元件章)。 重新檢視條件=第一個真實宿主依文件自行導入的回報;屆時若決定退場,走〈版本策略〉的棄用流程(標記棄用、保留至少一個 minor、下個 major 移除),不會無預警消失。

在 Storybook 開啟

用途​

三層 API,由小到大:

元件定位
Sidebar 家族側欄容器與結構件:Provider、Trigger、Header/Content/Footer、Group、Menu 系
SidebarNav吃 NavGroup[] 的三態渲染:連結/展開群組/收合態右彈選單,含〔例行〕〔試算〕標籤
AppShell佈局容器:側欄+頂列+主內容,轉發 Provider props;頂列與側欄貼著視窗頂端(sticky)
AppMenubar多應用版型的頂部功能選單——同一份 NavGroup[],一組一個頂層選單

純呈現,三不綁:不綁路由(連結用 renderLink 注入,預設真 <a>)、 不綁資料抓取、不碰儲存(收合狀態的持久化用受控 open/onOpenChange 交宿主)。 這是 ADR-0011 的退場前提——砍掉時宿主一小時內能用同一份 token 重寫自己的殼。

單應用版型:分區導覽+收合在 Storybook 開啟

多應用外殼的組法​

側欄放應用清單(一個應用=一個 NavLeaf),頂列分三段: 左=系統(SidebarTrigger+目前應用)、中=目前應用的 AppMenubar、 右=系統(搜尋、通知、使用者選單)。換應用時中段整組換掉, 側欄、功能選單、⌘K 三個出口吃同一份資料。

多應用外殼:側欄切應用、頂部切功能在 Storybook 開啟

「目前在哪個應用」用 SidebarNav 的 isActive 接:路徑落在應用自己的 url 底下, 或落在它任一個功能項上(findActiveNavLeaf 找得到)。應用各自部署在不同網域時, 同一個接縫改成比對網域。

const isAppActive = (app: NavLeaf, path: string) =>
isNavActive(app.url, path) || Boolean(findActiveNavLeaf(menusOf[app.url] ?? [], path));
const app = findActiveNavLeaf(apps, pathname, isAppActive)?.item;

<AppShell
sidebar={<Sidebar label="應用程式"><SidebarContent>
<SidebarNav groups={apps} currentPath={pathname} isActive={isAppActive} renderLink={renderLink} />
</SidebarContent></Sidebar>}
header={<>
<SidebarTrigger />{/* 左:系統 */}
<AppMenubar groups={menusOf[app.url]} currentPath={pathname} renderLink={renderLink} onAction={run} />
<div className="flex shrink-0 items-center gap-1">{/* 右:搜尋、通知、使用者 */}</div>
</>}
>…</AppShell>

完整組法(含通知 Popover、使用者 DropdownMenu、動作項開對話框)是 story 「頁面/多應用外殼」,內部試裝宿主 apps/host-v4 照同一個組法接 react-router。

何時不要用​

✅ 這樣做

後台系統的站台外殼——與〈後台系統的資訊架構〉的分區規範成套使用。

🚫 不要這樣

對外的行銷站、文件站、登入頁不要套這個殼——它們的導覽是內容結構, 不是工作節奏。登入頁在外殼之外(頁面章明文不收)。

✅ 這樣做

側欄項目用 NavGroup[] 描述,同一份資料餵 CommandPalette。

🚫 不要這樣

不要在側欄與指令面板各維護一份清單——改名只改一邊, 使用者會以為是兩個功能。

狀態​

  • 桌面收合(圖示欄):collapsible="icon"(預設)收成圖示欄;文字轉 sr-only (可及名稱不消失)、群組標題隱藏、兩層群組改成往右彈出的 DropdownMenu、hover 出提示泡泡(在圖示右側—— 在上方會蓋住上一項)。
  • 桌面收合(收到 0):collapsible="offcanvas" 收合時側欄整塊移出畫面, 工作區拿回整個寬度。滑鼠碰左緣會窺看:側欄浮在內容上(不推版面), 滑鼠離開、按 Esc 或選了目的地就收。鍵盤與觸控走 SidebarTrigger 釘選展開—— 只靠 hover 揭露的功能對觸控等於不存在。
  • 行動版:自動轉左滑抽屜(既有 Radix Dialog 組成——焦點鎖定、Esc、焦點歸還 全部繼承),分區與順序完全不變;點選目的地即關閉。
  • 選中態:bg-sidebar-accent(=該主題的 brand-subtle,識別層)+ aria-current="page";hover 走狀態層。active 判定用 isNavActive() 的多層 fallback(完整相符 → 去 query → 路徑邊界的父路徑),可用 isActive 覆寫。
  • 開闔狀態:useSidebar() 拿 toggle()/state——宿主要掛快捷鍵、 要存 localStorage 都從這裡接。

SPA 宿主的接法​

<SidebarNav
groups={navGroups}
currentPath={location.pathname}
renderLink={(props, item) => <Link to={item.url} {...props} />}
/>

renderLink 收到的 props(含 aria-current 與行動版關抽屜的 onClick)要原樣轉發。

無障礙​

  • 側欄是 nav 地標,label 預設「主導覽」;行動版抽屜以同名可及名稱呈現。
  • 收合態的連結名稱走 sr-only 保留——display:none 會把可及名稱一起藏掉, 這是 stories 的 play 直接驗的契約。
  • offcanvas 收到 0 時側欄是 inert:移出畫面不等於鍵盤走不進去, 不加 inert 的話 Tab 會停在看不見的連結上。
  • SidebarTrigger 自帶 aria-label 與 aria-expanded。
  • 聚焦環走中性 --ring(--sidebar-ring ≡ ring,ADR-0007 的延伸)。

Token​

側欄有自己的表面語意(主題頁有完整清單): --sidebar 是「比頁面底沉一階的安靜區」(唯一新顏色,反解生成); --sidebar-accent ≡ brand-subtle、--sidebar-primary ≡ brand、 --sidebar-ring ≡ ring——七個別名由 verify:color 的恆等守衛盯住。

取用​

npx shadcn@latest add https://kielchang.github.io/dooping-design-book/r/app-shell.json
npx shadcn@latest add https://kielchang.github.io/dooping-design-book/r/sidebar-nav.json

多應用版型再加:

npx shadcn@latest add https://kielchang.github.io/dooping-design-book/r/app-menubar.json

sidebar-nav 會自動帶入 sidebar、collapsible、dropdown-menu、badge、 tooltip 與 nav(NavGroup 型別)。token 需要 @dooping/tokens@^0.7.0 (--sidebar-* 自 0.7.0 起提供)。