版本紀錄

v1.18.0

每一版 token/元件/行為的變動都在這裡,同步發布於 GitHub Releases ↗。正本是 releases/*.md

v1.18.0

2026-08-29
TOKENSnone — 新增 0 / 改值 0 / 改名 0 / 移除 0 ← 128 顆逐字不變
COMPONENTSchanged(2 條 selector,都在 ≤820 抽屜段,都是修 bug)
BEHAVIORchanged(infotip 初始狀態 + plain 抽屜修復的行為結果)
BREAKINGno
下游動作吃 components.css/components.js 的:直接補灌即可(兩處都是修復,不需改呼叫端) 走 React 的:npx shadcn add @hhg/ds-<name>,本版起 11 個品項可 add 重跑 token 產生器不是必要的(token 零改動),版號會跟著走

這一版的主體是元件層與 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.0docs/behavior/appshell.md §7 的 29 條驗證紀錄是在 framed 下做的(⚠️ 29 是那份紀錄跑出的斷言數,下表的「26 步」是規格的編號步驟數 SIDE-I1SIDE-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-expandedhidden 必須一起壓;root._dsTip 冪等守門仍在最前面 ⇒ 宿主重複呼叫 DS.init(容器) 不會把已開啟的氣泡關掉。showcase.html 三處範例 markup 同步補 hiddenaria-expanded="false"

registry:九族換底完成,從 2 個品項變成 11 個

v1.17.0 的 registry.json 只登記 ds-selectds-button,另外九支 .jsx 在目錄裡但沒有登記shadcn build 產不出 r/*.jsonnpx shadcn add 拿不到東西。本版一次補齊:r/ 由 3 個檔變 12 個(11 個品項 + registry.json),逐檔確認 content 內含 DS_VERSION = 'v1.18.0'

品項遷移規則數樣式層驗收行為層驗收
ds-buttonbutton.jsx30281,792 次/白名單 7 筆無行為層
ds-inputinput.jsx26365,568 次/白名單 3 筆全 platform無行為層
ds-badgebadge.jsx768,544 次/白名單 1 筆無行為層
ds-tabstabs.jsx6159,936 次/白名單 3 筆vanilla 無行為層;React 的選中狀態管理是新增便利層
ds-segmentedsegmented.jsx14144,704 次/白名單 3 筆26 步 × 2 側 = 52/52
ds-comboboxcombobox.jsx21(+23 條 select 族沿用)1,148,112 次/白名單 2 筆82 條契約,81/83 列通過——未通過的兩列是 CB-I9(結構上不適用)與 CB-Q12(等值優先刻意不通過,見待拍板第 6 條)
ds-tabletable.jsx352,208,640 次/白名單 5 筆/sanity 13/13sticky-col 14 步,14/14 兩側量測值逐字相同
ds-dialogdialog.jsx22913,920 次/白名單 8 筆全 platform46 步,通過 45/不適用 1(CFM-I8 是規範性條目)/未通過 0 +邊界情境 6 條 × 2 側全過(不計入 46)
ds-appshellappshell.jsx61411,264 次/白名單 5 筆/sanity 42 條抽屜 26 步 × 2 側 = 52/52
ds-menumenu.jsx14297,024 次/白名單 3 筆/sanity 24 條選單 29 步 × 2 側 = 58/58

合計 236 條規則(去重:ds-combobox 只計自己的 21 條,那 23 條 select 族沿用已計在 ds-select 的 25 條裡)、5,999,504 次 computed 屬性逐一比對、白名單外差異 0、結構不一致 0、console 錯誤 0compare.mjs 全族 exit 0。 ⚠️ 上表的白名單欄不要相加badgetabssegmented 三族共用同一組 3 筆族設定白名單,欄位裡的 1/3/3 是各族的命中筆數。去重後全庫共 36 筆。Tailwind 表達不了的規則:0 條(九族逐族確認,含四層 background 簡寫、:has()::beforeword-break:keep-allposition:sticky、四個 ::-webkit-*)。

跨品項依賴宣告了兩條(不宣告的話 add 單獨一個會落地一支 import 不到來源的檔):

  • ds-combobox@hhg/ds-selectcombobox.jsx./selectcxROOTTRIGGEROPTIONuseFlipDirectionuseDismisscloseAllExcept
  • ds-dialog@hhg/ds-buttondialog.jsx 的動作列一律用 button.jsx<Button>,不寫死 class)

🔴 .ds-* 的 CSS 一條都還不能刪。族數進度 ≠ CSS 退役進度——本版第五、六、七次成立:.ds-select*.ds-combobox* 卡在 DS.initSelectinitCombo 仍是 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-heightcalc(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.md 26 步、combobox.md 82 步、dialog.md 46 步、appshell.md 26 步、menu.md 29 步、sticky-col.md 14 步都是照它逐步核對的。
  • docs/adoption/procurement.md——採購審核系統的採用策略。
  • migration/(工作台)——可重跑的等值驗收:families/<族>.mjs(矩陣+白名單)/build.mjscompare.mjsbehavior/<族>.mjslib/compare.mjsexit code 就是驗收結果(0 =白名單外零差異、零結構不一致、sanity 全過),CI 可以直接吃。
  • 網站三頁/prototype/changelog(build 時讀 releases/*.md,不手抄)、/roadmap/components 頁為本版九族補上可互動 demo。

產生器:build-tailwind.mjs 收斂成一份

repo 根新增 canonical 版 build-tailwind.mjssite/scripts/sync-ds.mjs 原本自帶一份 88 行的同規格實作(檔內註解自承「那支產生器尚未進 repo」),本版刪掉改 import root 版 ⇒ 同規格兩份實作結清README.md 那條「生成器還沒進 repo」欠條的最後一項清掉。

等值證明是逐字的:以 site 的 attribution 呼叫 root 版、對現行 tokens.json 產出,與收斂前 site 的現行產物 theme.css 逐字比對 0 差異(md5 兩者皆 5f4c14bf03478f3d3fc729b36e86bcfa)。唯一的差別是 generatedBysource 兩個參數化字串——呼叫端宣告自己是誰,所以產物的出處不說謊。

出廠檢查 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:113px/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 自己的 backgroundcolor 贏過 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-hiddenaria-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-metahidden 無效.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-28
TOKENS新增 3 / 改值 0 / 改名 0 / 移除 0 ← 125 → 128
COMPONENTSchanged(受影響 selector 見下)
BEHAVIORnone(components.js 只改檔頭版號)
BREAKINGno
下游動作重跑產生器(拿 3 顆新 token + 新的 controlScale 區塊) + 吃 components.css 的另讀下面那批 selector 的 diff

Token 層

新增 3 顆,零改值、零改名、零移除。既有 125 顆逐字不變。

token群組為什麼收
--hit-min44px命中盒(不是視覺尺寸階,新群組)觸控最小命中高度(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-h22px控件高度階層(元件層 token,沿用 --ds-tabs-pad 慣例)狀態標籤膠囊高。它是 .ds-badge 那條 white-space:nowrap前提(高度固定,文字一換行就溢出膠囊),下游已手抄四份
--sp-4848px間距階梯(第十二階,值即名).ds-empty 的 46px 是階梯外髒值(tokens.css 登記的「三個階梯外髒值」漏數的第四個)

用 token 前要知道的兩個契約:

  • ⚠️ --hit-min 的契約是「要被手指點到的最小高度」,不是視覺尺寸階。不要拿它當面板高度、間距或圖示直徑,也不要把它跟 --h-* 並列挑選。
  • ⚠️ --ds-badge-h 刻意不叫 --h-badge、不進 --h-* 家族--h-* 帶一個隱含契約:pointer:coarse 下放大到命中下限。badge 不是控件,跟著放大會撐爆表格列高,所以它刻意不在 coarse 放大清單裡

⚠️ tokens.csspointer:coarse 區塊仍是字面 44px、不是 var(--hit-min)(保守路線)。理由:tokens.jsonpointerCoarse 是原樣搬字串,寫成 var() 之後你會拿到字串 "var(--hit-min)"——對值做運算的產生器(Chakra theme 常見)會當場壞掉,而且是無聲的。該區塊「值必須符合 ^\d+(px)?$」的契約自本版起由產生器的出廠檢查 C 強制。

tokens.json 新增第四個頂層資料區塊 controlScale

tokenspointerCoarsebreakpoints 平行,一個都不要漏。來源是新增的手維護檔 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
  • radiusfont 跟著 height 一起帶:換階換的不只高度。只對高度會做出「dense 高但 form 圓角」的縫合怪,而它沒有同列參照物、比高度差幾 px 更難察覺。
  • 🔴 語意反轉要留意:本庫沒有「預設=md」這回事。 無後綴的 .ds-inputform 階)才是預設,-md-sm 是變體。你的框架預設多半是 md ⇒ 照 frameworkAliases 對映,不寫 size 的欄位會落到 toolbar 階,而本庫認為它應該落到 form。目前沒出事只是因為呼叫點都顯式寫了 size——下一個沒寫 size 的新欄位會靜靜掉錯階。
  • ⚠️ pointer:coarsetoolbar 階與 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-smdense--h-select--sp-12--r-btn--fs-label採用登記自 v1.2.0 掛著的「small button 尺寸」
.ds-select-md>.ds-select-triggertoolbar--h-btn「已知缺 token」表掛了六版的 .ds-select-md

⇒ 三階矩陣自此閉合。.ds-select-md> 直接子代,避免命中巢在裡面的 combobox/multicombo trigger。

改吃 token/算式/內部變數(computed 值零變動)

selector改動
.ds-optionmin-height:44pxvar(--hit-min)
coarse 的 .ds-linkmin-height:44pxvar(--hit-min)
coarse 的 .ds-checkbox,.ds-switchmin-height:44pxvar(--hit-min)
.ds-badgeheight:22pxvar(--ds-badge-h)
.ds-select-menumax-height:308pxcalc(7 * var(--hit-min))
.ds-timeline.ds-timeline-dot.ds-timeline-step::before收成 --dot:16px--dot-bw:2pxleft:7pxcalc((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-emptypadding:46pxvar(--sp-48)48px收斂。46 是階梯外髒值;依 v1.13.0 已拍板的通則「原實作的值與階梯對不上時一律收斂到階梯,1–2px 位移直接吸收」
.ds-stat-chipheight:31pxvar(--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-btn 22/.ds-avatar 30/.ds-notif-ico 30/.ds-modal-ico 40/.ds-side-mark 32/.ds-notif-badge 17/.ds-notif-dot 7。其中 .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 不在既有檢查①②的比對範圍內 ⇒ 完全沒有紅燈。
    • 出廠檢查 AcontrolScale 引用的每個 token 名必須存在。Bcomponents.css 動態掃尺寸變體 class,必須在 controlScale.members 或具名例外清單內。CpointerCoarse 每個值必須符合 ^\d+(px)?$。三道都會 exit 1
  • .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回填
TOKENS新增 0 / 改值 0 / 改名 0 / 移除 0 ← 125 顆,tokens.json 與 v1.15.0 只差 version 字串
COMPONENTSchanged(33 條規則,受影響 selector 見下)
BEHAVIORnone
BREAKINGno
下游動作吃 tokens.json 的:不必動(token 層零變動) 吃 components.css 的:讀下面那批 selector 的 diff

⚠️ 本則是 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
TOKENS新增 1 / 改值 1(附 舊值→新值)/ 改名 1 / 移除 0 ← 124 → 125
COMPONENTSchanged(檔頭:凍結宣告+版號補正;零 selector 規則變動
BEHAVIORnone
BREAKINGyes 🔴 ← --bp-lg 的值被換掉了,而且是靜默的
下游動作重跑產生器 + 讀下面那段(斷點對映必須手動確認一次)

⚠️ 本則是 2026-08-28 回填的。v1.15.0 發布當時 GitHub Releases 是零筆,tag push 本身不發任何通知。

🔴 BREAKING:--bp-lg 改名+改值

動作token舊值 → 新值
改值--bp-lg1200px992px
新增--bp-xl— → 1200px(=原本 --bp-lg 的值搬過來)

不是加一顆而已,是改名+改值。 斷點由 3 階改 4 階:640 / 820 / 992(新) / 1200。

為什麼 BREAKING=yes:判準是「下游不改任何一行,畫面或行為會變」,不是「API 有沒有移除」。這一顆是改值,所以它是靜默的——你的產生器重跑之後 lg: 的觸發點就換了,沒有任何編譯錯、沒有任何警告。

為什麼要改名而不是加一顆:走 Tailwind 之後 token 名會直接變成 variant 名(--bp-lglg:),而開發端既有的 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 規則變動

  1. 🔴 加入凍結宣告:本檔進入凍結狀態、逐族退役。元件層拍板走「形態乙」——改寫成 React 元件、樣式用 Tailwind v4 utility 寫在元件裡,透過 shadcn registry 分發。本檔自此不再是元件層的事實來源;已遷移的族其規則會被刪除。不要在本檔新增元件族;要改既有未遷移族的樣式仍改這裡。
  2. 補正檔頭版本號 v1.3.0v1.15.0(漂了十二版)。

⚠️ 這一版讓 components.css 全檔行號 +24,所有既有行號引用一次全失效,而且看起來仍然有效——原本指按鈕 transition 的第 21 行,升版後指到「本檔對應那幾族的規則會被刪除」這一句完整、可讀、但意思無關的話。 ⇒ 引用本庫規格請改用「selector 名稱+版本號」(例:.ds-badge @ DS v1.15.0),不要用行號。

同批新增的文件(非 token/非元件)

  • GUIDELINE.md 新增「元件層導入:Tailwind v4 + shadcn registry」整節(含拍板結果、形態乙的定義、遷移成本實測、三條實測踩出來的護欄疊層階梯對照表與遷移驗收)。
  • showcase.htmlshowcase-appshell.htmlGUIDELINE.mdREADME.md 首次補齊到 repo(在此之前 repo 上只有 token 層與元件層,視覺驗收基準完全沒有)。

採用者要做什麼

  1. 重跑產生器拿到新的 breakpoints(四顆)。
  2. 🔴 手動確認一次你的 lg:breakpoints.lg 現在指到 992 而不是 1200,並檢查 992–1200 這個區間的版面。這一步不能靠 diff——你的 theme 檔是生成的,舊值在你那邊已經被覆蓋掉了。
  3. components.css 的:讀凍結宣告,並把既有的行號引用改成 selector+版本號。

v1.14.0

2026-08-17回填BREAKING
TOKENS對下游:初次發布 124 顆(+ pointerCoarse 7 顆 + breakpoints 3 顆) 對庫內部(vs v1.13.1):新增 0 / 改值 41(附 舊值→新值)/ 改名 0 / 移除 0
COMPONENTSchanged(0 條規則改動;本版是首次交付整份 components.css)
BEHAVIOR首次交付 components.js
BREAKINGyes 🔴 ← 對 v1.13.1 的既有採用者:圓角與控件高度各升一階,外觀會變
下游動作初次導入:寫產生器讀 tokens.json(不要手抄) 已在用 v1.13.x 的:重跑產生器 + 跑一輪視覺驗收(外觀會變)

⚠️ 本則是 2026-08-28 回填的。v1.14.0 發布當時 GitHub Releases 是零筆。

ℹ️ 這是本 repo 的初版(tag message:「初版:token、元件層與轉換說明」),也是第一次把 tokens.jsonbuild-tokens.mjsCONVERSION.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-drawer18 → 24--h-input46 → 48
--r-menu16 → 20--h-btn40 → 42
--r-card14 → 18--h-select36 → 38
--r-input12 → 14--h-tab32 → 34
--r-fieldcard10 → 14--h-icon32 → 34
--r-btn8 → 12--h-seg31 → 33
--r-chip7 → 10--h-chip26 → 28
--r-tag6 → 8--h-row54 → 58
--r-check2 → 4--h-bar64 → 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 + pointerCoarse 7 顆 + breakpoints)/CONVERSION.md(轉換說明與三個實測踩過的坑)。
  • 已由 fresh-context agent 獨立驗收:124 顆零漏抓、零幻覺、零值差;pointerCoarse 7 顆不多不少;版本三處一致。
  • 同批修掉一個既有缺陷:tokens.css 的「白色當『材質』而非前景」群組標頭被 v1.9.0 插入的一組 token 拆離、描述不到任何東西(機械解析成 tokens.json 時它成了零顆 token 的空群組才被發現)。純註解移動、零 computed 值變動。

採用者要做什麼

  • 初次導入:寫一支產生器讀 tokens.json 吐你 stack 的 theme。🔴 不要手抄一次——手抄的那一刻起就是兩份事實來源,我們改版你不會知道。
  • 已在用 v1.13.x 的:這一版不是 no-op。圓角與控件高度各升一階,tokens.json 重跑之後外觀就會變 ⇒ 要跑一輪視覺驗收(基準= showcase.html,活型錄、開檔即現值)。
  • ⚠️ 注意 pointerCoarse 不是可有可無的一組覆寫值。漏掉它,觸控裝置上的按鈕會太小,而且在桌機瀏覽器測不出來