Linyao Design System
元件
本文件列出 Linyao Design System 的元件分類、共用 API 與組合邊界。實際可匯入項目以 packages/ui/src/index.ts 與建置產生的 dist/index.d.ts 為準。未列入公開匯出入口的原始檔不屬於公開 API。
共用 API
| 概念 | API | 規則 |
|---|---|---|
| 視覺/語意 | variant | 不以 intent/appearance 表達相同概念 |
| 尺寸 | size="sm"/size="md"/size="lg" | 元件只支援適用的範圍 |
| 方向 | orientation | 只用於有水平/垂直語意的元件 |
| 狀態 | disabled, readOnly, required, invalid, loading | 映射至原生、Base UI 或 RAC 語意 |
| 值 | value, defaultValue, onValueChange | 受控/非受控,不混用兩種狀態所有權 |
| 開啟狀態 | open, defaultOpen, onOpenChange | 彈出/展開元件的受控/非受控狀態 |
| 表單語意 | name, min, max, step | 底層基礎元件支援時傳遞 |
| 地區設定 | locale, hourCycle, firstDayOfWeek, structured value type | 只用於相關元件;時區由 ZonedDateTime 表示 |
| 組合 | children, parts, render | 優先組合,不分支修改原始碼 |
| 版面 | className, style | 用於位置/尺寸;顏色使用設計變數 |
受控:
const [value, setValue] = React.useState("line-a");
<SegmentedControl value={value} onValueChange={next => next && setValue(next)} aria-label="生產線">
<SegmentedControlItem value="line-a">生產線 A</SegmentedControlItem>
<SegmentedControlItem value="line-b">生產線 B</SegmentedControlItem>
</SegmentedControl>;
非受控:
<Select
defaultValue="normal"
options={[
{ value: "normal", label: "一般" },
{ value: "priority", label: "優先" }
]}
aria-label="派送模式"
/>
不得同時傳入 value 與 defaultValue。使用者控制值或開啟狀態時,也必須在回呼中更新狀態。Linyao Design System 不建立另一份業務狀態。
元件清單
基礎元件
Button:primary、secondary、neutral、quiet、danger;尺寸、載入、起始/結尾圖示。neutral在亮色與深色主題均使用高對比灰色表面;破壞性操作使用danger,不得以一般灰色按鈕代替。IconButton:只有圖示的操作,必須有可存取名稱。Link:保留連結與導覽語意。Badge、Avatar、Separator。Card與CloudBox別名:material/elevated/inset/outline/cloud 表面。SectionHeading。ListCell:前置內容、主要內容、標題、說明、metadata、後置內容。
Button 載入時會停用操作並設定 aria-busy。IconButton 的可見圖示屬於裝飾,可存取名稱必須由 aria-label 或 aria-labelledby 提供。
import { ArrowRightIcon } from "@phosphor-icons/react/dist/csr/ArrowRight";
import { FloppyDiskIcon } from "@phosphor-icons/react/dist/csr/FloppyDisk";
import { PlusIcon } from "@phosphor-icons/react/dist/csr/Plus";
import { Button, IconButton } from "@linyao.tw/ui";
<Button startIcon={<FloppyDiskIcon weight="bold" />}>儲存變更</Button>;
<Button endIcon={<ArrowRightIcon weight="bold" />}>繼續</Button>;
<IconButton aria-label="新增項目">
<PlusIcon weight="bold" />
</IconButton>;
startIcon/endIcon 是裝飾性的 ReactNode 插槽,尺寸與顏色由 Button 控制。載入時 spinner 取代起始圖示並隱藏結尾圖示,但可見標籤仍作為可存取名稱。只有圖示的操作應使用 IconButton。用戶端程式碼使用 Phosphor 個別 CSR 匯入;不得手寫 SVG 或以 Unicode 字元代替圖示。
表單輸入
Input。TextField、Textarea/TextView。SearchField、PasswordField、NumberField。CodeField、PhoneField、OTPField。FileUpload、DropZone。
欄位元件共用標籤、說明、錯誤、必填/無效/停用/唯讀狀態與 sm/md/lg 尺寸。沒有可見標籤時,實際控制項必須提供 aria-label 或 aria-labelledby。
PhoneField 不解析國碼或驗證電話號碼。CodeField 不決定驗證協定。OTPField 只處理多欄輸入;發送、重送、倒數與伺服器驗證由應用程式負責。
FileUpload/DropZone 只回傳瀏覽器 File 物件。上傳傳輸、限制、MIME/內容驗證、重試、進度來源與安全掃描由應用程式處理;accept 只提供瀏覽器提示。
選取元件
Checkbox、CheckboxGroup、CheckboxItem。RadioGroup、Radio、RadioItem。Switch。Slider。Toggle、ToggleGroup、SegmentedControl/SegmentedControlItem。Select,提供optionsAPI 與組合 parts。Combobox、Autocomplete,提供選項與組合 parts。DropdownMenu/Menu、ContextMenu。
Switch 用於立即生效的二元設定;Checkbox 用於表單中的獨立選項;RadioGroup 用於互斥選項;SegmentedControl 用於少量並列的檢視/模式。不得因外觀相近而交換語意。
一般選項可使用高階 Select/Combobox。需要群組、分隔線、複合項目或特殊 render 時,使用同一元件的 parts,不直接混用未樣式化基礎元件。
Base UI 1.8.0 已包含 #5058 的巢狀選單 portal owner 修正;Linyao Design System 不再使用 workspace patch。Storybook 保留開啟狀態的 role="group"、axe、方向鍵、Escape 與焦點返回回歸測試。Base UI 焦點守衛用於 portal 的 Tab 順序導引,不得刪除、設為 inert 或以 MutationObserver 改寫。
展開與結構
Accordion。Collapsible。Tabs:Root、List、Tab、Indicator 與 Panel。Indicator 預設隱藏。
Accordion 用於多個展開區段;Collapsible 用於單一區域;Tabs 用於同一內容中切換面板。需要 URL/連結語意時,應使用導覽元件,不以 Tabs 代替路由。
浮層元件
Tooltip、Popover、PreviewCard。Dialog/Modal。AlertDialog。Drawer。BottomSheet:把手、頁首/內容/頁尾與停駐點。
浮層元件保留 Base UI 的焦點限制、Escape、焦點返回、外部互動與定位。最小 Dialog:
<Dialog.Root>
<Dialog.Trigger render={triggerProps => <Button {...triggerProps}>開啟設定</Button>} />
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Viewport>
<Dialog.Popup hasCustomClose>
<Dialog.Header>
<Dialog.Title>設定</Dialog.Title>
<Dialog.Description>調整工作站設定。</Dialog.Description>
</Dialog.Header>
<Dialog.Body>{/* 欄位 */}</Dialog.Body>
<Dialog.Footer>
<Dialog.Close
render={closeProps => (
<Button {...closeProps} variant="secondary">
完成
</Button>
)}
/>
</Dialog.Footer>
</Dialog.Popup>
</Dialog.Viewport>
</Dialog.Portal>
</Dialog.Root>
自行提供 Dialog.Close 時,在 Dialog.Popup 設定 hasCustomClose,避免重複關閉按鈕。AlertDialog 只用於需要明確確認的決策。Tooltip 不能承載完成任務所需的唯一資訊;需要螢幕閱讀器與觸控使用者主動探索內容時,使用 Popover。
回饋元件
Alert/AlertView、Banner。ToastProvider、ToastViewport、ToastRoot與 Toast 管理工具。Progress、Meter。Spinner/Loader。Skeleton。EmptyState。
狀態使用 neutral、info、success、warning、danger。狀態不能只靠顏色,必須提供可理解的標題/說明或可存取標籤。
Toast 佇列由 Base UI 管理器處理。使用者決定加入佇列、去重複、重試與後端錯誤文案。需要長期保留或會影響任務完成的訊息,不應只使用短暫 Toast。
Progress 表示任務進度;Meter 表示已知範圍中的測量值。無確定進度的載入使用 Spinner/Loader,並由周圍區域提供適當的忙碌狀態關聯。
導覽
Breadcrumb、Pagination。NavigationMenu、Menubar、Toolbar。Header組合 parts。TabBar。CommandPalette。
路由由應用程式管理。連結目的地、目前路由、預先載入與導覽副作用都由使用者提供;接上路由連結時仍須保留錨點語意、可存取名稱與鍵盤行為。
Pagination 只呈現控制項與狀態,不取得資料或決定頁碼模型。CommandPalette 只提供 Dialog 與 Combobox 組合,不建立指令登錄、快捷鍵規則、權限或非同步搜尋後端。
資料與內容
- 使用原生語意的
Table、TableFrame。 DataTable:頁首/標題/說明/控制項/狀態/區域。ScrollArea。List、OrderedList、Collection與項目/內容/操作/metadata parts。
DataTable 不包含排序演算法、篩選狀態、伺服器分頁、列選取業務規則、資料取得或欄位 schema 引擎。
Collection 不處理大量資料虛擬化。大型清單的視窗化、尺寸測量與非同步載入由應用程式選擇方案,再套用 Linyao Design System 語意與設計變數。
日期與時間
Calendar:月份/年份導覽、鍵盤日曆格線、最小/最大值與不可用日期。DateField。DatePicker。DateRangePicker。TimeField、TimePicker。DateTimePicker。
Linyao Design System 從套件根目錄重新匯出常用值類別與剖析器:
import { useState } from "react";
import { CalendarDate, DateRangePicker } from "@linyao.tw/ui";
const [range, setRange] = useState({
start: new CalendarDate(2026, 9, 1),
end: new CalendarDate(2026, 9, 5)
});
<DateRangePicker label="維護期間" locale="zh-TW" value={range} onValueChange={next => next && setRange(next)} minValue={new CalendarDate(2026, 9, 1)} isDateUnavailable={date => date.day === 13} />;
值型別:
CalendarDate:不含時間/時區的日期,例如生日或結算日。CalendarDateTime:不含指定時區的當地日期與時間。ZonedDateTime:包含 IANA 時區與確切時間點。Time:不含日期的當地時間。
locale 控制語言與區段;hourCycle、granularity、firstDayOfWeek、minValue、maxValue 與 isDateUnavailable 由使用者指定。不得將 zh-TW、Asia/Taipei、YYYY/MM/DD 或 24 小時制設為套件預設值。
Linyao Design System 不自動將 CalendarDateTime 轉為 ZonedDateTime。需要確切時間點時,應用程式必須提供時區、處理 DST/不存在/重複的當地時間,並決定序列化方式。
不屬於元件層的功能
下列功能由應用程式處理:
- DataTable 排序/篩選/資料取得/虛擬化;
- 上傳傳輸或雲端供應商轉接器;
- 國際電話解析/國家資料庫;
- 指令登錄、全域鍵盤快捷鍵管理器或路由整合;
- 表單套件專用轉接器;
- 分析、持久化、權限、API 用戶端;
- 產品特定的日期格式、假日日曆、預約限制或時區轉換規則。
只有跨產品、沒有業務假設且可維持可存取性的需求,才評估新增 headless 轉接器或組合模式。
新增元件的判斷
- 現有
variant、size或語意變數是否已支援? - 是否可用現有 parts 組合?
- 差異是否只屬於使用者版面,可用
className解決? - 是否為應用程式業務邏輯?
- 只有共用、可重複且可存取的行為才新增元件。
分支修改會失去後續修正、設計變數、鍵盤行為與測試。確定需要新元件時,依貢獻指南完成 API、狀態、Storybook stories、測試、可存取性與套件匯出。
在地化
元件的預設字串(關閉、上一頁、清除選取、已選擇 N 個檔案等)集中在訊息套件,不再散落於各元件的預設參數。沒有 provider 時使用繁體中文套件,行為與過去一致:
import { MessagesProvider, enUSMessages } from "@linyao.tw/ui";
<MessagesProvider messages={enUSMessages}>
<App />
</MessagesProvider>;
部分覆寫會與外層套件合併,因此只調整單一字串不需要重寫整份:
<MessagesProvider messages={{ dialogClose: "收合面板" }}>
<SettingsDialog />
</MessagesProvider>
優先順序是「元件屬性 → 最近的 provider → 繼承的 provider → 繁體中文預設」。ComponentMessages 是完整的字串清單;codeSlotLabel 與 fileSelectionSummary 是函式,用於需要插入數字的字串。
一次設定兩邊:LinyaoProvider
字串套件與日期時間的地區設定是兩套獨立機制:前者是 MessagesProvider,後者是 React Aria 的 I18nProvider。分別設定很容易漂移,出現「按鈕是英文、日期格式是 zh-TW」這種組合。LinyaoProvider 從單一 locale 同時決定兩者:
import { LinyaoProvider } from "@linyao.tw/ui";
<LinyaoProvider locale="en-US">
<App />
</LinyaoProvider>;
locale 會挑選內建套件(zh 開頭用繁體中文,其餘用英文),同時提供給日期時間元件決定區段順序、星期名稱、數字格式與預設小時制。需要套件未內建的語言,或只想改幾個字串時,messages 會合併在挑選出的套件之上:
<LinyaoProvider locale="ja-JP" messages={{ dialogClose: "閉じる" }}>
<App />
</LinyaoProvider>
getMessagesForLocale(locale) 會回傳某個地區代碼對應的內建套件,可用於自行組合。省略 locale 時行為與單獨使用 MessagesProvider 相同:保持繁體中文預設,也不建立 I18nProvider。
個別日期時間元件的 locale 屬性仍然有效,會覆寫 provider 提供的值。
書寫方向
書寫方向不由 provider 控制。dir 屬於文件層級,和 data-lyds-theme 一樣必須在伺服器輸出的標記裡就正確,否則第一次繪製會閃動:
<html lang="ar" dir="rtl" data-lyds-theme="light"></html>
元件 CSS 全面使用 logical properties,設定 dir 後間距、圓角與圖示位置會自動跟著鏡射。
組合方式
Dialog、Drawer、Tabs、Accordion、Popover、Menu、Select、Combobox 與 CommandPalette 都同時提供 namespace 與扁平匯出,建議使用 namespace:
<CommandPalette.Root>
<CommandPalette.Trigger>開啟指令列</CommandPalette.Trigger>
<CommandPalette.Portal>
<CommandPalette.Backdrop />
<CommandPalette.Popup>
<CommandPalette.Input />
<CommandPalette.List>{/* … */}</CommandPalette.List>
</CommandPalette.Popup>
</CommandPalette.Portal>
</CommandPalette.Root>
CommandPalette 本身也是 root 元件,因此 <CommandPalette> 與 <CommandPalette.Root> 之外的 parts 都掛在同一個匯出上。完整規則見架構文件的組合模型。