kanban-tui 0.5.0

Terminal user interface for the kanban project management tool
Documentation
use crate::app::App;
use crate::components::*;
use ratatui::{
    layout::{Constraint, Direction, Layout},
    style::{Color, Style},
    widgets::{Block, Borders, Clear, Paragraph},
    Frame,
};

pub(crate) fn render_create_card_popup(app: &App, frame: &mut Frame) {
    render_input_popup(
        frame,
        "Create New Task",
        "Task Title:",
        app.input.as_str(),
        app.input.cursor_byte_offset(),
    );
}

pub(crate) fn render_set_card_points_popup(app: &App, frame: &mut Frame) {
    render_input_popup(
        frame,
        "Set Points",
        "Points (1-5 or empty):",
        app.input.as_str(),
        app.input.cursor_byte_offset(),
    );
}

pub(crate) fn render_set_card_priority_popup(app: &App, frame: &mut Frame) {
    use crate::components::{PriorityDialog, SelectionDialog};
    let dialog = PriorityDialog;
    dialog.render(app, frame);
}

pub(crate) fn render_set_multiple_cards_priority_popup(app: &App, frame: &mut Frame) {
    use crate::components::{BulkPriorityDialog, SelectionDialog};
    let dialog = BulkPriorityDialog {
        count: app.multi_select.selected_cards.len(),
    };
    dialog.render(app, frame);
}

pub(crate) fn render_order_cards_popup(app: &App, frame: &mut Frame) {
    use crate::components::{SelectionDialog, SortFieldDialog};
    let dialog = SortFieldDialog;
    dialog.render(app, frame);
}

pub(crate) fn render_assign_sprint_popup(app: &App, frame: &mut Frame) {
    use crate::components::{SelectionDialog, SprintAssignDialog};
    let dialog = SprintAssignDialog;
    dialog.render(app, frame);
}

pub(crate) fn render_assign_multiple_cards_popup(app: &App, frame: &mut Frame) {
    use crate::components::sprint_assign_list::{
        build_entries, render_entry_line, scroll_offset_to_show, section_header_for,
    };
    use ratatui::style::Modifier;
    use ratatui::text::{Line, Span};

    let area = centered_rect(60, 50, frame.area());

    frame.render_widget(Clear, area);

    let block = Block::default()
        .title(format!(
            "Assign {} Cards to Sprint",
            app.multi_select.selected_cards.len()
        ))
        .borders(Borders::ALL)
        .style(Style::default().bg(Color::Black));

    let inner = block.inner(area);
    frame.render_widget(block, area);

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .margin(2)
        .constraints([Constraint::Length(1), Constraint::Min(0)])
        .split(inner);

    let label = Paragraph::new("Select sprint:").style(Style::default().fg(Color::Yellow));
    frame.render_widget(label, chunks[0]);

    let mut lines = vec![];
    let mut entries_for_header = Vec::new();

    if let Some(board_idx) = app.selection.active_board_index {
        if let Some(board) = app.model.boards().get(board_idx) {
            let sprints = app.model.sprints();
            let entries = build_entries(sprints, board.id, chrono::Utc::now());
            for (idx, entry) in entries.iter().enumerate() {
                let is_selected = app.dialog_input.sprint_assign_selection.get() == Some(idx);
                lines.push(render_entry_line(entry, is_selected, None, board));
            }
            entries_for_header = entries;
        }
    }

    let selected = app.dialog_input.sprint_assign_selection.get().unwrap_or(0);
    let scroll = scroll_offset_to_show(selected, lines.len(), chunks[1].height as usize);
    let list = Paragraph::new(lines).scroll((scroll as u16, 0));
    frame.render_widget(list, chunks[1]);

    if let Some((header_idx, label)) = section_header_for(&entries_for_header, selected) {
        if header_idx < scroll && chunks[1].height > 0 {
            let overlay = Paragraph::new(Line::from(Span::styled(
                label.to_string(),
                Style::default()
                    .fg(Color::Yellow)
                    .add_modifier(Modifier::BOLD),
            )));
            let top_row = ratatui::layout::Rect {
                x: chunks[1].x,
                y: chunks[1].y,
                width: chunks[1].width,
                height: 1,
            };
            frame.render_widget(overlay, top_row);
        }
    }
}