@qijenchen/design-system 0.1.0-beta.69 → 0.1.0-beta.70
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/ds-canonical/fork/governance.lock +112 -0
- package/ds-canonical/fork/hooks/__pycache__/block_prototype_imports.cpython-312.pyc +0 -0
- package/ds-canonical/fork/hooks/block_prototype_imports.py +111 -0
- package/ds-canonical/fork/hooks/check_chrome_header_avatar_canonical.sh +95 -0
- package/ds-canonical/fork/hooks/check_consumer_app_invariants.sh +349 -0
- package/ds-canonical/fork/hooks/check_ds_anchor_preflight.sh +132 -0
- package/ds-canonical/fork/hooks/check_escape_marker_abuse.sh +140 -0
- package/ds-canonical/fork/hooks/check_field_family_invariants.sh +250 -0
- package/ds-canonical/fork/hooks/check_fork_product_quality.sh +36 -0
- package/ds-canonical/fork/hooks/check_item_list_gap.sh +54 -0
- package/ds-canonical/fork/hooks/check_layout_space_magic_numbers.sh +96 -0
- package/ds-canonical/fork/hooks/check_opacity_token_usage.sh +149 -0
- package/ds-canonical/fork/hooks/check_pattern_invariants.sh +196 -0
- package/ds-canonical/fork/hooks/check_sidebar_menu_button_implicit_wrap.sh +86 -0
- package/ds-canonical/fork/hooks/check_tailwind_wildcard_in_docs.sh +79 -0
- package/ds-canonical/fork/hooks/inject_deploy_url_after_push.sh +238 -0
- package/ds-canonical/fork/launchers/fork-governance-dispatcher.sh +44 -0
- package/ds-canonical/fork/launchers/inject_fork_governance_preamble.sh +46 -0
- package/ds-canonical/fork/launchers/settings-hooks.json +58 -0
- package/ds-canonical/fork/manifest.json +79 -0
- package/ds-canonical/fork/preamble.md +495 -0
- package/ds-canonical/hooks/check_consumer_app_invariants.sh +19 -0
- package/ds-canonical/references/scenario-definition.md +4 -4
- package/ds-canonical/skills/design-system-audit/SKILL.md +1 -1
- package/llms-full.txt +1 -1
- package/llms.txt +1 -1
- package/package.json +3 -1
- package/src/story-governance/category-matrix.json +132 -0
- package/src/tokens/utility-registry.json +124 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_layout_space_magic_numbers.sh — P0 BLOCKER
|
|
3
|
+
#
|
|
4
|
+
# 偵測 consumer / DS app code 用 Tailwind spacing magic numbers
|
|
5
|
+
# (`p-4` / `px-6` / `py-2` / `gap-3` 等)而非 layoutSpace token
|
|
6
|
+
# (`p-[var(--layout-space-N) N∈{loose,tight}]` / `gap-[var(--layout-space-N) N∈{loose,tight}]`)。
|
|
7
|
+
# 2026-05-27 user verbatim「機械無強制就不會做?那為何不全部 ssot 都要強制吻合?」
|
|
8
|
+
# 永久 codify — SSOT canonical 必 P0 BLOCKER,不分級。
|
|
9
|
+
#
|
|
10
|
+
# Anchor:user 質疑「content 自動繼承 layoutSpace SSOT 嗎?」
|
|
11
|
+
# - app-shell.spec.md:205 明文 `<main>` landmark padding=0 (intentional)
|
|
12
|
+
# - app-shell.spec.md:207-212 consumer 必遵循 layoutSpace.spec.md 6 條規則 + 親疏 3 級
|
|
13
|
+
#
|
|
14
|
+
# PostToolUse Edit/Write detect magic Tailwind spacing → P0 BLOCKER exit 2
|
|
15
|
+
# 強制改 token OR 加 `// @layout-space-magic-ok: <rationale>` escape comment(per-line)。
|
|
16
|
+
|
|
17
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
22
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
23
|
+
|
|
24
|
+
case "${TOOL:-}" in
|
|
25
|
+
Edit|Write|MultiEdit) ;;
|
|
26
|
+
*) exit 0 ;;
|
|
27
|
+
esac
|
|
28
|
+
|
|
29
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.notebook_path // ""' 2>/dev/null)
|
|
30
|
+
|
|
31
|
+
# Only check .tsx / .ts in app code
|
|
32
|
+
if ! echo "$FILE" | grep -qE '\.(tsx|ts)$'; then exit 0; fi
|
|
33
|
+
# Skip DS source (DS components have their own spacing logic via cva)
|
|
34
|
+
if echo "$FILE" | grep -qE 'packages/design-system/src/|node_modules/'; then exit 0; fi
|
|
35
|
+
|
|
36
|
+
# Get new content
|
|
37
|
+
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
|
|
38
|
+
[ -z "$NEW_CONTENT" ] && exit 0
|
|
39
|
+
|
|
40
|
+
# Escape clause:single line escape comment per line of magic number usage
|
|
41
|
+
# `// @layout-space-magic-ok: <rationale>` immediately above the line OR on same line
|
|
42
|
+
ESCAPE_MARKER='@layout-space-magic-ok:'
|
|
43
|
+
|
|
44
|
+
# Detect magic spacing classes (Tailwind class strings only — NOT JSX props like size={24})
|
|
45
|
+
# Match per line so we can check per-line escape comments
|
|
46
|
+
MAGIC_LINES=$(echo "$NEW_CONTENT" | grep -nE '\b(p|px|py|pt|pb|pl|pr|gap|space-x|space-y|m|mx|my|mt|mb|ml|mr)-(0\.5|[1-9][0-9]?(\.[0-9])?)\b')
|
|
47
|
+
|
|
48
|
+
if [ -z "$MAGIC_LINES" ]; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Filter out lines with escape marker on same line OR immediately preceding line
|
|
53
|
+
# 2026-06-03 修(doc-vs-code bug,M32):L41 文件宣稱支援「preceding line OR same line」,
|
|
54
|
+
# 但原 code 只檢查同行 → JSX className 行無法放同行 `//` comment(會破壞 JSX)→ escape 對 JSX
|
|
55
|
+
# 實質失效。補實作前一行檢查(grep -n 行號 → sed 取前一行),對齊文件 + 解 JSX 必需。
|
|
56
|
+
UNJUSTIFIED=""
|
|
57
|
+
while IFS= read -r line; do
|
|
58
|
+
# same-line marker
|
|
59
|
+
if echo "$line" | grep -qF "$ESCAPE_MARKER"; then continue; fi
|
|
60
|
+
# preceding-line marker(JSX `{/* @layout-space-magic-ok: ... */}` 在上一行)。
|
|
61
|
+
# 對齊 ESLint disable-next-line 慣例:前一行 marker 僅在該行是「註解專用行」(trimmed 開頭
|
|
62
|
+
# //、{/*、/*、*)時生效 — 否則上一行 code 的「同行 escape」會誤串到下一行(P8 conflict)。
|
|
63
|
+
lineno="${line%%:*}"
|
|
64
|
+
if [ "$lineno" -gt 1 ] 2>/dev/null; then
|
|
65
|
+
prev=$(echo "$NEW_CONTENT" | sed -n "$((lineno-1))p")
|
|
66
|
+
if echo "$prev" | grep -qF "$ESCAPE_MARKER" && echo "$prev" | grep -qE '^[[:space:]]*(//|\{?/\*|\*)'; then continue; fi
|
|
67
|
+
fi
|
|
68
|
+
UNJUSTIFIED="${UNJUSTIFIED}${line}\n"
|
|
69
|
+
done <<< "$MAGIC_LINES"
|
|
70
|
+
|
|
71
|
+
if [ -z "$UNJUSTIFIED" ]; then
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
cat >&2 << EOF
|
|
76
|
+
🚨 LAYOUT-SPACE-MAGIC-NUMBER BLOCKER(P0,2026-05-27 user verbatim「機械無強制就不會做?
|
|
77
|
+
為何不全部 ssot 都要強制吻合?」永久 codify)
|
|
78
|
+
|
|
79
|
+
Detected Tailwind spacing magic numbers in $FILE without escape:
|
|
80
|
+
$(echo -e "$UNJUSTIFIED" | sed 's/^/ /' | head -10)
|
|
81
|
+
|
|
82
|
+
per app-shell.spec.md L205-219 + layoutSpace.spec.md SSOT:consumer content 必遵循
|
|
83
|
+
layoutSpace 6 條規則 + 親疏 3 級,**禁** 硬寫 Tailwind magic numbers。改用:
|
|
84
|
+
p-[var(--layout-space-loose)] /* 16px 規則 1A/1B chrome / wrap */
|
|
85
|
+
p-[var(--layout-space-tight)] /* 12px 規則 3 親 gap */
|
|
86
|
+
gap-[var(--layout-space-distant)] /* 24px 規則 3 疏 gap */
|
|
87
|
+
space-y-[var(--layout-space-distant)]
|
|
88
|
+
|
|
89
|
+
修法 2 選 1:
|
|
90
|
+
(a) 改 token:換成 var(--layout-space-N) N∈{loose,tight} family per 6 規則 + 親疏 3 級
|
|
91
|
+
(b) Escape:在該 line 加 \`// @layout-space-magic-ok: <rationale>\` 顯式 documented
|
|
92
|
+
(eg.「\`gap-1\` 是 4px stack icon — non-spacing context,not consumer layout」)
|
|
93
|
+
|
|
94
|
+
完整 6 條規則 → packages/design-system/src/tokens/layoutSpace/layoutSpace.spec.md
|
|
95
|
+
EOF
|
|
96
|
+
exit 2
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
# Tailwind token registry enforcement hook(2026-05-18 升級 per audit Dim 47 + codex P1-5):
|
|
4
|
+
# 原 hook 名 `check_opacity_token_usage.sh`(keep filename for settings.json registration),
|
|
5
|
+
# 實際 logic 已升級成 full Tailwind utility registry compliance(per Dim 47 SSOT)。
|
|
6
|
+
#
|
|
7
|
+
# SSOT source:`node_modules/@qijenchen/design-system/src/tokens/utility-registry.json`(block list per category)。
|
|
8
|
+
# 對齊 Atlassian @atlaskit/tokens / Carbon @carbon/themes / Ant ConfigProvider / Polaris polaris-tokens
|
|
9
|
+
# token-first lint enforcement。
|
|
10
|
+
#
|
|
11
|
+
# 檢的 block category:
|
|
12
|
+
# - opacity:opacity-{5..95} numeric tier(原 logic,保留)
|
|
13
|
+
# - typography:text-{xs..9xl} / font-{thin|light|semibold|black} / leading-{N} numeric / tracking-{wide..widest|tighter|tight}
|
|
14
|
+
# - radius:rounded-{xl|2xl|3xl} / rounded(unscoped)
|
|
15
|
+
# - elevation:shadow-{sm|md|lg|xl|2xl|inner} / shadow(unscoped)
|
|
16
|
+
# - shadcn alias:bg-popover / text-muted-foreground / bg-accent 等
|
|
17
|
+
# - primitive 色名:bg-neutral-N / text-blue-N 等(越過 semantic 層)
|
|
18
|
+
#
|
|
19
|
+
# 例外:utility-registry.json `_meta.exceptions` 段定義的 anatomy stories / principles code blocks 豁免
|
|
20
|
+
# (本 hook 已 skip *.anatomy.stories.tsx / *.principles.stories.tsx 不檢)
|
|
21
|
+
#
|
|
22
|
+
# 修法:reuse semantic utility / token reference / var() bracket(詳 utility-registry.json `rationale_path`)。
|
|
23
|
+
|
|
24
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
25
|
+
|
|
26
|
+
INPUT=$(cat)
|
|
27
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
28
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
29
|
+
|
|
30
|
+
case "$TOOL" in
|
|
31
|
+
Edit|Write|MultiEdit) ;;
|
|
32
|
+
*) exit 0 ;;
|
|
33
|
+
esac
|
|
34
|
+
|
|
35
|
+
# Scope:.tsx + .css(DS source);skip stories / tests / token spec 自家 / anatomy/principles per registry exception
|
|
36
|
+
case "$FILE_PATH" in
|
|
37
|
+
*.tsx|*.css) ;;
|
|
38
|
+
*) exit 0 ;;
|
|
39
|
+
esac
|
|
40
|
+
case "$FILE_PATH" in
|
|
41
|
+
# 註解:anatomy + principles stories per utility-registry.json `_meta.exceptions` 豁免
|
|
42
|
+
*.anatomy.stories.tsx|*.principles.stories.tsx) exit 0 ;;
|
|
43
|
+
*.stories.tsx|*.test.*|*.spec.tsx) exit 0 ;;
|
|
44
|
+
*tokens/opacity/*|*tokens/typography/*|*tokens/radius/*|*tokens/elevation/*|*tokens/color/*) exit 0 ;;
|
|
45
|
+
esac
|
|
46
|
+
|
|
47
|
+
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""')
|
|
48
|
+
[ -z "$NEW_CONTENT" ] && exit 0
|
|
49
|
+
|
|
50
|
+
# Resolve registry path(absolute via $CLAUDE_PROJECT_DIR or relative fallback)
|
|
51
|
+
REGISTRY="${CLAUDE_PROJECT_DIR:-}/node_modules/@qijenchen/design-system/src/tokens/utility-registry.json"
|
|
52
|
+
if [ ! -f "$REGISTRY" ]; then
|
|
53
|
+
# Fallback: try relative from hook dir
|
|
54
|
+
REGISTRY="$(dirname "$0")/../../node_modules/@qijenchen/design-system/src/tokens/utility-registry.json"
|
|
55
|
+
fi
|
|
56
|
+
if [ ! -f "$REGISTRY" ]; then
|
|
57
|
+
# 沒 registry → fall back 到原 opacity-only logic(避免 hook 啞掉)
|
|
58
|
+
HITS=$(echo "$NEW_CONTENT" | grep -oE "opacity-[0-9]+" | grep -vE "^opacity-(0|100)$" | sort -u || true)
|
|
59
|
+
if [ -n "$HITS" ]; then
|
|
60
|
+
echo "" >&2
|
|
61
|
+
echo "⚠️ M23 violation(opacity tier;registry 不可用,fallback mode):" >&2
|
|
62
|
+
echo "$HITS" | sed 's/^/ /' >&2
|
|
63
|
+
echo "" >&2
|
|
64
|
+
echo " reuse opacity-disabled / alpha 色階。詳 tokens/opacity/opacity.spec.md" >&2
|
|
65
|
+
fi
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Aggregate violations across all categories
|
|
70
|
+
VIOLATIONS=""
|
|
71
|
+
|
|
72
|
+
# 1) Opacity numeric tier(opacity-5..95,除 0/100/disabled)
|
|
73
|
+
HITS_OPACITY=$(echo "$NEW_CONTENT" | grep -oE "\bopacity-[0-9]+\b" | grep -vE "^opacity-(0|100)$" | sort -u || true)
|
|
74
|
+
if [ -n "$HITS_OPACITY" ]; then
|
|
75
|
+
VIOLATIONS="${VIOLATIONS}\n [opacity] $(echo "$HITS_OPACITY" | tr '\n' ' ')"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# 2) Typography raw size(text-xs..9xl)— skip semantic text-h*/text-body/text-caption/text-helper/text-label
|
|
79
|
+
HITS_TEXT=$(echo "$NEW_CONTENT" | grep -oE "\btext-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)\b" | sort -u || true)
|
|
80
|
+
if [ -n "$HITS_TEXT" ]; then
|
|
81
|
+
VIOLATIONS="${VIOLATIONS}\n [typography size] $(echo "$HITS_TEXT" | tr '\n' ' ')"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# 3) Typography raw weight
|
|
85
|
+
HITS_FONT=$(echo "$NEW_CONTENT" | grep -oE "\bfont-(thin|extralight|light|semibold|extrabold|black)\b" | sort -u || true)
|
|
86
|
+
if [ -n "$HITS_FONT" ]; then
|
|
87
|
+
VIOLATIONS="${VIOLATIONS}\n [typography weight] $(echo "$HITS_FONT" | tr '\n' ' ')"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# 4) Leading numeric(leading-N where N is digit;skip leading-compact/normal/none/tight/snug/relaxed/loose)
|
|
91
|
+
HITS_LEADING=$(echo "$NEW_CONTENT" | grep -oE "\bleading-[0-9]+\b" | sort -u || true)
|
|
92
|
+
if [ -n "$HITS_LEADING" ]; then
|
|
93
|
+
VIOLATIONS="${VIOLATIONS}\n [leading numeric] $(echo "$HITS_LEADING" | tr '\n' ' ')"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# 5) Tracking raw(skip tracking-shortcut canonical token)
|
|
97
|
+
HITS_TRACKING=$(echo "$NEW_CONTENT" | grep -oE "\btracking-(tighter|tight|wide|wider|widest)\b" | sort -u || true)
|
|
98
|
+
if [ -n "$HITS_TRACKING" ]; then
|
|
99
|
+
VIOLATIONS="${VIOLATIONS}\n [tracking raw] $(echo "$HITS_TRACKING" | tr '\n' ' ')"
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# 6) Radius out-of-range
|
|
103
|
+
HITS_RADIUS=$(echo "$NEW_CONTENT" | grep -oE "\brounded-(xl|2xl|3xl)\b" | sort -u || true)
|
|
104
|
+
if [ -n "$HITS_RADIUS" ]; then
|
|
105
|
+
VIOLATIONS="${VIOLATIONS}\n [radius out-of-range] $(echo "$HITS_RADIUS" | tr '\n' ' ')"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# 7) Shadow Tailwind size(shadow-sm/md/lg/xl/2xl/inner — DS 用 shadow-[var(--elevation-N)] N∈{100,200})
|
|
109
|
+
HITS_SHADOW=$(echo "$NEW_CONTENT" | grep -oE "\bshadow-(sm|md|lg|xl|2xl|inner)\b" | sort -u || true)
|
|
110
|
+
if [ -n "$HITS_SHADOW" ]; then
|
|
111
|
+
VIOLATIONS="${VIOLATIONS}\n [elevation size] $(echo "$HITS_SHADOW" | tr '\n' ' ')"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# 8) Shadcn compat alias(bg-popover / text-muted-foreground 等)
|
|
115
|
+
HITS_SHADCN=$(echo "$NEW_CONTENT" | grep -oE "\b(bg-popover|text-popover-foreground|text-muted-foreground|bg-accent|text-accent-foreground|bg-destructive|bg-background|text-background|border-input)\b" | sort -u || true)
|
|
116
|
+
if [ -n "$HITS_SHADCN" ]; then
|
|
117
|
+
VIOLATIONS="${VIOLATIONS}\n [shadcn alias] $(echo "$HITS_SHADCN" | tr '\n' ' ')"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# 9) Primitive 色名作 utility(bg-neutral-N / text-blue-N 等)
|
|
121
|
+
HITS_PRIMITIVE=$(echo "$NEW_CONTENT" | grep -oE "\b(bg|text|border)-(neutral|blue|red|green|yellow|orange|purple|pink|cyan|teal|deep-orange|deep-purple|light-blue|light-green|lime|amber|indigo|brown|gray)-[0-9]+\b" | sort -u || true)
|
|
122
|
+
if [ -n "$HITS_PRIMITIVE" ]; then
|
|
123
|
+
VIOLATIONS="${VIOLATIONS}\n [primitive color as utility] $(echo "$HITS_PRIMITIVE" | tr '\n' ' ')"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
if [ -n "$VIOLATIONS" ]; then
|
|
127
|
+
cat >&2 <<'EOF_HEAD'
|
|
128
|
+
|
|
129
|
+
⚠️ M23 / Dim 47 violation:Tailwind utility 繞 SSOT registry。
|
|
130
|
+
EOF_HEAD
|
|
131
|
+
echo -e "$VIOLATIONS" >&2
|
|
132
|
+
cat >&2 <<'EOF_BODY'
|
|
133
|
+
|
|
134
|
+
SSOT:node_modules/@qijenchen/design-system/src/tokens/utility-registry.json(block list per category)
|
|
135
|
+
修法 quick map:
|
|
136
|
+
text-xs..9xl → text-h1..h6 / text-body / text-caption / text-helper / text-label
|
|
137
|
+
font-thin/...black → font-normal / font-medium / font-bold
|
|
138
|
+
leading-{N} → leading-compact / leading-normal
|
|
139
|
+
tracking-* → tracking-shortcut(or codify role-specific semantic utility)
|
|
140
|
+
rounded-xl.. → rounded-xs..rounded-lg / rounded-full
|
|
141
|
+
shadow-sm.. → shadow-[var(--elevation-100)] / shadow-[var(--elevation-200)]
|
|
142
|
+
bg-popover .. → direct semantic token(--surface-raised / --fg-muted / --error 等)
|
|
143
|
+
bg-neutral-N → semantic utility(bg-surface 等)或 bg-[var(--color-neutral-N)]
|
|
144
|
+
|
|
145
|
+
詳 utility-registry.json `_meta.spec_sources` + M23「DS 內既有 canonical 優先」。
|
|
146
|
+
EOF_BODY
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
exit 0
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Pattern invariants unified hook(2026-05-08 cluster C consolidation)
|
|
3
|
+
#
|
|
4
|
+
# Merges 4 PreToolUse hooks(原各檔已 retire,合併入此):
|
|
5
|
+
# C.1 overlay panel scroll chain(原 check_overlay_panel_scroll_chain,P1 WARN stderr)
|
|
6
|
+
# C.2 inline-action canonical gap(原 check_inline_action_canonical_gap,P1 WARN stderr)
|
|
7
|
+
# C.3 primitive wrapper padding(原 check_primitive_wrapper_padding,P0 BLOCK exit 2)
|
|
8
|
+
# C.4 row slot handcraft(原 check_row_slot_handcraft,P0 BLOCK exit 2)
|
|
9
|
+
#
|
|
10
|
+
# Why merge:皆 element-anatomy / overlay-surface SSOT 消費紀律 invariant,共用 INPUT
|
|
11
|
+
# parsing + tsx filter,散裝是 M17 + Anthropic ≤ 15 hook best-practice 偏離。
|
|
12
|
+
#
|
|
13
|
+
# Exit precedence:BLOCK(2)> WARN-stderr(0)。每 rule 獨立 fire,worst 勝。
|
|
14
|
+
#
|
|
15
|
+
# Per-rule allowlist:
|
|
16
|
+
# C.1: `// @scroll-chain-allow: <reason>`(any line)
|
|
17
|
+
# C.2: `// @inline-action-gap-allow: <reason>`(any line)
|
|
18
|
+
# C.3: `// @primitive-padding-allow: <reason>`(檔案前 5 行)
|
|
19
|
+
# C.4: `// @row-slot-handcraft-allow: <reason>`(any line)
|
|
20
|
+
|
|
21
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
22
|
+
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
INPUT=$(cat)
|
|
26
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
27
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
28
|
+
|
|
29
|
+
case "$TOOL" in
|
|
30
|
+
Edit|Write|MultiEdit) ;;
|
|
31
|
+
*) exit 0 ;;
|
|
32
|
+
esac
|
|
33
|
+
|
|
34
|
+
case "$FILE_PATH" in
|
|
35
|
+
*.tsx) ;;
|
|
36
|
+
*) exit 0 ;;
|
|
37
|
+
esac
|
|
38
|
+
|
|
39
|
+
NEW_CONTENT=$(echo "$INPUT" | jq -r '
|
|
40
|
+
(.tool_input.content // "") + "\n" +
|
|
41
|
+
(.tool_input.new_string // "") + "\n" +
|
|
42
|
+
([.tool_input.edits[]? | .new_string] | join("\n"))
|
|
43
|
+
' 2>/dev/null || echo "")
|
|
44
|
+
|
|
45
|
+
[ -z "${NEW_CONTENT//[[:space:]]/}" ] && exit 0
|
|
46
|
+
|
|
47
|
+
WORST=0
|
|
48
|
+
record_worst() { local lvl=$1; [ "$lvl" -gt "$WORST" ] && WORST=$lvl; }
|
|
49
|
+
|
|
50
|
+
# ── C.1 overlay panel scroll chain(P1 WARN stderr only)──────────────────────
|
|
51
|
+
if ! echo "$NEW_CONTENT" | grep -q '@scroll-chain-allow' \
|
|
52
|
+
&& echo "$NEW_CONTENT" | grep -q '<SurfaceBody'; then
|
|
53
|
+
SUSPECT_C1=$(printf '%s' "$NEW_CONTENT" | awk '
|
|
54
|
+
/^[[:space:]]*<div[[:space:]]/ {
|
|
55
|
+
line = $0
|
|
56
|
+
while (index(line, ">") == 0 && (getline next_line) > 0) line = line " " next_line
|
|
57
|
+
if (line ~ /w-\[/ && (line !~ /flex[^"]*flex-col/ || line !~ /h-full/ || line !~ /min-h-0/)) {
|
|
58
|
+
for (k = 0; k < 30 && (getline next_line) > 0; k++) {
|
|
59
|
+
if (next_line ~ /<SurfaceBody/) {
|
|
60
|
+
print "[panel root w-[...] 缺 flex flex-col h-full min-h-0]: " substr(line, 1, 100)
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
if (next_line ~ /<\/div>/) break
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
')
|
|
68
|
+
if [ -n "$SUSPECT_C1" ]; then
|
|
69
|
+
cat >&2 <<EOF
|
|
70
|
+
|
|
71
|
+
┄┄┄ C.1 check_pattern_invariants — overlay scroll chain WARN ┄┄┄
|
|
72
|
+
|
|
73
|
+
[P1] ${FILE_PATH}
|
|
74
|
+
${SUSPECT_C1}
|
|
75
|
+
|
|
76
|
+
⚠️ M25 canonical:Popover/HoverCard/Dialog/Sheet viewport-aware scroll 要求 root → SurfaceBody
|
|
77
|
+
所有中間 wrapper forward \`flex flex-col h-full\`。斷鏈 → SurfaceBody flex-1 失效。
|
|
78
|
+
|
|
79
|
+
修法:wrapper className 加 \`flex flex-col h-full\`(最小組合 \`flex flex-col h-full min-h-0\`)
|
|
80
|
+
詳 patterns/overlay-surface/overlay-surface.spec.md「Viewport-aware scroll chain invariant」/ M25
|
|
81
|
+
例外:行尾 \`// @scroll-chain-allow: <reason>\`
|
|
82
|
+
|
|
83
|
+
EOF
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# ── C.2 inline-action canonical gap(P1 WARN stderr only)─────────────────────
|
|
88
|
+
case "$FILE_PATH" in
|
|
89
|
+
*components/*.tsx|*patterns/*.tsx)
|
|
90
|
+
case "$FILE_PATH" in
|
|
91
|
+
*/item-anatomy.tsx|*.stories.tsx|*.test.*) ;; # SSOT/test skip
|
|
92
|
+
*)
|
|
93
|
+
if ! echo "$NEW_CONTENT" | grep -q '@inline-action-gap-allow' \
|
|
94
|
+
&& echo "$NEW_CONTENT" | grep -qE '<ItemInlineAction(Button)?\b|DropdownMenuTrigger.*ItemInlineAction'; then
|
|
95
|
+
WRONG_GAP=$(echo "$NEW_CONTENT" | grep -nE 'className=.*\bgap-(1|3|4|5|6|8|10|12)\b' | head -3 || true)
|
|
96
|
+
if [ -n "$WRONG_GAP" ]; then
|
|
97
|
+
cat >&2 <<EOF
|
|
98
|
+
|
|
99
|
+
┄┄┄ C.2 check_pattern_invariants — inline-action gap WARN ┄┄┄
|
|
100
|
+
|
|
101
|
+
[P1] ${FILE_PATH}
|
|
102
|
+
ItemInlineAction* consumer 用非 \`gap-2\` 的 gap class:
|
|
103
|
+
${WRONG_GAP}
|
|
104
|
+
|
|
105
|
+
⚠️ inline-action.spec.md:80:inline-action 跟 sibling gap 必 \`gap-2\`(8px)。
|
|
106
|
+
12px = --table-cell-px(cell L/R padding,不是 inline-action gap)。
|
|
107
|
+
|
|
108
|
+
修法:className gap → gap-2 / 加 \`// @inline-action-gap-allow: <reason>\` 行尾豁免
|
|
109
|
+
|
|
110
|
+
EOF
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
;;
|
|
114
|
+
esac
|
|
115
|
+
;;
|
|
116
|
+
esac
|
|
117
|
+
|
|
118
|
+
# ── C.3 primitive wrapper padding(P0 BLOCK exit 2)───────────────────────────
|
|
119
|
+
# File-level allowlist:檔頭前 5 行
|
|
120
|
+
ALLOW_C3=0
|
|
121
|
+
FIRST_LINES_NEW=$(printf '%s\n' "$NEW_CONTENT" | sed -n '1,5p')
|
|
122
|
+
echo "$FIRST_LINES_NEW" | grep -qE '//[[:space:]]*@primitive-padding-allow:' && ALLOW_C3=1
|
|
123
|
+
if [ -f "$FILE_PATH" ] && [ "$ALLOW_C3" = "0" ]; then
|
|
124
|
+
ON_DISK_FIRST=$(sed -n '1,5p' "$FILE_PATH" 2>/dev/null || true)
|
|
125
|
+
echo "$ON_DISK_FIRST" | grep -qE '//[[:space:]]*@primitive-padding-allow:' && ALLOW_C3=1
|
|
126
|
+
fi
|
|
127
|
+
if [ "$ALLOW_C3" = "0" ]; then
|
|
128
|
+
PRIMITIVES_REGEX='DateGrid|Calendar|Surface|SurfaceHeader|SurfaceBody|SurfaceFooter'
|
|
129
|
+
VIOLATIONS_C3=$(printf '%s' "$NEW_CONTENT" | perl -0777 -ne '
|
|
130
|
+
while (/<div\b[^>]*className\s*=\s*["\x27`][^"\x27`]*\bp-\d+[^"\x27`]*["\x27`][^>]*>(?:[^<]*<(?!\/div\b)){0,8}\s*<('"$PRIMITIVES_REGEX"')\b/gs) {
|
|
131
|
+
my $primitive = $1;
|
|
132
|
+
my $offset = $-[0];
|
|
133
|
+
my $before = substr($_, 0, $offset);
|
|
134
|
+
my $line = ($before =~ tr/\n//) + 1;
|
|
135
|
+
print "line $line: <div className=\"...p-X...\"> wrapping <$primitive>\n";
|
|
136
|
+
}
|
|
137
|
+
')
|
|
138
|
+
if [ -n "$VIOLATIONS_C3" ]; then
|
|
139
|
+
cat >&2 <<EOF
|
|
140
|
+
|
|
141
|
+
┄┄┄ C.3 check_pattern_invariants — primitive wrapper padding BLOCKER ┄┄┄
|
|
142
|
+
|
|
143
|
+
[P0] ${FILE_PATH}
|
|
144
|
+
${VIOLATIONS_C3}
|
|
145
|
+
|
|
146
|
+
SSOT primitive 自帶 outer padding,**consumer 不得另加 padding wrapper**:
|
|
147
|
+
- DateGrid / Calendar:root 自帶 p-3
|
|
148
|
+
- Surface{Header,Body,Footer}:各 segment 自帶 padding
|
|
149
|
+
|
|
150
|
+
修法:直接放 primitive,不包 padding div。
|
|
151
|
+
❌ <div className="p-2"><DateGrid /></div>
|
|
152
|
+
✅ <DateGrid />
|
|
153
|
+
|
|
154
|
+
整檔豁免:檔頭前 5 行加 \`// @primitive-padding-allow: <reason>\`(需 spec rationale)。
|
|
155
|
+
詳 mindset #2 + M1 SSOT 消費。
|
|
156
|
+
|
|
157
|
+
EOF
|
|
158
|
+
record_worst 2
|
|
159
|
+
fi
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# ── C.4 row slot handcraft(P0 BLOCK exit 2)──────────────────────────────────
|
|
163
|
+
case "$FILE_PATH" in
|
|
164
|
+
*components/*.tsx|*patterns/*.tsx)
|
|
165
|
+
case "$FILE_PATH" in
|
|
166
|
+
*/item-anatomy.tsx|*/field-wrapper.tsx|*.stories.tsx|*.test.*|*.spec.tsx) ;; # SSOT/test skip
|
|
167
|
+
*)
|
|
168
|
+
# 2026-05-30(dim 39 M7/M34 fix):order-INDEPENDENT — extract className attrs,require ALL 4 tokens
|
|
169
|
+
# present(natural Tailwind 序 `flex items-center gap-2 shrink-0 h-[1lh]` 之前漏抓)。BSD/GNU grep 通用。
|
|
170
|
+
if ! echo "$NEW_CONTENT" | grep -q '@row-slot-handcraft-allow' \
|
|
171
|
+
&& echo "$NEW_CONTENT" | grep -oE 'class(Name)?="[^"]*"' | grep -F 'h-[1lh]' | grep -F 'shrink-0' | grep -F 'flex' | grep -F 'items-center' >/dev/null 2>&1; then
|
|
172
|
+
cat >&2 <<EOF
|
|
173
|
+
|
|
174
|
+
┄┄┄ C.4 check_pattern_invariants — row slot handcraft BLOCKER ┄┄┄
|
|
175
|
+
|
|
176
|
+
[P0] ${FILE_PATH}
|
|
177
|
+
偵測自刻 row-layout slot:\`<span class="h-[1lh] shrink-0 flex items-center...">\`
|
|
178
|
+
M1+M17 違反 — 該消費 L1 primitive。
|
|
179
|
+
|
|
180
|
+
⚠️ M19 canonical(2026-05-05 v8):row prefix/suffix slot 必走 patterns/element-anatomy:
|
|
181
|
+
import { ItemPrefix, ItemSuffix } from '@/design-system/patterns/element-anatomy/item-anatomy'
|
|
182
|
+
<ItemPrefix><StartIcon /></ItemPrefix>
|
|
183
|
+
<ItemSuffix>{chevron}</ItemSuffix>
|
|
184
|
+
|
|
185
|
+
\`<ItemPrefix>\` / \`<ItemSuffix>\` 永遠 \`h-[1lh] shrink-0 flex items-center\`(item-anatomy.spec.md:175+190)。
|
|
186
|
+
例外:行尾 \`// @row-slot-handcraft-allow: <reason>\`
|
|
187
|
+
|
|
188
|
+
EOF
|
|
189
|
+
record_worst 2
|
|
190
|
+
fi
|
|
191
|
+
;;
|
|
192
|
+
esac
|
|
193
|
+
;;
|
|
194
|
+
esac
|
|
195
|
+
|
|
196
|
+
exit $WORST
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_sidebar_menu_button_implicit_wrap.sh — PreToolUse Edit/Write 攔 SidebarMenuButton 沒 asChild + children 含 ItemAvatar/Avatar/Icon 致隱式 wrap 垂直 stack(2026-05-27)
|
|
3
|
+
#
|
|
4
|
+
# Per user 2026-05-27 抓 UserFooter Avatar 垂直 stack drift:
|
|
5
|
+
# SidebarMenuButton(sidebar.tsx L1036-1043)在沒 asChild 時把所有 children 塞進 <ItemLabel> 單一 span,
|
|
6
|
+
# Avatar + text-span 都在同 span 內 → 強迫垂直堆疊。
|
|
7
|
+
#
|
|
8
|
+
# Canonical:含 Avatar/ItemAvatar/icon prefix 的 SidebarMenuButton 必用 asChild + <div> wrap,
|
|
9
|
+
# per sidebar.tsx:1025-1027 docblock:「asChild 的 consumer 自行放 icon + label」+ DS canonical
|
|
10
|
+
# sidebar.stories.tsx#UserFooter 範例 L76-104。
|
|
11
|
+
#
|
|
12
|
+
# Detection:
|
|
13
|
+
# `<SidebarMenuButton ...>` (no asChild) ... `<ItemAvatar` or `<Avatar` inside → BLOCKER
|
|
14
|
+
# ALLOW:`<SidebarMenuButton startIcon={X}>label</SidebarMenuButton>`(用 startIcon prop 不 wrap)
|
|
15
|
+
# ALLOW:`<SidebarMenuButton asChild>...</SidebarMenuButton>`(consumer 自管 layout)
|
|
16
|
+
|
|
17
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
21
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
22
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
23
|
+
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2>/dev/null)
|
|
24
|
+
NEW=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""' 2>/dev/null)
|
|
25
|
+
|
|
26
|
+
[ "$EVENT" != "PreToolUse" ] && exit 0
|
|
27
|
+
case "$TOOL" in Edit|Write|MultiEdit) ;; *) exit 0 ;; esac
|
|
28
|
+
case "$FILE_PATH" in *.tsx) ;; *) exit 0 ;; esac
|
|
29
|
+
case "$FILE_PATH" in *.test.tsx) exit 0 ;; esac
|
|
30
|
+
|
|
31
|
+
# Python multiline regex:`<SidebarMenuButton` 不含 `asChild` 的 block + 內含 Avatar/ItemAvatar prefix
|
|
32
|
+
HAS_DRIFT=$(printf '%s' "$NEW" | python3 -c '
|
|
33
|
+
import sys, re
|
|
34
|
+
content = sys.stdin.read()
|
|
35
|
+
# Find SidebarMenuButton blocks(opening tag → closing tag,non-greedy multiline)
|
|
36
|
+
# match opening `<SidebarMenuButton ...>` capture full open tag
|
|
37
|
+
for m in re.finditer(r"<SidebarMenuButton\b([^>]*)>(.*?)</SidebarMenuButton>", content, re.DOTALL):
|
|
38
|
+
open_tag, body = m.group(1), m.group(2)
|
|
39
|
+
# Skip if asChild present
|
|
40
|
+
if re.search(r"\basChild\b", open_tag):
|
|
41
|
+
continue
|
|
42
|
+
# Drift = body 含 <ItemAvatar 或 <Avatar(both = avatar prefix)
|
|
43
|
+
if re.search(r"<(ItemAvatar|Avatar)\b", body):
|
|
44
|
+
print("DRIFT")
|
|
45
|
+
sys.exit(0)
|
|
46
|
+
' 2>/dev/null)
|
|
47
|
+
|
|
48
|
+
[ "$HAS_DRIFT" != "DRIFT" ] && exit 0
|
|
49
|
+
|
|
50
|
+
# Override env(audit-logged)
|
|
51
|
+
if [ "${CLAUDE_BYPASS_SIDEBAR_MENU_BUTTON_WRAP:-0}" = "1" ]; then
|
|
52
|
+
mkdir -p "$(dirname "$0")/../logs" 2>/dev/null
|
|
53
|
+
printf '{"ts":"%s","event":"sidebar-menu-button-wrap-bypass","file":"%s"}\n' \
|
|
54
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$FILE_PATH" >> "$(dirname "$0")/../logs/governance-bypass.jsonl" 2>/dev/null
|
|
55
|
+
exit 0
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
REL=${FILE_PATH#"$CLAUDE_PROJECT_DIR"/}
|
|
59
|
+
|
|
60
|
+
cat >&2 <<'EOF'
|
|
61
|
+
🚨 SidebarMenuButton implicit-wrap canonical violation(per user 2026-05-27 UserFooter 垂直 stack 事件):
|
|
62
|
+
|
|
63
|
+
🔍 偵測:SidebarMenuButton 內含 ItemAvatar 或 Avatar 但 **無 asChild**
|
|
64
|
+
|
|
65
|
+
⚠️ 後果(per sidebar.tsx:1036-1043 source code):
|
|
66
|
+
SidebarMenuButton 沒 asChild → children 全塞進 ItemLabel(單 span)
|
|
67
|
+
→ Avatar + text 在同 span 內 → 強迫垂直堆疊(user 抓的 bug)
|
|
68
|
+
|
|
69
|
+
修法(per DS canonical sidebar.stories.tsx#UserFooter):
|
|
70
|
+
|
|
71
|
+
❌ SidebarMenuButton id="..." 直接放 ItemAvatar + span 為 children
|
|
72
|
+
|
|
73
|
+
✅ SidebarMenuButton asChild 包 div role="group" 內含 ItemAvatar + span data-sidebar="menu-label" min-w-0 flex-1 truncate
|
|
74
|
+
|
|
75
|
+
Or use startIcon prop(自動 layout,不 wrap):
|
|
76
|
+
✅ SidebarMenuButton startIcon={SomeLucideIcon} 直接 children 為純文字
|
|
77
|
+
|
|
78
|
+
Bypass(極罕見):CLAUDE_BYPASS_SIDEBAR_MENU_BUTTON_WRAP=1 env var(audit-logged)。
|
|
79
|
+
|
|
80
|
+
Citation:
|
|
81
|
+
- packages/design-system/src/components/Sidebar/sidebar.tsx:1025-1043(asChild docblock)
|
|
82
|
+
- packages/design-system/src/components/Sidebar/sidebar.stories.tsx UserFooter L76-104(canonical)
|
|
83
|
+
EOF
|
|
84
|
+
# 2026-05-31:exit 0 → exit 2(folded-hook-audit:原宣稱 BLOCKER 但 exit 0 = 假 enforcement;
|
|
85
|
+
# SidebarMenuButton implicit-wrap 是 SSOT canonical,verified clean on 現有 sidebar code + 有 env escape)。
|
|
86
|
+
exit 2
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_tailwind_wildcard_in_docs.sh — P0 BLOCKER
|
|
3
|
+
#
|
|
4
|
+
# Per 2026-05-28 beta.27 release fail anchor(6+ CI iterations 燒):
|
|
5
|
+
# Tailwind v4 vite plugin scans .md / .spec.md / docs by default。
|
|
6
|
+
# 文件範例 like `shadow-[var(--elevation-*)]` 或 `var(--field-height-*)` /
|
|
7
|
+
# `var(--elevation-100/200/300)` 是給 dev 看的 shorthand notation(`*` / `/` 代表
|
|
8
|
+
# enumeration placeholder),但 Tailwind 不知 → 當 literal class string 抓 → 產
|
|
9
|
+
# invalid CSS `var(--X-*)` `var(--X-A/B/C)` → Storybook FULL smoke 死。
|
|
10
|
+
#
|
|
11
|
+
# 機械強制 PreToolUse Edit/Write:阻止寫入新檔含此 anti-pattern。
|
|
12
|
+
# 改用 math notation:`var(--X-N) N∈{a,b,c}` Tailwind 不會誤判。
|
|
13
|
+
#
|
|
14
|
+
# Scope:所有 .md / .spec.md / .sh / .ts / .tsx 寫入時 grep new_string。
|
|
15
|
+
# Exit 2 BLOCKER + cite this anchor。Escape:`@tailwind-wildcard-allow:` comment。
|
|
16
|
+
|
|
17
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
22
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
23
|
+
|
|
24
|
+
case "${TOOL:-}" in
|
|
25
|
+
Edit|Write|MultiEdit) ;;
|
|
26
|
+
*) exit 0 ;;
|
|
27
|
+
esac
|
|
28
|
+
|
|
29
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
30
|
+
# Self-exemption:本 hook help-text 合法含 anti-pattern literal 作為文件範例(且 .sh 不被 Tailwind
|
|
31
|
+
# vite plugin 掃,無 build 風險)→ 不掃自己,避免 self-trigger false-positive(2026-05-30 test-surfaced)。
|
|
32
|
+
case "$FILE" in */check_tailwind_wildcard_in_docs.sh) exit 0 ;; esac
|
|
33
|
+
# 2026-06-11 R2 held-item #13:.claude/{tmp,logs} 歷史 artifact(codex brief / reply、audit findings
|
|
34
|
+
# json、preflight log)非 Tailwind 掃描對象 — src/globals.css 用 explicit positive @source(只掃 src/ +
|
|
35
|
+
# packages/{design-system,storybook-config} + .storybook/)且 `@source not "**/.claude/**"`;盤上 12 檔
|
|
36
|
+
# 已含 antiPattern 而 build 連續綠(至 beta.61)= 零 build 風險實證。掃描對象路徑(src/packages/docs)
|
|
37
|
+
# 保護不變。
|
|
38
|
+
case "$FILE" in */.claude/tmp/*|*/.claude/logs/*|.claude/tmp/*|.claude/logs/*) exit 0 ;; esac
|
|
39
|
+
# Only check files Tailwind v4 might scan
|
|
40
|
+
if ! echo "$FILE" | grep -qE '\.(md|spec\.md|sh|ts|tsx|css|json)$'; then exit 0; fi
|
|
41
|
+
|
|
42
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
|
|
43
|
+
[ -z "$CONTENT" ] && exit 0
|
|
44
|
+
|
|
45
|
+
# Escape clause
|
|
46
|
+
if echo "$CONTENT" | grep -qE '@tailwind-wildcard-allow:'; then exit 0; fi
|
|
47
|
+
|
|
48
|
+
# Detect anti-patterns(class form with wildcard / slash enumeration in CSS var)
|
|
49
|
+
# 2026-05-30 fix(test-surfaced M34 over-narrow):slash-segment 改 repeatable,
|
|
50
|
+
# 否則漏多段斜線列舉形式(beta.27 anchor 的 N-段 enum,hook header 列為必擋 anti-pattern)。
|
|
51
|
+
ANTI_PATTERNS=$(echo "$CONTENT" | grep -oE 'var\(--[a-z][a-z0-9-]*([\*/]+[a-z0-9-]*)+\)' | sort -u)
|
|
52
|
+
|
|
53
|
+
if [ -n "$ANTI_PATTERNS" ]; then
|
|
54
|
+
cat >&2 << EOF
|
|
55
|
+
🚨 TAILWIND v4 WILDCARD-IN-DOCS BLOCKER(P0,2026-05-28 beta.27 anchor)
|
|
56
|
+
|
|
57
|
+
File: $FILE
|
|
58
|
+
Detected anti-pattern(s):
|
|
59
|
+
$(echo "$ANTI_PATTERNS" | sed 's/^/ /')
|
|
60
|
+
|
|
61
|
+
Why blocked:
|
|
62
|
+
Tailwind v4 vite plugin scans .md/.ts/etc → 把 \`var(--X-*)\` / \`var(--X-A/B/C)\`
|
|
63
|
+
當 literal class string 抓 → 產 invalid CSS(CSS 變數名禁 \`*\` / \`/\`)→ Storybook
|
|
64
|
+
build 死 → release CI fail。本 anchor:beta.27 6+ CI iteration 燒此問題。
|
|
65
|
+
|
|
66
|
+
改用 math notation(Tailwind 不誤判):
|
|
67
|
+
var(--elevation-*) → var(--elevation-N) N∈{100,200}
|
|
68
|
+
var(--elevation-100/200) → var(--elevation-N) N∈{100,200}
|
|
69
|
+
var(--field-height-*) → var(--field-height-N) N∈{sm,md,lg}
|
|
70
|
+
var(--layout-space-*) → var(--layout-space-N) N∈{loose,tight}
|
|
71
|
+
var(--radix-*-available-height) → var(--radix-{popover|hover-card|dialog}-content-available-height)
|
|
72
|
+
|
|
73
|
+
Escape(極罕見,本 file 真需要 wildcard token literal):
|
|
74
|
+
add \`// @tailwind-wildcard-allow: <rationale>\` to file content
|
|
75
|
+
EOF
|
|
76
|
+
exit 2
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
exit 0
|