# build-site.ps1 v2 - generate site/data.js for the docs-style showcase # Adds: category classification, per-component spec section extraction, # design-contract parsing (core 6), design-token extraction. # Usage: run from repo root: & .\build-site.ps1 # NOTE: keep this script ASCII-only (Windows PowerShell parses BOM-less .ps1 in ANSI codepage). $ErrorActionPreference = 'Stop' $lib = ".design_library/kole-ui" $idx = Get-Content "$lib/components/index.json" -Raw -Encoding UTF8 | ConvertFrom-Json $fw = Get-ChildItem frameworks -File $fwMap = @{} foreach ($f in $fw) { $fwMap[$f.Name.ToLowerInvariant()] = $f.Name } function Find-FwFile([string]$name) { $key = $name.ToLowerInvariant() if ($fwMap.ContainsKey($key)) { $fwMap[$key] } else { $null } } # ---------------- category classification ---------------- $catMap = @{} $catMap['general'] = @('button','buttongroup','tag','card','layout','container','typography','icon','link') $catMap['navigation']= @('tabs','steps','breadcrumb','sidemenu','topmenu','mixednavigation','quicknav','anchornav','enhancedtabnav','dropdown','pageheader') $catMap['input'] = @('input','select','cascader','colorpicker','rangequickpicker','numberrangeinput','switchgroup','dragupload','signaturepad','codeinput','passwordinput','phoneinput','bankcardinput','idcardinput','plateinput','autocomplete','mention','transfer','rate','slider','listpicker','radiocard','checkboxcard','segmented','radio','checkbox','inputnumber','switch','timepicker','datepicker','datetimepicker','form') $catMap['display'] = @('table','treetable','expandabletable','mergedcelltable','summaryrowtable','fixedcolumntable','cardlist','timelinelist','steplist','chartpanel','dashboardcard','calendar','carousel','imagepreview','qrcode','countdown','tree','collapse','watermark','employeecard','pagination','badge','avatar','descriptions','divider','infinitescroll') $catMap['feedback'] = @('modal','confirmmodal','alertmodal','formmodal','fullscreenmodal','messagepro','notificationpro','progressvariants','skeletonpro','emptypro','loadingoverlay','resultvariants','exceptionvariants','popconfirm','bottomsheet','alert','tooltip','popover','drawer') $catMap['system'] = @('themeswitcher','layoutswitcher','densityswitcher','shortcutpanel','backtotop','pagetransition') $slugCat = @{} foreach ($ck in $catMap.Keys) { foreach ($s in $catMap[$ck]) { $slugCat[$s] = $ck } } # ---------------- component families (variant parameterization) ---------------- # families.json is generated by tools/gen-families.mjs from tools/lib/family-model.mjs. # A family expresses several same-origin components as ONE base component plus # parameter values. Component identity (slug) never changes: data.json still lists # all 79 components; the family layer is additive metadata on top of them. # # PS 5.1 notes (both are real traps, do not "simplify"): # 1. ConvertFrom-Json yields PSCustomObject, which Json-Value below does NOT # understand -- it would fall through to Json-Escape and emit "@{...}". # 2. An empty array returned from a function gets unrolled to $null. So arrays are # built and assigned to hashtable slots INSIDE the helper, never returned. $famDoc = $null $famRaw = '' $famOf = @{} $famPath = "$lib/families.json" if (Test-Path $famPath) { $famRaw = (Get-Content $famPath -Raw -Encoding UTF8).Trim() $famDoc = $famRaw | ConvertFrom-Json foreach ($f in $famDoc.families) { foreach ($m in $f.members) { $pv = [ordered]@{} if ($m.params) { foreach ($p in $m.params.PSObject.Properties) { $val = $p.Value if ($null -eq $val) { $pv[$p.Name] = $null } elseif ($val -is [System.Object[]] -or $val -is [System.Collections.ArrayList]) { $arr = @() foreach ($x in $val) { $arr += $x } $pv[$p.Name] = $arr } else { $pv[$p.Name] = $val } } } $famOf[[string]$m.slug] = [ordered]@{ family = [string]$f.id role = [string]$m.role params = $pv } } } Write-Output ("families: {0} families, {1} member components" -f $famDoc.totalFamilies, $famDoc.totalMembers) } else { Write-Warning "families.json not found; the family layer will be omitted from data.json" } # ---------------- spec section extraction ---------------- function Extract-SpecSection([string]$specPath, [string]$enToken) { if (-not (Test-Path $specPath)) { return $null } $lines = Get-Content $specPath -Encoding UTF8 $hit = -1 $re = [regex]('^(#{2,4})\s+(.*)$') for ($n = 0; $n -lt $lines.Count; $n++) { $m = $re.Match($lines[$n]) if ($m.Success -and ($m.Groups[2].Value -match ('\b' + [regex]::Escape($enToken) + '\b'))) { $hit = $n break } } if ($hit -lt 0) { return $null } $body = New-Object System.Collections.Generic.List[string] for ($n = $hit + 1; $n -lt $lines.Count; $n++) { if ($lines[$n] -match '^#{1,4}\s') { break } [void]$body.Add($lines[$n]) } while ($body.Count -gt 0 -and $body[0].Trim() -eq '') { $body.RemoveAt(0) } while ($body.Count -gt 0 -and $body[$body.Count-1].Trim() -eq '') { $body.RemoveAt($body.Count-1) } if ($body.Count -eq 0) { return $null } $summary = '' foreach ($L in $body) { if ($L.Trim() -ne '') { $summary = $L.Trim(); break } } return @{ lines = $body; summary = $summary } } # ---------------- design contract parsing (core 6) ---------------- function Parse-Contract([string]$path) { if (-not (Test-Path $path)) { return $null } $c = Get-Content $path -Raw -Encoding UTF8 | ConvertFrom-Json $dims = @() foreach ($d in $c.variantDimensions) { $dims += ,@{ name = $d.name; values = @($d.values) } } $variants = @() foreach ($v in $c.representativeVariants) { $variants += [string]$v.label } $usage = @(); foreach ($u in $c.usageHints) { $usage += [string]$u } $doNot = @(); if ($c.doNotInvent) { foreach ($x in $c.doNotInvent) { $doNot += [string]$x } } $unknowns = @(); if ($c.unknowns) { foreach ($x in $c.unknowns) { $unknowns += [string]$x } } $notes = @(); if ($c.clarificationNotes) { foreach ($n in $c.clarificationNotes) { $notes += [ordered]@{ item = [string]$n.item kind = [string]$n.kind meaning = [string]$n.meaning why = [string]$n.why decision = [string]$n.decision } } } function PsObjToMap($obj) { $m = [ordered]@{} if ($obj) { foreach ($p in $obj.PSObject.Properties) { $m[$p.Name] = [string]$p.Value } } return $m } return @{ dims = $dims variants = $variants usage = $usage structure = PsObjToMap $c.structurePatterns anatomy = PsObjToMap $c.anatomy doNot = $doNot unknowns = $unknowns clarificationNotes = $notes } } # ---------------- design tokens from colors_and_type.css ---------------- $tokenRe = [regex]'--(kole-[a-z0-9-]+)\s*:\s*([^;]+);(?:\s*/\*\s*(.*?)\s*\*/)?' $tokens = @() foreach ($m in $tokenRe.Matches((Get-Content "$lib/colors_and_type.css" -Raw -Encoding UTF8))) { $tokens += ,@{ name = $m.Groups[1].Value; value = $m.Groups[2].Value.Trim(); comment = $m.Groups[3].Value.Trim() } } # ---------------- assemble components ---------------- $kinds = @('html','css','jsx','vue2','vue3') $out = @() $missing = @() $noCat = @() $i = 0 foreach ($c in $idx.components) { $i++ $p = $c.frameworksPrefix $filesMap = [ordered]@{} $sizesMap = [ordered]@{} $sourcesMap = [ordered]@{} foreach ($k in $kinds) { $ext = switch ($k) { 'html' { ".html" } 'css' { ".css" } 'jsx' { ".jsx" } 'vue2' { ".vue2.vue" } 'vue3' { ".vue3.vue" } } $fname = Find-FwFile ("{0}{1}" -f $p, $ext) if ($fname) { $filesMap[$k] = "../frameworks/$fname" $fInfo = Get-Item ("frameworks/{0}" -f $fname) $sizesMap[$k] = [int]$fInfo.Length $sourcesMap[$k] = Get-Content $fInfo.FullName -Raw -Encoding UTF8 } else { $missing += ("{0}:{1}" -f $c.slug, $k) } } $cat = $null if ($slugCat.ContainsKey($c.slug)) { $cat = $slugCat[$c.slug] } else { $noCat += $c.slug } $nameStr = [string]$c.name $enToken = $nameStr.Split(' ')[-1] $spec = Extract-SpecSection (Join-Path "." $c.specFile) $enToken $contract = $null if ($c.contract) { $contract = Parse-Contract (Join-Path $lib $c.contract) } $entry = [ordered]@{ slug = $c.slug name = $nameStr tier = $c.tier confidence = $c.confidence specBatch = [int]$c.specBatch specFile = $c.specFile contractRef = $c.contract category = $cat family = $(if ($famOf.ContainsKey([string]$c.slug)) { $famOf[[string]$c.slug].family } else { $null }) familyRole = $(if ($famOf.ContainsKey([string]$c.slug)) { $famOf[[string]$c.slug].role } else { $null }) familyParams= $(if ($famOf.ContainsKey([string]$c.slug)) { $famOf[[string]$c.slug].params } else { $null }) specLines = $(if ($spec) { @($spec.lines) } else { @() }) specSummary = $(if ($spec) { $spec.summary } else { '' }) contract = $contract files = $filesMap sizes = $sizesMap sources = $sourcesMap } $out += $entry if (($i % 20) -eq 0) { Write-Host (" processed {0}/{1}" -f $i, $idx.components.Count) } } # ---------------- changelog extraction (site /changelog page + meta.version sync) ---------------- $changelog = @() if (Test-Path "CHANGELOG.md") { $clLines = Get-Content "CHANGELOG.md" -Encoding UTF8 $cur = $null foreach ($L in $clLines) { if ($L -match '^##\s+\[([0-9]+\.[0-9]+\.[0-9]+)\]\s*-\s*(\S+)') { if ($cur) { $changelog += $cur } $cur = @{ version = $Matches[1]; date = $Matches[2]; sections = @() } } elseif ($L -match '^###\s+(.+)$' -and $cur) { $cur.sections += ,@{ title = $Matches[1]; items = @() } } elseif ($L -match '^-\s+(.+)$' -and $cur -and $cur.sections.Count -gt 0) { $lastSec = $cur.sections[$cur.sections.Count - 1] $lastSec.items += $Matches[1] } } if ($cur) { $changelog += $cur } } $siteVersion = "1.0.0" if ($changelog.Count -gt 0) { $siteVersion = [string]$changelog[0].version } $catList = @() foreach ($ck in @('general','navigation','input','display','feedback','system')) { $catList += ,@{ key = $ck; count = @($slugCat.GetEnumerator() | Where-Object { $_.Value -eq $ck }).Count } } $coreCount = @($out | Where-Object { $_.tier -eq 'core' }).Count $meta = [ordered]@{ generated = Get-Date -Format "yyyy-MM-dd HH:mm" lib = "kole-ui" total = $out.Count core = $coreCount extension = ($out.Count - $coreCount) brand = "#2F54EB" version = $siteVersion families = $(if ($famDoc) { [int]$famDoc.totalFamilies } else { 0 }) familyMembers = $(if ($famDoc) { [int]$famDoc.totalMembers } else { 0 }) note = "generated by build-site.ps1 from components/index.json + frameworks/ + specs + contracts + tokens + families" } $data = [ordered]@{ meta = $meta categories = $catList tokens = $tokens families = '__AA_FAMILIES_RAW__' changelog = $changelog components = $out } # ---------------- serialize (fast on both PS5.1 and PS7) ---------------- function Json-Escape([string]$s) { if ($null -eq $s) { return '""' } $s = $s.Replace('\', '\\').Replace('"', '\"') $s = $s.Replace("`r`n", "\n").Replace("`r", "\n").Replace("`n", "\n") $s = $s.Replace("`t", "\t").Replace("`b", "\b").Replace("`f", "\f") $s = [regex]::Replace($s, "[\x00-\x08\x0B\x0C\x0E-\x1F]", { param($m) ('\u{0:x4}' -f [int][char]$m.Value) }) return ('"' + $s + '"') } function Json-Value($v) { if ($null -eq $v) { return 'null' } if ($v -is [bool]) { if ($v) { return 'true' } else { return 'false' } } if ($v -is [int] -or $v -is [long] -or $v -is [double]) { return [string]$v } if ($v -is [string]) { return Json-Escape $v } if ($v -is [System.Collections.IDictionary]) { $sb = New-Object System.Text.StringBuilder [void]$sb.Append('{') $first = $true foreach ($key in $v.Keys) { if (-not $first) { [void]$sb.Append(',') } $first = $false [void]$sb.Append((Json-Escape ([string]$key))) [void]$sb.Append(':') [void]$sb.Append((Json-Value $v[$key])) } [void]$sb.Append('}') return $sb.ToString() } if ($v -is [System.Collections.IEnumerable]) { $sb = New-Object System.Text.StringBuilder [void]$sb.Append('[') $first = $true foreach ($item in $v) { if (-not $first) { [void]$sb.Append(',') } $first = $false [void]$sb.Append((Json-Value $item)) } [void]$sb.Append(']') return $sb.ToString() } return (Json-Escape ([string]$v)) } $json = Json-Value $data # Splice the family layer in as raw JSON: families.json is already valid JSON, and # round-tripping it through PS objects would risk both of the traps noted above. if ($famRaw) { $json = $json.Replace('"__AA_FAMILIES_RAW__"', $famRaw) } else { $json = $json.Replace('"__AA_FAMILIES_RAW__"', 'null') } New-Item -ItemType Directory -Force -Path site | Out-Null $js = "/* AUTO-GENERATED by build-site.ps1 - do not edit by hand */`r`nwindow.KOLE_DATA = $json;`r`n" # UTF-8 WITHOUT BOM: BOM breaks strict JSON parsers (node require, agent toolchains) $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText((Join-Path (Get-Location) "site/data.js"), $js, $utf8NoBom) [System.IO.File]::WriteAllText((Join-Path (Get-Location) "site/data.json"), $json, $utf8NoBom) # ---------------- design token export (W3C DTCG + Figma Tokens) ---------------- # Two standard interchange formats so the tokens can be consumed outside this repo # without hand-copying values. Both are generated here, never edited by hand. function Get-TokenGroup([string]$name) { if ($name -match 'color|brand|text|bg|border|success|warning|error|info|mask') { return 'color' } if ($name -match 'font|type') { return 'typography' } if ($name -match 'radius') { return 'radius' } if ($name -match 'space|gap|padding') { return 'spacing' } if ($name -match 'shadow|elevation') { return 'shadow' } if ($name -match 'duration|ease|motion') { return 'motion' } if ($name -match 'size|height|width|icon') { return 'size' } return 'misc' } # W3C Design Tokens Community Group format: nested groups, $value / $type. $dtcg = [ordered]@{} foreach ($t in $tokens) { $g = Get-TokenGroup $t.name if (-not $dtcg.Contains($g)) { $dtcg[$g] = [ordered]@{} } $type = switch ($g) { 'color' { 'color' } 'radius' { 'dimension' } 'spacing' { 'dimension' } 'size' { 'dimension' } 'shadow' { 'shadow' } 'typography' { 'fontFamily' } default { 'other' } } $node = [ordered]@{ '$value' = $t.value; '$type' = $type } if ($t.comment) { $node['$description'] = $t.comment } $dtcg[$g][$t.name] = $node } $dtcgDoc = [ordered]@{ '$schema' = 'https://schemas.design-tokens.org/draft/dtcg.json' '$metadata' = [ordered]@{ name = 'Kole UI'; version = $siteVersion; brand = '#2F54EB'; generated = (Get-Date -Format 'yyyy-MM-dd HH:mm') } 'tokens' = $dtcg } New-Item -ItemType Directory -Force -Path "site/tokens" | Out-Null [System.IO.File]::WriteAllText((Join-Path (Get-Location) "site/tokens/dtcg.json"), (Json-Value $dtcgDoc), $utf8NoBom) # Figma Tokens (Tokens Studio) format: flat "global" set, value + type + description. $figmaGlobal = [ordered]@{} foreach ($t in $tokens) { $g = Get-TokenGroup $t.name $ft = switch ($g) { 'color' { 'color' } 'size' { 'sizing' } 'spacing' { 'spacing' } 'radius' { 'borderRadius' } 'shadow' { 'boxShadow' } default { 'other' } } $figmaGlobal[$t.name] = [ordered]@{ value = $t.value; type = $ft; description = $t.comment } } $figmaDoc = [ordered]@{ global = $figmaGlobal '$themes' = @() '$metadata' = [ordered]@{ tokenSetOrder = @('global') } } [System.IO.File]::WriteAllText((Join-Path (Get-Location) "site/tokens/figma.json"), (Json-Value $figmaDoc), $utf8NoBom) # Plain CSS custom properties - drop-in for any project that just wants the variables. $cssLines = New-Object System.Text.StringBuilder [void]$cssLines.AppendLine('/* Kole UI design tokens - generated by build-site.ps1 */') [void]$cssLines.AppendLine(':root {') foreach ($t in $tokens) { $note = if ($t.comment) { ' /* ' + $t.comment + ' */' } else { '' } [void]$cssLines.AppendLine((' --{0}: {1};{2}' -f $t.name, $t.value, $note)) } [void]$cssLines.AppendLine('}') [System.IO.File]::WriteAllText((Join-Path (Get-Location) "site/tokens/tokens.css"), $cssLines.ToString(), $utf8NoBom) Write-Output ("token export: site/tokens/ (dtcg.json + figma.json + tokens.css), {0} tokens" -f $tokens.Count) # ---------------- static thin shells + sitemap (SEO / crawler entry points) ---------------- # Set SITE_URL_BASE in the environment for deployed absolute URLs. $defaultSiteUrlBase = "https://YOUR-ACCOUNT.github.io/kole-ui/" if ([string]::IsNullOrWhiteSpace($env:SITE_URL_BASE)) { Write-Warning "SITE_URL_BASE is not set; using placeholder URL $defaultSiteUrlBase" $SiteUrlBase = $defaultSiteUrlBase } else { $SiteUrlBase = $env:SITE_URL_BASE.Trim() } $shellDir = "site/components" $siteBase = $SiteUrlBase.TrimEnd('/') $platforms = @('h5', 'react', 'vue2', 'vue3') $platformLabels = @{ h5 = 'H5'; react = 'React'; vue2 = 'Vue 2'; vue3 = 'Vue 3' } New-Item -ItemType Directory -Force -Path $shellDir | Out-Null function Html-Escape([string]$s) { if ($null -eq $s) { return '' } return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') } $siteBaseHtml = Html-Escape $siteBase foreach ($c in $out) { $title = Html-Escape ([string]$c.name) $desc = Html-Escape ([string]$c.specSummary) $slug = [string]$c.slug if ($slug -notmatch '^[a-z0-9]+(?:-[a-z0-9]+)*$') { throw "Invalid component slug '$slug'; expected lowercase letters, digits, and single hyphens only." } # Route targets are History-API paths (no "#"): app.js renders /site/component/ # and the web server falls back to /site/index.html. Shell depth decides the "../" prefix. $target = "../component/$slug" $targetJson = Json-Escape $target $shell = @" $title - Kole UI "@ [System.IO.File]::WriteAllText((Join-Path (Get-Location) "$shellDir/$slug.html"), $shell, $utf8NoBom) foreach ($platform in $platforms) { $platformLabel = $platformLabels[$platform] $platformDir = Join-Path (Get-Location) "$shellDir/$slug" New-Item -ItemType Directory -Force -Path $platformDir | Out-Null $target = "../../component/$slug/$platform" $targetJson = Json-Escape $target $platformShell = @" $title - $platformLabel - Kole UI "@ [System.IO.File]::WriteAllText((Join-Path $platformDir "$platform.html"), $platformShell, $utf8NoBom) } } $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine('') [void]$sb.AppendLine('') $urls = @($siteBase + "/site/index.html") foreach ($c in $out) { $urls += $siteBase + "/site/components/$($c.slug).html" foreach ($platform in $platforms) { $urls += $siteBase + "/site/components/$($c.slug)/$platform.html" } } # Template page library (S4-P11): typical admin pages under site/scenario/ $scenarioPages = @('login', 'dashboard', 'order-list', 'settings', 'user-management') foreach ($p in $scenarioPages) { $urls += $siteBase + "/site/scenario/$p.html" } foreach ($u in $urls) { [void]$sb.AppendLine(' ' + $u + '') } [void]$sb.AppendLine('') [System.IO.File]::WriteAllText((Join-Path (Get-Location) "sitemap.xml"), $sb.ToString(), $utf8NoBom) Write-Output ("thin shells: {0}, platform shells: {1}, sitemap.xml: {2} urls" -f $out.Count, ($out.Count * $platforms.Count), $urls.Count) $sizeKb = (Get-Item site/data.js).Length / 1KB Write-Output ("site/data.js written: {0:N0} KB, {1} components, {2} tokens" -f $sizeKb, $out.Count, $tokens.Count) if ($famDoc) { $famCount = @($out | Where-Object { $_.family }).Count Write-Output ("family layer: {0} families, {1}/{2} components carry family metadata" -f $famDoc.totalFamilies, $famCount, $out.Count) } if ($missing.Count -gt 0) { Write-Warning ("missing framework files: " + ($missing -join ', ')) } if ($noCat.Count -gt 0) { Write-Warning ("components without category: " + ($noCat -join ', ')) } if ($missing.Count -eq 0 -and $noCat.Count -eq 0) { Write-Output "all components complete (5-form files + category)" }