M10: Tailwind mobile merge keeps a max-width both frames share (#2272) (#2296)

inspect.code {language: "tailwind", mobile} dropped every mobile max-width and put the desktop one behind lg:, so a fixed-size layer that is the same size on both frames (a 48 x 48 badge) stretched to the full width below 1024 px. A mobile max-width that equals the desktop one is now kept as a plain class, with no lg: copy. A differing max-width still goes fluid on mobile with the desktop cap behind lg:.

Tests: the issue's two frames (a 1440 and a 390 page, each with a 48 x 48 badge) keep max-w-[48px] unprefixed, and a page-wide banner still gets its lg: cap only. The slashed-names test gave the CTAs frame the same 342 px on both frames, which the new rule rightly keeps, so its desktop frame is now 600 px wide. docs/mcp.md notes the rule.


Model-Effort: Opus 5.5 drove; Fable 5.2 queued; Opus 5.5 briefed; Sonnet 5.5 wrote at medium

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Trevor BrownandClaude Sonnet 5.5 authored and GitHub committed 2026-11-13 01:44:17 +01:00
1 parent 2408d0aa81
commit 3d22c606a7
3 files changed
+53 -4

No files matched your search

+2 -1
View File
@@ -413,10 +413,11 @@ fn zero_padding(sides: [bool; 4]) -> Vec<&'static str> {
pub(crate) fn merge(mobile: Option<&[(String, String)]>, desktop: &[(String, String)]) -> Vec<String> {
let hidden = [("display".to_string(), "hidden".to_string())];
// A mobile frame's widths are the phone's, not caps: below the breakpoint boxes fill the page.
// Except a cap desktop has too: that is the layer's own size (a badge, an avatar), kept at every width.
let fluid: Vec<(String, String)>;
let mobile = match mobile {
Some(m) => {
fluid = m.iter().filter(|(k, _)| k != "max-width").cloned().collect();
fluid = m.iter().filter(|(k, c)| k != "max-width" || desktop.iter().any(|(_, d)| d == c)).cloned().collect();
fluid.as_slice()
}
None => &hidden,
+48 -2
View File
@@ -176,7 +176,11 @@ fn slashed_layer_names_and_phone_widths_merge_right() {
for (name, width, dir, sizing) in [("Landing", 1440.0, "horizontal", "hug"), ("Landing mobile", 390.0, "vertical", "fill")] {
let p = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": width, "height": 600, "name": name})));
ex(&mut s, "layout.add", json!({"ids": [p], "direction": "vertical"}));
let ctas = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 342, "height": 120, "parent": p, "name": "CTAs"})));
let ctas = idv(&ex(
&mut s,
"frame.create",
json!({"x": 0, "y": 0, "width": if width > 1000.0 { 600.0 } else { 342.0 }, "height": 120, "parent": p, "name": "CTAs"}),
));
ex(&mut s, "layout.add", json!({"ids": [ctas], "direction": dir}));
let b = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 200, "height": 48, "parent": ctas, "name": "Button/Primary"})));
ex(&mut s, "layout.add", json!({"ids": [b], "direction": "horizontal"}));
@@ -190,7 +194,7 @@ fn slashed_layer_names_and_phone_widths_merge_right() {
let button =
out.lines().find(|l| l.contains("lg:") && l.contains("rounded") || l.contains("self-stretch") && l.contains("flex-row")).unwrap_or_default();
assert!(button.contains("self-stretch") && button.contains("lg:self-auto"), "the slashed name keeps its mobile classes: {out}");
assert!(!out.contains("max-w-[342px] ") || out.contains("lg:max-w-[342px]"), "no phone-width cap below lg: {out}");
assert!(!out.contains("max-w-[342px]") && out.contains("lg:max-w-[600px]"), "no phone-width cap below lg: {out}");
assert!(out.contains("Only in the mobile frame") && out.contains("Tag"), "{out}");
let divider = out.lines().filter(|l| l.contains("<div") && (l.contains("h-[1px]") || l.contains("h-px"))).count();
assert!(out.contains("w-full") && divider <= 2, "{out}");
@@ -353,3 +357,45 @@ fn font_family_with_quote_and_bracket_stays_a_valid_class_token() {
assert!(after.starts_with('>') && !attr.contains('\\'), "{out}");
assert!(!inner.is_empty() && !inner.contains(['\\', '"', ']']), "{out}");
}
/// Issue #2272: a desktop `page` 1440 wide and a mobile one 390 wide, each holding a fixed 48 x 48
/// `badge` and a `banner` that fills the page.
fn badge_frames(s: &mut Session) -> (NodeId, NodeId) {
let mut frames = Vec::new();
for (width, padding, x) in [(1440.0, 80, 0), (390.0, 24, 1600)] {
let p = idv(&ex(
s,
"frame.create",
json!({"name": "page", "x": x, "y": 0, "width": width, "height": 400, "props": {"layout": "vertical", "padding": padding, "gap": 16}}),
));
let badge_props = json!({"layout": "horizontal", "sizingH": "fixed", "sizingV": "fixed", "fill": "#E8EEF7"});
ex(s, "frame.create", json!({"name": "badge", "parent": p, "width": 48, "height": 48, "props": badge_props}));
let inner = width - 2.0 * f64::from(padding);
ex(
s,
"frame.create",
json!({"name": "banner", "parent": p, "width": inner, "height": 40, "props": {"layout": "horizontal", "fill": "#112233"}}),
);
frames.push(p);
}
(frames[0], frames[1])
}
#[test]
fn a_max_width_both_frames_share_stays_unprefixed() {
let mut s = Session::new();
let (desk, mob) = badge_frames(&mut s);
let out = code(&mut s, desk, json!({"mobile": mob}));
let badge = out.lines().find(|l| l.contains("bg-[#E8EEF7]")).unwrap_or_default();
assert!(badge.contains("max-w-[48px]") && !badge.contains("lg:max-w-[48px]"), "the badge keeps its cap at every width: {out}");
}
#[test]
fn a_max_width_that_differs_still_goes_fluid_on_mobile() {
let mut s = Session::new();
let (desk, mob) = badge_frames(&mut s);
let out = code(&mut s, desk, json!({"mobile": mob}));
let banner = out.lines().find(|l| l.contains("bg-[#112233]")).unwrap_or_default();
assert!(banner.contains("lg:max-w-[1280px]"), "the page-wide box is capped from lg: {banner}");
assert!(!banner.split_whitespace().any(|c| c == "max-w-[1280px]" || c == "max-w-[342px]"), "no phone-width cap below lg: {banner}");
}