HHG Design System v1.18.0
統一元件庫:色票(ink 近黑主色+橘 CTA)× OMS 幾何節奏(圓角/高度/字級階層)。
所有內部系統 prototype 與開發實作從這裡取元件;規範與使用時機見 GUIDELINE.md,改版紀錄見文末。
用觸控裝置開這一頁,所有控件會自動變高(@media (pointer: coarse) 改高度 token)——這是 v1.3.0 的 RWD 策略,見第 17 節。
v1.4.0 之後新增的在第 18–23 節:說明鈕/手機列表卡/工具列欄位/表格欄寬政策/應用外殼/圖示一律取自 lucide.dev。
v1.13.0 一次結清 backlog 的 P1 八項,全在第 24–31 節:頁標題區/中性 tag/兩欄詳情版面(container query)/精靈步驟條/黏頂黏底列/通知面板/可編輯明細表格/KPI 卡。
1. 色票 Color
單一主色紀律:選中態/主按鈕/焦點一律 --ink;橘色是稀有 CTA 與導覽選中系;狀態色只用於狀態。所有淺色底由 ink 的透明度衍生(5%/6%/8%)。
--ink #1B1D22 主色
--ink-hi #3D404A
--cta #F08200
--nav-sel-bg #FFF7E8
--ink-tint-05
--ink-tint-08
--field-bg #F5F6F8
--chip-bg #F0F1F4
--surface-2 #F1F2F6 次表面
--accent2-weak #FFEECB 橙 tint
--green #1E9E62
--amber #E0A100
--red #D8442B
--slate #565A66
填色上的前景(v1.8.0)——在此之前庫裡有 18 處寫死 #fff。淺色派看不出問題是巧合不是規則:
--ink 一旦變亮就是白字壓亮底,而主題層救不了寫死值。四顆不共用一顆,因為底色不同色相不同明度、將來不一定同時是白。
⚠️ --on-cta 是最可能先分家的一顆:--cta 配白字只有 2.66:1,是公司明文禁用的組合。現在維持白=照現況封存,不是說它合格;真要修,改這一顆就全庫生效。
--on-ink(--ink 底上的字)
--on-cta ⚠️ 2.66:1
--on-danger
--on-green
深底中性色階梯(v1.9.0,6 顆)——App shell 的深色側欄用。
⚠️ 不要拿 --on-ink 當深底文字色:它是純白,服務的是小面積填色控件;大面積深底上的文字用純白會過亮、邊緣起光暈,所以主文字是 93%。
⚠️ 深色端的 alpha 必須比淺色端大(淺 05/06/08 vs 深 06/10/14)——同暖色主題實測到的「暖底要 6/7/10 而非 5/6/8」:「淺底由主色透明度衍生」這條原則的前提,是底夠中性。
--on-dark .93 主文字
--on-dark-2 .52 次要文字
--on-dark-tint-06 hover
--on-dark-tint-10 計數膠囊
--on-dark-tint-14 頭像
--on-dark-line .08 分隔線
2. 幾何階層 Scale
同類元件一律取同級,不自創中間值(原則 4)。
🔴 本節刻意不手寫任何數字(v1.17.0):每個方塊的幾何都吃 var(--token),下面的現值是本頁載入時從 getComputedStyle 讀出來的
⇒ 改 tokens.css 這一節就跟著變,不可能再漂。在此之前這一節是硬寫的字面值,v1.14.0 圓角與控件高度各升一階之後它整組是舊的——而本頁是視覺驗收基準,寫錯比沒寫更糟。
要引用規格請用 token 名(例:--h-btn),不要引用這裡顯示的數字。
圓角
--r-*
r-drawer
r-menu
r-card
r-input
r-fieldcard
r-btn
r-chip
r-tag
r-check
⚠️ --r-fieldcard 與 --r-input 現在同值(上面兩個方塊看得出來圓角一樣),圓角階梯在這裡少一階。這是 warm 併入時就有的重疊,照拍板值進、列為待觀察,不自行發明新值修正。另有 --r-full(999,switch 軌/頭像/full pill)不在這條階梯上。
控件高度
--h-*
h-input 輸入
h-btn 按鈕
h-select
h-tab
h-icon
h-seg
h-chip
容器高度
--h-row/--h-bar
h-bar
h-row
--h-bar=抽屜 header/footer;--h-row=表格列/表頭。這兩顆比控件階都高,在 pointer:coarse 下不放大(base 本來就 ≥ --hit-min)。
命中盒
--hit-min
hit-min
🔴 這一顆不是尺寸階,不要拿它跟上面的 --h-* 並列挑選(v1.17.0 新增)。它是觸控最小命中高度(WCAG 2.5.5/2.5.8)=「這東西要被手指點到」的下限,也是原則 10 整段的立論基礎。庫內落點:.ds-option、coarse 下的 .ds-link/.ds-checkbox/.ds-switch、.ds-select-menu 的捲動上限(=可見 7 列 × 這一顆)。不要拿它當面板高度、間距或圖示直徑。
字級
--fs-*
頁標題 --fs-page
modal·抽屜標題 --fs-title
面板標題 --fs-panel
app shell 側欄品牌名 --fs-app
內文/輸入值 --fs-body
section 標題 --fs-section
欄位 label·按鈕 --fs-label
小 label/pill chip --fs-sub
hint/badge --fs-hint
fx chip/nav 副標 --fs-micro
數字展示階
--fs-num-*
1,284件
1,284,000
96,500
v1.13.0 新增 --fs-num-lg /-md /-sm + --ls-num。刻意不挪用標題階:「大號數字」(金額、件數、天數)在這批系統是反覆出現的獨立角色,綁在標題階上,將來調標題就會連帶改到金額。用這一階一律配 tabular-nums。
3. 按鈕 Button
primary=一般主行動(ink);cta=整頁唯一的關鍵行動才用橘;danger 只放在破壞性確認;secondary/ghost 為次要。
高度看它跟誰同列(原則 4,數字回 tokens.css 查):.ds-btn = toolbar 階(--h-btn)/.ds-btn-sm = dense 階(--h-select,v1.17.0 新增)。挑哪一個看它跟誰同列,不是看它自己重不重要。
⚠️ pointer:coarse 下兩階都放大到 --hit-min ⇒ 觸控裝置上 .ds-btn 與 .ds-btn-sm 一樣高。那是原則 10 刻意的設計,不是壞掉。
變體
上面五顆的 computed height 只准有一個值(原則 4 的自我檢查法)——.ds-btn-sm/.ds-input-sm/.ds-select-trigger/.ds-dashed-btn 全吃 --h-select。
<button class="ds-btn ds-btn-primary">主要動作</button>
<button class="ds-btn ds-btn-sm ds-btn-secondary">密集列的按鈕</button> <!-- v1.17.0 -->
4. 文字輸入 Input
淺灰底、圓角吃 --r-input;focus 轉白底+ink 框。label 在上、hint 在下。
尺寸看它跟誰同一列(數字一律回 tokens.css 查,本頁不複寫——那批數字在 v1.14.0 漂過一階):表單欄位 .ds-input=form 階(--h-input)/工具列·篩選列與按鈕並排 .ds-input-md=toolbar 階(--h-btn)/卡內密集區與 select 並排 .ds-input-sm=dense 階(--h-select)。同一列只准有一種高度。
機器可讀的對映見 tokens.json 的 controlScale 區塊(v1.17.0 新增,值是 token 名不是數字)。
🔴 上面那顆篩選下拉掛的是 .ds-select-md(v1.17.0 新增)。在它進庫之前,工具列放篩選下拉必然違反原則 4——trigger 高度恆為 dense 階,這一列會出現兩種高度。這個缺口本庫自己登記了六版。
5. 下拉選單 Select
trigger 白底+灰框(hover/開啟轉 ink 框),預設高度=dense 階(--h-select);選單白底 --r-menu、強陰影、選項高度=--hit-min(觸控命中下限,v1.17.0 起有名字)、選中=ink 底白字+勾。需要 JS:DS.initSelect / DS.init()。
三個尺寸情境:.ds-field 內=form 階(自動對齊同列的 .ds-input)/.ds-select-md=toolbar 階(v1.17.0 新增,見第 4 節的同列同高示範)/預設=dense 階。
⚠️ 選單捲動上限的政策是「可見 7 列再捲」,不是一個固定 px(v1.17.0 改寫成 calc(7 * var(--hit-min)))。收成數值 token 會把「7 列」這個推導關係固化掉——列高一改就露出半列,而且看起來還是有 token。捲動容器上限一律用「可見列數 × 列高」表達。
<div class="ds-select" data-ds-select>
<button type="button" class="ds-select-trigger" aria-haspopup="listbox" aria-expanded="false">
<span class="ds-select-value ds-placeholder">請選擇</span><span class="ds-caret">▾</span>
</button>
<div class="ds-select-menu" role="listbox">
<div class="ds-option" role="option" data-value="a"><span class="ds-option-check">✓</span>選項A</div>
</div>
</div>
<script>DS.init()</script> // 取值 el.dataset.value;監聽 'ds-change'
5b. 可搜尋下拉 Combobox(單選)/Multicombo(多選)
選元件之前先問「真實選項數是多少」——示意資料不能當作依據。
這兩個元件的出身就是一次事故:某欄位依 prototype 的 4 個示意值選了 pills,真實資料是 1,233 筆供應商=欄位完全不可用。
判準:互斥 >10 或來自人員/供應商/館別這類外部名冊 → combobox;可複選 >50 或來自外部名冊 → multicombo。
它不是新的 select,是「select +選單裡多一個搜尋框」——根節點同掛 .ds-select,trigger 幾何一個值都沒新增。
三條定死的規則:①搜尋框在選單內最上方 sticky,不在 trigger 裡 ②不做虛擬捲動,未輸入時只渲染前 50 筆+標明共 N 筆
③multicombo 的 trigger 最多 3 個 chip +「+N」、高度固定不長高(會長高的多選框是表單版面跳動的頭號來源)。
單選(大量名冊)
(尚未選擇)
多選(1,233 筆)
已選 0 項
markup 直接寫
<div class="ds-select ds-combobox" data-ds-combobox> <!-- 多選:ds-multicombo / data-ds-multicombo -->
<button type="button" class="ds-select-trigger" aria-haspopup="listbox" aria-expanded="false">
<span class="ds-select-value ds-placeholder">請選擇</span><span class="ds-caret" aria-hidden="true">▾</span>
</button>
<div class="ds-select-menu"></div> <!-- 內容由 JS 生:搜尋框/計數/清單 -->
</div>
// 選項少 → markup 寫 .ds-option,DS.init() 自動接管
// 選項多 → 傳陣列進來,1,233 筆不要先變成 1,233 個 DOM 節點
DS.initMulticombo(el, { options:[{value:'V001', label:'V001 采威', badge:'已持有', disabled:true}, …] });
DS.getValue(el) // 單選回字串/多選回陣列
DS.setValue(el, ['V001']) // 預設不發事件,要通知傳 {notify:true}
el.addEventListener('ds-change', e => e.detail.values) // 單選是 e.detail.value
6. Segmented(單選)/ Pill 多選
單選一組互斥選項(2–5 個、名稱短)用 segmented;可複選用 pill。選中一律 ink 底白字。切換會清空資料的 segmented 加 data-ds-guard 自動掛守門 modal。
8. 欄位卡片 Field Card(三態)
大量可開關欄位的表單用卡片:停用(灰、仍可見)→ 啟用(白底強框+控件展開)→ 必填未填(淡紅框)。點 switch 才切換、點卡不觸發。
9. Tabs / Badge / 統計 Chip / Mono Tag
tabs=同資料集的檢視切換(含計數);badge=狀態標籤(pill 形、狀態色底);統計 chip=列表底部彙總;mono tag=代號/單號等等寬字內容。
Badge
欄位字母
已開通
審核中
不通過
已移除
統計/代號
共 8 筆
顯示第 1-7 筆
REQ-0004
BG
10. 表格 Table + 分頁 Pagination
白卡容器吃 --r-card+大柔陰影;列高 --h-row;hover = --hover-bg。分頁鈕 --h-btn 見方,current=ink。(數字一律回 tokens.css 查或見第 2 節的現值,本處不複寫。)
v1.3.0 起「可不可以點」由 markup 決定,不再預設全部可點:整張表可點掛 .ds-table-clickable,只有某些列可點掛 tr.ds-row-clickable。
沒掛的列就沒有手指游標、沒有 hover 底——非互動表格(表單摘要、異動紀錄、子表)到處都是,預設全可點是在騙人,而這正是唯一採用者不敢用它、自己手抄一份的原因。
數字欄用 .ds-num(靠右+等寬數字);勾選格用 .ds-check-cell(命中盒吃 --h-btn,觸控下自動到 --hit-min;負 margin 撐可點區但不撐列高);勾選中的列有持續底色。
非互動表格(沒有 .ds-table-clickable)
滑過去沒有 hover 底、也沒有手指游標
共 8 筆
11. 表頭篩選 Popover
表頭欄位篩選:ink 標題列+計數 chip+checkbox 清單+清除選取。radius 12、三層陰影。
12. 對話框:兩種型態+唯讀模式 / Toast
「要不要」用 ds-modal(確認型),「填什麼」用 ds-dialog(作業型)。
確認型=使用者只做要或不要的判斷,沒有輸入欄位、不可捲,焦點預設在「取消」(Enter 不該直接執行破壞性動作),破壞性動作要用 refList 列出會動到誰。
作業型=要填東西才能完成,body 可捲、含欄位驗證與錯誤訊息,焦點預設在第一個可填欄位。
兩者共用:onConfirm 回傳 false =動作失敗、對話框不關。
唯讀檢視(稽核明細、異動紀錄)用 DS.view()=DS.dialog({dismissOnly:true}):footer 只有一顆「關閉」,因為確定/取消那一組會暗示有東西要提交。
它是作業型的一個模式,不是第三種型態——形態完全相同,差別只在動作集合是空的。
Toast(v1.3.0 才有定位與進出場):置中貼底、由下滑入、2.4 秒自動消失、不吃滑鼠事件;疊層 --z-toast 是第八階,必須蓋過 modal——對話框裡的動作失敗就是靠它報錯。
ℹ️ .ds-modal-ico(警示圓圖示)的展示在下面「試按:確認型」的對話框裡(danger:true 時出現在標題左側)。它的直徑是刻意的字面值、不進 tokens——單一落點、不是點擊目標,也不吃 --h-icon(那個家族在觸控下會放大)。判準見 GUIDELINE「什麼樣的幾何值會變成 token」。
Toast
靜態外觀(.ds-toast-static,只給型錄用):
✓已送出申請 REQ-0012
這張單所有狀態變更的完整歷程,只能檢視、不能修改。
DS.confirm({ title:'發起移除權限', body:'將對以下權限發起移除:',
refList:['王小明/金石堂/欣新網'], danger:true, confirmText:'發起移除', onConfirm:()=>{...} })
DS.dialog({ title:'不通過', bodyHTML:'<textarea class="ds-input" id="note"></textarea>',
confirmText:'不通過', confirmClass:'ds-btn-danger',
validate: body => body.querySelector('#note').value.trim() !== '',
onConfirm: body => { /* 回傳 false 代表失敗、對話框不關 */ } })
13. 版面容器:卡片/工具列/空狀態
元件庫原本只有「控件」沒有「裝控件的東西」,各專案因此各自發明一次。
ds-toolbar=tabs/搜尋/篩選/動作並排的那一列(放進去的控件一律取 40 這一階,靠右的動作用 margin-left:auto)。
ds-card=內容卡(head/body/actions)。表格滿版時包一層 ds-scrollx,它同時負責把最後一列的 hover 底色裁進卡片圓角。
ds-empty=空狀態,「查無符合」與「本來就沒有」要寫成兩種文案。
14. 忙碌態:按鈕 loading / 骨架屏
①送出中的按鈕不換文案、不換寬度(避免版面跳動),只換轉圈+鎖互動 ②同時標 aria-busy="true" ③首次載入用骨架屏,不要整頁 spinner——骨架保留版面、不會閃。動畫皆受 prefers-reduced-motion 約束。
15. 提示條 Inline Alert
「一整條有底色的訊息」——不通過原因、拒絕開通原因、管理員備註、同步佇列說明、頁面說明。
每個審批系統都要,而庫裡原本完全沒有,所以同一支 app 已經自己長出兩套。
顏色規則與 badge 不同,這是刻意的:底色表狀態、文字一律 --text、狀態色只上在圖示。
badge 那邊已拍板接受「狀態色文字對淺底未達 AA」的例外(2–3 個字、另有形狀輔助),但提示條裝的是整句要讀的內容——
red 文字對 red 底只有 3.77:1,兩行說明就真的讀不下去。圖示是非文字元素(門檻 3:1,通過),狀態照樣看得出來。
ARIA:頁面一載入就在的說明條不要加 role="alert"(assertive 會打斷螢幕閱讀器);只有動作後才冒出來的錯誤/成功條才加。
ℹ
這裡是所有申請單狀態變更的完整歷程,只能檢視、不能修改。同一筆申請的同一次狀態變更只會留下一筆紀錄。
✕
不通過原因(主管審核):此系統權限需先完成資安訓練,請於完訓後重新申請。
✓
管理員備註:已於 SAP 開通,帳號同 AD 帳號。
·
不指定變體=中性底(--surface-2),給「純粹要被看到、但不表狀態」的說明用。
<div class="ds-alert ds-alert-red">
<span class="ds-alert-ico" aria-hidden="true">✕</span>
<div class="ds-alert-body"><span class="ds-alert-title">不通過原因:</span>……</div>
</div>
<!-- 開頭那個標籤用 .ds-alert-title,不要把整段變粗體:兩行以上的 600 長句會變成一片黑塊 -->
16. 單據詳情:頭區 / 鍵值 / 狀態時間軸
所有「單據詳情頁」的骨架。鍵值原本在同一支 app 裡就有兩套版面而沒有規則,v1.3.0 收斂成一組 class +一個修飾子:
.ds-kv=掃描用。欄位多、值短(人名/日期/單號/數量),使用者是在「找某一個值」→ auto-fit 並排,一眼掃完,寬畫面不會變成一條孤獨長列。
.ds-kv.ds-kv-list=閱讀用。欄位少(≤6)、值長或需逐條確認(送出前的摘要、稽核明細),閱讀順序有意義 → 固定兩欄由上而下。
時間軸的顏色規則:已完成 ink(既成事實)/目前這一關 amber(狀態=進行中)/不通過 red。
進度的「目前」一律不用 ink——ink 是控件選中與主按鈕的顏色,拿來表示進度會讓「使用者選的」與「系統走到的」分不出來。
圓點是裝飾(aria-hidden),狀態只靠顏色的部分用 .ds-sronly 補字,目前這一關另加 aria-current="step"。
蝦皮
通過(待開通)
REQ-0004
李小美・行銷部
- 申請人
- 李小美
- 公司別
- 欣新網
- 角色
- 一般使用者
- 送出時間
- 2026-08-09 09:41
- 廠編/館別
- S001 台北旗艦 +3
送出前確認.ds-kv-list(閱讀用)
- 申請類型
- 申請新系統/權限
- 審核主管
- 王小明(行銷部)
- 說明
- 因支援 8 月檔期,需要蝦皮後台的訂單查詢與出貨作業權限,預計使用至 9 月底。
處理進度
-
送出申請
李小美・2026-08-09 09:41
-
主管審核
王小明・2026-08-10 14:02
-
管理員開通(進行中)
Jody Lin・已等 2 天
-
完成
尚未開始
不通過的樣子
-
送出申請
陳大文・2026-08-05 11:20
-
主管審核(不通過)
王小明・2026-08-06 09:03(需先完訓)
<ol class="ds-timeline">
<li class="ds-timeline-step is-done"> <!-- is-done / is-current / is-rejected / 不掛=尚未到 -->
<span class="ds-timeline-dot" aria-hidden="true"></span>
<div class="ds-timeline-name">主管審核</div>
<div class="ds-timeline-meta">王小明・2026-08-10 14:02</div>
</li>
</ol>
<dl class="ds-kv"> <!-- 加 .ds-kv-list =兩欄閱讀版 -->
<div class="ds-kv-item"><dt class="ds-kv-label">申請人</dt><dd class="ds-kv-value">李小美</dd></div>
</dl>
17. 觸控目標:由「輸入裝置」決定,不由「螢幕寬度」決定
v1.3.0 的 RWD 拍板。走 @media (pointer: coarse) 改高度 token,不寫 max-width 覆寫。
用寬度當代理指標兩種情況都會判錯:1024px 的 iPad 是手指、拉窄到 700px 的桌機視窗是滑鼠。
因為全庫元件都吃 token,改 token 即全庫生效,採用專案一行都不用寫——「不准覆寫 ds- class」這條規矩因此不再被迫破功。
用 pointer(主要輸入裝置)不用 any-pointer(任一輸入裝置),後者會讓「接了觸控螢幕的純鍵鼠桌機」也整套放大。
已知副作用:二合一筆電切平板模式會被判成 coarse——判斷是放大不會壞、只是密度低一點,接受誤判往「大」的方向。
這條不取代斷點:pointer 管「手指還是滑鼠」(目標大小),寬度管「放不放得下」(版面),兩者不混用。
滑鼠/觸控
| token | 滑鼠 fine | 觸控 coarse | 為什麼 |
| --h-input / --h-row / --h-bar | 46 / 54 / 64 | 不動 | 本來就 ≥44 |
| --h-btn | 40 | 44 | 主要點擊目標 |
| --h-select | 36 | 44 | select・combobox trigger、卡內小輸入 |
| --h-icon | 32 | 44 | 只有圖示、沒有文字幫忙擴大命中區 |
| --h-tab + --ds-tabs-pad | 32 + 4 | 40 + 2 | 不是 44:軌道外高必須=--h-btn,40+2×2=44 |
| --h-seg | 31 | 40 | 是真的單選控件,不是裝飾 chip |
| --h-chip | 26 | 32 | 刻意不到 44:chips 密集並排,全 44 會撐成兩三排、反而更難掃 |
命中盒分離
勾選框那顆 20px 方塊不放大(放大會讓表單像兒童玩具),改用外層 label 撐命中區:表格內用 .ds-check-cell(負 margin 撐可點區但不撐列高,見第 10 節),一般情境用 coarse 下的 .ds-checkbox{min-height:44px}。
18. 說明鈕 Infotip(v1.5.0)
由使用者一句話帶出,而那句話解掉了一個我們自己稽核沒抓到的結構問題:提示條其實有兩種,而它們被混用同一個元件。
①說明型(功能介紹/補充)=看過一次就不用再看 → 本元件(標題旁 i 鈕,點開)
②狀態型(這一筆特有:不通過原因、管理員備註)=必須被讀到、每筆內容都不同 → 維持 .ds-alert(第 15 節)
③區塊規則(只在某張卡內有效)→ .ds-alert 放在那張卡內
混用的代價是真的:把①塞進 .ds-alert,整頁最重的色塊每次都在搶視線,講的卻是看過一次就不用再看的事。
互動拍板:可 hover 可點擊。click/tap 是所有裝置都有的路徑(純 hover tooltip 等於手機使用者永遠看不到);hover/focus 只在 pointer:fine 綁。
WCAG 1.4.13 三條全滿足:可關(Esc/點外面)/可 hover(離開延遲 120ms,否則使用者碰不到氣泡裡的字)/持續(不自動消失)。
觸控下命中盒 44、視覺圓圈維持 22(負 margin 讓它在版面上仍只佔 22,標題列不會被撐高)。
稽核紀錄
這裡是所有申請單狀態變更的完整歷程,只能檢視、不能修改。同一筆申請的同一次狀態變更只會留下一筆紀錄。
⚠️ 這不推翻「說明要看得見」:頁面副標留著(縮短成一句定位),收進 i 鈕的是額外補充。
<span class="ds-infotip" data-ds-infotip>
<button class="ds-infotip-btn" aria-expanded="false" aria-label="關於這一頁">i</button>
<span class="ds-infotip-pop" hidden>……</span>
</span>
19. 手機列表卡 List Cards(v1.4.0)
起因是使用者回報「RWD 壞掉」。追下去發現:資料表在手機上只剩「能橫捲」——那不是 RWD,是把問題丟給使用者。
實測 390 寬:時間戳記 152px 吃掉可見區 42%,審核狀態在第 462px,要橫捲一次才看得到自己的申請過了沒,而「主管用手機審核」是這批系統的既定主情境。
判準:窄畫面不是把表格縮小,是換一種資訊結構。表格的前提是「欄位對齊、跨列比較」;手機一次只看得到一筆,比較的前提不成立,欄位對齊的好處歸零、橫捲的成本卻是全額。欄位重新分成四個角色:標題/狀態/摘要/次要資訊。
⚠️ 本組不含 media query,切不切換由採用者決定(有些列表在手機上就是要看全欄)。
REQ-0004
待審核
蝦皮 · 李小美
目前關卡 Jody Lin · 已等 2 天
REQ-0006
已開通
金石堂 OMS · 陳大文
2026-08-11 完成
20. 工具列欄位 Field Inline / 日期輸入的刻意例外(v1.4.0)
.ds-field-inline:工具列裡的「標籤+控件」水平組。來源是專案自己長的 .ffield,在專案裡活了快一個月,使用者一眼就說「這頁元件跟別頁不一樣」。
它不是做錯,是撞到庫沒有的需求——凡是清單頁有進階篩選(日期區間、人名、金額區間)就會再要一次。
⚠️ 與表單的 .ds-field(標籤在上)是兩件事:工具列一列要塞多組篩選,標籤只能在左。
日期輸入=刻意保留原生(拍板)。<input type="date"> 是全站唯一的原生控件,明文記為刻意的例外、不是待修的 bug:
自製日曆選擇器是最難寫對的元件之一(鍵盤/區間/時區/i18n/螢幕閱讀器),而原生在手機上直接叫出系統滾輪、比任何自製版本好用。
它的外框本來就吃 .ds-input,不一致的只有瀏覽器畫的內部。
21. 表格:語意換行 / 欄寬政策 / 黏右操作欄(v1.6.0・v1.7.0・v1.10.0)
v1.7.0 中文不准斷在詞中間(word-break:keep-all)。起因是 RWD 連兩版被推翻,而兩版的量測數字全是綠的——「六欄全見/零橫捲」都成立,畫面卻是爛的。
根因:中文沒有詞界,CSS 預設會在任意兩字之間斷行,於是 陳大文→陳大/文、共用雲端硬碟權限→…硬碟權/限,人名與系統名被腰斬。
⚠️ 它治不了拉丁/數字識別字串:REQ-0006 仍斷在連字號——那是 Latin 斷點,採用端要對這類欄位自己下 nowrap。
方法論教訓(比這條 CSS 重要):scrollWidth − clientWidth = 0 只證明「沒有捲軸」,body{overflow-x:hidden} 會把溢出靜靜吃掉;斷行品質這一層量測完全看不到,只有截圖看得到。
v1.10.0 欄寬政策(拍板):每一欄宣告最小寬度;加總放不下時,在列表區塊內橫捲——不擠壓、不腰斬。
它取代「調斷點/砍欄位」那一類做法:那些都在遷就當下的欄數,而欄數會變。這條把 RWD 從欄數問題變成算術問題。
級距 .ds-col-xs 64/-sm 88/-id 96/-md 120/-lg 180——⚠️ 是建議值不是量測值,採用前對真實資料量一次 min-content。
.ds-col-sticky 黏右操作欄:⚠️ 必須有不透明底、且要覆蓋三種列狀態(一般/hover/勾選),少一種滑過去就有色差斷層。
左緣陰影由 DS.initStickyCol() 依 scrollLeft 掛載——捲到最右時陰影消失(那時黏住的欄剛好落在自然位置、沒蓋住東西,多一條線是騙人的)。
⚠️ 表頭不要另給灰底:實測 thead/tr/th 背景全是 transparent,分界靠 1px 底線。(實作時踩過,黏住的那格會多一條灰帶。)
把視窗拉窄看黏右欄與左緣陰影;捲到最右陰影會消失。.ds-scrollx 本身的左右捲動陰影是 v1.6.0(純 CSS,遮罩層 background-attachment:local 疊陰影層 scroll 相減)——起因是 macOS 覆蓋式捲軸平常不顯示,所以「可以往右捲」在畫面上的事實就是「資料被切掉了」。
22. 應用外殼 App Shell + 錨定選單(v1.9.0)
庫成立以來最大的一塊缺口。三份實作對照抽出(不是憑一份定規格):權限申請 #sidebar(真實專案,有登入態/帳號選單/手機抽屜)+採購審核 .sidebar(第二支獨立 app,對本庫採用率 0)+theme-lab .lab-side(純 token 驅動、深色外框)。
前兩者幾乎逐值相同——216 寬/brand 內距 18·gap 11/mark 32/副標 10.5 uppercase/navitem 內距 10·12+圖示欄 20/計數 mono 11/頭像 30。兩支不同業務的 app 獨立長出同一份規格=通用性沒有疑問。
深色外框是預設(拍板);data-shell="plain" 是白側欄逃生門——它不是降級版,就是那兩份產品實作的現況,採用者可以先平移、之後再切深色,遷移不必一次到位。
⚠️ stacking context(原則 7):側欄是 position:sticky,而 sticky 會建立 stacking context——2026-08-11 踩過的真實 bug 就是側欄的通知面板被主內容蓋住。所以側欄自己有明確的 --z-nav,浮層必須掛在側欄內部;不要掛到主內容再想辦法拉高 z-index。
.ds-menu 錨定式通用選單(.ds-select-menu 是 select 專用,不能當通用 menu):預設往上開,因為它最常見的位置是側欄底部,往下開會開到視窗外。同時只開一個。
外框內距四邊一致 12px(v1.13.1 修正):原本是 0 12px 12px=上面沒有框,側欄與主面板直接貼著視窗頂緣——「外框」在頂邊根本不成立。⚠️ 改這個值要連動側欄的 sticky top(否則捲動時側欄內容會滑進框的頂邊)與側欄/主面板的高度算式;白側欄 plain 則要把 top 還原成 0。
⚠️ 本節用 <iframe> 嵌獨立 demo:.ds-appshell 是整頁版面(側欄 sticky、高度吃 100vh),塞進型錄卡片裡就得覆寫 ds- class 才看得對——那正是庫明文禁止的事。要嵌就嵌真的。
ℹ️ .ds-avatar(帳號頭像)只在上面這個 iframe 裡(showcase-appshell.html 的側欄底部帳號區),本頁沒有獨立展示——它是 app shell 的一部分,拆出來單看沒有意義。它的直徑是刻意的字面值:兩份獨立實作逐值相同,但與 .ds-notif-ico 的同一個數字是同值不同語意,刻意不收成一顆 token(判準見 GUIDELINE「什麼樣的幾何值會變成 token」)。
在上面那個框裡切 plain/深色外框。iframe 是 width:100%,所以拉窄瀏覽器視窗會連帶拉窄 iframe、觸發它自己的 ≤820 斷點——側欄變抽屜。
⚠️ demo 裡的導覽圖示是暫代的文字符號(▤ ✎ ◷ …),不是規範。正式圖示一律取自 Lucide↗在新分頁開啟,見第 23 節。
<div class="ds-appshell"> <!-- 預設=深色外框;白側欄加 data-shell="plain" -->
<aside class="ds-side">
<div class="ds-side-brand">…</div>
<div class="ds-side-cta"><button class="ds-btn ds-btn-cta">+ 新增</button></div>
<nav class="ds-side-nav">
<div class="ds-side-navlabel">作業</div>
<button class="ds-navitem" aria-current="page">
<span class="ds-navitem-ico">▤</span>待我處理<span class="ds-navitem-count">8</span>
</button>
</nav>
<div class="ds-side-foot"> <!-- position:relative =選單的定位基準 -->
<button class="ds-acct" data-ds-menu="acctMenu" aria-controls="acctMenu">…</button>
<div class="ds-menu" id="acctMenu">…</div>
</div>
</aside>
<main class="ds-main">…</main>
</div>
<div class="ds-side-overlay"></div> <!-- ≤820 抽屜遮罩 -->
23. 圖示 Icons(v1.11.0 規範)
所有頁面的圖示一律取自 lucide.dev↗在新分頁開啟(2026-08-13 使用者拍板,跨專案適用)。
在此之前圖示是各憑本事:有的用 emoji、有的用文字符號(▤ ✎ ◷)、有的自己畫 inline SVG——同一個「設定」在兩支 app 裡長得不一樣,而且線條粗細、圓角、視覺重量全對不齊。
為什麼是 Lucide:線條風格與本庫的幾何節奏一致(1.5–2px 描邊、圓端點)、ISC 授權可商用、有完整的 SVG 原始檔可直接內嵌。
怎麼用:從 Lucide 複製 SVG
內嵌到 HTML(
<svg>),不要用 icon font、也不要連外部 CDN——prototype 必須單檔零外部資源(Artifact 的 CSP)。
取色一律 currentColor,尺寸由容器決定(庫內
.ds-navitem-ico svg/
.ds-side-mark svg 已定為 18px)。
不要在 svg 上寫死 fill/stroke 顏色——寫死了,深色側欄與淺色側欄就要各準備一份。
純裝飾的圖示加
aria-hidden="true";圖示是唯一內容時(icon 鈕)必須有
aria-label。
⚠️
既有 prototype 裡的 emoji 與文字符號是技術債,不是規範——各專案下次動到那些位置時順手換掉,不必為此專門開一輪。
正確用法
settings(Lucide)內嵌 SVG,stroke="currentColor" → 跟著容器文字色走,深淺底都不用改。
<!-- 從 lucide.dev 複製 SVG,只改三件事:width/height 設 18、stroke 保持 currentColor、加 aria-hidden -->
<span class="ds-navitem-ico">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true">…</svg>
</span>
<!-- 圖示是唯一內容時,aria-label 不可省 -->
<button class="ds-btn-icon" aria-label="通知"><svg …></svg></button>
24. 頁標題區 Page Head(v1.13.0)
內容區的 h1+一句副標。h1 是 inline-flex 不是 flex——用 flex 的話 h1 會佔滿整行,掛在標題旁的說明鈕會被推到頁面最右邊去。
≤640 時標題降一階(26→19)、副標整個收掉:手機第一屏是稀缺資源(實績:主要按鈕曾被副標推出第一屏)。
⚠️ 上內距歸宿主、不歸元件(兩份實作都這樣做)。用 .ds-appshell 的新 app 若不自己給上內距,26px 標題會直接貼在圓角外殼上緣。
⚠️ 副標以 400 重量借用 --fs-section(13.5),刻意不新開一顆同值 token——兩顆同值的代價是「改一顆忘了另一顆」。
權限申請管理頁面層級的說明放這裡,不要用提示條佔掉一整行。
集中管理各系統的權限申請、審核與開通;此處僅顯示你有權限檢視的單據。
<div class="ds-page-head">
<h1>權限申請管理<span class="ds-infotip" data-ds-infotip>…</span></h1>
<p>一句話說明這一頁是什麼。</p>
</div>
<!-- 宿主每次重繪 page-head 之後要呼叫 DS.init(容器),否則說明鈕沒有行為 -->
25. 中性分類 tag Tag(v1.13.0)
它是「分類」,不是「狀態」。狀態要用 .ds-badge 家族(第 9 節)——終態中性用 .ds-badge-slate。
⚠️ 兩者的底色恰好同 hex,所以「看起來對」不代表用對了:「已入庫/已作廢」是狀態不是分類,別因為顏色一樣就拿 tag 去做。
一定要用 .ds-tags 容器包起來:相鄰兩顆之間的間距靠容器 gap,不靠 tag 自己的 margin。專案端曾因為樣板把兩顆 tag 直接相鄰輸出、中間沒有空白字元,畫面上兩顆黏在一起。
本元件是 v1.13.0 那批裡證據最強的一項:兩份獨立實作解析後六個外觀值 100% 相同,且零缺 token。
分類 tag
金石堂 OMS群組信箱唯讀
對照:狀態
已開通已移除
狀態用 badge,不要用 tag。
<span class="ds-tags">
<span class="ds-tag">金石堂 OMS</span><span class="ds-tag">群組信箱</span>
</span>
26. 兩欄詳情版面 Detail Body(v1.13.0・container query)
主欄+右側輔助欄(時間軸/備註通常住在輔助欄),接在第 16 節的 .ds-detail-head 後面=同一頁的下半身。
🔴 它用 container query,不是 media query——折疊的成因是主欄容器寬,不是視窗寬:同樣 900px 的視窗,側欄 216 或 248、內容內距 28 或 14,主欄實際可用寬差很多。改吃容器寬之後,側欄改寬或內距改動都不會讓斷點失效。
門檻 731 是算出來的不是挑的:輔助欄 --aux-w 292 + 溝寬 20 + 主欄最小可用寬 420 = 732。沿用 v1.10.0「把 RWD 從遷就版面變成算術」的精神。
⚠️ container-type:inline-size 蘊含 contain:layout=會成為 position:fixed 後代的定位基準——所以第 28 節的 .ds-stickybar-pinned(釘視窗底)不可以放在 .ds-detail-body 裡面。
⚠️ 多一層 .ds-detail-cols 是必要的不是贅餘:container-type 只讓後代查詢得到寬度,元素無法查詢自己。
把瀏覽器視窗拉窄就會看到它收成單欄、輔助欄的左框線轉成上框線。
申請內容
主欄:表格與表單住這裡。minmax(0,1fr) 不是 1fr——後者的 min 是 auto,裝寬表格會把整格撐爆。
<div class="ds-detail-body"> <!-- container,不是 grid -->
<div class="ds-detail-cols"> <!-- 真正的 grid -->
<div class="ds-detail-main">…</div>
<aside class="ds-detail-aux">…</aside>
</div>
</div>
27. 精靈步驟條 Steps(v1.13.0)
顏色直接照「進度指示的顏色」那張表,沒有另創一套:已完成=ink 實心+--on-ink 勾、進行中=--cta-ink 空心圈、未達=--line-strong 空心。
進行中不用 ink——ink 是「已完成」的顏色,撞色就分不出進度走到哪。
每一步一個包裹元素(.ds-step),不要把 dot 與 label 平鋪成兄弟節點:換行時會被拆到兩行。
≤640 只留目前那一步的文字。實算:三步約 336px,而 375 手機扣掉內容區左右內距只剩 347px=四步必爆。
⚠️ dot 的 28px 是元件內部尺寸、刻意寫字面值不開 token;也刻意不吃 --h-icon——後者在觸控下會變 44,而步驟條不是點擊目標,跟著放大只會把整條撐爆。
⚠️ 錯誤態 .is-rejected 是比照時間軸補的提議值,兩份來源實作都沒有。
- ✓選系統(已完成)
- 2填權限
- 3確認送出
第 2/3 個系統
<ol class="ds-steps">
<li class="ds-step is-done"><span class="ds-step-dot" aria-hidden="true">✓</span>
<span class="ds-step-label">選系統<span class="ds-sronly">(已完成)</span></span></li>
<li class="ds-step-line" aria-hidden="true"></li>
<li class="ds-step is-current" aria-current="step">…</li> <!-- 目前這一步 -->
<li class="ds-step">…</li> <!-- 未達 -->
</ol>
28. 黏頂/黏底操作列 Sticky Bar(v1.13.0)
兩個用途共用一個底:黏頂=勾選後出現的批次操作列;黏底=精靈頁腳/表單送出列。
黏頂偏移吃 --ds-sticky-top(預設 0)。預設 0 是刻意的:庫目前沒有頂欄元件,「沒有東西擋著」才是預設,給非 0 會逼每個採用者先寫一次覆寫。
覆寫要掛在容器上,不要掛 :root——同一支 app 兩個斷點會有兩種值(頂欄在文件流內時要讓開,改 absolute 退出文件流後偏移就是 0)。
⚠️ 它不是「頂欄高度」:那個情境裡頂欄仍然有高度,但偏移是 0。兩者不可共用一顆 token。
⚠️ ≤820 的 .ds-stickybar-pinned 必須是 fixed 不是 sticky:sticky 的包含塊是外層那張卡,捲過那張卡之後按鈕就停在畫面中間(實測 390 寬捲到頁尾時按鈕落在畫面中段)。因此它也不可以放進第 26 節的容器裡。
⚠️ 空的批次列要連 class 一起拿掉,不要 display:none——留著 class 會留下邊框與陰影的空殼。
🔴 採用前必讀:它的疊層吃 --z-sticky(100)。若專案還沒遷到 .ds-appshell、側欄是自己一套疊層,直接採用會讓手機打開抽屜時這條列浮在抽屜之上。要與 app shell 遷移綁在一起做。
已勾選 3 筆
黏頂:勾選數 chip + 主要動作
已選 3 個系統
<div class="ds-stickybar ds-stickybar-top">…</div> <!-- 黏頂:批次操作 -->
<div class="ds-stickybar ds-stickybar-bottom">…</div> <!-- 黏底:精靈頁腳 -->
/* 有黏頂欄的容器自己餵值;本型錄示範用 position:static 以免蓋住頁面 */
.ds-main{ --ds-sticky-top: calc(6px + var(--h-btn) + 6px + 1px); }
@media (min-width:821px){ .ds-main{ --ds-sticky-top: 0px; } }
29. 通知面板 Notification Panel(v1.13.0)
定位與開關全部複用第 22 節的 .ds-menu + DS.initMenu——往上開、Esc 關、點外面關、同時只開一個,那邊都有了。本元件只補「面板寬度變體 .ds-menu-panel」與內容結構。
⚠️ aria-haspopup 要傳 'true' 不能用預設的 'menu':面板是 role="region"、裡面是一般 button 不是 menuitem,宣告成 menu 是錯的 a11y 宣告。
逐則已讀,禁止「一開面板就全標已讀」;另可附一個手動的「全部標記已讀」。已讀時未讀點轉 transparent 而不是移除——位置保留,整排文字才不會左右跳動。
⚠️ 顏色不是唯一管道:未讀必須另掛 .ds-sronly「(未讀)」。
⚠️ 未讀點的橘是庫成立以來第一個具名的橘色例外(原則 2)——理由是「有事情等你看」正是這顆橘的語意,且它是 7px 的點、不與行動按鈕競爭。這是清單不是通則,要再開第三個落點得先登記。
類型圖示三態(info/ok/reject)來自另一支 app=兩邊合起來才是完整規格:一邊有面板沒分型、一邊有分型沒未讀狀態。
點鈴鐺試試(焦點會被送進面板,關閉時還給鈴鐺)。
<div class="ds-side-foot"> <!-- 定位基準 -->
<button class="ds-btn-icon ds-notif-bell" data-ds-menu="np"
data-ds-menu-haspopup="true" data-ds-menu-focusfirst aria-label="通知">
<svg …></svg><span class="ds-notif-badge">3</span></button>
<div class="ds-menu ds-menu-panel" id="np" role="region" aria-label="通知列表">
<div class="ds-notif-head">通知</div>
<div class="ds-notif-scroll">
<button class="ds-notif-item">
<span class="ds-notif-ico ds-notif-ico-ok">✓</span>
<span class="ds-notif-dot"></span>
<span class="ds-notif-body">
<span class="ds-notif-title">…<span class="ds-sronly">(未讀)</span></span>
<span class="ds-notif-desc">…</span><span class="ds-notif-time">…</span>
</span></button>
</div></div></div>
DS.notifBadge(el, n); // >99 顯示 99+,0 則整顆隱藏
30. 可編輯明細表格 Editable Table(v1.13.0)
「單頭+多明細」是這批系統的共同骨架(採購單、庫存調整、客供建檔)。.ds-table 是唯讀展示表,這是第二種型態。
它疊在 .ds-table 上,不自立表格家族:欄寬政策、黏右操作欄、.ds-num、語意換行全都是 .ds-table 的後代選擇器,自立根節點會一條都拿不到。
儲存格輸入框平常無框、focus 才顯框——否則整張表變成格子監獄。保留 1px 透明邊框佔位,顯框時版面才不會跳。
🔴 錯誤態連 placeholder 一起轉紅。理由不是好看:無框格子裡「必填未填」的畫面上只剩 placeholder,它維持灰色等於錯誤在最需要被看見的那一種情況下不可見。
⚠️ 可編輯表比唯讀表更需要逐欄宣告 .ds-col-*:輸入框 width:100% 讓儲存格的 min-content 塌到近乎 0,不宣告就會整排被壓扁、橫捲永遠不觸發。
⚠️ 窄畫面一律橫捲,不要改用列表卡(第 19 節):那個元件的前提是「一次看一筆、不做跨列比較」,而明細表的存在意義就是逐列填數字+跨列對齊比較。
⚠️ 誠實揭露:無框儲存格這套來自一份從未上線的設計稿(來源專案裡是零使用的死樣式,線上活著的儲存格編輯反而是有框的)。使用者拍板照設計稿做,但第一個採用者請把「看不看得出哪裡可以打字」列入驗收。
試試看:改數量(離開欄位才重算)、按「新增一列」、清空貨號看錯誤態。
品項明細
合計(未稅)
10,000
DS.initEditTable(el, {
compute : r => ({ amt: (+r.qty||0) * (+r.price||0) }), // 衍生格
rowTotal: r => (+r.qty||0) * (+r.price||0), // 合計
validate: (r,f) => f==='sku' && !r.sku, // true=該格 invalid
rowTemplate: () => `<tr>…</tr>` // 新增列的空白列
});
el._ds.addRow() / removeRow(i) / getRows() / recalc();
el.addEventListener('ds-change', e => e.detail.rows / e.detail.total);
/* 三條重算硬規則(都有實測教訓,不要順手優化掉)
① 打字時只改 model+切錯誤態,不重繪整表——重繪會讓游標跳走;重繪時機是 change/blur
② 衍生格與合計用 textContent 就地更新,不重建節點
③ 新增列後把焦點送到新列第一個可編輯格 */
31. 統計/KPI 卡(v1.13.0)
兩個尺寸是同一個元件的兩種嵌入層級,不是兩個元件:白卡版坐在頁面上(--surface+卡陰影),inset 版坐在卡片裡面(--surface-2)——「白卡裡再分一層」正是 --surface-2 被定義出來的用途。
網格用 auto-fit minmax(210px,1fr)=寬度不夠自己換行,零 media query。
數字一律 tabular-nums:大號數字會被逐列比較,比例字寬會讓小數點對不齊。字級走新的數字展示階 --fs-num-lg/md/sm(見第 2 節)——刻意不挪用標題階,否則將來調標題會連帶改到金額。
🔴 深色版的四個硬值全部收進 v1.9.0 既有的 --on-dark* 階梯,零新增 token:主數字原本是純白 → 改 --on-dark(.93),因為大面積深底用純白會過亮起光暈;兩種次要文字(.62/.55)收成同一階 --on-dark-2(.52),實算對比 7.18→5.45 仍遠過 AA。
⚠️ 深色 KPI 卡上先不要放 icon 鈕:.ds-btn-icon 吃 --muted=深壓深、看不見。這是 v1.9.0 就預告的「第二個情境」,根治方案待拍板。
⚠️ sparkline 刻意不升庫:它的高亮 bar 吃 --cta=把橘用在純裝飾上,牴觸原則 2,且只有實驗頁一個來源。
<div class="ds-kpis">
<div class="ds-kpi">
<span class="ds-kpi-label">待開通</span>
<span class="ds-kpi-value">8<small>件</small></span>
<div class="ds-kpi-foot"><span class="ds-kpi-delta">較上週 +2</span></div>
</div>
<div class="ds-kpi ds-kpi-dark">…</div> <!-- 深色特色版 -->
</div>
<div class="ds-kpis ds-kpis-inset"> <!-- 卡片裡面的小格版 -->
<div class="ds-kpi ds-kpi-inset">…</div>
</div>
Changelog
v1.17.0(2026-08-28)— 三顆新 token(--hit-min/--ds-badge-h/--sp-48)、兩個新尺寸變體(.ds-btn-sm/.ds-select-md)、tokens.json 新增 controlScale 對映區塊。起因=下游 hhg-procurement issue #1 的實測回報(41 個輸入呼叫點+35 個按鈕呼叫點)。
🔴 外觀變動一共兩處,都是 +2px:.ds-empty padding 收斂到 --sp-48(階梯外髒值)、.ds-stat-chip 改吃 --h-seg(修復:那個寫死的值就是 v1.13.1 以前的 --h-seg,v1.14.0 升階時它沒跟上,庫內部已裂成兩種)。其餘所有改動 computed 值零變動。
.ds-select-menu 的捲動上限改寫成 calc(7 * var(--hit-min))=「可見 7 列」的政策,不再是一個固定數字。.ds-timeline-dot 與 .ds-infotip-btn 的手算衍生值改成元件內部變數+calc(改一處漏一處就偏心,而且不會報錯)。
散文去數值:components.css 註解裡引用當前值的括號數字全部刪掉、只留 token 名——v1.14.0 全階升級時它們漂了一整階,下游照註解對齊會對到已經不存在的高度。同批補齊七處「刻意字面值」的理由註解。
產生器 build-tokens.mjs 進 repo(breakpoints 由寫死名單改動態掃、新增出廠檢查 A/B/C);新增 .github/workflows/release-on-tag.yml,每個 tag 自動開 GitHub Release。⚠️ 請把本 repo 的 Watch 設成 Custom → Releases,否則收不到通知。
v1.16.0(2026-08-25)— 觸控回饋:33 條 :hover 全數補上 :active。token 零變動、桌機外觀零變動。🔴 起因是結構性缺陷:改動前庫內 :hover 33 條、:active 0 條,等於在觸控裝置上把 hover 當成唯一的點擊回饋來源——而「主管用手機審核」是這批系統的既定主要情境。Tailwind v4 把 hover: 包進 @media (hover: hover),遷到形態乙之後這個缺陷會從「舊 CSS 剛好蓋住了」變成「使用者按下去沒有任何反應」。✅ 這也是 Tailwind 官方的建議(treating hover functionality as an enhancement, and not depending on it)。做法=X:hover{…} → X:hover,X:active{…},:active 複用 :hover 的宣告:桌機幾乎無感(滑鼠按下時本來就在 hover 狀態),觸控獲得「按下亮、放開消失」的正確回饋且沒有 sticky hover 殘留。⚠️ 刻意略過 1 條(日期輸入的 ::-webkit-calendar-picker-indicator opacity 微調,觸控上沒有意義)。
v1.15.0(2026-08-24)— 斷點由 3 階改 4 階:新增 992,原 --bp-lg(1200) 更名為 --bp-xl,--bp-lg 改為 992。唯一的 token 變動,元件層零變動、外觀零變動。
收 992 的依據=開發端 2026-08-17 建議書附的 4 處真實轉折(表頭 grid 進 4 欄/欄位卡分隔線/登入頁直排切橫排/系統圖頁單欄切兩欄+344px 輔助欄)。🔴 為什麼是改名不是加一顆:走 Tailwind 之後 token 名直接變成 variant 名(--bp-lg → lg:),開發端既有的 lg: 期望 992;保留 --bp-lg=1200 會讓他們既有的 lg: 靜靜變成 1200 才觸發,992–1200 這段版面無聲壞掉。✅ 改名衝擊實測=零(庫內對 --bp-lg 零引用,1200 從未出現在任何 @media)。⚠️ 992 目前在庫內零使用者,它是為下游而存在的常數。推翻原則 8「斷點只用三個」。
v1.14.0(2026-08-17)— warm 主題併入 tokens.css 成為庫預設:分發前的最後免費時機(分發給開發 repo 後,改預設主題的成本從「一個 prototype 重 sync」變成「四個 repo 各驗一次」)。41 顆 token 分三群改值:①暖化的中性色與 CTA 淺階 17 顆(含 --cta-ink 改用官方深階 #C96A00,⚠️對比未達 AA,caption 用途例外接受)②陰影與遮罩 4 顆(單層大陰影取代多層小陰影)③幾何 20 顆——圓角全階升一階(--r-btn 8→12、--r-card 14→18 等)、控件高度全階升一階(--h-row 54→58 等)。恆等式複算:滑鼠 34+2×4=42、觸控 40+2×2=44,皆成立。⚠️ --r-fieldcard 升階後與 --r-input 同值(皆 14),圓角階梯在此少一階,照拍板值進、列為待觀察不自行修正。⚠️ 對唯一採用者(權限申請 prototype)不是 no-op:下次 sync 圓角與控件高度會變。
v1.13.1(2026-08-14)— App shell 深色外框補上內距,四邊一致(見第 22 節):padding:0 12px 12px → 12px。原本側欄與主面板直接貼著視窗頂緣,外框只有三邊——「外框」在頂邊根本不成立;三份實作對照時照抄了這個值,沒人問過它為什麼缺一邊。⚠️ 連動三處:側欄 sticky top 0→12(否則捲動時內容會滑進框的頂邊)、側欄高度與主面板最小高度各扣 24(上+下)。⚠️ 白側欄 data-shell="plain" 必須把 top 還原成 0,否則上方會多一條沒來由的縫——同 v1.9.0 的教訓:加了一條情境覆寫,就要把它會遇到的每個情境都走一次。
v1.13.0(2026-08-14)— backlog 的 P1 一次結清八項(見第 24–31 節):頁標題區/中性 tag/兩欄詳情版面/精靈步驟條/黏頂黏底列/通知面板/可編輯明細表格/KPI 卡。同批新增 --fs-num-lg/md/sm+--ls-num、--aux-w、--ds-sticky-top。本輪通則(拍板):原實作的值與階梯對不上時一律收斂到階梯,唯一採用者的 1–2px 位移直接吸收。兩欄版面改用 container query——折疊的成因是主欄容器寬不是視窗寬,門檻 731 是算出來的(292+20+420)。⚠️ container-type 會成為 fixed 後代的定位基準 → 釘底列不可放在它裡面。升庫前的查證推翻了 backlog 三處前提:可編輯表的「無框儲存格」從未上線(死樣式)、.itbar 是收合不是新增列、兩組同名 class 不是雙實作背書(一組是複製、一組是假同源)。瀏覽器實測抓到三個真 bug:①DS.init() 的自動掛載把帶 opts 的顯式呼叫早退擋掉 → 合計顯示 0 而不是 10,000(修法:已初始化也要合併新 opts,只是不重複綁事件)②通知項的標題/描述/時間是 span=inline,全擠在同一行(補 display:block)③forEach(DS.initX) 會把 index 當第二引數傳進來,opts 必須做型別檢查。原則 2 開了第一個具名橘色例外:通知未讀點(同輪擋下 sparkline 的橘)。
v1.12.0(2026-08-14)— 修 .ds-scrollx 貼齊卡片時把卡片上圓角蓋掉(使用者回報「table 左上右上的圓角消失了」)。通則:凡是給捲動容器加不透明背景的地方,都要問「它會不會貼到某個圓角」——這與 v1.10.0 sticky 欄「必須有不透明底」是同一個 trade-off 的兩面:不透明底解決了透出,就會製造遮蓋。
v1.11.0(2026-08-13)— 圖示來源定為 Lucide(lucide.dev),跨專案適用(見第 23 節)。在此之前是各憑本事:emoji/文字符號/自畫 SVG 並存,同一個「設定」在兩支 app 裡長得不一樣。取色一律 currentColor、內嵌 SVG 不連 CDN(prototype 必須單檔零外部資源)。既有 prototype 裡的 emoji 是技術債不是規範,下次動到那些位置時順手換。
v1.10.0(2026-08-13)— 表格欄寬政策進庫(見第 21 節):每欄宣告 min-width、放不下就在列表區塊內橫捲、操作欄 sticky right:0。取代「調斷點/砍欄位」——那些都在遷就當下的欄數,而欄數會變。實作時踩到:初版另給表頭灰底,但實測 thead 背景是 transparent,結果黏住的那格多一條灰帶(已修)。
v1.9.0(2026-08-13)— App shell 進庫=.ds-appshell 家族(見第 22 節),backlog 唯一的 P1↑ 結案,連帶結清錨定式通用選單 .ds-menu。三份實作對照抽出;深色外框設為預設、data-shell="plain" 為逃生門;深底中性色階梯 6 顆 token(--on-dark 家族);.ds-btn-icon 深底不可用改為只在 framed 下覆寫。實作時踩到:那條覆寫在 ≤820 沒還原,而抽屜是白底 → 52% 白的圖示壓在白抽屜上整顆看不見(已修,並把 specificity 拉到 (0,4,0) 而非依賴規則順序)。
v1.8.0(2026-08-13)— 把庫裡最後 18 處寫死的 #fff 收成 token(--on-ink/--on-cta/--on-danger/--on-green +材質組 --focus-halo/--switch-off/--switch-thumb),視覺 no-op。淺色派看不出問題是巧合不是規則:--ink 一旦變亮就是白字壓亮底,而主題層覆蓋不了寫死值。連帶修一個沉睡的 bug:填色鈕的 spinner 是 currentColor 畫的,--on-cta/--on-danger 分家那天會消失在自己的底色裡 → 補兩條分色規則。
v1.7.0(2026-08-13)— .ds-table 中文不准斷在詞中間(word-break:keep-all,見第 21 節)。方法論教訓:斷行品質這一層量測完全看不到,只有截圖看得到。
v1.6.2(2026-08-12)— 修 .ds-badge 真 bug:補 white-space:nowrap。badge 高度寫死 22px,文字一換行就溢出膠囊。⚠️ 這條修正稍早已寫進 css 卻漏了 changelog=無聲上車,本筆是補登記——多 session 共寫時無聲改動最難追。
v1.6.1(2026-08-12)— .ds-search-ico 字級 14→16(使用者裁決)。刻意不吃 --fs-app:那顆的語意是 app shell 的品牌名/導覽圖示,借給輸入框內的圖示會讓一顆 token 同時代表兩種角色。
v1.6.0(2026-08-12)— .ds-scrollx 補捲動陰影(純 CSS)。起因是真實回饋:使用者看到表格右側被切斷、回報「RWD 壞掉」——技術上「可以往右捲」,但 macOS 覆蓋式捲軸平常不顯示,所以畫面上的事實就是「資料被切掉了」,使用者的判斷完全正確。⚠️ 這只是補救不是解法:真正放不下的清單要換資訊結構(.ds-listcards)。
v1.5.2(2026-08-12)— 字級階梯新增 --fs-app 16/--fw-app 700(app shell)。14 撐不起 app 名、17.5 是面板標題的語意,借用會讓那一階同時代表兩種角色——那正是階梯開始失效的起點。
v1.5.1(2026-08-12)— a.ds-btn 歸零底線+新增 .ds-ext-ico(外部連結 ↗)。導向別的網址一律用 <a>、不要 <button>+window.open:中鍵開新分頁、右鍵複製連結、螢幕閱讀器的「連結」語意全部免費。
v1.5.0(2026-08-12)— 新增 .ds-infotip 說明鈕(見第 18 節)。由使用者一句分型帶出,解掉一個我們自己稽核沒抓到的結構問題:提示條其實有兩種(說明型/狀態型),而它們被混用同一個元件。
v1.4.0(2026-08-12)— 由 prototype 驗收回饋帶出,三項全是使用者一眼看出、而我們自己稽核沒抓到的問題。①.ds-listcards 手機列表卡(見第 19 節)②.ds-field-inline 工具列欄位(見第 20 節)③日期輸入=刻意保留原生(拍板)。共同判準:使用者說「這裡怪怪的」,追下去往往不是那一頁做錯,而是庫沒有這個東西、於是各自長了一個。
v1.3.0(2026-08-11)— 三件事:補 P0 缺口/把 RWD 收回庫/收掉鏡射副本。①三個新元件:.ds-alert 提示條、.ds-timeline 狀態時間軸、.ds-detail-head + .ds-kv(把專案內並存的兩套鍵值版面收斂成一組,並寫明「掃描用 vs 閱讀用」)②觸控目標改由 @media (pointer: coarse) 改 token——決定因素是輸入裝置不是螢幕寬度,專案端從此一行都不用寫;連帶 .ds-tabs 補 flex-wrap、新增 --h-icon/--ds-tabs-pad、.ds-check-cell 定義命中盒與視覺尺寸分離 ③兩個可搜尋下拉 .ds-combobox/.ds-multicombo(不新增任何幾何值;定死「不做虛擬捲動、前 50 筆」與「trigger 最多 3 chip 不長高」),順手結清 invalid 態與程式 setter(DS.setValue/DS.getValue)兩條 backlog ④收掉三筆鏡射副本:.ds-table 拆互動列變體+.ds-num、.ds-toast 補定位與進出場+DS.toast()、唯讀對話框 DS.view()(是作業型的一個模式,不是第三種型態)⑤tokens 補 --surface-2/--accent2-weak/--z-toast ⑥規範:多選決策改依真實選項數三分+「示意資料不能當作選元件的依據」、原則 7 補 stacking context 陷阱(position:sticky 也建立 context)。
v1.2.0(2026-08-11)— 補元件庫最大的結構性缺口:原本只有「控件」、沒有「裝控件的東西」。①對話框拆成兩種型態(確認型 .ds-modal /作業型 .ds-dialog,拍板不收斂成一套)②版面容器進庫:.ds-card 家族/.ds-toolbar/.ds-scrollx/.ds-empty ③文字工具類:.ds-link/.ds-hint/.ds-mono/.ds-sronly ④忙碌態:.ds-btn.is-loading + .ds-skeleton ⑤--z-* 七階疊層與三個斷點常數,取代散落的裸數字。
v1.1.0(2026-08-11)— 四項規格漏洞修正:①表格列 hover 改吃 --hover-bg(原純黑 6% 疊層=全站唯一不沾主色的互動態)②.ds-tabs 軌道內距 3→4,容器 38→40=與搜尋框/按鈕同高(38 原本不在高度階梯上)③新增 .ds-input-md(40)給工具列/篩選列的輸入④.ds-dashed-btn 補 height 36(原本由 padding 撐成 22)。新增規範:同一列的控件只准有一種高度。
v1.0.0(2026-08-07)— 首發:以 OMS /mapping 盤點為幾何/交互基準、權限申請 prototype 色票為色彩基準。拍板:選單白底、選中一律 ink、橘為稀有 CTA、破壞性切換掛守門 modal。