版本紀錄
v1.18.0每一版 token/元件/行為的變動都在這裡,同步發布於 GitHub Releases ↗。正本是 releases/*.md。
v1.18.0
2026-08-29這一版的主體是元件層與 registry,tokens.* 零改動。 git diff v1.17.0..v1.18.0 -- tokens.json 只有一行:"version": "1.17.0" → "1.18.0",其餘逐字相同(已逐字驗過)。⇒ 又一個「token 層零變動、元件層有變動」的版本,只看 tokens.json 的 diff 會以為什麼都沒發生。
兩個 bug 修復(唯一會動到既有採用者的部分)
1. 🔴 data-shell="plain" 的抽屜在手機上會鎖死整頁
≤820 段的抽屜規則寫成合併選擇器 .ds-side,.ds-appshell[data-shell="plain"] .ds-side,對 plain 命中的是 (0,3,0),勝過 .ds-side.is-open 的 (0,2,0) ⇒ plain 的專案在手機上按漢堡鈕,body 會鎖住捲動、遮罩會蓋上來,但抽屜不會出現(只能按 Escape 或點遮罩逃出來)。
| selector | 動作 |
|---|---|
.ds-appshell[data-shell="plain"] .ds-side.is-open{transform:translateX(0)} | 新增((0,4,0),與同段 icon 鈕覆寫同一個手法:多帶一層把 specificity 拉上去,不靠「後面的規則贏」這種順序巧合) |
@media (prefers-reduced-motion:reduce) 段的 .ds-side{transition:none} | 改成 .ds-side,.ds-appshell[data-shell="plain"] .ds-side{transition:none}(同一個合併選擇器的第二個後果:還原規則原本只有 (0,1,0),對 plain 的 (0,3,0) 無效 ⇒ plain 的抽屜在 prefers-reduced-motion 下動畫照跑) |
受影響族:appshell(.ds-side 的 ≤820 抽屜態)。其餘族零改動。
⚠️ 這兩個缺口為什麼活到 v1.17.0:docs/behavior/appshell.md §7 的 29 條驗證紀錄是在 framed 下做的(⚠️ 29 是那份紀錄跑出的斷言數,下表的「26 步」是規格的編號步驟數 SIDE-I1…SIDE-R7,兩個計數不衝突),而型錄裡 plain 的切換鈕與抽屜是兩個獨立的示範,沒有人在 plain + 窄畫面下按過漢堡。
🔴 更值得記的是:這一族的行為契約全部照樣通過(class 有加、body 有鎖、焦點有進去)——因為它們量的是 JS 的可觀測行為,而破的是 CSS 的 cascade。⇒ 兩層要一起看,單驗行為層驗不出這種缺口。
影響面:採用登記表寫著「採用者可以先用 plain 平移過來、之後再拿掉屬性切到深色」——這條遷移路徑在 v1.18.0 之前不可用於行動裝置。目前唯一的 app shell 採用者 v1.14.0 已直接上 framed,零現存受害者。
2. infotip 初始狀態
DS.initInfotip 初始化時補一次 tipSet(root, false)(+5 行,其餘行為零變動)。原本開關狀態完全由互動時的 tipSet 寫入 ⇒ 採用端漏寫 hidden 的 markup,氣泡在第一次互動前永久展開,且 aria-expanded 與實際狀態不一致。
用 tipSet 而不是直接寫 pop.hidden,因為 aria-expanded 與 hidden 必須一起壓;root._dsTip 冪等守門仍在最前面 ⇒ 宿主重複呼叫 DS.init(容器) 不會把已開啟的氣泡關掉。showcase.html 三處範例 markup 同步補 hidden/aria-expanded="false"。
registry:九族換底完成,從 2 個品項變成 11 個
v1.17.0 的 registry.json 只登記 ds-select/ds-button,另外九支 .jsx 在目錄裡但沒有登記 ⇒ shadcn build 產不出 r/*.json、npx shadcn add 拿不到東西。本版一次補齊:r/ 由 3 個檔變 12 個(11 個品項 + registry.json),逐檔確認 content 內含 DS_VERSION = 'v1.18.0'。
| 品項 | 檔 | 遷移規則數 | 樣式層驗收 | 行為層驗收 |
|---|---|---|---|---|
ds-button | button.jsx | 30 | 281,792 次/白名單 7 筆 | 無行為層 |
ds-input | input.jsx | 26 | 365,568 次/白名單 3 筆全 platform | 無行為層 |
ds-badge | badge.jsx | 7 | 68,544 次/白名單 1 筆 | 無行為層 |
ds-tabs | tabs.jsx | 6 | 159,936 次/白名單 3 筆 | vanilla 無行為層;React 的選中狀態管理是新增便利層 |
ds-segmented | segmented.jsx | 14 | 144,704 次/白名單 3 筆 | 26 步 × 2 側 = 52/52 |
ds-combobox | combobox.jsx | 21(+23 條 select 族沿用) | 1,148,112 次/白名單 2 筆 | 82 條契約,81/83 列通過——未通過的兩列是 CB-I9(結構上不適用)與 CB-Q12(等值優先刻意不通過,見待拍板第 6 條) |
ds-table | table.jsx | 35 | 2,208,640 次/白名單 5 筆/sanity 13/13 | sticky-col 14 步,14/14 兩側量測值逐字相同 |
ds-dialog | dialog.jsx | 22 | 913,920 次/白名單 8 筆全 platform | 46 步,通過 45/不適用 1(CFM-I8 是規範性條目)/未通過 0 +邊界情境 6 條 × 2 側全過(不計入 46) |
ds-appshell | appshell.jsx | 61 | 411,264 次/白名單 5 筆/sanity 42 條 | 抽屜 26 步 × 2 側 = 52/52 |
ds-menu | menu.jsx | 14 | 297,024 次/白名單 3 筆/sanity 24 條 | 選單 29 步 × 2 側 = 58/58 |
合計 236 條規則(去重:ds-combobox 只計自己的 21 條,那 23 條 select 族沿用已計在 ds-select 的 25 條裡)、5,999,504 次 computed 屬性逐一比對、白名單外差異 0、結構不一致 0、console 錯誤 0、compare.mjs 全族 exit 0。 ⚠️ 上表的白名單欄不要相加:badge/tabs/segmented 三族共用同一組 3 筆族設定白名單,欄位裡的 1/3/3 是各族的命中筆數。去重後全庫共 36 筆。Tailwind 表達不了的規則:0 條(九族逐族確認,含四層 background 簡寫、:has()、::before、word-break:keep-all、position:sticky、四個 ::-webkit-*)。
跨品項依賴宣告了兩條(不宣告的話 add 單獨一個會落地一支 import 不到來源的檔):
ds-combobox→@hhg/ds-select(combobox.jsx從./select取cx/ROOT/TRIGGER/OPTION/useFlipDirection/useDismiss/closeAllExcept)ds-dialog→@hhg/ds-button(dialog.jsx的動作列一律用button.jsx的<Button>,不寫死 class)
🔴 .ds-* 的 CSS 一條都還不能刪。族數進度 ≠ CSS 退役進度——本版第五、六、七次成立:.ds-select*/.ds-combobox* 卡在 DS.initSelect/initCombo 仍是 vanilla 唯一實作;.ds-btn* 的阻擋條件已解除(dialog.jsx 改用 <Button>)但仍不能刪(components.js 本體與 prototype/prototype.html 的鏡射副本仍在產生 ds-btn markup);.ds-table 整族是 .ds-etable 的後代選擇器基座;.ds-menu 是通知面板族的定位與開關基座;.ds-input* 被 initCombo 產生的搜尋框用著。⇒ 排期要分開追蹤「元件遷移完成」與「CSS 可退役」兩條進度。
⚠️ .ds-etable(12 條)刻意不在本版範圍:它疊在 .ds-table 上,核心是 DS.initEditTable 的重算狀態機,而那支與對話框族耦合。本版遷的 35 條表格規則就是它的地基。
select 選單向上展開 — 結案(僅 React 層)
採用登記表自 v1.2.0 掛著的最後一條未結清項。registry/hhg/select.jsx 新增自動翻轉 + forceDirection 覆寫,行為契約寫成 SEL-O8–O13。門檻讀選單自己的 computed max-height(calc(7 * var(--hit-min)) = 308)+6px 間距 = 314px;兩側都不夠時取空間大的一側、不縮 max-height;只在開啟當下算一次。
components.css 零改動 ⇒ 過渡期兩層不一致是刻意的,vanilla 採用端看不到這個行為。
🔴 順手拔掉一個殘留:select.jsx 的選單裡原本寫死一個 <div>分組標題</div>(等值量測樣本殘留),任何用 Select 的頁面都會多出這一列。改成 opt-in 的 groupLabel prop(預設 null =不渲染)。
填色鈕 hover/active 統一(僅 React 層)
filter:brightness(1.08) 是以黑為原點的乘法,--ink #1B1D22 跑它只得到 OKLCH ΔL 0.0090(實測)=看不出來。把參考點換成白端,退化就消失。
⇒ React 版統一為 background: oklch(from <底色> calc(l + (1 - l) * 0.18) c h)(明度往白端推進剩餘空間的 18%,彩度、色相與前景色都不動)。係數 0.18 是校準值:讓 primary 落在 --ink-hi 明度上實測需 0.1843 ⇒ 庫唯一手調過的那顆 hover 成為公式錨點。
範圍=filled 三變體(primary/danger/cta);secondary/ghost/icon 不是填色鈕,逐屬性照搬。vanilla 一律不動,差異逐筆進遷移白名單(W1–W4)。
⚠️ 代價要講清楚:三個 filled 變體的 hover 與 vanilla 型錄不同,這是刻意的,不要當成 bug 回報。
新文件
docs/behavior/(新目錄,12 份 + README 索引)——components.js的框架無關行為規格:觸發條件/量測時機/狀態機/鍵盤操作表/已知矛盾,格式照DS.initEditTable那三條硬規則規格化。每族至少一輪實測驗證,實測發現五個原始碼註解與 GUIDELINE 都沒寫的行為,已逐一寫進規格。
🔴 這批規格是本版九族換底的行為層驗收基準——segmented-pills.md26 步、combobox.md82 步、dialog.md46 步、appshell.md26 步、menu.md29 步、sticky-col.md14 步都是照它逐步核對的。docs/adoption/procurement.md——採購審核系統的採用策略。migration/(工作台)——可重跑的等值驗收:families/<族>.mjs(矩陣+白名單)/build.mjs/compare.mjs/behavior/<族>.mjs/lib/。compare.mjs的 exit code 就是驗收結果(0 =白名單外零差異、零結構不一致、sanity 全過),CI 可以直接吃。- 網站三頁:
/prototype、/changelog(build 時讀releases/*.md,不手抄)、/roadmap;/components頁為本版九族補上可互動 demo。
產生器:build-tailwind.mjs 收斂成一份
repo 根新增 canonical 版 build-tailwind.mjs;site/scripts/sync-ds.mjs 原本自帶一份 88 行的同規格實作(檔內註解自承「那支產生器尚未進 repo」),本版刪掉改 import root 版 ⇒ 同規格兩份實作結清,README.md 那條「生成器還沒進 repo」欠條的最後一項清掉。
等值證明是逐字的:以 site 的 attribution 呼叫 root 版、對現行 tokens.json 產出,與收斂前 site 的現行產物 theme.css 逐字比對 0 差異(md5 兩者皆 5f4c14bf03478f3d3fc729b36e86bcfa)。唯一的差別是 generatedBy/source 兩個參數化字串——呼叫端宣告自己是誰,所以產物的出處不說謊。
出廠檢查 6 項(任一不過 exit 1,site build 也會 throw):顆數守恆/護欄 1(產物不得出現 var(--bp-*))/--breakpoint-* 必須字面值/inline 指向的 token 必須真的存在/命名空間內不得重名/--z-index-* 顆數 = 來源 --z-* 顆數。⚠️ 檢查 2/3 掃描前先 stripComments()——產物自己的註解裡就寫著「不可寫 var()」。--selftest 反測由 11 案例擴為 13 案例(11 個映射/護欄案例 + 假陽性防線 + 反向驗證)。
本版新登記的待拍板項(六條)
⚠️ 這些都不是本版引入的,是九族換底逐屬性比對時量出來的既有缺口。 全部有實測數字、全部不在元件層可修範圍(動了會改 token 或改 vanilla 外觀)。
| # | 項目 | 實測 |
|---|---|---|
| 1 | 🔴 --cta + --on-cta 靜止態對比只有 2.66:1 | 13px/500 的文字需 4.5:1。vanilla 是用「hover 時把文字改成 --ink」把它拉到 8.10——那是用狀態去補償靜止態的不合格;統一模式不做這件事(前景色不隨 hover 改變),所以這個既有問題露出來了。選項:改 --on-cta 為 --ink/壓暗 --cta/限定 CTA 只用在大字級。動哪一個都會改 token |
| 2 | 🔴 .ds-link{font:600 inherit/inherit var(--sans)} 是無效宣告 | inherit 不能當 font shorthand 的組件 ⇒ 整條被丟掉,.ds-link 實測 font-weight: 400,全庫的文字連結都不是設計者以為的 600。修法:拆成 font-weight:600;font-family:var(--sans);line-height:inherit;。⚠️ 會改變 vanilla 外觀,屬拍板項不是純修 bug;React 版目前照搬 400 以維持等值 |
| 3 | ⚠️ .ds-input 沒有 [disabled] 規則 | 停用的輸入框與能填的長得一樣(三個 disabled cell 兩側完全等值,且 .ds-input 自己的 background/color 贏過 UA 的 :disabled)。庫內有對照組:.ds-ecell:disabled{background:var(--surface-2);color:var(--muted);cursor:not-allowed} |
| 4 | ⚠️ .ds-input.ds-invalid 沒有連 placeholder 一起轉紅 | invalid 態的 placeholder 兩側都是 --faint。對照組 .ds-ecell.ds-invalid::placeholder{color:var(--red)} 的庫內理由(「空必填欄位畫面上只剩 placeholder」)對 .ds-field 一樣成立 |
| 5 | ⚠️ 三處文字對比未達 WCAG 4.5:1 | .ds-field-error --red on --ground(11.5px/400)= 4.11:1,而它是庫自己指定的錯誤主要文字通道;.ds-req --req-star on --ground(12px/600)= 2.48:1(已 aria-hidden + aria-required,剩下的是低視力視覺使用者);badge 五個 tone 的靜止態只有 slate 達標。通過的三個一併記著免得重算:placeholder --faint on --field-bg 4.66:1、.ds-field-hint 4.74:1、.ds-field-label 6.22:1 |
| 6 | ⚠️ .ds-combobox-meta 的 hidden 無效 | .ds-combobox-meta{display:flex} 蓋掉 UA 的 [hidden] ⇒ 空 meta 仍佔一個 6px gap。修法是補 .ds-combobox-meta[hidden]{display:none},兩層要一起改,否則等值就破了 |
⚠️ 另外兩條 appshell 族的既有缺口本版照搬不修,一併登記在 backlog:≤820 抽屜段有三處前景色沒有還原(framed 抽屜裡導覽項 hover 文字是 93% 白壓在 #FFFAF3 上、帳號鈕 hover 等於沒有回饋、選中計數的字看不見);.ds-navitem/.ds-menu-item/.ds-acct 都不在通用 focus 規則的分支清單裡 ⇒ 焦點環是瀏覽器預設的,而同一份 CSS 裡 .ds-btn/.ds-input/.ds-tab/.ds-option/.ds-page-btn/.ds-dashed-btn 都在清單裡——導覽與選單是鍵盤使用者的主要路徑。
v1.17.0
2026-08-28Token 層
新增 3 顆,零改值、零改名、零移除。既有 125 顆逐字不變。
| token | 值 | 群組 | 為什麼收 |
|---|---|---|---|
--hit-min | 44px | 命中盒(不是視覺尺寸階,新群組) | 觸控最小命中高度(WCAG 2.5.5/2.5.8)。收之前它是庫內六份無名的 44(.ds-option、coarse 的 --h-btn/--h-select/--h-icon、coarse 的 .ds-link、coarse 的 .ds-checkbox/.ds-switch),而原則 10 整段的立論基礎就是這個數字 |
--ds-badge-h | 22px | 控件高度階層(元件層 token,沿用 --ds-tabs-pad 慣例) | 狀態標籤膠囊高。它是 .ds-badge 那條 white-space:nowrap 的前提(高度固定,文字一換行就溢出膠囊),下游已手抄四份 |
--sp-48 | 48px | 間距階梯(第十二階,值即名) | .ds-empty 的 46px 是階梯外髒值(tokens.css 登記的「三個階梯外髒值」漏數的第四個) |
用 token 前要知道的兩個契約:
- ⚠️
--hit-min的契約是「要被手指點到的最小高度」,不是視覺尺寸階。不要拿它當面板高度、間距或圖示直徑,也不要把它跟--h-*並列挑選。 - ⚠️
--ds-badge-h刻意不叫--h-badge、不進--h-*家族。--h-*帶一個隱含契約:pointer:coarse下放大到命中下限。badge 不是控件,跟著放大會撐爆表格列高,所以它刻意不在 coarse 放大清單裡。
⚠️ tokens.css 的 pointer:coarse 區塊仍是字面 44px、不是 var(--hit-min)(保守路線)。理由:tokens.json 的 pointerCoarse 是原樣搬字串,寫成 var() 之後你會拿到字串 "var(--hit-min)"——對值做運算的產生器(Chakra theme 常見)會當場壞掉,而且是無聲的。該區塊「值必須符合 ^\d+(px)?$」的契約自本版起由產生器的出廠檢查 C 強制。
tokens.json 新增第四個頂層資料區塊 controlScale
與 tokens/pointerCoarse/breakpoints 平行,一個都不要漏。來源是新增的手維護檔 component-sizes.json。
"controlScale": {
"rows": {
"form": { "height": "--h-input", "radius": "--r-input", "font": "--fs-body", "members": [...] },
"toolbar": { "height": "--h-btn", "radius": "--r-btn", "font": "--fs-label", "members": [...] },
"dense": { "height": "--h-select", "radius": "--r-btn", "font": "--fs-label", "members": [...] }
},
"frameworkAliases": { "sm": "dense", "md": "toolbar", "lg": "form" },
"notScalable": ["--h-tab"]
}
- 值一律是 token 名而不是數字(數字回
tokens查一次 lookup)。發第二份數字就是第二份事實來源。 - 第一軸刻意是「列情境」而不是 sm/md/lg:原則 4 的判準是「它跟誰同列」,sm/md/lg 在語意上講的是「重要性」。只發三顆別名,工程師仍會按重要性挑,同列多高度不會被杜絕。sm/md/lg 降為
frameworkAliases。 radius與font跟著height一起帶:換階換的不只高度。只對高度會做出「dense 高但 form 圓角」的縫合怪,而它沒有同列參照物、比高度差幾 px 更難察覺。- 🔴 語意反轉要留意:本庫沒有「預設=md」這回事。 無後綴的
.ds-input(form階)才是預設,-md/-sm是變體。你的框架預設多半是md⇒ 照frameworkAliases對映,不寫 size 的欄位會落到toolbar階,而本庫認為它應該落到form階。目前沒出事只是因為呼叫點都顯式寫了 size——下一個沒寫 size 的新欄位會靜靜掉錯階。 - ⚠️
pointer:coarse下toolbar階與dense階會塌成同高(兩顆高度 token 都放大到--hit-min)。這是原則 10 刻意的設計,不是覆寫失效——第一次在 iPad/平板上驗收時不要去「修」它。 - ⚠️
--h-tab刻意不進rows(列在notScalable):它有恆等式綁著(--h-tab + 2×--ds-tabs-pad = --h-btn),不可以當成一個可自由選的高度階直接用在控件上,只能整組用.ds-tabs。 - 駁回下游提的
--h-input-sm/-md/-lg命名:--h-input-sm(38) 與--h-select(38) 同值不同名=第二事實來源,本庫已兩次明文拒絕同值複本。
元件層
新增兩個尺寸變體(純新增,庫內外零既有使用者 ⇒ 既有外觀零影響):
| selector | 階 | 內容 | 結清的欠條 |
|---|---|---|---|
.ds-btn-sm | dense | --h-select/--sp-12/--r-btn/--fs-label | 採用登記自 v1.2.0 掛著的「small button 尺寸」 |
.ds-select-md>.ds-select-trigger | toolbar | --h-btn | 「已知缺 token」表掛了六版的 .ds-select-md |
⇒ 三階矩陣自此閉合。.ds-select-md 用 > 直接子代,避免命中巢在裡面的 combobox/multicombo trigger。
改吃 token/算式/內部變數(computed 值零變動):
| selector | 改動 |
|---|---|
.ds-option | min-height:44px → var(--hit-min) |
coarse 的 .ds-link | min-height:44px → var(--hit-min) |
coarse 的 .ds-checkbox,.ds-switch | min-height:44px → var(--hit-min) |
.ds-badge | height:22px → var(--ds-badge-h) |
.ds-select-menu | max-height:308px → calc(7 * var(--hit-min)) |
.ds-timeline/.ds-timeline-dot/.ds-timeline-step::before | 收成 --dot:16px/--dot-bw:2px,left:7px → calc((var(--dot) - var(--dot-bw))/2) |
coarse 的 .ds-infotip-btn/.ds-infotip-btn::before/.ds-infotip-pop | 三處手算的 22px 收成 --ring(宣告在 .ds-infotip 上) |
.ds-select-menu的政策是「可見 7 列再捲」,不是 308px——308 就是 7 ×.ds-option列高。收成一顆數值 token 會把推導關係固化:列高一改就露出半列,而且看起來還是有 token,比手抄更難發現。⇒ 本庫政策:捲動容器上限一律用「可見列數 × 列高」表達。你自己的 220/400/440 同理(本庫不收那些數值,但請用同樣的寫法)。- timeline/infotip 那兩組原本是手算的衍生值:改圓點大小要同時改兩處、改圈大小要同時改三處,漏一處就偏心/偏位,而且不會報錯。
🔴 外觀變動一共兩處,都是 +2px:
| selector | 舊 → 新 | 性質 |
|---|---|---|
.ds-empty | padding:46px → var(--sp-48)=48px | 收斂。46 是階梯外髒值;依 v1.13.0 已拍板的通則「原實作的值與階梯對不上時一律收斂到階梯,1–2px 位移直接吸收」 |
.ds-stat-chip | height:31px → var(--h-seg)=33px | 修復。31 就是 v1.13.1 以前的 --h-seg 值,v1.14.0 全階升級時它因為寫死沒跟上 ⇒ 統計 chip 與 segmented chip 從此差 2px,庫內部已裂成兩種。v1.14.0 的 Changelog 把它與 .ds-modal-ico(40)、.ds-side-mark(32) 一起判為「巧合的 literal、非漏轉 var」——那個判斷對 40 與 32 成立,對 31 不成立(它在階梯上待過) |
文件層(零 computed 變動,但影響你怎麼引用本庫):
- 散文去數值:
components.css註解裡引用當前值的括號數字全部刪掉、只留 token 名(約 9 處)。起因是實測踩過:v1.14.0 全階升級只改了tokens.css,這些註解漂了一整階 ⇒ 照註解對齊會對到已經不存在的高度階。改法是刪數字、不是換新數字。同樣的處理也做在GUIDELINE.md的原則 4 與原則 10。 - 「刻意字面值」註解補齊七處:
.ds-infotip-btn22/.ds-avatar30/.ds-notif-ico30/.ds-modal-ico40/.ds-side-mark32/.ds-notif-badge17/.ds-notif-dot7。其中.ds-notif-ico原本只寫「不吃--h-icon」、沒寫「那該吃什麼」——那半句話就是下游只能寫裸 px 的根因,現在補完整。 components.css凍結宣告下新增行號警語:本檔行號不穩定,不要當引用錨點;引用規格用 selector 名稱+版本號(例:.ds-badge @ DS v1.17.0),也不要引用散文裡的數值。
交付物與機制
- 產生器
build-tokens.mjs進 repo(結清 README「生成器還沒進 repo」的欠條)。同批三改:- 🔴 breakpoints 由硬編碼三顆名單改為動態掃
--bp-*。寫死名單的坑已實測踩過一次:v1.15.0 加第四顆--bp-xl時靜靜漏掉,而 breakpoints 不在既有檢查①②的比對範圍內 ⇒ 完全沒有紅燈。 - 出廠檢查 A:
controlScale引用的每個 token 名必須存在。B:components.css動態掃尺寸變體 class,必須在controlScale.members或具名例外清單內。C:pointerCoarse每個值必須符合^\d+(px)?$。三道都會exit 1。
- 🔴 breakpoints 由硬編碼三顆名單改為動態掃
.github/workflows/release-on-tag.yml新增:tag push 自動開 GitHub Release。
🔴 請把本 repo 的 Watch 設成Custom → Releases,否則做了你也收不到。v1.14.0~v1.16.0 的 Release 會回填。CONVERSION.md大幅增補:rem/px 規範(本庫全庫零rem,這是刻意的)、同列同高自我檢查法(含可抄的斷言)、「什麼值進 token」三問判準+「本庫收 vs 專案自訂」界線表、捲動上限政策、裸 hex/CSS Module 死角、引用規範、版本落後守門腳本。
⚠️ 通知解決「知不知道」,守門解決「會不會忘」,兩個都要——一封四天前的通知一樣會被漏看。showcase.html:新增.ds-btn-sm/.ds-select-md示例(與同列元件並排、展示同高);補.ds-avatar(在showcase-appshell.html的 iframe 內)與.ds-modal-ico(在確認型對話框內)的展示指引——「在 DS 找不到」有一部分是型錄缺口造成的,這筆帳算本庫的。- 🔴 型錄第 2 節「幾何階層 Scale」在本版之前顯示的是 v1.13.1 的一整組舊值(圓角 18/16/14…、高度 46/40/36/31/26、頁標題 26)——v1.14.0 全階升級只改了
tokens.css,那一節是硬寫的所以沒跟上。如果你曾照型錄第 2 節對齊過幾何,請重新對一次。 本版把整節改成吃var(--token)+ 現值由getComputedStyle動態填,並補上原本缺席的--h-icon/--h-row/--h-bar/--hit-min/間距階梯(階數動態探測,--sp-48自動出現)⇒ 從此不可能再漂。第 10 節表格說明的舊值(radius 14/列高 54/分頁鈕 40×40/命中盒 40·觸控 44)同批改成 token 名。
對下游 issue 的回覆對應
本版落地的是 hhg-procurement issue #1 建議五~八的裁決。三件不收的事也一併明文(判準與逐項裁決表見 GUIDELINE.md 的「什麼樣的幾何值會變成 token」):
- 不為裝飾/指示器尺寸開新 token 家族(30/40/32/17/7 等):判準三條皆不滿足,且
--h-*的 coarse 契約排除它們 ⇒ 元件內部字面值+「刻意」註解。 - 不收捲動上限的數值(260/168/420/
min(46vh,…)):五個值回答五個不同的問題,收成一顆是把五個理由壓成一個數字。 - 「序號圓章 24px」本庫查無此物:全文零命中「序號」,最接近的對應物是
.ds-step-dot(28)。若指的是步驟指示請收斂到 28;未讀圓點的 6 同理請收斂到 7(相鄰中間值並存本身就違反原則 4)。
v1.16.0
2026-08-25回填⚠️ 本則是 2026-08-28 回填的。v1.16.0 發布當時 GitHub Releases 是零筆,tag push 本身不發任何通知 ⇒ 下游「期間沒有任何訊號」是精確的描述。
Token 層
零變動。 git diff v1.15.0..v1.16.0 -- tokens.json 只有一行:"version": "1.15.0" → "1.16.0"。
⇒ 這一版是「token 層零變動、元件層有變動」的例子。 只看 tokens.json 的 diff 會以為什麼都沒發生。
元件層:33 條 :hover 全數補上 :active
🔴 起因是結構性缺陷,不是美化:改動前庫內 :hover 33 條、:active 0 條 ⇒ 在觸控裝置上把 hover 當成唯一的點擊回饋來源,而「主管用手機審核」是這批系統的既定主要情境。Tailwind v4 把 hover: 包進 @media (hover: hover),遷到 React+Tailwind 之後這個缺陷會從「舊 CSS 剛好蓋住了」變成「使用者按下去沒有任何反應」。這也是 Tailwind 官方的建議(*treating hover functionality as an enhancement, and not depending on it*)。
做法:X:hover{…} → X:hover,X:active{…},:active 複用 :hover 的宣告。桌機幾乎無感(滑鼠按下時本來就在 hover 狀態),觸控獲得「按下亮、放開消失」的正確回饋,且沒有 sticky hover 殘留。
受影響 selector(引用一律用 selector 名稱,不用行號——理由見 CONVERSION.md):
.ds-btn-primary .ds-btn-secondary .ds-btn-ghost .ds-btn-danger .ds-btn-cta .ds-btn-icon
.ds-input .ds-select-trigger .ds-option .ds-tab .ds-seg .ds-pill .ds-page-btn
.ds-dashed-btn .ds-link .ds-ecell .ds-multicombo .ds-infotip-btn
.ds-navitem .ds-acct .ds-side .ds-appshell .ds-menu-item .ds-menu-danger
.ds-notif-item .ds-listcard-clickable .ds-table .ds-table-clickable .ds-row-clickable .ds-col-sticky
⚠️ 刻意略過 1 條:日期輸入的 ::-webkit-calendar-picker-indicator opacity 微調,在觸控上沒有意義。
:active 是新增態,不覆蓋任何既有樣式 ⇒ 桌機外觀零變動。
採用者要做什麼
- 吃
tokens.json的:不必動。 - 吃
components.css的:整檔換掉即可(33 條都是同一種機械改寫)。 - 視覺驗收:型錄
showcase.html已同步 v1.16.0。
⚠️ 一個要更正的既有描述
components.css 的凍結宣告與全檔行號 +24 發生在 v1.15.0,不是本版。本版對行號零影響(git diff v1.15.0..v1.16.0 -- components.css 是 33 加 33 減、淨 0)。⇒ 若你有既有的行號引用,它們是在 v1.15.0(8/24) 就失效的,比一般以為的早一天。
v1.15.0
2026-08-24回填BREAKING⚠️ 本則是 2026-08-28 回填的。v1.15.0 發布當時 GitHub Releases 是零筆,tag push 本身不發任何通知。
🔴 BREAKING:--bp-lg 改名+改值
| 動作 | token | 舊值 → 新值 |
|---|---|---|
| 改值 | --bp-lg | 1200px → 992px |
| 新增 | --bp-xl | — → 1200px(=原本 --bp-lg 的值搬過來) |
⇒ 不是加一顆而已,是改名+改值。 斷點由 3 階改 4 階:640 / 820 / 992(新) / 1200。
為什麼 BREAKING=yes:判準是「下游不改任何一行,畫面或行為會變」,不是「API 有沒有移除」。這一顆是改值,所以它是靜默的——你的產生器重跑之後 lg: 的觸發點就換了,沒有任何編譯錯、沒有任何警告。
為什麼要改名而不是加一顆:走 Tailwind 之後 token 名會直接變成 variant 名(--bp-lg → lg:),而開發端既有的 lg: 期望 992。若讓 --bp-lg 留在 1200,你們既有的 lg: 會靜靜變成 1200 才觸發 ⇒ 992–1200 這段版面無聲壞掉。
改名時的實測:庫內對 --bp-lg 的引用=0(1200 從未出現在任何 @media,庫內 @media 全寫死 640/820),兩個下游 prototype 也只用 --bp-md ⇒ 庫端零風險,風險全在下游的 Tailwind variant 名。
992 這一階目前在庫內零使用者——它是為下游而存在的常數,不要以為漏改。收 992 的依據=開發端 2026-08-17 建議書附的 4 處真實轉折(表頭 grid 進 4 欄/欄位卡分隔線/登入頁直排切橫排/系統圖頁單欄切兩欄+344px 輔助欄)。
⚠️ 不要改回三階、也不要把 --bp-lg 改回 1200。
⚠️ CSS 的 @media 不吃 custom property(@media (max-width:var(--bp-md)) 無效)。--bp-* 是規範用的常數表,不是可引用的變數——寫 media query 時照抄數字。轉到 Tailwind 的 screens 或 Chakra 的 breakpoints 時要對應過去,不要期待能 import 一個變數。
元件層
components.css +26 行,全部在檔頭註解,零 selector 規則變動:
- 🔴 加入凍結宣告:本檔進入凍結狀態、逐族退役。元件層拍板走「形態乙」——改寫成 React 元件、樣式用 Tailwind v4 utility 寫在元件裡,透過 shadcn registry 分發。本檔自此不再是元件層的事實來源;已遷移的族其規則會被刪除。不要在本檔新增元件族;要改既有未遷移族的樣式仍改這裡。
- 補正檔頭版本號
v1.3.0→v1.15.0(漂了十二版)。
⚠️ 這一版讓 components.css 全檔行號 +24,所有既有行號引用一次全失效,而且看起來仍然有效——原本指按鈕 transition 的第 21 行,升版後指到「本檔對應那幾族的規則會被刪除」這一句完整、可讀、但意思無關的話。 ⇒ 引用本庫規格請改用「selector 名稱+版本號」(例:.ds-badge @ DS v1.15.0),不要用行號。
同批新增的文件(非 token/非元件)
GUIDELINE.md新增「元件層導入:Tailwind v4 + shadcn registry」整節(含拍板結果、形態乙的定義、遷移成本實測、三條實測踩出來的護欄、疊層階梯對照表與遷移驗收)。showcase.html/showcase-appshell.html/GUIDELINE.md/README.md首次補齊到 repo(在此之前 repo 上只有 token 層與元件層,視覺驗收基準完全沒有)。
採用者要做什麼
- 重跑產生器拿到新的
breakpoints(四顆)。 - 🔴 手動確認一次你的
lg:/breakpoints.lg現在指到 992 而不是 1200,並檢查 992–1200 這個區間的版面。這一步不能靠 diff——你的 theme 檔是生成的,舊值在你那邊已經被覆蓋掉了。 - 吃
components.css的:讀凍結宣告,並把既有的行號引用改成 selector+版本號。
v1.14.0
2026-08-17回填BREAKING⚠️ 本則是 2026-08-28 回填的。v1.14.0 發布當時 GitHub Releases 是零筆。
ℹ️ 這是本 repo 的初版(tag message:「初版:token、元件層與轉換說明」),也是第一次把
tokens.json/build-tokens.mjs/CONVERSION.md交出去。所以下面的「新增/改值」有兩個視角,分開列。
🔴 這一版的主軸:warm 主題併入 tokens.css 成為庫預設
原本的 [data-theme="warm"] 主題層自此併入 tokens.css 成為預設,不再是可切換的主題。tokens.css 41 顆 token 逐顆改值(不新增第二份宣告、不加覆寫區塊);另有 14 顆 warm 值與原值完全相同、併入無作用。
為什麼是那個時間點:這批元件庫即將分發給 GitHub 上的開發專案吃 token,一旦分發出去,改預設主題的成本就從「一個 prototype 重 sync」變成「四個 repo 各驗一次」——那是分發前的最後一個免費時機。
改值分三群(41 顆)
① 暖化的中性色與 CTA 淺階(17 顆) --ground/--surface-2/--text/--muted/--faint/--disabled-fg/--disabled-fg-2/--line/--line-strong/--line-soft/--disabled-bg/--field-bg/--chip-bg/--cta-weak/--cta-ink/--hover-bg/--nav-sel-bg
- ⚠️
--cta-ink改用官方品牌橙深階#C96A00(原#A45D25是自製的、通過 AA 的值)。官方深階實測未達 AA:對白卡 3.79:1、對暖底 3.53:1(門檻 4.5)。這是 warm 探索階段就記錄且已接受的已知限制,用途仍嚴格限「caption 與小狀態提示」——連結、強調字、控件選中一律維持 ink。 - ⚠️
--surface-2改為#F2F1F1,與--slate-bg(#F1F2F6)從此不再同值。如果你手邊有抄過#F1F2F6當次表面色的地方,它現在是錯的。
② 陰影與遮罩(4 顆) --sh-card/--sh-pop/--sh-menu/--backdrop,從多層小陰影改成單層大陰影(warm 的「柔和」語彙)。
③ 幾何(20 顆)— 這一群是外觀變動的主要來源
| 圓角 | 舊 → 新 | 控件高度 | 舊 → 新 | |
|---|---|---|---|---|
--r-drawer | 18 → 24 | --h-input | 46 → 48 | |
--r-menu | 16 → 20 | --h-btn | 40 → 42 | |
--r-card | 14 → 18 | --h-select | 36 → 38 | |
--r-input | 12 → 14 | --h-tab | 32 → 34 | |
--r-fieldcard | 10 → 14 | --h-icon | 32 → 34 | |
--r-btn | 8 → 12 | --h-seg | 31 → 33 | |
--r-chip | 7 → 10 | --h-chip | 26 → 28 | |
--r-tag | 6 → 8 | --h-row | 54 → 58 | |
--r-check | 2 → 4 | --h-bar | 64 → 66 |
字級:--fs-page 26 → 27、--ls-page -.015em → -.008em。
@media (pointer:coarse)恆等式複算(覆寫值本身不變,只是 base 端起點變了):滑鼠--h-tab(34) + 2×--ds-tabs-pad(4) = --h-btn(42)✓;觸控--h-tab(40) + 2×--ds-tabs-pad(2) = --h-btn(44)✓。- 🔴 待觀察:
--r-fieldcard與--r-input升階後同為 14px,圓角階梯在此少一階(原本 10 vs 12 是相鄰兩級)。這是 warm 原值就有的重疊,照拍板值進,不自行發明新值修正。
元件層
components.css 掃過一輪,沒有找到上述 41 顆舊值被寫死(hex/border-radius/height 皆用 var())。三處巧合的 literal px(.ds-stat-chip height:31px、.ds-modal-ico height:40px、.ds-side-mark height:32px)當時判為「獨立元件的固定尺寸、非漏轉 var 的殘留」,本版未動。
⚠️ 2026-08-28 更正這個判斷:那句話對 40 與 32 成立,對 31 不成立——31 就是 v1.13.1 以前的
--h-seg值,它在階梯上待過,本版升階時它因為寫死所以沒跟上 ⇒ 統計 chip 與 segmented chip 從此差 2px,庫內部已裂成兩種。已在 v1.17.0 改吃--h-seg(+2px,屬修復)。
交付物(同批補登記,token 值零變動)
build-tokens.mjs(產生器)/tokens.json(產物:124 顆 token +pointerCoarse7 顆 +breakpoints)/CONVERSION.md(轉換說明與三個實測踩過的坑)。- 已由 fresh-context agent 獨立驗收:124 顆零漏抓、零幻覺、零值差;
pointerCoarse7 顆不多不少;版本三處一致。 - 同批修掉一個既有缺陷:
tokens.css的「白色當『材質』而非前景」群組標頭被 v1.9.0 插入的一組 token 拆離、描述不到任何東西(機械解析成tokens.json時它成了零顆 token 的空群組才被發現)。純註解移動、零 computed 值變動。
採用者要做什麼
- 初次導入:寫一支產生器讀
tokens.json吐你 stack 的 theme。🔴 不要手抄一次——手抄的那一刻起就是兩份事實來源,我們改版你不會知道。 - 已在用 v1.13.x 的:這一版不是 no-op。圓角與控件高度各升一階,
tokens.json重跑之後外觀就會變 ⇒ 要跑一輪視覺驗收(基準=showcase.html,活型錄、開檔即現值)。 - ⚠️ 注意
pointerCoarse不是可有可無的一組覆寫值。漏掉它,觸控裝置上的按鈕會太小,而且在桌機瀏覽器測不出來。