fix(agent): align mobile rail guidance

This commit is contained in:
Fini 2026-07-24 00:11:06 +08:00 committed by Kayshen-X
parent 364c670ac5
commit ee25b87d16
8 changed files with 77 additions and 24 deletions

View file

@ -11,8 +11,9 @@ category: domain
MOBILE APP — MANDATORY THREE-SECTION ARCHITECTURE:
Every mobile screen is composed as a vertical stack of exactly three sections.
You MUST define all three before generating any content.
Every mobile screen accounts for three logical layers: status chrome, app content,
and optional bottom navigation. These are architectural layers, not a requirement
to wrap every content section inside one padded App Content frame.
Screen-height contract: use numeric 390-393×844 as a temporary construction seed so an empty skeleton is visible. Before finishing, a normal content-driven mobile page switches its root to `height="fit_content"` (Hug), matching its completed flow. Keep a numeric viewport only when the user explicitly requested that viewport/device frame or the design deliberately contains one clipped viewport body that must consume remaining height.
@ -28,17 +29,25 @@ The status bar (time, signal, wifi, battery) is **automatically pre-inserted** b
Chip rows (filter/date/guests pills): each chip HUGS (width fit_content, single-line text, height 36-44, cornerRadius=full); the ROW clips overflow (clipContent) instead of squeezing chips — never let a pill's text wrap. A badge/pill/button frame ALWAYS carries its content (text or icon) — an empty decorated frame renders as a mystery blob.
ALL content elements must sit inside ONE wrapper container (vertical stack).
Keep the mobile root at 0 horizontal padding so status chrome, integrated bottom
navigation, and intentional full-bleed media can remain full width. Emit ordinary
app content as transparent root-direct section frames with
`width="fill_container"`, `height="fit_content"`, `layout="vertical"`, and the
same `padding: [0,24]` rail exactly once per section. Do not repeat that
horizontal inset on child wrappers. Only a deliberately clipped scroll viewport
under an explicit fixed-height root may use `height="fill_container"`; in that
case it is the ONE named remainder consumer, not a sizing mode copied onto its
sections.
The ordinary content wrapper is `width="fill_container"`, `height="fit_content"`, `layout="vertical"`. Its child sections/cards also Hug Height. Only a deliberately clipped scroll viewport under an explicit fixed-height root may use `height="fill_container"`; in that case it is the ONE named remainder consumer, not a sizing mode copied onto its sections.
Use gap and vertical padding, not margins or empty spacers, for rhythm between
sections. Give the last ordinary content section enough bottom padding to clear
the following bottom navigation.
Wrapper provides:
A clipped horizontal scroller is the rail exception: its section stays full
width, its section header gets 24px left/right padding, and its clipped
viewport gets a 24px leading inset with a flush 0px trailing edge.
- Consistent left/right padding: 16-28px (applied ONCE at wrapper level; reference-measured screens use [0,24] on the wrapper with a 402px root)
- Gap-based vertical spacing between sections (use gap, NOT margins): 24-40 by density
- padding-bottom equal to the gap value for bottom space (NOT spacer elements)
Content stacking order inside the wrapper:
Content stacking order across the ordinary sections:
1. Top context: title / navigation header / search / filters
2. Primary content: the main "job to be done" for this screen
@ -66,7 +75,8 @@ Rules:
DO NOT:
- Add per-section horizontal padding (wrapper handles it)
- Put the 24px content rail on the root page or duplicate it on an inner wrapper
- Let an ordinary root-direct content section touch the screen edge
- Use spacer elements for bottom space (use padding-bottom)
- Cram multiple competing sections above the fold

View file

@ -21,4 +21,4 @@ DESIGN CRAFT:
- Cards: consistent cornerRadius/padding/shadow. Content: image - title - description - action.
- Nav: logo + 3-5 links + CTA. space_between distribution. Keep minimal.
- Alternate section backgrounds (white/#F8FAFC) for natural separation.
- Mobile: one App Content wrapper owns horizontal padding and vertical gaps; inner sections should not each add competing gutters.
- Mobile: keep the page root at 0 horizontal padding for full-width chrome/full-bleed media; ordinary transparent root-direct content sections each own the same 24px rail exactly once. A clipped horizontal scroller stays full width, with a 24px-inset header and a 24px leading/0px trailing viewport inset.

View file

@ -138,9 +138,11 @@ AESTHETIC HYGIENE — keep these silent (never emit, the post-pass also strips t
- INNER LAYOUT FRAMES (sections, wrappers, header / body containers inside a card) DO NOT need
fill, stroke, OR shadow. They inherit from the page / card surface. Only opt into a fill /
border / shadow on the OUTER card, button, badge, chip — NEVER on the wrapper that holds it.
- ONE PAGE GUTTER, ON THE ROOT. The root frame carries the horizontal gutter (e.g.
`padding: [0,20]`); EVERY content section uses horizontal padding 0 and only sets vertical
padding. This is what keeps every section's left edge aligned — if sections each set their own
h-padding (one 20, one 16, one 0) their content no longer lines up. Hero / banner / image-bleed
sections sit edge-to-edge by simply NOT adding horizontal padding (the root gutter shows
through). Never stack both (root gutter + per-section h-padding = a doubled inset).
- MOBILE CONTENT RAIL LIVES ON ORDINARY SECTIONS, NOT THE PAGE ROOT. A mobile root may keep
horizontal padding 0 so the status bar, integrated bottom navigation, and intentional
full-bleed media can stay full width. Every ordinary transparent root-direct content section
owns the same 24px left/right rail exactly once (`padding: [0,24]`); do not repeat that inset
on an inner wrapper. A clipped horizontal scroller is the exception: keep its section/viewport
full width, inset its header 24px on both sides, and give the clipped viewport a 24px leading
inset with a flush 0px trailing edge. Never stack root + section or section + inner-wrapper
horizontal padding.

View file

@ -17,7 +17,9 @@ NO PHONE MOCKUP WRAPPER: The whole design IS a mobile screen. Do NOT wrap your s
MOBILE WIDTH SAFETY: Every visible child must stay inside the 390px screen width. Do not create horizontal rows, chips, cards, or buttons that overflow outside the root; wrap, shrink, or clip horizontal lists instead.
MOBILE SINGLE CONTENT RAIL: All non-chrome sections must align to the same 24px left/right content rail. Section roots should stay transparent with width="fill_container" and height="fit_content"; apply padding once at the section root or first content wrapper. Do not create full-width colored wrapper surfaces just to hold content.
MOBILE SINGLE CONTENT RAIL: The root page may keep 0 horizontal padding so the pre-inserted status bar, integrated bottom navigation, and intentional full-bleed media remain full width. Every ordinary transparent root-direct content section must own the same 24px left/right rail exactly once (`padding: [0,24]`) with width="fill_container" and height="fit_content". Do not repeat that inset on an inner wrapper, and do not create full-width colored wrapper surfaces just to hold content.
MOBILE SCROLLER RAIL: A clipped horizontal scroller is the exception to section padding. Keep the scroller section full width, inset its header 24px on both sides, and give the clipped viewport a 24px leading inset with a flush 0px trailing edge so its first item aligns to the content rail while the next item can clip.
{{mobileRhythm}}

View file

@ -294,9 +294,10 @@ mod tests {
"mobile-app domain skill must resolve for a mobile ask"
);
assert!(
prompt.contains("ordinary content wrapper")
&& prompt.contains("height=\"fit_content\""),
"mobile domain must teach Hug Height as the ordinary content default"
prompt.contains("transparent root-direct section frames")
&& prompt.contains("height=\"fit_content\"")
&& prompt.contains("padding: [0,24]"),
"mobile domain must teach Hug Height and the section-owned content rail"
);
}

View file

@ -132,6 +132,15 @@ mod tests {
assert!(skill.content.contains("Mobile top rhythm"));
assert!(skill.content.contains("Do not force bottom navigation"));
assert!(skill.content.contains("Not a floating pill"));
assert!(skill
.content
.contains("transparent root-direct section frames"));
assert!(skill.content.contains("padding: [0,24]"));
assert!(skill.content.contains("24px leading inset"));
assert!(skill.content.contains("0px trailing"));
assert!(!skill
.content
.contains("ALL content elements must sit inside ONE wrapper"));
assert!(skill
.content
.contains("Do not repeat the same predictable mobile stack"));

View file

@ -1037,6 +1037,11 @@ fn build_subagent_prompt_core(
.as_ref()
.map(|instruction| format!("{instruction}\n\n"))
.unwrap_or_default();
let spacing_rule = if is_mobile_layout {
"SPACING CONSISTENCY — MOBILE CONTENT RAIL: The root page may keep 0 horizontal padding for full-width status/navigation/full-bleed media. This ordinary transparent root-direct section owns padding:[0,24] exactly once; do not duplicate it on an inner wrapper. If this section is a clipped horizontal scroller, keep its section full width, inset its header 24px on both sides, and give the clipped viewport a 24px leading inset with a flush 0px trailing edge."
} else {
"SPACING CONSISTENCY: Use a single outer content gutter and consistent internal gaps. Do not create nested wrappers with conflicting padding or content touching edges."
};
// Two constraints differ by output protocol. The public subagent path uses
// the script-gen branch; the raw-JSONL branch is legacy-only for direct
@ -1064,7 +1069,7 @@ CRITICAL LAYOUT CONSTRAINTS:\n\
- Target content amount: ~{:.0}px tall. Generate enough elements to fill this area.\n\
- DENSITY: Do NOT pack the area edge-to-edge. Prefer fewer, stronger modules with visible negative space; most sections should have 3-5 primary rows/cards at most.\n\
- VISUAL HIERARCHY: Each section must have one clear focal element, secondary supporting text, and quieter metadata. Avoid equal-weight blocks competing for attention.\n\
- SPACING CONSISTENCY: Use a single outer content gutter and consistent internal gaps. Do not create nested wrappers with conflicting padding or content touching edges.\n\
- {spacing_rule}\n\
- CRAFT POLISH: Add refinement through restrained 1px low-contrast borders, tonal surfaces, small state badges, and subtle shadows. Avoid template-like thick outlines, giant pills, or flat blocks with no micro-detail.\n\
- MEDIA CONSISTENCY: Use photographic images sparingly and keep them visually consistent in subject, crop, tone, and radius. For food/category UI, prefer cohesive icon or illustration tiles over random unrelated photos.\n\
- ICON SCALE: Icons support content; keep most icons 16-22px inside 36-48px controls. Avoid oversized circular icon bubbles or repeated identical icon treatments unless the design brief calls for them.\n\

View file

@ -219,7 +219,7 @@ fn subagent_prompt_carries_ts_layout_contract() {
false,
);
let required = "Page sections:|Food Categories [category chips]|\"fill_container\"|\"fit_content\"|Generate enough elements|MOBILE STATUS BAR|time, signal, wifi, battery|NO PHONE MOCKUP WRAPPER|MOBILE WIDTH SAFETY|MOBILE SINGLE CONTENT RAIL|MOBILE SEARCH BAR|MOBILE SECTION CHROME|MOBILE VERTICAL RHYTHM|MOBILE TOP RHYTHM|MOBILE GRID ALIGNMENT|MOBILE CARD OVERLAYS|MOBILE IMAGE PRESENTATION|verify only rendering integrity|Do not judge or replace a displayed image during self-check based on subject relevance|explicit user-requested image edit remains allowed|NO BLANK PLACEHOLDERS|MOBILE NAV SURFACE|MOBILE NAV SHADOW|NO FIXED FOOD TEMPLATE|Do not default to the same search + categories + orange promo + two product cards composition|TYPOGRAPHY HIERARCHY|DENSITY|VISUAL HIERARCHY|SPACING CONSISTENCY|CRAFT POLISH|MEDIA CONSISTENCY|ICON SCALE|SIGNATURE MOMENT|WOW FACTOR|COMPOSITIONAL CONTRAST|PREMIUM DETAIL|NO DECORATION SPAM";
let required = "Page sections:|Food Categories [category chips]|\"fill_container\"|\"fit_content\"|Generate enough elements|MOBILE STATUS BAR|time, signal, wifi, battery|NO PHONE MOCKUP WRAPPER|MOBILE WIDTH SAFETY|MOBILE SINGLE CONTENT RAIL|MOBILE SCROLLER RAIL|MOBILE SEARCH BAR|MOBILE SECTION CHROME|MOBILE VERTICAL RHYTHM|MOBILE TOP RHYTHM|MOBILE GRID ALIGNMENT|MOBILE CARD OVERLAYS|MOBILE IMAGE PRESENTATION|verify only rendering integrity|Do not judge or replace a displayed image during self-check based on subject relevance|explicit user-requested image edit remains allowed|NO BLANK PLACEHOLDERS|MOBILE NAV SURFACE|MOBILE NAV SHADOW|NO FIXED FOOD TEMPLATE|Do not default to the same search + categories + orange promo + two product cards composition|TYPOGRAPHY HIERARCHY|DENSITY|VISUAL HIERARCHY|SPACING CONSISTENCY|CRAFT POLISH|MEDIA CONSISTENCY|ICON SCALE|SIGNATURE MOMENT|WOW FACTOR|COMPOSITIONAL CONTRAST|PREMIUM DETAIL|NO DECORATION SPAM";
// Mobile UI guardrails now load via the `mobile-ui` skill (system prompt);
// section + quality markers stay in the user prompt. Accept either. The
// `"fill_container"` / `"fit_content"` markers are quote-only (no
@ -234,6 +234,30 @@ fn subagent_prompt_carries_ts_layout_contract() {
"missing {required}"
);
}
let combined = format!("{}\n{}", cr.system_prompt, cr.user_prompt);
for required in [
"root page may keep 0 horizontal padding",
"ordinary transparent root-direct content section",
"padding:[0,24]",
"inset its header 24px on both sides",
"24px leading inset",
"0px trailing edge",
] {
assert!(
combined.contains(required),
"missing mobile content-rail rule {required}"
);
}
for stale in [
"ONE PAGE GUTTER, ON THE ROOT",
"ALL content elements must sit inside ONE wrapper",
"Add per-section horizontal padding (wrapper handles it)",
] {
assert!(
!combined.contains(stale),
"stale mobile gutter rule must not survive: {stale}"
);
}
assert!(!cr.system_prompt.contains("MOBILE IMAGE QUALITY"));
assert!(!cr.user_prompt.contains("MOBILE IMAGE QUALITY"));
}