fix(orchestrator): clamp mobile status-bar levels to root width

The mobile status-bar chrome injected by `mobile_status_bar_json`
hardcoded levels.x=286 — the right-aligned cellular/wifi/battery
group, sized for a 390-wide iPhone reference. When the C2 plan
work started honouring explicit prompt sizes via
`explicit_mobile_size` (accepts 240..=520 wide), a 320 x 568
iPhone-SE prompt rendered the chrome at x=286..364 — 44 px past
the right edge.

Take root `width` as an argument and derive levels.x as
`width - 78 - 26` (78 = chrome width, 26 = iOS safe-area gutter,
matches the existing 390 reference: 390 - 104 = 286). New test
exercises 320-wide root to lock in the no-overflow contract.
This commit is contained in:
Fini 2026-05-27 23:10:47 +08:00
parent 07277b381d
commit bb4c62519d
2 changed files with 37 additions and 4 deletions

View file

@ -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<PenNode, String> {
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!([])
};

View file

@ -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");