diff --git a/packages/pen-ai-skills/src/__tests__/detectors-spacing.test.ts b/packages/pen-ai-skills/src/__tests__/detectors-spacing.test.ts index 5a0260745..6e7f91825 100644 --- a/packages/pen-ai-skills/src/__tests__/detectors-spacing.test.ts +++ b/packages/pen-ai-skills/src/__tests__/detectors-spacing.test.ts @@ -131,6 +131,27 @@ describe('detectEdgeSectionPadding', () => { expect(detectEdgeSectionPadding(root)).toHaveLength(0); }); + // 2026-05-11 user-reported "Today's Special Deal" mobile design — Header, + // Categories, and Bottom Nav each carried their own horizontal padding, + // but Hero (the Today's Special card) did not. Previous version saw + // Hero's missing padding + ≥1 offending child and flagged → root got + // 16px gutter on top of every per-section-padded sibling, doubling the + // inset everywhere except Hero. New rule: if ANY non-fullbleed content + // child already has its own padding, the design has chosen the per- + // section gutter mode and root MUST NOT be flagged. + it('does NOT flag when ANY content section already has its own h-padding (per-section mode)', () => { + const root = mobileRoot([ + section('hero', [text('hero-text')]), // no padding — would have flagged alone + section('categories', [icon('i1'), icon('i2')], { padding: [12, 16] }), // per-section + section('content', [text('t')], { padding: [0, 24] }), // per-section + ]); + + // Categories + content set per-section gutters; Hero is left edge-to- + // edge by design (banner-style fits even without `role: hero`). Root + // should stay 0 — flagging would double-pad categories/content. + expect(detectEdgeSectionPadding(root)).toHaveLength(0); + }); + it('preserves vertical padding when suggesting the fix', () => { const root = mobileRoot( [section('a', [text('t1')]), section('b', [text('t2')])], diff --git a/packages/pen-ai-skills/src/diagnostics/detectors-spacing.ts b/packages/pen-ai-skills/src/diagnostics/detectors-spacing.ts index 6433184e1..a193eb7c6 100644 --- a/packages/pen-ai-skills/src/diagnostics/detectors-spacing.ts +++ b/packages/pen-ai-skills/src/diagnostics/detectors-spacing.ts @@ -133,17 +133,39 @@ export function detectEdgeSectionPadding(root: PenNode): Issue[] { if (isMobileRoot) { const rootPadL = getPaddingLeft(node); if (rootPadL === 0) { + // Two-pass scan over content children (skipping full-bleed + // chrome / image-only banners which are intentionally edge-to- + // edge): + // - any with own h-padding > 0 → design has chosen the + // per-section gutter pattern, + // adding root padding would + // double-inset everything else + // - any with no padding but text/icon descendants → would + // genuinely glue content to + // the viewport edge, so flag + // + // 2026-05-11 user feedback: previous version flagged whenever + // ≥1 child had no padding, even when other siblings already + // carried per-section padding. Result: root got extra 16px on + // top of the existing per-section gutters → "边距过大". The new + // rule treats any per-section-padded sibling as a signal that + // the design has opted out of root-level gutter and aborts the + // detector entirely. + let hasPerSectionPaddedSibling = false; const offendingChildren: PenNode[] = []; for (const child of (node as { children: PenNode[] }).children) { if (child.type !== 'frame') continue; const role = ((child as { role?: string }).role ?? '').toLowerCase(); if (FULL_BLEED_ROLES.has(role)) continue; if (isImageOnlySection(child)) continue; - if (getPaddingLeft(child) > 0) continue; + if (getPaddingLeft(child) > 0) { + hasPerSectionPaddedSibling = true; + continue; + } if (!hasTextOrIconDescendant(child)) continue; offendingChildren.push(child); } - if (offendingChildren.length > 0) { + if (offendingChildren.length > 0 && !hasPerSectionPaddedSibling) { const currentPad = (node as unknown as { padding?: unknown }).padding; let suggested: number[]; if (Array.isArray(currentPad) && currentPad.length === 4) {