diff --git a/crates/op-orchestrator/src/scaffold.rs b/crates/op-orchestrator/src/scaffold.rs index cf398076..40cb7434 100644 --- a/crates/op-orchestrator/src/scaffold.rs +++ b/crates/op-orchestrator/src/scaffold.rs @@ -84,7 +84,15 @@ fn status_bar_foreground(fill_hex: &str) -> &'static str { } } -fn mobile_status_bar_json(root_id: &str, fill_hex: &str) -> serde_json::Value { +/// Status-bar chrome for a mobile root frame. `width` is the root frame's +/// width so the right-aligned levels group (cellular/wifi/battery) clamps +/// to the screen edge instead of overflowing on explicit narrow widths +/// (e.g. 320 × 568 iPhone SE). Right-edge inset = 26 to match the iOS +/// safe-area gutter the 390-wide reference is built against. +fn mobile_status_bar_json(root_id: &str, fill_hex: &str, width: f64) -> serde_json::Value { + const LEVELS_WIDTH: f64 = 78.0; + const LEVELS_RIGHT_INSET: f64 = 26.0; + let levels_x = (width - LEVELS_WIDTH - LEVELS_RIGHT_INSET).max(0.0); let fg = status_bar_foreground(fill_hex); let fg_fill = solid_fill_json(fg); let time_label = serde_json::json!({ @@ -186,9 +194,9 @@ fn mobile_status_bar_json(root_id: &str, fill_hex: &str) -> serde_json::Value { "type": "frame", "id": format!("{root_id}-status-bar-levels"), "name": "Levels", - "x": 286, + "x": levels_x, "y": 24, - "width": 78, + "width": LEVELS_WIDTH, "height": 14, "layout": "none", "children": [cellular, wifi, battery] @@ -224,7 +232,7 @@ fn build_root_frame_node( is_mobile: bool, ) -> Result { let children = if is_mobile { - serde_json::json!([mobile_status_bar_json(id, fill_hex)]) + serde_json::json!([mobile_status_bar_json(id, fill_hex, width)]) } else { serde_json::json!([]) }; diff --git a/crates/op-orchestrator/src/scaffold_tests.rs b/crates/op-orchestrator/src/scaffold_tests.rs index 38d892d6..3d1686b0 100644 --- a/crates/op-orchestrator/src/scaffold_tests.rs +++ b/crates/op-orchestrator/src/scaffold_tests.rs @@ -116,6 +116,31 @@ fn build_scaffold_mobile_status_bar_uses_fixed_icon_positions() { } } +#[test] +fn build_scaffold_mobile_status_bar_clamps_levels_to_explicit_narrow_width() { + // iPhone SE: 320 wide. The pre-fix scaffold hardcoded levels.x=286 + // which put the right-aligned chrome (cellular/wifi/battery, 78 wide) + // at x=286..364 — 44 px off-screen on a 320-wide root. + let mut narrow = plan(); + narrow.root_frame.width = 320.0; + let cmds = build_scaffold(&narrow, true).expect("scaffold"); + match &cmds[0] { + EditorCommand::InsertSubtree { nodes, .. } => { + let status_json = serde_json::to_value(&nodes[0].children().expect("children")[0]) + .expect("status json"); + let levels = &status_json["children"][1]; + let levels_x = levels["x"].as_f64().expect("levels x"); + let levels_w = levels["width"].as_f64().expect("levels width"); + assert!( + levels_x + levels_w <= 320.0, + "levels right edge {} overflows root width 320", + levels_x + levels_w + ); + } + other => panic!("expected InsertSubtree, got {other:?}"), + } +} + #[test] fn build_scaffold_single_root_uses_safe_canvas_offset() { let cmds = build_scaffold(&plan(), true).expect("scaffold");