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::drawing::Draw;
use ftui::render::frame::Frame;
use crate::clock::Clock;
use crate::layout::{classify_width, timeline_time_width};
use crate::message::TimelineEventKind;
use crate::state::timeline::TimelineState;
use crate::view::common::discussion_tree::format_relative_time;
@@ -121,7 +122,9 @@ pub fn render_timeline(
if state.events.is_empty() {
render_empty_state(frame, state, area.x + 1, y, max_x);
} else {
render_event_list(frame, state, area.x, y, area.width, list_height, clock);
let bp = classify_width(area.width);
let time_col_width = timeline_time_width(bp);
render_event_list(frame, state, area.x, y, area.width, list_height, clock, time_col_width);
}
// -- Hint bar --
@@ -153,6 +156,7 @@ fn render_empty_state(frame: &mut Frame<'_>, state: &TimelineState, x: u16, y: u
// ---------------------------------------------------------------------------
/// Render the scrollable list of timeline events.
#[allow(clippy::too_many_arguments)]
fn render_event_list(
frame: &mut Frame<'_>,
state: &TimelineState,
@@ -161,6 +165,7 @@ fn render_event_list(
width: u16,
list_height: usize,
clock: &dyn Clock,
time_col_width: u16,
) {
let max_x = x + width;
@@ -198,10 +203,9 @@ fn render_event_list(
let mut cx = x + 1;
// Timestamp gutter (right-aligned in ~10 chars).
// Timestamp gutter (right-aligned, width varies by breakpoint).
let time_str = format_relative_time(event.timestamp_ms, clock);
let time_width = 10u16;
let time_x = cx + time_width.saturating_sub(time_str.len() as u16);
let time_x = cx + time_col_width.saturating_sub(time_str.len() as u16);
let time_cell = if is_selected {
selected_cell
} else {
@@ -211,8 +215,8 @@ fn render_event_list(
..Cell::default()
}
};
frame.print_text_clipped(time_x, y, &time_str, time_cell, cx + time_width);
cx += time_width + 1;
frame.print_text_clipped(time_x, y, &time_str, time_cell, cx + time_col_width);
cx += time_col_width + 1;
// Entity prefix: #42 or !99
let prefix = match event.entity_key.kind {