Files
aurora-admin f1fbfc2ddb
Regression / regression (push) Canceled after 0s
feat(品牌标识): 几何 K 图标(favicon/顶栏标记/theme-color) + 并行会话成果入库
## 品牌标识(本次会话)

起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」,
那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」,
移动端站已是「K」;移动端文档站则完全没有 favicon)。

- 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边;
  描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」
- 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark);
  顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28))
- 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg)
- 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留)
- 移动端 7 个模板补 favicon(此前计数 0)

验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/
theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致;
两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。

## 并行会话成果(本次一并入库)

- 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具
  + IconPreview 预览页 + ICON-SPEC.md 冻结规格
- 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁
- PC 组件:103 个大后台组件 / 组件11 批次
- uni-app:PC 端试点 + 移动端端实现 + 真实编译验证

## 工程

- .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库)
- CHANGELOG 补品牌标识条目
- ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
2026-09-21 10:05:48 +08:00

493 lines
21 KiB
PowerShell

# 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('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
}
$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/<slug>
# and the web server falls back to /site/index.html. Shell depth decides the "../" prefix.
$target = "../component/$slug"
$targetJson = Json-Escape $target
$shell = @"
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>$title - Kole UI</title>
<meta name="description" content="$desc">
<link rel="canonical" href="$siteBaseHtml/site/components/$slug.html">
<meta http-equiv="refresh" content="0;url=$target">
</head>
<body>
<script>location.replace($targetJson);</script>
<noscript><p><a href="$target">Open $title</a></p></noscript>
</body>
</html>
"@
[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 = @"
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>$title - $platformLabel - Kole UI</title>
<meta name="description" content="$desc">
<link rel="canonical" href="$siteBaseHtml/site/components/$slug/$platform.html">
<meta http-equiv="refresh" content="0;url=$target">
</head>
<body>
<script>location.replace($targetJson);</script>
<noscript><p><a href="$target">Open $title - $platformLabel</a></p></noscript>
</body>
</html>
"@
[System.IO.File]::WriteAllText((Join-Path $platformDir "$platform.html"), $platformShell, $utf8NoBom)
}
}
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine('<?xml version="1.0" encoding="UTF-8"?>')
[void]$sb.AppendLine('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">')
$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(' <url><loc>' + $u + '</loc></url>')
}
[void]$sb.AppendLine('</urlset>')
[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)" }