# Lint source files for design-token house-rule violations: raw hex/palette
# colors (ADR-001/ADR-006), non-system-stack fonts (ADR-003), and arbitrary
# corner-radius values (ADR-002).
# <path> may be a single file (.tsx/.jsx/.ts/.css) or a directory, scanned
# recursively for those extensions.
# scripts/lint_tokens.sh ./src/components/ui/Button.tsx
# scripts/lint_tokens.sh ./src
# Exit code is non-zero if any hard-fail check finds a violation. This script
# shells out to nothing but grep/find, so there is no external tool to degrade
# to a warning — every check always runs.
if [[ -z "$TARGET" ]]; then
echo "Usage: $0 <path>" >&2
if [[ ! -e "$TARGET" ]]; then
echo "error: '$TARGET' does not exist" >&2
warn() { printf '⚠️ %s\n' "$1"; }
error() { printf '❌ %s\n' "$1"; fail=1; }
ok() { printf '✅ %s\n' "$1"; }
# Build the file list once: either the single file given, or every
# .tsx/.jsx/.ts/.css file under a directory.
if [[ -d "$TARGET" ]]; then
while IFS= read -r -d '' f; do
done < <(find "$TARGET" -type f \( -name '*.tsx' -o -name '*.jsx' -o -name '*.ts' -o -name '*.css' \) -print0)
if [[ "${#FILES[@]}" -eq 0 ]]; then
echo "No .tsx/.jsx/.ts/.css files found under '$TARGET'." >&2
# Palette color names Tailwind ships with numeric scales (50-950). Any
# bg|text|border|ring|fill|stroke utility built from one of these is a raw
# palette reference, not a semantic token, and must be flagged.
PALETTE='slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose'
SCALE='50|100|150|200|250|300|350|400|450|500|550|600|650|700|750|800|850|900|950'
PALETTE_CLASS_RE="(bg|text|border|ring|fill|stroke)-(${PALETTE})-(${SCALE})\b"
HEX_RE='#[0-9A-Fa-f]{3,8}\b'
FONT_RE='(Fraunces|Source Sans( +3| +Pro)?|Inter|Roboto|Arial)'
ARBITRARY_RADIUS_RE='rounded-\[[^]]+\]'
ROUNDED_FULL_RE='rounded-full'
echo "== ADR-001/006: semantic tokens, no raw hex or palette classes =="
hex_hits=$(grep -rnE "$HEX_RE" "${FILES[@]}" 2>/dev/null || true)
palette_hits=$(grep -rnE "$PALETTE_CLASS_RE" "${FILES[@]}" 2>/dev/null || true)
if [[ -n "$hex_hits" || -n "$palette_hits" ]]; then
error "found raw hex codes and/or raw Tailwind palette classes — use semantic tokens instead (text-foreground, bg-card, border-border, text-primary, etc.):"
[[ -n "$hex_hits" ]] && echo "$hex_hits"
[[ -n "$palette_hits" ]] && echo "$palette_hits"
ok "no raw hex codes or raw Tailwind palette classes found"
echo "== ADR-003: system font stack only, no web fonts =="
font_hits=$(grep -rniE "font.{0,40}${FONT_RE}|${FONT_RE}.{0,40}font" "${FILES[@]}" 2>/dev/null || true)
if [[ -n "$font_hits" ]]; then
error "found a non-system font family — the only font stack is -apple-system/Segoe UI/system-ui; express hierarchy via ui-type-* tokens instead:"
ok "no Fraunces/Source Sans/Inter/Roboto/Arial references found"
echo "== ADR-002: corner radius uses the token scale =="
arbitrary_hits=$(grep -rnE "$ARBITRARY_RADIUS_RE" "${FILES[@]}" 2>/dev/null || true)
if [[ -n "$arbitrary_hits" ]]; then
error "found an arbitrary-value radius class — use the token scale instead (rounded-xl/lg/md/sm per ADR-002):"
ok "no arbitrary-value rounded-[...] classes found"
full_hits=$(grep -rnE "$ROUNDED_FULL_RE" "${FILES[@]}" 2>/dev/null || true)
if [[ -n "$full_hits" ]]; then
warn "found rounded-full — verify this is a circular element (avatar/dot/marker), not a button/input:"
ok "no rounded-full usage found"
if [[ "$fail" -eq 0 ]]; then
echo "All hard-fail checks passed."
echo "One or more checks failed — see ❌ lines above."