10 KiB
Home Scroll Top Mask Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a soft top gradient mask on the home page that fades in during page scroll, improving status-bar legibility and layering without changing the existing large-title scroll behavior.
Architecture: Keep the feature local to the home page. A small pure JavaScript utility calculates the 0–1 mask progress and is unit-tested with Node's built-in test runner. The home page measures the title position with Taro.createSelectorQuery(), listens to page scroll with Taro.usePageScroll(), and renders one fixed non-interactive mask above the page content.
Tech Stack: Taro 3.6.31, React 18, Sass, WeChat Mini Program custom navigation, Node 22 built-in test runner.
Global Constraints
- Only modify the home page feature:
src/pages/index/index.tsx,src/pages/index/index.scss, and the new home-top-mask utility/tests. - The large title
智绘微刻must continue scrolling naturally; no sticky title, no shrinking title, no title color animation. - Use a soft gradient only; do not use backdrop blur.
- Mask must be non-interactive with
pointer-events: none. - Reuse
useSafeArea()anduseStatusBar(resolvedTheme); do not hard-code device-specific status-bar heights. - Preserve the user's existing uncommitted changes in unrelated files and in
dist/.
File Structure
- Create:
src/utils/homeTopMask.js— pure helper that converts scroll position and title metrics into mask opacity. - Create:
tools/homeTopMask.test.mjs— Node unit tests for the progress helper. - Modify:
src/pages/index/index.tsx— measure the title, listen to page scroll, render the mask, and feed opacity/style into it. - Modify:
src/pages/index/index.scss— add fixed mask layout and light/dark gradient styles.
Task 1: Add the pure mask-progress helper with tests
Files:
- Create:
src/utils/homeTopMask.js - Create:
tools/homeTopMask.test.mjs
Interfaces:
-
Produces:
getTopMaskProgress({ scrollTop, startY, rangeY }) => number- Inputs are pixel numbers.
- Output is always a finite number between
0and1.
-
Step 1: Write the failing test
Create tools/homeTopMask.test.mjs with:
import test from 'node:test'
import assert from 'node:assert/strict'
import { getTopMaskProgress } from '../src/utils/homeTopMask.js'
test('returns 0 before the title reaches the status-bar area', () => {
assert.equal(
getTopMaskProgress({ scrollTop: 30, startY: 80, rangeY: 40 }),
0
)
})
test('ramps linearly through the transition range', () => {
assert.equal(
getTopMaskProgress({ scrollTop: 100, startY: 80, rangeY: 40 }),
0.5
)
})
test('returns 1 after the full transition range', () => {
assert.equal(
getTopMaskProgress({ scrollTop: 180, startY: 80, rangeY: 40 }),
1
)
})
test('clamps invalid values into a stable progress range', () => {
assert.equal(getTopMaskProgress({ scrollTop: -10, startY: 80, rangeY: 40 }), 0)
assert.equal(getTopMaskProgress({ scrollTop: 90, startY: 80, rangeY: 0 }), 1)
assert.equal(getTopMaskProgress({ scrollTop: Number.NaN, startY: 80, rangeY: 40 }), 0)
})
- Step 2: Run test to verify it fails
Run:
node --test tools/homeTopMask.test.mjs
Expected: FAIL because ../src/utils/homeTopMask.js does not exist.
- Step 3: Write minimal implementation
Create src/utils/homeTopMask.js with:
export function getTopMaskProgress({ scrollTop, startY, rangeY }) {
const safeScrollTop = Number.isFinite(scrollTop) ? scrollTop : 0
const start = Math.max(0, Number.isFinite(startY) ? startY : 0)
const range = Math.max(1, Number.isFinite(rangeY) ? rangeY : 1)
if (safeScrollTop <= start) {
return 0
}
return Math.min(1, (safeScrollTop - start) / range)
}
- Step 4: Run test to verify it passes
Run:
node --test tools/homeTopMask.test.mjs
Expected: PASS, 4 passing tests.
- Step 5: Commit only this task
git add src/utils/homeTopMask.js tools/homeTopMask.test.mjs
git commit -m "test: add home top mask progress helper"
Task 2: Add the fixed gradient mask to the home page
Files:
- Modify:
src/pages/index/index.tsx - Modify:
src/pages/index/index.scss
Interfaces:
-
Consumes:
getTopMaskProgress({ scrollTop, startY, rangeY }) => numberfromsrc/utils/homeTopMask.js.safe.statusBarHeight,safe.menuButtonTop, andsafe.menuButtonHeightfrom existinguseSafeArea().
-
Produces:
- A
View.home-top-maskelement on the home page. - Inline
heightandopacitystyles for that element.
- A
-
Step 1: Update the home page component
In src/pages/index/index.tsx, change the React import from:
import { useState } from 'react'
to:
import { useEffect, useRef, useState } from 'react'
Add this import after the existing local imports:
import { getTopMaskProgress } from '../../utils/homeTopMask'
Inside the Index component, immediately after:
const [searchKey, setSearchKey] = useState('')
add:
const [topMaskOpacity, setTopMaskOpacity] = useState(0)
const titleMetricsRef = useRef({ startY: 0, rangeY: 48, ready: false })
const scrollFrameRef = useRef<number | null>(null)
const latestScrollTopRef = useRef(0)
const applyTopMaskProgress = (scrollTop: number) => {
latestScrollTopRef.current = scrollTop
if (scrollFrameRef.current !== null) {
return
}
scrollFrameRef.current = (typeof requestAnimationFrame === 'function'
? requestAnimationFrame
: setTimeout
)(() => {
scrollFrameRef.current = null
const { startY, rangeY } = titleMetricsRef.current
setTopMaskOpacity(getTopMaskProgress({ scrollTop: latestScrollTopRef.current, startY, rangeY }))
}, 16) as unknown as number
}
useEffect(() => {
const measureTitle = () => {
Taro.createSelectorQuery()
.select('.home-header')
.boundingClientRect((rect: { top?: number; bottom?: number; height?: number } | null) => {
if (!rect || typeof rect.bottom !== 'number' || typeof rect.height !== 'number') {
const fallbackRange = Math.max(48, safe.headerPaddingTop * 0.45)
titleMetricsRef.current = {
startY: Math.max(0, safe.headerPaddingTop - safe.statusBarHeight),
rangeY: fallbackRange,
ready: true
}
applyTopMaskProgress(latestScrollTopRef.current)
return
}
const startY = Math.max(0, rect.bottom - safe.statusBarHeight - 8)
const rangeY = Math.max(32, rect.height * 0.7)
titleMetricsRef.current = { startY, rangeY, ready: true }
applyTopMaskProgress(latestScrollTopRef.current)
})
.exec()
}
Taro.nextTick(() => {
setTimeout(measureTitle, 80)
})
return () => {
if (scrollFrameRef.current !== null && typeof cancelAnimationFrame === 'function') {
cancelAnimationFrame(scrollFrameRef.current)
}
scrollFrameRef.current = null
}
}, [safe.headerPaddingTop, safe.statusBarHeight])
Taro.usePageScroll((e) => {
applyTopMaskProgress(e.scrollTop)
})
In the JSX, immediately after <ThemedPageMeta /> and before <View className='index-page'>, add:
<View
className='home-top-mask'
style={{
height: `${Math.max(safe.menuButtonTop + safe.menuButtonHeight + 18, safe.statusBarHeight + 52)}px`,
opacity: topMaskOpacity
}}
/>
- Step 2: Add mask styles
Append these styles to src/pages/index/index.scss:
.home-top-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 40;
pointer-events: none;
transform: translateZ(0);
will-change: opacity;
}
.theme-light .home-top-mask {
background: linear-gradient(
to bottom,
rgba(250, 247, 242, 0.98) 0%,
rgba(250, 247, 242, 0.92) 38%,
rgba(250, 247, 242, 0.48) 70%,
rgba(250, 247, 242, 0) 100%
);
}
.theme-dark .home-top-mask {
background: linear-gradient(
to bottom,
rgba(25, 25, 25, 0.98) 0%,
rgba(25, 25, 25, 0.92) 38%,
rgba(25, 25, 25, 0.48) 70%,
rgba(25, 25, 25, 0) 100%
);
}
- Step 3: Run the unit test
Run:
node --test tools/homeTopMask.test.mjs
Expected: PASS, 4 passing tests.
- Step 4: Run TypeScript/build verification
Run:
npx tsc --noEmit
Expected: exit code 0, no TypeScript errors.
Then run:
npm run build:weapp
Expected: exit code 0. The Taro build should compile src/pages/index/index.tsx and src/pages/index/index.scss into dist/.
- Step 5: Manual Mini Program verification checklist
Use WeChat DevTools to open /Users/broccoli/Project/wechat_wc/dist and verify:
- At scroll top, the top mask is invisible.
- Scrolling up around the
智绘微刻title position makes the soft mask fade in. - After continuing to scroll, the mask reaches full strength.
- Pulling back to top makes the mask fade out.
- The search box and cards remain clickable through the mask.
- Light and dark themes both show a clear status-bar area.
- The title is not sticky, does not shrink, and does not change color.
- Step 6: Commit only source files for this task
git add src/pages/index/index.tsx src/pages/index/index.scss
git commit -m "feat: add home scroll top mask"
Self-Review Notes
- Spec coverage: The plan implements page scroll listening, title measurement, a fixed soft gradient mask, safe-area reuse, light/dark support, no blur, no title transformation, and non-interactivity.
- Scope: No other pages change and no reusable component is introduced.
- Test strategy: The non-visual progress calculation is unit-tested first; Taro integration is verified by TypeScript/build plus the manual Mini Program checklist.
- Existing worktree: Existing unrelated modifications and generated
dist/changes must not be staged unless created by this implementation.