Develop or fix Android Compose UI pages using the HTML design spec (preview.html) as the single source of truth. Use for both new page development and fixing existing design mismatches. Triggers when the user mentions preview.html, wants to build/develop/implement a page from design, compare design vs implementation, fix UI discrepancies, or says "develop this page", "implement this screen", "fix this page", "compare with design", "not matching design".
Develop or fix a page/component to match the design spec in docs/design/preview.html.
Works for both new development (building a page from scratch based on the design) and fixing mismatches (adjusting an existing page to match the design).
The core principle: screenshots are for visual verification, HTML/CSS source is the spec. Never guess values from images — extract exact CSS values from the HTML source and map them precisely to Compose.
Ask the user which page/component needs fixing, or infer from context. The user may provide:
./scripts/screenshot.sh ./screencap/<name>.png)Use Playwright to open the HTML design spec and capture the target component:
1. Check if HTTP server is running:
lsof -i :8765 2>/dev/null | grep LISTEN
2. If not running, start it:
python3 -m http.server 8765 -d /path/to/project &>/dev/null &
3. Navigate:
browser_navigate → http://localhost:8765/docs/design/preview.html
4. Interact to reveal the component (click tabs, buttons, etc.)
5. Take screenshot:
browser_take_screenshot → save to screencap/design-<name>.png
If Playwright fails to launch Chrome ("Opening in existing browser session" error):
pkill -f "mcp-chrome" and wait 1 secondrm -rf ~/Library/Caches/ms-playwright/mcp-chrome-*/SingletonLockbrowser_navigateIf Playwright still fails after retries, skip the screenshot and go directly to Step 5 (reading HTML/CSS source). The HTML source is the authoritative spec anyway — screenshots are only for visual verification.
./scripts/screenshot.sh ./screencap/<name>.png
If the user already provided a screenshot, skip this step.
Use Read to view both images and create a difference table:
| Element | Design | Implementation | Status |
|---------|--------|----------------|--------|
| Title | "情绪日历" | "日历" | MISMATCH |
| Nav btn | 30x30 circle, border | 36x36, warmBg | MISMATCH |
| Stats | "记录天数" / "15天" | "追踪天数" / "1" | MISMATCH |
| ... | ... | ... | ... |
Focus on: text content, layout structure, spacing, colors, typography, component presence/absence, data format and display logic (e.g., value suffixes like "天"/"次", computed vs raw values).
When the difference table has 5+ mismatches, create TaskCreate tasks to track progress:
This is the critical step. Read docs/design/preview.html and extract exact CSS values for every mismatched element.
Read the relevant CSS classes and HTML structure. Record precise values:
padding, margin, gap → dp valuesfont-size, font-weight, letter-spacing → sp/weight valuesborder-radius → RoundedCornerShapebackground, color, border → ExtendedTheme.colors mappingdisplay: flex/grid → Row/Column/LazyVerticalGridflex-wrap → FlowRow (custom)opacity → Color.copy(alpha = N)var(--accent) → map to ExtendedTheme.colors.accentApply these precise mappings:
| CSS Property | Compose Equivalent |
|---|---|
padding: 7px 14px | Modifier.padding(horizontal = 14.dp, vertical = 7.dp) |
border-radius: 100px | RoundedCornerShape(100.dp) |
border-radius: 50% | CircleShape |
border-radius: 14px | RoundedCornerShape(14.dp) |
border: 1.5px solid X | Modifier.border(1.5.dp, color, shape) |
border: 1px solid X | Modifier.border(1.dp, color, shape) |
font-size: 12px | fontSize = 12.sp |
font-weight: 500 | fontWeight = FontWeight.Medium |
font-weight: 600 | fontWeight = FontWeight.SemiBold |
letter-spacing: 0.5px | letterSpacing = 0.5.sp |
line-height: 1 | lineHeight = fontSize.sp (same as font size) |
gap: 8px | Arrangement.spacedBy(8.dp) |
margin-bottom: 22px | Modifier.padding(bottom = 22.dp) |
display: grid; grid-template-columns: repeat(7,1fr) | 7-column Row with Modifier.weight(1f) per cell |
display: flex; flex-wrap: wrap; gap: 8px | FlowRow(horizontalSpacing = 8.dp, verticalSpacing = 8.dp) |
width: 100%; height: 48px | Modifier.fillMaxWidth().height(48.dp) |
aspect-ratio: 1 | Modifier.aspectRatio(1f) |
opacity: 0.25 | color.copy(alpha = 0.25f) |
text-transform: uppercase | .uppercase() |
cursor: pointer (on div) | Modifier.clickable { } |
overflow-x: auto / overflow-x: scroll | Modifier.horizontalScroll(rememberScrollState()) |
| CSS Variable | ExtendedTheme |
|---|---|
var(--accent) | colors.accent |
var(--accent-bg) | colors.accentBg |
var(--accent-soft) | colors.accentSoft |
var(--bg-warm) | colors.warmBackground |
var(--bg-card) | colors.cardBackground |
var(--text-primary) | MaterialTheme.colorScheme.onSurface |
var(--text-secondary) | colors.textSecondary |
var(--text-muted) | colors.textMuted |
var(--text-hint) | colors.textHint |
var(--border) | colors.border |
var(--border-light) | colors.borderLight |
var(--font-display) | MaterialTheme.typography.titleLarge (NotoSerifSC) |
/i18n skill for reference.// CSS: .cal-day-num 12px text-secondary) for future reference.The design spec and code are a two-way sync — sometimes code matches design, sometimes design needs to evolve.
Design is outdated — the implementation is correct but the spec is stale:
docs/design/preview.html to match realityDesign-first development — building a new feature or redesigning an existing page:
docs/design/preview.html with the new design (add CSS classes, HTML structure, JS interactions)This "design first, code second" workflow is common for feature additions like adding a time range selector, restructuring a page layout, or removing components that relied on hardcoded data.
When you remove a Compose component (e.g., deleting a PatternsCard), its associated i18n strings become orphaned. After removal:
Grep to search for the string resource names (e.g., pattern_morning) across all .kt files./scripts/check_i18n_consistency.sh to verify all locales stay in sync./gradlew assembleDemoDebug
./scripts/check_i18n_consistency.sh
Both must pass. The build confirms Compose code compiles; the i18n check confirms all 5 locale files have the same string keys in the same order.
Ask the user to open the target page/component on phone, then:
./scripts/screenshot.sh ./screencap/<name>.png
Read both the new phone screenshot and the design screenshot. Compare and report remaining differences. If there are issues, go back to Step 5 and fix.
Modifier.weight(1f) for chips/tags that should auto-size — this forces equal width and causes text wrapping. Use FlowRow instead.Dialog centered layout for bottom-sheet style modals unless the design explicitly shows centered.titleMedium (which is 16sp NotoSerif). Check .card-label CSS.stringResource(R.string.xxx) — never hardcode user-facing text.Row with Modifier.weight(1f) per cell (not LazyVerticalGrid) for 7-column calendar layouts — it renders all cells at once without lazy loading overhead.color.copy(alpha = 0.25f)), matching .cal-day.other { opacity: 0.25 }.stringResource for date format patterns (e.g., R.string.calendar_month_format) so each locale gets the right format (en: "MMMM yyyy", zh: "yyyy年M月").