diff --git a/crates/engine/src/dev/tailwind.rs b/crates/engine/src/dev/tailwind.rs index a29c0f316..8c96ac820 100644 --- a/crates/engine/src/dev/tailwind.rs +++ b/crates/engine/src/dev/tailwind.rs @@ -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 { 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, diff --git a/crates/engine/src/tailwind_tests.rs b/crates/engine/src/tailwind_tests.rs index 163e3580c..970ecd1ca 100644 --- a/crates/engine/src/tailwind_tests.rs +++ b/crates/engine/src/tailwind_tests.rs @@ -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("') && !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}"); +} diff --git a/docs/mcp.md b/docs/mcp.md index a6c04af07..21dfe8ca9 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1732,7 +1732,9 @@ permissions: without it nothing is downloaded); `plugin.directory.publish {path `--radius-*`, `--text-*`, `--spacing-*`, `--font-*`; a number's namespace and unit come from what layers bind it to), other modes on `[data-=""]`. With `mobile: ` (the same layer names, mobile layout) the code is mobile first: the mobile frame's classes, the - desktop frame's that differ behind `lg:`, and layers only on desktop `hidden` plus `lg:flex` + desktop frame's that differ behind `lg:` (a mobile `max-width` is dropped as the phone's own width, + unless desktop has the same cap: a fixed-size layer such as a badge keeps `max-w-[48px]` at every + width), and layers only on desktop `hidden` plus `lg:flex` (auto layout frames) or `lg:block` (other layers). - **Component code:** `inspect.code {id, language: react|leptos|vue|swiftui|compose|arkui|flutter}` on a component, a component set or a variant writes the component as a reusable unit: React (TSX) with a props