feat(tui): responsive breakpoints for detail views (bd-a6yb)

Apply breakpoint-aware layout to issue_detail and mr_detail views:
- Issue detail: hide labels on Xs, hide assignees on Xs/Sm, skip milestone row on Xs
- MR detail: hide branch names and merge status on Xs/Sm
- Issue detail allocate_sections gives description 60% on wide (Lg+) vs 40% narrow
- Add responsive tests for both detail views
- Close bd-a6yb: all TUI screens now adapt to terminal width

760 lib tests pass, clippy clean.
This commit is contained in:
teernisse
2026-02-18 23:59:47 -05:00
parent ae1c3e3b05
commit 026b3f0754
13 changed files with 345 additions and 104 deletions

View File

@@ -20,6 +20,7 @@ use ftui::render::cell::Cell;
use ftui::render::drawing::Draw;
use ftui::render::frame::Frame;
use crate::layout::{classify_width, search_show_project};
use crate::message::EntityKind;
use crate::state::search::SearchState;
use crate::text_width::cursor_cell_offset;
@@ -39,6 +40,8 @@ pub fn render_search(frame: &mut Frame<'_>, state: &SearchState, area: Rect) {
return;
}
let bp = classify_width(area.width);
let show_project = search_show_project(bp);
let mut y = area.y;
let max_x = area.right();
@@ -99,7 +102,7 @@ pub fn render_search(frame: &mut Frame<'_>, state: &SearchState, area: Rect) {
if state.results.is_empty() {
render_empty_state(frame, state, area.x + 1, y, max_x);
} else {
render_result_list(frame, state, area.x, y, area.width, list_height);
render_result_list(frame, state, area.x, y, area.width, list_height, show_project);
}
// -- Bottom hint bar -----------------------------------------------------
@@ -228,6 +231,7 @@ fn render_result_list(
start_y: u16,
width: u16,
list_height: usize,
show_project: bool,
) {
let max_x = x + width;
@@ -294,11 +298,13 @@ fn render_result_list(
let after_title =
frame.print_text_clipped(after_iid + 1, y, &result.title, label_style, max_x);
// Project path (right-aligned).
let path_width = result.project_path.len() as u16 + 2;
let path_x = max_x.saturating_sub(path_width);
if path_x > after_title + 1 {
frame.print_text_clipped(path_x, y, &result.project_path, detail_style, max_x);
// Project path (right-aligned, hidden on narrow terminals).
if show_project {
let path_width = result.project_path.len() as u16 + 2;
let path_x = max_x.saturating_sub(path_width);
if path_x > after_title + 1 {
frame.print_text_clipped(path_x, y, &result.project_path, detail_style, max_x);
}
}
}
@@ -476,4 +482,23 @@ mod tests {
render_search(&mut frame, &state, Rect::new(0, 0, 80, 10));
});
}
#[test]
fn test_render_search_responsive_breakpoints() {
// Narrow (Xs=50): project path hidden.
with_frame!(50, 24, |frame| {
let mut state = SearchState::default();
state.enter(fts_caps());
state.results = sample_results(3);
render_search(&mut frame, &state, Rect::new(0, 0, 50, 24));
});
// Standard (Md=100): project path shown.
with_frame!(100, 24, |frame| {
let mut state = SearchState::default();
state.enter(fts_caps());
state.results = sample_results(3);
render_search(&mut frame, &state, Rect::new(0, 0, 100, 24));
});
}
}