SkyLab Frontend — 樣式規範
本文件說明前端樣式架構與撰寫規範,適用於前端所有頁面與元件:新頁面、元件都應遵循此指南,確保視覺與程式碼風格一致。若想自行變更
_variables.scss、_themes.scss兩個檔案,請事先與前端討論。
目錄結構
src/assets/styles/
├── global.scss # 全域樣式入口(@use themes、backgrounds、reset;全頁背景層、toast 樣式)
├── _themes.scss # CSS 自訂屬性(亮色 / 深色主題)
├── _backgrounds.scss # 背景花色,由 body[data-bg] 切換
├── _variables.scss # SCSS 結構變數(間距、字體、斷點、圓角)
├── _mixins.scss # 可重用的 SCSS mixin
└── _reset.scss # CSS Reset元件 / 頁面的樣式請以 CSS Modules 撰寫,放在元件旁:
src/pages/personal/resources/
├── ResourcesPage.jsx
└── ResourcesPage.module.scss ← 與元件同名,同目錄在 SCSS Module 中使用共用變數與 mixin
vite.config.js 已透過 css.preprocessorOptions.scss.additionalData 對所有 SCSS 檔全域注入:
@use "@/assets/styles/variables" as *;
@use "@/assets/styles/mixins" as *;因此 .module.scss 內可直接使用 $spacing-*、$font-size-*、@include flex-center 等,不需要(也不應)在檔案開頭手動再加 @use variables / mixins——手動引入是冗餘的。舊檔殘留的手動引入無害,重構經過時順手移除即可。
注意:全域注入僅涵蓋
variables與mixins兩檔;_themes.scss的顏色是 CSS 自訂屬性(var(--color-*)),本來就不需引入。
⚠️ 變數使用原則
禁止在元件 SCSS 中自行新增新的 SCSS 變數或 CSS 自訂屬性。
請優先查閱並沿用 _variables.scss(間距、字體、圓角等)與 _themes.scss(顏色)中已定義的變數。若確實找不到對應的變數,應先討論是否有必要加入全域定義,而非在元件內自行宣告。
顏色系統
所有顏色一律使用 _themes.scss 中定義的 CSS 自訂屬性,不可在元件 SCSS 內直接寫死 HEX 色碼。狀態色也一律走 --color-success 等變數(僅下方明列的例外可寫死色碼)。
主要變數
背景
| 變數 | 用途 |
|---|---|
--color-bg-base | 頁面底色 |
--color-bg-gradient-blue/yellow/green | 三色暈染背景漸層 |
表面
| 變數 | 用途 |
|---|---|
--color-surface | 卡片、面板背景 |
--color-surface-glass | 毛玻璃效果背景 |
--color-surface-glass-border | 毛玻璃邊框 |
--color-sidebar | 側邊欄背景 |
品牌色
| 變數 | 用途 |
|---|---|
--color-primary | 主色(藍紫) |
--color-primary-dark | 深色主色(僅作底色,如 primary 按鈕 hover;深色模式仍為深色,當文字會不可讀) |
--color-primary-light | 淺色主色 |
--color-primary-on-surface | 品牌色文字/邊框用;亮暗兩色都達 AA。勿與 --color-text-primary 混淆(語序相反、兩者皆為藍色) |
文字
| 變數 | 用途 |
|---|---|
--color-text | 一般文字 |
--color-text-primary | 標題、強調文字 |
--color-text-secondary | 次要文字 |
--color-text-muted | 輔助說明、placeholder |
--color-text-on-primary | 主色背景上的文字(白) |
邊框與互動
| 變數 | 用途 |
|---|---|
--color-border | 一般邊框 |
--color-divider | 分隔線 |
--color-hover | Hover 背景 |
--color-row-hover | 表格列 hover 背景(比 --color-hover 深,避免與表頭同色) |
--color-overlay | Modal 遮罩 |
陰影
| 變數 | 用途 |
|---|---|
--shadow-sm | 細微陰影 |
--shadow-md | 中等陰影(卡片) |
--shadow-lg | 大陰影(Dialog) |
--shadow-glass | 毛玻璃陰影 |
專用色票各自只給一個元件用:--color-flow-*(拓撲畫布,見「卡片」)、--color-code-*(範例程式碼,見下方例外)、--color-terminal-*(首頁機器卡的終端輸出)、--color-folder-*(快速練習資料夾)、--color-kind-shared(機器來源徽章 MachineKindBadge)。
狀態色
前端使用以下五種語意顏色,黃橙色僅限「待審核 / pending」語意,不作為警示色——警示、錯誤一律紅色,統一走 --color-danger(不另設 --color-warning):
| 變數 | 亮色值 | 深色值 | 語意 | 使用情境 |
|---|---|---|---|---|
--color-success | #28a745 | 同左 | 🟢 正常 | 運行中、已連接、成功 |
--color-info | #2b4d98 | #89a5e0 | 🔵 一般 | 進行中、說明、一般標記 |
--color-pending | #d97706 | #f59e0b | 🟠 待審核 | 待審核、草稿、排程中、等待處理 |
--color-danger | #dc3545 | 同左 | 🔴 危險 | 錯誤、失敗、危險操作 |
--color-status-neutral | #6b7280 | #9ca3af | — | ⚫ 未啟用 |
危險操作的 hover 加深色用 --color-danger-dark(#b91c1c)。
例外:終端機式的內容面固定深色、不隨主題切換——VNC / xterm 畫面底(ConsoleDialog、Classroom 的
#1e1e1e)、任務 log 輸出區(JobsdialogOutput),以及錯誤頁插圖的白底(NotFoundPage的雲朵、CrashState的應用程式視窗、ConnectionLostState的雲與視窗皆為#fff,內部色塊也跟#fff混色——插圖在亮暗主題是同一張圖,亮色模式靠 drop-shadow 描輪廓)。例外:範例程式碼區塊(AI API 快速開始的程式碼範例、API 聊天回覆裡的程式碼區塊)固定深底、不隨主題切換,比照 VS Code 深色編輯器;色票統一用
_themes.scss的--color-code-*(bg/header/border/hover/text/muted),不要在頁面裡另寫一組深色。例外:Gateway 頁的類 VSCode 設定檔編輯器(
ConfigCodeEditor.module.scss)整組寫死 vs-dark 色票(#1e1e1e、#252526、#007acc等)與 13px/12px 字級,刻意不隨主題切換——外框需與 Monacotheme="vs-dark"一致,模擬 VSCode 視窗本身即為獨立配色的容器。
狀態 Badge 的標準寫法
膠囊外形與字級由 @include badge-base; 提供(inline-flex、膠囊圓角、12px/500、不換行),再加上內距與下面其中一組顏色:
.badge_success { background: color-mix(in srgb, var(--color-success) 12%, transparent); color: var(--color-success); }
.badge_info { background: color-mix(in srgb, var(--color-info) 12%, transparent); color: var(--color-info); }
.badge_pending { background: color-mix(in srgb, var(--color-pending) 12%, transparent); color: var(--color-pending); }
.badge_danger { background: color-mix(in srgb, var(--color-danger) 12%, transparent); color: var(--color-danger); }
.badge_muted { background: color-mix(in srgb, var(--color-status-neutral) 12%, transparent); color: var(--color-status-neutral); }一律用
var(--color-*),不要把狀態色寫死成 HEX——深色模式的 info / pending 亮色值才吃得到。
徽章一律不加外框:只用「狀態色 12% 淡底+狀態色文字」,不要再加
border(實線、虛線都不要)。狀態、類型、範圍這類膠囊標籤都算徽章,非狀態的中性標籤用上面的badge_muted。疊在畫布等會透出底圖的地方時,淡底改混var(--color-surface)(color-mix(in srgb, <色> 12%, var(--color-surface)))讓它不透明,仍然不加框。 分頁、SegmentedControl、Stepper 的選中框不在此列。
SCSS 變數(_variables.scss)
間距
$spacing-4: 4px $spacing-8: 8px $spacing-16: 16px
$spacing-24: 24px $spacing-32: 32px $spacing-48: 48px字體大小
$font-size-10: 10px $font-size-12: 12px $font-size-14: 14px $font-size-16: 16px
$font-size-18: 18px $font-size-24: 24px $font-size-28: 28px $font-size-32: 32px
$font-size-10僅限資料密集區(密集網格、卡片 meta 列)的次要標籤使用;一般內文、說明文字最小$font-size-12。
字重
$font-weight-400: 400 $font-weight-500: 500 $font-weight-700: 700圓角
$radius-8: 8px $radius-12: 12px $radius-16: 16px $radius-pill: 999px動畫
$transition-base: 0.2s ease $transition-slow: 0.3s ease斷點
$breakpoint-sm: 576px $breakpoint-md: 768px
$breakpoint-lg: 992px $breakpoint-xl: 1200px
$breakpoint-sidebar: 1024px // 側邊欄在抽屜與固定欄之間切換的寬度其他
$font-family-base $line-height-base: 1.6 $form-control-padding-x: 12px
$header-height: 60px $sidebar-width: 260px $sidebar-width-collapsed: 72px
$content-padding: 40px $container-max-width: 1200px
$z-ai-assistant: 2000 $z-screen-dialog: 2100 // 見 z-index 層級Mixin(_mixins.scss)
Flex 排版
@include flex-center; // display:flex; align-items:center; justify-content:center
@include flex-between; // display:flex; align-items:center; justify-content:space-between
@include flex-column; // display:flex; flex-direction:column文字截斷
@include text-truncate; // 單行截斷+省略號
@include text-clamp(3); // 多行截斷(預設 2 行)容器
@include container; // max-width: 1200px; margin-inline: auto; padding-inline: 16px毛玻璃效果
@include glass-surface; // 預設玻璃陰影 var(--shadow-glass)
@include glass-surface($shadow: var(--shadow-sm)); // 換陰影
@include glass-surface($shadow: none); // 不輸出 box-shadow
@include glass-surface(8px, 1.2); // 固定濾鏡參數(不跟隨風格切換,特殊情況才用)玻璃表面的 backdrop-filter 一律走 var(--glass-backdrop-filter) (sidebar 用 var(--sidebar-backdrop-filter)),讓「液態玻璃」等 風格變體能整體換濾鏡——不要在元件裡寫死 blur(12px) saturate(1.4)。
響應式斷點
@include respond-to(md) {
// min-width: 768px 時套用
}
@include respond-below(md) {
// 小於 768px 時套用(max-width: 767.98px)
}新版面一律 mobile-first,用 respond-to。respond-below 是給既有的 desktop-first 版面用的:反轉成 mobile-first 成本太高時,用它取代直接寫 max-width media query,讓斷點對齊共用級距。
Icon 使用規範
所有 Icon 一律透過 MIcon 元件呼叫,預設為 outlined 風格;需要實心(filled)時傳 filled prop,不要自己換 class。
import MIcon from "../components/MIcon";
<MIcon name="search" size={16} /> {/* material-icons-outlined(預設) */}
<MIcon name="star" size={16} filled /> {/* material-icons(filled,特別強調時才用) */}- 字型是 Material Icons(
@material-design-icons/font),不是 Material Symbols。Icon 名稱請到 Material Icons 圖示集 查詢(outlined 與 filled 同名);只有 Material Symbols 才有的名稱會直接顯示成文字 - 禁止直接使用
<span className="material-icons">、material-icons-outlined或其他 Icon 庫 - 禁止使用 SVG inline、emoji、或其他圖示系統混搭
命名規範
CSS Modules 類別名稱
使用 camelCase:
.cardHeader { }
.statusDot { }
.headerBtn { }BEM 風格的子變體
用底線 _ 區隔變體,而非 BEM 的 --:
.badge_success { }
.badge_danger { }
.dot_connected { }
.dot_error { }動畫 / 狀態後綴
| 後綴 | 用途 |
|---|---|
Out | 元素離場動畫(如 .powerMenuOut) |
Active | 主動選中狀態(如 .menuBtnActive) |
Disabled | 禁用樣式(優先用 CSS :disabled 偽類) |
頁面版面(Page Layout)
頁面根容器(.page)一律滿寬:不設 max-width、不使用 margin: 0 auto 置中,讓內容吃滿 DashboardLayout 的內容區。全站頁面留白因此一致,寬螢幕下不會出現「某些頁置中留白、某些頁滿寬」的落差。
標準寫法:
.page {
@include flex-column;
gap: $spacing-24;
padding: $spacing-8 $spacing-16;
flex: 1;
@media (min-width: $breakpoint-sidebar) { padding: 0; }
}例外:表單型窄頁可限制寬度——輸入欄位拉滿寬螢幕反而難用。這類例外限於「單欄表單」頁面,一般內容頁請維持滿寬。
元件樣式慣例
卡片(Card)
.card {
@include glass-surface;
border-radius: $radius-16;
@include flex-column;
overflow: hidden;
}- 可點卡片的 hover 只給淡底(
background: var(--color-hover))或陰影/上浮,並保留transition淡入淡出;不改邊框色,藍框只留給「選中/目前」狀態(如分頁 active、流程目前步驟) - 淡底不要用
linear-gradient疊層:漸層無法 transition,hover 會瞬間跳色 - 玻璃卡裡的內容區塊(資訊欄位、備註框、程式碼/日誌/金鑰等
pre、清單列、統計格)一律background: var(--color-surface)(白)+1px solid var(--color-border);不要鋪--color-hover、--color-bg-base等主色系淺藍。它們跟著主色走,使用者換了背景色,疊在透出背景的玻璃上就會糊。淺藍只留給 hover/選中狀態、按鈕、徽章、提示框 - 拓撲畫布(React Flow)不在白底規則內:底色、框線、陰影一律用畫布專用 token
background: var(--color-flow-bg)、1px solid var(--color-flow-border)、box-shadow: 0 4px 24px var(--color-flow-shadow),防火牆頁、課程環境編輯器、資源詳情、班級上課環境的唯讀拓撲都同一組
Dialog / Modal
新對話框一律用共用的 <Modal>(components/Modal/Modal),不要再手寫遮罩與卡片。它統一處理 portal 到 body、遮罩與進出場動畫、role/aria-modal/aria-labelledby、Esc 與點遮罩關閉(busy 時不關;疊兩層只關最上層)、 開啟時焦點移進對話框並在關閉後還原、Tab 鎖在對話框內、鎖住底下頁面捲動:
const presence = useDialogPresence(show);
{presence.open && (
<Modal
as="form" onSubmit={submit} // 表單型才傳
closing={presence.closing} onClose={() => setShow(false)} busy={saving}
title="重設密碼" description="新密碼只會顯示一次"
size="sm" // sm 400/md 640/lg 1100/xl 1280;錯誤 log 等寬內容用 log(560)
closeButton // 欄位多的表單:標題列帶 ×、內容區自己捲、按鈕列固定在底部
actions={<><button …>取消</button><button type="submit" …>送出</button></>}
>
…欄位…
</Modal>
)}- 精簡卡(預設):確認框、命名框、小表單;
closeButton版:欄位多的表單(如連線對話框),內容只有一兩句的提醒(如工作階段提醒)可加dividers={false}拿掉標題列與按鈕列的分隔線;bare:只給外框,標題列與內容自己排 - 終端機、VNC 這類 AI 讀不到的畫面用
layer="screen"(通常搭bare):蓋過 AI 助手、不讓位;鍵盤全部交給畫面,Esc 不關、Tab 不鎖(Tab 補全、vim 的 Esc 才能用)。全螢幕要的對話框本體用ref拿 - 導覽用的
data-guide等屬性直接傳給Modal(掛在對話框本體),×、按鈕列分別用closeProps、actionsProps - 前端所有對話框都建在
Modal上,包括useConfirm、ConnectionDialog與終端機/VNC/課堂觀看畫面;不要再手寫遮罩 - 寬度分級(
size):確認框/命名框sm400px;小型單欄表單md640px;一般lg1100px;寬版(如 VNC)xl1280px;錯誤 log 等寬內容log560px - 高度:
Modal卡片最高90vh;畫面型對話框(VNC/終端機主控台、課堂觀看)自己把高度固定在88vh - 全螢幕:可以全螢幕的對話框用
:fullscreen偽類設max-width: 100%; height: 100%; border-radius: 0(見ConsoleDialog.module.scss) - 遮罩:
var(--color-overlay)加backdrop-filter: blur(4px)、z-index: 300,portal 到document.body:祖先有backdrop-filter/transform時,position: fixed會被困在那一層、蓋不滿整個畫面。AI 助手面板開著時,遮罩會讓出面板的寬度(dialog-overlay-avoid-ai),對話框改在剩下的空間置中 - 卡片陰影:精簡卡用
glass-surface預設陰影;closeButton版用var(--shadow-lg),浮在遮罩上比較突出 - 危險操作的確認傳
role="alertdialog"(useConfirm已經這樣做)
確認彈窗(全站統一)
危險操作/二選一確認一律用共用的 useConfirm()(components/ConfirmDialog/ConfirmProvider), 不要在頁面內自建本地 ConfirmModal:
const confirm = useConfirm();
if (!(await confirm({ title, message, confirmText, danger: true }))) return;
// …按下確認後彈窗即關閉,進度用按鈕 disabled + toast 呈現,不在彈窗內轉圈外觀來自 Modal 的精簡卡:400px 的毛玻璃卡、$spacing-24 內距,標題/內文/按鈕列之間的間距全交給 $spacing-16 的 gap,不用 margin。
- 標題:
$font-size-16/$font-weight-700,danger 帶紅色warning圖示、一般帶琥珀色help圖示 - 內文:
$font-size-14次要色,加overflow-wrap: anywhere(常插入主機名稱等連續長字串) - 進出場:遮罩 fadeIn 0.15s、卡片 slideUp 0.18s,Esc 可關閉
按鈕
按鈕一律使用 _mixins.scss 的按鈕 mixin 組(六款),不要在頁面內重抄整組樣式:
.btnPrimary { @include btn-primary; } // 主要動作:每個畫面至多一顆
.btnSecondary { @include btn-secondary; } // 並列的一般動作、返回
.btnDanger { @include btn-danger; } // 只用在確認彈窗/最終確認步驟
.btnDangerOutline { @include btn-danger-outline; } // 頁面內破壞性動作的「入口」,點了才進 useConfirm
.btnGhost { @include btn-ghost; } // 低強調輔助動作:淡底文字鈕(平常即有 --color-hover 淡底,hover 深一階)
.btnGhostDanger { @include btn-ghost($danger: true); } // 淡紅底紅字變體(窄空間的刪除)
.iconBtn { @include btn-icon; } // 32×32 圖示鈕,JSX 必帶 aria-label
.iconBtnDanger { @include btn-icon($danger: true); } // 未 hover 前文字即為紅色
.menuBtn { @include btn-icon-secondary; } // 表格列的動作選單鈕(白底描邊),icon 一律 more_vert(⋮)
.dialogClose { @include btn-dialog-close; } // Dialog 專用右上關閉鈕:同 btn-icon($danger: true),僅未 hover 無底色- 共用基底(mixin 內建):高 36px、圓角
$radius-8、字級 14/500、 hover 一律:not(:disabled)、disabled 一律opacity: 0.5; cursor: not-allowed - 頁面差異(寬度、margin、grid 位置)寫在
@include之後;要更矮更小請先問是不是真的需要 - 既有頁面的「修飾類」家族(如表格列的
.actionBtn.actionBtnDanger)維持基類+修飾寫法, 但危險修飾類未 hover 前文字就要是紅色,且 hover 同樣必加:not(:disabled) - 下拉選單裡的危險項(PowerMenu、頁首 ⋯ 選單)屬選單樣式,不在此六款之列
表單(Form)
表單欄位一律使用 _mixins.scss 的表單 mixin 組,不要在頁面內另立一套字級與內距:
.field { @include form-field; } // 標籤 + 控制項的直式容器
.field input, .field select, .field textarea { @include form-control; }
.fieldInvalid.fieldInvalid { @include form-control-invalid; } // 送出時未填的欄位<label className={styles.field}>
<span>班級名稱</span>
<input className={invalid ? styles.fieldInvalid : undefined} aria-invalid={invalid} … />
</label>規則一:控制項高度固定 36px、字級 14px,刻意與
.btnPrimary/.btnSecondary一致——同一列的欄位與按鈕才會對齊。要更矮更小的表單請先問是不是真的需要, 不要在頁面裡改min-height或font-size。 高度的算法跟按鈕相同:全站行高 1.6(14px 字 → 22.4)+上下內距 6×2+框 1×2 ≈ 36px。 內距寫成 8px 就會變 40px;外框在容器上的搜尋框用min-height: 36px,不要寫 38px。 共用的 SegmentedControl 也固定 36px,跟欄位、按鈕放同一列不用另外鎖高度。
規則二:欄位怎麼排(幾欄、哪個跨欄)寫在頁面自己的 grid 上(
.formGrid、.createFormGrid、.fieldFull),mixin 只負責欄位本身長什麼樣。
規則三:一組「起—迄」的值是一個欄位,不是兩個。用
.timePair這種 成對控制項,標籤寫「上課時間」,不要拆成「開始時間」「結束時間」兩個.field。
規則四:textarea 一律固定高——
_reset.scss已全域設resize: none, 高度由 JSX 的rows或頁面 CSS 的height/min-height決定,元件內不要再寫resize。特殊情況(真的需要讓使用者拖高的長文編輯區)才在該頁明確寫回resize: vertical,讓例外看得見。
開關(Switch)與勾選框
「立刻生效」的啟用/停用一律用共用的 Switch(components/Switch/Switch),不要再用開關圖示按鈕(toggle_on / toggle_off)或勾選框代替:
<Switch checked={rule.enable !== 0} onChange={(next) => toggle(rule, next)} disabled={busy} ariaLabel={t("…enableSwitch")} title={t("…enableSwitch")} />- 分工:一切換就送出 →
Switch;要按「儲存」才生效的表單欄位 → 勾選框(.checkRow)。看到開關就知道會立刻生效,兩者別混用 - 外觀:36×20 膠囊軌道+16px 白色圓鈕,開=
--color-primary、關=灰,是一個button role="switch" - 表格、清單列裡只放開關本體,不寫「啟用」字樣(欄名或列內容已交代控制什麼),改用
ariaLabel+title;真的需要可見文字才給label,寫「控制什麼」、不隨狀態改字 - 目前狀態由軌道位置與顏色表達;列表上停用的那一列,狀態徽章直接換成紅色「停用」(例如防火牆規則的「允許」換成「停用」),不另外並排「已停用」徽章
- 停用的那一列只淡化內容,開關所在的操作欄不淡化,才看得出能再打開
- 目前用在:資源詳情進階設定的防火牆規則、防火牆頁規則面板、「PVE 管理」頁的節點列表
表格(Table)
列表頁表格一律使用 _mixins.scss 的表格 mixin 組,不要在頁面內重抄整組樣式:
.tableWrap { @include table-wrap; } // 玻璃容器 + 圓角 + 橫向卷動
.table { @include table-base; min-width: 720px; } // min-width 依內容自定,撐出卷動
.th { @include table-th; }
.tr { @include table-tr; } // 基底不含 hover,見下方規則
.td { @include table-td; }- 欄寬、對齊、特殊儲存格(
.thRight、.tdNowrap…)等頁面差異寫在@include之後 - RWD 行為統一為 容器橫向卷動(
table-wrap內建overflow-x: auto),不做表格轉卡片 - 表格嵌在既有卡片內時可只用
table-base/table-th/table-tr/table-td,省略外層table-wrap <table>本身由components/DataTable(TanStack Table,見開發指南)輸出,class 仍來自頁面自己的 SCSS Module:表格用className、列用rowClassName(row)、各欄在欄位定義的meta.thClassName/meta.tdClassName
規則一:整列 hover 變色 = 這一列可以點
整列 hover 變色是互動訊號,不是裝飾。列本身不可點(互動都在儲存格內的按鈕上)時, 不要讓整列變色,否則是假的可點暗示。
.tr { @include table-tr; } // 不可點:只有分隔線,無 hover
.trClickable { @include table-tr-clickable; } // 可點:游標 + hover 一起給- 判斷標準只有一個:
<tr>自己有沒有onClick(或role/tabIndex) - 同一張表可以混用(監控頁:節點列可點、VM 列不可點),所以用兩支 mixin 疊加,不用布林參數
- 不要自己寫
cursor: pointer或&:hover { background: … }——游標與 hover 會各自漂移 - 需要不同的 hover 色時(例如群組列本身已有底色),可在
@include之後覆寫background
規則二:列數會變的表格要固定欄寬
table-layout 預設的 auto 依全部列的內容計算欄寬。只要展開/收合會增減 完整欄位的列,每一欄的內容都變了,整張表就會跳動。
.table { @include table-base; table-layout: fixed; min-width: 1080px; }
.colStatus { width: 100px; } // 欄寬集中宣告,搭配 JSX 的 <colgroup>- 只有在展開列是
<td colSpan={N}>的整寬詳情面板時才不需要—— colspan 儲存格不參與個別欄寬計算,不會造成跳動 - 改用
fixed後,要移除儲存格內為了「跟 auto layout 搶寬度」而設的min-width, 它們只會讓內容溢出欄位 - 不指定寬度的那一欄會吸收剩餘空間(通常留給名稱欄)
規則三:儲存格圖示要帶文字沒有的資訊
MIcon 一律 aria-hidden,螢幕閱讀器讀不到。若圖示編碼的資訊 就寫在緊鄰的文字裡(型別、分類),它只是版面慣性,拿掉讓文字說話即可。
清單工具列與批量操作(DataToolbar)
清單或表格上方的「篩選+搜尋」與「勾選後的批量操作」一律用 components/DataToolbar 這一組,不要在頁面裡另做浮動操作列或自己的勾選框:
const selection = useRowSelection(shown.filter(canSelect).map((r) => r.id)); // 依畫面順序
const selected = shown.filter((r) => selection.isSelected(r.id));
<DataToolbar
selecting={selected.length > 0}
batch={(
<BatchBar countLabel={t("…selectedCount", { count: selected.length })} onClear={selection.clear} progress={progress}>
<BatchAction icon="block" label={t("…disable")} onClick={…} />
<BatchDivider />
<BatchAction icon="delete" label={t("…delete")} danger onClick={…} />
</BatchBar>
)}
>
<SegmentedControl … />
<ToolbarSearch value={query} onChange={setQuery} placeholder={t("…searchPlaceholder")} />
</DataToolbar>
<SelectCheckbox checked={selection.isSelected(r.id)} onToggle={(shift) => selection.toggle(r.id, shift)} ariaLabel={…} />- 工具列一直都在:平常沒有底色,放狀態切換與搜尋,控制項對齊清單左右緣;有勾選時原地換成批量列(淡主色底的浮起卡片,黏在頂端)。至少 56px 高,篩選在窄螢幕排成兩行時批量列沿用同一個高度——切換時清單不會跳
- 全選:表格放在表頭第一欄(
SelectCheckbox cell);沒有表頭的清單(使用者、申請、班級卡片)傳BatchBar的selectAll,放在「已選 N」前面 - 勾選框:整格或四周都是點擊範圍;列本身可點時,列的
onClick要略過label。Shift+點連選一段、Esc 清除選取、只勾一部分時全選框顯示「部分」 - 勾選狀態下整列都能點:已勾了至少一筆後,點整列也是勾選(
onClick={(e) => selection.rowClick(id, e)},回傳true就不要再開詳情);列裡其他按鈕、連結照舊。原本點了會進詳情的列(表格列、審核列、班級票券)在勾選狀態下改成勾選,取消全部勾選後恢復 - 只勾得到看得見的:
useRowSelection只認傳進去的那些 id;換篩選、翻頁、刷新後消失的自動取消勾選,免得切回來才發現它還勾著、被一起操作 - 動作:只送給適用的那幾筆(停用只送啟用中的…),全都不適用就停用按鈕,
title說明會略過幾筆;送出時progress讓整條換成「正在… 3 / 10」;做完靜默刷新,成功的取消勾選、失敗的保留勾選(selection.replace)可以直接再試 - 回不去的動作(刪除、移除、封存、拒絕)用
danger,放在BatchDivider後面的最右邊,先確認並列出名稱(最多 5 個,其餘「等 N」) - 沒有後端批量 API 時用
utils/runBatch逐筆呼叫單筆 API(預設同時 4 筆,一筆失敗不影響其他筆) - 勾選中的列底色用
@include row-selected;(自己就是卡片的清單列傳var(--color-surface)) - 導覽用的
data-guide等屬性可以直接傳給DataToolbar(掛在工具列外框),讓導覽步驟框住整條「篩選+搜尋」 - 加了勾選欄要重算寬度:清單列用 grid 時,一行排完或分兩行要看容器寬度(
container-type: inline-size+@container),不要看視窗——側邊欄出現(≥1024)後內容比 992 時還窄,用視窗切會在 1024~1180 之間撐出橫向捲動 - 目前用在:資源管理、使用者管理(批量啟用/停用、加入/移出群組、刪除)、申請審核(批量拒絕)、班級管理(批量封存並回收)、班級工作頁學生名單(批量移除)
Dropdown 選單
- 浮在清單上方的選單 portal 到
document.body、用position: fixed,由hooks/useAnchoredMenu定位:捲動與縮放時跟著錨點按鈕重算,依剩餘空間往下或往上展開,錨點捲出視窗就收起,點外面或按 Esc 也會關閉。PowerMenu、ConnectionDialog的連接埠建議,以及 AI API 頁與範本頁的選單都用它 - 選單底色用不透明的
var(--color-surface),不要套glass-surface:底下的清單列會透出來 - 關閉動畫用
useDialogPresence(open, 130)+ CSStransition,不用onAnimationEnd
步驟列(Stepper)
流程分頁/設定步驟一律用共用的 Stepper(components/Stepper/Stepper),不要再手刻箭頭分段(chevron)或自己畫圓點。 目前用在班級工作頁、教學環境編輯頁、一鍵建立班級(ClassSetupPage)、首次安裝初始化精靈(SetupPage)與首次登入引導(OnboardingPage)。
<Stepper
ariaLabel={t("...")}
steps={[{ key, label, done, disabled }]} // 圓點依序顯示 1、2、3…,done 時換成 ✓;disabled=精靈還不能跳過去的步驟
extras={[{ key, label, icon }]} // 選填:不算步驟的分頁,接在分隔線後面
activeKey={tab}
onSelect={(key) => ...}
/>- 外觀是直接放在頁面上的一排「圓點+標籤」(不鋪底色、不包卡片),步與步之間以連線相接;第一顆圓點貼齊頁面內容左緣
- 狀態只靠圓點表達:未完成=白底淡藍框編號、已完成=白底主色框 ✓、目前=主色實心加光暈;標籤只分目前(粗、深)與其他
- 連線兩端都「走到了」(已完成或目前)才上主色,其餘用淡主色
- 步驟列直接壓在漸層背景上,不要用
--color-border淺灰或淡色實心底:跟背景糊在一起看不到,一律用白底+藍色系框線 - 精靈式流程:已完成的步驟和第一個待辦步驟都能點(退回前面改完可以直接跳回來),再後面的步驟設
disabled:只擋點擊、不淡化,不要做成點了沒反應的按鈕。能不能到、打不打勾都看已存到後端的實際進度,不看「走過了沒」;網址指到還不能去的步驟時拉回能到的最後一步 - 精靈換步驟不自動存檔:目前這一步有沒存的修改時先
confirmLeave(),確認後捨棄再換(一鍵建立班級ClassSetupPage) - 精靈版面(
ClassSetupPage):步驟列下方是「目前這一步的卡片|右側摘要欄」。每一步都是同一張卡片(標題、內容、底部按鈕列),按鈕列用position: sticky; bottom: 0,卡片因此用overflow: clip(hidden會變成捲動容器,按鈕列就貼不住);卡片高度跟著內容走,不設min-height。分不分兩欄看頁面實際寬度(container: wizard / inline-size,≥760px 才分欄),不夠寬時摘要收成卡片上方可展開的一列;總覽步驟本身就是摘要,不放右欄 - 不算步驟的分頁(班級啟用後的上課進度、AI)放
extras:圓點改放圖示、不連線,不要硬塞成第 5、6 步 - 手機只留目前步驟的標籤,其他步驟剩圓點(標籤仍留給螢幕閱讀器);還放不下時橫向捲動
動畫規範
入場動畫
@keyframes slideUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
// 使用:animation: slideUp 0.18s cubic-bezier(0.25, 0.8, 0.25, 1);@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
// 使用:animation: fadeIn 0.15s ease;Dialog / Popup 的進出場(標準作法)
Dialog 一律「遮罩 fadeIn + 內容 slideUp」進場(Modal 已內建)。離場由共用 hook hooks/useDialogPresence.js 處理:關閉時先保留 DOM 150ms、設好 closing 讓淡出播完,再卸載。照上面的範例把 presence.closing 傳給 Modal 即可。
const dialog = useDialogPresence(editTarget); // 布林或資料物件皆可
// 關閉期間 dialog.item 會保留最後一筆資料,避免內容閃爍
{dialog.open && <EditDialog target={dialog.item} closing={dialog.closing} … />}共用 Dialog 元件(如 ConnectionDialog)接受 closing prop,由父層的 useDialogPresence 控制。自含式 Dialog(如 VncDialog、TerminalDialog、ClassroomWatchDialog)則在內部 setClosing(true) 後 setTimeout(onClose, 150)。
離場動畫(關閉)
優先使用 setTimeout + CSS transition,不使用 onAnimationEnd(有已知邊界問題)。選單可以直接用 useDialogPresence(open, 130),手寫的話長這樣:
// JSX
function closeMenu() {
setClosing(true);
setTimeout(() => { setOpen(false); setClosing(false); }, 130);
}// SCSS
.menu {
opacity: 1;
transform: translateY(0);
transition: opacity 0.12s ease, transform 0.12s ease;
}
.menuOut {
animation: none; // 覆蓋入場 animation,讓 transition 接管
opacity: 0;
transform: translateY(6px);
pointer-events: none;
}深色模式
主題切換透過 body.dark class 實現,所有顏色均已在 _themes.scss 中定義亮色 / 深色兩套值。
元件 SCSS 一律使用 CSS 自訂屬性,不需要自行寫 body.dark & 覆蓋。
如果某元件有特殊深色需求,把覆寫寫在 body.dark 底下:
:global(body.dark) & {
color: #xxx;
}z-index 層級
| 層級 | 值 | 用途 |
|---|---|---|
| 基礎卡片 | 1 | 一般卡片 |
| 卡片 hover / 選單 | 50 | 容器內的 Dropdown 選單 |
| 側邊欄/Sticky Header | 100 | 側邊欄與頁面頂部的列 |
| Portal 浮層選單 | 150 | portal 到 body 的 Dropdown(如 components/PowerMenu) |
| Dialog / Modal | 300 | 全頁覆蓋 Dialog |
| AI 助手 | 2000($z-ai-assistant) | 高於一般對話框,對話框開著時仍可使用 |
| 畫面型對話框 | 2100($z-screen-dialog) | Modal layer="screen"(VNC、終端機、課堂觀看):AI 讀不到的內容直接蓋過助手 |
| 導覽 UserGuide | 3000–3199 | 導覽聚光層必須壓過含 Dialog 在內的一切(overlay 3100、示範視窗 3099)。此區段保留給 UserGuide,一般元件勿用 |
| Toast | sonner 預設值 | sonner 自己的 toaster z-index 高於其他所有層級 |
⚠️ 注意:使用
backdrop-filter或transform的元素會建立新的 stacking context,子元素的z-index無法穿透至外層。若發現 Dropdown 被其他卡片遮住,請確認父元素是否有這類屬性。玻璃表面(
glass-surface)搭配overflow: hidden的容器還會裁掉溢出的 absolute 選單。浮層若可能超出容器範圍,改用createPortal掛到document.body並以position: fixed定位(範例:components/PowerMenu),且背景要用不透明的var(--color-surface),否則會透出底下的列表內容。