@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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_tabs_content_chrome_body_double_gap.sh — P0 BLOCKER(overlay-body layout-space guardian,2 checks)
|
|
3
|
+
#
|
|
4
|
+
# **Check 1**(tabs double-gap):`<TabsContent>` 被放進 chrome scroll body(`<DialogBody>` /
|
|
5
|
+
# `<SheetBody>` / `<SurfaceBody>`)內卻沒 `mt-0` override → 雙重 gap owner。
|
|
6
|
+
# **Check 2**(overlay-body fixed macro gap,2026-07-01 Sheet gap-4 錨例):浮層 body 自身 className
|
|
7
|
+
# 用固定 `gap-[4-9]`(macro)而非 layout-space token → lg density 不縮放,drift。詳見下方 Check 2 段。
|
|
8
|
+
#
|
|
9
|
+
# 兩者同屬「overlay-body layout-space 合規」domain,折進一 hook(不增 hook 數)。
|
|
10
|
+
#
|
|
11
|
+
# Root invariant(item-anatomy.spec.md「垂直 padding 歸屬 / 禁雙重 padding」+ layoutSpace.spec.md
|
|
12
|
+
# 規則 2「Header → element = 單一 tight」):兩元素之間的 gap 只能有一個 owner。
|
|
13
|
+
# - DialogBody/SheetBody/SurfaceBody 內層 padded div 已用 `pt-/py-[var(--layout-space-tight)]`
|
|
14
|
+
# 擁有 header→content 的 chrome gap。
|
|
15
|
+
# - TabsContent 預設帶 `mt-[var(--layout-space-tight)]`(tabs.tsx,為「緊接 tab bar 下方」而設)。
|
|
16
|
+
# - tabsSlot composition 把 tab bar 抬進 header,TabsContent 留在 body → 兩 owner 疊加 =
|
|
17
|
+
# 雙重 tight(md 24 / lg 32)。修法 = chrome body 內的 TabsContent 加 `className="mt-0"`
|
|
18
|
+
# 把 ownership 轉移給 body(對齊 AppShell 前例 app-shell.stories.tsx + Carbon/Radix「panel
|
|
19
|
+
# 單一 owner」benchmark)。
|
|
20
|
+
#
|
|
21
|
+
# Anchor:2026-07-01 user 在「專案設定」Dialog(dialog.stories.tsx WithTabsInHeader)DevTools
|
|
22
|
+
# 抓到「專案名稱」上方兩個 --layout-space-tight。歷次稽核漏抓,因既有
|
|
23
|
+
# check_layout_space_magic_numbers.sh **明確 skip packages/design-system/src/**(該 hook L34)。
|
|
24
|
+
# 本 hook **反轉 scope** 專掃 DS src + apps composition,補該盲區。
|
|
25
|
+
#
|
|
26
|
+
# Scope:只掃 packages/design-system/src/** + apps/** 的 .tsx(含 .stories.tsx)。
|
|
27
|
+
# Level:P0 BLOCKER exit 2(layout-space SSOT 一律 P0,非 P1 WARN — per
|
|
28
|
+
# feedback_ssot_mechanical_p0_not_p1_warn_2026_05_27)。窄 gate(必同時含 chrome body + TabsContent)
|
|
29
|
+
# 控 false-positive。Escape:`// @tabs-content-gap-ok: <rationale>`(同行或前一註解行)。
|
|
30
|
+
|
|
31
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
32
|
+
|
|
33
|
+
set -uo pipefail
|
|
34
|
+
|
|
35
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
36
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
37
|
+
|
|
38
|
+
case "${TOOL:-}" in
|
|
39
|
+
Edit|Write|MultiEdit) ;;
|
|
40
|
+
*) exit 0 ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.notebook_path // ""' 2>/dev/null)
|
|
44
|
+
|
|
45
|
+
# Scope:DS source + apps,.tsx only(INVERSE of check_layout_space_magic_numbers.sh 的 DS-src skip)
|
|
46
|
+
if ! echo "$FILE" | grep -qE '\.tsx$'; then exit 0; fi
|
|
47
|
+
if ! echo "$FILE" | grep -qE '(packages/design-system/src/|/apps/|(^|/)apps/)'; then exit 0; fi
|
|
48
|
+
|
|
49
|
+
# PostToolUse:file 已寫入磁碟,讀「完整檔」做 containment 分析(避免 hunk-only partial-edit 盲區 —
|
|
50
|
+
# 部分 edit 的 new_string 可能只含 TabsContent 行不含外層 DialogBody,window 分析會漏)。
|
|
51
|
+
[ -f "$FILE" ] || exit 0
|
|
52
|
+
CONTENT=$(cat "$FILE" 2>/dev/null)
|
|
53
|
+
[ -z "$CONTENT" ] && exit 0
|
|
54
|
+
|
|
55
|
+
# ══ Check 2:overlay body 自身用固定 macro gap-N → 該用 layout-space token(2026-07-01 Sheet gap-4 錨例)══
|
|
56
|
+
# Root(layoutSpace.spec.md 規則 2/3):浮層 body 直接堆疊內容的垂直 gap 屬 macro layout-space,該用
|
|
57
|
+
# density-scaling token(並列 = loose / functional 交互 = tight),非固定 gap-N(lg 不縮放 → 跟 DS
|
|
58
|
+
# 自己 dialog.stories 的 gap-[var(--layout-space-loose)] 不一致)。窄 gate:只抓 overlay body 自身
|
|
59
|
+
# className 的固定 gap-[4-9](macro range;排除 gap-1/2/3 micro + 已 tokenize 的 gap-[var)。
|
|
60
|
+
# 全庫掃證實零現存假陽性(純防未來 drift)。Escape:同行或前一註解行 @overlay-body-gap-ok:。
|
|
61
|
+
GAP_MARKER='@overlay-body-gap-ok:'
|
|
62
|
+
GAP_HITS=$(printf '%s\n' "$CONTENT" | grep -nE '<(DialogBody|SheetBody|PopoverBody|SurfaceBody)[^>]*gap-[4-9]([^0-9]|$)' 2>/dev/null | grep -v 'gap-\[var')
|
|
63
|
+
GAP_UNJUSTIFIED=""
|
|
64
|
+
if [ -n "$GAP_HITS" ]; then
|
|
65
|
+
while IFS= read -r hit; do
|
|
66
|
+
[ -z "$hit" ] && continue
|
|
67
|
+
ln="${hit%%:*}"
|
|
68
|
+
cur="${hit#*:}"
|
|
69
|
+
if echo "$cur" | grep -qF "$GAP_MARKER"; then continue; fi
|
|
70
|
+
if [ "$ln" -gt 1 ] 2>/dev/null; then
|
|
71
|
+
prev=$(printf '%s\n' "$CONTENT" | sed -n "$((ln-1))p")
|
|
72
|
+
if echo "$prev" | grep -qF "$GAP_MARKER" && echo "$prev" | grep -qE '^[[:space:]]*(//|\{?/\*|\*)'; then continue; fi
|
|
73
|
+
fi
|
|
74
|
+
GAP_UNJUSTIFIED="${GAP_UNJUSTIFIED}${ln}: $(echo "$cur" | sed 's/^[[:space:]]*//')\n"
|
|
75
|
+
done <<< "$GAP_HITS"
|
|
76
|
+
fi
|
|
77
|
+
if [ -n "$GAP_UNJUSTIFIED" ]; then
|
|
78
|
+
cat >&2 << EOF
|
|
79
|
+
🚨 OVERLAY-BODY FIXED-GAP BLOCKER(P0,2026-07-01 Sheet demo gap-4 錨例)
|
|
80
|
+
|
|
81
|
+
在 $FILE 偵測到浮層 body(Dialog/Sheet/Popover/Surface Body)自身用固定 macro gap-N:
|
|
82
|
+
$(echo -e "$GAP_UNJUSTIFIED" | sed 's/^/ /' | head -10)
|
|
83
|
+
|
|
84
|
+
── 為什麼 ──
|
|
85
|
+
浮層 body 直接堆疊內容(並列表單欄位 / 區塊)的垂直 gap 屬 macro layout-space,該用
|
|
86
|
+
density-scaling token(並列 = loose / functional 交互 = tight),非固定 gap-N(lg 不縮放 →
|
|
87
|
+
跟 DS 自己 dialog.stories 的 gap-[var(--layout-space-loose)] 不一致)。
|
|
88
|
+
|
|
89
|
+
── 修法 2 選 1 ──
|
|
90
|
+
(a) gap-N → gap-[var(--layout-space-loose)](並列)/ gap-[var(--layout-space-tight)](functional 交互)
|
|
91
|
+
(b) Escape:該行或前一註解行加 \`// @overlay-body-gap-ok: <rationale>\`(刻意 list 間距 / 視覺平衡)
|
|
92
|
+
|
|
93
|
+
canonical → tokens/layoutSpace/layoutSpace.spec.md 規則 2/3
|
|
94
|
+
EOF
|
|
95
|
+
exit 2
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# ══ Check 1:<TabsContent> 在 chrome body 內雙重 gap(原 beta.78 邏輯)══
|
|
99
|
+
# Gate:必同時含 chrome body open tag 且 TabsContent 才啟動(narrow surface,絕大多數 edit 靜默)
|
|
100
|
+
if ! echo "$CONTENT" | grep -qE '<(DialogBody|SheetBody|SurfaceBody)[ />]'; then exit 0; fi
|
|
101
|
+
if ! echo "$CONTENT" | grep -q '<TabsContent'; then exit 0; fi
|
|
102
|
+
|
|
103
|
+
ESCAPE_MARKER='@tabs-content-gap-ok:'
|
|
104
|
+
|
|
105
|
+
# 逐行 depth-counter(awk):進 chrome body +1、出 -1;TabsContent 在 depth>0 = 在 chrome body 內。
|
|
106
|
+
# - 註解行(JSDoc `*` / `//` / `{/*` / `/*` / `*/`)整行跳過,不計 depth 也不判 TabsContent
|
|
107
|
+
# (避免 JSDoc 內 `<DialogBody>` 範例污染 depth / 內含的 mt-0 example 被誤判)。
|
|
108
|
+
# - 同行 open 先加 depth 再判 TabsContent 再減 close → 同行 `<DialogBody><TabsContent>` 與
|
|
109
|
+
# `<TabsContent>...</DialogBody>` 都正確歸屬「inside」。
|
|
110
|
+
# - TabsContent opening tag 可能跨行:accumulate 到 `>` 再驗 className 有無 canonical `mt-0`
|
|
111
|
+
# (class-boundary anchored;非-0 / arbitrary 值需 escape marker)。
|
|
112
|
+
# 輸出:每個「chrome body 內且無 mt-0 override」的 TabsContent 起始行號(供 bash escape 過濾)。
|
|
113
|
+
CANDIDATES=$(printf '%s\n' "$CONTENT" | awk '
|
|
114
|
+
function check_tag(tag, ln) {
|
|
115
|
+
# 只認 canonical `mt-0`(class-boundary anchored):排除 `mt-0.5` / `mt-04` 等 substring 假通過
|
|
116
|
+
# (mt-0.5 是本 DS 真實 class,item-anatomy.stories.tsx:766;2px 不抵銷 pt-tight → 仍雙重)。
|
|
117
|
+
# arbitrary `mt-[...]` / 非-0 值 = 非 canonical → 需 escape marker 明示 documented(非靜默通過)。
|
|
118
|
+
if (tag ~ /(^|[^a-zA-Z0-9])mt-0([^a-zA-Z0-9.]|$)/) return
|
|
119
|
+
print ln
|
|
120
|
+
}
|
|
121
|
+
BEGIN { depth=0; collecting=0; buf=""; startline=0 }
|
|
122
|
+
{
|
|
123
|
+
raw=$0
|
|
124
|
+
if (collecting) {
|
|
125
|
+
buf = buf " " raw
|
|
126
|
+
if (raw ~ />/) { collecting=0; check_tag(buf, startline); buf="" }
|
|
127
|
+
next
|
|
128
|
+
}
|
|
129
|
+
t=raw; sub(/^[ \t]+/,"",t)
|
|
130
|
+
if (t ~ /^(\*|\/\/|\{?\/\*|\*\/)/) next # skip comment-only lines
|
|
131
|
+
tmp=raw; o=gsub(/<(DialogBody|SheetBody|SurfaceBody)[ \t\/>]/,"X",tmp)
|
|
132
|
+
tmpc=raw; c=gsub(/<\/(DialogBody|SheetBody|SurfaceBody)>/,"X",tmpc)
|
|
133
|
+
depth += o
|
|
134
|
+
if (raw ~ /<TabsContent/ && depth > 0) {
|
|
135
|
+
idx = index(raw, "<TabsContent")
|
|
136
|
+
rest = substr(raw, idx)
|
|
137
|
+
if (rest ~ />/) { gt = index(rest, ">"); check_tag(substr(rest, 1, gt), NR) }
|
|
138
|
+
else { collecting=1; buf=rest; startline=NR }
|
|
139
|
+
}
|
|
140
|
+
depth -= c
|
|
141
|
+
}
|
|
142
|
+
')
|
|
143
|
+
|
|
144
|
+
if [ -z "$CANDIDATES" ]; then exit 0; fi
|
|
145
|
+
|
|
146
|
+
# Escape 過濾:同行 OR 前一「註解專用行」含 @tabs-content-gap-ok: → 豁免(對齊
|
|
147
|
+
# check_layout_space_magic_numbers.sh L57-69 已 debug 過的 preceding-line marker 邏輯)。
|
|
148
|
+
UNJUSTIFIED=""
|
|
149
|
+
while IFS= read -r ln; do
|
|
150
|
+
[ -z "$ln" ] && continue
|
|
151
|
+
cur=$(echo "$CONTENT" | sed -n "${ln}p")
|
|
152
|
+
if echo "$cur" | grep -qF "$ESCAPE_MARKER"; then continue; fi
|
|
153
|
+
if [ "$ln" -gt 1 ] 2>/dev/null; then
|
|
154
|
+
prev=$(echo "$CONTENT" | sed -n "$((ln-1))p")
|
|
155
|
+
if echo "$prev" | grep -qF "$ESCAPE_MARKER" && echo "$prev" | grep -qE '^[[:space:]]*(//|\{?/\*|\*)'; then continue; fi
|
|
156
|
+
fi
|
|
157
|
+
UNJUSTIFIED="${UNJUSTIFIED}${ln}: $(echo "$cur" | sed 's/^[[:space:]]*//')\n"
|
|
158
|
+
done <<< "$CANDIDATES"
|
|
159
|
+
|
|
160
|
+
if [ -z "$UNJUSTIFIED" ]; then exit 0; fi
|
|
161
|
+
|
|
162
|
+
cat >&2 << EOF
|
|
163
|
+
🚨 TABS-CONTENT-CHROME-BODY DOUBLE-GAP BLOCKER(P0,2026-07-01 user「專案名稱上方兩個 space-tight」)
|
|
164
|
+
|
|
165
|
+
在 $FILE 偵測到 <TabsContent> 放在 chrome scroll body(DialogBody/SheetBody/SurfaceBody)內
|
|
166
|
+
但未 override margin-top:
|
|
167
|
+
$(echo -e "$UNJUSTIFIED" | sed 's/^/ /' | head -10)
|
|
168
|
+
|
|
169
|
+
── 為什麼是 bug ──
|
|
170
|
+
DialogBody/SheetBody/SurfaceBody 內層已用 pt-/py-[var(--layout-space-tight)] 擁有 header→content
|
|
171
|
+
的 chrome gap;TabsContent 預設又帶 mt-[var(--layout-space-tight)](tabs.tsx,為緊接 tab bar 而設)。
|
|
172
|
+
tabsSlot composition 把 tab bar 抬進 header 後,兩個 owner 疊加 = 雙重 tight(md 24 / lg 32)。
|
|
173
|
+
一軸只能有一個 spacing owner(item-anatomy.spec.md「垂直 padding 歸屬 / 禁雙重 padding」)。
|
|
174
|
+
|
|
175
|
+
── 修法 2 選 1 ──
|
|
176
|
+
(a) chrome body 內的 TabsContent 加 \`className="mt-0"\` 把 ownership 轉移給 body
|
|
177
|
+
(對齊 AppShell 前例 app-shell.stories.tsx + Carbon/Radix「panel 單一 owner」benchmark)
|
|
178
|
+
(b) Escape:該行或前一註解行加 \`// @tabs-content-gap-ok: <rationale>\`(顯式 documented 例外)
|
|
179
|
+
|
|
180
|
+
完整 canonical → components/Tabs/tabs.spec.md「出現在 Dialog」段
|
|
181
|
+
+ tokens/layoutSpace/layoutSpace.spec.md 規則 2
|
|
182
|
+
EOF
|
|
183
|
+
exit 2
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
"sourceHook": "check_tailwind_wildcard_in_docs.sh",
|
|
23
23
|
"bucket": "SHIP_AS_IS"
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
"file": "hooks/check_tabs_content_chrome_body_double_gap.sh",
|
|
27
|
+
"sourceHook": "check_tabs_content_chrome_body_double_gap.sh",
|
|
28
|
+
"bucket": "SHIP_AS_IS"
|
|
29
|
+
},
|
|
25
30
|
{
|
|
26
31
|
"file": "hooks/check_escape_marker_abuse.sh",
|
|
27
32
|
"sourceHook": "check_escape_marker_abuse.sh",
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_tabs_content_chrome_body_double_gap.sh — P0 BLOCKER(overlay-body layout-space guardian,2 checks)
|
|
3
|
+
#
|
|
4
|
+
# **Check 1**(tabs double-gap):`<TabsContent>` 被放進 chrome scroll body(`<DialogBody>` /
|
|
5
|
+
# `<SheetBody>` / `<SurfaceBody>`)內卻沒 `mt-0` override → 雙重 gap owner。
|
|
6
|
+
# **Check 2**(overlay-body fixed macro gap,2026-07-01 Sheet gap-4 錨例):浮層 body 自身 className
|
|
7
|
+
# 用固定 `gap-[4-9]`(macro)而非 layout-space token → lg density 不縮放,drift。詳見下方 Check 2 段。
|
|
8
|
+
#
|
|
9
|
+
# 兩者同屬「overlay-body layout-space 合規」domain,折進一 hook(不增 hook 數)。
|
|
10
|
+
#
|
|
11
|
+
# Root invariant(item-anatomy.spec.md「垂直 padding 歸屬 / 禁雙重 padding」+ layoutSpace.spec.md
|
|
12
|
+
# 規則 2「Header → element = 單一 tight」):兩元素之間的 gap 只能有一個 owner。
|
|
13
|
+
# - DialogBody/SheetBody/SurfaceBody 內層 padded div 已用 `pt-/py-[var(--layout-space-tight)]`
|
|
14
|
+
# 擁有 header→content 的 chrome gap。
|
|
15
|
+
# - TabsContent 預設帶 `mt-[var(--layout-space-tight)]`(tabs.tsx,為「緊接 tab bar 下方」而設)。
|
|
16
|
+
# - tabsSlot composition 把 tab bar 抬進 header,TabsContent 留在 body → 兩 owner 疊加 =
|
|
17
|
+
# 雙重 tight(md 24 / lg 32)。修法 = chrome body 內的 TabsContent 加 `className="mt-0"`
|
|
18
|
+
# 把 ownership 轉移給 body(對齊 AppShell 前例 app-shell.stories.tsx + Carbon/Radix「panel
|
|
19
|
+
# 單一 owner」benchmark)。
|
|
20
|
+
#
|
|
21
|
+
# Anchor:2026-07-01 user 在「專案設定」Dialog(dialog.stories.tsx WithTabsInHeader)DevTools
|
|
22
|
+
# 抓到「專案名稱」上方兩個 --layout-space-tight。歷次稽核漏抓,因既有
|
|
23
|
+
# check_layout_space_magic_numbers.sh **明確 skip packages/design-system/src/**(該 hook L34)。
|
|
24
|
+
# 本 hook **反轉 scope** 專掃 DS src + apps composition,補該盲區。
|
|
25
|
+
#
|
|
26
|
+
# Scope:只掃 packages/design-system/src/** + apps/** 的 .tsx(含 .stories.tsx)。
|
|
27
|
+
# Level:P0 BLOCKER exit 2(layout-space SSOT 一律 P0,非 P1 WARN — per
|
|
28
|
+
# feedback_ssot_mechanical_p0_not_p1_warn_2026_05_27)。窄 gate(必同時含 chrome body + TabsContent)
|
|
29
|
+
# 控 false-positive。Escape:`// @tabs-content-gap-ok: <rationale>`(同行或前一註解行)。
|
|
30
|
+
|
|
31
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
32
|
+
|
|
33
|
+
set -uo pipefail
|
|
34
|
+
|
|
35
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
36
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
37
|
+
|
|
38
|
+
case "${TOOL:-}" in
|
|
39
|
+
Edit|Write|MultiEdit) ;;
|
|
40
|
+
*) exit 0 ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.notebook_path // ""' 2>/dev/null)
|
|
44
|
+
|
|
45
|
+
# Scope:DS source + apps,.tsx only(INVERSE of check_layout_space_magic_numbers.sh 的 DS-src skip)
|
|
46
|
+
if ! echo "$FILE" | grep -qE '\.tsx$'; then exit 0; fi
|
|
47
|
+
if ! echo "$FILE" | grep -qE '(packages/design-system/src/|/apps/|(^|/)apps/)'; then exit 0; fi
|
|
48
|
+
|
|
49
|
+
# PostToolUse:file 已寫入磁碟,讀「完整檔」做 containment 分析(避免 hunk-only partial-edit 盲區 —
|
|
50
|
+
# 部分 edit 的 new_string 可能只含 TabsContent 行不含外層 DialogBody,window 分析會漏)。
|
|
51
|
+
[ -f "$FILE" ] || exit 0
|
|
52
|
+
CONTENT=$(cat "$FILE" 2>/dev/null)
|
|
53
|
+
[ -z "$CONTENT" ] && exit 0
|
|
54
|
+
|
|
55
|
+
# ══ Check 2:overlay body 自身用固定 macro gap-N → 該用 layout-space token(2026-07-01 Sheet gap-4 錨例)══
|
|
56
|
+
# Root(layoutSpace.spec.md 規則 2/3):浮層 body 直接堆疊內容的垂直 gap 屬 macro layout-space,該用
|
|
57
|
+
# density-scaling token(並列 = loose / functional 交互 = tight),非固定 gap-N(lg 不縮放 → 跟 DS
|
|
58
|
+
# 自己 dialog.stories 的 gap-[var(--layout-space-loose)] 不一致)。窄 gate:只抓 overlay body 自身
|
|
59
|
+
# className 的固定 gap-[4-9](macro range;排除 gap-1/2/3 micro + 已 tokenize 的 gap-[var)。
|
|
60
|
+
# 全庫掃證實零現存假陽性(純防未來 drift)。Escape:同行或前一註解行 @overlay-body-gap-ok:。
|
|
61
|
+
GAP_MARKER='@overlay-body-gap-ok:'
|
|
62
|
+
GAP_HITS=$(printf '%s\n' "$CONTENT" | grep -nE '<(DialogBody|SheetBody|PopoverBody|SurfaceBody)[^>]*gap-[4-9]([^0-9]|$)' 2>/dev/null | grep -v 'gap-\[var')
|
|
63
|
+
GAP_UNJUSTIFIED=""
|
|
64
|
+
if [ -n "$GAP_HITS" ]; then
|
|
65
|
+
while IFS= read -r hit; do
|
|
66
|
+
[ -z "$hit" ] && continue
|
|
67
|
+
ln="${hit%%:*}"
|
|
68
|
+
cur="${hit#*:}"
|
|
69
|
+
if echo "$cur" | grep -qF "$GAP_MARKER"; then continue; fi
|
|
70
|
+
if [ "$ln" -gt 1 ] 2>/dev/null; then
|
|
71
|
+
prev=$(printf '%s\n' "$CONTENT" | sed -n "$((ln-1))p")
|
|
72
|
+
if echo "$prev" | grep -qF "$GAP_MARKER" && echo "$prev" | grep -qE '^[[:space:]]*(//|\{?/\*|\*)'; then continue; fi
|
|
73
|
+
fi
|
|
74
|
+
GAP_UNJUSTIFIED="${GAP_UNJUSTIFIED}${ln}: $(echo "$cur" | sed 's/^[[:space:]]*//')\n"
|
|
75
|
+
done <<< "$GAP_HITS"
|
|
76
|
+
fi
|
|
77
|
+
if [ -n "$GAP_UNJUSTIFIED" ]; then
|
|
78
|
+
cat >&2 << EOF
|
|
79
|
+
🚨 OVERLAY-BODY FIXED-GAP BLOCKER(P0,2026-07-01 Sheet demo gap-4 錨例)
|
|
80
|
+
|
|
81
|
+
在 $FILE 偵測到浮層 body(Dialog/Sheet/Popover/Surface Body)自身用固定 macro gap-N:
|
|
82
|
+
$(echo -e "$GAP_UNJUSTIFIED" | sed 's/^/ /' | head -10)
|
|
83
|
+
|
|
84
|
+
── 為什麼 ──
|
|
85
|
+
浮層 body 直接堆疊內容(並列表單欄位 / 區塊)的垂直 gap 屬 macro layout-space,該用
|
|
86
|
+
density-scaling token(並列 = loose / functional 交互 = tight),非固定 gap-N(lg 不縮放 →
|
|
87
|
+
跟 DS 自己 dialog.stories 的 gap-[var(--layout-space-loose)] 不一致)。
|
|
88
|
+
|
|
89
|
+
── 修法 2 選 1 ──
|
|
90
|
+
(a) gap-N → gap-[var(--layout-space-loose)](並列)/ gap-[var(--layout-space-tight)](functional 交互)
|
|
91
|
+
(b) Escape:該行或前一註解行加 \`// @overlay-body-gap-ok: <rationale>\`(刻意 list 間距 / 視覺平衡)
|
|
92
|
+
|
|
93
|
+
canonical → tokens/layoutSpace/layoutSpace.spec.md 規則 2/3
|
|
94
|
+
EOF
|
|
95
|
+
exit 2
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# ══ Check 1:<TabsContent> 在 chrome body 內雙重 gap(原 beta.78 邏輯)══
|
|
99
|
+
# Gate:必同時含 chrome body open tag 且 TabsContent 才啟動(narrow surface,絕大多數 edit 靜默)
|
|
100
|
+
if ! echo "$CONTENT" | grep -qE '<(DialogBody|SheetBody|SurfaceBody)[ />]'; then exit 0; fi
|
|
101
|
+
if ! echo "$CONTENT" | grep -q '<TabsContent'; then exit 0; fi
|
|
102
|
+
|
|
103
|
+
ESCAPE_MARKER='@tabs-content-gap-ok:'
|
|
104
|
+
|
|
105
|
+
# 逐行 depth-counter(awk):進 chrome body +1、出 -1;TabsContent 在 depth>0 = 在 chrome body 內。
|
|
106
|
+
# - 註解行(JSDoc `*` / `//` / `{/*` / `/*` / `*/`)整行跳過,不計 depth 也不判 TabsContent
|
|
107
|
+
# (避免 JSDoc 內 `<DialogBody>` 範例污染 depth / 內含的 mt-0 example 被誤判)。
|
|
108
|
+
# - 同行 open 先加 depth 再判 TabsContent 再減 close → 同行 `<DialogBody><TabsContent>` 與
|
|
109
|
+
# `<TabsContent>...</DialogBody>` 都正確歸屬「inside」。
|
|
110
|
+
# - TabsContent opening tag 可能跨行:accumulate 到 `>` 再驗 className 有無 canonical `mt-0`
|
|
111
|
+
# (class-boundary anchored;非-0 / arbitrary 值需 escape marker)。
|
|
112
|
+
# 輸出:每個「chrome body 內且無 mt-0 override」的 TabsContent 起始行號(供 bash escape 過濾)。
|
|
113
|
+
CANDIDATES=$(printf '%s\n' "$CONTENT" | awk '
|
|
114
|
+
function check_tag(tag, ln) {
|
|
115
|
+
# 只認 canonical `mt-0`(class-boundary anchored):排除 `mt-0.5` / `mt-04` 等 substring 假通過
|
|
116
|
+
# (mt-0.5 是本 DS 真實 class,item-anatomy.stories.tsx:766;2px 不抵銷 pt-tight → 仍雙重)。
|
|
117
|
+
# arbitrary `mt-[...]` / 非-0 值 = 非 canonical → 需 escape marker 明示 documented(非靜默通過)。
|
|
118
|
+
if (tag ~ /(^|[^a-zA-Z0-9])mt-0([^a-zA-Z0-9.]|$)/) return
|
|
119
|
+
print ln
|
|
120
|
+
}
|
|
121
|
+
BEGIN { depth=0; collecting=0; buf=""; startline=0 }
|
|
122
|
+
{
|
|
123
|
+
raw=$0
|
|
124
|
+
if (collecting) {
|
|
125
|
+
buf = buf " " raw
|
|
126
|
+
if (raw ~ />/) { collecting=0; check_tag(buf, startline); buf="" }
|
|
127
|
+
next
|
|
128
|
+
}
|
|
129
|
+
t=raw; sub(/^[ \t]+/,"",t)
|
|
130
|
+
if (t ~ /^(\*|\/\/|\{?\/\*|\*\/)/) next # skip comment-only lines
|
|
131
|
+
tmp=raw; o=gsub(/<(DialogBody|SheetBody|SurfaceBody)[ \t\/>]/,"X",tmp)
|
|
132
|
+
tmpc=raw; c=gsub(/<\/(DialogBody|SheetBody|SurfaceBody)>/,"X",tmpc)
|
|
133
|
+
depth += o
|
|
134
|
+
if (raw ~ /<TabsContent/ && depth > 0) {
|
|
135
|
+
idx = index(raw, "<TabsContent")
|
|
136
|
+
rest = substr(raw, idx)
|
|
137
|
+
if (rest ~ />/) { gt = index(rest, ">"); check_tag(substr(rest, 1, gt), NR) }
|
|
138
|
+
else { collecting=1; buf=rest; startline=NR }
|
|
139
|
+
}
|
|
140
|
+
depth -= c
|
|
141
|
+
}
|
|
142
|
+
')
|
|
143
|
+
|
|
144
|
+
if [ -z "$CANDIDATES" ]; then exit 0; fi
|
|
145
|
+
|
|
146
|
+
# Escape 過濾:同行 OR 前一「註解專用行」含 @tabs-content-gap-ok: → 豁免(對齊
|
|
147
|
+
# check_layout_space_magic_numbers.sh L57-69 已 debug 過的 preceding-line marker 邏輯)。
|
|
148
|
+
UNJUSTIFIED=""
|
|
149
|
+
while IFS= read -r ln; do
|
|
150
|
+
[ -z "$ln" ] && continue
|
|
151
|
+
cur=$(echo "$CONTENT" | sed -n "${ln}p")
|
|
152
|
+
if echo "$cur" | grep -qF "$ESCAPE_MARKER"; then continue; fi
|
|
153
|
+
if [ "$ln" -gt 1 ] 2>/dev/null; then
|
|
154
|
+
prev=$(echo "$CONTENT" | sed -n "$((ln-1))p")
|
|
155
|
+
if echo "$prev" | grep -qF "$ESCAPE_MARKER" && echo "$prev" | grep -qE '^[[:space:]]*(//|\{?/\*|\*)'; then continue; fi
|
|
156
|
+
fi
|
|
157
|
+
UNJUSTIFIED="${UNJUSTIFIED}${ln}: $(echo "$cur" | sed 's/^[[:space:]]*//')\n"
|
|
158
|
+
done <<< "$CANDIDATES"
|
|
159
|
+
|
|
160
|
+
if [ -z "$UNJUSTIFIED" ]; then exit 0; fi
|
|
161
|
+
|
|
162
|
+
cat >&2 << EOF
|
|
163
|
+
🚨 TABS-CONTENT-CHROME-BODY DOUBLE-GAP BLOCKER(P0,2026-07-01 user「專案名稱上方兩個 space-tight」)
|
|
164
|
+
|
|
165
|
+
在 $FILE 偵測到 <TabsContent> 放在 chrome scroll body(DialogBody/SheetBody/SurfaceBody)內
|
|
166
|
+
但未 override margin-top:
|
|
167
|
+
$(echo -e "$UNJUSTIFIED" | sed 's/^/ /' | head -10)
|
|
168
|
+
|
|
169
|
+
── 為什麼是 bug ──
|
|
170
|
+
DialogBody/SheetBody/SurfaceBody 內層已用 pt-/py-[var(--layout-space-tight)] 擁有 header→content
|
|
171
|
+
的 chrome gap;TabsContent 預設又帶 mt-[var(--layout-space-tight)](tabs.tsx,為緊接 tab bar 而設)。
|
|
172
|
+
tabsSlot composition 把 tab bar 抬進 header 後,兩個 owner 疊加 = 雙重 tight(md 24 / lg 32)。
|
|
173
|
+
一軸只能有一個 spacing owner(item-anatomy.spec.md「垂直 padding 歸屬 / 禁雙重 padding」)。
|
|
174
|
+
|
|
175
|
+
── 修法 2 選 1 ──
|
|
176
|
+
(a) chrome body 內的 TabsContent 加 \`className="mt-0"\` 把 ownership 轉移給 body
|
|
177
|
+
(對齊 AppShell 前例 app-shell.stories.tsx + Carbon/Radix「panel 單一 owner」benchmark)
|
|
178
|
+
(b) Escape:該行或前一註解行加 \`// @tabs-content-gap-ok: <rationale>\`(顯式 documented 例外)
|
|
179
|
+
|
|
180
|
+
完整 canonical → components/Tabs/tabs.spec.md「出現在 Dialog」段
|
|
181
|
+
+ tokens/layoutSpace/layoutSpace.spec.md 規則 2
|
|
182
|
+
EOF
|
|
183
|
+
exit 2
|
|
@@ -38,7 +38,7 @@ N. <SSOT 是否該抽到更上層 primitive?>
|
|
|
38
38
|
- 必產 3-column cite table(claim / path:line / reasoning)
|
|
39
39
|
|
|
40
40
|
## 🚨 三 invariant — 2026-05-23 user 永久 directive(缺一 = brief 不合格)
|
|
41
|
-
1. **全盤閱讀全部 source**(CLAUDE.md / 31 active M-rules / 5 rules/*.md /
|
|
41
|
+
1. **全盤閱讀全部 source**(CLAUDE.md / 31 active M-rules / 5 rules/*.md / 90 audit dims SKILL.md / 全部 spec.md / 全部 stories / 全部 components / tokens / patterns / hooks / 全部 memory files / planning),**禁憑記憶**(spec/story/component/memory 數量隨時變動,故不硬編數字 — 一律「全部」;M-rule / audit-dim 數由 sync-governance-counters 機械守住)
|
|
42
42
|
2. **Triple-verify per finding**:(a) grep DS-wide 確認 pattern 存在 (b) Read 對應 spec.md / tsx 確認 problem 真存在 (c) 對照既有 canonical 確認非 documented exception。任一 NO → 自動撤回,**禁無病呻吟**
|
|
43
43
|
3. **禁抽樣** — DS-wide ALL files 全掃,context 不夠拆 stage 全跑完;sub-agent admission「I sampled / spot-check / representative」= audit incomplete reject
|
|
44
44
|
```
|
|
@@ -213,8 +213,10 @@ User 2026-05-15 verbatim 抓「DS 深度稽核漏 storybook content quality」+
|
|
|
213
213
|
| 86 | **Plugin manifest + symlink-vs-source 一致**(ds-repo-mode-only)| `node scripts/plugin-structure-validate.mjs`:`.claude-plugin/{plugin,marketplace}.json` valid + 5-manifest version 對齊 + plugin symlink set(skills / hooks / rules)== source(fork `/plugin install` 後拿到完整 governance 不缺漏)。3-allowlist 軸 2:plugin symlink == source;軸 3(mirror 涵蓋 apps/template + 排除 .claude/skills)走 dim 84 |
|
|
214
214
|
| 87 | **Scaffold 可建性 dogfood prepublish gate**(ds-repo-mode-only)| `node scripts/dogfood-prepublish-verify.mjs`:published scaffold 真能 `npm install` + `build-storybook`(consumer 視角,非僅 byte-mirror 比對),prepublish 攔「ship 出去裝不起來」|
|
|
215
215
|
| 88 | **Infra-self ref integrity**(ds-repo-mode-only;2026-05-30 加,根因防線)| `node scripts/check-dangling-infra-ref.mjs --check`(governance docs 引用的 hook/script 必存在磁碟 OR 同行 annotate retired/未實作;認 `check_X.sh`→`_X.sh` lib-consolidation form)+ `node scripts/check-skill-deadref.mjs --check`(禁 `CLAUDE.md line N` brittle ref + 已移除 section deny-list)。錨例 2026-05-30:抓 40 處死 hook ref + env-smoke set-e bug,前無機械防線。M7/M34 broad-vs-narrow gap closed。**2026-05-31 infra-audit 修**:SCAN_DIRS 補 `.claude/memory`(原漏=memory infra ref 零防線)。**已知 P3 限制**:annotation-bypass — 同行含 retired/folded/未實作 字樣即 Bucket A,未驗該 hook 真曾存在(fabricated「(retired)」ref 可繞);需 git-history verify 才全閉,暫接受(adversarial-only 風險,queued #9)|
|
|
216
|
+
| 89 | **Composition-boundary 雙重 spacing(tabsSlot-in-chrome-body)**(2026-07-01,item-anatomy 雙重 padding invariant)| `<TabsContent>` 放進 `DialogBody`/`SheetBody`/`SurfaceBody`(自帶 `pt-/py-tight`)內必 `mt-0` — chrome body 已擁有 header→content gap(規則 2),TabsContent 預設 `mt-tight` 雙重疊加(md 24 / lg 32)。一軸單一 spacing owner(`item-anatomy.spec.md`「垂直 padding 歸屬」)。Hook `check_tabs_content_chrome_body_double_gap.sh` 是 pre-write 層(P0 BLOCKER),本 dim 是 DS-wide sweep。Escape `// @tabs-content-gap-ok:`。SSOT `components/Tabs/tabs.spec.md`「出現在 Dialog」段 |
|
|
217
|
+
| 90 | **Layout-space macro 合規 sweep(judgment 層,DS-src 盲區)**(2026-07-01,user「確保都遵循沒偏移」)| **LLM adversarial per-component**:並列 **區塊/卡片/表單欄位** 之間的 macro 垂直/水平 gap、chrome 水平內距、header→content、content→action button —— 該用 density-scaling **layout-space token** 卻硬寫固定 `gap-N`/`space-y-N`/`p-N`/`px-N`(規則 1-6)。**判準邊界**見 `tokens/layoutSpace/layoutSpace.spec.md`「該用 token vs 刻意固定」段:**只抓 macro 結構違規,禁 flag 合法 magic number**(同質 list 列間距 / micro-icon / 刻意視覺平衡 / 元件內部固定如 FieldGroup)。**每 finding 必對抗二次驗證殺假陽性**(2026-07-01 錨例:sweep 35 raw → 8 confirmed → 逐項覆核剩真違規 Sheet gap-4;over-flag ProfileCard/apps-scaffold/list-gap 被判非違規)。既有 magic-number hook skip DS-src → 本 dim 是唯一 DS-src layout-space judgment 防線(regex 分不出 macro/micro,故靠 LLM judgment 而非硬 hook)。SSOT `layoutSpace.spec.md` 6 規則 + 邊界段 |
|
|
216
218
|
|
|
217
|
-
**Heavy dim(`--deep` mode 各必獨立 sub-agent 跑,不可 batch)**:12 / 24 / 25 / 40 / 41 / 42 / 43 / 45 / 49 / 50 / 51 / 52 / 53。Sub-agent prompt 嚴禁含「SKIP」keyword(per Phase 1 NO-SKIP invariant)。
|
|
219
|
+
**Heavy dim(`--deep` mode 各必獨立 sub-agent 跑,不可 batch)**:12 / 24 / 25 / 40 / 41 / 42 / 43 / 45 / 49 / 50 / 51 / 52 / 53 / 90。Sub-agent prompt 嚴禁含「SKIP」keyword(per Phase 1 NO-SKIP invariant)。
|
|
218
220
|
|
|
219
221
|
---
|
|
220
222
|
|
|
@@ -116,7 +116,7 @@ function PageHeader({ title, rightSlot }: { title: string; rightSlot?: ReactElem
|
|
|
116
116
|
|
|
117
117
|
function DashboardPage() {
|
|
118
118
|
return (
|
|
119
|
-
<div className="px-[var(--layout-space-loose)] py-[var(--layout-space-tight)] space-y-
|
|
119
|
+
<div className="px-[var(--layout-space-loose)] py-[var(--layout-space-tight)] space-y-[var(--layout-space-loose)]">
|
|
120
120
|
<section>
|
|
121
121
|
<h2 className="text-h5 mb-2">Today</h2>
|
|
122
122
|
<p className="text-body text-fg-secondary">
|
|
@@ -124,9 +124,9 @@ function DashboardPage() {
|
|
|
124
124
|
(DataTable / Chart / Card / Stat 等),never modify DS source。
|
|
125
125
|
</p>
|
|
126
126
|
</section>
|
|
127
|
-
<section className="grid grid-cols-3 gap-
|
|
127
|
+
<section className="grid grid-cols-3 gap-[var(--layout-space-loose)]">
|
|
128
128
|
{['Revenue', 'Active customers', 'Pending orders'].map((label) => (
|
|
129
|
-
<div key={label} className="rounded-lg border border-divider bg-surface p-
|
|
129
|
+
<div key={label} className="rounded-lg border border-divider bg-surface p-[var(--layout-space-loose)]">
|
|
130
130
|
<div className="text-caption text-fg-secondary">{label}</div>
|
|
131
131
|
<div className="text-h3 mt-1">—</div>
|
|
132
132
|
</div>
|
package/llms-full.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @qijenchen/design-system — 完整設計參考(llms-full)
|
|
2
2
|
|
|
3
|
-
> 全 component / pattern 的 variants / sizes / 禁止事項。build-time 從 spec.md frontmatter 生成,禁手改。v0.1.0-beta.
|
|
3
|
+
> 全 component / pattern 的 variants / sizes / 禁止事項。build-time 從 spec.md frontmatter 生成,禁手改。v0.1.0-beta.79。
|
|
4
4
|
|
|
5
5
|
# Components
|
|
6
6
|
|
package/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @qijenchen/design-system
|
|
2
2
|
|
|
3
3
|
> World-class React design system(Radix/shadcn + Tailwind v4 + 自訂 design token)。
|
|
4
|
-
> 54 components + 4 public patterns + design tokens。v0.1.0-beta.
|
|
4
|
+
> 54 components + 4 public patterns + design tokens。v0.1.0-beta.79。
|
|
5
5
|
|
|
6
6
|
本檔由 source(spec.md frontmatter + Storybook index)build-time 自動生成,**禁手改**(CI --check drift gate 守)。
|
|
7
7
|
每元件 / pattern 的完整 variants / sizes / 禁止事項 全文見 [llms-full.txt](./llms-full.txt)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qijenchen/design-system",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.79",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "World-class design system — components, patterns, tokens, hooks (single source of truth for team distribution).",
|
|
6
6
|
"type": "module",
|
|
@@ -123,6 +123,11 @@ export interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
123
123
|
* `> 99` 自動顯示 "99+"(交給內部 Badge 的 `max` 行為)。
|
|
124
124
|
*/
|
|
125
125
|
badgeCount?: number
|
|
126
|
+
/**
|
|
127
|
+
* badgeCount overlay 的 aria-label override(i18n)。預設英文 `${badgeCount} unread`;
|
|
128
|
+
* consumer 傳此 prop 本地化(對齊 `Notice` dismissAriaLabel pattern,a11y/i18n)。
|
|
129
|
+
*/
|
|
130
|
+
badgeAriaLabel?: string
|
|
126
131
|
/**
|
|
127
132
|
* 傳入 HoverCard 內容(如 ProfileCard),hover avatar 時自動顯示。
|
|
128
133
|
* 只有人員 avatar 需要傳;實體 avatar(專案、組織)不傳。
|
|
@@ -138,7 +143,7 @@ export interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
138
143
|
// (filter Avatar/PeoplePicker/FieldSurfaceProvider remounts)。
|
|
139
144
|
// code-quality-allow: long-function — size × shape × color × solid × status × badgeCount × hoverCard × img-fallback 多軸 prop 組合,拆 sub-fn 會跨 fn 傳 imgError state + isTableScrolling observer 結果
|
|
140
145
|
const AvatarInner = React.forwardRef<HTMLDivElement, AvatarProps>(
|
|
141
|
-
({ size = 32, shape = 'circle', src, alt, icon: Icon, color = 'neutral', solid = false, status, badgeCount, hoverCard, className, style, ...props }, ref) => {
|
|
146
|
+
({ size = 32, shape = 'circle', src, alt, icon: Icon, color = 'neutral', solid = false, status, badgeCount, badgeAriaLabel, hoverCard, className, style, ...props }, ref) => {
|
|
142
147
|
const [imgError, setImgError] = React.useState(false)
|
|
143
148
|
const documentTheme = useDocumentTheme()
|
|
144
149
|
const isTableScrolling = useTableIsScrolling()
|
|
@@ -277,7 +282,7 @@ const AvatarInner = React.forwardRef<HTMLDivElement, AvatarProps>(
|
|
|
277
282
|
style={{
|
|
278
283
|
boxShadow: `0 0 0 2px var(--surface-raised, var(--canvas))`,
|
|
279
284
|
}}
|
|
280
|
-
aria-label={`${badgeCount} unread`} // i18n-allow: DS default(
|
|
285
|
+
aria-label={badgeAriaLabel ?? `${badgeCount} unread`} // i18n-allow: DS default 英文(consumer 可傳 badgeAriaLabel i18n)
|
|
281
286
|
/>
|
|
282
287
|
)}
|
|
283
288
|
</div>
|
|
@@ -36,7 +36,7 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
|
|
|
36
36
|
return (
|
|
37
37
|
<Dialog {...props}>
|
|
38
38
|
<DialogContent className="overflow-hidden p-0 shadow-[var(--elevation-200)]">
|
|
39
|
-
<Command className="[&_[cmdk-group-heading]]:px-
|
|
39
|
+
<Command className="[&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-3 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-3 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
40
40
|
{children}
|
|
41
41
|
</Command>
|
|
42
42
|
</DialogContent>
|
|
@@ -48,7 +48,7 @@ const CommandInput = React.forwardRef<
|
|
|
48
48
|
React.ElementRef<typeof CommandPrimitive.Input>,
|
|
49
49
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
|
50
50
|
>(({ className, ...props }, ref) => (
|
|
51
|
-
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
|
51
|
+
<div className="flex items-center border-b border-divider px-3" cmdk-input-wrapper="">
|
|
52
52
|
<Search className="mr-2 h-4 w-4 shrink-0 text-fg-muted" />
|
|
53
53
|
<CommandPrimitive.Input
|
|
54
54
|
ref={ref}
|
|
@@ -107,7 +107,7 @@ const CommandGroup = React.forwardRef<
|
|
|
107
107
|
<CommandPrimitive.Group
|
|
108
108
|
ref={ref}
|
|
109
109
|
className={cn(
|
|
110
|
-
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-
|
|
110
|
+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-caption [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-fg-muted",
|
|
111
111
|
className
|
|
112
112
|
)}
|
|
113
113
|
{...props}
|
|
@@ -135,7 +135,7 @@ const CommandItem = React.forwardRef<
|
|
|
135
135
|
<CommandPrimitive.Item
|
|
136
136
|
ref={ref}
|
|
137
137
|
className={cn(
|
|
138
|
-
"relative flex cursor-default gap-2 select-none items-center rounded-md px-
|
|
138
|
+
"relative flex cursor-default gap-2 select-none items-center rounded-md px-3 py-1.5 text-body outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-neutral-hover data-[selected=true]:text-foreground data-[disabled=true]:text-fg-disabled [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
139
139
|
className
|
|
140
140
|
)}
|
|
141
141
|
{...props}
|
|
@@ -365,9 +365,16 @@ export const ListBody = {
|
|
|
365
365
|
* W4 header content row + tabs row flush stack(gap = 0)
|
|
366
366
|
*
|
|
367
367
|
* Anatomy(consumer 寫法):`<Tabs>` wrap 整 DialogContent → `<DialogHeader tabsSlot={<TabsList>}>` →
|
|
368
|
-
* `<DialogBody><TabsContent>...</TabsContent></DialogBody>`
|
|
368
|
+
* `<DialogBody><TabsContent className="mt-0">...</TabsContent></DialogBody>`
|
|
369
369
|
*
|
|
370
370
|
* 注意:TabsContent 必放 DialogBody 內(scroll + chrome padding 由 DialogBody 自管),不要自加 padding。
|
|
371
|
+
* ⚠️ 一軸單一 spacing owner(item-anatomy.spec.md「垂直 padding 歸屬 / 禁雙重 padding」):DialogBody
|
|
372
|
+
* 內層已用 `pt-[var(--layout-space-tight)]` 擁有 header→content 的 chrome gap;而 TabsContent 預設
|
|
373
|
+
* 帶 `mt-[var(--layout-space-tight)]`(tabs.tsx:489,為「緊接 tab bar 下方」而設)。tabsSlot 這條
|
|
374
|
+
* composition 把 tab bar 抬進 header,兩 owner 疊加 = 雙重 tight(md 24 / lg 32,user 2026-07-01 抓)。
|
|
375
|
+
* 故 chrome body 內的 TabsContent **必 `className="mt-0"`** 把 ownership 轉移給 DialogBody(對齊
|
|
376
|
+
* AppShell 前例 app-shell.stories.tsx:337 + tabs.spec.md「出現在 Dialog」段)。Hook
|
|
377
|
+
* `check_tabs_content_chrome_body_double_gap.sh` 機械強制。
|
|
371
378
|
*/
|
|
372
379
|
export const WithTabsInHeader = {
|
|
373
380
|
name: '標頭內含分頁',
|
|
@@ -390,7 +397,8 @@ export const WithTabsInHeader = {
|
|
|
390
397
|
<DialogTitle>專案設定</DialogTitle>
|
|
391
398
|
</DialogHeader>
|
|
392
399
|
<DialogBody>
|
|
393
|
-
|
|
400
|
+
{/* mt-0:DialogBody pt-tight 已擁有 header→content gap,TabsContent 預設 mt-tight 會雙重(見上方 JSDoc)*/}
|
|
401
|
+
<TabsContent value="general" className="mt-0">
|
|
394
402
|
<div className="flex flex-col gap-[var(--layout-space-loose)]">
|
|
395
403
|
<Field>
|
|
396
404
|
<FieldLabel>專案名稱</FieldLabel>
|
|
@@ -402,7 +410,7 @@ export const WithTabsInHeader = {
|
|
|
402
410
|
</Field>
|
|
403
411
|
</div>
|
|
404
412
|
</TabsContent>
|
|
405
|
-
<TabsContent value="members">
|
|
413
|
+
<TabsContent value="members" className="mt-0">
|
|
406
414
|
<div className="flex flex-col gap-3">
|
|
407
415
|
{[
|
|
408
416
|
['Alan Chen', '管理員'],
|
|
@@ -416,7 +424,7 @@ export const WithTabsInHeader = {
|
|
|
416
424
|
))}
|
|
417
425
|
</div>
|
|
418
426
|
</TabsContent>
|
|
419
|
-
<TabsContent value="integrations">
|
|
427
|
+
<TabsContent value="integrations" className="mt-0">
|
|
420
428
|
<div className="flex flex-col gap-3">
|
|
421
429
|
{[
|
|
422
430
|
{ key: 'slack', label: 'Slack 通知', on: true },
|
|
@@ -363,7 +363,7 @@ Field 內的資料輸入控件(Input / NumberInput / DatePicker / Select / Com
|
|
|
363
363
|
- **表單欄位的 label + control + description + error 標準佈局**:登入表單、設定頁、建立對話框
|
|
364
364
|
- **需要 required 星號、disabled 狀態、invalid 驗證的統一行為**
|
|
365
365
|
- **需要 `horizontal` / `vertical` 排版切換**:設定頁常用 horizontal(label 左 / control 右)
|
|
366
|
-
- **多欄位垂直堆疊**:搭配 `FieldGroup`
|
|
366
|
+
- **多欄位垂直堆疊**:搭配 `FieldGroup` 統一管理欄位垂直間距(三級固定值 `compact` / `normal` / `loose`,刻意不隨 density 縮放;具體 gap map 見上方 gap 表)
|
|
367
367
|
|
|
368
368
|
## 何時不用
|
|
369
369
|
|
|
@@ -299,9 +299,12 @@ export const radioGroupMeta = {
|
|
|
299
299
|
|
|
300
300
|
},
|
|
301
301
|
sizes: {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
// iconSize = 渲染指示器尺寸(對齊 checkbox/switch meta 慣例:checkbox iconSize=真 Check glyph 12/12/16)。
|
|
303
|
+
// Radio 指示器是 filled dot 非 glyph,真值 = dotSize 8/8/10(radio-group.tsx:179);
|
|
304
|
+
// 控件框 16/16/20 與 Checkbox 對齊但那是 box 非「指示器」,不入此鍵(避免 metadata 語意 drift)。
|
|
305
|
+
sm: { fieldHeight: 28, iconSize: 8, typography: 'body' },
|
|
306
|
+
md: { fieldHeight: 32, iconSize: 8, typography: 'body' },
|
|
307
|
+
lg: { fieldHeight: 36, iconSize: 10, typography: 'body-lg' },
|
|
305
308
|
},
|
|
306
309
|
states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],
|
|
307
310
|
tokens: {
|
|
@@ -102,7 +102,7 @@ export const UsageGuidance: Story = {
|
|
|
102
102
|
</SheetHeader>
|
|
103
103
|
{/* Body 必用 SheetBody(內建 ScrollArea + 浮層 padding SSOT px-loose/pt-tight/pb-bottom);
|
|
104
104
|
自組 ScrollArea + py-4 = 水平 0 padding 歷史 bug,詳 sheet.tsx SheetBody comment。 */}
|
|
105
|
-
<SheetBody className="flex flex-col gap-
|
|
105
|
+
<SheetBody className="flex flex-col gap-[var(--layout-space-loose)]">
|
|
106
106
|
<Field>
|
|
107
107
|
<FieldLabel>名稱</FieldLabel>
|
|
108
108
|
<Input defaultValue="產品路線圖" />
|
|
@@ -317,7 +317,7 @@ export const HeaderFooterStructureRule: Story = {
|
|
|
317
317
|
<SheetDescription>填寫基本資料後可進行付款設定</SheetDescription>
|
|
318
318
|
</SheetHeader>
|
|
319
319
|
{/* Body 必用 SheetBody(內建 ScrollArea + 浮層 padding SSOT) */}
|
|
320
|
-
<SheetBody className="flex flex-col gap-
|
|
320
|
+
<SheetBody className="flex flex-col gap-[var(--layout-space-loose)]">
|
|
321
321
|
<Field>
|
|
322
322
|
<FieldLabel>公司名稱</FieldLabel>
|
|
323
323
|
<Input placeholder="輸入公司名稱" />
|
|
@@ -186,7 +186,7 @@ const SheetTitle = React.forwardRef<
|
|
|
186
186
|
>(({ className, ...props }, ref) => (
|
|
187
187
|
<SheetPrimitive.Title
|
|
188
188
|
ref={ref}
|
|
189
|
-
className={cn("text-body-lg font-medium truncate
|
|
189
|
+
className={cn("text-body-lg font-medium truncate", className)}
|
|
190
190
|
{...props}
|
|
191
191
|
/>
|
|
192
192
|
))
|