Skip to main content

elf-ui-pattern

Elf Express UI 元件設計模式與可用性規範(元件 API、a11y / ARIA、鍵盤操作與 focus、表單、design token、 loading / empty / error 三態、響應式版面)。 當任務涉及:設計或修改可重用元件、按鈕 / 開關 / 導覽 / 對話框、ARIA 屬性、鍵盤快捷鍵、focus 管理、 表單欄位與驗證訊息、顏色 / 字型 / 陰影 token、非同步資料的載入 / 空 / 錯誤畫面、斷點或版面縮放、 ant-design-vue 元件(a-modal / a-popconfirm / a-spin / a-empty / a-result)選用時觸發。

Quellinformationen

Repository
elf-express/skillshare-source
Letzte Quellaktivität
27. September 2026 um 01:26
Erkannte Sprache von SKILL.md
Chinesisch
Sterne
0
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
2 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
elf-ui-pattern
description
Elf Express UI 元件設計模式與可用性規範(元件 API、a11y / ARIA、鍵盤操作與 focus、表單、design token、 loading / empty / error 三態、響應式版面)。 當任務涉及:設計或修改可重用元件、按鈕 / 開關 / 導覽 / 對話框、ARIA 屬性、鍵盤快捷鍵、focus 管理、 表單欄位與驗證訊息、顏色 / 字型 / 陰影 token、非同步資料的載入 / 空 / 錯誤畫面、斷點或版面縮放、 ant-design-vue 元件(a-modal / a-popconfirm / a-spin / a-empty / a-result)選用時觸發。
metadata
{"version":"1.0.0","owner":"Elf Express"}
# Elf Express UI 元件設計規範 > 事實來源:參考專案 `TypingTrainer/apps`(`styles/app.css` token、`ToggleSwitch.vue`、`AppSider.vue`、 > `SettingsView.vue` 更新狀態機、`useGridLayout`、`PracticeView.vue` focus 管理)。參考專案的 a11y 缺口在 > 「常見錯誤」列為反例。完整範本:[`references/components.md`](references/components.md)。 > 團隊 UI 函式庫:**ant-design-vue 4**(已決議;版本見 `elf-stack`,註冊方式見 `elf-vue` 規則 47)。 相關 skill:`elf-vue`(SFC / store 寫法)、`elf-i18n`(所有文案、IME 輸入)、`elf-e2e`(focus / 鍵盤行為的測試)、 `elf-unit`(元件測試)。通用 UI 美學參考第三方 skill `frontend/design/*`,本檔只規定團隊硬規則。 --- ## 1. 何時使用 - 新增或修改 `src/components/` 的可重用元件 - 任何可點擊、可輸入、可切換的 UI(按鈕、開關、分段選擇、導覽、對話框、表單) - 顯示非同步資料的畫面(清單、卡片、統計) - 新增顏色、字級、陰影、間距或調整版面斷點 --- ## 2. 固定規則 ### 2.1 元件 API(MUST) 1. **MUST** 區分兩類元件: - **通用元件**(`components/` 內一般元件):只透過 props / `defineModel` / emits / slots 溝通,**MUST NOT** 讀寫 Pinia store、router、API。 - **殼層元件**(`AppSider`、`TopBar`、`StatusBar` 等 `App*` / 殼層):可讀 store 與 router,只能在 `App.vue` 使用。 2. **MUST** 變體用字面值聯集 prop:`size?: 'sm' | 'md'`、`variant?: 'primary' | 'default' | 'danger'`;**MUST NOT** 用多個 boolean(`isSmall`、`isLarge`)。 3. **MUST** 布林 prop 為選填且預設 `false`(`disabled?: boolean`),命名不加 `is` 前綴。 4. **MUST** 內容用 slot、資料用 prop:可放任意標記的區塊(標題列右側動作、空狀態插圖)用具名 slot;文字 / 數字用 prop。 5. **MUST** emits 名稱用動詞原形小寫(`select`、`close`、`retry`);payload 為單一值或物件,**MUST NOT** 傳 DOM Event 給父層。 6. **MUST** 元件根元素接受外部 `class`(Vue 預設 attribute 透傳),**MUST NOT** 設定 `inheritAttrs: false` 除非同時手動 `v-bind="$attrs"` 到可互動元素。 ### 2.2 語意與 ARIA(MUST) 7. **MUST** 先用原生元素:可點擊 → `<button type="button">`;換頁 → `<RouterLink>`;輸入 → `<input>` / `<textarea>` / `<select>`。**MUST NOT** 在 `<div>` / `<span>` 上掛 `@click` 當按鈕。 8. **MUST** 自訂控制項加正確 role 與狀態:開關 `role="switch"` + `:aria-checked`;分段選擇 `role="radiogroup"` + 子項 `role="radio"` + `:aria-checked`;分頁 `role="tablist"/"tab"` + `:aria-selected`。 9. **MUST** 只有圖示的按鈕加 `:aria-label="t('...')"`(`title` 不算可及名稱);裝飾性圖示加 `aria-hidden="true"`。 10. **MUST** 視覺文字無法表達完整意義時,用 `aria-label` 補足(參考專案:畫面顯示 `v0.1.9`,可及名稱為「軟體更新 v0.1.9」)。 11. **MUST** 導覽清單用 `<nav>` 包住,目前頁面項目加 `aria-current="page"`。 12. **MUST** 每個頁面有一個 `<h1>`(可視覺隱藏),區塊標題依序 `h2` → `h3`,不跳級。 13. **MUST** 所有 `aria-label`、`placeholder`、`title`、`alt` 文字走 i18n(見 `elf-i18n`)。 ### 2.3 鍵盤與 focus(MUST) 14. **MUST** 所有互動元素可用 Tab 到達、Enter / Space 觸發;自訂元件若非原生 `<button>`,**MUST** 加 `tabindex="0"` 與 `@keydown.enter` / `@keydown.space.prevent`。 15. **MUST** 保留可見 focus 樣式:用 `:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }`。**MUST NOT** 寫 `outline: none` 而不提供替代。 16. **MUST** 全域快捷鍵只在 `App.vue`(或 `useShortcuts` composable 於 `App.vue` 呼叫)註冊於 `document`,`onBeforeUnmount` 移除;只用 `Ctrl/⌘ + 鍵` 組合,觸發時 `preventDefault()`。 17. **MUST** 快捷鍵與 Enter 送出處理先檢查 `e.isComposing`,組字中一律 return(避免中文輸入法選字時的 Enter 被當成送出)。 18. **MUST** 在畫面上提示快捷鍵(參考專案狀態列:`Ctrl+Enter 開始 · Ctrl+N 換一篇`),提示文字走 i18n。 19. **MUST** 若某元素必須持有 focus(例:打字練習的隱藏 IME 欄位),任何會搶走 focus 的操作(點工具列按鈕、換題、重置)結束後以 `nextTick(() => el.value?.focus())` 還回;此行為 **MUST** 由 e2e 測試覆蓋(jsdom 觀察不到 focus)。 20. **MUST** 對話框用 ant-design-vue 的 `a-modal`(`v-model:open`),開啟時 focus 移入、Esc 關閉、關閉後 focus 回到觸發按鈕(`a-modal` 預設行為);**MUST NOT** 設 `:keyboard="false"` 或 `:focus-trigger-after-close="false"`,**MUST NOT** 自寫對話框或 focus trap(範本見 references §10.2)。 ### 2.4 表單(MUST) 21. **MUST** 每個輸入都有 `<label>`(`for` / `id` 配對或包住 input);視覺上不顯示時用 `.visually-hidden` 類別,**MUST NOT** 只靠 `placeholder`。 22. **MUST** 驗證錯誤:輸入加 `:aria-invalid="!!error"` 與 `:aria-describedby="errorId"`,錯誤訊息元素 `:id="errorId"` 並放在輸入下方。 23. **MUST** 送出中:送出按鈕 `:disabled="pending"`、表單 `:aria-busy="pending"`;重複送出直接 return。 24. **MUST** 文字輸入用 `v-model.trim`(密碼除外);數字用 `type="number"` + `v-model.number`。 25. **MUST NOT** 在由 composition 事件驅動的「非受控」輸入欄位綁定 `:value` / `v-model`(會清掉輸入法組字緩衝);詳見 `elf-i18n` 的 IME 規則。 26. **MUST** 危險操作(刪除、覆蓋)需二次確認:單筆用 `a-popconfirm`(確認鈕 `danger`),批次 / 影響範圍大用 `Modal.confirm({ okType: 'danger' })`;**MUST NOT** 點一下直接執行(範本見 references §10.1)。 ### 2.5 Design token(MUST) 27. **MUST** 顏色、字型、陰影定義在 `src/styles/app.css` 的 `:root` CSS 變數,命名固定: | 類別 | token | | --- | --- | | 背景 | `--bg`(頁面)、`--surface`(卡片)、`--sider`(側欄) | | 文字 | `--text`(主)、`--text-2`(次)、`--text-3`(說明)、`--muted`(停用) | | 線條 | `--line`、`--line-2` | | 控制項 | `--seg-bg`(分段選擇底色) | | 品牌 | `--primary`、`--primary-hover`、`--primary-active` | | 語意 | `--success`、`--warning`、`--error` | | 陰影 | `--card-shadow`、`--overlay-shadow`、`--seg-shadow` | | 字型 | `--font`、`--font-mono` | 28. **MUST** 元件樣式只用 `var(--token)`;**MUST NOT** 在 `.vue` 的 `<style>`、`:style` 或 TS 寫十六進位色碼 / `rgb()`(半透明陰影除外,且應優先抽成 token)。 29. **MUST** ant-design-vue 主題(`<a-config-provider :theme="theme">` 的 `theme.token`)值集中在 `src/styles/theme.ts`,與 `app.css` 同值,並有同步測試 `theme.test.ts`(範本見 references §7)。**MUST NOT** 在 `App.vue` 內聯另一份色碼。 30. **MUST** 數字欄位(計數、百分比、計時)加 `.nums`(`font-variant-numeric: tabular-nums`),避免跳動。 31. **MUST** 規則 27 表格是**封閉清單**:`app.css` `:root` 只能出現表內名稱,**MUST NOT** 在元件或 `app.css` 自創 token。確實需要新 token 時,**MUST** 在同一個 PR:(1) 更新本 skill 規則 27 表格(PR 標註需 review);(2) 加入 `app.css`;(3) 若 ant-design-vue 也要用該色,加入 `theme.ts` 的 `TOKENS` 與 `CSS_VAR_OF`,讓 `theme.test.ts` 自動檢查同步。 ### 2.6 Loading / Empty / Error 三態(MUST) 32. **MUST** 非同步資料用狀態聯集 `'idle' | 'loading' | 'success' | 'error'`(在 store,見 `elf-vue`),**MUST NOT** 用多個互相矛盾的 boolean。 33. **MUST** template 依序判斷:首次 `loading`(尚無資料)→ `error` → `empty` → 內容(`v-if` / `v-else-if` / `v-else` 串接),同一時間只顯示一種;已有資料時的 `loading` 落在「內容」分支並疊加指示(規則 34)。 34. **MUST** 非同步區塊用 `AsyncBlock.vue`(references §1)呈現三態。loading:容器 `:aria-busy="true"`;首次載入顯示 `a-spin` + `role="status"` 文字;資料已存在時的重新載入 **MUST NOT** 清空畫面,改用 `<a-spin :spinning="status === 'loading'">` 包住既有內容(`AsyncBlock` 最後一個分支)。 35. **MUST** error:`a-result status="error"` + `role="alert"`,顯示 i18n 訊息 + 原因 + 「重試」`a-button`(emit `retry` 或呼叫 `load({ force: true })`)。 36. **MUST** empty:`a-empty`,`description` 說明「為什麼是空的」、預設 slot 放下一步動作(例:「尚無題庫」+「新增文本」按鈕;篩選無結果 +「清除搜尋」);**MUST NOT** 只顯示空白或「無資料」。用 `AsyncBlock` 時傳 `empty-text` 並填 `#empty-action`。 37. **MUST** 按鈕觸發的動作(檢查更新、儲存)以狀態機驅動按鈕文字與 `disabled`(參考 `SettingsView.vue`:`idle / checking / latest / found / installing / done / error`)。 38. mock 模式下 API **MUST** 保持非同步延遲(`delay()`,預設 120ms),確保 loading 畫面在開發時看得到。 ### 2.7 響應式版面(MUST) 39. **MUST** 殼層固定 `height: 100vh`:頂列 48px、內容區 `flex: 1; overflow-y: auto`、狀態列 48px;捲動只發生在內容區或卡片內部。 40. **MUST** flex / grid 中會捲動或縮小的子元素加 `min-width: 0; min-height: 0`(否則內容撐破容器、捲動失效)。 41. **MUST** 雙欄頁面在**內容區**寬度 < 620px 時改為單欄(參考專案 `useGridLayout` 的門檻)。優先用 CSS `@container` / `@media`;只有需要算出實際像素高度時(例:鍵盤高度 = 欄高 − 工具列高)才用 `useElementSize`(ResizeObserver)。 42. **MUST** 長文字加 `.ell`(單行省略)或允許換行,**MUST NOT** 讓文字溢出卡片。 43. **MUST** 最小支援視窗:寬 360px、高 540px 時所有功能可操作(值待確認,見第 6 節)。 ### 2.8 ant-design-vue 元件對照(MUST,已決議) 44. **MUST** 下列情境使用指定的 ant-design-vue 4 元件,**MUST NOT** 自寫同功能元件: | 情境 | 元件 | 對應規則 | | --- | --- | --- | | 對話框 / 表單對話框 | `a-modal`(`v-model:open`) | 20 | | 單筆危險操作確認 | `a-popconfirm`(`:ok-button-props="{ danger: true }"`) | 26 | | 批次 / 大範圍危險操作確認 | `Modal.confirm({ okType: 'danger' })` | 26 | | 載入中 / 重新載入 | `a-spin`(重新載入時包住內容) | 34 | | 空狀態 | `a-empty`(`description` + 預設 slot 動作) | 36 | | 載入失敗 | `a-result status="error"`(`#extra` 放重試) | 35 | | 按鈕 / 圖示按鈕 | `a-button`(圖示按鈕加 `:aria-label`) | 7、9 | | 開關 | `a-switch`(`v-model:checked`) | 8 | | 分段選擇 | `a-segmented`(`v-model:value`) | 8 | | 主題 | `a-config-provider`(`:theme` 來自 `styles/theme.ts`) | 29 | 45. 函式庫元件無法滿足時(產品專屬外觀、需要 radio 語意的自訂內容等)才可自寫,且 **MUST** 照 references 的自寫範本(`ToggleSwitch`、`SegmentedControl`、`FormField`)達成同等 a11y,並在 PR 說明為何不用函式庫元件。新用到的 `a-*` 元件 **MUST** 加入 `src/plugins/antd.ts`(見 `elf-vue`)。 46. SoybeanAdmin / NaiveUI 系既有 repo(例 `platform.client`)為歷史例外:在該 repo 內**以該 repo 的 CLAUDE.md 為準**(NCard、Modal `preset="card"` 等規格),本節對照表不適用。 --- ## 3. 標準範本 完整範本在 [`references/components.md`](references/components.md): - `AsyncBlock.vue` — 三態容器(`a-spin` / `a-result` / `a-empty` / default slot) - `FormField.vue` — label + 錯誤訊息 + aria 綁定 - `SegmentedControl.vue` — `role="radiogroup"` + 方向鍵 - `AppNav.vue` — `<nav>` + `RouterLink` + `aria-current` - `useShortcuts.ts` — 全域快捷鍵(含 `isComposing` 防護)與測試 - Focus 持有者模式(IME 欄位) - `SplitLayout.vue` — 以 `@container` 依內容區寬度切換雙欄 / 單欄 - `styles/app.css` token 區塊、`styles/theme.ts`、`theme.test.ts` 同步測試 - `a-popconfirm` 危險操作、`a-modal` 表單對話框 ### 3.1 三態 template 骨架 一律透過 `AsyncBlock`(完整原始碼見 references §1): ```vue <template> <section> <AsyncBlock :status="status" :empty="!items.length" :error="errorMessage" :empty-text="t('library.empty')" @retry="store.load({ force: true })" > <template #empty-action> <a-button type="primary" @click="emit('create')">{{ t('library.create') }}</a-button> </template> <ul class="list"> <li v-for="item in items" :key="item.id">{{ item.title }}</li> </ul> </AsyncBlock> </section> </template> ``` `AsyncBlock` 內部順序固定為 首次載入(`a-spin`)→ 錯誤(`a-result`)→ 空(`a-empty`)→ 內容(重新載入時以 `a-spin :spinning` 包住,不清空)。 ### 3.2 圖示按鈕 ```vue <a-button type="text" :aria-label="t('shell.toggleSidebar')" @click="settings.toggleSider()"> <template #icon><MenuFoldOutlined aria-hidden="true" /></template> </a-button> ``` ### 3.3 必要全域樣式(`src/styles/app.css`) ```css .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } .nums { font-variant-numeric: tabular-nums; } .ell { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } :where(button, a, input, select, textarea, [tabindex]):focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen