This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/scripts/generate-ui-inventory.sh
Jeffrey Smith ff19a1b4d3
All checks were successful
CI/CD / detect-changes (push) Successful in 3s
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-sqlite (push) Successful in 2m49s
CI/CD / test-go-pg (push) Successful in 2m51s
CI/CD / build-and-deploy (push) Successful in 49s
Feat v0.6.16 usability survey gate (#51)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-04-01 14:52:14 +00:00

288 lines
8.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# generate-ui-inventory.sh — Walk kernel + package CSS, extract class selectors with metadata.
# Outputs ui-inventory.json to stdout.
# Usage: bash scripts/generate-ui-inventory.sh [css-dir] [packages-dir]
set -euo pipefail
CSS_DIR="${1:-src/css}"
PKG_DIR="${2:-packages}"
# Collect all CSS files: kernel + packages
FILES=()
for f in "$CSS_DIR"/*.css; do [ -f "$f" ] && FILES+=("$f"); done
for f in "$PKG_DIR"/*/css/main.css; do [ -f "$f" ] && FILES+=("$f"); done
# Derive surface name from file path
surface_for() {
local f="$1"
if [[ "$f" == "$PKG_DIR"/* ]]; then
local slug
slug="$(basename "$(dirname "$(dirname "$f")")")"
echo "ext/$slug"
else
local base
base="$(basename "$f" .css)"
echo "kernel/$base"
fi
}
echo "["
first_entry=true
for f in "${FILES[@]}"; do
surface="$(surface_for "$f")"
relpath="$f"
in_comment=0
in_keyframes=0
in_fontface=0
brace_depth=0
media_breakpoint=""
current_selector=""
current_line=0
current_classes=""
current_spacing=""
current_fontsize=""
current_breakpoints=""
lineno=0
while IFS= read -r line; do
lineno=$((lineno + 1))
# Track block comments
if (( in_comment )); then
[[ "$line" == *"*/"* ]] && in_comment=0
continue
fi
if [[ "$line" == *"/*"* ]] && [[ "$line" != *"*/"* ]]; then
in_comment=1
continue
fi
# Trim leading whitespace
trimmed="${line#"${line%%[![:space:]]*}"}"
[[ -z "$trimmed" ]] && continue
[[ "$trimmed" == /\** ]] && continue
# Track @keyframes blocks
if [[ "$trimmed" =~ ^@keyframes ]]; then
in_keyframes=1
continue
fi
if (( in_keyframes )); then
[[ "$trimmed" == "}" ]] && in_keyframes=0
continue
fi
# Skip @font-face
if [[ "$trimmed" =~ ^@font-face ]]; then
in_fontface=1
continue
fi
if (( in_fontface )); then
[[ "$trimmed" == *"}"* ]] && in_fontface=0
continue
fi
# Track @media breakpoints
if [[ "$trimmed" =~ ^@media.*max-width ]]; then
if [[ "$trimmed" =~ 768 ]]; then
media_breakpoint="768px"
elif [[ "$trimmed" =~ 1024 ]]; then
media_breakpoint="1024px"
else
media_breakpoint="other"
fi
continue
fi
# Skip other @-rules
[[ "$trimmed" =~ ^@.+ ]] && continue
# Handle closing brace
if [[ "$trimmed" == "}" ]]; then
if [[ -n "$media_breakpoint" ]] && (( brace_depth == 0 )); then
media_breakpoint=""
fi
# If we had an open selector block, emit it
if [[ -n "$current_selector" ]]; then
# Build breakpoints array
bp_json="[]"
if [[ -n "$current_breakpoints" ]]; then
bp_json="["
bp_first=true
for bp in $current_breakpoints; do
$bp_first && bp_first=false || bp_json="$bp_json,"
bp_json="$bp_json\"$bp\""
done
bp_json="$bp_json]"
fi
# Build spacing array
sp_json="[]"
if [[ -n "$current_spacing" ]]; then
sp_json="["
sp_first=true
# Deduplicate
seen_sp=""
for sp in $current_spacing; do
[[ " $seen_sp " == *" $sp "* ]] && continue
seen_sp="$seen_sp $sp"
$sp_first && sp_first=false || sp_json="$sp_json,"
sp_json="$sp_json\"$sp\""
done
sp_json="$sp_json]"
fi
# Build font-size array
fs_json="[]"
if [[ -n "$current_fontsize" ]]; then
fs_json="["
fs_first=true
for fs in $current_fontsize; do
$fs_first && fs_first=false || fs_json="$fs_json,"
fs_json="$fs_json\"$fs\""
done
fs_json="$fs_json]"
fi
# Build classes array
cls_json="["
cls_first=true
for cls in $current_classes; do
$cls_first && cls_first=false || cls_json="$cls_json,"
cls_json="$cls_json\"$cls\""
done
cls_json="$cls_json]"
$first_entry && first_entry=false || printf ",\n"
printf ' {"surface":"%s","component":"%s","classes":%s,"file":"%s","line":%d,"responsive_breakpoints":%s,"spacing_tokens_used":%s,"font_size_tokens_used":%s}' \
"$surface" "$current_selector" "$cls_json" "$relpath" "$current_line" "$bp_json" "$sp_json" "$fs_json"
current_selector=""
current_classes=""
current_spacing=""
current_fontsize=""
current_breakpoints=""
fi
continue
fi
# Selector line — contains a class and an opening brace
if [[ "$trimmed" == *"."* ]] && [[ "$trimmed" == *"{"* ]]; then
# Extract the selector part (before {)
sel_part="${trimmed%%\{*}"
# Extract the property part (after {), may have properties on the same line
prop_part="${trimmed#*\{}"
prop_part="${prop_part%\}}"
# Get all class names from the selector
classes=""
tmp="$sel_part"
while [[ "$tmp" =~ \.([a-zA-Z_][a-zA-Z0-9_-]*) ]]; do
cls="${BASH_REMATCH[1]}"
classes="$classes $cls"
tmp="${tmp#*${BASH_REMATCH[0]}}"
done
# Use the first class as the component name
first_cls=""
for c in $classes; do first_cls="$c"; break; done
[[ -z "$first_cls" ]] && continue
current_selector="$first_cls"
current_line=$lineno
current_classes="$classes"
current_spacing=""
current_fontsize=""
current_breakpoints=""
[[ -n "$media_breakpoint" ]] && current_breakpoints="$media_breakpoint"
# Check inline properties (single-line rules like .foo { padding: ...; })
if [[ -n "$prop_part" ]]; then
# Extract spacing tokens
sp_tmp="$prop_part"
while [[ "$sp_tmp" =~ var\(--sp-([0-9h]+)\) ]]; do
current_spacing="$current_spacing --sp-${BASH_REMATCH[1]}"
sp_tmp="${sp_tmp#*${BASH_REMATCH[0]}}"
done
# Extract font-size
if [[ "$prop_part" =~ font-size:[[:space:]]*([^;]+) ]]; then
fs_val="${BASH_REMATCH[1]}"
fs_val="${fs_val%"${fs_val##*[![:space:]]}"}"
current_fontsize="$current_fontsize $fs_val"
fi
fi
# If the rule is self-closing (has } on the same line), emit now
if [[ "$trimmed" == *"}"* ]]; then
bp_json="[]"
if [[ -n "$current_breakpoints" ]]; then
bp_json="[\"$current_breakpoints\"]"
fi
sp_json="[]"
if [[ -n "$current_spacing" ]]; then
sp_json="["
sp_first=true
seen_sp=""
for sp in $current_spacing; do
[[ " $seen_sp " == *" $sp "* ]] && continue
seen_sp="$seen_sp $sp"
$sp_first && sp_first=false || sp_json="$sp_json,"
sp_json="$sp_json\"$sp\""
done
sp_json="$sp_json]"
fi
fs_json="[]"
if [[ -n "$current_fontsize" ]]; then
fs_json="["
fs_first=true
for fs in $current_fontsize; do
$fs_first && fs_first=false || fs_json="$fs_json,"
fs_json="$fs_json\"$fs\""
done
fs_json="$fs_json]"
fi
cls_json="["
cls_first=true
for cls in $current_classes; do
$cls_first && cls_first=false || cls_json="$cls_json,"
cls_json="$cls_json\"$cls\""
done
cls_json="$cls_json]"
$first_entry && first_entry=false || printf ",\n"
printf ' {"surface":"%s","component":"%s","classes":%s,"file":"%s","line":%d,"responsive_breakpoints":%s,"spacing_tokens_used":%s,"font_size_tokens_used":%s}' \
"$surface" "$current_selector" "$cls_json" "$relpath" "$current_line" "$bp_json" "$sp_json" "$fs_json"
current_selector=""
current_classes=""
current_spacing=""
current_fontsize=""
current_breakpoints=""
fi
continue
fi
# Property line inside an open block
if [[ -n "$current_selector" ]]; then
# Extract spacing tokens
sp_tmp="$trimmed"
while [[ "$sp_tmp" =~ var\(--sp-([0-9h]+)\) ]]; do
current_spacing="$current_spacing --sp-${BASH_REMATCH[1]}"
sp_tmp="${sp_tmp#*${BASH_REMATCH[0]}}"
done
# Extract font-size
if [[ "$trimmed" =~ font-size:[[:space:]]*([^;]+) ]]; then
fs_val="${BASH_REMATCH[1]}"
fs_val="${fs_val%"${fs_val##*[![:space:]]}"}"
current_fontsize="$current_fontsize $fs_val"
fi
fi
done < "$f"
done
echo ""
echo "]"