Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .claude/hooks/ui-kit-css-gate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail

# Blocks Claude Code Write/Edit/MultiEdit calls that would create or modify a
# per-component .css file under a ui-kit package's src/, per the repo rule:
# component styling goes through packages/uno-preset, .css is only for
# tokens.css and theme/ sheets. Mirrors scripts/check-ui-kit-css.ts's
# pattern/allowlist so the three enforcement layers (this hook, prek, CI)
# agree on exactly one definition of "banned path".
#
# Same stdin JSON contract as turbo-filter-gate.sh: a PreToolUse hook reads
# the tool call as JSON on stdin and blocks by exiting 2 with a stderr
# message. Anything unparseable fails open.

if ! command -v jq >/dev/null 2>&1; then
echo "ui-kit-css-gate: jq not on PATH, skipping check." >&2
exit 0
fi

INPUT="$(cat)"
FILE_PATH="$(jq -r '.tool_input.file_path // empty' <<<"$INPUT" 2>/dev/null || true)"

[ -n "$FILE_PATH" ] || exit 0

NORMALIZED="$FILE_PATH"
while printf '%s\n' "$NORMALIZED" | grep -Eq '(^|/)[^/]+/\.\.(/|$)|(^|/)\.(/|$)'; do
NORMALIZED="$(printf '%s\n' "$NORMALIZED" | sed -E 's#(^|/)[^/]+/\.\.(/|$)#\1#; s#(^|/)\.(/|$)#\1#; s#//#/#g')"
done

printf '%s\n' "$NORMALIZED" | grep -Eq '(^|/)packages/ui-kit-[^/]+/src/.*\.css$' || exit 0

BASENAME="$(basename "$NORMALIZED")"
if [ "$BASENAME" = "tokens.css" ]; then
exit 0
fi
if printf '%s\n' "$NORMALIZED" | grep -Eq '(^|/)theme/'; then
exit 0
fi

{
echo "ui-kit-css-gate: BLOCKED: $FILE_PATH is a banned per-component ui-kit .css file."
echo
echo "component styles belong in packages/uno-preset (keyframes -> src/animation.ts, animation"
echo "shortcuts -> src/motion.ts, effects/rules/preflights -> the preset), .css is only for"
echo "tokens/themes."
} >&2

exit 2
9 changes: 9 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/turbo-filter-gate.sh"
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/ui-kit-css-gate.sh"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
]
}
]
},
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ jobs:
with:
node-version: 22
cache: pnpm
# ui-kit component styling goes through packages/uno-preset; per-component .css files
# are banned. Needs only node + git, so it runs before install/build.
- run: node scripts/check-ui-kit-css.ts
- uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .turbo
Expand Down
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ repos:
pass_filenames: true
files: '\.(m?[jt]sx?|c[jt]s)$'
exclude: '^(.*/)?(dist|build|\.turbo)/'
- id: ui-kit-css-gate
name: ui-kit CSS gate (no per-component .css)
entry: node scripts/check-ui-kit-css.ts
language: system
pass_filenames: true
files: '^packages/ui-kit-[^/]+/src/.*\.css$'
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ README.md; this file is the non-obvious operational rules.
- TypeScript is strict (`noUncheckedIndexedAccess`, `verbatimModuleSyntax`, NodeNext). Avoid
`any`/`as`/`@ts-ignore`.
- oxfmt: no semicolons, single quotes, no bracket spacing, trailing commas, printWidth 120.
- Component styles go through `packages/uno-preset` (`animation.ts` / `motion.ts` / effects /
preflights); `.css` files in ui-kit `src/` are banned except `tokens.css` and `theme/` sheets,
enforced by prek + CI + the agent PreToolUse hook.

## TanStack Router

Expand Down
44 changes: 44 additions & 0 deletions scripts/check-ui-kit-css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env node
import {execFileSync} from 'node:child_process'
import {posix} from 'node:path'

const UI_KIT_CSS_PATTERN = /^packages\/ui-kit-[^/]+\/src\/.*\.css$/
const GIT_SCAN_PATHSPEC = ':(glob)packages/ui-kit-*/src/**/*.css'

const MESSAGE =
'component styles belong in packages/uno-preset (keyframes -> src/animation.ts, animation ' +
'shortcuts -> src/motion.ts, effects/rules/preflights -> the preset), .css is only for ' +
'tokens/themes.'

function isAllowlisted(path: string): boolean {
const segments = path.split('/')
const basename = segments[segments.length - 1]
if (basename === 'tokens.css') return true
return segments.includes('theme')
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function findBannedPaths(paths: string[]): string[] {
return paths
.map((path) => posix.normalize(path.replaceAll('\\', '/')))
.filter((path) => UI_KIT_CSS_PATTERN.test(path) && !isAllowlisted(path))
}

function scanTrackedFiles(): string[] {
const output = execFileSync('git', ['ls-files', GIT_SCAN_PATHSPEC], {encoding: 'utf8'})
return output.split('\n').filter((line) => line.length > 0)
}

function main(): void {
const argPaths = process.argv.slice(2)
const candidatePaths = argPaths.length > 0 ? argPaths : scanTrackedFiles()
const banned = findBannedPaths(candidatePaths)

if (banned.length === 0) return

console.error('check-ui-kit-css: banned ui-kit component .css file(s) found:\n')
for (const path of banned) console.error(` ${path}`)
console.error(`\n${MESSAGE}`)
process.exitCode = 1
}

main()
Loading