線稿索引所有 spec最後更新 2026-06-23
specs/admin-design-system.md

後台 Design System

一句話:後台 primitive 一律 import workspace 套件 @toko-cloud/ui/*packages/ui),domain 元件 import @/components/admin/*;page/route code 不手刻裸 HTML 元素、不在頁面內自刻/客製 primitive、不寫 arbitrary [hsl(...)]、不新增 ad-hoc CSS 變數、不寫靜態 inline style。本檔是後台唯一的元件目錄 +「該用 vs 禁止」對照 + 規則適用範圍 + 允許例外清單。

橫切規約文件。 描述後台 design system 的目標形狀與使用紀律;元件實作以 packages/ui/src/* 的 code 為準(code is truth),落地/遷移狀態見 roadmap。與公開站 design system 的分界以 ../CLAUDE.md「兩套 design system 不要混用」為權威,本檔只管後台那套。

1. 目標 / 問題

後台 primitive 已抽成 workspace 套件 packages/ui@toko-cloud/ui,31 個元件),但治理問題不變:純文件規約會漂移——該用的沒人用、缺中階組合件逼大家手刻、每頁各自重造 page header / 空狀態 / 提示盒。本檔把規則寫到無歧義,review/codex 以此為準。

治理決策(已拍板)

  1. 治理 = 純文件:本 spec(長期真相)+ ../CLAUDE.md / AGENTS.md 的硬規則段。不加 ESLint gate(程式碼由 agent 產出,文件即主要載體)。
  2. 缺元件/更新元件 = shadcn-first,基準是官方 Base UI 變體:來源是 shadcn-ui/ui repo 的 apps/v4/registry/bases/base/ui/<name>.tsx(raw GitHub,main 分支;CLI / registry JSON 沒有 base-ui 端點)。primitive 套件是 @base-ui/react(1.5 stable)——改名前的 @base-ui-components/react rc 已斷頭棄用。移植時官方的結構(function 元件、data-slot*.Props 型別、行為 props),不取 cn-* className 系統(綁官方主題 CSS)與 IconPlaceholder;樣式用 admin-tokens 的 Tailwind utility。shadcn 真的沒有才手刻 house 元件。
  3. 下拉政策:NativeSelect 已廢除(2026-06-12)。表單內靜態 enum → Select;選項來自 DB/API 或筆數會長 → Combobox(可搜尋);GET filter bar → FilterSelect(domain 層,選擇即提交)。不再有「簡單下拉用原生 <select>」的路徑。

2. 原則

  • import 優先於手刻:任何按鈕/輸入/表格/卡片/提示/空狀態,先找 @toko-cloud/ui/*(primitive)與 @/components/admin/*(domain)有沒有;有就 import,沒有就先查 shadcn 官方 base 變體、再考慮新增進 packages/ui不要在頁面 inline 重造、也不要把 domain 邏輯複製成新元件
  • @toko-cloud/ui 零 app 依賴:套件內禁止 @/ importcn 用套件內 ./lib/utils、跨元件相對路徑、hook 放 packages/ui/src/hooks/。app(@/components/admin/*、頁面)可以 import @toko-cloud/ui,反向不行。
  • admin-scope 限定:後台 layout(src/app/(admin)/layout.tsx)外層 <div className="admin-scope">;token 經 :where(.admin-scope) 零特異度 scope,不污染公開站。新元件樣式只依賴 admin-tokens 的語意 token。
  • portal 浮層要帶 admin-scope class:Base UI Portal 把浮層掛到 document.body.admin-scope 之外),token 變數會解析不到 → 透明背景。所以 dialog/alert-dialog/sheet 的 Backdrop+Popup、select/popover/dropdown-menu/tooltip/combobox 的 Positioner,className 第一個 class 一律 admin-scope新增任何會 portal 的元件都要比照,否則必出「浮層透明/overlay 蓋過本體」回歸。
  • Base UI render-prop 慣例:換底層 tag 用 render={<Link/>}不要寫 asChild。Base UI 1.5 的 className 型別是 string | (state)=>string,wrapper 一律 Omit<…,'className'> & { className?: string } 收窄才能進 cn()/cva。
  • 語意 token,不寫死顏色:顏色一律走 admin-tokens 的語意 utility(見 §6)。禁止 arbitrary [hsl(...)]、寫死 hex、為單一用途新增 CSS 變數。
  • inline style 僅限真動態值:靜態樣式一律 Tailwind utility / 元件 props;inline style 只准用在值要在 runtime 算的場景(見 §7)。

3. 規則適用範圍

這套「禁止裸元素 / 禁止 inline style」規則只治理 page/route code,不是全 repo 一刀切——否則會誤殺 DS 元件自己(packages/ui/src/input.tsx 內部就是裸 <input>)。範圍如下:

類別路徑規則
受治理(嚴格)src/app/(admin)/**(頁面、*-form.tsx*-dialog.tsx、view 等 route code)§5 全套硬規則生效
DS wrapper 內部(豁免)packages/ui/src/**這些就是包裝 primitive 的地方,內部本來就要用裸 <input>/<button>/<table>、CSS-var inline style(如 sidebar.tsx 的 rail 按鈕、寬度變數)。不受「禁止裸元素」約束
domain 元件(豁免內部、鼓勵複用)src/components/admin/**內部可組合 primitive;對 page code 而言它們是該優先複用的現成抽象(見 §4 domain 層
hidden / metadata input任意<input type="hidden" name=…>(帶 server-action 資料)、Select/Combobox 內部 hidden 提交 input 等不是 UI 控制項,不受約束
a11y-only markup任意純無障礙用途的原生元素(screen-reader 專用、role 載體)允許
列印頁(豁免)src/app/(admin)/admin/payment-requests/[id]/print/**@media print + window.print()、刻意在 .admin-scope 之外,保留裸 HTML + inline style

review / codex 以此範圍判定違規:只有「受治理」列的 raw 元素 / 靜態 inline style 算違規

4. 元件目錄

import 路徑:primitive 一律 @toko-cloud/ui/<檔名>;domain 元件在 @/components/admin/<檔名>。全部 31 個 primitive(一檔一 primitive,含 Select/Field/Card 等家族檔;packages/ui/src/*.tsx 的實際檔數,不計 hooks/lib/ 子目錄)已對齊 shadcn 官方 Base UI 變體(function 元件 + data-slot + @base-ui/react 1.5)。

基礎 / 表單

元件用途 / 何時用
Buttonbutton.tsx所有可點動作;換 tag 用 render。本地 variant:default(bg-foreground)/primary/destructive/outline/secondary/ghost/linktype 預設 'button'禁止裸 <button>/當按鈕用的 <a>
Inputinput.tsx單行輸入。禁止裸 <input type=text/email/number/date…>
Textareatextarea.tsx多行輸入。禁止裸 <textarea>
Labellabel.tsx表單標籤;多數情況改用 FieldLabel(見下)
Select 家族select.tsx表單內所有靜態 enum 下拉<Select name defaultValue items><form> 內即原生提交(Root 內 hidden input)。⚠️ 兩個必踩坑:(1) trigger 要顯示 label 必須給 items(popup 未開過時 Base UI 拿不到 SelectItem 的文字,會顯示原始 value)——items 與 .map() render 餵同一個常數避免兩份漂移;(2) '' 是合法值但不顯示 placeholder(1.5 只在 null 時顯示)——「全部/不限/繼承」選項要有自己的 label。受控用 value+onValueChange(參數是 value 不是 event,且型別 nullable 要 guard)
Comboboxcombobox.tsx選項來自 DB/API 或筆數會長(會員、行程、梯次、員工…)的可搜尋下拉。高階 wrapper:{ name, options: {value,label}[], defaultValue?, placeholder? }(hidden input 提交)。受控/多選/chips 用 ComboboxRoot + family 組合(multiple = 同名多 hidden input,action 端 formData.getAll
Checkboxcheckbox.tsx勾選(支援 name)。⚠️ server-action 布林語意:勾選送 value、未勾選預設送不出欄位;要表達 true/false 必須給 uncheckedValue(或 action 端以 key 存在與否判斷)。禁止裸 <input type=checkbox>
RadioGroup / RadioGroupItemradio-group.tsx單選群組。禁止裸 <input type=radio>
Switchswitch.tsx開關切換(新增官方 size prop)
Field 家族field.tsx原生表單欄位的標準包裝(純組合、不綁 RHF):Field/FieldLabel/FieldDescription/FieldError/FieldGroup/FieldSet/FieldLegend/FieldContent/FieldTitle/FieldSeparatorlabel 與控制項的間距由 Field 的 gap 管——禁止手刻 space-y-*+<Label>(就是 label/input 無間隔 bug 的來源)
InputGroup 家族input-group.tsx帶 icon / addon 的輸入框,取代 filter bar 手刻 relative+absolute 排 icon

RHF 版 form.tsx 已淘汰(零使用者;react-hook-form 仍用於公開站 checkout)。後台表單一律 <form action={serverAction}> + Field 家族。

排版 / 容器

元件用途
Card 家族card.tsxCard/CardHeader/CardTitle/CardDescription/CardContent/CardFooter/CardAction
PageHeaderpage-header.tsx頁首:標題 + 描述 + 右側 actions slot(house 元件)
StatCardstat-card.tsxKPI 數字卡:label / value / hint / tone / unit(house 元件)
Separatorseparator.tsx分隔線
Tabs 家族tabs.tsx分頁(新增官方 variant="line"
Table 家族table.tsx禁止裸 <table>/<thead>/<tbody>/<tr>/<th>/<td>

回饋 / 狀態

元件用途
Badgebadge.tsx狀態標籤。variant:default/secondary/destructive/outline/success/warning/info(tone 全走語意 token,見 §6)。禁止手刻 bg-[hsl(...)] 徽章
Alert 家族alert.tsx提示盒:Alert/AlertTitle/AlertDescription/AlertActionwarning/success/info 是本地擴充 variant
Empty 家族empty.tsx空狀態:Empty/EmptyHeader/EmptyMedia/EmptyTitle/EmptyDescription/EmptyContent
Skeletonskeleton.tsx載入骨架
Toaster / toast(sonner)sonner.tsx全域 toast(掛在 admin layout 的 .admin-scope 內)
Tooltiptooltip.tsx提示氣泡

覆蓋層 / 導航

元件用途
Dialog 家族dialog.tsx對話框(showCloseButton prop)
AlertDialog 家族alert-dialog.tsx確認/破壞性對話框
Sheet 家族sheet.tsx側邊抽屜(house wrapper on Base UI Dialog;新增上/下變體要加 cva 變體)
Popoverpopover.tsx浮層
DropdownMenu 家族dropdown-menu.tsx下拉選單(Content 預設 align='start',貼齊 trigger)。DropdownMenuLabel 是 plain div(Base UI GroupLabel 要求在 Group 內,radix 慣用法直接放 Content 下會噴錯)
Commandcommand.tsx命令面板(cmdk;官方 base 變體也仍用 cmdk)
Breadcrumb 家族breadcrumb.tsx麵包屑
Sidebar 家族sidebar.tsx後台側欄(CSS-var inline style 屬 DS 內部豁免;useIsMobile@toko-cloud/ui/hooks/use-mobile

domain 層(src/components/admin/*

primitive 之上、與業務語意綁定的元件——page code 該優先複用,不要重造或硬塞進 packages/ui

  • filter-select.tsxFilterSelect):GET filter bar 的標準下拉——name+options+defaultValue,選擇即 requestSubmit 所屬 form。
  • order-status-badge.tsxOrderStatusBadgePaymentStatusBadge 同檔)、assignment-badge.tsxAssignmentBadge):包裝 Badge 的業務狀態徽章。
  • status-filter-tabs.tsxStatusFilterTabs):列表頁的狀態篩選列。
  • permission-matrix.tsxPermissionMatrix):RBAC 權限矩陣。
  • mdx/mdx-field.tsx(+ mdx/* editor 群)、lodging/amenity-picker.tsx / lodging/gallery-manager.tsx 等模組級組合元件。

新 domain 元件放這層(不是 packages/ui);packages/ui 只放與業務無關的 primitive。

5. 該用 vs 禁止(治理核心)

這張表是無歧義的硬規則,僅適用 §3 的「受治理」範圍。寫後台頁面前先掃一遍;review/codex 以此為準。

場景❌ 禁止✅ 該用
按鈕<button>、當按鈕的 <a><Button> / <Button render={<Link/>}>
文字/數字/日期輸入<input><Input>
多行輸入<textarea><Textarea>
表單內靜態 enum 下拉<select><NativeSelect>(已廢除)<Select name defaultValue items> 家族
選項來自 DB/API 的下拉<select>、把幾百筆塞 <Select><Combobox>(可搜尋)
GET filter bar 下拉<select onChange=submit><FilterSelect>(domain 層)
勾選<input type=checkbox><Checkbox name=… uncheckedValue=…>
單選<input type=radio> + 手刻卡片<RadioGroup> / <RadioGroupItem>
表單欄位排版手刻 <div class="space-y-1.5"><Label>…、local Field<Field> / <FieldLabel> / <FieldDescription> / <FieldError>
表格<table>/<thead>/<tbody>/<tr>/<th>/<td><Table> 家族
提示 / 說明 / 危險盒手刻 <div class="rounded-md bg-muted/40 p-3">bg-destructive/5<Alert variant=…>
空狀態手刻 <p class="py-12 text-center text-muted-foreground"><Empty> 家族
KPI 數字卡手刻 Card + text-3xl tabular-nums<StatCard>
頁首手刻 <div class="flex justify-between"><h1><PageHeader>
業務狀態徽章手刻 Badge + 業務文案散落各頁複用 OrderStatusBadge / PaymentStatusBadge / AssignmentBadge(domain 層)
語意色(成功/警告/資訊/危險)text-[hsl(var(--success))]、任意 [hsl(...)]text-success / bg-warning/10 / <Badge variant="success"> / <Alert variant="warning">
顏色 / 間距 token寫死 hex/hsl、為單一用途新增 CSS 變數admin-tokens 既有語意 token 的 Tailwind utility
靜態 inline stylestyle={{ padding: 12 }} 等靜態值Tailwind utility / 元件 props
頁面內自刻 primitive在頁面/*-form.tsx 內手組 Base UI primitive、複製 SELECT_CLS 類字串先查 @toko-cloud/ui → 查 shadcn base 變體 → 新增進 packages/ui

6. token/tone 規則

不新增任何 token。 admin-tokens.css 已把 --success / --warning / --primary(可當 info 的藍)/ --destructive 等經 @theme inline 註冊成 --color-*,Tailwind named utility + opacity modifier 直接可用——bg-success/10text-successborder-warning/30bg-primary/10

Badge / Alert 的 tone variant 一律:

success: bg-success/10 text-success border-success/30
warning: bg-warning/10 text-warning border-warning/30
info:    bg-primary/10 text-primary border-primary/30

7. 允許的例外

§3 已列「檔案層級」豁免(DS wrapper 內部、domain 內部、hidden/a11y、列印頁)。本節補「受治理 page code 內」仍允許的 inline style 樣態,且只有這些:

  1. 真動態值的 inline style:值必須在 runtime 計算、無法用固定 utility 表達者,例如
    • 進度條 / 長條寬度:style={{ width: pct + '%' }}
    • 雜湊產生的色:style={{ background: avatarColor(seed) }}guides 頭像)
    • leaderboard 依資料算出的尺寸
  2. 第三方元件要求的 inline style:如圖表庫、@mdxeditor/editor 等自帶 style 的整合點。

例外以外的 inline style(靜態值)與裸元件一律視為違規。新增例外要在本節(或 §3)補一條並說明為何無法用 utility/元件表達。

8. 維護指引

  • 新增/更新元件:抓官方 base 變體(https://raw.githubusercontent.com/shadcn-ui/ui/main/apps/v4/registry/bases/base/ui/<name>.tsx)→ 對齊已安裝 @base-ui/react 的實際 .d.ts → 樣式換成 admin-tokens utility(不取 cn-*)→ 會 portal 的浮層加 admin-scope class(§2)→ export 名維持 superset。
  • e2e 與 Select/Combobox 互動:用 tests/e2e/_helpers/select.tspickOption(page, trigger, '選項文字')locator.selectOption() 只對原生 <select> 有效。既有 data-testid 不可變動(掛在 SelectTrigger / ComboboxInput 上)。
  • 驗證bun run typecheck + bun run test + bun run test:e2e 全綠;浮層類改動要實際開 dialog/dropdown/select 截圖確認(portal token 問題 typecheck 抓不到)。
  • 文件同步../CLAUDE.md「兩套 design system」段與 AGENTS.md 同步硬規則摘要。
  • 逐項「做了/沒做」記 roadmap,本 spec 不記完成度。

9. 開放議題

  • Tabsvariant="line"Switch sizeAlertDialog size 等官方新 variant 已移植但尚無使用面;要不要在現有頁面採用待設計決定。
  • settings 頁 ECPay 欄位有 Base UI dev 警告「changing the default value state of an uncontrolled FieldControl」(server action 後 defaultValue 變動);無功能影響,待改 controlled 或加 key 重掛。
  • 是否需要 DataTable(排序/分頁/篩選的高階表格)尚未有足夠重複需求支撐,暫不建;先用 Table + 既有 server-side 篩選。

10. 深淺色模式(Dark/Light)

0617 會議定調:人員長時間盯螢幕,後台要加深色 / 淺色切換。只限後台(公開站是另一套 Apple-style design system,自有主題,不在本檔範圍)。

10.1 鐵則:不新增 token,只翻值

深色模式沿用同一組語意 token--success / --warning / --primary / --destructive / 背景 / 前景…),只在 dark selector 下重新宣告它們的值。元件、utility(bg-success/10text-fg…)一律零改動——換的是 token 值,不是 token 名,也不是元件。這直接延伸 §6 的「不新增任何 token」。

10.2 selector 與 scope

  • 沿用零特異度慣例:淺色值留在 :where(.admin-scope);深色值放兩條並列的零特異度 selector(同塊翻值):
    • :where([data-theme='dark'] .admin-scope)主路徑。next-themes 把 data-theme 寫在 <html>,主樹 wrapper 與 portal 浮層(portal 到 document.body)都是 <html> 後代 → 一條全包。
    • :where(.admin-scope[data-theme='dark']):保留作防護(wrapper 自身若帶 data-theme 也命中);採 next-themes 後 data-theme 只在 <html>、wrapper 不帶,故此條目前不觸發,留著無害。
  • data-theme 由 next-themes 寫在 <html>attribute="data-theme")。永遠是 resolved 的 light|darksystemprefers-color-scheme 解析後寫入),不會是字面 system
  • portal 浮層自動跟著深色:dialog/select/dropdown 等浮層的 Positioner/Popup 第一個 class 已是 admin-scope§2 portal 規則),token 從最近的 .admin-scope 解析;因為 <html>data-theme,第一條 selector 讓浮層在深色下吃深色 token。移除該 selector → dialog/select/dropdown 等浮層在深色下仍是淺色。
  • 實作:apps/web/src/app/admin-tokens.css 的 dark 區塊、apps/web/src/components/theme-provider.tsx(next-themes Provider)、apps/web/src/components/admin/theme-toggle.tsx(topbar 切換鈕,useTheme())。

10.3 切換與持久化(無閃爍)

  • 切換鈕在 admin shell(topbar),三態 light / dark / system(system 跟作業系統 prefers-color-scheme)。
  • 機制:next-themes(對齊 shadcn dark-mode/next 與 apps/wireframescomponents/shell/theme.tsx)。 root layout 包 <ThemeProvider attribute="data-theme" defaultTheme="system" enableSystem>apps/web/src/components/theme-provider.tsx)。切換鈕用 useTheme();system 模式 OS 切換即時跟隨(next-themes 內建 matchMedia 監聽 + 跨分頁同步)。
  • 持久化 + 無 FOUC:next-themes 以 localStorage(storageKey="admin-theme")持久化,並在 <head> 注入 paint-前 inline script,在 hydrate 前把 resolved data-theme 補到 <html>,首屏即正確色(白底不閃)。
  • <html suppressHydrationWarning>(root layout):server HTML 無 data-theme(system 模式 server 無從得知 OS 偏好),next-themes 於 hydrate 前補上會觸發屬性 mismatch;shadcn dark-mode/next 明文要求加此屬性(只抑制 <html> 一層屬性比對,不 cascade 子樹)。

10.4 約束

  • opacity-modifier utility(bg-warning/10)在兩主題都成立:它是 token 顏色疊 alpha,深色翻值後自然跟著變,不需為深色另寫 tone。
  • 既有「禁止寫死 hex / arbitrary [hsl(...)]」(§6)在深色下更關鍵——寫死色不會跟著翻、會爆掉。深色模式是這條禁令的回歸測試。
  • 截圖驗證要雙主題各一輪(深色浮層 token、對比度問題 typecheck 抓不到,比照 §8)。

10.5 開放議題

selector 抉擇已決議(見 §10.2):採 [data-theme='dark'] .admin-scope(html,涵蓋主樹 + portal)為主路徑、.admin-scope[data-theme='dark'] 並列保留;主題引擎採 next-themesattribute="data-theme",對齊 shadcn dark-mode/next 與 wireframe theme.tsx)。

  • 深色色票需過 WCAG AA 對比;先定機制,色票另調。
  • 是否同步做公開站深色(範圍外,要動另一套 design system 的 @theme)。

Changelog

版本日期變更Commit
82026-06-23主題引擎由「cookie + data-theme 自管」改為 next-themes(對齊 shadcn dark-mode/next 與 wireframe theme.tsx):root layout <ThemeProvider attribute="data-theme"> + <html suppressHydrationWarning>;刪 admin-theme.ts 自管模組與 3 個自管測試;theme-toggle 改用 useTheme();持久化由 cookie 改 localStorage(storageKey="admin-theme");data-theme 只寫 <html>、主路徑 selector 改 [data-theme='dark'] .admin-scope。同步把 sidebar IA 對齊 wireframe §5(8 組功能軸取代舊 5 組 phase 制)。
72026-06-22N7/B5f 後台深淺色落地:cookie+data-theme 自管、雙 selector(wrapper .admin-scope[data-theme='dark'] 並列 html [data-theme='dark'] .admin-scope)portal 跟隨、sonner 去 next-themes、§10.4 hsl 清理(dashboard SVG / avatar / 通知點 / 斜紋)。§10.2/§10.3 內文吸收 selector 決議,§10.5 移除該開放議題。
62026-06-20對齊 code:primitive 數由 33 改 31(packages/ui/src/*.tsx 實際檔數,§1 / §4);§4 點明計法(一檔一 primitive、含 Select/Field/Card 等家族檔、不計 hooks//lib/ 子目錄)。
52026-06-20N7 深淺色模式(§10):後台深色沿用同一組語意 token、只在 :where(.admin-scope[data-theme='dark']) 翻值(不新增 token、元件零改動);cookie 持久化免 FOUC;portal 浮層跟隨。未實作。
42026-06-12primitive 抽成 workspace 套件 packages/ui@toko-cloud/ui),全面對齊 shadcn 官方 Base UI 變體(@base-ui/react 1.5 stable、function 元件、data-slot);NativeSelect 廢除,下拉三分法(Select / Combobox / FilterSelect);Field 家族取代手刻 label 排版;portal 浮層 admin-scope class 規則(修 overlay 蓋本體/浮層透明);dropdown 對齊預設 start;Select items'' placeholder 兩坑寫入目錄;badge/alert token 化落地;RHF form.tsx 淘汰
32026-06-03修正 domain 層檔名(PaymentStatusBadgeOrderStatusBadge 同檔);MDX/lodging domain 元件補實際檔路徑
22026-06-03納入 codex review:§3 規則適用範圍;NativeSelect 首選 + Select caveat;Checkbox uncheckedValue;shadcn 移植對齊已安裝 API;Alert 本地擴充;Badge tone 遷移目標
12026-06-03初版:元件目錄 +「該用 vs 禁止」+ token/tone 規則 + 允許例外;三決策