Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_h1Z8bhReFSk91gTrAJyLF.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"Cargo.toml": "Patch"
},
"note": "Preserve caller sizing and absolute rectangle geometry, emit route-owned pages, and require functional E2E plus native Figma visual evidence for completion. Teach split-screen concurrent desktop work with isolated project windows, inputs and visual-test viewports.",
"date": "2026-11-05T16:51:25.331465500Z"
}
26 changes: 26 additions & 0 deletions crates/devup-mcp-devup-ui/src/codegen/component/render_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,32 @@ fn render_node(
let attributes = properties.iter().map(|(name, value)| {
format!(" {name}={{{}}}", serde_json::to_string(value).unwrap())
}).collect::<String>();
// A reference only passes variant props. Its master component cannot
// reproduce an instance's parent allocation by itself. Keep that loss
// actionable instead of treating a named reference as layout evidence.
let mut placement_props = Vec::new();
layout::push_layout_props(snapshot, node, "Box", &mut placement_props,
context.root_layout, depth == 0, context.width_policy.as_ref(), false);
let placement_props: serde_json::Map<String, serde_json::Value> = placement_props.into_iter()
.filter(|(name, _)| matches!(name.as_str(), "w" | "h" | "boxSize" | "minW" | "maxW" | "minH" | "maxH" | "flex" | "flexShrink" | "alignSelf" | "gridColumn" | "gridRow"))
.map(|(name, PropValue::String(value))| (name, serde_json::Value::String(value))).collect();
if !placement_props.is_empty() {
context.diagnostics.push(Diagnostic {
code: "DEVUP_CODEGEN_INSTANCE_PLACEMENT_CONTRACT".into(),
node_id: Some(node.id.clone()),
severity: Some(DiagnosticSeverity::Warning),
fidelity_impact: Some(devup_mcp_figma::FidelityImpact::Approximated),
message: "Component reference does not apply the instance's caller sizing; preserve its root placement contract during assembly.".into(),
details: Some(serde_json::json!({
"component":component,"parentId":view.string("parentId"),
"requiredRootProps":placement_props,
"sourceSize":{"width":view.number("width"),"height":view.number("height")},
"appliedToReference":false,
"nextAction":"Apply these props to the actual component root through typed layout props or inline the captured instance. Do not add unsupported props to a component or assume maxW allocates width. Verify the assembled parent/child bounds alongside the Figma reference visual test."
})),
..Diagnostic::default()
});
}
let content = main_property.unwrap_or_else(|| format!("<{component}{attributes} />"));
let expression = if let Some(visible) = visible_property {
format!("{{{visible} && {content}}}")
Expand Down
7 changes: 7 additions & 0 deletions crates/devup-mcp-devup-ui/src/codegen/evidence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,13 @@ pub(super) fn placement_contract(
requirements
.push(include_str!("../text/evidence-placement_contract-adba181a.md").to_owned());
}
if parent.is_none()
&& ["layoutSizingHorizontal", "layoutSizingVertical"]
.iter()
.any(|axis| view.string(axis) == Some("FILL"))
{
requirements.push("This detached FILL root needs its original parent's layout direction and allocation. maxW/maxH alone do not allocate space. Restore the caller's flex/grid sizing from instanceOverrides or collect the containing parent; verify the assembled component in the browser.".into());
}
if vertical_fill
&& (embedded
|| !view.number("height").is_some_and(|h| {
Expand Down
20 changes: 18 additions & 2 deletions crates/devup-mcp-devup-ui/src/codegen/layout/layout_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub(super) fn push_layout_props(
.child_ids()
.any(|child| child == node.id)
});
let is_root = snapshot.roots.iter().any(|root| root == &node.id);
let is_root = is_render_root || snapshot.roots.iter().any(|root| root == &node.id);
// The parent of a collected root sits outside the collected subtree, so it
// cannot be looked up and the node's recorded parent type is the only
// account of it. Without that fallback a screen read as having no parent at
Expand Down Expand Up @@ -98,6 +98,15 @@ pub(super) fn push_layout_props(
} else if let Some((w, h)) = fixed_native_ellipse_size(&view, component, is_asset) {
width = Some(px(w));
height = Some(px(h));
} else if let Some((w, h)) = fixed_native_rectangle_size(&view, component, is_asset) {
// Keep the existing full-parent overlay relationship on matching
// axes. A smaller, intrinsically empty shape needs its own extent.
let spans = |axis: &str, extent: f64| parent.is_some_and(|p| {
let p = p.typed_view();
p.field_error(axis).is_none() && p.number(axis) == Some(extent)
});
width = Some(if spans("width", w) { "100%".into() } else { px(w) });
height = Some(if spans("height", h) { "100%".into() } else { px(h) });
} else if component == "Text" {
width = own_width.map(px);
height = Some("100%".to_owned());
Expand Down Expand Up @@ -308,6 +317,12 @@ pub(super) fn push_layout_props(
}
}

// A selected FILL root can have a FIXED opposite axis, so it may have
// taken the mixed-sizing branch before reaching the root branch above.
if is_render_root && !embedded_root && !absolute && !is_page_root && fill_w && width.is_none() {
width = Some("100%".into());
}

// A canvas with positioned children is their coordinate system, not just
// a responsive page measurement. Preserve each FIXED axis even on a page
// or section root. Embedded mode explicitly delegates dimensions to host.
Expand Down Expand Up @@ -686,7 +701,8 @@ pub(super) fn push_layout_props(
&& let (Some(own), Some(room)) =
(view.number(size), inner_extent(parent, size, near, far))
&& own + 0.5 < room
&& a_wider_box_would_show(&view, across_is_horizontal)
&& (view.node_type() == "INSTANCE"
|| a_wider_box_would_show(&view, across_is_horizontal))
{
string_prop(props, "alignSelf", "flex-start");
}
Expand Down
19 changes: 19 additions & 0 deletions crates/devup-mcp-devup-ui/src/codegen/layout/solid_ellipse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,22 @@ fn fixed_native_ellipse_size(view: &TypedNode<'_>, component: &str, is_asset: bo
let (width, height) = (view.number("width")?, view.number("height")?);
(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0).then_some((width, height))
}

/// Rectangles have no intrinsic content either. Figma's shape API may omit
/// layoutSizing*, unlike auto-layout frames; error-free measured axes and
/// non-stretch constraints are enough to retain their native fixed geometry.
fn fixed_native_rectangle_size(view: &TypedNode<'_>, component: &str, is_asset: bool) -> Option<(f64, f64)> {
if is_asset || component != "Box" || view.node_type() != "RECTANGLE"
|| view.string("layoutPositioning") != Some("ABSOLUTE")
|| view.child_ids().next().is_some()
|| ["width", "height", "layoutPositioning", "layoutSizingHorizontal", "layoutSizingVertical", "constraints"]
.iter().any(|field| view.field_error(field).is_some())
|| ["layoutSizingHorizontal", "layoutSizingVertical"].iter()
.any(|field| view.string(field).is_some_and(|sizing| sizing != "FIXED"))
|| ["horizontal", "vertical"].iter().any(|axis| view.value("constraints")
.and_then(|constraints| constraints.get(axis)).and_then(Value::as_str)
.is_some_and(|constraint| matches!(constraint, "STRETCH" | "SCALE")))
{ return None; }
let (width, height) = (view.number("width")?, view.number("height")?);
(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0).then_some((width, height))
}
4 changes: 2 additions & 2 deletions crates/devup-mcp-devup-ui/tests/fixed_absolute_ellipses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ fn fluid_unknown_and_unreadable_geometry_do_not_gain_fixed_shape_sizes() {
}

#[test]
fn relative_ellipse_and_other_shape_paths_keep_their_existing_sizing() {
fn relative_ellipses_keep_their_path_and_absolute_rectangles_preserve_measured_axes() {
for positioning in [json!("AUTO"), json!({"$unsupported":"unknown"})] {
let mut snapshot = source("I44:2832;44:3209");
snapshot
Expand All @@ -107,5 +107,5 @@ fn relative_ellipse_and_other_shape_paths_keep_their_existing_sizing() {
.unwrap()
.node_type = "RECTANGLE".into();
let output = render(&snapshot);
assert!(!output.contains("boxSize=\"48px\""), "{output}");
assert!(output.contains("boxSize=\"48px\""), "{output}");
}
132 changes: 132 additions & 0 deletions crates/devup-mcp-devup-ui/tests/fixtures/prime-placement.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
{
"fileKey": "prime-placement-feedback",
"roots": [
"row"
],
"diagnostics": [],
"nodes": {
"row": {
"id": "row",
"type": "FRAME",
"fields": {
"name": "RenderFixture",
"parentType": "FRAME",
"width": 980,
"height": 250,
"layoutMode": "HORIZONTAL",
"layoutSizingHorizontal": "FIXED",
"layoutSizingVertical": "HUG",
"itemSpacing": 40,
"childrenIds": [
"title",
"card"
],
"inferredAutoLayout": {
"layoutMode": "HORIZONTAL",
"itemSpacing": 40
}
}
},
"title": {
"id": "title",
"type": "INSTANCE",
"fields": {
"name": "StepTitle",
"parentId": "row",
"width": 420,
"height": 94,
"maxWidth": 420,
"layoutMode": "VERTICAL",
"layoutSizingHorizontal": "FILL",
"layoutSizingVertical": "HUG",
"itemSpacing": 12,
"childrenIds": [
"title-content"
],
"inferredAutoLayout": {
"layoutMode": "VERTICAL",
"itemSpacing": 12
}
}
},
"title-content": {
"id": "title-content",
"type": "FRAME",
"fields": {
"parentId": "title",
"width": 120,
"height": 94,
"layoutSizingHorizontal": "FIXED",
"layoutSizingVertical": "FIXED",
"fills": [
{
"type": "SOLID",
"color": {
"r": 0.1,
"g": 0.1,
"b": 0.1
}
}
]
}
},
"card": {
"id": "card",
"type": "FRAME",
"fields": {
"name": "StepCard",
"parentId": "row",
"width": 520,
"height": 218,
"layoutMode": "VERTICAL",
"layoutSizingHorizontal": "FIXED",
"layoutSizingVertical": "FIXED",
"layoutPositioning": "AUTO",
"childrenIds": [
"marker"
],
"fills": [
{
"type": "SOLID",
"color": {
"r": 0.97,
"g": 0.97,
"b": 0.98
}
}
],
"inferredAutoLayout": {
"layoutMode": "VERTICAL"
}
}
},
"marker": {
"id": "marker",
"type": "RECTANGLE",
"fields": {
"name": "Rectangle 37",
"parentId": "card",
"width": 16,
"height": 16,
"x": -48,
"y": 32,
"layoutPositioning": "ABSOLUTE",
"constraints": {
"horizontal": "MIN",
"vertical": "MIN"
},
"cornerRadius": 1000,
"fills": [
{
"type": "SOLID",
"color": {
"r": 0.3176470588235294,
"g": 0.23529411764705882,
"b": 0.9333333333333333
}
}
]
}
}
}
}
Loading
Loading