$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 $panelPath = Join-Path $root 'static/assets/modules/genealogy/opaque/g01-empty-panel.png' $framePath = Join-Path $root 'static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png' $pipelinePackage = Get-Content -LiteralPath (Join-Path $root 'design-pipeline/package.json') -Raw -Encoding utf8 $frameBuilderPath = Join-Path $root 'design-pipeline/scripts/build-g01-empty-frame.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.get("state") || "default"', 'empty-create-action', 'empty-search-action', 'empty-invite-action', 'empty-create-note', 'g01-empty-panel-frame.png', 'const createGenealogy = () =>', '/pages/genealogy/g03-create-genealogy', '/pages/genealogy/g06-search-genealogies', '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).*class="current-slip".*class="shortcut-grid".*class="section-divider".*\s*]*class="genealogy-list-scroll"[^>]*>.*class="genealogy-lower".*' -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*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame\.png"\)\s*center\s*/\s*100%\s+100%\s+no-repeat;' -Message 'G01 empty state must render the exact extracted transparent frame as its container background' 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*\{[^}]*height:\s*1120rpx;' -Message 'G01 empty panel does not use the approved visibly fuller 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*48rpx;' -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*28rpx;' -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;[^}]*height:\s*124rpx;' -Message 'G01 empty actions do not use the approved prominent size' Assert-Match -Content $g01 -Pattern '(?s)\.empty-search-action__copy,\s*\.empty-invite-action__copy\s*\{[^}]*font-size:\s*34rpx;' -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*31rpx;' -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*26rpx;[^}]*line-height:\s*38rpx;' -Message 'G01 empty-state create note does not use the approved readable size' if ($g01 -match '(?s).*?]+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 $panelPath)) { throw 'G01 requires its final raster empty-panel asset' } if (-not (Test-Path -LiteralPath $framePath)) { throw 'G01 requires the extracted transparent empty-panel frame 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-empty-frame"\s*:\s*"node scripts/build-g01-empty-frame\.mjs"' -Message 'G01 transparent frame builder is not exposed by the design pipeline' 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 $panel = [System.Drawing.Bitmap]::FromFile($panelPath) try { if ($panel.Width -ne 1122 -or $panel.Height -ne 1402) { throw 'G01 empty-panel asset must remain 1122 x 1402px' } $corners = @( $panel.GetPixel(0, 0).A, $panel.GetPixel(($panel.Width - 1), 0).A, $panel.GetPixel(0, ($panel.Height - 1)).A, $panel.GetPixel(($panel.Width - 1), ($panel.Height - 1)).A ) if (($corners | Where-Object { $_ -ne 255 }).Count -ne 0) { throw 'G01 empty-panel asset must have opaque outer corners' } } finally { $panel.Dispose() } $frame = [System.Drawing.Bitmap]::FromFile($framePath) try { if ($frame.Width -ne 1122 -or $frame.Height -ne 1402) { throw 'G01 transparent empty-panel frame must remain 1122 x 1402px' } foreach ($point in @(@(0, 0), @(1121, 0), @(0, 1401), @(1121, 1401), @(561, 701))) { 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'