@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.
- package/CLAUDE.md +1 -1
- package/dist/components/Avatar/avatar.d.ts +5 -0
- package/dist/components/Avatar/avatar.d.ts.map +1 -1
- package/dist/components/Avatar/avatar.js +2 -2
- package/dist/components/Avatar/avatar.js.map +1 -1
- package/dist/components/Command/command.js +4 -4
- package/dist/components/Command/command.js.map +1 -1
- package/dist/components/RadioGroup/radio-group.d.ts +3 -3
- package/dist/components/RadioGroup/radio-group.d.ts.map +1 -1
- package/dist/components/RadioGroup/radio-group.js +6 -3
- package/dist/components/RadioGroup/radio-group.js.map +1 -1
- package/dist/components/Sheet/sheet.js +1 -1
- package/dist/components/Sheet/sheet.js.map +1 -1
- package/dist/components/Tag/tag.d.ts +2 -0
- package/dist/components/Tag/tag.d.ts.map +1 -1
- package/dist/components/Tag/tag.js +3 -3
- package/dist/components/Tag/tag.js.map +1 -1
- package/dist/components/TimePicker/time-columns.d.ts.map +1 -1
- package/dist/components/TimePicker/time-columns.js +3 -0
- package/dist/components/TimePicker/time-columns.js.map +1 -1
- package/dist/components/TreeView/tree-view.d.ts.map +1 -1
- package/dist/components/TreeView/tree-view.js.map +1 -1
- package/ds-canonical/fork/governance.lock +7 -1
- package/ds-canonical/fork/hooks/check_tabs_content_chrome_body_double_gap.sh +183 -0
- package/ds-canonical/fork/manifest.json +5 -0
- package/ds-canonical/hooks/check_tabs_content_chrome_body_double_gap.sh +183 -0
- package/ds-canonical/skills/codex-collab/references/brief-template.md +1 -1
- package/ds-canonical/skills/design-system-audit/SKILL.md +3 -1
- package/ds-canonical/templates/dashboard-app.tsx +3 -3
- package/llms-full.txt +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/src/components/Avatar/avatar.tsx +7 -2
- package/src/components/Command/command.tsx +4 -4
- package/src/components/Dialog/dialog.stories.tsx +12 -4
- package/src/components/Field/field.spec.md +1 -1
- package/src/components/RadioGroup/radio-group.tsx +6 -3
- package/src/components/Sheet/sheet.principles.stories.tsx +2 -2
- package/src/components/Sheet/sheet.tsx +1 -1
- package/src/components/Tabs/tabs.spec.md +4 -0
- package/src/components/Tag/tag.tsx +5 -3
- package/src/components/TimePicker/time-columns.tsx +7 -0
- package/src/components/TreeView/tree-view.tsx +5 -4
- package/src/patterns/element-anatomy/item-anatomy.spec.md +2 -0
- 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)
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
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
|
---
|