Files
rag-manager/ingest_pipeline/cli/tui/layouts.py
2025-09-19 06:56:19 +00:00

381 lines
9.7 KiB
Python

"""Responsive layout system for TUI applications."""
from __future__ import annotations
from typing import Any
from textual.app import ComposeResult
from textual.containers import Container, VerticalScroll
from textual.widgets import Static
from typing_extensions import override
class ResponsiveGrid(Container):
"""Grid that auto-adjusts based on terminal size."""
DEFAULT_CSS = """
ResponsiveGrid {
layout: grid;
grid-size: 1;
grid-columns: 1fr;
grid-rows: auto;
grid-gutter: 1;
padding: 1;
}
ResponsiveGrid.two-column {
grid-size: 2;
grid-columns: 1fr 1fr;
}
ResponsiveGrid.three-column {
grid-size: 3;
grid-columns: 1fr 1fr 1fr;
}
ResponsiveGrid.auto-fit {
grid-columns: repeat(auto-fit, minmax(20, 1fr));
}
ResponsiveGrid.compact {
grid-gutter: 0;
padding: 0;
}
"""
def __init__(
self,
*children: Any,
columns: int = 1,
auto_fit: bool = False,
compact: bool = False,
**kwargs: Any,
) -> None:
"""Initialize responsive grid."""
super().__init__(*children, **kwargs)
self.columns = columns
self.auto_fit = auto_fit
self.compact = compact
def on_mount(self) -> None:
"""Apply responsive classes based on configuration."""
if self.auto_fit:
_ = self.add_class("auto-fit")
elif self.columns == 2:
_ = self.add_class("two-column")
elif self.columns == 3:
_ = self.add_class("three-column")
if self.compact:
_ = self.add_class("compact")
def on_resize(self) -> None:
"""Adjust layout based on terminal size."""
if self.auto_fit:
# Let CSS handle auto-fit
return
terminal_width = self.size.width
if terminal_width < 60:
# Force single column on narrow terminals
_ = self.remove_class("two-column", "three-column")
self.styles.grid_size_columns = 1
self.styles.grid_columns = "1fr"
elif terminal_width < 100 and self.columns > 2:
# Force two columns on medium terminals
_ = self.remove_class("three-column")
_ = self.add_class("two-column")
self.styles.grid_size_columns = 2
self.styles.grid_columns = "1fr 1fr"
elif self.columns == 2:
_ = self.add_class("two-column")
elif self.columns == 3:
_ = self.add_class("three-column")
class CollapsibleSidebar(Container):
"""Sidebar that can be collapsed to save space."""
DEFAULT_CSS = """
CollapsibleSidebar {
dock: left;
width: 25%;
min-width: 20;
max-width: 40;
background: $surface;
border-right: solid $border;
padding: 1;
transition: width 300ms;
}
CollapsibleSidebar.collapsed {
width: 3;
min-width: 3;
overflow: hidden;
}
CollapsibleSidebar.collapsed > * {
display: none;
}
CollapsibleSidebar .sidebar-toggle {
dock: top;
height: 1;
background: $primary;
color: $text;
text-align: center;
margin-bottom: 1;
}
CollapsibleSidebar .sidebar-content {
height: 1fr;
overflow-y: auto;
}
"""
def __init__(self, *children: Any, collapsed: bool = False, **kwargs: Any) -> None:
"""Initialize collapsible sidebar."""
super().__init__(**kwargs)
self.collapsed = collapsed
self._children = children
@override
def compose(self) -> ComposeResult:
"""Compose sidebar with toggle and content."""
yield Static("", classes="sidebar-toggle")
with VerticalScroll(classes="sidebar-content"):
yield from self._children
def on_mount(self) -> None:
"""Apply initial collapsed state."""
if self.collapsed:
_ = self.add_class("collapsed")
def on_click(self) -> None:
"""Toggle sidebar when clicked."""
self.toggle()
def toggle(self) -> None:
"""Toggle sidebar collapsed state."""
self.collapsed = not self.collapsed
if self.collapsed:
_ = self.add_class("collapsed")
else:
_ = self.remove_class("collapsed")
def expand_sidebar(self) -> None:
"""Expand sidebar."""
if self.collapsed:
self.toggle()
def collapse_sidebar(self) -> None:
"""Collapse sidebar."""
if not self.collapsed:
self.toggle()
class TabularLayout(Container):
"""Optimized layout for data tables with optional sidebar."""
DEFAULT_CSS = """
TabularLayout {
layout: horizontal;
height: 100%;
}
TabularLayout .main-content {
width: 1fr;
height: 100%;
layout: vertical;
}
TabularLayout .table-container {
height: 1fr;
overflow: auto;
border: solid $border;
background: $surface;
}
TabularLayout .table-header {
dock: top;
height: 3;
background: $primary;
color: $text;
padding: 1;
}
TabularLayout .table-footer {
dock: bottom;
height: 3;
background: $surface-lighten-1;
padding: 1;
border-top: solid $border;
}
"""
def __init__(
self,
table_widget: Any,
header_content: Any | None = None,
footer_content: Any | None = None,
sidebar_content: Any | None = None,
**kwargs: Any,
) -> None:
"""Initialize tabular layout."""
super().__init__(**kwargs)
self.table_widget = table_widget
self.header_content = header_content
self.footer_content = footer_content
self.sidebar_content = sidebar_content
@override
def compose(self) -> ComposeResult:
"""Compose layout with optional sidebar."""
if self.sidebar_content:
yield CollapsibleSidebar(self.sidebar_content)
with Container(classes="main-content"):
if self.header_content:
yield Container(self.header_content, classes="table-header")
yield Container(self.table_widget, classes="table-container")
if self.footer_content:
yield Container(self.footer_content, classes="table-footer")
class CardLayout(ResponsiveGrid):
"""Grid layout optimized for card-based content."""
DEFAULT_CSS = """
CardLayout {
grid-gutter: 2;
padding: 2;
}
CardLayout .card {
background: $surface;
border: solid $border;
border-radius: 1;
padding: 2;
height: auto;
min-height: 10;
}
CardLayout .card:hover {
border: solid $accent;
background: $surface-lighten-1;
}
CardLayout .card:focus {
border: solid $primary;
}
CardLayout .card-header {
dock: top;
height: 3;
background: $primary-lighten-1;
color: $text;
padding: 1;
margin: -2 -2 1 -2;
border-radius: 1 1 0 0;
}
CardLayout .card-content {
height: 1fr;
overflow: auto;
}
CardLayout .card-footer {
dock: bottom;
height: 3;
background: $surface-darken-1;
padding: 1;
margin: 1 -2 -2 -2;
border-radius: 0 0 1 1;
}
"""
def __init__(self, **kwargs: Any) -> None:
"""Initialize card layout with default settings for cards."""
# Default to auto-fit cards with minimum width
super().__init__(auto_fit=True, **kwargs)
class SplitPane(Container):
"""Resizable split pane layout."""
DEFAULT_CSS = """
SplitPane {
layout: horizontal;
height: 100%;
}
SplitPane.vertical {
layout: vertical;
}
SplitPane .left-pane,
SplitPane .top-pane {
width: 50%;
height: 50%;
background: $surface;
border-right: solid $border;
border-bottom: solid $border;
}
SplitPane .right-pane,
SplitPane .bottom-pane {
width: 50%;
height: 50%;
background: $surface;
}
SplitPane .splitter {
width: 1;
height: 1;
background: $border;
}
SplitPane.vertical .splitter {
width: 100%;
height: 1;
}
"""
def __init__(
self,
left_content: Any,
right_content: Any,
vertical: bool = False,
split_ratio: float = 0.5,
**kwargs: Any,
) -> None:
"""Initialize split pane."""
super().__init__(**kwargs)
self.left_content = left_content
self.right_content = right_content
self.vertical = vertical
self.split_ratio = split_ratio
@override
def compose(self) -> ComposeResult:
"""Compose split pane layout."""
if self.vertical:
_ = self.add_class("vertical")
pane_classes = ("top-pane", "bottom-pane") if self.vertical else ("left-pane", "right-pane")
yield Container(self.left_content, classes=pane_classes[0])
yield Static("", classes="splitter")
yield Container(self.right_content, classes=pane_classes[1])
def on_mount(self) -> None:
"""Apply split ratio."""
if self.vertical:
self.query_one(f".{self.__class__.__name__} .top-pane").styles.height = f"{self.split_ratio * 100}%"
self.query_one(f".{self.__class__.__name__} .bottom-pane").styles.height = f"{(1 - self.split_ratio) * 100}%"
else:
self.query_one(f".{self.__class__.__name__} .left-pane").styles.width = f"{self.split_ratio * 100}%"
self.query_one(f".{self.__class__.__name__} .right-pane").styles.width = f"{(1 - self.split_ratio) * 100}%"