126 lines
8.8 KiB
PowerShell
126 lines
8.8 KiB
PowerShell
$ErrorActionPreference = 'Stop'
|
|
|
|
function Assert-Match {
|
|
param([string]$Content, [string]$Pattern, [string]$Message)
|
|
if ($Content -notmatch $Pattern) { throw $Message }
|
|
}
|
|
|
|
function Assert-NoCssSurface {
|
|
param([string]$Content, [string]$ClassName)
|
|
foreach ($property in @('border', 'border-radius')) {
|
|
$pattern = "(?s)\\.$ClassName\\s*\\{[^}]*\\b$property\\s*:"
|
|
if ($Content -match $pattern) {
|
|
throw "G01 empty state must not construct .$ClassName with CSS $property"
|
|
}
|
|
}
|
|
}
|
|
|
|
$root = Split-Path -Parent $PSScriptRoot
|
|
$pages = Get-Content -LiteralPath (Join-Path $root 'pages.json') -Raw -Encoding utf8 | ConvertFrom-Json
|
|
$paths = @($pages.pages.path)
|
|
$g01 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g01-my-genealogies.vue') -Raw -Encoding utf8
|
|
$runtimeSmoke = Get-Content -LiteralPath (Join-Path $root 'tests/g01-empty-state-runtime-smoke.js') -Raw -Encoding utf8
|
|
$framePath = Join-Path $root 'static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png'
|
|
$frameManifestPath = Join-Path $root 'design-pipeline/manifests/g01-state-frame-v3.json'
|
|
$frameMasterPath = Join-Path $root 'docs/design/assets/g01-state/masters/g01-empty-panel-master.png'
|
|
$pipelinePackage = Get-Content -LiteralPath (Join-Path $root 'design-pipeline/package.json') -Raw -Encoding utf8
|
|
$frameBuilderPath = Join-Path $root 'design-pipeline/scripts/build-raster-assets.mjs'
|
|
|
|
if (-not ($paths -contains 'pages/genealogy/g01-my-genealogies')) { throw 'G01 route missing' }
|
|
if ($paths -contains 'pages/genealogy/g02-empty-genealogies') { throw 'G02 route must be removed; empty state belongs to G01' }
|
|
|
|
foreach ($required in @(
|
|
'forceEmptyState',
|
|
'query?.state',
|
|
'empty-create-action',
|
|
'empty-search-action',
|
|
'empty-invite-action',
|
|
'empty-create-note',
|
|
'@include adaptive-genealogy-state-panel;',
|
|
'const createGenealogy = () =>',
|
|
'const hasGenealogies = computed('
|
|
)) {
|
|
if ($g01 -notmatch [regex]::Escape($required)) { throw "Missing G01 empty-state contract: $required" }
|
|
}
|
|
|
|
foreach ($required in @(
|
|
'class="genealogy-fixed-zone"',
|
|
'class="genealogy-list-scroll"',
|
|
'scroll-y',
|
|
':scroll-top="listScrollCommand"',
|
|
'@scroll="handleListScroll"',
|
|
'const isListLayout = computed(',
|
|
'const resetListScroll = async () =>'
|
|
)) {
|
|
if ($g01 -notmatch [regex]::Escape($required)) { throw "Missing G01 split-scroll contract: $required" }
|
|
}
|
|
|
|
Assert-Match -Content $g01 -Pattern '(?s)<view class="genealogy-fixed-zone">.*class="current-slip".*class="shortcut-grid".*class="section-divider".*</view>\s*<scroll-view[^>]*class="genealogy-list-scroll"[^>]*>.*class="genealogy-lower".*</scroll-view>' -Message 'G01 fixed and scrolling regions are not separated correctly'
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-index--split\s*\{[^}]*height:\s*100vh;[^}]*padding-bottom:\s*calc\(112rpx\s*\+\s*env\(safe-area-inset-bottom\)\)' -Message 'G01 split layout must reserve the fixed tabbar and safe area'
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-list-scroll\s*\{[^}]*height:\s*0;[^}]*flex:\s*1;' -Message 'G01 list scroll region must consume the remaining computed height'
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel\s*\{[^}]*@include\s+adaptive\.adaptive-genealogy-state-panel;' -Message 'G01 empty state must consume the shared adaptive state-panel profile'
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-empty-state\s*\{[^}]*min-height:\s*1120rpx;' -Message 'G01 empty state does not reserve the approved visibly fuller height'
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel\s*\{[^}]*min-height:\s*1120rpx;' -Message 'G01 empty panel does not retain the approved minimum height'
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel__content\s*\{[^}]*justify-content:\s*center;[^}]*padding:\s*50rpx 28rpx 46rpx;' -Message 'G01 empty panel content is not centered with the approved responsive padding'
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.empty-seal\s*\{[^}]*width:\s*120rpx;[^}]*height:\s*184rpx;' -Message 'G01 empty seal does not use the approved prominent size'
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.empty-title\s*\{[^}]*margin-top:\s*30rpx;[^}]*font-size:\s*clamp\(24px, 48rpx, 30px\);' -Message 'G01 empty title does not use the approved prominent size and spacing'
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.empty-copy\s*\{[^}]*min-height:\s*84rpx;[^}]*margin-top:\s*18rpx;[^}]*font-size:\s*clamp\(16px, 28rpx, 20px\);' -Message 'G01 empty guidance does not use the approved prominent size and spacing'
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.empty-search-action,\s*\.empty-invite-action\s*\{[^}]*width:\s*560rpx;[^}]*min-height:\s*124rpx;' -Message 'G01 empty actions do not retain the approved prominent minimum size'
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.empty-search-action__copy,\s*\.empty-invite-action__copy\s*\{[^}]*font-size:\s*clamp\(19px, 34rpx, 24px\);' -Message 'G01 empty action labels do not use the approved prominent size'
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.empty-create-action\s*\{[^}]*min-height:\s*76rpx;[^}]*margin-top:\s*14rpx;' -Message 'G01 empty create action does not use the approved prominent hit area and spacing'
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.empty-create-action__copy\s*\{[^}]*font-size:\s*clamp\(17px, 31rpx, 22px\);' -Message 'G01 empty create action does not use the approved prominent size'
|
|
if ($g01 -match '(?s)\.empty-create-action__copy\s*\{[^}]*text-decoration:\s*underline') {
|
|
throw 'G01 empty create action must not render a text underline'
|
|
}
|
|
Assert-Match -Content $g01 -Pattern '(?s)\.empty-create-note\s*\{[^}]*font-size:\s*clamp\(16px, 26rpx, 20px\);[^}]*line-height:\s*max\(1.35em, clamp\(20px, 38rpx, 26px\)\);' -Message 'G01 empty-state create note does not use the approved readable size'
|
|
if ($g01 -match '(?s)<view v-else class="genealogy-empty-state">.*?<image[^>]+g01-empty-panel\.png') { throw 'G01 empty state must not retain the opaque panel background' }
|
|
|
|
foreach ($className in @('empty-panel', 'empty-create-action', 'empty-search-action')) {
|
|
Assert-NoCssSurface -Content $g01 -ClassName $className
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $framePath)) { throw 'G01 requires the extracted transparent empty-panel frame asset' }
|
|
if (-not (Test-Path -LiteralPath $frameManifestPath)) { throw 'G01 transparent frame requires its schema v3 owner' }
|
|
if (-not (Test-Path -LiteralPath $frameMasterPath)) { throw 'G01 transparent frame requires its traceable master asset' }
|
|
if (-not (Test-Path -LiteralPath $frameBuilderPath)) { throw 'G01 transparent empty-panel frame must remain reproducible from its builder' }
|
|
Assert-Match -Content $pipelinePackage -Pattern '"build:g01-state-frame"\s*:\s*"node scripts/build-raster-assets\.mjs design-pipeline/manifests/g01-state-frame-v3\.json"' -Message 'G01 transparent frame builder is not exposed by the design pipeline'
|
|
$frameManifest = Get-Content -LiteralPath $frameManifestPath -Raw -Encoding utf8 | ConvertFrom-Json
|
|
if ($frameManifest.kind -ne 'asset-build-manifest' -or $frameManifest.family -ne 'g01-state-frame-v3') {
|
|
throw 'G01 transparent frame manifest has the wrong owner identity'
|
|
}
|
|
if ($frameManifest.assets.Count -ne 1) { throw 'G01 transparent frame manifest must own exactly one output' }
|
|
$frameAsset = $frameManifest.assets[0]
|
|
if ($frameAsset.source -ne 'docs/design/assets/g01-state/masters/g01-empty-panel-master.png' -or
|
|
$frameAsset.output -ne 'static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png' -or
|
|
$frameAsset.processing.mode -ne 'warm-gold-frame-extract') {
|
|
throw 'G01 transparent frame manifest no longer owns the approved master, output and extraction algorithm'
|
|
}
|
|
$expectedFrameWidth = [int]$frameAsset.outputPixels.width
|
|
$expectedFrameHeight = [int]$frameAsset.outputPixels.height
|
|
Assert-Match -Content $runtimeSmoke -Pattern '\{\s*width:\s*412,\s*height:\s*1000\s*\}' -Message 'G01 runtime smoke must include the approved 412x1000 tall-screen stress viewport'
|
|
Add-Type -AssemblyName System.Drawing
|
|
$frame = [System.Drawing.Bitmap]::FromFile($framePath)
|
|
try {
|
|
if ($frame.Width -ne $expectedFrameWidth -or $frame.Height -ne $expectedFrameHeight) {
|
|
throw 'G01 transparent empty-panel frame no longer matches its schema v3 owner dimensions'
|
|
}
|
|
$lastX = $expectedFrameWidth - 1
|
|
$lastY = $expectedFrameHeight - 1
|
|
$centerX = [math]::Floor($expectedFrameWidth / 2)
|
|
$centerY = [math]::Floor($expectedFrameHeight / 2)
|
|
foreach ($point in @(@(0, 0), @($lastX, 0), @(0, $lastY), @($lastX, $lastY), @($centerX, $centerY))) {
|
|
if ($frame.GetPixel($point[0], $point[1]).A -ne 0) { throw 'G01 transparent empty-panel frame retains an opaque background pixel' }
|
|
}
|
|
$visible = 0
|
|
for ($y = 0; $y -lt $frame.Height; $y += 4) {
|
|
for ($x = 0; $x -lt $frame.Width; $x += 4) {
|
|
if ($frame.GetPixel($x, $y).A -gt 0) { $visible++ }
|
|
}
|
|
}
|
|
if ($visible -lt 1200 -or $visible -gt 12000) { throw "G01 transparent frame alpha coverage is implausible: $visible sampled pixels" }
|
|
} finally {
|
|
$frame.Dispose()
|
|
}
|
|
|
|
Write-Output 'G01-EMPTY-STATE-CONTRACT PASS'
|