docs: add home top mask implementation plan

This commit is contained in:
2026-08-08 01:19:22 +08:00
parent 5e7046f935
commit 9ead273766
@@ -0,0 +1,333 @@
# 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 `01` 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()` and `useStatusBar(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 `0` and `1`.
- [ ] **Step 1: Write the failing test**
Create `tools/homeTopMask.test.mjs` with:
```js
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:
```bash
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:
```js
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:
```bash
node --test tools/homeTopMask.test.mjs
```
Expected: PASS, 4 passing tests.
- [ ] **Step 5: Commit only this task**
```bash
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 }) => number` from `src/utils/homeTopMask.js`.
- `safe.statusBarHeight`, `safe.menuButtonTop`, and `safe.menuButtonHeight` from existing `useSafeArea()`.
- Produces:
- A `View.home-top-mask` element on the home page.
- Inline `height` and `opacity` styles for that element.
- [ ] **Step 1: Update the home page component**
In `src/pages/index/index.tsx`, change the React import from:
```ts
import { useState } from 'react'
```
to:
```ts
import { useEffect, useRef, useState } from 'react'
```
Add this import after the existing local imports:
```ts
import { getTopMaskProgress } from '../../utils/homeTopMask'
```
Inside the `Index` component, immediately after:
```ts
const [searchKey, setSearchKey] = useState('')
```
add:
```ts
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:
```tsx
<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`:
```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:
```bash
node --test tools/homeTopMask.test.mjs
```
Expected: PASS, 4 passing tests.
- [ ] **Step 4: Run TypeScript/build verification**
Run:
```bash
npx tsc --noEmit
```
Expected: exit code 0, no TypeScript errors.
Then run:
```bash
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:
1. At scroll top, the top mask is invisible.
2. Scrolling up around the `智绘微刻` title position makes the soft mask fade in.
3. After continuing to scroll, the mask reaches full strength.
4. Pulling back to top makes the mask fade out.
5. The search box and cards remain clickable through the mask.
6. Light and dark themes both show a clear status-bar area.
7. The title is not sticky, does not shrink, and does not change color.
- [ ] **Step 6: Commit only source files for this task**
```bash
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.