#!/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 "]"