zilliz 1.4.3

TUI and CLI tool for managing Zilliz Cloud clusters and Milvus operations
Documentation
//! Bordered modal card used by every wizard step.

use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Clear, Paragraph};

#[derive(Default)]
pub struct Card<'a> {
    pub eyebrow: Option<&'a str>, // SIGN IN · STEP 1 OF 3
    pub title: Option<&'a str>,   // Choose your region.
    pub body: Vec<Line<'a>>,      // selectable rows / paragraphs
    pub footer: Option<Line<'a>>, // info / warning line
    pub error: Option<&'a str>,   // inline error band
    pub step: Option<&'a str>,    // e.g. "1 / 3" — drawn bottom-right
    pub max_width: u16,           // desired width; clamped by terminal
}

impl<'a> Card<'a> {
    pub fn new() -> Self {
        Self {
            max_width: 76,
            ..Default::default()
        }
    }
}

/// Compute a centered rect inside `area` with the given width and a height
/// derived from the supplied content lines plus padding.
pub fn centered_card_rect(area: Rect, width: u16, height: u16) -> Rect {
    let width = width.min(area.width.saturating_sub(4));
    let height = height.min(area.height.saturating_sub(4));
    let x = area.x + (area.width.saturating_sub(width)) / 2;
    let y = area.y + (area.height.saturating_sub(height)) / 2;
    Rect {
        x,
        y,
        width,
        height,
    }
}

pub fn render(frame: &mut Frame, area: Rect, card: &Card<'_>) {
    // Compute height: 2 padding rows + eyebrow + title + body + spacers + footer + error
    let mut content_rows: u16 = 2; // top/bottom padding inside card
    if card.eyebrow.is_some() {
        content_rows += 1;
    }
    if card.title.is_some() {
        content_rows += 2; // title + blank below
    }
    content_rows += card.body.len() as u16;
    if card.footer.is_some() {
        content_rows += 2;
    }
    if card.error.is_some() {
        content_rows += 2;
    }
    // Border = 2 rows.
    let total_height = content_rows + 2;

    let outer = centered_card_rect(area, card.max_width, total_height);
    // Clear background so we don't bleed through to underlying screen.
    frame.render_widget(Clear, outer);

    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::default().fg(Color::DarkGray));
    let inner = block.inner(outer);
    frame.render_widget(block, outer);

    // Pad horizontally by 2 cols.
    let inner = Rect {
        x: inner.x + 2,
        y: inner.y + 1,
        width: inner.width.saturating_sub(4),
        height: inner.height.saturating_sub(2),
    };

    let mut lines: Vec<Line> = Vec::new();
    if let Some(eb) = card.eyebrow {
        lines.push(Line::from(Span::styled(
            eb.to_string(),
            Style::default().fg(Color::DarkGray),
        )));
    }
    if let Some(t) = card.title {
        lines.push(Line::from(Span::styled(
            t.to_string(),
            Style::default()
                .fg(Color::Rgb(150, 130, 255))
                .add_modifier(Modifier::BOLD),
        )));
        lines.push(Line::from(""));
    }
    for body_line in &card.body {
        lines.push(body_line.clone());
    }
    if let Some(footer) = &card.footer {
        lines.push(Line::from(""));
        lines.push(footer.clone());
    }
    if let Some(err) = card.error {
        lines.push(Line::from(""));
        lines.push(Line::from(Span::styled(
            format!("! {}", err),
            Style::default().fg(Color::Red),
        )));
    }

    let para = Paragraph::new(lines);
    frame.render_widget(para, inner);

    // Step indicator bottom-right inside the border.
    if let Some(step) = card.step {
        let step_width = step.len() as u16;
        if outer.width > step_width + 2 && outer.height > 1 {
            let rect = Rect {
                x: outer.x + outer.width - step_width - 2,
                y: outer.y + outer.height - 1,
                width: step_width,
                height: 1,
            };
            frame.render_widget(
                Paragraph::new(Line::from(Span::styled(
                    step.to_string(),
                    Style::default().fg(Color::DarkGray),
                ))),
                rect,
            );
        }
    }
}

/// Render a selectable list row used inside `Card.body`.
pub fn selectable_row<'a>(
    badge: &'a str,
    title: &'a str,
    subtitle: &'a str,
    selected: bool,
) -> Vec<Line<'a>> {
    let (title_style, sub_style, bg) = if selected {
        (
            Style::default()
                .fg(Color::White)
                .add_modifier(Modifier::BOLD),
            Style::default().fg(Color::Gray),
            Style::default().bg(Color::Rgb(40, 30, 80)),
        )
    } else {
        (
            Style::default().fg(Color::Gray),
            Style::default().fg(Color::DarkGray),
            Style::default(),
        )
    };
    vec![
        Line::from(vec![
            Span::styled(
                format!(" {} ", badge),
                Style::default()
                    .fg(Color::White)
                    .bg(Color::DarkGray)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled(title.to_string(), title_style),
        ])
        .style(bg),
        Line::from(vec![
            Span::raw("      "),
            Span::styled(subtitle.to_string(), sub_style),
        ])
        .style(bg),
    ]
}