Files
jiapuapp/.superpowers/sdd/review-task-1-779a442..2f22df6.diff
T
rain a127280ce6 1
2026-07-21 21:02:57 +08:00

144 lines
6.9 KiB
Diff

# Review package: 779a442..2f22df6
## Commits
2f22df6 fix: adapt shared page header to safe area
## Files changed
components/PageHeader.vue | 12 ++++++++++--
tests/shared-component-document-flow-contract.ps1 | 4 ++++
tests/shared-interaction-accessibility-contract.ps1 | 10 ++++++++++
3 files changed, 24 insertions(+), 2 deletions(-)
## Diff
diff --git a/components/PageHeader.vue b/components/PageHeader.vue
index 5ef28dd..17ab7ef 100644
--- a/components/PageHeader.vue
+++ b/components/PageHeader.vue
@@ -67,52 +67,60 @@
</view>
</template>
<script setup>
const props = defineProps({
title: { type: String, required: true },
action: { type: String, default: "" },
root: { type: Boolean, default: false },
unreadCount: { type: Number, default: 0 },
fallbackUrl: { type: String, default: "/pages/genealogy/g01-my-genealogies" },
+ customBack: { type: Boolean, default: false },
});
+const emit = defineEmits(["brand", "notice", "action", "back"]);
+
const goBack = () => {
+ if (props.customBack) {
+ emit("back");
+ return;
+ }
const stack = getCurrentPages();
if (stack.length > 1) {
uni.navigateBack();
return;
}
uni.reLaunch({ url: props.fallbackUrl });
};
</script>
<style scoped lang="scss">
.page-header-slot {
- height: 104rpx;
+ height: calc(104rpx + var(--status-bar-height, 0px));
flex: none;
}
.page-header-slot--root {
height: calc(124rpx + var(--status-bar-height, 0px));
}
.page-header {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 30;
display: flex;
align-items: center;
- height: 104rpx;
+ height: calc(104rpx + var(--status-bar-height, 0px));
padding: 0 24rpx;
+ padding-top: var(--status-bar-height, 0px);
box-sizing: border-box;
background: $brand-red;
color: #fff9ed;
}
.page-header--root {
height: calc(124rpx + var(--status-bar-height, 0px));
padding-top: var(--status-bar-height, 0px);
background-color: #b52e22;
overflow: hidden;
diff --git a/tests/shared-component-document-flow-contract.ps1 b/tests/shared-component-document-flow-contract.ps1
index e7b88ae..71e6b5f 100644
--- a/tests/shared-component-document-flow-contract.ps1
+++ b/tests/shared-component-document-flow-contract.ps1
@@ -28,20 +28,24 @@ if ($header -match '(?s)\.page-header--root \.header-side,\s*\.page-header--root
throw 'PageHeader root content must not use positioning'
}
Assert-Match $header '(?s)\.header-icon-button\s*\{[^}]*display:\s*grid;[^}]*place-items:\s*center;' 'PageHeader icon button must use a grid overlay'
if ($header -match '(?s)\.header-icon-button\s*\{[^}]*position\s*:') {
throw 'PageHeader icon button must not be a positioning context'
}
Assert-Match $header '(?s)\.header-logo,\s*\.header-notice-icon,\s*\.notice-dot\s*\{[^}]*grid-area:\s*1 / 1;' 'PageHeader icon and notice dot must share one grid cell'
if ($header -match '(?s)\.notice-dot\s*\{[^}]*position\s*:') {
throw 'PageHeader notice dot must use grid alignment instead of positioning'
}
+Assert-Match $header '(?s)\.page-header-slot\s*\{[^}]*height:\s*calc\(104rpx \+ var\(--status-bar-height, 0px\)\);' 'PageHeader secondary slot must reserve status bar plus 104rpx content height'
+Assert-Match $header '(?s)\.page-header\s*\{[^}]*height:\s*calc\(104rpx \+ var\(--status-bar-height, 0px\)\);[^}]*padding-top:\s*var\(--status-bar-height, 0px\);' 'PageHeader secondary header must place its 104rpx content below the status bar'
+Assert-Match $header '(?s)\.page-header-slot--root\s*\{[^}]*height:\s*calc\(124rpx \+ var\(--status-bar-height, 0px\)\);' 'PageHeader root slot height must remain unchanged'
+Assert-Match $header '(?s)\.page-header--root\s*\{[^}]*height:\s*calc\(124rpx \+ var\(--status-bar-height, 0px\)\);' 'PageHeader root height must remain unchanged'
if ($card -match '(?im)(?<![-\w])position\s*:') {
throw 'GenealogyCard must use grid instead of positioning'
}
Assert-Match $card '(?s)\.genealogy-card\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*72rpx minmax\(0, 1fr\);[^}]*column-gap:\s*22rpx;' 'GenealogyCard must own the two-column document-flow grid'
Assert-Match $card '(?s)\.row-frame\s*\{[^}]*grid-area:\s*1 / 1 / 2 / -1;[^}]*z-index:\s*1;[^}]*width:\s*calc\(100% \+ var\(--card-padding-x\) \+ var\(--card-padding-x\)\);[^}]*height:\s*calc\(100% \+ var\(--card-padding-y\) \+ var\(--card-padding-y\)\);[^}]*margin:\s*calc\(-1 \* var\(--card-padding-y\)\) calc\(-1 \* var\(--card-padding-x\)\);' 'GenealogyCard row frame must span the padded card grid above the fixed page background'
Assert-Match $card '(?s)\.surname-seal\s*\{[^}]*display:\s*grid;[^}]*grid-row:\s*1;[^}]*z-index:\s*2;[^}]*place-items:\s*center;' 'GenealogyCard surname seal must share the frame row and stay above it'
Assert-Match $card '(?s)\.card-main\s*\{[^}]*grid-row:\s*1;[^}]*z-index:\s*2;' 'GenealogyCard content must share the frame row and stay above it'
Assert-Match $card '(?s)\.surname-seal-frame,\s*\.surname-seal-copy\s*\{[^}]*grid-area:\s*1 / 1;' 'GenealogyCard seal frame and copy must share one grid cell'
if ($card -match '(?s)\.card-name\s*\{[^}]*(?:overflow:\s*hidden|text-overflow:\s*ellipsis|white-space:\s*nowrap)') {
diff --git a/tests/shared-interaction-accessibility-contract.ps1 b/tests/shared-interaction-accessibility-contract.ps1
index 84ad70c..1059467 100644
--- a/tests/shared-interaction-accessibility-contract.ps1
+++ b/tests/shared-interaction-accessibility-contract.ps1
@@ -10,20 +10,30 @@ if ($button -match '<view\s+[^>]*class="app-button"') { throw 'AppButton must us
$header = Read-Utf8 'components/PageHeader.vue'
foreach ($pattern in @(
'(?s)<button[^>]+class="header-back"[^>]+aria-label=',
'(?s)<button[^>]+class="header-icon-button header-notice"[^>]+aria-label=',
'(?s)<button[^>]+class="header-icon-button"[^>]+aria-label='
)) {
if ($header -notmatch $pattern) { throw "PageHeader accessibility contract missing: $pattern" }
}
if (-not $header.Contains('fallbackUrl')) { throw 'PageHeader must expose a deep-link fallback route' }
+foreach ($token in @(
+ 'customBack: { type: Boolean, default: false }',
+ 'const emit = defineEmits(["brand", "notice", "action", "back"]);',
+ 'if (props.customBack) {',
+ 'emit("back");',
+ 'uni.navigateBack();',
+ 'uni.reLaunch({ url: props.fallbackUrl });'
+)) {
+ if (-not $header.Contains($token)) { throw "PageHeader back contract missing: $token" }
+}
$dialog = Read-Utf8 'components/AppDialog.vue'
foreach ($token in @('role="dialog"', 'aria-modal="true"', 'tabindex="-1"', '@keydown.esc.stop="cancel"', 'max-height: calc(100vh - 80rpx)', 'overflow-y: auto')) {
if (-not $dialog.Contains($token)) { throw "AppDialog accessibility contract missing: $token" }
}
$toast = Read-Utf8 'components/AppToast.vue'
foreach ($token in @('v-show="visible"', 'role="status"', 'aria-live="polite"', 'aria-atomic="true"')) {
if (-not $toast.Contains($token)) { throw "AppToast accessibility contract missing: $token" }
}