@qijenchen/design-system 0.1.0-beta.76 → 0.1.0-beta.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CLAUDE.md +1 -1
  2. package/dist/components/Avatar/avatar.d.ts +5 -0
  3. package/dist/components/Avatar/avatar.d.ts.map +1 -1
  4. package/dist/components/Avatar/avatar.js +2 -2
  5. package/dist/components/Avatar/avatar.js.map +1 -1
  6. package/dist/components/Command/command.js +4 -4
  7. package/dist/components/Command/command.js.map +1 -1
  8. package/dist/components/RadioGroup/radio-group.d.ts +3 -3
  9. package/dist/components/RadioGroup/radio-group.d.ts.map +1 -1
  10. package/dist/components/RadioGroup/radio-group.js +6 -3
  11. package/dist/components/RadioGroup/radio-group.js.map +1 -1
  12. package/dist/components/Sheet/sheet.js +1 -1
  13. package/dist/components/Sheet/sheet.js.map +1 -1
  14. package/dist/components/Tag/tag.d.ts +2 -0
  15. package/dist/components/Tag/tag.d.ts.map +1 -1
  16. package/dist/components/Tag/tag.js +3 -3
  17. package/dist/components/Tag/tag.js.map +1 -1
  18. package/dist/components/TimePicker/time-columns.d.ts.map +1 -1
  19. package/dist/components/TimePicker/time-columns.js +3 -0
  20. package/dist/components/TimePicker/time-columns.js.map +1 -1
  21. package/dist/components/TreeView/tree-view.d.ts.map +1 -1
  22. package/dist/components/TreeView/tree-view.js.map +1 -1
  23. package/ds-canonical/fork/governance.lock +7 -1
  24. package/ds-canonical/fork/hooks/check_tabs_content_chrome_body_double_gap.sh +183 -0
  25. package/ds-canonical/fork/manifest.json +5 -0
  26. package/ds-canonical/hooks/check_tabs_content_chrome_body_double_gap.sh +183 -0
  27. package/ds-canonical/skills/codex-collab/references/brief-template.md +1 -1
  28. package/ds-canonical/skills/design-system-audit/SKILL.md +3 -1
  29. package/ds-canonical/templates/dashboard-app.tsx +3 -3
  30. package/llms-full.txt +1 -1
  31. package/llms.txt +1 -1
  32. package/package.json +1 -1
  33. package/src/components/Avatar/avatar.tsx +7 -2
  34. package/src/components/Command/command.tsx +4 -4
  35. package/src/components/Dialog/dialog.stories.tsx +12 -4
  36. package/src/components/Field/field.spec.md +1 -1
  37. package/src/components/RadioGroup/radio-group.tsx +6 -3
  38. package/src/components/Sheet/sheet.principles.stories.tsx +2 -2
  39. package/src/components/Sheet/sheet.tsx +1 -1
  40. package/src/components/Tabs/tabs.spec.md +4 -0
  41. package/src/components/Tag/tag.tsx +5 -3
  42. package/src/components/TimePicker/time-columns.tsx +7 -0
  43. package/src/components/TreeView/tree-view.tsx +5 -4
  44. package/src/patterns/element-anatomy/item-anatomy.spec.md +2 -0
  45. package/src/tokens/layoutSpace/layoutSpace.spec.md +18 -0
@@ -185,6 +185,8 @@ Tabs 是 **navigation anchor**,不是 compact control:
185
185
 
186
186
  **TabsContent 與 TabsList 間距(2026-06-12 user 拍板)**:`--layout-space-tight`(md 12px,density 連動),**內建於 TabsContent**(`mt-[var(--layout-space-tight)]`,className 可覆寫;full-height 佈局用 `mt-0`)。依 `layoutSpace.spec`「親疏 3 級」第一級(同 bundle,元件 spec own)+ 規則 3「直接功能依賴 = tight」精神;**不**取 Ant 的固定 16px(M23 自家 token 優先)。內容本身的排版歸 item-anatomy / 各內容元件管,本條只管 list↔content gap。
187
187
 
188
+ > ⚠️ **此預設只在 TabsContent 緊接 tab bar 當直接 sibling 時成立**(standalone 頁面 tabs、ChromeHeader-as-sibling)。當 `tabsSlot` composition 把 tab bar 抬進 chrome header、TabsContent 留在 `DialogBody`/`SheetBody`/`SurfaceBody`(chrome scroll body,已自帶 `pt-/py-[var(--layout-space-tight)]`)內時,body 已擁有 header→content gap,TabsContent 的預設 `mt-tight` 會**雙重疊加**(md 24 / lg 32)。此時 TabsContent **必 `className="mt-0"`** 把 ownership 轉移給 body — 詳見下方「出現在 Dialog」段。一軸單一 spacing owner,per `item-anatomy.spec.md`「垂直 padding 歸屬 / 禁雙重 padding」。Hook `check_tabs_content_chrome_body_double_gap.sh` 機械強制。
189
+
188
190
  ---
189
191
 
190
192
  ## Underline 與 TabsList border 的視覺關係
@@ -219,6 +221,8 @@ Tabs 常與容器 header 的底邊 border 合併——**視覺上只有一條線
219
221
 
220
222
  Size 建議:overlay / chrome header 內用 `sm`(32/40)— 對應 close X 也是 sm,視覺一致;**獨立取代 chrome header** 的 page-level workspace tabs 用 `lg`(48/56,= chrome-header-height)。
221
223
 
224
+ **間距 ownership**:tabsSlot 模式下 `TabsContent` 放 `DialogBody`/`SheetBody`/`SurfaceBody`(自帶 `pt-/py-tight`)內時**必 `className="mt-0"`** — body 已擁有 header→content gap,TabsContent 預設 `mt-tight` 會雙重(md 24 / lg 32,2026-07-01 user 抓)。詳「TabsContent 與 TabsList 間距」段;hook `check_tabs_content_chrome_body_double_gap.sh` 強制。
225
+
222
226
  ---
223
227
 
224
228
  ## 狀態
@@ -68,6 +68,8 @@ export interface TagProps
68
68
  avatar?: React.ReactNode
69
69
  /** 可移除——Tag 自動渲染 remove 按鈕並控制尺寸與互動樣式(從集合移除 item) */
70
70
  onRemove?: () => void
71
+ /** remove 按鈕的 aria-label 目標名(a11y)。children 為非字串 ReactNode 時建議傳,否則 SR 讀不出移除哪個 tag。預設取 string children。 */
72
+ dismissLabel?: string
71
73
  /** 深底模式(step-6 背景 + on-emphasis 配對前景;亮色 hue yellow/amber/orange/lime 用深字 --on-emphasis-dark,green 白字例外) */
72
74
  solid?: boolean
73
75
  /**
@@ -107,7 +109,7 @@ function TagDismiss({ onRemove, label, solid, color }: { onRemove: () => void; l
107
109
  icon={X}
108
110
  size="md"
109
111
  onClick={(e) => { e.stopPropagation(); onRemove() }}
110
- aria-label={`移除 ${label}`}
112
+ aria-label={label ? `移除 ${label}` : '移除'}
111
113
  style={solidColors ? ({ '--dismiss-hover': solidColors.hover, '--dismiss-active': solidColors.active } as React.CSSProperties) : undefined}
112
114
  hoverBgClassName={
113
115
  solidColors
@@ -121,7 +123,7 @@ function TagDismiss({ onRemove, label, solid, color }: { onRemove: () => void; l
121
123
  }
122
124
 
123
125
  function TagInner(
124
- { className, color, size, icon: Icon, avatar, onRemove, solid, unbounded = false, children, style, ...props }: TagProps,
126
+ { className, color, size, icon: Icon, avatar, onRemove, dismissLabel, solid, unbounded = false, children, style, ...props }: TagProps,
125
127
  forwardedRef: React.ForwardedRef<HTMLDivElement>,
126
128
  ) {
127
129
  const solidClass = solid ? SOLID_CLASSES[color ?? 'neutral'] : undefined
@@ -178,7 +180,7 @@ function TagInner(
178
180
  {Icon && <Icon size={16} aria-hidden />}
179
181
  {avatar && <span className="shrink-0 w-4 h-4 rounded-full overflow-hidden inline-grid place-content-center [&>*]:w-full [&>*]:h-full">{avatar}</span>}
180
182
  <span data-tag-text="" className="px-1 truncate min-w-0">{children}</span>
181
- {onRemove && <TagDismiss onRemove={onRemove} label={label} solid={solid} color={color ?? 'neutral'} />}
183
+ {onRemove && <TagDismiss onRemove={onRemove} label={dismissLabel || label} solid={solid} color={color ?? 'neutral'} />}
182
184
  </div>
183
185
  )
184
186
 
@@ -106,6 +106,11 @@ function TimeColumn({ values, selected, disabledSet, label, onSelect, withDivide
106
106
  isFirstRunRef.current = false
107
107
  }, [values, selected])
108
108
 
109
+ // a11y:穩定 id 前綴,讓 listbox `aria-activedescendant` 指向目前 active(= selected)option。
110
+ // single-tabstop + 方向鍵 listbox 應透過 activedescendant 告知 AT 哪個 option active,
111
+ // 否則 SR 方向鍵移動時讀不出目前 option(WAI-ARIA listbox virtual-focus 慣例)。
112
+ const baseId = React.useId()
113
+
109
114
  // WAI-ARIA listbox keyboard pattern:ArrowUp/Down 切 option / Home / End 跳邊界。
110
115
  // 對標 Ant TimePicker / Material TimePicker。Tab 跳離 listbox(走預設行為,不 stopPropagation)。
111
116
  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
@@ -147,6 +152,7 @@ function TimeColumn({ values, selected, disabledSet, label, onSelect, withDivide
147
152
  ref={listRef}
148
153
  role="listbox"
149
154
  aria-label={label}
155
+ aria-activedescendant={selected != null ? `${baseId}-opt-${selected}` : undefined}
150
156
  tabIndex={0}
151
157
  onKeyDown={handleKeyDown}
152
158
  className="flex flex-col py-2 focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-[-2px]"
@@ -157,6 +163,7 @@ function TimeColumn({ values, selected, disabledSet, label, onSelect, withDivide
157
163
  return (
158
164
  <button
159
165
  key={v}
166
+ id={`${baseId}-opt-${v}`}
160
167
  type="button"
161
168
  role="option"
162
169
  aria-selected={isSelected}
@@ -90,10 +90,11 @@ export interface TreeDragEndEvent {
90
90
  // Icon / chevron 尺寸——從 item-layout pattern module 引入(在檔頂 import),
91
91
  // 這裡本地不再宣告。所有 row primitives 共用同一個常數。
92
92
 
93
- // indentStep = chevronSize + gap-2(8px) 2026-05-04 SSOT token `--tree-indent-{sm,md,lg}`
94
- // `tokens/uiSize/uiSize.css`。DataTable nested rows 共用此 SSOT,跨元件視覺一致。
95
- // 結構對齊:子 chevron 對齊父 icon,子 icon 對齊父 label。
96
- // Numeric value 此處保留(drop indicator JS px 計算需 number),Tailwind class 走 token。
93
+ // indentStep = chevronSize + gap-2(8px)。值 {24,24,28} CSS token `--tree-indent-{sm,md,lg}`
94
+ // (`tokens/uiSize/uiSize.css`)**完全一致、必須同步維護**(改一處要兩處一起改;非「token 取代 literal」)。
95
+ // 為何兩源並存:本元件 render 用此 JS literal `indentPx`(L808 inline calc 需 number)+ drop-indicator
96
+ // pointer 數學(L352,需 number px 命中判斷,CSS var 在 JS 計算層拿不到);DataTable nested rows
97
+ // 則走 CSS token 的 Tailwind class(跨元件視覺一致)。結構對齊:子 chevron 對齊父 icon,子 icon 對齊父 label。
97
98
  const INDENT_STEP: Record<SizeKey, number> = { sm: 24, md: 24, lg: 28 }
98
99
 
99
100
  // ═══════════════════════════════════════════════════════════════════════════
@@ -245,6 +245,8 @@ Row 集合是**內容(content)**,不是區段(region)。加 py 到 row 集合會
245
245
 
246
246
  **曾經發生的 bug**:TreeView 原本硬寫 `py-2`,放進 `SidebarGroup`(也有 `py-2`)導致 label 和 first tree item 之間多出 8px 無法解釋的 gap;後來改成「只有 menu context 加 py-2」,結果 story 的 bordered wrapper 不屬於 menu context,items 貼邊。最終解法:TreeView root 一律不加 py,所有外層容器自己負責。
247
247
 
248
+ **同源 invariant(不只 row 集合)**:「一軸單一 spacing owner」也適用元件的**單一 margin**跨 composition boundary。2026-07-01:`TabsContent` 內建 `mt-[var(--layout-space-tight)]`(tab bar 下方間距),`tabsSlot` 模式把 tab bar 抬進 chrome header 後 TabsContent 留在 `DialogBody`(自帶 `pt-tight`)內 → header→content gap 兩個 owner 疊加 = 雙重 tight(「專案名稱」上方 md 24 / lg 32)。解法同上:chrome body 已擁有 gap,TabsContent 加 `mt-0` 轉移 ownership。詳 `components/Tabs/tabs.spec.md`「出現在 Dialog」段 + hook `check_tabs_content_chrome_body_double_gap.sh`。
249
+
248
250
  ### 實作規則
249
251
 
250
252
  - 寫 row 集合元件(新 tree / menu / list)時,**root div 不加 py**
@@ -145,6 +145,23 @@ Pattern C 的視覺邏輯見 `overlay-surface.spec.md`「Hover bg 貼邊 chrome
145
145
 
146
146
  ---
147
147
 
148
+ ## 該用 token(縮放)vs 刻意固定(magic number 合法)的邊界(2026-07-01 codify)
149
+
150
+ **核心**:layout-space token(density-scaling)只管 **macro 結構間距**;**不是所有 px 都該 token 化**。硬把刻意固定值 token 化 = over-tokenize(反 pattern,跟「硬寫該縮放的 gap」同樣是 bug 的反面)。
151
+
152
+ | 該用 layout-space token(density-scaling) | 刻意固定值 OK(magic number 合法) |
153
+ |---|---|
154
+ | 並列獨立 **區塊 / 卡片 / 表單欄位** 之間(規則 3 parallel = loose)| **同質 list 的列間距**(一串同類 rows,規則 3「同範疇 spec-own」)|
155
+ | **Header → 內容**(規則 2 = tight)| **micro / icon / 控件內部** 間距(gap-1/2、icon padding)|
156
+ | **Chrome 水平內距**(規則 6 = loose)| **為視覺平衡 / 對稱 刻意調的值**(設計裁量)|
157
+ | **內容 → action button**(規則 4 = bottom 48)| **元件自身刻意固定**(如 FieldGroup 三級固定 gap,不隨 density)|
158
+
159
+ **判準一句話**:是「並列/區塊/chrome/header→content/content→action 的 macro 結構間距、規則說該縮放」→ 用 token;是「list 列間距 / micro / 刻意固定 / 元件內部決定」→ magic number 合法,**不要 token 化**。
160
+
161
+ **機械強制**:`check_tabs_content_chrome_body_double_gap.sh` Check 2 抓「浮層 body 自身固定 macro gap」(明確 pattern);判斷題(這是並列還是 list?macro 還是 micro?)靠 **週期 LLM sweep**(`/design-system-audit` dim「layout-space macro 合規」)—— 純 regex 分不出,不硬機械化(否則 over-flag 合法 magic number)。
162
+
163
+ ---
164
+
148
165
  ## 心智模型(三步推理 + 具體 self-questions)
149
166
 
150
167
  ### Step 1 — 判角色(對單一元素)
@@ -290,6 +307,7 @@ document.documentElement.setAttribute('data-layout-space', 'lg')
290
307
 
291
308
  - **不抽 universal LayoutBody / FormLayout primitive**:world-class(Material / Polaris / Atlassian / Carbon / Mantine)都「每元件 own variant + 共享 token」;規則 1-6 universal,角色 scope-relative 易誤封裝。獨特 chrome 已在 `overlay-surface.spec.md` + `action-bar.spec.md`
292
309
  - **List-as-region in overlay body**(2026-05-01 canonical):當 Dialog / Sheet / Popover body 內容 = unbounded list-as-region(menu / Cmd+K / nav)時,consumer 用 className override 撤掉 chrome padding(`<DialogBody className="!px-0 !pt-0 !pb-0">`)+ 自管 list outer wrapper(`<div className="py-2">`)+ item 自帶 `px-loose rounded-md`。**不做成 body variant**(`flush?: boolean`)— Material/Atlassian/Mantine/shadcn 都 consumer 自管,Polaris flush API scope 極窄;variant 不解決底層脆弱(加 1 row search/banner 就破功),反把 1 surface decision 拆兩 API。詳 `overlay-surface.spec.md`「List-as-region in overlay body」+ `dialog.tsx:165` JSDoc
310
+ - **Tabs-in-chrome-body 單一 gap owner**(2026-07-01 canonical):`tabsSlot` 模式下 `TabsContent` 放進 `DialogBody`/`SheetBody`/`SurfaceBody`(自帶 `pt-/py-tight`)時**必 `className="mt-0"`** — chrome body 已擁有 header→content gap(規則 2「element → tight」),TabsContent 內建 `mt-tight` 會雙重疊加(md 24 / lg 32)。同「一軸單一 spacing owner」原則(`item-anatomy.spec.md`「垂直 padding 歸屬 / 禁雙重 padding」)。詳 `components/Tabs/tabs.spec.md`「出現在 Dialog」段 + hook `check_tabs_content_chrome_body_double_gap.sh`
293
311
  - **v1 → v6**(2026-04-30):block-adjacent 機械 → 親疏判 + bundled-family 分權 + region 二分(bounded/unbounded)+ 多 region 限制。詳 git + memory `feedback_layout_v6_canonical.md`
294
312
 
295
313
  ---